扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

FreeRTOS的5种堆方案,如何理解?

03/13 10:43
332
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大家好,我是杂烩君。今天分享FreeRTOS堆管理方案的内容。

1. 如何选择堆方案

FreeRTOS有5种堆管理方案(heap_1~heap_5):

方案 释放 合并 链表排序 碎片 核心特点
heap_1 不支持 无链表 顺序分配,最简单
heap_2 支持 不合并 按大小 严重 最佳匹配
heap_3 支持 依赖系统 无(用系统堆) 依赖系统 封装malloc/free
heap_4 支持 合并 按地址 首次匹配,官方推荐
heap_5 支持 合并 按地址 同heap_4,多RAM区域

通过修改FreeRTOSConfig.h中的宏定义选择,核心配置如下:

// 选择堆管理方案(仅需开启其中一个)
// #define configUSE_HEAP_1    1  // 启用heap_1方案
// #define configUSE_HEAP_2    1  // 启用heap_2方案
#define configUSE_HEAP_3    1  // 启用heap_3方案
// #define configUSE_HEAP_4    1  // 启用heap_4方案
// #define configUSE_HEAP_5    1  // 启用heap_5方案

// 配置堆内存总大小(heap_1/2/4/5适用,单位:字节)
#define configTOTAL_HEAP_SIZE    (10 * 1024)  // 10KB堆内存

5种堆方案互斥,只能启用其中一种;heap_3方案无需配置configTOTAL_HEAP_SIZE(依赖系统malloc/free),其余4种均需配置堆总大小。

2. 5种堆方案详解

2.1 heap_1:只分配,不释放

最简单的方案。在一个静态数组上维护一个偏移量,分配时向后移动,不提供释放功能。

源码中堆的核心数据结构只有两个变量:

/* heap_1.c */
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
static size_t xNextFreeByte = ( size_t ) 0U;

分配时检查剩余空间,够就返回 pucAlignedHeap + xNextFreeByte 并后移偏移量。逻辑像切蛋糕——只能从头往后切,切下来的不能放回去:

偏移量只往右走,永远不回头——这就是 heap_1 不支持释放的根本原因。

需要注意的是,heap_1 的 vPortFree() **并非"什么都不做"**,而是会触发断言:

void vPortFree( void * pv )
{
    ( void ) pv;
    configASSERT( pv == NULL );  /* 传入非NULL指针会触发断言失败 */
}

在开了 configASSERT 的工程里,误调 vPortFree 会直接卡死。

适用场景:启动时一次性创建所有内核对象,运行期间不删除。RAM极小(<10KB)的MCU优先考虑。

2.2 heap_2:支持释放,不合并空闲块

引入链表管理空闲块,每个内存块前面挂一个 BlockLink_t 头部:

/* heap_2.c / heap_4.c 共用此结构 */
typedef struct A_BLOCK_LINK {
    struct A_BLOCK_LINK * pxNextFreeBlock;
    size_t xBlockSize;
} BlockLink_t;

heap_2 的空闲链表按块大小从小到大排序,分配时从链表头开始遍历,找到第一个能满足需求的块——因为链表按大小排序,所以找到的就是最小够用的块,本质是最佳匹配算法。

释放时将块插回链表对应位置,但不合并相邻空闲块。长期分配释放不同大小的内存,碎片会越来越严重。连续内存不够,想分配 120B —— 失败!

这就是"内存碎片"——剩余总空间够,但全是不连续的小块,分不出大块。

适用场景:每次分配的大小固定(比如所有任务栈大小一样),此时释放后的块刚好能被下次分配复用,不会产生碎片。

2.3 heap_3:封装系统malloc/free

不自己管理堆,直接用编译器提供的 malloc()/free(),套一层调度器挂起来保证多任务安全:

/* heap_3.c */
void * pvPortMalloc( size_t xWantedSize )
{
    void * pvReturn;
    vTaskSuspendAll();       /* 挂起调度器,不是关中断 */
    {
        pvReturn = malloc( xWantedSize );
    }
    ( void ) xTaskResumeAll();
    return pvReturn;
}

vTaskSuspendAll()挂起调度器防止任务切换,中断仍然可以响应。这也意味着中断中不能调用 heap_3 的分配/释放

heap_3 本质就是一个"线程安全的壳",内存管理能力完全取决于编译器自带的 malloc 实现。

适用场景:快速原型开发、对内存效率要求不高的场合。

2.4 heap_4:支持空闲块合并(大多数项目首选)

和 heap_2 结构类似,但有两个关键区别:

空闲链表

按地址从低到高排序(heap_2 按大小排序)释放时合并相邻空闲块

分配采用首次匹配——遍历链表找到第一个够大的块就用。合并逻辑是 heap_4 的精华,释放时检查前后相邻块是否也空闲:

/* heap_4.c - prvInsertBlockIntoFreeList 合并逻辑(简化) */
puc = ( uint8_t * ) pxIterator;
if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
{
    /* 前一块的尾部恰好接上当前块的头部,合并 */
    pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
    pxBlockToInsert = pxIterator;
}
/* 同样检查与后一个空闲块是否地址连续,连续则合并 */

用一个例子说明合并过程——假设释放 B 块,它恰好夹在两个空闲块 A、C 之间:

正因为链表按地址排序,释放时才能判断前后块是否在地址上连续,从而合并。这就是 heap_2 做不到合并的根本原因——它按大小排序,地址关系全丢了。

适用场景:分配大小不固定、频率高、对碎片敏感的通用场景。绝大多数项目直接选它。

2.5 heap_5:多块不连续RAM

在 heap_4 基础上支持多块不连续 RAM 统一管理。使用前必须先调用 vPortDefineHeapRegions() 注册内存区域,数组按地址从低到高排列,以 {NULL, 0} 结尾:

const HeapRegion_t xHeapRegions[] = {
    { (uint8_t *) 0x20000000UL, 64 * 1024 },    /* 内部RAM 64KB */
    { (uint8_t *) 0x68000000UL, 1024 * 1024 },   /* 外部SDRAM 1MB */
    { NULL, 0 }                                    /* 结束标记 */
};

/* 必须在创建任何内核对象之前调用 */
vPortDefineHeapRegions( xHeapRegions );

vPortDefineHeapRegions() 内部会把这些不连续的区域串成一条按地址排序的空闲链表:

注册完成后,分配/释放逻辑与 heap_4 完全一致——分配时遍历整条链表,无论内存在哪块 RAM 上都能分配到。

适用场景:MCU有内部RAM + 外部SRAM/SDRAM,需要统一管理所有RAM资源。

3. 注意事项

heap_1 调 vPortFree 会断言失败,不是静默忽略

heap_3 中断不可用:它靠挂起调度器保护,中断里调用会出问题

heap_2 碎片陷阱:分配大小不固定时,碎片快速积累导致分配失败

configTOTAL_HEAP_SIZE 别设太满:给任务栈和全局变量留空间,建议预留 RAM 的 10%~20%

heap_5 别滥用:只有一块连续RAM时,heap_5比heap_4多了配置复杂度,没有任何优势

heap_4 覆盖了绝大多数嵌入式场景,也是 FreeRTOS 官方推荐的通用方案。

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

本公众号专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,公众号内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!