Read OSS

深入 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() 写入 stdout
  • read_post → 不返回任何内容(CLI 没有 POST 数据)
  • read_cookies → 返回 NULL(CLI 没有 Cookie)
  • register_server_variables → 用 argvargcSCRIPT_FILENAME 填充 $_SERVER
  • log_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.inihttpd.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 的"全量分配、一次性释放"模型运行得出奇高效的内存分配器。理解这些数据结构,是读懂引擎任何一部分代码的前提。