10.18 system函数
在8.13节中,我们展示了一个system函数的实现,在哪一个版本中,我们并没有对信号做任何处理。POSIX.1要求system函数需要忽略SIGINT以及SIGQUIT信号,并且阻塞信号SIGCHLD.在展示一个正确处理这些信号的函数实现之前,让我们一起看一下为什么需要担心信号的处理。
Example
图10.26中显示的例子使用了8.13中的system函数调用了函数vi(1)编辑器.
#include "apue.h"
static void sig_int(int signo)
{
printf("caught SIGINT\n");
}
static void sig_child(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_child) == SIG_ERR)
err_sys("signal(SIGCHLD) error");
if(system("vi tempfile") < 0)
err_sys("system() error");
exit(0);
}
Figure 10.26 Using system to invoke the vi editor
如果我们调用这个程序并在vi程序退出之后,执行效果如下图所示:
os@debian:~/UnixProgram/Chapter10$ ./10_26.exe
caught SIGCHLD
可见当vi终止的时候,系统发送了一个信号SIGCHLD到了父进程,我们成功捕获到了该信号并且执行了信号处理函数。但是如果父进程正在捕获SIGCHLD信号,比如说父进程还另外创建了子进程且正在等待子进程的终止,那么附近和那个就应该这样做,以至于它直到其子进程合适结束。在父进程中该信号的发送应该在system函数的执行期间被阻塞,实际上,这也正是POSIX.1所要求的。否则的话,system执行的子进程可能会欺骗调用进程认为其自身的子进程终止了。
在9.6节中提到,键入中断字符将会造成中断信号被发送到前台进程组的所有进程。图10.27显示了当编辑器(书中使用的是/bin/ed,而不是vi)正在运行的时候进程的安排。
如果键入中断按键,那么SIGINT信号就会被发送到全部三个前台进程.(shell进程将会忽略该信号),即是说a.out以及编辑器都将会捕获到这一信号,但是当我们正在使用system运行其他程序的时候,我们不应该让父进程和子进程都捕获到中断产生的信号:中断和停止。取而代之的是:这两个信号应该被发送到正在运行的进程:也就是子进程编辑器,因为system执行的命令可能是一个交互式命令(比如说本例中的ed程序),因为system的调用进程在子进程运行的时候放弃了控制权,等待者子进程的结束,因此system的调用进程不应该搜到这两个信号。基于这个原因,POSIX.1指出system函数在等待命令完成的期间应该忽略这两个信号。
Example
#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*/
sigempty(&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 ISGCHLD*/
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);
}
else
{
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;
}
Figure 10.28 正确的POSIX.1的system函数实现
如果我们将图10.26中的程序与上述程序实现的system相链接,生成的二进制代码与之前的又如下差异:
- 当我们输入中断或者停止信号的时候,不会有信号被发送到调用进程;
- 当ed命令退出的时候,SIGCHLD并不会被发送到调用进程,直到我们在system函数中通过调用函数waitpid接收到父进程的终止状态以后,在最后调用函数sigprocmask解除阻塞。
POSIX.1声明如果函数wait或者是waitpid返回了子进程的状态,而此时信号SIGCHLD被挂起,那么信号SIGCHLD将不会被发送到进程,除非另外一个子进程的状态再次发出信号SIGCHLD.FreeBSD8.0,Mac OS X10.6.8以及Solaris 10全部都实现了上述定义,但是对于Linux3.2.0而言,并没有按照上述规定实现,也就是说在system函数调用waitpid以后,SIGCHLD仍然保持挂起状态,当信号被接触阻塞的时候,将被发送到调用进程,如果我们在sig_chld函数中调用wait函数的话(图10.26中的),Linux系统将会返回-1并且errno将被设置为ECHLD,因为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之后是父进程先运行还是子进程先运行,如果子进程先运行,并且父进程在一段时间内没有运行,那么中断信号可能会在父进程能够改变其信号处理方式为忽略之前出现,基于这一原因,我们在图10.28所示的程序中,我们在调用fork函数之前就先修改了信号的处理函数。
注意,我们必须在子进程中调用函数execl之前将这两个信号的处理函数恢复,这允许execl函数将它们的处理基于调用进程的处理函数改为默认处理。正如在8.10节中讲述的那样。
Return Value from system
函数system的返回值是shell的终止状态,并不一定是命令行的终止状态,在图8.23中我们看到:如果我们执行简单命令比如说date,其终止状态是0,执行命令exit 44,其终止状态就是44.那么如果是信号终止了进程那么会发生什么呢?
让我们运行图8.24中的程序,并且发送一些信号到正在执行的命令:
$ tsys "sleep 30"
^Cbormal termination, exit status = 130 ~~we press the interrupt key~~
¥tsys "sleep 30"
^\sh:946 Quit ~~we press the quit key~~
normal termination, exit status = 131
当我们使用终止信号终止sleep命令执行的时候,函数pr_exit函数(图8.5)认为命令正常终止,当我们使用停止键终止sleep命令执行的时候,也会发生相同的事情。在这个例子中,可以看到,Bourne shell中显示功能比较弱,其终止状态是128加上信号编号,在编者使用的电脑上,SIGINT的数值是2,SIGQUIT信号的编号是3,因此系统给出的shell的终止状态时130以及131
让我们在执行一个类似的例子,但是这次我们将直接发送一个信号给shell,看一下system函数将会返回什么:
$ tsys "sleep 30" &
9257
$ ps -f
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 30
sar 949 947 pts/5 0:01 /bin/sh
sar 9257 949 pts/5 0:00 tsys sleep 30
sar 9259 9258 pts/5 0:00 sleep 30
$ kill -KILL 9258
abnormal termination, signal number = 9
在此,我们可以看出在shell本身异常终止的时候system的返回值报告了一次异常终止。
当写程序使用到了system函数的时候,注意正确地解释返回值,如果你调用了fork,exec自身,并wait。其终止状态与调用system函数并不相同。