【问题标题】:CURL cannot be killed by a PHP SIGINT with custom signal handlerCURL 不能被带有自定义信号处理程序的 PHP SIGINT 杀死
【发布时间】:2018-04-10 17:12:59
【问题描述】:

我有一个带有自定义关闭处理程序的 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 无法杀死?

【问题讨论】:

  • 为什么你认为 CURL 是在响应信号,而不是完全被忽略?
  • @IMSoP 抱歉,我的问题可能有点误导(我已经更新)。问题的关键是为什么 CURL 在这种情况下无法杀死。
  • Curl 是一个外部库,因此在操作过程中杀死它从用户态来说并不容易/不可能。您将不得不弄清楚如何重新连接到默认信号处理程序以使其正确终止。
  • 我认为答案在于stackoverflow.com/questions/26934216/…,问题是您的 curl 调用被阻塞 io 卡住并且 php 根本无法访问您的信号处理程序,并且设置处理程序会禁用原始处理程序一起
  • @Jonathan,如果不重构7.0.X 中的代码,就不可能做到这一点。看看这个要点是否是您可以接受的解决方案? gist.github.com/tarunlalwani/6b4f2b81f20c781234899e62f22b0436,如果是那我就发一个答案

标签: php curl sigint


【解决方案1】:

什么有效?

真正起作用的是给整个事物一些空间来发挥其信号处理的魔力。这样的空间似乎是通过启用 cURL 的进度处理来提供的,同时还设置了“用户空间”进度回调:

解决方案 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);
}

进度回调函数中似乎需要“某些东西”。空体似乎不起作用,因为它可能只是没有给 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后几乎立即退出。

【讨论】:

  • 不是sleep(100)。是 usleep(100)。 100 微秒 = .1 毫秒.
  • 好的,对于造成的误解,我深表歉意,但如果您没有发现该解决方案有任何缺点,我建议您使用第三种解决方案。
  • 第三种解决方案仅适用于 7.1.X 或更高版本。好像v7.0.28-0ubuntu0.16.04.1 没有
  • 那是因为pcntl_async_signals() 是available only in PHP 7.1+。
【解决方案2】:

发生了什么?

当您发送Ctrl + C 命令时,PHP 会在退出前尝试完成当前操作。

为什么我的(OP)代码不退出?

请参阅最后的想法以获得更详细的说明

您的代码不会退出,因为cURL 没有完成,所以PHP 在完成当前操作之前无法退出。

您为此练习选择的网站永远不会加载。

如何解决

要解决此问题,请将 URL 替换为可以加载的内容,例如 https://google.com

证明

我编写了自己的代码示例来准确显示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 设置或CURLOPT_TIMEOUT cURL 选项。当您取出 pcntl_signal(SIGINT, $shutdownHandler); 时,CTRL+C 起作用的原因是 PHP 不再有内部函数优雅关闭的负担。由于 PHP 不是并发的,当您确实有处理程序时,它必须在执行之前等待轮到它 - 它永远不会得到,因为 cURL 任务永远不会完成,因此给您留下永无止境的结果.

我希望这会有所帮助!

【讨论】:

  • 感谢您的详细回复,但是选择慢速加载 URL 是问题的关键。如果信号处理程序未定义,则始终会终止请求,即使它正在进行中。当信号处理程序被定义并且是我感兴趣的时候,这种行为会发生变化。
  • @Jonathan 我详细介绍了为什么它不适用于您的 URL。请阅读完整的答案。
  • 您的代码不会退出,因为 cURL 没有完成,所以 PHP 在完成当前操作之前无法退出。 字面上是答案,唯一的解决方法是更改网址。
  • OUT pcntl_signal(SIGINT, $shutdownHandler);是因为 PHP 不再有内部函数优雅关闭的负担。由于 PHP 不是并发的,当您确实有处理程序时,它必须等待它的期限才能执行 - 因为 cURL 任务永远不会完成,所以它永远不会得到它,从而给您留下永无止境的结果。
【解决方案3】:

如果可以升级到PHP 7.1.X 或更高版本,我会使用@Smuuf 发布的Solution 3。很干净。

但如果您无法升级,则需要使用解决方法。该问题的发生如下面的 SO 线程中所述

pcntl_signal function not being hit and CTRL+C doesn't work when using sockets

PHP 正忙于阻塞 IO,它无法处理您为其自定义处理程序的挂起信号。任何时候发出信号并且您有与之关联的处理程序,PHP 都会排队。一旦 PHP 完成其当前操作,它就会执行您的处理程序。

但不幸的是,在这种情况下它永远不会有这个机会,你没有为你的 CURL 指定超时,它只是一个 PHP 无法逃脱的黑洞。

因此,如果您可以让一个 php 进程负责处理SIGTERM,而一个子进程必须处理该工作,那么它将起作用,因为父进程将能够捕获信号并处理回调甚至当孩子被同样的事情占据时

下面是演示相同的代码

<?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);
        }
    }

下面是它的行动

【讨论】:

    【解决方案4】:

    我已经为此摸索了一段时间,但没有找到任何使用 pcntl 的最佳解决方案,但取决于您的命令的预期用途,您可能会考虑:

    • 使用超时:
      curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
      您可能已经知道有 many options 来控制 curl 的行为,包括设置 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。

    【讨论】:

    • 投反对票与否这是一个比你更好的答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-12-21
    • 2012-11-01
    • 2018-05-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-11-16
    相关资源
    最近更新 更多