1. 嵌入式系统的内存分配
嵌入式程序开发中,与PC程序开发不同,很少使用完全动态内存分配(malloc()/free()),主要基于以下一些原因:
因此在嵌入式系统中,往往采用静态内存分配,也就是所有的变量,无论是全局变量,还是局部变量,都是在编译时指定大小。这虽然避免了上述动态内存分配的问题,但也有其缺陷,在使用中如果不注意,也会带来新的问题。
如果只使用静态内存分配,在任务间、或者任务与中断服务程序之间进行数据交互时,就不太方便。此时就需要为不同的数据交互的任务组合分配不同的专用内存。如是一个由三个任务构成的系统,如果各个任务之间都有互相数据交互,则需要准备6个静态数据交互内存,而且其数量随着任务数的增加而呈指数增加。而且这种分配方式也很不灵活,可能为交互较少的任务分配了较多的内存,或者为交互较多的任务分配的内存不足等等。
图 1‑1 静态数据交互内存分配
嵌入式操作系统提供了消息(message)或者队列(queue)的手段来解决这个问题。在操作系统层面,为消息或者队列中的每个项都分配了固定大小的内存块。该内存块可以保存需要传输的数据,也可以保存需要交互数据的地址。如果采用消息或者队列的数据项进行直接数据传输时,在发送侧和接收侧都需要执行一次数据拷贝,影响执行效率;而且由于需要传输的数据大小一般依赖于实际应用,因此不可能在操作系统中规定一个数据项的内存大小。因此在实际应用中,消息的发送端将需要传输的数据放在缓存中,并将该内存地址放入消息和队列的数据项中进行发送;消息的接收端则从收到的数据项中取出对应的地址,从而取得所接收的数据。如图 1‑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(),如果检出某个缓存已泄露,则可以用申请时记录的文件名和行号来协助定位问题。