了解内存配置后的对象构造行为和内存释放前的对象析构行为后(参见博文:http://blog.csdn.net/peng_shakalaka/article/details/75452224(构造和析构),我们来学习对象构造前的空间配置和对象析构后的空间释放
对象构造前的空间配置和对象析构后的空间释放,SGI的设计思想如下:
*向system head 要求空间。
*考虑多线程状态。
*考虑内存不足时的应变状态。
*考虑多“小型区块”可能造成的内存碎片(fragment)问题。
//对象构造前的空间配置和对象析构后的空间释放,由头文件
<stl_alloc.h>
//负责
因此考虑到小型区块所可能造成的内存碎片问题,SGI设计了双层配置器,第一层配置器直接使用malloc() 和 free(),第二层配置器则视情况采用不同的策略:配置区块超过128bytes时,视之为“足够大”,调用第一级适配器;当配置区块小于128bytes时,视为“过小”,采用复杂的mermory pool整理方式。整个设究竟只开放第一级适配器,或者是同时开放第二级适配器,取决于宏定义 __USE_MALLOC是否被定义。
# ifdef __USE_MALLOC
...
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc; //alloc为第一级适配器
#else
...
typedef__default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc; //alloc为第二级适配器
#endif
其中 __malloc_alloc_template就是第一级适配器,__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) Alloc::deallocate(p, n * sizeof (T));
}
static void deallocate(T* p)
{
Alloc::deallocate(p, sizeof (T));
}
};
其中内部的四个成员函数都是简单的转调用,调用传递给配置器的成员函数。SGI STL容器都使用这个simple_alloc接口。
首先我们先来看看第一级适配器__malloc_alloc_template
先上代码:
#if 0
# include<new>
# define __THROW_BAD_ALLOC throw_bad_alloc;
#elif !define(__THROW_BAD_ALLOC)
# include<iostream.h>
# define __THROW_BAD_ALLOC cerr<< "out of memory"<<endl;
# exit(1);
#elif
template<int inst>
class __malloc_alloc_template{
private:
//以下函数处理内存不足的情况
//oom :out of memory.
static void *oom_malloc(size_t);
static void *oom_malloc(void *, size_t);
ststic void (* __malloc_alloc_oom_handler)();
public:
static void * allocate(size_t n)
{
//第一级适配器直接使用malloc()
void *result = malloc(b);
//当以下条件无法满足时,改用oom_malloc()
if(0 == result)
result = oom_malloc(n) ;
return result;
}
static void deallocate(void *p, size_t /* n */)
{
//第一级适配器直接使用free()
free(p);
}
static void * reallocate(void* p, size_t /* old_sz, size_t new_sz)
{
//第一级适配器直接使用realloc()
void* result = realloc(p, new_sz)
//以下条件无法满足时,改用oom_realloc()
if(result == 0)
result = oom_realloc(p, new_sz);
return result;
}
//下面是仿真C++的set_new_handler()。也就是你自己指定的out-of-memory handler
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
//初值为0,待客端设定
template<int inst>
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
template<int inst>
void* __malloc_alloc_template<inst>::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>::oom_realloc(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 = realloc(p, n); //再次尝试配置内存
if(result)
return result;
}
}
//直接将inst设置为0
typedef __malloc_alloc_template<0> malloc_alloc;
由以上代码我们可以看出:第一级适配器以malloc(), free(), realloc()等C函数执行实际的内存配置、释放、重配置操作,并且实现了类似C++的new_handler(详情见《Effective C++》2e,条款7)。它不能直接使用C++ new_handler机制,因为它并非使用::operator new来配置内存。
为什么SGI以malloc而不是::operator new来配置内存呢?可能是C++并未提供相应于realloc()的配置内存操作。所以SGI也就不能直接使用C++的set_new_handler(),必须仿真一个类似于set_new_handler().
另外要注意的是SGI第一级适配器的allocate()和realloc()都在调用malloc() 和realloc()失败后,改调用oom_malloc()和oom_realloc()。
后两者内部都有循环,不断调用“内存不足处理例程(由客户端设定)”,期望在某次调用之后,获得足够的内存。但如果内存处理例程没有被客户端设定,则oom_malloc()和oom_realloc()便毫不客气的返回__THROW_BAD_ALLOC,丢出bad_alloc异常信息,或利用exit(1)直接终止程序。
接下来我们来看第二级适配器 __defaul-alloc_template
第二级适配器多了一些机制,可以避免内存碎片问题,配置时的额外负担也是一个大问题。额外负担是永远无法避免,毕竟系统要靠这些多出来的空间来管理内存。但区块越小,额外负担占得比例就越大,显的越浪费。
第二级适配器的思想是:如果区块足够大,超过128bytes时,就已交给第一级适配器。当区块小于小于128bytes时,则以内存池(memory pool)管理:每次配置一大块内存,并维护对应之自由链表(free-list)。下次再有相同大小的内存需求时,就直接从free-list中取出。如果释还小额,就由适配器收回到free-list中。另外,为了方便管理。SGI第二级适配器会主动将任何的小额内存需求上调至8的倍数(如果客户端需要30bytes则上调至32bytes),并维护16个fre-list,各自管理大小为:8,16,2432,40,48,56,64,72,80,88,96,104,112,120,128bytes的小额区间。free-list节点结构为:
union obj{
union obj* free_list_link;
char client_data[1] ;
};
为什么要用union呢?因为使用了union之后达到了一物二用的效果(union最重要的特性)。我们要努力达到节省内存的效果。
下面是第二级适配器的部分实现内容:
enum {__ALLGN = 8}; //小型区块上调边界
enum {__MAX_BYTES = 128}; //小型区块上限
enum {__NFREELISTS = __MAX_BYTES / __ALLGN}; //free-list个数
template<bool threads, int inst>
class __default_alloc_template{
private:
//将bytes上调至8的倍数
static size_t ROUND_UP(size_t bytes){
return (((bytes)+__ALLGN-1) & ~(__ALLGN-1));
}
private:
//free-list节点构造
union obj{
union obj* free_list_link;
char client_data[1] ;
};
private:
//16个free-list
static obj * volatile free_list[__NFREELISTS];
//根据区块的大小,决定使用n号free-list。从0开始
static size_t FREELIST_INDEX(size_t bytes){
return (((bytes) + __ALLGN-1) / __ALLGN - 1);
}
//返回一个大小为n的对象,并尽可能的加入大小为n的其他区块到free-list
static void * refill(size_t n){
//配置一块空间,可容纳nobjs个大小为“size”的区块
//如果不能配置nobjs个区块,则nobjs数可能会降低
static char* chunk_alloc(soze_t size, int &nobjs);
//Chunk 配置状态
static char* start_free; //内存池起始地址
static char* end_free; //内存池结束地址
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);
}
};
//static data member定义于初值设定
template<bool threads, int inst>
char *__default_alloc_template<threads, inst>::start_free = ;
template<bool threads, int inst>
char *__default_alloc_template<threads, inst>::end_free = 0;
template<bool threads, int inst>
char *__default_alloc_template<threads, inst>::head_size = 0;
template<bool threads, int inst>
char *__default_alloc_template<threads, inst>::obj * volatile
__default_alloc_template<threads, inst>::free_list[NREELISTS] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
首先我们来看空间配置函数allocate():此函数先判断区块大小。大于
128bytes,就调用第一级适配器。小于128bytes的话,就先检查对应的free-list。如果free-list用对应的区块,就直接拿来用如果没有区块的话,就将区块大小上调至8的倍数,然后调用refill(),准备为free-list重新填充空间。
static void * allocate(size_t n)
{
obj* volatile * my_free_list;
obj* result;
//大于128bytes就调用第一级适配器
if(n > (size_t) __MAX_BYTES){
return (malloc_alloc::allocate(n));
}
//寻找16个free_list中适当的一个
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 = reslut->free_list_link;
};
空间释放函数deallocate():同样,首先判断区块大小,大于128bytes就调用第一级配置器,小于128bytes就调用与之对应的free list,将区块收回。
static void deallocate(void *p, size_t n)
{
obj *q = (obj *)p;
obj * volatile * my_free_list;
//大于128bytes就调用第一级适配器
if(n > (size_t)__MAX_BYTES)
{
malloc_alloc::deallocate;
return;
}
//寻找与之对应的free list
my_free_list = free_list + FREELIST_INDEX(n);
q->free_list_link = *my_free_list;
*my_free_list = q;
}
重新填充free lists:之前说过allocate()。当发现free list中没有可用空间时,调用refill(),准备为free list重新填充空间。新空间取自内存池(chunk_alloc()完成)。一般会取到20个新节点,但如果内存池内存不足,可能会小于20个。
template<bool threads, int inst>
void* default_alloc_temolate<threads, inst>::refill(size_t n)
{
int nobjs = 20;
//调用chunk_alloc(),尝试获得nobjs个区块作为free list的新节点
//(nobjspass by referrence)
char * chunk = chunk_alloc(n, nobjs);
objs* volatile * my_free_list;
objs * result;
objs * current_obj, * next_obj;
int i;
//如果只得到一个新节点,直接分配给调用者
if(1 == objs)
return(chunk);
//否则调整free list,纳入新节点
my_free_list = free_list + FREELIST_INDEX(n);
result = (obj*)chunk; //返回给调用者
my_free_list = next_obj = (obj*)(chunk+n);
//将free list 各个节点串联起来
for(i = 1; ; ++i)
{
current_obj = nex_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()函数负责从内存池中配置区块。但内存池是什么东西呢?程序在运行中如果malloc()、new等等需要从head中申请空间,每次这样操作比较麻烦,所以我们在程序运行之前,提前申请一块区间,当程序进行malloc(),new()时直接从内存池中获得空间,不需要到head中,这样可以提高效率。SGI空间适配器就是依靠内存池获得区块,当然,内存池中的空间是从head中获得。
下面代码来源:http://blog.csdn.net/u013074465/article/details/44560541
// 返回一个大小为 n 的对象,并且有时候会为适当的 free list 增加节点
// 假设 n 已经适当上调至 8 的倍数
/* We hold the allocation lock. */
template <bool threads, int inst>
void* __default_alloc_template<threads, 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_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);
}
内存池
从内存池中取空间给 free list使用,是chunk_alloc()的工作:
[cpp] view plain copy
// 假设 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);
// 以下试着让内存池中的残余零头还有利用价值(零头也应该是 8 的倍数)
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);
if (0 == start_free) {
// heap空间不足,malloc()失败
int i;
obj * __VOLATILE * my_free_list, *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.
// 试着检视我们手上拥有的东西。这不会造成伤害。我们不打算尝试配置
// 较小的区块,因为那在多进程(multi-process)机器上容易导致灾难
// 以下搜寻适当的 free list
// 所谓适当是指"尚有未用区块,且区块够大"之 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);
// 这会导致抛出异常(exception),或内存不足的情况获得改善
}
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
// 递归调用自己,为了修正 nobjs
return(chunk_alloc(size, nobjs));
}
}
由上面的代码我们可以看出,从内存池取空间给free list分为以下三种情况:
1)内存池空间完全满足需求,分配20块区间给free list。
2)内存池空间不足,不能完全满足需求量,但可供应至少一个区块。结果是有多少给多少。
3)连一个区块的空间都没有。chunk_alloc()函数进行的操作是,把内存池仅有的空间先给free list。然后内存池向head中索要空间,补充内存池,补充量的大小是需求量的2倍。但是万一整个head都没有内存了,chunk_alloc()函数便会从free list中寻找一块“未用且足够大的区块”,如果free list中没有的话,便调用第一级适配器的out-of-memory处理机制,看有没有其他内存可以释放,若果可以,成功,否则发出bad_alloc异常。