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中信號就簡單介紹到這裏,如果想更深入的瞭解,歡迎關注後續文章^^



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章