linux php 进程进阶(五) signal(信号)
signal
信号与中断流程介绍
信号 是指软件中断信号,简称软中断
- 中断源(中断信号产生位置):
- 终端设备驱动
- 在终端设备按下按键产生的中断信号
ctrl+c
,ctrl+z
,ctrl+\
- 硬件异常
- 软件产生
- 在终端使用
kill
命令 来发送中断信号- php中
posix_kill
函数pcntl_alarm
函数- SIGURG(TCP/IP),SIGALRM
- 中断响应(对信号的处理)
- 直接忽略 。但是有两种信号永远不会被忽略,一个是
SIGSTOP
,另一个是SIGKILL
,因为这两个信号提供了向内核最后的可靠的结束进程的办法。- 执行中断处理函数(信号处理函数,信号捕捉函数)
- 执行系统默认。大多数的系统默认响应就是终止进程。
-
中断返回:中断信号处理程序(信号处理函数,信号捕捉函数)完成后,就会返回继续执行主函数
-
中断处理过程图
中断源向进程发出中断信号称为中断请求信号
,
进程接收到中断信号可以进行响应,称为中断响应
为中断服务的程序称为中断处理程序 或叫 中断处理函数 | 中断服务程序
执行完中断处理函数后返回,中断返回
用人话来表达,就是说假如你是一个进程,你正在干活,突然施工队的喇叭里冲你嚷了一句:“吃饭了!”,于是你就放下手里的活儿去吃饭。你正在干活,突然施工队的喇叭里冲你嚷了一句:“发工资了!”,于是你就放下手里的活儿去领工资。你正在干活,突然施工队的喇叭里冲你嚷了一句:“有人找你!”,于是你就放下手里的活儿去看看是谁找你什么事情。当然了,你很任性,那是完全可以不鸟喇叭里喊什么内容,也就是忽略信号。也可以更任性,当喇叭里冲你嚷“吃饭”的时候,你去就不去吃饭,你去睡觉,这些都可以由你来。而你在干活过程中,从来不会因为要等某个信号就不干活了一直等信号,而是信号随时随地都可能会来,而你只需要在这个时候作出相应的回应即可,所以说,信号是一种软件中断,也是一种异步的处理事件的方式。
常用中断信号
信号 | 介绍 |
---|---|
SIGTSTP | 交互停止信号,终端挂起键 ctrl+z 终端驱动产生此信号 【终端停止符】 终止+core(core 文件是用来记录进程异常或者错误的文件方便使用gdb来调试) |
SIGTERM | 可以被捕捉,让程序先清理一些工作再终止 |
SIGSTOP | 作业控制信号,也是停止一个进程,跟SIGSTSP 一样 |
SIGQUIT | 退出键 ctrl+\ 终端驱动程序产生此信号,同时产生core文件(终端退出符) |
SIGINT | 中断键 delete / ctrl+c (终端中断符) |
SIGCHLD | 子进程终止时返回 (发送信号到父进程) |
SIGUSR1,SIGUSR2 | 用户自定义信号 |
SIGKILL,SIGSTOP | 不能被捕捉及忽略的,主要用于让进程可靠的终止和停止 |
可以编写一个php脚本使用 kill 命令来测试上面的信号
//demo1.php
fprintf(STDOUT,"PID: ".posix_getpid().PHP_EOL);
while(1){
;
}
-
自定义信号 SIGUSR1
-
作业控制信号 SIGSTOP 停止进程(停止并不是结束 使用
ps -aux|grep php
发现进程只是停止状态)
[1]+ 是作业编号 可以使用
jobs
命令查看当前终端作业
中断信号处理程序
- pcntl_signal() 信号处理函数
- 第一个参数$signo 信号编号 | 信号名字
- 第二个参数 $handler 中断信号处理程序 | 信号处理函数 | 信号处理程序
- 第三个参数 $restart_syscall = true 这个参数表示是否要重启被中断的系统调用
php解释器执行程序都是调用库函数或者系统调用函数由系统内核提供中断系统调用概念
当进程正在执行系统调用的时候,接收到中断信号,那么这个系统调用就会被中断。比如进程正在写文件途中接收到中断信号,写操作将无法恢复。
如果能恢复中断前正在执行的函数我们称为:可重入函数
,否则就是非可重入函数
如果中断后不能恢复,系统调用函数会返回 -1 ,errno 会设置为EINTR 中断错误,在编写网络程序时比较常用。
# 安装信号处理器;
pcntl_signal(SIGINT, function($signo){
fprintf(STDOUT,"我接收到一个信号,编号为:%d \n", $signo);
});
// 循环等待信号
while(1){
# 分发
pcntl_signal_dispatch();
fprintf(STDOUT,posix_getpid()."进程运行中\n");
sleep(1);
}
执行php脚本 按下ctrl+c
或者使用 kill -s SIGINT 31193
发送信号,pcntl_signal 函数捕获信号
注意pcntl_signal 回调函数内建议不要写过多业务逻辑,可以造成错误,一般我们把中断信号用于通知。
每个信号都有相应的动作(信号处理程序)
- 用户自定义的中断信号处理程序
- SIG_DEL 系统默认动作
pcntl_signal(SIGINT, SIG_DEL);
- 忽略 SIG_IGN(ignore)
pcntl_signal(SIGINT, SIG_IGN);
SIGKILL SIGSTOP
信号无法捕捉,忽略
进程启动的时候,信号的动作默认是系统行为,如果你在编写信号对应的处理程序,就会覆盖掉原来的动作。但是
SIGKILL SIGSTOP
信号无法覆盖
- 我们知道 fork() 函数通过系统调用创建一个与原来进程几乎完全相同的进程,那父进程的信号处理程序 子进程会继承吗?
function sigHandler($signo){
fprintf(STDOUT,"pid:".posix_getpid()."接收到一个信号,编号为:%d \n", $signo);
}
# 安装信号处理器;
pcntl_signal(SIGINT, 'sigHandler');
//fork 进程
$pid = pcntl_fork();
// 循环等待信号
while(1){
# 分发
pcntl_signal_dispatch();
fprintf(STDOUT,posix_getpid()."进程运行中\n");
sleep(1);
}
通过执行上面脚本 按下ctrl+c
或者对子进程单独发送信号,子进程是可以捕获的。所以说当父进程创建一个子进程的时候,子进程是继承父进程的中断信号程序的。
如果想子进程不继承父进程中断信号处理程序,覆盖掉即可 比如:
function sigHandler($signo){
fprintf(STDOUT,"pid:".posix_getpid()."接收到一个信号,编号为:%d \n", $signo);
}
# 安装信号处理器;
pcntl_signal(SIGINT, 'sigHandler');
//fork 进程
$pid = pcntl_fork();
if($pid == 0){
//已经重设信号处理
pcntl_signal(SIGINT, function($signo){
fprintf(STDOUT,"pid:".posix_getpid()."我是子进程我接收到一个信号,编号为:%d \n", $signo);
});
}
// 循环等待信号
while(1){
# 分发
pcntl_signal_dispatch();
fprintf(STDOUT,posix_getpid()."进程运行中\n");
sleep(1);
}
信号集
- 信号集是指信号的集合
- 主进程可以选择某些信号,被阻塞的信号集称为阻塞信号集,或者叫信号屏蔽字Block
- 当进程阻塞了某个信号(php 通过 pcntl_sigprocmask 来设置信号屏蔽字)
- pcntl_sigprocmask
- 第一个参数$how 是设置信号阻塞还是移除信号阻塞
- 第二个参数 $set 阻塞信号集合
- 第三个参数 $oldset 获取旧的阻塞信号集合
# 安装信号处理器;
pcntl_signal(SIGINT, function($signo){
fprintf(STDOUT,"我接收到一个信号,编号为:%d \n", $signo);
});
// 定义阻塞信号集
$sigset = [SIGINT,SIGUSR1];
//设置阻塞
pcntl_sigprocmask(SIG_BLOCK, $sigset);
// 循环等待信号
while(1){
# 分发
pcntl_signal_dispatch();
fprintf(STDOUT,posix_getpid()."进程运行中\n");
sleep(1);
}
执行脚本使用 kill -s SIGINT 3009
发送信号并没有执行捕获而是阻塞挂起了。
- 解除信号屏蔽
pcntl_signal(SIGINT, function($signo){
fprintf(STDOUT,"我接收到一个信号,编号为:%d \n", $signo);
});
// 定义阻塞信号集
$sigset = [SIGINT,SIGUSR1];
//设置阻塞
pcntl_sigprocmask(SIG_BLOCK, $sigset);
// 循环等待信号
$i = 10;
while($i--){
# 分发
pcntl_signal_dispatch();
fprintf(STDOUT,posix_getpid()."进程运行中\n");
sleep(1);
if($i == 5){
fprintf(STDOUT,"屏蔽已解除\n");
//解除信号屏蔽
//$oldset 会返回之前阻塞的信号集| 信号屏蔽字
pcntl_sigprocmask(SIG_UNBLOCK,[SIGINT,SIGUSR1], $oldset);
print_r($oldset);
}
}
可以看到执行结果,在移除屏蔽时使用ctrl+c
没有捕获,屏蔽移除后,使用 ctrl+c
可以捕获到信号发送,并且得到了之前阻塞的信号集
发送信号
1.发送信号的方式
- kill -s 信号编号 | 信号名字 进程pid
- 在程序中使用 posix_kill 给一个指定的进程或是进程组发送信号。
- pcntl_alarm 函数产生 SIGALRM 信号
- 在终端按下特殊键
ctrl+c, ctrl+z ,ctrl+\
- 网络端对端通讯产生SIGURG信号,读写管道文件也会产生SIGPIPE信号,SIGCHLD(当子进程结束时候产生),这些信号由内核,操作系统发送
posix_kill
第一个参数:
pid 大于0 的情况是指定某个进程发送信号
pid 等于0 的情况是发送信号给进程组中每个进程
pid 等于-1
的情况可以自己在虚拟机上试试,千万别在线上环境测试,可能造成系统出现问题
pcntl_signal(SIGINT,function($signo){
fprintf(STDOUT,"PID %d 接收到 %d 信号 \n",posix_getpid(),$signo);
});
//$mapPid 里面是兄弟进程关系
$mapPid = [];
$pid = pcntl_fork();
if($pid > 0){
$mapPid[] = $pid;
$pid = pcntl_fork();
if($pid > 0){
$mapPid[] = $pid;
while(1){
pcntl_signal_dispatch();
//posix_kill 第一个参数pid大于0指定某个进程发送
//posix_kill pid 等于0 发送信号给进程组中每个进程
posix_kill($mapPid[0],SIGINT);
sleep(2);
}
exit(0);
}
}
// 这里是子进程代码
while(1){
pcntl_signal_dispatch();
fprintf(STDOUT, "pid=%d ppid=%d pgid=%d ...\n",posix_getpid(),posix_getppid(),posix_getpgrp());
sleep(2);
}
执行上面代码只有第一个子进程收到信号,两个子进程为兄弟进程关系
- 设置 posix_kill 函数 pid 等于0 发送信号给进程组中每个进程,兄弟进程与父进程同属一个进程组,
pcntl_signal(SIGINT,function($signo){
fprintf(STDOUT,"PID %d 接收到 %d 信号 \n",posix_getpid(),$signo);
});
//$mapPid 里面是兄弟进程关系
$mapPid = [];
$pid = pcntl_fork();
if($pid > 0){
$mapPid[] = $pid;
$pid = pcntl_fork();
if($pid > 0){
$mapPid[] = $pid;
while(1){
pcntl_signal_dispatch();
//posix_kill 第一个参数pid大于0指定某个进程发送
//posix_kill pid 等于0 发送信号给进程组中每个进程
posix_kill(0,SIGINT);
sleep(2);
}
exit(0);
}
}
// 这里是子进程代码
while(1){
pcntl_signal_dispatch();
fprintf(STDOUT, "pid=%d ppid=%d pgid=%d ...\n",posix_getpid(),posix_getppid(),posix_getpgrp());
sleep(2);
}
父子进程都捕获到了信号
SIGALRM 信号
- 一个定时信号 php中由 pcntl_alarm 函数实现
# 安装信号处理器;
pcntl_signal(SIGALRM, function($signo){
fprintf(STDOUT,"我接收到一个信号,编号为:%d \n", $signo);
});
pcntl_alarm(2);
// 循环等待信号
while(1){
# 分发
pcntl_signal_dispatch();
fprintf(STDOUT,posix_getpid()."进程运行中\n");
sleep(1);
}
执行脚本两秒后捕获到SIGALRM
信号
2. 实现一个每两秒执行一次的函数
function sigHandler($signo){
fprintf(STDOUT,"我接收到一个信号,编号为:%d \n", $signo);
//再次设置定时信号
pcntl_alarm(2);
}
# 安装信号处理器;
pcntl_signal(SIGALRM, 'sigHandler');
//设置定时信号
pcntl_alarm(2);
// 循环等待信号
while(1){
# 分发
pcntl_signal_dispatch();
fprintf(STDOUT,posix_getpid()."进程运行中\n");
sleep(1);
}
执行脚本得到有规律的信号发送,2秒一次
3. 需要注意 每次对 pcntl_alarm()
函数的调用都会取消之前设置的alarm信号。也就是说只会执行最后一个 pcntl_alarm()
函数的调用,之前设置的无效
# 安装信号处理器;
pcntl_signal(SIGALRM, function($signo){
fprintf(STDOUT,"我接收到一个信号,编号为:%d \n", $signo);
});
pcntl_alarm(1);
pcntl_alarm(3);
pcntl_alarm(5);
// 循环等待信号
while(1){
# 分发
pcntl_signal_dispatch();
fprintf(STDOUT,posix_getpid()."进程运行中\n");
sleep(1);
}
可以看到脚本执行到第5秒,捕获到信号,而不是第1秒或者第3秒
4. 如果 pcntl_alarm()
函数 参数为0,则之前设置的闹钟信号会被取消,并不会触发信号捕获函数
5. 翻看workerman定时器源码发现它也是使用SIGALRM 闹钟信号实现
SIGCHLD信号
- SIGCHLD 信号默认忽略
- 可以解决僵尸进程问题,及时回收子进程。
# 安装信号处理器;
pcntl_signal(SIGCHLD, function($signo){
fprintf(STDOUT,"我接收到一个信号,编号为:%d \n", $signo);
$pid = pcntl_waitpid(-1, $status, WNOHANG);
if($pid > 0){
fprintf(STDOUT,"PID=%d 子进程退出了",$pid);
}
});
$pid = pcntl_fork();
if($pid > 0){
// 循环等待信号
while(1){
# 分发
pcntl_signal_dispatch();
fprintf(STDOUT,posix_getpid()."进程运行中\n");
sleep(1);
}
}else{
fprintf(STDOUT,"PID=%d 子进程结束 \n",posix_getpid());
exit(10);
}
pcntl_signal 缺点(下面是引用韩天峰大佬的文章 原文地址 PHP官方的pcntl_signal性能极差)
-
PHP官方的pcntl_signal性能不好,因为php的信号处理函数是基于
ticks
来实现的,而不是注册到真正系统底层的信号处理函数中。而如果使用ticks的话,比如delare ticks=1
, 那么每执行一条php语句都会调用上面的函数一次。而实际大部分时间里面并没有信号需要处理,所以这会造成极大的浪费。 -
如果一个服务器程序1秒中接收1000次请求,平均每个请求要执行1000行PHP代码。那么PHP的pcntl_signal,就带来了额外的 1000 * 1000,也就是100万次空的函数调用。这样会浪费大量的CPU资源。
-
通过查看
pcntl.c
的源码实现发现。pcntl_signal的实现原理是,触发信号后先将信号加入一个队列中。然后在PHP的ticks回调函数中不断检查是否有信号,如果有信号就执行PHP中指定的回调函数,如果没有则跳出函数。
PHP_MINIT_FUNCTION(pcntl)
{
php_register_signal_constants(INIT_FUNC_ARGS_PASSTHRU);
php_pcntl_register_errno_constants(INIT_FUNC_ARGS_PASSTHRU);
php_add_tick_function(pcntl_signal_dispatch TSRMLS_CC);
return SUCCESS;
}
pcntl_signal_dispatch 函数的实现:
void pcntl_signal_dispatch()
{
//.... 这里略去一部分代码,queue即是信号队列
while (queue) {
if ((handle = zend_hash_index_find(&PCNTL_G(php_signal_table), queue->signo)) != NULL) {
ZVAL_NULL(&retval);
ZVAL_LONG(¶m, queue->signo);
/* Call php signal handler - Note that we do not report errors, and we ignore the return value */
/* FIXME: this is probably broken when multiple signals are handled in this while loop (retval) */
call_user_function(EG(function_table), NULL, handle, &retval, 1, ¶m TSRMLS_CC);
zval_ptr_dtor(¶m);
zval_ptr_dtor(&retval);
}
next = queue->next;
queue->next = PCNTL_G(spares);
PCNTL_G(spares) = queue;
queue = next;
}
}
- 比较好的做法是去掉ticks,转而使用
pcntl_signal_dispatch
,在代码循环中自行处理信号。workerman 就没有使用declare ticks
,而是在主事件循环中调用pcntl_signal_dispatch
函数实现,这样就把pcntl_signal_dispatch
的调用频率下降了很多,而且还保证能达到近似实时的信号处理。 - 事实上,一般需要信号处理的代码都是后端服务程序,而一般的后端服务程序都是按照事件处理的结构来编写的,也就是说,这种程序里面必定会有个主事件循环。在事件循环的每次循环中主动调用pcntl_signal_dispatch,就能基本实时的把信号处理掉,而且还能保证一个比较好的性能。
- 而swoole中因为底层是C实现的,信号处理不受PHP的影响。swoole使用了目前Linux系统中最先进的signalfd来处理信号,几乎是没有任何额外消耗的。
- 所有最好使用swoole正确的编写需要php处理信号功能的代码。