Linux 进程控制 - 两次 fork
使用两次 fork 避免僵尸进程
一、僵尸进程和孤儿进程
在 Unix/Linux 中,子进程往往通过其父进程创建,但是子进程和父进程谁先结束却是不确定的。前面一篇文章主要探讨了父进程调用 wait/waitpid 可以在子进程结束后获取其结束状态。我们暂且把这个过程称为 父进程为子进程收尸。
1.1 僵尸进程
子进程结束后,父进程此时没有调用 waitpid,也就是父进程因为一些原因并没有给子进程收尸,其中的原因挺多的,也许是父进程自己比较忙,还没有执行到 waitpid;也有可能是父进程压根忘了有这个儿子,代码里根本没有 waitpid 这个东西。
子进程死了,没人收尸,子进程死不瞑目,变成了僵尸,这就是僵尸进程。
僵尸进程:子进程终止,父进程并没有调用 wait/waitpid 获取子进程的终止状态,且父进程还没有结束(子进程没有被 init 收养),那么当子进程结束后,它的进程描述符仍然保存在系统中,这就成了僵尸进程。
危害:虽然进程结束后资源都被内核释放,但是仍然为其保留了一部分信息:进程描述符,CPU 时间,退出状态等,进程号被一直占用着,造成危害。
1.2 孤儿进程
子进程还没有结束,但是父进程结束了,这个时候子进程失去其唯一的父进程,成为了 孤儿进程,这个时候,回收子进程的任务就交给了 init 进程,这个时候内核会将其父进程改为 init,即进程 ID 为 1 的进程,这个过程为 init 收养子进程。
子进程的父进程虽然结束了,但是却有强大的 init 进程作为它的继父为它进行一切善后工作,因此孤儿进程并没有什么危害。
二、两次 fork 避免僵尸进程的技巧
现在考虑一个子进程何时成为僵尸进程:
- 子进程先结束,父进程压根没有 wait/waitpid,在父进程结束前,子进程处于僵尸态。
- 子进程先结束,父进程正忙,还没执行到 waitpid,在父进程执行到 waitpid 前,子进程处于僵尸态。
而避免僵尸子进程有两种方法:
- 父进程调用 waitpid.
- 父进程早早结束,让 init 收养子进程。
而两次 fork 的技巧则是综合了这两种方法:
通俗点讲,就是爷爷第一次 fork 生一个老爸,老爸出生后立刻 fork 生下儿子,这个时候老爸的任务就结束了,可以死掉了 (exit),这个时候儿子被强大的 init 收养,爷爷爱干啥干啥,从而儿子永远不会成为僵尸进程。
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid;
pid = fork();
if (pid == 0) {
pid = fork();
if (pid > 0) {
//父亲生下儿子直接退出,儿子会被收养
exit(0);
}
sleep(0.5);
printf("I'm son after second fork. ");
printf("my parent's pid: %d\n", getppid());
exit(0);
}
//爷爷生下父亲后直接等待为其收尸
waitpid(pid, NULL, 0);
//爷爷尽情快活
exit(0);
}
程序输出:
I'm son after second fork. my parent's pid: 1
子进程被 init 收养,不会成为僵尸进程。