僵尸(zombie)进程

        什么是僵尸进程?

        僵尸进程是指的父进程已经退出,而该进程dead之后没有进程接受,就成为僵尸进程(zombie)进程。任何进程在退出前(使用exit退出) 都会变成僵尸进程(用于保存进程的状态等信息),然后由init进程接管。如果不及时回收僵尸进程,那么它在系统中就会占用一个进程表项,如果这种僵尸进程过多,最后系统就没有可以用的进程表项,于是也无法再运行其它的程序。


        僵尸进程是怎么产生的?

        一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁, 而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit,它的作用是 使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。

        在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集。除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装 SIGCHLD 信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了, 那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是 为什么系统中有时会有很多的僵尸进程。

  • 如果子进程还没有结束时,父进程就结束了,那么init进程会自动接手这个子进程,进行回收。 

  • 如果父进程是循环,又没有安装 SIGCHLD信号处理函数调用 wait或 waitpid()等待子进程结束。那么子进程结束后,没有回收,就产生僵尸进程了。


        怎么查看僵尸进程?

        利用命令ps,可以看到有父进程ID为1的进程是孤儿进程;s(state)状态为Z的是僵尸进程。

        注意:孤儿进程(orphan process)是尚未终止但已停止(相当于前台挂起)的进程,但其父进程已经终止,由init收养;而僵尸进程则是已终止的进程,其父进程不一定终止。

        示例: fork_zombie.php

<?php
$pid = pcntl_fork();
if($pid == -1){
    exit("fork fail");
}elseif($pid){
    $id = getmypid();   
    echo "Parent process,pid {$id}, child pid {$pid}\n";   
   
    while(1)
    {
        sleep(3);    
    }
    
}else{
    $id = getmypid();   
    echo "Child process,pid {$id}\n";   
    sleep(2);
    exit();
}

        命令行里运行程序,然后新终端查看:

[root@localhost ~]# ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'
Z+     5954   5955 [php] <defunct>

        出现了一个僵尸进程。这时候就算手动结束脚本程序也无法关闭这个僵尸子进程了。需要使用 kill-9关闭


        怎样来清除僵尸进程?

  1. 改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后, 会发送SIGCHLD信号给父进程,父进程收到此信号后,执行 waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用wait,内核也会向它发送SIGCHLD消息,尽管对的默认处理是忽略, 如果想响应这个消息,可以设置一个处理函数。

  2. 把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程,关机或重启后所有僵尸进程都会消失。


        避免Zombie Process的方法

  1. 在SVR4中,如果调用signal或sigset将SIGCHLD的配置设置为忽略,则不会产生僵死子进程。另外,使用SVR4版的 sigaction,则可设置SA_NOCLDWAIT标志以避免子进程僵死。 Linux中也可使用这个,在一个程序的开始调用这个函数signal(SIGCHLD,SIG_IGN)。

  2. 调用fork两次,父进程fork一个子进程,然后继续工作,子进程fork一 个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收 还要自己做。

  3. 用waitpid等待子进程返回。


    

pcntl_signal

bool  pcntl_signal  (   int  $signo  ,  callback $handler  [,   bool  $restart_syscalls  =   true   ]   )

该函数为 signo指定的信号安装一个新的信号处理器。


安装SIGCHLD信号

        如上,我们知道如果父进程是循环,又没有安装 SIGCHLD信号处理函数调用 wait或 waitpid()等待子进程结束。那么子进程结束后,没有回收,就产生僵尸进程了。通过安装SIGCHLD信号处理函数来解决僵尸进程问题。示例:

<?php
//表示每执行一条低级指令,就检查一次信号,如果检测到注册的信号,就调用其信号处理器
declare(ticks = 1);

//安装SIGCHLD信号
pcntl_signal(SIGCHLD, function(){
    echo "SIGCHLD \r\n";
    pcntl_wait($status);
}); //#2

$pid = pcntl_fork();
if($pid == -1){
    exit("fork fail");
}elseif($pid){
    $id = getmypid();   
    echo "Parent process,pid {$id}, child pid {$pid}\n";
    
    //先sleep一下,否则代码一直循环,无法处理信号接收
    while(1){sleep(3);} //#1
}else{
    $id = getmypid();   
    echo "Child process,pid {$id}\n";   
    sleep(2); 
    exit();
}

        第一次注释掉 #1和 #2处的代码,父进程提前结束,子进程被init进程接手,所以没有产生僵尸进程。 第二次我们注释掉 #2处的代码,开启 #1处的代码,即父进程是个死循环,又没有回收子进程,就产生僵尸进程了。 第三次我们开启 #1处和 #2处的代码,父进程由于安装了信号处理,并调用wait函数等待子进程结束,所以也没有产生僵尸进程。

        对子进程的结束不感兴趣 如果父进程不关心子进程什么时候结束,那么可以用 pcntl_signal(SIGCHLD,SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号。这样我们就不写子进程退出的处理函数了

        注意:

        如果去掉 declare(ticks=1);无法响应信号。因php的信号处理函数是基于ticks来实现的,而不是注册到真正系统底层的信号处理函数中

       

安装其他信号

        我们可以在主进程安装更多信号,例如:

<?php
declare( ticks  =  1 );
//信号处理函数
function  sig_handler ( $signo )
{
     switch ( $signo ) {
        case  SIGTERM :
            // 处理SIGTERM信号
            exit;
            break;
        case  SIGHUP :
            //处理SIGHUP信号
            break;
        case  SIGUSR1 :
            echo  "Caught SIGUSR1...\n" ;
            break;
        default:
            // 处理所有其他信号
      }
}

echo  "Installing signal handler...\n" ;

//安装信号处理器
pcntl_signal ( SIGTERM ,  "sig_handler" );
pcntl_signal ( SIGHUP ,   "sig_handler" );
pcntl_signal ( SIGUSR1 ,  "sig_handler" );

echo  "Generating signal SIGTERM to self...\n" ;

//向当前进程发送SIGUSR1信号
posix_kill ( posix_getpid (),  SIGUSR1 );

echo  "Done\n"

        注:通过 kill-l 可以看到Linux下所有的信号常量。


        PHP的 ticks=1 表示每执行1行PHP代码就回调此函数(指的 pcntl_signal_dispatch),作用就是查看是否收到了信号需要处理,如果有信号的话,就调用相应的信号处理函数。

所以上述问题比较好的做法是去掉ticks,转而手动调用 pcntl_signal_dispatch,在代码循环中自行处理信号。

        我们把上一小节的例子改改,不使用ticks:

<?php 
//declare( ticks  =  1 );

//信号处理函数
function  sig_handler ( $signo )
{
     switch ( $signo ) 
     {
         case  SIGUSR1 :
            echo  "Caught SIGUSR1...\n" ;
            break;
         default:
            // 处理所有其他信号
      }
}

echo  "Installing signal handler...\n" ;

//安装信号处理器
pcntl_signal ( SIGUSR1 ,  "sig_handler" );

echo  "Generating signal SIGTERM to self...\n" ;

//向当前进程发送SIGUSR1信号
posix_kill ( posix_getpid (),  SIGUSR1 );
pcntl_signal_dispatch();

echo  "Done\n";

        运行结果:

Installing signal handler...
Generating signal SIGTERM to self...
Caught SIGUSR1...
Done

        相比每执行一条php语句都会调用 pcntl_signal_dispatch 一次,效率好多了。

pcntl_alarm

        int pcntl_alarm ( int $seconds )

        该函数创建一个计时器,在指定的秒数后向进程发送一个 SIGALRM 信号。每次对 pcntl_alarm() 的调用都会取消之前设置的alarm信号。注意不是定时器,只会运行一次。

        下面是一个隔5秒发送一个SIGALRM信号,并由signal_handler函数获取,然后打印一个 SIGALRM 的例子:

<?php 
declare(ticks = 1);

//安装SIGALRM信号
pcntl_signal(SIGALRM, function(){
    echo "SIGALRM\n";
    pcntl_alarm(5);  //再次调用,会重新发送一个SIGALRM信号
});
pcntl_alarm(5);//发送一个SIGALRM信号

echo "run...\n";

//死循环,否则进程会退出
while(1){sleep(1);}

        注:如果不想使用ticks,那么需要在主循环里主动增加 pcntl_signal_dispatch()调用。




冷暖自知一抹茶ck
请先登录后发表评论
  • 最新评论
  • 总共0条评论