You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP7.4中register_shutdown_function与ignore_user_abort失效问题求助

问题

我有一个用于流式输出ping结果的PHP页面,功能正常,但当用户关闭连接后,ping命令仍会在后台持续运行。我已尝试使用ignore_user_abort()和register_shutdown_function()函数,但均未生效。该服务运行在搭载uhttpd的OpenWRT系统中。

类实现代码

class process
{
    private $proc;
    private $pid;
    private $descriptorspec;

    function start($cmd)
    {
        $cwd = '/tmp';
        $this->proc = proc_open($cmd, $this->descriptorspec, $pipes, $cwd);
        $this->pid = proc_get_status($this->proc)['pid'];
        session_write_close();
        ignore_user_abort(true);
        set_time_limit(0);
        ini_set('zlib.output_compression', 0);
        ini_set('implicit_flush', 1);

        while (!feof($pipes[1])) {
            echo ">" . fread($pipes[1], 4096);
            flush();
            ob_flush();
            while (ob_get_level()) {
                ob_end_clean();
            }
            if (connection_status() != CONNECTION_NORMAL) {
                shell_exec("killall ping &");
                break;
            }
        }
        pclose($this->proc);
    }

    function __construct()
    {
        register_shutdown_function(array($this, "onShutdown"));
        $this->descriptorspec = array(
            0 => array("pipe", "r"),  // stdin是子进程读取的管道
            1 => array("pipe", "w"),  // stdout是子进程写入的管道
            2 => array("file", "/tmp/app/proc.error.output.txt", "a") // stderr写入到文件
        );
    }

    function onShutdown()
    {
        shell_exec("killall ping &");
    }

    function __destruct()
    {
        shell_exec("killall ping");
//        posix_kill($this->pid, SIGKILL);
//        proc_terminate($this->proc);
//        pclose($this->proc);
    }
}

函数式实现代码

global $proc, $pid;
while (@ ob_end_flush()) ;

$descriptorspec = array(
    0 => array("pipe", "r"),  // stdin是子进程读取的管道
    1 => array("pipe", "w"),  // stdout是子进程写入的管道
    2 => array("file", "/tmp/app/proc.error.output.txt", "a") // stderr写入到文件
);
$cwd = '/tmp';
$proc = proc_open($cmd, $descriptorspec, $pipes, $cwd);
$pid = proc_get_status($proc)['pid'];
session_write_close();
ignore_user_abort(true);
set_time_limit(0);
register_shutdown_function('on_shutdown');

while (ob_get_level()){
    ob_get_contents();
    ob_end_clean();
}

//ob_start();
while (!feof($pipes[1])) {
    echo fread($pipes[1], 4096);
    flush();
    ob_flush();
    if (connection_aborted() != 0) {
        posix_kill($pid, SIGKILL);
        pclose($proc);
    }
}
//ob_end_flush();
pclose($proc);

解决方案

问题根源在于uhttpd的特性和PHP连接检测机制的配合问题,以下是针对性修复方案:

  1. 关闭ignore_user_abort(true)
    你设置的ignore_user_abort(true)会强制PHP在用户断开连接后继续执行脚本,直接阻止了自动终止逻辑。改为ignore_user_abort(false),让PHP在连接断开时触发终止流程。

  2. 用proc_terminate()替代killall
    killall ping会杀掉所有ping进程,容易误杀其他用户的任务。直接用proc_terminate()终止当前启动的子进程,配合proc_close()清理资源,精准且安全。

  3. 优化连接检测逻辑
    uhttpd不会实时传递连接断开信号,在循环中加入短延时(如usleep(100000)),给PHP留出检测连接状态的时间。同时确保每次输出后都执行连接检测。

  4. 彻底清理输出缓冲
    残留的输出缓冲会导致PHP无法及时感知连接断开,启动脚本前需彻底清空所有缓冲层。

修改后的类实现示例

class process
{
    private $proc;
    private $pid;
    private $descriptorspec;

    function start($cmd)
    {
        $cwd = '/tmp';
        $this->proc = proc_open($cmd, $this->descriptorspec, $pipes, $cwd);
        $this->pid = proc_get_status($this->proc)['pid'];
        session_write_close();
        // 关闭ignore_user_abort,允许PHP在连接断开时终止脚本
        ignore_user_abort(false);
        set_time_limit(0);
        ini_set('zlib.output_compression', 0);
        ini_set('implicit_flush', 1);
        // 彻底清空输出缓冲
        while (ob_get_level()) {
            ob_end_clean();
        }

        while (!feof($pipes[1])) {
            $output = fread($pipes[1], 4096);
            if ($output) {
                echo ">" . $output;
                flush();
            }
            // 检测连接是否断开
            if (connection_aborted()) {
                proc_terminate($this->proc, SIGKILL);
                proc_close($this->proc);
                break;
            }
            // 短延时,让PHP有机会检测连接状态
            usleep(100000);
        }
        // 确保资源被清理
        if (is_resource($this->proc)) {
            proc_close($this->proc);
        }
    }

    function __construct()
    {
        register_shutdown_function(array($this, "onShutdown"));
        $this->descriptorspec = array(
            0 => array("pipe", "r"),
            1 => array("pipe", "w"),
            2 => array("file", "/tmp/app/proc.error.output.txt", "a")
        );
    }

    function onShutdown()
    {
        if (is_resource($this->proc)) {
            proc_terminate($this->proc, SIGKILL);
            proc_close($this->proc);
        }
    }

    function __destruct()
    {
        $this->onShutdown();
    }
}

修改后的函数式实现示例

global $proc, $pid;
// 彻底清空输出缓冲
while (@ ob_end_flush()) ;

$descriptorspec = array(
    0 => array("pipe", "r"),
    1 => array("pipe", "w"),
    2 => array("file", "/tmp/app/proc.error.output.txt", "a")
);
$cwd = '/tmp';
$proc = proc_open($cmd, $descriptorspec, $pipes, $cwd);
$pid = proc_get_status($proc)['pid'];
session_write_close();
// 关闭ignore_user_abort
ignore_user_abort(false);
set_time_limit(0);
register_shutdown_function('on_shutdown');

while (ob_get_level()){
    ob_end_clean();
}

while (!feof($pipes[1])) {
    $output = fread($pipes[1], 4096);
    if ($output) {
        echo $output;
        flush();
    }
    if (connection_aborted()) {
        proc_terminate($proc, SIGKILL);
        proc_close($proc);
        break;
    }
    usleep(100000);
}

if (is_resource($proc)) {
    proc_close($proc);
}

function on_shutdown() {
    global $proc;
    if (is_resource($proc)) {
        proc_terminate($proc, SIGKILL);
        proc_close($proc);
    }
}

额外注意事项

  • 确保OpenWRT的PHP环境中output_buffering设置为Off,避免缓冲影响连接检测。
  • 测试时用ps | grep ping验证用户断开连接后ping进程是否被终止。
  • 若使用posix_kill,需确保PHP安装了posix扩展,推荐用proc_terminate兼容性更好。

内容的提问来源于stack exchange,提问作者lordkian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 18:05:23