pcntl_fork()仅复制进程,不能直接并行调用函数;需fork后用if/else分流,子进程须exit()结束,父进程须wait回收,否则产生僵尸进程。

pcntl_fork 不能直接“并行调用函数”
很多人以为 pcntl_fork() 是个“并发执行函数”的工具,比如想写 pcntl_fork(call_user_func('send_email')) —— 这完全不行。pcntl_fork() 只做一件事:复制当前进程(包括全部内存、变量、执行位置),返回两个几乎一模一样的进程,之后靠 pcntl_wait() 或 pcntl_waitpid() 协调。真正的“并行逻辑”必须手动写在子进程中,且父子进程从 fork 后各自走自己的分支。
正确写法:fork 后用 if/else 区分子父进程
常见错误是 fork 后不判断返回值,导致父子进程都往下执行同一段代码,结果任务被重复跑多次。正确结构必须显式分流:
$pid = pcntl_fork();
if ($pid == -1) {
die('fork failed');
} elseif ($pid == 0) {
// 子进程:执行你要并行的任务
echo "Child: processing task A\n";
sleep(2);
exit(0); // 子进程必须 exit,否则会继续 fork 下一轮
} else {
// 父进程:可继续 fork 其他子进程,或等待
echo "Parent: forked child $pid\n";
}
- 子进程必须以
exit()或pcntl_exit()结束,否则它会继续执行后续的pcntl_fork(),造成进程爆炸 - 父进程不要直接 exit,否则子进程变成 init 的子进程(虽能运行,但难以管理)
- 多个任务要并行?循环 fork 多次,每次都在
if ($pid == 0)里写对应逻辑,或统一传参区分
必须 wait,否则僵尸进程堆积
子进程结束后,内核保留其退出状态,直到父进程调用 pcntl_wait() 或 pcntl_waitpid() 获取——否则就是僵尸进程。线上跑几小时后 ps aux | grep 'Z' 就能看到一堆 Z 状态。
常用做法是 fork 完所有子进程后,用循环 wait:
立即学习“PHP免费学习笔记(深入)”;
// 假设 $pids = [$pid1, $pid2, $pid3];
foreach ($pids as $pid) {
pcntl_waitpid($pid, $status); // 阻塞等待指定子进程
}
- 用
pcntl_waitpid($pid, $status, WNOHANG)可非阻塞轮询,适合需要超时控制的场景 - 如果子进程可能提前退出,建议在 fork 后立即设置
pcntl_signal(SIGCHLD, 'sigchild_handler')并在 handler 里调用pcntl_waitpid(-1, $status, WNOHANG)回收,避免漏收 -
pcntl_wait()默认只等一个子进程;要等全部,得循环调用或配合信号处理
注意扩展限制和替代方案
pcntl 扩展默认不启用,CLI 模式下需确认已加载(php -m | grep pcntl),且 完全不支持 Web SAPI(如 Apache mod_php、php-fpm)——在网页请求中调用 pcntl_fork() 会静默失败或直接崩溃。
- Web 场景别硬上 pcntl:改用消息队列(Redis List + worker)、Swoole TaskWorker、或异步 HTTP 客户端(如 Guzzle 的 Promise)
- CLI 脚本中,注意 ulimit -u(用户最大进程数),fork 太多会触发
fork failed: Resource temporarily unavailable - 子进程无法共享父进程的资源句柄(如 MySQL 连接、cURL handle),必须在子进程中重新初始化
真正难的不是 fork 几次,而是确保每个子进程干净退出、状态可捕获、资源不泄漏、错误不静默——这些细节漏掉一个,线上就容易半夜告警。











