system函数
在APUE8.13节,我们显示了一个system函数的实现。那个版本中并不处理任何信号。POSIX.1要求system忽略SIGINT和SIGQUIT并且阻塞SIGCHLD。之前显示了一个正确处理这些信号的版本,现在让我们看一下为什么要担心这些信号的处理。
例子
Figure 10.26的程序显示了一个使用APUE8.13节中的system函数调用ed编辑器。该程序捕获了SIGINT和SIGCHLD信号。如果我们调用这个程序,我们会得到:
$ ./a.out
a append text to the editor's buffer
Here is one line of text
. period on a line by itself stops append mode
1,$p print first through last lines of buffer to see what's there
Here is one line of text
w temp.foo write the buffer to a file
25 editor says it wrote 25 bytes
q and leave the editor
caught SIGCHLD
|
当编辑器终止时,系统发送SIGCHLD信号给它的父进程(也就是a.out进程)。我们捕获到它并且从信号处理函数中返回。But if it is catching the SIGCHLD signal, the parent should be doing so because it has created its own children, so that it knows when its children have terminated. 在system函数运行时信号传送到父进程里,该信号应该是被阻塞的。而且这也是POSIX.1指定的。另外,当由system函数穿件的子进程终止时,它应该欺骗system的调用者去认为自己的一个子进程已经终止了。那么调用者会使用wait函数去获得子进程的结束状态,从而防止system函数获得子进程的结束状态和返回值。
如果我们再次运行这个程序,这次给编辑器发送一个中断信号,我们会得到:
$ ./a.out
a append text to the editor's buffer
hello, world
. period on a line by itself stops append mode
1,$p print first through last lines to see what's there
hello, world
w temp.foo write the buffer to a file
13 editor says it wrote 13 bytes
^? type the interrupt character
? editor catches signal, prints question mark
caught SIGINT and so does the parent process
q leave editor
caught SIGCHLD
|
回忆APUE9.6节,输入中断字符引发的中断信号会发送到所有前台进程组的进程中。Figure10.27显示了当编辑器运行时,进程的安排。
在这个例子中,SIGINT被发送到三个前台进程中。(shell忽略它)就像我们从输出看到的那样,a.out进程和编辑器都捕获到了这个信号。但是当我们使用system函数运行另一个程序时,这两个进程不会都成为父进程并且子进程捕获两个终端产生的信号:中断和退出。这两个信号应该已经被发送到该运行的子进程。因为命令是被system函数执行的,该命令可以是一个交互命令(就像ed程序一样),并且因为system函数的调用者已经放弃了对该执行命令的控制权,等待它结束,system函数的调用者应该不会收到终端产生的这两种信号。这就是为什么POSIX.1指定system函数应该在命令执行期间忽略这两种信号(中断和退出)。
Figure 10.26. Using system to invoke the ed editor#include "apue.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) err_sys("signal(SIGINT) error"); if (signal(SIGCHLD, sig_chld) == SIG_ERR) err_sys("signal(SIGCHLD) error"); if (system("/bin/ed") < 0) err_sys("system() error"); exit(0); } |
例子
Figure10.28显示了system函数处理信号的实现。
如果我们我们在Figure10.26中使用我们这个版本的system函数,生成的二进制代码至少有以下一种缺陷。
- 当我们输入中断或退出字符时,没有信号发送到调用进程。
- 当ed命令退出时,SIGCHLD信号没有被发送到调用进程。取而代之的是,它被阻塞了直到我们在最近一次调用sigprocmask对其进行解锁,之后system函数通过调用waitpid“找回”子进程的终止状态。
(POSIX.1声明当SIGCHLD是pending状态时,如果wait或waitpid返回子进程的状态,那么SIGCHLD应该不能被传送到进程,除非其它子进程的状态也是可用的。本书中的四种实现并没有讨论它的语法。而是使用SIGCHLD在system函数调用waitpid后保留pending状态;当信号被解锁后,它会被传送到调用者进程那里。如果我们在Figure10.26的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后是父进程先运行还是子进程先运行。如果子进程先运行并且父进程在之后的一段时间没有运行,中断信号有可能在父进程之前产生,从而改变它的预置行为(disposition)被忽略。对于这个理由,在Figure10.28我们在fork之前改变信号的预置行为(disposition)。
注意,我们必须在子进程调用execl之前重置这两个信号(中断和退出)的预置行为(disposition)。基于调用者的预置行为(dispositions),允许execl改变它们的部署为默认,就像我们在APUE8.10节描述的那样。
Figure 10.28. Correct POSIX.1 implementation of system function#include <sys/wait.h>
#include <errno.h>
#include <signal.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);
}
|
system的返回值
要知道system的返回值,它是shell的终止状态,并不一直是命令的终止状态。我们已经在Figure8.23看了一些例子,结果是我们所期望的那样:如果我们执行一个简单命令,类似date,终止状态是0。执行shell命令exit 44给我们一个终止状态44。使用了信号会发生什么呢?
让我们运行Figure8.24中的程序,并发送一些信号给正在执行的命令:
$ tsys "sleep 30"
^?normal termination, exit status = 130 we type the interrupt key
$ tsys "sleep 30"
^\sh: 946 Quit we type the quit key
normal termination, exit status = 131
|
当我们使用中断信号终止sleep函数,pr_exit函数(FIGURE8.5)会认为它是被正常终止。同样的事情会发生在当我们使用退出键关闭sleep时。当命令被信号终止它的结束状态是128加上信号号时,在文档贫瘠的Bourne Shell上会发生什么?
$ sh make sure we're running the Bourne shell
$ sh -c "sleep 30"
^? type the interrupt key
$ echo $? print termination status of last command
130
$ sh -c "sleep 30"
^\sh: 962 Quit - core dumped type the quit key
$ echo $? print termination status of last command
131
$ exit leave Bourne shell
|
在该系统上使用SIGINT的值是2,SIGQUIT的值是3,所以给我们shell的终止状态是130和131。
让我们试一下类似的例子,但是这次我们将直接发送一个信号到shell并且看system返回了什么:
$ tsys "sleep 30" & start it in background this time
9257
$ ps -f look at the process IDs
UID PID PPID TTY TIME CMD
sar 9260 949 pts/5 0:00 ps -f
sar 9258 9257 pts/5 0:00 sh -c sleep 60
sar 949 947 pts/5 0:01 /bin/sh
sar 9257 949 pts/5 0:00 tsys sleep 60
sar 9259 9258 pts/5 0:00 sleep 60
$ kill -KILL 9258 kill the shell itself
abnormal termination, signal number = 9
|
这里我们能看到system的返回值,仅当shell它自己不正常终止时才会报告了不正常终止的提示。
当我们使用system函数写一个程序时,先确定正确的返回值。如果你调用了fork,exec或wait,终止状态不同于你调用的system。