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 + Xmax 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);
        }
    }

下面是实际操作