在使用c/c++开发过程中经常会用到多进程,需要fork一些子进程,但是如果不注意的话,就有可能导致子进程结束后变成了僵尸进程。从而逐渐耗尽系统资源。

什么是僵尸进程

如果父进程在子进程之前终止,则所有的子进程的父进程都会改变为init进程,我们称这些进程由init进程领养。这时使用ps命令查看后可以看到子进程的父进程ppid已经变为了1。

而当子进程在父进程之前终止时,内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态、以及该进程使用的CPU时间总量。其他的进程所使用的存储区,打开的文件都会被内核释放。

一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息,释放它仍占用的资源)的进程被称为僵尸进程。 ps命令将僵尸进程的状态打印为Z。

可以设想一下,比如一个web服务器端,假如每次接收到一个连接都创建一个子进程去处理,处理完毕后结束子进程。假如在父进程中没有使用wait或waitpid函数进行善后,这些子进程将全部变为僵尸进程,Linux系统的进程数一般有一个固定限制值,僵尸进程将会逐渐耗尽系统资源。

查看僵尸进程的例子

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
 
int main(int argc, char **argv)
{
    pid_t pid;
    for (int i=0; i<5; i++) {
        if ((pid = fork()) < 0) {
            printf("fork error,%s\n", strerror(errno));
            return -1;
        }
        
        /* child */
        if (pid == 0) {
            sleep(1);
            exit(0);
        }
    }  
    /* parent */
    sleep(20);
    return 0;
}

编译完成后,在执行程序的命令后加上 “&” 符号,表示让当前程序在后台运行。

之后输入

ps –e –o pid,ppid,stat,command|grep [程序名]

可以看到如下的结果

2915  1961 S    ./dd
2917  2915 Z    [dd] <defunct>
2918  2915 Z    [dd] <defunct>
2919  2915 Z    [dd] <defunct>
2920  2915 Z    [dd] <defunct>
2921  2915 Z    [dd] <defunct>

可以看到5个子进程都已经是僵尸进程了。

SIGCHLD信号和处理僵尸进程

当子进程终止时,内核就会向它的父进程发送一个SIGCHLD信号,父进程可以选择忽略该信号,也可以提供一个接收到信号以后的处理函数。对于这种信号的系统默认动作是忽略它。

我们不希望有过多的僵尸进程产生,所以当父进程接收到SIGCHLD信号后就应该调用 wait 或 waitpid 函数对子进程进行善后处理,释放子进程占用的资源。

下面是一个捕获SIGCHLD信号以后使用wait函数进行处理的简单例子:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
 
void deal_child(int sig_no)
{
    wait(NULL);
}
 
int main(int argc, char **argv)
{
    signal(SIGCHLD, deal_child);
 
    pid_t pid;
    for (int i=0; i<5; i++) {
        if ((pid = fork()) < 0) {
            printf("fork error,%s\n",strerror(errno));
            return -1;
        }  
 
        /* child */
        if (pid == 0) {
            sleep(1);
            exit(0);
        }  
    }  
    /* parent */
    for(int i=0; i<100000; i++) {
        for (int j=0; j<100000; j++) {
            int temp = 0;
        }  
    }  
    return 0;
}

同样在后台运行后使用ps命令查看进程状态,结果如下:

6622  1961 R    ./dd
6627  6622 Z    [dd] <defunct>
6628  6622 Z    [dd] <defunct>

发现创建的5个进程,有3个已经被彻底销毁,但是还有2个仍然处于僵尸进程的状态。

这是因为当5个进程同时终止的时候,内核都会向父进程发送SIGCHLD信号,而父进程此时有可能仍然处于信号处理的deal_child函数中,那么在处理完之前,中间接收到的SIGCHLD信号就会丢失,内核并没有使用队列等方式来存储同一种信号。

正确地处理僵尸进程的方法

为了解决上面出现的这种问题,我们需要使用waitpid函数。

函数原型

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

若成功则返回进程ID,如果设置为非阻塞方式,返回0表示子进程状态未改变,出错时返回-1。

options参数可以设置为WNOHANG常量,表示waitpid不阻塞,如果由pid指定的子进程不是立即可用的,则立即返回0。

只需要修改一下SIGCHLD信号的处理函数即可:

void deal_child(int sig_no)
{
    for (;;) {
        if (waitpid(-1, NULL, WNOHANG) == 0)
            break;
    }  
}

再次执行程序后使用ps命令查看,发现已经不会产生僵尸进程了。