54 读书笔记:第10章 信号 (7)

10.17 abort函数

        abort函数的功能是使异常程序终止。

       #include <stdlib.h>

       void abort(void);
        // 此函数不返回

        此函数将SIGABRT信号发送给调用进程(进程不应忽略此信号)。ISO C规定,调用abort将向主机环境递送一个未成功终止的通知,其方法是调用raise(SIGABRT)函数。

        ISO C要求若捕捉到此信号而且相应信号处理程序返回,abort仍不会返回到其调用者。如果捕捉到此信号,则信号处理程序不能返回的唯一方法是它调用exit、_exit、_Exit、longjmp或siglongjmp。POSIX.1也说明abort并不理会进程对此信号的忽略和阻塞。

        让进程捕捉SIGABRT的意图是:在进程终止之前由其执行所需的清理操作。如果进程并不在信号处理程序中终止自己,POSIX.1声明当信号处理程序返回时,abort终止该进程。

        ISO C针对此函数的规范将下列问题留由实现决定:是否要冲洗输出流以及是否要删除临时文件。POSIX.1的要求则更进一步,它要求如果abort调用终止进程,则它对所有打开标准I/O流的效果应当与进程终止前对每个流调用fclose相同。因为大多数UNIX tempfile(临时文件)的实现在创建文件之后会立即调用unlink,所以ISO C关于临时文件的警告通常与我们无关。

        《UNIX环境高级编程》P275:程序清单10-18 abort的POSIX.1实现

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void abort(void)
{
    sigset_t            mask;
    struct sigaction    action;

    // 调用者不能忽略SIGABRT
    sigaction(SIGABRT, NULL, &action);
    if (action.sa_handler == SIG_IGN) {
        action.sa_handler = SIG_DFL;
        sigaction(SIGABRT, &action, NULL);
    }   
    if (action.sa_handler == SIG_DFL)
        ffluse(NULL);                   // 冲洗所有输出流

    // 调用者不能阻塞SIGABRT
    sigfillset(&mask);
    sigdelset(&mask, SIGABRT);          // 仅关闭SIGABRT掩码
    sigprocmask(SIG_SETMASK, &mask, NULL);
    kill(getpid(), SIGABRT);            // 发送信号

    // 如果运行到此处,则进程捕获到SIGABRT并返回了
    ffluse(NULL);                       // 冲洗输出流
    action.sa_handler = SIG_DFL;
    sigaction(SIGABRT, &action, NULL);  // 重置为默认
    sigprocmask(SIG_SETMASK, &mask, NULL);
    kill(getpid(), SIGABRT);
    exit(1);
}

        首先查看是否执行默认动作,若是则冲洗所有标准I/O流。这并不等价于对所有打开的流调用fclose,但是当进程终止时,系统会关闭所有打开的文件。如果进程捕获此信号并返回,那么因为进程可能产生了更多的输出,所以再一次冲洗所有的流。不进行冲洗处理的唯一条件是如果进程捕捉此信号,然后调用_exit或_Exit。在这种情况下,内存中任何未冲洗的标准I/O缓冲区都被丢弃。

        如果调用kill使其为调用者产生信号,并且如果该信号是不被阻塞的,则在kill返回前该信号(或某个未决的、为阻塞的信号)就被传送给了该进程。我们阻塞了除SIGABRT之外的所有信号,这样就可知如果对kill的调用返回了,则该进程一定已捕捉到了该信号,并且也从该信号处理程序返回。

10.18 system函数

        POSIX.1要求system忽略SIGINT和SIGQUIT,阻塞SIGCHLD。

        程序清单10-19使用8.13节中的system版本,用其调用ed(1)编辑器。这里调用ed的原因是:它是捕捉中断和退出信号的交互式程序。若从shell调用ed,并键入中断字符,则它捕捉中断信号并打印问号。它还将退出符的处理方式设置为忽略。

        《UNIX环境高级编程》P276:程序清单10-19 用system调用ed编辑器(有改动)

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>

int system(const char *command);

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)
        fprintf(stderr, "signal(SIGINT) error\n");

    if (signal(SIGCHLD, sig_chld) == SIG_ERR)
        fprintf(stderr, "signal(SIGCHLD) error\n");

    if (system("/bin/ed") < 0)
        fprintf(stderr, "system() error\n");

    exit(0);
}

int system(const char *command)
{
    pid_t   pid;
    int     status;

    if (command == NULL)
        return(1);              // UNIX中system总是可用的

    if ((pid = fork()) < 0) {
        status = -1;            // 进程创建出错
    } else if (pid == 0) {      // 子进程
        execl("/bin/sh", "sh", "-c", command, (char *)0);
        _exit(127);
    } else {                    // 父进程
        while (waitpid(pid, &status, 0) < 0) {
            if (errno != EINTR) {
                status = -1;    // 除了EINTR之外的出错
                break;
            }
        }
    }   

    return status;
}

        程序清单10-19用于捕捉SIGINT和SIGCHLD信号。若调用它则可得:

$ ./19
a                               将正文添加至编辑器缓冲区
Here is one line of text
.                               行首的点停止添加方式
1,$p                            打印缓冲区的第1行至最后1行 
Here is one line of text
w temp.foo                      将缓冲区写至一个文件
25                              编辑器称写了25个字符
q                               离开编辑器
caught SIGCHLD

        当编辑器终止时,系统向父进程发送SIGCHLD信号。父进程捕捉它,然后从信号处理程序返回。但是若父进程正在捕捉SIGCHLD信号,那么正在执行system函数时,应当阻塞对父进程递送SIGCHLD信号。否则,当system创建的子进程结束时,system的调用者可能错误地认为,它自己的一个子进程结束了。于是,调用者将会调用一种wait函数以获得子进程的终止状态,这样就阻止了system函数获得子进程的终止状态,并将其作为它的返回值。

        如果再次执行该程序,在这次运行时将一个中断信号传送给编辑器,则可得:

$ ./19
a                   将正文添加至编辑器缓冲区
hello, world
.                   行首的点停止添加方式
1,$p                打印缓冲区中的第1行至最后1行 
hello, world
w temp.foo          将缓冲区写至一个文件
13                  编辑器称写了13个字节
^C                  键入中断符(Ctrl+C)
?                   编辑器捕捉信号,打印问号
caught SIGINT       父进程执行同一操作
q                   离开编辑器
caught SIGCHLD

        键入中断字符可使中断信号传送给前台进程组中的所有进程。如图所示,显示了编辑程序正在运行时的各个进程的关系:

                        

        在这一实例中,SIGINT被送给三个前台进程(shell进程忽略此信号)。从输出可见,./19进程和ed进程捕捉该信号。但是,当用system运行另一个程序(如ed)时,不应使父、子进程两者都捕捉终端产生的两个信号:中断和退出。这两个信号只应送给正在运行的程序:子进程。因为由system执行的控制命令可能是交互式命令,以及因为system的调用者在执行程序时放弃了控制,等待该执行程序的结束,所以system的调用者就不应接收这两个终端产生的信号。

        程序清单10-20显示了system函数的另一个实现,它进行了所要求的信号处理。(有改动)

#include <sys/wait.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>

int system(const char *cmdstring)
{
    pid_t               pid;
    int                 status;
    struct sigaction    ignore, saveintr, savequit;
    sigset_t            chldmask, savemask;

    if (cmdstring == NULL)
        return(1);

    // 忽略SIGINT和SIGQUIT
    ignore.sa_handler = SIG_IGN;
    sigemptyset(&ignore.sa_mask);
    ignore.sa_flags = 0;
    if (sigaction(SIGINT, &ignore, &saveintr) < 0)
        return(-1);
    if (sigaction(SIGQUIT, &ignore, &savequit) < 0)
        return(-1);

    // 屏蔽SIGCHLD  
    sigemptyset(&chldmask);
    sigaddset(&chldmask, SIGCHLD);
    if (sigprocmask(SIG_BLOCK, &chldmask, &savemask) < 0)
        return(-1);

    if ((pid = fork()) < 0) {
        status = -1; 
    } else if (pid == 0) {                      // 子进程
        // 恢复之前的信号处理动作,恢复信号屏蔽字
        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; 
                break;
            }
        }
    }   

    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);
}

        与前一个的差别:

        (1) 当我们键入中断或退出字符时,不向调用进程发送信号。

        (2) 当ed命令终止时,不向调用进程发送SIGCHLD信号。作为替代,在程序末尾的sigprocmask调用对SIGCHLD信号阻塞之前,SIGCHLD信号一直被阻塞。而对sigprocmask函数的这一次调用是在waitpid取得了子进程的终止状态之后。

        在fork之前改变对信号的配置的原因:fork之后不能保证是父进程还是子进程先运行。如果子进程先运行,父进程在一段时间后再运行,那么父进程将中断信号的配置更改为忽略之前,可能产生这种信号。

        注意,子进程在调用execl之前要恢复这两个信号的配置,这就允许在调用者配置的基础上,execl可将它们的配置设置为默认值。

system的返回值

        注意system的返回值,它是shell的终止状态,但shell的终止状态并不总是执行命令字符串的终止状态。

        Bourne shell有一个特性,其终止状态是128加上一个信号编号,该信号终止了正在执行的命令。用交互式shell可以看到这一点。

$ sh -c "sleep 30"
^C
$ echo $?
130
$ sh -c "sleep 30"
^\Quit (core dumped)
$ echo $?
131

        在所使用系统中,SIGINT的值为2,SIGQUIT的值为3,于是给出shell终止状态130、131。

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

你可能感兴趣的:(读书笔记,《UNIX环境高级编程》)