{linux程序}之内存池

http://www.cnblogs.com/bangerlee/archive/2011/08/31/2161421.html

引言

C/C++下内存管理是让几乎每一个程序员头疼的问题,分配足够的内存、追踪内存的分配、在不需要的时候释放内存——这个任务相当复杂。而直接使用系统调用malloc/free、new/delete进行内存分配和释放,有以下弊端:

  1. 调用malloc/new,系统需要根据“最先匹配”、“最优匹配”或其他算法在内存空闲块表中查找一块空闲内存,调用free/delete,系统可能需要合并空闲内存块,这些会产生额外开销
  2. 频繁使用时会产生大量内存碎片,从而降低程序运行效率
  3. 容易造成内存泄漏


内存池(memory pool)是代替直接调用malloc/freenew/delete进行内存管理的常用方法,当我们申请内存空间时,首先到我们的内存池中查找合适的内存块,而不是直接向操作系统申请,优势在于:

  1. 比malloc/free进行内存申请/释放的方式快
  2. 不会产生或很少产生堆碎片
  3. 可避免内存泄漏


内存池设计

看到内存池好处这么多,是不是恨不能马上抛弃malloc/free,投奔内存池的怀抱呢?且慢,在我们自己动手实现内存池之前还需要明确以下几个问题:

  1. 内存池的空间如何获得?是程序启动时分配一大块空间还是程序运行中按需求分配?
  2. 内存池对到来的内存申请,有没有大小的限制?如果有,最小可申请的内存块为多大,最大的呢?
  3. 如何合理设计内存块结构,方便我们进行内存的申请、追踪和释放呢?
  4. 内存池占用越多空间,相对应其他程序能使用的内存就越少,是否要设定内存池空间的上限?设定为多少合适呢?

带着以上问题,我们来看以下一种内存池设计方案。


内存池实现方案一

这里下载该内存池实现的源码。

首先给出该方案的整体架构,如下:

内存池实现架构

图1.内存池架构图

结构中主要包含blocklist 和pool这三个结构体,block结构包含指向实际内存空间的指针,前向和后向指针让block能够组成双向链表;list结构中free指针指向空闲 内存块组成的链表,used指针指向程序使用中的内存块组成的链表,size值为内存块的大小,list之间组成单向链表;pool结构记录list链表的头和尾。


内存跟踪策略

该方案中,在进行内存分配时,将多申请12个字节,即实际申请的内存大小为所需内存大小+12。在多申请的12个字节中,分别存放对应的list指针(4字节)、used指针(4字节)和校验码(4字节)。通过这样设定,我们很容易得到该块内存所在的list和block,校验码起到粗略检查是否出错的作用。该结构图示如下:

结点头12字节说明

图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的内存),申请和释放的频率基本保持一致(因申请多而释放少会占用过多内存,最终导致系统崩溃)。


这篇文章讲解了内存管理的基本知识,以一个简单的内存池实现例子作为敲门砖,引领大家认识内存池,下一篇为内存池进阶文章,讲解apache服务器中内存池的实现方法。

http://hi.baidu.com/haven2002/item/bb523eca223b3c09ac092f52

一 linux内存管理以及内存碎片产生原因


                     
        最底层使用伙伴算法管理内存页面。系统将所有空闲内存页面分10个组,每个组中的内存块大小依次是1,2,4......512个内存页面,每组中的内存 块大小相同,并且以链表结构保存。大小相同,并且内存地址连续的两个内存块称为伙伴。伙伴算法的中心思想就是将成为伙伴的空闲内存合并成一个更大的内存 块。
        os中使用get_free_page获取空闲页面,如果找不到合适大小的空闲页面,则从更大的组中找到空闲内存块,分配出去,并将剩余内存分割,插入到 合适的组中。当归还内存时,启动伙伴算法合并空闲内存。如果不停的申请内存,并且部分归还,但归还的内存不能成为伙伴,长期运行后,所有内存将被分割成不 相邻的小块,当再次申请大块内存时,则可能由于找不到足够大的连续内存块而失败,这种零散的不相邻的小块内存称之为内存碎片。当然这只是理论上的说明,伙 伴算法本身就是为了解决内存碎片问题。

二  malloc子系统内存管理(dlmalloc)
        应用层面的开发并不是直接调用sbrk/mmap之类的函数,而是调用malloc/free等malloc子系统提供的函数,linux上安装的大多为 DougLea的dlmalloc或者其变形ptmalloc。下面以dlmalloc为例说明malloc工作的原理。
1 dlmalloc下名词解释:
   boundary tag:
 边界标记,每个空闲内存块均有头部表识和尾部标识,尾部表识的作为是合并空闲内存块时更快。这部分空间属于无法被应用层面使用浪费的内存空间。
   smallbins: 小内存箱。dlmalloc将8,16,24......512大小的内存分箱,相临箱子中的内存相差8字节。每个箱子中的内存大小均相同,并且以双向链 表连接。
   treebins: 树结构箱。大于512字节的内存不再是每8字节1箱,而是一个范围段一箱。比如512~640, 640~896.....每个箱子的范围段依次是128,256,512......。每箱中的结构不再是双向链表,而是树形结构。
   dv chunk:  当申请内存而在对应大小的箱中找不到大小合适的内存,则从更大的箱中找一块内存,划分出需要的内存,剩余的内存称之为dv chunk.
   top chunk: 当dlmalloc中管理的内存都找不到合适的内存时,则调用sbrk从系统申请内存,可以增长内存方向的chunk称为top chunk.
2 内存分配算法
        从合适的箱子中寻找内存块-->从相临的箱子中寻找内存块-->从dv chunk分配内存-->从其他可行的箱子中分配内存-->从top chunk中分配内存-->调用sbrk/mmap申请内存
3 内存释放算法
       临近内存合并-->如属于top chunk,判断top chunk>128k,是则归还系统
                              -->不属于chunk,则归相应的箱子

dlmalloc还有小内存缓存等其他机制。可以看出经过dlmalloc,频繁调用 malloc/free并不会产生内存碎片,只要后续还有相同的内存大小的内存被申请,仍旧会使用以前的合适内存,除非大量调用malloc之后少量释放 free,并且新的malloc又大于以前free的内存大小,造成dlmalloc不停的从系统申请内存,而free掉的小内存因被使用的内存割断,而 使top chunk<128k,不能归还给系统。即便如此,占用的总内存量也小于的确被使用的内存量的2倍(使用的内存和空闲的内存交叉分割,并且空闲的内 存总是小于使用的内存大小)。因此可以说,在没有内存泄露的情况,常规频繁调用malloc/free并不会产生内存碎片。

三  应用层内存池
即便没有内存碎片问题,应用层 仍然需要内存池,原因如下:
1 使用的内存固定可控  稳定性角度
2 减少与内核态交互的可能  性能角度
3 减少互斥操作  性能角度,各个线程直接调用malloc,极有可能有线程进入竞态条件,陷入内核态。
其中稳定性只能是聊以自慰的说法,os本身都不可信,哪里还 来得稳定性的说法。最重要的出发点,是应用层控制内存,提高应用层性能。那么如何创建使用内存池,才能充分提高内存使用的性能呢?我们先从著名的内存池看 起。

四  常见内存池
变长内存池:
1 apr pool : 针对业务处理,将整个业务场景分段,不同阶段使用不同类型内存池,内存归还池后并不能被再次使用,而是池本身可以被重复使用,特浪费内存。
2 obstack: gcc自带变长内存池

定长内存池:
1 SGI STL:  针对小内存做池,字节长度为8,16......128共16个池,每个池中内存大小相同,使用链表连接,小内存采取永不归还malloc子系统策略,大 于128直接调用malloc。SGI STL为gcc携带的stl实现。vc以及bc携带的stl,虽然也有allocator对象,但并没有真正的池,而是直接调用malloc。

2 boost/loki
两种内存池采用类似的底层算法,以loki为例子,首次申请一块定长内存,loki会一次性申请255个,之后再次申请从 该池中直接获取,从池中申请释放内存算法示例如下:


(1)首次申请内存后,对空闲内存编号,并且前一个内存保存下一块内存的编号,一变量NextBlock保存下次可以申请出的内存块,首次 NextBlock=0
(2)当申请出3块内存后,NextBlock=3
(3)当第二块内存归还时,根据内存基址找到所属的内存 chunk,对比chunk基址以及该持中内存块长度,找到该块编号,尾部编号保存NextBlock,NextBlock=1
(4)再次归还第 三块,第三块尾部保存上次的NextBlock,NextBlock=2
(5)再次申请内存,根据NextBlock指定分配出的内 存,NextBlock等于该块内存尾部指向的值1.
loki和boost对内存的处理上有稍许差别,包括内存的组织层次上,这些差别我个人看都 是loki相对于boost的缺点。
loki/boost代表了当前内存池的最高水准,该池无任何冗余头部(free的内存才保存冗余信息),更 节省内存;另外分配释放内存快速,只有固定极少的常数步骤计算。
以上算法只是给内存池的后续使用打下坚实基础,并没有给出内存池的使用方式。

五 内存池使用方式分类
      loki给出了内存池使用的策略,分以下3种:
1 全局内存池  所有相同长度的内存申请,使用同一个内存池,不同长度内存申请使用不同内存池。对池中的内存进行申请释放操作时,对池执行加锁操作。
2 对象内存池 每个对象一个内存池。内存申请释放执行加锁操作。
3 线程内存池 相同长度的内存并且在同一个内存中的内存申请释放使用线程内存池,内存申请释放不执行加锁操作。

     对比第三部分,应用层使用内存池的原因。显然全局内存池并没有解决性能问题,各线程并发申 请内存,仍然存在类似直接调用malloc的互斥问题。
而对象内存池将这种互斥进一步降低,仅仅跨线程对同一对象申请释放内存才会遇到互斥问题。
而 线程内存池无疑是最高效的,没有锁开销。
      可见最佳的内存池使用方式为,对存在跨线程操作的对象,使用对象内存池,对于只在同一线程内 操作的对象使用线程内存池。对象可以通过重载对象的operator new, operator delete等实现。
      boost 库极其适合进一步封装,供对象内存池和线程内存池(结合thread-specific storage)使用。

六 Linux下内存池终结者
       tcmalloc, 可以通过cache等机制智能判断应该使用对象内存池还是线程内存池,编码不需要任何额外策略,直接使用new/delete,只要最后连接上 libtcmalloc之类的库即可。可惜仅仅支持linux。
      已有明确的测试数据支持,链接tcmalloc后,原cpu居高不 下,突高的服务器程序,大大减少了直接调用malloc的互斥竞态条件出现,cpu趋于平稳。典型的就是linux下链接tcmalloc后重编译的 mysql。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值