目录
- 一、前言
- [二、堆的核心概念:可分配 / 释放的内存空间](#二、堆的核心概念:可分配 / 释放的内存空间)
- 三、简单堆分配实现:从数组到内存管理
- 3.1 基础堆分配函数(my_malloc)
- 3.2 实际调试效果验证(结合内存截图)
- 3.3 简单实现的致命缺陷:无法释放内存
- 四、堆的释放难题:链表式管理方案
- 4.1 核心思路:用 "头部 + 链表" 记录内存信息
- 4.2 链表结构设计与初始状态
- 4.3 堆分配与释放的链表变化(结合示意图)
- 五、下一篇预告
- 六、结尾
一、前言
大家好,我是 Hello_Embed 。上一篇我们吃透了 ARM 架构的底层逻辑,搞懂了寄存器和栈如何支撑函数调用 ------ 而这一篇,我们将聚焦 FreeRTOS 内存管理的 "半壁江山"------堆(Heap)。
在 FreeRTOS 开发中,堆的作用至关重要:动态创建任务、队列、信号量时,都需要通过堆分配内存;甚至很多外设驱动的缓冲区,也依赖堆来灵活管理空间。这篇笔记会从最朴素的堆概念讲起,先实现一个极简的堆分配函数,再剖析它的缺陷,最终引出工业级堆管理的核心思路(链表式管理),帮大家彻底搞懂 "堆如何实现内存的分配与释放"。
二、堆的核心概念:可分配 / 释放的内存空间
提到 "堆",很多人会和上一篇讲的 "栈" 混淆 ------ 其实两者的核心区别很明确:
- 栈:自动分配 / 释放,用于函数局部变量、参数传递,由 CPU 寄存器 SP 自动管理;
- 堆:手动分配 / 释放,用于程序运行中需要灵活控制生命周期的内存(比如动态创建的任务结构体),需通过代码主动管理。
堆的本质,就是一块预留的空闲内存空间,我们可以通过代码向它 "申请" 一块内存(分配),不用时再 "归还" 给它(释放),实现内存的重复利用。
最朴素的堆,就是一个普通数组 ------ 比如 char heap_buf[1024],这 1024 字节的连续空间,就是我们的 "堆空间",后续的内存分配都围绕它展开。
三、简单堆分配实现:从数组到内存管理
我们先从最基础的代码入手,实现一个极简的堆分配函数my_malloc,通过它理解堆分配的核心逻辑。
3.1 基础堆分配函数(my_malloc)
c
// 定义堆空间:1024字节的连续数组
char heap_buf[1024];
// 堆分配指针:记录当前已分配到的位置(初始为0,指向堆空间首地址)
int pos = 0;
/**
* @brief 简单堆分配函数
* @param size:需要分配的内存字节数
* @retval 分配成功:返回内存首地址;分配失败(超出堆空间):返回NULL(本实现暂未处理)
*/
void *my_malloc(int size)
{
int old_pos = pos; // 保存当前分配位置(后续作为内存首地址返回)
pos += size; // 分配size字节后,更新pos到新的空闲位置
// 注意:实际应用需添加判断(pos <= 1024),避免堆溢出
return &heap_buf[old_pos]; // 返回分配的内存首地址
}
这个函数的逻辑非常直白,相当于 "切蛋糕":
- 堆空间
heap_buf是一整块 "蛋糕",pos是 "刀的位置",初始在蛋糕最开头(0 字节处); - 申请
size字节时,先记下当前刀的位置(old_pos),再把刀向后移size字节(pos += size); - 返回
old_pos对应的地址,就是分配给用户的 "蛋糕块"。
3.2 实际调试效果验证(结合内存截图)
我们在main函数中调用my_malloc分配 100 字节,并写入 'A'~'Z' 的字符,通过调试器观察内存变化:
c
int main(void)
{
int i;
// 分配100字节内存,buf指向分配的内存首地址
char *buf = my_malloc(100);
// 向分配的内存中写入数据:buf[0]='A', buf[1]='B'...buf[25]='Z'
for(i = 0; i < 26; i++)
buf[i] = 'A' + i; // 原笔记此处格式有误,修正为'A' + i
}
调试结果与内存分析:
-
堆空间分配大小:从调试器可见,分配的内存大小为 0x64(即 100 字节),与
my_malloc(100)一致;

-
堆空间首地址与内容:
heap_buf的首地址为 0x20000004,分配的 100 字节从该地址开始,前 26 字节依次存入 'A'~'Z',与代码逻辑完全匹配;
这说明我们的极简堆分配函数是有效的 ------ 它能成功分配内存并供用户使用。
3.3 简单实现的致命缺陷:无法释放内存
虽然my_malloc能分配内存,但它没有对应的 "释放函数"(比如my_free),核心原因有两个:
- 没有记录 "分配大小" :用户调用
my_free(buf)时,只传入了内存首地址buf,但函数不知道这块内存占多大 ------ 无法确定要把pos回退多少字节; - 内存碎片问题 :即使知道大小,直接回退
pos也会导致 "内存空洞"。比如先分配 100 字节,再分配 100 字节,若释放第一个 100 字节,pos回退到 100 字节处,但第二个 100 字节仍占用 200~300 字节,此时申请 150 字节会因空间不连续而失败,造成内存浪费。
要解决这些问题,就需要更智能的堆管理方式 ------链表式堆管理。
四、堆的释放难题:链表式管理方案
工业级堆管理的核心思路是:给每块分配的内存加一个 "头部",用链表串联所有空闲内存块------ 通过头部记录内存大小,通过链表管理空闲块的位置,实现精准分配与释放。
4.1 核心思路:用 "头部 + 链表" 记录内存信息
- 头部(Head):在每块内存的开头,额外分配几个字节存储 "管理信息",核心是内存大小 和下一个空闲块的地址;
- 链表:把所有空闲的内存块用链表串联起来,分配时从链表中找合适的块,释放时把块重新加入链表。
这样一来,释放内存时,只需通过buf地址找到头部(buf减去头部大小),就能获取内存大小,再通过链表把空闲块连接起来,实现重复利用。
4.2 链表结构设计与初始状态
我们用结构体定义内存块的 "头部",并通过全局变量管理链表头:
c
// 内存块头部结构体:存储管理信息
struct head
{
int size; // 该内存块的总大小(头部+用户数据)
struct head next_free; // 下一个空闲内存块的地址(注意:实际需定义为指针struct head*)
};
struct head g_heap; // 链表头:管理所有空闲内存块
初始状态说明(堆空间为 1024 字节):
- 堆空间的第一个字节就是头部,头部的
size设为 1024(整个堆的大小); - 头部的
next_free设为NULL(表示当前只有一块空闲内存); - 全局链表头
g_heap.next_free指向堆空间的头部 ------ 相当于链表的 "入口",所有分配操作都从这里开始。
4.3 堆分配与释放的链表变化(结合示意图)
场景 1:分配 100 字节内存
- 从链表头
g_heap.next_free找到空闲块(大小 1024 字节); - 从该块中拆分出 "头部(8 字节,假设 int 和指针各 4 字节)+100 字节用户数据",共 108 字节;
- 剩余内存大小为 1024-108=916 字节,作为新的空闲块,更新链表:
g_heap.next_free指向新空闲块的头部; - 返回用户数据的首地址(头部地址 + 8 字节)给用户。
分配后的内存布局与链表结构:
场景 2:再分配 50 字节内存
重复上述流程:从 916 字节的空闲块中拆分出 "头部 + 50 字节"(共 58 字节),剩余 916-58=858 字节,g_heap.next_free指向 858 字节的空闲块头部。
场景 3:释放之前分配的 100 字节内存
- 用户调用
my_free(buf),buf是 100 字节用户数据的首地址; - 计算头部地址:
buf - 8(减去头部大小 8 字节),从头部中读取size=108(总大小); - 把这块 108 字节的内存块重新加入空闲链表:让原空闲块的
next_free指向该块,该块的next_free设为NULL; - 此时空闲链表中有两块空闲块:108 字节和 858 字节,后续分配时会优先从链表头开始查找合适的块。
释放后的链表结构:

核心优势:
- 分配时:通过链表遍历空闲块,找到能容纳
size的块(按需拆分); - 释放时:通过头部获取大小,将块重新接入链表,实现内存重复利用;
- 解决碎片:后续可通过 "内存合并" 优化(把相邻的空闲块合并成大块),减少碎片问题。
五、下一篇预告
本次我们彻底搞懂了堆的核心逻辑 ------ 从极简分配到链表式管理,而堆的 "孪生兄弟" 栈,在 FreeRTOS 中同样关键(每个任务都有独立的任务栈)。下一篇我们将聚焦:
- 堆与栈的核心区别(生长方向、管理方式、使用场景);
- FreeRTOS 任务栈的本质(为什么任务必须有独立栈);
- 任务栈的配置与溢出风险(栈大小如何确定)。
六、结尾
本篇笔记的核心,是从 "能用" 到 "好用" 拆解堆的实现 ------ 极简的my_malloc让我们理解了堆的本质是 "空闲空间的分配",而链表式管理则解决了 "释放与复用" 的工业级需求。
在 FreeRTOS 中,堆的应用非常直接:xTaskCreateDynamic(动态创建任务)、xQueueCreate(创建队列)等函数,本质都是调用 FreeRTOS 提供的堆管理接口(如pvPortMalloc)分配内存。理解堆的管理逻辑,不仅能帮我们合理配置堆大小,还能在出现 "内存分配失败" 时快速定位问题(比如堆空间不足、碎片过多)。
堆和栈是嵌入式内存管理的基石,也是 FreeRTOS 任务调度的核心依赖 ------ 搞懂它们,就等于掌握了 RTOS 内存管理的 "钥匙"。我是 Hello_Embed,下一篇我们将深入栈的世界,揭开 FreeRTOS 任务栈的神秘面纱,继续扎实推进 RTOS 的学习之旅!