前文中已经介绍过,在内存管理中,调整已分配内存块的大小是高频且关键的操作。为便于系统化把握实现细节,本文选取三条主干函数路径展开:
zend_mm_realloc_heap():通用入口,覆盖 small / large / 指针无效等多数分支逻辑,决定"就地扩缩"还是"新块复制+旧块释放"。zend_mm_realloc_huge():面向巨大块(huge)的专用路径,含不同平台(如 Windows、Linux)的对齐与扩缩策略优化。erealloc2():与 erealloc() 调用链相同,但开启"只复制指定大小"的开关,用于控制复制成本。
目标是:在不改变源码语义 的前提下,用逐行注释 与条件分支编号方式还原关键决策点,直观呈现"什么时候原地扩缩、什么时候新分配并迁移"的工程权衡与实现细节。
一、zend_mm_realloc_heap() 函数
zend_mm_realloc_heap() 是内存管理系统中最核心的函数之一,用于在不丢失数据的前提下,调整已分配内存块的大小。函数会根据不同类型的内存块(small / large / huge)采用不同的策略,并优先尝试"原地扩缩"以避免拷贝操作。
函数的核心代码如下,每个条件分支已标注编号,便于对应业务逻辑分支表(见前文)追踪:
c
// 找到相对于 ZEND_MM_CHUNK_SIZE 的偏移量
page_offset = ZEND_MM_ALIGNED_OFFSET(ptr, ZEND_MM_CHUNK_SIZE);
if (UNEXPECTED(page_offset == 0)) { // 情况1:无偏移,是 huge 块
if (EXPECTED(ptr == NULL)) { // 情况1.1:如果指针无效
return _zend_mm_alloc(heap, size); // 直接开辟新内存
} else { // 情况1.2:原 huge 块有效
return zend_mm_realloc_huge(heap, ptr, size, copy_size); // 调整 huge 块大小
}
} else { // 情况2:有偏移,是 small 或 large
// 取回所在 chunk 的指针
zend_mm_chunk *chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(ptr, ZEND_MM_CHUNK_SIZE);
int page_num = (int)(page_offset / ZEND_MM_PAGE_SIZE); // 计算 page 页码
zend_mm_page_info info = chunk->map[page_num]; // 地图信息
if (info & ZEND_MM_IS_SRUN) { // 情况2.1: 如果是 small 块
// 取回 ZEND_MM_BINS_INFO() 中的配置行号
int old_bin_num = ZEND_MM_SRUN_BIN_NUM(info);
do { // 这个 do 是为了 break
old_size = bin_data_size[old_bin_num]; // ZEND_MM_BINS_INFO 中的 size
if (size <= old_size) { // 情况2.1.1:如果想把内存划分得更小
// 情况2.1.1.1:旧的行号 > 0 且新尺寸小于前一档的尺寸
// 必须满足这个要求才可以把内存划得更小
if (old_bin_num > 0 && size < bin_data_size[old_bin_num - 1]) {
// 计算 size 在 ZEND_MM_BINS_INFO 数组中的序号,size 最大是 3072
ret = zend_mm_alloc_small(heap, ZEND_MM_SMALL_SIZE_TO_BIN(size));
// 要复制的内容大小,use_copy_size:只复制这么多,多余的丢掉
copy_size = use_copy_size ? MIN(size, copy_size) : size;
memcpy(ret, ptr, copy_size); // 复制内容到新地址
zend_mm_free_small(heap, ptr, old_bin_num); // 释放原来的小块
} else { // 情况2.1.1.2:不能划得更小,就不重新分配
ret = ptr;
}
// 情况2.1.2:size <= 3072,目标是大一些的 small 块
} else if (size <= ZEND_MM_MAX_SMALL_SIZE) {
ret = zend_mm_alloc_small(heap, ZEND_MM_SMALL_SIZE_TO_BIN(size)); // 分配 small 块
copy_size = use_copy_size ? MIN(old_size, copy_size) : old_size; // 确定要 copy 的大小
memcpy(ret, ptr, copy_size); // 复制内容
zend_mm_free_small(heap, ptr, old_bin_num); // 释放原来的 small 块
} else { // 情况2.1.3:需要的尺寸大于 small 块,转为 large 或 huge
break; // 调用 zend_mm_realloc_slow() 函数
}
return ret; // 返回指针
} while (0);
// 情况2.2:如果是 large 块
} else /* if (info & ZEND_MM_IS_LARGE_RUN) */ {
// 这一串 page 的总大小
old_size = ZEND_MM_LRUN_PAGES(info) * ZEND_MM_PAGE_SIZE;
// 情况2.2.1:如果需要的尺寸在 large 范围内
if (size > ZEND_MM_MAX_SMALL_SIZE && size <= ZEND_MM_MAX_LARGE_SIZE) {
new_size = ZEND_MM_ALIGNED_SIZE_EX(size, ZEND_MM_PAGE_SIZE); // 大小向后对齐
if (new_size == old_size) { // 情况2.2.1.1:新旧相同
return ptr; // 无需重新分配
} else if (new_size < old_size) { // 情况2.2.1.2:缩小
int new_pages_count = (int)(new_size / ZEND_MM_PAGE_SIZE); // 新 page 数
int rest_pages_count = (int)((old_size - new_size) / ZEND_MM_PAGE_SIZE); // 剩余 page
chunk->map[page_num] = ZEND_MM_LRUN(new_pages_count); // 更新地图信息
chunk->free_pages += rest_pages_count; // 增加空闲 page
zend_mm_bitset_reset_range(chunk->free_map, page_num + new_pages_count, rest_pages_count); // 标记空闲段
return ptr;
} else { // 情况2.2.1.3:扩大
int new_pages_count = (int)(new_size / ZEND_MM_PAGE_SIZE);
int old_pages_count = (int)(old_size / ZEND_MM_PAGE_SIZE);
// 情况2.2.1.3.1:如果后面有足够的空闲 page
if (page_num + new_pages_count <= ZEND_MM_PAGES &&
zend_mm_bitset_is_free_range(chunk->free_map, page_num + old_pages_count, new_pages_count - old_pages_count)) {
chunk->free_pages -= new_pages_count - old_pages_count; // 减少空闲页
zend_mm_bitset_set_range(chunk->free_map, page_num + old_pages_count, new_pages_count - old_pages_count); // 标记使用
chunk->map[page_num] = ZEND_MM_LRUN(new_pages_count); // 更新 page 数
return ptr;
}
// 情况2.2.1.3.2:page 不够,进入慢路径
}
}
// 情况2.2.2:需要的尺寸不在 large 范围内,进入慢路径
}
}
// 处理前面未覆盖到的情况:2.1.3、2.2.1.3.2、2.2.2
copy_size = MIN(old_size, copy_size); // 要复制的大小
return zend_mm_realloc_slow(heap, ptr, size, copy_size); // 重新创建内存
从整体来看,这个函数承担了 几乎所有类型内存的动态扩缩,优先尝试原位更新以减少复制开销。 当页结构不再连续或目标块超出类别范围时,才进入 zend_mm_realloc_slow() 的"慢路径",重新分配并迁移数据。
二、zend_mm_realloc_huge() 函数
zend_mm_realloc_huge() 专门负责调整巨大块(huge block)的内存大小。与普通内存不同,巨大块直接由操作系统管理,因此扩缩时需要结合系统接口(如 mremap()、munmap()、VirtualAlloc())完成。 其核心目标是:尽量原地修改,不成功则重新分配。
函数实现如下:
c
static zend_never_inline void *zend_mm_realloc_huge(
zend_mm_heap *heap, void *ptr, size_t size, size_t copy_size) {
size_t old_size;
size_t new_size;
old_size = zend_mm_get_huge_block_size(heap, ptr); // 获取原块大小
// 若请求尺寸大于 2MB - 4B(约 2044K),进入 huge 分支
if (size > ZEND_MM_MAX_LARGE_SIZE) {
#ifdef ZEND_WIN32 // Windows 操作系统
// Windows 无法动态扩展 huge 块,统一按 2MB 对齐
// REAL_PAGE_SIZE = 4K, ZEND_MM_CHUNK_SIZE = 2M
new_size = ZEND_MM_ALIGNED_SIZE_EX(size, MAX(REAL_PAGE_SIZE, ZEND_MM_CHUNK_SIZE));
#else // 其他系统
// 按操作系统页大小对齐
new_size = ZEND_MM_ALIGNED_SIZE_EX(size, REAL_PAGE_SIZE);
#endif
if (new_size == old_size) { // 情况1:新旧大小一致
zend_mm_change_huge_block_size(heap, ptr, new_size);
return ptr;
} else if (new_size < old_size) { // 情况2:缩小内存
if (zend_mm_chunk_truncate(heap, ptr, old_size, new_size)) {
heap->real_size -= old_size - new_size; // 更新已用内存
zend_mm_change_huge_block_size(heap, ptr, new_size);
return ptr;
}
// truncate 失败则调用慢路径
} else /* if (new_size > old_size) */ { // 情况3:扩大内存
if (zend_mm_chunk_extend(heap, ptr, old_size, new_size)) {
heap->real_size += new_size - old_size;
zend_mm_change_huge_block_size(heap, ptr, new_size);
return ptr;
}
// extend 失败也进入慢路径
}
}
// 若 size < 2MB 或前面调整失败,进入慢路径
return zend_mm_realloc_slow(heap, ptr, size, MIN(old_size, copy_size));
}
从逻辑上看,这个函数主要包含三层判断:
- 按平台对齐策略确定新尺寸;
- 比较新旧尺寸并选择截短或扩展路径;
- 失败时进入
zend_mm_realloc_slow()重新分配。
整体设计体现了"原地修改优先"的原则:任何时候只要系统允许,Zend 都尽量避免重新分配和拷贝操作。
zend_mm_realloc_huge() 函数还调用到3个函数:zend_mm_change_huge_block_size() 函数、zend_mm_chunk_truncate() 函数和zend_mm_chunk_extend() 函数。
zend_mm_change_huge_block_size() 函数
zend_mm_change_huge_block_size() 负责在巨大块链表中更新块大小。 逻辑简单,仅遍历链表查找目标块并修改其 size。
c
static void zend_mm_change_huge_block_size(zend_mm_heap *heap, void *ptr, size_t size){
zend_mm_huge_list *list = heap->huge_list;
while (list != NULL) { // 遍历 huge 块列表
if (list->ptr == ptr) { // 找到目标块
list->size = size; // 更新大小记录
return;
}
list = list->next; // 继续遍历
}
}
由于巨大块数量有限且变化频率低,这种线性查找方式性能损耗极小。
zend_mm_chunk_truncate() 函数
此函数用于 截短 已有的 huge 内存块。在非 Windows 系统中,底层调用 munmap() 直接释放多余内存段。
c
static int zend_mm_chunk_truncate(zend_mm_heap *heap, void *addr, size_t old_size, size_t new_size) {
#ifndef _WIN32
// 非 windows 系统中,从 new_size 到 old_size 释放掉多余的内存
zend_mm_munmap((char*)addr + new_size, old_size - new_size);
return 1;
#else
// Windows 无法截短,交由慢路径处理
return 0;
#endif
}
这一步操作属于"立即回收"型行为,可显著减少内存浪费。
zend_mm_chunk_extend() 函数
与前者相对,该函数用于 扩展 已有 huge 块的长度。其实现根据操作系统能力不同而选择不同方案:
c
static int zend_mm_chunk_extend(zend_mm_heap *heap, void *addr, size_t old_size, size_t new_size) {
#ifdef HAVE_MREMAP // 若支持 mremap()
// 使用 mremap() 直接扩展映射区
void *ptr = mremap(addr, old_size, new_size, 0);
if (ptr == MAP_FAILED) {
return 0; // 失败则进入慢路径
}
return 1; // 成功返回 1
#elif !defined(_WIN32)
// 非 Windows 系统使用 mmap_fixed 追加空间
return (zend_mm_mmap_fixed((char*)addr + old_size, new_size - old_size) != NULL);
#else
// Windows 无法动态扩展
return 0;
#endif
}
这段逻辑中最关键的优化是
mremap()的使用,它能在不移动内存的前提下扩大空间,大幅减少拷贝和页表重映射开销。
整体来看,zend_mm_realloc_huge() 及其相关辅助函数构成了 PHP 在巨大内存块上的自适应管理机制。 它通过跨平台分支和多级回退,保证了兼容性、性能与稳定性的平衡。
三、erealloc2() 函数
erealloc2() 与 erealloc() 的调用路径一致,区别仅在于调用 zend_mm_realloc_heap() 时打开了"仅按指定大小复制"的限制 (use_copy_size = 1)。这使得在缩小或按需复制时,拷贝字节数受 copy_size 控制,更利于在部分场景下降低不必要的内存拷贝成本。
c
// 第三个参数 1:仅复制指定大小(copy_size)
// 第四个参数 copy_size:要求复制的字节数
return zend_mm_realloc_heap(AG(mm_heap), ptr, size, 1, copy_size);
关于此限制的影响与使用分支,已在 zend_mm_realloc_heap() 的代码路径中标注:仅在特定分支(如 2.1.1.2、2.1.2)会根据 use_copy_size 生效,从而精确控制复制规模,避免无意义的数据搬运。
四、小结
调整内存大小的设计取向是:就地优先、跨平台优化、失败回退。当原地策略不可行时,统一落回 zend_mm_realloc_slow() 进行"分配---复制---释放"的通用流程,在性能与健壮性之间取得平衡。