内存管理篇(一)· zend_alloc 的基本概念

前言

zend_alloc 是 PHP 最关键的内存管理工具,本文将首先介绍zend_alloc的整体架构与原理,为深入理解 zend_alloc 的实现奠定基础,然后在后续章节中逐步展开相关文件、主要功能、数据结构以及初始化流程。

注:本文所指"用户态内存",是指 PHP 进程在用户空间中由 Zend 引擎自行管理的堆内存,用于脚本执行时的变量、结构体、字符串等数据,不包含内核态或共享内存区域。

新的内存管理器(从 PHP 5.2 开始使用)会从整体上减少内存分配,并提升内存分配速度。

zend_alloc 是为 PHP 设计的、对现代 CPU 缓存友好的内存管理工具。大部分构思来源于 jemalloctcmalloc 的实现。

所有的内存分配被划分成三种:这些分类依据的是分配块的大小,每种类型对应不同的分配策略。

  • 巨大块(huge block) :尺寸比 chunk(默认2MB)更大,使用 mmap() 函数来进行分配。分配结果在内存中对齐到2MB(大小是2MB的倍数)。

  • 大块(large block) :每个 chunk 包含一串4KB大小的 page。大块的内存块总是对齐到 page 边界(向上取整到 page 的倍数)。大块分配用于3KB至2MB之间的内存请求,以整页方式对齐分配。

  • 小块(small block) :小于 page 大小的3/4(3KB)。小块尺寸向上取最接近的预定义数字的值(在 ZEND_MM_BINS_INFO 中共有30个预定义的数字:8, 16, 24, 32, ... 3072)。小块内存是通过 run 分配的。每个 run 被分配成一个单独的或一些连续的 page。每个 run 里的空间被分配成一串空间的元素链表,占用的总空间是8B的倍数。

zend_alloc 通过 chunk 从操作系统中分配内存。这些 chunk 和巨大的内存块总是与 chunk 对齐(大小是 chunk 的倍数)。因此,PHP 可以通过位运算快速确定任意指针属于哪个 chunk。

普通 chunk 的开头保留一个特殊用途的单独 page,用于存放:

  • 空闲 page 的 bitset

  • 预定义的小尺寸有效运行的 bitset

  • 保存 chunk 中每个 page 使用信息的 page 映射表

zend_alloc 提供类似 emalloc / efree / erealloc 的 API,但它还提供了专用的和优化的方法来分配预定义大小的内存块(例如 emalloc_2()emalloc_4()、...、emalloc_large() 等)。当需要的内存大小已知时,这个类库使用 C 语言预处理器(preprocessor)技术和更多的专用方法来代替调用 emalloc() 函数。

来源:zend_alloc.c

本篇文章旨在介绍 zend_alloc 的实现逻辑和原理,源码版本为 8.2.5

本篇文章偏向底层源码解析,适合对 PHP 内核、内存管理、C 语言感兴趣的读者。如果你只想快速了解 PHP 是如何管理内存的,可以重点阅读"小结"和"结构关系图"部分。


一、相关文件

zend_alloc 内存管理相关的文件主要有以下三个:

文件名 说明
zend_alloc.h C 语言头文件,定义接口与结构体
zend_alloc.c 核心业务逻辑实现
zend_alloc_sizes.h 存放常用常量和宏定义

这三个文件构成了 PHP 内存管理器的完整实现:定义接口、实现逻辑与配置常量。

关键常量

以下是 zend_alloc_sizes.h 文件中定义的关键常量:

arduino 复制代码
#define ZEND_MM_CHUNK_SIZE ((size_t) (2 * 1024 * 1024)) // chunk 大小 2MB
#define ZEND_MM_PAGE_SIZE  (4 * 1024)                  // 每个 page 4KB
#define ZEND_MM_PAGES      (ZEND_MM_CHUNK_SIZE / ZEND_MM_PAGE_SIZE) // 一个 chunk 可分成512个 page
#define ZEND_MM_FIRST_PAGE (1)                         // 第一个 page 编号1
#define ZEND_MM_MIN_SMALL_SIZE 8                       // 小块最小 8B
#define ZEND_MM_MAX_SMALL_SIZE 3072                    // 小块最大 3072B
#define ZEND_MM_MAX_LARGE_SIZE (ZEND_MM_CHUNK_SIZE - (ZEND_MM_PAGE_SIZE * ZEND_MM_FIRST_PAGE)) // 大块内存最大2MB - 4KB = 2044KB
常量名 说明
ZEND_MM_CHUNK_SIZE 2MB PHP 向操作系统申请的最小内存单元
ZEND_MM_PAGE_SIZE 4KB chunk 内的最小逻辑页大小
ZEND_MM_PAGES 512 每个 chunk 包含512页
ZEND_MM_FIRST_PAGE 1 第0页为管理页,从第1页开始分配
ZEND_MM_MIN_SMALL_SIZE 8B 小块最小分配单位(8 字节对齐)
ZEND_MM_MAX_SMALL_SIZE 3072B 小块最大值(page 的3/4)
ZEND_MM_MAX_LARGE_SIZE 2044KB 大块分配上限(去除管理页)

这些常量定义了 PHP 内存分配的几何结构,是后续所有 chunk、page、run 计算的基础。


二、主要功能

了解内存管理器的主要功能,可以从 zend_alloc.h 中的标准封装宏程序(Standard wrapper macros)列表开始。这些封装宏的设计目的在于统一内存管理接口,便于调试与内存统计,从而在整个 Zend 引擎中提供一致的内存操作入口。以下列出了 Zend 内存分配器对外提供的标准 API 及其底层实现关系:

功能分组 宏名称 目标方法 说明
分配内存 emalloc() _emalloc() 通用方法,外部大量调用
emalloc_large() _emalloc_large() 分配大块内存,内部少量调用
emalloc_huge() _emalloc_huge() 分配巨大块内存,内部少量调用
safe_emalloc() _safe_emalloc() 分配内存,带安全保护,外部大量调用
ecalloc() _ecalloc() 分配内存并清零,外部大量调用
释放内存 efree() _efree() 通用方法,外部大量调用
efree_large() _efree_large() 释放大块内存,内部少量调用
efree_huge() _efree_huge() 释放巨大块内存,内部少量调用
调整内存大小 erealloc() _erealloc() 调整内存大小,外部大量调用
erealloc2() _erealloc2() 调整内存大小,内部少量调用
safe_erealloc() _safe_erealloc() 调整内存大小,带安全保护,外部大量调用
创建内存并复制指定内容 estrdup() _estrdup() 使用 _emalloc() 函数创建并复制指定字符串(不传入长度)
estrndup() _estrndup() 使用 _emalloc() 函数创建并复制指定字符串(传入长度)
zend_strndup() _zend_strndup() 使用原生函数创建并复制指定字符串(传入长度)

可见,内存管理器最重要的功能是分配内存、释放内存和调整内存大小。这些接口构成了 Zend 引擎内存管理的外部 API 层,是所有扩展模块和核心组件的基础。下面以这几项主要功能为线索,依次展开介绍。


三、基本数据结构

为了支撑上节所述的三种内存分配方式,zend_alloc 定义了一系列核心数据结构,用于记录堆(heap)、块(chunk)、页(page)及运行状态。内存分配中用到的主要结构如下:

名称 结构体 别名 备注
- _zend_alloc_globals zend_alloc_globals 用作全局入口
heap _zend_mm_heap zend_mm_heap 堆结构,是整个内存系统的根节点
chunk _zend_mm_chunk zend_mm_chunk 块,分配单元,管理页集合
page _zend_mm_page zend_mm_page 页,最小逻辑内存单位(4KB)

zend_alloc_globals 结构体在 zend_alloc.c 中声明。它在全局只有一个实例,充当全局入口,指向当前进程的 zend_mm_heap 对象。它的结构非常简单,仅包含一个指向 zend_mm_heap 实例的指针。

zend_mm_heap 结构体是整个内存的根,所有 chunk 都与它关联。它除了保留主 chunk 的指针,还要记录内存使用情况,主要结构如下:

元素名 类型 说明
free_slot zend_mm_free_slot[] 空闲小块内存链表指针,30个元素
huge_list zend_mm_huge_list* 巨大块内存链表指针
main_chunk zend_mm_chunk* 主 chunk
cached_chunks zend_mm_chunk* chunk 链表指针
chunks_count int chunk 数量
peak_chunks_count int chunk 最大数量
cached_chunks_count int 缓存 chunk 数量
avg_chunks_count double 平均 chunk 数量
last_chunks_delete_boundary int chunk 清理阈值
last_chunks_delete_count int 清理时删除的 chunk 数量
real_size size_t 当前使用的内存大小
limit size_t 最大可内存大小
overflow int 内存是否已经溢出

以上是 zend_mm_heap 的关键元素,其中前4个用于管理分配与回收,其他用于内存清理与性能监控。

zend_mm_chunk 结构体是 PHP 的基本内存分配单元,每个 chunk 固定2MB,内部按页(page)划分,页的使用状态由位图和映射表记录。内存管理器向操作系统申请内存时,总是以 chunk 为单位;清理释放内存时,也是以 chunk 为单位。主体结构如下:

元素名 类型 说明
heap zend_mm_heap* 指向所属 heap
next zend_mm_chunk* 指向前一个 chunk
prev zend_mm_chunk* 指向后一个 chunk
free_pages uint32_t 空闲 page 数量
free_tail uint32_t 末尾空闲 page 数量
num uint32_t chunk 的创建序号
reserve char[] 未使用字段
heap_slot zend_mm_heap 主 chunk 会用到它
free_map zend_mm_page_map 64B(512 bit),每个 bit 标记一个 page 的使用状态
map zend_mm_page_info[] 512个 page,每个 page 分配一个32位整数(4B),共2KB

zend_mm_page 结构体是内存分配的中间单位,其大小由前文中提到的 ZEND_MM_PAGE_SIZE 常量决定,常值为4KB。page 的结构非常简单:

元素名 类型 说明
bytes char[ZEND_MM_PAGE_SIZE] 占4KB内存(不仅当作 char 型使用)

结构关系图(heap → chunk → page → run)

scss 复制代码
zend_mm_heap (heap)
│
├── zend_mm_chunk (2MB)
│   │
│   ├── chunk header(结构体头部字段:heap / next / prev / ...)
│   │
│   ├── 第0页:管理页(地图)
│   │   ├── free_map(64B 位图,标记 512 个 page 是否空闲)
│   │   └── map(page 信息表:512 × 4B ≈ 2KB)
│   │
│   └── 第1页 ~ 第511页:可分配页区
│        ├── run(small bins,小块分配区)
│        └── lrun(large block,大块:整页对齐)
│
└── huge block (>2MB)
     └── 通过 mmap 独立映射(不占用上述 chunk 页区)

四、启动内存管理

在进行内存分配前,要先启动内存管理器。zend.c 中的 zend_startup() 函数用于启动 PHP 的相关组件,无论是命令行模式、FPM 模式还是调试模式,PHP 在所有运行模式下都会调用该函数。zend_startup() 函数调用 zend_alloc.c 中的 start_memory_manager() 函数来启动内存管理器。

初始化过程主要包括以下函数调用路径:

scss 复制代码
zend_startup()
  └── start_memory_manager()
        └── alloc_globals_ctor()
              └── zend_mm_init()
                    └── zend_mm_chunk_alloc_int()
                          └── zend_mm_mmap()

该路径展示了从全局初始化到堆构建,再到底层内存映射的完整流程。

zend_mm_init() 函数调用 zend_mm_chunk_alloc_int() 函数,分配内存并创建主 chunk,同时在主 chunk 内初始化 zend_mm_heap 实例。由此可见:

  1. 主 chunk 的大小是 ZEND_MM_CHUNK_SIZE,值为2MB。因此 PHP 程序只要启动,就会在这里分配一个2MB的空间。
  2. zend_mm_heap 实例不是单独创建的,而是主 chunk 中的一部分。

至此,PHP 的内存管理体系完成初始化,主 chunk 与堆实例一同构成了整个内存分配的起点。

小结

本文从文件结构、核心常量、功能接口、数据结构到初始化流程,解析了 zend_alloc 的设计思路与运行机制。可以看到,PHP 的内存管理体系以 chunk 为核心、page 为基础单元,通过层次化的结构设计和高效的位图标记,实现了对不同大小内存块的精确控制。下一篇文章将深入分析内存分配与回收的细节,包括小块(small block)与大块(large block)的具体实现策略。

如果你对 PHP 内存管理有不同的理解,或者希望我在后续文章中讲解具体的分配策略,欢迎留言讨论~

相关推荐
星光一影9 小时前
大型酒店管理系统源码(多酒店版)
mysql·php
从零开始的ops生活17 小时前
【Day 80】Linux-NAS 和 SAN 存储
linux·运维·php
shizhenshide17 小时前
为什么有时候 reCAPTCHA 通过率偏低,常见原因有哪些
开发语言·php·验证码·captcha·recaptcha·ezcaptcha
偶尔贪玩的骑士1 天前
Kioptrix Level 1渗透测试
linux·开发语言·网络安全·php
迎風吹頭髮1 天前
Linux服务器编程实践58-getnameinfo函数:通过socket地址获取主机名与服务名
开发语言·数据库·php
探索宇宙真理.1 天前
WordPress Flex QR Code Generator文件上传 | CVE-2025-10041 复现&研究
经验分享·php·安全漏洞
PFinal社区_南丞1 天前
构建可维护的正则表达式系统-pfinal-regex-center设计与实现
后端·php
wearegogog1231 天前
负荷聚类及其在MATLAB中的实现
matlab·php·聚类
BingoGo1 天前
PHP 8.5 新特性 闭包可以作为常量表达式了
后端·php