FreeRTOS五种内存分配方案对比:heap_1到heap_5,选错了就是内存泄漏
嵌入式系统崩在哪里?十有八九不是算法错了,是内存漏了。FreeRTOS把内存管理的选择权交给了开发者——五种heap方案,从"只分不收"到"多段合并",选对了系统稳如磐石,选错了就是慢性内存泄漏,三个月后必死机。
heap_1:最极端的确定性——只分配,不释放。 它的原理简单到近乎粗暴:一块静态数组ucHeap[configTOTAL_HEAP_SIZE],一个指针xNextFreeByte,每次分配就把指针往后挪,永远不回头。没有空闲链表,没有碎片合并,甚至没有vPortFree()函数——你敢调用,断言直接触发。
static uint8_t ucHeap[configTOTAL_HEAP_SIZE];
static size_t xNextFreeByte = 0;
void *pvPortMalloc(size_t xWantedSize) {
void *pvReturn = NULL;
if ((xNextFreeByte + xWantedSize) <= configTOTAL_HEAP_SIZE) {
pvReturn = &ucHeap[xNextFreeByte];
xNextFreeByte += xWantedSize;
}
return pvReturn;
}
void vPortFree(void *pv) { /* 空实现,永远不会被调用 */ }
这方案零碎片、零开销、零不确定性。代价是所有任务、队列、信号量必须在启动时一次创建完毕,运行期间一个都不能删。安全关键系统、DO-178B认证场景,heap_1是唯一答案。
heap_2:能收不能合——碎片的温床。 它引入了空闲链表和最佳适配算法(Best Fit),每次分配遍历整个链表,找大小最接近的块。支持释放了,但释放时只把块插回链表,绝不合并相邻空闲块。
typedef struct A_BLOCK_LINK {
struct A_BLOCK_LINK *pxNextFreeBlock;
size_t xBlockSize;
} BlockLink_t;
void *pvPortMalloc(size_t xWantedSize) {
BlockLink_t *pxBlock = xStart.pxNextFreeBlock;
while (pxBlock->xBlockSize < xWantedSize) {
pxBlock = pxBlock->pxNextFreeBlock;
}
// 分割块,剩余部分留在链表中
if (pxBlock->xBlockSize > xWantedSize + heapSTRUCT_SIZE) {
BlockLink_t *pxNewBlock = (BlockLink_t *)((uint8_t *)pxBlock + xWantedSize);
pxNewBlock->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
prvInsertBlockIntoFreeList(pxNewBlock);
}
pxBlock->pxNextFreeBlock = NULL;
return (void *)(pxBlock + 1);
}
反复分配释放不同大小的块,链表里会塞满碎片小块。总空闲量可能还有5KB,但就是分不出连续3KB——分配失败,系统卡死。老项目偶尔还能见到它,新项目请绕道。
heap_3:穿了马甲的malloc——最不推荐。 整个实现就两行:pvPortMalloc调用malloc,vPortFree调用free,外加挂起调度器做线程保护。不受configTOTAL_HEAP_SIZE约束,由链接器决定堆大小。但标准库的malloc不是为实时系统设计的——分配时间不可预测,碎片控制靠运气,内核监控函数xPortGetFreeHeapSize()永远返回0。除非你在Linux上跑FreeRTOS模拟环境,否则别碰它。
heap_4:工程界的最优解——首次适配加自动合并。 这是当前量产项目的默认选择。分配时采用首次适配算法(First Fit),找到第一个够大的块就用,不遍历全部链表,速度快。释放时才是精髓:插入空闲链表的瞬间,检查前后相邻块是否空闲,若是则立即合并为一整块。
void vPortFree(void *pv) {
BlockLink_t *pxBlock = (BlockLink_t *)pv - 1;
pxBlock->xBlockSize |= 0x01; // 清除分配标志
prvInsertBlockIntoFreeList(pxBlock); // 插入时自动合并前后空闲块
}
static void prvInsertBlockIntoFreeList(BlockLink_t *pxBlockToInsert) {
BlockLink_t *pxIterator = &xStart;
while (pxIterator->pxNextFreeBlock < pxBlockToInsert) {
pxIterator = pxIterator->pxNextFreeBlock;
}
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
pxIterator->pxNextFreeBlock = pxBlockToInsert;
// 合并前块
if ((void *)(pxBlockToInsert + 1) + pxBlockToInsert->xBlockSize == (void *)pxIterator) {
pxBlockToInsert->xBlockSize += pxIterator->xBlockSize;
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
}
// 合并后块(略)
}
所有分配释放操作在临界区内完成(挂起调度器而非关中断),兼顾实时性与并发安全。xPortGetMinimumEverFreeHeapSize()能监控历史最低水位,内存泄漏无处遁形。
heap_5:heap_4的终极形态——跨越多段不连续内存。 原理与heap_4完全一致,但调用vPortDefineHeapRegions()后,可以把分散在Flash、SRAM、外部RAM的多段物理地址整合为统一内存池。适合RAM布局复杂的高端MCU。
选型的本质不是比功能多寡,而是匹配你的内存使用模式。任务生命周期固定、永不删除,heap_1零风险;需要动态创建销毁且长期运行,heap_4是唯一不会让你半夜被告警电话叫醒的方案;heap_2和heap_3,能不用就别用。内存泄漏不会立刻杀死系统,它只会在最关键的时刻——给你致命一击。





