当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]嵌入式系统崩在哪里?十有八九不是算法错了,是内存漏了。FreeRTOS把内存管理的选择权交给了开发者——五种heap方案,从"只分不收"到"多段合并",选对了系统稳如磐石,选错了就是慢性内存泄漏,三个月后必死机。

嵌入式系统崩在哪里?十有八九不是算法错了,是内存漏了。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,能不用就别用。内存泄漏不会立刻杀死系统,它只会在最关键的时刻——给你致命一击。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭