SIGCHLD信号
上一章中讲解了进程相关的知识:在程序中可以使用fork()函数创建子进程,使用wait()与waitpid()函数使父进程阻塞,并通过循环,不断获取子进程状态,以保证父进程能顺利回收子进程。但是循环是极其浪费cpu的,能否使用循环之外的方法来解决这个问题呢?
本章学习的信号就是解决该问题的另一种方法。内核中的父子进程是异步运行的,当出现以下几种情况时,内核会向父进程发送17号信号SIGCHLD:
● 子进程终止时;
● 子进程接收到SIGSTOP信号停止时;
● 子进程处在停止态,接收到SIGCONT信号后被唤醒时。
SIGCHLD信号的默认处理动作是忽略,我们可以在程序中捕获该信号,为信号设置信号处理函数,促使父进程完成子进程的回收。
由第六章中创建进程的案例可知,代码中的代码段可分为3个部分:fork()之前的部分、父进程分支和子进程分支。SIGCHLD信号在进程状态变化时自动产生,程序中无需额外设置产生信号的代码;若在fork()之前的部分或子进程分支中注册捕获函数,那么子进程也能接收信号,因此信号捕获函数应在父进程分支中注册。
案例9:使用信号机制回收子进程。
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5 #include <signal.h>
6 void sys_err(char *str)
7 {
8 perror(str);
9 exit(1);
10 }
11 void do_sig_child(int signo) //信号处理函数
12 {
13 waitpid(0,NULL,WNOHANG);
14 }
15 int main(void)
16 {
17 pid_t pid;
18 int i;
19 for (i = 0; i < 5; i++) { //子进程创建
20 if ((pid = fork()) == 0)
21 break;
22 else if (pid < 0) //容错处理
23 sys_err("fork");
24 }
25 if (pid == 0) { //子进程分支
26 int n = 1;
27 while (n--) {
28 printf("child ID %d\n", getpid());
29 }
30 exit(i+1);
31 }
32 else if (pid > 0) { //父进程分支
33 struct sigaction act;
34 act.sa_handler = do_sig_child;
35 sigemptyset(&act.sa_mask);
36 act.sa_flags = 0;
37 sigaction(SIGCHLD, &act, NULL);
38 while (1) {
39 printf("Parent ID %d\n", getpid());
40 sleep(1);
41 }
42 }
43 return 0;
44 }
该案例在if分支中使用while()循环保持父进程运行(模拟父进程运行),等待子进程发送的信号递达。编译案例,执行程序,执行结果如下所示:
Parent ID 3521
child ID 3525
child ID 3523
Parent ID 3521
child ID 3526
Parent ID 3521
child ID 3524
child ID 3522
Parent ID 3521
……
按照原本的设想,父进程保持运行,子进程发送信号到父进程,那么子进程应被父进程中的信号捕获函数全部回收,但使用“ps aux”查看系统中的进程,发现除父进程外,还有如下的一个子进程存在:
itheima 3526 0.0 0.0 0 0 pts/0 Z+ 16:37 0:00 [te] <defunct>
并且由其中的STAT项可知,该子进程变成了一个僵尸进程。
这是因为,这个子进程与其它某个子进程同时死亡,并递送了SIGCHLD信号到父进程,父进程同时接收到两个信号,但由于SIGCHLD信号属于不可靠信号,其中没有消息队列,因此有一个SIGCHLD信号会被忽略,父进程只会调用一次信号处理函数,回收一个子进程。
也就是说,在调用信号处理函数之前,同时有多个子进程死亡,若要解决这个问题,可以对信号捕捉函数进行修改,使其能在一次调用中,同时处理多个子进程。改良后的代码如下所示。
案例10:改良以上使用信号回收子进程的代码,使信号捕捉函数可以回收多个子进程。
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5 #include <signal.h>
6 void sys_err(char *str)
7 {
8 perror(str);
9 exit(1);
10 }
11 void do_sig_child(int signo) //信号处理函数
12 {
13 int status;
14 pid_t pid;
15 while ((pid = waitpid(0, &status, WNOHANG)) > 0) {//判断子进程状态
16 if (WIFEXITED(status))
17 printf("child %d exit %d\n", pid, WEXITSTATUS(status));
18 else if (WIFSIGNALED(status))
19 printf("child %d cancel signal %d\n", pid, WTERMSIG(status));
20 }
21 }
22 int main(void)
23 {
24 pid_t pid;
25 int i;
26 for (i = 0; i < 10; i++) {
27 if ((pid = fork()) == 0) //创建一个子进程
28 break;
29 else if (pid < 0) //容错处理
30 sys_err("fork");
31 }
32 if (pid == 0) { //子进程执行流程
33 int n = 1;
34 while (n--) {
35 printf("child ID %d\n", getpid());
36 sleep(1);
37 }
38 return i+1;
39 }
40 else if (pid > 0) { //父进程执行流程
41 struct sigaction act;
42 act.sa_handler = do_sig_child;
43 sigemptyset(&act.sa_mask);
44 act.sa_flags = 0;
45 sigaction(SIGCHLD, &act, NULL); //注册捕获函数
46 while (1) { //保证父进程运行
47 printf("Parent ID %d\n", getpid());
48 sleep(1);
49 }
50 }
51 return 0;
52 }
该案例中将waitpid()的参数options设置为WNOHANG,当有信号递达时,捕获该信号,并在信号处理函数中结合while循环,通过waitpid()函数不断判断系统中是否有已退出的子进程,若有则获取子进程的pid,对其进行回收。信号处理函数中用到了两个用于判断进程退出状态的宏函数:WIFSIGNALED()和WTERMSIG(),这两个宏函数是与信号相关的宏函数,参数也是status,功能分别如下:
● WIFSIGNALED():若子进程由信号终止,则返回true;
● WTERMSIG():返回导致子进程终止的信号的编号,只有放WIFSIGNALED返回true时,才应使用此宏。
编译案例10,执行程序,执行结果如下所示:
Parent ID 3593
child ID 3597
child 3597 exit 4
Parent ID 3593
child ID 3598
child 3598 exit 5
Parent ID 3593
child ID 3595
child ID 3596
child 3595 exit 2
Parent ID 3593
child 3596 exit 3
Parent ID 3593
child ID 3594
child 3594 exit 1
Parent ID 3593
……
执行“ps aux”命令,内存中只有父进程在运行,说明案例10实现成功。