内容:

本文将介绍几种常用的内存池技术的实现,这是我最近学习各大开源的内存池技术遗留下来的笔记,其主要内容包括:

 

    • STL内存池以及类STL内存池实现
    • Memcached内存池实现
    • 固定规格内存池实现 
    • Nginx内存池实现 

 

 

一.类STL的内存池实现方式

SGI STL的内存池分为一级配置器和二级配置器,

一级配置器主要处理分配空间大小大于128Byte的需求,其内部实现就是直接使用malloc  realloc 和free.

二级配置器则使用使用free_list的数组链表的方式来管理内存,SGI的Allocate最小的分辨单位为8Byte,其free_list数组存着8*n(n=1...16)大小内存的首地址,大小同样的内存块使用链表的形式相连

  free_list[0] --------> 8 byte

  free_list[1] --------> 16 byte

  free_list[2] --------> 24 byte

  free_list[3] --------> 32 byte
  ... ...
  free_list[15] -------> 128 byte

 因为其对内存的管理的最小分辨度为8Byte,所以当我们申请的内存空间不是 8的倍数的时候,内存池会将其调整为8的倍数大小,这叫内存对齐。当然这也免不了带来内存浪费,例如我们只需要一个10Byte的大小,内存池经过内存对 齐后,会给我们一个16Byte的大小,而剩余的6Byte,在这次使用中根本没有用到。(对于chunk_allocate的优化请见探究操作系统的内存分配(malloc)对齐策略一文的末尾处)
 

类STL的内存池一般都有如下API

void* allocate(size_t __n) //外部API,分配内存
void deallocate(void* __p, size_t __n) //外部API,回收内存,以供再利用   
char*  chunk_alloc(size_t __size, int& __nobjs) //内部函数,用于分配一个大块

void* refill(size_t n)  //内部函数,用于allocate从free_list中未找到可使用的块时调用

 这种内存池的工作流程大致如下:

  • 外部调用 allocate向内存池申请内存
  • allocate通过内存对齐的方式在free_list找到合适的内存块链表头
  • 判断链表头是否为NULL,为NULL则表示没有此规格空闲的内存,如果不为NULL,则返那块内存地址,并将此块内存地址移除它对应的链表
  • 如果为NULL,则调用refill在freelist上挂载20个此规格的内存空间(形成链表),也就是保证此规格的内存空间下次请求时够用 
  • refill的内部调用了chunk_alloc函数,chunk_alloc的职责就是负责内存池的所有内存的生产,在生产的时候他为了保证下次能有内存用,所以会将空间*2,所以这个申请流程总的内存消耗为:(对需求规格内存对齐后的大小)*20*2
下面举一个例子来简单得说明一下:
  •     当第一次调用chunk_alloc(32,10)的时候,表示我要申请10块__Obje(free_list), 每块大小32B,此时,内存池大小为0,从堆空间申请32*20的大小的内存,把其中32*10大小的分给free_list[3]。
  •    我再次申请64*5大小的空间,此时free_list[7]为0, 它要从内存池提取内存,而此时内存池剩下320B,刚好填充给free_list[7],内存池此时大小为0。
  •    第三次请求72*10大小的空间,此时free_list[8]为0,它要从内存池提取内存,此时内存池空间不足,再次从堆空间申请72*20大小的空间,分72*10给free_list用。 

首次申请20Byte后的状态图: 

 内存池技术畅想
    




内存池技术畅想

 在未设置预分配的STL内存池中,某个中间状态的整体图

内存池技术畅想
    




内存池技术畅想 

 

由于STL源码可阅读性不强,各种宏等等满目不堪,所以我这里就不贴SGI 的源码了,我在这里贴一个简单易懂的山寨版本, 基本的思路是一模一样的,这个实现没有了一级和二级配置器,而是在需要的时候直接malloc或者从free_list找。

View Code 

 

 

 二.MemCached内存池实现

与 类STL内存池不同的是, 用于缓存的内存池不是解决小对象的内存分配可能导致堆内存碎片多的问题,缓存内存池要为缓存系统的所有存储对象分配空间,无论大小。因为缓存系统通常对其 占用的最大内存有限制,所以也就不能在没有空间用的时候随便malloc来实现了。 MemCached的内存池的基本想法是避免重复大量的初始化和清理 操作。

 
Memcached 中内存分配机制主要理念 
1.  先为分配相应的大块内存,再在上面进行无缝小对象填充 
2.  懒惰检测机制,Memcached 不花过多的时间在检测各个item对象是否超时,当 get获取数据时,才检查item对象是否应该删除,你不访问,我就不处理。 
3.  懒惰删除机制,在 memecached 中删除一个 item对象的时候,并不是从内存中释放,而是单单的进行标记处理,再将其指针放入 slot回收插糟,下次分配的时候直接使用。


MemCached内存池Slab Allocation的主要术语
Page
分配给Slab的内存空间,默认是1MB。分配给Slab之后根据slab的大小切分成chunk。

内存池技术畅想
    




内存池技术畅想 

Chunk
用于缓存记录的内存空间。
Slab Class

特定大小的chunk的组。 

内存池技术畅想
    




内存池技术畅想 

Memcached 的内存分配以page为单位,默认情况下一个page是1M ,可以通过-I参数在启动时指定。如果需要申请内存 时,memcached会划分出一个新的page并分配给需要的slab区域。Memcached并不是将所有大小的数据都放在一起的,而是预先将数据空 间划分为一系列slabs,每个slab只负责一定范围内的数据存储,其大小可以通过启动参数设置增长因子,默认为1.25,即下一个slab的大小是上 一个的1.25倍。如 下图,每个slab只存储大于其上一个slab的size并小于或者等于自己最大size的数据。如下图所示,需要存储一个100Bytes的对象时,会 选用112Bytes的Slab Classes

 内存池技术畅想
    




内存池技术畅想

 基于这种实现的内存池也会遇到STL内存池一样的问题,那就是资源的浪费,我只需要100Byte的空间,你却给了我128Bytes,剩余的28Bytes就浪费了

内存池技术畅想
    




内存池技术畅想 

 

其主要API:

slabs_init() 
slab初始化,如果配置时采用预分配机制(prealloc)则在先在这使用malloc分配所有内存。 
再根据增长因子factor 给每个 slabclass 分配容量。 
slabs_clsid() 
计算出哪个 slabclass 适合用来储存大小给定为 size的item, 如果返回值为 0则存储的物件过大,无法进行存储。 
do_slabs_alloc() 
在这个函数里面,由宏定义来决定采用系统自带的 malloc 机制还是 memcached的slab机制对内存进行分配,理所当然,在大多数情况下,系统的malloc会比slab慢上一个 数量级。 分配时首先考虑slot 内的空间(被回收的空间),再检查 end_page_ptr 指针指向的的空闲空间,还是没有的空间的话,再试试分配新的内存。如果所有空间都用尽的 时候, 则返回NULL表示目前资源已经枯竭了。 
do_slabs_free() 
首先检查当目前的插糟是否已经达到可用总插糟的总容量,如果达到就为其重新分配空间,再将该回收的 item的指针插入对应当前 id的 slabclass 的插糟 (slots) 之中。  

 

 关 于MemCached还有个问题需要解释下,在预分配的场景下,有的同事认为MemCached不适合大量存储某个特定大小范围内的对象,他们认为预分配 的条件下,每个SlabClasses的总大小是固定的(为一个Page),其实不是,MemCached预分配并不会消耗掉所有的内存,在请求空间的时 候,如果发现这个型号的Chunks都被用完了,就会新增一个分页到这个Slab Classes,所以是不会出现那位同事说的那个问题的...(可见代码slabs.c中do_slabs_alloc函数中 do_slabs_newslab的调用)

 

三.固定大小内存池

上面两种内存池的实现,都会造成一定程度的内存浪费,如果我存的对象大小基本是固定的,尽管有很多不同的对象,有没有不会浪费内存的的简单方式呢?

既然需要存的对象大小是固定的,那么我们的内存池对于内存的管理可以这样实现:

class IovecContainer
{
public:
list<char*> m_objList;

}; 

class MemoryPool
{
public:
void* allocate(size_t __n)  //外部API,分配内存
void deallocate(void* __p, size_t __n) //外部API,回收内存,以供再利用   
private:
map<int, IovecContainer* > m_mapPool;

char*  chunk_alloc(size_t __size, int& __nobjs) //内部函数,用于分配一个大块
void* refill(size_t n)  //内部函数,用于allocate从free_list中未找到可使用的块时调用 

}; 

这样的实现对于这个特定的需求非常好用,不回浪费掉剩余空间,但是这样的实现局限性就高了,我们不能用这个内存池来存储大小不定的对象(如string),如果用了,此内存池形同虚设,并且还浪费内存,所以具体怎么选择还是要看需求来定... 

 

 四.Nginx内存池实现
 关于Nginx内存池实现网上有比较多的分析文章,这里我就不重复造轮子了,直接贴链接,有兴趣的可以关注下:

 

http://blog.csdn.net/v_july_v/article/details/7040425
http://bbs.chinaunix.net/thread-3626006-1-1.html;
http://blog.csdn.net/livelylittlefish/article/details/6586946;
http://blog.chinaunix.net/space.php?uid=7201775;

淘宝数据共享平台博客:http://www.tbdata.org/archives/1390   


 

参考资料:http://www.cnblogs.com/sld666666/archive/2010/07/01/1769448.html

 

转自:http://www.cnblogs.com/Creator/archive/2012/04/11/2430592.html

 

 

 

 

相关文章: