不是。fork后内存并非零复制,php的opcache初始化、require加载、静态变量赋值等全局写操作会立即触发cow页复制,导致10个worker进程rss达单进程的9.2倍;必须将所有修改内存的初始化动作(如preload、autoload、时区设置)移至onworkerstart之前,在master进程一次性完成,确保fork后子进程复用只读页。

Workerman 多进程启动时,fork 之后的内存真的“零复制”吗?
不是。PHP 进程 fork 后,Linux 内核确实用 Copy-on-Write(COW)机制延迟物理页复制,但 PHP 自身行为会提前触发大量写操作,让 COW 失效——比如 opcache 初始化、require 加载文件后对 opcode 缓存的写入、静态变量赋值、date_default_timezone_set() 等全局状态修改。这些都会让子进程立即分配新内存页,失去 COW 优势。
实测发现:未做干预时,10 个 Worker 进程总 RSS 可能达单进程的 9.2 倍(而非理想中的 ≈1.05 倍)。关键不在“有没有 fork”,而在“哪些代码在 fork 后第一时间写了内存”。
哪些初始化动作必须挪到 onWorkerStart 之前?
所有会修改进程私有内存的全局操作,都应压到主进程(master)里一次性做完,确保 fork 后子进程直接复用干净的只读页。常见踩坑点:
-
opcache.enable=1必须在 php.ini 中开启,且opcache.preload文件要在Worker::runAll()前 require —— 否则每个 Worker 各自 preload,重复编译+写缓存 - 不要在
onWorkerStart里require_once 'vendor/autoload.php',改用 composer 的classmap或提前include到主进程 - 避免在
onWorkerStart中调用set_error_handler()、mb_internal_encoding()、iconv_set_encoding()等会写 PHP 内部状态的函数 - 配置类如
Config::load('app')若内部用了static $cache = [],必须确保加载发生在 fork 前;否则每个 Worker 都新建一份空数组,看似小,百万连接下就是 GB 级冗余
如何验证 COW 是否真正生效?
别信理论,看 /proc/PID/smaps 里的 Shared_Clean 和 Private_Dirty 字段:
启动 Workerman 后,挑一个稳定 Worker 进程(PID=12345),执行:grep -E "^(Shared_Clean|Private_Dirty)" /proc/12345/smaps | awk '{sum += $2} END {print sum}'
对比 master 进程(通常是 PID 最小的那个)的同样计算结果。若两者差值 30MB,大概率是某处初始化逻辑漏到了 fork 后。
另一个快速信号:ps aux --sort=-%mem | grep php 输出中,多个 Worker 的 RSS 值是否高度接近(标准差
PHP 8.4+ 的 clone with 能替代 COW 吗?
不能。COW 是内核级 fork 优化,clone with 是用户态对象克隆语法糖,解决的是业务层会话对象复制开销,和进程级内存共享无关。但它能间接帮 COW:比如把原本放在 $connection->session 里的大数组,改成只存 ID + 用 clone with 按需构造轻量副本,就减少了每个连接对象对私有内存页的写压力,降低触发 COW 的概率。
真正容易被忽略的一点:COW 效果不取决于你写了多少行代码,而取决于第 1 行 fork() 调用后,前 10ms 内有多少内存地址被首次写入——这 10ms 的安静,决定了后续百万连接的内存基线。











