FreeRTOS内存管理
目录
- FreeRTOS内存管理
- 1. 为什么不直接使用C库函数的malloc和free函数
- 2. FreeRTOS的五种内存管理方式
- 3. heap4源码分析
- 3.1 堆内存池
- 3.2 内存块的链表数据结构
- 3.3 堆的初始化
- 3.4 堆的内存分配
- 3.5 堆的内存释放
- 4. 总结
1. 为什么不直接使用C库函数的malloc和free函数
在C语言的库函数中,有malloc、free等函数,但是在FreeRTOS中,它们不适用:
- 不适合用在资源紧缺的嵌入式系统中
- 这些函数的实现过于复杂、占据的代码空间太大
- 并非线程安全的(thread-safe)
- 运行有不确定性:每次调用这些函数时花费的时间可能都不相同
- 内存碎片化
- 使用不同的编译器时,需要进行复杂的配置
- 有时候难以调试
2. FreeRTOS的五种内存管理方式
FreeRTOS中内存管理的接口函数为:pvPortMalloc、vPortFree,对应于C库的malloc、free。
- heap_1:只分配,不回收;只实现了pvPortMalloc,没有实现vPortFree,因此不会产生碎片问题,分配时间确定
- heap_2:采用最佳匹配算法,会产生大量内存碎片,没有对内存碎片进行合并,分配时间不定
- heap_3:采用标准C库的malloc、free,由于并非线程安全,因此heap_3中先暂停rtos调度器,再进行分配,会产生内存碎片,时间不定
- heap_4:是目前常用的堆管理方式,采用首次适应算法,能够合并相邻内存块,减少内存碎片,同样分配时间不定
- heap_5:在heap_4的基础上,它可以管理多块、分隔开的内存。
3. heap4源码分析
目前heap_4的使用最为广泛,本文基于heap_4.c的源码进行进一步分析。
源码路径:Middlewares\Third_Party\FreeRTOS\Source\portable\MemMang\heap_4.c``
3.1 堆内存池
/* Allocate the memory for the heap. */
#if( configAPPLICATION_ALLOCATED_HEAP == 1 )/* The application writer has already defined the array used for the RTOSheap - 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 */
heap4采用的是线性数组结构,大小为configTOTAL_HEAP_SIZE个字节
如果定义了configAPPLICATION_ALLOCATED_HEAP宏,可以由用户层自行定义内存池的位置
3.2 内存块的链表数据结构
/* Define the linked list structure. This is used to link free blocks in order
of their memory address. */
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;
通过BlockLink_t这个链表对空闲的内存块进行管理,这个结构体包括内存块的大小以及指向下一个内存块头的指针,并且由两个静态链表指针xStart、pxEnd来标识开头和结尾。除了xStart以外,其余内存控制块都是在内存池中,用指针的形式进行访问。也就是说,xStart是作为哨兵节点,xStart的pxNextFreeBlock指针就是第一个空闲块节点。
static BlockLink_t xStart, *pxEnd = NULL;
另外,heapMINIMUM_BLOCK_SIZE限制了内存块的最小值,当内存块小于heapMINIMUM_BLOCK_SIZE时,就不再维护这个内存碎片了。
3.3 堆的初始化
通过prvHeapInit函数完成堆的初始化
static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;
size_t uxAddress;
size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;/* Ensure the heap starts on a correctly aligned boundary. */uxAddress = ( size_t ) ucHeap;// 内存对齐if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 ){uxAddress += ( portBYTE_ALIGNMENT - 1 );uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );xTotalHeapSize -= uxAddress - ( size_t ) ucHeap;}pucAlignedHeap = ( uint8_t * ) uxAddress;// 初始化xStart/* xStart is used to hold a pointer to the first item in the list of freeblocks. The void cast is used to prevent compiler warnings. */xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;xStart.xBlockSize = ( size_t ) 0;/* pxEnd is used to mark the end of the list of free blocks and is insertedat the end of the heap space. */// 初始化pxEnduxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;uxAddress -= xHeapStructSize;uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );pxEnd = ( void * ) uxAddress;pxEnd->xBlockSize = 0;pxEnd->pxNextFreeBlock = NULL;/* To start with there is a single free block that is sized to take up theentire heap space, minus the space taken by pxEnd. */pxFirstFreeBlock = ( void * ) pucAlignedHeap;pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;pxFirstFreeBlock->pxNextFreeBlock = pxEnd;/* Only one block exists - and it covers the entire usable heap space. */xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;/* Work out the position of the top bit in a size_t variable. */xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}
该函数主要完成了内存块链表的初始化。初始化完成后,xStart.pxNextFreeBlock指向了内存池对齐后的首地址,pxEnd指针则指向了内存池末尾往前的xHeapStructSize个字节大小位置。
因此,除了内存对齐后减少的空间外,内存池末尾还留有xHeapStructSize个字节存放pxEnd指向的内存头,用于标识末尾。
/* The size of the structure placed at the beginning of each allocated memory
block must by correctly byte aligned. */
static const size_t xHeapStructSize = ( sizeof( BlockLink_t ) + ( ( size_t ) ( portBYTE_ALIGNMENT - 1 ) ) ) & ~( ( size_t ) portBYTE_ALIGNMENT_MASK );#define portBYTE_ALIGNMENT 8
#define portBYTE_ALIGNMENT_MASK ( 0x0007 )
在stm32中,xHeapStructSize为8字节。
3.4 堆的内存分配
通过pvPortMalloc函数完成堆的内存分配
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;vTaskSuspendAll(); // 进入临界区{/* 如果是第一次调用pvPortMalloc,那么就调用prvHeapInit来初始化堆内存池 */if( pxEnd == NULL ){prvHeapInit();}// 判断申请的内存大小是否超过上限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; // 申请的内存要加上额外的内存头8字节/* Ensure that blocks are always aligned to the required numberof bytes. */if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 ){/* 内存对齐 */xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );}}// 想要申请的内存比剩余的空闲内存小if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) ){/* 遍历各个空闲内存块,找到第一个空闲内存块,它的大小比想要申请的内存大 */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 );/* 把 pxBlock 从空闲链表中移除 */pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;/* 判断产生的内存碎片是否大于规定的最小内存碎片 */if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE ){/* 创建一个新的空闲内存块 */pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );/* 计算两个内存块的大小 */pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;pxBlock->xBlockSize = xWantedSize;/* 将新的内存块插入到空闲链表中 */prvInsertBlockIntoFreeList( pxNewBlockLink );}/* 更新剩余的空闲内存 */xFreeBytesRemaining -= pxBlock->xBlockSize;if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining ){xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;}/* The block is being returned - it is allocated and ownedby the application and has no "next" block. */pxBlock->xBlockSize |= xBlockAllocatedBit;pxBlock->pxNextFreeBlock = NULL;}}}traceMALLOC( pvReturn, xWantedSize );}( void ) xTaskResumeAll(); // 退出临界区// 如果定义了分配内存的钩子函数并且分配失败,就去调用#if( configUSE_MALLOC_FAILED_HOOK == 1 ){if( pvReturn == NULL ){extern void vApplicationMallocFailedHook( void );vApplicationMallocFailedHook();}}#endifreturn pvReturn; // 返回分配的内存块(跳过了内存头部的)
}
/*-----------------------------------------------------------*/
- 当首次调用pvPortMalloc,会调用prvHeapInit对内存链表进行初始化
- 判断申请的内存大小是否超过上限
- 遍历空闲内存块链表,找到第一个内存块大小是足够分配的
- 将该内存块从链表中剔除,如果剩余空间大小超过了最小内存块大小,那么就创建新的内存块,并插回内存块链表
- 如果分配失败,并且用户注册了对应的回调函数,就去调用
注意到:动态内存对于内存的消耗是大于应用层申请的内存大小,原因在于内存对齐和内存块头部的开销导致。
堆的内存分配和内存释放,都涉及到将内存块插回空闲链表中,heap4_c实现了空闲内存可合并,一定程度上解决内存碎片问题,就在prvInsertBlockIntoFreeList函数中体现。
prvInsertBlockIntoFreeList函数如下:
static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{BlockLink_t *pxIterator;uint8_t *puc;/* 找到一个空闲块,这个块的地址比插入的块地址低,下一个块的地址比插入的块地址高 */for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock ){/* Nothing to do here, just iterate to the right position. */}/* 找到插入位置后, 判断这个位置的尾地址能否和插入块衔接起来,如果可以就合并 */puc = ( uint8_t * ) pxIterator;if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert ){pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;pxBlockToInsert = pxIterator;}/* 判断插入块能否和 插入位置的下一个内存块衔接起来,如果可以就合并 */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;}}
- 首先在空闲内存链表中,找到插入位置,即这个块的地址比插入块地址低,这个块的下一个块地址比插入块地址高
- 判断插入块能否和左边的空闲内存块以及右边的空闲内存块合并,也就是边界相等,如果可以就合并成一个大的内存块
所以说:heap4_c所谓的空闲内存可合并,只是合并相邻的内存碎片,这是基于内存池的线性连续而设计的。
因此,为了尽可能的减少内存碎片,提升内存合并的作用,尽可能把上电后不释放的动态内存在初始化阶段申请(比如说动态分配的任务,包括TCB和任务栈),然后对于重复申请释放的动态内存,在初始化阶段结束后再分配和使用。也就是说,堆的前半部分内存都用于不释放的动态内存,然后后半部分就用来一些频繁申请释放的动态内存。
3.5 堆的内存释放
通过vPortFree函数完成堆的内存释放
vPortFree函数如下:
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 -= xHeapStructSize;/* This casting is to keep the compiler from issuing warnings. */pxLink = ( void * ) puc;/* Check the block is actually allocated. */configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );configASSERT( pxLink->pxNextFreeBlock == NULL );if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 ){if( pxLink->pxNextFreeBlock == NULL ){/* The block is being returned to the heap - it is no longerallocated. */pxLink->xBlockSize &= ~xBlockAllocatedBit;vTaskSuspendAll();{/* Add this block to the list of free blocks. */xFreeBytesRemaining += pxLink->xBlockSize;traceFREE( pv, pxLink->xBlockSize );prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );}( void ) xTaskResumeAll();}}}
}
/*-----------------------------------------------------------*/
释放内存比较简单,就是把申请释放的内存地址前移xHeapStructSize找到内存头部的位置,
然后进入临界区,把这个内存块插入到空闲链表当中,并增大xFreeBytesRemaining变量(记录当前空闲内存大小的变量)的大小,最后退出临界区。
4. 总结
- heap_4的堆内存池是建立在一片线性连续的内存上的(全局数组)。
- 在申请和释放内存时,有查询空闲块链表的操作,其最坏的时间复杂度是On,时间是不确定的。
- 另外由于需要内存头部来维护空闲链表以及内存对齐,这导致了实际可分配的动态内存小于分配的这个线性连续的内存。
- heap4只能合并相邻的内存碎片,并不能彻底解决内存碎片问题。
参考学习:
freeRTOS动态内存heap4源码分析_freertos heap4-CSDN博客
【FreeRTOS】FreeRTOS内存管理的五种方式-CSDN博客
FreeRTOS 内存管理策略_freertos查看内存碎片功能-CSDN博客