学科分类
目录
Linux编程

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实现成功。

点击此处
隐藏目录