在8.13节,我们展示了一个system函数的实现。然而,那个版本没有处理信号。POSIX.1要求system忽略
SIGINT和SIGQUIT并阻塞SIGCHLD。在展示正确处理这些信号的版本之前,我们看下为什么需要担心这些信号的处理。
下 面的代码使用了8.13节的system版本来调用ed编辑器。(这个编辑器作为UNIX系统的一部分已经有很长时
间了。我们在这里使用它是因为它是一个 捕获中断和退出信号的交互式程序。如果我们调用一个外壳并输入中
断符,那么它捕获这个中断符并打印一个问号。ed程序也设置了退出信号的布署以便它被忽 略。)
#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); }
当 编辑器终止时,系统向父进程(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函数的一个实现:
#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); }
POSIX.1 指出如果wait或waitpid在SIGCHLD待定时返回了一个子进程的状态,那么SIGCHLD不应该被分发给进
程,除非另一个子进程的状态也可 用。本书的四个实现没有一个实现了这个语义。相反,在system函数调用
waitpid后SIGCHILD仍保持待定;当信号被反阻塞时,它被分发给了 调用者。如果我们在sig_chld里调用
wait,它将返回-1,errno被设为ECHILD,因为system函数已经得到了子进程的终止状态。
许多老的书本都用如下方式忽略中断和退出信号:
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);
这个代码的问题是我们不能保证在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时的并不相同。