分享

固定大小块的内存池设计

 心不留意外尘 2016-09-06

http://blog.csdn.net/wbwwf8685/article/details/51897461

2016

摘要

在一些场景下,应用需要频繁的申请和释放一个或多个大小相同的内存(例如长时间传输固定大小的数据块),这时候如果调用的是普通的malloc和free函数,效率就相对较低,且分配和释放的次数多,会容易导致内存碎片,增加系统对堆内存管理的负担。

针对这一需求,Nucleus提供了一种非常高效的内存池方案,称为partition memory。它可以快速的提供给用户固定大小的内存,也避免了内存碎片的问题。

本文对该partitionmemory机制的实现细节进行剖析,并在结尾处提出了一点优化的建议。


数据结构分析

PM_PCB是patition内存池的主要管理结构,其定义如下:

(为专注于内存管理本身,本文去掉了一些和Nucleus任务suspend和resume的一些数据和代码细节。如果内存池已用尽,可以把task suspend住)

typedef struct PM_PCB_STRUCT 

{

    VOID               *pm_start_address;        /* 内存池起始地址 */

    UNSIGNED            pm_pool_size;          /* 内存池总大小      */

    UNSIGNED            pm_partition_size;     /* 每个partition的大小  */

    UNSIGNED            pm_available;          /* 待分配的partition个数   */

    UNSIGNED            pm_allocated;          /* 已分配的partitions个数   */

    struct PM_HEADER_STRUCT             pm_available_list;     /* 待分配的partition链表  */

} PM_PCB;    


PM_HEADER是每个patition的头部,它的定义如下:

typedef struct PM_HEADER_STRUCT

{

    struct PM_HEADER_STRUCT       *pm_next_available;     /* 指向下一个待分配的 partition             */

    PM_PCB             *pm_partition_pool;     /*回指向PM_PCB结构 */

} PM_HEADER;

简单来说,partition memory机制就是一个PM_PCB结构和其管理的一条PM_HEADER链表之间发生的故事。


Partition_Pool的建立

Partition_Pool的建立一般在其创建时就会一次性完成。它需要一个较大的内存段作为内存池的“堆”,一般是以全局数组的形式。这里以一个具体的例子来进行解析。整个内存池是大小是1024字节,每个固定的大小的partition大小是64字节

char g_Pool_64_byte[1024];

第一步,对PM_PCB的各数据段作初始化,如下所示

typedef struct PM_PCB_STRUCT 

{

    VOID               *pm_start_address;        /* 指向g_Pool_64_byte首地址  */

    UNSIGNED            pm_pool_size;          /* 1024   */

    UNSIGNED            pm_partition_size;     /*64  */

    UNSIGNED            pm_available;          /*   0  */

    UNSIGNED            pm_allocated;          /*  0  */

    struct PM_HEADER_STRUCT             pm_available_list;     /* 指向 NULL*/

} PM_PCB;

紧接着,重头戏来了,就是对于内存的划分。这是一个循环的过程,先看第一个partition划分处理之后的样子

一个完整的partition需要64+8个字节,其中64就是图中标注棕色的区域,它的真正返回给用户使用的内存块;而这里的8字节就是sizeof(struct PM_HEADER)。它包含两个指针pm_next_available指向下一个可分配的partition,此时只有一个,故其指向NULL;pm_partition_pool回指向PM_PCB结构,在释放内存的时候使用,后面会详说。PM_PCB结构的pm_start_address和pm_available_list都指向g_Pool_64_byte的首地址。

再看第二个partition划分开来的样子

从第二个partition开始,单链表的雏形就可以看出来了。第二个内存块的pm_next_available指向前一次划分好的partition,PM_PCB结构的pm_available_list指向第二个partition的首地址。同时pm_available字段增加到了2。

下面一张图描绘了所有所有partition划分完毕后的情形,每个partition的pm_next_available都指向前一次划分好的partition首地址,PM_PCB结构的pm_available_list指向最后一个partition的首地址。同时pm_available字段增加到了14。

由于每个partition实际占用的内存大小是64+8=72.所以这一整块1024的堆被切割划分为了14块之后总共用去了72*14=1008,最后剩下的16个字节内存块怎么办?答案很简单,就是只能浪费不用了。这里也就同时要求了应用程序开发人员最好能对partition memory内部的实现机制能用一定的了解,至少知道在定义全局堆数组时,应该定义的大小是72的整数倍而不是64的整数倍。


内存的分配

做一个内存池,最终目的当然还是给应用程序提供内存。有了前面已建立完整的partition pool,分配和释放内存其实就是标准的单链表摘链和挂链的操作。直接以图的形势来表达,更清楚。


分配内存的过程其实就是链表节点摘链和操作pm_available_list,将它当前指向的partition的可用内存块(即图中的绿色区域)的首地址返回给用户,同时移动到它的pm_next_available。pm_available字段减少到了13。pm_allocated字段增加1


内存的释放

按顺序的申请和释放非常简单,这里就不再赘述了。这里以一个稍微有点特殊的例子来演示内存释放的过程。假设当前第1、2、3块partition已被分配,现在用户释放了第2块partition。是怎样一个过程?

如下图所示,左边是第1、2、3块partition已被分配,此时pm_available_list指向第4个partition.而右边图示,如果第2块被释放,则pm_available_list指向第2块,第二块的pm_next_available指向第4块partition


这个内存池设计是否有优化的空间

在这个分析的过程中,每个partition都保留了这个字段回指向PM_PCB结构,但是可能有的朋友已经注意到了,没看到PM_HEADER的pm_partition_pool指针用在哪里。那它存在的意义是什么呢?就是在释放内存时,可以少传一个参数,Nucleus里,释放partition的函数原型如下:

STATUS PMC_Deallocate_Partition(VOID *partition)

该函数内部实现可以根据这个partition指针去找到PM_HEADER结构,再通过pm_partition_pool找到对应的PM_PCB结构以进行后续的操作。

楼主以为,这是有些浪费的设计,每个partition里的pm_partition_pool指针所占的4字节完全可以节省下来。

只要在PMC_Deallocate_Partition的入参里加入pm_partition_pool的地址就可以了。也不用担心用户是否会拿不到该pool的指针,因为内存使用的一项重要规则就是“谁申请,谁释放”,在申请的时候用户就必须拥有该结构,在释放的时候自然也会有。申请内存函数原型如下:

PMC_Allocate_Partition(NU_PARTITION_POOL*pool_ptr,VOID **return_pointer);

如果能把每个partition的4字节都节省下来,那么在本文的例子里,每块partition的大小就变成了64+4,可以使用的总块数就增加了一块

68 * 15 = 1020


Over

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多