分析 PHP-FPM 的源码是一个非常有深度的话题。它能帮助我们彻底理解 PHP 在 Web 环境下的运行模式、进程管理和性能优化。

我会从 架构、核心流程、关键源码文件、重要机制 这几个方面为你剖析 PHP-FPM 的源码。

1. 核心架构:Master-Worker 模型

首先,你需要理解 PHP-FPM 的整体架构。它是一个典型的 多进程模型,由一个 Master 主进程 和多个 Worker 工作进程 组成。

  • Master Process (主进程):

    • 角色: 总指挥,管理者。

    • 职责:

      1. 读取并解析配置文件 (php-fpm.conf 和 pool.d/*.conf)。

      2. 监听网络端口(TCP 或 Unix Socket)。

      3. 创建、管理、监控 Worker 进程。

      4. 接收外部信号(如 reload, stop, quit),并对 Worker 进程进行相应管理。

      5. 自身不处理任何 PHP 请求

  • Worker Processes (工作进程):

    • 角色: 真正的劳动者。

    • 职责:

      1. 从 Master 进程继承已经打开的监听 socket。

      2. 通过 accept() 阻塞式地等待来自 Web Server (如 Nginx) 的 FastCGI 连接请求。

      3. 接收到请求后,解析 FastCGI 协议,加载并执行对应的 PHP 脚本。

      4. 将执行结果(HTML、JSON等)通过 FastCGI 协议返回给 Web Server。

      5. 循环等待下一个请求。

这个架构的好处是 稳定性和隔离性

  • 稳定性: 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)
  1. 入口 (fpm_main.c:main()):

    • 解析命令行参数(如 -c php.ini, -y php-fpm.conf)。

    • 调用 fpm_init() 开始初始化。

  2. 初始化 (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 并退出父进程,成为守护进程。

  3. 创建 Worker (fpm_children.c:fpm_children_create_initial()):

    • Master 进程根据每个 pool 的配置(pm = static/dynamic/ondemand),fork 出初始数量的 Worker 进程。

  4. 进入主循环 (fpm.c:fpm_run() -> fpm_event_loop()):

    • Master 进程调用 fpm_event_loop(),进入事件监听循环。它不再主动做事,而是被动地等待事件发生,例如:

      • 信号: 有信号需要处理。

      • 定时器: dynamic 模式下,需要定时检查 Worker 数量是否需要调整。

      • 子进程退出 (SIGCHLD): 有 Worker 进程挂了,需要处理后事并可能创建新的。

3.2 请求处理流程 (Worker Process)
  1. Worker 的诞生:

    • Worker 是由 Master fork() 出来的,所以它继承了 Master 的内存空间和已经打开的监听 socket。

    • fork() 之后,子进程(Worker)会进入 fpm_children.c:fpm_child_main() 函数。

  2. Worker 的主循环 (fpm_child_main()):

    • Worker 进程在一个 while 循环里工作,这个循环的核心是 fcgi_accept_request() (fastcgi.c)。

    • fcgi_accept_request() 本质上是一个 accept() 系统调用,它会阻塞在这里,直到 Web Server (Nginx) 发来一个新的 FastCGI 请求。

  3. 处理请求:

    • 一旦 accept() 成功,Worker 就与 Nginx 建立了一个连接。

    • 它会读取 FastCGI 协议头和环境变量($_SERVER 变量的来源)。

    • 调用 php_request_startup() 和 php_execute_script(),这正式进入了 PHP 的 Zend 引擎,开始执行 PHP 脚本。

    • 执行完毕后,将 stdout 的输出(即页面内容)封装成 FastCGI 响应,发送回 Nginx。

    • 关闭连接。

  4. 循环或退出:

    • 处理完一个请求后,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),该函数会:

    1. 通过共享内存(fpm_shm.c)检查当前 idle 和 busy 的 Worker 数量。

    2. 如果 idle 的 Worker 太多(超过 pm.max_spare_servers),就 kill 掉一些。

    3. 如果 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> 时:

  1. Master 进程的信号处理器 sig_handler 被触发,识别到是 SIGUSR2。

  2. 它设置一个全局标志位,表示需要进行重启。

  3. 在下一次事件循环中,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() 阻塞等待请求,简单高效。

  • 平滑重启是精髓:通过信号和父子进程替换,实现服务不中断。

  • 共享内存是高效通信的基石:用于状态同步。

如果你想自己阅读源码,建议按以下顺序:

  1. fpm_main.c: 从 main 函数开始,了解启动流程。

  2. fpm_conf.c: 看看配置文件是如何被解析成内存中的数据结构的。

  3. fpm_children.c: 理解 fork 和 Worker 进程的管理。

  4. fpm_process_ctl.c & fpm_events.c: 探索 Master 进程的事件循环,这是 FPM 的心脏。

  5. fpm_signals.c: 学习信号处理,特别是 SIGUSR2 的平滑重启逻辑。

  6. fpm_child_main() in fpm_children.c: 跟踪一个 Worker 进程从诞生到处理请求再到消亡的完整生命周期。

希望这个详尽的分析能帮助你深入理解 PHP-FPM 的内部工作原理。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐