15. FreeRTOS的内存管理
一、FreeRTOS的内存管理
内存管理是指软件运行时对内存资源的分配和使用的一种计数,其最主要的目的就是为了能够高效且快速地分配,并且在释放的时候释放不再使用的内存空间。
在使用 FreeRTOS 创建任务、队列、信号量等对象的时候,FreeRTOS 一般都提供了两种方法,一种是动态地申请创建对象时所需要的内存,这种方法也叫 动态方法;一种是由用户自定义对象,在编译器编译程序的时候,会为已经在程序中定义好的对象分配一定的内存空间,这种方法也叫 静态方法。
- 动态方法创建:自动地从 FreeRTOS 管理的内存堆中申请创建对象所需的内存,并且在对象删除后,可将这块内存释放回 FreeRTOS 管理的内存堆。
- 静态方法创建:需用户提供各种内存空间,并且使用静态方式占用的内存空间一般固定下来了,即使任务、队列等被删除后,这些被占用的内存空间一般没有其他用途。
静态方法 创建任务、队列、信号量等对象的 API 函数一般是以“Static”结尾的,例如静态创建任务的 API 函数 xTaskCreateStatic()
。使用静态方式创建各种对象时,需要用户提供各种内存空间,例如任务的栈空间、任务控制块所用内存空间等等,并且使用静态方式占用的内存空间一般固定下来了,即使任务、队列等被删除后,这些被占用的内存空间也没有其他用途。
在使用 动态方式 管理内存的时候,FreeRTOS 就能够在创建任务、队列、信号量等对象的时候,自动地从 FreeRTOS 管理的内存堆中申请所创建对象所需的内存,在对象被删除后,又可以将这块内存释放会 FreeRTOS 管理的内存堆,这样看来,动态方式管理内存相比与静态方式,显得灵活许多。
除了 FreeRTOS 提供的动态内存管理方法,标准的 C 库也提供了函数 malloc()
和函数 free()
来实现动态地申请和释放内存,但是一般不建议在嵌入式系统中使用函数 malloc()
和函数 free()
(特别是在使用了实时操作系统的嵌入式开发中)。这是因为函数 malloc()
和函数 free()
在申请和释放内存的时候,会导致产生大量的内存碎片,最后甚至可能出现明明还有大量的空闲内存,但是却无法再申请内存的情况,这是因为大量地内存碎片占据了空闲内存,这些内存碎片数量多、单个占用内存小、地址空间不连续,这将是的无法再分配较大块的内存。
为此,FreeRTOS 提供了动态方式管理内存的方法。不同的嵌入式系统对于动态内存管理的需求不同,因此 FreeRTOS 提供了多种内存管理算法选项,并将其作为 FreeRTOS 移植层的一部分,这样一来,FreeRTOS 的使用者就能够根据自己实际的需求选的何时的动态内存管理算法,并将其移植到系统中。
如果要使用动态内存管理,因此需要配置 FreeRTOSConfig.h 文件,具体的配置如下所示:
#define configSUPPORT_DYNAMIC_ALLOCATION 1 // 1: 支持动态申请内存, 默认: 1
#define configTOTAL_HEAP_SIZE ((size_t)(10 * 1024)) // FreeRTOS 堆中可用的 RAM 总量, 单位: Byte, 无默认需定义
二、FreeRTOS内存管理算法
FreeRTOS 一共提供了 5 种动态内存管理算法,这 5 种动态内存管理算法本别对应了 5 个 C 源文件,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c,它们的各自的特点如下所示:
2.1、heap_1内存管理算法
heap_1 内存管理算法是 5 种内存管理算法中最实现简单的内存管理算法,只实现了 pvPortMalloc()
,没有实现vPortFree()
,也就是说,它只能申请内存,无法释放内存。如果创建好的任务、队列、信号量等都不需要被删除,那么可以使用heap_1 内存管理算法。
heap_1 的实现最为简单,管理的内存堆是一个数组,在申请内存的时候, heap_1 内存管理算法只是简单地从数组中分出合适大小的内存,内存堆数组的定义如下所示:
#if ( configAPPLICATION_ALLOCATED_HEAP == 1 ) // 此宏用于定义 FreeRTOS 内存堆的定义方式
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; // 用户自定义一个大数组作为 FreeRTOS 管理的内存堆
#else
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; // 定义一个大数组作为 FreeRTOS 管理的内存堆
#endif
从上面的代码中可以看出,heap_1 内存管理算法管理的内存堆实际上是一个大小为 configTOTAL_HEAP_SIZE
字节的数组,宏 configTOTAL_HEAP_SIZE
可以在 FreeRTOSConfig.h 文件中进行配置。宏 configAPPLICATION_ALLCOATED_HEAP
允许用户将内存堆定义在指定的地址中,常用于将内存堆定义在外扩的 RAM 中。
用户可以通过函数 xPortGetFreeHeapSize()
获取内存堆剩余空间的大小,根据系统运行时内存堆的剩余空间大小,对 configTOTAL_HEAP_SIZE
配置项进行优化配置。
heap_1.c 文件中用于申请内存的函数为函数 pvPortMalloc()
,此函数的定义如下所示:
void * pvPortMalloc( size_t xWantedSize )
{
void * pvReturn = NULL;
static uint8_t * pucAlignedHeap = NULL;
// 确保申请的内存大小按照 portBYTE_ALIGNMENT 字节对齐,如果审定的内存大小没有按照 portBYTE_ALIGNMENT 字节对齐,
// 则会加大申请的内存大小,是指按 portBYTE_ALIGNMENT 字节对齐
#if ( portBYTE_ALIGNMENT != 1 )
{
if( xWantedSize & portBYTE_ALIGNMENT_MASK )
{
/* Byte alignment required. Check for overflow. */
if( ( xWantedSize + ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ) ) > xWantedSize )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
else
{
xWantedSize = 0;
}
}
}
#endif /* if ( portBYTE_ALIGNMENT != 1 ) */
vTaskSuspendAll(); // 挂起任务调度器
{
if( pucAlignedHeap == NULL )
{
// 确保内存堆的起始地址按照 portBYTE_ALIGNMENT 字节对齐
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) & ucHeap[ portBYTE_ALIGNMENT - 1 ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
}
// 申请的内存大小需大于 0,检查内存堆中是否有足够的空间
if( ( xWantedSize > 0 ) &&
( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) )
{
// 计算申请到内存的起始地址,内存堆的对齐地址+内存堆已分配的大小
pvReturn = pucAlignedHeap + xNextFreeByte;
xNextFreeByte += xWantedSize; // 更新内存堆已分配的大小
}
traceMALLOC( pvReturn, xWantedSize ); // 用于调式
}
( void ) xTaskResumeAll();
#if ( configUSE_MALLOC_FAILED_HOOK == 1 ) // 此宏用于开启动态内存申请失败钩子函数
{
if( pvReturn == NULL ) // 动态内存申请失败
{
vApplicationMallocFailedHook(); // 调用动态内存申请失败钩子函数
}
}
#endif
return pvReturn; // 返回申请到内存的首地址
}
从上面的代码中可以看出,heap_1 内存管理算法的申请内存函数的实现非常简单,就是从内存堆的低地址开始往高地址分配内存,内存堆的结构示意图如下所示:
从上图可以看出,heap_1 内存管理算法管理下的内存堆利用率是非常高的,除了内存堆起始地址的位置可能会因地址对齐产生一小块无用内存外,内存堆中其余的内存空间都可以用来分配,并且也不会产生内存碎片。
heap_1.c 文件中用于释放内存的函数为函数 vPortFree()
,此函数的定义如下所示:
void vPortFree( void * pv )
{
( void ) pv; // 没有实现释放内存的功能
configASSERT( pv == NULL );
}
从上面的代码中可以看出,heap_1 内存管理算法的内存释放函数并没有实现,因此使用heap_1 内存管理算法申请的内存,是无法释放的。
heap_1内存管理算法,只能申请无法释放。
2.2、heap_2内存管理算法
相比于 heap_1 内存管理算法, heap_2 内存管理算法使用 最适应算法,并且支持释放内存。heap_2 内存管理算法并不能将相邻的空闲内存块合并成一个大的空闲内存块,因此 heap_2 内存管理算法不可避免地会产生内存碎片。heap_2 内存管理算法使用于频繁的创建和删除任务,且所创建的任务堆栈都相同的情景。
内存碎片是由于多次申请和释放内存,但释放的内存无法与相邻的空闲内存合并而产生的,具体的产生过程,如下图所示:
如上图所示,当内存堆被多次申请和释放后,由于相邻的小空闲内存无法合并成一个大的空闲内存,从而导致即使内存堆中有足够多的空闲内存,也无法再分配出一块大内存。
最适应算法 是指,假设 heap 有 3 块空闲内存(按内存块大小由小到大排序):5 字节、25 字节、50 字节。现在需要新创建一个任务需要申请 20 字节的内存。
- 找出最小的、能满足
pvPortMalloc()
的内存:25字节。 - 把它划分为 20 字节、5 字节,返回这 20 字节的地址,剩下的 5 字节仍然是空闲状态,留给后续的
pvPortMalloc()
使用。
heap_2 内存管理算法的内存堆与 heap_1 内存管理算法的内存堆一样,都是一个数组, 定义如下所示:
#if ( configAPPLICATION_ALLOCATED_HEAP == 1 ) // 此宏用于定义 FreeRTOS 内存堆的定义方式
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; // 用户自定义一个大数组作为 FreeRTOS 管理的内存堆
#else
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; // 定义一个大数组作为 FreeRTOS 管理的内存堆
#endif
从上面的代码中可以看出,heap_2 内存管理算法中定义的内存堆与 heap_1 内存管理算法一样,可以在 FreeRTOSConfig.h 文件中配置 configTOTAL_HEAP_SIZE
配置项,以配置内存堆的字节大小,同样地,也可以用过 configAPPLICATION_ALLOCATED_HEAP
配置项将内存堆定义在指定的内存地址中。
用户可以通过函数 xPortGetFreeHeapSize()
获取内存堆中未分配的内存总量,并根据系统运行时内存堆中剩余内存的大小,针对性地对 configTOTAL_HEAP_SIZE
配置项进行优化配置。
为了能够实现内存的释放功能,heap_2 内存管理算法引入了 内存块 的概念。在内存堆中的内存都是以内存块表示的,heap_2 内存管理算法中内存块的定义如下:
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK * pxNextFreeBlock; // 指向下一个内存块
size_t xBlockSize; // 内存块的大小
} BlockLink_t;
从上面的代码中可以看出,每一个内存块都包含了一个用于指向下一个内存块的指针 pxNextFreeBlock,并记录了内存块的大小,内存块的大小就包含了内存块的内存块结构体占用的内存空间和内存块中可使用的内存大小,因此内存块的结构如下图所示:
heap_2 内存管理算法会通过内存块中的 pxNextFreeBlock 指针,将还未分配的内存块链成一个单向链表,这个单向链表就叫做 空闲块链表。空闲块链表 中的内存块是按照内存块的大小 从小到大 排序的,因此空闲块链表中相邻的两个内存块,其在内存中也不一定相邻。为了方便管理这个空闲块链表,在 heap_2.c 文件中定义了两个内存块来作为空闲块链表的链表头和链表尾,这两个内存块的定义如下:
static BlockLink_t xStart, xEnd;
其中,xStart 作为空闲块链表的链表头,xEnd 作为空闲块链表的链表尾,需要注意的是,xStart 和 xEnd 并不是内存堆中的内存块,因此 xStart 和 xEnd 内存块并不包含可分配的内存。
heap_2.c 文件中用于初始化内存堆的函数为函数 prvHeapInit()
,此函数的定义如下所示:
static void prvHeapInit( void ) /* PRIVILEGED_FUNCTION */
{
BlockLink_t * pxFirstFreeBlock;
uint8_t * pucAlignedHeap;
// 确保内存堆的起始地址按照 portBYTE_ALIGNMENT 字节对齐
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) & ucHeap[ portBYTE_ALIGNMENT - 1 ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap; // xStart 内存块的下一个内存块指向内存堆
xStart.xBlockSize = ( size_t ) 0; // xStart 内存块的大小固定为 0
xEnd.xBlockSize = configADJUSTED_HEAP_SIZE; // xEnd 内存块的大小用于指示内存堆的总大小
xEnd.pxNextFreeBlock = NULL; // xEnd 内存块没有下一个内存块
pxFirstFreeBlock = ( BlockLink_t * ) pucAlignedHeap; // 将整个内存堆作为一个内存块
pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE; // 设置内存块的大小
pxFirstFreeBlock->pxNextFreeBlock = &xEnd; // 内存块的下一个内存块指向 xEnd
}
从上面的代码中可以看出,初始化内存堆的时候,同时也初始化了 xStart 和 xEnd,初始化好后的内存堆和 xStart、xEnd 如下图所示:
heap_2 内存管理算法支持释放已经分配的内存,被释放的内存将被作为空闲内存块添加到空闲块链表,这一操作通过宏 prvInsertBlockIntoFreeList()
完成,此宏的定义如下所示:
#define prvInsertBlockIntoFreeList( pxBlockToInsert ) \
{ \
BlockLink_t * pxIterator; \
size_t xBlockSize; \
\
/* 获取待插入空闲内存块的大小 */ \
xBlockSize = pxBlockToInsert->xBlockSize; \
\
/* 从 xStart 开始,遍历整个内存块单向链表,找到第一个内存大小不小于待插入空闲内存块的空闲内存块的上一个空闲内存块 */ \
for( pxIterator = &xStart; pxIterator->pxNextFreeBlock->xBlockSize < xBlockSize; pxIterator = pxIterator->pxNextFreeBlock ) \
{ \
/* 什么都不做,找到内存块该插入的位置 */ \
} \
\
/* 将待插入的内存块,插入链表中的对应位置 */ \
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; \
pxIterator->pxNextFreeBlock = pxBlockToInsert; \
}
从上面的代码中可以看出,将空闲的内存块插入空闲块链表,首先会从头遍历空闲块链表找到第一个内存大小不小于待插入空闲内存块的空闲内存块的上一个空闲内存块,然后将待插入空闲内存块插入到这个空闲内存块的后面,如下图所示:
heap_2.c 文件中用于申请内存的函数为函数 pvPortMalloc()
,此函数的定义如下所示:
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;
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( xHeapHasBeenInitialised == pdFALSE ) // 如果内存堆未初始化,则先初始化内存堆
{
prvHeapInit(); // 初始化内存堆
xHeapHasBeenInitialised = pdTRUE; // 标记内存堆已经初始化
}
if( heapBLOCK_SIZE_IS_VALID( xWantedSize ) != 0 )
{
if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) ) // 所需的内存大小需大于 0,且小于内存堆中可分配内存大小
{
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
// 从头遍历内存块链表,找到第一个内存大小适合的内存块
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}
if( pxBlock != &xEnd ) // 判断是否找到了符合条件的内存块
{
// 将返回值设置为符合添加内存块中可分配内存的起始地址,即内存块的内存地址偏移内存块结构体大小的地址
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );
// 将符合条件的内存块从空闲块链表中移除
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; // 更新内存堆中可分配的内存大小
heapALLOCATE_BLOCK( pxBlock );
pxBlock->pxNextFreeBlock = NULL;
}
}
}
traceMALLOC( pvReturn, xWantedSize ); // 用于调试
}
( void ) xTaskResumeAll(); // 恢复任务调度器
#if ( configUSE_MALLOC_FAILED_HOOK == 1 ) // 此宏用于开启动态内存申请失败钩子函数
{
if( pvReturn == NULL ) // 动态申请内存失败
{
vApplicationMallocFailedHook(); // 调用动态内存申请失败钩子函数
}
}
#endif
return pvReturn; // 返回申请到内存的首地址
}
从上面的代码中可以看出,heap_2 内存管理算法申请内存的过程,大致如下:
- 因为空闲块链表中的空闲内存块是按照内存块的大小从小到大排序的,因此从头开始遍历空闲块链表,找到第一个大小适合的空闲内存块。
- 找到大小适合的空闲内存块后,由于找到的空闲内存块可能比需要申请的内存大,因此需要将整个内存块分为两个小的内存块,其中一个内存块的大小就是需要申请内存的大小,另一个小内存块作为空闲内存块重新插入空闲块链表。
heap_2.c 文件中用于释放内存的函数为函数 vPortFree()
,此函数的定义如下所示:
void vPortFree( void * pv )
{
uint8_t * puc = ( uint8_t * ) pv;
BlockLink_t * pxLink;
if( pv != NULL ) // 被释放的对象需不为空
{
puc -= xHeapStructSize; // 获取内存块的起始地址
pxLink = ( void * ) puc; // 获取内存块
configASSERT( heapBLOCK_IS_ALLOCATED( pxLink ) != 0 );
configASSERT( pxLink->pxNextFreeBlock == NULL );
if( heapBLOCK_IS_ALLOCATED( pxLink ) != 0 )
{
if( pxLink->pxNextFreeBlock == NULL )
{
/* The block is being returned to the heap - it is no longer
* allocated. */
heapFREE_BLOCK( pxLink );
#if ( configHEAP_CLEAR_MEMORY_ON_FREE == 1 )
{
( void ) memset( puc + xHeapStructSize, 0, pxLink->xBlockSize - xHeapStructSize );
}
#endif
vTaskSuspendAll(); // 挂起任务调度器
{
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); // 将被释放的内存块插入空闲块链表
xFreeBytesRemaining += pxLink->xBlockSize; // 更新内存堆中可分配的内存大小
traceFREE( pv, pxLink->xBlockSize ); // 用于调式
}
( void ) xTaskResumeAll(); // 恢复任务调度器
}
}
}
}
从上面的代码中可以看出,heap_2 内存管理算法的释放函数含简单,就是将带释放的内存块插入到空闲块链表中。
2.3、heap_3内存管理算法
heap_3 内存管理算法本质使用的是调用标准 C 库提供的内存管理函数 malloc()
函数和 free()
函数,标准 C 库的内存管理需要链接器设置好一个堆,这个堆将作为内存管理的内存堆使用,在启动文件中可以配置这个堆的大小。
heap_3.c 文件中用于申请内存的函数为函数 pvPortMalloc()
,此函数的定义如下所示:
void * pvPortMalloc( size_t xWantedSize )
{
void * pvReturn;
vTaskSuspendAll(); // 挂起任务调度器
{
pvReturn = malloc( xWantedSize ); // 调用C库函数申请内存
traceMALLOC( pvReturn, xWantedSize ); // 用于调式
}
( void ) xTaskResumeAll(); // 恢复任务调度器
#if ( configUSE_MALLOC_FAILED_HOOK == 1 ) // 此宏用于开启动态内存申请失败钩子函数
{
if( pvReturn == NULL ) // 动态申请内存失败
{
vApplicationMallocFailedHook(); // 调用动态内存申请失败钩子函数
}
}
#endif
return pvReturn;
}
从上面的代码中可以看出,heap_3 内存管理算法实际上是调用了 C 库的内存申请函数 malloc()
申请内存,并且会在申请内存的前后挂起和恢复任务调度器,以确保线程安全。
heap_3.c 文件中用于释放内存的函数为函数 vPortFree()
,此函数的定义如下所示:
void vPortFree( void * pv )
{
if( pv != NULL ) // 被释放的对象需不为空
{
vTaskSuspendAll(); // 挂起任务调度器
{
free( pv ); // 调用C库函数释放内存
traceFREE( pv, 0 ); // 用于调试
}
( void ) xTaskResumeAll(); // 恢复任务调度器
}
}
从上面的代码中可以看出,heap_3 内存管理算法同样是简单地调用了 C 库的内存释放函数 free()
来释放内存,同时在释放内存前后挂起和恢复任务调度器,以确保线程安全。
2.4、heap_4内存管理算法
heap_4 内存管理算法使用了首次适应算法,与 heap_2 内存管理算法一样,heap_4 内存管理算法也支持内存的申请与释放,并且 heap_4 内存管理算法还能够将空闲且相邻的内存进行合并,从而减少内存碎片的现象。
heap_4 内存管理算法的内存堆与 heap_1、heap_2 内存管理算法的内存堆一样,都是一个数组,定义如下所示:
#if ( configAPPLICATION_ALLOCATED_HEAP == 1 ) // 此宏用于定义 FreeRTOS 内存堆的定义方式
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; // 用户自定义一个大数组作为 FreeRTOS 管理的内存堆
#else
PRIVILEGED_DATA static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; // 定义一个大数组作为 FreeRTOS 管理的内存堆
#endif
从上面的代码中可以看出,heap_4 内存管理算法中定义的内存堆与 heap_1、heap_2 内存管理算法一样,可以在 FreeRTOSConfig.h 文件中配置 configTOTAL_HEAP_SIZE
配置项,以配置内存堆的字节大小,同样地,也可以用过 configAPPLICATION_ALLOCATED_HEAP
配置项将内存堆定义在指定的内存地址中。
用户可以通过函数 xPortGetFreeHeapSize()
获取内存堆中未分配的内存总量,根据系统运行时内存堆中剩余的内存空间大小,就可以针对性地对 configTOTAL_HEAP_SIZE
配置项进行优化配置。
与 heap_2 内存管理算法相似,heap_4 内存管理算法也引入了内存块的概念。在内存堆中内存以内存块表示,首先来看一下 heap_4 内存管理算法中内存块的定义:
typedef struct A_BLOCK_LINK // 内存块结构体
{
struct A_BLOCK_LINK * pxNextFreeBlock; // 指向下一个内存块
size_t xBlockSize; // 最高位表示内存块是否已经被分配,其余位表示内存块的大小
} BlockLink_t;
与 heap_2 内存管理算法类似,heap_4 内存管理算法的内存块结构体中都包含了两个成员变量,其中成员变量 pxNextFreeBlock 与 heap_2 内存管理算法一样,都是用来指向下一个空闲内存块的。成员变量 xBlockSize,这个成员变量与 heap_2 内存管理算法中的有些不同,这个成员变量的数据类型为 size_t 对于 32 位的 STM32 而言,这是一个 32 位无符号数,其中 xBlockSize 的最高位用来标记内存块是否已经被分配,当内存块被分配后,xBlockSize 的最高位会被置 1,反之,则置 0,其余位用来表示内存块的大小,因为 xBlockSize 是一个 32 位无符号数,因此能用第 0 位至第 30 位来表示内存块的大小,也因此内存块的最大大小被限制在 0x80000000,即申请内存的大小不能超过 0x80000000 字节。
heap_4 内存管理算法同样会通过内存块中的 pxNextFreeBlock 指针,将还未分配的内存块链成一个单向链表,这个单向链表就叫做空闲块链表。与 heap_2 内存管理算法不同的是,heap_4内存管理算法中空闲块链表中的内存块并不是按照内存块大小的顺序从小到大排序,而是按照空闲块链表中内存块的起始地址大小从小到大排序,这也是为了后续往空闲块链表中插入内存块时,能够将相邻的内存块合并。为了方便管理这个空闲块链表,在 heap_4.c 文件中还定义了一个内存块和一个内存块指针来作为空闲块链表的链表头和指向空闲块链表链表尾,这个两个定义如下:
PRIVILEGED_DATA static BlockLink_t xStart, * pxEnd = NULL;
其中,xStart 作为空闲块链表的链表头,pxEnd 指向空闲块链表的链表尾,需要注意的是,xStart 不是内存堆中的内存块,而 pxEnd 所指向的内存块则是占用了内存堆中一个内存块结构体大小内存的,只是 pxEnd 指向的链表尾内存块的内存大小为 0,因此 xStart 内存块和 pxEnd 指向的内存块并不包含可分配的内存。
heap_4.c 文件中用于初始化内存堆的函数为函数 prvHeapInit()
,此函数的定义如下所示:
static void prvHeapInit( void ) /* PRIVILEGED_FUNCTION */
{
BlockLink_t * pxFirstFreeBlock;
portPOINTER_SIZE_TYPE uxStartAddress, uxEndAddress;
size_t xTotalHeapSize = configTOTAL_HEAP_SIZE; // 获取内存堆的大小,即配置项 configTOTAL_HEAP_SIZE 的值
uxStartAddress = ( portPOINTER_SIZE_TYPE ) ucHeap; // 获取内存堆的起始地址
// 将内存堆的起始地址按 portBYTE_ALIGNMENT 字节向上对齐,并且重新计算地址对齐后内存堆的大小
if( ( uxStartAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
{
uxStartAddress += ( portBYTE_ALIGNMENT - 1 );
uxStartAddress &= ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK );
xTotalHeapSize -= ( size_t ) ( uxStartAddress - ( portPOINTER_SIZE_TYPE ) ucHeap );
}
#if ( configENABLE_HEAP_PROTECTOR == 1 )
{
vApplicationGetRandomHeapCanary( &( xHeapCanary ) );
}
#endif
// xStart 内存块的下一个内存块指向内存堆
xStart.pxNextFreeBlock = ( void * ) heapPROTECT_BLOCK_POINTER( uxStartAddress );
xStart.xBlockSize = ( size_t ) 0; // xStart 内存块的大小固定为 0
// 从内存堆的末尾与空出一个内存块结构体的内存,并让 pxEnd 指向这个内存块
uxEndAddress = uxStartAddress + ( portPOINTER_SIZE_TYPE ) xTotalHeapSize; // 获取内存堆的结束地址
uxEndAddress -= ( portPOINTER_SIZE_TYPE ) xHeapStructSize; // 为 pxEnd 预留内存空间
uxEndAddress &= ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ); // 地址按 portBYTE_ALIGNMENT 字节向下对齐
pxEnd = ( BlockLink_t * ) uxEndAddress; // 设置 pxEnd
pxEnd->xBlockSize = 0; // pxEnd 内存块的大小固定为 0
pxEnd->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( NULL ); // pxEnd 指向的内存块没有下一个内存块
pxFirstFreeBlock = ( BlockLink_t * ) uxStartAddress; // 将内存堆作为一个空闲内存块
// 设置空闲内存块的大小,空闲内存块的大小为 pxEnd 指向的地址减内存块结构体的大小
pxFirstFreeBlock->xBlockSize = ( size_t ) ( uxEndAddress - ( portPOINTER_SIZE_TYPE ) pxFirstFreeBlock );
pxFirstFreeBlock->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( pxEnd ); // 空闲内存块的下一个内存块指向 pxEnd
// 此时内存堆中只有一个空闲内存块,并且这个内存块覆盖了整个内存堆空间
xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
}
从上面的代码中可以看出,初始化内存堆的时候,同时也初始化了 xStart 和 pxEnd,初始化好后的内存堆和 xStart、pxEnd 如下图所示:
从上图中可以看出,heap_4 内存管理算法初始化后的内存堆被分成了两个内存块,分别被内存块指针 pxFirstFreeBlock 和内存块指针 pxEnd 所指向,其中内存块指针 pxEnd 所指向的内存块就是空闲块链表的链表尾,虽然这个链表尾内存块占用了内存堆中的内存,但是并不能作为空闲内存被分配,而被内存块指针 pxFirstFreeBlock 所指向的内存块才是可以被分配的空闲内存块。
heap_4 内存管理算法整体与 heap_2 内存管理算法很相似,但是 heap_4 内存管理算法相较于 heap_2 内存管理算法能够将物理内存空间上相邻的两个空闲内存块合并成一个大的空闲内存块,而这正是在将空闲内存块插入空闲块链表的时候实现的。在 heap_4.c 文件中定义了函数 prvInsertBlockIntoFreeList()
,用于将空闲内存块插入空闲块链表,此函数的定义如下所示:
static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert ) /* PRIVILEGED_FUNCTION */
{
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 ) // 要合并的内存块不能未 pxEnd
{
// 将两个内存块合并
pxBlockToInsert->xBlockSize += heapPROTECT_BLOCK_POINTER( pxIterator->pxNextFreeBlock )->xBlockSize;
pxBlockToInsert->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( pxIterator->pxNextFreeBlock )->pxNextFreeBlock;
}
else
{
// 将待插入内存块插入到 pxEnd 前面
pxBlockToInsert->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( pxEnd );
}
}
else
{
// 将待插入内存块插入到找到的内存块的下一个内存块前面
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
}
// 判断找到的内存块是否不因为与待插入内存块的低地址相邻,而与待插入内存块合并
if( pxIterator != pxBlockToInsert )
{
// 将找到的内存块的下一个内存块指向待插入内存块
pxIterator->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( pxBlockToInsert );
}
else
{
// 如果已经合并了, 那么找到的内存块指向下一个内存块的指针已经被设置了,不应该再被设这,否为会指向它本身
mtCOVERAGE_TEST_MARKER();
}
}
从上面的代码中可以看出,与 heap_2 内存管理算法将空闲块链表中的空闲内存块按照内存块的内存大小从小到大排序的方式不同,heap_4 内存管理算法是将空闲内存块链表中的空闲内存块按照内存块在物理内存上的起始地址从低到高进行排序的,也正是因此,才能够更加方便地找出物理内存地址相邻的空闲内存块,并将其进行合并。
从代码中可以看到,在将空闲内存块插入空闲块链表之前,会先从头开始遍历空闲块链表,按照内存块在物理内存上起始地址从低到高的排序规则,找到空闲块要插入的位置。接着判断待插入空闲内存块的起始地址或结束地址是否分别与该位置前面内存块的结束地址或该位置后面内存块的起始地址相同,如果相同侧表示待插入的空闲内存块在物理地址上与该位置前面的内存块或该位置后面的内存块相邻,那么就将响铃的两个空闲内存块合并成一个大的内存块,再将这个大的内存块插入到空闲块链表中,这个操作的示意图如下所示(以待插入空闲内存块与找到位置的上一个内存块相邻为例)。
heap_4.c 文件中用于申请内存的函数为函数 pvPortMalloc()
,此函数的定义如下所示:
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;
/* Ensure that blocks are always aligned to the required number
* of bytes. */
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
{
/* Byte alignment required. */
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 )
{
// 申请的内存大小需大于 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 );
// 计算出的新地址也要按 portBYTE_ALIGNMENT 字节对齐
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 ) */
// 申请到内存的起始地址, 需按 portBYTE_ALIGNMENT 字节对齐
configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
return pvReturn; // 返回申请到内存的首地址
}
heap_4.c 文件中用于释放内存的函数为函数 vPortFree()
,此函数的定义如下所示:
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();
}
}
}
2.5、heap_5内存管理算法
heap_5 内存管理算法是在 heap_4 内存管理算法的基础上实现的,因为 heap_5 内存管理算法使用与 heap_4 内存管理算法相同的内存分配、释放和合并算法,但是 heap_5 内存管理算法在 heap_4 内存管理算法的基础上实现了管理多个非连续内存区域的能力。
heap_5 内存管理算法默认并没有定义内存堆,需要用户手动调用函数 vPortDefindHeapRegions()
,并传入作为内存堆的内存区域的信息,对其进行初始化。初始化后的内存堆将被作为空闲内存块链接到空闲块链表中,再接下来的内存申请与释放就和 heap_4 内存管理算法一致了。
要注意的是,因为 heap_5 内存管理算法并不会自动创建好内存堆,因此需要用户手动为 heap_5 初始化好作为内存堆的内存区域后,才能够动态创建任务、队列、信号量等对象。
heap_5 内存管理算法定义了一个结构体,用于表示内存区域的信息,该结构体的定义如下所示:
typedef struct HeapRegion
{
uint8_t * pucStartAddress; // 内存区域的起始地址
size_t xSizeInBytes; // 内存区域的大小,单位:字节
} HeapRegion_t;
通过这个结构体就能够表示内存区域的信息了,要注意的是系统中有多个内存区域需要由 heap_5 内存管理算法管理,切记不能多次调用内存区域初始化函数。
我们可以参考以下方式创建内存区域信息数组:
const HeapRegion_t xHeapRegions[] =
{
{(uint8_t *)0x80000000, 0x10000}, // 内存区域 1
{(uint8_t *)0x90000000, 0xA0000}, // 内存区域 2
{NULL, 0} // 数组终止标志
};
vPortDefineHeapRegions(xHeapRegions);
如以上例子所示,定义了一个内存区域信息结构体 HeapRegion_t 类型的数组,数组中包含了两个内存区域的信息,这些内存区域信息必须按照内存区域起始地址的高低,从低到高进行排序,最后以一个起始地址为 NULL,大小为 0 的“虚假”内存区域信息作为内存区域信息数组的终止标志。
heap_5.c 文件中用于初始化内存区域的函数为函数 vPortDefineHeapRegions()
,此函数的定义如下所示:
void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions ) /* PRIVILEGED_FUNCTION */
{
BlockLink_t * pxFirstFreeBlockInRegion = NULL;
BlockLink_t * pxPreviousFreeBlock;
portPOINTER_SIZE_TYPE xAlignedHeap;
size_t xTotalRegionSize, xTotalHeapSize = 0;
BaseType_t xDefinedRegions = 0;
portPOINTER_SIZE_TYPE xAddress;
const HeapRegion_t * pxHeapRegion;
configASSERT( pxEnd == NULL ); // 此函数只能被调用一次
#if ( configENABLE_HEAP_PROTECTOR == 1 )
{
vApplicationGetRandomHeapCanary( &( xHeapCanary ) );
}
#endif
pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] ); // 获取内存区域信息中的第 0 个信息
// 作为内存堆的内存区域大小需大于 0,此处用于遍历内存区域信息数组,直到数组终止标志
while( pxHeapRegion->xSizeInBytes > 0 )
{
xTotalRegionSize = pxHeapRegion->xSizeInBytes; // 获取内存区域的大小
xAddress = ( portPOINTER_SIZE_TYPE ) pxHeapRegion->pucStartAddress; // 获取内存区域的起始地址
if( ( xAddress & portBYTE_ALIGNMENT_MASK ) != 0 ) // 将内存区域的地址地址按 portBYTE_ALIGNMENT 字节向上对齐
{
xAddress += ( portBYTE_ALIGNMENT - 1 );
xAddress &= ~( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK;
// 更新起始地址对齐后内存区域的大小
xTotalRegionSize -= ( size_t ) ( xAddress - ( portPOINTER_SIZE_TYPE ) pxHeapRegion->pucStartAddress );
}
xAlignedHeap = xAddress; // 获取对齐后的内存区域起始地址
// 判断初始化的内存区域是否为第 0 个内存区域,如果初始化的内存区域为第 0 个内存其余,
// 则需要初始化 xStart 内存块,反之,则无需重复初始化 xStart 内存块
if( xDefinedRegions == 0 )
{
// xStart 内存块的下一个内存块指向内存堆 0
xStart.pxNextFreeBlock = ( BlockLink_t * ) heapPROTECT_BLOCK_POINTER( xAlignedHeap );
xStart.xBlockSize = ( size_t ) 0; // xStart 内存块的大小固定为 0
}
else
{
// 如果初始化的内存区域不是内存区域 0,那么 pxEnd 应该已经被初始化过了
configASSERT( pxEnd != heapPROTECT_BLOCK_POINTER( NULL ) );
// 本次初始化的内存区域的起始地址应大于 pxEnd,因为形参 pxHeapRegions 中的内存区域信息
// 是按照内存区域起始地址的高低从低到高进行排序的
configASSERT( ( size_t ) xAddress > ( size_t ) pxEnd );
}
#if ( configENABLE_HEAP_PROTECTOR == 1 )
{
if( ( pucHeapLowAddress == NULL ) ||
( ( uint8_t * ) xAlignedHeap < pucHeapLowAddress ) )
{
pucHeapLowAddress = ( uint8_t * ) xAlignedHeap;
}
}
#endif /* configENABLE_HEAP_PROTECTOR */
pxPreviousFreeBlock = pxEnd; // 记录前一个内存区域的 pxEnd(如果有)
xAddress = xAlignedHeap + ( portPOINTER_SIZE_TYPE ) xTotalRegionSize; // 获取内存堆的结束地址
xAddress -= ( portPOINTER_SIZE_TYPE ) xHeapStructSize; // 为 pxEnd 预留内存空间
xAddress &= ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ); // 地址按 portBYTE_ALIGNMENT 字节向下对齐
pxEnd = ( BlockLink_t * ) xAddress; // 设置 pxEnd
pxEnd->xBlockSize = 0; // pxEnd 内存块的大小固定为 0
pxEnd->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( NULL ); // pxEnd 指向的内存块没有下一个内存块
pxFirstFreeBlockInRegion = ( BlockLink_t * ) xAlignedHeap; // 将内存堆作为该内存区域的一个空闲内存块
// 设置空闲内存块的大小,空闲内存块的大小为 pxEnd 的地址减内存块结构体的大小
pxFirstFreeBlockInRegion->xBlockSize = ( size_t ) ( xAddress - ( portPOINTER_SIZE_TYPE ) pxFirstFreeBlockInRegion );
// 空闲内存块的下一个内存块指向 pxEnd
pxFirstFreeBlockInRegion->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( pxEnd );
// 判断本次初始化的内存区域是否为第 0 个内存区域,如果不是第 0 个内存区域,
// 那么就将上一个内存区域的 pxEnd 中用于指向下一个内存块的指针指向本次初始化后的空闲内存块
if( pxPreviousFreeBlock != NULL )
{
pxPreviousFreeBlock->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( pxFirstFreeBlockInRegion );
}
xTotalHeapSize += pxFirstFreeBlockInRegion->xBlockSize; // 更新所有内存堆的大小
#if ( configENABLE_HEAP_PROTECTOR == 1 )
{
if( ( pucHeapHighAddress == NULL ) ||
( ( ( ( uint8_t * ) pxFirstFreeBlockInRegion ) + pxFirstFreeBlockInRegion->xBlockSize ) > pucHeapHighAddress ) )
{
pucHeapHighAddress = ( ( uint8_t * ) pxFirstFreeBlockInRegion ) + pxFirstFreeBlockInRegion->xBlockSize;
}
}
#endif
// 准备处理内存区域数组中的下一个内存区域信息元素
xDefinedRegions++;
pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );
}
// 此时所有内存堆中还没有被申请的内存
xMinimumEverFreeBytesRemaining = xTotalHeapSize;
xFreeBytesRemaining = xTotalHeapSize;
// 检查是否有实际的内存区域被初始化
configASSERT( xTotalHeapSize );
}
从上面的代码中可以看出,heap_5 内存管理算法的内存区域初始化与 heap_4 内存管理算法的内存堆初始化是有写相似的地方的,heap_5 内存管理算法初始化后的内存区域示意图,如下所示: