Linux 僵死进程及其处理方法

时间:2024-05-31 14:38:26
  • 什么是僵尸进程?

        首先内核会释放终止进程(调用了exit系统调用)所使用的所有存储区,关闭所有打开的文件等,但内核为每一个终止子进程保存了一定量的信息。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。

        而僵尸进程就是指:一个进程执行了exit系统调用退出,而其父进程并没有为它收尸(调用wait或waitpid来获得它的结束状态)的进程。

        任何一个子进程(init除外)在exit后并非马上就消失,而是留下一个称外僵尸进程的数据结构,等待父进程处理。这是每个子进程都必需经历的阶段。另外子进程退出的时候会向其父进程发送一个SIGCHLD信号。

 

  • 僵尸进程的目的?

          设置僵尸状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)。

 

  • 僵尸进程的危害:
  1. 僵尸进程的PID还占据着,意味着海量的子进程会占据满进程表项,会使后来的进程无法fork.
  2. 僵尸进程的内核栈无法被释放掉,为啥会留着它的内核栈,因为在栈的最低端,有着thread_info结构,它包含着 struct_task 结构,这里面包含着一些退出信息

 

  • 僵死进程与孤儿进程的区别?

回答这个问题很简单, 父进程和子进程终止关系有两种:父进程先于子进程终止和子进程先于父进程终止。

孤儿进程——父进程先于子进程终止

终止进程的子进程的父进程更改为init进程,也就是父进程ID更改为1。当一个进程终止时,内核会检查所有的活动进程,找出正要终止进程的子进程并将其父进程更改为init进程。

僵尸进程———子进程先于父进程终止

内核会为每个终止子进程保留一定量的信息,父进程就可以通过调用wait函数来获取这些信息。如果父进程没有调用wait函数的话,则该资源就会一直被占用。

 

  • 如何避免僵尸进程?

1.wait和waitpid函数:父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出wait会导致父进程阻塞。waitpid可以通过传递WNOHANG使父进程不阻塞立即返回。 

2.sigaction信号处理函数(交给内核处理):如果父进程很忙可以用sigaction注册信号处理函数,在信号处理函数调用wait/waitpid等待子进程退出。(sigaction函数类似于signal函数,而且完全可以代替后者,也更稳定)

3.signal忽略SIGCHLD信号(交给内核处理) :通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收。如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。

4.fork两次:通过两次调用fork。父进程首先调用fork创建一个子进程然后waitpid等待子进程退出,子进程再fork一个孙进程后退出。这样子进程退出后会被父进程等待回收,而对于孙子进程其父进程已经退出所以孙进程成为一个孤儿进程,孤儿进程由init进程接管,孙进程结束后,init会等待回收。

 

第三种方法忽略SIGCHLD信号,这常用于并发服务器的性能的一个技巧因为并发服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源

 

 1 wait()函数   

 #include <sys/types.h> 

 #include <sys/wait.h>

 pid_t wait(int *status);

   进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。 

   参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样:

 pid = wait(NULL);

   如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

  • wait系统调用会使父进程暂停执行,直到它的一个子进程结束为止。
  • 返回的是子进程的PID,它通常是结束的子进程
  • 状态信息允许父进程判定子进程的退出状态,即从子进程的main函数返回的值或子进程中exit语句的退出码。
  • 如果status不是一个空指针,状态信息将被写入它指向的位置

可以上述的一些宏判断子进程的退出情况:

Linux 僵死进程及其处理方法

2 waitpid()函数

#include <sys/types.h> 

#include <sys/wait.h>

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

参数:

status:如果不是空,会把状态信息写到它指向的位置,与wait一样

options:允许改变waitpid的行为,最有用的一个选项是WNOHANG,它的作用是防止waitpid把调用者的执行挂起。

返回值:如果成功返回等待子进程的ID,失败返回-1

对于waitpid的p i d参数的解释与其值有关:

pid==-1 等待任一子进程。此种情况下,waitpid等效于wait。 pid>0 等待进程ID为pid的子进程。 pid==0 等待调用者进程组内的任一子进程。 pid<-1 等待组ID等于|pid|的任一子进程

 

参数options是以下各标志的按位或运算或为0。

常量 说明 WCONTINUED 等待一进城,它以前曾被停止,此后又继续,但状态尚未报告。 WEXITED 等待已退出的进程。 WNOHANG 如无可用的子进程退出状态,立即返回而非阻塞。 WNOWAIT 不破坏子进程退出状态。该子进程退出状态可由后续的wait、waitid或waitpid调用获取。 WSTOPPED 等待一进程,它已经停止,但其状态尚未报告。

 

wait与waitpid区别:

  • 在一个子进程终止前, wait 使其调用者阻塞,而waitpid 有一选择项WNOHANG,可使调用者不阻塞。
  • waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的特定进程。
  • 实际上wait函数是waitpid函数的一个特例。waitpid(-1, &status, 0)

 

示例:

如以下代码会创建100个子进程,但是父进程并未等待它们结束,所以在父进程退出前会有100个僵尸进程。

#include <stdio.h>
#include <unistd.h>

  int main() {

  int i;

  pid_t pid;

  for(i=0; i<100; i++) {

     pid = fork();

     if(pid == 0)

     break;
}

  if(pid>0) {

  printf("press Enter to exit...");

  getchar();

}

return 0;

}

 

其中一个解决方法即是编写一个SIGCHLD信号处理程序来调用wait/waitpid来等待子进程返回。

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

void wait4children(int signo) {

   int status;

   wait(&status);

}

   int main() {

   int i;

   pid_t pid;

   signal(SIGCHLD, wait4children);

   for(i=0; i<100; i++) {

   pid = fork();

   if(pid == 0)

      break;
   }

   if(pid>0) {

      printf("press Enter to exit...");

      getchar();

   }

return 0;

}

但是通过运行程序发现还是会有僵尸进程,而且每次僵尸进程的数量都不定。这是为什么呢?其实主要是因为Linux的信号机制是不排队的,假如在某一时间段多个子进程退出后都会发出SIGCHLD信号,但父进程来不及一个一个地响应,所以最后父进程实际上只执行了一次信号处理函数。但执行一次信号处理函数只等待一个子进程退出,所以最后会有一些子进程依然是僵尸进程。

虽然这样但是有一点是明了的,就是收到SIGCHLD必然有子进程退出,而我们可以在信号处理函数里循环调用waitpid函数来等待所有的退出的子进程。至于为什么不用wait,主要原因是在wait在清理完所有僵尸进程后再次等待会阻塞。

 

 

技巧:利用信号处理技术消灭僵尸进程

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

void read_childproc(int sig)

{

   int status;

   pid_t id=waitpid(-1,&status,WNOHANG); //调用waitpid()函数销毁子进程

   if(WIFEXITED(status)){

      printf("Removed proc id: %d \n",id);

      printf("Child send: %d \n",WEXITSTATUS(status));

}

}

int main(){

   pid_t pid;

   struct sigaction act;

   act.sa_handler=read_childproc;

   sigemptyset(&act.sa_mask);

   act.sa_flags=0;

   sigaction(SIGCHLD,&act,0); //注册信号,当子进程终止时产生SIGCHILD信号

   pid=fork();

   if(pid==0) {    //子进程1

   puts("Hi! I'm child one process");

   sleep(10);

   return 12;

   }

   else

  {

   printf("Child proc id: %d \n",pid);

   pid=fork(); //子进程2

   if(pid==0){

      puts("Hi!I'm child two process");

      sleep(10);

      exit(24);
   }

   else

   {

    int i;

    printf("Child proc id: %d \n",pid);

    for(i=0;i<5;++i){

        puts("wait...");

        sleep(5);
}

}

}

return 0;

}

 

部分参考jessica的 http://www.cnblogs.com/wuchanming/p/4020463.html

部分参考尹圣雨《TCP/TP 网络编程》