0%

STL第一级配置器

我们再来学习内存的配置与释放(定义在头文件 <stl_alloc.h> 中)。其设计思想为:

  • 向 system heap 要求空间;
  • 考虑多线程 (multi-threads) 状态;
  • 考虑内存不足时的应变措施;
  • 考虑过多 “小型区块” 可能造成的内存碎片 (fragment) 问题。

我们后面将通过分析源码来了解这设计思想是如何在设计中体现的。SGI 设计了双层级配置器,第一级配置器直接使用 malloc() 和 free(),第二级则视情况采用不同策略,并采用了复杂的内存池(memory pool) 整理方式。
整个设计究竟只开放第一级配置器,或是同时开放第二级配置器,取决于宏 __USE_MALLOC 是否被定义。

1
2
3
4
5
6
7
8
9
10
# ifdef __USE_MALLOC
...
/*__malloc_alloc_template 就是第一级配置器*/
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc;
#else
...
/*__default_alloc_template 就是第二级配置器*/
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;
#endif

最后都定位于 alloc,因为通常我们使用缺省的空间配置器,而 SGI STL 的每个容器都已经指定其缺省的空间配置器 alloc。
无论 alloc 被定义为第一级或第二级配置器,SGI 还为它再封装一个接口如下,使配置器的接口能够符合STL规格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<class _Tp, class _Alloc>
class simple_alloc {
public:
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));
}
};

这内部四个成员函数其实都是单纯的转调用,调用传递给配置器的成员函数。SGI STL 容器全部使用这个simple_alloc 接口。

本文就着重分析第一级配置器 __malloc_alloc_template

先学习 allocate()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#define __THROW_BAD_ALLOC fprintf(stderr, "out of memory\n"); exit(1)
static void* allocate(size_t __n)
{
void* __result = malloc(__n); //调用malloc()分配内存,向 system heap 要求空间
if (0 == __result) __result = _S_oom_malloc(__n); //malloc分配失败,调用_S_oom_malloc()
return __result; //oom means "out of memory"
}

#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
template <int __inst>
void(*__malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;
#endif
//内存不足处理例程,初值为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;
/*由于初值设定为0,如果用户没有自定义相应的内存不足处理例程,那么还是抛出异常*/
if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }
(*__my_malloc_handler)(); //用户有自定义(释放内存),则进入相应的处理程序
__result = malloc(__n);
if (__result) return(__result);
}
//不断的尝试释放和配置是因为用户不知道还需要释放多少内存来满足分配需求,只能逐步的释放配置
}

上面并非死循环,它有两个退出条件:1.用户没有定义相应的内存不足处理例程,即没有通过释放内存来解决现有内存分配不足的问题,结果抛出异常,直接退出(宏定义);2.在用户定义了释放内存程序例程后,成功分配指定大小内存,返回指向该内存区域的首地址。

再学习 reallocate(),与allocate() 类似,就直接贴源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
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
3
4
static void deallocate(void* __p, size_t /* __n */)
{
free(__p); //第一级配置器直接使用free()
}

可以很清楚的看出,第一级配置器以 malloc(),free(),realloc() 等 C 函数执行实际的内存配置、释放、重配置操作,因此,SGI 不能直接使用 C++ 的 set_new_handler(),必须仿真一个类似的 set_malloc_handler()。

1
2
3
4
5
6
7
/*该函数接收一个返回值为空,参数为空的函数指针作为参数,最后返回一个返回值和参数均为空的函数指针*/
static void (* __set_malloc_handler(void (*__f)()))()
{
void (* __old)() = __malloc_alloc_oom_handler; //保存原有处理例程
__malloc_alloc_oom_handler = __f; //重新指定异常处理例程
return(__old);
}

这个函数重新指定了内存分配异常处理函数,并返回原有的内存分配异常处理函数,即设置新处理例程的同时也保存了原有的处理例程。

回看源码中的 allocate() 和 reallocate() 函数中的用户定义内存不足异常处理例程正是通过这里来指定的。

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

所以可以看出,设计 “内存不足处理例程” 是客端的责任,设定 “内存不足处理例程” 也是客端的责任。也就是说 SGI STL 不为你设定。