## 孤儿进程 > 老爹死了你还没死! 如果父进程先于子进程退出,则子进程成为孤儿进程,此时将自动被PID为1的进程(即init)接管。孤儿进程退出后,它的清理工作有祖先进程init自动处理。init清理没那么及时,毕竟不是亲爹。 >[warning] 子进程退出时,应当由父进程回收资源。应当避免父进程退出了,子进程还在的情况。__即: 应当避免孤儿进程。所以父进程要等子进程运行完了再退出。__ **参考代码** ``` $pid = pcntl_fork(); //父进程和子进程都会执行下面代码 if ($pid == -1) { //错误处理:创建子进程失败时返回-1 die('could not fork'); } else if ($pid) { echo '父进程 id = ' . posix_getpid() . PHP_EOL; exit(); // 父进程退出 } else { //子进程得到的$pid为0, 所以这里是子进程执行的逻辑。 echo '子进程 id = ' . posix_getpid() . PHP_EOL; while(true){ sleep(1); // 子进程一直运行 } echo '子进程最后的父进程 id = ' . posix_getppid() . PHP_EOL; } ``` **运行截图** ![](https://img.kancloud.cn/1f/e8/1fe85dc391092ce501a71dd5b1438269_1242x208.png) ## 僵尸进程 > 你死了没人给你收尸! 如果子进程先退出,系统不会自动清理掉子进程的环境,而必须由父进程调用`wait`或`waitpid`函数来完成清理工作,如果父进程不做清理工作,则已经退出的子进程将成为僵尸进程(defunct),在系统中如果存在的僵尸(zombie)进程过多,将会影响系统的性能,所以必须对僵尸进程进行处理。 **参考案例** ``` // 创建一个子进程 $pid = pcntl_fork(); //父进程和子进程都会执行下面代码 if ($pid == -1) { //错误处理:创建子进程失败时返回-1 die('could not fork'); } else if ($pid) { echo '父进程 id = ' . posix_getpid() . PHP_EOL; while(true){ sleep(1); // 但是父进程没有回收 } } else { //子进程得到的$pid为0, 所以这里是子进程执行的逻辑。 echo '子进程 id = ' . posix_getpid() . PHP_EOL; exit(); // 子进程已经退出 } ``` **截图** ![](https://img.kancloud.cn/26/90/26900ba55cba855504180cee514eeb21_1273x115.png) 子进程变成僵尸进程标志性的`[ defunct ]`,还会看到一个新的列叫做`[ Z+ ]`,此处Z即为Zombie之意。 >[danger] ### 避免僵尸进程 在PHP中则是由`pcntl_wait()`和`pcntl_waitpid()`两个函数来解决僵尸进程 ### pcntl_wait() 函数 > pcntl_wait — 等待或返回 fork 的子进程状态 >[warning] wait函数挂起当前进程的执行直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数。**如果一个子进程在调用此函数时已经退出(俗称僵尸进程),此函数立刻返回。子进程使用的所有系统资源将被释放。** **参考代码** ``` <?php $pid = pcntl_fork(); if ($pid == -1) { die('could not fork'); } else if ($pid) { // 父进程 echo '父进程 id = ' . posix_getpid() . PHP_EOL; $i_ret = pcntl_wait($status); // 子进程退出立即执行。返回退出的子进程进程号,发生错误时返回 -1 echo $i_ret . ' : ' . $status . PHP_EOL; // 保持父进程不退出 sleep(10); } else { // 子进程 for ($i = 1; $i <= 10; $i++) { sleep(1); echo "子进程 id = " . posix_getpid() . " 倒计时 : " . $i . PHP_EOL; } } ``` **执行截图** ![](https://img.kancloud.cn/a5/ca/a5ca30df53c2481f507139ef16a0b997_1194x260.png) > `pcntl_wait() `函数在成功回收了子进程后,该函数当即会返回被回收子进程的PID **语法详解** - 原型:`pcntl_wait ( int &$status [, int $options = 0 ] ) : int` - 参数:$status这种用法叫做[ 值 - 参数 ],pcntl_wait()会将状态信息存储到这个变量中;$options是一个选项配置,如果贵系统支持wait3系统调用,这个参数就会生效,反之则传进去也无法生效(使你我同僚倍感欣慰的是,绝大多数系统已经对wait3实现了支持甚至是wait4)。$option值则有`WNOHANG`或`WUNTRACED`二者可供选择,而且也可以以二者进行或运算使得函数兼具两种特性 - 返回:如尚未遇到任何错误,该函数返回被回收的子进程PID;如若出错则会告知吾辈-1 >[warning] 默认情况下,以类pcntl_wait( $status )的方式发起调用则程式必为之所阻塞,一直到子进程结束该函数则会返回。 ### WNOHANG 如果没有子进程退出立刻返回,程序则不会被阻塞。 ``` <?php $pid = pcntl_fork(); if ($pid == -1) { die('could not fork'); } else if ($pid) { // 父进程 echo '[x] 父进程 id = ' . posix_getpid() . PHP_EOL; $i_ret = pcntl_wait($status, WNOHANG); // 使其实现非阻塞 echo '[x] 返回退出的子进程进程号 ' . $i_ret . ' : status = ' . $status . PHP_EOL; // while保持父进程不退出 while (true) { sleep(1); } } else { // 子进程 for ($i = 1; $i <= 3; $i++) { sleep(1); echo "[x] 子进程 id = " . posix_getpid() . " 倒计时 : " . $i . PHP_EOL; } } ``` ![](https://img.kancloud.cn/c3/62/c3623b907c91114467d5196d3f67cb59_1263x258.png) >[warning] 以上程序中,由于`pcntl_wait()`使用`WNOHANG`参数使其实现非阻塞,以至于子进程尚未结束生命周期而父进程便已然走完了`pcntl_wait()`流程而陷于其后的while()轮回之中。其结果推理便可自然可知,子进程必然无法逃脱沦为僵尸进程的厄运。 ### WUNTRACED 子进程已经退出并且其状态未报告时返回。 ### 参数$status 与之配合的函数有如下列表: - pcntl_wexitstatus:此函数可检测进程退出时的错误码,在*NIX里进程退出时默认错误码是0,诸君亦可返其他任意数值,诸如exit( 250 ),此君可根据$status获取子进程退出时的错误码 - pcntl_wifexited:此君根据$status判断子进程是否正常退出。APUE曾有记载进程完成自然生命周期亦或exit()均可视之为正常退出,被abort亦或终止于[ 信号 ](signal) - pcntl_wifsignaled:此君较之前者,则用之于检查子进程是否因信号而中断 - pcntl_wifstopped:此君用于检测子进程是否已停止(注意停止不是终止,诸君要理解为临时挂起),然需使用了WUNTRACED作为$option的pcntl_waitpid()函数调用产生的status时才有效 - pcntl_wstopsig:此君则依赖前者,即仅在pcntl_wifstopped()返回 TRUE 时有效 - pcntl_wtermsig:此君依赖于pcntl_wifsignaled()为ture时检测子进程因何种信号[ signal ]而终止 **参考代码** ``` <?php $pid = pcntl_fork(); if ($pid === 0) { // 子进程 for ($i = 1; $i <= 3; $i++) { sleep(1); echo "[x] 子进程 id = " . posix_getpid() . " running " . $i . PHP_EOL; } exit(11); } // 父进程 echo '[x] 父进程 id = ' . posix_getpid() . PHP_EOL; $i_ret = pcntl_wait($status, WUNTRACED); echo $i_ret . " : " . $status . PHP_EOL; $ret = pcntl_wexitstatus($status); var_dump($ret); $ret = pcntl_wifexited($status); var_dump($ret); $ret = pcntl_wifsignaled($status); var_dump($ret); $ret = pcntl_wifstopped($status); var_dump($ret); ``` ![](https://img.kancloud.cn/e7/ee/e7eece90b0b3e42e60064f532c3402a1_424x208.png) ## pcntl_waitpid() 函数 > 挂起当前进程的执行直到参数pid指定的进程号的进程退出, 或接收到一个信号要求中断当前进程或调用一个信号处理函数。 >[warning] 如果pid指定的子进程在此函数调用时已经退出(俗称僵尸进程),此函数 将立刻返回。 **语法** ``` pcntl_waitpid ( int $pid , int &$status , int $options = 0 ) : int ``` **$pid 参数** - `<-1`:等待任意进程组ID等于参数pid给定值的绝对值的进程 - `=-1`:等待任意子进程;与pcntl_wait函数行为一致 - `=0`:等待任意与调用进程组ID相同的子进程 - `>0`:等待进程号等于参数pid值的子进程 >[success] pcntl_waitpid()将会存储状态信息到status 参数上,这个通过status参数返回的状态信息可以用以下函数 pcntl_wifexited(), pcntl_wifstopped(), pcntl_wifsignaled(), pcntl_wexitstatus(), pcntl_wtermsig()以及 pcntl_wstopsig()获取其具体的值。 ## pcntl_wifstopped()函数 > pcntl_wifstopped — 检查子进程当前是否已经停止。 ``` pcntl_wifstopped ( int $status ) : bool ``` >[warning] 仅查子进程当前是否停止; 此函数只有作用于使用了WUNTRACED作为 option的pcntl_waitpid()函数调用产生的status时才有效。 >[danger] **重要: 在此我需要向诸君说明一个进程的[ 终止 ]和[ 停止 ]是两个决然不同的概念** > - **[ 终止 ]意味着进程君生命周期已经完成,或正常完成或者异常终止;** > - **[ 停止 ]意味着临时挂起,还会复活继续活动。** > > **在*NIX中,可以[ kill -STOP pid ]将指定pid的进程临时挂起,此后便可使用pcntl_wifstopped()检测其是否可以挂起停止,与之相反,便可用[ kill -CONT pid ]使之复活。** **参考代码** ``` <?php $pid = pcntl_fork(); if ($pid === 0) { // 子进程 for ($i = 1; $i <= 300; $i++) { echo "[x] 子进程 id = " . posix_getpid() . " 心跳活着 " . $i . PHP_EOL; sleep(1); } exit(); } // 父进程 echo '[x] 父进程 id = ' . posix_getpid() . PHP_EOL; while (true) { $i_ret = pcntl_waitpid(0, $status, WNOHANG | WUNTRACED); $isStop = pcntl_wifstopped($status); // 检查子进程当前是否已经停止 echo "[x] 子进程当前是否已经停止: " . json_encode($isStop) . PHP_EOL; sleep(1); } ``` ![](https://img.kancloud.cn/cf/eb/cfeb8edf199f7349eccf293a3f0bd9b1_1143x220.png) >[warning] 正如上图所示,在`kill -STOP 22295`后,诸君可曾尝试`kill -CONT 22295`?如有,可曾观察程序" 是否停止:true "恢复为" 是否停止:false "? > 答案是:事实上是没有恢复 > 原因是:此处即为PHP文档描述于进程控制粒度之粗狂,如诸君使用C语言便可使用使用WCONTINUED选项使进程文案恢复为" 是否停止:false "。然则,PHP文档虽未标注,我们却可通过如下方式使用该选项`$i_ret = pcntl_waitpid( 0, $status, WNOHANG | WUNTRACED | WCONTINUED )` > *WCONTINUED选项相关资料可见于APUE 193页(第三版)* >[danger] `WNOHANG`,**此参使得pcntl_wait()亦或pcntl_waitpid()在尚未遇到任何子进程生命周期完成时马上返回,而不会阻塞等待任一子进程结束。** > 这一功能最大的作用就是:**我们期盼获得到所有子进程的状态而不是想被阻塞,这一要点在有多个子进程的时候显得颇为至关重要。**