僵尸进程的产生和防范

僵尸进程

僵尸进程的产生

fork产生的父进程和子进程有退出的先后顺序,如果子进程在父进程前退出就会产生,而父进程又没有回收子进程占用的资源,子进程就会变成一个僵尸进程。我们用如下代码做个实验

#include 

int main(int argc, char const *argv[])
{
    pid_t pid = fork();
    if(pid == 0){ // 子进程
        sleep(3);
    }else{
        sleep(5);
    }
    return 0;
}

父进程在子进程后退出,所以空闲的2s内子进程会是一个僵尸进程

hankinzhang 75009 0.0 0.0 4267924 800 s005 S+ 1:07下午 0:00.00 ./defunc
hankinzhang 75010 0.0 0.0 0 0 s005 Z+ 1:07下午 0:00.00 (defunc)

反之,如果父进程在子进程前退出,子进程就会被init进程接管,变成init进程的子进程(修改前面的代码)

365067280 75337 1 0 1:09下午 ttys005 0:00.00 ./defunc

僵尸产生的原因

一个进程在调用exit命令结束自己的生命的时候,其实,它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。

避免产生僵尸进程

父进程通过wait/waitpid来等待子进程结束

wait函数是waitpid的一个特例,相当于waitpid(-1, &status, 0),即等待所有子进程。wait/waitpid会阻塞父进程,等子进程退出后再进行后面的语句。

此方法会导致主进程阻塞,如果主进程有比较复杂的逻辑,不建议使用

#include 
#include 
#include 

int main(int argc, char const *argv[])
{
    pid_t pid = fork();
    if(pid == 0){ // 子进程
        int i = 3;
        while(i--){
            printf("This is child\n");
            sleep(1);
        }
    }else{
        int status;
        waitpid(pid, &status, 0);
    }
    return 0;
}

用signal函数为SIGCHILD安装handler

子进程结束后,父进程会收到SIGCHLD信号,可以在handler中调用wait回收,利用sigaction的简单用法。

#include 
#include 
#include 
#include 
void recycle(int sig){
    int status;
    wait(&status);
}

int main(int argc, char const *argv[])
{
    struct sigaction act;
    act.sa_handler = recycle;
    sigaction(SIGCHLD,&act,NULL);

    pid_t pid = fork();
    if(pid == 0){ // 子进程
        int i = 3;
        while(i--){
            printf("This is child\n");
            sleep(1);
        }
    }else{
        int i = 5;
        while(i--){
            sleep(1);
        }
    }
    return 0;
}

忽略SIGCHLD信号

系统默认的方法是进入僵尸进程状态并且通知主进程来清理,如果忽略了SIGCHLD,就会引起子进程直接被init接管,进而被直接清理。直接调用signal函数忽略SIGCHLD信号

#include 
#include 
#include 
#include 

int main(int argc, char const *argv[])
{
    signal(SIGCHLD, SIG_IGN);
    pid_t pid = fork();
    if(pid == 0){ // 子进程
        int i = 3;
        while(i--){
            printf("This is child\n");
            sleep(1);
        }
    }else{
        int i = 5;
        while(i--){
            sleep(1);
        }
    }
    return 0;
}
     struct sigaction sa;
     sa.sa_handler = SIG_IGN;
 #ifdef SA_NOCLDWAIT
     sa.sa_flags = SA_NOCLDWAIT;
 #else
     sa.sa_flags = 0;
 #endif
     sigemptyset(&sa.sa_mask);
     sigaction(SIGCHLD, &sa, NULL);

fork两次

这种方法在服务器编程中经常用到,用来防止产生僵尸进程,也就是子进程在父进程fork出自己以后再fork一次,然后自己立刻推出,让init接管孙子进程。很多服务的daemon进程都是这样产生。以防止tty上的输入信号对服务造成影响。

/* * Avoid zombie processes by calling fork twice. * APUE-2e 程序清单8-5 */
#include 
#include 
#include 
#include 

int main()
{
    pid_t pid;
    if( (pid = fork()) < 0 )
    {
        printf("fork error.\n");
        exit(-1);
    }
    else if(pid == 0)   /* first child */
    {
        for(int i = 0; i < 10; i++){
            // 第二次fork
            if( (pid = fork()) < 0 )
            {
                printf("fork error.\n");
                exit(-1);
            }
            else if(pid > 0)
            {
                continue;
            }

            /* We're the second child; our parent becomes init as soon as our real parent exits. */\
            /* ---------------handle tasks--------------- */
            printf("second child, parent pid = %d\n", getppid());
            int n = 10;
            while(n--){
                sleep(1);
            }
            exit(0);
        }
        exit(0);
    }

    if(waitpid(pid, NULL, 0) != pid)    /* wait for first child */
    {
        printf("waitpid error.\n");
        exit(1);
    }
    printf("parent, first child pid = %d\n", pid);
    /* ---------------handle tasks--------------- */

    exit(0);
}

TODO

你可能感兴趣的:(僵尸进程的产生和防范)