# 8.5.7 显式地等待信号

有时候主程序需要显式地等待某个信号处理程序运行。例如,当 Linux shell 创建一个前台作业时,在接收下一条用户命令之前,它必须等待作业终止,被 SIGCHLD 处理程序回收。

图 8-41 给出了一个基本的思路。父进程设置 SIGINT 和 SIGCHLD 的处理程序,然后进入一个无限循环。它阻塞 SIGCHLD 信号,避免 8.5.6 节中讨论过的父进程和子进程之间的竞争。创建了子进程之后,把 pid 重置为 0,取消阻塞 SIGCHLD,然后以循环的方式等待 pid 变为非零。子进程终止后,处理程序回收它,把它非零的 PID 赋值给全局 pid 变量。这会终止循环,父进程继续其他的工作,然后开始下一次迭代。

code/ecf/waitforsignal.c

#include "csapp.h"

volatile sig_atomic_t pid;

void sigchld_handler(int s)
{
    int olderrno = errno;
    pid = waitpid(-1, NULL, 0);
    errno = olderrno;
}

void sigint_handler(int s)
{
}

int main(int argc, char **argv)
{
    sigset_t mask, prev;

    Signal(SIGCHLD, sigchld_handler);
    Signal(SIGINT, sigint_handler);
    Sigemptyset(&mask);
    Sigaddset(&mask, SIGCHLD);

    while (1) {
        Sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */
        if (Fork() == 0) /* Child */
            exit(0);

        /* Parent */
        pid = 0;
        Sigprocmask(SIG_SETMASK, &prev, NULL); /* Unblock SIGCHLD */

        /* Wait for SIGCHLD to be received (wasteful) */
        while (!pid)
            ;

        /* Do some work after receiving SIGCHLD */
        printf(".");
    }
    exit(0);
}

图 8-41 用循环来等待信号。这段代码正确,但循环是一种浪费

当这段代码正确执行的时候,循环在浪费处理器资源。我们可能会想要修补这个问题,在循环体内插入 pause

while (!pid) /* Race! */
    pause();

注意,我们仍然需要一个循环,因为收到一个或多个 SIGINT 信号,pause 会被中断。不过,这段代码有很严重的竞争条件:如果在 while 测试后和 pause 之前收到 SIGCHLD 信号,pause 会永远睡眠。

另一个选择是用 sleep 替换 pause

while (!pid) /* Too slow! */
    sleep(1);

当这段代码正确执行时,它太慢了。如果在 while 之后 pause 之前收到信号,程序必须等相当长的一段时间才会再次检查循环的终止条件。使用像 nanosleep 这样更高精度的休眠函数也是不可接受的,因为没有很好的方法来确定休眠的间隔。间隔太小,循环会太浪费。间隔太大,程序又会太慢。

合适的解决方法是使用 sigsuspend

#include <signal.h>

int sigsuspend(const sigset_t *mask);

返回: -1。

sigsuspend 函数暂时用 mask 替换当前的阻塞集合,然后挂起该进程,直到收到一个信号,其行为要么是运行一个处理程序,要么是终止该进程。如果它的行为是终止,那么该进程不从 sigsuspend 返回就直接终止。如果它的行为是运行一个处理程序,那么 sigsuspend 从处理程序返回,恢复调用 sigsuspend 时原有的阻塞集合。

sigsuspend 函数等价于下述代码的原子的(不可中断的)版本:

sigprocmask(SIG_SETMASK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);

原子属性保证对 sigprocmask(第 1 行)和 pause(第 2 行)的调用总是一起发生的,不会被中断。这样就消除了潜在的竞争,即在调用 sigprocmask 之后但在调用 pause 之前收到了一个信号。

图 8-42 展示了如何使用 sigsuspend 来替代图 8-41 中的循环。在每次调用 sigsuspend 之前,都要阻塞 SIGCHLD。sigsuspend 会暂时取消阻塞 SIGCHLD,然后休眠,直到父进程捕获信号。在返回之前,它会恢复原始的阻塞集合,又再次阻塞 SIGCHLD。如果父进程捕获一个 SIGINT 信号,那么循环测试成功,下一次迭代又再次调用 sigsuspend。如果父进程捕获一个 SIGCHLD,那么循环测试失败,会退出循环。此时,SIGCHLD 是被阻塞的,所以我们可以可选地取消阻塞 SIGCHLD。在真实的有后台作业需要回收的 shell 中这样做可能会有用处。

sigsuspend 版本比起原来的循环版本不那么浪费,避免了引入 pause 带来的竞争,又比 sleep 更有效率。

code/ecf/sigsuspend.c

#include "csapp.h"

volatile sig_atomic_t pid;

void sigchld_handler(int s)
{
    int olderrno = errno;
    pid = Waitpid(-1, NULL, 0);
    errno = olderrno;
}

void sigint_handler(int s)
{
}

int main(int argc, char **argv)
{
    sigset_t mask, prev;

    Signal(SIGCHLD, sigchld_handler);
    Signal(SIGINT, sigint_handler);
    Sigemptyset(&mask);
    Sigaddset(&mask, SIGCHLD);

    while (1) {
        Sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */
        if (Fork() == 0) /* Child */
            exit(0);

        /* Wait for SIGCHLD to be received */
        pid = 0;
        while (!pid)
            sigsuspend(&prev);

        /* Optionally unblock SIGCHLD */
        Sigprocmask(SIG_SETMASK, &prev, NULL);

        /* Do some work after receiving SIGCHLD */
        printf(".");
    }
    exit(0);
}

图 8-42 用 sigsuspend 来等待信号