目录

1.什么是僵尸进程?产生条件

2.僵尸进程的解决方法

1.wait

2.waitpid

3.信号

4.两次fork


1.什么是僵尸进程?产生条件

当fork一个新进程的时候,子进程一般会和父进程同时运行。当子进程结束的时候,它与父进程的关联还会保持,直到父进程也正常终止或者wait,子进程才结束。因此,进程中代表子进程的表项不会立即释放。虽然子进程已经不能正常运行,但是它仍然存在于系统之中,因为它的退出码还要保存起来,以备父进程今后的wait调用使用。

这种情况我们称已经结束了但是还不能释放的子进程为僵尸进程。

当某一子进程结束、中断或恢复执行时,内核会发送SIGCHLD信号予其父进程。在默认情况下,父进程会以SIG_IGN函数忽略之。 某一子进程终止执行后,若其父进程未提前调用wait,则内核会持续保留子进程的退出状态等信息,以使父进程可以wait获取之 。而因为在这种情况下,子进程虽已终止,但仍在消耗系统资源,所以其亦称僵尸进程。wait相当于SIGCHLD信号的处理函数中。

首先我们来看一个产生僵尸进程的程序:

#include <stdio.h>
# include<stdlib.h>
# include<string.h>
# include<unistd.h>
# include<assert.h>
# include<sys/wait.h>
# include<sys/types.h>
int main()
{
    int n = 0;
    char *s = NULL;
    pid_t pid = fork();
    assert(pid>=0);
    if(pid==0)
    {
        n=5;
        s="child";
    }
    else
    {
        n=15;
        s="parent";

    }
    int i=0;
    for(;i<n;++i)
    {
        printf("s=%s\n",s);
        sleep(1);
    }
    if(pid)
        printf("parent end\n");
    
    else
        printf("child end\n");
        
    exit(0);
}

上述程序子进程会先于父进程结束,当打印出child end的时候,子进程就已经不在执行了,我们通过ps查看它的状态:

可以看到进程4962为子进程,此时标识为defunct,标识它是一个僵尸进程。

2.僵尸进程的解决方法

1.wait

wait函数的形式为:

pid_t wait(int *stat_loc)

返回值为子进程的PID,如果stat_loc不为空,那么内核保存的子进程的状态信息将会写入它所指向的位置。

注意:wait函数会使父进程在函数调用处挂起(阻塞),当子进程结束之后,父进程才会继续执行wait之后的代码。

wait函数将父子进程分离开来,让本来并发执行的父子进程变成了异步执行。

#include <stdio.h>
# include<stdlib.h>
# include<string.h>
# include<unistd.h>
# include<assert.h>
# include<sys/wait.h>
# include<sys/types.h>
int main()
{
    int n = 0;
    char *s = NULL;
    pid_t pid = fork();
    assert(pid>=0);
    if(pid==0)
    {
        n=5;
        s="child";
    }
    else
    {
        n=15;
        s="parent";

    }
    wait((int*)0);
    int i=0;
    for(;i<n;++i)
    {
        printf("s=%s\n",s);
        sleep(1);
    }
    if(pid)
        printf("parent end\n");
    
    else
        printf("child end\n");
        
    exit(0);
}

执行结果如下,可以发现,在子进程结束之后,父进程才从wait处继续执行。

2.waitpid

首先看以下waitpid函数的形式:

pid-t waitpid(pid_t pid,int *stat_loc,int options);

返回值为清理掉的子进程的pid,如果为-1,标识没有子进程被清理。

stat_loc的含义和wait中一样。

pid的参数含义如下:

>0 回收指定ID的子进程
-1 回收任意子进程(相当于wait)
0 回收和当前调用waitpid一个组的所有子进程
< -1 回收指定进程组内的任意子进程

options选项经常使用的主要有以下两个:如果不想使用options,可以设置为0


WNOHANG
如果pid指定的子进程没有结束,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。
WUNTRACED
如果子进程进入暂停状态,则马上返回

waitpid函数相比于wait函数,除了能等待指定的进程,还能再等待指定进程的同时通过设置options选项为WNOHANG让父进程不会在waitpid函数中阻塞。

因此如果想让父进程周期性地检查某个特定的子进程是否结束,就可以使用waitpid。


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

int main(void)
{
    pid_t pid, wpid;

    pid = fork();
    if(pid == -1){
        perror("fork error");
        exit(1);
    } else if(pid == 0){        //son
        printf("I'm process child, pid = %d\n", getpid());
        sleep(5);                
        exit(0);
    } else {                    //parent
        do {
            wpid = waitpid(pid, NULL, WNOHANG);
            //wpid = wait(NULL);
            printf("---wpid = %d\n", wpid);
            if(wpid == 0){
                printf("NO child exited\n");
                sleep(1);        
            }
        } while (wpid == 0);        //子进程不可回收

        if(wpid == pid)      //回收了指定子进程
            printf("I'm parent, I catched child process,"
                    "pid = %d\n", wpid);
    return 0;
}

上述程序,在子进程结束之前,父进程一直在do while语句里面执行,并没有挂起。程序执行结果如下如所示:

3.信号

信号量说一种软中断机制。我们知道,任何一个子进程结束,内核都会给父进程发送一个SIGCHLD信号,如果我们在父进程定义一个信号处理函数,在函数中进行wait操作或者waitpid操作,当任何一个子进程结束的时候,都会触发signal函数,这样我们就可以通过signal+wait的方法,更加灵活的处理僵尸进程了。

如下所示:

#include <stdio.h>
# include<stdlib.h>
# include<string.h>
# include<unistd.h>
# include<assert.h>
# include<sys/wait.h>
# include<sys/types.h>
#include<signal.h>
void childHandle(int sig)
{
    pid_t pid;
    if(sig==SIGCHILD)
        pid = wait(NULL);
    printf("child process %d is over\n");
}
int main()
{
    signal(SIGCHLD,childHandle);
    int n = 0;
    char *s = NULL;
    pid_t pid = fork();
    assert(pid>=0);
    if(pid==0)
    {
        n=5;
        s="child";
    }
    else
    {
        n=15;
        s="parent";

    }
    int i=0;
    for(;i<n;++i)
    {
        printf("s=%s\n",s);
        sleep(1);
    }
    if(pid)
        printf("parent end\n");
    
    else
        printf("child end\n");
        
    exit(0);
}

程序打印结果如下:

可以看到在子进程结束之后,signal函数捕捉了SIGCHLD信号,并且对信号进行了childHandle处理操作。当然,处理函数中的wait方法的地方也可以使用waitpid的方法。

4.两次fork

首先我们了解一下孤儿进程,孤儿进程正好和僵尸进程相反,孤儿进程是父进程先于子进程结束,这时,子进程就会成为一个孤儿进程,孤儿进程会立即被init收养,完成善后操作,期间不需要人工的参与。

因此两次fork解决僵尸进程的思路如下:

当父进程fork以后,产生一个子进程,在子进程中再进行fork,会产生一个孙子进程,我们可以让子进程立即退出,然后在父进程里面wait保证子进程不会成为僵尸进程,然后我们的孙子进程失去了父亲,变成了一个孤儿进程,被init收养,init会处理孙子进程的善后操作。我们要执行的代码就可以放进孙子进程里面了。

按理来说,这种方法比上述三种方法都要繁琐,但是这种方法在服务器中经常会被用到。这是因为前几种方法在子进程结束的时候都会对父进程造成消耗。而两次fork的方法,只有在连接的时候造成消耗(子进程立即退出,和连接几乎同时发生),而后面的孙子进程完全脱离了服务器进程,不会再对服务器有任何负担。

另外:子进程的善后操作的处理方式为以上三种操作,甚至也可以两次fork,只要你不嫌麻烦。

代码实现:

#include <stdio.h>
# include<stdlib.h>
# include<string.h>
# include<unistd.h>
# include<assert.h>
# include<sys/wait.h>
# include<sys/types.h>
#include<signal.h>
void child_handle(int sig)
{
    pid_t pid;
    if(sig==SIGCHLD)
        pid = wait(NULL);
    printf("child process %d is over\n",pid);
}
int main()
{
    signal(SIGCHLD,child_handle);
    int n = 0;
    char *s = NULL;
    pid_t pid = fork();
    assert(pid>=0);
    if(pid==0)
    {
        pid_t son = fork();
        if(son<0)
            perror("fork error");
        if(son>0)
        {
            exit(0);
        }
        n=5;
        s="son";
    }
    else
    {
        n=15;
        s="parent";

    }
    int i=0;
    for(;i<n;++i)
    {
        printf("s=%s\n",s);
        sleep(1);
    }
    printf("process %d is over\n",getpid());
        
    exit(0);
}

执行结果:

可以看到父进程pid为6248,子进程pid为6249,孙子进程oid为6250,当孙子进程结束以后(父进程结束之前)ps一下:

从上图可以发现,并没有僵尸进程的出现。

 

 

Logo

更多推荐