本篇介绍程序库中的内存池算法。

 **3.1**
    首先来看一下内存池算法用到的一些类型和常量。

    下面的类型和常量定义在头文件 yc_definition.h 内;
    硬件字节类型 :ylib_byte_t;
    硬件机器字类型 :ylib_word_t;
    一个字节所占用的二进制位数 :BYTE_BITS;
    字节类型的最大值 :YLIB_BYTE_MAX;
    字类型的最大值 :YLIB_WORD_MAX;

    下面的类型和常量定义在实现文件 yc_memory.c 内;

    内存页类型:
    typedef struct memory_page
    {
        struct memory_page*  next;
        ylib_word_t          use;
    } mempage_t;

    内存链类型:
    typedef struct memory_list
    {
        size_t      useable;
        mempage_t*  buffer[MEMORY_POOL_BUFFER];
        mempage_t*  first;
    } memlist_t;

    内存池每个链的字节对齐数 :MEMORY_POOL_ALIGN;
    内存池每个链的缓存页数:MEMORY_POOL_BUFFER;
    内存池管理的最小字节数 :MEMORY_POOL_MIN_BYTES;
    内存池管理的最大字节数 :MEMORY_POOL_MAX_BYTES;
    每个内存页包含的内存块数 :MEMORY_POOL_BLOCKS;
    内存池包含的内存链数 :MEMORY_POOL_LISTS。


    **3.2**
    内存池原理图:
    索引:0      1     2     3           ......         MEMORY_POOL_LISTS - 1
    --------------------------------------------------------------------------
    | useable |     |     |     |        ......        |                     |
    --------------------------------------------------------------------------
    |  buffer |
    -----------
    |  first  |
    -----------
         |
         |
         |   --------
         ->| next |--
             |------|   |
             | use  |   |
             |------|   |
             | page |   |
             --------   |
                        |
     ----------
     |
     |   --------
     ->| next | --> NULL
         |------|
         | use  |
         |------|
         | page |
         --------

    实际上,内存池的全称应该是小内存块内存池,内存池管理的内存块的上限是
MEMORY_POOL_MAX_BYTES 个字节,超过这个数值的申请要求会被转调用至 malloc 函数,
正因为此,所以在释放内存的时候必须显示地指定要释放的内存块的大小,这样才能确保
正确地释放。
    内存池是一个 memlist_t 指针类型的数组,数组的大小为 MEMORY_POOL_LISTS 数组内
的每个指针元素指向一条内存链,每个内存链由大小不同的内存页以单向链表的形式组成。
为了方便管理,凡是向内存池申请小于等于 MEMORY_POOL_MAX_BYTES 个字节的内存块都会
被自动调整为 MEMORY_POOL_ALIGN 的倍数,而每个内存链则管理着不同大小的内存块,可
用该公式计算:内存链管理的内存块大小 = MEMORY_POOL_MIN + 索引 * MEMORY_POOL_ALIGN。
    由于每个内存链管理的内存块都很小,为了减少向系统申请内存的次数和内存碎片,内
存链的每个节点并不是内存块,而是一种比内存块大的多的被称为内存页的数据结构。内存
页分由三部分组成:
    1、use 是一个管理内存页中的内存块分配状态的机器字,该字的每一位对应一个内存
块,换言之,每个内存页中包含的内存块的总数就是该字的总位数;
    2、next 是一个指向下一个内存页的指针;
    3、内存页中用以分配内存块的缓存区,缓存大小 = 该链表管理的内存块大小 × use的位数。


    **3.2**
    分配功能的实现。

    内存功能功能由函数 pool_alloc 实现,其声明如下:
    void* pool_alloc( size_t bytes );
    函数的参数是申请的动态内存字节数,返回值为一个指向动态内存首地址的空指针,
如果分配失败,返回的值为 NULL。
    如前所述,内存池处理的内存块大小是有上限的,当申请的字节数大于阈值的时候,则
直接调用标准库中的 malloc 函数,只有当申请的字节数小于等于阈值的时候才会使用内存
池进行分配,这个阈值就是前面提到的 MEMORY_POOL_MAX_BYTES,默认值为128。
    为了便于分配,内存池不一定会刚好分配用户申请的那么大的内存,而是会对申请的
值进行一些调整。内存池不会分配小于 MEMORY_POOL_MIN_BYTES 的内存块,算法会将分配
的字节数调整至刚好不小于申请的字节数的 MEMORY_POOL_ALIGN 的倍数,默认管理的最小
内存块是8字节,对齐的字节数为4字节。举几个例子:假设用户申请的是4个字节,由于小
于最小内存块的8字节,算法自动将申请的字节数调整为8;再假设用户申请的是14个字节,
由于14不是4的倍数,算法自动将申请的字节数调整为刚好大于14的4的倍数,也就是16。
    在完成了字节对齐后,就可以确定由哪个内存链来进行分配,确定内存链后,先进入
该链的页面缓存,在页面缓存中寻找是否有可供使用的内存页。链的页面缓存是长度等于
MEMORY_POOL_BUFFER 的一个 mempage_t* 数组,对数组进行遍历,寻找第一个不等于 NULL
的值,这个值就是需要的内存页指针。需要注意的是,被分配的内存页是连在 mempage_t
下面的,所以在查找内存块时,需要对指针进行偏移操作,跳过 sizeof(mempage_t) 个字
节。
    在找到了内存页后,进入页面分配函数 page_alloc,该函数负责从内存页中分配内存
块。前面提到过,使用内存页中 use 成员的二进制位来记录内存分配的状态,因此,查找
可被分配的内存块实际上就是查找 use 中等于 0 的第一个二进制位。很显然最直接的方法
就是对 use 的二进制位从前至后进行遍历,直到遇到第一个等于 0 的二进制位为止。但是
这种方法的效率实在太差,内存池使用了查表算法进行优化。一个 ylib_word_t 是由 N 个
字节组成的,因此先遍历 use 中的字节,如果其值等于 YLIB_BYTE_MAX ,则表示这个字节
已被分配完,因为只有当字节的所有位均为 1 时才会等于这个值;不等于 YLIB_BYTE_MAX
则表示该字节所映射的内存块还有空闲的,可以被分配。在确定了所在的字节后,便可以用
查表法快速的确定该字节第一个等于 0 的二进制位了。在 yc_memory.c 中有一个名为 bitmap
的数组,将字节所表示的整数值作为索引映射至数组,得到的返回值即为第一个等于 0 的
二进制位索引。后面的工作就简单了,根据字节索引和位索引计算出所映射的内存块地址,
通过掩码做位或运算将该位置 1,返回前再次判断一下 use 的值是否等于 YLIB_WORD_MAX,
以确定内存页是否已满,如果已满,则需将内存链的 useable 成员减一,同时将该页自页
面缓存中退出,最后将内存块地址返回给用户。
    但是缓存并不总是会有内存页的,当缓存为空时,就需要对链表进行遍历,以找到可用
的内存页。对链表的遍历并不是找到第一个可分配的内存页后就停止,这样做会让后面的分
配在不久之后又必须要遍历链表,来一趟不容易,不如来了以后多做点事,免得以后老是要
跑来爬楼梯。所以内存池的链表遍历实际上是一个整理操作,它会在遍历的过程中动态调整
链表的排列,把找到的可供分配的内存页逐一上调到链表的首位,这样遍历完成后,所有的
可分配内存页都被上调至了链表的前部,在调整的同时用找到的未满内存页重新填充页面缓
存。这里需要注意的是遍历的页面数,其实并不需要将链表这个遍历,在遍历的同时,用一
个无负号整数 count 记录遍历过的未满内存页,当 count == useable 的时候,表示后面已
经没有可供分配的内存页了,此时就可退出循环。
    以上的行为均在 useable 大于 0 的时候发生,而当 useable 等于 0 的时候,既不需
要遍历页面缓存,也不需要遍历链表,只需直接调用底层动态内存分配函数 MEMALLOC 分配
一个内存页,将 use 赋值为 1,将分配的内存页挂在内存链的首位,同时放入页面缓存,最
后返回内存页的第一个内存块即可。


    **3.3**
    归还功能的实现。

    归还实际上分为两个操作,一个是归还,一个是释放。
    归还操作由函数 pool_dealloc 实现,其声明如下:
    void pool_dealloc( void* ptr, size_t bytes );
    函数的第一个参数是指向要归还的内存块的指针,第二个参数是要归还的内存块的大小。
    进入函数后,先判断指针是否为空,接着判断内存块的大小,如果大于内存池所能管理
的最大内存块,则直接调用 MEMFREE 将之释放;如果小于等于则有可能是由内存池分配出去
的。注意,只是有可能而已。
    根据内存块的大小,先计算出内存块理论上所属的内存链,然后对该内存链进行遍历,
在遍历内存页的过程中,判断内存块的地址是否落在内存页的首地址和末地址之间,如果是
的话,则表示找到了内存块所属的内存页,如果没有,则继续遍历,如果遍历至链尾依然没
有找到,则表示该内存块不是由内存池分配的,直接调用 MEMFREE 将之释放。
    在确定了内存块所属的内存页后,依然不能马上执行归还操作,还必须确定内存块的地
址是否正确。由于分配时,是以 BLOCK_SIZE(index) 来进行对齐的,所以归还时的地址也必
须能满足这个对齐要求,亦即满足条件:(块地址 - 页首地址) % BLOCK_SIZE(index) == 0,
验证无误后,方能执行归还操作。
    归还的操作很简单,只需要将该 use 中映射至该内存块的二进制位置 0 即可。随后将
内存页调整至链首,这样做一来可以方便下次归还,二来可以方便当页面缓存使用完后,对
链表的快速遍历。
    归还操作只是把归还的内存块重新放进内存池,并不会把空闲的内存页释放给系统。假
应用程序申请了大量的内存而后又将之归还给了内存池,此时内存池的使用率接近 0%,而
其他的应用程序却因为大量内存被内存池占用而无法正常运行,此时需要将内存池占用的大
量空闲内存释放给系统以供其他应用程序使用。很显然,归还操作并不能胜任,这个工作必
须由释放操作来完成。
    释放操作由函数 pool_free 实现,其声明如下:
    void pool_free( void* ptr, size_t bytes );
    函数的第一个参数是指向要归还的内存块的指针,第二个参数是要归还的内存块的大小。
    释放操作分为两步,第一步直接调用 pool_dealloc 完成内存块的归还,第二步遍历整
个链表,在遍历的过程中将空闲的内存页释放给系统。注意,第一个参数是可以为 NULL 的,
此时,将跳过第一步,直接执行第二步。
    为什么在释放操作里是对整个链表的遍历呢?因为我预期大部分的时候执行的都会是归
还操作,只有当系统内存紧张的时候才需要执行释放操作,此时释放操作执行一次即可满足
要求,接下来就又可以执行归还操作了。譬如,可以调用 get_pool_dealloc_count 函数了
解执行了归还的次数,当达到某个阈值的时候就可以调用 pool_free 释放一些空闲的内存页。


    **3.4**
    并发控制。

    在多线程环境下,内存池的操作将会因线程之间的切换而崩溃!为此,内存池在实现的
时候提供了并行加锁和解锁操作。为了移植,加锁和解锁的实现交由用户来实现,内存池只
负责调度。
    设置加锁函数:void set_pool_lock( void (*lock)(size_t index) )。
    设置解锁函数:void set_pool_unlock( void (*unlock)(size_t index) )。
    两个函数的参数都是一个声明原型如 void f(size_t) 的函数指针。这里解释一下传递
进来的加锁和解锁函数为什么需要一个 size_t index 参数。这个 index 参数就是内存链的
索引值,仔细观察一下就会发现,实际上每个内存链是相互独立的,内存池最多可以允许
MEMORY_POOL_LISTS 个在不同的链表内的线程同时操作。通过 index 这个参数,实现加锁和
解锁功能的用户就可以对不同的链表分别进行加锁和解锁。例如在 Windows 系统下,可以先
调用 get_pool_lists_count 函数以获取内存链的总数,然后创建一个互斥量数组,数组的大
小就等于内存链的总数,在实现的加锁和解锁函数里可以根据 index 参数决定是对哪个互斥
量进行操作。


    **3.5**
    模板化。

    在 C++ 中使用内存池可以借助模板来进行一下包装。STL 中的内存分配器已经为我们
提供了一个样本。
    模板化的内存池源码在 young/youngcpp/ycpp_memory.hpp 中。下面只列出三个主要的
成员函数。
    pointer allocate( size_type n )
    {
        return (pointer)( youngc::pool_alloc( sizeof(T) * n ) );
    }

    void deallocate( pointer ptr, size_type n )
    {
        youngc::pool_dealloc( ptr, sizeof(T) * n );
    }

    ~pool_allocator()
    {
        youngc::pool_free( NULL, sizeof(T) );
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

为毛呀

非常感谢你对我的支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值