20.内存管理

一、简介

在使用 FreeRTOS 创建任务、队列、信号量等对象的时候,FreeRTOS 一般都提供了两种方
法,一种是动态地申请创建对象时所需要的内存,这种方法也叫动态方法;一种是由用户自定
义对象,在编译器编译程序的时候,会为已经在程序中定义好的对象分配一定的内存空间,这
种方法也叫静态方法。

静态方法创建任务、队列、信号量等对象的 API 函数一般是以“Static”结尾的,例如静态
创建任务的 API 函数 xTaskCreateStatic()。使用静态方式创建各种对象时,需要用户提供各种内存空间,例如任务的栈空间、任务控制块所用内存空间等等,并且使用静态方式占用的内存空间一般固定下来了,即使任务、队列等被删除后,这些被占用的内存空间也没有其他用途。

在使用动态方式管理内存的时候,FreeRTOS 就能够在创建任务、队列、信号量等对象的时
候,自动地从 FreeRTOS 管理的内存堆中申请所创建对象所需的内存,在对象被删除后,又可
以将这块内存释放会 FreeRTOS 管理的内存堆,这样看来,动态方式管理内存相比与静态方式,显得灵活许多。

除了 FreeRTOS 提供的动态内存管理方法,标准的 C 库也提供了函数 malloc()和函数 free()
来实现动态地申请和释放内存,但是标准 C 库的动态内存管理方法有如下几个缺点:

  1. 并不适用于所有嵌入式系统。
  2. 占用大量的代码空间。
  3. 没有线程安全的相关机制。
  4. 具有不确定性,体现在每次执行的时间不同。

为此,FreeRTOS 提供了动态方式管理内存的方法。不同的嵌入式系统对于动态内存管理的
需求不同,因此 FreeRTOS 提供了多种内存管理算法选项,并将其作为 FreeRTOS 移植层的一部分,这样一来,FreeRTOS 的使用者就能够根据自己实际的需求选的何时的动态内存管理算法,并将其移植到系统中。

二、FreeRTOS 内存管理算法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1. heap_1 内存管理算法

heap_1 内存管理算法是 5 种内存管理算法中最实现简单的内存管理算法,但是由 heap_1 内
存管理算法申请的内存,是无法被释放的。尽管如此,heap_1 内存管理算法依然适用于个别嵌
入式应用,这是因为个别嵌入式应用会在系统启动时创建所需的任务、队列、信号量等,接着
在整个程序的运行过程中,这些创建好的任务、队列、信号量等都不需要被删除,因此也就无
需释放这些任务、队列、信号量等创建时申请的内存。
在这里插入图片描述

(1)内存堆:

heap_1 内存管理算法管理的内存堆是一个数组,在申请内存的时候,heap_1 内存管理算法
只是简单地从数组中分出合适大小的内存,内存堆数组的定义如下所示:

/* 此宏用于定义 FreeRTOS 内存堆的定义方式 */
#if ( configAPPLICATION_ALLOCATED_HEAP == 1 )
 /* 用户自定义一个大数组作为 FreeRTOS 管理的内存堆 */
 extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#else
 /* 定义一个大数组作为 FreeRTOS 管理的内存堆 */
 static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#endif

从上面的代码中可以看出,heap_1 内存管理算法管理的内存堆实际上是一个大小为
configTOTAL_HEAP_SIZE 字节的数组,宏 configTOTAL_HEAP_SIZE 可以在 FreeRTOSConfig.h文件中进行配置。宏 configAPPLICATION_ALLCOATED_HEAP 允许用户将内存堆定义在指定的地址中,常用于将内存堆定义在外扩的 RAM 中。用户可以通过函数 xPortGetFreeHeapSize()获取内存堆剩余空间的大小,根据系统运行时内存堆的剩余空间大小,对 configTOTAL_HEAP_SIZE 配置项进行优化配置。

(2)特性:
  1. 适用于一旦创建好任务、队列、信号量等就不会删除的应用,实际上大多数的 FreeRTOS
    应用都是这样的。
  2. 具有确定性,体现在每次执行的时间都是一样的,而且不会产生内存碎片。
  3. 实现的方式非常简单,分配的内存都是从一个静态分配的数组中分配的,因此也意味着
    并不适用于那些真正需要动态申请和释放内存的应用。
(3)内存申请函数详解:

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 )
 {
 if ((xWantedSize +
 (portBYTE_ALIGNMENT - (xWantedSize & portBYTE_ALIGNMENT_MASK)))
 > xWantedSize )
 {
 xWantedSize +=
 (portBYTE_ALIGNMENT - (xWantedSize & portBYTE_ALIGNMENT_MASK));
 }
 else
 {
 xWantedSize = 0;
 }
 }
}
#endif
 
 /* 挂起任务调度器 */
 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 )
 {
 extern void vApplicationMallocFailedHook( void );
 /* 调用动态内存申请失败钩子函数 */
 vApplicationMallocFailedHook();
 }
}
#endif
 
 /* 返回申请到内存的首地址 */
 return pvReturn;
}

从上面的代码中可以看出,heap_1 内存管理算法的申请内存函数的实现非常简单,就是从
内存堆的低地址开始往高地址分配内存,内存堆的结构示意图如下所示:
在这里插入图片描述
从上图可以看出,heap_1 内存管理算法管理下的内存堆利用率是非常高的,除了内存堆起
始地址的位置可能会因地址对齐产生一小块无用内存外,内存堆中其余的内存空间都可以用来
分配,并且也不会产生内存碎片。

(4)内存释放函数详解:

heap_1.c 文件中用于释放内存的函数为函数 pvPortFree(),此函数的定义如下所示:

void vPortFree( void * pv )
{
 /* 没有实现释放内存的功能 */
 ( void ) pv;
 
 configASSERT( pv == NULL );
}

从上面的代码中可以看出,heap_1 内存管理算法的内存释放函数并没有实现,因此使用
heap_1 内存管理算法申请的内存,是无法释放的。

2. heap_2 内存管理算法

相比于 heap_1 内存管理算法,heap_2 内存管理算法使用了最适应算法,以支持释放先前申
请的内存,但是 heap_2 内存管理算法并不能将相邻的空闲内存块合并成一个大的空闲内存块,因此 heap_2 内存管理算法不可避免地会产生内存碎片。内存碎片是由于多次申请和释放内存,但释放的内存无法与相邻的空闲内存合并而产生的,具体的产生过程,如下图所示:
在这里插入图片描述
适用场景:频繁的创建和删除任务,且所创建的任务堆栈都相同,这类场景下Heap_2没有碎片化的问题

如上图所示,当内存堆被多次申请和释放后,由于相邻的小空闲内存无法合并成一个大的
空闲内存,从而导致即使内存堆中有足够多的空闲内存,也无法再分配出一块大内存。
在这里插入图片描述

(1)内存堆:

heap_2 内存管理算法的内存堆与 heap_1 内存管理算法的内存堆一样,都是一个数组, 定义如下所示:

/* 此宏用于定义 FreeRTOS 内存堆的定义方式 */
#if ( configAPPLICATION_ALLOCATED_HEAP == 1 )
 /* 用户自定义一个大数组作为 FreeRTOS 管理的内存堆 */
 extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#else
 /* 定义一个大数组作为 FreeRTOS 管理的内存堆 */
 static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#endif

从上面的代码中可以看出,heap_2 内存管理算法中定义的内存堆与 heap_1 内存管理算法
一样,可以在 FreeRTOSConfig.h 文件中配置 configTOTAL_HEAP_SIZE 配置项,以配置内存堆的字节大小,同样地,也可以用过 configAPPLICATION_ALLOCATED_HEAP 配置项将内存堆定义在指定的内存地址中。

用户可以通过函数 xPortGetFreeHeapSize()获取内存堆中未分配的内存总量,并根据系统运
行时内存堆中剩余内存的大小,针对性地对 configTOTAL_HEAP_SIZE 配置项进行优化配置。

(2)特性:
  1. 可以使用在可能会删除已经创建好的任务、队列、信号量等的应用程序中,但是要注意
    内存碎片的产生。
  2. 不应该被使用在多次申请和释放不固定大小内存的情况,因为这可能会导致内存碎片的
    情况变得严重,例如多次地创建和删除任务、队列等,并且每次创建的任务栈大小、队列长度
    等都是不固定的,或需要在应用程序中调用函数 pvPortMalloc()和函数 vPortFree()来申请和释放不固定大小的内存等,以上这些情况都应该慎用 heap_2 内存管理算法。
  3. 具有不确定性,但是执行的效率比标准 C 库的内存管理高得多。
(3)内存块详解:

为了能够实现内存的释放功能,heap_2 内存管理算法引入了内存块的概念。在内存堆中的
内存都是以内存块表示的,首先来看一下 heap_2 内存管理算法中内存块的定义:

typedef struct A_BLOCK_LINK
{
 struct A_BLOCK_LINK * pxNextFreeBlock; /* 指向下一个内存块 */
 size_t xBlockSize; /* 内存块的大小 */
} BlockLink_t;

从上面的代码中可以看出,每一个内存块都包含了一个用于指向下一个内存块的指针
pxNextFreeBlock,并记录了内存块的大小,内存块的大小就包含了内存块的内存块结构体占用
的内存空间和内存块中可使用的内存大小,因此内存块的结构如下图所示(下图展示了一段 24
字节大小的内存作为内存块的示意图):
在这里插入图片描述
heap_2 内存管理算法会通过内存块中的 pxNextFreeBlock 指针,将还未分配的内存块链成
一个单向链表,这个单向链表就叫做空闲块链表。空闲块链表中的内存块是按照内存块的大小
从小到大排序的,因此空闲块链表中相邻的两个内存块,其在内存中也不一定相邻。为了方便
管理这个空闲块链表,在 heap_2.c 文件中定义了两个内存块来作为空闲块链表的链表头和链表
尾,这两个内存块的定义如下:

static BlockLink_t xStart, xEnd;

其中,xStart 作为空闲块链表的链表头,xEnd 作为空闲块链表的链表尾,需要注意的是,
xStart 和 xEnd 并不是内存堆中的内存块,因此 xStart 和 xEnd 内存块并不包含可分配的内存。

(4)内存堆初始化详解:

heap_2.c 文件中用于初始化内存堆的函数为函数 prvHeapInit(),此函数的定义如下所示:

static void prvHeapInit( void )
{
 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 内存块的下一个内存块指向内存堆 */
 xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
 /* xStart 内存块的大小固定为 0 */
 xStart.xBlockSize = ( size_t ) 0;
 
 /* xEnd 内存块的大小用于指示内存堆的总大小 */
 xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;
 /* xEnd 内存块没有下一个内存块 */
 xEnd.pxNextFreeBlock = NULL;
 
 /* 将整个内存堆作为一个内存块 */
 pxFirstFreeBlock = ( void * ) pucAlignedHeap;
  /* 设置内存块的大小 */
 pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;
 /* 内存块的下一个内存块指向 xEnd */
 pxFirstFreeBlock->pxNextFreeBlock = &xEnd;
}

从上面的代码中可以看出,初始化内存堆的时候,同时也初始化了 xStart 和 xEnd,初始化
好后的内存堆和 xStart、xEnd 如下图所示:
在这里插入图片描述

(5)空闲块链表插入空闲内存块:

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; \
}

从上面的代码中可以看出,将空闲的内存块插入空闲块链表,首先会从头遍历空闲块链表
找到第一个内存大小不小于待插入空闲内存块的空闲内存块的上一个空闲内存块,然后将待插
入空闲内存块插入到这个空闲内存块的后面,如下图所示:
在这里插入图片描述

(6)内存申请函数详解:

heap_2.c 文件中用于申请内存的函数为函数 pvPortMalloc(),此函数的定义如下所示:

void * pvPortMalloc( size_t xWantedSize )
{
 BlockLink_t * pxBlock, * pxPreviousBlock, * pxNewBlockLink;
 static BaseType_t xHeapHasBeenInitialised = pdFALSE;
 void * pvReturn = NULL;
 
 /* 挂起任务调度器 */
 vTaskSuspendAll();
 {
 /* 如果内存堆未初始化,则先初始化内存堆 */
 if( xHeapHasBeenInitialised == pdFALSE )
 {
 /* 初始化内存堆 */
 prvHeapInit();
 /* 标记内存堆已经初始化 */
 xHeapHasBeenInitialised = pdTRUE;
 }
 
 /* 申请的内存大小需大于 0 */
 if( ( xWantedSize > 0 ) &&
 ( ( xWantedSize + heapSTRUCT_SIZE ) > xWantedSize ) )
 {
 /* 将所需申请的内存大小加上内存块结构体的大小 */
 xWantedSize += heapSTRUCT_SIZE;
 
 /* 将所需申请的内存大小按 portBYTE_ALIGNMENT 字节对齐 */
 if( ( xWantedSize +
 (portBYTE_ALIGNMENT-(xWantedSize&portBYTE_ALIGNMENT_MASK)))
 > xWantedSize)
 {
 xWantedSize +=
 (portBYTE_ALIGNMENT-(xWantedSize&portBYTE_ALIGNMENT_MASK));
 configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
 }
 else
 {
 xWantedSize = 0;
 }
 }
 else
 {
 xWantedSize = 0;
 }
 
 /* 所需的内存大小需大于 0,
 * 且小于内存堆中可分配内存大小
 */
 if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
 {
 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 ) +
 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;
}

从上面的代码中可以看出,heap_2 内存管理算法申请内存的过程,大致如下:

  1. 因为空闲块链表中的空闲内存块是按照内存块的大小从小到大排序的,因此从头开始遍
    历空闲块链表,找到第一个大小适合的空闲内存块。
  2. 找到大小适合的空闲内存块后,由于找到的空闲内存块可能比需要申请的内存大,因此
    需要将整个内存块分为两个小的内存块,其中一个内存块的大小就是需要申请内存的大小,另
    一个小内存块作为空闲内存块重新插入空闲块链表。
(7)内存释放函数详解:

heap_2.c 文件中用于释放内存的函数为函数 pvPortFree(),此函数的定义如下所示:

void vPortFree( void * pv )
{
 uint8_t * puc = ( uint8_t * ) pv;
 BlockLink_t * pxLink;
 
 /* 被释放的对象需不为空 */
 if( pv != NULL )
 {
 /* 获取内存块的起始地址 */
 puc -= heapSTRUCT_SIZE;
 
 /* 获取内存块 */
 pxLink = ( void * ) puc;
 
 /* 挂起任务调度器 */
 vTaskSuspendAll();
 {
 /* 将被释放的内存块插入空闲块链表 */
 prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
 /* 更新内存堆中可分配的内存大小 */
 xFreeBytesRemaining += pxLink->xBlockSize;
 /* 用于调式,不用理会 */
 traceFREE( pv, pxLink->xBlockSize );
 }
 /* 恢复任务调度器 */
 ( void ) xTaskResumeAll();
 }
}

从上面的代码中可以看出,heap_2 内存管理算法的释放函数含简单,就是将带释放的内存
块插入到空闲块链表中。

3.heap_3 内存管理算法

heap_3 内存管理算法是对标准 C 库提供的函数 malloc()和函数 free()的简单封装,以确保线
程安全。

(1)内存堆:

heap_3 内存管理算法本质使用的是调用标准 C 库提供的内存管理函数,标准 C 库的内存
管理需要链接器设置好一个堆,这个堆将作为内存管理的内存堆使用,在启动文件中可以配置
这个堆的大小,如下所示:

; 配置堆的大小
Heap_Size EQU 0x00000200
; AREA: 开辟一段内存空间
; HEAP: 段名为 HEAP
; NOINIT: 不进行初始化
; READWRITE: 可读可写
; ALIGN=3: 以 823 次方)字节对齐
 AREA HEAP, NOINIT, READWRITE, ALIGN=3
; 堆的起始地址
__heap_base
; 分配一个 Heap_Size 大小的内存空间
Heap_Mem SPACE Heap_Size
; 堆的结束地址
__heap_limit

通过修改 Heap_Size 的值就可以修改堆的大小,要注意的是,在使用 heap_3 内存管理算法
的时候,配置项 configTOTAL_HEAP_SIZE 是无效的。

(2)特性:
  1. 需要链接器提供一个堆,还需要编译器的库提供用于申请内存的函数 malloc()和用于释
    放内存的函数 free()。
  2. 具有不确定性。
  3. 有可能会大大地增减编译后的代码量。
(3)内存申请函数详解:

heap_3.c 文件中用于申请内存的函数为函数 pvPortMalloc(),此函数的定义如下所示:

void * pvPortMalloc( size_t xWantedSize )
{
 void * pvReturn;
  /* 挂起任务调度器 */
 vTaskSuspendAll();
 {
 /* 调用 C 库函数申请内存 */
 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;
}
 

从上面的代码中可以看出,heap_3 内存管理算法实际上是调用了 C 库的内存申请函数
malloc()申请内存,并且会在申请内存的前后挂起和恢复任务调度器,以确保线程安全。

(4)内存释放函数详解:

heap_3.c 文件中用于释放内存的函数为函数 pvPortFree(),此函数的定义如下所示:

void vPortFree( void * pv )
{
 /* 被释放的对象需不为空 */
 if( pv )
 {
 /* 挂起任务调度器 */
 vTaskSuspendAll();
 {
 /* 调用 C 库函数释放内存 */
 free( pv );
 /* 用于调试,不用理会 */
 traceFREE( pv, 0 );
 }
 /* 恢复任务调度器 */
 ( void ) xTaskResumeAll();
 }
}

从上面的代码中可以看出,heap_3 内存管理算法同样是简单地调用了 C 库的内存释放函数
free()来释放内存,同时在释放内存前后挂起和恢复任务调度器,以确保线程安全。

4. heap_4 内存管理算法

heap_4 内存管理算法使用了首次适应算法,与 heap_2 内存管理算法一样,heap_4 内存管
理算法也支持内存的申请与释放,并且 heap_4 内存管理算法还能够将空闲且相邻的内存进行合并,从而减少内存碎片的现象。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(1)内存堆:

heap_4 内存管理算法的内存堆与 heap_1、heap_2 内存管理算法的内存堆一样,都是一个数
组, 定义如下所示:

/* 此宏用于定义 FreeRTOS 内存堆的定义方式 */
#if ( configAPPLICATION_ALLOCATED_HEAP == 1 )
 /* 用户自定义一个大数组作为 FreeRTOS 管理的内存堆 */
 extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#else
 /* 定义一个大数组作为 FreeRTOS 管理的内存堆 */
 PRIVILEGED_DATA static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#endif

从上面的代码中可以看出,heap_4 内存管理算法中定义的内存堆与 heap_1、heap_2 内存管
理算法一样,可以在 FreeRTOSConfig.h 文件中配置 configTOTAL_HEAP_SIZE 配置项,以配置内存堆的字节大小,同样地,也可以用过 configAPPLICATION_ALLOCATED_HEAP 配置项将内存堆定义在指定的内存地址中。

用户可以通过函数 xPortGetFreeHeapSize()获取内存堆中未分配的内存总量,根据系统运行
时内存堆中剩余的内存空间大小,就可以针对性地对 configTOTAL_HEAP_SIZE 配置项进行优
化配置。

(2)特性:
  1. 适用于在程序中多次创建和删除任务、队列、信号量等的应用。
  2. 与 heap_2 内存管理算法相比,即使多次分配和释放随机大小的内存,产生内存碎片的几率也要小得多。
  3. 具有不确定性,但是执行的效率比标准 C 库的内存管理高得多。
(3)内存块详解:

与 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
指向的内存块并不包含可分配的内存。

(4)内存堆初始化详解:

heap_4.c 文件中用于初始化内存堆的函数为函数 prvHeapInit(),此函数的定义如下所示:
在这里插入图片描述

static void prvHeapInit( void )
{
 BlockLink_t * pxFirstFreeBlock;
 uint8_t * pucAlignedHeap;
 size_t uxAddress;
 /* 获取内存堆的大小,
 * 即配置项 configTOTAL_HEAP_SIZE 的值
 */
 size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;
 
 /* 获取内存堆的起始地址 */
 uxAddress = ( size_t ) ucHeap;
 
 /* 将内存堆的起始地址按 portBYTE_ALIGNMENT 字节向上对齐
 * 并且重新计算地址对齐后内存堆的大小
 */
 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.pxNextFreeBlock = ( void * ) pucAlignedHeap;
 /* xStart 内存块的大小固定为 0 */
 xStart.xBlockSize = ( size_t ) 0;
 
 /* 从内存堆的末尾与空出一个内存块结构体的内存
 * 并让 pxEnd 指向这个内存块
 */
 /* 获取内存堆的结束地址 */
 uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
 /* 为 pxEnd 预留内存空间 */
 uxAddress -= xHeapStructSize;                              
 /* 地址按 portBYTE_ALIGNMENT 字节向下对齐 */
 uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
 /* 设置 pxEnd */
 pxEnd = ( void * ) uxAddress;
 /* pxEnd 内存块的大小固定为 0 */
 pxEnd->xBlockSize = 0;
 /* pxEnd 指向的内存块没有下一个内存块 */
 pxEnd->pxNextFreeBlock = NULL;
 
 /* 将内存堆作为一个空闲内存块 */
 pxFirstFreeBlock = ( void * ) pucAlignedHeap;
 /* 设置空闲内存块的大小
 * 空闲内存块的大小为 pxEnd 指向的地址减内存块结构体的大小
 */
 pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
 /* 空闲内存块的下一个内存块指向 pxEnd */
 pxFirstFreeBlock->pxNextFreeBlock = pxEnd;
 
 /* 此时内存堆中只有一个空闲内存块,
 * 并且这个内存块覆盖了整个内存堆空间
  */
 xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
 xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
 
 /* 此变量限制了内存块的大小,
 * 在 32 位系统中,这个值的计算结果为 0x80000000,
 * 内存块结构体中的成员变量 xBlockSize 的最高位
 * 用来标记内存块是否被分配,
 * 其余位用来表示内存块的大小,
 * 因此内存块的大小最大为 0x7FFFFFFF,
 * 即内存块的大小小于 xBlockAllocatedBit 的值
 */
 xBlockAllocatedBit = ((size_t)1) << ((sizeof(size_t)*heapBITS_PER_BYTE)-1);
}

从上面的代码中可以看出,初始化内存堆的时候,同时也初始化了 xStart 和 pxEnd,初始
化好后的内存堆和 xStart、pxEnd 如下图所示:
在这里插入图片描述
从上图中可以看出,heap_4 内存管理算法初始化后的内存堆被分成了两个内存块,分别被
内存块指针 pxFirstFreeBlock 和内存块指针 pxEnd 所指向,其中内存块指针 pxEnd 所指向的内存块就是空闲块链表的链表尾,虽然这个链表尾内存块占用了内存堆中的内存,但是并不能作为空闲内存被分配,而被内存块指针 pxFirstFreeBlock 所指向的内存块才是可以被分配的空闲内存块。

(5)空闲块链表插入空闲内存块:

在这里插入图片描述

heap_4 内存管理算法整体与 heap_2 内存管理算法很相似,但是 heap_4 内存管理算法相较
于 heap_2 内存管理算法能够将物理内存空间上相邻的两个空闲内存块合并成一个大的空闲内
存块,而这正是在将空闲内存块插入空闲块链表的时候实现的。在 heap_4.c 文件中定义了函数prvInsertBlockIntoFreeList(),用于将空闲内存块插入空闲块链表,此函数的定义如下所示:

static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert )
{
 BlockLink_t * pxIterator;
 uint8_t * puc;
 
 /* 从头开始遍历空闲块链表,
 * 找到第一个下一个内存块的起始地址比待插入内存块高的内存块
 */
 for( pxIterator = &xStart;
 pxIterator->pxNextFreeBlock < pxBlockToInsert;
 pxIterator = pxIterator->pxNextFreeBlock )
 {
 /* 什么都不做 */
 }
 
 /* 获取找到的内存块的起始地址 */
 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 * ) pxIterator->pxNextFreeBlock )
 {
 /* 要合并的内存块不能未 pxEnd */
 if( pxIterator->pxNextFreeBlock != pxEnd )
 {
 /* 将两个内存块合并 */
 pxBlockToInsert->xBlockSize +=
 pxIterator->pxNextFreeBlock->xBlockSize;
 pxBlockToInsert->pxNextFreeBlock =
 pxIterator->pxNextFreeBlock->pxNextFreeBlock;
 }
 else
 {
 /* 将待插入内存块插入到 pxEnd 前面 */
 pxBlockToInsert->pxNextFreeBlock = pxEnd;
 }
 }
 else
 {
 /* 将待插入内存块插入到找到的内存块的下一个内存块前面 */
 pxBlockToInsert->pxNextFreeBlock =pxIterator ->pxNextFreeBlock;
 }
 
 /* 判断找到的内存块是否不因为与待插入内存块的低地址相邻,
 * 而与待插入内存块合并
 */
 if( pxIterator != pxBlockToInsert )
 {
 /* 将找到的内存块的下一个内存块指向待插入内存块 */
 pxIterator->pxNextFreeBlock = pxBlockToInsert;
 }
 else
 {
 /* 如果已经合并了,
 * 那么找到的内存块指向下一个内存块的指针已经被设置了,
 * 不应该再被设这,否为会指向它本身
 */
 mtCOVERAGE_TEST_MARKER();
 }
}

从上面的代码中可以看出,与 heap_2 内存管理算法将空闲块链表中的空闲内存块按照内存
块的内存大小从小到大排序的方式不同,heap_4 内存管理算法是将空闲内存块链表中的空闲内
存块按照内存块在物理内存上的起始地址从低到高进行排序的,也正是因此,才能够更加方便
地找出物理内存地址相邻的空闲内存块,并将其进行合并。

从代码中可以看到,在将空闲内存块插入空闲块链表之前,会先从头开始遍历空闲块链表,
按照内存块在物理内存上起始地址从低到高的排序规则,找到空闲块要插入的位置。接着判断
待插入空闲内存块的起始地址或结束地址是否分别与该位置前面内存块的结束地址或该位置后
面内存块的起始地址相同,如果相同侧表示待插入的空闲内存块在物理地址上与该位置前面的
内存块或该位置后面的内存块相邻,那么就将响铃的两个空闲内存块合并成一个大的内存块,
再将这个大的内存块插入到空闲块链表中,这个操作的示意图如下所示(以待插入空闲内存块与找到位置的上一个内存块相邻为例):
在这里插入图片描述

(6)内存申请函数详解:

heap_4.c 文件中用于申请内存的函数为函数 pvPortMalloc(),此函数的定义如下所示:
在这里插入图片描述

void * pvPortMalloc( size_t xWantedSize )
{
 BlockLink_t * pxBlock, * pxPreviousBlock, * pxNewBlockLink;
 void * pvReturn = NULL;
 
 /* 挂起任务调度器 */
 vTaskSuspendAll();
 {
 /* 如果内存堆未初始化,则先初始化内存堆 */
 if( pxEnd == NULL )
 {
 /* 初始化内存堆 */
 prvHeapInit();
 }
 else
 {
 mtCOVERAGE_TEST_MARKER();
 }
 /* 需要申请的内存大小不能超过内存块的最大大小限制
 * 如果超过此限制,则内存申请失败
 */
 if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
 {
 /* 申请的内存大小需大于 0及检查是否溢出 */
 if( ( xWantedSize > 0 ) &&
 ( ( xWantedSize + xHeapStructSize ) > xWantedSize ) )
 {
 /* 将所需申请的内存大小加上内存块结构体的大小 */
 xWantedSize += xHeapStructSize;
 
 /* 将所需申请的内存大小按 portBYTE_ALIGNMENT 字节对齐 */
 if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
 {
 if((xWantedSize + (portBYTE_ALIGNMENT -
 (xWantedSize & portBYTE_ALIGNMENT_MASK)))
 > xWantedSize )
 {
 xWantedSize +=
 ( portBYTE_ALIGNMENT -
 ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
 configASSERT(
 ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
 }
 else
 {
 xWantedSize = 0;
 }
 }
 else
 {
 mtCOVERAGE_TEST_MARKER();
 }
 }
 else
 {
 xWantedSize = 0;
 }
 
 /* 所需的内存大小需大于 0,
 * 且小于内存堆中可分配内存大小
 * */
 if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
 {
 pxPreviousBlock = &xStart;
 pxBlock = xStart.pxNextFreeBlock;
 
 /* 从头遍历内存块链表,找到第一个内存大小适合的内存块 */
 while( ( pxBlock->xBlockSize < xWantedSize ) &&
 ( pxBlock->pxNextFreeBlock != NULL ) )
 {
 pxPreviousBlock = pxBlock;
 pxBlock = pxBlock->pxNextFreeBlock;
 }
 
 /* 判断是否找到了符合条件的内存块 */
 if( pxBlock != pxEnd )
 {
 /* 将返回值设置为符合添加内存块中可分配内存的起始地址
 * 即内存块的内存地址偏移内存块结构体大小的地址
 */
 pvReturn = ( void * )
 ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) +
 xHeapStructSize );
 
 /* 将符合条件的内存块从空闲块链表中移除 */
 pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
 
 /* 如果内存块中可分配内存比需要申请的内存大
 * 那么这个内存块可以被分配两个内存块
 * 一个作为申请到的内存块
 * 一个作为空闲块重新添加到空闲块链表中
 */
 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;
 
 /* 将新的空闲内存块插入到空闲块链表中 */
 prvInsertBlockIntoFreeList( pxNewBlockLink );
 }
 else
 {
 mtCOVERAGE_TEST_MARKER();
 }
 
 /* 更新内存堆中可分配的内存大小 */
 xFreeBytesRemaining -= pxBlock->xBlockSize;
 
 /* 更新空闲内存块中内存最小的空闲内存块的内存大小 */
 if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
 {
 xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
 }
 else
 {
 mtCOVERAGE_TEST_MARKER();
 }
 
 /* 标记内存块已经被分配,将最高位置1 */
 pxBlock->xBlockSize |= xBlockAllocatedBit;/* 内存块从空闲块链表中移除后
 * 将内存块结构体中指向下一个内存块的指针指向空
 */
 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 )
 {
 extern void vApplicationMallocFailedHook( void );
 /* 调用动态内存申请失败钩子函数 */
 vApplicationMallocFailedHook();
 }
 else
 {
 mtCOVERAGE_TEST_MARKER();
 }
}
#endif
 
 /* 申请到内存的起始地址,
 * 需按 portBYTE_ALIGNMENT 字节对齐
 */
 configASSERT((((size_t)pvReturn) & (size_t)portBYTE_ALIGNMENT_MASK) == 0);
 /* 返回申请到内存的首地址 */
 return pvReturn;
}

从上面的代码中可以看出,heap_4 内存管理算法申请内存的过程的整个逻辑与 heap_2 内
存管理算法是大同小异的。

(7)内存释放函数详解:

heap_4.c 文件中用于释放内存的函数为函数 pvPortFree(),此函数的定义如下所示:
在这里插入图片描述

void vPortFree( void * pv )
{
uint8_t * puc = ( uint8_t * ) pv;
 BlockLink_t * pxLink;
 
 /* 被释放的对象需不为空 */
 if( pv != NULL )
 {
 /* 获取内存块的起始地址 */
 puc -= xHeapStructSize;
 
 /* 获取内存块 */
 pxLink = ( void * ) puc;
 
 /* 待释放的内存块必须是已经被分配的内存块 */
 configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
 /* 待释放的内存块不能在空闲块链表中 */
 configASSERT( pxLink->pxNextFreeBlock == NULL );
 
 /* 判断待释放的内存块是否是已经被分配的内存块 */
 if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )
 {
 /* 判断待释放的内存块是否不在空闲块链表中 */
 if( pxLink->pxNextFreeBlock == NULL )
 {
 /* 将待释放的内存块标记为未被分配 */
 pxLink->xBlockSize &= ~xBlockAllocatedBit;
 
 /* 挂起任务调度器 */
 vTaskSuspendAll();
 {
 /* 更新内存堆中可分配的内存大小 */
 xFreeBytesRemaining += pxLink->xBlockSize;
 /* 用于调式,不用理会 */
 traceFREE( pv, pxLink->xBlockSize );
 /* 将新的空闲内存块插入到空闲块链表中 */
 prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
 /* 更新成功释放内存的次数 */
 xNumberOfSuccessfulFrees++;
 }
 /* 恢复任务调度器 */
 ( void ) xTaskResumeAll();
 }
 else
 {
mtCOVERAGE_TEST_MARKER();
 }
 }
 else
 {
 mtCOVERAGE_TEST_MARKER();
 }
 }
}

从上面的代码中可以看出,heap_4 内存管理算法释放函数的逻辑与 heap_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 初始化好作为内存堆的内存区域后,才能够动态创建任务、队列、信号量等对象。

(1)内存区域信息结构体

heap_5 内存管理算法定义了一个结构体,用于表示内存区域的信息,该结构体的定义如下
所示:

ypedef 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 的“虚假”内存区域信息作为内存区域信息数
组的终止标志。

(2)初始化内存区域

heap_5.c 文件中用于初始化内存区域的函数为函数 vPortDefineHeapRegions(),此函数的定
义如下所示:

void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions )
{
 BlockLink_t * pxFirstFreeBlockInRegion = NULL, * pxPreviousFreeBlock;
 size_t xAlignedHeap;
 size_t xTotalRegionSize, xTotalHeapSize = 0;
 BaseType_t xDefinedRegions = 0;
 size_t xAddress;
 const HeapRegion_t * pxHeapRegion;
 
 /* 此函数只能被调用一次 */
 configASSERT( pxEnd == NULL );
 
 /* 获取内存区域信息中的第 0 个信息 */
 pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );
 
 /* 作为内存堆的内存区域大小需大于 0
 * 此处用于遍历内存区域信息数组,
 * 直到数组终止标志
 */
 while( pxHeapRegion->xSizeInBytes > 0 )
 {
 /* 获取内存区域的大小 */
 xTotalRegionSize = pxHeapRegion->xSizeInBytes;
 
 /* 获取内存区域的起始地址 */
 xAddress = ( size_t ) pxHeapRegion->pucStartAddress;
 
 /* 将内存区域的地址地址按 portBYTE_ALIGNMENT 字节向上对齐 */
 if( ( xAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
 {
 xAddress += ( portBYTE_ALIGNMENT - 1 );
 xAddress &= ~portBYTE_ALIGNMENT_MASK;
 
 /* 更新起始地址对齐后内存区域的大小 */
 xTotalRegionSize -=
 xAddress - ( size_t ) pxHeapRegion->pucStartAddress;
 }
 /* 获取对齐后的内存区域起始地址 */
 xAlignedHeap = xAddress;
 
 /* 判断初始化的内存区域是否为第 0 个内存区域
 * 如果初始化的内存区域为第 0 个内存其余,
 * 则需要初始化 xStart 内存块,
 * 反之,则无需重复初始化 xStart 内存块
 */
 if( xDefinedRegions == 0 )
 {
 /* xStart 内存块的下一个内存块指向内存堆 0 */
 xStart.pxNextFreeBlock = ( BlockLink_t * ) xAlignedHeap;
 /* xStart 内存块的大小固定为 0 */
 xStart.xBlockSize = ( size_t ) 0;
 }
 else
 {
 /* 如果初始化的内存区域不是内存区域 0,
 * 那么 pxEnd 应该已经被初始化过了
 */
 configASSERT( pxEnd != NULL );
 
 /* 本次初始化的内存区域的起始地址应大于 pxEnd,
 * 因为入参 pxHeapRegions 中的内存区域信息
 * 是按照内存区域起始地址的高低从低到高进行排序的
 */
 configASSERT( xAddress > ( size_t ) pxEnd );
 }
 
 /* 记录前一个内存区域的 pxEnd(如果有) */
 pxPreviousFreeBlock = pxEnd;
 
 /* 获取内存堆的结束地址 */
 xAddress = xAlignedHeap + xTotalRegionSize;
 /* 为 pxEnd 预留内存空间 */
 xAddress -= xHeapStructSize;
 /* 地址按 portBYTE_ALIGNMENT 字节向下对齐 */
 xAddress &= ~portBYTE_ALIGNMENT_MASK;
 /* 设置 pxEnd */
 pxEnd = ( BlockLink_t * ) xAddress;
 /* pxEnd 内存块的大小固定为 0 */
 pxEnd->xBlockSize = 0;
 /* pxEnd 指向的内存块没有下一个内存块 */
 pxEnd->pxNextFreeBlock = NULL;
 
 /* 将内存堆作为该内存区域的一个空闲内存块 */
 pxFirstFreeBlockInRegion = ( BlockLink_t * ) xAlignedHeap;
 /* 设置空闲内存块的大小
 * 空闲内存块的大小为 pxEnd 的地址减内存块结构体的大小
 */
 pxFirstFreeBlockInRegion->xBlockSize =
 xAddress - ( size_t ) pxFirstFreeBlockInRegion;
 /* 空闲内存块的下一个内存块指向 pxEnd */
 pxFirstFreeBlockInRegion->pxNextFreeBlock = pxEnd;
 
 /* 判断本次初始化的内存区域是否为第 0 个内存区域
 * 如果不是第 0 个内存区域,
 * 那么就将上一个内存区域的 pxEnd 中用于指向
 * 下一个内存块的指针指向本次初始化后的空闲内存块
 */
 if( pxPreviousFreeBlock != NULL )
 {
 pxPreviousFreeBlock->pxNextFreeBlock = pxFirstFreeBlockInRegion;
 }
 
 /* 更新所有内存堆的大小 */
 xTotalHeapSize += pxFirstFreeBlockInRegion->xBlockSize;
 
 /* 准备处理内存区域数组中的下一个内存区域信息元素 */
 xDefinedRegions++;
 pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );
 }
 
 /* 此时所有内存堆中还没有被申请的内存 */
 xMinimumEverFreeBytesRemaining = xTotalHeapSize;
 xFreeBytesRemaining = xTotalHeapSize;
 
 /* 检查是否有实际的内存区域被初始化 */
 configASSERT( xTotalHeapSize );
 
 /* 此变量限制了内存块的大小,
 * 在 32 位系统中,这个值的计算结果为 0x80000000,
 * 内存块结构体中的成员变量 xBlockSize 的最高位
 * 用来标记内存块是否被分配,
 * 其余位用来表示内存块的大小,
 * 因此内存块的大小最大为 0x7FFFFFFF,
  * 即内存块的大小小于 xBlockAllocatedBit 的值
 */
 xBlockAllocatedBit = ((size_t)1) << ((sizeof(size_t)*heapBITS_PER_BYTE)-1);
}

从上面的代码中可以看出,heap_5 内存管理算法的内存区域初始化与 heap_4 内存管理算
法的内存堆初始化是有写相似的地方的,heap_5 内存管理算法初始化后的内存区域示意图,如
下所示(以两个内存区域为例):
在这里插入图片描述
heap_5 内存管理算法与 heap_4 内存管理算法大部分的差异在于初始化,其余的内存块插
入空闲块链表、内存申请与释放,都是大同小异的,这里就不在专门分析 heap_5 内存管理算法的这个部分了,请读者参考 heap_4 内存管理的算法的相关内容。

三、相关实验

在这里插入图片描述

#include "freertos_demo.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./SYSTEM/delay/delay.h"
#include "./MALLOC/malloc.h"
/*FreeRTOS*********************************************************************************************/
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
/******************************************************************************************************/
/*FreeRTOS配置*/

/* START_TASK 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define START_TASK_PRIO         1
#define START_TASK_STACK_SIZE   128
TaskHandle_t    start_task_handler;
void start_task( void * pvParameters );

/* TASK1 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define TASK1_PRIO         2
#define TASK1_STACK_SIZE   128
TaskHandle_t    task1_handler;
void task1( void * pvParameters );

/******************************************************************************************************/

/**
 * @brief       FreeRTOS例程入口函数
 * @param       无
 * @retval      无
 */
void freertos_demo(void)
{    
    xTaskCreate((TaskFunction_t         )   start_task,
                (char *                 )   "start_task",
                (configSTACK_DEPTH_TYPE )   START_TASK_STACK_SIZE,
                (void *                 )   NULL,
                (UBaseType_t            )   START_TASK_PRIO,
                (TaskHandle_t *         )   &start_task_handler );
    vTaskStartScheduler();
}


void start_task( void * pvParameters )
{
    taskENTER_CRITICAL();               /* 进入临界区 */
    xTaskCreate((TaskFunction_t         )   task1,
                (char *                 )   "task1",
                (configSTACK_DEPTH_TYPE )   TASK1_STACK_SIZE,
                (void *                 )   NULL,
                (UBaseType_t            )   TASK1_PRIO,
                (TaskHandle_t *         )   &task1_handler );

    vTaskDelete(NULL);
    taskEXIT_CRITICAL();                /* 退出临界区 */
}

/* 任务一,申请内存以及释放内存,并显示空闲内存大小 */
void task1( void * pvParameters )
{
    uint8_t key = 0, t = 0;
    uint8_t * buf = NULL;
    while(1) 
    {
        key = key_scan(0);
        if(key == KEY0_PRES)
        {
            buf = pvPortMalloc(30);                 /* 申请内存 */
            if(buf != NULL)
            {
                printf("申请内存成功!\r\n");
            }else printf("申请内存失败\r\n");
        }else if(key == KEY1_PRES)
        {
            if(buf != NULL)
            {
                vPortFree(buf);                     /* 释放内存 */
                printf("释放内存!!\r\n");
            }                
        }
        if(t++ > 50)
        {
            t = 0;
            printf("剩余的空闲内存大小为:%d\r\n",xPortGetFreeHeapSize());
        }
        vTaskDelay(10);
    }
}

  • 30
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值