接着上回,这节开始说allocte内存分配的实现
目录
allocate源码流程:
_S_refill 的实现:
_S_chunk_alloc的实现:
deallocate:
reallocate:
二级空间配置器的逻辑步骤:
假如现在申请n个字节:
1、判断n是否大于128,如果大于128则直接调用一级空间配置器。如果不大于,则将n上调至8的倍数处,然后再去自由链表中相应的结点下面找,如果该结点下面挂有未使用的内存,则摘下来直接返回这块空间的地址。否则的话我们就要调用refill(size_t n)函数去内存池中申请。
2、向内存池申请的时候可以多申请几个,STL默认一次申请nobjs=20个,将多余的挂在自由链表上,这样能够提高效率。
进入refill函数后,先调chunk_alloc(size_t n,size_t& nobjs)函数去内存池中申请,如果申请成功的话,再回到refill函数。
这时候就有两种情况,如果nobjs=1的话则表示内存池只够分配一个,这时候只需要返回这个地址就可以了。否则就表示nobjs大于1,则将多余的内存块挂到自由链表上。
如果chunk_alloc失败的话,在他内部有处理机制。
3、进入chunk_alloc(size_t n,size_t& nobjs )向内存池申请空间的话有三种情况:
3.1、内存池剩余的空间足够nobjs*n这么大的空间,则直接分配好返回就可以了。
3.2、内存池剩余的空间leftAlloc的范围是n<=leftAlloc<nobjs*n,则这时候就分配nobjs=(leftAlloc)/n这么多个的空间返回。
3.3、内存池中剩余的空间连一个n都不够了,这时候就要向heap申请内存,不过在申请之前先要将内存池中剩余的内存挂到自由链表上,之后再向heap申请。
3.3.1、如果申请成功的话,则就再调一次chunk_alloc重新分配。
3.3.2、如果不成功的话,这时候再去自由链表中看看有没有比n大的空间,如果有就将这块空间还给内存池,然后再调一次chunk_alloc重新分配。
3.3.3、如果没有的话,则就调用一级空间配置器分配,看看内存不足处理机制能否处理。
allocate源码流程:
static void* allocate(size_t __n)
{
void* __ret = 0;
//如果申请大于128字节了,就不受内存池管了
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n);
}//通过malloc去开辟
else {
//小于等于,通过内存池去开辟分配了
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
// Acquire the lock here with a constructor call.
// This ensures that it is released in exit or during stack
// unwinding.
# ifndef _NOTHREADS
/*REFERENCED*/
_Lock __lock_instance;
# endif
_Obj* __RESTRICT __result = *__my_free_list;
if (__result == 0)
__ret = _S_refill(_S_round_up(__n));
else {
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
}
}
return __ret;
};
整个流程:利用栈上对象出作用域自动析构的特点,构造析构加锁解锁,完全支持线程安全
_S_refill 的实现:
底层分配内存的接口
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20;
char* __chunk = _S_chunk_alloc(__n, __nobjs);
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
if (1 == __nobjs) return(__chunk);
__my_free_list = _S_free_list + _S_freelist_index(__n);
/* Build free list in chunk */
__result = (_Obj*)__chunk;
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
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);
}
以要开辟8字节内存为例:
让__my_free_list指向对应下标位置,让result指向了链表chunk块头,一次移动一块,让头指针往下移
_S_refill 就干了两件事,一件是从内存池分配了每条带20个结点的链表,另一件就是根据指针移动分配内存结点,把各个chunk内存块连接起来
下面就是通过_S_chunk_alloc去从内存池里分配一块一块的chunk
_S_chunk_alloc的实现:
template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size,
int& __nobjs)
{
char* __result;
size_t __total_bytes = __size * __nobjs;//内存池要分配的字节数8*20 ,20个结点
size_t __bytes_left = _S_end_free - _S_start_free;//剩余的字节数,0,320
if (__bytes_left >= __total_bytes) {//chunk够用,递归第二遍开始走320>160
__result = _S_start_free; //如果剩余的大小大于等于申请的大小,则返回这个这
_S_start_free += __total_bytes;//直接加到160,40个结点分两半
return(__result);
} else if (__bytes_left >= __size) {
//如果剩余的内存足够分配一个新size, 比如8字节链表下还有20块,现在要分配16字节的
//8*20=160 刚好能分配10个16字节,这样的话就把这边8字节的块合成10块当16字节分配了
//然后把这些chunk块写到16字节编号下面
//等于分配的40个字节,前20个给8字节,后20备用,如果需要16,32...字节就按剩下的块整合
//按照新字节大小分配出去,把整合剩的挂到新size编号上
__nobjs = (int)(__bytes_left/__size);
__total_bytes = __size * __nobjs;
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
} else { //内存池中的内存已经不够一个size了,一开始走这
size_t __bytes_to_get = //最开始开辟40个结点
2 * __total_bytes + _S_round_up(_S_heap_size >> 4);//0>>4 上调字节
// Try to make use of the left-over piece.
if (__bytes_left > 0) {
_Obj* __STL_VOLATILE* __my_free_list =
_S_free_list + _S_freelist_index(__bytes_left);
((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
*__my_free_list = (_Obj*)_S_start_free;
}
_S_start_free = (char*)malloc(__bytes_to_get);//320
if (0 == _S_start_free) {//如果从内存池中申请开辟失败
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.
for (__i = __size;
__i <= (size_t) _MAX_BYTES;//走到128
__i += (size_t) _ALIGN) { //一次走8
__my_free_list = _S_free_list + _S_freelist_index(__i);
__p = *__my_free_list;//遍历编号,走到能分配的位置>=当前size,去分配chunk
if (0 != __p) {
//比如要分配40字节,40上也不够了,就走到48上去,56上去..
*__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.
}
}
//找不到能分配的就malloc从堆往内存池申请
_S_end_free = 0; // In case of exception.
_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_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;//走完一遍,__bytes_left就有值了
return(_S_chunk_alloc(__size, __nobjs));//递归调用
}
}
如果没有分配,就一上来分配20个结点,再*2,_S_heap_size是0,所以最初一共分配40个,把40个平分两半,把前20个就返回回去,进到_S_refill 里面,把这20个结点连起来,用出去,后20个作为备用
如果前20各用完了,用到备用20个的时候,要申请大于当前编号字节数的字节时走第二个else if
- 如果剩余的内存足够分配一个新size, 比如8字节链表下还有20块,现在要分配16字节的
- 8*20=160 刚好能分配10个16字节,这样的话就把这边8字节的块合成10块当16字节分配了
- 然后把这些chunk块写到16字节编号下面
- 等于分配的40个字节,前20个给8字节,后20备用,如果需要16,32...字节就按剩下的块整合
- 按照新字节大小划分分配出去,把整合剩的挂到新size编号上
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);
如果我们备用chunk剩了32字节,现在要分配40字节,我们连一块也分配不出去,此时进到最后一个else
剩的32也不能浪费了,先把这32字节接到对应到能用到的编号下,所以放到了32字节编号下,32字节obj就指向剩的这32字节头,把这32字节和编号下原有的链表串起来了,
如果当前位置上剩的不够分配,就从当前位置8字节循环为往后找,看哪个位置上有chunk能分配
上述所有操作开辟失败了就malloc从堆申请,这块malloc也是很巧妙的,一直循环去申请直到申请成功
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);
}
}
deallocate:
reallocate:
template <bool threads, int inst>
void*
__default_alloc_template<threads, inst>::reallocate(void* __p,
size_t __old_sz,
size_t __new_sz)
{
void* __result;
size_t __copy_sz;
//大于128
if (__old_sz > (size_t) _MAX_BYTES && __new_sz > (size_t) _MAX_BYTES) {
return(realloc(__p, __new_sz));
}
//如果是小块内存的扩容缩容
if (_S_round_up(__old_sz) == _S_round_up(__new_sz)) return(__p);//调整8倍数
__result = allocate(__new_sz);//如果在不同字节下,重新allocate分配new_sz
__copy_sz = __new_sz > __old_sz? __old_sz : __new_sz;//扩容 缩容
memcpy(__result, __p, __copy_sz);//拷贝
deallocate(__p, __old_sz);
return(__result);
}
空间配置器的其他问题:
1、在空间配置器中所有的函数和变量都是静态的,所以他们在程序结束的时候才会被释放发。二级空间配置器中没有将申请的内存还给操作系统,只是将他们挂在自由链表上。所以说只有当你的程序结束了之后才会将开辟的内存还给操作系统。
2、由于它没有将内存还给操作系统,所以就会出现二种极端的情况。
2.1、假如我不断的开辟小块内存,最后将整个heap上的内存都挂在了自由链表上,但是都没有用这些空间,再想要开辟一个大块内存的话会开辟失败。
2.2、再比如我不断的开辟char,最后将整个heap内存全挂在了自由链表的第一个结点的后面,这时候我再想开辟一个16个字节的内存,也会失败。
总的来说上面的情况只是小概率情况。如果非得想办法解决的话,我想的是:针对2.1我们可以引入释放二级空间配置器的方法,但是这个释放比较麻烦。针对2.2我们可以合并自由链表上的连续的小的内存块。
3、二级空间配置器会造成内碎片问题,极端的情况下一直申请char,则就会浪费7/8的空间。