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

  • 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)采用不同的策略,并优先尝试“原地扩缩”以避免拷贝操作。

函数的核心代码如下,每个条件分支已标注编号,便于对应业务逻辑分支表(见前文)追踪:

// 找到相对于 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_huge() 函数

zend_mm_realloc_huge() 专门负责调整巨大块(huge block)的内存大小。与普通内存不同,巨大块直接由操作系统管理,因此扩缩时需要结合系统接口(如 mremap()munmap()VirtualAlloc())完成。 其核心目标是:尽量原地修改,不成功则重新分配

函数实现如下:

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_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。

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() 直接释放多余内存段。

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 块的长度。其实现根据操作系统能力不同而选择不同方案:

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
}

整体来看,zend_mm_realloc_huge() 及其相关辅助函数构成了 PHP 在巨大内存块上的自适应管理机制。 它通过跨平台分支和多级回退,保证了兼容性、性能与稳定性的平衡。


三、erealloc2() 函数

erealloc2() 与 erealloc() 的调用路径一致,区别仅在于调用 zend_mm_realloc_heap() 时打开了“仅按指定大小复制”的限制use_copy_size = 1)。这使得在缩小或按需复制时,拷贝字节数受 copy_size 控制,更利于在部分场景下降低不必要的内存拷贝成本。

// 第三个参数 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() 进行“分配—复制—释放”的通用流程,在性能与健壮性之间取得平衡。

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


本文项目地址:github.com/xuewolf/php…

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]