本文主要参考STL源码剖析,但书中对某些地方写的不是很详细,所以根据个人的理解增加了一些细节的说明,便于回顾。
由于小型区块分配时可能造成内存破碎问题,SGI设计了两级配置器,第一级配置器直接使用malloc和free,第二级配置器则视情况采取不同的策略:当配置的区块超过128Bytes时,调用第一级配置器;当配置区块小于128Bytes时,采用复杂的内存池整理方式,而不再求助于第一级配置器。使用第一级配置器还是同时开放第二级配置器,取决于__USE_MALLOC是否被定义。
#ifdef __USE_MALLOC
...
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc; //令alloc为第一级配置器
#else
...
//令alloc为第二级配置器
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS,0> alloc;
#endif
其中__malloc_alloc就是第一级配置器,__default_alloc_template就是第二级配置器
无论alloc被定义为何种配置器,SGI再为之包装一个接口如下,使配置器的接口能符合STL规格:
template<class T, class Alloc>
class simple_alloc{
public:
static T *allocate(size_t n){
return {0 == n ? 0 : (T*)Alloc::allocate(n * sizeof(T));
}
static T *allocate(void){
return (T*)Alloc::allocate(sizeof(T));
}
static void deallocate(T *p, size_t n){
if(0 != n) Allocate::deallocate(p, n * sizeof(T));
}
static void deallocate(T *p){
Alloc::deallocate(p, sizeof(T));
}
}
可以看出,其内部四个成员函数都是单纯的函数调用。SGI STL容器全都使用这个simple_alloc接口(缺省使用alloc为配置器)。
一二级配置器的关系如下(图摘自STL源码剖析)
接口包装及实际运用方式如下(图摘自STL源码剖析):
第二级配置器的设计思想是:每次配置一大块连续内存,并维护其对应的自由链表(free-list,大小相同的区块串接在一起),下次若内存需求,先从free-list中找到对应大小的区块所在的链表,然后直接从该链表拨出一个区块给客户端使用。客户端释放小额区块时,就由配置器回收到free-lists中。为了方便管理,SGI第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数(实际区块 >= 内存需求),并维护16个free-lists,各自管理大小分别为8, 16, 24, 32, 40, 48, 56, 64, 72, 80,88,96,104,112,120,128 字节的小额区块。每个free-lists是一系列大小相同的区块串成的链表,便于分配和回收。free-lists的节点结构如下:
union obj{
union obj* free_list_link;
char client_data[1] /* the client sees this */
}
插曲:书上对节点如此设计的原因解释如下:不造成内存的浪费(存储额外的链表指针)。
STL源码中使用联合union来设计,并且第二个字段设置为client_data[1],是使用了柔性数组。从第一个字段看,obj可被视为一个指针,指向另一个obj,从第二个地段看,obj可被视为一个大小不定的内存区块(柔性数组),数组长度视分配的内存而定。
柔性数组简单介绍如下:
结构中最后一个元素允许是未知大小的数组(长度为0或者1),这个数组就是柔性数组。但结构中的柔性数组前面必须至少一个其他成员,柔性数组不占用结构体的内存。包含柔数组成员的结构用malloc函数进行内存的动态分配,且分配的内存应该大于结构的大小以适应柔性数组的预期大小,如下一个例子:
Struct Packet
{
int len;
char data[1]; //使用[1]比使用[0]兼容性好
};
对于编译器而言,数组名仅仅是一个符号,它不会占用任何空间,它在结构体中,只是代表了一个偏移量。当使用packet存储数据时,使用
char *tmp = (char*)malloc(sizeof(Packet)+1024)
申请一块连续的内存空间,这块内存空间的长度是Packet的大小加上1024数据的大小。包中的数据存放在data中。
回到正题,这里用柔性数组,主要是用来表示16种不同大小的内存区块(前面提到过的,8,16,24……),在源码中根本没有用到client_data,而obj是在内存配置器内部定义的,用户更是用不上。或许这就是设计者对代码精炼的追求吧。使用union联合体的内存使用方式如下:(union大小为4)
所以使用起来正如书中那样:
第二级配置器部分实现内容如下:
enum {__ALIGN = 8}; //小型区块的上调边界
enum {__MAX_BYTES = 128}; //小型区块的上界
enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; //free-list个数
template <bool threads, int inst>
class __default_alloc_template {
private:
/*将bytes上调至8的倍数
用二进制理解,byte整除align时尾部为0,结果仍为byte;否则尾部肯定有1存在,加上
align - 1之后定会导致第i位(2^i = align)的进位,再进行&操作即可得到8的倍数
*/
static size_t ROUND_UP(size_t bytes) {
return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
}
private:
union obj { //free-list的节点
union obj * free_list_link;
char client_data[1]; /* The client sees this. */
};
private:
//16个free-lists
static obj * __VOLATILE free_list[__NFREELISTS];
//根据区块大小,找到合适的free-list,返回其下标(从0起算)
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;
static char *end_free;
static size_t heap_size;
public:
static void * allocate(size_t n);
static void * deallocatr(void *p, size_t n);
static void * reallocate(void *p, size_t old_sz, size_t new_sz);
};
//以下是static data member的定义与初值设定
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>
__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, };
弄清结点结构之后,先说一下allocate的基本流程,有了大概的了解之后,再进入源码分析。allocate首先判断所需区块的大小,大于128Bytes就调用第一级配置器,小于128Bytes就检查对应的free-list,如果free-list之内有可用的区块,就直接拿来用,否则就将区块大小调至8的倍数,调用refill函数为free-list重新填充空间。
allocate函数如下:
//n must be > 0
static void * allocate(size_t n)
{
obj * __VOLATILE * my_free_list;
obj * __RESTRICT result;
//大于128就调用第一级配置器
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));
return r;
}
//调整free-list,指向拨出区块的下一个区块
*my_free_list = result -> free_list_link;
return (result);
};
refill调用chunk_alloc获取连续的内存空间,然后将这块连续的内存空间编排入相应的free-list中(缺省情况下取得20个区块,若内存池空间不够,获得区块数可能小于20),最后返回这块内存空间的首址。而chunk_alloc负责从内存池中取空间给free-list使用,由于只有这里涉及到了内存池容量的变化,故内存池的起始、结束位置只在chunk_alloc中发生变化。
refill函数如下:
//返回一个大小为n的对象,并且有时候会适当的free-list增加节点
//假设n已经适当上调至8的倍数
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
int nobjs = 20;
//尝试获得nobjs个区块作为free-list的新节点
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_list + FREELIST_INDEX(n);
//在chunk这段连续内存内建立free-list
result = (obj *)chunk; //这一块准备返回给客户端
//将free-list指向新配置的连续内存空间
//allocate中my_free-list为0才进入本函数,故无需存储现在的*my_free-list,直接覆盖即可
*my_free_list = next_obj = (obj *)(chunk + n);
//将free-list的各节点串接起来
for (i = 1; ; i++) {
current_obj = next_obj;
next_obj = (obj *)((char *)next_obj + n); //每一个区块大小为n
if (nobjs - 1 == i) { //最后一块
current_obj -> free_list_link = 0;
break;
} else {
current_obj -> free_list_link = next_obj;
}
}
return(result);
}
chunk_alloc取空间的原则如下:尽量从内存池中取,内存池不够了,才使用free-list中的可用区块。具体分三种情况:
①若当前内存池剩余空间完全满足需求,直接从内存池中拨出去,调整内存池起址即可;
②内存池剩余空间不能完全满足,但足以应对一个(含)以上的区块,一个给客户端使用,剩余的编入free-list;③内存池连一个区块的大小都无法提供,由于内存池分配时大小为8的倍数,每次拨出也是8的倍数,故剩余空间也是8的倍数,可以编入一个区块到相应大小的free-list中。此时内存池全部容量已用完。接下来使用heap分配新的内存(由于内存池中的内存要保持连续,否则按区块大小编排free-list也无从谈起,故在使用heap分配内存之前,内存池中的内存要保证全部用完)。
i.若堆空间也不足了,那么从size起,在每一个free-list中寻找可用区块,直到找到可用区块,将该区块归还给内存池,再调用一次chunk_alloc(这次调用一定进入情况①或者②),从而修改调整内存池、nobjs。若free-lists中都没有一个可用区块,则调用第一级配置器,看out-of-memory机制是否有对策。
ii.否则,直接使用堆分配的内存,此时内存池已有足够的空间,再调用一次chunk_alloc,调整nobjs。
chunk_alloc函数代码如下:
//size此时已适当上调至8的倍数
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; //8的倍数
size_t bytes_left = end_free - start_free; //8的倍数
if (bytes_left >= total_bytes) { //情况1
//内存池剩余空间完全满足需求量
result = start_free;
start_free += total_bytes;
return(result);
} else if (bytes_left >= size) { //情况2
//虽不足以完全满足,但足够供应一个(含)以上的区块
//从start_free开始一共total_bytes分配出去,其中前size个bytes给客户端,剩余的给free-list
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) {
obj * __VOLATILE * my_free_list =
free_list + FREELIST_INDEX(bytes_left); //找到大小相同区块所在的free-list
((obj *)start_free) -> free_list_link = *my_free_list; //将内存池剩余空间编入free-list中
*my_free_list = (obj *)start_free;
}
//此时内存池的空间已用完
//配置heap空间,用来补充内存池
start_free = (char *)malloc(bytes_to_get);
if (0 == start_free) {
//heap空间不足,malloc失败
int i;
obj * __VOLATILE * my_free_list, *p;
//转而从free-lists中找寻可用的区块(其大小够用)
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;
//再次从内存池中索要连续空间来满足客户端需求
return(chunk_alloc(size, nobjs)); //由于此时i >= size,故此次只会进入情况1/2
}
}
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;
//再次尝试分配
return(chunk_alloc(size, nobjs));
}
}
以上就是SGI 空间配置器的内存分配机制。