空间配置器
主要分三个文件实现,我们已经介绍过第一个文件了(对象的构造和析构 http://blog.csdn.net/hj605635529/article/details/70238270),
现在我们来介绍第二个文件 stl_alloc.h 也就是空间配置器的精华所在 文件中定义了一、二两级配置器 其设计思想为:
向 system heap 要求空间;
考虑多线程 (multi-threads) 状态;
考虑内存不足时的应变措施;
考虑过多 “小型区块” 可能造成的内存碎片 (fragment) 问题。
内存的配置和释放
在内存配置方面,STL分为两级配置器,当请求的内存大于128b的时候调用第一级配置器,当请求的内存小于等于128b的时候调用第二级配置器。先来看看下面这张表,大概就能知道第一级和第二级配置器主要干了些什么,其他的一些细节如内存池是怎么工作的,下面会给出具体解释。
第一级配置器
//一级配置器
template <int __inst>
class __malloc_alloc_template {
private:
//调用malloc函数不成功后调用
static void* _S_oom_malloc(size_t);
//调用realloc函数不成功后调用
static void* _S_oom_realloc(void*, size_t);
//类似于C++的set_new_handle错误处理函数一样,如果不设置,在内存不足时,返回THROW_BAD_ALLOC
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
static void (* __malloc_alloc_oom_handler)();
#endif
public:
//第一级配置器分配内存
static void* allocate(size_t __n)
{
void* __result = malloc(__n); //直接调用malloc来分配内存
if (0 == __result) __result = _S_oom_malloc(__n); //如果分配失败,则调用_S_oom_malloc()函数
return __result;
}
//第一级配置器直接调用free释放内存。
static void deallocate(void* __p, size_t /* __n */)
{
free(__p);
}
//直接调用reallloc来分配内存
static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
{
void* __result = realloc(__p, __new_sz);
if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);//如果realloc分配不成功,调用_S_oom_realloc()
return __result;
}
//异常处理函数,内存分配失败后的处理
// 函数的返回值是一个函数指针,参数也是一个函数指针,这两个函数指针的类型都是返回值为void,参数为void的函数
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
// 以下是针对内存分配失败后的处理
//首先,将__malloc_alloc_oom_handler的默认值设为0
template <int __inst>
void (* __malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;
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;
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);// 如果分配成功则返回指针
}
}
//不断的尝试释放和申请是因为用户不知道还需要释放多少内存来满足分配需求,只能逐步的释放申请
上面并非死循环,它有两个退出条件:1.用户没有定义相应的内存不足处理例程,即没有通过释放内存来解决现有内存分配不足的问题,结果抛出异常,直接退出(宏定义);2.在用户定义了释放内存程序例程后,成功分配指定大小内存,返回指向该内存区域的首地址。
第二级配置器
当用户申请一个内存后,第二级配置器首先将这个空间大小上调到8的倍数,然后找到对应的free_list链表,
enum { _ALIGN = 8 }; //小型区块的上调边界
enum { _MAX_BYTES = 128 }; //小型区块的上限
enum { _NFREELISTS = 16 }; // _MAX_BYTES/_ALIGN //free-list 编号数
//配置内存后,维护对应内存块的空闲链表节点结构
union _Obj {
union _Obj* _M_free_list_link; //空闲链表
char _M_client_data[1]; /* The client sees this. 用户使用的*/
};
如此巧妙地运用union来管理节点,如果还没有被分配,那么free_list_link有效,如果已经分配给用户,那么client_data[1]有效。不会造成多余的浪费!
enum {__ALIGN = 8}; //小型区块的上调边界
enum {__MAX_BYTES = 128}; //小型区块的上限
enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; //free-lists个数
//第一参数用于多线程,这里不做讨论。
template <bool threads, int inst>
class __default_alloc_template
{
private:
// 此函数将bytes的边界上调至8的倍数
static size_t ROUND_UP(size_t bytes)
{
return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
}
private:
// 此union结构体上面已经解释过了
union obj
{
union obj * free_list_link;
char client_data[1];
};
private:
//16个free-lists
static obj * __VOLATILE free_list[__NFREELISTS];
// 根据待待分配的空间大小, 在free_list中选择合适的大小
static size_t FREELIST_INDEX(size_t bytes)
{
return (((bytes) + __ALIGN-1)/__ALIGN - 1);
}
// 返回一个大小为n的对象,并可能加入大小为n的其它区块到free-lists
static void *refill(size_t n);
// 配置一大块空间,可容纳nobjs个大小为“size”的区块
// 如果配置nobjs个区块有所不便,nobjs可能会降低,所以需要用引用传递
static char *chunk_alloc(size_t size, int &nobjs);
// 内存池
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>
__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()
借用《STL源码剖析》里面的一张图,来说明空间配置函数的调用过程:(看图放松,放松完继续看源码!别偷懒)
static void * allocate(size_t n)
{
obj * volatile * my_free_list;
obj * 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()
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
int nobjs = 20; // 默认获取20个
char * chunk = chunk_alloc(n, nobjs); //找内存池要空间
obj * volatile * my_free_list;
obj * result;
obj * current_obj, * next_obj;
int i;
// 如果内存池仅仅只够分配一个对象的空间, 直接返回即可
if(1 == nobjs) return(chunk);
// 内存池能分配更多的空间,调整free_list纳入新节点
my_free_list = free_list + FREELIST_INDEX(n);
// 在chunk的空间中建立free_list
result = (obj *)chunk;
*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()
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);//返回起始地址
}
// 如果内存池中剩余的容量不够分配, 但是能至少分配一个节点时,
// 返回所能分配的最多的节点, 返回start_free指向的内存块
// 并且重新设置内存池起始点
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);
// 将剩余的内存分配给指定的free_list[FREELIST_INDEX(bytes_left)]
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;
}
start_free = (char *)malloc(bytes_to_get);
// 分配失败, 搜索原来已经分配的内存块, 看是否有大于等于当前请求的内存块
if (0 == start_free) {// heap里面空间不足,malloc失败
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) {
*my_free_list = p -> free_list_link;
start_free = (char *)p;
end_free = start_free + i;
// 内存池更新完毕, 重新分配需要的内存
return(chunk_alloc(size, nobjs));
//任何剩余零头将被编入适当的free_list以留备用
}
}
// 再次失败, 直接调用一级配置器分配, 期待异常处理函数能提供帮助
// 不过在我看来, 内存分配失败进行其它尝试已经没什么意义了,
// 最好直接log, 然后让程序崩溃
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));
}
}
内存释放函数deallocate()
内存释放函数会将释放的空间交还给free_list以留备用。其过程如下图所示:
其实就是一个简单的单链表插入的过程。其源代码如下:
static void deallocate(void *p, size_t n)
{
obj *q = (obj *)p;
obj * volatile * my_free_list;
// 大于128的直接交由第一级配置器释放
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;
}
配置器的使用
通过以上的图和源代码,基本上将STL的两层配置器讲完了,接下来就来熟悉一下怎么使用配置器。
STL将上述配置器封装在类simple_alloc中,提供了四个用于内存操作的接口函数,分别如下:
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)); }
};