PHP Conference Japan 2024

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
"捕获到 SIGUSR1...\n";
break;
default:
// 处理所有其他信号
}

}

echo
"正在安装信号处理器...\n";

// 设置信号处理器
pcntl_signal(SIGTERM, "sig_handler");
pcntl_signal(SIGHUP, "sig_handler");
pcntl_signal(SIGUSR1, "sig_handler");

// 或使用对象
// pcntl_signal(SIGUSR1, array($obj, "do_something"));

echo"正在向自身生成信号 SIGUSR1...\n";

// 将 SIGUSR1 发送到当前进程 ID
// posix_* 函数需要 posix 扩展
posix_kill(posix_getpid(), SIGUSR1);

echo
"完成\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 年前
请记住,声明滴答处理程序在 CPU 周期方面可能会非常昂贵:每 n 个滴答,都会执行信号处理开销。

因此,不要声明 tick=1,而是测试 tick=100 是否可以完成工作。如果可以,则速度可能会提高五倍。

由于您的脚本可能会因为阻塞操作(如 cURL 下载)而错过一些信号,因此在关键位置调用 pcntl_signal_dispatch(),例如在主循环的开头。
25
webmaster at ajeux dot com
15 年前
对于 PHP >= 5.3.0,现在应该使用 pcntl_signal_dispatch() 来代替 declare(ticks = 1)。
20
rbotzer at yahoo dot com
16 年前
您无法为 SIGKILL (kill -9) 分配信号处理程序。
6
benjamin at josefus dot /NO+SPAM/ net
15 年前
自从 php >= 5.3 支持闭包以来,您现在可以直接定义回调函数。
试试这个

<?php
declare(ticks = 1);

pcntl_signal(SIGUSR1, function ($signal) {
echo
'HANDLE SIGNAL ' . $signal . PHP_EOL;
});

posix_kill(posix_getpid(), SIGUSR1);

die;
?>
8
ieure at php dot net
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 sec timeout
if($readable === 0){
// we got a timeout
$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 sec timeout
if($readable === 0){
// we got a timeout
$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) 的注意事项。

我一直遇到一个重复的问题,即在使用信号处理程序跟踪派生子进程时,我的脚本会“意外退出”(_exit_,而不是崩溃:退出代码始终为 0)。

似乎信号处理不是问题所在(实际上,PHP 也没有“问题”)。将“restart_syscalls”设置为 FALSE 似乎是问题的原因。

我没有广泛调试这个问题——只是观察到这个问题非常间歇性,似乎与我在 conjunction with restart_syscalls=FALSE 中使用 usleep() 有关。

我的理论是 usleep() 跟踪时间的方式不正确——此处进行了描述:http://man7.org/linux/man-pages/man2/restart_syscall.2.html

长话短说,我重新启用了 (这是默认值) restart_syscalls,问题就消失了。

如果您好奇——register_shutdown_function 仍然可以正确处理——因此 PHP 绝对没有_崩溃_。然而,有趣的是,我的过程代码在处理信号后从未“恢复”。

我不认为这是一个错误。我相信这是无知的用户错误。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 "received SIGUSR1";});
pcntl_signal(SIGUSR2,function(){echo "received SIGUSR2";});
posix_kill(posix_getpid(),SIGUSR1);
posix_kill(posix_getpid(),SIGUSR2);

// 返回 "received SIGUSR1",然后是 "received 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 年前
当你在检查套接字的循环内运行脚本,并且它挂在该检查上(无论是由于缺陷还是设计),它在接收到一些数据之前无法处理信号。

建议的解决方法是使用 stream_set_blocking 函数或在有问题的读取操作上使用 stream_select。
0
匿名用户
16 年前
多个子进程返回少于在给定时刻退出的子进程数量的 SIGCHLD 信号是 Unix(POSIX)系统的正常行为。SIGCHLD 可以理解为“一个或多个子进程状态发生变化——去检查你的子进程并收集它们的状态值”。在大多数 Unix 系统中,信号是作为位掩码实现的,因此对于一个进程,在任何给定的内核周期中只能设置 1 个 SIGCHLD 位。
0
aeolianmeson at NOSPAXM dot blitzeclipse dot com
18 年前
此问题至少出现在 PHP 5.1.2 中。

当通过 CTRL+C 或 CTRL+BREAK 发送 SIGINT 时,会调用处理程序。如果此处理程序向其他子进程发送 SIGTERM,则不会接收到信号。

SIGINT 可以通过 posix_kill() 发送,并且其工作方式完全符合预期——这仅适用于通过硬中断启动的情况。
0
zenyatta22 at hotmail
18 年前
使用阻塞套接字时,进程处理不可用!请记住这一点。
1
daniel[at]lorch.cc
22年前
有两份文档我认为值得阅读

Unix 信号编程
http://users.actcom.co.il/~choo/lupg/tutorials/

Beej 的 Unix 进程间通信指南
http://www.ecst.csuchico.edu/~beej/guide/ipc/

另外,查看一下手册页

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 个。在此期间,父进程无法执行任何其他操作(响应其他事件)。

不,这不是我的子进程代码中的错误。显然,发送信号后,需要发生一些“幕后”工作来确认信号完成,而当你立即退出时,它就无法发生,因此系统只是不断尝试。

解决方法:我在子进程中发送信号后、退出之前引入了一个小的延迟。
不再出现信号循环……

----------

附注:关于下面的说明。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”

- EXIT
- 收到 SIGCHLD

这按预期工作,但现在看看当三个子进程快速连续退出时会发生什么

- EXIT
- EXIT
- EXIT
- 收到 SIGCHLD
- 收到 SIGCHLD

由于这个怪癖,任何尝试通过在 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 使用的是实时信号。这意味着如果当前正在处理一个信号,则其他信号不会丢失。

举个例子
<?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(
'创建子进程失败');
}

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

function childFinished($signal)
{
global
$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('创建子进程失败');

/* 子进程 */
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

function childFinished($signal)
{
global
$kids;

while(
pcntl_waitpid(-1, $status, WNOHANG) > 0 )
$kids--;
}

?>
-2
frustrated at outdated docs
10年前
请注意,在 5.3 及更高版本中,需要声明 ticks 或调用 `pcntl_signal_dispatch()` 才能使 `pcntl_signal` 发挥作用。我希望文档能更清楚地说明这一点。
To Top