堆内存管理核心逻辑 超详细复习笔记

一、知识总览

这部分聚焦 手动实现的堆内存管理流程,从堆初始化、内存分配(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 字节。
  • 流程
    1. 遍历空闲链表:从 g_heap 开始,找第一个 size ≥ 100 + 头大小 的空闲块(需预留 “头” 的空间 )。
    2. 分割内存块
      • 若找到足够大的块(如初始块 size=0 ),分割为两部分:
        • 已分配块头|100(“头” 记录 size=100 + 头大小 ,用户实际用 100 字节 )。
        • 剩余空闲块:更新 g_heap 或链表,标记剩余空间为新空闲块。
    3. 返回指针:将已分配块的 “数据区指针”(跳过 “头” 的位置 )返回给用户(buf 指向 头|100 的数据区 )。

2. 多次分配

  • 需求示例
char *buf = my_malloc(100);
char *buf2 = my_malloc(50);
char *buf3 = my_malloc(100);

依次申请 100、50、100 字节。

  • 流程
    1. 第一次分配 100 字节:分割初始块,更新链表。
    2. 第二次分配 50 字节:遍历链表,找足够大的块(如剩余空闲块或新释放的块 ),分割出 头|50,更新链表。
    3. 第三次分配 100 字节:继续遍历,分割出 头|100,更新链表。
  • 关键:每次分配需遍历链表找 “足够大的块”,若找不到则分配失败(返回 NULL )。嵌入式场景需提前规划堆大小,避免分配失败。

(四)内存释放

1. 单次释放(以释放 100 字节为例)

  • 需求my_free(buf);,释放之前分配的 100 字节。
  • 流程
    1. 定位内存块头:通过 buf 回推找到 “头” 的位置(buf 指向数据区,需减去 “头” 的大小,得到 head 指针 )。
    2. 标记为空闲块:将该块的 size 标记为实际大小(含头 ),并将 next_free 指向 g_heap.next_free(插入空闲链表头部,或按地址排序插入,依实现逻辑 )。
    3. 合并相邻空闲块(可选 ):检查释放块的前后是否有空闲块,若有则合并,避免内存碎片(复杂实现需此步骤,简化版可能省略 )。
  • 关键:释放时必须准确找到 “头” 的位置,若 buf 非法(如未分配、已释放 ),会损坏堆结构,引发崩溃。
  •  

2. 释放与合并

  • 场景:释放块与前后空闲块相邻,需合并为更大空闲块,减少碎片。
  • 流程
    1. 释放块后,检查前一个块(prev )和后一个块(next )是否为空闲块(通过 size 或标记判断 )。
    2. 若 prev 是空闲块,合并 prev 和当前块;若 next 是空闲块,合并当前块和 next;若都有,合并三者为一个大空闲块。
  • 核心:合并碎片可提高后续分配成功率,避免因小碎片无法满足大申请而失败。

(五)简化线性分配

  • 逻辑:图展示简化的 “线性分配”,用 pos 标记当前分配位置,每次分配从 pos 开始,按需求 size 向后延伸(pos += size ),无法释放和碎片整理。
  • 优缺点
    • 优点:实现简单,适合无需释放、一次性分配的场景(如初始化时预分配 )。
    • 缺点:无法释放和复用内存,容易耗尽堆空间,仅适用于极简场景。
  • 对比:与链表式管理(支持分配 / 释放 / 合并 )相比,线性分配更简单但灵活度低,理解差异可选择合适策略。

三、知识串联(从初始化到分配 / 释放 )

  1. 初始化:堆初始为一个大空闲块,用 g_heap 管理,size 为总大小,next_free 为 NULL 。
  2. 分配my_malloc 遍历空闲链表,找到足够大的块,分割为 “已分配块” 和 “剩余空闲块”,更新链表;多次分配时,重复此过程,动态分割空闲块 。
  3. 释放my_free 定位内存块的 “头”,标记为空闲块,插入空闲链表;复杂实现会合并相邻空闲块,减少碎片 。
  4. 对比简化逻辑:线性分配无需链表,按顺序分配,无法释放,适合极简场景 。

四、易错点 & 补充说明

(一)易错点

  1. 指针回推错误:释放时无法正确回推 “头” 的位置(如 buf 被非法修改 ),会操作错误内存块,损坏堆结构,引发崩溃。
  2. 内存碎片忽视:释放时不合并相邻空闲块,频繁分配 / 释放小内存会导致大量碎片,后续大内存申请可能失败(即使总空闲空间足够,因碎片分散无法满足连续需求 )。
  3. 混淆 “已分配” 与 “空闲” 状态:分配后未正确标记块状态,或释放后未及时更新链表,会导致重复分配同一内存、空闲块丢失等问题,破坏堆完整性 。

(二)补充拓展

  • 堆管理算法差异:除 “空闲链表 + 合并” 外,还有 “伙伴系统”“ slab 分配器” 等高级算法,FreeRTOS 中 heap_4.c 使用 “最佳匹配 + 合并”,heap_5.c 支持跨内存区域分配,需依场景选择 。
  • 内存池与堆的区别:内存池是预分配固定大小的块,适合频繁分配相同大小内存的场景(如消息队列 ),相比堆更高效且无碎片;堆适合动态大小的分配,但需管理碎片 。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值