1.僵尸进程
1.1 僵尸进程的由来和概念
通常,子进程结束之后,需要父进程为子进程进行回收,俗称“收尸”,则回收子进程占用的一些内存资源,父进程通过调用wait()(或其变体 waitpid()、waitid()等)函数回收子进程资源,归还给系统。
如果子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个僵尸进程。
当父进程调用 wait()(或其变体,下文不再强调)为子进程“收尸”后,僵尸进程就会被内核彻底删除。另外一种情况,如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并自动调用 wait(),故而从系统中移除僵尸进程。
僵尸进程有什么影响?
例如:如果父进程创建了某一子进程,子进程已经结束,而父进程还在正常运行,但父进程并未调用 wait()回收子进程,此时子进程变成一个僵尸进程。首先来说,这样的程序设计是有问题的,如果系统中存在大量的僵尸进程,它们势必会填满内核进程表,从而阻碍新进程的创建。需要注意的是,僵尸进程是无法通过信号将其杀死的,即使是“一击必杀”信号 SIGKILL 也无法将其杀死,那么这种情况下,只能杀死僵尸进程的父进程(或等待其父进程终止),这样 init 进程将会接管这些僵尸进程,从而将它们从系统中清理掉!所以,在我们的一个程序设计中,一定要监视子进程的状态变化,如果子进程终止了,要调用 wait()将其回收,避免僵尸进程。
编写示例代码,产生一个僵尸进程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
/* 创建子进程 */
switch (fork())
{
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程<%d>被创建\n", getpid());
sleep(1);
printf("子进程结束\n");
_exit(0);
default:
/* 父进程 */
break;
}
for ( ; ; )
sleep(1);
exit(0);
}
在上述代码中,子进程已经退出,但其父进程并没调用 wait()为其“收尸”,使得子进程成为一个僵尸进程,使用命令ps -aux可以查看到该僵尸进程,测试结果如下:
通过命令可以查看到子进程 6042 依然存在,可以看到它的状态栏显示的是“Z”(zombie,僵尸),表示它是一个僵尸进程。僵尸进程无法被信号杀死,大家可以试试,要么等待其父进程终止、要么杀死其父进程,让 init 进程来处理,当我们杀死其父进程之后,僵尸进程也会被随之清理。
tips:想结束这个进程,直接启动线程的终端直接关掉再查看,就不会有了。
2. SIGCHLD 信号
2.1 信号,进程的回收
SIGCHLD 信号,前面有讲过一点,但当以下两种情况下,父进程会收到信号:
- 当父进程的某个子进程终止时,父进程会收到 SIGCHLD 信号;
- 当父进程的某个子进程因收到信号而停止(暂停运行)或恢复时,内核也可能向父进程发送该信号。
向大家说明一下:子进程的终止属于异步事件,父进程事先是无法预知的,如果父进程有自己需要做的事情,它不能一直wait()阻塞等待子进程终止(或轮训),这样父进程将啥事也做不了,那么有什么办法来解决这样的尴尬情况,当然有办法,那就是通过 SIGCHLD 信号。
这就是为什么,我们还要这重复信号的作用!!!
那既然子进程状态改变时(终止、暂停或恢复),父进程会收到 SIGCHLD 信号,SIGCHLD 信号的系统默认处理方式是将其忽略,所以我们就需要捕获它,所以我们要绑定信号处理函数,在信号处理函数中调用 wait()收回子进程,回收完毕之后再回到父进程自己的工作流程中。
下面给初学者一点技巧和方法:
问题:当调用信号处理函数时,会暂时将引发调用的信号添加到进程的信号掩码中(除非 sigaction()指定了 SA_NODEFER 标志),这样一来,当 SIGCHLD 信号处理函数正在为一个终止的子进程“收尸”时,如果相继有两个子进程终止,即使产生了两次 SIGCHLD 信号,父进程也只能捕获到一次SIGCHLD 信号,结果是,父进程的 SIGCHLD 信号处理函数每次只调用一次 wait(),那么就会导致有些僵尸进程成为“漏网之鱼”
解决: 在 SIGCHLD 信号处理函数中循环以非阻塞方式来调用 waitpid(),直至再无其它终止的子进程需要处理为止,所以,通常 SIGCHLD 信号处理函数内部代码如下所示:
while (waitpid(-1, NULL, WNOHANG) > 0)
continue;
直至 waitpid()返回 0,表明再无僵尸进程存在;或者返回-1,表明有错误发生。
应在创建任何子进程之前,为 SIGCHLD 信号绑定处理函数.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
static void wait_child(int sig)
{
/* 替子进程收尸 */
printf("父进程回收子进程\n");
while (waitpid(-1, NULL, WNOHANG) > 0)
continue;
}
int main(void)
{
struct sigaction sig = {0};
/* 为 SIGCHLD 信号绑定处理函数 */
sigemptyset(&sig.sa_mask);
sig.sa_handler = wait_child;
sig.sa_flags = 0;
if (-1 == sigaction(SIGCHLD, &sig, NULL))
{
perror("sigaction error");
exit(-1);
}
/* 创建子进程 */
switch (fork())
{
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程<%d>被创建\n", getpid());
sleep(1);
printf("子进程结束\n");
_exit(0);
default:
/* 父进程 */
break;
}
sleep(3);
exit(0);
}
本文参考正点原子的嵌入式LinuxC应用编程。