本节我们继续深耕 SGI STL 二级空间配置器 核心源码,重点学习两大核心函数:allocate 内存分配函数 和 _S_refill 内存池填充函数。
一、allocate 主函数源码整体解析
1. 完整源码
cpp
static void* allocate(size_t __n)
{
void* __ret = 0;
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n);
}
else {
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
// 构造函数加锁,栈销毁自动解锁,保证线程安全
# ifndef _NOTHREADS
_Lock __lock_instance;
# endif
_Obj* __RESTRICT __result = *__my_free_list;
if (__result == 0)
__ret = _S_refill(_S_round_up(__n));
else {
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
}
}
return __ret;
};
2. 核心分配逻辑

函数入参 __n 代表用户申请的内存字节数,整体分为两套分配策略,分界值为 _MAX_BYTES (128 字节):
- 大于 128 字节 :不走内存池,直接调用一级空间配置器,底层通过
malloc向系统申请内存; - 小于等于 128 字节 :走二级空间配置器内存池,通过自由链表复用内存块,规避频繁调用
malloc造成的性能损耗和内存碎片。
3. 小内存块(≤128 字节)详细分配流程
① 定位对应规格自由链表
通过二级指针 __my_free_list,结合 _S_freelist_index 计算出的下标,精准定位 _S_free_list 自由链表数组中对应的内存块规格。
举例:用户申请 15 字节内存,经过下标计算会定位到1 号自由链表,对应 16 字节规格的内存块。
② 多线程加锁保证安全
自由链表的增、删、改操作是非线程安全的,因此通过 RAII 机制创建锁对象,在核心逻辑执行前加锁,函数栈销毁时自动解锁,避免多线程竞争问题。
③ 空闲内存块分配逻辑
cpp
_Obj* __RESTRICT __result = *__my_free_list;
// 取出当前自由链表的首个节点
if (__result == 0)
// 链表为空,调用_S_refill填充内存池
__ret = _S_refill(_S_round_up(__n));
else {
// 链表不为空,执行头删操作,取出空闲块分配给用户
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
}
__result = *__my_free_list 用于获取当前规格自由链表的首个空闲内存块。
每个 chunk 内存块通过联合体实现内存复用:空闲时存储下一个节点地址,被使用时存储用户业务数据。
- 链表为空(__result == 0) :当前规格无空闲内存块,先通过
_S_round_up完成 8 字节对齐,再调用_S_refill填充内存池; - 链表不为空(__result != 0) :执行链表头删操作,将链表头节点取出分配给用户,链表头指针后移。
二、_S_refill 内存池填充函数(自由链表补给站)
当对应规格自由链表无空闲块时,会触发 _S_refill 函数,核心作用:向内存池批量申请连续内存、切分固定大小 chunk 块、构建自由链表,为后续内存申请提前储备空闲块,大幅提升分配效率。
1. 完整源码
cpp
template <bool __threads, int __inst>
void* __default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20;// 默认一次性申请20个对齐后的内存块
// 向内存池申请大块内存,__nobjs为引用传递,会更新实际分配块数
char* __chunk = _S_chunk_alloc(__n, __nobjs);
_Obj* __STL_VOLATILE* __my_free_list;// 对应规格自由链表头指针
_Obj* __result; // 返回给用户的内存块
_Obj* __current_obj; // 链表构建当前节点
_Obj* __next_obj; // 链表构建下一个节点
int __i; // 循环计数器
// 特殊情况:仅分配到1个块,直接返回无需建链表
if (1 == __nobjs) return(__chunk);
// 定位当前内存规格对应的自由链表
__my_free_list = _S_free_list + _S_freelist_index(__n);
/* 切分大块内存,构建自由链表 */
// 第一个块分配给用户
__result = (_Obj*)__chunk;
// 剩余块挂载到自由链表,从第二个块开始
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
// 循环串联所有剩余内存块
for (__i = 1; ; __i++) {
__current_obj = __next_obj;
// 按字节精准偏移,获取下一个内存块
__next_obj = (_Obj*)((char*)__next_obj + __n);
// 最后一个节点,尾指针置空
if (__nobjs - 1 == __i) {
__current_obj -> _M_free_list_link = 0;
break;
}
// 普通节点,串联下一个块
else {
__current_obj -> _M_free_list_link = __next_obj;
}
}
// 返回首个内存块给用户
return(__result);
}
2. 核心执行流程

① 批量申请内存块
cpp
int __nobjs = 20;
char* __chunk = _S_chunk_alloc(__n, __nobjs);
默认一次性向内存池申请 20 个 对齐后的固定大小内存块;__nobjs 是引用传递,若内存池资源不足,会自动更新为实际分配到的块数。__chunk 指向申请到的连续大块内存首地址。
② 特殊场景处理(仅分配 1 个块)
cpp
if (1 == __nobjs) return(__chunk);
若内存池资源极度紧张,仅能分配 1 个内存块,直接将该块返回给用户,无需构建自由链表(无剩余块可挂载)。
③ 定位对应自由链表
通过下标计算,找到当前内存规格对应的自由链表数组槽位,为后续挂载空闲块做准备。
④ 内存切分 + 构建单向链表
以8 字节内存块 为例,申请 20 个块会得到一段连续内存:[块0][块1][块2]...[块19]
- 块 0:直接分配给用户使用,作为函数返回值;
- 块 1~ 块 19:全部串联成单向链表,挂载到对应规格的自由链表上,供下次复用。
3. 重点难点:指针偏移必须强转 char*
源码中最核心的细节,也是高频面试考点:
cpp
__next_obj = (_Obj*)((char*)__next_obj + __n);
*为什么不能直接使用 _Obj 指针偏移?**
_Obj是联合体,固定占用 4 字节(指针大小);- 若直接通过
_Obj*偏移,+__n会触发指针类型偏移规则(偏移大小 = 类型大小 × 偏移数),造成内存越界; char*是 1 字节粒度指针,强转后+__n可以实现精准按字节偏移,完美切分固定大小的内存块。
4. 链表构建最终效果
批量申请 20 个内存块后:
- 用户获得 块 0,正常使用;
- 自由链表挂载:
块1 → 块2 → 块3 ... → 块19 → nullptr; - 后续同规格内存申请,直接从链表头部取块,无需调用系统内存申请函数,性能极高。
三、整体流程总结
- 用户调用
allocate申请小内存块,先通过辅助函数完成内存对齐、定位对应自由链表; - 判断链表是否有空闲块,有则直接头删分配,无则调用
_S_refill批量补给内存; _S_refill一次性申请 20 个内存块,一个交付用户,剩余批量挂载到自由链表;- 用户使用完毕后,通过
deallocate释放内存,内存块重新挂回自由链表,实现内存循环复用。
四、核心设计优势
- 规避频繁调用
malloc,减少系统调用开销; - 批量申请、按需分配,极大减少小内存块碎片;
- O (1) 链表头尾操作,内存分配释放效率拉满;
- 多线程加锁,保证并发场景下内存池操作安全。
(原笔记:GameServer-Learning/00-Notes/C++/SGI_MemoryPool at main · maomianbaobumoyu/GameServer-Learning)