内存池技术畅想

Posted CPP开发者

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了内存池技术畅想相关的知识,希望对你有一定的参考价值。


来源: 大熊先生

链接:http://www.cnblogs.com/Creator/archive/2012/04/11/2430592.html


内容:


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

  • STL 内存池以及类 STL 内存池实现

  • Memcached 内存池实现

  • 固定规格内存池实现

  • nginx 内存池实现


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


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


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



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,则调用 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 找。


内存池技术畅想
内存池技术畅想
内存池技术畅想
内存池技术畅想
内存池技术畅想
内存池技术畅想
内存池技术畅想
内存池技术畅想
内存池技术畅想
内存池技术畅想
内存池技术畅想
内存池技术畅想
内存池技术畅想
内存池技术畅想


二. 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


【今日微信公号推荐↓】


更多推荐请看

以上是关于内存池技术畅想的主要内容,如果未能解决你的问题,请参考以下文章

简易内存池

内存池进程池线程池

揭秘池化技术--内存池的实现

内存池介绍

实现高并发内存池

高并发内存池