system函数

system函数

在APUE8.13节,我们显示了一个system函数的实现。那个版本中并不处理任何信号。POSIX.1要求system忽略SIGINTSIGQUIT并且阻塞SIGCHLD。之前显示了一个正确处理这些信号的版本,现在让我们看一下为什么要担心这些信号的处理。

例子

Figure 10.26的程序显示了一个使用APUE8.13节中的system函数调用ed编辑器。该程序捕获了SIGINTSIGCHLD信号。如果我们调用这个程序,我们会得到:

    $ ./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-27

例子

Figure10.28显示了system函数处理信号的实现。

如果我们我们在Figure10.26中使用我们这个版本的system函数,生成的二进制代码至少有以下一种缺陷。

  1. 当我们输入中断或退出字符时,没有信号发送到调用进程。
  2. 当ed命令退出时,SIGCHLD信号没有被发送到调用进程。取而代之的是,它被阻塞了直到我们在最近一次调用sigprocmask对其进行解锁,之后system函数通过调用waitpid“找回”子进程的终止状态。

(POSIX.1声明当SIGCHLD是pending状态时,如果waitwaitpid返回子进程的状态,那么SIGCHLD应该不能被传送到进程,除非其它子进程的状态也是可用的。本书中的四种实现并没有讨论它的语法。而是使用SIGCHLDsystem函数调用waitpid后保留pending状态;当信号被解锁后,它会被传送到调用者进程那里。如果我们在Figure10.26的sig_chld程序中调用了wait,它可能会返回1并设置errnoECHILD,因为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函数写一个程序时,先确定正确的返回值。如果你调用了forkexecwait,终止状态不同于你调用的system

发表回复