FreeRTOS 内核在创建任务、队列、信号量等对象时需要动态分配内存。但标准 C 库的 malloc()/free() 在嵌入式系统上有诸多问题(不可重入、实现不一定确定、可能产生碎片等),因此 FreeRTOS 将内存管理作为可移植层的一部分,提供了 5 种内存分配方案,位于 portable/MemMang/ 目录下。
你可以选择其中一种(也可以自己实现),在项目中编译对应的 heap_x.c 文件。
五种方案总览
| 方案 | 分配 | 释放 | 合并相邻空闲块 | 适用场景 |
|---|---|---|---|---|
| heap_1 | ✅ | ❌ 不支持 | N/A | 只创建不删除的系统(最简单安全) |
| heap_2 | ✅ | ✅ | ❌ 不合并 | ⚠️ 已被 heap_4 取代 |
| heap_3 | ✅ | ✅ | 取决于编译器 | 使用编译器自带的 malloc/free |
| heap_4 | ✅ | ✅ | ✅ 合并 | 最常用,反复创建删除的系统 |
| heap_5 | ✅ | ✅ | ✅ 合并(支持多内存区) | 内存不连续的系统 |
heap_1.c:只分配不释放
最简单的实现。预先定义一个大数组 ucHeap[configTOTAL_HEAP_SIZE] 作为堆空间,每次分配就从数组中切一块,指针往后挪。不支持 vPortFree()。
分配过程:
初始:|----------------------------------------------------------| ucHeap
↑
pucAlignedHeap
第一次分配(TCB):|███████|---------------------------------------------|
↑ ↑
start pucAlignedHeap(后移)
第二次分配(栈):|███████|████████|--------------------------------------|
↑ ↑
start pucAlignedHeap(继续后移)
适用场景:
- 系统启动时创建所有任务和内核对象,之后永远不删除
- 对安全性要求极高的系统(不存在碎片问题,行为完全确定)
- 大多数嵌入式产品实际上都属于这种模式
优点:代码最简单,执行时间确定,永远不会产生碎片。 缺点:不能释放内存。
heap_2.c:最佳适应(已废弃)
引入了空闲链表来管理空闲内存块,支持 vPortFree()。使用最佳适应算法(Best Fit):分配时找能满足要求的最小空闲块。
分配示例:
空闲链表中有三块:5字节、25字节、100字节
请求分配 20 字节 → 选择 25 字节块(最接近且够大)
→ 分割为 20 字节(分配出去)+ 5 字节(回收到空闲链表)
致命缺点:不会合并相邻空闲块。释放后如果相邻有空闲块,它们不会合并成大块,导致内存碎片化。比如分配 20 字节后释放,留下 5 字节小块,多次分配释放后会产生大量无法利用的小碎片。
已被 heap_4 取代
heap_2 保留仅为了向后兼容。新代码使用 heap_4。
heap_3.c:包装标准 malloc
对编译器标准库的 malloc() 和 free() 做了一层线程安全包装——通过临时挂起调度器保证 malloc/free 不会被并发调用。
void *pvPortMalloc(size_t xWantedSize) {
void *pvReturn;
vTaskSuspendAll(); // 挂起调度器
pvReturn = malloc(xWantedSize); // 调用标准库 malloc
xTaskResumeAll(); // 恢复调度器
return pvReturn;
}
特点:
- 不使用
configTOTAL_HEAP_SIZE,堆大小由链接器脚本决定 - 不定义
ucHeap数组 - 线程安全,但
malloc()/free()的执行时间通常不确定 - 取决于编译器提供的 malloc 实现是否适合嵌入式(有些会产生碎片)
适用场景:你信任编译器的 malloc 实现,或者需要和使用 malloc 的其他代码库共存。
heap_4.c:首次适应+合并(最常用)
和 heap_1/heap_2 一样使用静态数组 ucHeap[configTOTAL_HEAP_SIZE],但采用首次适应算法(First Fit),并且会合并相邻空闲块(coalescence),大幅减少碎片。
分配过程:
空闲块:5字节、200字节、100字节
请求 20 字节 → 从第一块开始扫描 → 第一个够大的是 200 字节块
→ 分割为 20 字节(分配)+ 180 字节(回收到空闲链表)
关键特性——释放时合并相邻块:
释放一个块时,检查它的"左邻"和"右舍"是否也是空闲块
如果是,就把它们合并成一个更大的连续块
例如释放 20 字节块后:
左边 5 字节空闲,右边 180 字节空闲
→ 合并成 5+20+180 = 205 字节的大块
这有效解决了 heap_2 的碎片化问题。只要总剩余空间足够,总能分配到较大的连续块。
适用场景:
- 需要反复创建/删除任务、队列、信号量等内核对象的系统
- 绝大多数有动态内存需求的应用
- 最推荐使用的方案
内存碎片问题
虽然 heap_4 会合并相邻块,但在某些分配/释放模式下仍可能产生碎片。例如:
分配 [A][B][C][D] → 释放 B 和 D(不相邻)→ 空闲块 [A][ ][C][ ]
B 和 D 之间隔着 C,无法合并。如果需要分配大于 B 或 D 的块,即使总剩余空间够大也分不出来。
减少碎片的方法:
- 尽量以**后进先出(LIFO)**的顺序分配/释放(后分配的先释放)
- 避免频繁分配/释放不同大小的块
- 一次性分配够大的
configTOTAL_HEAP_SIZE - 对于任务栈,考虑静态分配(
xTaskCreateStatic)
heap_5.c:支持非连续内存
和 heap_4 使用相同的分配/释放/合并算法,区别在于 heap_5 可以将多个不连续的内存区域合并成一个堆使用。
// 需要在调用任何内核分配函数之前,先初始化 heap_5:
HeapRegion_t xHeapRegions[] = {
{ (uint8_t *)0x20000000, 0x10000 }, // 内部 SRAM:64KB,起始地址 0x20000000
{ (uint8_t *)0x60000000, 0x8000 }, // 外部 SRAM:32KB,起始地址 0x60000000
{ NULL, 0 } // 数组终止标记
};
vPortDefineHeapRegions(xHeapRegions);
适用场景:
- 系统有多个 RAM 区域(如内部 SRAM + 外部 SDRAM/SRAM)
- 内存映射中非连续的系统
- 希望把大缓冲区放在外部 RAM 而内核对象放在内部 RAM
使用 heap_5 必须先调用 vPortDefineHeapRegions()
在初始化堆区域之前,不能调用任何会分配内存的 API(包括
xTaskCreate())。
堆大小配置:configTOTAL_HEAP_SIZE
对于 heap_1、heap_2、heap_4、heap_5,堆大小由 FreeRTOSConfig.h 中的 configTOTAL_HEAP_SIZE 决定(单位:字节):
#define configTOTAL_HEAP_SIZE ((size_t)(20 * 1024)) // 20KB 堆
如何确定堆大小?
FreeRTOS 提供了查询剩余堆空间的函数:
// 返回当前空闲堆内存大小(字节)
size_t xPortGetFreeHeapSize(void);
// 返回历史上最小的剩余堆空间(高水位标记)
size_t xPortGetMinimumEverFreeHeapSize(void); // heap_4 和 heap_5 支持
调优方法:
- 先设一个偏大的值(比如 10KB)
- 系统正常运行所有功能一段时间
- 调用
xPortGetMinimumEverFreeHeapSize()查看最低剩多少 - 最终设置 = 最低剩余 + 20%~30% 安全余量 + 实际使用量
内核对象内存占用参考
| 对象 | 大致内存占用(heap_4,Cortex-M) |
|---|---|
| 任务(含 TCB) | ~80 字节 + 栈大小(由 usStackDepth 决定) |
| 队列 | ~80 字节 + 队列长度 × 项目大小 |
| 二值/计数信号量 | ~80 字节(队列结构复用) |
| 互斥锁 | ~80 字节 + 少量额外字段 |
| 事件组 | ~36 字节 |
| 软件定时器 | ~60 字节/个 |
| 定时器命令队列 | ~80 字节 + configTIMER_QUEUE_LENGTH × 命令大小 |
内存分配失败钩子
// FreeRTOSConfig.h
#define configUSE_MALLOC_FAILED_HOOK 1
// 应用层必须提供这个函数
void vApplicationMallocFailedHook(void)
{
// pvPortMalloc() 返回 NULL 时被调用
// 可以在这里:记录错误、LED闪烁报警、系统重启等
taskDISABLE_INTERRUPTS();
for(;;); // 死循环,方便调试
}
选择建议
| 你的情况 | 选择 |
|---|---|
| 启动后创建所有对象,之后不删除 | heap_1(最安全确定) |
| 需要动态创建/删除对象 | heap_4(首选) |
| 有外部 RAM 或内存不连续 | heap_5 |
| 必须用编译器的 malloc(和第三方库兼容) | heap_3 |
| 新代码 | 不要用 heap_2 |
相关:02-任务管理与调度 | 10-FreeRTOSConfig配置 | 返回目录