僵尸进程如何产生的

本文详细解释了Linux系统中僵尸进程的概念及产生原因,并提供了避免僵尸进程产生的编程方法。

Linux 系统中僵尸进程和现实中僵尸(虽然我也没见过)类似,虽然已经死了,但是由于没人给它们收尸,还能四处走动。僵尸进程指的是那些虽然已经终止的进程,但仍然保留一些信息,等待其父进程为其收尸。配图源自 Flickr 

僵尸进程如何产生的?

如果一个进程在其终止的时候,自己就回收所有分配给它的资源,系统就不会产生所谓的僵尸进程了。那么我们说一个进程终止之后,还保留哪些信息?为什么终止之后还需要保留这些信息呢?

一个进程终止的方法很多,进程终止后有些信息对于父进程和内核还是很有用的,例如进程的ID号、进程的退出状态、进程运行的CPU时间等。因此进程在终止时,回收所有内核分配给它的内存、关闭它打开的所有文件等等,但是还会保留以上极少的信息,以供父进程使用。父进程可以使用 wait/waitpid 等系统调用来为子进程收拾,做一些收尾工作。

因此,一个僵尸进程产生的过程是:父进程调用fork创建子进程后,子进程运行直至其终止,它立即从内存中移除,但进程描述符仍然保留在内存中(进程描述符占有极少的内存空间)。子进程的状态变成EXIT_ZOMBIE,并且向父进程发送SIGCHLD 信号,父进程此时应该调用 wait() 系统调用来获取子进程的退出状态以及其它的信息。在 wait 调用之后,僵尸进程就完全从内存中移除。因此一个僵尸存在于其终止到父进程调用 wait 等函数这个时间的间隙,一般很快就消失,但如果编程不合理,父进程从不调用 wait 等系统调用来收集僵尸进程,那么这些进程会一直存在内存中。

在 Linux 下,我们可以使用 ps 等命令查看系统中僵尸进程,僵尸进程的状态标记为‘Z’:

产生一个僵尸进程

根据上面的描述,我们很容易去写一个程序来产生僵尸进程,如下代码:

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

int main()
{
    //fork a child process
    pid_t pid = fork();

    if (pid > 0)   //parent process
    {
        printf("in parent process, sleep for one miniute...zZ...\n");
        sleep(60);
        printf("after sleeping, and exit!\n");
    }
    else if (pid == 0)  
    {
        //child process exit, and to be a zombie process
        printf("in child process, and exit!\n");
        exit(0);
    }

    return 0;
}

父进程并没有写 wait 等系统调用函数,因此在子进程退出之后变成僵尸进程,父进程并没有为其去收尸。我们使用下面命令编译运行该进程,然后查看系统中进程状态:

guohailin@guohailin:~/Documents$ gcc zombie.c -o zombie
guohailin@guohailin:~/Documents$ ./zombie 
in parent process, sleep for one miniute...zZ...
in child process, and exit!

# 打开另一个终端:
guohailin@guohailin:~$ ps aux | grep -w 'Z'
1000      2211  1.2  0.0      0     0 ?        Z    13:24   6:53 [chromium-browse] <defunct>
1000      4400  0.0  0.0      0     0 ?        Z    10月16   0:00 [fcitx] <defunct>
1000     10871  0.0  0.0      0     0 pts/4    Z+   22:32   0:00 [zombie] <defunct>

从上面可以看出,系统中多了一个僵尸进程。但如果等父进程睡眠醒来退出之后,我们再次查看系统进程信息,发现刚才的僵尸进程不见了。

guohailin@guohailin:~/Documents$ ./zombie 
in parent process, sleep for one miniute...zZ...
in child process, and exit!
after sleeping, and exit!
guohailin@guohailin:~/Documents$ ps aux | grep -w 'Z'
1000      2211  1.2  0.0      0     0 ?        Z    13:24   6:53 [chromium-browse] <defunct>
1000      4400  0.0  0.0      0     0 ?        Z    10月16   0:00 [fcitx] <defunct>

这是为什么呢?父进程到死都也没有为其子进程收尸呀,怎么父进程退出之后,那个僵尸进程就消失了呢?难道父进程在退出时会为子进程收拾吗?其实不然....真正的原因是:父进程死掉之后,其所有子进程过继给 init 进程,init 进程成为该僵尸进程的新进程,init 进程会周期性地去调用 wait 系统调用来清除它的僵尸孩子。因此,你会发现上面例子中父进程死掉之后,僵尸进程也跟着消失,其实是 init 进程为其收尸的!

怎样避免僵尸进程的产生

不能使用 kill 后接 SIGKILL 信号这样的命令像杀死普通进程一样杀死僵尸进程,因为僵尸进程是已经死掉的进程,它不能再接收任何信号。事实上,如果系统中僵尸进程并不多的话,我们也无需去消除它们,少数的僵尸进程并不会对系统的性能有什么影响。

那么在编程时,如果能避免系统中大量产生僵尸进程呢?根据上面描述的,子进程在终止时会向父进程发 SIGCHLD 信号,Linux 默认是忽略该信号的,我们可以显示安装该信号,在信号处理函数中调用 wait 等函数来为其收尸,这样就能避免僵尸进程长期存在于系统中了。示例代码如下:

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

sig_atomic_t child_exit_status;

void clean_up_child_process(int signal_num)
{
    /* clean up child process */
    int status;
    wait (&status);

    /* store its exit status in a global variable */
    child_exit_status = status;
}

int main()
{
    /* handle SIGCHLD by calling clean_up_child_process  */
    struct sigaction sigchild_action;
    memset(&sigchild_action, 0, sizeof(sigchild_action));
    sigchild_action.sa_handler = &clean_up_child_process;
    sigaction(SIGCHLD, &sigchild_action, NULL);

    /* fork a child, and let the child process dies before parent */
    pid_t c_pid;
    c_pid = fork();
    if (c_pid > 0)
    {
        printf("in parent process, and sleep for on mininute...zZ...\n");
        sleep(60);
    }
    else if(c_pid == 0)
    {
        printf("in child process, and exit now\n");
        exit(0);
    }
    else
    {
        printf("fork failed!\n");
    }

    return 0;
}   

参考资料

<think>嗯,用户想了解如何避免在Linux中产生僵尸进程的方法。首先,我需要回忆一下僵尸进程的定义和产生原因。根据引用中的内容,僵尸进程是当子进程结束后,父进程没有及时调用wait或waitpid来获取子进程的退出状态,导致子进程的进程描述符仍然留在系统中。这种情况下,虽然子进程已经终止,但资源未被完全释放,形成僵尸进程。 接下来,用户需要的是避免的方法。根据提供的引用,有几个解决方案。第一个方法是父进程显式调用wait或waitpid函数。这需要父进程在子进程结束后主动回收子进程的状态。例如,在代码中,父进程可以在创建子进程后调用wait,这样当子进程结束时,父进程会立即处理,防止僵尸进程产生。不过,这种方法可能会阻塞父进程,特别是如果父进程需要处理多个子进程的话,可能需要使用非阻塞的方式或者信号处理。 第二个方法是使用信号处理机制。当子进程结束时,系统会向父进程发送SIGCHLD信号。父进程可以注册一个处理函数,在这个函数中调用wait或waitpid来回收子进程。这样父进程不需要主动轮询,而是通过异步的方式处理子进程的退出。引用3中提到,如果父进程没有处理这个信号,子进程就会保持僵尸状态。需要注意的是,在处理SIGCHLD信号时,通常要循环调用waitpid直到没有更多的子进程退出,因为多个子进程可能同时结束,而信号可能会合并。 第三个方法是让父进程忽略SIGCHLD信号。根据引用3,如果父进程显式地忽略SIGCHLD信号,系统会自动清理子进程,不会让其成为僵尸进程。这可以通过signal(SIGCHLD, SIG_IGN)来实现。不过需要注意,这种方法可能在某些系统上有不同的行为,比如传统的System V系统可能不支持,而现代的BSD和Linux系统通常支持。 第四种方法是让父进程成为孤儿进程的父进程,即init进程。当父进程在子进程之前结束,子进程会成为孤儿进程,被init进程接管。init进程会定期调用wait来回收这些子进程,避免僵尸进程。这通常是通过两次fork来实现的,第一次fork后父进程立即退出,让子进程由init接管,这样原来的子进程由init来负责回收。 现在需要把这些方法整理成结构清晰的回答,按照用户的要求,分点说明,并引用对应的引用来源。同时,用户可能需要具体的代码示例,比如在引用4中给出的例子,父进程没有调用wait,导致子进程成为僵尸进程。正确的做法是在父进程中调用wait或使用信号处理。 另外,用户可能关心这些方法的适用场景和注意事项。例如,使用wait可能会阻塞父进程,而信号处理需要注意可重入性,忽略信号可能在不同系统中的兼容性问题。此外,两次fork的方法适用于需要长时间运行的父进程,避免自己无法及时回收子进程的情况。 需要确保回答中提到的每种方法都有对应的引用支持,例如引用4中的代码示例展示了未处理的情况,而正确的代码应该加入wait调用。同时,引用3中提到了信号处理和忽略信号的方法,引用2中提到了孤儿进程被init接管的情况。 总结起来,避免僵尸进程的方法包括: 1. 父进程主动调用wait/waitpid 2. 注册SIGCHLD信号处理函数 3. 忽略SIGCHLD信号 4. 利用孤儿进程机制 每种方法都需要详细说明,并引用相关段落作为支持。此外,可能需要给出具体的代码示例或命令行操作,比如使用ps命令查找僵尸进程,或者修改代码加入信号处理逻辑。 最后,生成相关问题,如僵尸进程的危害、检测方法等,但根据用户提供的引用,可能的问题包括如何检测僵尸进程、SIGCHLD信号的作用、孤儿进程的处理等。</think>### 避免僵尸进程产生的方法 #### 1. **父进程主动调用`wait`/`waitpid`** 父进程通过调用`wait`或`waitpid`系统调用,主动回收子进程的退出状态信息。这是最直接的方法。 **示例代码**(引用自[^4]): ```cpp #include <iostream> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> using namespace std; int main() { pid_t pid = fork(); if (pid == 0) { // 子进程 exit(0); } if (pid > 0) { // 父进程 wait(nullptr); // 添加wait调用 cout << "Child process " << pid << " has been reaped." << endl; } } ``` **关键点**: - `wait`会阻塞父进程,直到子进程退出;`waitpid`可通过参数`WNOHANG`实现非阻塞轮询。 - 若父进程有多个子进程,需循环调用`wait`或`waitpid`[^2][^4]。 --- #### 2. **注册`SIGCHLD`信号处理函数** 当子进程终止时,系统会向父进程发送`SIGCHLD`信号。父进程可通过注册信号处理函数自动回收子进程。 **示例代码**: ```cpp #include <signal.h> #include <sys/wait.h> void sigchld_handler(int sig) { while (waitpid(-1, nullptr, WNOHANG) > 0); // 循环回收所有终止的子进程 } int main() { signal(SIGCHLD, sigchld_handler); // 注册信号处理函数 // 后续fork子进程逻辑... } ``` **关键点**: - 需在信号处理函数中循环调用`waitpid`,防止多个子进程同时退出时信号丢失[^3]。 - 避免在信号处理函数中使用非异步安全函数(如`printf`)[^3]。 --- #### 3. **显式忽略`SIGCHLD`信号** 通过设置`SIGCHLD`信号为`SIG_IGN`,系统会自动回收子进程,无需父进程干预。 **示例代码**: ```cpp #include <signal.h> int main() { signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD信号 // 后续fork子进程逻辑... } ``` **关键点**: - 此方法仅适用于现代Linux系统(BSD兼容),传统System V系统可能不生效[^3]。 - 需注意信号处理设置的时机(在`fork`之前)[^3]。 --- #### 4. **利用孤儿进程机制** 通过两次`fork`,使子进程成为孤儿进程,最终由`init`进程(PID=1)接管并回收。 **示例代码**: ```cpp #include <unistd.h> int main() { pid_t pid = fork(); if (pid == 0) { // 第一次fork的子进程 pid_t grandchild_pid = fork(); if (grandchild_pid == 0) { // 第二次fork的孙进程 // 实际任务逻辑... } else { exit(0); // 第一次子进程立即退出,孙进程由init接管 } } else { wait(nullptr); // 父进程回收第一次子进程 } } ``` **关键点**: - `init`进程会定期调用`wait`回收所有孤儿进程[^3]。 --- ### 检测僵尸进程的命令 通过`ps`命令查找状态为`Z`的进程: ```bash ps aux | grep 'Z' ``` ### 总结 | 方法 | 适用场景 | 注意事项 | |--------------------|----------------------------|--------------------------| | 主动调用`wait` | 简单单子进程场景 | 阻塞父进程 | | 信号处理 | 多子进程异步回收 | 需处理信号合并问题 | | 忽略`SIGCHLD` | 现代Linux系统 | 兼容性问题 | | 孤儿进程机制 | 长时间运行父进程 | 需两次`fork` | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值