PHP内核详解 · 内存管理篇(八)· 调整内存块大小的关键函数

前文中已经介绍过,在内存管理中,调整已分配内存块的大小是高频且关键的操作。为便于系统化把握实现细节,本文选取三条主干函数路径展开:

  • 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));
}

从逻辑上看,这个函数主要包含三层判断:

  1. 按平台对齐策略确定新尺寸;
  2. 比较新旧尺寸并选择截短或扩展路径;
  3. 失败时进入 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() 进行"分配---复制---释放"的通用流程,在性能与健壮性之间取得平衡。

相关推荐
JaguarJack7 小时前
为什么 PHP 闭包要加 static?
后端·php·服务端
ServBay1 天前
垃圾堆里编码?真的不要怪 PHP 不行
后端·php
用户962377954481 天前
CTF 伪协议
php
BingoGo4 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack4 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo5 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack5 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack6 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo6 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack7 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel