Linux僵尸进程以及wait函数

僵尸进程就是已经结束的进程(几乎不占计算机资源),但是它并没有从进程列表中删除。僵尸进程太多会导致操作系统的进程数目过多,从而占满了OS的进程表。进而导致无法创建新进程,致使OS崩溃。

僵尸进程几乎不占资源,它没有可执行代码,也不能被调度,但是它占据着进程表中的一个位置,记载着该进程的PCB信息。它需要等待他的父进程来终结它。一旦它的父进程是一个循环,不会结束(父进程不去调用wait函数或者waitpid函数)。那么子进程将会一直保持僵尸状态。那么它将一直占用进程号,系统就没法回收利用。

在Linux下使用top命令可以查看当前进程数目,以及进程的状态。例如:

可以看到我的系统暂时并没有僵尸进程(zombie) 。挂起的进程倒是一大堆。

僵尸进程产生的原因:每个Linux进程在进程表中都有一个进入点,内核执行该进程时,使用到的一切信息都存入在进程点。我们可以使用ps命令来查看进程状态。当一个父进程以fork()系统调用建立一个新的子进程后,内核就会在进程表中给这个子进程分配一个进入点,然后将相关信息存储在该进入点所对应的进程表内。这些信息中有一项是其父进程的识别码。而当这个子进程结束的时候(调用exit命令结束),其实他并没有真正的被销毁,而是留下一个僵尸进程的。此时原来进程表中的数据会被该进程的退出码(exit code)、执行时所用的CPU时间等数据所取代,这些数据会一直保留到系统将它传递给它的父进程为止。

我们用下面的代码来产生僵尸进程

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

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

    if (0 == pid)
    {
        sleep(3);
        printf("I am son,my pid is:%d\n",getpid());
        _exit(0);
    }
    else if(0 < pid)
    {
        while (1)
        {
            sleep(1);
            printf("I am parent!\n");
        }
    }
    else 
    {
        perror("fork fail");
        _exit(1);
    }
    
    return 0;
}

运行结果如下:

当子进程打印完I am son之后,子进程结束,并且使用了_exit进行了退出。同时我们另开一个终端,输入top命令,将会看有一个僵尸进程存在。

如何避免僵尸进程:

  1. 可以在父进程中通过调用wait()和waitpid函数等待子进程结束,但是这会导致父进程挂起。
  2. 父进程不能挂起,父进程要做的工作很多,很忙。那么可以使用signal函数为信号SIGCHLD注册处理函数。当子进程结束之后会发出SIGCHLD信号,然后父进程会收到该信号,可以在信号处理函数中使用wait函数来回收子进程。
  3. 如果父进程不关心子进程什么时候结束(比如fork后使用了execl函数启动了另外一个可执行程序),那么可以使用single(SIGCHLD,SIG_IGN)通知内核来回收子进程。
  4. fork两次,首先父进程fork一个子进程,然后继续工作,子进程fork一个孙子进程后退出,那么孙子进程将会变成孤儿进程(因为他父亲死了,这就是孤儿),从而被init进程接管。但是子进程的回收仍旧需要父进程来做,好处是不用使用wait()来挂起了,父进程可以忙自己的。

使用wait函数和waitpid函数。

wait函数:需要头文件#include<sys/wait.h>

函数原型:pid_t wait(int *status);

函数功能:阻塞(睡眠)进程,等待子进程结束,负责为子进程回收资源。参数是接收子进程退出状态,返回值是子进程的PID,出错为-1。

如果父进程有多个子进程,那么当其中某一个子进程终止的时候,wait函数就会立即回收该子进程,并且返回。如果父进程关心子进程的终止状态(正常结束还是异常结束),那么需要传递一个参数来获取状态,如果不关心,可以传递参数NULL。

系统提供了下面的宏来取得子进程退出状态,信号编号等信息。

我们经常使用的就是前4个宏。下面的例子指出了如何使用带参数的wait函数。

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

int main()
{
    pid_t pid1,pid2;
    int statu,m = -1;
    pid1 = fork();
    if(0 == pid1)
    {
        printf("This is son! it's PID %d\n",getpid());
        exit(3);                    //结束进程,此处返回一个特殊的值3
    }
    if(0 < pid1)
    {
        pid2 = wait(&statu);        //父进程等待子进程执行,子进程结束后的状态保存在statu里
        if (WIFEXITED(statu))       //如果为真,子进程正常结束,提取状态
        {
            m = WEXITSTATUS(statu);
        }
        printf("This is father!\n");
        printf("son exit code is %d\n",m);
        printf("son PID is %d\n",pid2);
    }
    return 0;
}

执行结果如下:

waitpid函数和wait的不同之处在于,waitpid函数多了两个参数,使我们能控制等待的进程,以及是否等待子进程。

函数原型:pid_t waitpid(pid_t pid, int *status, int options);

函数功能:pid是控制等待的进程,status和wait中的意义一样,options参数一般用于控制父进程是否等待。

pid == -1 :等待任何子进程,相当于wait函数。

pid == 0:等待同一个进程组中的任何子进程(如果子进程已经加入了别的进程组,waitpid 不会等待它)。

pid > 0:等待任何子进程识别码为pid的子进程

pid < -1:等待进程组识别码为pid绝对值的任何子进程

options:它的取值组合由系统预定义的。可以为0和一些宏的或。当他为0时,和wait()一样,阻塞父进程,等待子进程退出。当他取值为WNOHANG时,如果没有已经结束的子进程则马上返回,返回值为0。最常用的就是这两个。

将上面代码中的

pid2 = wait(&statu);

替换为下面这句代码

pid2 = waitpid(pid1,&statu,WNOHANG);

运行结果将会发生变化:

显而易见,父进程没有等待子进程,直接执行,打印父进程中代码,打印了m的初始值-1。在代码中设置了选项 WNOHANG,而调用中 waitpid() 发现没有已退出的子进程可等待,返回0。所以取到的子进程的PID是0,说明子进程在运行,还未结束。

——————————————————————————————————————————————————

杀死系统偶尔出现的僵死进程

有时候系统会出现僵尸进程,这个时候,会发现使用kill -9已经无法杀死它。此时,如果想杀死子进程,那么我们需要杀死它的父进程。查看系统是否有僵死进程出现,使用命令top即可。当我们知道系统有僵尸进程出现之后,使用ps aux | grep Z命令来查看处于僵尸状态的进程。

可以看到有一个僵尸进程。我们知道了它的PID以后,使用ps -ostat,ppid命令来查看它的父进程。

有了它的父进程PID以后,我们就可以执行kill -9命令杀死它的父进程来清理系统的僵尸进程了。

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值