嵌入式操作系统FreeRTOS内存管理
FreeRTOS的内存管理和堆的问题。从 V9.0.0 开始,FreeRTOS 应用程序可以完全静态分配,这意味着无需包含堆内存管理器。
https://www.freertos.org/a00111.html
嵌入式专栏
1
动态内存分配与FreeRTOS的关联性:
要让 FreeRTOS 对象(如任务、队列、信号量和事件组)变得尽可能易于使用,这些内核对象不是在编译时静态分配,而是在运行时动态分配。每次创建内核对象时,FreeRTOS 都会分配 RAM;
嵌入式专栏
2
嵌入式专栏
3
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。
A 显示创建任何任务之前的数组。整个数组都可用。
B 显示已创建一个任务后的数组。
C 显示已创建三个任务后的数组。
Heap_2
Heap_2 包含在 FreeRTOS 发行版中以保持向后兼容性。建议不要用于新设计,而是考虑使用 heap_4,因为其中提供了更多功能。
也可以使用 Heap_2.c,但要细分由 configTOTAL_HEAP_SIZE 确定大小的数组。它使用最适合算法分配内存。与 heap_1 不同,它允许释放内存。再次说明,数组是静态声明的,因此应用程序看起来会消耗大量RAM,甚至在从数组中分配任何内存之前就是如此。
最适合算法可确保 pvPortMalloc() 使用的可用内存块在大小方面与所要求的字节数最接近。例如,考虑以下情形:
适合所请求字节数的最小可用 RAM 块是 25 字节块,因此,pvPortMalloc() 将 25 字节块拆分成一个 20 字节块和一个 5 字节块,然后返回一个指向 20 字节块的指针。(上面是过于简化了,因为 heap_2 要存储堆区域中块大小的信息,因此两个拆分块的总和实际上将小于 25。) 新的 5 字节块保持可用于将来对pvPortMalloc() 的调用。
与 heap_4 不同,heap_2 不将相邻的可用块合并为单个更大的块。因此,它更容易碎片化。但是,如果分配的块与后续释放的块大小始终相同,则碎片化不是问题。Heap_2 适合反复创建和删除任务的应用程序,但前提是分配给所创建的任务的堆栈大小不发生变化。
下图显示在创建和删除任务时从 heap_2 数组中分配和释放 RAM 的过程。
图中显示当创建、删除以及后续再次创建任务时,最适合算法的工作原理。
· 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() 使用第一个大小足以容纳所要求的字节数的可用内存块。例如,考虑以下情形:
适合所请求字节数的第一个可用 RAM 块是 200 字节块,因此,pvPortMalloc() 将 200 字节块拆分成一个 20字节块和一个 180 字节块,然后返回一个指向 20 字节块的指针。(上面是过于简化了,因为 heap_4 要存储堆区域中块大小的信息,因此两个拆分块的总和将小于 200 字节。) 新的 180 字节块保持可用于将来对pvPortMalloc() 的调用。
Heap_4 会将相邻的可用内存块组合(合并)成一个较大的块,同时最大限度地降低碎片化风险。Heap_4 适用于反复分配和释放不同大小的 RAM 块的应用程序。
下图显示从 heap_4 数组中分配和释放 RAM 的过程。它演示 heap_4 首个适合算法(带内存合并)在分配和释放内存时的工作方式。
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(),然后才可创建任何内核对象(任务、队列、信号等)。
------------ END ------------