信號是事件發生時對進程的通知機制,有時又稱爲軟件中斷。一個進程可以向另一個進程發送信號,比如子進程結束時都會向父進程發送一個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中信號就簡單介紹到這裏,如果想更深入的瞭解,歡迎關注後續文章^^