FreeRTOS堆内存管理

动态内存分配相关选项
从FreeRTOS V9.0.0起,可以在编译时静态分配内核对象,或者在运行时动态分配内核对象。为了使FreeRTOS尽可能易于使用,这些内核对象不是在编译时静态分配的,而是在运行时动态分配的。FreeRTOS每次创建内核对象时都会分配RAM,并且每次删除内核对象时都会释放RAM。该策略减少了设计和规划工作,简化了API,并最大程度地减少了RAM占用空间。

动态内存分配是C编程概念,而不是FreeRTOS或多任务专用的概念。它与FreeRTOS有关,因为内核对象是动态分配的,而通用编译器提供的动态内存分配方案并不总是适用于实时应用程序。

可以使用标准C库malloc()和free()函数来分配内存,但是由于以下一种或多种原因,它们可能不合适,也可能不合适:

  1. 在小型嵌入式系统上并不总是可用。
  2. 它们的实现可能相对较大,占用了宝贵的代码空间。
  3. 它们很少是线程安全的。
  4. 它们不是确定性的。执行功能所需的时间因调用而异。
  5. 他们可能遭受支离破碎。
  6. 它们会使链接器配置复杂化。
  7. 如果允许堆空间增长到其他变量使用的内存中,它们可能是难以调试错误的根源。

动态内存分配

在FreeRTOS V9.0.0中,内核对象可以在编译时静态分配,也可以在运行时动态分配:早期版本的FreeRTOS使用内存池分配方案,即在编译时预先分配不同大小的内存块池,然后由内存分配功能。尽管这是在实时系统中使用的常见方案,但事实证明它是许多支持请求的源头,主要是因为它无法充分有效地使用RAM使其无法用于非常小的嵌入式系统,因此该方案被放弃。

FreeRTOS现在将内存分配视为可移植层的一部分(与核心代码库的一部分相对)。这是由于不同的嵌入式系统具有不同的动态内存分配和时序要求这一事实,因此,单个动态内存分配算法仅适用于部分应用程序。同样,从核心代码库中删除动态内存分配,使应用程序编写者可以在适当时提供自己的特定实现。

当FreeRTOS需要RAM而不是调用malloc()时,它将调用pvPortMalloc()。释放RAM时,内核将调用vPortFree(),而不是调用free()。pvPortMalloc()与标准C库malloc()函数具有相同的原型,而vPortFree()与标准C库free()函数具有相同的原型。
其原型为:
void *pvPortMalloc( size_t xWantedSize )
void vPortFree( void *pv )

pvPortMalloc()和vPortFree()是公共函数,因此也可以从应用程序代码中调用。

  1. 如果将堆中的可用RAM分成彼此分开的小块,则认为堆是碎片化的。如果堆是零散的,那么即使堆中没有任何空闲块足以容纳该块,即使堆中所有单独的空闲块的总大小比其大很多倍,分配块的尝试也会失败。无法分配的块的大小。

  2. 从FreeRTOS V9.0.0起,可以在编译时静态分配内核对象,也可以在运行时动态配内核对象:FreeRTOS带有pvPortMalloc()和vPortFree()的五个示例实现,FreeRTOS应用程序可以使用示例实现之一,也可以提供自己的示例实现。

这五个示例分别在heap_1.c,heap_2.c,heap_3.c,heap_4.c和heap_5.c源文件中定义,所有这些文件均位于FreeRTOS / Source / portable / MemMang目录中。

从FreeRTOS的V9.0.0 FreeRTOS的应用程序可以完全静态分配,不再需要包括一个堆内存管理器。

Heap_1

小型专用嵌入式系统通常在启动调度程序之前仅创建任务和其他内核对象。在这种情况下,内存仅在应用程序开始执行任何实时功能之前由内核动态分配,并且在应用程序的生命周期内仍会分配内存。这意味着选择的分配方案不必考虑任何更复杂的内存分配问题,例如确定性和分段性,而只需考虑诸如代码大小和简单性之类的属性。

  1. Heap_1.c实现了pvPortMalloc()的非常基本的版本,而不实现vPortFree()。
  2. 在一些禁止使用动态内存分配的某些商业关键和安全关键系统也有可能使用heap_1。由于与不确定性,内存碎片和分配失败相关的不确定性,关键系统通常禁止动态内存分配,但是Heap_1始终是确定性的,无法碎片化内存。
调用:
  1. 随着对pvPortMalloc()的调用,heap_1分配方案将一个简单的数组细分为较小的块,该阵列称为FreeRTOS堆。
  2. 数组的总大小(以字节为单位)由FreeRTOSConfig.h中configTOTAL_HEAP_SIZE定义设置。以这种方式定义大型阵列可能会使应用程序看起来消耗大量RAM,甚至在未从阵列分配任何内存之前。
  3. 每个创建的任务都需要一个任务控制块(TCB)和一个要从堆中分配的堆栈。如下图所示:演示了heap_1如何在创建任务时细分简单数组。
适用场景:

heap_1适用于永不删除任务或其他内核对象的应用程序。
在这里插入图片描述

Heap_2

Heap_2.c可以通过细分由configTOTAL_HEAP_SIZE定义尺寸的数组来工作。它使用最佳匹配算法来分配内存,并且与heap_1不同,它确实允许释放内存。同样,该数组是静态声明的,因此,即使在未分配数组中的任何内存之前,该应用程序也会看起来消耗大量RAM。
最佳拟合算法可确保pvPortMalloc()使用大小与请求的字节数最接近的空闲内存块。例如,请考虑以下情形:

  1. 堆包含三个空闲内存块,分别为5字节,25字节和100字节。
  2. 调用pvPortMalloc()以请求20个字节的RAM。
  3. 满足请求的字节数的最小RAM空闲块是25个字节的块,因此pvPortMalloc()将25个字节的块分为20个字节的块和5个块的一个字节,然后返回指向20字节块的指针。新的5字节块仍可用于以后对pvPortMalloc()的调用。

与heap_4不同,Heap_2不会将相邻的空闲块合并为一个较大的块,因此更容易产生碎片。但是,如果分配的块和随后释放的块始终是相同大小,则碎片不会成为问题。

适用场景:

Heap_2适用于重复创建和删除任务的应用程序,前提是分配给创建的任务的堆栈大小不变。
在这里插入图片描述

如上图所示:演示了在创建、删除任务,然后再次创建任务时的最合适的算法的工作流程。

  1. 图中A 显示了创建三个任务后的阵列。大的空闲块保留在阵列的顶部。。
  2. 图中B 显示删除其中一项任务后的阵列。数组顶部的大空闲块仍然保留。现在还有两个较小的空闲块,这两个空闲块是先前已分配给TCB和已删除任务的堆栈。这两个空闲块在heap_2中不会融合在一起,还是两个空闲块
  3. 图中C 显示了创建另一个任务后的情况。创建任务导致对pvPortMalloc()的两次调用,一次调用分配新的TCB,一次调用分配任务堆栈。使用xTaskCreate()API函数创建任务。对任务堆栈的大小是在xTaskCreate()函数内部实现的。
    其中,每个TCB的大小都完全相同,因此最佳匹配算法可确保先前分配给已删除任务的TCB的RAM块被重新使用,以分配新任务的TCB。

如果分配给新创建的任务的堆栈大小与分配给先前删除的任务的堆栈大小相同,那么,最佳拟合算法可确保将先前分配给已删除任务的堆栈的RAM块重新用于分配新任务的堆栈。其阵列顶部较大的未分配块保持不变。

特性:

Heap_2不是确定性的,但比malloc()和free()的大多数标准库实现要快。

Heap_3

Heap_3.c使用标准库malloc()和free()函数,因此堆的大小由链接器配置定义,并且对configTOTAL_HEAP_SIZE设置,对内存大小没有影响。

Heap_3通过暂时挂起FreeRTOS调度程序来使malloc()和free()线程安全。

Heap_4

和heap_1和heap_2一样,heap_4通过将数组细分为较小的块来工作。与以前一样,该数组是静态声明的,并由configTOTAL_HEAP_SIZE确定其大小,因此即使在实际上未从该数组分配任何内存之前,该应用程序仍会占用大量RAM。
Heap_4使用第一个适合算法来分配内存。与heap_2不同,heap_4将相邻的空闲内存块合并(合并)为单个较大的块,从而最大程度地减少了内存碎片的风险。
第一种适合算法可确保pvPortMalloc()使用足够大的第一个可用内存块来容纳请求的字节数。例如,以下情形:

  1. 堆包含三个可用内存块,按照它们在阵列中出现的顺序,分别是5字节,200字节和100字节。
  2. 调用pvPortMalloc()以请求20个字节的RAM。
  3. 所请求的字节数将适合的RAM的第一个空闲块是200字节的块,因此pvPortMalloc()将200字节的块分成一个20字节的块和一个180字节的块,然后返回指向20字节块的指针。新的180字节块仍可用于以后对pvPortMalloc()的调用。

Heap_4将相邻的空闲块合并(合并)为一个较大的块,从而最大程度地减少了分段的风险,使其适合重复分配和释放不同大小的RAM块的应用程序。
在这里插入图片描述如上图所示:演示了具有内存合并功能的heap_4以及首次适合算法在分配和释放内存时的工作过程。

  1. A 显示了创建三个任务后的阵列。大的空闲块保留在阵列的顶部。
  2. B 显示删除其中一项任务后的阵列。数组顶部的大空闲块仍然保留。还有一个空闲块,其中的TCB和堆栈是之前已分配已删除的任务。请注意,与演示heap_2时不同,在删除TCB时释放的内存和在删除堆栈时释放的内存不会保留为两个单独的空闲块,而是合并在一起以创建更大的单个空闲块。
  3. C 显示了创建FreeRTOS队列后的情况。使用xQueueCreate()API函数创建队列,如第4.3 节所述。xQueueCreate()调用pvPortMalloc()分配队列使用的RAM。由于heap_4使用第一种适合算法,因此pvPortMalloc()将从第一个可用RAM块分配RAM,该块足够大以容纳队列,在图中,该队列是删除任务时释放的RAM。但是,队列不会占用空闲块中的所有RAM,因此该块被分为两部分,未使用的部分仍可用于以后对pvPortMalloc()的调用。
    4.D 显示了直接从应用程序代码而不是通过调用FreeRTOS API函数间接调用pvPortMalloc()之后的情况。用户分配的块足够小以适合第一个空闲块,第一个空闲块是分配给队列的内存和分配给后续TCB的内存之间的块。删除任务时释放的内存现在已分为三个单独的块;第一个块保存队列,第二个块保存用户分配的内存,第三个块保持空闲。
  4. E 显示删除队列后的情况,这将自动释放已分配给已删除队列的内存。现在,用户分配的块的任一侧都有可用内存。
  5. F 显示用户分配的内存也已释放后的情况。用户分配的块已使用的内存已与任一侧的空闲内存合并,以创建更大的单个空闲块。
特性:

Heap_4不是确定性的,但是比malloc()和free()的大多数标准库实现要快。

设置使用Heap_4

有时,应用程序编写者有必要将heap_4使用的数组放在特定的内存地址。例如,FreeRTOS任务使用的堆栈是从堆中分配的,因此可能有必要确保堆位于快速内部存储器中,而不是在慢速外部存储器中。
默认情况下,heap_4使用的数组是在heap_4.c源文件中声明的,并且其起始地址由链接器自动设置。但是,如果在FreeRTOSConfig.h中将configAPPLICATION_ALLOCATED_HEAP编译时配置常量设置为1,则必须由使用FreeRTOS的应用程序声明该数组。如果将数组声明为应用程序的一部分,则应用程序的编写器可以设置其起始地址。
如果在FreeRTOSConfig.h中将configAPPLICATION_ALLOCATED_HEAP设置为1,则必须在应用程序的源文件之一中声明一个名为ucHeap的uint8_t数组,并通过configTOTAL_HEAP_SIZE设置对其进行尺寸设置。
将变量放置到特定内存地址所需的语法取决于所使用的编译器,因此请参考编译器的文档。以下是两个编译器的示例:
uint8_t ucHeap[configToTAL_HEAP_SIZE] __attribute__ ((section(".my_heap")));
如上所示:显示了GCC编译器声明该数组并将其放置在名为.my_heap的内存部分中所需的语法。

Heap_5

heap_5用于分配和释放内存的算法与heap_4使用的算法相同。与heap_4不同,heap_5不限于从单个静态声明的数组分配内存;heap_5可以从多个单独的内存空间分配内存。

适用场景:

Heap_5是当运行FreeRTOS的系统提供的RAM在系统的内存映射中没有显示为单个连续的(无空间)块时,此选项很有用。

使用:

在使用heap_5时,必须在调用pvPortMalloc()之前显式初始化的内存分配方案。使用vPortDefineHeapRegions()API函数初始化Heap_5。使用heap_5时,必须先调用vPortDefineHeapRegions(),然后才能创建任何内核对象(任务,队列,信号量等)。
vPortDefineHeapRegions()API 函数简介
vPortDefineHeapRegions()用于指定每个单独的内存区域的起始地址和大小,这些区域共同构成了heap_5使用的总内存。
vPortDefineHeapRegions()API函数原型
void vPortDefineHeapRegions(const HeapRegion_t * const pxHeapRegions);
每个单独的存储区均由HeapRegion_t类型的结构描述。所有可用内存区域的描述作为HeapRegion_t结构的数组传递到vPortDefineHeapRegions()中。如下:

typedef struct HeapRegion

{

	/ *将成为堆一部分的内存块的起始地址。* /
	uint8_t * pucStartAddress;
	/ *内存块的大小(以字节为单位)。* /
	size_t xSizeInBytes;
} HeapRegion_t;

vPortDefineHeapRegions()参数
在这里插入图片描述通过示例的方式,考虑图中所示的假想的存储器映射A,其中包含的RAM三个独立的块:RAM1,RAM2和RAM3。假定可执行代码被放置在未示出的只读存储器中。在这里插入图片描述

显示了一个HeapRegion_t结构的数组,这些结构一起完整描述了三个RAM块。

/ *定义三个RAM区域的起始地址和大小。* /

#define RAM1_START_ADDRESS((uint8_t *)0x00010000)
#define RAM1_SIZE(65 * 1024)

#define RAM2_START_ADDRESS((uint8_t *)0x00020000)
#define RAM2_SIZE(32 * 1024)

#define RAM3_START_ADDRESS((uint8_t *)0x00030000)
#define RAM3_SIZE(32 * 1024)

/ *创建一个HeapRegion_t定义的数组,为三个RAM区域中的每个区域创建一个索引,并以NULL地址终止该数组。HeapRegion_t结构必须按起始地址顺序出现,包含最低起始地址的结构首先出现。* /

const HeapRegion_t xHeapRegions [] =
{
	{RAM1_START_ADDRESS,RAM1_SIZE},
	{RAM2_START_ADDRESS,RAM2_SIZE},
	{RAM3_START_ADDRESS,RAM3_SIZE},
	{NULL0} / *标记数组的结尾。* /
};
int main(void{
	/ *初始化heap_5。* /
	vPortDefineHeapRegions(xHeapRegions);
	/ *在此处添加应用程序代码。* /
}

尽管上述代码中,正确地描述了RAM,但是它并没有演示一个可用的示例,因为它将所有RAM分配给了堆,而没有RAM可供其他变量使用。
在构建项目时,构建过程的链接阶段会为每个变量分配一个RAM地址。链接器可使用的RAM通常由链接器配置文件(例如链接器脚本)描述。
在图中 B假定在RAM1链接脚本包含的信息,但不包括在RAM2或RAM3的信息。因此,链接器已将变量放置在RAM1中,仅RAM0的地址0x0001nnnn上方的部分可用于heap_5。实际值0x0001nnnn将取决于所链接的应用程序中包含的所有变量的总大小。链接器将所有RAM2和所有RAM3保留为未使用状态,而剩下的整个RAM2和整个RAM3可供heap_5使用。

如果使用清单6中所示的代码,则分配给地址0x0001nnnn下的heap_5的RAM将与用于保存变量的RAM重叠。为避免这种情况,xHeapRegions []数组中的第一个HeapRegion_t结构可以使用0x0001nnnn的起始地址,而不是0x00010000的起始地址。但是,这不是推荐的解决方案,因为:

  1. 起始地址可能不容易确定。
  2. 链接器使用的RAM数量在以后的版本中可能会更改,因此必须更新HeapRegion_t结构中使用的起始地址。
  3. 如果链接器使用的RAM和heap_5使用的RAM重叠,则生成工具将不知道,因此无法警告应用程序编写器。

在下面的程序示例中,展示了一个更方便和可维护的示例。它声明了一个称为ucHeap的数组。ucHeap是一个普通变量,因此它成为链接器分配给RAM1的数据的一部分。xHeapRegions数组中的第一个HeapRegion_t结构描述了ucHeap的起始地址和大小,因此ucHeap成为了heap_5管理的内存的一部分。可以增加ucHeap的大小,直到链接器使用的RAM耗尽了所有RAM1,如图 C所示。

/ *定义链接器未使用的两个RAM区域的起始地址和大小。* /

#define RAM2_START_ADDRESS((uint8_t *)0x00020000)
#define RAM2_SIZE(32 * 1024)

#define RAM3_START_ADDRESS((uint8_t *)0x00030000)
#define RAM3_SIZE(32 * 1024)

/ *声明一个数组,该数组将成为heap_5使用的堆的一部分。链接器会将阵列放置在RAM1中。* /
#define RAM1_HEAP_SIZE(30 * 1024)静态uint8_t ucHeap [RAM1_HEAP_SIZE];
/ *创建一个HeapRegion_t定义数组。清单6中的第一个条目描述了所有RAM1,因此heap_5将使用所有RAM1,这次,第一个条目仅描述了ucHeap数组,因此heap_5将仅使用RAM1中包含ucHeap数组的部分。HeapRegion_t结构必须仍然按起始地址顺序出现,包含最低起始地址的结构首先出现。* /

const HeapRegion_t xHeapRegions [] =
{
	{ucHeap,RAM1_HEAP_SIZE},
	{RAM2_START_ADDRESS,RAM2_SIZE},
	{RAM3_START_ADDRESS,RAM3_SIZE},
	{NULL0} / *标记数组的结尾。* /
};

上述代码中,展示的技术的优点包括:

  1. 不必使用硬编码的起始地址。
  2. 链接器将自动设置HeapRegion_t结构中使用的地址,因此,即使链接器使用的RAM数量在以后的版本中发生变化,该地址也始终是正确的。
  3. 分配给heap_5的RAM不可能与链接器放置在RAM1中的数据重叠。
  4. 如果ucHeap太大,则应用程序将不会链接。

堆相关函数
xPortGetFreeHeapSize()API 函数
xPortGetFreeHeapSize()API函数返回调用该函数时堆中的可用字节数。它可用于优化堆大小。
如:如果在创建所有内核对象之后xPortGetFreeHeapSize()返回2000,则configTOTAL_HEAP_SIZE的值可以减少2000。

xPortGetFreeHeapSize()API 函数的限制
不能在使用heap_3中使用。

xPortGetFreeHeapSize()API函数原型
size_t xPortGetFreeHeapSize(void)
在这里插入图片描述
xPortGetMinimumEverFreeHeapSize()API 函数
xPortGetMinimumEverFreeHeapSize()API函数返回自FreeRTOS应用程序开始执行以来堆中已存在的未分配字节的最小数量。

xPortGetMinimumEverFreeHeapSize()返回的值指示应用程序距离堆空间耗尽有多接近。
如:如果xPortGetMinimumEverFreeHeapSize()返回200,则自从应用程序开始执行以来的某个时间,它能使用的堆空间大小在不足堆空间的200个字节之内。

xPortGetMinimumEverFreeHeapSize()API 函数的适用示例
xPortGetMinimumEverFreeHeapSize()仅在使用heap_4或heap_5时可用。

xPortGetMinimumEverFreeHeapSize()API函数原型
size_t xPortGetMinimumEverFreeHeapSize(void)
在这里插入图片描述

malloc的失败钩子函数

  1. 可以直接从应用程序代码中调用pvPortMalloc()。每次创建内核对象时,它在FreeRTOS源文件中也被调用。内核对象的示例包括任务,队列,信号量和事件组,
  2. 就像标准库malloc()函数一样,如果pvPortMalloc()由于请求的大小的块不存在而无法返回RAM块,则它将返回NULL。如果由于应用程序编写者正在创建内核对象而执行pvPortMalloc(),并且对pvPortMalloc()的调用返回NULL,则不会创建内核对象。
  3. 如果对pvPortMalloc()的调用返回NULL,则可以将所有示例堆分配方案配置为调用钩子(或回调)函数。
  4. 如果configUSE_MALLOC_FAILED_HOOK在FreeRTOSConfig.h中设置为1,则应用程序必须提供一个分配失败的钩子函数,该函数的名称和原型如下代码所示。
  5. 可以通过适合该应用程序的任何方式来实现该功能。

malloc失败的钩子函数名称和原型。
vApplicationMallocFailedHook(void);

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值