其实这两个概念并不难理解,只要明白了以下两个关键点就可以了:

  • 除了init进程(进程号为1,从名字就能知道负责系统启动后的相关的初始化工作)外,其他所有的进程都有父进程。也就是进程是一个树形结构,树的顶端是init进程
  • 一个进程都会占用系统资源(内存、文件描述符等),当进程结束后就需要由其父进程来释放它占用的资源
  • 那么父进程如何释放子进程资源呢?父进程需要通过wait/waitpid等系统调用函数来等待进程结束,并且可以通过这些函数来获取子进程的退出状态(正常退出,信号杀死等等)

孤儿进程

明白了父子进程的关系,那么有一个问题:如果父进程先于子进程终止,子进程的资源岂不是没人来释放了? 对于这种父进程比自身先终止的进程,顾名思义称之为孤儿进程

对于孤儿进程,内核自然不会放任不管,内核会将init进程设置为孤儿进程的父进程,美其名曰收养,从而当此进程退出后由init来回收其资源。

可以写一个小程序来验证一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();

    if( pid > 0 ){
        //父进程就退出了
        printf("我是父进程,我的进程号是:%d\n",getpid());
        sleep(1);
    }else{
        printf("我是子进程,我的父进程是%d\n",getppid());
        sleep(2);
        printf("我是子进程,父进程终止后,我的父进程是:%d\n",getppid());
    }
    return 0;
}

在这个小程序中,对于父进程先获取其进程号,为:11552,然后让其休眠1秒。对于子进程,先获取父进程号,此时父进程还未终止,所以取到的父进程号也为:11552。然后让其休眠两秒,2秒过后,父进程已经终止了,再取其父进程号,发现已经是1号进程,也就是init了。

僵尸进程

我们考虑父子进程的另一种情况:如果父进程还未调用wait族函数来等待并回收子进程资源,子进程就已经终止了,那么子进程的资源会被如何回收?

我们可能会想,内核应该会自动回收……吧?确实,内核会回收该进程的大部分资源,但是内核会在进程表中保留这一条进程记录,它的ID,它的终止状态等信息。为什么要这么做,而不是全部回收呢,因为内核要保留这些信息,以便父进程调用wait函数时能告知子进程已经终止和终止原因。

也就说是,虽然子进程提前终止了,但父进程还可以在终止后通过调用wait族函数来获取子进程终止原因。

这种已经终止但仍然还保留有一条进程记录的进程,就称谓僵尸进程。为什么叫僵尸呢,因为为了保留这条记录以便父进程获知子进程终止状态,这类进程无论如何都杀不死,即使使用的是KILL -9 进程号也就是发送SIGKILL信号也不行。

那么我们考虑,如何才能杀死僵尸进程呢? 也很简单:

  • 等待其父进程调用wait族函数将其彻底回收
  • 将其父进程杀死,这样这个僵尸进程就由init进程来接管了,init就会自动调用wait来将其回收了。

也写个小程序验证一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if( pid > 0 ){
        printf("我是父进程,进程号是:%d\n",getpid());
        //父进程什么都不做,一直在休眠
        sleep(100000);
    }else{
        printf("我是子进程,进程号是:%d\n",getpid());
    }
    return 0;
}

运行这个程序,会打印出父子进程号,然后父进程就休眠了,而子进程无事可做就终止了。这时候如果通过ps命令查看子进程的状态,就能看到子进程处于Z+,也就是僵尸状态了。而此时如果杀掉父进程,会发现子进程也不存在了,因为子进程被init回收了。

试试杀掉子进程?没用的,不然怎么叫僵尸呢!