linux下进程终止时,内核会向父进程发送一个SIGCHLD信号,其有几个特点:
1.在一个信号处理函数运行期间,正被递交的信号是阻塞的。
2.如果一个信号在被阻塞期间产生了一次或多次,那么该信号被解阻塞之后通常只递交一次,也就是说linux信号默认是不排队的。
举个栗子:
进程的第一个子进程终止产生信号(1),(1)信号处理过程中第二个子进程终止产生信号(2),此时(1)处于正在处理状态,(2)处于等待处理状态,接着又有第三、四、五子进程终止产生信号(3)(4)(5);(1)信号处理函数执行完后,只进行(2)信号的处理,其余信号(3)(4)(5)则丢失。
编写linux多进程的并发服务程序时,我们需要处理客户端退出,服务端fork出的子进程变为僵死(zombie)进程的情况,通常这样来做:
signal(SIGCHLD, sig_chld); void sig_chld(int signo) { pid_t pid; int stat; while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) { printf("child %d terminated\n", pid); } return; }
这里我们指定WNOHANG选项,它告知waitpid在有尚未终止的子进程在运行时不要阻塞。我们不能在循环中调用wait,因为没有办法防止wait在正在运行的子进程尚有未终止时阻塞。另外值得注意的是,在信号处理函数中使用printf等不可重入函数是不推荐的。
查看waitpid的man手册,有这样一段描述:
POSIX.1-2001 specifies that if the disposition of SIGCHLD is set to SIG_IGN or the SA_NOCLDWAIT flag is set for SIGCHLD (see sigaction(2)), then children that terminate do not become zombies and a call to wait() or waitpid() will block until all children have terminated, and then fail with errno set to ECHILD.
提供了另外两种僵死进程处理的办法:signal(SIGCHLD, SIG_IGN); 或为SIGCHLD设置SA_NOCLDWAIT标志。
上述方法,在高并发场景下仍存在缺陷,SIGCHLD信号在上面代码的作用,其实只是通知进行waitpid调用,waitpid循环处理所有已经终止的子进程。假设一种情况,服务端支持最大2k的并发,满负载时2k个客户端同时退出,按上面例子会有大量信号丢失,产生的僵死进程在下次父进程收到SIGCHLD信号时得到清理,此时服务端只能提供少量并发请求(最坏情况2个并发);
简短描述一下该问题:高并发下,上述代码的waitpid在清理僵死进程时不及时。
一种解决办法是我们自己来维护子进程的退出状态。创建一个队列,子进程退出时将pid入队列,在线程中阻塞读并处理队列。