使用SIGCHLD信号异步清理子进程
2015年04月19日 Linux

首先说为什么要清理子进程(由父进程),在Unix系统中经常会听到一个词“僵尸进程”(Zombie Process[可不是植物大战僵尸^_*]),而“僵尸进程”就是由子进程而来的。

“僵尸子进程”产生的原因,简单来说,就是子进程结束的时候,它的父进程没有wait它,内核释放了这个子进程的空间但进程却没有从进程表中被删除(空占进程表),而Unix保证每个进程都可以访问其子进程的退出信息,但必须wait它才可以取出对应进程的退出信息,这样儿就出现了一个问题,父进程可能太忙没有wait子进程,子进程却结束了,子进程对应的退出信息就会一直保留(包括进程ID等),资源会一直被占用,小数量的可能没有危害,一旦“僵尸进程”数量变多,主要是进程数限制,可能就会造成无法产生新的进程。

这就是典型的“僵尸进程”的危害。(另外说明一种情况,如果在子进程结束前,父进程已经结束,这样不会造成“僵尸进程”,因为Unix提供一种机制,如果一个进程结束,系统会检查进程,如果有进程正好是已经结束进程的子进程,那么这个进程就会被init进程接管,也就是说init进程变成了它的父进程。)

下面是一个小例子,先对“僵尸进程”有一个视觉的了解:(zombie.c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
//产生子进程
int spawn_process()
{
pid_t child_pid;
child_pid = fork();

//父进程返回子进程ID
if (child_pid != 0)
{
return child_pid;
}
else//子进程中,做一些事情
{
sleep(10);
abort();
}
}

int main(int argc, char *argv[])
{
int child_status;
//产生子进程并wait
spawn_process();
wait(&child_status);
printf("child_process finished!\n");
return 0;
}

正常编译运行,主程序会sleep一分钟。
此时打开另一个终端窗口,输入:

1
ps –e –o pid,ppid,state,cmd

查看所有进程,包括进程ID,父进程ID,进程状态,进程命令行,如果不出什么差错,应该可以看到类似下面的信息:

这个进程状态为Z,命令行为defunct的就是大名鼎鼎的“僵尸进程”了(Z->Zombie)。而我们应该怎么避免呢?根据上面,首先肯定想到,由父进程wait子进程,直接到子进程结束,再由父进程wait到其状态。但是这样又带来一个问题,父进程与子进程的运行本身就是一个异步的过程,父进程没法预料什么时候子进程结束,一直wait子进程?那父进程就会一直挂起到子进程结束的那一刻,下面看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
//产生子进程
int spawn_process()
{
pid_t child_pid;
child_pid = fork();

//父进程返回子进程ID
if (child_pid != 0)
{
return child_pid;
}
else//子进程中,做一些事情
{
sleep(10);
abort();
}
}

int main(int argc, char *argv[])
{
int child_status;
//产生子进程并wait
spawn_process();
wait(&child_status);
printf("child_process finished!\n");
return 0;
}

正常编译运行,可以看到,直到子进程结束,父进程也就是main中的printf才打印出child_process finished!,在此之前进程一直处于挂起状态(Hang Up),也就是父进程要一直等,等子进程结束,父进程才会结束,这显然不是我们想要的优雅的解决方式。

一种优雅的解决方式就是,当每个子进程结束的时候,通知父进程(儿子出去玩不知道什么时候回来,父亲不知道什么时候该做饭等他,如果儿子回来的时候打电话告诉父亲,父亲再等他,这样儿父亲还不耽误其他时间),而子进程怎么通知父进程呢?通知的方法有几种,进程间通信(将在以后讨论),现在最容易想到的就是信号,Linux下一个子进程结束的时候,会向父进程发送一个SIGCHLD信号,而Linux系统对这个信号的默认是什么也不做,于是,我们就可以通过捕获SIGCHLD信号,再去让父进程wait子进程,这样就可以父进程就可以异步的清理子进程,也就是只在子进程结束的时候才去wait子进程退出状态。接下来的问题就是如何捕获对应的信号,设置对应的信号处理了。这里我们使用sigaction,简单说一下sigaction,它是用来设定对应信号处理的函数,三个参数,第一个设置要处理信号的信号值,第二个参数,设置对应的sigaction结构,结构中最重要的是信号处理函数,也就是对应的handler域,第三个也是一个sigaction结构,可以理解为保存上一个的信号处理的sigaction结构,如果第二个参数不为空则当前使用第二个参数中的处理,如果第三个参数不空,上一个处理的sigaction就会保存在这里(如要具体了解,参见man 2 sigaction)。

下面给出例子,父进程通过捕获SIGCHLD信号,wait子进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>

//全局变量保存退出状态
sig_atomic_t exit_status;

int spawn_process()
{
pid_t child_pid;
child_pid = fork();

//父进程返回子进程ID
if (child_pid != 0)
{
return child_pid;
}
else//子进程中,做一些事情
{
sleep(10);
abort();
}
}
//SIGCHLD信号处理函数
void cleanup_child_process(int sig_num)
{
int status;
wait(&status);//wait子进程
printf("child_process prepare terminate!");
exit_status = status;//保存退出状态信息
}

int main(int argc, char *argv[])
{
/* 通过调用cleanup_child_process处理SIGCHLD信号 */
struct sigaction sigchild_action;
memset(&sigchild_action, 0, sizeof(sigchild_action));
sigchild_action.sa_handler = &cleanup_child_process;
sigaction(SIGCHLD, &sigchild_action, NULL);//设置对应的信号处理

//产生子进程并wait
spawn_process();
while (1)
{
printf("parent_process do something!\n");
sleep(1);
}

return 0;
}

编译运行,可以看到,这次父进程没有一直Hang Up等待子进程结束,父进程一直在做自己的事,直接接收到了SIGCHLD信号,才对子进程做出处理,这样儿,就通过对SIGCHLD信号的处理达到了对子进程异步清理(wait)的目的。