堆内存管理:malloc/free原理、堆内存分配策略、常见堆实现

大家好,我是你们的嵌入式安全讲师。今天我们来聊聊堆内存管理。说实话,堆这块是很多嵌入式工程师的“噩梦”——内存泄漏、碎片化、野指针,我当年刚入行时也被折腾得够呛。但搞懂 malloc 和 free 背后的原理,很多问题就迎刃而解了。

一、malloc/free 到底干了什么?

很多人以为 malloc 就是“向操作系统要一块内存”,free 就是“还回去”。其实没那么简单。在嵌入式裸机或 RTOS 环境下,堆管理是由一个叫做“堆管理器”的模块来完成的。malloc 和 free 只是它的两个接口。

我个人习惯把堆管理器想象成一个“仓库管理员”。你 malloc(100),管理员就去仓库里找一块至少 100 字节的空闲区域,划给你用,然后在账本上记一笔。你 free(ptr),管理员就把这块区域重新标记为“空闲”,并尝试跟旁边的空闲区域合并。

核心要点: malloc 不初始化内存,free 不清理内存内容。你 free 之后,那块内存里的数据可能还在,只是被标记为可用。这就是安全漏洞的温床。

二、堆内存分配策略

堆管理器怎么找空闲块?不同的策略,性能差异很大。我遇到过一些项目,因为选错了策略,导致系统运行几天后突然卡死——堆碎片化太严重了。

1. 首次适应(First Fit)

从堆头开始找,找到第一个大小足够的空闲块就分配。优点是速度快,缺点是容易把大块切碎,产生很多小碎片。

2. 最佳适应(Best Fit)

遍历所有空闲块,找到大小最接近请求的那个。碎片最少,但遍历开销大。我记得有一次调试一个音频处理模块,用 Best Fit 后性能下降明显,后来换成了 Next Fit 才解决。

3. 最差适应(Worst Fit)

找最大的空闲块,切出一块给你。目的是保留中等大小的块,但实际效果一般,容易把大块浪费掉。

4. 快速适应(Fast Fit / Segregated Fit)

把空闲块按大小分类,比如 16 字节、32 字节、64 字节……各自维护一个链表。分配时直接去对应大小的链表里取。速度极快,但内存利用率稍低。很多商业 RTOS 的堆实现都用了这个思路。

策略 分配速度 碎片程度 适用场景
First Fit 小内存、频繁分配
Best Fit 大内存、对碎片敏感
Worst Fit 中等 中等 不常用
Fast Fit 极快 中等 实时系统、固定大小分配

三、常见堆实现:dlmalloc 与 tlsf

讲完策略,我们看看两个经典的堆实现。这两个我在 NXP 芯片上都实际用过,各有千秋。

1. dlmalloc(Doug Lea's malloc)

这是 Linux glibc 早期版本的默认 malloc 实现。它使用边界标记法(Boundary Tag),每个内存块的头尾都保存了大小信息。free 的时候可以快速合并相邻空闲块。

dlmalloc 的优点是通用性强,代码成熟。但它的实时性不好——最坏情况下的执行时间不可预测。我曾经在一个 NXP i.MX RT 系列的项目里用 dlmalloc,结果发现中断服务程序里调用 malloc 有时会卡几十微秒,直接导致音频丢帧。后来我换成了 tlsf。

避坑指南: 我曾经在中断里调用 malloc,结果死锁了。因为 dlmalloc 内部用了锁,而中断优先级高于任务,导致锁无法释放。记住:中断里别调 malloc!

2. tlsf(Two-Level Segregated Fit)

tlsf 是专门为实时系统设计的堆分配器。它的核心思想是用两级位图来管理空闲块:第一级按 2 的幂次分桶(比如 16-31、32-63...),第二级在每个桶内再细分成 4 等份。分配时,通过位图运算直接定位到合适的空闲块,时间复杂度是 O(1)。

tlsf 的实时性极好,最坏情况下的执行时间也是常数。我在 NXP S32K 系列的车规芯片上用过 tlsf,配合 FreeRTOS,内存分配延迟稳定在 1 微秒以内。嗯,这里要注意:tlsf 的内存开销比 dlmalloc 大一些,因为它需要维护位图和二级链表。

警告: tlsf 虽然快,但它的内存池需要预先分配好。你不能像 dlmalloc 那样动态扩展堆空间。在资源受限的 MCU 上,要提前算好最大内存需求。

四、知识体系结构图

下面我用一张 SVG 图来总结本章的核心逻辑。你想想看,堆管理其实就三件事:怎么找、怎么分、怎么合。

堆内存管理核心逻辑 分配策略 堆实现 安全与避坑 First Fit / Best Fit Worst Fit / Fast Fit dlmalloc(通用型) tlsf(实时型) 中断中禁止 malloc free 后指针置 NULL 选择策略 → 选择实现 → 注意安全

五、实战建议

最后,我给大家几个实战中的小建议:

  • 尽量静态分配:在嵌入式系统中,能用全局数组或静态变量解决的问题,就别用 malloc。堆是最后的手段。
  • 配对检查:每次 malloc 都要有对应的 free,而且要在同一个层级。我见过太多“在函数 A 里 malloc,在函数 B 里 free”的代码,最后内存泄漏得一塌糊涂。
  • 监控堆使用量:在 NXP 芯片上,你可以定期调用 mallinfo() 或自定义的堆统计函数,看看还有多少空闲内存。如果空闲内存持续下降,说明有泄漏。
  • 考虑内存保护单元(MPU):如果芯片支持 MPU,可以把堆区域设置为不可执行,防止堆溢出攻击。这个我们在后面的安全章节会详细讲。
个人经验: 我习惯在项目初期就封装一层内存管理接口,比如 my_malloc、my_free,里面加上调试信息和统计功能。这样后期排查问题会轻松很多。别等到内存泄漏了再临时加,那时候代码已经改不动了。

好了,堆内存管理就讲到这里。记住:理解原理比背代码更重要。你搞懂了 malloc 和 free 是怎么找块、怎么切块、怎么合并的,遇到问题就能快速定位。下一章我们会讲内存泄漏的检测方法,到时候这些知识都会用上。


公众号:蓝海资料掘金营,微信deep3321