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 个注释

Ryan Jentzsch
6 年前
如果您使用的是 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
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() 也永远不会在时间之前被执行。
Zsolt Szilagyi
11 年前
请记住,声明一个滴答处理程序在 CPU 周期方面可能变得非常昂贵:每 n 个滴答,信号处理开销都会被执行。

因此,与其声明 tick=1,不如测试 tick=100 是否可以完成工作。如果是,您可能会获得五倍的速度。

由于您的脚本可能总是会由于阻塞操作(如 cURL 下载)而错过一些信号,因此请在关键位置调用 pcntl_signal_dispatch(),例如在主循环的开头。
webmaster at ajeux dot com
15 年前
对于 PHP >= 5.3.0,您现在应该使用 pcntl_ signal_ dispatch(),而不是 declare(ticks = 1)。
rbotzer at yahoo dot com
16 年前
您不能为 SIGKILL (kill -9) 分配信号处理程序。
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;
?>
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__);
}

我在文档中没有看到关于这种行为的任何提及。
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();
}
}
?>
rob at robertjohnkaper dot com
17 年前
提示:在使用对象时,不要从构造函数或从构造函数中调用的方法中设置信号处理程序 - 您的内部变量将是未初始化的。
wally at soggysoftware dot co dot uk
8 年前
关于 pcntl_signal(...) 中的第三个参数 (restart_syscalls) 的一个警告。

我一直在遇到一个反复出现的问题,即 (似乎随机地) 我的脚本会在使用信号处理程序跟踪分叉子进程时“意外退出” (_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 绝对没有 _崩溃_。有趣的是,我的过程代码在信号处理后从未“恢复”。

我不认为这是一个错误。我认为这是无知的用户错误。YMMV。
nate at example dot com
17 年前
如果您有一个脚本,需要某些部分不受信号 (尤其是 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 "still going (${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;

// 更多不重要的东西。如果信号是在…期间接收到的
// 上面的重要处理,脚本将在此退出
?>
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"

?>
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(
"Signal caught and registered.\n");
var_dump($arrsignals);
}

pcntl_signal(SIGTERM, 'handler');

// 等待来自命令行的信号(只是一个简单的 'kill (pid)')。
$n = 15;
while(
$n)
{
sleep(1);
$n--;
}

print(
"terminated.\n\n");
var_dump($arrsignals);
?>

Dustin
wm161 at wm161 dot net
18 年前
当你在一个检查套接字的循环中运行脚本,并且它挂在那个检查上(无论是由于缺陷还是设计),它就无法处理信号,直到收到一些数据。

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

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

SIGINT 可以通过 posix_kill() 发送,并且它按预期工作——这仅适用于通过硬中断启动的情况。
zenyatta22 at hotmail
18 年前
在使用阻塞套接字时,无法使用进程处理!请牢记这一点。
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
ewout dot NOSPAM dot zigterman at custosys dot nl
15 年前
我发现,当你在一个 'deamon' 脚本中使用 pcntl_signal,并且你在创建子进程之前运行它时,它不会按预期工作。

相反,你需要在要创建的子进程的子进程代码中使用 pcntl_signal

如果你想为 'deamon' 部分缓存信号,你应该在父进程代码中使用 pcntl_signal。
codeslinger at compsalot dot com
19 年前
我遇到一个有趣的问题。CLI 4.3.10 Linux

父进程创建了一个子进程。子进程做了一些事情,完成后,它向父进程发送了 SIGUSR1,然后立即退出。

结果
子进程变成一个僵尸进程,等待父进程收集它,这是正常的。

但是父进程无法收集子进程,因为它收到了无数的 SIGUSR1。事实上,在我关闭它(SIGKILL)之前,它记录了超过 200000 个。父进程在那段时间里无法做任何其他事情(响应其他事件)。

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

解决方案:我在子进程中添加了一个小的延迟,在发送信号后,并在退出之前。
没有更多 Sig 循环了...

----------

附注:关于下面的注释。sleep 函数的全部意义在于允许处理其他事件。因此,是的,当你执行 sleep 操作时,你的非租户代码会突然重新进入,因为你刚刚将控制权交给了下一个挂起的事件。

忽略信号只是一种选择,如果该信号对你的程序不重要.... 更好的方法是不要在信号事件中进行长时间的处理。相反,设置一个全局标志,并尽快退出。让程序的另一部分检查标志并在信号事件之外进行处理。通常你的程序在接收信号时处于某种循环中,因此定期检查标志应该不是问题。
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;
}
}
anxious2006
16 年前
在我的设置(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 时减少来限制最大子进程数量的代码,最终都会只剩下一个子进程(或不再创建子进程),因为“活动子进程”计数总是高于最大值。我注意到使用 pcntl_wait() 后进行减少也有类似的行为。希望对此有一个解决方法。
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";
}
?>
dorogikh dot alexander at gmail dot com
9 年前
看起来 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 信号。所有信号都会被处理。

补充说明:
我猜测“所有信号”。我没找到实际信号队列的大小。如果有谁知道,请告诉我。
fx4084 at gmail dot com
9 年前
<?php
pcntl_signal
(SIGTERM, function($signo) {
echo
"\n This signal is called. [$signo] \n";
Status::$state = -1;
});

class
Status{
public static
$state = 0;
}

$pid = pcntl_fork();
if (
$pid == -1) {
die(
'could not 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);
}
?>
seong at respice dot net
15 年前
注意,在回调函数中使用对象时。这似乎会提升引用计数。在重复的子进程中你可能不希望这样做。
imslushie at hotmaildotcom dot com
17 年前
我一直无法正常回收子进程。大多数时候,子进程都能被正确回收,但*有时*它们会变成僵尸进程。我使用以下代码捕获 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('failed to fork :(');

/* 子进程 */
if ($pid == 0)
{
/* 执行某些操作 */
exit(0);
}

/* 父进程 */
else { $kids++; }
}

/* 完成后,清理子进程 */
print "正在回收 $kids 个子进程...\n";
while (
$kids) sleep(1);

print
"完成。\n";
?>

问题在于,$kids 从未变成零,所以它会一直等待。在苦苦思索后(我对 UNIX 分叉不太熟悉),我终于读了 Perl IPC 文档,找到了解决方案!原来是因为信号处理程序不是可重入的,所以我的处理程序在使用时不会再次被调用。导致我问题的场景是,一个子进程退出并调用信号处理程序,该处理程序会 pcntl_waitpid() 它并减少计数器。与此同时,另一个子进程在第一个子进程被回收之前退出,所以第二个子进程永远不会通知父进程!

解决方案是在 SIGCHLD 处理程序中持续回收子进程,只要有子进程要回收。以下是*修复后的* childFinished 函数

<?php

function childFinished($signal)
{
global
$kids;

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

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