第二级配置器 : --->_default_alloc_template:
第二级配置器多了一些机制,避免太多小额区块造成内存的碎片,小额区块带来的不仅仅是内存碎片,配置时额外的负担也是一个大问题。
如下图:
第二级配置器的做法是如果区块够大,超过128bytes时,就移交第一级配置器处理。
当区块小于128bytes时,则以内存池(memory pool)管理,这种方法成为次层配置。
每次配置一大块内存,并维护对应的自由链表(free_list),下次若再有相同大小的内存需求,就直接从free_lists中拨出。如果客户端释还小额区块,则有配置器回收到free_lists中。
为了方便管理,SGI第二级配置器会主动将任何小额区块上调至8的倍数(例如客户端要求30bytes,那么就自动调整为32bytes),并维护16个free_lists,各自管理大小分别为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128bytes的小额区块。
Free_lists的节点结构如下:
union obj
{
union obj * free_list_link;
char client_data[1];
}
上述obj所用的union,由于union之故,从其第一阻断观之,obj可被视为一个指针,指向相同形式的另一个obbj,从其第二字段观之,obj可被视为一个指针,指向实际区块。(达到一物两用,不会为了维护链表所必须的指针而造成内存的另一种浪费,不会造成额外负担)
如下图:
free_lists的结构如下:
第二级配置器的部分实现内容:
enum {_ALIGN = 8}; //小型区块的上调边界
enum {_MAX_BYTES = 128}; //小型区块的上限
enum {_NFREELISTS = _MAX_BYTES/_ALIGN}; //free_lists个数
//以下是第二级配置器
//注意,无”template型别参数”,且第二参数完全没派上用场
//第一参数用于多线程环境下,在这里不讨论多线程环境
template <bool threads, int inst>
class __default_alloc_template
{
private:
//ROUND_UP()将bytes提升至8的倍数
static size_t ROUND_UP(size_t bytes)
{
return ((bytes + __ALIGN - 1) & ~(__ALIGN - 1));
}
private:
union obj //free_lists节点构造
{
union obj * free_list_link;
char client_data[1];
};
private:
//16个free_lists
static obj * volatile free_list[__NFREELISTS];
//根据区块大小,决定使用第n号free-list, n从1算
static size_t FREELIST_INDEX(size_t bytes)
{
return (((bytes) + __ALIGN - 1) / __ALIGN - 1);
}
//返回一个大小为n的对象,并可能加入大小为n的其他区块到free list
static void *refill(size_t n);
//配置一大块空间,可容纳nobjs个大小为"size"的区块
//如果配置nobjs个区块有所不便,nobjs可能会降低
static char* chunk_alloc(size_t size, int& nobjs);
// chunk allocation state
static char *start_free; //内存池起始位置,只在chunk_alloc()中变化
static char *end_free; //内存池结束位置,只在chunk_alloc()中变化
static size_t heap_size;
public:
static void * allocate(size_t n);
static void deallocate(void *p, size_t n);
static void * reallocate(void *p, size_t old_sz, size_t new_sz);
};
template <bool threads, int inst>
char *__default_alloc_template<threads, inst>::start_free = 0;
template <bool threads, int inst>
char *__default_alloc_template<threads, inst>::end_free = 0;
template <bool threads, int inst>
size_t __default_alloc_template<threads, inst>::heap_size = 0;
template <bool threads, int inst>
typename __default_alloc_template<threads, inst>::obj * volatile
__default_alloc_template<threads, inst>::free_list[__NFREELISTS] =
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
(1)空间配置函数---->allocate()
身为一个配置器,_default_alloc_template拥有配置器的标准接口函数allocate(),此函数首先判断区块大小,大于128bytes就调用第一级配置器,小于128bytes就检查对应的free list。如果free list之内有可用的区块,就直接拿来用,如果没有可用的区块,就将区块大小上调至8倍数边界,然后调用refill(),准备为free list重新填充空间。
代码如下:
static void * allocate(size_t n)
{
obj * volatile *my_free_list;
obj * result;
if(n > (size_t) _MAX_BYTES)
{
return (malloc_alloc::allocate(n))
}
//寻找16个free lists中适当的一个
my_free_list = free_list + FREELIST_INDEX(n);
result = *my_free_list;
if(result == 0)
{
//没找到合适的free list,准备重新填充free list
void *r=refill(ROUND_UP(n));
//refill为free list重新填充空间,新的空间取自内存池(由chunk_alloc()完成)
return r;
}
//调整free list
*my_free_list = result->free_list_link;
return (result);
}
图示如下:
(2)空间释放函数---->deallocate()
身为一个配置器,_default_alloc_template拥有配置器标准接口函数deallocate()。该函数首先判断区块大小,大雨128bytes就调用第一级配置器,小于128bytes就找出对应的free list,将区块回收。
代码如下:
static void deallocate(void *p,size_t n) //p不可以是0
{
obj * q=(obj*)p;
obj * volatile * my_free_list;
//大于128bytes就调用第一级配置器
if(n > (size_t)_MAX_BYTES)
{
malloc_alloc::deallocate(p,n);
return ;
}
//寻找对应的free list
my_free_list = free_list + FREELIST_INDEX(n);
//调整free list,回收区块
q->free_list_link = *my_free_list;
*my_free_list = q;
}
图示如下:
(3)重新填充free lists---->refill()
前面allocate()中说过,当他发现free list中没有可用区块了时,就调用refill(),准备为free list重新填充空间。新的空间将取自内存池(经由chunk_alloc()完成),缺省取得20个新节点(新区块),但万一内存池空间不够,获得的节点数(区块数)可能小于20。
refill()函数做了两件事:
(1)利用chunk_alloc申请空间
(2)将申请的空间第一块传给客端,其他块串起来。
代码如下:
//返回一个大小为n的对象,并且有时候会为适当的free list增加节点
//假设n已经适当调至8的倍数
template <bool thread,int inst>
void* _default_alloc_template<thread,inst>::refill(size_t n)
{
int nobjs = 20;
//调用chunk_alloc(),尝试取得nobjs个区块作为free_list的新节点
//注意参数nobjs是pass by reference
char * chunk = chunk_alloc(n, nobjs);
obj * volatile * my_free_list;
obj * result;
obj * current_obj, * next_obj;
int i;
//如果只获得一个区块,这个区块就分配给调用者,free_list无新节点
if(1 == nobjs)
{
return (chunk);
}
//否则准备调整free list,纳入新节点
//my_free_list指向自己在free_lists中的位置
my_free_list = free_list + FREELIST_INDEX(n);
//以下在chunk空间内建立free list
result = (obj *)chunk; //这一块准备返回给客端
//以下导引free_list指向新配置的空间(取自内存池)
*my_free_list = next_obj = (obj *)(chunk + n);
//以下将free_list的各节点串接起来
for(i = 1; ; i++)
{
//从1开始,因为第0个将返回给客端
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);
}
注:如果chunk_alloc()申请不到空间,chunk_alloc()会抛出异常,这个异常是由一级空间配置器oom机制抛出的。
(4)内存池(memory pool)---->chunk_alloc()
从内存池中取空间给free list使用,是chunk_alloc()的工作。
代码如下:
//假设size已经适当的上调至8的倍数
//注意参数nobjs是pass by reference
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
{
size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
//以下试着让内存池的残余零头还有利用价值
if(bytes_left > 0)
{
//内存池还有一些零头,先配置给适当的free_list
//首先寻找适当的free_list
obj * volatile * my_free_list = free_list + FREELIST_INDEX(bytes_left);
//调整free_list,将内存池中的残馀空间编入
((obj*)start_free)->free_list_link = *my_free_list;
*my_free_list = (obj*)start_free;
}
//配置heap空间,用来补充内存池
start_free = (char*)malloc(bytes_to_get);
//heap空间不足时,malloc()失败
if(0 == start_free)
{
int i;
obj *volatile * my_free_list, *p;
//搜索适当的free_list
//适当即"尚有未用区块。且区块足够大"
for(i = size; i <= __MAX_BYTES; i += __ALIGN)
{
my_free_list = free_list + FREELIST_INDEX(i);
p = *my_free_list;
if(0 != p)
{
//free_list 内尚有未用区块
//调整free_list以释放未用区块
*my_free_list = p->free_list_link;
start_free = (char*)p;
end_free = start_free + i;
//递归调用自己,为了修正nobjs
return (chunk_alloc(size, nobjs));
//注意:任何残余零头终将被编入适当的free list中备用
}
}
end_free = 0;
//如果出现意外(山穷水尽,到处都没有内存可用了)
//调用第一级配置器,看看out-of-memory机制能否尽点力
start_free = (char*)malloc_alloc::allocate(bytes_to_get);
//这会导致抛出异常,或内存不足的情况获得改善
}
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
//递归调用自己,为了修正nobjs
return (chunk_alloc(size, nobjs));
}
上述chunk_alloc()函数以end_free - start_free来判断内存池的水量。如果水量充足,就直接调出20个区块返回给free list;如果水量不足以提供20个区块,但还是足够供应一个以上的区块,就拨出这不足20个区块的空间出去,同时nobjs参数将被修改为实际能够供应的区块数;如果内存池连一个区块空间都无法供应,对客端显然无法交代,此时便需要利用malloc()从heap中配置内存,为内存池注入活水源头以应付需求。新水量的大小为需求量的2倍,再加上一个随着配置次数增加而愈来愈大的附加量。
举例说明:
假设程序一开始,客端就调用chunk_alloc(32,20),于是malloc()配置40个32bytes区块,其中第一个交出,另19个交给free_list[3]维护,余20个留给内存池,接下来客端调用chunk_alloc(64,20),此时free_list[7]空空如也,必须向内存池要求支持,内存池只够供应(32*20)/64=10个64bytes区块,就把这10个区块返回,第一个交给客端,余9个由free_list[7]维护,此时内存池全空。接下来调用chunk_alloc(96,20),此时free_list[11]空空如也,必须向内存池要求支持,而内存池此时也是空的,于是以malloc()配置40+n(附加量)个96bytes区块,其中第一个交出,另19个交给free list[11]维护,余20+n(附加量)个区块留给内存池。
万一到了山穷水尽的一步,整个system heap空间都不够了(以至于无法为内存池注入活水源头),malloc()行动失败,chunk_alloc()就四处寻找有无”尚有未用区块,且区块足够大”的free list,找到了就挖出一块交出,找不到就调用第一级配置器。第一级配置器其实也是用malloc()来配置内存的,但它由out_of_memory处理机制(类似于new_handler机制),或许有机会释放其他的内存拿来此处使用,如果可以,就成功,否则会发出bad_alloc异常。
图示如下: