linux php 进程进阶(五) signal(信号)
signal
信号与中断流程介绍
信号 是指软件中断信号,简称软中断
- 中断源(中断信号产生位置):
- 软件产生
- 中断响应(对信号的处理)
-
中断返回:中断信号处理程序(信号处理函数,信号捕捉函数)完成后,就会返回继续执行主函数
-
中断处理过程图
中断源向进程发出中断信号称为中断请求信号
,
进程接收到中断信号可以进行响应,称为中断响应
为中断服务的程序称为中断处理程序 或叫 中断处理函数 | 中断服务程序
执行完中断处理函数后放回,中断放回
常用中断信号
信号 | 介绍 |
---|---|
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
发现进程只是停止状态)
中断信号处理程序
- pcntl_signal() 信号处理函数
# 安装信号处理器;
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 函数捕获信号
- 我们知道 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
# 安装信号处理器;
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.发送信号的方式
posix_kill
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处理信号功能的代码。