关于CSAPP中图8-37的while循环作用及主程序与handler2并发执行流程的技术问询
嘿,这两个问题都是CSAPP信号处理章节里非常容易踩坑的点,我来给你掰扯清楚~
1. 为什么while这么关键?
咱们先抓住Unix信号的核心特性:同类型信号是不排队的。也就是说,如果多个子进程几乎同时退出,内核只会给父进程发送一次SIGCHLD信号,而不是每个子进程对应发一次。
看回图8-36的bug代码:它用if (waitpid(...) < 0),每次触发SIGCHLD时只回收一个僵尸进程。假设三个子进程同时退出,内核只发一次SIGCHLD,handler1只会回收一个,剩下两个子进程就会变成僵尸进程,一直占用系统资源,直到父进程退出。
而图8-37里的while (waitpid(-1, NULL, 0) > 0)就完美补上了这个漏洞:
waitpid(-1, NULL, 0)会回收任意一个僵尸子进程,返回值是被回收子进程的PID;- 当没有僵尸进程时,
waitpid会返回-1,同时errno被设为ECHILD; - 所以
while循环会一直调用waitpid,把当前所有的僵尸进程全部回收干净,直到没有剩下的为止。
这样哪怕一次SIGCHLD信号对应多个子进程退出,也能一次性把所有僵尸进程都处理掉,不会留下漏网之鱼。
2. 主程序和handler2的“并发”执行流程到底是怎样的?
首先要纠正一个小误解:这不是多线程并行执行,而是异步中断式的交替执行——因为整个进程是单线程的,信号处理函数是通过打断主程序的执行流来运行的。具体流程可以拆成这几步:
主程序初始化阶段:
主程序先调用signal(SIGCHLD, handler2)注册信号处理函数,然后循环fork出3个子进程。每个子进程打印信息后立刻调用exit(0)退出,内核会标记这些子进程为僵尸状态,并向父进程发送SIGCHLD信号。信号触发与上下文切换:
此时主程序可能处于两个状态:要么卡在read(STDIN_FILENO, buf, sizeof(buf))等待用户输入,要么已经进入后面的死循环while(1);。当SIGCHLD信号到达时,内核会暂停主程序当前的执行,保存好当前的寄存器、程序计数器等上下文信息,然后切换到handler2的代码开始执行。handler2执行阶段:
handler2先保存原来的errno(避免覆盖主程序的错误状态),然后进入while循环回收所有僵尸子进程,每回收一个就打印提示,直到waitpid返回-1且errno为ECHILD(确认没有僵尸进程了)。之后睡眠1秒,恢复原来的errno,然后返回。回到主程序继续执行:
handler2执行完后,内核会恢复之前保存的主程序上下文,让主程序从被打断的那个位置继续执行——比如如果之前在等用户输入,就继续等;如果在死循环,就继续循环下去。
还要补充一个细节:默认情况下,在信号处理函数执行期间,当前信号(这里是SIGCHLD)会被内核屏蔽,也就是说如果在handler2执行时又有子进程退出,内核不会再次触发SIGCHLD,而是把信号标记为“待处理”,等handler2执行完后再处理。不过因为我们用了while循环一次性回收所有僵尸,所以即使有新的信号待处理,后续触发时依然能把所有僵尸收干净。
附原文代码:
图8-36(存在bug的版本)
/* figure 8-36 */ /* WARNING: This code is buggy! */ void handler1(int sig) { int olderrno = errno; if ((waitpid(-1, NULL, 0)) < 0) sio_error("waitpid error"); Sio_puts("Handler reaped child\n"); Sleep(1); errno = olderrno; } int main() { int i, n; char buf[MAXBUF]; if (signal(SIGCHLD, handler1) == SIG_ERR) unix_error("signal error"); /* Parent creates children */ for (i = 0; i < 3; i++) { if (Fork() == 0) { printf("Hello from child %d\n", (int)getpid()); exit(0); } } /* Parent waits for terminal input and then processes it */ if ((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0) unix_error("read"); printf("Parent processing input\n"); while (1) ; exit(0); }
图8-37(修复后的版本)
/* figure 8-37 An improved version of Figure 8.36 that correctly accounts for the fact that signals are not queued */ void handler2(int sig) { int olderrno = errno; while (waitpid(-1, NULL, 0) > 0) { Sio_puts("Handler reaped child\n"); } if (errno != ECHILD) Sio_error("waitpid error"); Sleep(1); errno = olderrno; }
备注:内容来源于stack exchange,提问作者Ldemon

