pcntl_signal回调中不能做数据库IO,因其运行在异步信号上下文中,而MySQL扩展非async-signal-safe,调用会导致崩溃或连接异常;应仅设标志位,由主循环安全执行IO。

pcntl_signal 回调里为什么不能做数据库 IO
因为 pcntl_signal 注册的回调函数运行在信号中断上下文中,PHP 的 MySQL 扩展(mysqli、PDO)不是异步信号安全(async-signal-safe)的。调用它们可能破坏连接状态、导致内存越界或进程直接崩溃——这不是“偶尔出错”,而是 POSIX 信号机制决定的硬限制。
常见现象包括:mysqli_query(): MySQL server has gone away、Segmentation fault (core dumped)、或脚本静默退出。这些都不是 PHP Bug,是调用非安全函数触发的未定义行为。
- 信号处理器应极快返回,只做「标记」和「通知」,不执行 I/O、不分配堆内存、不调用任何扩展函数(除
pcntl_signal、exit等少数几个) - 即使你用
pcntl_async_signals(true)启用异步信号,也不能绕过这个限制:它只是让信号更及时到达,不改变回调执行环境的安全性 - 协程框架(如 Swoole)或 EventLoop(如 ReactPHP)中注册的信号处理器同样受此约束,不能因“常驻进程”就误以为可以随便连 DB
用全局标志 + 主循环轮询替代
把“想做的事”从信号回调里移出来,放到主业务循环中判断执行。这是最兼容、最稳定的做法。
例如你想在收到 SIGUSR1 时重载配置并刷新缓存,不要在信号回调里写 $pdo->query("SELECT ..."),而是:
立即学习“PHP免费学习笔记(深入)”;
- 在信号回调中仅设置一个全局布尔变量:
$GLOBALS['reload_config'] = true - 主循环每次迭代前检查该标志:
if ($GLOBALS['reload_config'] ?? false) { doReload(); $GLOBALS['reload_config'] = false; } -
doReload()函数里再安全地连接数据库、读取配置、更新 Redis 缓存等
注意:如果主循环有阻塞操作(如 sleep(5)),需确保在 sleep 前/后都调用 pcntl_signal_dispatch()(PHP pcntl_async_signals(true)(PHP ≥ 8.0),否则信号可能被延迟甚至丢失。
需要真正异步触发 DB 操作怎么办
如果业务逻辑要求“一收到信号就必须立刻触发一次 DB 查询”,那说明你的架构不适合用信号直连 DB——得换通信方式。
可行替代路径:
- 用
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1])阻塞信号,再通过pcntl_waitpid(-1, $status, WNOHANG)或stream_select()监听子进程/管道/Socket 事件,在事件回调中做 DB 操作(信号只用于唤醒等待) - 改用 Unix Domain Socket 或 TCP 端口监听外部命令(比如
echo "reload" | nc -U /tmp/worker.sock),主进程用stream_socket_accept()接收后处理 DB - 在信号回调中写一个轻量级文件标记(
file_put_contents('/tmp/reload.flag', '1')),主循环定期file_exists()+unlink(),再触发 DB 操作(注意并发时加锁)
这些方案都绕开了信号上下文的限制,代价是多一层间接性,但换来的是可预测性和稳定性。
容易忽略的兼容性细节
PHP 7.3 默认不启用 pcntl_async_signals,必须显式调用 pcntl_async_signals(true) 才能让信号不依赖 declare(ticks=1)。但即便启用了,也不会让 mysqli/PDO 变成信号安全——这点文档没明说,但所有线上事故都指向同一结论。
另一个坑:sleep() 和 usleep() 在信号到达时会被中断并返回剩余秒数,如果你没检查返回值,就以为“睡够了”,实际逻辑可能错乱。正确写法是:
$remaining = 5;
while ($remaining > 0) {
$remaining = sleep($remaining);
}
信号处理的边界很窄,DB IO 是明确越界的行为。宁可多一层调度,也不要赌扩展函数在中断上下文里的表现。



















