什么是僵尸进程?
僵尸进程是指的父进程已经退出,而该进程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关闭。
怎样来清除僵尸进程?
改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后, 会发送SIGCHLD信号给父进程,父进程收到此信号后,执行 waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用wait,内核也会向它发送SIGCHLD消息,尽管对的默认处理是忽略, 如果想响应这个消息,可以设置一个处理函数。
把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程,关机或重启后所有僵尸进程都会消失。
避免Zombie Process的方法
在SVR4中,如果调用signal或sigset将SIGCHLD的配置设置为忽略,则不会产生僵死子进程。另外,使用SVR4版的 sigaction,则可设置SA_NOCLDWAIT标志以避免子进程僵死。 Linux中也可使用这个,在一个程序的开始调用这个函数signal(SIGCHLD,SIG_IGN)。
调用fork两次,父进程fork一个子进程,然后继续工作,子进程fork一 个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收 还要自己做。
用waitpid等待子进程返回。
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 一次,效率好多了。
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()调用。
本文为崔凯原创文章,转载无需和我联系,但请注明来自冷暖自知一抹茶ckhttp://www.cksite.cn