天天看點

Unix/Linux程式設計:SIGCHLD信号為SIGCHLD建立信号處理程式向已停止的子程序發送 SIGCHLD 信号忽略終止的子程序

子程序的終止屬異步事件,父程序無法預知其子程序何時終止(即使父程序向子程序發送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 信号。

Unix/Linux程式設計:SIGCHLD信号為SIGCHLD建立信号處理程式向已停止的子程式發送 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()。

繼續閱讀