最近在看APUE第10章中關于system函數的POSIX.1的實作。關于POSIX.1要求system函數忽略SIGINT和SIGQUIT,并且阻塞信号SIGCHLD的論述,了解得不是很透徹,本文就通過實際的執行個體來一探究竟吧。
一、為什麼要阻塞SIGCHLD信号
#include <stdlib.h>
int system(const char *command);
函數工作大緻流程:system()函數先fork一個子程序,在這個子程序中調用/bin/sh -c來執行command指定的指令。/bin/sh在系統中一般是個軟連結,指向dash或者bash等常用的shell,-c選項是告訴shell從字元串command中讀取要執行的指令(shell将擴充command中的任何特殊字元)。父程序則調用waitpid()函數來為變成僵屍的子程序收屍,獲得其結束狀态,然後将這個結束狀态傳回給system()函數的調用者。
知道了以上基本知識點,也就好了解為什麼偏偏是SIGCHLD信号了,而不是其他的信号:因為fork的子程序結束後,核心會向其父程序發送SIGHLD信号,即system()函數的調用者。
那麼為什麼在調用system()函數,運作command指定的指令時要阻塞SIGCHLD這個信号呢? 接下來我們就通過兩個不同的system版本對比運作的結果,進而找到阻塞SIGCHLD信号的真正原因。
先來具體看看這兩個不同的system函數實作版本:
system_without_signal.c:
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include "apue.h"
/* version without signal handling */
int system_without_signal(const char *cmd_string)
{
pid_t pid;
int status = -1;
if (cmd_string == NULL)
return (1); /* always a command processor with UNIX */
if ((pid = fork()) < 0) {
status = -1; /* probably out of processes */
} else if (pid == 0) { /* child */
execl("/bin/sh", "sh", "-c", cmd_string, (char *)0);
_exit(127); /* execl error */
} else { /* parent */
// sleep(1);
pid_t wait_pid;
while ((wait_pid = waitpid(pid, &status, 0)) < 0) {
printf("[in system_without_signal]: errno = %d(%s)\n",
errno, strerror(errno));
if (errno != EINTR) {
status = -1; /* error other than EINTR form waitpid() */
break;
}
}
printf("[in system_without_signal]: pid = %ld, wait_pid = %ld\n",
(long)pid, (long)wait_pid);
pr_exit("[in system_without_signal]", status);
}
return (status);
}
system_with_signal.c
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
/* with appropriate signal handling */
int system_with_signal(const char *cmd_string)
pid_t pid;
int status;
struct sigaction ignore, saveintr, savequit;
sigset_t chld_mask, save_mask;
/* ignore signal SIGINT and SIGQUIT */
ignore.sa_handler = SIG_IGN;
ignore.sa_flags = 0;
sigemptyset(&ignore.sa_mask);
if (sigaction(SIGINT, &ignore, &saveintr) < 0)
return (-1);
if (sigaction(SIGQUIT, &ignore, &savequit) < 0)
/* block SIGCHLD and save current signal mask */
sigemptyset(&chld_mask);
sigaddset(&chld_mask, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &chld_mask, &save_mask) < 0)
} else if (pid == 0) { /* child */
/* restore previous signal actions & reset signal mask */
sigaction(SIGINT, &saveintr, NULL);
sigaction(SIGQUIT, &savequit, NULL);
sigprocmask(SIG_SETMASK, &save_mask, (sigset_t *)NULL);
_exit(127);
} else { /* parent */
int wait_pid;
// sleep(10); /* */
printf("[in system_with_signal]: errno = %d(%s)\n",
status = -1; /* error other than EINTR from waitpid() */
printf("[in system_with_signal]: pid = %ld, wait_pid = %ld\n",
pr_exit("[in system_with_signal]", status);
/* in parent: restore previous signal action & reset signal mask */
if (sigaction(SIGINT, &saveintr, NULL) < 0)
if (sigaction(SIGQUIT, &savequit, NULL) < 0)
if (sigprocmask(SIG_SETMASK, &save_mask, (sigset_t *)NULL) < 0) /* */
好,接下來具體看一個例子:
#define SETSIG(sa, sig, fun, flags) \
do { \
sa.sa_handler = fun; \
sa.sa_flags = flags; \
sigemptyset(&sa.sa_mask); \
sigaction(sig, &sa, NULL); \
} while (0)
extern int system_without_signal(const char *cmd_string);
static void sig_chld(int signo)
printf("\nenter SIGCHLD handler\n");
int exit_status = -1;
int errno_saved = errno;
pid = wait(&exit_status);
if (pid != -1) {
printf("[in sig_chld] reaped %ld child,", (long)pid);
pr_exit("wait: ", exit_status);
printf("\n");
} else {
printf("[in sig_chld] wait error: errno = %d(%s)\n\n",
errno = errno_saved;
printf("leave SIGCHLD handler\n");
int main(int argc, const char *argv[])
struct sigaction sigchld_act;
SETSIG(sigchld_act, SIGCHLD, sig_chld, 0);
int status;
if ((status = system_without_signal("/bin/ls -l; exit 44")) < 0) {
err_sys("system() error(status = %d): ", status);
pr_exit("system() return:", status);
exit(EXIT_SUCCESS);
在這個例子中,我們調用的是system_without_signal,即不處理信号的system實作版本,并且調用者還設定了SIGCHLD的信号處理函數。好,基于這些條件,接下來我們考慮兩種情形:
情形1:在子程序正在運作指定程式時,或者說在子程序結束之前,父程序中的waitpid阻塞在那裡。
這種情形下,一旦子程序結束,核心會向應用程式遞送SIGCHLD信号,運作信号處理函數,在信号處理函數中調用wait系列函數,那麼現在問題來了:究竟是信号處理函數中的wait系列函數還是system_without_signal中的waitpid為子程序收屍呢? 答案是未知的。因為信号本身是異步的,我們掌控不了(在我的系統中,waitpid還總能正确的擷取子程序退出狀态,而在信号處理函數中的wait卻傳回-1,errno設定為ECHLD,表明沒有可收屍的子程序,見下圖。但是,在你的系統中,結果也許就是相反的噢)。是以,在這種情形下,我們得出的結論是:盡管system函數完成了其任務(正确執行了我們指定的程式),但卻有可能傳回-1。很顯然,這不是我們希望發生的。
情形2:在一個繁忙的系統中,很可能在調用waitpid之前子程序就已經結束了,此時核心會向父程序遞送SIGCHLD信号。
在這種情形下,問題就更明顯了。在調用waitpid之前就已經調用了SIGCHLD信号的信号處理函數,信号處理函數中的wait函數為子程序收了屍,那麼接下來的waitpid不就擷取不了子程序的退出狀态了嗎? 事實也的确如此!我們可以在waitpid之前調用加個sleep來模拟系統負荷重的情形,會發現waitpid會出錯,傳回-1,errno設定為ECHLD,表明沒有可收屍的子程序,最終system函數傳回-1。是以,在這種情形下,我們得出的結論是:盡管system函數完成了其任務(正确執行了我們指定的程式),但卻一直傳回-1。很顯然,這也不是我們希望發生的。
如果将上面例子中的system_without_signal替換成system_with_signal,那麼system函數在調用fork之前就已經阻塞了SIGCHLD信号的話,那麼就不會出現上述兩種情況了。因為阻塞了SIGCHLD信号,那麼不管system函數建立的子程序什麼時候結束,即不管SIGCHLD信号什麼時候來,在沒有解除阻塞之前,是不會處理該信号的,即SIGCHLD信号是未決的。是以,無論如何,waitpid都會正确擷取子程序的退出狀态。隻有在最後調用sigprocmask時,系統才會解除對SIGCHLD的阻塞。解除阻塞後,這才調用信号處理函數,不過這次信号處理函數中的wait會出錯,傳回-1,errno設定為ECHLD,表明沒有可收屍的子程序。那麼system函數就能正确的傳回子程序的退出狀态了。
看到這裡,你可能會說,問題都是SIGCHLD信号處理函數中的wait惹的禍,如果去掉SIGCHLD信号處理函數中的wait函數,不就不會帶來上述的兩個問題了嗎? 我的答案是:的确可以避免上述兩個問題,即system函數可以正确的擷取子程序的退出狀态。但是這樣做還是會有問題的:我們先不管在SIGCHLD信号處理函數中不調用wait系列函數這種不正統的做法,我們在這裡考慮這樣一種情形:如果信号處理函數需要運作一分鐘的時間才傳回(實際程式設計中,信号處理函數要盡量短噢,這裡隻是一種極端的假設),那麼system函數豈不是也要阻塞一分鐘才能傳回?因為如果不阻塞SIGCHLD信号并且主程序注冊了SIGCHLD信号處理函數(未調用wait系列函數),那麼就需要等主程序的信号處理函數傳回後waitpid才能接受到子程序的退出狀态,也就是信号處理函數需要運作多長時間,那麼system也就需要這麼多時間才能傳回。一個函數的運作受到外界不确定因素的影響,這種情形還是應該避免的。是以在調用system函數的時候阻塞SIGCHLD,這樣在執行期間信号被阻塞就不會調用信号處理函數了,system中的waitpid就能"及時"地擷取到子程序的狀态。-- 但是仔細想想,其實system函數還是避免不了這種情形的,因為在最後調用sigprocmask解除阻塞時(一般在sigprocmask傳回之前,就至少遞送一個阻塞的信号),還是會調用信号處理函數,system依然會阻塞,唯一的不同是,這種情況下waitpid是在調用信号處理函數之前就擷取了子程序的退出狀态,避免了多線程的諸多影響。是以,在平時的程式設計實踐當中,信号處理函數要盡量的短,這樣才不會對其他函數造成不必要的未知影響。
好,稍微總結一下:
system函數之是以阻塞SIGCHLD,是為了保證system函數能夠正确擷取子程序的退出狀态,并傳回給system的調用者。
由此我們也可以引申出以下結論:
如果以後要寫一個函數,函數中fork了一個子程序,并且定義的函數要得到子程序的一些資訊,例如子程序的ID、子程序的終止狀态等,而該函數的調用者所注冊的SIGCHLD信号處理函數會影響這個函數擷取這些資訊,是以為了避免該函數在擷取這些資訊之前,由于子程序的終止觸發SIGCHLD信号而先調用信号處理函數,在fork之前應該将SIGCHLD信号阻塞,在函數正确擷取相關資訊後,才對SIGCHLD信号解除阻塞。
二、為什麼忽略SIGINT和SIGQUIT
關于這點,APUE的解釋已經很明白了:因為由system執行的指令可能是互動式指令(例如ed程式),以及因為system的調用者在指定的指令執行期間放棄了對程式的控制(waitpid阻塞在那裡),等待該執行程式的結束,是以system的調用者就不應該接收SIGINT和SIGQUIT信号,而隻由子程序接收,這也是在子程序中一開始恢複SIGINT和SIGQUIT信号的原因。其實說白了,還是因為希望擷取子程序的退出狀态不受到外界幹擾。
三、system函數的傳回值
很多人不推薦使用system函數,是因為它的傳回值很多人沒有弄清楚。
(1)當參數command是NULL的時候
在參數為NULL的情況下,system函數的傳回值很簡單明了,隻有0和1。傳回1,表明系統的指令處理程式,即/bin/sh是可用的。相反,如果指令處理程式不可用,則傳回0。我們可以通過一個簡單的實驗來驗證下這個結論:
<span style="font-family:Microsoft YaHei;">#include <stdio.h>
int ret = system(NULL);
printf("ret = %d\n", ret);
return 0;
</span>
在我的系統上通過ls -l /bin/sh可以看出/bin/sh是個軟連結,指向/bin/dash這個SHELL,我們可以通過unlink指令先取消這個軟連結,會發現程式傳回0,如果再次建立這個軟連結,則system傳回1.
(2)當參數command不是NULL的時候
當參數不為NULL的時候,情況有些小複雜,根據APUE這裡可以分為以下三種情況:
(2.1)如果fork等系統調用失敗,或者waitpid函數發生除EINTR外的錯誤時,system傳回-1
這種情況下,我們沒有辦法了,隻能檢測errno的值來判斷是哪個系統調用出錯以及出錯的原因!
那麼為什麼要排除waitpid發生EIINTR呢? 對于這個問題,我們可以假設system函數的調用者設定了SIGUSR1信号的處理函數,那麼當waitpid阻塞在那裡時,向程式發送SIGUSR1信号,則waitpid會傳回-1,errno被設定為EINTR。是以應該排除EINTR錯誤值,否則就擷取不到/bin/sh的退出狀态了。
(2.2)一切緻使execl失敗的情況下,system傳回127
緻使execl失敗的原因應該隻有兩個:/bin/sh不存在,再者就是指定的shell指令是非法的。
int ret = system("no_such_command");
pr_exit("", ret);
測試結果:
第一次傳回127是因為非法的指令,第二次卻是/bin/sh不存在導緻的。
那麼現在的問題是:如果指定的指令執行成功,且指令的傳回值正好也是127,那麼如何分辨是什麼原因呢(例如上述程式中的是system("exit 127"))? 貌似沒有辦法哦,是以我們在程式中盡量避免使用127作為傳回值。
(2.3)除此之外,system傳回/bin/sh的終止狀态
到這裡,要強調的一點是:system傳回的是/bin/sh的結束狀态,而不是我們指定的指令的傳回狀态,盡管大部分時間它們是一樣的。因為/bin/sh也有可能異常終止,例如人為的通過kill向其發送SIGKILL,那麼/bin/sh退出狀态就是9,而這跟指定的指令沒有任何關系。
盡管有時參數command代表的指令執行過程中出了錯,但這不會影響/bin/sh的正常退出,看下面執行個體:
其中的tsys請自行參考APUE。很明顯,xxx目錄是不存在的,ls執行過程中發生了錯誤,傳回值為2,shell接收到的就是512(為什麼是512,具體下篇文章),shell将該值轉換成2後,最後由waitpid接收到該終止狀态,即512,pr_exit列印的結果是2,正是ls傳回的終止狀态。
好了,通過之前的陳述我們知道system函數的傳回值即shell的終止狀态,這個終止狀态是通過waitpid獲得的,那麼怎麼解釋這個傳回值也就很明朗了 -- 使用檢查waitpid傳回值的那些宏就可以了,這也正式pr_exit實作的方式(參考APUE第8章)。
以上說的都是指令正常終止,那麼如果是異常終止了?system函數傳回值可以正确反映這種狀态嗎?我們通過實驗來驗證,先看信号SIGINT:
再來看下信号SIGQUIT:
可見通過system函數的傳回值是不可能知道程式是異常終止的,上面的傳回值之是以分别是130和131,是/bin/sh特殊處理的:當正在執行的指令是被信号終止的話,那麼終止狀态是128加上這個信号的編碼。
這裡提醒一下讀者,如果你照着APUE的實驗操作,即直接在終端鍵入Ctrl+C和Ctrl+\的話,你的結果可能與作者的是不一樣的。我的結果就與作者的不一樣:
你的系統上的結果也許和我的也不一樣的,原因是不同的shell對信号的處理方式是不一樣的,APUE作者使用的shell對SIGINT和SIGQUI的處理應該都是忽略,從我上面的結果可以看出,dash忽略信号SIGQUIT。未完待續!
參考連結:
<a href="http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=2078496" target="_blank">http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=2078496</a>
<a href="http://blog.chinaunix.net/uid-24774106-id-3048281.html?page=3" target="_blank">http://blog.chinaunix.net/uid-24774106-id-3048281.html?page=3</a>