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 RTOS
heap - probably so it can be placed in a special segment or address. */
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#else
static 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 free
blocks. 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 inserted
at the end of the heap space. */
// 初始化pxEnd
uxAddress = ( ( 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 the
entire 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_t
structure in addition to the requested amount of bytes. */
if( xWantedSize > 0 )
{
xWantedSize += xHeapStructSize; // 申请的内存要加上额外的内存头8字节
/* Ensure that blocks are always aligned to the required number
of 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 size
was not found. */
if( pxBlock != pxEnd )
{
/* Return the memory space pointed to - jumping over the
BlockLink_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 owned
by 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();
}
}
#endif
return 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 block
before and the block after, then it's pxNextFreeBlock pointer will have
already been set, and should not be set here as that would make it point
to 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 immediately
before 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 longer
allocated. */
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博客