如果您使用的是 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
指定的信号的当前信号处理器。
版本 | 描述 |
---|---|
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() 不堆叠信号处理程序,而是替换它们。
如果您使用的是 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 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();
}
}
?>
关于 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。
如果您有一个脚本需要某些部分不被信号(尤其是 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 "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("已捕获并注册信号。\n");
var_dump($arrsignals);
}
pcntl_signal(SIGTERM, 'handler');
// 等待来自命令行的信号(只是一个简单的“kill (pid)”)。
$n = 15;
while($n)
{
sleep(1);
$n--;
}
print("已终止。\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
我发现,当你在“守护进程”脚本中使用 pcntl_signal,并且在 fork 子进程之前运行它时,它不会按预期工作。
相反,你需要在正在 fork 的子进程的子进程代码中使用 pcntl_signal
如果你想为“守护进程”部分缓存信号,你应该在父进程代码中使用 pcntl_signal。
我遇到一个有趣的问题。CLI 4.3.10 Linux
父进程 fork 了一个子进程。子进程做了一堆事情,完成后它向父进程发送 SIGUSR1 并立即退出。
结果
子进程变成了僵尸进程,等待父进程收集它,这是应该的。
但是父进程无法收集子进程,因为它正在接收无休止的 SIGUSR1 信号。事实上,在我关闭它 (SIGKILL) 之前,它记录了超过 200000 个。在此期间,父进程无法执行任何其他操作(响应其他事件)。
不,这不是我的子进程代码中的错误。显然,发送信号后,需要发生一些“幕后”工作来确认信号完成,而当你立即退出时,它就无法发生,因此系统只是不断尝试。
解决方法:我在子进程中发送信号后、退出之前引入了一个小的延迟。
不再出现信号循环……
----------
附注:关于下面的说明。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”
- EXIT
- 收到 SIGCHLD
这按预期工作,但现在看看当三个子进程快速连续退出时会发生什么
- EXIT
- EXIT
- EXIT
- 收到 SIGCHLD
- 收到 SIGCHLD
由于这个怪癖,任何尝试通过在 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 使用的是实时信号。这意味着如果当前正在处理一个信号,则其他信号不会丢失。
举个例子
<?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('创建子进程失败');
}
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('创建子进程失败');
/* 子进程 */
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--;
}
?>
请注意,在 5.3 及更高版本中,需要声明 ticks 或调用 `pcntl_signal_dispatch()` 才能使 `pcntl_signal` 发挥作用。我希望文档能更清楚地说明这一点。