FreeRTOS——堆内存管理

FreeRTOS 基础系列文章

 基本对象
  FreeRTOS——任务
  FreeRTOS——队列
  FreeRTOS——信号量
  FreeRTOS——互斥量
  FreeRTOS——任务通知
  FreeRTOS——流和消息缓冲区
  FreeRTOS——软件定时器
  FreeRTOS——事件组

 内存管理
  FreeRTOS——静态与动态内存分配
  FreeRTOS——堆内存管理
  FreeRTOS——栈溢出保护

 代码组织
  FreeRTOS——源代码组织
  FreeRTOS——创建新的项目
  FreeRTOS——配置文件


FreeRTOS——堆内存管理

  • 内存管理
  • RTOS 源代码下载中包含的内存分配实现
  • heap_1.c
  • heap_2.c
  • heap_3.c
  • heap_4.c
  • heap_5.c

内存管理

每次创建任务、队列、互斥量、软件计时器、信号量或事件组时,RTOS 内核都需要 RAM。RAM 可以在 RTOS API 对象创建函数中从 RTOS 堆自动动态分配,也可以由应用程序设计者提供。

如果 RTOS 对象是动态创建的,那么有时可以使用标准 C 库 malloc()free() 函数来实现此目的,但是······

  1. 它们并不总是在嵌入式系统上可用,
  2. 它们占用了宝贵的代码空间,
  3. 它们不是线程安全的,并且
  4. 它们不是确定性的(执行函数所花费的时间会因调用而异)

······所以通常需要另一种内存分配实现。

一个嵌入式/实时系统可能与另一个系统具有非常不同的 RAM 和时序要求 —— 因此单个 RAM 分配算法将只适用于应用程序的一个子集(还可以有其他分配算法)。

为了解决这个问题,FreeRTOS 将内存分配 API 保留在其可移植层中。可移植层位于实现核心 RTOS 功能的源文件之外,允许提供适用于正在开发的实时系统的特定于应用程序的实现。当 RTOS 内核需要 RAM 时,它不会调用 malloc(),而是调用 pvPortMalloc()。当 RAM 被释放时,RTOS 内核不会调用 free(),而是调用 vPortFree()。

FreeRTOS 提供了多种堆管理方案,其复杂性和功能各不相同。也可以提供您自己的堆实现,甚至可以同时使用两个堆实现。同时使用两个堆实现允许将任务堆栈和其他 RTOS 对象放置在快速的内部 RAM 中,并将应用程序数据放置在较慢的外部 RAM 中。


RTOS 源代码下载中包含的内存分配实现

FreeRTOS 下载中包括五个示例的内存分配实现,每个都在以下小节中进行了描述。这些小节还包括关于提供的每个实现最适合在哪些情况下使用的信息。

每个提供的实现都包含在一个单独的源文件中(分别为 heap_1.cheap_2.cheap_3.cheap_4.cheap_5.c),它们位于主 RTOS 源代码下载的 Source/Portable/MemMang目录中。可以根据需要添加其他实现。在一个项目中一次只应该包含这些源文件中的一个 [ 由这些可移植层函数定义的堆将由 RTOS 内核使用,即使使用 RTOS 的应用程序选择使用自己的堆实现 ]

如下:

  • heap_1 - 最简单的,不允许释放内存。
  • heap_2 - 允许释放内存,但不合并相邻的空闲块。
  • heap_3 - 简单地包装标准 malloc() 和 free() 以确保线程安全。
  • heap_4 - 合并相邻的空闲块以避免碎片。包括绝对地址放置选项。
  • heap_5 - 根据 heap_4,能够跨越多个非相邻内存区域的堆。

注意:

  • heap_1 不太有用,因为 FreeRTOS 添加了对静态分配的支持。
  • heap_2 现在被认为是遗留的,因为较新的 heap_4 实现是首选。

heap_1.c

heap_1 不太有用,因为 FreeRTOS 添加了对静态分配的支持。

heap_1 是最简单的实现。一旦分配了内存,它不允许释放内存。尽管如此,heap_1.c 适用于大量嵌入式应用程序。这是因为许多小型且深度嵌入的应用程序会在系统启动时创建所有所需的任务、队列、信号量等,然后在程序的生命周期内使用所有这些对象(直到应用程序再次关闭或重新启动)。什么都不会被删除。

该实现只是在请求 RAM 时将单个阵列细分为更小的块。数组的总大小(堆的总大小)由 configTOTAL_HEAP_SIZE 设置 —— 它在 FreeRTOSConfig.h 中定义。提供configAPPLICATION_ALLOCATED_HEAP FreeRTOSConfig.h 配置常量以允许将堆放置在内存中的特定地址。

xPortGetFreeHeapSize() API 函数返回未分配的堆空间总量,允许优化 configTOTAL_HEAP_SIZE 设置。

heap_1 实现:

  • 如果您的应用程序从不删除任务、队列、信号量、互斥量等(实际上涵盖了使用 FreeRTOS 的大多数应用程序),则可以使用。
  • 总是确定性的(总是花费相同的时间来执行)并且不会导致内存碎片。
  • 非常简单,从静态分配的数组分配内存,这意味着它通常适用于不允许真正动态内存分配的应用程序。

heap_2.c

heap_2 现在被认为是遗留的,因为 heap_4 是首选。

heap_2 使用最佳拟合算法,并且与方案 1 不同,它允许释放先前分配的块。它不会将相邻的空闲块组合成单个大块。参阅 heap_4.c了解合并空闲块的实现。

可用堆空间的总量由 configTOTAL_HEAP_SIZE 设置 —— 它在 FreeRTOSConfig.h 中定义。提供configAPPLICATION_ALLOCATED_HEAP FreeRTOSConfig.h 配置常量以允许将堆放置在内存中的特定地址。

xPortGetFreeHeapSize() API 函数返回未分配的堆空间总量(允许优化 configTOTAL_HEAP_SIZE 设置),但不提供有关如何将未分配的内存分成更小的块的信息。

这个实现:

  • 即使在应用程序重复删除任务、队列、信号量、互斥量等情况时也可以使用,下面是关于内存碎片的注意事项。

  • 如果分配和释放的内存大小是随机的,则不应使用。例如:

    如果应用程序动态创建和删除任务,并且分配给正在创建的任务的堆栈大小始终相同,那么在大多数情况下可以使用 heap2.c。但是,如果分配给正在创建的任务的堆栈大小并不总是相同,那么可用的空闲内存可能会碎片化为许多小块,最终导致分配失败。在这种情况下,heap_4.c 将是更好的选择。

    如果应用程序动态创建和删除队列,并且每种情况下队列存储区都相同(队列存储区是队列项大小乘以队列长度),那么大多数情况下可以使用 heap_2.c。但是,如果每种情况下的队列存储区域都不相同,那么可用的空闲内存可能会分成许多小块,最终导致分配失败。在这种情况下,heap_4.c 将是更好的选择。

    应用程序直接调用 pvPortMalloc()vPortFree(),而不是仅通过其他 FreeRTOS API 函数间接调用。

  • 如果您的应用程序队列、任务、信号量、互斥量等以不可预测的顺序排列,可能会导致内存碎片问题。这对几乎所有的应用程序都不太可能,但应该记住。

  • 不是确定性的 —— 但是比大多数标准 C 库的 malloc 实现更高效。

heap_2.c 适用于许多必须动态创建对象的小型实时系统。有关将空闲内存块组合成单个更大块的类似实现,请参见 heap_4。


heap_3.c

这为标准 C 库 malloc() 和 free() 函数实现了一个简单的封装器,在大多数情况下,这些函数将随您选择的编译器一起提供。封装器只是使 malloc() 和 free() 函数线程安全。

这个实现:

  • 需要链接器设置堆,编译器库提供 malloc() 和 free() 实现。
  • 不是确定性的。
  • 可能会大大增加 RTOS 内核代码的大小。

请注意,当使用 heap_3 时,FreeRTOSConfig.h 中的 configTOTAL_HEAP_SIZE 设置无效。


heap_4.c

该方案使用快速拟合算法,与方案 2 不同的是,它将相邻的可用内存块合并成一个大的块(它包含合并算法)。

可用堆空间的总量由 configTOTAL_HEAP_SIZE 设置 —— 它在 FreeRTOSConfig.h 中定义。提供configAPPLICATION_ALLOCATED_HEAP FreeRTOSConfig.h 配置常量以允许将堆放置在内存中的特定地址。

xPortGetFreeHeapSize() API 函数返回调用该函数时未分配的堆空间总量,而 xPortGetMinimumEverFreeHeapSize() API 函数返回 FreeRTOS 应用程序自启动以来系统的历史最小可用堆空间量。这两个函数都没有提供有关如何将未分配的内存分成更小的块的信息。

vPortGetHeapStats() API 函数提供附加信息。它填充 heap_t 结构的成员,如下所示。

/* vPortGetHeapStats()函数的原型。 */
void vPortGetHeapStats( HeapStats_t *xHeapStats );

/* Heap_stats_t结构的定义。 */
typedef struct xHeapStats
{
	/* 当前可用的堆大小——这是所有空闲块的总和,而不是可以分配的最大块。 */
	size_t xAvailableHeapSpaceInBytes;      
	
	/* 调用vPortGetHeapStats()时堆中所有空闲块的最大大小(以字节为单位)。 */
	size_t xSizeOfLargestFreeBlockInBytes; 	
	
	/* 调用vPortGetHeapStats()时堆中所有空闲块的最小大小(以字节为单位)。 */
	size_t xSizeOfSmallestFreeBlockInBytes; 
	
	/* 调用vPortGetHeapStats()时堆中可用内存块的数量。 */
	size_t xNumberOfFreeBlocks;		
	
	/* 自系统启动以来堆中的总空闲内存(所有空闲块的总和)的历史最小值。 */
	size_t xMinimumEverFreeBytesRemaining;	
	
	/* 返回有效内存块的pvPortMalloc()调用次数。 */
	size_t xNumberOfSuccessfulAllocations;	

	/* 成功释放内存块的vPortFree()调用次数。 */
	size_t xNumberOfSuccessfulFrees;	
} HeapStats_t;

heap_4:

  • 即使在应用程序重复删除任务、队列、信号量、互斥体量等情况时也可以使用。
  • 与 heap_2 实现相比,导致堆空间严重碎片化为多个小块的可能性要小得多 —— 即使分配和释放的内存大小是随机的。
  • 不是确定性的 —— 但比大多数标准 C 库 malloc 实现更高效。

heap_4.c 对于想要在应用程序代码中直接使用可移植层内存分配方案的应用程序特别有用(而不是通过调用本身调用 pvPortMalloc() 和 vPortFree() 的 API 函数间接使用)。


heap_5.c

该方案使用与 heap_4 相同的快速拟合和内存合并算法,并允许堆跨越多个非相邻(非连续)内存区域。

Heap_5 是通过调用 vPortDefineHeapRegions() 初始化的,并且在 vPortDefineHeapRegions() 执行之后才能使用。创建 RTOS 对象(任务、队列、信号量等)将隐式调用 pvPortMalloc(),因此在使用 heap_5 时,必须在创建任何此类对象之前调用 vPortDefineHeapRegions()

vPortDefineHeapRegions() 接受单个参数。该参数是一个 HeapRegion_t 结构数组。HeapRegion_tportable.h 中定义为

typedef struct HeapRegion
{
    /* 将成为堆的一部分的内存块的起始地址。*/
    uint8_t *pucStartAddress;

    /* 内存块的大小。 */
    size_t xSizeInBytes;
} HeapRegion_t;

使用 {NULL, 0} 的区域定义结束数组,数组中定义的内存区域必须按地址从低地址到高地址的顺序出现。以下源代码片段提供了一个示例。

/* 分配两个内存块供堆使用。第一个块是从地址0x80000000开始的0x10000字节,
第二个块是从地址0x90000000开始的0xa0000字节。从0x80000000开始的块具有
较低的起始地址,因此首先出现在数组中。 */
const HeapRegion_t xHeapRegions[] =
{
    { ( uint8_t * ) 0x80000000UL, 0x10000 },
    { ( uint8_t * ) 0x90000000UL, 0xa0000 },
    { NULL, 0 } /* 终止该数组。 */
};

/* 将数组传递给vPortDefineHeapRegions()。 */
vPortDefineHeapRegions( xHeapRegions );

xPortGetFreeHeapSize() API 函数返回调用该函数时未分配的堆空间总量,而 xPortGetMinimumEverFreeHeapSize() API 函数返回 FreeRTOS 应用程序自启动以来系统的历史最小可用堆空间量。这两个函数都没有提供有关如何将未分配的内存分成更小的块的信息。

vPortGetHeapStats() API函数提供关于堆状态的附加信息。

你可能感兴趣的:(FreeRTOS,freertos,操作系统)