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
相关产品推荐
相关产品推荐

