良许Linux教程网 Linux教程 嵌入式操作系统FreeRTOS内存管理

嵌入式操作系统FreeRTOS内存管理

FreeRTOS的内存管理和堆的问题是一个重要话题。从V9.0.0版本开始,FreeRTOS应用程序可以完全静态分配,这意味着没有必要包含堆内存管理器。

关于FreeRTOS内存管理的详细信息可以参考以下链接:
FreeRTOS内存管理地址

动态内存分配与FreeRTOS的关联性:

为了让FreeRTOS的对象(如任务、队列、信号量和事件组)更易于使用,这些内核对象在编译时不是静态分配的,而是在运行时进行动态分配。每次创建内核对象时,FreeRTOS会分配RAM;每次删除内核对象时,RAM则被释放。这种策略减少了设计和规划的工作量,简化了API,并最大程度地减少了RAM的使用。

动态内存分配是一个涉及C语言编程的概念,并非仅适用于FreeRTOS或多任务处理。它与FreeRTOS相关,因为内核对象是通过动态分配来实现的。尽管通用编译器提供了动态内存分配方案,但并不总是适合实时应用程序。

可以使用标准 C 语言库函数 malloc() 和 free() 分配内存,但由于以下一个或多个原因,这些函数可能不适合或不适用:

它们并非始终适用于小型嵌入式系统。

****•****它们的实现可能相当大,占用宝贵的代码空间。

****•****它们极少是线程安全的。

****•****它们不是确定性的。执行函数所花的时间量将因调用不同而异。

****•****它们可能会碎片化。如果堆中的可用 RAM 被拆分为若干较小且彼此分离的块,则认为堆已碎片化。当堆已碎片化时,如果堆中没有一个可用块的大小足以包含某个数据块,则尝试分配此数据块时将失败,即使堆中所有单独块的总大小比无法分配的数据块大小大很多倍,也是如此。

****•****它们可能使链接器配置变得更复杂。

如果允许堆空间增长而占用了其他变量使用的内存,则这些函数可能成为一些难以调试的错误的来源。

动态内存分配的选项

早期版本的 FreeRTOS 使用内存池分配方案,在编译时预分配由不同大小的内存块组成的池,然后由内存分配函数返回。虽然这是实时系统中常用的方案,但它产生了大量支持请求。由于该方案使用 RAM 的效率无法满足非常小型的嵌入式系统的需要,因此已被放弃。

FreeRTOS 现在将内存分配视为可移植层的一部分(而不是核心代码库的一部分)。这样做的原因是我们已认识到嵌入式系统具有变化多端的动态内存分配和计时要求。单个动态内存分配算法仅适用于一部分应用程序。此外,通过从核心代码库中删除动态内存分配,应用程序编写者可以适时提供其自己的特定实现。

当 FreeRTOS 需要 RAM 时,它调用 pvPortMalloc() 而不是 malloc()。释放 RAM 时,内核调用 vPortFree(),而不是 free()。pvPortMalloc() 与标准 C 语言库函数 malloc() 具有相同的原型。vPortFree() 与标准 C 语言库函数 free() 具有相同的原型。

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

FreeRTOS 附带了 pvPortMalloc() 和 vPortFree() 的五个示例实现,本指南将一一介绍。FreeRTOS 应用程序可以使用其中一个示例实现或提供自己的实现。

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

FreeRTOS现有5种内存分配

heap_1:最简单,不允许释放内存。

heap_2:允许释放内存,但不能合并相邻的空闲块。

heap_3:简单包装标准的malloc()和free()以确保线程安全。

heap_4:合并相邻的空闲块以避免碎片。包括绝对地址放置选项。

heap_5:按照heap_4,具有跨多个不相邻的内存区域扩展堆的能力。

Heap_1

它常用于小型专用嵌入式系统,以便仅在启动计划程序之前创建任务和其他内核对象。在应用程序开始执行任何实时功能之前,内核动态分配内存,并且内存在应用程序的生命周期内保持已分配状态。这意味着所选分配方案不必考虑任何更复杂的内存分配问题(例如确定性和碎片化),而是可以考虑如代码大小和简单性等属性。

Heap_1.c 实现 pvPortMalloc() 的一个非常基本的版本。它不实现 vPortFree()。从不删除任务或其他内核对象的应用程序可以使用 heap_1。

某些商业关键和安全关键型系统可能禁止动态内存分配,这些系统也可能能够使用 heap_1。由于存在与非确定性、内存碎片化以及分配失败等相关的不确定性,因此,关键系统通常禁止动态内存分配,但 heap_1 始终是确定性的且无法对内存进行碎片化。

当调用 pvPortMalloc() 时,heap_1 分配方案将一个简单的数组细分成更小的块。此数组称为 FreeRTOS堆。

数组的总大小(以字节为单位)由定义 configTOTAL_HEAP_SIZE 在 FreeRTOSConfig.h 中设置。以这种方式定义大型数组可能让应用程序看起来会消耗大量 RAM,甚至从数组中分配任何内存之前就是如此。

每个创建的任务都要求从堆中分配一个任务控制块 (TCB) 和一个堆栈。

下图显示了在创建任务时 heap_1 如何细分简单的数组。每次创建任务时,都会从 heap_1 数组分配 RAM。

image-20230814211112854
image-20230814211112854

A 显示创建任何任务之前的数组。整个数组都可用。

B 显示已创建一个任务后的数组。

C 显示已创建三个任务后的数组。

Heap_2

Heap_2 包含在 FreeRTOS 发行版中以保持向后兼容性。建议不要用于新设计,而是考虑使用 heap_4,因为其中提供了更多功能。

也可以使用 Heap_2.c,但要细分由 configTOTAL_HEAP_SIZE 确定大小的数组。它使用最适合算法分配内存。与 heap_1 不同,它允许释放内存。再次说明,数组是静态声明的,因此应用程序看起来会消耗大量RAM,甚至在从数组中分配任何内存之前就是如此。

最适合算法可确保 pvPortMalloc() 使用的可用内存块在大小方面与所要求的字节数最接近。例如,考虑以下情形:

• 堆包含三个可用内存块,大小分别为 5 字节、25 字节和 100 字节。

• 调用 pvPortMalloc() 以请求 20 字节的 RAM。

适合所请求字节数的最小可用 RAM 块是 25 字节块,因此,pvPortMalloc() 将 25 字节块拆分成一个 20 字节块和一个 5 字节块,然后返回一个指向 20 字节块的指针。(上面是过于简化了,因为 heap_2 要存储堆区域中块大小的信息,因此两个拆分块的总和实际上将小于 25。) 新的 5 字节块保持可用于将来对pvPortMalloc() 的调用。

与 heap_4 不同,heap_2 不将相邻的可用块合并为单个更大的块。因此,它更容易碎片化。但是,如果分配的块与后续释放的块大小始终相同,则碎片化不是问题。Heap_2 适合反复创建和删除任务的应用程序,但前提是分配给所创建的任务的堆栈大小不发生变化。

下图显示在创建和删除任务时从 heap_2 数组中分配和释放 RAM 的过程。

image-20230814211109191
image-20230814211109191

图中显示当创建、删除以及后续再次创建任务时,最适合算法的工作原理。

• A 显示已创建三个任务后的数组。大型可用块保持在数组的顶部。

• B 显示已删除其中一个任务后的数组。这些区域有:

数组顶部的大型可用块保持原样。此外,目前有两个较小的可用块,它们之前分配给了已删除任务的TCB 和堆栈。

• C 显示已创建另一个任务后的数组。创建任务导致两次调用 pvPortMalloc():一次是分配新的 TCB,一次是分配任务堆栈。使用 xTaskCreate() API 函数创建任务,如创建任务 (p. 23)中所述。在 xTaskCreate() 内部发生两次 pvPortMalloc() 调用。

每个 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 不同,它会将相邻的可用内存块组合(合并)成一个较大的块。这样可以最大程度地减小内存碎片化风险。

首个适合算法可确保 pvPortMalloc() 使用第一个大小足以容纳所要求的字节数的可用内存块。例如,考虑以下情形:

• 堆包含三个可用内存块。它们在数组中以下面的顺序显示:5 字节、200 字节和 100 字节。

• 调用 pvPortMalloc() 以请求 20 字节的 RAM。

适合所请求字节数的第一个可用 RAM 块是 200 字节块,因此,pvPortMalloc() 将 200 字节块拆分成一个 20字节块和一个 180 字节块,然后返回一个指向 20 字节块的指针。(上面是过于简化了,因为 heap_4 要存储堆区域中块大小的信息,因此两个拆分块的总和将小于 200 字节。) 新的 180 字节块保持可用于将来对pvPortMalloc() 的调用。

Heap_4 会将相邻的可用内存块组合(合并)成一个较大的块,同时最大限度地降低碎片化风险。Heap_4 适用于反复分配和释放不同大小的 RAM 块的应用程序。

下图显示从 heap_4 数组中分配和释放 RAM 的过程。它演示 heap_4 首个适合算法(带内存合并)在分配和释放内存时的工作方式。

image-20230814211104050
image-20230814211104050

A 显示已创建三个任务后的数组。大型可用块保持在数组顶部。

B 显示已删除其中一个任务后的数组。

C 显示已创建一个 FreeRTOS 队列后的数组。

D 显示在从应用程序代码中直接调用 pvPortMalloc()(而不是通过间接调用 FreeRTOS API 函数)后的数组。

E 显示删除队列后的数组,此时会自动释放分配给已删除队列的内存。此时,用户分配的块的两侧都有可用内存。

F 显示的也是已释放用户分配的内存之后的数组。用户分配的块所用的内存已与两侧的可用内存组合成一个更大的可用块。

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

Heap_5

heap_5 用来分配和释放内存的算法与 heap_4 的完全相同。与 heap_4 不同,heap_5 不限于从单个静态声明的数组分配内存。Heap_5 可以从多个单独的内存空间分配内存。当运行 FreeRTOS 的系统提供的 RAM在系统的内存映射中未作为单个邻接的(没有空间)块出现时,Heap_5 很有用。

Heap_5 是唯一一个必须在调用 pvPortMalloc() 之前显式初始化的内存分配方案。它使用 vPortDefineHeapRegions() API 函数进行初始化。当使用 heap_5 时,必须先调用vPortDefineHeapRegions(),然后才可创建任何内核对象(任务、队列、信号等)。

以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !

137e00002230ad9f26e78-265x300
本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部