SGI STL 的配置器与众不同,也与标准规范不同,其名称是 a1loc 而非 allocator,而且不接受任何参数。
第一级配置器
- 以 malloc(),free(), realloc() 等 C 函数执行实际的内存配置、释放、重配置操作。
- 实现出类似 C++ new-handler 的机制。所谓 C++ new handler 机制是,你可以要求系统在内存配置需求无法被满足时,调用一个你所指定的函数。
- 第一级配置器内存分配失败一般是由于内存不足 out-of-memory(oom),处理异常时,首先用户自己设计异常处理例程,若用户没有定义,仅仅是打印错误信息并强制退出。
- allocate() 和 realloc() 都是在调用 malloc() 和 realloc() 不成功后,改调用 oom_malloc() 和 oom_realloc()。后两者都有内循环,不断调用“内存不足处理例程”,期望在某次调用之后,获得足够的内存而圆满完成任务。
__malloc_alloc_template 定义
template <int __inst>
class __malloc_alloc_template {
private:
//@ 以下函数将用来处理内存不足的情况
static void* _S_oom_malloc(size_t);
static void* _S_oom_realloc(void*, size_t);
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
static void (* __malloc_alloc_oom_handler)();
#endif
public:
//@ 第一级配置器直接调用 malloc()
static void* allocate(size_t __n)
{
void* __result = malloc(__n);
// 以下无法满足需求时,改用 _S_oom_malloc()
if (0 == __result) __result = _S_oom_malloc(__n);
return __result;
}
//@ 第一级配置器直接调用 free()
static void deallocate(void* __p, size_t /* __n */)
{
free(__p);
}
//@ 第一级配置器直接调用 realloc()
static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
{
void* __result = realloc(__p, __new_sz);
//@ 以下无法满足需求时,改用 _S_oom_realloc()
if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
return __result;
}
//@ 以下仿照 C++ 的 set_new_handler(),可以通过它指定自己的 out-of-memory handler
//@ 为什么不使用 C++ new-handler 机制,因为第一级配置器并没有 ::operator new 来配置内存
static void (* __set_malloc_handler(void (*__f)()))()
{
void (* __old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = __f;
return(__old);
}
};
// malloc_alloc out-of-memory handling
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
//@ 初值为0,由客户自行设定
template <int __inst>
void (* __malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;
#endif
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);
}
}
template <int __inst>
void* __malloc_alloc_template<__inst>::_S_oom_realloc(void* __p, size_t __n)
{
void (* __my_malloc_handler)();
void* __result;
//@ 给一个已经分配了地址的指针重新分配空间,参数 __p 为原有的空间地址,__n 是重新申请的地址长度
for (;;) {
//@ 当 "内存不足处理例程" 并未被客户设定,便调用 __THROW_BAD_ALLOC,丢出 bad_alloc 异常信息
__my_malloc_handler = __malloc_alloc_oom_handler;
if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }
(*__my_malloc_handler)(); //@ 调用处理例程,企图释放内存
__result = realloc(__p, __n); //@ 再次尝试配置内存,扩大内存大小
if (__result) return(__result);
}
}
//@ 直接将参数 __inst 指定为0
typedef __malloc_alloc_template<0> malloc_alloc;
第二级配置器
- 如果区块足够大,超过 128 bytes,就交给第一级配置器处理。
- 当区块小于 128 bytes,则使用 memory bool 管理,这种方法又称为 sub-allocation:
- 每次配置一大块内存,并维护 free-list。
- 下一次若有相同大小的内存需求,则直接从 free-list 中拨出。
- 如果客户端释还小区块,就由配置器回收到 free-list 中。
- 为了方便管理,SGI 第二级配置器会主动将任何小的区块的内存需求量上调到 8 的倍数,例如客户端需要 30 bytes,就会自动调整到 32 bytes。第二级配置器维护了16个 free-list,各自管理大小分别为:8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128 bytes 大小的区块。
free -list 的结构:
//@ free-list 的节点结构,降低维护链表 list 带来的额外负担
union _Obj {
union _Obj* _M_free_list_link; //@ 指向相同形式的另一个 _Obj
char _M_client_data[1]; //@ 指向实际的区块
};
小区块常量:
enum {_ALIGN = 8}; //@ 小区块的上调边界
enum {_MAX_BYTES = 128}; //@ 小区块的上限
enum {_NFREELISTS = 16}; //@ _MAX_BYTES/_ALIGN free-list 的个数
__default_alloc_template 定义
//@ 无 “template 类型参数”,且第二参数也没有用,其中第一参数用于多线程环境下
template <bool threads, int inst>
class __default_alloc_template {
...
//@ 将任何小额区块的内存需求量上调至 8 的倍数
static size_t
_S_round_up(size_t __bytes)
{ return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1)); }
...
//@ 维护 16 个空闲链表(free list),初始化为0,即每个链表中都没有空闲数据块
static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS];
//@ 根据申请数据块大小找到相应空闲链表的下标,n 从 0 起算
static size_t _S_freelist_index(size_t __bytes) {
return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1);
}
//@ 传回一个大小为 n 的对象,并可能加入到大小为 n 的其它区块到 free-list
static void* _S_refill(size_t __n);
//@ 配置一个大块空间,可容纳 __nobjs 个大小为 __size 的区块
//@ 如果配置 __nobjs 个区块有所不便,__nobjs 会降低
static char* _S_chunk_alloc(size_t __size, int& __nobjs);
// Chunk allocation state.
static char* _S_start_free; //@ 内存池起始位置。只在 _S_chunk_alloc() 中变化
static char* _S_end_free; //@ 内存池结束位置。只在 _S_chunk_alloc() 中变化
static size_t _S_heap_size; //@ 内存池大小
};
空间配置函数 allocate()
空间配置函数 allocate() 的具体实现步骤如下:
- 若用户申请的内存大于 128bytes,则调用第一级配置器分配空间。
- 若小于 128bytes 检查对应的自由链表 free_list,如果自由链表存在可用的区块,则直接使用,若不存在,则调用填充函数 refill() 为自由链表重新填充空间。
//@ 申请大小为n的数据块,返回该数据块的起始地址
static void* allocate(size_t __n)
{
void* __ret = 0;
//@ 如果需求区块大于 128 bytes,就转调用第一级配置
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n); //@ 第一级配置器
}
else {
//@ 根据申请空间的大小寻找相应的空闲链表(16个空闲链表中的一个)
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
# ifndef _NOTHREADS
/*REFERENCED*/
_Lock __lock_instance;
# endif
_Obj* __RESTRICT __result = *__my_free_list;
//@ 空闲链表没有可用数据块,就将区块大小先调整至 8 倍数边界,然后调用 _S_refill() 重新填充
if (__result == 0)
__ret = _S_refill(_S_round_up(__n));
else {
//@ 如果空闲链表中有空闲数据块,则取出一个,并把空闲链表的指针指向下一个数据块
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
}
}
return __ret;
};
空间释放函数 deallocate()
首先判断区块的大小,大于 128bytes 直接调用第一级配置器,若小于 128bytes,则找出相应的自由链表free_list,将其回收。
//@ 空间释放函数 deallocate()
static void deallocate(void* __p, size_t __n)
{
if (__n > (size_t) _MAX_BYTES)
malloc_alloc::deallocate(__p, __n); //@ 大于 128 bytes,就调用第一级配置器的释放
else {
//@ 否则将空间回收到相应空闲链表(由释放块的大小决定)中
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
_Obj* __q = (_Obj*)__p;
// acquire lock
# ifndef _NOTHREADS
/*REFERENCED*/
_Lock __lock_instance;
# endif /* _NOTHREADS */
__q -> _M_free_list_link = *__my_free_list; //@ 调整空闲链表,回收数据块
*__my_free_list = __q;
// lock is released here
}
}
重新填充函数 refill()
重新填充函数 refill() 是在自由链表不存在可用的区块时被调用。
默认是为自由链表申请20个节点,第1个返回给客户端,剩下19个留给自由链表管理。
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20;
//@ 调用 _S_chunk_alloc(),缺省取 20 个区块作为 free list 的新节点
//@ __nobjs 是引用传参
char* __chunk = _S_chunk_alloc(__n, __nobjs);
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
//@ 如果只获得一个数据块,那么这个数据块就直接分给调用者,空闲链表中不会增加新节点
if (1 == __nobjs) return(__chunk);
//@ 否则根据申请数据块的大小找到相应空闲链表
__my_free_list = _S_free_list + _S_freelist_index(__n);
/* Build free list in chunk */
__result = (_Obj*)__chunk;
//@ 第0个数据块给调用者,地址访问即 chunk~chunk + n - 1
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
//@ 将free-list 的各个节点串联起来
//@ 从1 开始,因为第0个传回给客户端
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);
}
内存池管理 chunk_alloc()
chunk_alloc 函数具体实现步骤如下:
- 内存池剩余空间完全满足20个区块的需求量,则直接获取对应大小的空间。
- 内存池剩余空间不能完全满足20个区块的需求量,但是足够供应一个及以上的区块,则获取满足条件的区块个数的空间。
- 内存池剩余空间不能满足一个区块的大小,则:
- 首先判断内存池中是否有残余零头内存空间,如果有则进行回收,将其编入free_list。
- 然后向 heap 申请空间,补充内存池。heap 有足够的空间,空间分配成功。heap空间不足,即 malloc() 调用失败。则
- 查找 free_list 中尚有未用区块,调整以进行释放,将其编入内存池。然后递归调用 chunk_alloc 函数从内存池取空间供 free_list 备用。
- 搜寻 free_list 释放空间也未能解决问题,这时候调用第一级配置器,利用 out-of-memory 机制尝试解决内存不足问题。
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; //@ 计算内存池剩余空间
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);
//@ 尝试让 memory pool 中的残余空间利用起来
//@ 内存池的剩余空间分给合适的空闲链表
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;
}
//@ 配置 heap 空间,用来补充内存池
_S_start_free = (char*)malloc(__bytes_to_get);
//@ heap 空间不足,malloc() 失败
if (0 == _S_start_free) {//@ 若堆空间不足
size_t __i;
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __p;
// Try to make do with what we have. That can't
// hurt. We do not try smaller requests, since that tends
// to result in disaster on multi-process machines.
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));
// Any leftover piece will eventually make it to the
// right free list.
}
}
_S_end_free = 0; // In case of exception.
_S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get); //@ 调用第一级配置器
// This should either throw an
// exception or remedy the situation. Thus we assume it succeeded.
}
_S_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;
return(_S_chunk_alloc(__size, __nobjs)); //@ 递归调用自己
}
}
总结
- 第一级配置器:
__malloc_alloc_template
,第二级配置器:__default_alloc_template
。GCC 默认使用第二级配置器,其作用是避免太多小额区块造成内存的碎片 - 第一级配置器直接使用 malloc()、free()、realloc() 等 C 函数,实现出类似 C++ new-handler 的机制。
- 第二级配置器根据实际情况采取不同的策略:
- 当配置区块超过 128 bytes,视为足够大,呼叫第一级配置器。
- 当配置区块小于 128 bytes,视为比较小,则不求助第一级配置器。
- 空间配置:allocate,空间释放:deallocate。
- 重新装填:refill,内存池管理:chunk_alloc。