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