5.SGI STL 二级空间配置器 _S_chunk_alloc核心函数解析

上节我们学习了allocate和自由链表填充函数 _S_refill,这一节继续深挖底层核心函数 _S_chunk_alloc,它是二级空间配置器真正负责从内存池或是系统堆中划分内存块的核心接口。

函数整体作用:接收需要划分的单个 chunk 块大小__size、期望分配的内存块个数__nobjs,其中个数以引用形式传入,函数内部可实际修改数量,最终向调用方返回连续内存的起始地址。

cpp 复制代码
template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size, 
                                                            int& __nobjs)
{
    char* __result;
    size_t __total_bytes = __size * __nobjs;//计算本次需求的总内存字节数
    size_t __bytes_left = _S_end_free - _S_start_free;
    // _S_start_free:内存池空闲区域起始地址
    // _S_end_free:内存池空闲区域末尾地址
    // _S_heap_size:记录内存池整体向系统申请的堆内存总量,三者均为静态全局变量,初始值都为0
    if (__bytes_left >= __total_bytes) {
        // 剩余空间足够满足全部需求,直接批量分配
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return(__result);
    } else if (__bytes_left >= __size) {
        // 剩余空间不足整体需求,但足够存放至少一个内存块
        __nobjs = (int)(__bytes_left/__size);
        __total_bytes = __size * __nobjs;
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return(__result);
    } else {
        // 剩余空间连单个内存块都放不下,必须向系统堆重新申请内存
        size_t __bytes_to_get = 
	  2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
        // 处理内存池遗留碎片,避免内存资源浪费
        if (__bytes_left > 0) {
            _Obj* __STL_VOLATILE* __my_free_list =
                        _S_free_list + _S_freelist_index(__bytes_left);

            ((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
            *__my_free_list = (_Obj*)_S_start_free;
        }
        // 调用系统malloc申请大块连续内存
        _S_start_free = (char*)malloc(__bytes_to_get);
        // 内存申请失败,进入应急兜底逻辑
        if (0 == _S_start_free) {
            size_t __i;
            _Obj* __STL_VOLATILE* __my_free_list;
	    _Obj* __p;
            // 从当前所需内存大小开始,向更大规格遍历所有自由链表
            for (__i = __size;
                 __i <= (size_t) _MAX_BYTES;
                 __i += (size_t) _ALIGN) {
                __my_free_list = _S_free_list + _S_freelist_index(__i);
                __p = *__my_free_list;
                if (0 != __p) {
                    // 取出空闲内存块临时充当内存池使用
                    *__my_free_list = __p -> _M_free_list_link;
                    _S_start_free = (char*)__p;
                    _S_end_free = _S_start_free + __i;
                    return(_S_chunk_alloc(__size, __nobjs));
                }
            }
	    _S_end_free = 0;	
            // 所有自由链表无空闲资源,调用一级配置器做最终尝试
            _S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
        }
        // 更新全局内存统计与内存池边界
        _S_heap_size += __bytes_to_get;
        _S_end_free = _S_start_free + __bytes_to_get;
        // 递归调用自身,使用新申请的内存完成分配
        return(_S_chunk_alloc(__size, __nobjs));
    }
}

零基础首次分配演示

我们以最常用的 单个块8字节,一次性需要20个块 为例梳理流程:程序首次使用内存池,全局内存池指针全部置零,__bytes_left 计算结果为 0,直接进入扩容分支。按照计算公式算出需要申请 320 字节内存,等价于一次性开辟 40 个 8 字节大小的 chunk 块。STL 这样设计的目的是一次性多申请一倍内存预留,减少程序运行过程中频繁调用系统 malloc 带来的性能损耗。

内存开辟成功后,更新堆内存总大小,划定内存池首尾范围,随后递归再次调用_S_chunk_alloc

第二次递归执行时,内存池已经拥有 320 字节空闲空间,完全满足 20 个 8 字节块的需求。函数直接取出对应长度内存,向后移动空闲起始指针,将内存首地址返回给上层_S_refill函数。

上层_S_refill拿到连续内存后,会完成内存块切割与链表挂载工作:将第一个内存块交付用户使用,剩余所有空闲块串联成单向链表,挂载到对应规格的自由链表中。后续allocate函数申请同规格内存时,直接从自由链表头部取块即可,无需重复走系统申请流程,分配效率大幅提升,直到自由链表内所有块被取完,才会再次触发内存池扩容。


四种实战分配场景详解

场景 1

**【前置条件】**8 字节规格自由链表内所有空闲内存块已经全部被程序调用使用完毕,但是内存池内部还预留着上次扩容多出的 20 个 8 字节空闲块。

此时用户再次申请 8 字节内存,allocate检测到自由链表为空,调用_S_chunk_alloc。当前内存池剩余空间充足,直接移动空闲指针划分内存,快速返回地址完成分配。随着程序不断申请内存,预留内存耗尽后会再次触发扩容,此时_S_heap_size已经记录了历史申请内存大小,后续扩容的内存体量会逐步增大,进一步降低系统调用频率。

场景 2

**【前置条件】**程序仅完成过一次 8 字节内存申请,当前内存池内部剩余可用空闲空间固定为 160 字节。

现在业务需要申请规格为 16 字节的内存块,期望一次性分配 20 个,总需求内存为 320 字节。首先对比判断,160 字节不足以满足整体需求,但足够容纳单个 16 字节内存块。函数自动计算出当前空间最多能划分出 10 个 16 字节块,修改传入的块数量参数,划分对应长度内存并移动指针。

内存交付到_S_refill后,会把这 10 个连续内存块串联成链表,挂载至 16 字节对应的自由链表数组位置,链表第一个空闲块预留出来等待分配给用户使用。

场景 3

**【前置条件】**程序仅完成过一次 8 字节内存申请,当前内存池内部剩余可用空闲空间固定为 160 字节。

现在需要申请 128 字节规格的内存块,依旧期望分配 20 个。经过条件判断,剩余空间可以容纳单个 128 字节内存块,最终仅划分出 1 个内存块交付使用。本次分配结束后,内存池内部会剩余 32 字节的零碎空闲空间。

内存送入_S_refill函数后,程序判断本次仅分配到单个内存块,不需要执行链表串联逻辑,直接将内存地址返回即可,精简执行流程,减少不必要的性能开销。

场景 4

**【前置条件】**严格承接场景 3 执行完毕后的内存状态,此时内存池内仅剩下 32 字节零碎空闲空间,无大块连续内存。

现在业务需要申请 40 字节规格的内存块,一次性需要 20 个,整体需求内存达到 800 字节。当前仅剩的 32 字节空间,连一个 40 字节的内存块都无法存放,直接进入内存扩容分支。

第一步优先处理零碎内存资源:这 32 字节空间虽然无法满足当前业务需求,但不能直接丢弃。程序通过下标计算匹配到 32 字节对应的自由链表,将这块碎片内存以头插的方式加入空闲链表。举个实际例子:原本 32 字节自由链表中存有 5 个空闲块,插入这块碎片后,链表内空闲块数量变为 6 个,最大化利用已申请的内存资源。

处理完碎片后,正式向系统堆申请大容量连续内存,满足本次批量分配需求。

分支一:系统内存充足,malloc 申请成功

更新内存池相关全局变量,递归调用自身完成 40 字节内存块的划分,后续正常交由上层函数处理链表挂载逻辑。

分支二:系统内存紧张,malloc 申请失败

程序启动应急自救逻辑,源码设计中规定只从当前需要的内存大小向更大规格遍历自由链表,不向更小规格查找,目的是规避多进程环境下小内存碎片过多引发的分配异常问题。

遍历过程中一旦找到任意规格的空闲内存块,立刻取出该内存块临时作为新的内存池,重新划定内存池边界,再次递归尝试完成本次内存分配。

cpp 复制代码
        if (0 == _S_start_free) {
            size_t __i;
            _Obj* __STL_VOLATILE* __my_free_list;
	    _Obj* __p;
            for (__i = __size; //从当前40字节开始遍历
                 __i <= (size_t) _MAX_BYTES;//遍历上限128字节
                 __i += (size_t) _ALIGN) {//每次递增8字节对齐单位
                __my_free_list = _S_free_list + _S_freelist_index(__i);
                __p = *__my_free_list;
                if (0 != __p) {
                    *__my_free_list = __p -> _M_free_list_link;
                    _S_start_free = (char*)__p;
                    _S_end_free = _S_start_free + __i;
                    return(_S_chunk_alloc(__size, __nobjs));
                }
            }
	    _S_end_free = 0;	
            // 穷尽所有空闲资源后,调用一级配置器做最后尝试
            _S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
        }

如果遍历完 40~128 字节所有自由链表,依旧找不到任何可用空闲内存,代表当前系统内存资源已经极度紧张。程序清空内存池标记,调用一级空间配置器进行最终内存申请。

cpp 复制代码
  static void* allocate(size_t __n)
  {
    void* __result = malloc(__n);
    // 普通malloc失败,进入内存耗尽处理逻辑
    if (0 == __result) __result = _S_oom_malloc(__n);
    return __result;
  }

最终兜底的_S_oom_malloc是 STL 预留的内存异常处理接口,内部依靠函数指针绑定用户自定义的内存释放回调函数。程序会无限循环执行回调函数,主动释放程序闲置内存,反复重试内存申请;如果用户没有注册自定义回调函数,程序会直接抛出内存分配异常,终止异常流程。

cpp 复制代码
template <int __inst>
void (* __malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;

template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)
{
    void (* __my_malloc_handler)();
    void* __result;

    for (;;) {
        __my_malloc_handler = __malloc_alloc_oom_handler;
        // 无自定义内存回收回调,直接抛出异常
        if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }
        // 执行用户自定义内存释放逻辑
        (*__my_malloc_handler)();
        __result = malloc(__n);
        if (__result) return(__result);
    }
}

(原笔记:GameServer-Learning/00-Notes/C++/SGI_MemoryPool at main · maomianbaobumoyu/GameServer-Learning)

相关推荐
学掌门3 小时前
JavaScript:为什么命名参数比位置参数更好
开发语言·javascript·ecmascript
码界筑梦坊3 小时前
124-基于Python的航空旅客满意度数据可视化分析系统
开发语言·python·信息可视化·数据分析·flask·毕业设计
XMYX-03 小时前
31 - Go url 解析:从字符串到结构化请求的完整路径
开发语言·golang
hhb_6183 小时前
PHP开发实战:高频难点解析与优化方案
开发语言·php
夕除3 小时前
spring boot 8
java·开发语言
AI玫瑰助手3 小时前
Python流程控制:pass语句的作用与使用场景
开发语言·python·信息可视化
-快乐的程序员-3 小时前
C++的md5函数
开发语言·c++
Huangjin007_3 小时前
【C++ STL篇(九)】map容器——零基础入门与核心用法精讲
开发语言·c++·算法
qq_4924484463 小时前
Jmeter Transaction Controller(事务控制器) 的 TPS(每秒事务数)严格固定为 1
java·开发语言·jmeter