大家好,我是码农先森。
信号是一种轻量级的消息传递机制,通常用于软件中断异步通知。比如我们常用的 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
// 定义信号处理函数
function signalHandler($signal)
{
switch ($signal) {
case SIGINT:
// 比如公司广播响起了吃 下午茶 的音乐
echo "捕获到 SIGINT 信号... " . PHP_EOL;
// 接下来可以自行实现相应的逻辑
exit;
case SIGTERM:
// 比如公司广播又响起了线上 Bug 的告警声
echo "捕获到 SIGTER 信号... " . PHP_EOL;
// 接下来可以自行实现相应的逻辑
exit;
case SIGALRM:
// 比如公司广播又通知你参加某某答辩会
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@root php_signal]$ php index.php
当前进程 PID: 43195
[manongsen@root php_signal]$ kill -SIGTERM 43195
[manongsen@root php_signal]$ php index.php
当前进程 PID: 43195
捕获到 SIGTERM 信号...
再来看一个进程间信号传递的例子,假设你在当前这家公司被内耗死了,然后你开始找一个替补的人,好不容易找到一个卷王,但是干了一段时间就跑路了,最后你也无奈了索性也摆烂跑路了。
使用 pcntl_fork 创建了一个子进程,父进程中调用 pcntl_signal 函数给 SIGCHLD 信号注册回调函数 signalHandler 然后父进程一直监听信号。直到子进程 exit 退出了,最后父进程触发了回调函数 signalHandler 开始回收子进程资源,之后父进程也 exit 退出了。
<?php
// 定义信号处理函数
function signalHandler($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');
} else if( $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@root php_signal]$ php index.php
父进程: 47008
子进程: 47009
子进程: 47009 退出了
父进程: 47008, 收到子进程: 47009, 退出信号: 20
父进程: 47008, 最后也退出了
有了上面这两个例子的基础,我们再来看 Workerman 源码中对信号的使用就会更得心应手,翻开 Worker.php 文件中的第 548 行,这个 runAll 函数中会调用 installSignal 函数注册信号回调函数,还会调用 monitorWorkers 函数时刻监听子进程的信号。
<?php
// workerman/Worker.php:548
public static function runAll()
{
// ...
// 注册信号回调函数
static::installSignal();
// ...
// 监听 Worker 子进程信号
static::monitorWorkers();
}
看到 installSignal 这个函数的实现,是不是有种似曾相似的感觉,和上面例子的注册方式基本一样。
<?php
// workerman/Worker.php:1140
protected static function installSignal()
{
// ...
// 信号回调函数
$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:1220
public static function signalHandler($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:1735
protected static function monitorWorkersForLinux()
{
// 默认运行状态
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 源码中的使用姿势,大家最好是可以实践一下文中的这些例子,实践过后相信对信号会有更深一步的理解,希望对大家能有所帮助。