8.13节已经有了一个system函数的实现,但是该版本并不执行任何信号处理。POSIX.1要求system忽略SIGINT
和SIGQUIT
,阻塞SIGCHLD
。在给出一个正确地处理这些信号的一个版本之前,先说明为什么要考虑信号处理。
实例
图10-26中的程序使用8.13节中的system版本,用其调用ed(1)编辑器。(ed编辑器很久以来就是UNIX的组成部分。在这里使用它的原因是:它是捕捉中断和退出信号的交互式程序。若从shell调用ed,并键入中断字符,则它捕捉中断信号并打印问号。ed程序对退出信号的处理方式设置为忽略。)
图10-26中的程序用于捕捉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
信号。
父进程捕捉它,执行其处理程序sig_chid
,然后从信号处理程序返回。但是若父进程正捕捉SIGCHLD
信号(因为它创建了子进程,所以应当这样做以便了解它的子进程在何时终止),那么正在执行system
函数时,应当阻塞对父进程递送SIGCHLD
信号。实际上,这就是POSIX.1所说明的。否则,当system
创建的子进程结束时,system
的调用者可能错误地认为,它自己的一个子进程结束了。于是,调用者将会调用一种wait
函数以获得子进程的终止状态,这样就阻止了system
函数获得子进程的终止状态,并将其作为它的返回值。
#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);
}
图10-26 用syetem调用ed编辑器
如果再次执行该程序,在这次运行时将一个中断信号传送给编辑器,则可得:
$ ./a.out
a 将正文追加至编辑器缓冲区
hello, world
. 行首的点停止追加方式
1,$p 打印缓冲区中的第一行至最后一行,以便观察其内容
hello, world
w temp.foo 将缓冲区写至一文件
13 编辑器称写了13个字节
^C 键入中断符
? 编辑器捕捉信号,打印问号
caught SIGINT 父进程执行同一操作
q 离开编辑器
caught SIGCHLD
回忆9.6节可知,键入中断字符可使中断信号传送给前台进程组中的所有进程。图10-27展示了编辑器正在运行时的各个进程的关系。
图10-27 图10-26程序运行时的前台和后台进程组
在这一实例中,SIGINT
被送给3个前台进程(shell进程忽略此信号)。从输出中可见,a.out进程和ed进程捕捉该信号。
但是,当用system
运行另一个程序时,不应使父、子进程两者都捕捉终端产生的两个信号:中断和退出。这两个信号只应发送给正在运行的程序:子进程。因为由system执行的命令可能是交互式命令(如本例中的ed),以及因为system
的调用者在程序执行时放弃了控制,等待该执行程序的结束,所以system
的调用者就不应接收这两个终端产生的信号。这就是为什么POSIX.1规定system
的调用者在等待命令完成时应当忽略这两个信号的原因。
实例
图10-28中的程序是system函数的另一个实现,它进行了所要求的信号处理。
#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);
}
图10-28 system函数的POSIX.1正确实现
如果将图10-26中的程序与system
函数的这一实现相链接,那么所产生的二进制代码与上一个有缺陷的程序相比较,存在如下差别。
- 当我们键入中断字符或退出字符时,不向调用进程发送信号。
- 当
ed
命令终止时,不向调用进程发送SIGCHLD
信号。作为替代,在程序末尾的sigprocmask
调用对SIGCHLD
信号解除阻塞之前,SIGCHLD
信号一直被阻塞。而对sigprocmask
函数的这一次调用是在system
函数调用waitpid
获取子进程的终止状态之后。
POSIX.1说明,在
SIGCHLD
未决期间,如若wait
或waitpid
返回了子进程的状态,那么SIGCHLD
信号不应递送给该父进程,除非另一个子进程的状态也可用。FreeBSD 8.0、Mac OS X10.6.8和Solaris 10都实现了这种语义,而Linux 3.2.0没有实现这种语义,在system
函数调用了waitpid
后,SIGCHLD
保持为未决;当解除了对此信号的阻塞后,它被递送至调用者。如果我们在图10-26的sig_chld
函数中调用wait
,Linux系统将返回−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
之后不能保证父进程还是子进程先运行。如果子进程先运行,父进程在一段时间后再运行,那么在父进程将中断信号的处理更改为忽略之前,就可能产生这种信号。由于这种原因,图10-28中在fork
之前就改变对该信号的配置。
注意,子进程在调用execl
之前要先恢复这两个信号的处理。如同8.10节中所说明的一样,这就允许在调用者配置的基础上,execl
可将它们的配置更改为默认值。
system的返回值
注意system
的返回值,它是shell的终止状态,但shell的终止状态并不总是执行命令字符串进程的终止状态。图8-23中有一些例子,其结果正是我们所期望的。如果执行一条如date
那样的简单命令,其终止状态是0。执行shell命令exit 44,则得终止状态44。在信号方面又如何呢?
$ tsys "sleep 30"
^Cnormal termination, exit status = 130 键入中断符
$ tsys "sleep 30"
^\sh: 946 Quit 键入退出符
normal termination, exit status = 131
当用中断信号终止sleep
时,pr_exit
函数(见图8-5)认为它正常终止。当用退出符杀死sleep
进程时,会发生同样的事情。终止状态130、131又是怎样得到的呢?原来Bourne shell有一个在其文档中没有说清楚的特性,其终止状态是128加上一个信号编号,该信号终止了正在执行的命令。用交互方式使用shell可以看到这一点。
$ sh 确保运行Bourne shell
$ sh -c "sleep 30"
^C 键入中断符
$ echo $? 打印最后一条命令的终止状态
130
$ sh -c "sleep 30"
^\sh: 962 Quit - core dumped 键入退出符
$ echo $? 打印最后一条命令的终止状态
131
$ exit 离开Bourne shell
在所使用的系统中,SIGINT
的值为2,SIGQUIT
的值为3,于是给出shell终止状态130、131。
再试一个类似的例子,这一次将一个信号直接送给shell,然后观察system返回什么:
$ tsys "sleep 30" & 这一次在后台启动它
9257
$ ps -f 查看进程ID
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 杀死shell自身
abnormal termination, signal number = 9
从中可见,仅当shell本身异常终止时,system
的返回值才报告一个异常终止。
其他的shell在处理终端产生的信号(如
SIGINT
和SIGQUIT
)时表现出来的行为各不相同。例如在bash和dash中,键入中断或退出符会导致带有对应信号编号的表示异常终止的退出状态。但是,如果发现正在执行sleep的进程并直接给它发送信号,这样信号只会到达单个进程而不是整个前台进程组。这些shell与Bourne shell类似,以正常终止状态128加上信号编号退出。
在编写使用system
函数的程序时,一定要正确地解释返回值。如果直接调用fork
、exec
和wait
,则终止状态与调用system
是不同的。