php无真正多进程,仅通过pcntl_fork()或proc_open()模拟,web场景禁用pcntl,cli需手动回收子进程、重建资源、重注册信号;推荐队列、协程等替代方案。

PHP 里没有真正的多进程,只有模拟或借壳
PHP 本身是无状态、请求生命周期短的语言,不原生支持像 Go 或 Rust 那样的轻量级并发进程。所谓“多进程”,实际是靠 pcntl_fork() 派生子进程,或用 proc_open() / exec() 启外部命令——前者受限于 Unix 系统且不能用在 Web SAPI(如 Apache mod_php),后者本质是进程间协作而非编程模型。
常见错误现象:pcntl_fork() 在 Nginx + PHP-FPM 下直接返回 false,或者 fork 后子进程写日志/连数据库失败;proc_open() 启的进程成了孤儿,父进程没 wait 就退出,导致僵尸进程堆积。
- Web 场景下别硬上
pcntl_fork(),FPM 工作进程会拒绝 fork,Apache 的 prefork MPM 虽支持但极易引发资源竞争 - CLI 场景可用
pcntl_fork(),但必须手动pcntl_wait()或pcntl_waitpid()回收,否则子进程变僵尸 -
proc_open()更安全,适合调外部二进制(如ffmpeg、convert),但注意管道阻塞:不读 stderr/stdout 可能卡死整个进程
pcntl_fork() 后资源不能共享,变量不是“复制”而是“快照”
fork 后父子进程内存独立,修改全局变量、静态属性、PDO 连接、Redis 实例等,彼此完全不可见。这不是 bug,是 copy-on-write 机制决定的——你改的只是自己那份副本。
典型误用:$db = new PDO(...); pcntl_fork(); $db->exec("INSERT ..."); —— 子进程执行时可能报 PDOException: SQLSTATE[HY000]: General error: 2006 MySQL server has gone away,因为父进程的连接句柄在 fork 后失效,子进程复用它会出错。
立即学习“PHP免费学习笔记(深入)”;
- fork 前关闭所有数据库连接、Redis 客户端、文件句柄;子进程里重新初始化
- 不要依赖全局状态传递数据,改用
pcntl_signal()发信号 + 共享内存(shmop_*)或临时文件交换小量数据 - 避免在 fork 后继续用 Laravel/Eloquent 等带连接池或单例缓存的框架组件,它们内部状态大概率不同步
信号处理必须显式启用,且不能在 fork 前注册
pcntl_signal() 默认不生效,必须配合 pcntl_signal_dispatch() 或设置 ticks(declare(ticks=1))。更重要的是:信号处理器是在当前进程上下文注册的,fork 后子进程不会继承父进程的信号处理逻辑。
常见错误:pcntl_signal(SIGUSR1, 'handle'); pcntl_fork(); —— 子进程收不到 SIGUSR1,因为 handler 没在子进程中重注册。
- 每个子进程启动后第一件事就是调用
pcntl_signal()注册自己需要响应的信号 - Web SAPI 下禁用信号(
pcntl_signal直接失败),CLI 才可靠 - 避免在信号回调里做耗时操作(如写文件、查库),只设标志位,主循环里检查
替代方案比硬啃 pcntl 更实用
多数业务场景要的不是“多进程”,而是“并发执行任务”或“后台长时间运行”。与其踩 pcntl 的坑,不如换更稳的路:
- 队列:用 Redis +
php artisan queue:work(Laravel)或amqp扩展跑 worker 进程,天然隔离、可伸缩 - 协程:Swoole 4.0+ 或 RoadRunner 提供真正的并发模型,
Swoole\Process封装了 fork/wait/信号,比裸 pcntl 安全得多 - 系统级调度:简单定时任务用
system("nohup php job.php > /dev/null 2>&1 &"),但务必加nohup和重定向,否则 Web 请求一结束进程就被 kill
真正难的从来不是“怎么 fork”,而是“怎么让多个进程不互相踩脚、不抢资源、不出僵尸、不丢数据”。这些细节藏在 signal 处理时机、连接重建逻辑、标准流重定向里,漏掉任何一环,程序就只在测试环境跑得通。











