1. FreeRTOS 内存管理

1.1 简介

​ FreeRTOS 采用 动态内存分配(Heap)静态内存分配(Static) 两种方式来管理任务、队列、信号量等系统对象的内存。默认情况下,FreeRTOS 使用 堆(Heap) 来进行动态内存管理,而用户也可以通过 静态分配 来确保内存使用的确定性,避免动态内存碎片问题。如果是动态创建的,那么标准 C 库 malloc() 和 free() 函数有时可用于此目的,但是有

以下缺点:

  • 它们在嵌入式系统上并不总是可用。
  • 它们占用了宝贵的代码空间。
  • 它们不是线程安全的。
  • 它们不是确定性的 (执行函数所需时间将因调用而异)。

​ 所以更多的时候需要的不是一个替代的内存分配实现。一个嵌入式/实时系统的 RAM和定时要求可能与另一个非常不同,所以单一的 RAM 分配算法将永远只适用于一个应用程序子集。为了避免此问题,FreeRTOS 将内存分配 API 保留在其可移植层,提供了五种内存管理算法。

FreeRTOS 提供了 5 种不同的内存分配方案,对应 5 种 heap_x.c 文件:

内存管理方案 描述
heap_1.c 简单的 malloc/free 实现,但不支持释放,适用于任务生命周期固定的场景。
heap_2.c 支持释放内存,但不会合并相邻的空闲块,可能导致碎片化。
heap_3.c 直接使用标准库 malloc/free,适用于底层提供良好 malloc 实现的系统,但开销较大。
heap_4.c 改进的 heap_2.c,支持合并相邻空闲块,减少碎片化,推荐使用。
heap_5.c 在 heap_4.c 基础上支持多个独立的堆区域,适用于复杂内存布局的 MCU(如 RAM 分布在不同区域)。

1.2 FreeRTOS 内存管理算法详解

1.2.1 heap_1 算法

​ heap_1 是最简单的实现方式。内存一经分配,它不允许内存再被释放。尽管如此,heap_1.c 还是适用于大量嵌入式应用程序。适用于任务固定、无需删除的系统(例如裸机环境)。

请添加图片描述

实现方式

  • 维护一个 全局静态数组 ucHeap[] 作为堆空间。
  • 每次 pvPortMalloc()ucHeap[] 中分配一块内存,仅增加指针偏移量
  • 不提供 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) { /* 空实现,不支持释放 */ }

1.2.2 heap_2 算法

​ heap_2 使用最佳适应算法,并且与方案 1 不同,它允许释放先前分配的块,它不将相邻的空闲块组合成一个大块。

​ 如果动态地创建和删除任务,且分配给正在创建任务的堆栈大小总是相同的,那么 heap2.c 可以在大多数情况下使用。但是,如果分配给正在创建任务的堆栈的大小不是总相同,那么可用的空闲内存可能会被碎片化成许多小块,最终导致分配失败。
请添加图片描述

​ 如上图,现在有一个任务请求分配 18 字节的内存。最佳适应算法将选择大小为 20 字节的块,因为它与请求的大小最接近。在选择这个块后,分配器可能会将该块分割为两部分,一部分大小为 18 字节,用于任务的内存,另一部分大小为 2 字节,留作未分配的块。

实现方式

  • 使用链表管理内存块,分配时从空闲块链表找到合适大小的块
  • 释放时,仅标记为空闲,但不合并相邻块。

码源分析(简化分析)

typedef struct A_BLOCK_LINK
{
    struct A_BLOCK_LINK * pxNextFreeBlock;
    size_t xBlockSize;                     
} BlockLink_t;

void *pvPortMalloc(size_t xWantedSize) {
    BlockLink_t *pxBlock = NULL;
    pxBlock = 找到合适大小的空闲块();
    if (pxBlock) { 标记为已使用 }
    return (void *)(pxBlock + 1);
}

void vPortFree(void *pv) {
    BlockLink_t *pxBlock = ((BlockLink_t *)pv) - 1;
    pxBlock->pxNextFreeBlock = 头部空闲块;
    头部空闲块 = pxBlock;
}

1.2.3 heap_3 算法

​ heap_3 使用 C 库的 malloc 和 free 函数来进行内存分配和释放。它通过分配固定大小的块来管理内存,这些块的大小在配置 FreeRTOS 时进行定义,不会动态改变,不需 FreeRTOS 维护堆。

假设我们使用 Heap_3 管理内存,其中块的大小固定为 32 字节。初始时,整个内存被分割成大小为 32 字节的块:

  • 块 1(32 字节)。
  • 块 2(32 字节)。
  • 块 3(32 字节)。

现在,有一个任务请求分配 20 字节的内存。Heap_3 算法将选择块 1,并将其分割成两部分:

  • 分配给任务的内存块(20 字节)。
  • 剩余未分配的块(12 字节)。

再假设另一个任务请求分配 40 字节的内存。由于没有足够大的块可供分配,heap_3将返回分配失败的状态。

​ heap_3 的特点是块大小固定,这样可以简化内存管理。然而,也因为块大小不可变,可能导致内存碎片问题,即一些块可能无法完全被利用,从而浪费了一些内存。

实现方式

  • 直接调用标准 malloc()free(),FreeRTOS 不再管理内存

码源分析(简化分析)

void *pvPortMalloc(size_t xSize) {
    return malloc(xSize);
}

void vPortFree(void *pv) {
    free(pv);
}

1.2.4 heap_4 算法

​ heap_4 使用第一适应算法,并且会将相邻的空闲内存块合并成大内存块,减少内存碎片。支持释放,并合并相邻的空闲块,减少碎片化(比 heap_2.c 更优)。 适用于大多数动态分配的系统,如 任务频繁创建/删除的系统**。**稍微增加了一些 CPU 计算开销,但比 heap_3.c 效率更高。

​ 第一适应算法会在可用内存块中选择第一个足够大的内存块进行分配。

请添加图片描述

假设有一个内存块链表,其中包含以下顺序的内存块:

  • 大小为 40 字节的块。
  • 大小为 30 字节的块。
  • 大小为 15 字节的块。
  • 大小为 20 字节的块。

​ 如果一个任务需要申请 25 字节的内存,第一适应算法将选择大小为 40 字节的块,因为它是第一个足够大以容纳任务需求的内存块。(如果是 heap_2 的最佳适应算法,会选择30 字节的块)

实现方式

  • 采用 链表管理空闲块,支持 合并相邻的空闲块,减少碎片化。

码源分析(简化分析)

//  heap_4.c 里,FreeRTOS 通过 BlockLink_t 双向链表 维护内存块:
typedef struct A_BLOCK_LINK
{
    struct A_BLOCK_LINK *pxNextFreeBlock;  /* 指向下一个空闲块 */
    size_t xBlockSize;                     /* 该块的大小 */
} BlockLink_t;

/* 分配内存
遍历空闲块链表,找到合适大小的块。
删除该块,并返回其地址。如果 该块比请求的大小大,则拆分成两个块(分配部分 + 剩余部分)
*/
void *pvPortMalloc(size_t xWantedSize) {
    BlockLink_t *pxBlock = NULL, *pxPreviousBlock = NULL;
    void *pvReturn = NULL;

    vTaskSuspendAll(); /* 进入临界区,防止任务切换 */

    pxPreviousBlock = &xStart;  // 指向空闲链表头
    pxBlock = 找到合适大小的空闲块();

    if (pxBlock != NULL) { // 找到合适的空闲块
        pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock; // 从链表移除
        pvReturn = (void *)(pxBlock + 1);  // 跳过 BlockLink_t 头部,返回数据部分
    }

    xTaskResumeAll(); /* 退出临界区 */
    return pvReturn;
}

// 释放内存,合并相邻的空闲块,减少碎片化
void vPortFree(void *pv) {
    BlockLink_t *pxBlock = ((BlockLink_t *)pv) - 1; // 找到对应的 BlockLink_t 结构
    vTaskSuspendAll(); /* 进入临界区 */

    pxBlock->pxNextFreeBlock = xStart.pxNextFreeBlock;  // 插入空闲链表
    xStart.pxNextFreeBlock = pxBlock;

    /* **合并相邻空闲块** */
    pxBlock = xStart.pxNextFreeBlock;
    while (pxBlock->pxNextFreeBlock != NULL) {
        if (((uint8_t *)pxBlock + pxBlock->xBlockSize) == (uint8_t *)pxBlock->pxNextFreeBlock) {
            pxBlock->xBlockSize += pxBlock->pxNextFreeBlock->xBlockSize;
            pxBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock->pxNextFreeBlock;
        }
        pxBlock = pxBlock->pxNextFreeBlock;
    }

    xTaskResumeAll(); /* 退出临界区 */
}

1.2.5 heap_5 算法

​ heap_5 使用与 heap_4 相同的第一适应和内存合并算法,允许堆跨越多个不相邻(非连续)内存区域。适用于内存地址不连续的复杂场景,如:用于 MCU 有多个 RAM 段(如 STM32F7、L4 系列),需要 同时使用内部 RAM 和外部 RAM

实现方式

  • 维护多个独立的堆区域,每个区域有单独的 pvPortMalloc()vPortFree()
  • FreeRTOS 自动选择合适的堆区域 进行分配。

码源分析(简化分析)

typedef struct HeapRegion {
    uint8_t *pucStartAddress; /* RAM 起始地址 */
    size_t xSizeInBytes;      /* RAM 大小 */
} HeapRegion_t;

// 定义多个 RAM 区域,将每个 RAM 区域作为一个 Heap,并加入 Heap 管理列表
void vPortDefineHeapRegions(const HeapRegion_t *pxHeapRegions) {
    while (pxHeapRegions->xSizeInBytes > 0) {
        BlockLink_t *pxFirstFreeBlock;
        uint8_t *pucAlignedHeap = (uint8_t *)pxHeapRegions->pucStartAddress;
        size_t xBlockSize = pxHeapRegions->xSizeInBytes;

        pxFirstFreeBlock = (BlockLink_t *)pucAlignedHeap;
        pxFirstFreeBlock->xBlockSize = xBlockSize;
        pxFirstFreeBlock->pxNextFreeBlock = xStart.pxNextFreeBlock;
        xStart.pxNextFreeBlock = pxFirstFreeBlock;

        pxHeapRegions++;
    }
}

/* 在多个 RAM 区域中查找内存
在 所有 RAM 区域 中查找 合适的内存块,如果一个区域不够大,就继续查找下一个 RAM 区域
*/
void *pvPortMalloc(size_t xWantedSize) {
    void *pvReturn = NULL;
    BlockLink_t *pxBlock, *pxPreviousBlock;

    vTaskSuspendAll(); /* 进入临界区 */

    pxPreviousBlock = &xStart;
    pxBlock = xStart.pxNextFreeBlock;
    
    while (pxBlock->xBlockSize < xWantedSize && pxBlock->pxNextFreeBlock != NULL) {
        pxPreviousBlock = pxBlock;
        pxBlock = pxBlock->pxNextFreeBlock;
    }
    
    if (pxBlock->xBlockSize >= xWantedSize) {
        pvReturn = (void *)(pxBlock + 1);
        pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
    }

    xTaskResumeAll(); /* 退出临界区 */
    return pvReturn;
}

1.3 相关 API 函数

API 函数 描述
pvPortMalloc(size_t xSize) 分配内存,类似 malloc()
vPortFree(void *pv) 释放内存,类似 free()
xPortGetFreeHeapSize() 获取当前 剩余堆内存大小(单位:字节)
xPortGetMinimumEverFreeHeapSize() 获取系统运行过程中 最小的可用堆大小
vPortInitialiseBlocks() Heap1/Heap2 专用,初始化空闲块(Heap4/5 不需要)

1.4 实验

  • start_task:用来创建其他 1 个任务。
  • task1:用于按键扫描,当 KEY1 按下则申请内存,当 KEY2 按下则释放内存,当 KEY3 按下打印剩余内存信息。
void memory_test(void) {
    printf("系统启动,空闲堆内存: %d 字节\n", xPortGetFreeHeapSize());

    void *ptr1 = pvPortMalloc(100);
    printf("分配 100 字节后,剩余堆内存: %d 字节\n", xPortGetFreeHeapSize());

    void *ptr2 = pvPortMalloc(200);
    printf("分配 200 字节后,剩余堆内存: %d 字节\n", xPortGetFreeHeapSize());

    vPortFree(ptr1);
    printf("释放 100 字节后,剩余堆内存: %d 字节\n", xPortGetFreeHeapSize());

    vPortFree(ptr2);
    printf("释放 200 字节后,剩余堆内存: %d 字节\n", xPortGetFreeHeapSize());

    printf("系统最小可用堆内存: %d 字节\n", xPortGetMinimumEverFreeHeapSize());
}

void task1(void *pvParameters)
{
    void *ptr = NULL;
    while (1)
    {
        if (key[0].flag)
        {
            ptr = pvPortMalloc(128);
            if (ptr != NULL)
                printf("分配内存成功,地址: %#Xp \r\n",ptr);
            else
                printf("分配内存成功失败 \r\n");

            key[0].flag = 0;
        }
        if (key[1].flag)
        {
            vPortFree(ptr);
            printf("释放内存成功\r\n");
            key[1].flag = 0;
        }
        if (key[2].flag)
        {
            printf("当前空闲堆内存: %d 字节\n", xPortGetFreeHeapSize());
            printf("系统最小空闲堆内存: %d 字节\n", xPortGetMinimumEverFreeHeapSize());
            key[2].flag = 0;
        }
        if (key[3].flag)
        {
            memory_test();
            key[3].flag = 0;
        }

        vTaskDelay(100);
    }
}

请添加图片描述

memory_test函数运行结果:

系统启动,空闲堆内存: 7432 字节
分配 100 字节后,剩余堆内存: 7320 字节
分配 200 字节后,剩余堆内存: 7104 字节
释放 100 字节后,剩余堆内存: 7216 字节
释放 200 字节后,剩余堆内存: 7432 字节
系统最小可用堆内存: 7104 字节

从运行结果来看,FreeRTOS 的动态内存管理包含了一些额外的 管理开销,这导致 分配的内存比请求的要多,原因如下:

1 ) 内存管理中的块头

FreeRTOS 的 Heap 内存管理(heap_1、heap_2、heap_4、heap_5) 采用 链表结构 来管理已分配和未分配的内存块,每个分配的内存块都会带有一个 块头(Block Header) 来存储内存管理信息,如:

  • 块大小(Block Size)
  • 指向下一个空闲块的指针(pxNextFreeBlock)
  • 对齐填充(Alignment Padding)

这部分 块头(Metadata) 通常占用 8 字节(32 位 MCU)或 16 字节(64 位 MCU)
2 ) 内存对齐

FreeRTOS heap_4 里通常定义了 xHeapStructSize,确保块头加上数据大小后仍满足对齐要求。

/* The size of the structure placed at the beginning of each allocated memory
 * block must by correctly byte aligned. */
static const size_t xHeapStructSize = ( sizeof( BlockLink_t ) + ( ( size_t ) ( portBYTE_ALIGNMENT - 1 ) ) ) & ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
// 这里 xHeapStructSize 计算值为 16

FreeRTOS 保证内存地址对齐(在这里为 16 字节对齐),以提高 CPU 访问效率,因此:

  • 如果分配的内存不是 16 的倍数,FreeRTOS 可能会 填充额外的字节,以满足对齐要求。

📌 示例: 假设 pvPortMalloc(100)

  • 100 + 8(块头)= 108
  • 108 不是 16 的倍数,需要 填充 4 字节
  • 最终分配 = 112 字节

📌 再看 pvPortMalloc(200)

  • 200 + 8(块头)= 208
  • 208 不是 16 的倍数,需要 填充 8 字节
  • 最终分配 = 216 字节
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐