原文地址:http://twei.site/2017/08/08/PHP%E7%9A%84%E5%A4%9A%E8%BF%9B%E7%A8%8B-%E9%98%B2%E6%AD%A2%E5%83%B5%E5%B0%B8%E8%BF%9B%E7%A8%8B/
正文
多进程编码中,一个不得不注意的问题就是僵尸进程(zombie process)。在 PHP 的多进程编码中,也是如此。
什么是僵尸进程
僵尸进程:一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait() 或 waitpid() 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这类子进程被称为僵尸进程。
我们详细理解下,在 UNIX/Linux 中,正常情况下,子进程是通过 fork 父进程创建的。子进程和父进程的运行是一个异步过程,理论上父进程无法知道子进程的运行状态。
但知道子进程运行状态是一个很合理的需求,所以 UNIX 提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息,就可以得到。
这种机制就是: 在每个进程退出的时候,内核释放该进程的一部分资源,包括打开的文件、占用的内存等,同时仍然为其保留一定的信息(包括进程号 the process ID,退出状态 the termination status of the process,运行时间 the amount of CPU time taken by the process等)。父进程可以通过 wait()/waitpid() 来获取这些信息,然后操作系统才释放。
这样就产生了一个问题,如果父进程不调用 wait()/waitpid() 的话,那么保留的信息就不会释放,其进程号就会一直被占用,就像僵尸一样,所以把这些进程称为僵尸进程。
僵尸进程的坏处
上面说到僵尸进程由于父进程不回收系统保留的信息而一直占用着系统资源,其中有一项叫做进程描述符。系统通过分配它来启动一个进程。
但是系统所能使用的进程号是有限的,如果存在大量的僵尸进程,系统将因为没有可用的进程号而导致系统不能产生新的进程。
如何查看僵尸进程
僵尸进程在系统中用 <defunct>
或 <z>
表示,通过 ps -ef
指令查看进程,如果发现某个进程的状态为 <defunct>/<z>
,说明该进程是一个僵尸进程。
避免僵尸进程的方法
通过WAIT/WAITPID系统
在 pcntl 中,父进程可以通过 pcntl_wait() 和 pcntl_waitpid() 函数来等待子进程结束,并回收。
1 <?php 2 $pid = pcntl_fork(); 3 4 if($pid == -1) { 5 die('fork error'); 6 }elseif($pid) { 7 // 父进程阻塞着等待子进程的退出 8 pcntl_wait($status); 9 //pcntl_waitpid($pid, $status); 10 11 // 非阻塞方式,通过WNOHANG区分 12 //pcntl_wait($status, WNOHANG); 13 //pcntl_waitpid($pid, $status, WNOHANG); 14 15 // 如没有上面的函数,在父进程sleep的时候,子进程就是僵尸进程 16 sleep(10); 17 }else{ 18 echo "child \r\n"; 19 exit; 20 } 21 ?>
int pcntl_wait ( int &$status [, int $options ] ):阻塞当前进程,直到当前进程的一个子进程退出或者收到一个结束当前进程的信号。
int pcntl_waitpid ( int $pid , int &$status [, int $options ] ):功能同 pcntl_wait,区别为 waitpid 为等待指定 pid 的子进程。当 pid 为 -1 时 pcntl_waitpid 与 pcntl_wait 一样。
阻塞:父进程一直等待,直到收到一个子进程结束的信号再执行;
非阻塞:父进程和子进程同时执行,不用等子进程执行完。在子进程退出后,再回收。
通过SIGCHLD信号
当子进程结束后,系统会像父进程发送 SIGCHLD 信号给父进程,通知父进程子进程已经结束,但父进程默认不处理。我们可以在父进程收到这个信号时调用 wait()/waitpid() 来回收。
1 <?php 2 // 每执行一次低级语句会检查一次该进程是否有未处理过的信号 3 declare(ticks = 1); 4 5 // 信号处理函数 6 function sig_func() { 7 echo "SIGCHLD \r\n"; 8 9 // 阻塞 10 pcntl_wait($status); 11 //pcntl_waitpid(-1, $status); 12 13 // 非阻塞 14 //pcntl_wait($status, WNOHANG); 15 //pcntl_waitpid(-1, $status, WNOHANG); 16 } 17 18 pcntl_signal(SIGCHLD, 'sig_func'); 19 20 $pid = pcntl_fork(); 21 22 if($pid == -1) { 23 die('fork error'); 24 }elseif($pid) { 25 // 父进程一直执行 26 while(1) { 27 sleep(5); 28 } 29 }else{ 30 echo "child \r\n"; 31 exit; 32 }
在子进程退出后,父进程收到了 SIGCHLD 信号,回调了 sig_func 函数,在内部执行了 pcntl_wait 函数,回收了子进程。
通过操作系统信号
如果父进程不关心子进程什么时候结束,那么可以用 pcntl_signal(SIGCHLD, SIG_IGN)
通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号。
1 <?php 2 declare(ticks = 1); 3 4 pcntl_signal(SIGCHLD, SIG_IGN); 5 6 $pid = pcntl_fork(); 7 8 if($pid == -1) { 9 die('fork error'); 10 } else if ($pid) { 11 for(;;) { 12 sleep(3); 13 } 14 } else { 15 echo "child \r\n"; 16 exit; 17 }