swoole 默认屏蔽所有信号,必须用 swoole\process::signal 在主线程、事件循环前注册,且不能在协程里调用;pcntl_signal 在 worker 进程中基本无效。

直接说结论:Swoole 默认屏蔽所有信号,必须用 Swoole\Process::signal 在主线程、事件循环前注册,且不能在协程里调用;pcntl_signal 在 Worker 进程中基本无效。
为什么 pcntl_signal 在 Swoole 里不触发
Swoole 启动时会调用底层 swSignal_none(),把所有线程(包括 Worker)的信号全部 SIG_BLOCK。此时你调用 pcntl_signal(SIGTERM, $cb),内核根本不会把信号投递给 PHP 层回调——它被静默丢弃了。
常见现象:kill -SIGTERM {pid} 后进程无反应、日志没输出、协程照常跑。用 strace -p {pid} -e trace=rt_sigprocmask 能看到信号始终处于阻塞态。
关键点:
-
pcntl_signal只适用于传统 CLI 短生命周期脚本,不兼容 Swoole 的多线程+事件循环模型 - 即使你在
onWorkerStart里调用,也晚了——信号屏蔽已在 Server 启动前完成 - PHP 7.4+ 的
pcntl_async_signals(true)对 Swoole 无效,它无法穿透 Swoole 的信号拦截层
Swoole\Process::signal 的正确注册时机与限制
这是唯一能生效的信号注册方式,它绕过 PHP 用户层,直接调用 Swoole 底层 swSignal_add(),写入全局 signals[] 数组并绑定到主线程。
必须满足三个硬性前提:
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
- 只能在主线程(Manager 或 Worker 的主循环线程)中调用,不能在
onRequest、go协程或定时器回调里执行 - 必须在
$server->start()之前完成,否则事件循环已接管,信号监听器进不了队列 - 回调函数必须是纯同步逻辑:禁止调用
Swoole\Coroutine::sleep()、$server->shutdown()、数据库操作等协程 API
典型写法:
Swoole\Process::signal(SIGTERM, function () {
echo "SIGTERM received\n";
SwooleG::set(['shutdown' => true]);
});
$server->start(); // signal 必须在这行之前
信号处理中容易忽略的系统级干扰
即使代码注册正确,仍可能收不到信号,原因常来自操作系统层:
- Docker 容器中,若没用
tini作 PID 1,SIGTERM会被容器 init 劫持,Worker 进程根本收不到 - systemd 服务单元中,
KillMode=control-group(默认)会导致信号发给整个 cgroup,而非主进程;应设为KillMode=mixed - OOM Killer 触发的是
SIGKILL,它不可捕获、不可忽略,任何注册都无效;若频繁出现,优先查dmesg -T | grep "killed process" -
SIGHUP在 Swoole 中默认用于重载配置,覆盖它可能导致热更新失效
优雅退出时连接清理的时序陷阱
信号只是起点,真正难点在后续状态协同。直接调用 $server->shutdown() 会立刻断开所有连接,引发批量 Connection reset by peer。
安全做法是分阶段推进:
- 信号回调里只设标志位(如
$isShuttingDown = true),不执行任何 I/O - 在
onRequest中检查该标志,拒绝新请求并返回 503 - 用定时器轮询空闲连接,对每个
$fd先调用$server->exist($fd)确认存活,再close - 等待所有活跃协程自然结束,最后退出进程
最易被忽略的是:残留协程仍可能向已关闭的 $fd 写入,报错 SWOOLE_ERROR_SESSION_CLOS(2026)。这说明状态清理和协程生命周期没对齐,不是信号没收到,而是“收到之后怎么收尾”没设计好。










