PHP

信号

Posted by epimetheusQ on 2021-09-10

信号

关于php的declare语句中的tick的解释

什么是declare

declare是php的流程控制结构,directive目前支持两个指令(ticks和encoding),ticks的使用配合register_tick_function()(当然还有unregister_tick_function())使用。declare结构用来设定一段代码的执行指令。declare的语法和其它流程控制结构相似:

1
2
declare(directive)
statement

declare:声明,宣布
directive:指示,指令
statement:声明,陈述

tick是在一个declare代码段中解释器每执行N条低级语句就会发生的事件。N的值是在declare中的directive部分用tick=N来指定。

在每个tick中出现的事件是由register_tick_function()来指定的。

通俗解释一下:

ticks参数标识运行多少语句调用一次register_tick_function()。
register_tick_function函数定义了每个tick事件发生时的处理函数。那么什么是tick事件?
tick是一个事件,tick时间在php每执行N条低级语句就发生一次,N由declare语句指定。
可以用register_tick_function()来指定tick事件发生时应该执行的操作。

什么是低级语句

1.简单语句:空语句(就一个;号),return,break,continue,throw, goto,global,static,unset,echo, 内置的HTML文本,分号结束的表达式等均算一个语句。
2.复合语句:完整的if/elseif,while,do...while,for,foreach,switch,try...catch等算一个语句。
3.语句块:{} 括出来的语句块。
4.最后特别的:declare块本身也算一个语句(按道理declare块也算是复合语句,但此处特意将其独立出来)。

declare用法

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
function doticks (){
echo 'ticks';
}

register_tick_function('doticks');

declare(ticks = 1) {
for ($x = 1; $x < 10; $x++) {
echo $x * $x . '<br />';
}
}

运行结果是:

1
2
3
4
5
6
7
8
9
10
1
ticksticks4
ticksticks9
ticksticks16
ticksticks25
ticksticks36
ticksticks49
ticksticks64
ticksticks81
ticksticksticksticks

解释:

首先完整的for循环算一个语句,但必须要等循环结束才算,因此在编译时for循环里面的echo 算第一个语句。
所以第一个doticks是在第一个echo后执行的,也就是1输出后才发生第一个tick事件。
在$x 从1到9的循环中,每个循环包括两个语句,一个echo, 一个for循环。在81输出后,因为echo是一条语句,因此输出第一个ticks。
同时$x=9的这个for循环也结束了。

这是只是到ticksticksticks81这里!那最后一行连续输出4个ticks是怎么实现的呢?

1.81输出后,因为本身是echo语句,所以算一个低级语句,输出一个ticks。
2.81输出后,本次循环完成,算一个低级语句,输出一个ticks。
3.当$x=10时,不满足循环条件,该for循环结束,输出一个ticks。
4.declare之前强调的,本身就属于一个低级语句,输出一个ticks。

php PCNTL函数

了解了ticks机制,我们演示一下pcntl函数

pcntl函数简介

PHP的进程控制支持实现了Unix方式的进程创建,程序执行,信号处理以及进程的中断。进程控制不能被应用在Web服务器环境,当其被用于Web环境时可能会带来意外的结果。

PCNTL现在使用ticks作为信号处理的回调机制,ticks在速度上远远超过了之前的处理机制。这个变化与“用户ticks”遵循了相同的语义。您可以使用declare()语句在程序中指定允许发生回调的位置。这使得我们对异步事件处理的开销最小化。在编译PHP时,启用pcntl将始终承担这种开销,无论您的脚本中是否真正使用了pcntl。

信号常量介绍

SIGHUP = 1
SIGINT = 2
SIGQUIT = 3
SIGILL = 4
SIGTRAP = 5
SIGABRT = 6
SIGIOT = 6
SIGBUS = 10
SIGFPE = 8
SIGUSR1 = 30
SIGSEGV = 11
SIGUSR2 = 31
SIGPIPE = 13
SIGALRM = 14
SIGTERM = 15
SIGSTKFLT not defined 
SIGCLD not defined 
SIGCHLD = 20
SIGCONT = 19
SIGTSTP = 18
SIGTTIN = 21
SIGTTOU = 22
SIGURG = 16
SIGXCPU = 24
SIGXFSZ = 25
SIGVTALRM = 26
SIGPROF = 27
SIGWINCH = 28
SIGPOLL not defined 
SIGIO = 23
SIGPWR not defined 
SIGSYS = 12
SIGBABY = 12
SIG_BLOCK = 1
SIG_UNBLOCK = 2
SIG_SETMASK = 3
SIGHUP     终止进程     终端线路挂断
SIGINT     终止进程     中断进程
SIGQUIT   建立CORE文件终止进程,并且生成core文件
SIGILL   建立CORE文件       非法指令
SIGTRAP   建立CORE文件       跟踪自陷
SIGBUS   建立CORE文件       总线错误
SIGSEGV   建立CORE文件       段非法错误
SIGFPE   建立CORE文件       浮点异常
SIGIOT   建立CORE文件       执行I/O自陷
SIGKILL   终止进程     杀死进程
SIGPIPE   终止进程     向一个没有读进程的管道写数据
SIGALARM   终止进程     计时器到时
SIGTERM   终止进程     软件终止信号
SIGSTOP   停止进程     非终端来的停止信号
SIGTSTP   停止进程     终端来的停止信号
SIGCONT   忽略信号     继续执行一个停止的进程
SIGURG   忽略信号     I/O紧急信号
SIGIO     忽略信号     描述符上可以进行I/O
SIGCHLD   忽略信号     当子进程停止或退出时通知父进程
SIGTTOU   停止进程     后台进程写终端
SIGTTIN   停止进程     后台进程读终端
SIGXGPU   终止进程     CPU时限超时
SIGXFSZ   终止进程     文件长度过长
SIGWINCH   忽略信号     窗口大小发生变化
SIGPROF   终止进程     统计分布图用计时器到时
SIGUSR1   终止进程     用户定义信号1
SIGUSR2   终止进程     用户定义信号2
SIGVTALRM 终止进程     虚拟计时器到时

pcntl函数

pcntl_signal($signo, $handler); 为signo指定的信号安装一个新的信号处理器。

signo 信号编号,在php中为给定的常量,可以使用 handler 信号处理器可以是用户创建的函数方法或者名字,也可以是系统常量 SIG_IGN(忽略信号处理程序)或SIG_DFL(默认信号处理程序)。

posix_kill($pid, $signo); 在代码中向指定程序发送信号。

$pid 是进程的pid号码,$signo代表要对该进程执行的操作,也就是信号常量

pcntl_async_signals($bools);查看或者开启/关闭异步信号处理。

$bools就是可以添加bool值,用于启动无需ticks(这会带来很多额外开销的异步信号处理),开启/关闭异步信号处理或者返回当前的设定,如果不传参数pcntl_async_signals()返回当前是否开启了异步信号处理,如果传参数就是设置是否开启异步信号处理。使用该信号可以不需要再把代码放在ticks里

pcntl_sigprocmask($how, $set, $oldset);增加或解除信号屏蔽。

$how有三个值:SIG_BLOCK把信号加入到当前阻塞信号中。SIG_UNBLOCK从当前阻塞信号中移出信号。SIG_SETMASK用给定的信号列表替换当前阻塞信号列表, $set为信号列表, $oldset为一个输出参数,用来返回之前的阻塞信号列表数组

pcntl_alarm()
为进程设置一个alarm闹钟信号

pcntl_errno()
设置别名

pcntl_exec()
在当前进程空间执行指定程序

pcntl_fork()
在当前进程当前位置产生分支(子进程)。

pcntl_getpriority()
获取任意进程的优先级

pcntl_setpriority()
修改任意进程的优先级

pcntl_signal_dispatch()
调用等待信号的处理器

pcntl_sigprocmask()
设置或者检索阻碍信号

pcntl_sigtimedwait()
带超时机制的信号等待

pcntl_sigwaitinfo()
等待信号

pcntl_wait()
pcntl_waitpid()
等待或返回fork的子进程状态

pcntl_wexitstatus()
返回一个中断的子进程的返回码

pcntl_wifexited()
检查状态码是否代表一个正常的退出

pcntl_wifsignaled()
检查子进程状态码是否代表由于某个信号而中断

pcntl_wifstopped()
检查子进程当前是否已经停止

pcntl_wstopsig()
返回导致子进程停止的信号

pcntl_wtermsig()
返回导致子进程中断的信号

pcntl_strerror()
检索与抛出errno关联的系统错误消息

pcntl_signal_get_handler()
获取指定信号的当前处理程序

pcntl_get_last_error()
返回最后一个错误异常

pcntl函数用法

pcntl_signal()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

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信号,但是由于我们自定义了信号处理,所以这时候不会结束进程,而是继续打印一个字符串。

打印结果:

1
2
3
4
5
6
7
8
9
1
2
^C捕捉到了SIGINT信号
3
4
5
6
7
^\[1] 798 quit php declare.php

其中,最后一行是我按下Ctrl + \ ,也就是发送信号 SIGQUIT 来结束程序。

posix_kill()

我们尝试使用一下posix_kill()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

// 若捕捉到前端按键 ctrl + c ,则在终端打印echo 文字,并且对当前进程执行关闭操作。
pcntl_signal(SIGINT, function () {
echo "捕获到SIGINT 信号 \n";
posix_kill(posix_getpid(), SIGQUIT);
});

pcntl_signal(SIGQUIT, function () {
echo "catch signal SIGQUIT \n";
});

declare(ticks = 1) {
while(1){
sleep(1);
}
}

?>

上面程序意思是,当用户输入ctrl + c 或者 ctrl + \的时候都关闭该进程。运行结果是:

1
2
^C捕获到 SIGINT 信号
catch signal SIGQUIT

linux结束程序,使用命令 kill -9 pid。

pcntl_async_signals()

下面介绍一下php7.1信号新特性 pcntl_async_signals(),写个例子我们可以先看是否开启了异步信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

// 查看当前是否开启异步信号
$status = pcntl_async_signals();
var_dump($status);

// 开启异步信号
pcntl_async_signals(true);

// 查看当前是否开启异步信号
$status = pcntl_async_signals();
var_dump($status);

?>

我们可以查看一下返回值

1
2
bool(false)
bool(true)

我们可以通过上面这个例子,在写一个简单的应用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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中了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0
1
2
^C捕获到SIGINT信号
3
4
5
^C捕获到SIGINT信号
6
7
8
9
^C捕获到SIGINT信号
10
11
^\Quit
pcntl_sigprocmask()

我们还可以进行简单的信号屏蔽,利用函数pcntl_sigprocmask()来实现。

信号中还有一个很重要的概念是信号屏蔽,我们可以对进程设置暂时屏蔽某些信号,进程中有标记那些信号被屏蔽的一个“列表”,称之为信号屏蔽字,这是在向进程发送处于被屏蔽的信号,信号不会立即送达进程,而是被存入称作“信号未决字”的列表中,而这些信号被解除屏蔽时,信号会被立即送达进程。pcntl_sigprocmask方法来增加和解除信号屏蔽。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

// 屏蔽 SIGINT SIGQUIT 信号
pcntl_sigprocmask(SIG_BLOCK, [SIGINT, SIGQUIT], $oldset);
print_r($oldset);

for($i = 0; $i<=100;$i++) {
echo '$i = ' . $i . PHP_EOL;
sleep(1);

if ($i == 10) {
pcntl_sigprocmask(SIG_UNBLOCK, [SIGINT], $oldset);
echo "解除信号屏蔽\n";
}
}

?>

循环到$i=10之前ctrl+c和循环到$i>10打印结果是不一样的,没有执行到10之前终止掉,ctrl+c不会终止程序,但是当$i=10时程序会立刻断掉。执行到$i>10,键入ctrl+c会立刻断掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

// $i<=10键入 ctrl+c
Array
(
)
$i = 0
$i = 1
$i = 2
$i = 3
$i = 4
$i = 5
^C$i = 6
$i = 7
$i = 8
$i = 9
$i = 10

// $i>10 键入ctrl+c
Array
(
)
$i = 0
$i = 1
$i = 2
$i = 3
$i = 4
$i = 5
$i = 6
$i = 7
$i = 8
$i = 9
$i = 10
解除信号屏蔽
$i = 11
$i = 12
$i = 13
$i = 14
$i = 15
$i = 16
$i = 17
^C

可以看到一旦解除了信号的屏蔽,信号就会立即送达。如果你希望一段代码执行结束之后再恢复信号,必须这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

<?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); //代码块执行完解除信号屏蔽
}

这样就可以保证无论什么时候向进程发送信号,这块代码总能执行完程序才会退出!