5.信号

一.概述

信号可以用来向进程传递消息,当操作系统不想让某个进程运行的时候,会给这个进程发送相应的结束信号。man page的第七章专门来讲Signal, 可以通过man 7 signal 指令来查看。

信号可以由以下途径产生:

1) 终端特殊按键

Ctrl+c    SIGINT
Ctrl+z    SIGTSTP
Ctrl+\     SIGQUIT

2) 硬件异常

* 除0操作
* 访问非法内存

3) 某些特殊函数

kill()&raise()&abort()&alarm()

二.信号产生函数

◆kill()函数

kill() 函数可以向指定的进程发送指定的信号

int kill(pid_t pid, int sig)
pid > 0
sig发送给ID为pid的进程
pid == 0
sig发送给与发送进程同组的所有进程
pid <
0
sig发送给组ID为|-pid|的进程,并且发送进程具有向其发送信号的权限
pid == -1
sig发送给发送进程有权限向他们发送信号的系统上的所有进程
sig为0时,用于检测,特定为pid进程是否存在,如不存在,返回-1

例:向某个进程发送指定的信号

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>

int main(int argc, char *argv[])
{
    
    if (argc < 3) {
        printf("Invalid arguments\n");
        exit(1);
    }
    
    if (kill((pid_t)atoi(argv[1]), atoi(argv[2])) < 0) {
        perror("kill");
        exit(2);
    }    
    
    return 0;
}

运行:

结束掉5297这个进程组

./kill -5297 9

注意

普通用户只能向自己创建的进程发信号,无法向root用户或其它用户生成的进程发信号。

◆raise()函数

raise()函数可以向自己发送指定的信号

 #include <signal.h>
 int raise(int sig);

例:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

int main()
{

    printf("This is a new question\n");
    printf("This is a dog\n");
    printf("This is a cat\n");
    
    raise(9);    

    return 0;
}

◆abort()函数

abort()函数相当于调用进程向自己发送了一个SIGABRT(6)的信号

#include <stdlib.h>
void abort(void);

例:

#include <stdio.h>
#include <stdlib.h>


int main()
{

    printf("This is new question -------------1\n");
    printf("This is new question -------------2\n");
    printf("This is new question -------------3\n");
    printf("This is new question -------------4\n");

    sleep(5);    
    abort();
    return 0;
}

◆alarm()函数

操作系统在管理进程的时候,会为每个进程都分配一个定时器(闹钟)——alarm, 而alarm()函数可以指定定时的秒数,当指定的秒数到达后,会向当前进程发送一个SIGALRM的信号,该信号的默认处理动作是终止当前进程。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

例:

#include <stdio.h>
#include <unistd.h>


int main()
{
    
    int i = 0;    
    
    alarm(5);
    while(1) {
        printf("current i is: %d\n", i);
        i++;
    }
    return 0;
}

运行, 5秒后会给该进程发送一个SIGALRM信号,终止当前进程。

注意:

alarm()函数仅仅是设定定时器的秒数,并不会导致进程阻塞

三.信号集处理函数

事实上,每个进程的PCB里面都维护了两个信号集(PEND未决信号集和阻塞信号集)。

 

         =============         =============          =============
         PCB                PEND未决信号集          阻塞信号集
向进程发送信号==>                1号信号 0               1号信号 0     ==> handler(默认,忽略,捕捉)
                         2号信号 0              2号信号 1
                         3号信号 0              3号信号 0
                         4号信号 0              4号信号 0
                         5号信号 0              5号信号 0
                          ...                  ...

  内核把信号发送给进程,进程内部维护了两个信号集(未决信号集,阻塞信号集),未决信号集用来记录:当前这个信号产生了,但是最终还没有被当前进程响应。在信号未产生之前,未决信号集里面各个信号的标志位默认初始值都是0,而当指定编号的信号到来后,它会把未决信号集里指定编号的信号的标志位置为1,表示该信号产生了,同时它会继续把这个事件向阻塞信号集传送,此时如果阻塞信号集里指定编号的信号的标志位为1,则代表已阻塞了该信号,此时该信号便不能被通过。同时,handler所对应的默认处理动作就无法被执行,因为它被阻塞了,没有通过,而这个信号的状态就被称为未决态(信号产生了,但是还没有被执行),

  反之,假设信号产生了,同时在阻塞信号集里,该信号的标志位为0,(表示没有阻塞此信号), 那么这个信号便会继续向下传递,此时就会去判断该信号的默认处理动作,根据其动作再执行相应操作, 同时,内核会将未决信号集中该信号的标志位重新翻转成0。

信号产生并且被响应,这个过程我们称之为递达态
信号产生没有被响应,这个过程我们称之为未决态

有以下场景需要注意:

如果在阻塞信号集里将某个信号(举例:如2号信号SIGINT)的标志位置为1,则该信号会被阻塞,此时频繁按Ctrl+C,该信号仍然只会被记录一次,在linux中,前32个信号不支持排队,即:不管信号产生多少次,只记录一次,后32个信号(实时信号,跟驱动绑定的比较深)支持排队。

  内核提供了一些信号集处理函数来操作信号集(注意是信号集,不是阻塞信号集和未决信号集),信号集在linux中以sigset_t 这个结构体来表示。

  常用的信号集处理函数有:

int sigemptyset(sigset_t *set);             // 清空信号集, 把所有信号的标志位全部置为0
int sigfillset(sigset_t *set);              // 把信号集里所有信号的标志位全部置为1
int sigaddset(sigset_t *set, int signo);        // 把信号集里某一个信号的标志位置为1
int sigdelset(sigset_t *set, int signo);        // 把信号集里某一个信号的标志位置为0
int sigismember(const sigset_t *set, int signo);   // 判断这个信号集的某个信号的标志位是否已被置为1,这是一个测试函数

四.操作阻塞信号集,读取未决信号集

内核允许我们去读取和更改进程的阻塞信号集(也叫信号屏蔽字),但不允许我们去更改进程的未决信号集,未决信号集只能去读取。

操作阻塞信号集的步骤为:

1.先定义一个信号集sigset,再调用sigemptyset();,将这个信号集清空;
2.然后再sigaddset() 把指定信号的标志位置为1
3.然后再通过注册的方法,把当前信号集注册到阻塞信号集当中。

sigprocmask()函数

内核提供了一个sigprocmask()来注册信号集,它可以读取或更改进程的信号屏蔽字

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1

how参数的含义

SIG_BLOCK    set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCK    set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
SIG_SETMASK   设置当前信号屏蔽字为set所指向的值,相当于mask=set

第二个参数是将要被注册的信号集,第三个是原有信号集,若不需要可置为NULL。

sigpending()函数

sigpending()函数用于读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

#include <signal.h>
int sigpending(sigset_t *set);

例:将SIGQUIT 信号添加到进程的阻塞信号集,便Ctrl+\按键无效:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>


/**
*@brief 打印信号集的内容
*@param s 传递的信号集
*/
void printSigset(sigset_t *s)
{
    int i;
    for (i = 1; i < 31; i++) {
        // 判断信号集中某个信号的标志位是否已置为1
        printf("signo[%d] flag is: %d\n", i, sigismember(s, i));
        puts("");
    }
}


int main()
{
    
    sigset_t s, p;
    sigemptyset(&s);        // 将信号集中所有信号的标志位全置为0
    sigaddset(&s, SIGQUIT);        // 将信号集中SIGQUIT信号的标志位置为1
    
    sigprocmask(SIG_BLOCK, &s, NULL); // 将该信号集添加到阻塞信号集
    
    while (1) {
        sigpending(&p);        // 获取未决信号集内容
        printSigset(&p);    
        sleep(1);
    }
    
    return 0;
}

运行结果:

 

signo[1] flag is: 0

signo[2] flag is: 0

signo[3] flag is: 1

signo[4] flag is: 0

signo[5] flag is: 0

signo[6] flag is: 0

signo[7] flag is: 0

signo[8] flag is: 0

signo[9] flag is: 0

signo[10] flag is: 0

signo[11] flag is: 0

...

五.设置信号捕捉函数

可以给指定的信号设置信号捕捉函数,这样当信号到来时就会执行指定的动作,不再被终止。Linux中提供了一个叫sigaction的函数用来设置信号捕捉:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);

struct sigaction 定义:
struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    (*sa_restorer)(void);
};

struct sigaction说明:

sa_handler和sa_sigaction都是指将要设置的信号处理函数的函数原型,内核会负责调用信号处理函数,同时内核会将该信号的信号ID传递过来,sa_mask代表临时信号屏蔽字,sa_flags 通过该参数可以指定究竟调用上面的哪个函数

临时信号屏蔽字可用于处理以下情况:

假设我为5号信号设置信号处理函数,那么当5号信号到来的时候,就会执行5号信号对应的信号处理函数;若此时当3号信号到来,假如不做处理,系统又会跑去执行3号信号的信号处理函数,这时5号信号的信号处理函数才执行一半,为了规避这个问题,可以在执行5号信号的信号处理函数时,设置信号屏蔽字(sa_mask),阻止某些信号的执行。

信号屏蔽字的另一个作用:

假设信号处理函数里面比较耗时间,需要执行5秒,那么在这5秒之间,用户如果又按Ctrl+C,发出了一个SIGINT信号,会出现什么场景?

信号机制是这样处理的:当它正在执行2号函数的信号处理函数的时候,内核会自动帮你屏蔽当前信号,也就是说,当你执行信号处理函数的时候,阻塞信号集中2号信号的标志位会自动置为1,阻塞当前信号,当信号处理函数执行完毕后,内核会自动将标志位再自动翻转成0。除此之处,可能因为某些特殊需求,当你在执行信号处理函数的时候,还想屏蔽某些信号,还可以通过设置临时信号屏蔽字

mask |= sa_mask

sigaddset(&act.sa_mask, 信号编号); 的方式予以实现。

注意:

即使不使用临时信号屏蔽字,也应该调用sigemptyset(&act.sa_mask),将sa_mask的值清空,因为act是一个局部变量,里面的sa_mask是一个垃圾值。

例1 添加信号屏蔽字:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>


void handle_signal(int signo)
{
    printf("I have received the signal\n");
    sleep(5); // 模拟正在处理...
    printf("The signo is: %d\n", signo);
}


int main()
{
    struct sigaction act;
    act.sa_handler = handle_signal;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, SIGQUIT); //设置临时信号屏蔽字
    act.sa_flags = 0;
    
    if (sigaction(SIGINT, &act, NULL) < 0) {
        perror("sigaction");
        exit(1);
    }
    
    while (1) {
        printf("****************************\n");
        sleep(1);
    }
    return 0;
}

运行并测试:

****************************
****************************
^CI have received the signal
^C^C^CThe signo is: 2
I have received the signal
The signo is: 2
****************************
****************************
****************************
****************************
^CI have received the signal
^\^\^\^\^\^\^\The signo is: 2
Quit (core dumped)

当我按下Ctrl+C,时,再连续按下Ctrl+C,内核不会再去调用信号处理函数,直到先前的信号处理函数执行完毕,同时,若在执行信号处理函数时再按下Ctrl+/,内核也会暂时阻塞SIGQUIT信号。

例2:使用SIGUSR1和SIGUSR2信号实现父子进程的同步输出:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>


static void handler_child_signal(int signo)
{
    printf("I am PARENT process, I have received child signal\n");
    printf("The signo is: %d\n", signo);
}

static void handler_parent_signal(int signo)
{
    printf("I am CHILD process, I have received parent signal\n");
    printf("The signo is: %d\n", signo);
}


int main()
{

    pid_t pid;

    pid = fork();
    if (pid > 0) { //parent
        int i = 10;
        struct sigaction act;
        act.sa_handler = handler_child_signal;
        sigemptyset(&act.sa_mask);
        sigaddset(&act.sa_mask, SIGQUIT);
        act.sa_flags = 0;
        sigaction(SIGUSR1, &act, NULL);
        
        while(i--) {
            sleep(1);
            kill(pid, SIGUSR2);
        }
    } else if (pid == 0) { // child
        int j = 5;
        struct sigaction act;
        act.sa_handler = handler_parent_signal;        
        sigemptyset(&act.sa_mask);        
        sigaddset(&act.sa_mask, SIGQUIT);
        act.sa_flags = 0;
        sigaction(SIGUSR2, &act, NULL);

        while(j--) {
            sleep(1);
            kill(getppid(), SIGUSR1);
        }
    } else {
        perror("fork");
        exit(1);
    }
    
    return 0;
}

运行结果:

I am CHILD process, I have received parent signal
The signo is: 12
I am PARENT process, I have received child signal
The signo is: 10
I am CHILD process, I have received parent signal
The signo is: 12
I am CHILD process, I have received parent signal
The signo is: 12
I am PARENT process, I have received child signal
The signo is: 10
I am CHILD process, I have received parent signal
The signo is: 12
I am PARENT process, I have received child signal
The signo is: 10
I am CHILD process, I have received parent signal
The signo is: 12
I am PARENT process, I have received child signal
The signo is: 10
I am CHILD process, I have received parent signal
The signo is: 12
I am PARENT process, I have received child signal
The signo is: 10

六.信号引起的时序竞态和异步IO

信号会导致异步事件发生,因此在捕捉函数里面应尽量调用可重入函数.

◆ C标准库提供的信号处理函数

typedef void (*sighandler_t)(int)
sighandler_t signal(int signum, sighandler_t handler)
int system(const char *command)
集合fork,exec,wait一体

◆ pause()函数

pause()函数的调用过程为:当代码执行到pause()函数后,内核会使调用进程挂起,直到有信号递达,如果递达信号是忽略,则继续挂起。注意:是递达!,只有当信号真正被信号处理函数接收并处理才算递达,当信号被信号处理函数处理完毕后,会继续执行pause()以下的代码。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

static void handle_signal(int signo)
{
    printf("received a signal...signo is: %d\n", signo);
}


int main()
{
    
    struct sigaction act;
    act.sa_handler = handle_signal;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGUSR1, &act, NULL);
    pause();
    // Process Resuming...    
    printf("process resuming.... \n");
    return 0;
}

另外开启一个窗口,给指定进程发送SIGUSR1信号,打印:

received a signal...signo is: 10
process resuming....

因为信号会导致代码跳转去执行信号处理函数,因此在使用信号时,应尽量使用可重入函数不含全局变量和静态变量是可重入函数的一个要素。

七.使用SIGCHLD+waitpid的形式来回收子进程

当子进程终止(运行结束)的时候,内核会向父进程发一个SIGCHLD的信号,告诉父进程你儿子挂了(父进程有义务要回收子进程),利用这个机制,可以在父进程收到SIGCHLD信号的时候再去调用wait/waitpid()函数来回收子进程。这比使用轮询或者阻塞的方式来回收子进程更为合理。因此,推荐使用SIGCHLD+waitpid()的形式来回收子进程。

SIGCHLD信号的产生途径主要有以下几种:

子进程终止时
子进程接收到SIGSTOP信号停止时
子进程处在停止态,接受到SIGCONT后唤醒时

而使用waitpid()函数的好处在于:waitpid可以通过设置某些参数,从而实现非阻塞,且waitpid()可以通过status参数来获取到子进程退出时的状态。系统提供了一些宏函数来判断子进程的终止状态:

pid_t waitpid(pid_t pid, int *status, int options)
options
    WNOHANG
        没有子进程结束,立即返回
    WUNTRACED
        如果子进程由于被停止产生的SIGCHLD, waitpid则立即返回
    WCONTINUED
        如果子进程由于被SIGCONT唤醒而产生的SIGCHLD, waitpid则立即返回

获取status
    WIFEXITED(status)
        子进程正常exit终止,返回真
            WEXITSTATUS(status)返回子进程正常退出值
    WIFSIGNALED(status)
        子进程被信号终止,返回真
            WTERMSIG(status)返回终止子进程的信号值
    WIFSTOPPED(status)
        子进程被停止,返回真
            WSTOPSIG(status)返回停止子进程的信号值
    WIFCONTINUED(status)
        子进程由停止态转为就绪态,返回真            

示例:使用SIGCHLD信号来回收子进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>



void handle_signal(int signo)
{
    printf("I have received a signal, the signo is: %d\n", signo);
    int status;
    pid_t pid;
    while ((pid = waitpid(0, &status, WNOHANG))>0) {
        if (WIFEXITED(status))
            printf("child %d exit %d\n", pid, WEXITSTATUS(status));
        else if (WIFSIGNALED(status))
            printf("child %d cancel signal %d\n", pid, WTERMSIG(status));
    }
}


int main()
{

    pid_t pid;
    pid = fork();
    if (pid > 0) {
        struct sigaction act;
        act.sa_handler = handle_signal;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGCHLD, &act, NULL);
        while (1);
    } else if (pid == 0) {
        int n = 10;
        while (n--) {
            sleep(2);
            printf("I am child process[%d]\n", getpid());
        }
    } else {
        perror("fork");
        exit(1);
    }     
    return 5;
}

另外开辟一个终端,通过ps aux 获取到子进程ID,向子进程(6579)发送kill -9 信号

kill -9 6579

运行结果:

I am child process[6579]
I am child process[6579]
I am child process[6579]
I am child process[6579]
I am child process[6579]
I am child process[6579]
I am child process[6579]
I am child process[6579]
I am child process[6579]
I have received a signal, the signo is: 17
child 6579 cancel signal 9

八.向信号捕捉函数传参

之前使用kill()函数发送信号给进程是无法传递参数的,可以通过sigqueue函数向指定进程发送信号并传递参数:

int sigqueue(pid_t pid, int sig, const union sigval value)
union sigval {
int sival_int;
void  *sival_ptr;
};

这里传递的类型是一个联合体,也就意味着可以向一个进程传递一个整型或是一个地址。

需要注意两点:

1.因为两个进程的地址(用户空间)不一样,所以传递一个地址过去会很怪异(如果这两个进程有血缘关系则另当别论,因为fork出来的子进程继承了父进程的一些东西)。因此,不推荐传递地址。别的情况比如我在父进程的数据段上定义了一个变量n,这个变量n所在地址在两个子进程当中所在的地址是相同的。这种情况下传递变量n是没有问题的。

2.如果要给进程传递参数,接收的时候就不能再使用sigaction结构体中的sa_handler这个函数指针来接收了,必须使用另一个函数指针sa_sigaction来接收,同时,还需要将sa_flags设置为:SA_SIGINFO。sa_sigaction的函数原型为:

void (*sa_sigaction)(int, siginfo_t *, void *)

第二个参数siginfo_t *中包含了传递过来的参数,保存在成员si_value中,  siginfo_t 的数据结构如下:

siginfo_t {
               int      si_signo;    /* Signal number */
               int      si_errno;    /* An errno value */
               int      si_code;     /* Signal code */
               int      si_trapno;   /* Trap number that caused
                                        hardware-generated signal
                                        (unused on most architectures) */
               pid_t    si_pid;      /* Sending process ID */
               uid_t    si_uid;      /* Real user ID of sending process */
               int      si_status;   /* Exit value or signal */
               clock_t  si_utime;    /* User time consumed */
               clock_t  si_stime;    /* System time consumed */
               sigval_t si_value;    /* Signal value */
               int      si_int;      /* POSIX.1b signal */
               void    *si_ptr;      /* POSIX.1b signal */
               int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
               int      si_timerid;  /* Timer ID; POSIX.1b timers */
               void    *si_addr;     /* Memory location which caused fault */
               long     si_band;     /* Band event (was int in
                                        glibc 2.3.2 and earlier) */
               int      si_fd;       /* File descriptor */
               short    si_addr_lsb; /* Least significant bit of address
                                        (since Linux 2.6.32) */
           }

例:向指定进程传递一个整型值

发送进程代码:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>


int main(int argc, char *argv[])
{
    
    char *pid_str = argv[1];
    char *sig_str = argv[2];
    char *int_str = argv[3];

    pid_t pid = atoi(pid_str);
    int signo = atoi(sig_str);    
    int var = atoi(int_str);
    
    printf("send pid is: %d, signo is: %d, value is: %d\n", pid, signo, var);
    union sigval value;
    value.sival_int = var;
    sigqueue(pid, signo, value);    

    return 0;
}

接收进程代码:

#include <signal.h>
#include <stdio.h>


void handle_signal(int signo, siginfo_t *siginfo, void *var)
{
    printf("I have received a signal, the signo is: %d\n", signo);
    union sigval aaa = siginfo->si_value;
    printf("The Transmission int is: %d\n", aaa.sival_int);

}


int main()
{
    struct sigaction act;
    act.sa_sigaction = handle_signal;
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_SIGINFO;
    sigaction(SIGUSR1, &act, NULL);
    while(1);
    return 0;
}

分别编译,先运行接收进程,再运行发送进程,并向发送传递传递参数:

./app 6868 10 123

父进程打印如下:

I have received a signal, the signo is: 10
The Transmission int is: 123

 

posted @ 2018-02-28 16:42  夜行过客  阅读(269)  评论(0编辑  收藏  举报