一、知识总览
这部分聚焦 手动实现的堆内存管理流程,从堆初始化、内存分配(my_malloc
)到释放(my_free
),拆解 “内存如何被申请、标记、回收”,是理解嵌入式系统动态内存管理、FreeRTOS 内存分配的基础。
二、核心概念分步解析
(一)堆的基础结构
- 数据结构定义:
struct head {
int size; // 记录内存块总大小(含“头”自身 )
void *next_free; // 指向下一个空闲块,串联成空闲链表
};
struct head g_heap; // 全局堆管理结构体,作为空闲链表的头
- 作用:
size
:标记内存块长度,分配 / 释放时用于计算边界、判断是否足够。next_free
:通过指针串联空闲块,实现 “按需查找可用内存”,避免遍历整个堆。
- 图示:上图展示堆被划分为多个带 “头” 的块(如
头|100
、头|50
),“头” 里的size
和next_free
串联空闲块,直观呈现 链表式管理 结构。
(二)堆初始化
- 初始状态:
堆内存初始是 一块连续大空闲块,用g_heap
管理:g_heap.size = 0
g_heap.next_free = NULL
(无其他空闲块,自身是唯一空闲块 )
- 关键:初始化是起点,将整块内存标记为 “空闲”,后续分配 / 释放基于此修改链表。
(三)内存分配
1. 单次分配(以分配 100 字节为例)
- 需求:
char *buf = my_malloc(100);
,申请 100 字节。 - 流程:
- 遍历空闲链表:从
g_heap
开始,找第一个size ≥ 100 + 头大小
的空闲块(需预留 “头” 的空间 )。 - 分割内存块:
- 若找到足够大的块(如初始块
size=0
),分割为两部分:- 已分配块:
头|100
(“头” 记录size=100 + 头大小
,用户实际用 100 字节 )。 - 剩余空闲块:更新
g_heap
或链表,标记剩余空间为新空闲块。
- 已分配块:
- 若找到足够大的块(如初始块
- 返回指针:将已分配块的 “数据区指针”(跳过 “头” 的位置 )返回给用户(
buf
指向头|100
的数据区 )。
- 遍历空闲链表:从
2. 多次分配
- 需求示例:
char *buf = my_malloc(100);
char *buf2 = my_malloc(50);
char *buf3 = my_malloc(100);
依次申请 100、50、100 字节。
- 流程:
- 第一次分配 100 字节:分割初始块,更新链表。
- 第二次分配 50 字节:遍历链表,找足够大的块(如剩余空闲块或新释放的块 ),分割出
头|50
,更新链表。 - 第三次分配 100 字节:继续遍历,分割出
头|100
,更新链表。
- 关键:每次分配需遍历链表找 “足够大的块”,若找不到则分配失败(返回
NULL
)。嵌入式场景需提前规划堆大小,避免分配失败。
(四)内存释放
1. 单次释放(以释放 100 字节为例)
- 需求:
my_free(buf);
,释放之前分配的 100 字节。 - 流程:
- 定位内存块头:通过
buf
回推找到 “头” 的位置(buf
指向数据区,需减去 “头” 的大小,得到head
指针 )。 - 标记为空闲块:将该块的
size
标记为实际大小(含头 ),并将next_free
指向g_heap.next_free
(插入空闲链表头部,或按地址排序插入,依实现逻辑 )。 - 合并相邻空闲块(可选 ):检查释放块的前后是否有空闲块,若有则合并,避免内存碎片(复杂实现需此步骤,简化版可能省略 )。
- 定位内存块头:通过
- 关键:释放时必须准确找到 “头” 的位置,若
buf
非法(如未分配、已释放 ),会损坏堆结构,引发崩溃。
2. 释放与合并
- 场景:释放块与前后空闲块相邻,需合并为更大空闲块,减少碎片。
- 流程:
- 释放块后,检查前一个块(
prev
)和后一个块(next
)是否为空闲块(通过size
或标记判断 )。 - 若
prev
是空闲块,合并prev
和当前块;若next
是空闲块,合并当前块和next
;若都有,合并三者为一个大空闲块。
- 释放块后,检查前一个块(
- 核心:合并碎片可提高后续分配成功率,避免因小碎片无法满足大申请而失败。
(五)简化线性分配
- 逻辑:图展示简化的 “线性分配”,用
pos
标记当前分配位置,每次分配从pos
开始,按需求size
向后延伸(pos += size
),无法释放和碎片整理。 - 优缺点:
- 优点:实现简单,适合无需释放、一次性分配的场景(如初始化时预分配 )。
- 缺点:无法释放和复用内存,容易耗尽堆空间,仅适用于极简场景。
- 对比:与链表式管理(支持分配 / 释放 / 合并 )相比,线性分配更简单但灵活度低,理解差异可选择合适策略。
三、知识串联(从初始化到分配 / 释放 )
- 初始化:堆初始为一个大空闲块,用
g_heap
管理,size
为总大小,next_free
为NULL
。 - 分配:
my_malloc
遍历空闲链表,找到足够大的块,分割为 “已分配块” 和 “剩余空闲块”,更新链表;多次分配时,重复此过程,动态分割空闲块 。 - 释放:
my_free
定位内存块的 “头”,标记为空闲块,插入空闲链表;复杂实现会合并相邻空闲块,减少碎片 。 - 对比简化逻辑:线性分配无需链表,按顺序分配,无法释放,适合极简场景 。
四、易错点 & 补充说明
(一)易错点
- 指针回推错误:释放时无法正确回推 “头” 的位置(如
buf
被非法修改 ),会操作错误内存块,损坏堆结构,引发崩溃。 - 内存碎片忽视:释放时不合并相邻空闲块,频繁分配 / 释放小内存会导致大量碎片,后续大内存申请可能失败(即使总空闲空间足够,因碎片分散无法满足连续需求 )。
- 混淆 “已分配” 与 “空闲” 状态:分配后未正确标记块状态,或释放后未及时更新链表,会导致重复分配同一内存、空闲块丢失等问题,破坏堆完整性 。
(二)补充拓展
- 堆管理算法差异:除 “空闲链表 + 合并” 外,还有 “伙伴系统”“ slab 分配器” 等高级算法,FreeRTOS 中
heap_4.c
使用 “最佳匹配 + 合并”,heap_5.c
支持跨内存区域分配,需依场景选择 。 - 内存池与堆的区别:内存池是预分配固定大小的块,适合频繁分配相同大小内存的场景(如消息队列 ),相比堆更高效且无碎片;堆适合动态大小的分配,但需管理碎片 。