8.13节已经有了一个system函数的实现,但是该版本并不执行任何信号处理。POSIX.1要求system忽略SIGINTSIGQUIT,阻塞SIGCHLD。在给出一个正确地处理这些信号的一个版本之前,先说明为什么要考虑信号处理。

实例

图10-26中的程序使用8.13节中的system版本,用其调用ed(1)编辑器。(ed编辑器很久以来就是UNIX的组成部分。在这里使用它的原因是:它是捕捉中断和退出信号的交互式程序。若从shell调用ed,并键入中断字符,则它捕捉中断信号并打印问号。ed程序对退出信号的处理方式设置为忽略。)

图10-26中的程序用于捕捉SIGINTSIGCHLD信号。若调用它则可得:

$ ./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未决期间,如若waitwaitpid返回了子进程的状态,那么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在处理终端产生的信号(如SIGINTSIGQUIT)时表现出来的行为各不相同。

例如在bash和dash中,键入中断或退出符会导致带有对应信号编号的表示异常终止的退出状态。但是,如果发现正在执行sleep的进程并直接给它发送信号,这样信号只会到达单个进程而不是整个前台进程组。这些shell与Bourne shell类似,以正常终止状态128加上信号编号退出。

在编写使用system函数的程序时,一定要正确地解释返回值。如果直接调用forkexecwait,则终止状态与调用system是不同的。

results matching ""

    No results matching ""