前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >和老李一起搞山寨Workerman(三)

和老李一起搞山寨Workerman(三)

作者头像
老李秀
发布2019-12-11 10:21:09
1.1K0
发布2019-12-11 10:21:09
举报
文章被收录于专栏:可能是东半球最正规的API社区

各位佬们腿子们,侬们好,我是老李,我今天打算换种语风,你们准备好我要开始了。

今日清晨睡梦中醒来,已是7点40分有余。静躺在床上,耳边远处又断断续续萦绕着老人们在楼下的野园子里跟着于魁智吊嗓子的声音,时不时还夹杂着一段段抖空竹的风哨声。

挣扎着从被窝中起来,拉开窗子是灰蒙蒙一片。怎奈的这雾霾犹如灰色氤氲般穿插围绕着远处的楼宇,即便天公既已如此,也不想使得自己心境受之影响。

念及昨日之篇章,洋洋洒洒三千余字满篇pcntl,尽然毫无WM之踪迹。然君子曰[ 骐骥一跃,不能十步;驽马十驾,功在不舍 ],又云[ 蟹六跪而二螯,非蛇鳝之穴无可寄托者,用心躁也 ],然则鲤鱼若欲跃明日之龙门,乃需夯实根基,其次需戒躁。

归正文,PHP之多进程是具备生产力价值的,反观其基于pthread扩展的多线程,则显得颇味同嚼蜡。然无论是多进程抑或多线程,均需PHP-CLI SAPI而非PHP-FPM,若于PHP-FPM中尝试二者则往往有难以预料的异常。原因在于PHP-FPM本身随常驻内存,然而其中的代码并非常驻内存。

于高处观pcntl_fork(),可认为其为进程的制造者,一旦进程完成了其生命周期,还是需要善后回收。故我思虑来回,今日篇章的主题即可围绕[ 僵尸进程 ]与[ 孤儿进程 ]进行。

平日里,我们大多都会使用PS命令查看系统进程现状,而PS数据实则取于*NIX进程表,英吉利语简称则为PCB。每每有进程生老病死,则系统进程表乃其[ 生死簿 ]。我们也大可不必被系统进程表名头纸老虎给吓住了,其本质上就是一种链表数据结构。设想,你需要设计一种结构以存储如下信息:

  • 进程PID
  • 进程当前状态
  • 进程持有的堆栈等数据
  • 进程父进程PID
  • 进程最近一次运行时间
  • ... ...
  • ... ...

你会怎么如何设计?此行为可类比于为业务系统设计MySQL数据表,如此类比可消除在座诸位抵触或惧怕心理否?续上前面接着说,系统持有进程表之动机在于:每每进程让出CPU,进程当前所处环境信息等则存于进程表;每每进程再次占据CPU时间片,则由进程表取出前次让出CPU时之信息用以恢复状态。

僵尸进程:子进程完成其生命周期后,父进程任之不管不管,子进程残留数据诸如PID、持有的资源等,久而久之则危害操作系统。在*NIX系统中,僵尸进程常有[Z+]标志符。

孤儿进程:子进程尚未完成生命周期,父进程已提前完成生命周期,此子进程则为孤儿进程,可[ 望文生义 ]。孤儿进程一旦形成,则自动由系统头号进程init来完成收养。孤儿进程实属常见,见之不必惊慌。

看诸位困意满面,想必定是未提供CV代码所致。下面蝇头文案程序,则展示子进程由init收养之实:

代码语言:javascript
复制
<?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 );  
}

继而则是僵尸进程:

代码语言:javascript
复制
<?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()回收 ]的简要流程:

代码语言:javascript
复制
<?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 ( int &$status [, int $options = 0 ] ) : int
  • 参数:$status这种用法叫做[ 值 - 参数 ],pcntl_wait()会将状态信息存储到这个变量中;$options是一个选项配置,如果贵系统支持wait3系统调用,这个参数就会生效,反之则传进去也无法生效(使你我同僚倍感欣慰的是,绝大多数系统已经对wait3实现了支持甚至是wait4)。$option值则有WNOHANG或WUNTRACED二者可供选择,而且也可以以二者进行或运算使得函数兼具两种特性
  • 返回:如尚未遇到任何错误,该函数返回被回收的子进程PID;如若出错则会告知吾辈-1

默认情况下,以类pcntl_wait( $status )的方式发起调用则程式必为之所阻塞,一直到子进程结束该函数则会返回;如我们将WNOHANG作为作为$option传入,程序则不会被阻塞。授诸位蝇头繁码,诸位略感一二:

代码语言:javascript
复制
<?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:

  • 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 ]而终止

蝇头繁码贴,诸君共勉之:

代码语言:javascript
复制
<?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。只是有些复杂:

  • <-1:等待任意进程组ID等于参数pid给定值的绝对值的进程
  • =-1:等待任意子进程;与pcntl_wait函数行为一致
  • =0:等待任意与调用进程组ID相同的子进程
  • >0:等待进程号等于参数pid值的子进程

此处,可结合pcntl_wifstopped()稍做演示。在此我需要向诸君说明一个进程的[ 终止 ]和[ 停止 ]是两个决然不同的概念,[ 终止 ]意味着进程君生命周期已经完成,或正常完成或者异常终止;而[ 停止 ]意味着临时挂起,还会复活继续活动。在*NIX中,可以[ kill -STOP pid ]将指定pid的进程临时挂起,此后便可使用pcntl_wifstopped()检测其是否可以挂起停止,与之相反,便可用[ kill -CONT pid ]使之复活。

代码语言:javascript
复制
<?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()在尚未遇到任何子进程生命周期完成时马上返回,而不会阻塞等待任一子进程结束,这一功能最大的作用就是:我们期盼获得到所有子进程的状态而不是想被阻塞,这一要点在有多个子进程的时候显得颇为至关重要。

正式结束前,我们以一篇功能略微完善的程式结尾,望诸君共勉之。

代码语言:javascript
复制
<?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快点儿,写完了没,我一个人过不了这一关...

老李:快了快了...

永强:赶紧写完完事儿了,非得整什么新风格,沙雕...说说啥感受?啥感受?...

老李:那家伙...

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-11-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能API社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 MySQL
腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档