2024 日本 PHP 研討會

pcntl_signal

(PHP 4 >= 4.1.0, PHP 5, PHP 7, PHP 8)

pcntl_signal安裝訊號處理器

說明

pcntl_signal(int $signal, callable|int $handler, bool $restart_syscalls = true): bool

函式 pcntl_signal() 會安裝新的訊號處理器,或替換 signal 指定的訊號之目前的訊號處理器。

參數

signal

訊號編號。

handler

訊號處理器。這可以是一個 callable,它將被調用來處理訊號,或者兩個全域常數 SIG_IGNSIG_DFL,它們將分別忽略訊號或恢復預設的訊號處理器。

如果給定一個 callable,它必須實作以下簽章

handler(int $signo, mixed $siginfo): void
signal
正在處理的訊號。
siginfo
如果作業系統支援 siginfo_t 結構,這將是一個依賴於訊號的訊號資訊陣列。

注意事項:

請注意,當您將處理器設定為物件方法時,該物件的引用計數會增加,這會使其持續存在,直到您將處理器更改為其他內容,或者您的腳本結束。

restart_syscalls

指定當此訊號到達時是否應該使用系統呼叫重新啟動。

返回值

成功時返回 true,失敗時返回 false

更新日誌

版本 說明
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() 不會堆疊訊號處理器,而是取代它們。

另請參閱

新增註釋

使用者貢獻的註釋 28 則註釋

62
Ryan Jentzsch
7 年前
如果您使用的是 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 裡頭有一個簡單的範例說明如何使用。
31
kuba at valentine dot dev
5 年前
請記住,訊號處理器會立即被呼叫,所以每當你的行程收到已註冊的訊號時,像 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() 也永遠不會在其時間之前被執行。
18
Zsolt Szilagyi
11 年前
請記住,宣告一個 tick 處理器在 CPU 週期方面可能會變得非常昂貴:每 n 個 ticks 就會執行一次訊號處理的額外負擔。

因此,不要宣告 tick=1,試試看 tick=100 是否能完成工作。如果可以,你的速度可能會提高五倍。

由於您的腳本可能會因為 cURL 下載等阻塞操作而遺漏一些信號,請在重要位置呼叫 pcntl_signal_dispatch(),例如在主迴圈的開頭。
25
ajeux.com 的 webmaster
15 年前
對於 PHP >= 5.3.0,您現在應該使用 pcntl_signal_dispatch(),而不是 declare(ticks = 1)。
20
yahoo.com 的 rbotzer
16 年前
您無法為 SIGKILL (kill -9) 指派信號處理器。
6
josefus./NO+SPAM/net 的 benjamin
15 年前
由於 php >= 5.3 支援閉包(Closure),您現在可以直接定義回呼函式。
試試這個

<?php
declare(ticks = 1);

pcntl_signal(SIGUSR1, function ($signal) {
echo
'處理信號 ' . $signal . PHP_EOL;
});

posix_kill(posix_getpid(), SIGUSR1);

die;
?>
8
php.net 的 ieure
19 年前
這裡發生了一些奇怪的信號交互作用。我正在運行 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__);
}

我在文檔中沒有看到任何關於這種行為的說明。
1
Aurelien Marchand
13 年前
我在使用物件方法處理信號時遇到了一些問題,這個方法我也用於其他用途。具體來說,我想用我自己的方法 "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();
}
}
?>
3
rob at robertjohnkaper dot com
18 年前
提示:使用物件時,不要在建構子中設定訊號處理器,甚至不要在從建構子呼叫的方法中設定訊號處理器 - 您的內部變數將不會被初始化。
1
wally at soggysoftware dot co dot uk
8 年前
關於 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)。
2
nate at example dot com
18 年前
如果您有一個腳本,需要某些部分不被訊號(尤其是 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;

// 更多不重要的程式碼。如果在上面的重要處理過程中收到訊號,腳本將在此處退出

?>
-1
Aurelien Marchand
6 年前
需要說明的是,「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"

?>
1
aeolianmeson at NOSPAM dot blitzeclipse dot com
18 年前
至少在 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
1
wm161 at wm161 dot net
18 年前
當您在迴圈內執行腳本並檢查 socket 時,如果它掛在該檢查上(無論是設計缺陷還是故意設計),在收到一些數據之前,它無法處理信號。

建議的解決方法是在有問題的讀取操作上使用 stream_set_blocking 函式或 stream_select。
0
匿名
16 年前
多個子進程返回的數量少於在特定時間點退出的子進程數量,對於 Unix (POSIX) 系統來說,SIGCHLD 信號是正常行為。SIGCHLD 可以理解為「一個或多個子進程的狀態已更改——請檢查您的子進程並收集它們的狀態值」。在大多數 Unix 系統中,信號是透過位元遮罩實現的,因此在任何給定的核心時間刻度中,一個進程只能設置一個 SIGCHLD 位元。
0
aeolianmeson at NOSPAXM dot blitzeclipse dot com
18 年前
此問題至少在 PHP 5.1.2 中出現。

當透過 CTRL+C 或 CTRL+BREAK 發送 SIGINT 時,會呼叫處理程序。如果此處理程序向其他子進程發送 SIGTERM,則不會收到信號。

可以透過 posix_kill() 發送 SIGINT,而且它會完全按照預期工作——這僅適用於透過硬中斷啟動的情況。
0
zenyatta22 at hotmail
18 年前
使用阻塞 socket 時,進程處理不可用!請記住這一點。
1
daniel[at]lorch.cc
22 年前
我認為有兩份文件值得一讀

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
-1
ewout dot NOSPAM dot zigterman at custosys dot nl
15 年前
我發現當你在一個「守護行程」腳本中使用 pcntl_signal,並且在 fork 子行程之前運行它時,它並不會如預期般運作。

你應該在 fork 出來的子行程程式碼中使用 pcntl_signal

而如果你想在「守護行程」部分捕捉訊號,你應該在父行程程式碼中使用 pcntl_signal。
0
codeslinger at compsalot dot com
19 年前
我遇到一個有趣的問題。CLI 4.3.10 Linux

父行程 fork 了個子行程。子行程執行了一堆工作,完成後向父行程發送 SIGUSR1 訊號並立即退出。

結果
子行程變成了一個殭屍行程,等待父行程回收它,就如同它應該做的那樣。

但是父行程無法回收子行程,因為它不斷收到大量的 SIGUSR1 訊號。事實上,在我將它關閉 (SIGKILL) 之前,它記錄了超過 200000 個 SIGUSR1 訊號。父行程在這段時間內無法執行任何其他操作(回應其他事件)。

不,這不是我子行程程式碼中的錯誤。顯然,在發送訊號後,需要進行一些「幕後」工作來確認訊號完成,而當你立即退出時,這項工作就無法完成,所以系統只能不斷重試。

解決方案:我在子行程發送訊號後、退出前加入一小段延遲。
不再有訊號迴圈了...

----------

附註:關於下面的說明,sleep 函式的重點在於允許處理其他事件。所以,是的,當你執行 sleep 時,你的不可重入程式碼會突然被重新進入,因為你剛剛把控制權交給了下一個待處理的事件。

忽略訊號只在你認為該訊號對你的程式不重要的情況下才是個選項...。更好的方法是不要在訊號事件處理函式內進行冗長的處理。相反的,設定一個全域旗標,然後盡快離開訊號處理函式。讓你的程式的其他部分檢查這個旗標,並在訊號事件處理函式之外進行處理。通常你的程式在接收訊號時會處於某種迴圈中,所以定期檢查旗標應該不成問題。
-2
Holger Hees
16 年前
你應該使用以下程式碼來避免 anxious2006 所描述的情況(子行程幾乎同時退出)

public function sig_handler($signo){
switch ($signo) {
case SIGCLD
while( ( $pid = pcntl_wait ( $signo, WNOHANG ) ) > 0 ){
$signal = pcntl_wexitstatus ( $signo );
}
break;
}
}
-2
anxious2006
17 年前
在我的設定下 (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() 後遞減時也觀察到類似的行為。希望有解決方法。
-1
S. Kr@cK
12 年前
用於取得行程訊號名稱字串的靜態類別方法

(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";
}
?>
-1
dorogikh dot alexander at gmail dot com
10 年前
看起來 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 訊號。所有訊號都會被處理。

備註:
我猜是「所有訊號」。我找不到真正的訊號佇列大小。如果有人能指點我,我會很感激。
-1
fx4084 at gmail dot com
10 年前
<?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);
}
?>
-1
seong at respice dot net
15 年前
在回呼函式中使用物件時要小心,這樣似乎會增加引用計數。您可能不希望這種情況在重複的子行程中發生。
-1
imslushie at hotmaildotcom dot com
18 年前
我在回收子行程時遇到問題。大多數情況下,子行程都能正常回收,但*有時*它們會變成殭屍行程。我使用以下程式碼擷取 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--;
}

?>
-2
對過時的文檔感到沮喪
10 年前
請注意,在 5.3 及更高版本中,需要宣告 ticks 或呼叫 pcntl_signal_dispatch() 才能使 pcntl_signal 發揮作用。我希望文件能更清楚地說明這一點。
To Top