PHP实现系统编程(三) --- 信号

信号是事件发生时对进程的通知机制,有时又称为软件中断。一个进程可以向另一个进程发送信号,比如子进程结束时都会向父进程发送一个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中信号就简单介绍到这里,如果想更深入的了解,欢迎关注后续文章^^



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值