各位佬们腿子们,侬们好,我是老李,我今天打算换种语风,你们准备好我要开始了。
今日清晨睡梦中醒来,已是7点40分有余。静躺在床上,耳边远处又断断续续萦绕着老人们在楼下的野园子里跟着于魁智吊嗓子的声音,时不时还夹杂着一段段抖空竹的风哨声。
挣扎着从被窝中起来,拉开窗子是灰蒙蒙一片。怎奈的这雾霾犹如灰色氤氲般穿插围绕着远处的楼宇,即便天公既已如此,也不想使得自己心境受之影响。
念及昨日之篇章,洋洋洒洒三千余字满篇pcntl,尽然毫无WM之踪迹。然君子曰[ 骐骥一跃,不能十步;驽马十驾,功在不舍 ],又云[ 蟹六跪而二螯,非蛇鳝之穴无可寄托者,用心躁也 ],然则鲤鱼若欲跃明日之龙门,乃需夯实根基,其次需戒躁。
归正文,PHP之多进程是具备生产力价值的,反观其基于pthread扩展的多线程,则显得颇味同嚼蜡。然无论是多进程抑或多线程,均需PHP-CLI SAPI而非PHP-FPM,若于PHP-FPM中尝试二者则往往有难以预料的异常。原因在于PHP-FPM本身随常驻内存,然而其中的代码并非常驻内存。
于高处观pcntl_fork(),可认为其为进程的制造者,一旦进程完成了其生命周期,还是需要善后回收。故我思虑来回,今日篇章的主题即可围绕[ 僵尸进程 ]与[ 孤儿进程 ]进行。
平日里,我们大多都会使用PS命令查看系统进程现状,而PS数据实则取于*NIX进程表,英吉利语简称则为PCB。每每有进程生老病死,则系统进程表乃其[ 生死簿 ]。我们也大可不必被系统进程表名头纸老虎给吓住了,其本质上就是一种链表数据结构。设想,你需要设计一种结构以存储如下信息:
你会怎么如何设计?此行为可类比于为业务系统设计MySQL数据表,如此类比可消除在座诸位抵触或惧怕心理否?续上前面接着说,系统持有进程表之动机在于:每每进程让出CPU,进程当前所处环境信息等则存于进程表;每每进程再次占据CPU时间片,则由进程表取出前次让出CPU时之信息用以恢复状态。
僵尸进程:子进程完成其生命周期后,父进程任之不管不管,子进程残留数据诸如PID、持有的资源等,久而久之则危害操作系统。在*NIX系统中,僵尸进程常有[Z+]标志符。
孤儿进程:子进程尚未完成生命周期,父进程已提前完成生命周期,此子进程则为孤儿进程,可[ 望文生义 ]。孤儿进程一旦形成,则自动由系统头号进程init来完成收养。孤儿进程实属常见,见之不必惊慌。
看诸位困意满面,想必定是未提供CV代码所致。下面蝇头文案程序,则展示子进程由init收养之实:
<?php
$i_pid = pcntl_fork();
if ( 0 == $i_pid ) {
// 子进程10秒钟后退出.
for ( $i = 1; $i <= 10; $i++ ) {
sleep( 1 );
echo "我的父进程是:".posix_getppid().PHP_EOL;
}
}
else if ( $i_pid > 0 ) {
// 父进程休眠2s后退出.
sleep( 2 );
}
继而则是僵尸进程:
<?php
/*
子进程在10s后退出,退出后父进程已然还在运行中
但是父进程尚未做任何工作
所以按照定义,子进程将会成为僵尸进程.
*/
$i_pid = pcntl_fork();
if ( 0 == $i_pid ) {
// 子进程10s后退出.
sleep( 10 );
}
else if ( $i_pid > 0 ) {
// 父进程休眠1000s后退出.
sleep( 1000 );
}
依据上图红线标注信息可知,子进程PID为19041,其父进程PID为19040,进程名称由[ php Core.php ]变成僵尸进程标志性的[ defunct ],如果你是用ps -aux命令,将还会看到一个新的列叫做[ Z+ ],此处Z即为Zombie之意。
如若我们继续深思一步,如若子进程生命周期为10s,其父进程生命周期为20s,则其父进程的后半10s生命周期中,子进程必为僵尸进程,然而整体20s后,父进程也完成自身生命周期,此时根据我们理论:子进程将会交由init进程处理,init进程则会完成该子进程的回收清理工作,诸位可自行尝试。
随着篇幅继续,主要矛盾由僵尸进程的产生逐步转移到了如何解决僵尸进程。在PHP中则是由pcntl_wait()和pcntl_waitpid()两个函数来解决。当两个函数同时出现的时候,可考虑通过对比方式使得记忆更加深刻。
下面的程式向我们表述了[ 一个生命周期为10s中的子进程在结束后被主进程通过pcntl_wait()回收 ]的简要流程:
<?php
$i_pid = pcntl_fork();
if ( 0 == $i_pid ) {
// 在子进程中
for( $i = 1; $i <= 10; $i++ ) {
sleep( 1 );
echo "子进程PID ".posix_getpid()."倒计时 : ".$i.PHP_EOL;
}
}
else if ( $i_pid > 0 ) {
$i_ret = pcntl_wait( $status );
echo $i_ret.' : '.$status.PHP_EOL;
// while保持父进程不退出
while ( true ) {
sleep( 1 );
}
}
我贴一下运行结果图,在座可略感一二:
由下图可知Core.php并未出现僵尸进程,而pcntl_wait()函数在成功回收了子进程后,该函数当即会返回被回收子进程的PID。
我认为在此处简要描述下pcntl_wait()的原型还是有些许必要的,以参数和返回为序分别予以陈述:
默认情况下,以类pcntl_wait( $status )的方式发起调用则程式必为之所阻塞,一直到子进程结束该函数则会返回;如我们将WNOHANG作为作为$option传入,程序则不会被阻塞。授诸位蝇头繁码,诸位略感一二:
<?php
$i_pid = pcntl_fork();
if ( 0 == $i_pid ) {
// 在子进程中
for( $i = 1; $i <= 10; $i++ ) {
sleep( 1 );
echo "子进程PID ".posix_getpid()."倒计时 : ".$i.PHP_EOL;
}
}
else if ( $i_pid > 0 ) {
$i_ret = pcntl_wait( $status, WNOHANG );
echo $i_ret.' : '.$status.PHP_EOL;
// while保持父进程不退出
while ( true ) {
sleep( 1 );
}
}
以上程序中,由于pcntl_wait()使用WNOHANG参数使其实现非阻塞,以至于子进程尚未结束生命周期而父进程便已然走完了pcntl_wait()流程而陷于其后的while()轮回之中。其结果我们推理便可自然可知,子进程必然无法逃脱沦为僵尸进程的厄运:
至于WUNTRACED参数,我们可以尝试跳过,此参数字面意思为:子进程已经退出并且其状态未报告时返回。我认为现在我们更有必要去了解一下$status值参数,与之配合的函数有如下列表,该函数族拥有切唯一的一个参数$status:
蝇头繁码贴,诸君共勉之:
<?php
$i_pid = pcntl_fork();
if ( 0 == $i_pid ) {
for( $i = 1; $i <= 3; $i++ ) {
echo "子进程running".PHP_EOL;
sleep( 1 );
}
exit( 11 );
}
echo '父进程block在此'.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 );
pcntl_wait()可告一段落,无奈尚有pcntl_waitpid(),此君对于子进程回收控制力度与粒度绝非pcntl_wait()可比,此君你我皆不可弃之。
春宵一刻值千金,绝知此事要躬行
较之pcntl_wait(),pcntl_waitpid()仅多了一个参数:$pid。只是有些复杂:
此处,可结合pcntl_wifstopped()稍做演示。在此我需要向诸君说明一个进程的[ 终止 ]和[ 停止 ]是两个决然不同的概念,[ 终止 ]意味着进程君生命周期已经完成,或正常完成或者异常终止;而[ 停止 ]意味着临时挂起,还会复活继续活动。在*NIX中,可以[ kill -STOP pid ]将指定pid的进程临时挂起,此后便可使用pcntl_wifstopped()检测其是否可以挂起停止,与之相反,便可用[ kill -CONT pid ]使之复活。
<?php
$i_pid = pcntl_fork();
if ( 0 == $i_pid ) {
for( $i = 1; $i <= 300; $i++ ) {
echo "child alive".PHP_EOL;
sleep( 1 );
}
exit;
}
while( true ) {
$i_ret = pcntl_waitpid( 0, $status, WNOHANG | WUNTRACED );
$ret = pcntl_wifstopped( $status );
echo "是否停止:".json_encode( $ret ).PHP_EOL;
sleep( 1 );
}
[ 篇后不可错过之深思:正如上图所示,在kill -STOP 21098后,诸君可曾尝试kill -CONT 21098?如有,可曾观察程序" 是否停止:true "恢复为" 是否停止:false "?事实上是没有恢复的,何故?此处即为PHP文档描述于进程控制粒度之粗狂,如诸君使用C语言便可使用使用WCONTINUED选项使进程文案恢复为" 是否停止:false "。然则,PHP文档虽未标注,我们却可通过如下方式使用该选项$i_ret = pcntl_waitpid( 0, $status, WNOHANG | WUNTRACED | WCONTINUED )。WCONTINUED选项相关资料可见于APUE 242页 ]
在篇章即将结束前夕,我们需要重新认识一下WNOHANG,此参使得pcntl_wait()亦或pcntl_waitpid()在尚未遇到任何子进程生命周期完成时马上返回,而不会阻塞等待任一子进程结束,这一功能最大的作用就是:我们期盼获得到所有子进程的状态而不是想被阻塞,这一要点在有多个子进程的时候显得颇为至关重要。
正式结束前,我们以一篇功能略微完善的程式结尾,望诸君共勉之。
<?php
// 数组用于收集子进程
$a_child_pid = [];
// fork出十个子进程
for ( $i = 1; $i <= 10; $i++ ) {
$i_pid = pcntl_fork();
// 每个子进程随机运行1-5秒钟
if ( 0 == $i_pid ) {
$i_rand_time = mt_rand( 1, 5 );
sleep( $i_rand_time );
exit;
}
// 父进程收集所有子进程PID
else if ( $i_pid > 0 ) {
$a_child_pid[] = $i_pid;
}
}
while( true ) {
if ( count( $a_child_pid ) <= 0 ) {
exit( "所有进程均已终止".PHP_EOL );
}
foreach( $a_child_pid as $i_item_key => $i_item_pid ) {
$i_wait_ret = pcntl_waitpid( $i_item_pid, $i_status, WNOHANG | WUNTRACED | WCONTINUED );
if ( -1 == $i_wait_ret || $i_wait_ret > 0 ) {
unset( $a_child_pid[ $i_item_key ] );
}
// 如果子进程是正常结束
if ( pcntl_wifexited( $i_status ) ) {
// 获取子进程结束时候的 返回错误码
$i_code = pcntl_wexitstatus( $i_status );
echo $i_item_pid."正常结束,最终返回:".$i_code.PHP_EOL;
}
// 如果子进程是被信号终止
if ( pcntl_wifsignaled( $i_status ) ) {
// 获取是哪个信号终止的该进程
$i_signal = pcntl_wtermsig( $i_status );
echo $i_item_pid."由信号结束,信号为:".$i_signal.PHP_EOL;
}
// 如果子进程是[临时挂起]
if ( pcntl_wifstopped( $i_status ) ) {
// 获取是哪个信号让他挂起
$i_signal = pcntl_wstopsig( $i_status );
echo $i_item_pid."被挂起,挂起信号为:".$i_signal.PHP_EOL;
}
// sleep使父进程不会因while导致CPU爆炸.
sleep( 1 );
}
}
!!!你们真的以为这就结束了?!!!
永强:你TM快点儿,写完了没,我一个人过不了这一关...
老李:快了快了...
永强:赶紧写完完事儿了,非得整什么新风格,沙雕...说说啥感受?啥感受?...
老李:那家伙...