两种内存池管理方法对比

目录

一、问题背景

二、两种内存池管理

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的实现,看到了一种不同于固定大小内存块的内存池管理方式。它使用了可变大小的内存块分配,支持内存块大小自定义。为了学习其内存管理思想,故特此记录下这两种内存池管理方式的差异。本文将分别介绍了这两种内存池管理方法的实现方式,最后对比两种方式的优缺点和适用场景。

 

二、两种内存池管理

2.1 固定大小内存块分配(参考正点原子STM32F4 malloc.c)

固定大小内存块的方式,简单的说就是将内存按照相同大小划分成若干个内存块。每一个内存块有一个对应的内存管理表。如果对应的内存管理表为0,则标志未使用。非零表示此内存已经被分配使用中。

分块式内存管理由内存池和内存管理表两部分组成。内存池被等分为 n块,对应的内存管理表,大小也为 n,内存管理表的每一个项对应内存池的一块内存。

内存管理表的项值代表的意义为:当该项值为 0 的时候,代表对应的内存块未被占用,当该项值非零的时候,代表该项对应的内存块已经被占用,其数值则代表被连续占用的内存块数。比如某项值为 10,那么说明包括本项对应的内存块在内,总共分配了 10 个内存块给外部的某个指针。

内寸分配方向如图所示,是从顶->底的分配方向。即首先从最末端开始找空内存。当内存管理刚初始化的时候,内存表全部清零,表示没有任何内存块被占用。

                       两种内存池管理方法对比_第1张图片

                                                                                      图1 内存块物理上分布图

2.1.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  
} 

2.1.2 分配原理

当指针 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

2.1.3 释放原理

当 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

2.2 可变大小内存块分配(参考WSF BLE协议栈buffer management)

可见,上面固定大小内存块方式在查找和释放时,都需要遍历、置/清内存表标志位,比较费时间。而可变大小内存块的思想是:将待分配的内存分为大、中、小这么几类。每一类由相同大小的内存块通过一个单链表链接在一起组成。初始化后,空闲链表*pFree就指向这个单链表头。

当需要申请内存的时候,按照从小到大的顺序,在这几类里面找到刚好能够容纳下申请内存的buf。如果找到,就直接将这类内存块的空闲链表头返回给它即可。然后空闲链表头指向next即可完成内存的申请。

在释放的时候,按照从大到小的顺序,找到和待释放的内存块大小相同的那类内存块,将其重新插入那类内存块空闲链表头,即完成释放。

这就是可变大小内存池的分配基本思路。当然我们这里举例是只有大、中、小三类的内存块,实际可根据自己的需求增加,当然可以更多大小不同的内存块。

2.2.1 初始化

在实现方面,由内存低地址的内存池描述结构体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张图片

                                                                                      图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;
}

2.2.2 分配原理

从内存池描述结构的头开始,根据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张图片

                                                                                    图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;
}

2.2.3 释放原理

需要释放相应的内存块,释放时,直接将内存块重新加入空闲块链表即可。由于有好几种不同大小的内存池,所以会先查找这个内存块属于哪一个内存池。释放时会先初始化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.灵活度不够,分配不好很容易内存碎片;

如有不妥之处,欢迎指正!谢谢

你可能感兴趣的:(内存管理)