FreeRTOS中不同版本Heap的区别以及适用情况

2025-09-26 03:10:37 2014德国世界杯

一.在freeRTOS中heap是用来干嘛的?

heap(堆)是操作系统用于动态内存分配的一块内存区域。在嵌入式系统开发中,动态内存管理是非常重要的一部分,它允许程序在运行时根据需要分配和释放内存资源。

二.为什么要自己实现内存管理?

为了让FreeRTOS更容易使用,task、 queue、 semaphores和event group等内核对象一般都是动态分配:用到时分配,不使用时释放。 使用内存的动态管理功能,简化了程序设计:不再需要小心翼翼地提前规划各类对象,简化API函数的涉及,甚至可以减少内存的使用。

内存的动态管理是C程序的知识范畴,并不属于FreeRTOS的知识范畴,但是它跟FreeRTOS关系是如此紧密。

在C语言的库函数中,有mallc、 free等函数,但是在FreeRTOS中,它们不适用:

⚫ 不适合用在资源紧缺的嵌入式系统中

⚫ 这些函数的实现过于复杂、占据的代码空间太大

⚫ 并非线程安全的(thread-safe)

⚫ 运行有不确定性:每次调用这些函数时花费的时间可能都不相同

⚫ 内存碎片化

⚫ 使用不同的编译器时,需要进行复杂的配置

⚫ 有时候难以调试

三. FreeRTOS 的 5 中内存管理方法

选择哪种堆管理方案取决于具体的应用需求和目标平台的特性。FreeRTOS中内存管理的接口函数为: pvPortMalloc 、 vPortFree,对应于C库的malloc、free。

1.Heap_1

它只实现了pvPortMalloc,没有实现vPortFree。

如果你的程序不需要删除内核对象,那么可以使用heap_1:

实现最简单没有碎片问题 一些要求非常严格的系统里,不允许使用动态内存,就可以使用 heap_1

FreeRTOS在创建任务时,需要2个内核对象: task control block(TCB)、 stack。使用heap_1时,内存分配过程如下图所示:

⚫ A:创建任务之前整个数组都是空闲的

⚫ B:创建第 1 个任务之后,蓝色区域被分配出去了

⚫ C:创建 3 个任务之后的数组使用情况

2.Heap_2

Heap_2之所以还保留,只是为了兼容以前的代码。新设计中不再推荐使用Heap_2。 建议:使用Heap_4来替代Heap_2,更加高效。

Heap_2也是在数组上分配内存,跟Heap_1不一样的地方在于:

Heap_2 使用最佳匹配算法(best fit)来分配内存 它支持 vPortFree最佳匹配算法:

假设 heap 有 3 块空闲内存: 5 字节、 25 字节、 100 字节 pvPortMalloc 想申请 20 字节 找出最小的、能满足 pvPortMalloc 的内存: 25 字节 把它划分为 20 字节、 5 字节 ◼ 返回这 20 字节的地址

◼ 剩下的 5 字节仍然是空闲状态,留给后续的 pvPortMalloc 使用

注意:Heap_2不会合并相邻的空闲内存,所以Heap_2会导致严重的"碎片化"问题。如果申请、分配内存时大小总是相同的,这类场景下Heap_2没有碎片化的问题。 适用场景:频繁地创建、删除任务,但是任务的栈大小都是相同的(创建任务时,需要分配TCB和栈, TCB总是一样的)。

使用heap_2时,内存分配过程如下图所示:

⚫ A:创建了 3 个任务

⚫ B:删除了一个任务,空闲内存有 3 部分:顶层的、被删除任务的 TCB 空

间、被删除任务的 Stack 空间

⚫ C:创建了一个新任务,因为 TCB、栈大小跟前面被删除任务的 TCB、栈大

小一致,所以刚好分配到原来的内存

3.Heap_3

Heap_3 使用标准 C 库里的 malloc、 free 函数,所以堆大小由链接器的配置决定,配置

项 configTOTAL_HEAP_SIZE 不再起作用。

C库里的malloc、 free函数并非线程安全的, Heap_3中先暂停FreeRTOS的调度器,再去

调用这些函数,使用这种方法实现了线程安全。

4.Heap_4

跟 Heap_1、 Heap_2 一样, Heap_4 也是使用大数组来分配内存。

Heap_4使用首次适应算法(first fit)来分配内存。它还会把相邻的空闲内存合并为一个更

大的空闲内存,这有助于较少内存的碎片问题。

首次适应算法:

假设堆中有 3 块空闲内存: 5 字节、 200 字节、 100 字节 pvPortMalloc 想申请 20 字节 找出第 1 个能满足 pvPortMalloc 的内存: 200 字节 把它划分为 20 字节、 180 字节 返回这 20 字节的地址 剩下的 180 字节仍然是空闲状态,留给后续的 pvPortMalloc 使用 Heap_4会把相邻空闲内存合并为一个大的空闲内存,可以较少内存的碎片化问题。适用

场景:频繁地分配、释放不同大小的内存。

Heap_4的使用过程举例如下:

⚫ A:创建了 3 个任务

⚫ B:删除了一个任务,空闲内存有 2 部分:

(1) 顶层的

(2) 被删除任务的 TCB 空间、被删除任务的 Stack 空间合并起来的

⚫ C:分配了一个 Queue,从第 1 个空闲块中分配空间

⚫ D:分配了一个 User 数据,从 Queue 之后的空闲块中分配

⚫ E:释放的 Queue, User 前后都有一块空闲内存

⚫ F:释放了 User 数据, User 前后的内存、 User 本身占据的内存, 合并为一个

大的空闲内存

5.Heap_5

Heap_5 分配内存、释放内存的算法跟 Heap_4 是一样的。

相比于Heap_4, Heap_5并不局限于管理一个大数组:它可以管理多块、分隔开的内存。

在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用Heap_5。

既然内存时分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:

⚫ 在使用 pvPortMalloc 之前,必须先指定内存块的信息

⚫ 使用 vPortDefineHeapRegions 来指定这些信息

怎么指定一块内存?使用如下结构体:

typedef struct HeapRegion

{

uint8_t * pucStartAddress; // 起始地址

size_t xSizeInBytes; // 大小

} HeapRegion_t;

怎么指定多块内存?使用一个HeapRegion_t数组,在这个数组中,低地址在前、高地址在后。

比如:

HeapRegion_t xHeapRegions[] =

{

{ ( uint8_t * ) 0x80000000UL, 0x10000 }, // 起始地址0x80000000,大小0x10000

{ ( uint8_t * ) 0x90000000UL, 0xa0000 }, // 起始地址0x90000000,大小0xa0000

{ NULL, 0 } // 表示数组结束

};

vPortDefineHeapRegions函数原型如下:

void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );

把 xHeapRegions 数组传给 vPortDefineHeapRegions 函数,即可初始化 Heap_5。

最新发表
友情链接