首页
/ PHP Zend Engine 内核解读:Zend 内存管理器与 ZEND_VM 操作码执行架构(基于 Zend/README.md)

PHP Zend Engine 内核解读:Zend 内存管理器与 ZEND_VM 操作码执行架构(基于 Zend/README.md)

2026-09-05 12:24:29作者:晏闻田Solitary

本文基于 php-src 仓库中的 Zend/README.md 展开,完整继承其中两大核心主题:Zend 内存管理器(memory manager)的设计目标与调试手段,以及 ZEND_VM 操作码执行器的专用化(specialization)、线程化模型(threading)与生成机制,并结合 Zend/zend_alloc.cZend/zend_vm_def.hZend/zend_vm_gen.php 等源码逐层印证。读完本文,你将掌握如何在源码层面理解 Zend 的内存分配策略、如何用 USE_ZEND_ALLOCvalgrind 定位内存泄漏,以及如何读懂执行器生成脚本 zend_vm_gen.php 的输入输出与各命令行参数,从而有能力修改操作码 handler 并重新生成执行器。

一、文档定位:Zend/README.md 讲了什么

Zend/README.md 是 Zend 引擎目录下的说明文档,聚焦两块 PHP 执行内核最底层的能力:

  1. Zend memory manager(Zend 内存管理器):自 PHP 5.2 引入,目标是降低内存分配开销、加速内存管理;文档给出了关闭 Zend MM 后用 valgrind 排查泄漏的标准姿势,以及针对共享扩展泄漏的 ZEND_DONT_UNLOAD_MODULES 环境变量;
  2. ZEND_VM(操作码虚拟机):允许按操作数的 op_type 对操作码 handler 做专用化,支持多种执行线程化模型;文档给出了 handler/helper 的编写模板、新旧执行器宏的完整对照表,以及生成器脚本 zend_vm_gen.php 的用法。

这两个主题一个管"内存怎么分",一个管"字节码怎么跑",合起来构成了理解 PHP 引擎性能与底层调试的入口。

二、Zend 内存管理器:设计目标与实现结构

2.1 设计目标

Zend/README.md 的表述,新内存管理器(自 PHP 5.2 起可用)的目标是减少内存分配开销并加速内存管理。从 Zend/zend_alloc.c 开头的文件头注释看,这套实现(内部代号 zmm/zend_alloc)自述为"现代、CPU 缓存友好的内存管理器",并且多数设计思想借鉴自 jemalloc 与 tcmalloc

2.2 三类分配:Huge / Large / Small

Zend/zend_alloc.c 头部的注释明确给出分配大小的三段式划分:

类别 条件 分配方式
Huge 大于 CHUNK 大小(默认约 2 MB) 直接 mmap() 分配,按 2 MB 边界对齐
Large 一个 CHUNK 内的若干 4096K 页(页对齐块) 块始终按页边界对齐
Small 小于 3/4 页大小 向上取整到预定义小尺寸档(30 档:8, 16, 24, 32 … 3072 字节),从 RUN 中分配;每个 RUN 是连续的一页或几页,块用空闲元素链表管理,结果按 8 字节对齐

其中 CHUNK 的数值在 Zend/zend_alloc_sizes.h 中定义,可以直接核对:

#define ZEND_MM_CHUNK_SIZE ((size_t) (2 * 1024 * 1024))    /* 2 MB */
#define ZEND_MM_PAGES      (ZEND_MM_CHUNK_SIZE / ZEND_MM_PAGE_SIZE)  /* 512 */
#define ZEND_MM_FIRST_PAGE (1)

也就是说一个 CHUNK 固定 2 MB,默认包含 512 页,第 0 页被保留为管理页——注释说明该页存放"空闲页位图、各小尺寸 RUN 可用位图、记录每页使用情况的页映射表(page map)"等元数据。

2.3 CHUNK 对齐带来的好处

头注释还解释了一个关键设计:zend_alloc 向操作系统按 CHUNK 取内存,CHUNK 与 huge 块都按 CHUNK 边界对齐,因此给定任意指针都能极快反查其所属 CHUNK(对齐后取基址即可)。这一技巧在源码中随处可见,例如 Zend/zend_alloc.c 中的 ZEND_MM_ALIGNED_BASE(ptr, ZEND_MM_CHUNK_SIZE) 系列宏,以及基于 ZEND_MM_ALIGNED_OFFSET 计算页内偏移的逻辑。

2.4 对外 API 与编译期优化

zend_alloc 对外提供常见的 emalloc / efree / erealloc API,此外还提供针对预定义尺寸的专用分配例程(如 emalloc_2()emalloc_4()emalloc_large() 等)。库利用 C 预处理技巧:当请求大小在编译期已知时,把对 emalloc() 的调用替换为更高效的专用例程。

2.5 调试:关闭 Zend MM 后用 valgrind 定位泄漏

Zend/README.md 给出的标准调试流程是两条命令。

正常方式(使用 Zend MM,valgrind 无法跟踪其内部分配):

sapi/cli/php -r 'leak();'

关闭 Zend MM 后运行(切回系统 malloc,valgrind 可正确跟踪):

USE_ZEND_ALLOC=0 valgrind --leak-check=full sapi/cli/php -r 'leak();'

从源码看,该开关的读取位于 Zend/zend_alloc.calloc_globals_ctor():启动时读取环境变量 USE_ZEND_ALLOC,若非零则继续走 Zend 自研堆;设为 0 时则把堆标记为 ZEND_MM_CUSTOM_HEAP_STD,并把底层分配函数换成系统 malloc/free/realloc。同一处还处理了 USE_TRACKED_ALLOC 组合开关:USE_ZEND_ALLOC=0USE_TRACKED_ALLOC=1 时会改用带跟踪记录的 tracked_malloc/tracked_free/tracked_realloc,在进程内记录分配以便自动释放核对。相关测试用例如 Zend/tests/bug40770.phptZend/tests/memory_get_peak_usage.phpt 也都是先读 USE_ZEND_ALLOC 再决定是否跳过,说明"Zend MM 开/关两种模式都要跑通"是该测试套件的基本约定。

USE_ZEND_ALLOC 外,同一初始化函数中还处理了两个调试/调优环境变量:

  • USE_ZEND_ALLOC_HUGE_PAGES:置真时设置 zend_mm_use_huge_pages,让堆使用大页(huge pages)分配;
  • ZEND_MM_DEBUG:启用 poison_malloc/poison_free 等"毒化"调试处理函数,用于检测堆损坏(该能力编译期由 ZEND_MM_CUSTOM 等开关控制)。

2.6 共享扩展的泄漏排查:ZEND_DONT_UNLOAD_MODULES

Zend/README.md 指出,自 PHP 5.3.11 起,可以防止共享扩展在退出时被卸载,从而使 valgrind 能正确跟踪共享扩展中的内存泄漏——机制是环境变量 ZEND_DONT_UNLOAD_MODULES:设置后,共享扩展关闭阶段的 DL_UNLOAD() 会被跳过。

仓库源码中该变量在两处被检查,可印证文档描述:

  • Zend/zend_extensions.c(约 L231):if (extension->handle && !getenv("ZEND_DONT_UNLOAD_MODULES")) —— 模块清理路径中决定是否调用 dlclose
  • Zend/zend_API.c(约 L3420):if (!getenv("ZEND_DONT_UNLOAD_MODULES")) —— API 清理路径中同一判断。

因此针对"共享扩展里的泄漏"这类 valgrind 报不出来的场景,推荐的完整命令形态是:

USE_ZEND_ALLOC=0 ZEND_DONT_UNLOAD_MODULES=1 valgrind --leak-check=full sapi/cli/php -r 'require "ext.so"; leak();'

(其中 ext.so 为待排查的共享扩展,命令形式基于文档中两条命令的组合,扩展名按实际构建产物替换。)

三、ZEND_VM:操作码 handler 专用化与执行模型

3.1 核心思想

Zend/README.md 对 ZEND_VM 的概括是:按操作数的 op_type 字段专用化(specialize)操作码 handler,并支持多种执行方法——调用线程化(call threading)、switch 线程化、直接线程化(direct/ goto threading)。文档原文称:由此 ZE2 在纯 PHP 代码执行上获得了 20% 以上的加速(配合专用化执行器与直接线程化执行方式);同时文档也提醒,多数 PHP 应用的瓶颈在系统调用与数据库调用而非原始执行速度,所以实际收益"因应用而异"。

旧版 zend_execute.c 的大部分代码迁移到了 Zend/zend_vm_def.h,操作码 handler 与 helper 都在这里定义。

3.2 操作码 handler 模板与实际示例

文档给出的标准模板:

ZEND_VM_HANDLER(<OPCODE-NUMBER>, <OPCODE>, <OP1_TYPES>, <OP2_TYPES>)
{
    <HANDLER'S CODE>
}

参数含义(按文档原文继承):

  • <OPCODE-NUMBER>:操作码编号(0, 1, …);
  • <OPCODE>:操作码名称(如 ZEND_NOPZEND_ADD);
  • <OP1_TYPES> / <OP2_TYPES>:允许的操作数 op_type 掩码,可组合 UNUSEDCONSTVARTMPCV,也可用 ANY 掩码禁用对该操作数的专用化;专用化器只为掩码中定义的类型组合生成代码
  • <HANDLER'S CODE>:handler 本体,多数与旧版 zend_execute.c 相同,但改用宏访问操作数与执行器内部数据。

Zend/zend_vm_def.h 中的真实 handler 与模板完全一致,例如:

ZEND_VM_HANDLER(8, ZEND_CONCAT, CONST|TMP|CV, CONST|TMP|CV, SPEC(NO_CONST_CONST))
ZEND_VM_HANDLER(22, ZEND_ASSIGN, VAR|CV, CONST|TMP|CV, SPEC(RETVAL))
ZEND_VM_HANDLER(80, ZEND_FETCH_R, CONST|TMP|CV, UNUSED, VAR_FETCH)

可以看到掩码写法(CONST|TMP|CVVAR|UNUSED|THIS|CV 等)与文档描述吻合,且当前代码还带有第三类扩展位(VAR_FETCHCACHE_SLOTSPEC(...) 等),用于向生成脚本声明操作数的扩展语义——这些扩展标志的编码表就定义在 Zend/zend_vm_gen.php 顶部的 $vm_ext_decode / $vm_op_flags 中。

3.3 新旧执行器宏对照表(文档原文完整继承)

Zend/README.md 给出了新宏与旧代码的一一对照,是迁移/阅读执行器代码的关键速查表:

新宏 对应的旧代码
EXECUTE_DATA execute_data
ZEND_VM_DISPATCH_TO_HANDLER(<OP>) return <OP>_helper(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)
ZEND_VM_DISPATCH_TO_HELPER(<NAME>) return <NAME>(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)
ZEND_VM_DISPATCH_TO_HELPER_EX(<NAME>,<PARAM>,<VAL>) return <NAME>(<VAL>, ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)
ZEND_VM_CONTINUE() return 0
ZEND_VM_NEXT_OPCODE() NEXT_OPCODE()
ZEND_VM_SET_OPCODE(<TARGET>) SET_OPCODE(<TARGET>)
ZEND_VM_INC_OPCODE() INC_OPCOD()
ZEND_VM_RETURN_FROM_EXECUTE_LOOP() RETURN_FROM_EXECUTE_LOOP()
ZEND_VM_C_LABEL(<LABEL>): <LABEL>:
ZEND_VM_C_GOTO(<LABEL>) goto <LABEL>
OP<X>_TYPE opline->op<X>.op_type
GET_OP<X>_ZVAL_PTR(<TYPE>) get_zval_ptr(&opline->op<X>, EX(Ts), &free_op<X>, <TYPE>)
GET_OP<X>_ZVAL_PTR_PTR(<TYPE>) get_zval_ptr_ptr(&opline->op<X>, EX(Ts), &free_op<X>, <TYPE>)
GET_OP<X>_OBJ_ZVAL_PTR(<TYPE>) get_obj_zval_ptr(&opline->op<X>, EX(Ts), &free_op<X>, <TYPE>)
GET_OP<X>_OBJ_ZVAL_PTR_PTR(<TYPE>) get_obj_zval_ptr_ptr(&opline->op<X>, EX(Ts), &free_op<X>, <TYPE>)
IS_OP<X>_TMP_FREE() IS_TMP_FREE(free_op<X>)
FREE_OP<X>() FREE_OP(free_op<X>)
FREE_OP<X>_IF_VAR() FREE_VAR(free_op<X>)
FREE_OP<X>_VAR_PTR() FREE_VAR_PTR(free_op<X>)

3.4 Executor helpers

文档说明执行器 helper 可以无参或带一个参数,分别用如下构造定义:

ZEND_VM_HELPER(<HELPER-NAME>, <OP1_TYPES>, <OP2_TYPES>)
{
    <HELPER'S CODE>
}

ZEND_VM_HELPER_EX(<HELPER-NAME>, <OP1_TYPES>, <OP2_TYPES>, <PARAM_SPEC>)
{
    <HELPER'S CODE>
}

handler 通过 ZEND_VM_DISPATCH_TO_HELPER(<NAME>) 把"慢路径"(如需要错误处理、类型不匹配时的兜底逻辑)分派到 helper,而专用化生成的快路径里只保留最常见的类型组合。

四、执行器生成器:zend_vm_gen.php

4.1 输入、输出与包含关系

Zend/README.md 明确了生成流程:

Zend/zend_vm_gen.php 文件头的注释同样写明:"This script creates zend_vm_execute.h and zend_vm_opcodes.{h,c} from existing zend_vm_def.h and zend_vm_execute.skl",与文档一致。

4.2 命令行参数

文档列出的三个开关在 Zend/zend_vm_gen.php 的参数解析代码中均可对应(约 L3268-L3299):

  • --with-vm-kind=CALL|SWITCH|GOTO|HYBRID:选择操作码线程化模型,脚本帮助文本标注默认值为 HYBRID
  • --without-specializer:禁用操作码专用化(生成的 handler 不区分 op_type 组合);
  • --with-lines:启用 #line 指令,便于调试时直接定位到原始的 zend_vm_def.h 行号(文档说明用原始定义文件调试执行器需要该选项)。

文档给出的默认生成命令:

# Default VM kind is HYBRID
php zend_vm_gen.php --with-vm-kind=HYBRID

Zend/zend_vm_gen.php 源码结构看,脚本内还定义了第五种常量 ZEND_VM_KIND_TAILCALL = 5(与 CALL=1、SWITCH=2、GOTO=3、HYBRID=4 并列于 $vm_kind_name 表),即当前仓库的执行器生成器在文档所列四种模型之外还实现了 tail-call 模型,但文档正文与帮助文本仍以 CALL|SWITCH|GOTO|HYBRID 作为面向使用者的选项集;以实际参数解析为准选用。

4.3 各线程化模型的差别(结合源码结构)

Zend/zend_vm_gen.php 的骨架生成逻辑可以推断四种模型在代码形态上的差异:

  • CALL threading:每条 opline 对应一次函数调用 case <opcode>: 后调用 handler,依赖编译器内联;
  • SWITCH threading:一个大 switch(opcode) 循环体;
  • GOTO(direct)threading:用 goto <opcode>_case: 标签直接跳转,避免分支预测开销;
  • HYBRID:混合策略,对热点/长路径用直接跳转,其余退回 switch 结构,是仓库默认(--with-vm-kind=HYBRID),兼顾代码体积与跳转距离(x86 jmp 有 ±2GB 位移限制,巨型 switch 可能超出编码范围,混合模型可缓解这一问题——此点属于从生成器结构出发的合理推断)。

无论选哪种模型,专用化(specializer)都会为每个 handler 的每个允许 op_type 组合展开生成独立函数体,这也是文档强调"只为定义的类型组合生成代码"的原因:类型组合越少,展开后的代码越紧凑、分支预测越友好。

五、修改执行器后的完整工作流

综合 Zend/README.md 与源码,一次典型的"改操作码 handler"工作流是:

  1. Zend/zend_vm_def.h 中修改或新增 ZEND_VM_HANDLER(...) / ZEND_VM_HELPER(...) 定义(可参考 ZEND_VM_HANDLER(22, ZEND_ASSIGN, VAR|CV, CONST|TMP|CV, SPEC(RETVAL)) 等现有写法);
  2. 执行生成脚本(默认):
php Zend/zend_vm_gen.php --with-vm-kind=HYBRID
  1. 得到新的 Zend/zend_vm_execute.hZend/zend_vm_opcodes.h,重新编译即可;
  2. 调试生成产物时用 --with-lines 重新生成,使 zend_vm_execute.h 携带 #line 指令,断点可直接映射回 zend_vm_def.h 的源码行。

六、延伸阅读入口

主题 文件
本文对应的原始文档 Zend/README.md
内存管理器实现(zmm) Zend/zend_alloc.cZend/zend_alloc.hZend/zend_alloc_sizes.h
共享扩展卸载开关 Zend/zend_extensions.cZend/zend_API.c
操作码 handler 定义 Zend/zend_vm_def.h
执行循环骨架与生成器 Zend/zend_vm_execute.sklZend/zend_vm_gen.php
执行器入口 Zend/zend_execute.c
依赖 Zend MM 行为的测试示例 Zend/tests/gh11189.phptZend/tests/new_oom.phpt

需要强调的适用前提:本文所有结论以当前仓库快照为准;USE_ZEND_ALLOCUSE_ZEND_ALLOC_HUGE_PAGESZEND_DONT_UNLOAD_MODULES 等均为运行时环境变量,--with-vm-kind 等参数仅影响本地生成的执行器文件,不会改动源码行为本身;文档中"20% 以上加速"的表述引自项目自带文档的历史结论,具体性能收益仍取决于应用负载(系统调用、数据库访问往往才是瓶颈)。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384