子进程把结果交回父进程,别用临时文件也别指望共享变量——最省事的是 fork 之前用 stream_socket_pair() 建一对 unix socket,fork 后父子各关掉自己不用的一端,子进程 write、父进程 read 就完了。
[$parentSock, $childSock] = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
$pid = pcntl_fork();
if ($pid === 0) {
fclose($parentSock); // 子进程只留自己那端
fwrite($childSock, json_encode(['shard' => $i, 'rows' => 1234]));
fclose($childSock);
exit(0); // 这行不能省
}
fclose($childSock);
$result = json_decode(stream_get_contents($parentSock), true);
fclose($parentSock);
pcntl_waitpid($pid, $status);
顺序很关键:先把数据读完,再 waitpid。socket pair 的内核缓冲区只有几十 KB,你如果先阻塞在 pcntl_waitpid,子进程把缓冲写满就会卡住,父进程等子进程、子进程等父进程读,直接死锁。数据量大的话让子进程分片 write + 父进程循环 fread,别一次性 stream_get_contents 硬扛。要跨多个 worker 汇总就用 SysV 消息队列(msg_get_queue / msg_send / msg_receive),但记得用完 msg_remove_queue,否则队列会一直挂在系统里。
fork 之后有三个坑比传数据更容易翻车:一是已建立的 MySQL / Redis 连接会被复制,父子共用一个 socket 描述符会导致协议错乱或 "server has gone away",正确做法是 fork 前不连、fork 后在子进程里各自重连;二是输出缓冲被复制,fork 前务必 fflush(STDOUT) 或 ob_end_flush(),不然同一段 echo 会打印两遍;三是信号,PHP 7.1+ 直接 pcntl_async_signals(true) + pcntl_signal(SIGTERM, ...),父进程收到退出信号后转发给所有子进程再 waitpid 收尾,才不会留一堆孤儿进程。
如果是给站里写批处理脚本,建议别在 web 请求里 fork,挂到计划任务跑 CLI 更稳;Clara 这边有 Cron::register 注册定时任务,配一条系统 crontab 每分钟打 /cron 就能准点跑,比在 fpm 里折腾多进程省心得多。