僵尸进程处理
客户正常断开但服务器 未处理SIGCHLD信号,会使得服务器子进程僵死。
设置僵尸进程的目的:
维护子进程信息,以便父进程在以后某时候获取。信息包括子进程ID,终止状态,资源利用信息(CPU时间、内存使用量等)。如果某进程结束,而该进程有子进程处于僵死状态,那么它的所有僵死子进程的父进程ID会变为1(init进程),init进程会清理它们,即init进程会wait它们,去除其僵死状态。
处理僵死进程方法:
(1)忽略SIGCHLD信号可以防止僵尸进程,在server端main函数listen之后添加: Signal (SIGCHLD, SIG_IGN);
如下是代码:
//使用sigaction函数取代unix早期版本的signal函数的实现
typedef void Sigfunc(int);
Sigfunc *
Signal(int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
}
else {
#ifdef SA_RESTART //如果系统可以重启被中断的系统调用的话,被中断的系统调用将由内核重启
act.sa_flags |= SA_RESTART;
#endif
}
if (sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
}
(2)通过wait/waitpid方法。
//使用waitpid的信号处理函数
void onSignalCatch(int signalNumber)
{
pid_t pid;
pid = wait(NULL);
printf("child %d terminated.\n", pid);
return;
}
在服务器端listen调用后加入:
Signal(SIGCHLD, onSignalCatch); //这个处理信号的函数必须在fork第一个子进程之前完成,且仅做一次
再次执行程序,三个客户端正常终止后,服务器端结果如下:

上述的方法调用wait的函数onSignalCatch作为信号处理函数,其正常结束的过程如下:
(1)键入EOF终止客户。客户 TCP 发送FIN给服务器,服务器响应ACK。
(2)收到客户FIN的服务器TCP发送EOF给 子进程阻塞的read ,子进程结束。
(3)当SIGCHLD信号递交时, 父进程阻塞于 accept 调用。信号处理函数onSignalCatch执行,其wait调用子进程pid并打印、返回。
(4)信号是父进程在 阻塞于慢系统调用accept时捕获 的,所以,内核就会 使accept返回EINTR错误(被中断的系统调用)。而 父进程不处理该错误,被中断的系统调用重启(上述自定义的Signal函数中:设置了SA_RESTART标志),所以accept没返回错误 。
需要C/C++ Linux高级服务器架构师学习资料后台私信“资料”(包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)

处理被中断的系统调用
accept被称为 慢系统调用 ,多数网络支持函数都属于这一类。 适用于慢系统调用的基本规则是 :当阻塞于某个慢系统调用的进程捕获某个信号且相应信号处理函数返回后,该系统调用可能返回一个EINTR错误。
从上图可以看出,三次客户的终止都没有使得阻塞于accept的服务器终止;因为内核会自动重启被中断的系统调用。而 有些系统 的标准C函数库中的signal函数不会使得内核自动重启被中断的系统调用,也就是SA_RESTART标志在系统函数的signal函数中没有被设置, 在这些系统中服务器程序将终止。
那么并非所有的系统都会将被中断的系统调用重启,所以 要处理被中断的系统调用,将accept的调用改为:
if ((connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; //有些系统不会重启被这中断的系统调用,所以要处理
else
err_exit("accept error, server.\n");
}
wait函数和waitpid函数
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options); //参数pid允许指定想等待的进程ID,为-1表示等待第一个终止的子进程
//均返回:成功返回进程ID,出错返回0或-1
//statloc指针返回子进程终止状态(一个整数)
//options是附加选项,最常用的是WNOHANG,告知内核在有尚未终止的子进程在运行时不阻塞


问题:
在上述情况下,建立信号处理函数在其中调用wait仍然会有僵死进程。问题在于:所有5个信号都在信号处理函数执行前产生;而 在一台机器上 运行客户和服务器端,信号处理函数只执行一次,因为unix信号一般是不排队的。如果 在不同机器上 运行客户端服务器端,信号处理函数执行次数不确定,对于留下几个僵死进程以及哪几个会僵死都是不确定的。
如下图分别为创建的5个连接、客户端exit后服务端的四个僵死子进程:


解决方法:
使用waitpid而不是wait。如下的处理函数管用,因为在一个循环内调用waitpid,以获取进程终止状态。 waitpid函数的参数options指定为WNOHANG ,告知waitpid在尚有未终止的子进程时,不阻塞。而使用wait无法阻止其在还有子进程未结束时阻塞。
//使用waitpid的信号处理函数
void onSignalCatch(int signalNumber)
{
pid_t pid;
int stat;
//pid = wait(&stat);
//下列函数的第一个参数为-1表示等待第一个终止的子进程
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated.\n", pid);
return;
}
结果如下:


至此,客户服务器代码就完成了,加入了对僵死进程的处理。