目录
一、问题背景
二、两种内存池管理
2.1 固定大小内存块分配(参考正点原子STM32F4 malloc.c)
2.1.1 初始化
2.1.2 分配原理
2.1.3 释放原理
2.2 可变大小内存块分配(参考WSF BLE协议栈buffer management)
2.2.1 初始化
2.2.2 分配原理
2.2.3 释放原理
三、对比和总结
最近在调试ambiq apollo3的蓝牙时,其使用了ARM Cordio WSF的蓝牙协议栈。通过学习wsf_buf.c的实现,看到了一种不同于固定大小内存块的内存池管理方式。它使用了可变大小的内存块分配,支持内存块大小自定义。为了学习其内存管理思想,故特此记录下这两种内存池管理方式的差异。本文将分别介绍了这两种内存池管理方法的实现方式,最后对比两种方式的优缺点和适用场景。
固定大小内存块的方式,简单的说就是将内存按照相同大小划分成若干个内存块。每一个内存块有一个对应的内存管理表。如果对应的内存管理表为0,则标志未使用。非零表示此内存已经被分配使用中。
分块式内存管理由内存池和内存管理表两部分组成。内存池被等分为 n块,对应的内存管理表,大小也为 n,内存管理表的每一个项对应内存池的一块内存。
内存管理表的项值代表的意义为:当该项值为 0 的时候,代表对应的内存块未被占用,当该项值非零的时候,代表该项对应的内存块已经被占用,其数值则代表被连续占用的内存块数。比如某项值为 10,那么说明包括本项对应的内存块在内,总共分配了 10 个内存块给外部的某个指针。
内寸分配方向如图所示,是从顶->底的分配方向。即首先从最末端开始找空内存。当内存管理刚初始化的时候,内存表全部清零,表示没有任何内存块被占用。
图1 内存块物理上分布图
在移植这种内存池分配前,我们需要预先定义待分配的内存物理地址和内存块大小。如下所示,在STM32F4 MCU中我们有内部SRAM、内部CCRAM和外部SRAM,共三处内存。通过MEMx_BLOCK_SIZE定义内存块的大小,我们这里将这三处内存都设置为32byte。
MEMx_MAX_SIZE表示内存管理大小。而内存表由于和内存一一对应,故MEMx_MAX_SIZE/MEMx_BLOCK_SIZE就能得到实际的内存表大小(个数)。
//malloc.h 头文件定义:
//mem1内存参数设定.mem1完全处于内部SRAM里面.
#define MEM1_BLOCK_SIZE 32 //内存块大小为32字节
#define MEM1_MAX_SIZE 20*1024 //最大管理内存 100K
#define MEM1_ALLOC_TABLE_SIZE MEM1_MAX_SIZE/MEM1_BLOCK_SIZE //内存表大小
//mem2内存参数设定.mem2处于CCM,用于管理CCM(特别注意,这部分SRAM,仅CPU可以访问!!)
#define MEM2_BLOCK_SIZE 32 //内存块大小为32字节
#define MEM2_MAX_SIZE 60 *1024 //最大管理内存60K
#define MEM2_ALLOC_TABLE_SIZE MEM2_MAX_SIZE/MEM2_BLOCK_SIZE //内存表大小
//mem3内存参数设定.mem3的内存池处于外部SRAM里面
#define MEM3_BLOCK_SIZE 32 //内存块大小为32字节
#define MEM3_MAX_SIZE 960 *1024 //最大管理内存960K
#define MEM3_ALLOC_TABLE_SIZE MEM3_MAX_SIZE/MEM3_BLOCK_SIZE //内存表大小
// malloc.c源码定义
//内存池(32字节对齐)
__align(32) u8 mem1base[MEM1_MAX_SIZE]; //内部SRAM内存池
__align(32) u8 mem2base[MEM2_MAX_SIZE] __attribute__((at(0X10000000))); //内部CCM内存池
__align(32) u8 mem3base[MEM3_MAX_SIZE] __attribute__((at(0X68000000))); //外部SRAM内存池
//内存管理表
u16 mem1mapbase[MEM1_ALLOC_TABLE_SIZE]; //内部SRAM内存池MAP
u16 mem2mapbase[MEM2_ALLOC_TABLE_SIZE] __attribute__((at(0X10000000+MEM2_MAX_SIZE))); //内部CCM内存池MAP
u16 mem3mapbase[MEM3_ALLOC_TABLE_SIZE] __attribute__((at(0X68000000+MEM3_MAX_SIZE))); //外部SRAM内存池MAP
//内存管理参数
const u32 memtblsize[SRAMBANK]={MEM1_ALLOC_TABLE_SIZE,MEM2_ALLOC_TABLE_SIZE,MEM3_ALLOC_TABLE_SIZE};//内存表大小
const u32 memblksize[SRAMBANK]={MEM1_BLOCK_SIZE,MEM2_BLOCK_SIZE,MEM3_BLOCK_SIZE};//内存分块大小
const u32 memsize[SRAMBANK]={MEM1_MAX_SIZE,MEM2_MAX_SIZE,MEM3_MAX_SIZE};
初始化时,只用将内存池和内存管理表都清零即可。
//内存管理初始化
//memx:所属内存块
void my_mem_init(u8 memx)
{
mymemset(mallco_dev.memmap[memx], 0,memtblsize[memx]*2);//内存状态表数据清零
mymemset(mallco_dev.membase[memx], 0,memsize[memx]);//内存池所有数据清零
mallco_dev.memrdy[memx]=1; //内存管理初始化OK
}
当指针 p 调用 malloc 申请内存的时候,先判断 p 要分配的内存块数(m),然后从第 n 项开始,向下查找,直到找到 m 块连续的空内存块(即对应内存管理表项为 0),然后将这 m 个内存管理表项的值都设置为 m(标记被占用),最后,把最后的这个空内存块的地址返回指针 p,完成一次分配。注意,如果当内存不够的时候(找到最后也没找到连续的 m 块空闲内存),则返回 NULL 给 p,表示分配失败。
//内存分配(内部调用)
//memx:所属内存块
//size:要分配的内存大小(字节)
//返回值:0XFFFFFFFF,代表错误;其他,内存偏移地址
u32 my_mem_malloc(u8 memx,u32 size)
{
signed long offset=0;
u32 nmemb; //需要的内存块数
u32 cmemb=0;//连续空内存块数
u32 i;
if(!mallco_dev.memrdy[memx])mallco_dev.init(memx);//未初始化,先执行初始化
if(size==0)return 0XFFFFFFFF;//不需要分配
nmemb=size/memblksize[memx]; //获取需要分配的连续内存块数
if(size%memblksize[memx])nmemb++;
for(offset=memtblsize[memx]-1;offset>=0;offset--)//搜索整个内存控制区(每次都要从头开始搜索,而且不排序,效率低--yulong)
{
if(!mallco_dev.memmap[memx][offset]){
cmemb++;//连续空内存块数增加(为0)
}
else {
cmemb=0;
}
//连续内存块清零
if(cmemb==nmemb) //找到了连续nmemb个空内存块
{
for(i=0;i
当 p 申请的内存用完,需要释放的时候,调用 free 函数实现。 free 函数先判断 p 指向的内存地址所对应的内存块,然后找到对应的内存管理表项目,得到 p 所占用的内存块数目 m(内存管理表项目的值就是所分配内存块的数目),将这 m 个内存管理表项目的值都清零,标记释放,完成一次内存释放。
//释放内存(内部调用)
//memx:所属内存块
//offset:内存地址偏移
//返回值:0,释放成功;1,释放失败;
u8 my_mem_free(u8 memx,u32 offset)
{
int i;
if(!mallco_dev.memrdy[memx])//未初始化,先执行初始化
{
mallco_dev.init(memx);
return 1;//未初始化
}
if(offset
可见,上面固定大小内存块方式在查找和释放时,都需要遍历、置/清内存表标志位,比较费时间。而可变大小内存块的思想是:将待分配的内存分为大、中、小这么几类。每一类由相同大小的内存块通过一个单链表链接在一起组成。初始化后,空闲链表*pFree就指向这个单链表头。
当需要申请内存的时候,按照从小到大的顺序,在这几类里面找到刚好能够容纳下申请内存的buf。如果找到,就直接将这类内存块的空闲链表头返回给它即可。然后空闲链表头指向next即可完成内存的申请。
在释放的时候,按照从大到小的顺序,找到和待释放的内存块大小相同的那类内存块,将其重新插入那类内存块空闲链表头,即完成释放。
这就是可变大小内存池的分配基本思路。当然我们这里举例是只有大、中、小三类的内存块,实际可根据自己的需求增加,当然可以更多大小不同的内存块。
在实现方面,由内存低地址的内存池描述结构体poolDescriptor和跟在后面的pool buffer(数据实际存放位置)组成。一个poolDescripter数据项对应一个pool buffer。
对于可变大小的内存池分配,用户在使用前,需要先配置内存池描述结构体。在大小上一般我们倾向于将其设置为4个字节的倍数,方便单片机内存32位对齐。然后由小变大的原则,16、32、64、280...。在数量上根据就需要根据自己的内存使用量预估,找到合适的个数。
内存池描述结构体wsfBufPool_结构如下:
/*! Buffer pool descriptor structure */
typedef struct
{
uint16_t len; /*! length of buffers in pool */
uint8_t num; /*! number of buffers in pool */
} wsfBufPoolDesc_t;
/* Internal buffer pool */
typedef struct
{
wsfBufPoolDesc_t desc; /* number of buffers and length */
wsfBufMem_t *pStart; /* start of pool */
wsfBufMem_t *pFree; /* first free buffer in pool */
} wsfBufPool_t;
如下所示,前面为每个内存池buf大小(单位为word),后面为此大小的内存池个数。一共定义是4个内存此描述结构体。即16个word的内存有8个(16*4*8byte=512byte), 32个word的内存有4个,64个word的内存有6个,280个word的内存有4个。
// Default pool descriptor.
static wsfBufPoolDesc_t g_psPoolDescriptors[WSF_BUF_POOLS] =
{
{ 16, 8 },
{ 32, 4 },
{ 64, 6 },
{ 280, 4 }
};
在内存初始化时,初始化函数就会读取这个用户自定义的内存分配描述结构体,初始化内存池。
如上面设置了pool的个数为4个,依次初始化位于内存低地址的内存管理结构体poolDescriptor:
跟着其后面的是buffer storage。buffer storage需要初始化为大小相同内存块,通过一个单向链表连接在一起,最后一块内存的*pNext指向NULL。*pStart会一直指向这个pool的头地址,用于保存每个内存池起始位置。*pFree会随着alloc和free移动,保证始终指向空闲的内存块。
按照上面的结构体,初始化后的整体内存分布如下:
图2 内存物理上总体分布图
精简后的初始化源代码:
/*************************************************************************************************/
/*!
* \fn WsfBufInit
*
* \brief Initialize the buffer pool service. This function should only be called once
* upon system initialization.
*
* \param bufMemLen Length of free memory
* \param pBufMem Free memory buffer for building buffer pools
* \param numPools Number of buffer pools.
* \param pDesc Array of buffer pool descriptors, one for each pool.
*
* \return Amount of pBufMem used or 0 for failures.
*/
/*************************************************************************************************/
uint16_t WsfBufInit(uint16_t bufMemLen, uint8_t *pBufMem, uint8_t numPools, wsfBufPoolDesc_t *pDesc)
{
wsfBufPool_t *pPool;
wsfBufMem_t *pStart;
uint16_t len;
uint8_t i;
wsfBufMem = (wsfBufMem_t *) pBufMem;
pPool = (wsfBufPool_t *) wsfBufMem; //internal buffer management structure.在bufMem的最前面。 --yulong
/* buffer storage starts after the pool structs */
pStart = (wsfBufMem_t *) (pPool + numPools); //具体分派内存的起始地址。
wsfBufNumPools = numPools; //初始化pool数目4
/* create each pool; see loop exit condition below */
while (TRUE)
{
/* exit loop after verification check */
if (numPools-- == 0)
{
break;
}
/* adjust pool lengths for minimum size and alignment。调整最小4字节,以及对其 */
//.....
pPool->desc.num = pDesc->num;
pDesc++; //遍历入参Desc的每个内存池描述Desc
pPool->pStart = pStart; //初始化起始地址和free地址
pPool->pFree = pStart;
/* initialize free list */
len = pPool->desc.len / sizeof(wsfBufMem_t); // 4字节
for (i = pPool->desc.num; i > 1; i--)
{
/* pointer to the next free buffer is stored in the buffer itself */
pStart->pNext = pStart + len;
pStart += len;
}
/* last one in list points to NULL */
pStart->pNext = NULL;
pStart += len; //跳到这个内存池的末尾
/* next pool */
pPool++;
}
wsfBufMemLen = (uint8_t *) pStart - (uint8_t *) wsfBufMem;
WSF_TRACE_INFO1("Created buffer pools; using %u bytes", wsfBufMemLen);
return wsfBufMemLen;
}
从内存池描述结构的头开始,根据pPool->desc.len,按len从小往大的找。直到找到能够容纳下申请内存大小的那个pool,然后将这个pool的pFree赋给pBuf,同时将pFree指向下一个空闲块pNext。最后返回pBuf即完成内存申请。核心代码下面两句:
/* allocation succeeded */
pBuf = pPool->pFree;
/* next free buffer is stored inside current free buffer */
pPool->pFree = pBuf->pNext;
值得注意的是,虽然每个空闲内存块为了维护链表,前面都有一个*pNext指针和magic num(可选),但是当分配给用户后,由于其不再需要组织成一个链表,所以*pNext的值没有任何意义,而magic num是free过后才赋值的。因此用户在使用时,可以使用全部的内存块,不会为了维护链表,造成额外的空间浪费。
图3 内存块复用内存空间
去掉辅助功能后的精简代码:
/*************************************************************************************************/
/*!
* \fn WsfBufAlloc
* \brief Allocate a buffer.
* \param len Length of buffer to allocate.
* \return Pointer to allocated buffer or NULL if allocation fails.
*/
/*************************************************************************************************/
void *WsfBufAlloc(uint16_t len)
{
wsfBufPool_t *pPool;
wsfBufMem_t *pBuf;
uint8_t i;
WSF_CS_INIT(cs); //线程保护,准备进入临界区
pPool = (wsfBufPool_t *) wsfBufMem; // 获得前面的内存池描述结构头
for (i = wsfBufNumPools; i > 0; i--, pPool++) //从小->大找pool池
{
/* if buffer is big enough */
if (len <= pPool->desc.len) //如果找到了就分配,没有找到找下一个更大的pool池
{
/* enter critical section */
WSF_CS_ENTER(cs);
/* if buffers available */
if (pPool->pFree != NULL)
{
/* allocation succeeded */
pBuf = pPool->pFree;
/* next free buffer is stored inside current free buffer */
pPool->pFree = pBuf->pNext;
/* exit critical section */
WSF_CS_EXIT(cs);
WSF_TRACE_ALLOC2("WsfBufAlloc len:%u pBuf:%08x", pPool->desc.len, pBuf);
return pBuf;
}
/* exit critical section */
WSF_CS_EXIT(cs);
}
}
return NULL;
}
需要释放相应的内存块,释放时,直接将内存块重新加入空闲块链表即可。由于有好几种不同大小的内存池,所以会先查找这个内存块属于哪一个内存池。释放时会先初始化pPool为内存最后(内存最大)的那个内存desc pool,从后向前找。当pBuf>= pPool->pStart时,说明待释放的内存就在这个pool里面。所以执行下面两行即可:
/* pool found; put buffer back in free list */
// 注:*p为待free的内存地址
p->pNext = pPool->pFree;
pPool->pFree = p;
如果开启了Free检测,这会检查magic num,避免被多次Free,造成错误。
精简后的代码如下:
/*************************************************************************************************/
/*!
* \fn WsfBufFree
* \brief Free a buffer.
* \param pBuf Buffer to free.
* \return None.
*/
/*************************************************************************************************/
void WsfBufFree(void *pBuf)
{
wsfBufPool_t *pPool;
wsfBufMem_t *p = pBuf;
WSF_CS_INIT(cs);
/* iterate over pools starting from last pool */
pPool = (wsfBufPool_t *) wsfBufMem + (wsfBufNumPools - 1); // 从尾部desc倒着遍历buf。
while (pPool >= (wsfBufPool_t *) wsfBufMem)
{
/* if the buffer memory is located inside this pool */
if (p >= pPool->pStart)
{
/* enter critical section */
WSF_CS_ENTER(cs);
/* pool found; put buffer back in free list */
p->pNext = pPool->pFree;
pPool->pFree = p;
/* exit critical section */
WSF_CS_EXIT(cs);
WSF_TRACE_FREE2("WsfBufFree len:%u pBuf:%08x", pPool->desc.len, pBuf);
return;
}
/* next pool */
pPool--;
}
return;
}
通过对比,可以看出这两种内存管理方式实现区别是:一种用位图(这里用2个byte当做一个位标记),一种用链表。
这两种分配方式各有优缺点,在实际应用时, 可根据需求作出相应的选择。如当注重实时性和可靠性,而且申请内存大小范围已知可控,那我们可以选择可变大小的内存块。当注重灵活性时,可以选择固定大小的内存块,虽然这会牺牲点效率。
当然想要兼容这两种,可以尝试混合使用呀。比如:如果申请和内存池大小接近的内存就使用可变大小内存池。而其他的较大的,或者较小的就在固定大小内存池里去遍历申请。
优点 | 缺点 | |
固定大小内存块 | 1.灵活度高,能以较少的内存碎片,分配各种大小的内存; | 1.效率低,每次都要从尾部遍历一遍memmap,找合适大小的内存,置/清内存表; |
可变大小内存块 | 1.申请和释放速度快,效率高; | 1.需提前估算大概的内存需求大小; 2.灵活度不够,分配不好很容易内存碎片; |
如有不妥之处,欢迎指正!谢谢