目录
构造和析构基本工具:construct() 和 destroy()
在STL中,空间配置在C++的基础上增加了一些特性。STL allocator 将这两个阶段分开操作,内存配置操作由空间配置器stl::alloc中的alloc::allocate(),内存释放由alloc::deallocate()负责;对象构造操作由::construct()负责,对象析构操作由::destroy()负责。SGI STL中考虑到了异常处理,内置轻量级内存池(主要用于处理小块内存的分配,应对内存碎片问题)实现,多线程中的内存分配处理(主要是针对内存池的互斥访问)等。STL标准告诉我们,配置器定义于 < memory > 之中:
构造和析构基本工具:construct() 和 destroy()
为了精密分工,STL allocator将new和delete两阶段操作区分开来。
- 内存配置由alloc::allocator()负责
- 内存释放操作由alloc::deallocator()负责
- 对象构造由::constructor()负责
- 对象析构由::destroy()负责
/*
** <stl_construct.h>部分源码剖析
** AUTHOR:ZYZMZM
** DATE: 15/4/2019
*/
#ifndef __SGI_STL_INTERNAL_CONSTRUCT_H
#define __SGI_STL_INTERNAL_CONSTRUCT_H
#include <new.h> // 使用定位new,要包含此头文件
/*
** 使用定位new,将初值设定到指针所指的空间上
*/
template <class _T1, class _T2>
inline void _Construct(_T1* __p, const _T2& __value) {
new ((void*) __p) _T1(__value);
}
/*
** 普通new,调用T1::T1(),即T1的默认构造函数
*/
template <class _T1>
inline void _Construct(_T1* __p) {
new ((void*) __p) _T1();
}
/*
** _Destroy版本一:接收一个指针,将该指针所指析构
** 直接调用该对象的析构函数
*/
template <class _Tp>
inline void _Destroy(_Tp* __pointer) {
__pointer->~_Tp();
}
/*
** _Destroy版本二:接收两个迭代器,准备将[__first,__last)
** 范围内的所有对象析构掉,首先利用__VALUE_TYPE()获得迭代器
** 所指对象的型别,再利用_type_traits<_Tp>判断该型别的析构
** 函数是否无关痛痒(trival destructor),若是(__true_type),
** 则什么也不做就结束;若否(__false_type),就以循环方式寻访
** 整个范围,并在循环中每经历一个对象,就调用版本二的_Destroy
** 具体见下面几个函数
*/
template <class _ForwardIterator>
inline void _Destroy(_ForwardIterator __first, _ForwardIterator __last) {
__destroy(__first, __last, __VALUE_TYPE(__first));
}
/*
** 判断元素的数值型别(value type),是否有trival destructor
*/
template <class _ForwardIterator, class _Tp>
inline void
__destroy(_ForwardIterator __first, _ForwardIterator __last, _Tp*)
{
typedef typename __type_traits<_Tp>::has_trivial_destructor
_Trivial_destructor;
__destroy_aux(__first, __last, _Trivial_destructor());
}
/*
** 若否(__false_type),即元素的数值型别为no-trival destructor
** 就以循环方式寻访整个范围,并在循环中每经历一个对象,就调用版
** 本二的_Destroy
*/
template <class _ForwardIterator>
void
__destroy_aux(_ForwardIterator __first, _ForwardIterator __last, __false_type)
{
for ( ; __first != __last; ++__first)
destroy(&*__first);
}
/*
** 若否(__true_type),即元素的数值型别为trival destructor,
** 那么就则什么也不做就结束
*/
template <class _ForwardIterator>
inline void __destroy_aux(_ForwardIterator, _ForwardIterator, __true_type) {}
/*
** 以下是_Destroy版本二针对迭代器为
** char\int\long\float\double\wchar_td 特化版本
*/
inline void _Destroy(char*, char*) {}
inline void _Destroy(int*, int*) {}
inline void _Destroy(long*, long*) {}
inline void _Destroy(float*, float*) {}
inline void _Destroy(double*, double*) {}
#ifdef __STL_HAS_WCHAR_T
inline void _Destroy(wchar_t*, wchar_t*) {}
#endif /* __STL_HAS_WCHAR_T */
上述的constructor()的一个版本接受一个指针p和一个初值value,该函数的用途是将初值设定到指针所指的空间上,也就是定位new(placement new)运算子的作用。而_Destroy的接受范围的版本需要特别注意,当范围非常大时,不断调用析构函数可能会导致效率的极具降低,所以此时我们需要判断析构函数是否是trival destructor是非常有必要的。具体的执行顺序,见上述的源码剖析。
空间的配置与释放:std::alloc
上边我们讲述了内存配置后对象构造行为和内存释放前的对象析构行为,现在我们分析内存的配置和释放。
对象构造前的空间配置和对象析构后的空间释放,由<stl_alloc.h>负责,SGI STL对此的设计哲学如下:
向 system heap 申请空间
考虑多线程状态
考虑内存不足的应变措施
考虑过多"小型区块"可能造成的内存碎片问题
下面介绍的内容都不考虑多线程的情况,简化问题的复杂度。
C++的内存配置的基本操作时 ::operator new() 和 ::operator delete()。这两个全局函数相当于C中的 malloc() 和 free() 函数,正是如此SGI STL正是以malloc() 和 free() 完成内存的配置与释放。
考虑到小型区块所可能造成的内存碎片问题,SGI 设计了双层配置器,第一级配置器直接使用malloc() 和 free(),第二级配置器按照不同情况采用不同策略:
当配置区块超过128bytes时,视之为"足够大",调用第一级配置器。
当配置区块小于128bytes时,视之为"过小",为了降低额外负担,便采用复杂的memory pool整理方式(内存池),而不再求助于第一级配置器。
整个设计究竟只开放第一级配置器,或者是同时开放第二级配置器,取决于 __USE_MALLOC是否被定义。无论 alloc 被定义成第一级还是第二级配置器, SGI 都将 alloc 进行了上层封装,类似于一个转接器,使其配置器的接口符合 STL 规范:
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 的所有容器全部用该接口,比如我们常用的vector的定义:
//默认缺省alloc为空间配置器
template<class T, class Alloc = alloc>
class vector
{
protected:
// 使用了simple_alloc,每次配置一个元素大小
typedef simple_alloc<value_type, Alloc> data_allocator;
void deallocate()
{
if(...)
data_allocator::deallocate(start, end_of_storage - start);
}
//...
};
二级空间配置器简述
一级空间适配器是对malloc的简单包装,它内部的allocate()和reallocate()都是在调用malloc()和realloc(),如果调用失败,再调用oom_malloc() 和oom_realloc() 清理内存,重复多次后,如果还不成功便调用_THROW_BAD_ALLOC,丢出bad_alloc异常信息。二级空间配置器对内存的管理减少了小区块造成的内存碎片,它主要是:如果所要申请的空间大于128字节,则直接交至一级空间配置器处理,如果小于128字节,则使用二级空间配置器,它是用一个16个元素的自由链表来管理的,每个位置下挂载着大小(分别为8、16、24、32、48、56、64、72、80、88、96、104、112、120、128字节),每次将所需内存提升到8的倍数。
有关一二级配置器的关系,接口包装,及实际运用方式,见下图:
一级空间配置器 _ _malloc_alloc_template剖析
首先我们来剖析第一级空间配置器的源码:
/*
** <stl_alloc.h>部分源码剖析
** AUTHOR:ZYZMZM
** DATE: 15/4/2019
*/
// 需注意,无模板类型参数
template <int __inst>
class __malloc_alloc_template {
private:
/* 以下三个函数处理malloc 或realloc分配内存失败的情况 */
static void* _S_oom_malloc(size_t);
static void* _S_oom_realloc(void*, size_t);
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
static void (* __malloc_alloc_oom_handler)();
#endif
public:
/* 一级空间配置器直接使用malloc()开辟内存 */
static void* allocate(size_t __n)
{
void* __result = malloc(__n);
/* 内存不足,改用 _S_oom_malloc() */
if (0 == __result) __result = _S_oom_malloc(__n);
return __result;
}
/* 一级空间配置器直接使用free()释放内存 */
static void deallocate(void* __p, size_t /* __n */)
{
free(__p);
}
/* 一级空间配置器直接使用realloc()追加内存 */
static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
{
void* __result = realloc(__p, __new_sz);
/* 内存不足,改用 _S_oom_realloc() */
if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
return __result;
}
/*
** 设置__set_malloc_handler指向的函数为内存分配操作失败时调用的处理函数
** 最内部的__f是一个函数指针,其参数为void,返回值为void。
** 首先__set_malloc_handle是一个函数,其参数为形如__f的函数指针,
** 其返回值又是个指针,这个指针的类型是void(*)(),用法应该如下形式:
** void (*foo)();
** void (*bar)();
** foo = __set_malloc_handler(bar);
*/
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
/* 初值为0,有待客端设定 */
template <int __inst>
void (* __malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;
#endif
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);
}
}
// 注意,以下直接将参数inst指定为0
typedef __malloc_alloc_template<0> malloc_alloc;
第一级配置器以 malloc()、free()、realloc() 等 C 函数执行实际的内存配置、释放、重配置操作,并实现出类似 C++ new-handler 的机制,不过并不能直接使用 C++ new-handler 机制, 因为其并非使用 ::operator new 来配置内存。C++ new-handler 机制是,可以要求系统在配置需求无法被满足时,调用你所指定的函数。一旦 ::operator new 无法完成任务,在丢出 std::bad_alloc 异常状态之前,会先调用由客端指定的处理例程,该处理例程通常被称为new-handler。
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)终止程序。
二级空间配置器 _ _default_alloc_template剖析
SGI STL 第二级配置器使用的是memory pool,即内存池,相比较于第一级空间配置器,第二级空间配置器多了许多限制,主要是为了防止申请小额区块过多而造成内存碎片。
SGI 第二级配置器的做法是:
如果区块足够大,超过128bytes时,就移交第一级配置器处理。
当区块小于128bytes时,则以内存池的方式管理,又称“次层配置”
第二级配置器也叫做次层配置器:每次配置一大块内存,并维护对应的自由链表:free-list。为了方便管理,SGI STL 第二级配置器会主动为小额区块的需求量的大小上调至 8 的倍数,并维护16个 free-list即size为16的链表数组,依次为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128,free-list节点结构如下:
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
};
_Obj 用的是 union,_M_free_list_link是一个指针,指向下一个 _Obj,_M_client_data 也可以当做一个指针,指向实际区块,如下图所示,这样做的好处是不会为了维护链表所必须的指针而造成内存的另一种浪费。
下面是二级空间配置器的部分实现内容:
template <bool threads, int inst>
class __default_alloc_template {
private:
enum {_ALIGN = 8}; // 小型区块的上调边界
enum {_MAX_BYTES = 128}; // 小型区块的上限
enum {_NFREELISTS = 16}; // _MAX_BYTES/_ALIGN , 即free list的个数
/* 将__bytes上调至8的倍数 */
static size_t
_S_round_up(size_t __bytes)
{ return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1)); }
/* free list的节点构造:使用union共用体,共享内存,得到一物二用 */
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
};
private:
/* free list初始化为空 */
static _Obj* __STL_VOLATILE _S_free_list[];
/* 16个free list存储在_S_free_list中 */
static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS];
/* 根据区块大小,返回第n号free list,n从0算起 */
static size_t _S_freelist_index(size_t __bytes) {
return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1);
}
/*返回一个大小为n的对象,可能向free list添加大小为n的其他区块*/
// Returns an object of size __n, and optionally adds to size __n free list.
static void* _S_refill(size_t __n);
/*
** 配置一块大的空间,其中能够容纳 nobjs 个大小为 size 的区块
** 如果内存不够配置nobjs个区块,nobjs可能会降低
*/
// Allocates a chunk for nobjs of size size. nobjs may be reduced
// if it is inconvenient to allocate the requested number.
static char* _S_chunk_alloc(size_t __size, int& __nobjs);
// Chunk allocation state.
static char* _S_start_free; // 内存池的起始位置
static char* _S_end_free; // 内存池的结束位置
static size_t _S_heap_size; // 内存池的总容量
public:
/* __n must be > 0 */
static void* allocate(size_t __n) {/* 后面详述 */};
/* __p may not be 0 */
static void deallocate(void* __p, size_t __n) {/* 后面详述 */};
static void* reallocate(void* __p, size_t __old_sz, size_t __new_sz);
} ;
空间配置函数allocate()
__default_alloc_template 拥有配置器的标准接口函数 allocate(),毫无疑问,该函数的流程如下:
首先判断区块大小,大于128bytes就调用第一级配置器
小于或者等于128bytes 就检查对应的free-list
如果 free-list 之内有可用的区块,则直接拿来用
如果没有可用的区块,就将区块上调边界到8的倍数,然后调用 refill(),准备为free-list重新填充空间
/*
** <stl_alloc.h>部分源码剖析
** AUTHOR:ZYZMZM
** DATE: 15/4/2019
*/
/* __n must be > 0 */
static void* allocate(size_t __n)
{
void* __ret = 0;
/* 所申请的空间大于128bytes,直接调用一级空间配置器*/
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n);
}
else {
/* 在16个free list中,寻找最合适的一个 */
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
/* 将寻找结果存入__result */
_Obj* __RESTRICT __result = *__my_free_list;
/* 没有可用的free list,调用refill函数准备重新填充free list */
if (__result == 0)
__ret = _S_refill(_S_round_up(__n));
/*
** 存在可用的free list,进行分配,并调整free list
** 就是将当前分配出去的free list的下个节点存储在__my_free_list中
** 等待下次继续分配
*/
else {
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
}
}
return __ret;
}
区块自free list分配给客端的操作,如下图所示:
空间释放函数deallocate()
__default_alloc_template 拥有配置器的标准接口函数 deallocate(),该函数的流程如下:
该函数首先需要判断区块的大小,大于128bytes就调用第一级配置器
小于或者等于128bytes就找出对应的 free-list,将区块回收
/*
** <stl_alloc.h>部分源码剖析
** AUTHOR:ZYZMZM
** DATE: 15/4/2019
*/
/* __p may not be 0 */
static void deallocate(void* __p, size_t __n)
{
/* __n大于128bytes,直接调用一级空间配置器*/
if (__n > (size_t) _MAX_BYTES)
malloc_alloc::deallocate(__p, __n);
else {
/* 寻找对应的free list */
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
/*
** 调整free list,回收区块,操作方式就是找到区块对应的
** free list之后,将该区块的_M_free_list_link指向
** __my_free_list即free list的首部,再将 __my_free_list
** 指向该回收区块即完成了区块回收工作
*/
_Obj* __q = (_Obj*)__p;
__q -> _M_free_list_link = *__my_free_list;
*__my_free_list = __q;
// lock is released here
}
区块回收纳入free list 的操作,如下图所示:
重新填充free lists
之前我们剖析的空间配置函数allocate()中,当它发现 free list中没有可用区块时,就调用refill(),准备为free list重新填充空间。新的空间将取自内存池(经由_S_chunk_alloc() 完成)。缺省取得20个新节点(新区快),但万一内存池空间不足,获得的节点数(区块数)可能小于20:
/*
** <stl_alloc.h>部分源码剖析
** AUTHOR:ZYZMZM
** DATE: 15/4/2019
*/
/*
** 返回一个大小为n的对象,可能向free list
** 添加大小为n的其他区块,即增加节点
** 假设n已经上调至8的倍数
*/
/* Returns an object of size __n, and optionally adds to size __n free list.*/
/* We assume that __n is properly aligned. */
/* We hold the allocation lock. */
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20;
/* 调用_S_chunk_alloc(),尝试取得__nobjs个区块作为free list的新节点 */
char* __chunk = _S_chunk_alloc(__n, __nobjs);
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
/*
** 如果只获得了一个区块,那么这个区块就分配给调用者
** 而free list无新节点维护
*/
if (1 == __nobjs) return(__chunk);
/* 否则,free list会维护新的节点,开始调整free list */
__my_free_list = _S_free_list + _S_freelist_index(__n);
/* 在__chunk空间内建立free list */
/* Build free list in chunk */
__result = (_Obj*)__chunk; // 这个区块是返回给调用者的
/* __my_free_list 指向 __chunk之后的__n个新空间的起始地址 */
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
/* 将free list的各个节点串接起来 */
for (__i = 1; ; __i++) {
__current_obj = __next_obj;
__next_obj = (_Obj*)((char*)__next_obj + __n);
if (__nobjs - 1 == __i) { // 串接到最后一个节点
__current_obj -> _M_free_list_link = 0;
break;
} else { // 串接到其他节点
__current_obj -> _M_free_list_link = __next_obj;
}
}
return(__result); // 将__chunk区块返回给调用者
}
流程如下:
内存池
这个是STL的核心方法
_S_chunk_alloc() 函数负责从内存池取出空间给free-list:
如果内存池内存充足,则直接拿出足够的内存块给自由链表
如果内存不够所有需求但是对一小块需求能满足,则拿出一小块内存给自由链表并返回
如果一点儿内存也没有,则进行遍历压榨
最终如果真的没有,就只能求助于第一级配置器。
/*
** <stl_alloc.h>部分源码剖析
** AUTHOR:ZYZMZM
** DATE: 15/4/2019
*/
/*
** 假设__size已经上调至8的倍数
** 请注意__nobjs是按引用传递(pass by reference)
*/
template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size,
int& __nobjs)
{
char* __result;
/* 计算申请的大小(bytes) */
size_t __total_bytes = __size * __nobjs;
/* 计算内存池memory的剩余空间 */
size_t __bytes_left = _S_end_free - _S_start_free;
/* 如果空间足够大,那么直接给用户分配所需的大小 */
if (__bytes_left >= __total_bytes) {
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result); // 直接分配所需
} else if (__bytes_left >= __size) {
/* 如果内存池剩余空间不能完全满足需求,但足够分配至少一个区块*/
__nobjs = (int)(__bytes_left/__size); // 计算出内存池够分配的个数
__total_bytes = __size * __nobjs;
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result); // 返回这些区块
} else {
/*
** 如果内存池一个区块都无法提供,那么尝试让残余的零头能够被利用
** 需要向heap中申请内存,申请内存的大小即为__bytes_to_get,是用户
** 所申请空间的两倍 再加上一个随着申请次数直接增大的附加量
*/
size_t __bytes_to_get =
2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
// Try to make use of the left-over piece.
if (__bytes_left > 0) {
/* 寻找适当的free list */
_Obj* __STL_VOLATILE* __my_free_list =
_S_free_list + _S_freelist_index(__bytes_left);
/* 调整free list,将内存池中的残余空间加入 */
((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
*__my_free_list = (_Obj*)_S_start_free;
}
/* 配置heap空间,用来补充内存池 */
_S_start_free = (char*)malloc(__bytes_to_get);
if (0 == _S_start_free) {
/* malloc失败,表示heap空间也不足 */
size_t __i;
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __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.
/* 以下搜寻适当的free list,适当是指"尚有未用区块,且区块够大" */
for (__i = __size;
__i <= (size_t) _MAX_BYTES;
__i += (size_t) _ALIGN) {
__my_free_list = _S_free_list + _S_freelist_index(__i);
__p = *__my_free_list;
if (0 != __p) {
/*
** __my_free_list指向可用区块起始,那么其不为0则表示
** free list有未用区块,那么接下来调整free list,因为
** 要返回未用区块给调用者
*/
*__my_free_list = __p -> _M_free_list_link;
_S_start_free = (char*)__p;
_S_end_free = _S_start_free + __i;
return(_S_chunk_alloc(__size, __nobjs));
// Any leftover piece will eventually make it to the
// right free list.(注意任残余零头终将被编入free list中)
}
}
/* 出现意外,到处都没有内存可用了 */
_S_end_free = 0; // In case of exception
/* 调用一级空间配置器(因为其有out of memory处理机制) */
_S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
// This should either throw an
// exception or remedy the situation. Thus we assume it
// succeeded.
}
/* _S_start_free不为0,向heap申请内存分配成功,开始调整内存池 */
_S_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;
return(_S_chunk_alloc(__size, __nobjs));
}
}
假设程序开始运行,客端就开始调用_S_chunk_alloc(32,20)函数,假设 malloc() 一次性配置40个32bytes的内存区块:
20个作为函数返回值之后1个被交给客户端;剩下19个交给 free-list[3] 维护,要的时候再去取即可;另外20个留给内存池;接下来客端调用_S_chunk_alloc(64,20),此时 free-list[7] 空空如也,必须向内存池寻求支持,内存池能够供应 20 x 32 / 64 = 10个区块,就把这10个区块返回,其中1个交给客端,剩下9个交给 free-list[7] 维护,内存池空了。假设接下来再调用 _S_chunk_alloc(96,20) ,此时 free-list[11] 空空如也,必须向内存池寻求帮助,而内存池也是空的,于是又开始调用 malloc() 分配 20 +n 个 96大小的内存块 (n为附加量,其值随着申请次数而增大),其中20内存块返回,1个交给客端,剩下19个交给 free-list[11] 维护,而还有n个 96 大小的内存块就交给内存池维护…如果最终内存里面的堆区没有内存了,无法为内存池注入新的内存,malloc() 行动失败,_S_chunk_alloc()就在free-list里寻找有没有 “尚未应用区块,且区块足够大”,找到就挖出一块并交出,找不到就调用第一级配置器,第一级配置器其实也是用 malloc() 来配置内存,但它有 out-of-memory 处理机制(类似new handler),或许有机会释放其他的内存并拿来此处使用,如果可以就内存配置成功,否则就抛出 bad_alloc()异常。
以上便是整个二级空间配置器的设计,有关内存池实际操练结果如下图所示:
总结过程如下:
注意chunk_alloc通过传入nobjs引用默认是20通过递归修正nobjs来保证递归出口,传入的size表示字节数所以分配指针时需要转成char*放到自由链表时需要转成obj*
chunk_alloc函数具体实现步骤如下:
- 内存池剩余空间完全满足20个区块的需求量,则直接获取对应大小的空间。
- 内存池剩余空间不能完全满足20个区块的需求量,但是足够供应一个及以上的区块,则获取满足条件的区块个数的空间。
- 内存池剩余空间不能满足一个区块的大小,则:
- 首先判断内存池中是否有残余零头内存空间,如果有则进行回收,将其编入free_list。
- 然后向heap申请空间,补充内存池。
- heap有足够的空间,空间分配成功。
- heap空间不足,即malloc()调用失败。则
- 查找free_list中尚有未用区块,调整以进行释放,将其编入内存池。然后递归调用chunk_alloc函数从内存池取空间供free_list备用。
- 搜寻free_list释放空间也未能解决问题,这时候调用第一级配置器,利用out-of-memory机制尝试解决内存不足问题。
多线程环境下内存池互斥访问
在第二级配置器中,存在着多线程环境的内存池管理,解决多线程环境下内存池互斥访问,需在自由链表free_list中进行修改调整,我们从SGI STL第二级配置器源码中看到,嵌套一个类class _Lock ,该类的作用是解决互斥访问,并且只有两个函数:构造函数和析构函数;使用构造函数对内存池进行加锁,使用析构函数对内存池进行解锁。关于多线程内存池互斥访问的源代码如下:
#ifdef __STL_THREADS
# include <stl_threads.h>//包含线程文件
# define __NODE_ALLOCATOR_THREADS true
# ifdef __STL_SGI_THREADS
// We test whether threads are in use before locking.
// Perhaps this should be moved into stl_threads.h, but that
// probably makes it harder to avoid the procedure call when
// it isn't needed.
extern "C" {
extern int __us_rsthread_malloc;
}
// The above is copied from malloc.h. Including <malloc.h>
// would be cleaner but fails with certain levels of standard
// conformance.
# define __NODE_ALLOCATOR_LOCK if (threads && __us_rsthread_malloc) \
{ _S_node_allocator_lock._M_acquire_lock(); }
# define __NODE_ALLOCATOR_UNLOCK if (threads && __us_rsthread_malloc) \
{ _S_node_allocator_lock._M_release_lock(); }
# else /* !__STL_SGI_THREADS */
# define __NODE_ALLOCATOR_LOCK \
{ if (threads) _S_node_allocator_lock._M_acquire_lock(); }//获取锁
# define __NODE_ALLOCATOR_UNLOCK \
{ if (threads) _S_node_allocator_lock._M_release_lock(); }//释放锁
# endif
#else
// Thread-unsafe
# define __NODE_ALLOCATOR_LOCK
# define __NODE_ALLOCATOR_UNLOCK
# define __NODE_ALLOCATOR_THREADS false
#endif
# ifdef __STL_THREADS
static _STL_mutex_lock _S_node_allocator_lock;//互斥锁变量
# endif
// It would be nice to use _STL_auto_lock here. But we
// don't need the NULL check. And we do need a test whether
// threads have actually been started.
class _Lock;
friend class _Lock;
class _Lock {//解决内存池在多线程环境下的管理
public:
_Lock() { __NODE_ALLOCATOR_LOCK; }
~_Lock() { __NODE_ALLOCATOR_UNLOCK; }
};
小结
STL中的内存分配器核心就是基于空闲列表(free list)的分配策略,最主要的特点是通过组织16个空闲列表,对小对象的分配做了优化。
1)小对象的快速分配和释放。当一次性预先分配好一块固定大小的内存池后,对小于128字节的小块内存分配和释放的操作只是一些基本的指针操作,相比于直接调用malloc/free,开销小。
2)避免内存碎片的产生。零乱的内存碎片不仅会浪费内存空间,而且会给OS的内存管理造成压力。
3)尽可能最大化内存的利用率。当内存池尚有的空闲区域不足以分配所需的大小时,分配算法会将其链入到对应的空闲列表中,然后会尝试从空闲列表中寻找是否有合适大小的区域,
但是,这种内存分配器局限于STL容器中使用,并不适合一个通用的内存分配。因为它要求在释放一个内存块时,必须提供这个内存块的大小,以便确定回收到哪个free list中,而STL容器是知道它所需分配的对象大小的,比如上述:
stl::vector<int> array;
array是知道它需要分配的对象大小为sizeof(int)。一个通用的内存分配器是不需要知道待释放内存的大小的,类似于free(p)。