首先说为什么要清理子进程(由父进程),在Unix系统中经常会听到一个词“僵尸进程”(Zombie Process[可不是植物大战僵尸^_*]),而“僵尸进程”就是由子进程而来的。
“僵尸子进程”产生的原因,简单来说,就是子进程结束的时候,它的父进程没有wait它,内核释放了这个子进程的空间但进程却没有从进程表中被删除(空占进程表),而Unix保证每个进程都可以访问其子进程的退出信息,但必须wait它才可以取出对应进程的退出信息,这样儿就出现了一个问题,父进程可能太忙没有wait子进程,子进程却结束了,子进程对应的退出信息就会一直保留(包括进程ID等),资源会一直被占用,小数量的可能没有危害,一旦“僵尸进程”数量变多,主要是进程数限制,可能就会造成无法产生新的进程。
这就是典型的“僵尸进程”的危害。(另外说明一种情况,如果在子进程结束前,父进程已经结束,这样不会造成“僵尸进程”,因为Unix提供一种机制,如果一个进程结束,系统会检查进程,如果有进程正好是已经结束进程的子进程,那么这个进程就会被init进程接管,也就是说init进程变成了它的父进程。)
下面是一个小例子,先对“僵尸进程”有一个视觉的了解:(zombie.c)
1 |
|
正常编译运行,主程序会sleep一分钟。
此时打开另一个终端窗口,输入:1
ps –e –o pid,ppid,state,cmd
查看所有进程,包括进程ID,父进程ID,进程状态,进程命令行,如果不出什么差错,应该可以看到类似下面的信息:
这个进程状态为Z,命令行为defunct的就是大名鼎鼎的“僵尸进程”了(Z->Zombie)。而我们应该怎么避免呢?根据上面,首先肯定想到,由父进程wait子进程,直接到子进程结束,再由父进程wait到其状态。但是这样又带来一个问题,父进程与子进程的运行本身就是一个异步的过程,父进程没法预料什么时候子进程结束,一直wait子进程?那父进程就会一直挂起到子进程结束的那一刻,下面看一个例子:
1 |
|
正常编译运行,可以看到,直到子进程结束,父进程也就是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 |
|
编译运行,可以看到,这次父进程没有一直Hang Up等待子进程结束,父进程一直在做自己的事,直接接收到了SIGCHLD信号,才对子进程做出处理,这样儿,就通过对SIGCHLD信号的处理达到了对子进程异步清理(wait)的目的。