信号是事件发生时对进程的通知机制,有时又称为软件中断。一个进程可以向另一个进程发送信号,比如子进程结束时都会向父进程发送一个SIGCHLD(17号信号)来通知父进程,所以有时信号也被当作一种进程间通信的机制。
在linux系统下,通常我们使用 kill -9 XXPID来结束一个进程,其实这个命令的实质就是向某进程发送 SIGKILL(9号信号),对于在前台运行的程序我们通常用 Ctrl + c 快捷键来结束运行,该快捷键的实质是向当前进程发送 SIGINT (2号信号),而进程收到该信号的默认行为是结束运行。我们可以用命令 kill -l 来查看系统的信号列表:
其中1 ~ 31 号信号为标准信号或者传统信号,而大于31号信号为实时信号,这里我们主要介绍 标准信号。进程收到一个信号时,视信号的不同,有以下几种不同的行为:
1)忽略信号,进程就像没收到过信号一样,比如父进程收到子进程发送的 SIGCHLD 信号
2)结束进程, 比如进程收到 SIGINT (Ctrl + c) 信号
3)暂停运行
4)从之前的暂停状态恢复运行
PHP的pcntl扩展以及posix扩展为我们提供了若干操作信号的方法:
pcntl_signal_dispatch — 调用等待信号的处理器
pcntl_signal_get_handler — Get the current handler for specified signal
pcntl_signal — 安装一个信号处理器
pcntl_sigprocmask — 设置或检索阻塞信号
pcntl_sigtimedwait — 带超时机制的信号等待
pcntl_sigwaitinfo — 等待信号
posix_kill — 向一个进程发送信号
pcntl_signal 方法可以让我们自定义进程对信号的处理动作,但是在linux系统中,SIGKILL(9号信号)和 SIGSTOP (19号信号)这两个信号是无法被我们自己捕获和处理的,SIGKILL总是会结束进程运行,SIGSTOP总是能暂停进程。
bool pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls = true ] )
signo
信号编号
handler
信号处理器可以是用户创建的函数或方法的名字,也可以是系统常量 SIG_IGN(译注:忽略信号处理程序)或SIG_DFL(默认信号处理程序)
在PHP中,有自己触发信号回调的机制
PCNTL现在使用了ticks作为信号处理的回调机制,ticks在速度上远远超过了之前的处理机制。
这个变化与“用户ticks”遵循了相同的语义。您可以使用declare() 语句在程序中指定允许发生回调的位置。这使得我们对异步事件处理的开销最小化。
在编译PHP时 启用pcntl将始终承担这种开销,不论您的脚本中是否真正使用了pcntl。 PHP 4.3.0使用ticks作为信号处理回调机制,这比以前的机制快了很多。
这个变化与 "用户ticks" 遵循了相同的语义。您可以使用declare() 语句在程序中指定允许发生回调的位置。
所以,在使用pcntl_signal方法之前,我们先要了解下PHP中的 “用户ticks”,而 “用户ticks” 又牵涉到 PHP中的流程控制结构 declare
文档中对declare介绍的已经比较清楚了:
declare 结构用来设定一段代码的执行指令。declare 的语法和其它流程控制结构相似:
declare (directive)
statement
directive 部分允许设定 declare 代码段的行为。目前只认识两个指令:ticks(更多信息见下面 ticks 指令)以及 encoding(更多信息见下面 encoding 指令)。
declare 代码段中的 statement 部分将被执行——怎样执行以及执行中有什么副作用出现取决于 directive 中设定的指令。
declare 结构也可用于全局范围,影响到其后的所有代码(但如果有 declare 结构的文件被其它文件包含,则对包含它的父文件不起作用)。
declare 目前只支持 ticks 和 encoding 两个指令,这里我们只介绍 ticks,而 encoding指令也很简单,有兴趣可以自己研究
Tick(时钟周期)是一个在 declare 代码段中解释器每执行 N 条可计时的低级语句就会发生的事件。N 的值是在 declare 中的 directive 部分用 ticks=N 来指定的。
不是所有语句都可计时。通常条件表达式和参数表达式都不可计时。
在每个 tick 中出现的事件是由 register_tick_function() 来指定的。更多细节见下面的例子。注意每个 tick 中可以出现多个事件。
也就是说PHP解释器每执行N条可计时的语句就会触发一个tick事件,但是文档中对可计时语句没有明确界定,下面代码演示下。
<?php
//注册一个tick回调函数
register_tick_function(function(){
echo "触发了ticks " . microtime(TRUE) . PHP_EOL;
});
//每执行两条语句触发一个tick
declare(ticks=2)
{
$a = 1;
$a = 2;
$a = 3;
$a = 4;
$a = 5;
}
//declare 结构外面的语句不会触发tick
$a = 1;
$a = 2;
$a = 3;
$a = 4;
$a = 5;
执行结果:
[root@localhost signal]# php ticks.php
触发了ticks 1503549318.3483
触发了ticks 1503549318.3484
再看一个例子:
<?php
//注册一个tick回调函数
register_tick_function(function(){
echo "触发了ticks " . microtime(TRUE) . PHP_EOL;
});
$a = 0;
//每执行6条语句触发一个tick
declare(ticks=6)
{
while(1)
{
$a++;
echo '$a = ' . $a . PHP_EOL;
sleep(1);
}
}
$a = 1;
$a = 2;
$a = 3;
运行结果:
[root@localhost signal]# php ticks.php
$a = 1
$a = 2
触发了ticks 1503549933.6921
$a = 3
$a = 4
触发了ticks 1503549935.6946
$a = 5
$a = 6
触发了ticks 1503549937.6983
$a = 7
$a = 8
触发了ticks 1503549939.7012
$a = 9
...
了解了ticks机制,我们来演示下 pcntl_signal 函数:
<?php
// 为 2号 信号注册信号处理函数
pcntl_signal(SIGINT, function(){
echo "捕获到了 SIGINT 信号" . PHP_EOL;
});
declare(ticks = 1)
{
$a = 0;
while(1)
{
$a++;
echo $a . PHP_EOL;
sleep(1);
}
}
这个程序会一直打印$a的值,当我们按下 Ctrl + c 时,就给程序发送了一个 SIGINT 信号,但由于我们自定义了信号处理,所以这时不会结束进程,而是打印一个字符串。
[root@localhost signal]# php pcntl_signal.php
1
2
3
4
^C捕获到了 SIGINT 信号
5
6
7
8
9
^C捕获到了 SIGINT 信号
10
11
^C捕获到了 SIGINT 信号
12
^C捕获到了 SIGINT 信号
这时我们可以用 Ctrl + \ (SIGQUIT )来结束程序。
PHP中这种ticks 触发信号处理函数的机制导致了PHP在对信号处理时有很大的缺陷,如果PHP中有造成阻塞的语句,由于语句无法执行结束,无法触发tick事件,信号处理函数也就不会被回调。比如编写一个socket服务端程序:
<?php
// 为 SIGINT 信号注册信号处理函数
pcntl_signal(SIGINT, function(){
echo "捕获到了 SIGINT 信号" . PHP_EOL;
});
declare(ticks=1)
{
$servsock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($servsock, '127.0.0.1', 8888);
socket_listen($servsock, 1024);
while(1)
{
$connsock = socket_accept($servsock); //如果没有客户端过来连接,这里将一直阻塞
if ($connsock)
{
echo "客户端连接服务器: $connsock\n";
}
}
}
运行后代码一直阻塞在 socket_accept 函数这个地方等待客户端连接,这时的信号处理函数将无法被调用,直到某个客户端来连接:
[root@localhost signal]# php socket.php
^C^C^C^C^C捕获到了 SIGINT 信号
捕获到了 SIGINT 信号
捕获到了 SIGINT 信号
捕获到了 SIGINT 信号
捕获到了 SIGINT 信号
客户端连接服务器: Resource id #5
^C^C^C^\Quit
连按了五次 Ctrl + c ,信号函数都没有被调用,然后有一个客户端连接到了服务器,信号处理函数连续被调用了五次。
作为对比,我这里用C写一个逻辑相同的程序来做对比,看看C语言里的信号处理是否有这种情况存在:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
void sighandler(int signo)
{
if (signo == SIGINT)
{
printf("捕获到了 SIGINT 信号\n");
}
}
int main()
{
if (signal(SIGINT, sighandler) == SIG_ERR)
{
printf("注册信号处理方法失败\n");
exit(1);
}
int servfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr, clientaddr;
socklen_t st = sizeof(clientaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8888);
servaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
bind(servfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(servfd, 1024);
while(1)
{
int connfd = accept(servfd, (struct sockaddr *)&clientaddr, &st);
if (connfd > 0)
{
printf("客户端连接到服务器: %d\n", connfd);
}
}
}
编译后运行:
[root@localhost signal]# ./a.out
^C捕获到了 SIGINT 信号
^C捕获到了 SIGINT 信号
^C捕获到了 SIGINT 信号
^C捕获到了 SIGINT 信号
客户端连接到服务器: 4
^C捕获到了 SIGINT 信号
^C捕获到了 SIGINT 信号
^\Quit
可以看到,同样是阻塞的状态,C中的信号函数仍旧能被正常调用,这也暴露了PHP中的信号触发机制是存在缺陷的,并且这种缺陷会让我们有时在处理信号时变的非常麻烦,尤其是代码中存在阻塞的情况,信号处理函数很可能不能被触发。后续说到守护进程时还会再提到这个问题。
posix_kill 可以在代码中向指定程序发送信号
bool posix_kill ( int $pid , int $sig )
比如在程序中向自己发送信号:
<?php
pcntl_signal(SIGINT, function(){
echo "捕获到 SIGINT 信号\n";
posix_kill(posix_getpid(), SIGQUIT); //向自己发送 SIGQUIT 信号
});
pcntl_signal(SIGQUIT, function(){
echo "catch signal SIGQUIT \n";
});
declare(ticks = 1)
{
while(1)
{
sleep(1);
}
}
运行:
[root@localhost signal]# php posix_kill.php
^C捕获到 SIGINT 信号
catch signal SIGQUIT
不过现在想要结束程序,需要使用 kill -9 了。
PHP7.1信号新特性 -- pcntl_async_signals() 方法
一个新的名为 pcntl_async_signals() 的方法现在被引入, 用于启用无需 ticks (这会带来很多额外的开销)的异步信号处理。详情请查看 PHP7.1新特性
pcntl_async_signals — 开启/关闭异步信号处理或返回当前的设定
bool pcntl_async_signals ([ bool $on
= NULL
] )
如果不传参数, pcntl_async_signals() 返回当前是否开启了异步信号处理, 如果传参就是设置是否开启异步信号处理
<?php
$status = pcntl_async_signals();
var_dump($status);
pcntl_async_signals(true);
$status = pcntl_async_signals();
var_dump($status);
[root@localhost php]# php72 pcntl_async_signals.php
bool(false)
bool(true)
看一个简单demo:
<?php
pcntl_async_signals(true); //开启异步信号处理
pcntl_signal(SIGINT, function(){
echo '捕获到SIGINT信号' . PHP_EOL;
});
$i = 0;
while(1)
{
echo $i++ . PHP_EOL;
sleep(1);
}
以上代码不停的打印数字,当键入ctrl+c 向进程发送SIGINT信号时,打印一句话,可以看到不需要再把代码放在ticks里了
[root@localhost php]# php72 pcntl_async_signals.php
0
1
2
^C捕获到SIGINT信号
3
4
5
^C捕获到SIGINT信号
6
7
8
9
^C捕获到SIGINT信号
10
11
^\Quit
二、信号屏蔽
信号中还有一个重要概念是信号屏蔽,我们可以对进程设置暂时屏蔽某些信号,进程中有标记哪些信号被屏蔽的一个“列表”,称之为信号屏蔽字,这时再向进程发送处于被屏蔽的信号,信号不会立即送达给进程,而是被存入称作信号未决字 的“列表”中,而当这些信号被解除屏蔽时,信号会被立即送达进程。
PHP中可以使用 pcntl_sigprocmask 方法来增加和解除信号屏蔽。
bool pcntl_sigprocmask ( int $how , array $set [, array &$oldset ] )
$how
设置pcntl_sigprocmask()函数的行为。 可选值:
SIG_BLOCK: 把信号加入到当前阻塞信号中。
SIG_UNBLOCK: 从当前阻塞信号中移出信号。
SIG_SETMASK: 用给定的信号列表替换当前阻塞信号列表。
$set
信号列表。
$oldset
oldset是一个输出参数,用来返回之前的阻塞信号列表数组。
代码示例:
<?php
pcntl_sigprocmask(SIG_BLOCK, array(SIGINT, SIGQUIT), $oldset); //屏蔽 SIGINT SIGQUIT 信号
print_r($oldset);
for($i = 0; $i < 15; $i++)
{
echo '$i = ' . $i . PHP_EOL;
sleep(1);
if ($i == 10)
{
pcntl_sigprocmask(SIG_UNBLOCK, array(SIGINT), $oldset); // 解除对 SIGINT 的屏蔽
echo "解除信号屏蔽\n";
}
}
程序运行时 我们发送一个SIGINT (Ctrl + c)给进程:
[root@localhost signal]# php pcntl_sigprocmask.php
Array
(
)
$i = 0
$i = 1
$i = 2
^C$i = 3
$i = 4
$i = 5
$i = 6
$i = 7
$i = 8
$i = 9
$i = 10
可以看到 一旦解除了信号屏蔽,信号屏蔽期间发送的信号会立即送达。 如果程序中的一段代码,我们要保证这段代码在执行过程中每次执行都能完整的执行完,就可以用信号的这个特点,比如:
<?php
while(1)
{
pcntl_sigprocmask(SIG_BLOCK, array(SIGINT, SIGQUIT, SIGTERM), $oldset); //进入循环时 屏蔽信号
/* 假设下面这段代码必需要完整执行 */
echo "----------------------start-----------------------\n";
echo "11111111\n";
sleep(1);
echo "22222222222\n";
sleep(1);
echo "33333333\n";
sleep(1);
echo "-------------------------end-----------------------\n";
pcntl_sigprocmask(SIG_UNBLOCK, array(SIGINT, SIGQUIT, SIGTERM), $oldset); //代码块执行完解除信号屏蔽
}
这样就可以确保无论什么时候向进程发送信号,这个代码块总能执行完程序才会退出:
[root@localhost signal]# php pcntl_sigprocmask2.php
----------------------start-----------------------
11111111
22222222222
33333333
-------------------------end-----------------------
----------------------start-----------------------
11111111
^C22222222222
33333333
-------------------------end-----------------------
[root@localhost signal]#
后面说到守护进程时,为了确保子进程把任务执行完才退出,我们也会用到这个技术。
PHP中信号就简单介绍到这里,如果想更深入的了解,欢迎关注后续文章^^