目录
heap_1.c内存管理算法
heap_2.c内存管理算法
heap_3.c内存管理算法
heap_4.c内存管理算法
heap_5.c内存管理算法
内存管理对应用程序和操作系统来说非常重要,而内存对于嵌入式系统来说是寸土寸金的资源,FreeRTOS操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些函数的具体实现。这样做大有好处,可以增加系统的灵活性,不同的应用场合可以使用不同的内存分配实现,选择对自己更有利的内存管理算法。比如对于安全型的嵌入式系统,通常不允许动态分配内存,那么可以采用非常简单的内存管理策略,一经申请的内存,甚至不允许被释放。
FreeRTOS内核规定的几个内存管理的函数原型为:
1. void *pvPortMalloc( size_t xSize ) :内存申请函数
2. void vPortFree( void *pv ) :内存释放函数
3. void vPortInitialiseBlocks( void ) :初始化内存堆函数
4. size_t xPortGetFreeHeapSize( void ) :获取当前未分配的内存堆大小
5. size_t xPortGetMinimumEverFreeHeapSize( void ):获取未分配的内存堆历史最小值
FreeRTOS中内存管理的接口函数为pvPortMalloc、vPortFree,对应于C库的malloc和free函数。
源码中对应的提供了5个文件,对应内存管理的5种方法。
文件 | 优点 | 缺点 |
heap_1.c | 分配简单,时间确定 | 只分配,不回收 |
heap_2.c | 动态分配,最佳匹配 | 碎片,时间不定 |
heap_3.c | 调用标准库函数 | 速度慢,时间不定 |
heap_4.c | 相邻空闲内存可以合并 | 可解决碎片问题,时间不定 |
heap_5.c | 在heap_4.c基础上支持分隔的内存块 | 可解决碎片问题,时间不定 |
对于heap_1.c和heap_2.c和heap_4.c这三种内存管理算法,内存堆实际上是一个很大的静态数组,定义为:
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
其中宏configTOTAL_HEAP_SIZE用来定义内存堆的大小,这个宏在FreeRTOSConfig.h中设置。
对于heap_3.c,这种策略只是简单的包装了标准库中的malloc()和free()函数,包装后的malloc()和free()函数具备线程保护。因此,内存堆需要通过编译器或者启动文件设置堆空间。
heap_5.c比较有趣,它允许程序设置多个非连续内存堆,比如需要快速访问的内存堆设置在片内RAM,稍微慢速访问的内存堆设置在外部RAM。每个内存堆的起始地址和大小由应用程序设计者定义。(需要手动自己定义一个数组,手动调用函数初始化内存)
heap_1.c内存管理算法
通过阅读heap_1.c的源码分析可以知道,它只实现了pvPortMalloc,没有实现vPortFree.
如果你的程序不需要删除内核对象,那么可以使用heap_1.c,它只分配不回收内存:
- 实现最简单
- 没有碎片问题
- 一些要求非常严格的系统里,不允许使用动态内存,就可以使用heap_1
对于这种内存分配,我们可以把内存看成一个一根完整的长面包,每次申请内存,就从一端切下适当长度的面包返回给申请者,知道面包被分配完毕,就这么简单。
它的实现原理很简单,首先定义一个大数组:
/* Allocate the memory for the heap. */
#if ( configAPPLICATION_ALLOCATED_HEAP == 1 )/* The application writer has already defined the array used for the RTOS
* heap - probably so it can be placed in a special segment or address. */extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#elsestatic uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#endif /* configAPPLICATION_ALLOCATED_HEAP */
然后对于 pvPortMalloc 调用时,从这个数组中分配空间。
FreeRTOS在创建任务时,需要2个内核对象:task control block(TCB)、stack。
使用heap_1时,内存分配过程如下图所示:
- A:创建任务之前整个数组都是空闲的
- B:创建第1个任务之后,蓝色区域被分配出去了
- C:创建3个任务之后的数组使用情况
内存申请:pvPortMalloc
/*-----------------------------------------------------------*/void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn = NULL;
static uint8_t *pucAlignedHeap = NULL;/* Ensure that blocks are always aligned to the required number of bytes. */#if( portBYTE_ALIGNMENT != 1 ){if( xWantedSize & portBYTE_ALIGNMENT_MASK ){/* Byte alignment required. */xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );}}#endifvTaskSuspendAll();{if( pucAlignedHeap == NULL ){/* Ensure the heap starts on a correctly aligned boundary. */pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );}/* Check there is enough room left for the allocation. */if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) )/* Check for overflow. */{/* Return the next free byte then increment the index past thisblock. */pvReturn = pucAlignedHeap + xNextFreeByte;xNextFreeByte += xWantedSize;}traceMALLOC( pvReturn, xWantedSize );}( void ) xTaskResumeAll();#if( configUSE_MALLOC_FAILED_HOOK == 1 ){if( pvReturn == NULL ){extern void vApplicationMallocFailedHook( void );vApplicationMallocFailedHook();}}#endifreturn pvReturn;
}
如果是第一次执行这个函数,需要将变量pucAlignedHeap指向内存堆区域第一个地址对齐处。我们上面说内存堆其实是一个大数组,编译器为这个数组分配的起始地址是随机的,可能不符合我们的对齐需要,这时候要进行调整。比如数组地址是0x20000001,这时候一些芯片是对齐才能工作,所以代码一开始设置的内存初始状态如下图所示:
在申请内存过程中同样会对要申请的内存大小做一个字节对齐的操作(向上对齐) ,比如对于8字节对齐的系统来说, 你申请11个字节大小的内存,这时候向上对齐就是16字节,接下来就是挂起调度器,防止多个任务同时申请内存,因为内存是不可重入的(使用了静态变量)。
分配内存的过程如下图:
下面是一个更加清晰的整体的一个分配内存的图:
内存分配完成后,不管有没有分配成功都恢复之前挂起的调度器。
如果内存分配不成功,这里最可能是内存堆空间不够用了,会调用一个钩子函数vApplicationMallocFailedHook()。这个钩子函数由应用程序提供,通常我们可以打印内存分配设备信息或者点亮也故障指示灯。
heap_2.c内存管理算法
Heap_2之所以还保留,只是为了兼容以前的代码。新设计中不再推荐使用Heap_2。建议使用Heap_4来替代Heap_2,更加高效。
Heap_2也是在数组上分配内存,跟Heap_1不一样的地方在于:
- Heap_2使用最佳匹配算法(best fit)来分配内存
- 它支持vPortFree
最佳匹配算法:
- 假设heap有3块空闲内存:5字节、25字节、100字节
- pvPortMalloc想申请20字节
- 找出最小的、能满足pvPortMalloc的内存:25字节
- 把它划分为20字节、5字节
- 返回这20字节的地址
- 剩下的5字节仍然是空闲状态,留给后续的pvPortMalloc使用
与Heap_4相比,Heap_2不会合并相邻的空闲内存,所以Heap_2会导致严重的"碎片化"问题。
但是,如果申请、分配内存时大小总是相同的,这类场景下Heap_2没有碎片化的问题。所以它适合这种场景:频繁地创建、删除任务,但是任务的栈大小都是相同的(创建任务时,需要分配TCB和栈,TCB总是一样的)。
虽然不再推荐使用heap_2,但是它的效率还是远高于malloc、free。
Tips: 其实heap_2.c算法就是在heap_1.c的基础上进行的改进,其中可以思考为什么heap_1.c没办法实现free函数吗,因为free函数释放内存只传入你要释放内存的地址,对于要释放多少呢,heap_1.c算法是没有记录的,我记得我在学习C语言的时候也想过这个问题,free函数只传入要释放内存的地址,它怎么知道要释放多大呢,在操作系统的学习中了解过这部分知识,其实申请的时候,会有一个头部在申请内存的前面,这个添加头部的方法其实很常见。
局部静态变量pucAlignedHeap指向对齐后的内存堆起始位置。地址对齐的原因在第一种内存管理策略中已经说明。假如内存堆数组ucHeap从RAM地址0x10002003处开始,系统按照8字节对齐,则对齐后的内存堆与第一个内存管理策略一样,如图所示:
与第一种内存管理策略不同,第二种内存管理策略使用一个链表结构来跟踪记录空闲内存块,将空闲块组成一个链表。头部结构体定义为:
typedef struct A_BLOCK_LINK
{struct A_BLOCK_LINK *pxNextFreeBlock; /*<< The next free block in the list. */size_t xBlockSize; /*<< The size of the free block. */
} BlockLink_t;static BlockLink_t xStart, xEnd;
两个BlockLink_t类型的全局静态变量xStart和xEnd用来标识空闲内存块的起始和结束。刚开始时,整个内存堆有效空间就是一个空闲块:
当申请N字节内存时,实际上不仅需要分配N字节内存,还要分配一个BlockLink_t类型结构体空间,用于描述这个内存块,结构体空间位于空闲内存块的最开始处。当然,和第一种内存管理策略一样,申请的内存大小加上BlockLink_t类型结构体大小之后也要向上扩大到对齐字节数的整数倍 。
我们看一下内存申请过程:首先计算实际要分配的内存大小,判断申请的内存是否合法。如果合法则从链表头xStart开始查找,如果某个空闲块的xBlockSize字段大小能容得下要申请的内存,则从这块内存取出合适的部分返回给申请者,剩下的内存块组成一个新的空闲块,按照空闲块的大小顺序插入到空闲块链表中,小块在前大块在后。
/*-----------------------------------------------------------*/void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
static BaseType_t xHeapHasBeenInitialised = pdFALSE;
void *pvReturn = NULL;vTaskSuspendAll();{/* If this is the first call to malloc then the heap will requireinitialisation to setup the list of free blocks. */if( xHeapHasBeenInitialised == pdFALSE ){prvHeapInit();xHeapHasBeenInitialised = pdTRUE;}/* The wanted size is increased so it can contain a BlockLink_tstructure in addition to the requested amount of bytes. */if( xWantedSize > 0 ){xWantedSize += heapSTRUCT_SIZE;/* Ensure that blocks are always aligned to the required number of bytes. */if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 ){/* Byte alignment required. */xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );}}if( ( xWantedSize > 0 ) && ( xWantedSize < configADJUSTED_HEAP_SIZE ) ){/* Blocks are stored in byte order - traverse the list from the start(smallest) block until one of adequate size is found. */pxPreviousBlock = &xStart;pxBlock = xStart.pxNextFreeBlock;while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) ){pxPreviousBlock = pxBlock;pxBlock = pxBlock->pxNextFreeBlock;}/* If we found the end marker then a block of adequate size was not found. */if( pxBlock != &xEnd ){/* Return the memory space - jumping over the BlockLink_t structureat its start. */pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );/* This block is being returned for use so must be taken out of thelist of free blocks. */pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;/* If the block is larger than required it can be split into two. */if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE ){/* This block is to be split into two. Create a new blockfollowing the number of bytes requested. The void cast isused to prevent byte alignment warnings from the compiler. */pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );/* Calculate the sizes of two blocks split from the singleblock. */pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;pxBlock->xBlockSize = xWantedSize;/* Insert the new block into the list of free blocks. */prvInsertBlockIntoFreeList( ( pxNewBlockLink ) );}xFreeBytesRemaining -= pxBlock->xBlockSize;}}traceMALLOC( pvReturn, xWantedSize );}( void ) xTaskResumeAll();#if( configUSE_MALLOC_FAILED_HOOK == 1 ){if( pvReturn == NULL ){extern void vApplicationMallocFailedHook( void );vApplicationMallocFailedHook();}}#endifreturn pvReturn;
}
阅读源码的个人理解:
第一次调用malloc申请内存时需要进行heap的初始化操作,而它的初始化操作就是将heap加上一个头部,以及初始化xStart和xEnd,heap_2.c的内存管理是通过块Block进行管理的。
- 在申请内存块的时候,使用的是最佳匹配算法,在空闲块的链表中找到一个刚好合适又最小的块,如果申请之后还剩下比较大的内存则会进行拆分,把剩余的内存块添加回空闲内存块管理的链表中,按照内存大小进行插入。
- 正是因为上面空闲内存的插入是按照大小进行插入这一个特点,注定了空闲内存块之间是不能进行合并的,所以会出现内存碎片的情况.
所以我们可以知道heap_2.c适合对于那些频繁申请释放固定大小内存的场合,这样不会造成内存碎片。
/*-----------------------------------------------------------*/void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;if( pv != NULL ){/* The memory being freed will have an BlockLink_t structure immediatelybefore it. */puc -= heapSTRUCT_SIZE;/* This unexpected casting is to keep some compilers from issuingbyte alignment warnings. */pxLink = ( void * ) puc;vTaskSuspendAll();{/* Add this block to the list of free blocks. */prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );xFreeBytesRemaining += pxLink->xBlockSize;traceFREE( pv, pxLink->xBlockSize );}( void ) xTaskResumeAll();}
}
阅读代码释放内存的原理也很简单,无非就是通过要释放内存的地址往前推得到内存块结构体,就可以知道要释放的内存大小了。
heap_3.c内存管理算法
第三种内存管理策略简单的封装了标准库中的malloc()和free()函数,采用的封装方式是操作内存前挂起调度器、完成后再恢复调度器。封装后的malloc()和free()函数具备线程保护。
第一种和第二种内存管理策略都是通过定义一个大数组作为内存堆,数组的大小由宏configTOTAL_HEAP_SIZE指定。第三种内存管理策略与前两种不同,它不再需要通过数组定义内存堆,而是需要使用编译器设置内存堆空间,一般在启动代码中设置。因此宏configTOTAL_HEAP_SIZE对这种内存管理策略是无效的。
heap_4.c内存管理算法
跟Heap_1、Heap_2一样,Heap_4也是使用大数组来分配内存。
Heap_4使用首次适应算法(first fit)来分配内存。它还会把相邻的空闲内存合并为一个更大的空闲内存,这有助于较少内存的碎片问题。
首次适应算法:
- 假设堆中有3块空闲内存:5字节、200字节、100字节
- pvPortMalloc想申请20字节
- 找出第1个能满足pvPortMalloc的内存:200字节
- 把它划分为20字节、180字节
- 返回这20字节的地址
- 剩下的180字节仍然是空闲状态,留给后续的pvPortMalloc使用
Heap_4会把相邻空闲内存合并为一个大的空闲内存,可以较少内存的碎片化问题。适用于这种场景:频繁地分配、释放不同大小的内存。
Tips:其实个人感觉第四种内存管理算法就是在第二种内存管理算法heap_2.c的基础上再次进行改进而已, 由于heap_2.c算法存在内存碎片的问题,所以heap_4.c可以将相邻的内存块进行合并,这就意味着空闲内存块之间是按照地址进行存放的,不是像heap_2.c一样按照大小进行排序管理的。
和第二种内存管理策略一样,它也使用一个链表结构来跟踪记录空闲内存块。头部结构体定义是一样的。
与第二种内存管理策略一样,空闲内存块也是以单链表的形式组织起来的,BlockLink_t类型的全局静态变量xStart表示链表头,但第四种内存管理策略的链表尾保存在内存堆空间最后位置,并使用BlockLink_t指针类型局部静态变量pxEnd指向这个区域(第二种内存管理策略使用静态变量xEnd表示链表尾)(这里我想吐槽一下,感觉完全没必要啊,完全可以使用和heap_2.c一样的管理,可能这里是为了节省内存,能省就省就和迷你列表项一样🤣)
重点:第四种内存管理算法和第二种内存管理算法之间最大的不同就是,第四种内存管理算法的空闲内存块链表不是以内存块大小为存储顺序,而是以内存块起始地址大小为存储顺序的,地址小的在前,地址大的在后,其实这好像是必须的,因为你要合并相邻的内存块肯定地址要按顺序来才能合并嘛🤣,这是为了适应合并算法而作的改变。
/*-----------------------------------------------------------*/void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;vTaskSuspendAll();{/* If this is the first call to malloc then the heap will requireinitialisation to setup the list of free blocks. */if( pxEnd == NULL ){prvHeapInit();}else{mtCOVERAGE_TEST_MARKER();}/* Check the requested block size is not so large that the top bit isset. The top bit of the block size member of the BlockLink_t structureis used to determine who owns the block - the application or thekernel, so it must be free. */if( ( xWantedSize & xBlockAllocatedBit ) == 0 ){/* The wanted size is increased so it can contain a BlockLink_tstructure in addition to the requested amount of bytes. */if( xWantedSize > 0 ){xWantedSize += xHeapStructSize;/* Ensure that blocks are always aligned to the required numberof bytes. */if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 ){/* Byte alignment required. */xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) ){/* Traverse the list from the start (lowest address) block untilone of adequate size is found. */pxPreviousBlock = &xStart;pxBlock = xStart.pxNextFreeBlock;while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) ){pxPreviousBlock = pxBlock;pxBlock = pxBlock->pxNextFreeBlock;}/* If the end marker was reached then a block of adequate sizewas not found. */if( pxBlock != pxEnd ){/* Return the memory space pointed to - jumping over theBlockLink_t structure at its start. */pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );/* This block is being returned for use so must be taken outof the list of free blocks. */pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;/* If the block is larger than required it can be split intotwo. */if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE ){/* This block is to be split into two. Create a newblock following the number of bytes requested. The voidcast is used to prevent byte alignment warnings from thecompiler. */pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );/* Calculate the sizes of two blocks split from thesingle block. */pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;pxBlock->xBlockSize = xWantedSize;/* Insert the new block into the list of free blocks. */prvInsertBlockIntoFreeList( pxNewBlockLink );}else{mtCOVERAGE_TEST_MARKER();}xFreeBytesRemaining -= pxBlock->xBlockSize;if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining ){xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;}else{mtCOVERAGE_TEST_MARKER();}/* The block is being returned - it is allocated and ownedby the application and has no "next" block. */pxBlock->xBlockSize |= xBlockAllocatedBit;pxBlock->pxNextFreeBlock = NULL;xNumberOfSuccessfulAllocations++;}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}traceMALLOC( pvReturn, xWantedSize );}( void ) xTaskResumeAll();#if( configUSE_MALLOC_FAILED_HOOK == 1 ){if( pvReturn == NULL ){extern void vApplicationMallocFailedHook( void );vApplicationMallocFailedHook();}else{mtCOVERAGE_TEST_MARKER();}}#endifconfigASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );return pvReturn;
}
申请内存的代码和heap_2.c相比无非就是heap的初始化对于xEnd有点不一样,一个是指针一个是变量,然后就是空闲内存块的插入算法是不一样,一个按照地址来一个按照大小插入。
/*-----------------------------------------------------------*/static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
BlockLink_t *pxIterator;
uint8_t *puc;/* Iterate through the list until a block is found that has a higher addressthan the block being inserted. */for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock ){/* Nothing to do here, just iterate to the right position. */}/* Do the block being inserted, and the block it is being inserted aftermake a contiguous block of memory? */puc = ( uint8_t * ) pxIterator;if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert ){pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;pxBlockToInsert = pxIterator;}else{mtCOVERAGE_TEST_MARKER();}/* Do the block being inserted, and the block it is being inserted beforemake a contiguous block of memory? */puc = ( uint8_t * ) pxBlockToInsert;if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock ){if( pxIterator->pxNextFreeBlock != pxEnd ){/* Form one big block from the two blocks. */pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;}else{pxBlockToInsert->pxNextFreeBlock = pxEnd;}}else{pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;}/* If the block being inserted plugged a gab, so was merged with the blockbefore and the block after, then it's pxNextFreeBlock pointer will havealready been set, and should not be set here as that would make it pointto itself. */if( pxIterator != pxBlockToInsert ){pxIterator->pxNextFreeBlock = pxBlockToInsert;}else{mtCOVERAGE_TEST_MARKER();}
}
这个链表插入函数,在内存释放函数中也会进行调用,它也是内存释放的关键代码, 它里面最关键的就是要进行前后相邻内存块是否能进行合并的判断和合并操作,先和前面的内存块判断是否进行合并,然后再和后面的内存块进行同样的操作。总的来说其实不是特别难,而且heap_4.c是一种比较均衡的内存管理算法,克服了前面heap_1.c和heap_2.c的问题,所以大多数还是采用这个内存管理算法的(以前不懂得时候,就感觉它是万能的,选这个准没错😂)。
heap_5.c内存管理算法
Heap_5分配内存、释放内存的算法跟Heap_4是一样的。
相比于Heap_4,Heap_5并不局限于管理一个大数组:它可以管理多块、分隔开的内存。
在嵌入式系统中,内存的地址可能并不连续,我们heap_4.c是一块连续的内存(声明的一个大数组嘛),而有些时候有的系统中内存地址可能不连续,这种场景下可以使用Heap_5。
既然内存是分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:
在使用pvPortMalloc之前,必须先指定内存块的信息(需要用户自己指定)
使用vPortDefineHeapRegions来指定这些信息
怎么指定一块内存?使用如下结构体:
typedef struct HeapRegion
{uint8_t * pucStartAddress; // 起始地址size_t xSizeInBytes; // 大小
} HeapRegion_t;
怎么指定多块内存?使用一个HeapRegion_t数组,在这个数组中,低地址在前、高地址在后(必须这样做)。
比如:
HeapRegion_t xHeapRegions[] =
{{ ( uint8_t * ) 0x80000000UL, 0x10000 }, // 起始地址0x80000000,大小0x10000{ ( uint8_t * ) 0x90000000UL, 0xa0000 }, // 起始地址0x90000000,大小0xa0000{ NULL, 0 } // 表示数组结束};
因为heap_5.c的malloc函数没有像前面几种内存管理算法一样,第一次调用的时候进行了内存heap的初始化,所以在使用任何内存分配和释放操作前调用vPortDefineHeapRegions()函数初始化这些内存堆(用户自己手动调用)。
第一、第二和第四种内存管理策略都是利用一个大数组作为内存堆使用,并且只需要应用程序指定数组的大小(通过宏configTOTAL_HEAP_SIZE定义),数组定义由内存管理策略实现。第五种内存管理策略有些不同,首先它允许跨内存区定义多个内存堆,比如在片内RAM中定义一个内存堆,还可以在片外RAM再定义内存堆;其次,用户需要指定每个内存堆区域的起始地址和内存堆大小、将它们放在一个HeapRegion_t结构体类型数组中,并需要在使用任何内存分配和释放操作前调用vPortDefineHeapRegions()函数初始化这些内存堆。
/*-----------------------------------------------------------*/void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions )
{
BlockLink_t *pxFirstFreeBlockInRegion = NULL, *pxPreviousFreeBlock;
size_t xAlignedHeap;
size_t xTotalRegionSize, xTotalHeapSize = 0;
BaseType_t xDefinedRegions = 0;
size_t xAddress;
const HeapRegion_t *pxHeapRegion;/* Can only call once! */configASSERT( pxEnd == NULL );pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );while( pxHeapRegion->xSizeInBytes > 0 ){xTotalRegionSize = pxHeapRegion->xSizeInBytes;/* Ensure the heap region starts on a correctly aligned boundary. */xAddress = ( size_t ) pxHeapRegion->pucStartAddress;if( ( xAddress & portBYTE_ALIGNMENT_MASK ) != 0 ){xAddress += ( portBYTE_ALIGNMENT - 1 );xAddress &= ~portBYTE_ALIGNMENT_MASK;/* Adjust the size for the bytes lost to alignment. */xTotalRegionSize -= xAddress - ( size_t ) pxHeapRegion->pucStartAddress;}xAlignedHeap = xAddress;/* Set xStart if it has not already been set. */if( xDefinedRegions == 0 ){/* xStart is used to hold a pointer to the first item in the list offree blocks. The void cast is used to prevent compiler warnings. */xStart.pxNextFreeBlock = ( BlockLink_t * ) xAlignedHeap;xStart.xBlockSize = ( size_t ) 0;}else{/* Should only get here if one region has already been added to theheap. */configASSERT( pxEnd != NULL );/* Check blocks are passed in with increasing start addresses. */configASSERT( xAddress > ( size_t ) pxEnd );}/* Remember the location of the end marker in the previous region, ifany. */pxPreviousFreeBlock = pxEnd;/* pxEnd is used to mark the end of the list of free blocks and isinserted at the end of the region space. */xAddress = xAlignedHeap + xTotalRegionSize;xAddress -= xHeapStructSize;xAddress &= ~portBYTE_ALIGNMENT_MASK;pxEnd = ( BlockLink_t * ) xAddress;pxEnd->xBlockSize = 0;pxEnd->pxNextFreeBlock = NULL;/* To start with there is a single free block in this region that issized to take up the entire heap region minus the space taken by thefree block structure. */pxFirstFreeBlockInRegion = ( BlockLink_t * ) xAlignedHeap;pxFirstFreeBlockInRegion->xBlockSize = xAddress - ( size_t ) pxFirstFreeBlockInRegion;pxFirstFreeBlockInRegion->pxNextFreeBlock = pxEnd;/* If this is not the first region that makes up the entire heap spacethen link the previous region to this region. */if( pxPreviousFreeBlock != NULL ){pxPreviousFreeBlock->pxNextFreeBlock = pxFirstFreeBlockInRegion;}xTotalHeapSize += pxFirstFreeBlockInRegion->xBlockSize;/* Move onto the next HeapRegion_t structure. */xDefinedRegions++;pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );}xMinimumEverFreeBytesRemaining = xTotalHeapSize;xFreeBytesRemaining = xTotalHeapSize;/* Check something was actually defined before it is accessed. */configASSERT( xTotalHeapSize );/* Work out the position of the top bit in a size_t variable. */xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}
这个函数可以看到,代码量还是很大的🤣,但其实原理就是以链表的形式管理这些分隔的内存块。
一旦内存堆初始化之后,内存申请和释放都和第四种内存管理策略相同,不再单独分析。
第五种内存管理策略允许内存堆跨越多个非连续的内存区,并且需要显示的初始化内存堆,除此之外其它操作都和第四种内存管理策略十分相似。