使用两次 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 收养,不会成为僵尸进程。