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

PHP如何阻止SIGTERM信号冒泡传递给proc_open启动的子进程

问题描述

需求为按下CTRL+C触发SIGINT,或向PHP父进程发送SIGTERM信号时,不直接杀死proc_open启动的子进程,等待子进程完成优雅关闭流程后父进程再退出。
最初遇到的现象是PHP会直接立即终止proc_open启动的子进程,尝试添加pcntl_waitpid($childPid)相关代码未达到预期。
初始测试代码如下:

<?php

// Handle close signals (like CTRL+C).
function closeChildApplication(int $signal) {
    // ???
}

// Async signals instead of declare(TICKS=1) PHP 7.1+
pcntl_async_signals(true);
pcntl_signal(SIGTERM, 'closeChildApplication', false);
pcntl_signal(SIGINT, 'closeChildApplication', false);

// Create process.
$descriptors = [
    0 => ['pipe', 'r'],
    1 => ['pipe', 'w'],
    2 => ['pipe', 'w']
];
$proc = proc_open('<command-to-run>', $descriptors, $pipes);
if (!is_resource($proc)) {
    throw new \RuntimeException('Could not execute <command-to-run>.');
}

// Not sending any data to child, so close STDIN immediately.
fclose($pipes[0]);

// Non blocking mode.
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);

do {
    $status = proc_get_status($proc);

    $data = '';
    while (!feof($pipes[1]) && false !== ($line = fgets($pipes[1]))) {
        $data .= $line;
    }
    if (!empty(trim($data))) {
        // STDOUT logic here.
    }

    $data = '';
    while (!feof($pipes[2]) && false !== ($line = fgets($pipes[2]))) {
        $data .= $line;
    }
    if (!empty(trim($data))) {
        // STDERR logic here.
    }

    sleep(1);
} while ($status['running']);

// Not running anymore, close resource.
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($proc);

问题更新

排查后确认PHP本身默认会等待子进程关闭,实际问题是被启动的目标程序自身对SIGTERM、SIGINT信号处理逻辑不完善,收到信号后直接立即退出,不会执行清理操作完成优雅关闭。已经验证可以通过向子进程的STDIN发送QUIT命令触发其正常的干净关闭流程。
新遇到的问题:

  • PHP父进程收到SIGTERM/SIGINT信号时,信号会同步传递给proc_open启动的子进程,导致子进程被强制杀死,无法执行优雅关闭流程
  • 测试pcntl_signal(SIGTERM, SIG_IGN, false);写法:可以让PHP忽略对应信号,同时阻止信号发送给子进程,但这种模式下无法感知进程是否收到了目标信号
  • 测试自定义空信号处理函数写法:
function closeChildApplication(int $signal) {
    // do nothing in here.
}

pcntl_signal(SIGTERM, 'closeChildApplication', false);

这种写法下PHP会忽略信号继续运行,但子进程依然会收到信号被杀死。
核心诉求:找到方法阻止信号冒泡传递给子进程,或是在忽略信号的前提下,能够在循环逻辑中检测到是否收到了对应信号,触发子进程的优雅关闭流程。

解决方案

问题的核心原因是:CTRL+C触发的SIGINT、系统发送的SIGTERM默认会被操作系统发送给整个前台进程组下的所有进程,父子进程同属一个进程组时,即使父进程通过pcntl注册了信号处理函数拦截自身收到的信号,内核还是会直接给同组的子进程发送信号,导致子进程被强制杀死。
解决思路分三步:

  • 启动子进程时将其放到独立的进程组/新会话,从系统层面隔离信号传递,让子进程不会收到发给父进程所在进程组的信号
  • 父进程注册信号处理函数,仅用一个全局变量标记收到退出信号,不做其他复杂操作,既可以感知信号事件,又不会中断主流程
  • 主循环检测到退出标记后,通过STDIN管道向子进程发送QUIT命令触发优雅关闭,持续轮询读取子进程输出直到子进程主动退出,再清理所有资源退出

完整可运行代码如下:

<?php
// 退出状态标记,信号处理函数仅修改该值,不执行IO等复杂操作
$shouldExit = false;
$quitCommandSent = false;

// 信号处理函数
function handleShutdownSignal(int $signal) {
    global $shouldExit;
    $shouldExit = true;
}

// 开启异步信号处理(PHP 7.1+ 无需配置declare(ticks=1))
pcntl_async_signals(true);
pcntl_signal(SIGTERM, 'handleShutdownSignal', false);
pcntl_signal(SIGINT, 'handleShutdownSignal', false);

// 进程描述符配置
$descriptors = [
    0 => ['pipe', 'r'],
    1 => ['pipe', 'w'],
    2 => ['pipe', 'w']
];

// 进程启动选项,根据操作系统配置隔离信号
$procOptions = [];
if (PHP_OS_FAMILY !== 'Windows') {
    // Linux/macOS下让子进程启动在独立会话,脱离父进程的进程组,不会继承父进程收到的信号
    $procOptions['setsid'] = true;
} else {
    // Windows下绕过cmd shell,创建独立进程组,隔离CTRL+C等控制台事件
    $procOptions['bypass_shell'] = true;
    $procOptions['create_process_group'] = true;
}

$proc = proc_open('<command-to-run>', $descriptors, $pipes, null, null, $procOptions);
if (!is_resource($proc)) {
    throw new \RuntimeException('Could not execute <command-to-run>.');
}

// 低版本PHP不支持proc_open的setsid选项时,手动调用posix函数设置子进程为独立进程组
// $procStatus = proc_get_status($proc);
// if (PHP_OS_FAMILY !== 'Windows' && function_exists('posix_setpgid')) {
//     posix_setpgid($procStatus['pid'], $procStatus['pid']);
// }

// 配置输出管道为非阻塞模式
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);

do {
    $status = proc_get_status($proc);

    // 收到退出信号且子进程还在运行、还没发过QUIT命令时,发送优雅关闭指令
    if ($shouldExit && !$quitCommandSent && $status['running']) {
        fwrite($pipes[0], "QUIT\n");
        fclose($pipes[0]); // 写完关闭STDIN,通知子进程输入结束
        $quitCommandSent = true;
    }

    // 读取STDOUT
    $data = '';
    while (!feof($pipes[1]) && false !== ($line = fgets($pipes[1]))) {
        $data .= $line;
    }
    if (!empty(trim($data))) {
        // 此处编写STDOUT处理逻辑
    }

    // 读取STDERR
    $data = '';
    while (!feof($pipes[2]) && false !== ($line = fgets($pipes[2]))) {
        $data .= $line;
    }
    if (!empty(trim($data))) {
        // 此处编写STDERR处理逻辑
    }

    // 未收到退出信号时1秒轮询一次,收到退出信号后缩短到0.1秒轮询,加快退出响应
    if (!$shouldExit) {
        sleep(1);
    } else {
        usleep(100000);
    }
} while ($status['running']);

// 清理资源
if (is_resource($pipes[0])) {
    fclose($pipes[0]);
}
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($proc);

注意事项:

  • 不要提前关闭STDIN管道,否则无法向子进程写入QUIT命令触发优雅关闭,等发送完关闭命令后再关闭即可
  • 不要使用SIG_IGN作为信号处理回调,这种模式下PHP会完全忽略信号,无法感知退出事件,自然没法触发子进程的关闭流程
  • 信号处理函数里不要做复杂的IO、进程操作,只修改标记位即可,避免信号处理上下文下出现不可预期的问题
  • 如果子进程的优雅关闭指令不是QUIT\n,替换成实际对应的指令即可,注意多数交互式程序需要加换行符才会识别输入的命令

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 06:18:21