配置器
STL一级适配器和二级适配器
考虑到小型区块所可能造成的内存破碎问题,SGI 设计了双层级配置器,第一级用 malloc()和 free(),第二级配置器则视情况采用不同的策略:当配置区块超过 128 bytes 时,视之为 “足够大”,便调用第一级配置器;当配置区块小于 128 bytes 时,视之为 “过小”,为了降低额外负担,便采用复杂的**内存池(memory pool)**整理方式,而不再求助于第一级配置器。
一级适配器
第一级配置器以 malloc(), free(), realloc()等 C 函数执行实际的内存配置、释放、重配置操作,并实现出类似 C++ new-handler7 的机制。
malloc()和 realloc()不成功后,改调用 oom_malloc() 和 oom_realloc()。
后两者都有内循环,不断调用 “内存不足处理例程”,期望在某次调用之后,获得足够的内存而圆满完成任务。但如果 “内存不足处理例程”并未被客端设定,oom_malloc()和 oom_realloc() 便老实不客气地调用 __THROW_BAD_ALLOC,丢出 bad_alloc 异常信息,或利用 exit(1)硬生生中止程序。
二级适配器
请求内存的布局:
SGI 第二级配置器的做法是,如果区块够大,超过 128 bytes 时,就移交第一级配置器处理。当区块小于 128 bytes 时,则以内存池(memory pool)管理,此法又称为次层配置(sub-allocation):每次配置一大块内存,并维护对应之自由链表(free-list)。下次若再有相同大小的内存需求,就直接从 free-lists 中拨出。如果客端释还小额区块,就由配置器回收到 free-lists 中——是的,别忘了,配置器除了负责配置,也负责回收。为了方便管理,SGI 第二级配置器会主动将任何小额区块的内存需求量上调至 8 的倍数(例如客端要求 30 bytes,就自动调整为 32 bytes),并维护 16 个 free-lists,各自管理大小分别为 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88,96,104,112,120,128 bytes 的小额区块。free-lists 的节点结构如下:
union obj {
union obj * free_list_link;
char client_data[1];
};
自由链表实现:
空间配置函数allocate()
身为一个配置器,__default_alloc_template 拥有配置器的标准接口函数allocate()。此函数首先判断区块大小,大于128 bytes 就调用第一级配置器,小于 128 bytes 就检查对应的 free list。如果 free list 之内有可用的区块,就直接拿来用,如果没有可用区块,就将区块大小上调至 8 倍数边界,然后调用 refill(),准备为free list重新填充空间。填充空间的意思时从内存池中拿出一部分空间填充到空闲链表中去。
如下图:
也就是说,当可以从空闲链表中找到可以分配的空间,就从空闲链表调出一个空间,如下图:
调出去之后后面的空闲节点向前移动:
回收区块的时候直接将这个区块的指针插入到对应节点的第一个空闲指针即可:
也即:
当发现对应的链表没有空闲分区的时候,也就是下面这种情况
然后就调用 refill(),准备为 free list 重新填充空间。新的空间将取自内存池(经由chunk_alloc() 完成)。缺省取得20 个新节点(新区块),但万一内存池空间不足,获得的节点数(区块数)可能小于20。
总的来说:
- 向内存池申请空间的起始地址
- 如果只申请到一个对象的大小, 就直接返回一个内存的大小, 如果有更多的内存, 就继续执行
- 从第二个块内存开始, 把从内存池里面分配的内存用链表给串起来, 并返回一个块内存的地址给用户
refill代码 如下(假设 n 已经适当上调至 8 的倍数):
// 内存填充
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
int nobjs = 20;
char * chunk = chunk_alloc(n, nobjs); // 向内存池申请空间的起始地址
obj * __VOLATILE * my_free_list;
obj * result;
obj * current_obj, * next_obj;
int i;
// 如果只申请到一个对象的大小, 就直接返回一个内存的大小
if (1 == nobjs) return(chunk);
my_free_list = free_list + FREELIST_INDEX(n);
// 申请的大小不只一个对象的大小的时候
result = (obj *)chunk;
// my_free_list指向内存池返回的地址的下一个对齐后的地址
*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 -> free_list_link = 0;
break;
}
else
{
current_obj -> free_list_link = next_obj;
}
}
return(result);
}
从内存池中取空间给 freelist 使用,是 chunk_alloc()的工作,其主要做了下面的工作:
- 内存池的大小大于需要的空间, 直接返回起始地址(nobjs默认设置为20, 所以每次调用都会给链表额外的19个内存块)
- 内存池的内存不足以马上分配那么多内存, 但是还能满足分配一个即以上的大小, 那就全部分配出去
- 如果一个对象的大小都已经提供不了了, 先将零碎的内存块给一个小内存的链表来保存, 然后就准备调用malloc申请40块+额外大小的内存块(额外内存块就由heap_size决定), 如果申请失败跳转到步骤4, 成功跳转到步骤6
- 充分利用更大内存的链表, 通过递归来调用他们的内存块
- 如果还是没有内存块, 直接调用一级配置器来申请内存, 还是失败就抛出异常, 成功申请就继续执行
- 重新修改内存起始地址和结束地址为当前申请的地址块, 重新调用chunk_alloc分配内存
内存池的代码如下:
// 内存池
template <bool threads, int inst>
char* __default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs)
{
char * result;
size_t total_bytes = size * nobjs; // 链表需要申请的内存大小
size_t bytes_left = end_free - start_free; // 内存池里面总共还有多少内存空间
// 内存池的大小大于需要的空间, 直接返回起始地址
if (bytes_left >= total_bytes)
{
result = start_free;
start_free += total_bytes; // 内存池的首地址往后移
return(result);
}
// 内存池的内存不足以马上分配那么多内存, 但是还能满足分配一个即以上的大小, 那就按对齐方式全部分配出去
else if (bytes_left >= size)
{
nobjs = bytes_left/size;
total_bytes = size * nobjs;
result = start_free;
start_free += total_bytes; // 内存池的首地址往后移
return(result);
}
else
{
// 如果一个对象的大小都已经提供不了了, 那就准备调用malloc申请两倍+额外大小的内存
size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
// Try to make use of the left-over piece.
// 内存池还剩下的零头内存分给给其他能利用的链表, 也就是绝不浪费一点.
if (bytes_left > 0)
{
// 链表指向申请内存的地址
obj * __VOLATILE * my_free_list = free_list + FREELIST_INDEX(bytes_left);
((obj *)start_free) -> free_list_link = *my_free_list;
*my_free_list = (obj *)start_free;
}
start_free = (char *)malloc(bytes_to_get);
// 内存不足了
if (0 == start_free)
{
int i;
obj * __VOLATILE * my_free_list, *p;
// 充分利用剩余链表的内存, 通过递归来申请
for (i = size; i <= __MAX_BYTES; i += __ALIGN)
{
my_free_list = free_list + FREELIST_INDEX(i);
p = *my_free_list;
if (0 != p)
{
*my_free_list = p -> free_list_link;
start_free = (char *)p;
end_free = start_free + i;
return(chunk_alloc(size, nobjs));
}
}
// 如果一点内存都没有了的话, 就只有调用一级配置器来申请内存了, 并且用户没有设置处理例程就抛出异常
end_free = 0; // In case of exception.
start_free = (char *)malloc_alloc::allocate(bytes_to_get);
}
// 申请内存成功后重新修改内存起始地址和结束地址, 重新调用chunk_alloc分配内存
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
return(chunk_alloc(size, nobjs));
}
}
上述的 chunk_alloc()函数以 end_free - start_free 来判断内存池的水量。如果水量充足,就直接调出 20 个区块返回给 free list。如果水量不足以提供 20个区块,但还足够供应一个以上的区块,就拨出这不足 20 个区块的空间出去。这时候其 pass by reference 的 nobjs 参数将被修改为实际能够供应的区块数。如果内存池连一个区块空间都无法供应,对客端显然无法交待,此时便需利用 malloc()从 heap 中配置内存,为内存池注人活水源头以应付需求。新水量的大小为需求量的两倍,再加上一个随着配置次数增加而愈来愈大的附加量。
内存池实际操练结果:
参考地址:
- STL源码分析目录
- 《STL源码分析》- 侯捷