内存池技术细节剖析.docx
- 文档编号:9213944
- 上传时间:2023-02-03
- 格式:DOCX
- 页数:10
- 大小:384.13KB
内存池技术细节剖析.docx
《内存池技术细节剖析.docx》由会员分享,可在线阅读,更多相关《内存池技术细节剖析.docx(10页珍藏版)》请在冰豆网上搜索。
内存池技术细节剖析
引言
C/C++下内存管理是让几乎每一个程序员头疼的问题,分配足够的内存、追踪内存的分配、在不需要的时候释放内存——这个任务相当复杂。
而直接使用系统调用malloc/free、new/delete进行内存分配和释放,有以下弊端:
1.调用malloc/new,系统需要根据“最先匹配”、“最优匹配”或其他算法在内存空闲块表中查找一块空闲内存,调用free/delete,系统可能需要合并空闲内存块,这些会产生额外开销
2.频繁使用时会产生大量内存碎片,从而降低程序运行效率
3.容易造成内存泄漏
内存池(memorypool)是代替直接调用malloc/free、new/delete进行内存管理的常用方法,当我们申请内存空间时,首先到我们的内存池中查找合适的内存块,而不是直接向操作系统申请,优势在于:
1.比malloc/free进行内存申请/释放的方式快
2.不会产生或很少产生堆碎片
3.可避免内存泄漏
内存池设计
看到内存池好处这么多,是不是恨不能马上抛弃malloc/free,投奔内存池的怀抱呢?
且慢,在我们自己动手实现内存池之前还需要明确以下几个问题:
1.内存池的空间如何获得?
是程序启动时分配一大块空间还是程序运行中按需求分配?
2.内存池对到来的内存申请,有没有大小的限制?
如果有,最小可申请的内存块为多大,最大的呢?
3.如何合理设计内存块结构,方便我们进行内存的申请、追踪和释放呢?
4.内存池占用越多空间,相对应其他程序能使用的内存就越少,是否要设定内存池空间的上限?
设定为多少合适呢?
带着以上问题,我们来看以下一种内存池设计方案。
内存池实现方案一(转自)
从这里下载该内存池实现的源码。
首先给出该方案的整体架构,如下:
图1.内存池架构图
结构中主要包含block、list和pool这三个结构体,block结构包含指向实际内存空间的指针,前向和后向指针让block能够组成双向链表;list结构中free指针指向空闲内存块组成的链表,used指针指向程序使用中的内存块组成的链表,size值为内存块的大小,list之间组成单向链表;pool结构记录list链表的头和尾。
内存跟踪策略
该方案中,在进行内存分配时,将多申请12个字节,即实际申请的内存大小为所需内存大小+12。
在多申请的12个字节中,分别存放对应的list指针(4字节)、used指针(4字节)和校验码(4字节)。
通过这样设定,我们很容易得到该块内存所在的list和block,校验码起到粗略检查是否出错的作用。
该结构图示如下:
图2.内存块申请示意图
图中箭头指示的位置为内存块真正开始的位置。
内存申请和释放策略
申请:
根据所申请内存的大小,遍历list链表,查看是否存在相匹配的size;
存在匹配size:
查看free时候为NULL
free为NULL:
使用malloc/new申请内存,并将其置于used所指链表的尾部
free不为NULL:
将free所指链表的头结点移除,放置于used所指链表的尾部
不存在匹配size:
新建list,使用malloc/new申请内存,并将其置于该list的used所指链表尾部
返回内存空间指针
释放:
根据内存跟踪策略,获取list指针和used指针,将其从used指针所指的链表中删除,放置于free指针所指向的链表
对方案一的分析
对照“内存池设计”一节中提出的问题,我们的方案一有以下特点:
1.程序启动后内存池并没有内存块,到程序真正进行内存申请和释放的时候才接管内存块管理;
2.该内存池对到来的申请,对申请大小并不做限制,其为每个size值创建链表进行内存管理;
3.该方案没有提供限定内存池大小的功能
结合分析,可以得出该方案应用场景如下:
程序所申请的内存块大小比较固定(比如只申请/释放1024bytes或2048bytes的内存),申请和释放的频率基本保持一致(因申请多而释放少会占用过多内存,最终导致系统崩溃)。
1.内存分配结点
在了解整个内存池架构前,我们先来了解APR内存池中最基本的单元——内存分配结点。
内存分配结点被用来描述每次分配的内存块,对应的结构名为apr_memnode_t,定义在文件apr_allocator.h中,其定义如下:
?
/*basicmemorynodestructure*/
structapr_memnode_t{
apr_memnode_t*next;/** apr_memnode_t**ref;/** apr_uint32_tindex;/** apr_uint32_tfree_index;/** char*first_avail;/** char*endp;/** }; 即使结构中的每个字段,源文件中都给出了注释,但对于每个字段的用途,还是很难让人理解。 这里先给出每个字段的简略解析: 1.next: 指向下一个结点的指针; 2.ref: 指向上一结点的next结点,上一结点的next指向本结点,因此**ref就是本结点自身; 3.index: 既指示了该结点的大小,同时指示了该结点所在链表的索引下标值; 4.free_index: 所描述的内存块中未被占用的空间(这里和上面的index和它们字面意思有出入,理解的时候要留意); 5.first_avail: 指向可用空间开始位置的指针; 6.endp: 指向可用空间结尾位置的指针。 该结点示意图如下: 2.内存分配器 在ARP内存池中,使用内存分配器对内存分配结点进行管理,它在apr_pools.c中定义如下: ? structapr_allocator_t{ apr_uint32_tmax_index; apr_uint32_tmax_free_index; apr_uint32_tcurrent_free_index; apr_pool_t*owner; apr_memnode_t*free[MAX_INDEX]; }; 1.max_index: free指针数组的下标,free[max_index]指向已有的最大内存块链表; 2.max_free_index: 内存分配器所能容纳的最大内存空间数值; 3.current_free_index: 内存分配器中还能接收的空间大小,与max_free_index结合使用,解决限制内存池空间大小的问题; 4.owner: 指示该分配器属于哪个内存池; 5.free: 指向一组链表的头结点,该链表中每个结点指向内存结点组成的链表,MAX_INDEX为20。 内存分配器及其管理的内存结点图示如下: 从上图我们可以清晰地看出,free数组的下标从1到MAX_INDEX-1,分别指向一条结点大小固定的链表,下标增加1,结点的大小增加4k,因此free[MAX_INDEX]所指向的链表的结点大小为84k,这也是内存池使用者所能申请的最大”规则结点“,超过该大小的结点将下标0指向的链表进行管理。 要明白free数组下标和结点大小的关系,我们需要知道宏定义APR_ALIGN: ? #defineAPR_ALIGN(size,boundary)\ (((size)+((boundary)-1))&~((boundary)-1)) 该宏所做的无非就是计算出最接近size的boundary的整数倍的整数。 通常情况下size大小为整数即可,而boundary则必须保证为2的倍数。 比如APR_ALIGN(7,4)为8;APR_ALIGN(21,8)为24;APR_ALIGN(21,16)则为32。 对于每次空间申请,APR先对齐空间大小: ? size=APR_ALIGN(size+APR_MEMNODE_T_SIZE,4096); 结果是size的值变成4096(4k,2的12次方)的倍数,最后,通过左移与我们的下标对应起来: ? index=(size>>BOUNDARY_INDEX)-1;//BOUNDARY_INDEX=12 3.内存池结点 APR的内存池结点,在apr_pools.c文件中定义如下: ? structapr_pool_t{ …… apr_allocator_t*allocator; apr_memnode_t*active; …… }; 该结构中的字段比较多,我们主要关注以上列出的两个字段。 1.allocator: 指向相应的内存分配器; 2.active: 指向使用中内存链表的指针 内存池结点及其管理的内存结点如下图所示: 需要留意的是虽然该结构字面意义上为“内存池结构”,但是它负责管理使用中的内存。 APR内存池的内存管理 对APR内存池各个结构有了初步了解之后,我们来看APR中是如何利用这些结构进行内存管理的。 1.内存申请 内存申请的核心函数是allocator_alloc函数,参数为一个指向内存分配器的指针和所要申请空间的大小,内存分配就是对内存分配器进行操作(以下列出的字段参见“内存分配器”章节),其内存申请的策略如下: 1.根据申请空间的大小size,生成索引index,如果索引数值在1~max_index范围内,那就在index~max_index范围内的链表中返回一块内存; 2.如果索引数值index>max_index,则在free[0]链表中查找一块合适的内存; 3.经上两步仍未找到空闲内存块,则通过malloc(size)返回一块新生成的内存。 2.内存释放 在内存申请中提到,假如内存分配器中没有合适的内存块,将会调用malloc获取一块,但是新分配的内存并不挂接到内存分配器链表中,而是在调用allocator_free进行内存释放的时候,内存才可能挂到内存分配器链表上。 内存释放策略如下: 1.如果结点的大小超过了完全释放的阙值max_free_index,那么我们就不能将其简单的归还到索引链表中,而必须将其完全归还给操作系统; 2.如果index 因此可以将该结点返回到对应的“规则链表”中; 3.如果结点超过了“规则结点”的范围,但是并没有超过阙值max_free_index,此时我们则可以将其置于“索引0”链表的首部中。 3.内存池结点管理的内存 刚开始,由内存分配器管理的链表并没有挂接任何内存,也就是说内存池是空的,当我们申请内存时,必然进行“内存申请”中的第三步操作,新分配的内存就由我们的内存池结点进行管理。 先来看用于内存池结点管理的一个宏定义: ? /*Nodelistmanagementhelpermacros;list_insert()inserts'node' *before'point'.*/ #definelist_insert(node,point)do{\ node->ref=point->ref;\ *node->ref=node;\ node->next=point;\ point->ref=&node->next;\ }while(0) 在内存池结点初次建立时,链表状态如下图: 再次申请一个结点,运行list_insert(node,point)后,结果如下图: 我们可以通过allocator_free调用或apr_pool_clear、apr_pool_destroy调用(它们内部调用allocator_free)进行内存池结点的释放,所释放的内存将按照“内存释放”中的策略归还内存池或操作系统。 小结 free数组下标的双重含义(即是数组下标,又指示内存块大小)、通过阙值max_free_index限制内存池大小,这些都较难理解,但也是是APR内存池实现中出彩的部分,另外,APR内存池还涉及到父/子/兄弟内存池、内存池生命周期的概念,同学们可以通过文章末尾给出的里面的参考文档,进行更深入地学习。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 内存 技术 细节 剖析