STL 空间分配器(二)

三.内存的分配与释放

3.1概述

         SGI STL中设计了两层配置器,分别负责配置不同大小的内存区,当请求内存大于128bytes时采用第一级配置器,直接调用malloc()与free().,而当请求内存小于128bytes时采用第二级配置器,其中使用了一种类似于buddy算法(但简单很多)的方式进行内存分配。

         不论是一级还是二级配置器都不接受任何template型别参数,因为内存分配应该只与要请求的大小有关,而无关于这块内存需要存放的元素类型,内存分配器只负责内存分配,不负责对象构造。SGI为内存配置器封装了一个统一的接口simple_alloc : 

template<class _Tp, class _Alloc>
class simple_alloc {

public:
     // 根据类型与个数计算出要申请的内存大小在传给具体的_Alloc类型配置器
    static _Tp* allocate(size_t __n)
      { return 0 == __n ? 0 : (_Tp*) _Alloc::allocate(__n * sizeof (_Tp)); }
    static _Tp* allocate(void)
      { return (_Tp*) _Alloc::allocate(sizeof (_Tp)); }
    static void deallocate(_Tp* __p, size_t __n)
      { if (0 != __n) _Alloc::deallocate(__p, __n * sizeof (_Tp)); }
    static void deallocate(_Tp* __p)
      { _Alloc::deallocate(__p, sizeof (_Tp)); }
};

其接受两个template型别参数,一个是要申请的对象类型,一个是内存配置器类型,其本质是对其它内存配置器的转调用。SGI STL容器全都使用这个接口。举个例子当一个vector<int>容器要申请空间时,容器调用内部的sample_alloc对象,而该对象转掉其它配置器。 

3.2第一级配置器

     SGI STL中第一级配置器即_malloc_alloc_template。其通过调用malloc和free函数管理内存,代码如下:

template <int __inst>
class __malloc_alloc_template { // 第一级配置器

private:
  static void* _S_oom_malloc(size_t);
  static void* _S_oom_realloc(void*, size_t);

  // 指向用户设置的内存分配错误处理函数
  static void (* __malloc_alloc_oom_handler)();

public:
  static void* allocate(size_t __n) {
    void* __result = malloc(__n);
    if (0 == __result) __result = _S_oom_malloc(__n); //当malloc无法满足需求时,调用_S_oom_malloc
    return __result;
  }

  static void deallocate(void* __p, size_t /* __n */) {
    free(__p); // 一级配置器释放内存
  }

  static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz) {
    void* __result = realloc(__p, __new_sz);// 第一级配置器直接使用realloc()
    if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
    return __result;
  }

  // 类似于c++的new handler机制,当系统无法满足调用需求时,调用一个__malloc_alloc_oom_handler所指向的函数
  // 【注】:由于CSDN的c++代码段不认识该语法因此暂且使用/**/将其注释起来,便于前面代码的阅读
 /* static void (* __set_malloc_handler(void (*__f)())) () {
    void (* __old)() = __malloc_alloc_oom_handler;
    __malloc_alloc_oom_handler = __f;
    return __old;
  }*/
};

       在说明上面的代码段之前先介绍一下c++ new handler机制:你可以设置一个回调函数,当系统分配内存失败时先调用该回调函数,否则将直接抛出std::bad_alloc异常。c++中使用set_new_handler函数来设置该回调,SGI SLT中也模仿了该机制,即调用__set_malloc_handler函数设置__malloc_alloc_oom_handler回调,此处回调是用函数指针实现的,当然也可以利用std::function。设置回调的函数写法有一些特别:static void (* __set_malloc_handler(void (*__f)())) (),我们可以从里到外这样去读:__set_malloc_handler包含一个参数列表,因此其是一个函数,参数类型为void (*__f)(),即一个指向void()类型签名的函数的函数指针,接着往外看可知返回值为void(*)()。此处可再举一个例子供读者参考:

#include <iostream>
using namespace std;

int addFun(int a, int b) {
	return a + b;
}

int(*add(int(*f)(int, int), int a, int b))(int, int) {
	cout << f(a, b) << endl;
	return f;
}

int main()
{
	int(*funP)(int, int) = add(addFun,1,2);
	cout<< funP(2,3);
	return 0;
}

   当第一级配置器调用malloc分配内存失败时,便会调用_S_oom_malloc函数,该函数的定义如下:

template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)
{
    void (* __my_malloc_handler)();
    void* __result;

    for (;;) { // while(1)
        __my_malloc_handler = __malloc_alloc_oom_handler;
        if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; } // 若果未设置分配失败处理函数则通过标准错误输出异常,并退出
        (*__my_malloc_handler)();                            // 调用分配失败处理函数
        __result = malloc(__n);                              // 重新调用_malloc进行回调
        if (__result) return(__result);                      // 只有当成功时才返回,测试发现c++的new handler也同样
    }
}

注意上面代码发现只用当在用户未设置错误处理回调时才会抛出std::bad_alloc异常,当用户设置了错误回调,而用户又未在回调中设置某种退出条件,如:exit(1),或释放一些内存,则STL不会抛出std::bad_alloc,而是会一直调用malloc尝试分配内存,直至成功(当用户的处理函数处置有误时,这可能会造成一个死循环),即__result不为空。同样的在测试c++ new handler 也出现了这种情况,以下为测试代码:

void my_new_handler() {
	cout << "new error";
}

int main()
{
	set_new_handler(my_new_handler);
	int *bigmm = new int[0x1fffffff];
	return 0;
}

将一直输出"new error"(形成了一个死循环),而不会抛出异常。至于reallocate()函数的处理类似于allocate,此处不在赘述。

 

3.2 第二级配置器

        第二级配置器是通过一些机制来处理小块内存的分配,其思想类以于一种简化的buddy算法。直接通过代码描述不够直观,因此先通过一些结构图来进行总体介绍。

       二级分配器通过维护一个包含16个链表的数组来进行内存分配的,其中从下标0处的链表只包含大小为8的内存块,1处只包含大小为16的,2处为24,类推,依次为8,16,24,32,40,48,56,64,64,72,80,88,96,104,112,120,128,即 下标i处只包含(i+ 1) * 8 大小的内存块。并且为了在维护链表的同时尽量增加内存利用率,每块内存的起始位置使用如下结构体进行维护:

  union _Obj {
        union _Obj* _M_free_list_link; // 指向同链表的下一个obj,即下一块同等大小的内存块的起点
        char _M_client_data[1];    /* The client sees this.        */
  };

    该结构十分巧妙的使用了共用体来提高内存利用率,其优点如下:1)当块连在链表中时,该段区间用于指向下一块节点。2)当用户在使用该块内存时(已从链表中断开),看到的是_M_client_data,仍可用于存储数据。因而不会为了维护链表而浪费一块区间。这种手法在Java这种强类型语言中不可为,c++为非强类型语言。这种手法也十分常用,可以称之为对内存的重新解释,比如:

#include <iostream>

using namespace std;

int main()
{
	int num = 10;
	char *ptr = reinterpret_cast<char*>(&num);
	*(ptr) = 5;
	cout << num;
	return 0;
}

     以上代码输出为5,因为我们先重新解释了int num这块内存,用改变了其高字节部分,因此变为了5。

二级配置器的总体结构图如下所示

               

   至于代码部分由于还是比较好理解的,因此不再进行详述,仅仅对其分配和回收流程做一个说明。

   内存的分配流程

    当申请的内存大于128bytes时直接转而调用一级配置,否则将申请大小提升至8的倍数,接着在该大小的链表中去除第一个可用的内存块。当链表中无可用内存块时,SGI会进行一些列操作尝试获取内存块。首先,SGI会向内存池申请1 ~20个该种内存块大小的空间,默认会申请20个,可当内存池空间不足时,则尽可能多的进行申请。之后,若是内存池空间也不足则将尝试从对内存进行空间申请。若是对内存的空间也不足,则SGI会尝试将更大内存块链表中的内存块一个个的释放回内存池,每释放一个回内存池就再次尝试从内存池分配需求大小的空间,若以上都失败了则进行最后一步的垂死挣扎,调用malloc函数尝试分配内存。

    内存的分配流程

    内存的回收流程就简单很多,将回收的内存块通过头插法直接链入相应的内存块链表即可

四.内存基本处理工具

     见博文《STL 空间配置器(三)》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值