子进程的终止属异步事件,父进程无法预知其子进程何时终止(即使父进程向子进程发送SIGKILL信号,子进程终止的确切时间还依赖于系统的调度:子进程下一次在何时使用CPU)。父进程应该使用wait()或者类似调用来防止僵尸子进程的累积,以及采用如下两个方法来避免这一问题:
- 父进程调用不带 WNOHANG 标志的 wait(),或 waitpid()方法,此时如果尚无已经终止的子进程,那么调用将会阻塞
- 父进程周期性地调用带有 WNOHANG 标志的 waitpid(),执行针对已终止子进程的非阻塞式检查(轮询)。
这两种方法使用起来都有所不便:
- 一方面,可能并不希望父进程以阻塞的方式来等待进程的终止
- 另一方面,反复调用非阻塞的waitpid()会造成CPU资源的浪费,并增加应用程序设计的复杂的。
为了规避这些问题,可以采用针对SIGCHLD信号的处理程序
为SIGCHLD建立信号处理程序
无论一个子进程于何时终止,系统都会向其父进程发送 SIGCHLD 信号。对该信号的默认处理是将其忽略,不过也可以按照信号处理程序来捕获它。在处理程序中,可以使用wait()或者类似方法来收拾僵尸进程。
但是,当调用信号处理程序时,会暂时将引发调用的信号阻塞起来(除非为 sigaction()指定了 SA_NODEFER 标志),且不会对 SIGCHLD 之流的标准信号进行排队处理。这样一来,当SIGCHILD信号处理函数正在为一个终止的子进程运行时,如果相继由两个子进程终止,即使产生了两次SIGCHLD信号,父进程也只能捕获到一个。结果是,如果父进程的SIGCHLD信号处理程序每次只调用一次wait(),那么一些僵尸进程可能会成为“漏网之鱼”。
解决方案是:在SIGCHLD处理的程序内部循环以WNOHANG标准来调用waitpid(),直至再无其他终止的子进程需要处理为止。通常 SIGCHLD 处理程序都简单地由以下代码组成,仅仅捕获已终止子进程而不关心其退出状态
while(waitpid(-1, NULL, WNOHANG) > 0) continue;
上述循环会一直持续下去,直至 waitpid()返回 0,表明再无僵尸子进程存在,或-1,表示有错误发生(可能是 ECHILD,意即再无更多的子进程)
SIGCHLD 处理程序的设计问题
假设创建 SIGCHLD 处理程序的时候,该进程已经有子进程终止。那么内核会立即为父进程产生 SIGCHLD 信号吗?SUSv3 对这一点并未规定。一些源自系统 V(System V)的实现在这种情况下会产生 SIGCHLD 信号;而另一些系统,包括 Linux,则不这么做。为保障可移植性,应用应在创建任何子进程之前就设置好 SIGCHLD 处理程序,将这一隐患消解于无形
需要更深入考虑的问题是可重入性(reentrancy):在信号处理程序中使用系统调用(比如waitpid())可能会改变全局变量errno的值。当主程序试图显示设置errno或是在系统调用失败后检查 errno 值时,这一变化会与之发生冲突。出于这一原因,有时在编写 SIGCHLD 信号处理程序时,需要在一进入处理程序时就使用本地变量来保存 errno 值,而在返回前加以恢复。
看个例子:
//通过 SIGCHLD 信号处理程序捕获已终止的子进程
void /* Examine a wait() status using the W* macros */
printWaitStatus(const char *msg, int status)
{
if (msg != NULL)
printf("%s", msg);
if (WIFEXITED(status)) {
printf("child exited, status=%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("child killed by signal %d (%s)",
WTERMSIG(status), strsignal(WTERMSIG(status)));
#ifdef WCOREDUMP /* Not in SUSv3, may be absent on some systems */
if (WCOREDUMP(status))
printf(" (core dumped)");
#endif
printf("\n");
} else if (WIFSTOPPED(status)) {
printf("child stopped by signal %d (%s)\n",
WSTOPSIG(status), strsignal(WSTOPSIG(status)));
#ifdef WIFCONTINUED /* SUSv3 has this, but older Linux versions and
some other UNIX implementations don't */
} else if (WIFCONTINUED(status)) {
printf("child continued\n");
#endif
} else { /* Should never happen */
printf("what happened to this child? (status=%x)\n",
(unsigned int) status);
}
}
从程序下面的执行例子可以看出,尽管有 3 个子进程退出,而父进程只捕获到两次 SIGCHLD 信号。
向已停止的子进程发送 SIGCHLD 信号
正如可以使用 waitpid()来监测已停止的子进程一样,当信号导致子进程停止时,父进程也就有可能收到SIGCHLD 信号。调用 sigaction()设置 SIGCHLD 信号处理程序时,如传入 SA_ NOCLDSTOP 标志即可控制这一行为。若未使用该标志,系统会在子进程停止时向父进程发送 SIGCHLD 信号;反之,如果使用了这一标志,那么就不会因子进程的停止而发出 SIGCHLD信号
因为默认情况下会忽略信号 SIGCHLD,SA_NOCLDSTOP 标志仅在设置 SIGCHLD 信号处理程序时才有意义。而且,SA_NOCLDSTOP 只对SIGCHLD 信号起作用
SUSv3 也允许,当信号SIGCONT 导致已停止的子进程恢复执行时,向其父进程发送SIGCHLD信号。(相当于 waitpid()的 WCONTINUED 标志。)始于版本 2.6.9,Linux 内核实现了这一特性
忽略终止的子进程
更有可能像这样处理终止子进程:将对SIGCHLD的处置显式设置为SIG_IGN,系统从而会将其后终止的子进程立即删除,而不是转为僵尸进程。这时,会将子进程的状态之不问,故而所有后续的 wait()(或类似)调用不会返回子进程的任何信息
注意,虽然对信号 SIGCHLD 的默认处置就是将其忽略,但显式设置对 SIG_IGN 标志的处置还是会导致这里所描述的行为差异。在这方面,对信号 SIGCHLD 的处理非常独特,不同于其他信号
如果许多Unix实现一样,在Linux系统中将对SIGCHLD信号的处置置为SIG_IGN并不会影响任何僵尸进程的状态,对它们的等待仍然要照常进行。在其他一些 UNIX 实现中(例如 Solaris 8),将对 SIGCHLD 的处置设置为 SIG_IGN 确实会删除所有已有的僵尸进程。
信号 SIGCHLD 的 SIG_IGN 语义由来已久,源于系统 V(System V)。SUSv3 也规定了此
处所描述的行为,不过原始的 POSIX.1 标准对此则未作表述。因此,在一些较老的 UNIX 实现中,忽略 SIGCHLD 并不影响僵尸进程的创建。要防止产生僵尸进程,唯一完全可移植的方法就是(可能是从SIGCHLD 信号处理程序的内部)调用 wait()或者waitpid()。