FreeRTOS从入门到精通 第十九章(内存管理)
参考教程:【正点原子】手把手教你学FreeRTOS实时系统_哔哩哔哩_bilibili
一、FreeRTOS内存管理介绍
1、创建对象的方法
(1)动态方法创建:自动地从FreeRTOS管理的内存堆(其实就是全局中的一个超大数组,这个数组由FreeRTOS管理)中申请创建对象所需的内存,并且在对象删除后,可将这块内存释放回FreeRTOS管理的内存堆。
(2)需用户提供各种内存空间,并且使用静态方式占用的内存空间一般固定下来了,即使任务、队列等被删除后,这些被占用的内存空间一般没有其它用途。
2、标准C库的动态内存管理方法的缺点
(1)占用大量的代码空间,不适合用在资源紧缺的嵌入式系统中。
(2)没有线程安全的相关机制。
(3)运行有不确定性,每次调用这些函数时花费的时间可能都不相同。
(4)有内存碎片化的现象。
3、FreeRTOS内存管理算法
(1)FreeRTOS提供了5种动态内存管理算法,分别为:heap_1、heap_2、heap_3、heap_4、heap_5。
算法 | 优点 | 缺点 |
heap_1 | 内存分配简单,运行时间确定 | 只允许申请内存,不允许释放内存 |
heap_2 | 允许申请和释放内存 | 不能合并相邻的空闲内存块会产生碎片、时间不定 |
heap_3 | 直接调用C库函数malloc()和 free() ,简单 | 速度慢、时间不定 |
heap_4 | 相邻空闲内存可合并,减少内存碎片的产生 | 时间不定 |
heap_5 | 相当于能够管理多个非连续内存区域的 heap_4 | 时间不定 |
(2)heap_1内存管理算法:
①heap_1只实现了pvPortMalloc,没有实现vPortFree,也就是说,它只能申请内存,无法释放内存。如果工程中创建好的任务、队列、信号量等都不需要被删除,那么可以使用heap_1内存管理算法。
②heap_1的实现最为简单,管理的内存堆是一个数组,在申请内存的时候, heap_1内存管理算法只是简单地从数组中分出合适大小的内存,内存堆数组的定义如下所示:
/* 定义一个大数组作为 FreeRTOS 管理的内存堆 */
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
③heap_1内存管理算法的分配过程如下图所示:
(3)heap_2内存管理算法:
①相比于heap_1内存管理算法,heap_2内存管理算法使用最适应算法,并且支持释放内存。
②heap_2内存管理算法并不能将相邻的空闲内存块合并成一个大的空闲内存块,因此 heap_2内存管理算法不可避免地会产生内存碎片(内存碎片是由于多次申请和释放内存,但释放的内存无法与相邻的空闲内存合并而产生的)。
③最适应算法举例:
假设heap有3块空闲内存(按内存块大小由小到大排序)——5字节、25字节、50字节,现在新创建一个任务需要申请20字节的内存
[1] 第一步:找出最小的、能满足pvPortMalloc的内存——25字节
[2] 第二步:把它划分为20字节、5字节,返回这20字节的地址,剩下的5字节仍然是空闲状态,留给后续的pvPortMalloc使用
④适用场景:频繁的创建和删除任务,且所创建的任务堆栈都相同,这类场景下Heap_2没有碎片化的问题。
⑤heap_2内存管理算法的分配过程如下图所示:
(4)heap_4内存管理算法:
①heap_4内存管理算法使用了首次适应算法,也支持内存的申请与释放,并且能够将空闲且相邻的内存进行合并,从而减少内存碎片的现象。
②首次适应算法举例:
假设heap有3块空闲内存(按内存块地址由低到高排序):5字节、50字节、25字节,现在新创建一个任务需要申请20字节的内存
[1] 第一步:找出第一个能满足pvPortMalloc的内存——50字节
[2] 第二步:把它划分为20字节、30字节;返回这20字节的地址,剩下30字节仍然是空闲状态,留给后续的pvPortMalloc使用
③heap_4内存管理算法会把相邻的空闲内存合并为一个更大的空闲内存,这有助于减少内存的碎片问题。
④heap_4内存管理算法适用于频繁地分配、释放不同大小内存的场景。
⑤heap_4内存管理算法的分配过程如下图所示:
(5)heap_5内存管理算法:
①heap_5内存管理算法是在heap_4内存管理算法的基础上实现的,但是heap_5内存管理算法在heap_4内存管理算法的基础上实现了管理多个非连续内存区域的能力 。
②heap_5内存管理算法默认并没有定义内存堆,需要用户手动指定内存区域的信息,对其进行初始化。
③指定一块内存需使用如下结构体:
typedef struct HeapRegion
{
uint8_t * pucStartAddress; /* 内存区域的起始地址 */
size_t xSizeInBytes; /* 内存区域的大小,单位:字节 */
} HeapRegion_t;
④指定多块且不连续的内存举例:
const HeapRegion_t xHeapRegions[] =
{
{ (uint8_t *)0x80000000, 0x10000 }, /* 内存区域 1 */
{ (uint8_t *)0x90000000, 0xA0000 }, /* 内存区域 2 */
{ NULL, 0 } /* 数组终止标志 */
};
vPortDefineHeapRegions(xHeapRegions);
⑤heap_5内存管理算法适用于那些内存的地址并不连续的场景。
二、FreeRTOS内存管理相关API函数介绍
1、常用的FreeRTOS内存管理相关API函数
(1)pvPortMalloc函数:
①pvPortMalloc函数用于申请内存(建议与释放内存一一对应)。
②函数定义:
void * pvPortMalloc
(
size_t xWantedSize //申请的内存大小,以字节为单位
);
//返回一个指针指向分配的内存块,如果申请内存失败,则返回NULL
(2)vPortFree函数:
①vPortFree函数用于释放内存(建议与申请内存一一对应)。
②函数定义:
void vPortFree
(
void * pv //指向一个要释放的内存块的指针
);
(3)xPortGetFreeHeapSize函数:
①xPortGetFreeHeapSize函数用于获取当前空闲内存的大小。
②函数定义:
size_t xPortGetFreeHeapSize
(
void
);
//返回当前剩余的空闲内存大小
2、heap_4内存管理算法源码剖析
(1)内存块结构体源码:
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK * pxNextFreeBlock; //指向下一空闲内存块的指针
size_t xBlockSize; //内存块的大小(前31位表示大小,第32位置1表示块被使用)
} BlockLink_t;
(2)prvHeapInit函数:
①作用:初始化内存堆。
②源码剖析:
static void prvHeapInit( void )
{
BlockLink_t * pxFirstFreeBlock;
portPOINTER_SIZE_TYPE uxStartAddress, uxEndAddress;
size_t xTotalHeapSize = configTOTAL_HEAP_SIZE; //获取内存堆的总大小
uxStartAddress = ( portPOINTER_SIZE_TYPE ) ucHeap; //获取FreeRTOS管理的内存堆的首地址
//8字节对齐操作,如果内存堆的首地址不能被8整除,说明首地址需要做一些偏移,舍弃一点点内存
if( ( uxStartAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
{
//首地址偏移,对齐8字节
uxStartAddress += ( portBYTE_ALIGNMENT - 1 );
uxStartAddress&=~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK);
//FreeRTOS管理的内存总大小修改
xTotalHeapSize -= (size_t) ( uxStartAddress - ( portPOINTER_SIZE_TYPE ) ucHeap );
}
#if ( configENABLE_HEAP_PROTECTOR == 1 )
{
vApplicationGetRandomHeapCanary( &( xHeapCanary ) );
}
#endif
//创建起始内存块,它的下一空闲内存块地址为FreeRTOS管理的内存堆首地址
xStart.pxNextFreeBlock = ( void * ) heapPROTECT_BLOCK_POINTER( uxStartAddress );
xStart.xBlockSize = ( size_t ) 0;
//获取FreeRTOS管理的内存堆的末尾地址(需考虑字节对齐损失的内存及末尾内存块的占用空间)
uxEndAddress = uxStartAddress + ( portPOINTER_SIZE_TYPE ) xTotalHeapSize;
uxEndAddress -= ( portPOINTER_SIZE_TYPE ) xHeapStructSize;
uxEndAddress &= ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK );
pxEnd = ( BlockLink_t * ) uxEndAddress; //FreeRTOS管理的内存堆的末尾地址
pxEnd->xBlockSize = 0; //末尾内存块的大小为0
pxEnd->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( NULL ); //末尾内存块无下一空闲块
//创建FreeRTOS管理的内存堆的第一个内存块(不是起始内存块),它的内存块大小几乎占满整个内存堆,具体见示意图
pxFirstFreeBlock = ( BlockLink_t * ) uxStartAddress;
pxFirstFreeBlock->xBlockSize = ( size_t ) ( uxEndAddress - ( portPOINTER_SIZE_TYPE ) pxFirstFreeBlock );
pxFirstFreeBlock->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( pxEnd );
//初始化历史剩余最小的空闲空间(当前内存堆还未被使用,故为最大值)
xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
//初始化当前内存堆的空闲空间(当前内存堆还未被使用,故为最大值)
xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
}
③初始化后的内存堆,其结构如下图所示:
(3)prvInsertBlockIntoFreeList函数:
①作用:将空闲内存块插入空闲列表中(顺序由低地址到高地址),并将相邻且空闲的内存块合并。
②源码剖析:
static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert )
{
BlockLink_t * pxIterator;
uint8_t * puc;
//从起始内存块开始遍历,找到空闲内存块的插入位置(列表中内存块按地址升序进行插入排序)
for( pxIterator = &xStart;
heapPROTECT_BLOCK_POINTER(pxIterator->pxNextFreeBlock)<pxBlockToInsert; pxIterator = heapPROTECT_BLOCK_POINTER(pxIterator->pxNextFreeBlock));
if( pxIterator != &xStart )
{
heapVALIDATE_BLOCK_POINTER( pxIterator );
}
//获取空闲内存块插入位置的上一内存块地址
puc = ( uint8_t * ) pxIterator;
//通过内存块首地址和大小判断要插入的内存块和上一内存块是不是相邻的空闲内存块,是则合并之
if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
{
pxIterator->xBlockSize += pxBlockToInsert->xBlockSize; //两个内存块大小相加
pxBlockToInsert = pxIterator; //要插入内存块的首地址更改为上一内存块的首地址
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//获取插入空闲内存块的首地址
puc = ( uint8_t * ) pxBlockToInsert;
//通过内存块首地址和大小判断要插入的内存块和下一内存块是不是相邻的空闲内存块,是则合并之
if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) heapPROTECT_BLOCK_POINTER( pxIterator->pxNextFreeBlock ) )
{
if( heapPROTECT_BLOCK_POINTER( pxIterator->pxNextFreeBlock ) != pxEnd ) //判断下一内存块是否不是末尾内存块
{
pxBlockToInsert->xBlockSize += heapPROTECT_BLOCK_POINTER( pxIterator->pxNextFreeBlock )->xBlockSize; //两个内存块大小相加
pxBlockToInsert->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( pxIterator->pxNextFreeBlock )->pxNextFreeBlock; //待插入内存块的下一空闲内存块首地址更改为原先的下下个空闲内存块首地址
}
else
{
pxBlockToInsert->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( pxEnd ); //待插入的下一内存块的首地址为末尾内存块首地址
}
}
else
{
//按照一般链表一样做插入操作即可
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
}
if( pxIterator != pxBlockToInsert ) //判断待插入内存块和上一内存块是否没有合并
{
//按照一般链表一样做插入操作即可
pxIterator->pxNextFreeBlock =heapPROTECT_BLOCK_POINTER(pxBlockToInsert); }
else
{
mtCOVERAGE_TEST_MARKER();
}
}
③当空闲列表插入了新的空闲内存块后,其结构如下图所示:
(4)pvPortMalloc函数:
①作用:在FreeRTOS管理的内存堆中申请内存。
②源码剖析:
void * pvPortMalloc( size_t xWantedSize )
{
BlockLink_t * pxBlock;
BlockLink_t * pxPreviousBlock;
BlockLink_t * pxNewBlockLink;
void * pvReturn = NULL;
size_t xAdditionalRequiredSize;
if( xWantedSize > 0 )
{
//增加申请的需求,保证内存块的结构体所占空间也被考虑
if( heapADD_WILL_OVERFLOW( xWantedSize, xHeapStructSize ) == 0 )
{
xWantedSize += xHeapStructSize;
//判断是否需要8字节对齐,如需要则进行之
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
{
xAdditionalRequiredSize = portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK );
if( heapADD_WILL_OVERFLOW( xWantedSize, xAdditionalRequiredSize ) == 0 )
{
xWantedSize += xAdditionalRequiredSize;
}
else
{
xWantedSize = 0;
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
xWantedSize = 0;
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
vTaskSuspendAll(); //挂起任务调度器
{
if( pxEnd == NULL ) //判断内存堆是否已初始化,没有则初始化之
prvHeapInit();
else
mtCOVERAGE_TEST_MARKER();
if( heapBLOCK_SIZE_IS_VALID( xWantedSize ) != 0 )
{
if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) ) //判断申请的内存是否合法以及是否没有超限
{
//从低地址开始找,找到一个满足此次申请的内存块
pxPreviousBlock = &xStart;
pxBlock = heapPROTECT_BLOCK_POINTER(xStart.pxNextFreeBlock);
heapVALIDATE_BLOCK_POINTER( pxBlock );
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != heapPROTECT_BLOCK_POINTER( NULL ) ) )
{
pxPreviousBlock = pxBlock;
pxBlock = heapPROTECT_BLOCK_POINTER( pxBlock->pxNextFreeBlock );
heapVALIDATE_BLOCK_POINTER( pxBlock );
}
//如果未遍历到末尾内存块,说明找到合适的内存块满足此次申请
if( pxBlock != pxEnd )
{
//将遍历到的合适内存块首地址作为返回值
pvReturn = ( void * ) ( ( ( uint8_t * ) heapPROTECT_BLOCK_POINTER( pxPreviousBlock->pxNextFreeBlock ) ) + xHeapStructSize );
heapVALIDATE_BLOCK_POINTER( pvReturn );
//该内存块需要从空闲列表中移除,移除方式和一般链表差不多
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
configASSERT( heapSUBTRACT_WILL_UNDERFLOW( pxBlock->xBlockSize, xWantedSize ) == 0 );
//如果这个内存块比申请需求的内存大,可将此块分割成两个内存块
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
{
//获取新内存块的首地址
pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );
//计算分割出来的两个内存块的大小
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
//将分割出来且没有使用的内存块插入空闲列表中
pxNewBlockLink->pxNextFreeBlock = pxPreviousBlock->pxNextFreeBlock;
pxPreviousBlock->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( pxNewBlockLink );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//更新当前剩余的空闲空间
xFreeBytesRemaining -= pxBlock->xBlockSize;
//更新历史最小剩余空间大小
if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
{
xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
heapALLOCATE_BLOCK( pxBlock ); //标记内存块已被使用
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 )
vApplicationMallocFailedHook();
else
mtCOVERAGE_TEST_MARKER();
}
#endif /* if ( configUSE_MALLOC_FAILED_HOOK == 1 ) */
configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
return pvReturn;
}
(5)vPortFree函数:
①作用:释放在FreeRTOS管理的内存堆中申请的内存。
②源码剖析:
void vPortFree( void * pv )
{
uint8_t * puc = ( uint8_t * ) pv;
BlockLink_t * pxLink;
if( pv != NULL ) //判断要释放的内存块首地址是否有效
{
//减去内存块结构体大小,获取“真正的内存块地址”,也即内存块结构体首地址
puc -= xHeapStructSize;
pxLink = ( void * ) puc;
heapVALIDATE_BLOCK_POINTER( pxLink );
configASSERT( heapBLOCK_IS_ALLOCATED( pxLink ) != 0 );
configASSERT( pxLink->pxNextFreeBlock == NULL );
if(heapBLOCK_IS_ALLOCATED(pxLink) != 0) //判断要释放的内存块是否被使用
{
if( pxLink->pxNextFreeBlock == NULL ) //判断要释放的内存块是否已不在空闲列表
{
heapFREE_BLOCK( pxLink );
#if ( configHEAP_CLEAR_MEMORY_ON_FREE == 1 )
{
if( heapSUBTRACT_WILL_UNDERFLOW( pxLink->xBlockSize, xHeapStructSize ) == 0 )
{
( void ) memset( puc + xHeapStructSize, 0, pxLink->xBlockSize - xHeapStructSize );
}
}
#endif
vTaskSuspendAll(); //挂起任务调度器
{
xFreeBytesRemaining+=pxLink->xBlockSize;//更改当前剩余内存大小
traceFREE( pv, pxLink->xBlockSize );
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); //将内存块插入空闲列表
xNumberOfSuccessfulFrees++; //成功释放内存块的次数自增
}
( void ) xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}