本文主要参考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 */ }
插曲:书上对节点如此设计的原因解释如下:不造成内存的浪费(存储额外的链表指针)。但其实采用如下设计,也没有浪费内存:
struct obj{ union obj * free_list_link; }这种方法对内存的使用具体如下:
而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函数如下:
//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)); } }
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。
以上就是SGI 空间配置器的内存分配机制。