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() 进行"分配---复制---释放"的通用流程,在性能与健壮性之间取得平衡。

相关推荐
权泽谦3 小时前
从零搭建一个 PHP 登录注册系统(含完整源码)
android·开发语言·php
JaguarJack4 小时前
深入理解 Laravel Middleware:完整指南
后端·php·laravel
西部森林牧歌14 小时前
Arbess零基础学习 - 使用Arbess+GitLab实现PHP项目构建/主机部署
ci/cd·gitlab·php·tiklab devops
Q_Q51100828518 小时前
python+django/flask的校园活动中心场地预约系统
spring boot·python·django·flask·node.js·php
蒲公英源码18 小时前
基于PHP+Vue+小程序快递比价寄件系统
vue.js·小程序·php
Q_Q196328847520 小时前
python+django/flask基于机器学习的就业岗位推荐系统
spring boot·python·django·flask·node.js·php
韩立学长20 小时前
【开题答辩实录分享】以《奇妙英语角小程序的设计与实现》为例进行答辩实录分享
小程序·php
Tigshop开源商城系统1 天前
Tigshop 开源商城系统 php v5.1.9.1版本正式发布
java·大数据·开源·php·开源软件
拾忆,想起1 天前
超时重传 vs 快速重传:TCP双保险如何拯救网络丢包?
java·开发语言·网络·数据库·网络协议·tcp/ip·php