一、摘要

  pool_allocator是一种基于单锁内存池的空间分配器,其内部采用内存池思想,通过构建16个空闲内存块队列,来进行内存的申请和回收处理。每个空闲队列管理的内存块大小固定,且均为8的倍数,范围从8到128字节,按8的倍数递增。该空间分配器最小分配单位为8字节,低于128字节(含128)的内存块申请,采用内存池分配策略,高于128字节的内存块申请,直接从操作系统new申请。

内存池示意图:

二、内存池实现

class __pool_alloc_base
{
typedef std::size_t size_t; protected:
enum { _S_align = 8 };
enum { _S_max_bytes = 128 };
enum { _S_free_list_size = (size_t)_S_max_bytes / (size_t)_S_align }; union _Obj
{
union _Obj* _M_free_list_link; // 指向下一个内存块
char _M_client_data[1];  // 表示该Obj也即是内存块.
}; static _Obj* volatile _S_free_list[_S_free_list_size]; // 指针数组,指向16个空闲队列链表起始地址, static char* _S_start_free; // 未归空闲队列的堆内存起始地址
static char* _S_end_free; // 未归空闲队列的堆内存末端地址
static size_t _S_heap_size; // 总共申请的堆内存大小 // 向上取整至8的倍数,_S_align等于8
size_t _M_round_up(size_t __bytes)
{ return ((__bytes + (size_t)_S_align - 1) & ~((size_t)_S_align - 1)); } // 根据内存块大小定位空闲链表下标
_GLIBCXX_CONST _Obj* volatile* _M_get_free_list(size_t __bytes) throw (); __mutex& _M_get_mutex() throw (); // 申请n字节大小的内存块,n需为8的倍数,内部可能会填充空闲队列
void* _M_refill(size_t __n); // 尝试申请nobjs个n字节大小的内存块。不一定能申请到nobjs个,最终申请的内存块个数通过nobjs引用出参
char* _M_allocate_chunk(size_t __n, int& __nobjs);
};

内存池的基类__pool_alloc_base主要实现内存池内存管理的细节,包含空闲链表的建立、查找、内存块的申请。其重点在于_M_refill()和_M_allocate_chunk()接口。各成员含义见如上注释。

_M_get_free_list()

__pool_alloc_base::_Obj* volatile* __pool_alloc_base::_M_get_free_list(size_t __bytes) throw ()
{
size_t __i = ((__bytes + (size_t)_S_align - 1) / (size_t)_S_align - 1);
return _S_free_list + __i;
}

内存池通过_M_get_free_list()接口从16个空闲链表中定位到指定空闲链表。因为16个空闲链表管理的内存从8到128字节,按8的倍数按序递增,故可以根据申请的内存大小,定位到链表数组下标。链表数组首地址加上相应的偏移(即下标),即可取到对应的空闲链表。函数内部对申请字节做了向上往8的倍数取整的处理,非8倍数的内存大小申请,最终也是由向上一级的链表管理。

_M_refill()

void* __pool_alloc_base::_M_refill(size_t __n)
{
int __nobjs = 20; // 尝试从堆内存中取出__nobjs个__n字节大小的内存块
char* __chunk = _M_allocate_chunk(__n, __nobjs);
_Obj* volatile* __free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj; // 如果只取出了一个,就返回给上一级,如果取出有剩余,将剩余的内存块存入链表
if (__nobjs == 1)
return __chunk; __free_list = _M_get_free_list(__n); // 第一个内存块返回给上一级
__result = (_Obj*)(void*)__chunk; // 空闲链表指向第二个空闲块
*__free_list = __next_obj = (_Obj*)(void*)(__chunk + __n);
for (int __i = 1; ; __i++)
{
// 这里循环构建空闲链表,每个节点指向一块__n大小的内存块,最后一个节点指向null
__current_obj = __next_obj;
__next_obj = (_Obj*)(void*)((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;
}

__M_refill()函数在空闲链表为空情况下会调用(详见下面allocate介绍),它会尝试从堆内存(这里有可能是已经从堆中申请出来的未归入空闲链表管理的大块内存,也可能是直接new,向操作系统申请内存)申请__nobjs个__n大小的内存块,也可能会申请不到__nobjs个。如果申请成功,返回第一个内存块给上层应用,如果申请有剩余,会将剩下的__nobjs-1个内存块用来构建空闲链表。

下列为refill()的操作图示,_M_allocate_chunk(32,4)申请4个32字节的内存块,从内存大块取出4个,一个返回给用户,3个归入链表

具体的内存申请细节见_M_allocate_chunk()

_M_allocate_chunk()

char* __pool_alloc_base::_M_allocate_chunk(size_t __n, int& __nobjs)
{
// __total_bytes为总申请内存大小,__bytes_left为大块内存剩余的内存大小
char* __result;
size_t __total_bytes = __n * __nobjs;
size_t __bytes_left = _S_end_free - _S_start_free; // 情况1,如果剩余空间充足,返回内存块,_S_start_free向后偏移
if (__bytes_left >= __total_bytes)
{
__result = _S_start_free;
_S_start_free += __total_bytes;
return __result ;
}
// 情况2,如果剩余空间不足分配__nobjs个内存块,但仍能分配至少一块,此时能分配多少就给多少,_S_start_free向后偏移
else if (__bytes_left >= __n)
{
__nobjs = (int)(__bytes_left / __n);
__total_bytes = __n * __nobjs;
__result = _S_start_free;
_S_start_free += __total_bytes;
return __result;
}
// 情况3,如果一块也分配不了
else
{
// 剩余内存不足分配一块,将其全部归入其他链表,不存在无法归入的情况,因为申请的都是8字节倍数的内存,最终剩余的也是8的倍数
if (__bytes_left > 0)
{
_Obj* volatile* __free_list = _M_get_free_list(__bytes_left);
((_Obj*)(void*)_S_start_free)->_M_free_list_link = *__free_list;
*__free_list = (_Obj*)(void*)_S_start_free;
} // 尝试申请2倍多的内存
size_t __bytes_to_get = (2 * __total_bytes + _M_round_up(_S_heap_size >> 4));
__try
{
_S_start_free = static_cast<char*>(::operator new(__bytes_to_get));
}
__catch(const std::bad_alloc&)
{
// 如果operator new都失败,操作系统堆内存不足了,此时只能想办法从上一级的链表拿内存块
size_t __i = __n;
for (; __i <= (size_t) _S_max_bytes; __i += (size_t) _S_align)
{
// 向上逐级找,找到有空闲内存块的链表,找到后,该内存块作为新的大块内存,
// start和end指针分别指向首端和末端,递归调用_M_allocate_chunk,直至找到满足大小的内存块,并更新__nobjs
_Obj* volatile* __free_list = _M_get_free_list(__i);
_Obj* __p = *__free_list;
if (__p != 0)
{
*__free_list = __p->_M_free_list_link;
_S_start_free = (char*)__p;
_S_end_free = _S_start_free + __i;
return _M_allocate_chunk(__n, __nobjs);
}
}
// 如果所有链表都没有空闲块,此时也没办法了,只能再次抛出异常了
_S_start_free = _S_end_free = 0;
__throw_exception_again;
} // 如果operator new成功,start和end指针分别指向新内存块的首端和末端,递归调用_M_allocate_chunk,
// 直至找到满足大小的内存块,并更新__nobjs
_S_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;
return _M_allocate_chunk(__n, __nobjs);
}
}

了解_M_allocate_chunk()的实现,得先明确如下三个变量的用途

char* __pool_alloc_base::_S_start_free = 0;
char* __pool_alloc_base::_S_end_free = 0;
size_t __pool_alloc_base::_S_heap_size = 0;

指针_S_start_free指向内存块的首部,指针_S_end_free指向内存块的尾端,变量_S_heap_size 记录内存池运行以来从堆空间申请的总内存大小。_S_start_free指向的内存块,初始由operator new向操作系统申请,在整个内存分配的过程中,这个内存大块的空间逐渐减少,但_S_start_free始终都会指向其剩余可用部分的起始地址,而_S_end_free始终指向内存块的尾端。

_M_allocate_chunk()内部分多种情况,进行内存分配:

1、初始分配时,即_S_start_free = 0;  _S_end_free = 0; _S_heap_size = 0;内存池暂未向系统申请内存。此时无内存可用,进入上述代码情况3的分支,先向操作系统申请内存,申请的大小为需求大小的两倍加_S_heap_size的1/16(每次内存大块没了的时候都会申请这么多)。若申请成功,将申请的内存大块分出需求部分给用户,_S_start_free和_S_end_free调整偏移,记录剩余部分,这时通过递归调用分配用户内存。

2、1场景下递归调用,此时已有内存大块,内存大块充足,进入代码情况1分支,分配内存,同时_S_start_free做好偏移。

3、用户多次申请内存,此时内存大块已不足用户需求,但仍能提供部分内存块。此时进入代码情况2分支,能分多少就给分多少,同时_S_start_free做好偏移。

4、用户再次申请内存,此时内存大块已不能提供一个内存块给用户,此时进入代码情况3分支,先将剩下的内存块归入空闲队列(如果此刻剩内存,那么归入的队列的级别一定是低于当前申请的内存级别的),此时再次回到初始分配,也就是我们上面的步骤1,调用operator new申请内存。

5、假设步骤1operator new申请失败,此时系统已经没内存了,此时还有办法,向上级空闲链表拿内存块出来。如果有的话,_S_start_free指向拿出的内存块,_S_end_free 指向尾端,递归调用,回到上面几个步骤,申请内存。

6、如果步骤5向上级空闲链表拿内存块,但拿不到,都空了,那么这会就毫无办法了,只能抛出异常了。

下图图示为operator new失败的场景

看下__pool_alloc 子类实现

template<typename _Tp>
class __pool_alloc : private __pool_alloc_base
{
private:
static _Atomic_word _S_force_new; public:
typedef std::size_t size_type;
typedef std::ptrdiff_t difference_type;
typedef _Tp* pointer;
typedef const _Tp* const_pointer;
typedef _Tp& reference;
typedef const _Tp& const_reference;
typedef _Tp value_type; template<typename _Tp1>
struct rebind
{ typedef __pool_alloc<_Tp1> other; }; #if __cplusplus >= 201103L
// _GLIBCXX_RESOLVE_LIB_DEFECTS
// 2103. propagate_on_container_move_assignment
typedef std::true_type propagate_on_container_move_assignment;
#endif __pool_alloc() _GLIBCXX_USE_NOEXCEPT { }
__pool_alloc(const __pool_alloc&) _GLIBCXX_USE_NOEXCEPT { } template<typename _Tp1>
__pool_alloc(const __pool_alloc<_Tp1>&) _GLIBCXX_USE_NOEXCEPT { } ~__pool_alloc() _GLIBCXX_USE_NOEXCEPT { } pointer
address(reference __x) const _GLIBCXX_NOEXCEPT
{ return std::__addressof(__x); } const_pointer
address(const_reference __x) const _GLIBCXX_NOEXCEPT
{ return std::__addressof(__x); } size_type
max_size() const _GLIBCXX_USE_NOEXCEPT
{ return std::size_t(-1) / sizeof(_Tp); } #if __cplusplus >= 201103L
template<typename _Up, typename... _Args>
void
construct(_Up* __p, _Args&&... __args)
{ ::new((void *)__p) _Up(std::forward<_Args>(__args)...); } template<typename _Up>
void
destroy(_Up* __p) { __p->~_Up(); }
#else
// _GLIBCXX_RESOLVE_LIB_DEFECTS
// 402. wrong new expression in [some_] allocator::construct
void
construct(pointer __p, const _Tp& __val)
{ ::new((void *)__p) _Tp(__val); } void
destroy(pointer __p) { __p->~_Tp(); }
#endif _GLIBCXX_NODISCARD pointer
allocate(size_type __n, const void* = 0); void
deallocate(pointer __p, size_type __n);
};

其重点在于allocate()和deallocate()的实现

allocate()

template<typename _Tp>
_GLIBCXX_NODISCARD _Tp*
__pool_alloc<_Tp>::allocate(size_type __n, const void*)
{
using std::size_t;
pointer __ret = 0;
if (__builtin_expect(__n != 0, true))
{
// __n超限,抛出异常
if (__n > this->max_size())
std::__throw_bad_alloc(); const size_t __bytes = __n * sizeof(_Tp); #if __cpp_aligned_new
// 内存对齐处理
if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
{
std::align_val_t __al = std::align_val_t(alignof(_Tp));
return static_cast<_Tp*>(::operator new(__bytes, __al));
}
#endif if (_S_force_new == 0)
{
if (std::getenv("GLIBCXX_FORCE_NEW"))
__atomic_add_dispatch(&_S_force_new, 1);
else
__atomic_add_dispatch(&_S_force_new, -1);
}
// 申请字节超过128,或指定强制new,则采用operator new方式申请内存
if (__bytes > size_t(_S_max_bytes) || _S_force_new > 0)
__ret = static_cast<_Tp*>(::operator new(__bytes));
else
{
// 根据申请的字节定位到所属空闲链表,后续涉及空闲链表操作,加锁处理
_Obj* volatile* __free_list = _M_get_free_list(__bytes); __scoped_lock sentry(_M_get_mutex());
_Obj* __restrict__ __result = *__free_list;
// 空闲链表为空,调用refill,申请内存并填充空闲链表
if (__builtin_expect(__result == 0, 0))
__ret = static_cast<_Tp*>(_M_refill(_M_round_up(__bytes)));
else
{
// 从该空闲链表取出内存块返回给上层
*__free_list = __result->_M_free_list_link;
__ret = reinterpret_cast<_Tp*>(__result);
}
if (__ret == 0)
std::__throw_bad_alloc();
}
}
return __ret;
}

allocate()先判断申请的内存是否超过内存池限制,或者是否启用强制new,如果是的话,就调用operator new申请内存,否则从内存池取内存块。先根据申请大小定位到对应的空闲链表,如果链表非空,就从链表取内存块,否则,调用_M_refill()申请并填充链表。

deallocate()

template<typename _Tp>
void
__pool_alloc<_Tp>::deallocate(pointer __p, size_type __n)
{
using std::size_t;
if (__builtin_expect(__n != 0 && __p != 0, true))
{
#if __cpp_aligned_new
// 内存对齐处理
if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
{
::operator delete(__p, std::align_val_t(alignof(_Tp)));
return;
}
#endif
const size_t __bytes = __n * sizeof(_Tp);
// 同步allocate处理,若申请字节超过128,或指定强制new,采用operator delete释放内存
if (__bytes > static_cast<size_t>(_S_max_bytes) || _S_force_new > 0)
::operator delete(__p);
else
{
// 将内存块回收到对应链表
_Obj* volatile* __free_list = _M_get_free_list(__bytes);
_Obj* __q = reinterpret_cast<_Obj*>(__p); __scoped_lock sentry(_M_get_mutex());
__q ->_M_free_list_link = *__free_list;
*__free_list = __q;
}
}
}

STL空间分配器源码分析(三)pool_allocator的更多相关文章

  1. STL空间分配器源码分析(二)mt_allocator

    一.简介 mt allocator 是一种以2的幂次方字节大小为分配单位的空间配置器,支持多线程和单线程.该配置器灵活可调,性能高. 分配器有三个通用组件:一个描述内存池特性的数据,一个包含该池的策略 ...

  2. STL空间分配器源码分析(一)

    一.摘要 STL的空间分配器(allocator)定义于命名空间std内,主要为STL容器提供内存的分配和释放.对象的构造和析构的统一管理.空间分配器的实现细节,对于容器来说完全透明,容器不需关注内存 ...

  3. tomcat源码分析(三)一次http请求的旅行-从Socket说起

    p { margin-bottom: 0.25cm; line-height: 120% } tomcat源码分析(三)一次http请求的旅行 在http请求旅行之前,我们先来准备下我们所需要的工具. ...

  4. 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入

    使用react全家桶制作博客后台管理系统   前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...

  5. Duilib源码分析(三)XML解析器—CMarkup

    上一节介绍了控件构造器CDialogBuilder,接下来将分析其XML解析器CMarkup: CMarkup:xml解析器,目前内置支持三种编码格式:UTF8.UNICODE.ASNI,默认为UTF ...

  6. Kafka源码分析(三) - Server端 - 消息存储

    系列文章目录 https://zhuanlan.zhihu.com/p/367683572 目录 系列文章目录 一. 业务模型 1.1 概念梳理 1.2 文件分析 1.2.1 数据目录 1.2.2 . ...

  7. ABP源码分析三:ABP Module

    Abp是一种基于模块化设计的思想构建的.开发人员可以将自定义的功能以模块(module)的形式集成到ABP中.具体的功能都可以设计成一个单独的Module.Abp底层框架提供便捷的方法集成每个Modu ...

  8. ABP源码分析三十一:ABP.AutoMapper

    这个模块封装了Automapper,使其更易于使用. 下图描述了改模块涉及的所有类之间的关系. AutoMapAttribute,AutoMapFromAttribute和AutoMapToAttri ...

  9. ABP源码分析三十三:ABP.Web

    ABP.Web模块并不复杂,主要完成ABP系统的初始化和一些基础功能的实现. AbpWebApplication : 继承自ASP.Net的HttpApplication类,主要完成下面三件事一,在A ...

随机推荐

  1. CentOS7.5安装Ansible

    安装ansible: 查看可用的ansible版本: yum list|grep ansible   方法一: 系统可用ansible版本太低,安装epel源: yum install epel-re ...

  2. 部署 Nginx +uwsgi+centos7+django+supervisor 项目

    部署CRM项目 前言 使用软件 nginx 使用nginx是为了它的反向代理功能,项目会通过Django+uWSGI+Nginx进行服务器线上部署. uWSGI python web服务器开发使用WS ...

  3. 利用公网Msf+MS17010跨网段攻击内网

    其实很多小白在对利用msf进行ms17010攻击的时候总是不成功,其实这都是因为网上大部分的文章都是写的内网对内网(192.168.1.2-->192.168.1.3)这样的案例,今天写了一下利 ...

  4. error C4996: 'std::_Copy_impl'

    以下代码段在VS2008编译可以通过,只是会提示不安全: std::vector<unsigned char> fileData ="asdfsfsfsfsdf";// ...

  5. JDBC-连接Druid数据池

    •导入Druid-jar包 •下载地址 链接:https://pan.baidu.com/s/1A3tSCT9BDXkzB7zbukzyPg?pwd=0000 提取码:0000 下载后具体导入操作和这 ...

  6. HashMap?ConcurrentHashMap?

    Java团长在csdn 2019-02-06 19:58:00  104241  收藏 666     来源:crossoverJie(ID:crossoverJie) 前言 Map 这样的 Key ...

  7. 什么是线程组,为什么在 Java 中不推荐使用?

    ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象, 也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式. 为什么不推荐使用?因为使用有很多的安全隐患吧 ...

  8. java-中的代理

    静态代理: 例子: 接口: public interface InterfaceBase { void proxy(); } 接口实现类: public class InterfaceBaseReal ...

  9. scrapy基于请求传参实现深度爬取

    请求传参实现深度爬取 请求传参: 实现深度爬取:爬取多个层级对应的页面数据 使用场景:爬取的数据没有在同一张页面中 在手动请求的时候传递item:yield scrapy.Request(url,ca ...

  10. ubuntu vmware kernel module updater

    Ubuntu 19.04 - VMWare内核模块更新程序问问题 4 3我运行了这个命令: apt-cache search linux-headers-$(uname -r)它返回输出 linux- ...