子程序的終止屬異步事件,父程序無法預知其子程序何時終止(即使父程序向子程序發送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 處理程式都簡單地由以下代碼組成,僅僅捕獲已終止子程序而不關心其退出狀态
上述循環會一直持續下去,直至 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()。