如果您使用的是 PHP >= 7.1,請不要使用 `declare(ticks=1)`,而是使用 `pcntl_async_signals(true)`
使用 `pcntl_async_signals()` 不會造成效能損失或額外負擔。參考這篇部落格文章 https://blog.pascal-martin.fr/post/php71-en-other-new-things.html 裡頭有一個簡單的範例說明如何使用。
(PHP 4 >= 4.1.0, PHP 5, PHP 7, PHP 8)
pcntl_signal — 安裝訊號處理器
函式 pcntl_signal() 會安裝新的訊號處理器,或替換 signal
指定的訊號之目前的訊號處理器。
signal
訊號編號。
handler
訊號處理器。這可以是一個 callable,它將被調用來處理訊號,或者兩個全域常數 SIG_IGN
或 SIG_DFL
,它們將分別忽略訊號或恢復預設的訊號處理器。
如果給定一個 callable,它必須實作以下簽章
signal
siginfo
注意事項:
請注意,當您將處理器設定為物件方法時,該物件的引用計數會增加,這會使其持續存在,直到您將處理器更改為其他內容,或者您的腳本結束。
restart_syscalls
指定當此訊號到達時是否應該使用系統呼叫重新啟動。
版本 | 說明 |
---|---|
7.1.0 | 從 PHP 7.1.0 開始,處理器回呼函式會被賦予第二個參數,其中包含特定訊號的 siginfo。僅當作業系統具有 siginfo_t 結構時,才會提供此資料。如果作業系統未實作 siginfo_t,則提供 NULL。 |
範例 #1 pcntl_signal() 範例
<?php
// 需要 tick 運作
declare(ticks = 1);
// 訊號處理函式
function sig_handler($signo)
{
switch ($signo) {
case SIGTERM:
// 處理關閉任務
exit;
break;
case SIGHUP:
// 處理重新啟動任務
break;
case SIGUSR1:
echo "Caught SIGUSR1...\n";
break;
default:
// 處理所有其他訊號
}
}
echo "Installing signal handler...\n";
// 設定訊號處理器
pcntl_signal(SIGTERM, "sig_handler");
pcntl_signal(SIGHUP, "sig_handler");
pcntl_signal(SIGUSR1, "sig_handler");
// 或使用物件
// pcntl_signal(SIGUSR1, array($obj, "do_something"));
echo"Generating signal SIGUSR1 to self...\n";
// 發送 SIGUSR1 給目前的處理程序 ID
// posix_* 函式需要 posix 擴充套件
posix_kill(posix_getpid(), SIGUSR1);
echo "Done\n";
?>
pcntl_signal() 不會堆疊訊號處理器,而是取代它們。
如果您使用的是 PHP >= 7.1,請不要使用 `declare(ticks=1)`,而是使用 `pcntl_async_signals(true)`
使用 `pcntl_async_signals()` 不會造成效能損失或額外負擔。參考這篇部落格文章 https://blog.pascal-martin.fr/post/php71-en-other-new-things.html 裡頭有一個簡單的範例說明如何使用。
請記住,訊號處理器會立即被呼叫,所以每當你的行程收到已註冊的訊號時,像 sleep() 和 usleep() 這樣的阻塞函式就會被中斷(或者更像是結束),就像它們的時間到了。
這是預期的行為,但文件中沒有提到。睡眠中斷在某些情況下可能會非常不幸,例如,當您運行一個守護行程,該守護行程應該每隔 X 秒/分鐘準確地執行一些重要的任務時——這個任務可能會被過早或過於頻繁地呼叫,而且很難除錯找出究竟是為什麼發生的。
簡而言之,請記住訊號處理器可能會干擾程式碼的其他部分。
以下是一個範例解決方案,確保函式每分鐘執行一次,無論我們的迴圈被傳入訊號(在這個例子中是 SIGUSR1)中斷多少次。
<?php
pcntl_async_signals(TRUE);
pcntl_signal(SIGUSR1, function($signal) {
// 訊號被呼叫時執行某些動作
});
function everyMinute() {
// 執行一些重要的工作
}
$wait = 60;
$next = 0;
while (TRUE) {
$stamp = time();
do {
if ($stamp >= $next) { break; }
$diff = $next - $stamp;
sleep($diff);
$stamp = time();
} while ($stamp < $next);
everyMinute();
$next = $stamp + $wait;
sleep($wait);
}
?>
所以在這個無限迴圈中,do {} while() 計算並補足缺少的 sleep() 時間,以確保我們的 everyMinute() 函式不會被過早呼叫。這裡的兩個 sleep() 函式都被涵蓋,因此即使行程在其運行期間收到多個 SIGUSR1 訊號,everyMinute() 也永遠不會在其時間之前被執行。
請記住,宣告一個 tick 處理器在 CPU 週期方面可能會變得非常昂貴:每 n 個 ticks 就會執行一次訊號處理的額外負擔。
因此,不要宣告 tick=1,試試看 tick=100 是否能完成工作。如果可以,你的速度可能會提高五倍。
由於您的腳本可能會因為 cURL 下載等阻塞操作而遺漏一些信號,請在重要位置呼叫 pcntl_signal_dispatch(),例如在主迴圈的開頭。
對於 PHP >= 5.3.0,您現在應該使用 pcntl_signal_dispatch(),而不是 declare(ticks = 1)。
由於 php >= 5.3 支援閉包(Closure),您現在可以直接定義回呼函式。
試試這個
<?php
declare(ticks = 1);
pcntl_signal(SIGUSR1, function ($signal) {
echo '處理信號 ' . $signal . PHP_EOL;
});
posix_kill(posix_getpid(), SIGUSR1);
die;
?>
這裡發生了一些奇怪的信號交互作用。我正在運行 PHP 4.3.9。
當 PHP 腳本收到任何信號時,sleep() 呼叫似乎會被中斷。但是當你在信號處理器內部使用 sleep() 時,事情就變得奇怪了。
通常,信號處理器是不可重入的。也就是說,如果信號處理器正在運行,發送另一個信號將沒有任何作用。然而,sleep() 似乎覆蓋了 PHP 的信號處理。如果你在信號處理器內部使用 sleep(),則會收到信號並且 sleep() 會被中斷。
可以這樣解決這個問題
function handler($signal)
{
// 忽略此信號
pcntl_signal($signal, SIG_IGN);
sleep(10);
// 重新安裝信號處理器
pcntl_signal($signal, __FUNCTION__);
}
我在文檔中沒有看到任何關於這種行為的說明。
我在使用物件方法處理信號時遇到了一些問題,這個方法我也用於其他用途。具體來說,我想用我自己的方法 "do_reap()" 來處理 SIGCHLD,這個方法我也會在 stream_select 超時後呼叫,並且它使用非阻塞的 pcntl_waitpid 函式。
收到信號時會呼叫該方法,但它無法回收子進程。
唯一有效的方法是創建一個新的處理器,它本身呼叫 do_reap()。
換句話說,以下方法無效
<?php
class Parent {
/* ... */
private function do_reap(){
$p = pcntl_waitpid(-1,$status,WNOHANG);
if($p > 0){
echo "\nReaped zombie child " . $p;
}
public function run(){
/* ... */
pcntl_signal(SIGCHLD,array(&$this,"do_reap"));
$readable = @stream_select($read,$null,$null,5); // 5 秒逾時
if($readable === 0){
// 發生逾時
$this->do_reap();
}
}
?>
但這樣可行
<?php
class Parent {
/* ... */
private function do_reap(){
$p = pcntl_waitpid(-1,$status,WNOHANG);
if($p > 0){
echo "\nReaped zombie child " . $p;
}
public function run(){
/* ... */
pcntl_signal(SIGCHLD,array(&$this,"child_died"));
$readable = @stream_select($read,$null,$null,5); // 5 秒逾時
if($readable === 0){
// 發生逾時
$this->do_reap();
}
private function child_died(){
$this->do_reap();
}
}
?>
提示:使用物件時,不要在建構子中設定訊號處理器,甚至不要在從建構子呼叫的方法中設定訊號處理器 - 您的內部變數將不會被初始化。
關於 pcntl_signal(...) 中的第三個參數 (restart_syscalls) 的注意事項。
我持續遇到一個重複出現的問題,在使用訊號處理器追蹤已 fork 的子行程時,我的腳本會(看似隨機地)「意外退出」(_exit_,不是當機:退出代碼始終為 0)。
看起來訊號處理本身沒有問題(事實上,PHP 並沒有「出錯」)。將「restart_syscalls」設定為 FALSE 似乎是問題的根源。
我沒有深入除錯這個問題——只是觀察到這個問題是間歇性發生的,而且似乎與我在 restart_syscalls=FALSE 的情況下使用 usleep() 有關。
我的推論是 usleep() 錯誤地追蹤時間——如同這裡所描述的:http://man7.org/linux/man-pages/man2/restart_syscall.2.html
長話短說,我重新啟用了 restart_syscalls(這是預設值),然後問題就消失了。
如果您好奇的話——register_shutdown_function 仍然被正確處理——所以 PHP 絕對_沒有_崩潰。然而有趣的是,我的程序代碼在訊號被處理後從未「恢復」。
我不認為這是一個 bug。我相信這是使用者操作失誤。您的結果可能有所不同 (YMMV)。
如果您有一個腳本,需要某些部分不被訊號(尤其是 SIGTERM 或 SIGINT)中斷,但又希望您的腳本能盡快處理該訊號,那麼只有一種方法可以做到。將腳本標記為已收到訊號,並等待腳本表示已準備好處理它。
以下是一個範例腳本
<?
$allow_exit = true; // 是否允許退出?
$force_exit = false; // 是否需要退出?
declare(ticks = 1);
register_tick_function('check_exit');
pcntl_signal(SIGTERM, 'sig_handler');
pcntl_signal(SIGINT, 'sig_handler');
function sig_handler () {
global $allow_exit, $force_exit;
if ($allow_exit)
exit;
else
$force_exit = true;
}
function check_exit () {
global $allow_exit, $force_exit;
if ($force_exit && $allow_exit)
exit;
}
$allow_exit = false;
$i = 0;
while (++$i) {
echo "仍在運行 (${i})\n";
if ($i == 10)
$allow_exit = true;
sleep(2);
}
?>
當您的腳本可以隨時安全退出時,請將 $allow_exit 設定為 true。在您確實需要腳本繼續運行的區段中,請將 $allow_exit 設定為 false。在 $allow_exit 為 false 時收到的任何訊號,在您將 $allow_exit 設定為 true 之前都不會生效。
<?
$allow_exit = true;
// 不重要的程式碼,退出不會造成任何損害
$allow_exit = false;
// 非常重要的程式碼,不能被中斷
$allow_exit = true;
// 更多不重要的程式碼。如果在上面的重要處理過程中收到訊號,腳本將在此處退出
?>
需要說明的是,「pcntl_signal() 並不會堆疊訊號處理程式,而是取代它們。」這句話的意思是,您仍然可以為不同的訊號設置不同的函數,但每個訊號只能有一個函數。
換句話說,這樣做會如預期般運作
<?php
pcntl_async_signals(true);
pcntl_signal(SIGUSR1,function(){echo "收到 SIGUSR1";});
pcntl_signal(SIGUSR2,function(){echo "收到 SIGUSR2";});
posix_kill(posix_getpid(),SIGUSR1);
posix_kill(posix_getpid(),SIGUSR2);
// 返回 "收到 SIGUSR1" 然後 "收到 SIGUSR2"
?>
至少在 5.1.4 版中,傳遞給處理程序的參數並非嚴格的整數。
我遇到過這樣的問題:嘗試將信號添加到陣列中,但在檢視陣列時(並非在添加後立即檢視),陣列會完全亂掉。當處理程序是一個方法(array($this, 'methodname')) 或傳統函式時,就會發生這種情況。
為避免此錯誤,請將參數強制轉換為整數類型。
(請注意,每個換行符號可能只顯示為 'n'。)
<?php
print("pid= " . posix_getpid() . "\n");
declare(ticks=1);
$arrsignals = array();
function handler($nsig)
{
global $arrsignals;
$arrsignals[] = (int)$nsig;
print("已捕捉並記錄信號。\n");
var_dump($arrsignals);
}
pcntl_signal(SIGTERM, 'handler');
// 從命令列等待信號(只需一個簡單的 'kill (pid)')。
$n = 15;
while($n)
{
sleep(1);
$n--;
}
print("已終止。\n\n");
var_dump($arrsignals);
?>
Dustin
當您在迴圈內執行腳本並檢查 socket 時,如果它掛在該檢查上(無論是設計缺陷還是故意設計),在收到一些數據之前,它無法處理信號。
建議的解決方法是在有問題的讀取操作上使用 stream_set_blocking 函式或 stream_select。
多個子進程返回的數量少於在特定時間點退出的子進程數量,對於 Unix (POSIX) 系統來說,SIGCHLD 信號是正常行為。SIGCHLD 可以理解為「一個或多個子進程的狀態已更改——請檢查您的子進程並收集它們的狀態值」。在大多數 Unix 系統中,信號是透過位元遮罩實現的,因此在任何給定的核心時間刻度中,一個進程只能設置一個 SIGCHLD 位元。
此問題至少在 PHP 5.1.2 中出現。
當透過 CTRL+C 或 CTRL+BREAK 發送 SIGINT 時,會呼叫處理程序。如果此處理程序向其他子進程發送 SIGTERM,則不會收到信號。
可以透過 posix_kill() 發送 SIGINT,而且它會完全按照預期工作——這僅適用於透過硬中斷啟動的情況。
我認為有兩份文件值得一讀
Unix 信號程式設計
http://users.actcom.co.il/~choo/lupg/tutorials/
Beej's Guide to Unix Interprocess Communication (Beej 的 Unix 進程間通訊指南)
http://www.ecst.csuchico.edu/~beej/guide/ipc/
另外,請查看 man 頁面
http://www.mcsr.olemiss.edu/cgi-bin/man-cgi?signal+5
我發現當你在一個「守護行程」腳本中使用 pcntl_signal,並且在 fork 子行程之前運行它時,它並不會如預期般運作。
你應該在 fork 出來的子行程程式碼中使用 pcntl_signal
而如果你想在「守護行程」部分捕捉訊號,你應該在父行程程式碼中使用 pcntl_signal。
我遇到一個有趣的問題。CLI 4.3.10 Linux
父行程 fork 了個子行程。子行程執行了一堆工作,完成後向父行程發送 SIGUSR1 訊號並立即退出。
結果
子行程變成了一個殭屍行程,等待父行程回收它,就如同它應該做的那樣。
但是父行程無法回收子行程,因為它不斷收到大量的 SIGUSR1 訊號。事實上,在我將它關閉 (SIGKILL) 之前,它記錄了超過 200000 個 SIGUSR1 訊號。父行程在這段時間內無法執行任何其他操作(回應其他事件)。
不,這不是我子行程程式碼中的錯誤。顯然,在發送訊號後,需要進行一些「幕後」工作來確認訊號完成,而當你立即退出時,這項工作就無法完成,所以系統只能不斷重試。
解決方案:我在子行程發送訊號後、退出前加入一小段延遲。
不再有訊號迴圈了...
----------
附註:關於下面的說明,sleep 函式的重點在於允許處理其他事件。所以,是的,當你執行 sleep 時,你的不可重入程式碼會突然被重新進入,因為你剛剛把控制權交給了下一個待處理的事件。
忽略訊號只在你認為該訊號對你的程式不重要的情況下才是個選項...。更好的方法是不要在訊號事件處理函式內進行冗長的處理。相反的,設定一個全域旗標,然後盡快離開訊號處理函式。讓你的程式的其他部分檢查這個旗標,並在訊號事件處理函式之外進行處理。通常你的程式在接收訊號時會處於某種迴圈中,所以定期檢查旗標應該不成問題。
你應該使用以下程式碼來避免 anxious2006 所描述的情況(子行程幾乎同時退出)
public function sig_handler($signo){
switch ($signo) {
case SIGCLD
while( ( $pid = pcntl_wait ( $signo, WNOHANG ) ) > 0 ){
$signal = pcntl_wexitstatus ( $signo );
}
break;
}
}
在我的設定下 (FreeBSD 6.2-RELEASE / PHP 5.2.4 CLI),我注意到當子行程退出時,父行程中的 SIGCHLD 處理函式並非總是會被呼叫。這似乎發生在兩個子行程幾乎同時退出的情況下。
在這種情況下,子行程會印出「EXIT」,而父行程會印出「SIGCHLD received」
- EXIT
- SIGCHLD received
這樣可以正常運作,但現在看看當三個子行程快速連續退出時會發生什麼事
- EXIT
- EXIT
- EXIT
- SIGCHLD received
- SIGCHLD received
由於這個怪異的現象,任何試圖透過在 fork 時遞增、在 SIGCHLD 時遞減來限制子行程最大數量的程式碼最終都會只有一個子行程(或不再 fork),因為「活動中子行程」的計數總是高於最大值。我在使用 pcntl_wait() 後遞減時也觀察到類似的行為。希望有解決方法。
用於取得行程訊號名稱字串的靜態類別方法
(self::$processSignalDescriptions 用於快取結果)
<?php
public static function getPOSIXSignalText($signo) {
try {
if (is_null(self::$processSignalDescriptions)) {
self::$processSignalDescriptions = array();
$signal_list = explode(" ", trim(shell_exec("kill -l")));
foreach ($signal_list as $key => $value) {
self::$processSignalDescriptions[$key+1] = "SIG".$value;
}
}
return isset(self::$processSignalDescriptions[$signo])?self::$processSignalDescriptions[$signo]:"UNKNOWN";
} catch (Exception $e) {}
return "UNKNOWN";
}
?>
看起來 PHP 使用的是即時訊號 (RealTime signals)。這表示如果一個訊號正在被處理,其他訊號不會遺失。
舉例來說:
<?php
pcntl_signal(SIGHUP, SIG_IGN)
?>
在 strace 的紀錄中看起來像這樣:
"rt_sigaction(SIGHUP, {SIG_IGN, [], SA_RESTORER|SA_RESTART, 0x7f8caf83cc30}, {SIG_DFL, [], 0}, 8) = 0"
測試程式碼:
<?php
pcntl_signal(SIGHUP, function($signo){
echo "1\n";
sleep(2);
echo "2\n";
});
while(true){
sleep(1);
pcntl_signal_dispatch();
}
?>
執行此程式碼,並發送任意數量的 SIGHUP 訊號。所有訊號都會被處理。
備註:
我猜是「所有訊號」。我找不到真正的訊號佇列大小。如果有人能指點我,我會很感激。
<?php
pcntl_signal(SIGTERM, function($signo) {
echo "\n 收到訊號 [$signo] \n";
Status::$state = -1;
});
class Status{
public static $state = 0;
}
$pid = pcntl_fork();
if ($pid == -1) {
die('無法 fork');
}
if($pid) {
// 父行程
} else {
while(true) {
// 訊息派送中...
pcntl_signal_dispatch();
if(Status::$state == -1) {
// 執行一些動作並結束迴圈
break;
}
for($j = 0; $j < 2; $j++) {
echo '.';
sleep(1);
}
echo "\n";
}
echo "完成 \n";
exit();
}
$n = 0;
while(true) {
$res = pcntl_waitpid($pid, $status, WNOHANG);
// 如果子行程結束,則結束主行程
if(-1 == $res || $res > 0)
break;
// 5 秒後發送信號..
if($n == 5)
posix_kill($pid, SIGTERM);
$n++;
sleep(1);
}
?>
我在回收子行程時遇到問題。大多數情況下,子行程都能正常回收,但*有時*它們會變成殭屍行程。我使用以下程式碼擷取 CHLD 訊號來回收子行程
<?php
函式 childFinished($signal)
{
全域變數 $kids;
$kids--;
pcntl_waitpid(-1, $status);
}
$kids = 0;
pcntl_signal(SIGCHLD, "childFinished");
for ($i = 0; $i < 1000; $i++)
{
while ($kids >= 50) sleep(1);
$pid = pcntl_fork();
if ($pid == -1) die('fork 失敗 :(');
/* 子行程 */
if ($pid == 0)
{
/* 執行某些動作 */
exit(0);
}
/* 父行程 */
else { $kids++; }
}
/* 完成後,清理子行程 */
print "正在回收 $kids 個子行程...\n";
while ($kids) sleep(1);
print "完成.\n";
?>
問題是,$kids 的值永遠不會變成零,所以它會一直等待下去。絞盡腦汁一番後(我對 UNIX fork 還很陌生),我終於讀了 Perl IPC 文件,找到了解法!原來是因為訊號處理器不可重入,所以我的處理器在使用中時不會再次被呼叫。造成我困擾的情況是,一個子行程會結束並呼叫訊號處理器,它會執行 pcntl_waitpid() 並減少計數器。同時,另一個子行程會在第一個子行程仍在被回收時結束,所以第二個子行程永遠不會通知父行程!
解決方案是在 SIGCHLD 處理器中持續回收子行程,只要有子行程需要回收。以下是 *修正後* 的 childFinished 函式
<?php
函式 childFinished($signal)
{
全域變數 $kids;
while( pcntl_waitpid(-1, $status, WNOHANG) > 0 )
$kids--;
}
?>
請注意,在 5.3 及更高版本中,需要宣告 ticks 或呼叫 pcntl_signal_dispatch() 才能使 pcntl_signal 發揮作用。我希望文件能更清楚地說明這一點。