最近忙里偷闲整理linux c系统编程中的相关知识点,今天来介绍的是僵尸进程与孤儿进程,并结合具体的代码示例,如果你有任何疑问欢迎留言讨论。如果你对这部分概念比较清楚,可以直接跳转到相关代码部分进行验证。

基本概念

孤儿进程:父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

产生的问题

在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果父进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。某一子进程的父进程终止后,对getpid()的调用将返回1。这是判定某一子进程生父是否在世的方法之一,当然前提是该子进程由init之外的进程创建。

任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。

僵尸进程危害场景:
如果父进程创建了某一子进程,但并未执行wait(),那么再内核的进程表中将为子进程永久保留一条记录。如果存在大量此类僵尸进程,他们势必将填满内核进程表,从而阻碍新进程的创建。因为无法用信号杀死僵尸进程,那么从系统中将其移除的唯一方法就是杀掉他们的父进程或者等待父进程终止,此时init进程接管和等待这些僵尸进程,从而从系统中将他们清理掉。
实例代码:

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

int main()
{
    pid_t pid;
    //创建一个进程
    pid = fork();
    //创建失败
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    //子进程
    if (pid == 0)
    {
        //输出进程ID和父进程ID
        printf("I am the child process.pid: %d\tppid:%d\n",getpid(),getppid());
        //睡眠5s,保证父进程先退出
        sleep(5);
        //父进程退出后,init接管孤儿进程,此时父进程号为1
        printf("I am the child process.pid: %d\tppid:%d\n",getpid(),getppid());
        printf("child process is exited.\n");
        //睡眠60s,通过pstree验证init已接管孤儿进程 
        sleep(60);
    }
    //父进程
    else
    {
        printf("I am father process. pid:%d\n",getpid());
        //父进程睡眠2s,保证在子进程第一次打印父进程号时不是1
        sleep(2);
        printf("father process is  exited.\n");
    }
    return 0;
}


通过例子我们可以清晰的看到此时init接管了孤儿进程。

僵尸进程

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

int main()
{
    pid_t pid;
    pid = fork();
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    else if (pid == 0)
    {
        printf("I am child process.I am exiting.\n");
        exit(0);
    }
    printf("I am father process.I will sleep five seconds\n");
    //等待子进程先退出
    sleep(5);
    //输出进程信息
    system("ps -o pid,ppid,state,tty,command");
    printf("father process is exiting.\n");
    return 0;
}


上图中我们执行了两次,第二次执行时的代码如下:

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

int main()
{
    pid_t pid;
    pid = fork();
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    else if (pid == 0)
    {
        printf("I am child process.I am exiting.\n");
        exit(0);
    }
    printf("I am father process.I will sleep five seconds\n");
    //等待子进程先退出
    sleep(5);
    //输出进程信息
    system("ps -o pid,ppid,state,tty,command");
    sleep(1000);
    printf("father process is exiting.\n");
    return 0;
}

通过上面的例子我们看到在执行上面的代码后,我们去grep oraphan第一次没有僵尸进程是因为父进程退出后init去接管和等待僵尸进程,从而从系统中将他们清除。第二次运行时我们在system和printf之间添加了sleep,验证在父进程存在期间僵尸进程会一直存在。因此从系统中将其移除的唯一方法就是杀掉僵尸进程的父进程(或等待其父进程终止)。
在阅读UNIX高级环境编程时,书中提到了另一种避免僵尸进程的方法:fork两次。如果一个进程要fork一个子进程,但不要求它等待子进程终止,也不希望子进程处于僵死状态直到父进程终止,实现这一要求的诀窍是调用fork两次。代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t  pid;
    //创建第一个子进程
    pid = fork();
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    //第一个子进程
    else if (pid == 0)
    {
        //子进程再创建子进程
        printf("I am the first child process.pid:%d\tppid:%d\n",getpid(),getppid());
        pid = fork();
        if (pid < 0)
        {
            perror("fork error:");
            exit(1);
        }
        //第一个子进程退出
        else if (pid >0)
        {
            printf("first procee is exited.\n");
            exit(0);
        }
        //第二个子进程
        //睡眠3s保证第一个子进程退出,这样第二个子进程的父亲就是init进程里
        sleep(3);
        printf("I am the second child process.pid: %d\tppid:%d\n",getpid(),getppid());
        exit(0);
    }
    //父进程处理第一个子进程退出
    if (waitpid(pid, NULL, 0) != pid)
    {
        perror("waitepid error:");
        exit(1);
    }
    exit(0);
}

在第二个子进程中调用sleep以 保 证 在 打 印 父 进 程 I D 时 第 一 个 子 进 程 已 终 止 。 在 f o r k 之后, 父、子进程都可继续执行——我们无法预知哪一个会先执行。如果不使第二个子进程睡眠,则在fork之后,它可能比其父进程先执行,于是它打印的父进程ID将是 创建它的父进程, 而不 是init进程(进程号1)
运行结果:
root@ubuntu:~/100gdb# ./forktwice
I am the first child process.pid:6200 ppid:6199
first procee is exited.
root@ubuntu:~/100gdb# I am the second child process.pid: 6201 ppid:1

root@ubuntu:~/100gdb#
子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。
当一个子进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件,所以这种信号也是内核向父进程发的异步通知。