FreeRTOS系列第8篇---FreeRTOS内存管理
该博客中贴了代码。
内存管理对应用程序和操作系统来说都非常重要。现在很多的程序漏洞和运行崩溃都和内存分配使用错误有关。FreeRTOS操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的。
每当创建任务、队列、互斥量、软件定时器、信号量或事件组时,RTOS内核会为它们分配RAM。标准函数库中的malloc()和free()函数能够用于完成这个任务,但是:
- 在嵌入式系统中,它们并不总是可以使用的;
- 它们会占用更多宝贵的代码空间;
- 它们没有线程保护;
- 它们不具有确定性(每次调用执行的时间可能会不同);
?为什么有这些问题?
FreeRTOS内核规定的几个内存管理函数原型为:
- void *pvPortMalloc( size_t xSize ) :内存申请函数
- void vPortFree( void *pv ) :内存释放函数
- void vPortInitialiseBlocks( void ) :初始化内存堆函数
- size_t xPortGetFreeHeapSize( void ) :获取当前未分配的内存堆大小
- size_t xPortGetMinimumEverFreeHeapSize( void ):获取未分配的内存堆历史最小值
因此,提供一个替代的内存分配方案通常是必要的。 嵌入式/实时系统具有千差万别的RAM和时间要求,因此一个RAM内存分配算法可能仅属于一个应用的子集。
FreeRTOS提供的内存分配方案分别位于不同的源文件(heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c)之中。
一、 FreeRTOS 内存 方案
1.1 heap_1.c
- 用于从不会删除任务、队列、信号量、互斥量等的应用程序(实际上大多数使用FreeRTOS的应用程序都符合这个条件)
- 执行时间是确定的并且不会产生内存碎片
- 实现和分配过程非常简单,需要的内存是从一个静态数组中分配的,意味着这种内存分配通常只是适用于那些不进行动态内存分配的应用。
一旦分配内存之后,它甚至不允许释放分配的内存。大多数深度嵌入式(deeplyembedded)应用只是在系统启动时创建所有任务、队列、信号量等,并且直到程序结束都会一直使用它们,永远不需要删除。
API函数xPortGetFreeHeapSize()返回未分配的堆栈空间总大小,可以通过这个函数返回值对configTOTAL_HEAP_SIZE进行合理的设置。
static size_t xNextFreeByte = ( size_t ) 0;//记录已经非陪的内存大小,用来定位下一空闲的内存堆位置
static uint8_t *pucAlignedHeap = NULL;//堆对齐,这样访问速度更快
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn = NULL;
static uint8_t *pucAlignedHeap = NULL;
/* 确保申请的字节数,宏portBYTE_ALIGNMENT 是对齐字节数,默认为8 */
#if( portBYTE_ALIGNMENT != 1 )
{
//进行位与运算,来判断是否为8字节对齐,等于0就说明时对齐的
//portBYTE_ALIGNMENT_MASK 为0x0007
if( xWantedSize & portBYTE_ALIGNMENT_MASK )
{
//将字节进行对齐
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
}
#endif
// 挂起任务调度器,因为申请内存过程中需要保护,不能被打断
vTaskSuspendAll();
{
if( pucAlignedHeap == NULL )
{
/* 第一次使用,确保内存堆起始位置正确对齐 */
//内存堆ucheap:由编译器分配,不一定为8字节对齐的。
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
}
/* 边界检查,检查可用内存是否够分配,变量xNextFreeByte是局部静态变量,初始值为0 */
if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) )
{
/* 返回申请的内存起始地址并更新索引 */
pvReturn = pucAlignedHeap + xNextFreeByte;
//内存申请完成后,更新变量xNextFreeByte
xNextFreeByte += xWantedSize;
}
}
( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
// 成功返回内存首地址,失败返回NULL
return pvReturn;
}
1.2 heap_2.c
heap_2.c适用于需要动态创建任务的大多数小型实时系统(smallreal time)。
和方案1不同,这个方案使用一个最佳匹配算法,它允许释放之前分配的内存块,但是它不会把相邻的空闲块合成一个更大的块(换句话说,这会造成内存碎片)。
有效的堆栈空间大小由位于FreeRTOSConfig.h文件中的configTOTAL_HEAP_SIZE宏来定义。
为了实现内存释放,heap_2引入内存块的概念,每分出去一段内存就是一个内存块,剩下的空闲内存也是一个内存块。
//使用一个链表结构来跟踪记录空闲内存块,将空闲块组成一个链表。8个字节
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock; /*指向列表中下一个空闲块*/
size_t xBlockSize; /*当前空闲块的大小,包括链表结构大小*/
} BlockLink_t;
为了方便管理,可用的内存块会被组织在一个链表里,局部静态变量xStart, xEnd用来记录这个链表的头尾
static BlockLink_t xStart, xEnd;
每个内存块前面都有BlockLink_t类型变量来描述此内存块。如下图:
- 不能用在分配和释放随机字节堆栈空间的应用程序
如果一个应用程序动态的创建和删除任务,并且分配给任务的堆栈空间总是同样大小,那么大多数情况下heap_2.c是可以使用的。但是,如果分配给任务的堆栈不总是相等,那么释放的有效内存可能碎片化,形成很多小的内存块。最后可能剩余很多个很小的空闲块。(如下图)最后会因为没有足够大的连续堆栈空间而造成内存分配失败。在这种情况下,heap_4.c是一个很好的选择。 - 应用程序直接调用pvPortMalloc() 和 vPortFree()函数,而不仅是通过FreeRTOS API间接调用。
pvPortMalloc()申请内存
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
static BaseType_t xHeapHasBeenInitialised = pdFALSE;
void *pvReturn = NULL;
/* 挂起调度器 */
vTaskSuspendAll();
{
/* 如果是第一次调用内存分配函数,这里先初始化内存堆,如图2-2所示 */
if( xHeapHasBeenInitialised == pdFALSE )
{
prvHeapInit();//完成内存堆的对齐,xstart和xEnd的初始化
xHeapHasBeenInitialised = pdTRUE;
}
/* 调整要分配的内存值,需要增加上链表结构体空间,heapSTRUCT_SIZE表示经过对齐扩展后的结构体大小 */
//即实际申请的内存需要加上BlockLink_t的大小
if( xWantedSize > 0 )
{
xWantedSize += heapSTRUCT_SIZE;
/* 调整实际分配的内存大小,向上扩大到对齐字节数的整数倍 */
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
}
if( ( xWantedSize > 0 ) && ( xWantedSize < configADJUSTED_HEAP_SIZE ) )
{
/* 空闲内存块是按照块的大小排序的,从链表头xStart开始,小的在前大的在后,以链表尾xEnd结束 */
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
/* 搜索最合适的空闲块 */
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}
/* 如果搜索到链表尾xEnd,说明没有找到合适的空闲内存块,否则进行下一步处理 */
if( pxBlock != &xEnd )
{
/* 返回内存空间,注意是跳过了结构体BlockLink_t空间. */
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );
/* 这个块就要返回给用户,因此它必须从空闲块中去除. */
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;
}
}
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{ /* 如果内存分配失败,调用钩子函数 */
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
释放内存:vPortFree()
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;
if( pv != NULL )
{
//puc:要释放内存的首地址,是pvReturn所指向的地址
/* 根据传入的参数找到链表结构 */
puc -= heapSTRUCT_SIZE;
/* 预防某些编译器警告 */
pxLink = ( void * ) puc;
vTaskSuspendAll();
{
/* 将这个块添加到空闲块列表 */
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
/* 更新未分配的内存堆大小 */
xFreeBytesRemaining += pxLink->xBlockSize;
traceFREE( pv, pxLink->xBlockSize );
}
( void ) xTaskResumeAll();
}
}
1.3 heap_3.c
heap_3.c简单的包装了标准库中的malloc()和free()函数,包装后的malloc()和free()函数具备线程保护。
因此,内存堆需要通过编译器或者启动文件设置堆空间。
功能:
- 第一种和第二种内存管理策略都是通过定义一个大数组作为内存堆,数组的大小由宏configTOTAL_HEAP_SIZE指定。需要编译器设置一个堆栈空间,一般在启动代码中设置,并且编译器库提供malloc()和free()函数。
- 不具有确定性
- 可能明显的增大RTOS内核的代码大小
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn;
vTaskSuspendAll();
{
pvReturn = malloc( xWantedSize );
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
1.4 heap_4.c
heap_4.c还特别适用于移植层代码,可以直接使用pvPortMalloc()和 vPortFree()函数来分配和释放内存.
这个方案使用一个最佳匹配算法,但不像方案2那样。它会将相邻的空闲内存块合并成一个更大的块(包含一个合并算法)。
与heap_2不同:
- 链表尾保存在内存堆空间的最后位置,pxEnd,第二种是xEnd指向链表尾
- 第四种小地址在前,大地址在后(为了适应合并算法),而第二种是小内存在前。
API函数xPortGetFreeHeapSize()返回剩下的未分配堆栈空间的大小(可用于优化设置configTOTAL_HEAP_SIZE宏的值),但是不能提供未分配内存的碎片细节信息。
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;
vTaskSuspendAll();
{
/* 如果是第一次调用内存分配函数,则初始化内存堆 */
if( pxEnd == NULL )
{
prvHeapInit();
}
/* 申请的内存大小合法性检查:是否过大.结构体BlockLink_t中有一个成员xBlockSize表示块的大小,这个成员的最高位被用来标识这个块是否空闲.因此要申请的块大小,不能使用这个位.*/
if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
{
/* 计算实际要分配的内存大小,包含链接结构体BlockLink_t在内,并且要向上字节对齐 */
if( xWantedSize > 0 )
{
xWantedSize += xHeapStructSize;
/* 对齐操作,向上扩大到对齐字节数的整数倍 */
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
}
}
if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
{
/* 从链表xStart开始查找,从空闲块链表(按照空闲块地址顺序排列)中找出一个足够大的空闲块 */
pxPreviousBlock = &xStart;//上一个块是xStart
pxBlock = xStart.pxNextFreeBlock;//将要判断的块
//遍历可用空间,直到内存大小满足想要申请的。
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock;// pxPreviousBlock的下一个内存块就是找到的可用内存块
pxBlock = pxBlock->pxNextFreeBlock;
}
/* 如果最后到达结束标识,则说明没有合适的内存块,否则,进行内存分配操作*/
if( pxBlock != pxEnd )
{
/* 返回分配的内存指针,要跳过内存开始处的BlockLink_t结构体 */
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );
/* 将已经分配出去的内存块从空闲块链表中删除 */
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
/* 如果剩下的内存足够大,则组成一个新的空闲块 */
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;
/* 将剩余的空闲块插入到空闲块列表中,按照空闲块的地址大小顺序,地址小的在前,地址大的在后 */
prvInsertBlockIntoFreeList( pxNewBlockLink );
}
/* 计算未分配的内存堆空间,注意这里并不能包含内存碎片信息 */
xFreeBytesRemaining -= pxBlock->xBlockSize;
/* 保存未分配内存堆空间历史最小值 */
if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
{
xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
}
/* 将已经分配的内存块标识为"已分配" */
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();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
return pvReturn;
}
1.5 heap_5.c
Heap_5通过调用vPortDefineHeapRegions()函数实现初始化,它允许程序设置多个非连续内存堆,如片内RAM和片外RAM。使用heap_5创建任何对象前,要先执行vPortDefineHeapRegions()函数。
创建RTOS对象(任务、队列、信号量等等)会隐含的调用pvPortMalloc().