《STL源码剖析》-内存适配器(二)配置器剖析

了解内存配置后的对象构造行为和内存释放前的对象析构行为后(参见博文: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异常。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值