有关defunct进程(僵尸进程)的处理原则

本文详细解释了僵尸进程的概念、产生的原因及危害,同时提供了多种避免僵尸进程产生的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

僵尸进程:就是已经结束了的进程,但是没有从进程表中删除。太多了会导致进程表里面条目满了,进而导致系统崩溃,倒是不占用其他系统资源。

僵尸进程的查看:
ps -ef
出现:
root     13028 12956 0 10:51 pts/2    00:00:00 [ls] <defunct>
最后有defunct的标记,就表明是僵尸进程。

产生的原因:
每个Linux进程在进程表里都有一个进入点(entry),核心程序执行该进程时使用到的一切信息都存储在进入点。当用ps命令察看系统中的进程信息 时,看到的就是进程表中的相关数据。当以fork()系统调用建立一个新的进程后,核心进程就会在进程表中给这个新进程分配一个进入点,然后将相关信息存 储在该进入点所对应的进程表内。这些信息中有一项是其父进程的识别码。当这个进程走完了自己的生命周期后,它会执行exit()系统调用,此时原来进程表 中的数据会被该进程的退出码(exit code)、执行时所用的CPU时间等数据所取代,这些数据会一直保留到系统将它传递给它的父进程为止。由此可见,defunct进程的出现时间是在子进 程终止后,但是父进程尚未读取这些数据之前。

产生的例程:
#include "sys/types.h"
#include "sys/wait.h"

#include "stdio.h"
#include "unistd.h"

int main(int argc, char* argv[])
{
while(1)
{
    pid_t chi = fork();
    if(chi == 0)
    {
      execl("/bin/ls",NULL);
    }

    sleep(2);
}
}

对付这种的方法:
外部的方法:
执行pstree,看到:
     |-sshd-+-sshd---bash
     |      |-sshd---sftp-server
     |      `-sshd-+-bash---vi
     |             |-bash---myfork---2*[ls]
也就是主进程是myfork
将myfork干掉就可以了

编程的方法:
1、主进程等待子进程
#include "sys/types.h"
#include "sys/wait.h"

#include "stdio.h"
#include "unistd.h"

int main(int argc, char* argv[])
{
while(1)
{
    pid_t chi = fork();
    if(chi == 0)
    {
      execl("/bin/ls",NULL);
    }

    int status;
    wait(&status);
    sleep(2);
}
}

也就是加上了wait

2、在主进程里面补充对child 退出的signal的处理:
void sig_child(int signo)

{

pid_t   pid;

int   stat,i;

while((pid = waitpid(-1, &stat, WNOHANG)) > 0);

return;

} 

signal(SIGCHLD,sig_child);
 
 
在测试基于 DirectFB+Gstreamer 的视频联播系统的一个 Demo 的时候,其中大量使用 system 调用的语句,例如在 menu 代码中的 system("./play") ,而且多次执行,这种情况下,在 ps -ef 列表中出现了大量的 defunct 进程,对程序的运行时有害的。按说system的源码中应该已经包含了wait,但也不能排除开发板上这个版本的system中可能没有wait,总之,开发板上在调用system后添加wait之后,defunct进程不复存在了。

下面谈谈 defunct 进程,中文翻译叫僵尸进程。下文整理于网络以及APUE一书。

一、什么是僵尸进程

在UNIX 系统中,一个进程结束了,但是他的父进程没有等待(调用wait / waitpid)他,那么他将变成一个僵尸进程。当用ps命令观察进程的执行状态时,看到这些进程的状态栏为defunct。僵尸进程是一个早已死亡的进程,但在进程表(processs table)中仍占了一个位置(slot)。

但是如果该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程。因为每个进程结束的时候,系统都会扫描当前系统中所运行的所有进程,看看有没有哪个进程是刚刚结束的这个进程的子进程,如果是的话,就由Init进程来接管他,成为他的父进程,从而保证每个进程都会有一个父进程。而Init进程会自动wait其子进程,因此被Init接管的所有进程都不会变成僵尸进程。

二、UNIX下进程的运作方式

每个Unix进程在进程表里都有一个进入点(entry),核心进程执行该进程时使用到的一切信息都存储在进入点。当用 ps 命令察看系统中的进程信息时,看到的就是进程表中的相关数据。当以fork()系统调用建立一个新的进程后,核心进程就会在进程表中给这个新进程分配一个进入点,然后将相关信息存储在该进入点所对应的进程表内。这些信息中有一项是其父进程的识别码。

子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。那么会不会因为父进程太忙来不及 wait 子进程,或者说不知道子进程什么时候结束,而丢失子进程结束时的状态信息呢?

不会。因为UNIX提供了一种机制可以保证,只要父进程想知道子进程结束时的状态信息,就可以得到。这种机制就是:当子进程走完了自己的生命周期后,它会执行exit()系统调用,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出码exit code,退出状态the terminationstatus of the process,运行时间the amount of CPU time taken by the process等),这些数据会一直保留到系统将它传递给它的父进程为止,直到父进程通过wait / waitpid来取时才释放。

也就是说,当一个进程死亡时,它并不是完全的消失了。进程终止,它不再运行,但是还有一些残留的数据等待父进程收回。当父进程 fork() 一个子进程后,它必须用 wait() (或者 waitpid())等待子进程退出。正是这个 wait() 动作来让子进程的残留数据消失。

三、僵尸进程的危害

如果父进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统的进程表容量是有限的,所能使用的进程号也是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。


所以,defunct进程不仅占用系统的内存资源,影响系统的性能,而且如果其数目太多,还会导致系统瘫痪。而且,
由于调度程序无法选中Defunct 进程,所以不能用kill命令删除Defunct 进程,惟一的方法只有重启系统。

四、僵尸进程的产生

如果子进程死亡时父进程没有 wait(),通常用 ps 可以看到它被显示为“<defunct>”,这样就产生了僵尸进程。它将永远保持这样直到父进程 wait()。

由此可见,defunct进程的出现时间是在子进程终止后,但是父进程尚未读取这些数据之前。利用这一点我们可以用下面的程序建立一个defunct 进程:
C代码 复制代码  收藏代码
  1. #include <stdio.h>   
  2.   
  3. #include<sys/types.h>   
  4.   
  5. main()   
  6. {   
  7.   
  8.     if(!fork())   
  9.     {   
  10.   
  11.         printf(“child pid=%d\n”, getpid());   
  12.   
  13.         exit(0);   
  14.   
  15.     }   
  16.   
  17.     sleep(20);   
  18.   
  19.     printf(“parent pid=%d \n”, getpid());   
  20.   
  21.     exit(0);   
  22.   
  23. }  
 

当上述程序以后台的方式执行时,第17行强迫程序睡眠20秒,让用户有时间输入ps -e指令,观察进程的状态,我们看到进程表中出现了defunct进程。当父进程执行终止后,再用ps -e命令观察时,我们会发现defunct进程也随之消失。这是因为父进程终止后,init 进程会接管父进程留下的这些“孤儿进程”(orphan process),而这些“孤儿进程”执行完后,它在进程表中的进入点将被删除。如果一个程序设计上有缺陷,就可能导致某个进程的父进程一直处于睡眠状态或是陷入死循环,父进程没有wait子进程,也没有终止以使Init接管,该子进程执行结束后就变成了defunct进程,这个defunct 进程可能会一直留在系统中直到系统重新启动。

 

 

在看一个产生僵尸进程的例子。

子进程要执行的程序test_prog

 

 

C代码 复制代码  收藏代码
  1. //test.c   
  2. #include <stdio.h>   
  3. int main()   
  4. {   
  5.         int i = 0;   
  6.         for (i = 0 ; i < 10; i++)   
  7.         {   
  8.                 printf ("child time %d\n", i+1);   
  9.                 sleep (1);   
  10.         }   
  11.         return 0;   
  12. }  
 

 

 

父进程father的代码father.c

 

 

C代码 复制代码  收藏代码
  1. #include <stdio.h>   
  2. #include <unistd.h>   
  3. #include <sys/types.h>   
  4. #include <sys/wait.h>   
  5. int main()   
  6. {   
  7.         int pid = fork ();   
  8.         if (pid == 0)   
  9.         {   
  10.                 system ("./test_prog");   
  11.                 _exit (0);   
  12.         }else  
  13.         {   
  14.                 int i = 0;   
  15.                 /*  
  16.                                 int status = 0;  
  17.                 while (!waitpid(pid, &status, WNOHANG))  
  18.                 {  
  19.                         printf ("father waiting%d\n", ++i);  
  20.                         sleep (1);  
  21.                 }*/  
  22.                 while (1)   
  23.                 {   
  24.                         printf ("father waiting over%d\n", ++i);   
  25.                         sleep (1);   
  26.                 }   
  27.                 return 0;   
  28.         }   
  29.   
  30. }  
 

 

 

执行./father,当子进程退出后,由于父进程没有对它的退出进行关注,会出现僵尸进程

 

 

C代码 复制代码  收藏代码
  1. 20786 pts/0    00:00:00 father   
  2. 20787 pts/0    00:00:00 father <defunct>  

 

    总结:子进程成为 defunct 直到父进程 wait(),除非父进程忽略了 SIGCLD 。更进一步,父进程没有 wait() 就消亡(仍假设父进程没有忽略 SIGCLD )的子进程(活动的或者 defunct)成为 init 的子进程,init 着手处理它们。


五、如何避免僵尸进程

 

1、父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起。

 

在上个例子中,如果我们略作修改,在第8行sleep()系统调用前执行wait()或waitpid()系统调用,则子进程在终止后会立即把它在进程表中的数据返回给父进程,此时系统会立即删除该进入点。在这种情形下就不会产生defunct进程。


2. 如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler。在子进程结束后,父进程会收到该信号,可以在handler中调用wait回收。

 

3. 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCLD, SIG_IGN)或signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号 


4. fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要自己做。 下面就是Stevens给的采用两次folk避免僵尸进程的示例:

 

 

C代码 复制代码  收藏代码
  1. #include "apue.h"   
  2. #include <sys/wait.h>   
  3.   
  4. int  
  5. main(void)   
  6. ...{   
  7.      pid_t    pid;   
  8.   
  9.     if ((pid = fork()) < 0) ...{   
  10.          err_sys("fork error");   
  11.      } else if (pid == 0) ...{     /**//* first child */  
  12.         if ((pid = fork()) < 0)   
  13.              err_sys("fork error");   
  14.         else if (pid > 0)   
  15.              exit(0);    /**//* parent from second fork == first child */  
  16.         /**//*  
  17.           * We're the second child; our parent becomes init as soon  
  18.           * as our real parent calls exit() in the statement above.  
  19.           * Here's where we'd continue executing, knowing that when  
  20.           * we're done, init will reap our status.  
  21.          */  
  22.          sleep(2);   
  23.          printf("second child, parent pid = %d ", getppid());   
  24.          exit(0);   
  25.      }   
  26.        
  27.     if (waitpid(pid, NULL, 0) != pid)  /**//* wait for first child */  
  28.          err_sys("waitpid error");   
  29.   
  30.     /**//*  
  31.       * We're the parent (the original process); we continue executing,  
  32.       * knowing that we're not the parent of the second child.  
  33.      */  
  34.      exit(0);   
  35. }  
 
<think>嗯,用户想了解defunct进程的存在时间及处理机制。这个问题涉及Linux进程管理的底层原理,需要从操作系统层面解释清楚。 用户可能正在排查服务器性能问题,或者发现系统中有大量defunct进程感到困惑。从问题表述看,用户对Linux进程管理有一定基础认知,但需要更深入的机制说明。 参考提供的两段引用材料:引用[1]说明父进程为init的defunct进程可能存在1-2分钟是正常的,特别在复杂shell脚本中常见;引用[2]则解释了defunct进程本质是已终止但未被父进程回收的进程尸体。 需要重点解释三个核心机制: 1. 进程终止时的状态转换流程,特别是Zombie状态的存在意义 2. 父进程通过wait()系统调用回收子进程的机制 3. 当父进程异常时init进程接管孤儿进程的特殊处理 对于存在时间这个关键点,要区分两种情况:普通父进程可能立即回收,而init进程回收会有延迟。可以举例说明:比如正常退出的子进程,如果父进程主动调用waitpid()就能立即回收;但如果父进程崩溃,子进程被init接管后,init默认每5分钟批量回收一次。 处理方案需要分层说明:常规处理如SIGCHLD信号处理,异常处理如重启父进程,极端情况下的手动杀进程方法。特别要警告直接杀defunct进程的风险——可能导致资源泄漏。 最后生成的延伸问题要覆盖应用场景(如高并发服务)、排查工具(ps/pstree组合使用)、以及和类似状态(如D状态)的对比分析,这样能帮助用户建立完整的知识框架。</think>关于defunct进程僵尸进程)的存在时间及处理机制,以下是基于Linux进程管理原理的详细说明: ### 一、defunct进程的存在时间 1. **存在周期** defunct进程本质是**已终止但未被父进程回收**的进程尸体。其存在时间取决于: - **父进程回收时机**:父进程需调用`wait()`或`waitpid()`系统调用读取子进程退出状态 - **最长存在时间**:理论上可**无限期存在**,直到: ```mermaid graph LR A[父进程调用wait] --> B[立即回收] C[父进程崩溃] --> D[init进程接管] D --> E[init定期回收] ``` 2. **特殊场景(父进程为init)** 当父进程ID=1(init/systemd)时: - 存在1-2分钟属正常现象(引用[^1]) - init进程会**周期性批量回收**僵尸进程(通常5分钟/次) ### 二、处理机制与解决方案 #### 常规处理 1. **等待父进程回收** - 父进程正常调用`wait()`后立即消失 - 编程规范示例: ```c pid_t pid = fork(); if (pid == 0) { // 子进程执行代码 exit(0); } else { waitpid(pid, &status, 0); // 必须调用 } ``` 2. **重启父进程** - 重启父进程会触发其退出前回收所有子进程 - 操作命令:`systemctl restart <service>` #### 异常处理 当父进程无法回收时: 1. **手动发送`SIGCHLD`信号** ```bash kill -s SIGCHLD <parent_pid> # 通知父进程回收 ``` 2. **终止父进程**(极端情况) - init会自动接管并回收其僵尸子进程 ```bash kill -9 <parent_pid> ``` > ⚠️ **严禁直接杀僵尸进程**:`kill -9`对defunct进程无效(因其已死亡),强行删除可能导致资源泄漏。 ### 三、关键机制图解 ```mermaid graph TB subgraph 进程生命周期 A[子进程终止] --> B[进入Zombie状态] B -->|父进程调用wait| C[资源完全释放] B -->|父进程处理| D[持续存在] end subgraph init进程 E[父进程崩溃] --> F[子进程被init接管] F -->|定时扫描| G[回收僵尸进程] end ``` ### 四、最佳实践 1. **开发层面** - 父进程必须实现`SIGCHLD`信号处理程序 - 多进程服务使用`epoll_wait()`等时需处理僵尸进程 2. **运维层面** - 监控命令组合: ```bash ps aux | grep 'defunct' # 定位僵尸进程 pstree -p -s <zombie_pid> # 查看进程关系链 ``` - 高并发场景需调整`init`回收频率(修改`/etc/init`配置) > 引用说明:僵尸进程的持续存在是因内核保留退出状态供父进程查询[^2],而init进程的定期回收机制解释了其短暂存留现象[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值