嵌入式系统的内存管理系统

1. 嵌入式系统的内存分配

嵌入式程序开发中,与PC程序开发不同,很少使用完全动态内存分配(malloc()/free()),主要基于以下一些原因:

  • 为了支持动态内存分配必须实现一套完善的内存管理系统,包括空闲/占用内存的管理、内存分配策略、防止内存碎片化对策等等。这会使得系统过于庞大,占用过多的代码空间。在一些存储资源有限的嵌入式系统中,过大的代码空间开销是不可接受的。
  • malloc()/free()函数往往是非线程安全的,在使用之前,必须确认。如果不是线程安全的,则在调用侧必须进行保护。
  • 由于需要搜索合适大小的空闲内存块,因此每次malloc()的执行时间是不确定的。同样,在释放的时候,如果同时进行内存碎片合并回收,则free()的执行时间也是不确定的。因此在需要确保执行时间确定性的系统中,不能使用。
  • 嵌入式系统由于功能单一(相对于个人计算系统而言),对任意大小的动态内存分配的需求不强烈。
  • 跟PC系统中一样,动态内存分配还是很多问题的根源(内存泄露、内存越界、重复释放等),而且这样的问题不易调试和定位。

因此在嵌入式系统中,往往采用静态内存分配,也就是所有的变量,无论是全局变量,还是局部变量,都是在编译时指定大小。这虽然避免了上述动态内存分配的问题,但也有其缺陷,在使用中如果不注意,也会带来新的问题。

  • 降低了内存的使用效率。但这是避免动态内存分配的必要代价。
  • 多个任务访问同一个全局变量时,需要施加访问控制。根据所采用的操作系统的不同,访问控制可以通过互斥锁,也可以通过中断禁止、调度禁止等方法实现。因此,尽量避免让多个任务(包括中断服务程序)访问同一个全局变量,除非有必须这么做的理由。
  • 局部变量使用静态内存分配时,需要经常关注任务堆栈的大小,防止溢出,特别是局部变量所占空间较大时。因此,如果局部变量所占的空间较大,有时可以考虑采用全局变量来代替,以免任务栈过大。

如果只使用静态内存分配,在任务间、或者任务与中断服务程序之间进行数据交互时,就不太方便。此时就需要为不同的数据交互的任务组合分配不同的专用内存。如是一个由三个任务构成的系统,如果各个任务之间都有互相数据交互,则需要准备6个静态数据交互内存,而且其数量随着任务数的增加而呈指数增加。而且这种分配方式也很不灵活,可能为交互较少的任务分配了较多的内存,或者为交互较多的任务分配的内存不足等等。

嵌入式系统的内存管理系统_第1张图片

图 1‑1 静态数据交互内存分配

嵌入式操作系统提供了消息(message)或者队列(queue)的手段来解决这个问题。在操作系统层面,为消息或者队列中的每个项都分配了固定大小的内存块。该内存块可以保存需要传输的数据,也可以保存需要交互数据的地址。如果采用消息或者队列的数据项进行直接数据传输时,在发送侧和接收侧都需要执行一次数据拷贝,影响执行效率;而且由于需要传输的数据大小一般依赖于实际应用,因此不可能在操作系统中规定一个数据项的内存大小。因此在实际应用中,消息的发送端将需要传输的数据放在缓存中,并将该内存地址放入消息和队列的数据项中进行发送;消息的接收端则从收到的数据项中取出对应的地址,从而取得所接收的数据。如图 1‑2所示。

这样的缓存系统可以看作是一个简化版的动态内存分配系统。它只支持固定大小的内存块的分配,从而大大简化了动态内存管理系统的负担。而在嵌入式系统中,固定大小的内存块可以完全满足需求。本章主要介绍一种固定大小内存分配的实现机制。

嵌入式系统的内存管理系统_第2张图片

 图 1‑2 通过缓存区进行数据传递

2. 缓存管理系统的实现

1节所要求的缓存系统必须满足几个条件:

  • 位于全局变量区,各个任务都可以访问。
  • 需要支持申请和释放过程。
  • 为了缓存管理上的便利,每个缓存块按固定大小分配,或者按几个可以选择的大小分配。
  • 保证申请和释放的时间是确定的。

为此建立的一个缓存系统如下所示(为了简洁起见,省略了一部分注释):

mm.h

#ifndef __MM_H_

#define __MM_H_

#include

void vInitMM(void);

uint8_t *pucAllocateBuffer(void);

void vFreeBuffer(uint8_t *pucBuf);

#endif /*__MM_H_*/

头文件中声明了三个函数,分别用于初始化缓存系统,分配缓存和释放缓存。除了初始化函数,其他两个与动态内存分配的malloc/free非常类似,使用方法上也没有什么区别。初始化函数是新增加的,必须在整个系统初始时调用。其实动态内存管理系统也有类似的初始化过程,不过是作为操作系统初始化的一部分而执行。

具体的实现代码如下所示:

mm.c

#include "mm.h"

//The size of each buffer block.

#define BLOCK_SIZE               (256)

//The number of buffer blocks.

#define BLOCK_NUM                (20)

//The head for each buffer block, it's only used by MM system.

typedef struct {

  uint8_t  ucIsUsed;  /* Indicate if the buffer is allocated (TRUE), or not (FALSE)*/

   /*

    * Index of the next empty buffer if the buffer is linked to the empty buffer pool. This

    * index is the one into pBlocks, If a buffer block is allocated, this value is meaningless.

    * For the last one of empty buffer link it is set to BLOCK_NUM.

    */

  uint8_t  ucNextIdx;

} mm_head_t;

//Strcture of a buffer block.

typedef struct {

  mm_head_t    head;

  uint8_t      pucBlock[BLOCK_SIZE];   

}mm_block_t;

//Memory for all buffer blocks.

static mm_block_t pBlocks[BLOCK_NUM];

//The head of index of empty memory block, which its BLOCK_NUM means

//no empty block head.

static uint8_t ucEmptyBlockHead;

#define BLOCK_OFFSET        ((uint8_t)(((mm_block_t *)0)->pucBlock))

void vInitMM(void) {

  uint8_t     ucStep;

  mm_block_t *pMMTmp;

  memset((void *)pBlocks, 0, sizeof(mm_block_t) * BLOCK_NUM);

  ucEmptyBlockHead = 0;

  pMMTmp = &pBlocks[ucEmptyBlockHead];

  for(ucStep = 0; ucStep < BLOCK_NUM; ucStep++) {

    pMMTmp->head.ucNextIdx = ucStep + 1;

    if(pMMTmp->head.ucNextIdx < BLOCK_NUM) {

      pMMTmp = &pBlocks[pMMTmp->head.ucNextIdx];

    }

  }

}

uint8_t *pucAllocateBuffer(void) {

  uint8_t *pAllocBuf;

  if(BLOCK_NUM == ucEmptyBlockHead) {

                return NULL; //No empty buffer exists.

  }

  pAllocBuf = pBlocks[ucEmptyBlockHead].pucBlock;

  pBlocks[ucEmptyBlockHead].head.ucIsUsed = TRUE;

  ucEmptyBlockHead = pBlocks[ucEmptyBlockHead].head.ucNextIdx;

  return pAllocBuf;

}

void vFreeBuffer(uint8_t *pucBuf) {

  mm_block_t *pMMTmp;

  uint8_t     ucStep;

  if(NULL == pucBuf) {

                return; //Nothing to do.

  }

  //Look for the corresponding block of the buffer.

  //ATTENTION: This loop makes the execution time not determined.

  pMMTmp = NULL;

  for(ucStep = 0; ucStep < BLOCK_NUM; ucStep++) {

    if(pucBuf == (uint8_t *)&pBlocks[ucStep] + BLOCK_OFFSET) {

      pMMTmp = &pBlocks[ucStep];

      break;

    }

  }

  if(NULL == pMMTmp) { //Invalid address. Output warning

     return;

  }

  //Valid address.

  //Link it to the head of the empty buffers.

  if(pMMTmp->head.ucIsUsed) {

    pMMTmp->head.ucIsUsed = FALSE;

    pMMTmp->head.ucNextIdx = ucEmptyBlockHead;

    ucEmptyBlockHead = ucStep;

  }

  else {

    //Double freeing, Nothing to do.

  }

  return;

}

首先定义了一个管理缓存的数据结构mm_block_t,由用于系统管理的缓存头mm_head_t和缓存数据块。缓存头用于缓存系统的管理,而缓存数据块则用于实际的数据保存与传递。缓存头由一个使用标志ucIsUsed和指向下一个缓存的指针ucNextIdx构成。

用于缓存系统的内存由全局数组变量pBlocks保留,其中可分配的缓存数量由宏BLOCK_NUM指定。在初始化的时候,将所有空的缓存块构成一个单向链表,其头指针保存在变量ucEmptyBlockHead中。由该头指针和缓存块头中的指针ucNextIdx就可以构成一个完整的单向链表。为了节省内存,此处的指针全部采用pBlocks数组的下标来表示。

在分配缓存的时候,只是将空缓存链表中的第一个元素分配出去,同时将空缓存头指针指向下一个元素。这样就可以保证缓存分配时间是确定的。这也是建立空缓存单向链表的目的。

在释放缓存的时候,为了将该缓存重新放入空缓存链表中,需要找到该缓存所对应的全局数组pBlocks中的下标。在上述例子中,通过一个循环扫描pBlocks中所有的元素来实现。这就导致释放函数的执行时间是不确定的。

3. 执行时间确定的缓存管理系统

正如2节所说明的,该缓存管理系统分配缓存的执行时间是确实的,释放缓存的执行时间是不确定的。这在绝大部分嵌入式系统中是可以接受的,但在一些对执行时间非常敏感的系统(如机床控制系统等)中,则需要保证分配和释放的执行时间都是确定的。

为了保证释放缓存的执行时间确定,关键是需要避免释放时的循环查找,这有两种方法可以做到。第一种将上述指针链表直接用内存块的地址,而不是pBlocks数组的下标来表示。当然这会导致用户链表的内存开销的增加。其代码如下所示(省略了初始化函数和缓存分配函数):

mm.c

//The head for each buffer block, it's only used by MM system.

typedef struct {

  uint8_t  ucIsUsed;  /* Indicate if the buffer is allocated (TRUE), or not (FALSE)*/

   /*

    * Pointer to the head of the next empty buffer if the buffer is linked to the empty buffer pool.

    * If a buffer block is allocated, this value is meaningless. For the last one of empty buffer link

    * it is set to NULL.

    */

  mm_head_t  *pNextBuffer;

} mm_head_t;

//Strcture of a buffer block.

typedef struct {

  mm_head_t    head;

  uint8_t      pucBlock[BLOCK_SIZE];   

}mm_block_t;

//The head of index of empty memory block, which its BLOCK_NUM means

//no empty block head.

static mm_head_t *pEmptyBlockHead;

void vFreeBuffer(uint8_t *pucBuf) {

  mm_block_t *pMMTmp;

  uint8_t     ucStep;

  if(NULL == pucBuf) {

    return; //Nothing to do.

  }

  //Get the address of the corresponding block of the buffer.

  pMMTmp = (mm_block_t *)(pucBuf - BLOCK_OFFSET);

  //Check validity of the address.

  if((pMMTmp < pBlocks) || ((pMMTmp – pBlocks) / sizeof(pBlocks[0]) >= BLOCK_NUM)) {

    return;  //Invalid address.

  }

  //Link it to the head of the empty buffers.

  if(pMMTmp->head.ucIsUsed) {

    pMMTmp->head.ucIsUsed = FALSE;

    pMMTmp->head. pNextBuffer = pEmptyBlockHead;

    pEmptyBlockHead = (mm_head_t *)pMMTmp;

  }

  return;

}

另一种方法是保持用pBlocks的数组下标建立链表的方式,但采用计算的方式来获取待释放内存的数组下标。

  void vFreeBuffer(uint8_t *pucBuf) {

  mm_block_t *pMMTmp;

  uint8_t     ucStep;

  if(NULL == pucBuf) {

    return; //Nothing to do.

  }

  //Look for the corresponding block of the buffer.

  pMMTmp = (mm_block_t *)( pucBuf - BLOCK_OFFSET);

  if(pMMTmp < pBlocks) {

    return;

  }

  ucStep = (uint8_t)(((uint8_t *)pMMTmp - (uint8_t *)pBlocks) / sizeof(pBlocks[0]));

  if((ucStep >= BLOCK_NUM) || (pucBuf != (uint8_t *)&pBlocks[ucStep] + BLOCK_OFFSET)) {

    //Invalid address. Output warning

    return;

  }

  //Link it to the head of the empty buffers.

  if(pMMTmp->head.ucIsUsed) {

    pMMTmp->head.ucIsUsed = FALSE;

    pMMTmp->head.ucNextIdx = ucEmptyBlockHead;

    ucEmptyBlockHead = ucStep;

  }

  else {

    //Double freeing, Nothing to do.

  }

  return;

}

在上一节的实现中,虽然对被释放的内存地址做了检查。但这种检查只是确保了被释放的地址在缓存区的范围内,并没有检查是否是一个合法的缓存起始地址。而其他两种实现中,只有是合法的缓存起始地址时,才会执行释放操作。这就保证了对错误地址的释放操作不会导致缓存系统崩溃。

4. 带内存泄露检查的缓存系统的实现

这种缓存管理还是存在动态内存分配同样的问题:没有办法检出内存泄漏。由于这种缓存系统只用于任务之间、或者任务与中断处理函数之间的数据传递,该数据在使用完后,缓存应该释放,因此该缓存的占用时间不应该太长。利用这一点,我们可以增加检查代码,以检出缓存泄露问题。由于检查代码增加了内存使用和处理代码,破坏了执行时间的确定性,因此将它限制在只有调试时有效。在以下的示例代码中,用一个宏__DEBUG决定是否包含检查代码。

mm.h

#ifndef __MM_H_

#define __MM_H_

#include

#include

#define __DEBUG

//Macros for cyclic operation.

#define CYCLE_REDUCTION_UL(a, b)     ((a)>=(b)?((a) - (b)):(0xFFFFFFFF - (b) + (a) + 1))

void vInitMM(void);

#ifdef __DEBUG

uint8_t *pucAllocateBuffer(char *pcFileName, uint32_t ulLineNo);

#define AllocateBuffer()   pucAllocateBuffer(__FILE__, __LINE__)

#else  /*__DEBUG*/

uint8_t *pucAllocateBuffer(void);

#define AllocateBuffer()   pucAllocateBuffer()

#endif /*__DEBUG*/

void vFreeBuffer(uint8_t *pucBuf);

#ifdef __DEBUG

void checkLeakedMemoryBlock(void);

#define Get_Cur_Time_S()   ((uint32_t)time(NULL))

#endif /*__DEBUG*/

#endif /*__MM_H_*/

以下是源文件:

mm.c

#ifdef __DEBUG

//Maximum length in byte of filename, used to locate the file allocated the buffer.

#define FILE_NAME_MAX_LENGTH     (32)

#define MEM_BLOCK_MAX_LIFE_S     (5)

#endif /*__DEBUG*/

//The size of each buffer block.

#define BLOCK_SIZE               (256)

//The number of buffer blocks.

#define BLOCK_NUM                (20)

//The head for each buffer block, it's only used by MM system.

typedef struct {

  uint8_t  ucIsUsed;  /* Indicate if the buffer is allocated (TRUE), or not (FALSE)*/

   /*

    * Index of the next empty buffer if the buffer is linked to the empty buffer pool. This

    * index is the one into pBlocks, If a buffer block is allocated, this value is meaningless.

    * For the last one of empty buffer link it is set to BLOCK_NUM.

    */

  uint8_t  ucNextIdx;

#ifdef __DEBUG

  /*The file name to apply for the buffer. */

   int8_t  pcFileName[FILE_NAME_MAX_LENGTH];

  /*The line number to apply for the buffer. */

  uint16_t usLine;

  /*The time in second to indicate how long the buffer has been allocated. */

  uint8_t  ucLife_S;

#endif /*__DEBUG*/

} mm_head_t;

//Strcture of a buffer block.

typedef struct {

  mm_head_t    head;

  uint8_t      pucBlock[BLOCK_SIZE];   

}mm_block_t;

//Memory for all buffer blocks.

mm_block_t pBlocks[BLOCK_NUM];

//The head of index of empty memory block, which its BLOCK_NUM means no empty block head.

uint8_t ucEmptyBlockHead;

#ifdef __DEBUG

uint32_t   ulBufferLeakCheckTime_S;

#endif /*__DEBUG*/

#define BLOCK_OFFSET        ((uint8_t)(((mm_block_t *)0)->pucBlock))

void vInitMM(void) {

  uint8_t     ucStep;

  mm_block_t *pMMTmp;

  memset((void *)pBlocks, 0, sizeof(mm_block_t) * BLOCK_NUM);

  ucEmptyBlockHead = 0;

  pMMTmp = &pBlocks[ucEmptyBlockHead];

  for(ucStep = 0; ucStep < BLOCK_NUM; ucStep++) {

    pMMTmp->head.ucNextIdx = ucStep + 1;

    if(pMMTmp->head.ucNextIdx < BLOCK_NUM) {

      pMMTmp = &pBlocks[pMMTmp->head.ucNextIdx];

    }

  }

#ifdef __DEBUG

  ulBufferLeakCheckTime_S = Get_Cur_Time_S();

#endif /*__DEBUG*/

}

#ifdef __DEBUG

uint8_t *pucAllocateBuffer(char *pcFileName, uint32_t ulLineNo) {

#else

uint8_t *pucAllocateBuffer(void) {

#endif

  uint8_t *pAllocBuf;

  if(BLOCK_NUM == ucEmptyBlockHead) {

    printf("No empty buffer exists.\n");

    return NULL; //No empty buffer exists.

  }

#ifdef __DEBUG

  //Record the file name to apply for the memory block.

  if(NULL != pcFileName) {

    uint16_t usLength  = (uint16_t)strlen(pcFileName) + 1;

    uint16_t usOffset  = 0;

    /*If the file name is longer than the buffer size, the last part are extracted and saved.*/

    if(usLength > FILE_NAME_MAX_LENGTH) {

      usOffset = usLength - FILE_NAME_MAX_LENGTH + 1;

      usLength = FILE_NAME_MAX_LENGTH - 1;

    }

    memcpy(pBlocks[ucEmptyBlockHead].head.pcFileName, pcFileName + usOffset, usLength);

    pBlocks[ucEmptyBlockHead].head.pcFileName[FILE_NAME_MAX_LENGTH - 1] = '\0';

  }

  //Record the line number to apply for the memory block.

  pBlocks[ucEmptyBlockHead].head.usLine = (uint16_t)ulLineNo;

  //Record the time to apply for the memory block.

  pBlocks[ucEmptyBlockHead].head.ucLife_S = MEM_BLOCK_MAX_LIFE_S;

#endif /*__DEBUG*/

  pAllocBuf = pBlocks[ucEmptyBlockHead].pucBlock;

  pBlocks[ucEmptyBlockHead].head.ucIsUsed = TRUE;

  ucEmptyBlockHead = pBlocks[ucEmptyBlockHead].head.ucNextIdx;

  return pAllocBuf;

}

void vFreeBuffer(uint8_t *pucBuf) {

  mm_block_t *pMMTmp;

  uint8_t     ucStep;

  if(NULL == pucBuf) {

    printf("Try to free empty buffer.\n");

    return; //Nothing to do.

  }

  //Look for the corresponding block of the buffer.

  //ATTENTION: This loop makes the execution time not determined.

  pMMTmp = (mm_block_t *)( pucBuf - BLOCK_OFFSET);

  if(pMMTmp < pBlocks) {

    //Invalid address. Output warning

    return;

  }

  ucStep = (uint8_t)(((uint8_t *)pMMTmp - (uint8_t *)pBlocks) / sizeof(pBlocks[0]));

  if((ucStep >= BLOCK_NUM) || (pucBuf != (uint8_t *)&pBlocks[ucStep] + BLOCK_OFFSET)) {

    //Invalid address. Output warning

    return;

  }

  //Link it to the head of the empty buffers.

  if(pMMTmp->head.ucIsUsed) {

    pMMTmp->head.ucIsUsed = FALSE;

    pMMTmp->head.ucNextIdx = ucEmptyBlockHead;

    ucEmptyBlockHead = ucStep;

  }

  else {

    //Double freeing, warning.

   printf("Try to double free.\n");

  }

  return;

}

#ifdef __DEBUG

void checkLeakedMemoryBlock(void) {

  uint32_t ulCurTime_S;

  uint8_t  ucStep, ucPassedTime_S;

  //Check how many seconds are passed.

  ulCurTime_S = Get_Cur_Time_S();

  ucPassedTime_S = CYCLE_REDUCTION_UL(ulCurTime_S, ulBufferLeakCheckTime_S);

  if(0 == ucPassedTime_S)

    return;

  //Update the monitoring time.

  ulBufferLeakCheckTime_S = ulCurTime_S;

  //Check all buffers.

  for(ucStep = 0; ucStep < BLOCK_NUM; ucStep++) {

    if(pBlocks[ucStep].head.ucIsUsed) {

       if(pBlocks[ucStep].head.ucLife_S <= ucPassedTime_S) {

         //Output buffer leak warning.

        printf("Overtime: %d\n", ucStep);

        pBlocks[ucStep].head.ucIsUsed = FALSE;

        pBlocks[ucStep].head.ucNextIdx = ucEmptyBlockHead;

        ucEmptyBlockHead = ucStep;

      }

      else {

        pBlocks[ucStep].head.ucLife_S -= ucPassedTime_S;

      }

    }

  }

}

#endif /*__DEBUG*/

为了有助于定位缓存泄露,每一次申请缓存时,在缓存头中记录了申请文件名和源代码的行号,同时还记录了缓存申请时的时间(以秒为单位)。为了防止文件名过长,在上述例子中只记录了最后的31字节。另外还定义了一个检查缓存泄露的函数checkLeakedMemoryBlock(),如果检出某个缓存已泄露,则可以用申请时记录的文件名和行号来协助定位问题。


checkLeakedMemoryBlock()设计为一个周期性执行的函数,因此必须在某个周期性执行的循环中运行,一般是在某个低优先级任务的主循环中调用。为了维护各个缓存的寿命,用一个全局变量ulBufferLeakCheckTime_S记录以秒为单位的当前时间,在每次循环中计算经过的时间,然后从各个缓存的寿命中减去该时间。如果剩余寿命为0,则认为该缓存已泄露,可以通过打印LOG给出报警信息。为了有助于定位缓存泄露,每一次申请缓存时,在缓存头中记录了申请文件名和源代码的行号,同时还记录了缓存申请时的时间(以秒为单位)。为了防止文件名过长,在上述例子中只记录了最后的31字节。另外还定义了一个检查缓存泄露的函数checkLeakedMemoryBlock(),如果检出某个缓存已泄露,则可以用申请时记录的文件名和行号来协助定位问题。

你可能感兴趣的:(嵌入式开发,内存管理,任务间通信)