Linux — 多进程编程之 – 僵尸进程

一、僵尸进程产生原因

  僵尸进程:进程结束,父进程没有对其资源回收。

  僵尸进程的产生:

1、子进程先于父进程结束
2、父进程不结束/不退出    —>不会被init收养/不会被init回收
3、父进程不执行wait等函数 —> wait 等函数用于回收子进程

  1、僵尸进程测试示例如下所示。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, const char *argv[])
{
    /* 定义进程PID记录的变量 */
    pid_t pid = 0;
    /* 输出调试语句 */
    printf("Zombie process test!n");

    /* 调用fork()函数创建进程 */
    pid = fork();
    /* 函数返回值判断 */
    if (pid == -1) /* 返回错误 */
    {
        perror("fork error");
        return -1;
    }
    else if (pid == 0) /* 子进程 */
    {
        /* 子进程 */
        printf("I'm child, my PID = %d, my parent PID = %dn", getpid(), getppid());
    }
    else /* 父进程 */
    {
        sleep(1); /* 延时片刻,保证子进程先运行 */
        printf("I'm parent, return PID = %d, parent pid = %dn", pid, getpid());

        while (1); /* 父进程保持不退出 */
    }

    return 0;
}

  编译上述程序并进程运行之后,显示效果下图1.2所示。

图1.2 测试程序运行效果

  此时在另外一个终端可以查看当前进程的状态。使用指令ps -ajx 查看,显示效果下图1.3所示。

图1.3 程序运行效果

  ps工具标识进程的5种状态码表示如下所示。

D 不可中断状态 uninterruptible sleep (usually IO)
R 运行状态 runnable (on run queue)
S 中断状态 sleeping
T 停止状态 traced or stopped
Z 僵死状态 a defunct (”zombie”) process

二、僵尸进程的危害

  unix提供了一种可以保证父进程获取子进程结束时的状态信息的机制,也就是:

  任何一个子进程(init除外)在退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然会保留一些信息(包括进程号退出状态运行时间等),称为僵尸进程(Zombie)的数据结构,直到父进程通过 wait()waitpid() 来取时才被释放。

  但问题时如果父进程不调用 wait()waitpid() 的话, 那保留的信息就不会被释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。

  综上所示,僵尸进程的危害一般为:

1、僵尸进程会造成一定的资源浪费,占用不必要的资源
2、当进程id达到了最大值的时候,因为有僵尸进程占用了部分进程id,使得无法再打开新的进程。

  所以,僵尸进程的危害是大大的,是要避免的。

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

三、僵尸进程避免

3.1、方式一:调用wait()/waitpid()函数

  可以通过在父进程中调用wait()函数或者waitpid()函数,使得在子进程退出的时刻被父进程回收即可。

  1、wait()waitpid()函数原型如下所示。

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);

pid_t waitpid(pid_t pid, int *wstatus, int options);

  2、wait()waitpid()函数说明如下所示。

  pid_t wait(int *wstatus)函数说明。

功能
  wait()函数用于使父进程(也就是调用wait()的进程)阻塞,直到一个子进程结束或者该进程接到了一个指定的信号为止。如果该父进程没有子进程或者他的子进程已经结束,则wait()会立即返回-1
参数
  wstatus:指向的整形对象来保存子进程结束时的状态,另外子进程的结束状态可由Linux中一些特定的宏来测定。
返回值
  成功:已回收的子进程的进程等
  失败:-1

  pid_t waitpid(pid_t pid, int *wstatus, int options)函数说明。

功能
  waitpid()的作用和wait()一样,但它并不一定等待第一个终止的子进程。waitpid()有若干选项,可提供一个非阻塞版本的wait()功能。实际上wait()函数只是waitpid()函数的一个特例,在Linux内部实现wait()函数时直接调用的就是waitpid()函数。
参数
  pid:参数pid的值有下面几种情况,分别表示为:
    pid < -1回收其组ID等于pid的绝对值的任一子进程
    pid = -1回收任何一个子进程,此时和wait()作用—样
    pid = 0回收其组ID等于调用进程的组ID的任一子进程
    pid > 0回收进程ID等于pid的子进程
  wstatus:与wait()函数中参数相同
  options:参数options的值等于或等于以下零个或多个常数,分别为:
    WNOHANG:如果指定的子进程没有结束,则waitpid()函数不阻塞立即返回,且返回值为0
    WUNTRACED:由pid指定的任意子进程如果已经被暂停,且其状态子暂停以来还没有报告过,则返回其状态
    WCONTINUED(自Linux 2.6.10起) :如果已经停止的子进程通过传递的SIGCONT恢复,则返回其状态
返回值
  > 0:已经结束的子进程的进程号
  = 0:使用选项WNOHANG且没有子进程退出
  =-1:发生错误

  3、wait()waitpid()函数示例如下所示。

#include <stdio.h>
#include <stdlib.h>    /* exit    */
#include <sys/types.h> /* waitpid */
#include <sys/wait.h>  /* waitpid */
#include <unistd.h>    /* getpid等 */

int main(int argc, const char *argv[])
{
    /* 定义进程PID记录的变量 */
    pid_t pid = 0;
    /*  定义接收进程退出时候的状态 */
    int status = 0;
    /* 定义一个函数的返回值 */
    pid_t retval = 0;
    /* 输出调试语句 */
    printf("Zombie process test!n");

    /* 调用fork()函数创建进程 */
    pid = fork();
    /* 函数返回值判断 */
    if (pid == -1) /* 返回错误 */
    {
        perror("fork error");
        return -1;
    }
    else if (pid == 0) /* 子进程 */
    {
        /* 子进程 */
        printf("I'm child, my PID = %d, my parent PID = %dn", getpid(), getppid());
        /* 子进程延时片刻 */
        printf("sleep(5) ...n");
        sleep(5);
        /* 调用函数退出子进程 */
        exit(EXIT_SUCCESS);
    }
    else /* 父进程 */
    {
        printf("I'm parent, return PID = %d, my pid = %dn", pid, getpid());

        /************************************************************************
        *  情景一:回收任意一个子进程
        *  如果不关注子进程退出状态,则参数status可为NULL,下同
        *************************************************************************/
        // retval = wait(&status); /* 或者使用 wait(NULL); */
        // retval = waitpid(-1, &status, 0); /* 或者使用 waitpid(-1, NULL, 0); */
        /************************************************************************
        *  情景二:回收进程ID为pid的子进程
        *************************************************************************/
        // retval = waitpid(pid, &status, 0); /* 或者使用 waitpid(pid, NULL, 0); */
        /************************************************************************
        *  情景三:组ID等于pid的绝对值的任一子进程
        *************************************************************************/
        // retval = waitpid(-pid, &status, 0); /* 或者使用 waitpid(-pid, NULL, 0); */
        /************************************************************************
        *  情景四:组ID等于调用进程的组ID的任一子进程
        *************************************************************************/
        retval = waitpid(0, &status, 0); /* 或者使用 waitpid(0, NULL, 0); */

        printf("Parent wait/waitpid OK, status = %d, retval = %dn", status, retval);

        getchar(); /* 父进程保持不退出,方便查阅相关状态 */
    }

    return 0;
}

  上面的程序编写了 wait()waitpid() 函数的集中情况,有兴趣的伙伴可以自行编译并测试。本次只测试目前代码中有效的部分效果。

  编译上面的程序并执行,执行效果如下图1.4所示。

图1.4 程序运行效果图

  此时在另外一个终端可以查看当前进程的状态。使用指令ps -ajx 查看,显示效果下图1.5所示。

图1.5 程序运行进程状态效果图

  4、waitpid()函数使用 非阻塞模式 示例如下所示。

#include <stdio.h>
#include <stdlib.h>    /* exit    */
#include <sys/types.h> /* waitpid */
#include <sys/wait.h>  /* waitpid */
#include <unistd.h>    /* getpid等 */

int main(int argc, const char *argv[])
{
    /* 定义进程PID记录的变量 */
    pid_t pid = 0;
    /*  定义接收进程退出时候的状态 */
    int status = 0;
    /* 定义一个函数的返回值 */
    int retval = 0;
    /* 输出调试语句 */
    printf("Zombie process test!n");

    /* 调用fork()函数创建进程 */
    pid = fork();
    /* 函数返回值判断 */
    if (pid == -1) /* 返回错误 */
    {
        perror("fork error");
        return -1;
    }
    else if (pid == 0) /* 子进程 */
    {
        /* 子进程 */
        printf("I'm child, my PID = %d, my parent PID = %dn", getpid(), getppid());
        /* 子进程延时片刻 */
        printf("sleep(5) ...n");
        sleep(5);
        /* 调用函数退出子进程 */
        exit(EXIT_SUCCESS);
    }
    else /* 父进程 */
    {
        usleep(20); /* 延时片刻,保证子进程先运行 */
        printf("I'm parent, return PID = %d, parent pid = %dn", pid, getpid());

        /**************************************************************
         *  使用选项 WNOHANG 设置为非阻塞
         *  如果需要回收多个子进程,那么可以使用下面的方式进行逐个回收
         **************************************************************/
        while ((retval = waitpid(-1, &status, WNOHANG)) == 0)
        {
            printf("my PID = %d, retval = %dn", getpid(), retval);
            sleep(1);
        }

        printf("Parent wait/waitpid OK, status = %d, retval = %dn", status, retval);

        getchar(); /* 父进程保持不退出,方便查阅相关状态 */
    }

    return 0;
}

  编译上面的程序并执行,执行效果如下图1.6所示。

图1.6 程序运行效果图

3.2、方式二:调用signal()函数注册信号SIGCHLD的处理操作

  在子进程状态变化的时候,父进程会收到SIGCHLD信号。Linux系统提供了一个信号处理函数signal(函数原型如下所示),那么想要避免僵尸进程产生,我们可以从此信号入手进行相关的操作。

  1、如果父进程不关注子进程的退出状态,那么在创建子进程的时候告诉系统,所以在子进程退出之后,系统不再等待父进程进程相关资源的回收,而是直接将此子进程回收。

signal(SIGCLD, SIG_IGN);
signal(SIGCHLD, SIG_IGN);

  2、如果父进程需要关注子进程的退出状态,但是父进程同时需要处理大量的业务,并且因为 wait() 函数会阻塞,那么可以用 signal() 函数为 SIGCHLD 信号注册 handler 方法。在父进程收到该信号,在 handler 中调用 wait/waitpid 进行回收。

signal(SIGCHLD, sighandler);

#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数:
  signum:指定信号的代码
  handler:有三种可选值
    1、SIG_IGN:忽略改信号
    2、SIG_DFL:采用系统默认的方式进行信号处理
    3、自定义的信号处理函数
返回值:
  成功:以前的信号处理函数
  错误:-1

说明:
    typedef void (*sighandler_t)(int);
  等价于:
    typedef void (*)(int) sighandler_t;
  所以为了便于理解,上面的函数可以写为:
    sighandler_t signal(int signum, void (*handler)(int));
  该函数第二个参数和返回值类型都是指向一个无返回值并且带一个整形参数的函数的指针。

  那么下面就上述两种场景分别进程代码测试。

   1、使用 signal 函数忽略子进程退出的信号,由 init 进程回收子进程的资源。

#include <signal.h> /* signal */
#include <stdio.h>
#include <stdlib.h>    /* exit    */
#include <sys/types.h> /* waitpid */
#include <sys/wait.h>  /* waitpid */
#include <unistd.h>    /* getpid等 */

int main(int argc, const char *argv[])
{
    /* 定义进程PID记录的变量 */
    pid_t pid = 0;
    /*  定义接收进程退出时候的状态 */
    int status = 0;

    /* 输出调试语句 */
    printf("Zombie process test!n");

    /* 使用信号,忽略子进程退出的信号 */
    signal(SIGCHLD, SIG_IGN);

    /* 调用fork()函数创建进程 */
    pid = fork();
    /* 函数返回值判断 */
    if (pid == -1) /* 返回错误 */
    {
        perror("fork error");
        return -1;
    }
    else if (pid == 0) /* 子进程 */
    {
        /* 子进程 */
        printf("I'm child, my PID = %d, my parent PID = %dn", getpid(), getppid());
        /* 子进程延时片刻 */
        printf("sleep(5) ...n");
        sleep(5);
        printf("I'm child, The time has come and I'm going to quit.n");
        /* 调用函数退出子进程 */
        exit(EXIT_SUCCESS);
    }
    else /* 父进程 */
    {
        usleep(20); /* 延时片刻,保证子进程先运行 */

        printf("I'm parent, return PID = %d, my pid = %dn", pid, getpid());

        getchar(); /* 父进程保持不退出,方便查阅相关状态 */
    }

    return 0;
}

  编译上面的程序并执行,执行效果如下图1.7所示。

图1.7 程序运行效果图

  此时在另外一个终端可以查看当前进程的状态。使用指令ps -ajx 查看,显示效果下图1.8所示。

图1.8 程序运行进程状态效果图

   2、进行 signal() 函数为 SIGCHLD 信号注册 handler 方法回收子进程的资源。

注意:
  相对于上面的代码,在进行 signal() 函数为 SIGCHLD 信号注册 handler 方法。
需要注意的是,子进程状态变化的时候父进程会收到SIGCHLD信号。而在子进程中调用 sleep() 函数将进程状态变为了 睡眠状态,此时父进程同样会收到 SIGCHLD 信号,如果此时进行资源回收,可能会产生意想不到的后果。

  综上所述,测试程序如下所示。

#include <signal.h> /* signal */
#include <stdio.h>
#include <stdlib.h>    /* exit    */
#include <sys/types.h> /* waitpid */
#include <sys/wait.h>  /* waitpid */
#include <unistd.h>    /* getpid等 */

void func_handler(int signo)
{
    /*  定义接收进程退出时候的状态 */
    int status = 0;
    /*  定义接收函数返回的状态 */
    pid_t retval = 0;

    /* 调用waitpid回收子进程,如果使用wait则会阻塞 */
    while ((retval = waitpid(-1, &status, WNOHANG)) > 0)
        printf("Parent wait/waitpid OK, status = %d, retval = %dn", status, retval);
}

int main(int argc, const char *argv[])
{
    /* 定义进程PID记录的变量 */
    pid_t pid = 0;

    /* 输出调试语句 */
    printf("Zombie process test!n");

    /* 使用信号 */
    signal(SIGCHLD, func_handler);

    /* 创建3个进程进程多进程测试 */
    for (int i = 0; i < 3; i++)
    {
        /* 调用fork()函数创建进程 */
        pid = fork();
        /* 函数返回值判断 */
        if (pid == -1) /* 返回错误 */
        {
            perror("fork error");
            return -1;
        }
        else if (pid == 0) /* 子进程 */
        {
            unsigned long n = 0, m = 0;
            /* 子进程 */
            printf("I'm child, my PID = %d, my parent PID = %dn", getpid(), getppid());
            /* 子进程延时片刻 */
            printf("sleep(5) ...n");
            /* 调用sleep函数会改变进程的状态并向父进程发送SIGCHLD信号 */
            // sleep(5);
            /* 此处根据自己真实环境编写模拟延时功能,延时时间大概为5秒,可自行调整 */
            while (n < 50000)
            {
                if (m > 100000)
                    m = 0, n++;
                m++;
            }
            printf("I'm child, The time has come and I'm going to quit.n");
            /* 调用函数退出子进程 */
            exit(EXIT_SUCCESS);
        }
        else /* 父进程 */
        {
            usleep(20); /* 延时片刻,保证子进程先运行 */

            printf("I'm parent, return PID = %d, my pid = %dn", pid, getpid());
        }
        sleep(1);
    }
    /* 在终端输出换行,方便进程调试信息的显示观察 */
    putchar(10);

    /* 爆出父进程不退出 */
    getchar();

    return 0;
}

  编译上面的程序并执行,执行效果如下图1.7所示。

图1.9 程序运行效果图

  此时在另外一个终端可以查看当前进程的状态。使用指令ps -ajx 查看,显示效果下图1.8所示。

图1.10 程序运行进程状态效果图

3.3、方式三:多次fork()并产生孤儿进程

  关于孤儿进程,请查看博文:Linux – 多进程编程之 - 基础实现、孤儿进程

一般情况下,调用 fork() 函数两次,第一次 fork 之后,在子进程中再次进行 fork,这样在系统中就会存在三个同样的进程。然后将第一次 fork 之后的子进程(也就是第二次 fork 的父进程)退出,那么第二次 fork 之后的子进程就会变成孤儿进程。作为孤儿进程,就会被 init 进程接管,在进程退出后,init 进程回收其资源。

  但是需要注意的是,第一次 fork 之后的子进程退出后,也是需要回收的,那么此回收工作就需要第一次 fork 的父进程进行回收,不然同样会产生僵尸进程。

  综上所述,测试程序如下所示。

#include <stdio.h>
#include <stdlib.h>    /* exit    */
#include <sys/types.h> /* waitpid */
#include <sys/wait.h>  /* waitpid */
#include <unistd.h>    /* getpid等 */

int Create_Second_SubProc()
{
    /* 定义进程PID记录的变量 */
    pid_t pid_second = 0;

    /* 调用fork()函数创建第二代子进程进程 */
    pid_second = fork();
    if (pid_second == -1) /* 返回错误 */
    {
        perror("2nd fork error");
        return -1;
    }
    else if (pid_second == 0) /* 子进程 */
    {
        /* 子进程 */
        printf("I'm 2nd child, my PID = %d, my parent PID = %dn", getpid(), getppid());
        /* 子进程延时片刻 */
        printf("sleep(5) ...n");
        sleep(5);
        printf("I'm 2nd child, The time has come and I'm going to quit.n");
        /* 调用函数退出子进程 */
        exit(EXIT_SUCCESS);
    }
    else /* 父进程 */
    {
        /* 创建成功,此处是一代子进程的代码 */
        printf("I'm 2nd parent, return PID = %d, my pid = %dn", pid_second, getpid());
        /* 调用函数退出子进程 */
        exit(EXIT_SUCCESS);
    }
}

int main(int argc, const char *argv[])
{
    /* 定义进程PID记录的变量 */
    pid_t pid_first = 0;

    /*  定义接收进程退出时候的状态 */
    int status = 0;
    /*  定义接收函数返回的状态 */
    pid_t retval = 0;

    /* 输出调试语句 */
    printf("Zombie process test!n");

    /* 调用fork()函数创建新的子进程 */
    pid_first = fork();
    /* 函数返回值判断 */
    if (pid_first == -1) /* 返回错误 */
    {
        perror("1st fork error");
        return -1;
    }
    else if (pid_first == 0) /* 子进程 */
    {
        /* 子进程 */
        printf("I'm 1st child, my PID = %d, my parent PID = %dn", getpid(), getppid());
        /* 子进程延时片刻 */
        printf("sleep(5) ...n");
        sleep(5);
        /* 调用函数再次创建子进程 */
        Create_Second_SubProc();
    }
    else /* 父进程 */
    {
        usleep(20); /* 延时片刻,保证子进程先运行 */

        printf("I'm parent, return PID = %d, my pid = %dn", pid_first, getpid());

        /* 一代子进程退出后,父进程必须要进程回收,不然就成为僵尸进程 */
        if (waitpid(pid_first, &status, 0) == pid_first)
        {
            printf("I'm parent, 1st child wait success. status = %dn", status);
        }

        getchar(); /* 父进程保持不退出,方便查阅相关状态 */
    }

    return 0;
}

  编译上面的程序并执行,执行效果如下图1.7所示。

图1.11 程序运行效果图

  此时在另外一个终端可以查看当前进程的状态。使用指令ps -ajx 查看,显示效果下图1.8所示。

图1.12 程序运行进程状态效果图

  
  好啦,废话不多说,总结写作不易,如果你喜欢这篇文章或者对你有用,请动动你发财的小手手帮忙点个赞,当然 关注一波 那就更好了,就到这儿了,么么哒(*  ̄3)(ε ̄ *)。



上一篇:Linux – 多进程编程之 - 基础实现、孤儿进程
下一篇:Linux – 多进程编程之 - 守护进程

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>