8.5.7 显式地等待信号
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 来等待信号