分析 PHP-FPM 的源码
分析 PHP-FPM 的源码是一个非常有深度的话题。它能帮助我们彻底理解 PHP 在 Web 环境下的运行模式、进程管理和性能优化。
我会从 架构、核心流程、关键源码文件、重要机制 这几个方面为你剖析 PHP-FPM 的源码。
1. 核心架构:Master-Worker 模型
首先,你需要理解 PHP-FPM 的整体架构。它是一个典型的 多进程模型,由一个 Master 主进程 和多个 Worker 工作进程 组成。
-
Master Process (主进程):
-
角色: 总指挥,管理者。
-
职责:
-
读取并解析配置文件 (php-fpm.conf 和 pool.d/*.conf)。
-
监听网络端口(TCP 或 Unix Socket)。
-
创建、管理、监控 Worker 进程。
-
接收外部信号(如 reload, stop, quit),并对 Worker 进程进行相应管理。
-
自身不处理任何 PHP 请求。
-
-
-
Worker Processes (工作进程):
-
角色: 真正的劳动者。
-
职责:
-
从 Master 进程继承已经打开的监听 socket。
-
通过 accept() 阻塞式地等待来自 Web Server (如 Nginx) 的 FastCGI 连接请求。
-
接收到请求后,解析 FastCGI 协议,加载并执行对应的 PHP 脚本。
-
将执行结果(HTML、JSON等)通过 FastCGI 协议返回给 Web Server。
-
循环等待下一个请求。
-
-
这个架构的好处是 稳定性和隔离性:
-
稳定性: Master 进程只负责管理,非常稳定,不易崩溃。即使某个 Worker 进程因执行有问题的 PHP 代码而崩溃,Master 进程可以立刻创建一个新的 Worker 来替代它,保证了服务的可用性。
-
隔离性: 每个请求由一个独立的 Worker 进程处理,一个请求的崩溃不会影响到其他请求。
2. 源码目录与关键文件
PHP-FPM 的源码位于 PHP 源码树的 sapi/fpm/ 目录下。以下是核心文件的功能解读:
| 文件名 | 核心功能 |
| fpm_main.c | 程序入口。包含 main() 函数,是整个 FPM 生命周期的起点。负责解析命令行参数、初始化、进入主循环。 |
| fpm.c | FPM 的核心全局函数和数据结构。包括初始化 (fpm_init)、关闭 (fpm_cleanups) 等。 |
| fpm_conf.c | 配置文件解析。负责读取和解析 php-fpm.conf 以及各个进程池(pool)的配置文件。 |
| fpm_process_ctl.c | Master 进程的控制循环。Master 进程的大部分时间都在这个文件实现的循环里,等待信号和事件。 |
| fpm_children.c | Worker 进程管理。包含了 fork 创建子进程、维护子进程状态、重启子进程等所有与 Worker 相关的操作。 |
| fpm_events.c | 事件驱动模块。封装了 select, poll, epoll, kqueue 等 I/O 多路复用模型,Master 进程用它来高效地处理信号和定时器事件。 |
| fpm_sockets.c | Socket 相关操作。负责创建监听的 TCP Socket 或 Unix Domain Socket。 |
| fpm_shm.c | 共享内存管理。Worker 进程通过共享内存向 Master 进程报告自己的状态(如 Idle, Busy),这是实现 status 页面和动态进程管理的基础。 |
| fpm_status.c | 状态页(Status Page) 的实现。当你访问 FPM 的 status 页面时,就是由这里的代码从共享内存中读取数据并格式化输出。 |
| fastcgi.c | FastCGI 协议的实现。Worker 进程接收和发送数据时,需要遵循此协议。 |
| fpm_log.c | 日志记录功能。 |
3. 核心工作流程分析
3.1 启动流程 (Master Process)
-
入口 (fpm_main.c:main()):
-
解析命令行参数(如 -c php.ini, -y php-fpm.conf)。
-
调用 fpm_init() 开始初始化。
-
-
初始化 (fpm.c:fpm_init()):
-
加载配置: 调用 fpm_conf_init_main(),它会进一步调用 fpm_conf_read_config() (fpm_conf.c) 来解析配置文件,加载全局配置和所有 pool 的配置。
-
设置日志: 初始化日志系统。
-
创建 Socket: 为每个 pool 调用 fpm_sockets_init_main() (fpm_sockets.c) 创建监听 socket。
-
设置信号处理器: 设置 Master 进程对 SIGINT, SIGTERM, SIGUSR1, SIGUSR2 等信号的响应函数。
-
初始化事件模块: 调用 fpm_event_init() (fpm_events.c) 选择最高效的事件模型(如 epoll)。
-
Daemonize: 如果配置了 daemonize,程序会 fork 并退出父进程,成为守护进程。
-
-
创建 Worker (fpm_children.c:fpm_children_create_initial()):
-
Master 进程根据每个 pool 的配置(pm = static/dynamic/ondemand),fork 出初始数量的 Worker 进程。
-
-
进入主循环 (fpm.c:fpm_run() -> fpm_event_loop()):
-
Master 进程调用 fpm_event_loop(),进入事件监听循环。它不再主动做事,而是被动地等待事件发生,例如:
-
信号: 有信号需要处理。
-
定时器: dynamic 模式下,需要定时检查 Worker 数量是否需要调整。
-
子进程退出 (SIGCHLD): 有 Worker 进程挂了,需要处理后事并可能创建新的。
-
-
3.2 请求处理流程 (Worker Process)
-
Worker 的诞生:
-
Worker 是由 Master fork() 出来的,所以它继承了 Master 的内存空间和已经打开的监听 socket。
-
fork() 之后,子进程(Worker)会进入 fpm_children.c:fpm_child_main() 函数。
-
-
Worker 的主循环 (fpm_child_main()):
-
Worker 进程在一个 while 循环里工作,这个循环的核心是 fcgi_accept_request() (fastcgi.c)。
-
fcgi_accept_request() 本质上是一个 accept() 系统调用,它会阻塞在这里,直到 Web Server (Nginx) 发来一个新的 FastCGI 请求。
-
-
处理请求:
-
一旦 accept() 成功,Worker 就与 Nginx 建立了一个连接。
-
它会读取 FastCGI 协议头和环境变量($_SERVER 变量的来源)。
-
调用 php_request_startup() 和 php_execute_script(),这正式进入了 PHP 的 Zend 引擎,开始执行 PHP 脚本。
-
执行完毕后,将 stdout 的输出(即页面内容)封装成 FastCGI 响应,发送回 Nginx。
-
关闭连接。
-
-
循环或退出:
-
处理完一个请求后,Worker 会检查已处理的请求数。
-
如果达到了 pm.max_requests(如果配置了),Worker 进程会主动退出。Master 进程会收到 SIGCHLD 信号,然后创建一个新的 Worker 进程来替代它。这样做是为了防止因长时间运行可能导致的内存泄漏。
-
如果没有达到,Worker 会回到 while 循环的开头,再次调用 fcgi_accept_request() 等待下一个请求。
-
4. 关键机制深度解析
4.1 进程管理模型 (PM: static, dynamic, ondemand)
这是 FPM 的核心功能之一,主要实现在 fpm_children.c 和 fpm_process_ctl.c 中。
-
static: 在 fpm_children_create_initial() 中一次性 fork 出 pm.max_children 个子进程,之后数量不再变化。实现最简单。
-
dynamic: Master 进程会启动一个定时器事件。在事件循环中,定时器会触发一个回调函数(fpm_pctl_perform_idle_server_maintenance),该函数会:
-
通过共享内存(fpm_shm.c)检查当前 idle 和 busy 的 Worker 数量。
-
如果 idle 的 Worker 太多(超过 pm.max_spare_servers),就 kill 掉一些。
-
如果 idle 的 Worker 太少(低于 pm.min_spare_servers),就 fork 一些新的。
-
-
ondemand: 启动时不创建任何 Worker。当 Master 的监听 socket 上有连接请求时(通过 fpm_event 模块感知),Master 才会 fork 一个 Worker 去处理。处理完后,如果空闲超时(pm.process_idle_timeout),Worker 会被 kill 掉。
4.2 信号处理与平滑重启 (Graceful Reload)
这是 FPM 最为人称道的功能,主要在 fpm_signals.c 和 fpm_process_ctl.c 中实现。
当你执行 kill -USR2 <master_pid> 时:
-
Master 进程的信号处理器 sig_handler 被触发,识别到是 SIGUSR2。
-
它设置一个全局标志位,表示需要进行重启。
-
在下一次事件循环中,Master 检测到这个标志位,开始执行平滑重启逻辑:
-
重新加载配置文件。
-
启动新一代 Worker 进程:像 FPM 第一次启动一样,fork 出一批新的 Worker 进程。这些新 Worker 使用新的配置。
-
向老一代 Worker 发送 SIGQUIT 信号:这个信号告诉老的 Worker 们:“处理完你手头上的请求就优雅地退出吧,别再接新活了”。
-
老 Worker 收到 SIGQUIT 后,完成当前请求,然后退出循环,进程终止。
-
Master 进程通过 waitpid() 回收所有退出的老 Worker 进程。
-
整个过程,服务不中断,新的请求会被新的 Worker 处理,老的请求会由老的 Worker 处理完,实现了零停机配置更新。
4.3 共享内存 (SHM) 的作用
共享内存是 Master 和 Worker 之间进行低开销、高效率通信的关键。
-
创建: Master 进程在启动时,会调用 fpm_shm_create() 创建一块共享内存。
-
写入: 每个 Worker 进程在自己的循环中,会更新它在共享内存中对应的状态槽(slot),标记自己是 IDLE 还是 BUSY。
-
读取:
-
Master 进程在 dynamic 模式下,需要读取共享内存来决定是否增减 Worker。
-
当外部请求 FPM 的 status 页面时,一个临时的 Worker 进程会被 Master 指派去读取整个共享内存区域,汇总所有 Worker 的状态,然后格式化输出。
-
总结与阅读建议
-
核心是 Master-Worker 架构:Master 只管理,Worker 只干活。
-
Master 是事件驱动的:使用 epoll/kqueue 高效处理信号和定时任务,CPU 占用极低。
-
Worker 是阻塞循环的:accept() 阻塞等待请求,简单高效。
-
平滑重启是精髓:通过信号和父子进程替换,实现服务不中断。
-
共享内存是高效通信的基石:用于状态同步。
如果你想自己阅读源码,建议按以下顺序:
-
fpm_main.c: 从 main 函数开始,了解启动流程。
-
fpm_conf.c: 看看配置文件是如何被解析成内存中的数据结构的。
-
fpm_children.c: 理解 fork 和 Worker 进程的管理。
-
fpm_process_ctl.c & fpm_events.c: 探索 Master 进程的事件循环,这是 FPM 的心脏。
-
fpm_signals.c: 学习信号处理,特别是 SIGUSR2 的平滑重启逻辑。
-
fpm_child_main() in fpm_children.c: 跟踪一个 Worker 进程从诞生到处理请求再到消亡的完整生命周期。
希望这个详尽的分析能帮助你深入理解 PHP-FPM 的内部工作原理。
更多推荐


所有评论(0)