waitpid(or wait)和SIGCHILD的关系

    我们知道一个子进程在退出的时候会给其父进程发送一个SIGCHILD信号以告诉父进程"我已经退出了",在父进程中为了避免僵尸进程一般都会在SIGCHILD信号处理函数中调用waitpid or wait来回收子进程的退出状态。示例如下:

#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
#include<string.h>
#include<errno.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
  
static void sig_handler(int );
  
int main(int argc, char *argv[])
{
    pid_t pid;
  
    if(signal(SIGCHLD, sig_handler) == SIG_ERR)
    {
        fprintf(stderr, "signal error : %s\n", strerror(errno));
        return 1;
    }
  
    if((pid = fork()) < 0)
    {
        fprintf(stderr, "fork error : %s\n", strerror(errno));
        return 1;
    }else if(pid == 0)
    {
        sleep(1);
        printf("I am child, ppid is : %d\n", getppid());
        exit(0);
    }else
    {
        printf("I am parent, child pid is : %d\n", pid);
        sleep(5);
    }
    return 0;
}
  
static void sig_handler(int signum)
{
    int saveerr = errno;
    if(waitpid(-1, NULL, 0) < 0)
    {
        fprintf(stderr, "waitpid error : %s\n", strerror(errno));
        errno = saveerr;
        return;
    }
    printf("child is exit.\n");
    errno = saveerr;
}
    那么SIGCHILD和waitpid到底是一个什么关系呢?
    其实这两者之间没有必然的关系。
  1. SIGCHILD只是在子进程退出的时候发送给父进程的一个信号值,这是一种异步通知父进程的方式.父进程可以捕获,忽略这个信号,忽略这个信号也是避免僵尸进程的一种方式.
  2. waitpid or wait回收子进程的结束状态,避免子进程进入僵尸状态.
  3. 主进程可以直接调用waitpid or wait来回收子进程的结束状态,不一定非得通过SIGCHILD信号处理函数,也就是说waitpid or wait不是依靠SIGCHLD信号是否到达来判断子进程是否结束.但是如果主进程除了回收子进程状态以外还有其他的业务需要处理那么最好是通过SIGCHILD信号处理函数来调用waitpid or wait,因为这是异步的操作.
  4. 如果注册了SIGCHLD信号处理函数,那么就需要等待SIGCHLD信号的到达并且完成信号处理函数,waitpid or wait才能接受到子进程的的退出状态.
    前面的3点可能很多同学都知道,但是对第4点可能没有关注过,其实以前我也没有关注过第4点,某天man system的时候看到"During execution of the command, SIGCHLD will be blocked",觉得有点不解."为什么要阻塞SIGCHLD信号呢?是不是跟waitpid有关呢?"

    在system实现中会调用waitpid来回收子进程的状态,首先想到的一点是:阻塞SIGCHLD是为了避免主进程已经注册的SIGCHLD处理函数回收所有的子进程状态,那么在system中的waitpid调用会导致ECHILD(No child processes)的错误.为了证实自己的想法是否正确在网上查了一下,最后发现还跟第4点有关系,因为如果不阻塞SIGCHLD信号并且主进程注册了SIGCHLD信号处理函数,那么就需要等主进程的信号处理函数返回waitpid才能接受到子进程的退出状态,也就是如果信号处理函数需要1min才能处理完那么system也需要1min才能返回.所以在调用system函数的时候阻塞SIGCHLD,这样在执行期间信号被阻塞就不会调用信号处理函数了,system中的waitpid就能"及时"的获取到子进程的状态,然后"及时"退出.

#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<string.h>
#include<errno.h>
  
void sig_handler(int signo)
{
    printf("receve sig : %d\n", signo);
    sleep(3);
}
int main(void)
{
    pid_t pid;
    /*sigset_t mask, savemask;*/
  
    /*sigemptyset(&mask);*/
    /*sigaddset(&mask, SIGCHLD);*/
    /*if(sigprocmask(SIG_BLOCK, &mask, &savemask) < 0)*/
    /*{*/
        /*fprintf(stderr, "sigprocmask error : %s\n", strerror(errno));*/
    /*}*/
  
    signal(SIGCHLD, sig_handler);
    if((pid = fork()) < 0)
    {
        fprintf(stderr, "fork error : %s\n", strerror(errno));
        return 1;
    }else if(pid == 0)
    {
        /*if(sigprocmask(SIG_SETMASK, &savemask, NULL) < 0)*/
        /*{*/
            /*fprintf(stderr, "sigprocmask error : %s\n", strerror(errno));*/
        /*}*/
        sleep(1);
        printf("I am child.\n");
        exit(1);
    }else
    {
        fprintf(stderr, "wait child exit.\n");
        if(waitpid(pid, NULL, 0) < 0)
        {
            fprintf(stderr, "waitpid error : %s\n", strerror(errno));
        }
        fprintf(stderr, "child exit.\n");
        /*if(sigprocmask(SIG_SETMASK, &savemask, NULL) < 0)*/
        /*{*/
            /*fprintf(stderr, "sigprocmask error : %s\n", strerror(errno));*/
        /*}*/
    }
    return 0;
}

    上面的示例中"child exit"的输出要等sig_handler处理完才会输出,如果把注释的语句去掉并注释signal(SIGCHLD, sig_handler);那么"child exit"在子进程退出以后马上就会输出.

     下面再考虑一下这个场景:有一个主进程会不断的生成子进程,为了避免僵尸进程,在主进程中注册了SIGCHLD信号,并且在信号处理函数中根据如下代码来回收子进程的退出状态,这段代码是否正确?是否能完全避免僵尸进程?

void sig_handler(int signo)
{
    while(waitpid(-1, NULL, 0) > 0);
}

    我们知道linux下的SIGCHLD是不排队的信号,也就是说如果有两个SIGCHLD信号同时到达那么sig_handler函数只会被调用一次,那么是不是就只有一个子进程会回收另外一个不会回收成为僵尸进程?答案是:否

  1. 前面已经提到了,子进程的状态回收和SIGCHLD信号没有关系,无论是否收到子进程的SIGCHLD信号waitpid都可以回收已经结束的子进程状态.
  2. 即使有信号的丢失,只要sig_handler函数调用一次就能回收所有已经结束的子进程状态.所以丢失多少个SIGCHLD信号都没有关系,只要最后调用了sig_handler一次就可以回收所有的状态。



你可能感兴趣的:(linux,信号,waitpid,SIGCHLD)