2、heap_1源码深度解析:最简单的分配器,不支持释放,适用于任务创建等一次性分配场景
各位同学,今天我们来聊聊 FreeRTOS 里最“单纯”的内存分配器——heap_1。
说实话,我第一次接触 FreeRTOS 时,看到有 5 种 heap 方案,心里是有点懵的。但后来我明白了,简单有简单的好处。heap_1 就是那种“一招鲜,吃遍天”的典型代表。
2.1 heap_1 的核心思想:一根筋分配到底
heap_1 的设计哲学,说白了就是四个字:只分不收。
它把一大块静态数组当作内存池,每次调用 pvPortMalloc() 就从池子里切一块出去。切完就完了,不支持释放,也不支持碎片整理。
你可能会问:“不支持释放,那有什么用?”
嗯,这个问题问得好。我当年也有同样的疑惑。直到我在一个工业控制项目里,需要创建 10 个任务、3 个队列、2 个信号量,这些资源一旦创建好,整个生命周期都不会销毁。这时候 heap_1 简直就是天选之子。
- 任务创建(xTaskCreate)—— 一次性分配任务栈和TCB
- 队列创建(xQueueCreate)—— 队列控制块一次性分配
- 信号量/互斥量创建 —— 内核对象一次性分配
- 任何“分配后永不释放”的场景
2.2 源码解剖:不到 100 行的精华
heap_1 的实现代码非常短,我数过,核心逻辑不到 100 行。咱们直接看关键部分。
2.2.1 内存池的定义
/* 定义一个大数组作为堆内存 */
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
这里 configTOTAL_HEAP_SIZE 是在 FreeRTOSConfig.h 里定义的宏。我习惯把它设成 任务栈总大小的 1.5 倍,留点余量。
2.2.2 分配器的核心:pvPortMalloc
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn = NULL;
static uint8_t *pucAlignedHeap = NULL;
/* 对齐处理:确保返回地址按 8 字节对齐 */
#if ( portBYTE_ALIGNMENT != 1 )
if( xWantedSize & portBYTE_ALIGNMENT_MASK )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
#endif
vTaskSuspendAll();
{
if( pucAlignedHeap == NULL )
{
/* 第一次调用时,对齐堆起始地址 */
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
}
/* 检查剩余空间是否足够 */
if( ( xWantedSize > 0 ) && ( ( xNextFreeByte + xWantedSize ) < configTOTAL_HEAP_SIZE ) )
{
pvReturn = pucAlignedHeap + xNextFreeByte;
xNextFreeByte += xWantedSize;
}
}
xTaskResumeAll();
return pvReturn;
}
这段代码的逻辑非常直白:
- 对齐处理:把请求的大小向上对齐到 8 字节(或 4 字节,取决于架构)。
- 挂起调度器:防止分配过程中被任务切换打断。
- 检查剩余空间:如果够,就从当前空闲位置切一块出去。
- 更新空闲指针:
xNextFreeByte往前移动。 - 恢复调度器:完事。
vTaskSuspendAll() 来保证原子性。如果你在中断里调用 pvPortMalloc,会出大问题。我曾经在一个项目中,不小心在定时器中断里调用了任务创建函数,结果 heap 指针乱飞,系统直接挂掉。排查了整整两天才找到原因。
2.2.3 释放函数:vPortFree
void vPortFree( void *pv )
{
/* 啥也不做 */
( void ) pv;
}
你没看错,vPortFree 就是一个空函数。它存在的唯一意义,就是让编译器别报“未定义函数”的警告。
所以,如果你在代码里调用了 vPortFree,它不会报错,但也不会释放内存。嗯,这其实是个陷阱。我建议你在项目初期就明确告诉团队:用了 heap_1,就别想着释放。
2.3 性能分析:快,是真的快
| 指标 | heap_1 | 备注 |
|---|---|---|
| 分配时间复杂度 | O(1) | 固定几条指令,极快 |
| 释放时间复杂度 | 不支持 | 无 |
| 内存碎片 | 无 | 线性分配,永不产生碎片 |
| 内存利用率 | 高 | 仅浪费对齐填充的字节 |
| 确定性 | 完全确定 | 每次分配时间固定 |
你看这个表格,heap_1 在确定性和速度上都是满分。对于实时系统来说,这太重要了。
2.4 避坑指南:我曾经踩过的坑
- 坑一:堆大小设得太小
我曾经在一个项目中,把configTOTAL_HEAP_SIZE设成 2KB,结果任务创建到第 5 个就失败了。后来一算,每个任务栈至少 512 字节,加上 TCB 开销,2KB 根本不够。建议先估算所有任务栈总和,再乘以 1.5。 - 坑二:在中断里分配内存
前面说了,heap_1 依赖vTaskSuspendAll()来保护临界区。中断里不能挂起调度器,所以千万别在中断里调用pvPortMalloc。我那次血的教训就是:定时器中断里创建了一个任务,结果系统直接 hard fault。 - 坑三:误以为可以释放
有些新手看到vPortFree存在,就以为可以释放。结果内存越用越少,最后系统崩溃。我的建议是:在代码注释里明确写上“此项目使用 heap_1,禁止调用 vPortFree”。
2.5 知识体系:一张图看懂 heap_1
下面我用一张 SVG 图,把 heap_1 的整个工作流程画出来。你看完就明白了。
这张图很直观:分配就是指针往前挪,释放就是啥也不干。所以 heap_1 的内存使用量只会单调递增,永远不会减少。
2.6 什么时候选 heap_1?
我个人的判断标准很简单:
- 选 heap_1:如果你的系统里,所有动态内存都在初始化阶段分配完,之后再也不动。比如:创建固定数量的任务、队列、信号量。
- 不选 heap_1:如果运行过程中需要释放内存(比如动态加载/卸载模块),或者不确定内存使用模式,那就选 heap_4 或 heap_5。
好了,heap_1 就讲到这里。它虽然简单,但用对了地方,就是一把利器。记住:简单不等于弱,合适才是王道。
公众号:蓝海资料掘金营,微信deep3321