在上一节利用fork实现服务端与多个客户端建立连接中,我们使用fork
函数来实现服务端既可以accept
新的客户端连接请求,又可以接收已连接上的客户端发来的消息。但在Linux中,在子进程终止后,父进程需要处理该子进程的终止状,否则子进程将成为僵尸进程,本节就来探讨一下僵尸进程的处理。
僵尸进程(Zombie Process
)是操作系统中的一种特殊进程状态,它通常出现在一个子进程终止,但其父进程尚未能够处理该子进程的终止状态。
特点:
僵尸进程不执行任何代码,它们仅仅是一个进程描述符和一些状态信息,如退出状态码,占用少量系统资源。如果大量的僵尸进程积累,可能会导致系统资源耗尽。
解决方法:
wait()
或waitpid()
等系统调用来等待子进程的退出状态信息,从而释放子进程的资源,同时告知操作系统可以回收子进程的进程表项SIGCHLD
信号处理程序来处理子进程的退出状态在上一篇文章的例子中,如果在客户端的进程终止后,服务端没有回收子进程的话,将产生一个僵尸进程。
我们可以使用top
指令来看系统中现在有多少个僵尸进程:
我们还可以使用ps -aux |grep Z
来查看具体的僵尸进程的信息:
图中STAT
(状态)为Z+
(Zombie
)的即为僵尸进程。
最简单的,我们可以使用SIG_IGN
来忽略SIGCHLD
信号,这样内核会在子进程终止时立即将其资源释放,而不需要父进程调用wait
或waitpid
来获取子进程的终止状态以释放资源。
可以看到此时是没有产生僵尸进程的:
使用signal(SIGCHLD, SIG_IGN);
的方式处理僵尸进程有一些局限性和潜在的问题:
如果需要掌握子进程退出的情况,建议注册信号回调函数,然后使用wait
或waitpid
来处理僵尸进程。
如下图所示,可以使用wait
函数来回收子进程的资源。
运行程序,创建一个客户端然后关闭,再创建一个客户端然后再关闭,结果如下:
可以看到服务端正常地回收了资源,此时使用top
查看也是没有僵尸进程的。
但是在多个客户端同时关闭的情况下,wait
会产生问题
我们现在对客户端的代码做出如下修改:
现在来看一下这10个套接字同时退出后会发生什么:
可以看到我们注册的SIGCHLD
回调函数只被触发了4次,也就是说只有4个子进程的资源被回收了。此时用top
查看僵尸进程的数量,果然还有6个:
实际上也好理解,这些套接字在非常短的时间间隔内同时关闭,对于Linux的内核来看,应该是有一个进程专门用来处理这些信号,在上一个信号还在处理的同时又来了多个信号,那么下次OS只会响应一个信号,而不会调用多次回调函数,然后调用一次wait
就回收一个子进程。
所以如果我们多次测试,可以发现每次被回收的进程的数量都是不同的,这和OS内部的任务调度有关,但基本上不可能10个全部回收。
那我们是否可以在sigchld_handler
中调用while循环无限地wait
来解决这个问题呢?
答案是否定的。因为 wait
是一个阻塞调用,会导致信号处理函数阻塞,而信号处理函数的处理应该尽量迅速。
这时我们就可以使用waitpid
函数:
pid_t waitpid(pid_t pid, int *status, int options);
其中第三个参数options
的常用值如下(可以使用按位或运算符|
组合多个选项):
WNOHANG
:在没有终止的子进程时立即返回,不阻塞。如果指定了这个选项,waitpid
将立即返回,不会等待子进程终止。WUNTRACED
:也等待已停止的子进程的状态。WCONTINUED
:也等待被停止的子进程被继续的状态。所以我们只要在信号处理回调函数中使用waitpid(-1, &status, WNOHANG)
即可避免前面回收资源不完全的情况。
void sigchld_handler(int signo) {
pid_t pid;
int status;
// 在信号处理函数中循环调用waitpid以获取所有子进程的终止状态,其中-1表示等待任意子进程
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf("Child process %d exited with status %d\n", pid, WEXITSTATUS(status));
}
}
结果如下: