内存池的设计

一【内存池概述】

内存池是一种存在于进程中,对程序运行时动态分配的内存进行管理的机制。它主要有三个功能:1减少内存碎片;2防止内存泄露;3减少因频繁请求内存动态分配而造成系统调用过于频繁。

第一点:内存碎片分为内部碎片和外部碎片。内部碎片:就是系统给你分配了一个内存块,你只使用了其中一部分,那剩余的一部分就是内部碎片了。而外部碎片:就是系统内存空间里面夹杂在两个已分配内存块之间的空闲内存。一般情况下,我们所说的内存碎片就是指外部碎片,因为外部碎片要比内部碎片造成更大的危害。内部碎片会伴随着内存的释放而消失,但外部碎片则会一直存在于进程的生命期里面,等到系统里面的外部碎片多到一定程度时,即使系统空闲内存的总大小大于你请求的内存大小,但是由于找不到连续的合适的地址空间,请求依然失败,进程也因此崩溃。外部碎片的问题很难完全解决,最常用的办法就是按照某种规律来分配内存大小,比如:按照2的n次方或者4k为单位分配,这样就提高了每一个内存块的重复使用的可能性,以此来降低碎片的危害,一般的内存池中就是使用该方法的。不过,在linux内核里面则使用了伙伴算法(Buddy System)来解决碎片问题,该算法将一块连续的内存空间划分为大小为2的n次方的内存块,然后每一个内存块都与地址相邻且大小一样的内存块结为伙伴。当两个伙伴块都处于空闲状态时,系统就会将这两个内存块合并成一个新的内存块,然后系统又会检查这个新的内存块的伙伴块是否处于一个空闲状态,如果是空闲状态,则继续合并,直到最终块的伙伴处于一个非空闲状态,或者所有内存块都被合并。

第二点:内存泄露问题一直以来是C++/C程序员头疼的问题,以前出现这个问题时很难调试,不过现在有了valgrind之类的工具要好点。内存池则通过对动态分配内存阶段性的释放(比如:层次结构的内存池会在某个会话阶段终止时或者某个连接结束时释放内存子池),在一定程度上避免了该问题的发生。为什么说是“在一定程度上”,那是因为如果对于一些内存的动态分配(比如:进程运行初期)或者没有具备阶段性释放内存机制的内存池(非层次结构的内存池),是无法判断该不该释放该内存块的。

第三点:我们知道在调用malloc库函数时,先按照first-fit策略查找已分配的内存块中有没有合适的内存块,如果找到,则将该内存块标记为已分配,并将地址返回。可是,当运气不好,没找到的时候,就要通过brk系统调用来扩展段地址获取内存块。这两个步骤都需要一定的开销,尤其是brk调用,涉及到用户态与内核态的切换,开销要更大一些。

如果你是开发一个用户交互式的桌面应用,一般来说是用不到内存池的,因为这种程序运行的时间相对不长,就算内存那块儿出了问题导致进程崩溃,重启下程序就可以了。但是,你要是开发一个后台服务系统,那就要考虑通过使用内存池来提高系统的稳定性了。

二【内存池的设计】

内存池的设计差不多都是通过一组空闲列表来实现的,每组列表维护一系列大小相同的空闲块。


如STL中的pool_allocator。STL中的pool_allocator有个特点:它的每个内存块的管理信息(在空闲块列表中指向下一个空闲块的指针)直接放在所分配的内存块中,当该内存块被分配出去之后整个内存块都可以被用户使用。该机制是通过联合体来实现的。

    typedef union obj {   
        union obj *free_list_link;   
        char client_data[1];   
    }obj;  


其它的一些内存池则是将内存块分配两部分:管理区和空闲区,管理区的大小是固定的,用来记录空闲区的大小、是否被使用、指向下一个空闲块的指针等信息,空闲区则留给用户使用。

每个空闲列表管理2的(2+i)次方大小的空闲规则块。当用户申请一块空闲块时,会根据用户申请的大小选择一块合适的空闲块,计算公式为:(size+(boundary-1))&~((boundary-1))。当找不到合适的内存块,就使用malloc和new向系统申请内存空间,这时为了减少系统调用次数,一次会多申请一些内存块,其中一块直接返回给申请者,剩下的就填充空闲列表。

一些网络服务器(apache 、proftp)里面使用了层次化的内存池,它将内存池分为:分配子(allocator)和内存池(pool)两部分。分配子类似于上文提到的空闲列表,只不过它的0号的空闲块列表比较特殊,放的是比最大规则块还要大的内存块,这样就提高了内存分配的灵活性,如下图。

当用户申请一个较大的内存块,但是并没有超出内存池大小的上限时,就可以将该块放入0号列表管理。而内存池则是一个简单的空闲列表,空闲列表中的内存块都是由分配子分配的,而且是按照从大到小的顺序链接的,如图。

另外,内存池可以按照树形结构将他们组织在一起,父子内存池可以共享同一个分配子。

为什么要将内存池以树形结构组织呢?其实,这样做说到底就是为网络服务器的业务逻辑服务的。我们知道一般的网络服务器器可以提供多个虚拟的服务器,我们为每个虚拟服务器分配一个内存池,然后又为该服务器上的每个连接分配一个内存池,最后为该连接中的每一个会话分配一个内存池。因为服务器、连接、会话之间的关系是树形结构的,所以这种层次化的内存池可以很方便的与这种业务结构对应起来,当用户关闭某个服务器、或者断开某个网络连接时,只要调用父节点的内存池释放函数,就可以将它和所属的子池一并释放,非常方便。

当然,如果你整个系统里面只使用一个内存池对全局的内存进行管理也是可以的,但是这存在一些缺点:1.当你在某个会话里面申请了一个内存块,最后缺因为出现了异常导致流程跳转,使得这个内存块没有归还内存池,这样这个内存块在进程生命期内就无法再次被利用;而层次化的内存池只要等到该会话生命期结束,会话的子池生命期结束时,它所有的内存就会被释放(如果会话子池的分配子用的是父池,那么内存就归还给父池,如果分配子用的是自己的,那么就连着分配子一起还给系统)。2.当网络服务器运行一段时间之后,就有可能产生大量的内存块,如果这么多内存块都由一个内存池进行管理,就会变的很低效。


【参考】

《stl源码剖析》http://ishare.iask.sina.com.cn/f/10466019.html

http://blog.csdn.net/tingya/article/details/547322

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章