深入 php-src:架构、层次与请求生命周期
前置知识
- ›C 语言基础(结构体、指针、函数指针)
- ›对解释型语言工作原理有基本了解
- ›理解进程生命周期的相关概念
深入 php-src:架构、层次与请求生命周期
PHP 驱动着全球约 77% 的已知服务端语言网站。然而,大多数 PHP 开发者从未真正了解过代码背后的引擎。php-src 仓库超过两百万行 C 代码,光是这个数字就足以让经验丰富的系统程序员望而却步。本文将为你提供一张导航地图,帮助你在这片代码海洋中找到方向。我们将把整个代码库拆解为四个架构层次,分析让 PHP 在 Apache、Nginx 或 CLI 终端中保持一致行为的契约机制,并完整追踪一个 PHP 请求从第一次 main() 调用到最终关闭的全过程。
读完本系列之后,你打开 php-src 中的任何文件,都能清楚地知道它在整体架构中处于什么位置。
顶层目录结构
在深入架构之前,先来熟悉一下仓库的顶层目录布局。每个目录都有明确的职责分工:
| 目录 | 作用 |
|---|---|
Zend/ |
Zend 引擎——词法分析器、解析器、编译器、VM、内存分配器、GC、类型系统 |
main/ |
PHP 运行时胶水层——生命周期管理、INI 系统、流(Streams)、SAPI 桥接 |
sapi/ |
服务器 API 入口——CLI、FPM、CGI、Apache 模块、Embed、phpdbg |
ext/ |
72+ 个内置扩展——standard、json、opcache、pdo、curl 等 |
TSRM/ |
线程安全资源管理器——线程本地存储抽象层 |
build/ |
构建系统脚本(autoconf、libtool 辅助工具) |
win32/ |
Windows 专用构建配置与兼容层 |
tests/ |
引擎与扩展的 .phpt 测试文件 |
Zend/Optimizer/ |
基于 SSA 的优化器(位于 Zend/ 目录下,但属于独立子系统) |
graph TD
subgraph "php-src repository"
SAPI["sapi/<br/>CLI, FPM, CGI, Apache, Embed"]
MAIN["main/<br/>Runtime, INI, Streams, SAPI bridge"]
EXT["ext/<br/>72+ bundled extensions"]
ZEND["Zend/<br/>Engine: lexer, parser, compiler, VM, GC"]
OPT["Zend/Optimizer/<br/>SSA optimizer"]
TSRM_DIR["TSRM/<br/>Thread safety"]
BUILD["build/, win32/<br/>Build system"]
TESTS["tests/<br/>.phpt test suite"]
end
SAPI --> MAIN
MAIN --> ZEND
EXT --> ZEND
OPT --> ZEND
ZEND --> TSRM_DIR
提示: 查找某个功能时,可以按以下思路快速定位:PHP 层面的函数看
ext/,语言语义看Zend/,运行时行为看main/,宿主环境集成看sapi/。
四层架构模型
php-src 由四个堆叠的层次构成。每一层只依赖其下方的层,各层职责清晰独立:
flowchart TB
subgraph L4["Layer 4: SAPIs"]
CLI["CLI"]
FPM["FPM"]
CGI["CGI"]
APACHE["Apache"]
EMBED["Embed"]
end
subgraph L3["Layer 3: PHP Runtime (main/)"]
LIFECYCLE["Lifecycle Orchestration"]
INI["INI System"]
STREAMS["Streams I/O"]
SAPI_BRIDGE["SAPI Bridge"]
end
subgraph L2["Layer 2: Zend Engine"]
COMPILER["Compiler"]
VM["Virtual Machine"]
MM["Memory Manager"]
GC["Garbage Collector"]
TYPES["Type System"]
end
subgraph L1["Layer 1: TSRM"]
TLS["Thread-Local Storage"]
end
L4 --> L3
L3 --> L2
L2 --> L1
第一层——TSRM 位于最底部,负责对全局状态提供线程安全的访问。在常见的非 ZTS(非线程安全)构建版本中,TSRM 会被编译掉——PG()、EG()、CG()、SG() 等全局访问宏会直接解析为结构体字段访问。在 ZTS 构建版本中(用于事件驱动型 SAPI 或 Windows IIS),这些宏则通过线程本地存储查找来访问。
第二层——Zend 引擎 是语言核心,包含词法分析器、解析器、AST、编译器、虚拟机、内存分配器、垃圾回收器,以及基础类型系统(zval、HashTable、zend_string、zend_object)。引擎本身不了解 HTTP、文件 I/O 或配置文件——它只负责编译和执行 PHP opcodes。
第三层——PHP 运行时(main/) 承担着引擎与外部世界之间的桥接工作。它负责编排启动与关闭的生命周期、解析 INI 文件、管理流(Streams)I/O 抽象,并通过 SAPI 桥接将引擎与宿主环境解耦。
第四层——SAPI 是各个入口点。每个 SAPI 都实现了一套契约(一张函数指针表),告诉运行时如何在特定宿主环境中读取输入、写入输出、发送头信息以及记录错误日志。
全局状态访问宏值得特别关注。每一层都有自己的全局状态结构体,通过专用宏来访问:
| 宏 | 结构体 | 所属层 | 内容 |
|---|---|---|---|
PG() |
php_core_globals |
运行时 | INI 设置、错误处理、文件上传状态 |
EG() |
zend_executor_globals |
引擎 | 当前 execute_data、符号表、异常状态 |
CG() |
zend_compiler_globals |
引擎 | 当前 op_array、AST、编译状态 |
SG() |
sapi_globals_struct |
运行时 | 请求信息、请求头、当前 SAPI 模块 |
这些宏在 php-src 中随处可见,能够快速识别它们是读懂代码的关键。
SAPI 契约
SAPI(Server API)契约是 PHP 最精妙的设计之一。它是一个名为 sapi_module_struct 的结构体,包含约 30 个函数指针,将 PHP 与宿主环境之间的所有交互抽象封装起来。
其定义位于 main/SAPI.h,核心回调如下:
classDiagram
class sapi_module_struct {
+char *name
+char *pretty_name
+startup(sapi_module_struct*) int
+shutdown(sapi_module_struct*) int
+activate() int
+deactivate() int
+ub_write(char*, size_t) size_t
+flush(void*) void
+header_handler(sapi_header_struct*, ...) int
+send_headers(sapi_headers_struct*) int
+send_header(sapi_header_struct*, void*) void
+read_post(char*, size_t) size_t
+read_cookies() char*
+register_server_variables(zval*) void
+log_message(char*, int) void
+get_fd(int*) int
+ini_defaults(HashTable*) void
}
CLI SAPI 是一个具体的实现示例。在 sapi/cli/php_cli.c 中,模块定义将各回调与 CLI 专属实现绑定在一起:
ub_write→ 通过fwrite()写入stdoutread_post→ 不返回任何内容(CLI 没有 POST 数据)read_cookies→ 返回NULL(CLI 没有 Cookie)register_server_variables→ 用argv、argc和SCRIPT_FILENAME填充$_SERVERlog_message→ 写入stderr
正是这套契约机制,使得 Zend 引擎从不直接调用 write() 或 fwrite(),而是始终通过 sapi_module.ub_write() 来完成输出——无论 PHP 是作为 Apache 模块、FastCGI 工作进程还是嵌入式脚本引擎运行,都能得到正确的处理结果。
各 SAPI 入口点对比
每个 SAPI 都有自己的 main() 函数,但最终都会汇聚到相同的生命周期调用上。以下是主要 SAPI 之间的差异:
| SAPI | 入口文件 | 进程模型 | 请求循环方式 |
|---|---|---|---|
| CLI | sapi/cli/php_cli.c |
单进程,单请求 | 执行脚本后退出 |
| FPM | sapi/fpm/fpm/fpm_main.c |
主进程 + 工作进程池 | 每个工作进程运行 accept() 循环 |
| CGI | sapi/cgi/cgi_main.c |
由 Web 服务器按请求生成 | 处理单个请求后退出 |
| Apache | sapi/apache2handler/sapi_apache2.c |
以 .so 模块形式加载 |
由 Apache 请求处理器调用 |
| Embed | sapi/embed/php_embed.c |
嵌入宿主应用程序 | 宿主程序控制生命周期 |
CLI SAPI 是其中最简单的:其 main() 解析命令行参数,调用 php_module_startup(),执行单个请求,然后关闭。FPM 则最为复杂:它 fork 出多个工作进程,管理具有可配置大小的进程池,每个工作进程循环执行 accept() → php_request_startup() → 执行 → php_request_shutdown()。
尽管存在这些差异,所有 SAPI 最终都会调用 main/main.c 中相同的四个生命周期函数——这就是它们的共同汇聚点。
请求生命周期
生命周期是 PHP 执行模型的骨架。无论是 CLI、FPM 还是 Apache,每个 PHP 进程都遵循相同的四阶段模式:
sequenceDiagram
participant SAPI as SAPI main()
participant Runtime as main/main.c
participant Zend as Zend Engine
participant Ext as Extensions
Note over SAPI,Ext: Phase 1: Module Startup (once per process)
SAPI->>Runtime: php_module_startup()
Runtime->>Zend: zend_startup()
Zend->>Zend: Init memory manager, scanner, compiler, VM
Runtime->>Runtime: Parse php.ini
Runtime->>Ext: Call each extension's MINIT()
Note over SAPI,Ext: Phase 2: Request Startup (once per request)
SAPI->>Runtime: php_request_startup()
Runtime->>Zend: zend_activate()
Zend->>Zend: Reset memory arena, init symbol tables
Runtime->>Ext: Call each extension's RINIT()
Note over SAPI,Ext: Phase 3: Execution
SAPI->>Zend: zend_execute_scripts()
Zend->>Zend: Compile source → opcodes
Zend->>Zend: Execute opcodes in VM
Note over SAPI,Ext: Phase 4: Request Shutdown
SAPI->>Runtime: php_request_shutdown()
Runtime->>Ext: Call each extension's RSHUTDOWN()
Runtime->>Zend: zend_deactivate()
Zend->>Zend: Free request memory, destroy symbol tables
Note over SAPI,Ext: Phase 5: Module Shutdown (once per process)
SAPI->>Runtime: php_module_shutdown()
Runtime->>Ext: Call each extension's MSHUTDOWN()
Runtime->>Zend: zend_shutdown()
阶段一:模块启动(Module Startup) 在进程启动时执行一次(或 Apache 模块加载时执行一次)。核心函数是 main/main.c 中的 php_module_startup()。它调用 zend_startup() 来初始化引擎——内存管理器、词法扫描器、编译器、执行器及内置函数。随后解析 php.ini,注册核心 INI 设置,并遍历扩展列表,依次调用每个扩展的 MINIT(模块初始化)钩子。扩展就是在这一阶段注册各自的类、常量和内部函数的。
阶段二:请求启动(Request Startup) 在每个请求处理前执行。main/main.c 中的 php_request_startup() 调用 zend_activate() 重置请求级内存 arena、重新初始化符号表并清空执行器状态。接着调用每个扩展的 RINIT(请求初始化)钩子——例如 session 扩展在此打开 session 存储,opcache 在此预热优化器。
阶段三:执行(Execution) 是你的 PHP 代码真正运行的阶段。SAPI 调用 zend_execute_scripts(),将源文件编译为 op_array(或从 OPcache 中取出缓存版本),然后交由 VM 执行。
阶段四:请求关闭(Request Shutdown) 与启动阶段相对应。php_request_shutdown() 依次调用每个扩展的 RSHUTDOWN 钩子,再由 zend_deactivate() 销毁所有请求级数据。在 FPM 和 Apache 中,进程随后会回到阶段二,处理下一个请求。
阶段五:模块关闭(Module Shutdown) 在进程退出时执行。各扩展收到 MSHUTDOWN 调用,zend_shutdown() 最终将引擎彻底关闭。
提示: 模块启动(一次性)与请求启动(每次请求)之间的清晰分离,正是 PHP "无共享"架构高效运转的根本原因。每个请求都从干净的初始状态开始,不会受到前一个请求的任何状态污染。这也是为什么 PHP 通常无需重启就能加载代码变更(除非 OPcache 已缓存旧版本)。
配置:INI 系统
PHP 的配置系统与生命周期紧密相连。INI 文件在模块启动阶段完成解析,而变更模式(change-mode)系统则控制着哪些设置可以在哪个生命周期阶段被修改。
flowchart TD
A["Process Start"] --> B["Scan for php.ini"]
B --> C["Parse php.ini directives"]
C --> D["Apply PHP_INI_SYSTEM settings"]
D --> E["Extensions register INI entries in MINIT"]
E --> F["Per-request: scan .user.ini"]
F --> G["Apply PHP_INI_PERDIR settings"]
G --> H["Runtime: ini_set() calls"]
H --> I["Apply PHP_INI_USER / PHP_INI_ALL settings"]
每条 INI 指令都有一个变更模式,决定了它可以在何处被修改:
| 模式 | 常量值 | 可设置的位置 |
|---|---|---|
PHP_INI_SYSTEM |
4 | 仅限 php.ini——需要重启进程才能生效 |
PHP_INI_PERDIR |
6 | php.ini、.user.ini 或 httpd.conf |
PHP_INI_USER |
7 | 以上所有位置 + 运行时 ini_set() |
PHP_INI_ALL |
7 | 与 USER 相同——可在任何位置设置 |
INI 条目定义于 Zend/zend_ini.h,各扩展在 MINIT 阶段通过 STD_PHP_INI_ENTRY 等宏进行注册。实际的解析发生在 php_module_startup() 内部,由 php_init_config() 负责定位并解析 INI 文件。
.user.ini 功能(由 user_ini.filename 控制)允许在非 CLI SAPI 中按目录覆盖配置。这些文件在请求启动阶段进行扫描,并配有可配置的缓存 TTL(user_ini.cache_ttl),因此不会带来每次请求都要访问文件系统的额外开销。
下一步
现在我们已经有了这张导航地图:四层架构、SAPI 契约,以及贯穿每个 PHP 请求的生命周期。下一篇文章将聚焦于 Zend 引擎的核心数据结构——代表每个 PHP 值的 16 字节 zval、驱动 PHP 数组的双模式 HashTable,以及让 PHP 的"全量分配、一次性释放"模型运行得出奇高效的内存分配器。理解这些数据结构,是读懂引擎任何一部分代码的前提。