看看 PHP 高性能框架 Workerman 源码中信号的使用姿势

信号是一种轻量级的消息传递机制,通常用于软件中断异步通知。比如我们常用的 kill -9 xxx
首页 新闻资讯 行业资讯 看看 PHP 高性能框架 Workerman 源码中信号的使用姿势

大家好,我是码农先森。

信号是一种轻量级的消息传递机制,通常用于软件中断异步通知。比如我们常用的 kill -9 xxx 就是对 xxx 程序发送了强制终止其进程的 SIGKILL 信号,还有我们经常会用的 Ctrl+C 或 Ctrl+D 等键盘操作事件,也会产生 SIGINT 终端退出信号。

但在我们平时的业务编程中很少会涉及到信号,这是由于信号通常都是用在进程的管理上,比如父子进程的通信、进程资源的管理等。因此在 PHP-FPM 模式下是不会用多进程编程的,如果那位大胆的朋友用了,那恭喜你会收获到意想不到的惊喜。

不过如果经常使用 Workerman 或 Swoole 编程的朋友,那对信号应该颇有了解了吧。这里不管大家有没有接触过信号,都建议学习其原理和用法,兴许对以后的编程有所帮助,毕竟技术这东西就怕用时方恨少。为了更通俗易懂的描述信号,我用一个大白话的例子来讲解一下。假设你是一个打工仔,这里好像不用假设因为你就是个打工仔哈哈,这时你正戴着耳机沉浸在代码的世界了。

突然公司广播响起了吃「下午茶」的音乐,于是你便快马加鞭赶到下午茶的餐桌面前蓄势待发。才刚吃上鸡腿,公司广播又响起了线上 Bug 的告警声,你不得不叼着鸡腿,又回到座位上排查起问题了。线上问题还没解决,这是公司广播又通知你参加某某答辩会,但这次你完全没有时间顾及这个通知,所以你完全忽略了。

这里的公司你可以理解为操作系统,广播通知理解为信号,你自己理解为一个进程。你在接收到信号之后,可以做对应的事情,同样也可以做与通知不相干的事情,比如通知叫你改线上的 Bug 但是你舍不得下午茶还想继续吃,那你也可以任性不及时改「只不过事后就要挨批了哈哈」。

还有就是也可以把通知不当回事,不予理会完全忽略不处理。这段大白话虽说的好,但中国有句古话「说的好听不如做的好看」,那么接下来我们就以实际的例子来以身入代码。

下面这个是当前进程接收外部信号的例子,使用 pcntl_signal 注册指定信号的回调函数,回调函数 signalHandler 会根据接收到的不同信号进入到相应的 switch 分支中执行对应的处理逻辑。

处理完之后,便可以退出程序。其中的 pcntl_signal_dispatch 函数起着监听待处理信号,并执行信号所注册回调函数的作用。还有 where(true) 会一直循环执行,直到接收到信号。

<?php// 定义信号处理函数functionsignalHandler($signal){
    switch($signal){caseSIGINT:// 比如公司广播响起了吃 下午茶 的音乐echo"捕获到 SIGINT 信号... ".PHP_EOL;// 接下来可以自行实现相应的逻辑exit;caseSIGTERM:// 比如公司广播又响起了线上 Bug 的告警声echo"捕获到 SIGTER 信号... ".PHP_EOL;// 接下来可以自行实现相应的逻辑exit;caseSIGALRM:// 比如公司广播又通知你参加某某答辩会echo"捕获到 SIGALRM 信号... ".PHP_EOL;// 接下来可以自行实现相应的逻辑// 当然你也可以不处理,任由它去exit;default:
            echo"捕获到未知信号: $signal ".PHP_EOL;exit;}
}// 注册信号回调函数// 就好比接受到不同的通知,就应该要做对应的事情// 这里统一回调 signalHandler 这个函数,你也可以定义不同的函数pcntl_signal(SIGINT,'signalHandler');pcntl_signal(SIGTERM,'signalHandler');// 输出一下当前的进程号,方便测试echo"当前进程 PID: ".getmypid().PHP_EOL;// 不断监听信号,接收到信号后调用相应的信号回调函数// 这里就像你自己的耳朵一直在等待接收通知while(true){
    pcntl_signal_dispatch();}

执行 php index.php 命令启动程序,然后通过 kill 向进程发送 SIGTERM 信号,最终会在 signalHandler 回调函数中捕获到该信号。

[manongsen@rootphp_signal]$ phpindex.php 
当前进程 PID:43195[manongsen@rootphp_signal]$kill-SIGTERM43195[manongsen@rootphp_signal]$ phpindex.php 
当前进程 PID:43195捕获到 SIGTERM 信号...

再来看一个进程间信号传递的例子,假设你在当前这家公司被内耗死了,然后你开始找一个替补的人,好不容易找到一个卷王,但是干了一段时间就跑路了,最后你也无奈了索性也摆烂跑路了。

使用 pcntl_fork 创建了一个子进程,父进程中调用 pcntl_signal 函数给 SIGCHLD 信号注册回调函数 signalHandler 然后父进程一直监听信号。直到子进程 exit 退出了,最后父进程触发了回调函数 signalHandler 开始回收子进程资源,之后父进程也 exit 退出了。

<?php// 定义信号处理函数functionsignalHandler($signal){
    $pid=getmypid();// 回收子进程资源// 你接收到了他跑路的信号,开始回收他的代码权限、账号等资源$status=0;$cid=pcntl_wait($status,\WUNTRACED);echo"父进程: {$pid}, 收到子进程: {$cid}, 退出信号: {$signal}".PHP_EOL;// 最后你抵不住也跑路了echo"父进程: {$pid}, 最后也退出了".PHP_EOL;exit;}// 在当前进程 Fork 一个子进程// 比如终于招聘了一个替补你的人$pid=pcntl_fork();if($pid<0){// 这里被你放鸽子了// 你也摆烂直接跑路算了exit('fork error');}elseif($pid>0){// 父进程执行空间 ...echo"父进程: ".getmypid().PHP_EOL;// 注册信号回调函数// SIGCHLD 表示一个子进程已经终止或停止// 你相当于给他触发跑路的信号,准备了一个处理方案pcntl_signal(SIGCHLD,"signalHandler");// 监听信号// 你相当于一直监听他的动向while(true){
      pcntl_signal_dispatch();}
}// 子进程执行空间 ...$cid=getmypid();echo"子进程: {$cid}".PHP_EOL;// 休眠 5 秒钟sleep(5);// 退出// 假设他干了5秒钟就跑路了exit;

执行 php index.php 可以看到相应的执行结果。

[manongsen@rootphp_signal]$ phpindex.php 
父进程:47008子进程:47009子进程:47009退出了
父进程:47008,收到子进程:47009,退出信号:20父进程:47008,最后也退出了

有了上面这两个例子的基础,我们再来看 Workerman 源码中对信号的使用就会更得心应手,翻开 Worker.php 文件中的第 548 行,这个 runAll 函数中会调用 installSignal 函数注册信号回调函数,还会调用 monitorWorkers 函数时刻监听子进程的信号。

<?php// workerman/Worker.php:548publicstaticfunctionrunAll(){// ...// 注册信号回调函数static::installSignal();// ...// 监听 Worker 子进程信号static::monitorWorkers();}

看到 installSignal 这个函数的实现,是不是有种似曾相似的感觉,和上面例子的注册方式基本一样。

<?php// workerman/Worker.php:1140protected staticfunctioninstallSignal(){// ...// 信号回调函数$signalHandler='\Workerman\Worker::signalHandler';// 退出信号\pcntl_signal(\SIGINT,$signalHandler,false);\pcntl_signal(\SIGTERM,$signalHandler,false);\pcntl_signal(\SIGHUP,$signalHandler,false);\pcntl_signal(\SIGTSTP,$signalHandler,false);// 优雅退出信号\pcntl_signal(\SIGQUIT,$signalHandler,false);// 重载信号\pcntl_signal(\SIGUSR1,$signalHandler,false);// 优雅重载信号\pcntl_signal(\SIGUSR2,$signalHandler,false);// 状态信号\pcntl_signal(\SIGIOT,$signalHandler,false);// 连接状态信号\pcntl_signal(\SIGIO,$signalHandler,false);// SIGPIPE 管道信号,忽略该信号\pcntl_signal(\SIGPIPE,\SIG_IGN,false);}

这个函数 signalHandler 接收到子进程的信号,并且根据不同的信号 switch 到不同的 case 分支。

<?php// workerman/Worker.php:1220publicstaticfunctionsignalHandler($signal){
    switch($signal){// 退出case\SIGINT:case\SIGTERM:case\SIGHUP:case\SIGTSTP:
            static::$_gracefulStop=false;// 这里会把所有的 Woker 子进程 Kill 掉,并且销毁相应子进程的 Event-Loop 事件循环static::stopAll();break;// 优雅的退出case\SIGQUIT:// 优雅退出在调用 posix_kill 函数时传递的是 SIGTERM 信号static::$_gracefulStop=true;static::stopAll();break;// 重载case\SIGUSR2:case\SIGUSR1:if(static::$_status===static::STATUS_SHUTDOWN||static::$_status===static::STATUS_RELOADING){return;}
            static::$_gracefulStop=$signal===\SIGUSR2;static::$_pidsToRestart=static::getAllWorkerPids();// 对 Worker 进程实现重载static::reload();break;// 展现状态case\SIGIOT:// 这里会输出一些 全局状态信息、进程状态信息// 比如 load-average、event-loop、workers-count 等static::writeStatisticsToStatusFile();break;// 展现连接状态case\SIGIO:// 这里会输出一些进程连接相关的统计信息// 比如 Recv-Q、Send-Q、Bytes-R、Bytes-W、Status 等static::writeConnectionsStatisticsToStatusFile();break;}
}

最后启动监听函数 monitorWorkersForLinux 除了监听信号之后,还做了些资源的回收工作,此外还会回调用户自定义的 Stop 函数,便于做一些业务上的处理。

<?php// workerman/Worker.php:1735protected staticfunctionmonitorWorkersForLinux(){// 默认运行状态static::$_status=static::STATUS_RUNNING;// 一直监听直到所有的进程退出while(1){
        \pcntl_signal_dispatch();$status=0;// 这些做一些子进程资源的回收工作$pid=\pcntl_wait($status,\WUNTRACED);\pcntl_signal_dispatch();// ...// 做一些清理工作if(static::$_status===static::STATUS_SHUTDOWN&&!static::getAllWorkerPids()){// 这里还会回调用户设定的 Stop 回调函数,便于做一些业务上的处理// 然后直接 exit 当前进程static::exitAndClearAll();}
    }
}

在涉及到多进程编程的场景中信号的使用最为频繁,比如 Workerman 在 Master 主进程中通过信号监听了 Worker 子进程的状态,可以及时获取到子进程的运行信息,一旦子进程挂了也可以迅速的拉取,在整个服务进程都退出后也可以及时回收内存资源。

还有在 Linux 系统下我们也经常使用 Kill 命令来向进程传递信号,不过大多数情况都是用在了强制结束进程上,这是最简单粗暴杀死进程的方法。不过我之前遇到过某些病毒进程,无论如何都 Kill 不掉,不知大家有没有见识过?

这次我主要分享了信号的一些基础知识,以及在 Workerman 源码中的使用姿势,大家最好是可以实践一下文中的这些例子,实践过后相信对信号会有更深一步的理解,希望对大家能有所帮助。

11    2024-10-21 09:06:15    信号 Master Workerman