您的位置:首页 > 编程语言 > C语言/C++

深度剖析空间配置器(二)一二级配置器

2017-04-19 12:28 543 查看
空间配置器

主要分三个文件实现,我们已经介绍过第一个文件了(对象的构造和析构 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.在用户定义了释放内存程序例程后,成功分配指定大小内存,返回指向该内存区域的首地址。

可以很清楚的看出,第一级配置器以 malloc(),free(),realloc() 等 C 函数执行实际的内存申请、释放、重申请操作,

总结:SGI 第一级配置器的 allocate() 和 reallocate() 都是在调用 malloc() 和 realloc() 不成功后,改调用 _S_oom_malloc() 和 _S_oom_realloc()。后两者都有内循环,不断调用 “内存不足处理例程”,期望在某次调用之后,可以获得足够的内存来完成所需求的内存分配,如果 “内存不足处理例程” 并未被客端设定,_S_oom_malloc() 和 _S_oom_realloc() 便会调用 __THROW_BAD_ALLOC,丢出 bad_alloc
异常信息,而后直接利用 exit(1) 中止程序。


第二级配置器

我们重点来看看第二级配置器,这才是SGI的经典,我们需要再次知道第二级分配器是怎么“被”工作的。
(1)当用户申请的内存大于128bytes时,SGI配置器就会将这个工作交由第一级分配器来完成
(2)当用户申请的内存大小小于128bytes时,SGI配置器就会将这个工作交由第二级分配器来完成。此时,第二级分配器就要开始工作了。第二级分配器的原理较为简单,就是向内存池中申请一大块内存空间,然后按照大小分为16组,(8,16…..128),每一个大小都对应于一个free_list链表,这个链表上面的节点就是可以使用的内存空间,需要注意的是,配置器只能分配8的倍数的内存,如果用户申请的内存大小不足8的倍数,配置器将自作主张的为用户上调到8的倍数。

当然,第二级配置器的原理远没有这么简单,上面我们说到第二级配置器如何管理内存,现在,我们要开始为用户分配内存和回收内存了。

当用户申请一个内存后,第二级配置器首先将这个空间大小上调到8的倍数,然后找到对应的free_list链表,
(1)如果链表尚有空闲节点,那么就直接取下一个节点分配给用户,
(2)如果对应链表为空,那么就需要重新申请这个链表的节点,默认为20个此大小的节点。
(3)如果内存池已经不足以支付20个此大小的节点,但是足以支付一个或者更多的该节点大小的内存时,返回可完成的节点个数。
(4)如果已经没有办法满足该大小的一个节点时,就需要重新申请内存池了!所申请的内存池大小为:2*total_bytes+ROUND_UP(heap_size>>4),total_bytes是所申请的内存大小,SGI将申请2倍还要多的内存。为了避免内存碎片问题,需要将原来内存池中剩余的内存分配给free_list链表。
(4)的情况分为两种:
1)如果内存池申请内存失败了,也就是heap_size不足以支付要求时,SGI的次级配置器将使用最后的绝招查看free_list数组,查看是否有足够大的没有被使用的内存,
2)如果这些办法都没有办法满足要求时,只能调用第一级配置器了,我们需要注意,第一级配置器虽然是用malloc来分配内存,但是有new-handler机制(out-of-memory),如果无法成功,只能抛出bad_alloc异常而结束分配。

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, };


看完上面这一堆源码,你可能早就头晕眼花,一脸懵逼了,没事,我再来用一张思维导图来帮你理一理思绪:



二级配置器1

接下来又是枯燥的源码时间!相信有上面这张图,看源码的思路就比较清晰了。


空间配置函数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)); }
};
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息