如果您使用的是 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() 也永远不会在时间之前被执行。
请记住,声明一个滴答处理程序在 CPU 周期方面可能变得非常昂贵:每 n 个滴答,信号处理开销都会被执行。
因此,与其声明 tick=1,不如测试 tick=100 是否可以完成工作。如果是,您可能会获得五倍的速度。
由于您的脚本可能总是会由于阻塞操作(如 cURL 下载)而错过一些信号,因此请在关键位置调用 pcntl_signal_dispatch(),例如在主循环的开头。
对于 PHP >= 5.3.0,您现在应该使用 pcntl_ signal_ dispatch(),而不是 declare(ticks = 1)。
由于 php >= 5.3 支持闭包,您现在可以直接定义回调。
试试这个
<?php
declare(ticks = 1);
pcntl_signal(SIGUSR1, function ($signal) {
echo 'HANDLE SIGNAL ' . $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) 的一个警告。
我一直在遇到一个反复出现的问题,即 (似乎随机地) 我的脚本会在使用信号处理程序跟踪分叉子进程时“意外退出” (_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。
如果您有一个脚本,需要某些部分不受信号 (尤其是 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;
// 更多不重要的东西。如果信号是在…期间接收到的
// 上面的重要处理,脚本将在此退出
?>
为了明确起见,短语“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"
?>
至少在 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
当你在一个检查套接字的循环中运行脚本,并且它挂在那个检查上(无论是由于缺陷还是设计),它就无法处理信号,直到收到一些数据。
建议的解决方法是使用 stream_set_blocking 函数或在有问题的读取操作上使用 stream_select。
多个子进程在给定的时刻返回少于退出子进程数量的 SIGCHLD 信号是 Unix(POSIX)系统的正常行为。SIGCHLD 可以被理解为“一个或多个子进程状态发生变化——检查你的子进程并收集它们的状态值”。在大多数 Unix 系统中,信号是使用位掩码实现的,因此在任何给定的内核滴答中,一个进程只能设置 1 个 SIGCHLD 位。
此问题至少在 PHP 5.1.2 中存在。
当通过 CTRL+C 或 CTRL+BREAK 发送 SIGINT 时,会调用处理程序。如果此处理程序向其他子进程发送 SIGTERM,则不会收到信号。
SIGINT 可以通过 posix_kill() 发送,并且它按预期工作——这仅适用于通过硬中断启动的情况。
我建议阅读两份文档
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
我发现,当你在一个 'deamon' 脚本中使用 pcntl_signal,并且你在创建子进程之前运行它时,它不会按预期工作。
相反,你需要在要创建的子进程的子进程代码中使用 pcntl_signal
如果你想为 'deamon' 部分缓存信号,你应该在父进程代码中使用 pcntl_signal。
我遇到一个有趣的问题。CLI 4.3.10 Linux
父进程创建了一个子进程。子进程做了一些事情,完成后,它向父进程发送了 SIGUSR1,然后立即退出。
结果
子进程变成一个僵尸进程,等待父进程收集它,这是正常的。
但是父进程无法收集子进程,因为它收到了无数的 SIGUSR1。事实上,在我关闭它(SIGKILL)之前,它记录了超过 200000 个。父进程在那段时间里无法做任何其他事情(响应其他事件)。
不,这不是我的子进程代码中的错误。显然,在发送信号后,需要进行一些“幕后”工作来确认信号完成,当你立即退出时,它就无法完成,因此系统只是不断尝试。
解决方案:我在子进程中添加了一个小的延迟,在发送信号后,并在退出之前。
没有更多 Sig 循环了...
----------
附注:关于下面的注释。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 时减少来限制最大子进程数量的代码,最终都会只剩下一个子进程(或不再创建子进程),因为“活动子进程”计数总是高于最大值。我注意到使用 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 使用的是实时信号。这意味着,如果一个信号当前正在被处理,那么其他信号不会丢失。
举个例子
<?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 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);
}
?>
我一直无法正常回收子进程。大多数时候,子进程都能被正确回收,但*有时*它们会变成僵尸进程。我使用以下代码捕获 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--;
}
?>
请注意,在 5.3 及更高版本中,声明滴答或调用 pcntl_signal_dispatch() 是使 pcntl_signal 发挥作用所必需的。我希望文档能更清楚地说明这一点。