在8.13節,我們展示了一個system函數的實現。然而,那個版本沒有處理信號。POSIX.1要求system忽略SIGINT和SIGQUIT並阻塞SIGCHLD。在展示正確處理這些信號的版本之前,我們看下為什麼需要擔心這些信號的處理。
下 面的代碼使用了8.13節的system版本來調用ed編輯器。(這個編輯器作為UNIX系統的一部分已經有很長時間了。我們在這裡使用它是因為它是一個 捕獲中斷和退出信號的交互式程序。如果我們調用一個外殼並輸入中斷符,那麼它捕獲這個中斷符並打印一個問號。ed程序也設置了退出信號的布署以便它被忽 略。)
<span style="font-size:18px;"><strong>#include <signal.h> static void sig_int(int signo) { printf("caught SIGINT\n"); } static void sig_chld(int signo) { printf("caught SIGCHLD\n"); } int main(void) { if (signal(SIGINT, sig_int) == SIG_ERR) { printf("signal(SIGINT) error\n"); exit(1); } if (signal(SIGCHLD, sig_chld) == SIG_ERR) { printf("signal(SIGCHLD) error\n"); exit(1); } if (system("/bin/ed") < 0) { printf("system() error"); exit(1); } exit(0); }</strong></span>
上面的代碼同時捕獲了SIGINT和SIGCHLD。運行結果為:
$ ./a.out
a (添加文本命令)
Here is one line of text
. (終止添加模式)
1,$p (從第一行開始打印)
Here is one line of text
w temp.foo (把緩沖寫入文件)
25 (寫了25個字節)
q (退出)
caught SIGCHLD
當 編輯器終止時,系統向父進程(a.out進程)發送SIGCHLD信號。我們捕獲它並從信號處理器返回。但是如果它正在捕獲SIGCHLD信號,父進程應 該正這樣做,因為它已經創建了它自己的子進程,以便知道它的子進程何時終止。在system函數執行時這個信號的分發應該在父進程裡被阻塞。事實上,這是 POSIX.1規定的。否則,當system創建的子進程終止時,它將誤導system的調用者認為它自己的一個子進程終止了。調用者然後會使用某個 wait函數來得到子進程的終止狀態,因而避免system函數得到子進程的終止狀態作為它的返回值。
如果我們再次運行程序,這次向編輯器發送一個中斷信號,會有:
$ ./a.out
a
hello, world
.
1,$p
hello, world
w temp.foo
13
^Ccaught SIGINT
?
q
caught SIGCHLD
回想9.6節,輸入中斷符會導致中斷信號被發送給前台進程組的所有進程。前台進程有a.out,/bin/sh和/bin/ed。
在 這個例子裡,SIGINT被發送給所有這三個前台進程。(後台的外殼忽略這個信號。)正如我們能從輸出看到的,a.out進程和編輯器捕獲了這個信號。但 是當我們用system函數運行另一個程序時,我們不該讓父進程和子進程同時捕獲兩個終端產生的信號:中斷和退出。這兩個信號應該被發送給實際正在運行的 程序:子進程。
因為system執行的命令可以是一個交互式命令(這個例子裡是ed程序),而且system的調用者在程序執行時放棄了控制而等待它的結 束,所以system的調用者不應該收到這兩個終端產生的信號。這是為什麼POSIX.1規定system函數應該在等待命令完成時忽略這兩個信號。
下面的代碼展示了含所需的信號處理的system函數的一個實現:
<span style="font-size:18px;"><strong>#include <sys/wait.h> #include <errno.h> #include <unistd.h> int system(const char *cmdstring) /* with appropriate signal handling */ { pid_t pid; int status; struct sigaction ignore, saveintr, savequit; sigset_t chldmask, savemask; if (cmdstring == NULL) return(1); /* always a command processor with UNIX */ ignore.sa_handler = SIG_IGN; /* ignore SIGINT and SIGQUIT */ sigemptyset(&ignore.sa_mask); ignore.sa_flags = 0; if (sigaction(SIGINT, &ignore, &saveintr) < 0) return(-1); if (sigaction(SIGQUIT, &ignore, &savequit) < 0) return(-1); sigemptyset(&chldmask); /* now block SIGCHLD */ sigaddset(&chldmask, SIGCHLD); if (sigprocmask(SIG_BLOCK, &chldmask, &savemask) < 0) return(-1); if ((pid = fork()) < 0) { status = -1; /* probably out of processes */ } else if (pid == 0) { /* child */ /* restore previous signal actions & reset signal mask */ sigaction(SIGINT, &saveintr, NULL); sigaction(SIGQUIT, &savequit, NULL); sigprocmask(SIG_SETMASK, &savemask, NULL); execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); _exit(127); /* exec error */ } else { /* parent */ while (waitpid(pid, &status, 0) < 0) if (errno != EINTR) { status = -1; /* error other than EINTR from waitpid() */ break; } } /* restore previous signal actions & reset signal mask */ if (sigaction(SIGINT, &saveintr, NULL) < 0) return(-1); if (sigaction(SIGQUIT, &savequit, NULL) < 0) return(-1); if (sigprocmask(SIG_SETMASK, &savemask, NULL) < 0) return(-1); return(status); }</strong></span>
如果我們使用這個版本的system,得到的結果和前面(有缺陷的)那個的結果不同在於:
1、沒有信號被發送給調用進程,當我們輸入中斷和退出符;
2、當ed命令退出時,SIGCHLD不會被發送到調用進程。事實上,它被阻塞,直到我們在最後一個sigprocmask的調用裡反阻塞它,在system函數通過調用waitpid得到子進程的終止狀態之後。
POSIX.1 指出如果wait或waitpid在SIGCHLD待定時返回了一個子進程的狀態,那麼SIGCHLD不應該被分發給進程,除非另一個子進程的狀態也可 用。本書的四個實現沒有一個實現了這個語義。相反,在system函數調用waitpid後SIGCHILD仍保持待定;當信號被反阻塞時,它被分發給了 調用者。如果我們在sig_chld裡調用wait,它將返回-1,errno被設為ECHILD,因為system函數已經得到了子進程的終止狀態。
許多老的書本都用如下方式忽略中斷和退出信號:
<span style="font-size:18px;"><strong>if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { /* child */ execl(...); _exit(127); } /* parent */ old_intr = signal(SIGINT, SIG_IGN); old_quit = signal(SIGQUIT, SIG_IGN); waitpid(pid, &status, 0); signal(SIGINT, old_intr); signal(SIGQUIT, old_quit);</strong></span>
這個代碼的問題是我們不能保證在fork後父子進程誰先運行。如果子進程先運行而父進程在之後一段時間之內沒有運行,那麼一個中斷信號可能在父進程改變它的布署為被忽略是被產生。由於這個原因,我們新的system函數裡在fork之間改變信號的布署。
注意我們必須在子進程裡調用execl之前重置這兩個信號的布署。這允許execl改變它們的布署為默認,基於調用者的布署,如在8.10節裡描述的。
sytem的返回值
注 意system的返回值。它是外殼的終止狀態,並不總是命令字符串的終止狀態。我們在第8章看到過一些例子,而且結果和我們預料的一樣:如果我們執行一個 簡單的命令,比如date,那麼終止狀態是0。執行外殼命令exit 44給我們一個44的終止狀態。用信號會發生什麼呢?
讓我們運行第8章的程序並發送一些信號給正在執行的命令:
$ tsys "sleep 30"
^Cnormal termination, exit status = 130
$ tsys "sleep 30"
^\sh: 946 quit
normal termination, exit status = 131
(我系統上沒有這個問題。pr_exit打印出期望的值:異常退出。可能我的系統的system運行時,中斷信號由“sh -c sleep 30”,而不是“sleep 30”響應。
當 我們用中斷信號終止sleep時,pr_exit函數認為它正常終止。當我們用退出鍵殺死sleep時會發生同樣的事。這裡發生的事是Bourne外殼有 一個糟糕文檔的特性,它終止狀態是128加上一個信號號,當它正在執行的命令被一個信號終止時。我們可以用外殼交互地看下這個:
$ sh -c "sleep 30"
^C
$ ehco $?
130
$ sh -c "sleep 30"
^\sh: 962 Quit - core dumped
$ ehco $?
131
$ exit
在被使用的系統上,SIGINT的值為2,SIGQUIT的值為3,所以給了我們130和131的終止狀態。
讓我們嘗試一個相似的例子,但是這次我們將直接向外殼發送一個信號並看system返回了什麼:
$ ./tsys "sleep 30" &
$ ps -f
UID PID PPID C STIME TTY TIME CMD
tommy 8956 8949 0 12:04 pts/0 00:00:00 bash
tommy 9122 8956 0 12:23 pts/0 00:00:00 sh
tommy 9135 9122 0 12:25 pts/0 00:00:00 ./tsys sleep 30
tommy 9136 9135 0 12:25 pts/0 00:00:00 sh -c sleep 30
tommy 9137 9136 0 12:25 pts/0 00:00:00 sleep 30
tommy 9138 9122 0 12:25 pts/0 00:00:00 ps -f
$ kill -KILL 9136 (殺死“sh -c sleep 30”)
$ Killed
abnormal termination, signal number = 9
這裡,我們可以看到system的返回值只當外殼自身異常終止時報告一個異常終止。如果殺死“sleep 30”而不是“sh -c sleep 30”:
$ ./tsys "sleep 30" &
$ ps -f
UID PID PPID C STIME TTY TIME CMD
tommy 8956 8949 0 12:04 pts/0 00:00:00 bash
tommy 9356 8956 0 12:47 pts/0 00:00:00 sh
tommy 9357 9356 0 12:47 pts/0 00:00:00 ./tsys sleep 30
tommy 9358 9357 0 12:47 pts/0 00:00:00 sh -c sleep 30
tommy 9359 9358 0 12:47 pts/0 00:00:00 sleep 30
tommy 9360 9356 0 12:47 pts/0 00:00:00 ps -f
$ kill -KILL 9359
$ Killed
normal termination, exit status = 137
一個使用system函數的程序時,要確保正確地解釋返回值。如果你調用fork、exec和wait,終止狀態和你調用system時的並不相同。