SIGCLD语义

SIGCLD语义

SIGCLDSIGCHLD这两个信号一直都很容易混淆。首先,SIGCLD(没有H)是System V的名字,而且这个信号在BSD系统中有不同的语义,被命名为SIGCHLD。POSIX.1信号同样命名为SIGCHLD

BSD中的SIGCHLD信号的语义是正常的,和其它信号的语义是一样的。当信号发生时,子进程的状态会发生改变,并且我们需要去调用wait某一版本中的一个,去判断发生了什么。

System V中,无论可时,SIGCLD信号的处理方式都不同于其它信号。如果我们使用signalsigset(这个函数是用于兼容更老的SVR3系统,在该系统中部署信号的)部署该信号,基SVR4的系统延续了这个有问题的传统(也就是兼容性限制)。SIGCLD的处理方式由以下组成:

  1. 如果进程特别指定该信号部署到SIG_IGN,该进程的子进程将不会产生僵尸进程。注意:这不同于它的默认动作(SIG_DFL),默认为忽略。取而代之在终止时会丢弃子进程的状态。如果它连续的调用wait函数(几个版本中的一个),调用的进程会阻塞程序,直到所有它的子进程终止,并且wait返回1的同时把errno设置成ECHILD。(该信号的默认动作是忽略,但是这个默认动作不会有预计的效果发生。取尔代之,我们必须手动指定它的动作为SIG_IGN。)
    POSIX.1没有指定当SIGCHLD被忽略后会发生什么,所以这个行为是允许的。
    BSD4.4中,如果SIGCHLD被忽略后会产生僵尸进程。如果想避免僵尸进程,必须对子进程使用wait函数。FreeBSD 5.2.1工作方式和BSD4.4类似。Mac OS X 10.3,当SIGCHLD被忽略时,无论怎样都不会建立僵尸进程。
    SVR4中,如果signalsigset被调用,设置SIGCHLD的默认动作是忽略,僵尸进程是绝对不会产生的。Solaris 9、Linux 2.4.22和SVR4的行为是一样的。
    对于sigaction,我们能设置SA_NOCLDWAIT标记去避免僵尸进程。这个动作支持:FreeBSD5.2.1, Linux 2.4.22, Mac OS X 10.3和Solaris 9。
  2. 如果我们设置SIGCLD的行为是“捕获”,内核会立即检查所有子进程是否已经准备好wait了,如果是这样,调用SIGCLD处理函数。

第二项改变了信号的动作,我们要为信号写一个处理动作。下面用一个例子来解释。

例子:

Figure 10.6. System V SIGCLD handler that doesn’t work
#include      "apue.h"
#include      <sys/wait.h>

static void sig_cld(int);

int
main()
{
    pid_t   pid;

    if (signal(SIGCLD, sig_cld) == SIG_ERR)
        perror("signal error");
    if ((pid = fork()) < 0) {
        perror("fork error");
    } else if (pid == 0) {      /* child */
        sleep(2);
        _exit(0);
    }
    pause();    /* parent */
    exit(0);
}

static void
sig_cld(int signo)   /* interrupts pause() */
{
    pid_t   pid;
    int     status;

    printf("SIGCLD received\n");
    if (signal(SIGCLD, sig_cld) == SIG_ERR) /* reestablish handler */
        perror("signal error");
    if ((pid = wait(&status)) < 0)      /* fetch child status */
        perror("wait error");
    printf("pid = %d\n", pid);
}

信号处理函数做的第一件事是再次调用signal函数,去重新连接信号处理函数(这个动作是当信号被重置为它的默认动作并丢失时,最小化时间窗口”this action was to minimize the window of time when the signal is reset back to its default and could get lost.”)。这显示在上面的例子中。这个例子不能在一些平台运行。如果我们编译并运行它在一些传统的System V平台上(如:OpenServer 5或UnixWare 7),输出将会是一些连续的SIGCLD received行。最终,进程运行超出栈空间并异常终止。

FreeBSD 5.2.1Mac OS X 10.3并没有显示出这个问题,因为基于BSD的系统通常不支持历史上的System V中的SIGCLD语法。Linux 2.4.22同样没有显示这个问题,因为当一个进程准备去捕获SIGCHLD并且子进程已经准备好wait时,它并不调用SIGCHLD信号处理函数,即便SIGCLDSIGCHLD定义了相同的值。Solaris 9,虽然在这种情况下会调用用信号处理函数,但是它在内核中包含了额外的代码去避免这个问题。

虽然APUE中描述的四种平台都解决了这个问题,但实际平台上这个问题还是存在,只是没有被定位到。

这个示例程序的问题是:在信号处理函数的开始位置调用signal,也就是引用之前讨论的第2项——内核检查子进程是否须要wait,所以它会产生新的调用到信号处理函数。这个信号处理函数调用了signal,并且所有的过程要重来一次。

为了修复这个问题,我们必须移动signal函数到wait调用之后。通过这么做,我们在得到子进程终止状态后调用signal;只有当一些子进程刚好结束,内核才再次产生信号。

POSIX.1声明,当我们为SIGCHLD建立信号处理函数,并且存在一个终止的子进程我们还没有wait时,并未详细指出信号是否已经产生。这允许之前描述的行为。但是因为POSIX.1在信号发生时,并不会重置信号的默认动作,所以并不须要我们总是在信号处理函数中为SIGCHLD建立信号处理函数。

去熟悉你所使用实现的SIGCHLD语义。特别是你要知道一些系统是通过#define SIGCHLD来定义SIGCLD的,反之亦然。可以通过改名的方式让你的程序在其它系统上编译,但是如果那个系统是依赖于另一种语义的话,那可能不能工作。

发表回复