我的动物公司免安装绿色中文版
· 2025-11-02
在 PHP 的内存管理体系中,小块内存(small block) 指的是大小不超过 ZEND_MM_MAX_SMALL_SIZE(3072 Bytes)的内存请求。这类内存的使用频率极高,因此 Zend 。整体思想是“用少量的空间浪费换取极高的分配效率”。
整个分配流程可分为三步:
目标是在尽量少的系统调用下,快速获得可用的小内存块。
小块内存的分配逻辑,并不是“一次分配一个”,而是每次批量分配一串小块。原因很简单:小块使用极其频繁,如果每次都直接向操作系统申请,就会造成严重的性能瓶颈。因此 Zend 在启动阶段,就通过 ZEND_MM_BINS_INFO() 预先定义了 30 种小块内存配置。
num列 行号 |
size列 大小(Bytes) |
elements列 每次分配数量 |
总大小(Bytes) | pages列 占用page数 |
page使用率 |
| 0 | 8 | 512 | 4096 | 1 | 100.00% |
| 1 | 16 | 256 | 4096 | 1 | 100.00% |
| 2 | 24 | 170 | 4080 | 1 | 99.61% |
| 3 | 32 | 128 | 4096 | 1 | 100.00% |
| 4 | 40 | 102 | 4080 | 1 | 99.61% |
| 5 | 48 | 85 | 4080 | 1 | 99.61% |
| 6 | 56 | 73 | 4088 | 1 | 99.80% |
| 7 | 64 | 64 | 4096 | 1 | 100.00% |
| 8 | 80 | 51 | 4080 | 1 | 99.61% |
| 9 | 96 | 42 | 4032 | 1 | 98.44% |
| 10 | 112 | 36 | 4032 | 1 | 98.44% |
| 11 | 128 | 32 | 4096 | 1 | 100.00% |
| 12 | 160 | 25 | 4000 | 1 | 97.66% |
| 13 | 192 | 21 | 4032 | 1 | 98.44% |
| 14 | 224 | 18 | 4032 | 1 | 98.44% |
| 15 | 256 | 16 | 4096 | 1 | 100.00% |
| 16 | 320 | 64 | 20480 | 5 | 100.00% |
| 17 | 384 | 32 | 12288 | 3 | 100.00% |
| 18 | 448 | 9 | 4032 | 1 | 98.44% |
| 19 | 512 | 8 | 4096 | 1 | 100.00% |
| 20 | 640 | 32 | 20480 | 5 | 100.00% |
| 21 | 768 | 16 | 12288 | 3 | 100.00% |
| 22 | 896 | 9 | 8064 | 2 | 98.44% |
| 23 | 1024 | 8 | 8192 | 2 | 100.00% |
| 24 | 1280 | 16 | 20480 | 5 | 100.00% |
| 25 | 1536 | 8 | 12288 | 3 | 100.00% |
| 26 | 1792 | 16 | 28672 | 7 | 100.00% |
| 27 | 2048 | 8 | 16384 | 4 | 100.00% |
| 28 | 2560 | 8 | 20480 | 5 | 100.00% |
| 29 | 3072 | 4 | 12288 | 3 | 100.00% |
这张表格定义了所有小块的分配策略。看似繁琐,但非常直观。
例如:
zend_mm_small_size_to_bin() 函数在分配小块内存前,Zend 需要根据请求大小找到对应的配置行号(bin)。这由以下函数完成:
// 根据内存大小获取配置行号
static zend_always_inline int zend_mm_small_size_to_bin(size_t size)
在运行时,ZEND_MM_BINS_INFO() 表会被拆分成三个全局数组,以便快速查找:
bin_data_size[] // 存放每档小块的实际大小
bin_elements[] // 存放每次批量分配的块数
bin_pages[] // 存放每档占用的页数
函数的逻辑非常高效:它会根据 size 快速定位到最合适的档位,然后返回行号。通过这个行号,就能从 bin_pages[bin_num] 得到需要分配的页数。
分配链路如下:
zend_mm_alloc_small() → zend_mm_alloc_small_slow() → zend_mm_alloc_pages()
zend_mm_alloc_small() 函数该函数是小块分配的核心入口,逻辑清晰简洁:
static zend_always_inline void *zend_mm_alloc_small(zend_mm_heap *heap, int bin_num) {
// 如果有空闲的小块,直接取用
if (EXPECTED(heap->free_slot[bin_num] != NULL)) {
zend_mm_free_slot *p = heap->free_slot[bin_num]; // 当前空闲块
heap->free_slot[bin_num] = p->next_free_slot; // 更新链表头
return p;
} else {
// 否则分配新的 page 串
return zend_mm_alloc_small_slow(heap, bin_num);
}
}
zend_mm_alloc_small_slow() 函数当空闲链表为空时,系统会调用慢路径:
// bin_num 是配置行号,由 zend_mm_small_size_to_bin() 计算得到
static zend_never_inline void *zend_mm_alloc_small_slow(
zend_mm_heap *heap, uint32_t bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
在这个函数中,Zend 首先调用 zend_mm_alloc_pages() 分配连续的页,然后更新每一页的地图信息:
// 第一个 page 存放配置信息行号,并添加 SRUN 标记
chunk->map[page_num] = ZEND_MM_SRUN(bin_num);
// 如果需要多个 page
if (bin_pages[bin_num] > 1) {
uint32_t i = 1;
do {
// 后续 page 添加 SRUN + LRUN 双标记
chunk->map[page_num + i] = ZEND_MM_NRUN(bin_num, i);
i++;
} while (i < bin_pages[bin_num]);
}
在 chunk 结构中,除了 bitset 外,还有一个 map 数组。它由 512 个 32 位整数构成,每个 page 对应一个元素,用于存储该页的“角色标记”。
#define ZEND_MM_LRUN_PAGES_MASK 0x000003ff // 低10位:页数或偏移
#define ZEND_MM_SRUN_BIN_NUM_MASK 0x0000001f // 低5位:bin编号
#define ZEND_MM_SRUN_FREE_COUNTER_MASK 0x01ff0000 // [16,24) 位:空闲计数
#define ZEND_MM_NRUN_OFFSET_MASK 0x01ff0000 // NRUN 页偏移
可以看到,这里大量使用位运算掩码,是为了在有限的 32 位空间内高效编码三种信息:标记位、bin号、偏移量。
段1主要用来存放ZEND_MM_IS_LRUN和ZEND_MM_IS_SRUN标记,只使用前两个位。
段2和段3用于存放两个数字,在不同的状态中用法略有不同。
page在使用过程中有的四种状态对应如下:
对应的宏定义如下:
#define ZEND_MM_LRUN(count) (0x40000000 | count) // 大块页
#define ZEND_MM_SRUN(bin_num) (0x80000000 | bin_num) // 小块首页
#define ZEND_MM_SRUN_EX(bin_num,count) (0x80000000 | bin_num | count << 16)
#define ZEND_MM_NRUN(bin_num,offset) (0xC0000000 | bin_num | offset << 16)
当分配完一串小块后,Zend 会使用链表把它们串起来,以便后续快速取用。这是通过 zend_mm_free_slot 结构体实现的:
// 用于连接空闲小块的链表结构
typedef struct _zend_mm_free_slot zend_mm_free_slot;
struct _zend_mm_free_slot {
zend_mm_free_slot *next_free_slot; // 指向下一个空闲小块
};
创建链表的逻辑如下:
// 计算链表的首尾地址
end = (zend_mm_free_slot*)((char*)bin + (bin_data_size[bin_num] * (bin_elements[bin_num] - 1)));
// 小块内存链表开头的指针,每个配置一个指针,共30个指针
// heap->free_slot[bin_num] 本身就是第一个元素,所以它里面的指针要指向第二个元素
heap->free_slot[bin_num] = p = (zend_mm_free_slot*)((char*)bin + bin_data_size[bin_num]);
do {
// 每个元素的 next 指向下一个小块
p->next_free_slot = (zend_mm_free_slot*)((char*)p + bin_data_size[bin_num]);
p = (zend_mm_free_slot*)((char*)p + bin_data_size[bin_num]);
} while (p != end);
p->next_free_slot = NULL; // 最后一个元素终止
除了常规分配方式,Zend 还提供了更安全的内存分配函数 safe_emalloc() 与 ecalloc(),它们在执行前会检测是否存在整数溢出风险。
调用链如下:
ecalloc() → _ecalloc() → _emalloc() → zend_mm_alloc_heap()
safe_emalloc() → _safe_emalloc() → _emalloc() → zend_mm_alloc_heap()
两者区别:
_ecalloc() 会将分配的内存全部置 0;_safe_emalloc() 多接收一个 offset 参数,用于额外偏移。源码如下:
ZEND_API void* ZEND_FASTCALL _ecalloc(size_t nmemb, size_t size){
void *p;
size = zend_safe_address_guarded(nmemb, size, 0); // 检查溢出
p = _emalloc(size); // 分配内存
memset(p, 0, size); // 初始化为 0
return p;
}
ZEND_API void* ZEND_FASTCALL _safe_emalloc(size_t nmemb, size_t size, size_t offset){
// 检测内存是否会溢出,并分配内存
return _emalloc(zend_safe_address_guarded(nmemb, size, offset));
}
溢出检测通过 zend_safe_address() 实现:
// 检测乘加是否越界
static zend_always_inline size_t zend_safe_address(size_t nmemb, size_t size, size_t offset, bool *overflow){
size_t res = nmemb * size + offset; // 整数结果
double _d = (double)nmemb * (double)size + (double)offset; // 浮点校验
double _delta = (double)res - _d; // 误差检测
if (UNEXPECTED((_d + _delta) != _d)) {
*overflow = 1;
return 0; // 溢出则返回 0
}
*overflow = 0;
return res;
}
在 Zend 内存分配系统中:
三种机制形成了分层架构:既保证了分配性能,又平衡了内存利用率。
如果你对 PHP 内存管理有不同的理解,或者希望我在后续文章中讲解具体的分配策略,欢迎留言讨论~
本文项目地址:github.com/xuewolf/php…