CURL 不能被带有自定义信号处理程序的 PHP SIGINT 杀死
CURL cannot be killed by a PHP SIGINT with custom signal handler
我有一个带有自定义关闭处理程序的 PHP 命令行应用程序:
<?php
declare(ticks=1);
$shutdownHandler = function () {
echo 'Exiting';
exit();
};
pcntl_signal(SIGINT, $shutdownHandler);
while (true) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://blackhole.webpagetest.org');
curl_exec($ch);
curl_close($ch);
}
如果我在 CURL 请求正在进行时使用 Ctrl+C 终止脚本,它没有任何效果。命令只是挂起。如果我删除自定义关闭处理程序,Ctrl+C 会立即终止 CURL 请求。
当我定义 SIGINT
处理程序时,为什么 CURL 无法终止?
我为此苦思冥想了一段时间,没有找到任何使用 pcntl 的最佳解决方案,但根据您的命令的预期用途,您可以考虑:
- 使用超时:
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
您可能已经知道有 many options to control the behavior of curl, including setting a progress callback function.
- 使用
file_get_contents()
而不是 curl。
A solution using the Ev extension :
对于信号处理代码,它可以像这样简单:
$w = new EvSignal(SIGINT, function (){
exit("SIGINT received\n");
});
Ev::run();
Ev 扩展安装的快速总结:
sudo pecl install Ev
您需要通过添加到 php cli 的 php.ini 文件来启用扩展,可能是 /etc/php/7.0/cli/php.ini
extension="ev.so"
如果 pecl 抱怨缺少 phpize,请安装 php7.0-dev.
什么有效?
似乎真正有用的是给整个东西一些 space 来发挥它的信号处理魔力。这样的 space 似乎是通过启用 cURL 的进度处理来提供的,同时还设置了 "userland" 进度回调:
解决方案 1
while (true) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_NOPROGRESS, false); // "true" by default
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function() {
usleep(100);
});
curl_setopt($ch, CURLOPT_URL, 'http://blackhole.webpagetest.org');
curl_exec($ch);
curl_close($ch);
}
进度回调函数中好像需要"something"。空体似乎不起作用,因为它可能只是没有给 PHP 太多时间来处理信号 (硬核推测).
解决方案 2
即使 PHP 7.1
上没有 declare(ticks=1);
,将 pcntl_signal_dispatch()
放入回调似乎也能正常工作。
...
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function() {
pcntl_signal_dispatch();
});
...
解决方案 3 ❤ (PHP 7.1+)
使用 pcntl_async_signals(true)
而不是 declare(ticks=1);
可以工作 即使进度回调函数主体为空 .
这可能是我个人会使用的,所以我将把完整的代码放在这里:
<?php
pcntl_async_signals(true);
$shutdownHandler = function() {
die("Exiting\n");
};
pcntl_signal(SIGINT, $shutdownHandler);
while (true) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function() {});
curl_setopt($ch, CURLOPT_URL, 'http://blackhole.webpagetest.org');
curl_exec($ch);
curl_close($ch);
}
所有这三种解决方案都会导致 PHP 7.1
在点击 CTRL+C
后几乎立即 退出。
发生了什么事?
当您发送 Ctrl + C
命令时,PHP
尝试在退出前完成当前操作。
为什么我的(OP 的)代码没有退出?
在最后查看最后的想法以获得更详细的解释
您的代码没有退出,因为 cURL
没有完成,所以 PHP
在完成当前操作之前无法退出。
您为此练习选择的网站永远不会加载。
如何修复
要修复,请将 URL 替换为可以加载的内容,例如 https://google.com,例如
证明
我写了自己的代码示例来向我展示 when/where PHP
决定退出:
<?php
declare(ticks=1);
$t = 1;
$shutdownHandler = function () {
exit("EXITING NOW\n");
};
pcntl_signal(SIGINT, $shutdownHandler);
while (true) {
print "$t\n";
$t++;
}
当 运行 在终端中执行此操作时,您可以更清楚地了解 PHP 的运行方式:
在上图中,您可以看到当我通过 Ctrl + C
发出 SIGINT 命令时(如箭头所示),它完成了正在执行的操作,然后退出。
这意味着如果我的修复是正确的,那么在 OP 的代码中杀死 curl 所需要做的就是一个简单的 URL
更改:
<?php
declare(ticks=1);
$t = 1;
$shutdownHandler = function () {
exit("\nEXITING\n");
};
pcntl_signal(SIGINT, $shutdownHandler);
while (true) {
echo "CURL$t\n";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://google.com');
curl_exec($ch);
curl_close($ch);
}
然后 运行:
Viola! 正如预期的那样,脚本在当前进程完成后终止,就像它应该的那样。
最后的想法
您尝试 curl 的网站实际上是在无休止地发送您的代码。唯一能够停止进程的操作是 CTRL + X
、max execution time setting, or the CURLOPT_TIMEOUT
cURL 选项。当您使用 OUT pcntl_signal(SIGINT, $shutdownHandler);
时 CTRL+C
起作用的原因是因为 PHP 不再有内部函数正常关闭的负担。由于 PHP 不是并发的,当你有处理程序时,它必须在执行之前等待轮到它 - 它永远不会得到,因为 cURL
任务永远不会完成,从而离开你结果永无止境。
希望对您有所帮助!
如果可以升级到 PHP 7.1.X
或更高版本,我会使用 @Smuuf 发布的 Solution 3
。那是干净的。
但如果您无法升级,则需要使用变通方法。问题发生在下面的 SO thread
pcntl_signal function not being hit and CTRL+C doesn't work when using sockets
PHP 正忙于阻塞 IO,它无法处理您为其自定义处理程序的挂起信号。任何时候发出信号并且您有与之关联的处理程序,PHP 都会排队。一旦 PHP 完成其当前操作,它就会执行您的处理程序。
但不幸的是,在这种情况下,它永远不会有机会,您没有为 CURL 指定超时,它只是一个 PHP 无法逃脱的黑洞。
因此,如果您可以让一个 php 进程负责处理 SIGTERM
而一个子进程必须处理该工作,那么它就可以工作,因为父进程将能够捕获信号并处理回调,即使 child 被相同的占用
下面是演示相同内容的代码
<?php
$pid = pcntl_fork();
if ($pid == -1) {
die('could not fork');
} else if ($pid) {
// we are the parent
declare (ticks = 1);
//pcntl_async_signals(true);
$shutdownHandler = function()
{
echo 'Exiting' . PHP_EOL;
};
pcntl_signal(SIGINT, $shutdownHandler);
pcntl_wait($status); //Protect against Zombie children
} else {
// we are the child
echo "hanging now" . PHP_EOL;
while (true) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://blackhole.webpagetest.org');
curl_exec($ch);
curl_close($ch);
}
}
下面是实际操作
我有一个带有自定义关闭处理程序的 PHP 命令行应用程序:
<?php
declare(ticks=1);
$shutdownHandler = function () {
echo 'Exiting';
exit();
};
pcntl_signal(SIGINT, $shutdownHandler);
while (true) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://blackhole.webpagetest.org');
curl_exec($ch);
curl_close($ch);
}
如果我在 CURL 请求正在进行时使用 Ctrl+C 终止脚本,它没有任何效果。命令只是挂起。如果我删除自定义关闭处理程序,Ctrl+C 会立即终止 CURL 请求。
当我定义 SIGINT
处理程序时,为什么 CURL 无法终止?
我为此苦思冥想了一段时间,没有找到任何使用 pcntl 的最佳解决方案,但根据您的命令的预期用途,您可以考虑:
- 使用超时:
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
您可能已经知道有 many options to control the behavior of curl, including setting a progress callback function. - 使用
file_get_contents()
而不是 curl。
A solution using the Ev extension :
对于信号处理代码,它可以像这样简单:
$w = new EvSignal(SIGINT, function (){
exit("SIGINT received\n");
});
Ev::run();
Ev 扩展安装的快速总结:
sudo pecl install Ev
您需要通过添加到 php cli 的 php.ini 文件来启用扩展,可能是 /etc/php/7.0/cli/php.ini
extension="ev.so"
如果 pecl 抱怨缺少 phpize,请安装 php7.0-dev.
什么有效?
似乎真正有用的是给整个东西一些 space 来发挥它的信号处理魔力。这样的 space 似乎是通过启用 cURL 的进度处理来提供的,同时还设置了 "userland" 进度回调:
解决方案 1
while (true) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_NOPROGRESS, false); // "true" by default
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function() {
usleep(100);
});
curl_setopt($ch, CURLOPT_URL, 'http://blackhole.webpagetest.org');
curl_exec($ch);
curl_close($ch);
}
进度回调函数中好像需要"something"。空体似乎不起作用,因为它可能只是没有给 PHP 太多时间来处理信号 (硬核推测).
解决方案 2
即使 PHP 7.1
上没有 declare(ticks=1);
,将 pcntl_signal_dispatch()
放入回调似乎也能正常工作。
...
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function() {
pcntl_signal_dispatch();
});
...
解决方案 3 ❤ (PHP 7.1+)
使用 pcntl_async_signals(true)
而不是 declare(ticks=1);
可以工作 即使进度回调函数主体为空 .
这可能是我个人会使用的,所以我将把完整的代码放在这里:
<?php
pcntl_async_signals(true);
$shutdownHandler = function() {
die("Exiting\n");
};
pcntl_signal(SIGINT, $shutdownHandler);
while (true) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function() {});
curl_setopt($ch, CURLOPT_URL, 'http://blackhole.webpagetest.org');
curl_exec($ch);
curl_close($ch);
}
所有这三种解决方案都会导致 PHP 7.1
在点击 CTRL+C
后几乎立即 退出。
发生了什么事?
当您发送 Ctrl + C
命令时,PHP
尝试在退出前完成当前操作。
为什么我的(OP 的)代码没有退出?
在最后查看最后的想法以获得更详细的解释
您的代码没有退出,因为 cURL
没有完成,所以 PHP
在完成当前操作之前无法退出。
您为此练习选择的网站永远不会加载。
如何修复
要修复,请将 URL 替换为可以加载的内容,例如 https://google.com,例如
证明
我写了自己的代码示例来向我展示 when/where PHP
决定退出:
<?php
declare(ticks=1);
$t = 1;
$shutdownHandler = function () {
exit("EXITING NOW\n");
};
pcntl_signal(SIGINT, $shutdownHandler);
while (true) {
print "$t\n";
$t++;
}
当 运行 在终端中执行此操作时,您可以更清楚地了解 PHP 的运行方式:
在上图中,您可以看到当我通过 Ctrl + C
发出 SIGINT 命令时(如箭头所示),它完成了正在执行的操作,然后退出。
这意味着如果我的修复是正确的,那么在 OP 的代码中杀死 curl 所需要做的就是一个简单的 URL
更改:
<?php
declare(ticks=1);
$t = 1;
$shutdownHandler = function () {
exit("\nEXITING\n");
};
pcntl_signal(SIGINT, $shutdownHandler);
while (true) {
echo "CURL$t\n";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://google.com');
curl_exec($ch);
curl_close($ch);
}
然后 运行:
Viola! 正如预期的那样,脚本在当前进程完成后终止,就像它应该的那样。
最后的想法
您尝试 curl 的网站实际上是在无休止地发送您的代码。唯一能够停止进程的操作是 CTRL + X
、max execution time setting, or the CURLOPT_TIMEOUT
cURL 选项。当您使用 OUT pcntl_signal(SIGINT, $shutdownHandler);
时 CTRL+C
起作用的原因是因为 PHP 不再有内部函数正常关闭的负担。由于 PHP 不是并发的,当你有处理程序时,它必须在执行之前等待轮到它 - 它永远不会得到,因为 cURL
任务永远不会完成,从而离开你结果永无止境。
希望对您有所帮助!
如果可以升级到 PHP 7.1.X
或更高版本,我会使用 @Smuuf 发布的 Solution 3
。那是干净的。
但如果您无法升级,则需要使用变通方法。问题发生在下面的 SO thread
pcntl_signal function not being hit and CTRL+C doesn't work when using sockets
PHP 正忙于阻塞 IO,它无法处理您为其自定义处理程序的挂起信号。任何时候发出信号并且您有与之关联的处理程序,PHP 都会排队。一旦 PHP 完成其当前操作,它就会执行您的处理程序。
但不幸的是,在这种情况下,它永远不会有机会,您没有为 CURL 指定超时,它只是一个 PHP 无法逃脱的黑洞。
因此,如果您可以让一个 php 进程负责处理 SIGTERM
而一个子进程必须处理该工作,那么它就可以工作,因为父进程将能够捕获信号并处理回调,即使 child 被相同的占用
下面是演示相同内容的代码
<?php
$pid = pcntl_fork();
if ($pid == -1) {
die('could not fork');
} else if ($pid) {
// we are the parent
declare (ticks = 1);
//pcntl_async_signals(true);
$shutdownHandler = function()
{
echo 'Exiting' . PHP_EOL;
};
pcntl_signal(SIGINT, $shutdownHandler);
pcntl_wait($status); //Protect against Zombie children
} else {
// we are the child
echo "hanging now" . PHP_EOL;
while (true) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://blackhole.webpagetest.org');
curl_exec($ch);
curl_close($ch);
}
}
下面是实际操作