PHP 的进程 fork 机制,是整个 PHP Web 运行模式的基石。
核心:Apache/Nginx 如何产生 PHP 进程
PHP 本身并不负责 fork ,fork 动作是由**与 Web Server 配合的 SAPI(Server API)**完成的。最常见的是 PHP-FPM(FastCGI Process Manager)。
1. PHP-FPM 的进程池模型
PHP-FPM 在启动时,会预先 fork 出一堆空闲的 PHP 解释器进程,等着处理请求。
启动 PHP-FPM:
Master 进程 (root)
│
│ fork() 系统调用
├── Worker 进程 1 (空闲)
├── Worker 进程 2 (空闲)
├── Worker 进程 3 (空闲)
├── ... (默认启动几十到几百个)
└── Worker 进程 N (空闲)
- Master 进程:负责管理,不处理请求。挂了就 fork 新的补上。
- Worker 进程:真正干活的。每个 Worker 就是一个完整的、独立的 PHP 解释器实例。
2. 请求到来时的流程
1. 用户请求 http://xxx/seckill.php
│
▼
2. Nginx (Web Server)
│ 通过 FastCGI 协议转发
▼
3. PHP-FPM Master 进程
│ 从空闲进程池挑一个 Worker
▼
4. PHP-FPM Worker 进程 7 (PID 12345)
│ - 这个进程已经被 fork 好了,空转着等活儿
│ - 它有自己的独立内存空间(包括自己的堆)
│ - 加载 seckill.php 并执行
│ - 返回响应给 Nginx
│ - 进程状态变回"空闲",不会销毁
▼
5. Worker 进程 7 继续等待下一个请求
关键点:Worker 进程是复用的(不是每个请求来都要 fork 一次新进程)。
底层:fork() 系统调用的逻辑
Linux 的 fork() 创建一个子进程,核心动作是将父进程的内存完整复制一份。
父进程 (PID 1000)
┌────────────────────────────┐
│ 代码段 │
│ 数据段 (stock = 100) │
│ 堆 │
│ 栈 │
│ 文件描述符表 │
└────────────────────────────┘
│
│ fork()
▼
子进程 (PID 1001)
┌────────────────────────────┐
│ 代码段 (复制) │
│ 数据段 (stock = 100 复制) │ ← 独立!和父进程完全隔离
│ 堆 (复制) │
│ 栈 (复制) │
│ 文件描述符表 (复制) │
└────────────────────────────┘
写时复制(Copy-On-Write, COW)优化:
现代 OS 不会真的立刻完全复制内存,太慢了。而是:
- fork 刚完成时,父子进程共享同一块物理内存(只读)。
- 当任一进程尝试写入某块内存时,OS 才真正复制那块内存给写的那一方。
所以 PHP Worker 进程刚 fork 出来时,和父进程共享内存;一旦 Worker 开始修改 对象的 变量,OS 就为它复制一块新的,从此两者完全隔离。
完整流程一览 比如:
php
// seckill.php
$stock = 100; // 这个变量只在当前 Worker 进程的堆里
Nginx 收到两个并发请求
│
├── 请求1 → PHP-FPM → Worker A (PID 100)
│ │
│ ├── $stock = 100(Worker A 自己的堆)
│ ├── $stock-- → 变成 99
│ └── 返回 "成功"
│
└── 请求2 → PHP-FPM → Worker B (PID 101)
│
├── $stock = 100(Worker B 自己的堆)← 还是 100!
├── $stock-- → 变成 99
└── 返回 "成功"
→ 超卖了!因为两个 Worker 各减各的,互不影响。
这就是为什么 PHP 做秒杀只能用 Redis 之类的"外部公务员"来管理库存。
总结
| 步骤 | 谁做的 | 做什么 |
|---|---|---|
| 启动 | PHP-FPM Master | fork 出一堆 Worker 进程 |
| 处理请求 | Nginx | FastCGI 协议发给某个空闲 Worker |
| 内存隔离 | Linux 内核 | fork() + COW 保证每个 Worker 内存独立 |
| 干掉进程 | PHP-FPM Master | Worker 处理过多请求后被干掉,fork 新的顶上 |
所以 PHP 程序员在写代码的时候,完全不需要考虑"另一个用户会不会改了我这个变量",因为每个用户的请求跑在不同的进程、不同的物理内存里。
这种"共享无"架构让 PHP 开发心智负担极低,但也正是这个特性,让纯 PHP 进程内缓存不可能存在,必须依赖外部服务来共享数据。