1. 内存泄漏基础概念

各位同学好,我是老周。做嵌入式调试十几年了,今天咱们聊聊内存泄漏——这个让无数嵌入式工程师头疼的问题。

说实话,我见过太多项目因为内存泄漏翻车了。有的产品上线三个月开始随机死机,有的跑着跑着就卡死,还有的干脆启动不了。嗯,这些问题的根源,十有八九都是内存泄漏。

1.1 什么是内存泄漏

内存泄漏,说白了就是:你申请了一块内存,用完之后忘了还回去。

在C语言里,我们用 malloc()、calloc()、realloc() 申请堆内存。用完之后,必须用 free() 释放。如果你只申请不释放,这块内存就永远被占着,谁也拿不走。

核心定义:内存泄漏是指动态分配的内存对象,在生命周期结束后,由于程序逻辑错误导致无法被释放,从而永久占用堆空间的现象。

看个最简单的例子:

void leak_example(void)
{
    char *buf = (char *)malloc(1024);
    // 使用 buf 处理数据...
    // 忘记调用 free(buf);
    return;  // buf 泄漏了!
}

每次调用这个函数,就泄漏 1KB。一天调用一万次呢?那就是 10MB。嵌入式设备总共才几十MB内存,几天就爆了。

我在项目中遇到过最离谱的一次——一个通信协议栈,每次收到心跳包就泄漏 64 字节。设备运行三个月后,内存耗尽,整机重启。查了整整一周才定位到问题。

1.2 内存泄漏 vs 内存溢出

这两个概念经常被搞混。我面试的时候,十个候选人里有六七个分不清。咱们一次说清楚。

对比维度 内存泄漏 内存溢出
本质 申请了没释放 访问了不该访问的区域
表现 可用内存逐渐减少 程序崩溃、异常复位
发生时机 持续累积,缓慢恶化 瞬间发生,立即崩溃
典型场景 忘记 free()、丢失指针 数组越界、栈溢出、野指针
调试难度 极难定位,需要长时间观察 相对容易,崩溃点即线索
后果 系统逐渐变慢,最终死机 立即死机或数据损坏

举个例子你就明白了:

  • 内存泄漏:你借了本书,看完不还。图书馆的书越来越少,别人借不到。
  • 内存溢出:你借了本书,却跑到图书馆的仓库里乱翻,把书架推倒了。

两者经常同时出现。泄漏多了,可用内存变少,再申请就容易溢出。我见过一个案例:泄漏导致堆碎片化严重,最后 malloc() 返回了 NULL,程序没检查就直接写数据——砰,内存溢出,系统复位。

避坑指南:我曾经接手过一个项目,工程师把泄漏和溢出混为一谈。他花了两个月在找溢出点,实际上问题是泄漏导致的堆耗尽。方向错了,再努力也是白费。所以,第一步就要分清你面对的是哪种问题。

1.3 嵌入式系统的特殊性

你可能会想:内存泄漏不是PC上也有吗?有什么特别的?

区别大了去了。嵌入式系统有它自己的脾气。

1.3.1 资源极度受限

PC上有几十GB内存,泄漏几百KB根本感觉不到。嵌入式设备呢?

  • 低端MCU:4KB ~ 64KB RAM
  • 中端MCU:64KB ~ 512KB RAM
  • 高端MPU:128MB ~ 1GB RAM

泄漏 1KB 在PC上不算事,在 32KB 的MCU上,那就是 3% 的内存没了。泄漏个几十次,系统直接罢工。

1.3.2 没有操作系统兜底

PC上的程序退出后,操作系统会回收所有内存。嵌入式系统呢?

  • 裸机程序:跑起来就不停,泄漏的内存永远回不来
  • RTOS:任务退出时,OS不会自动释放任务申请的堆内存

你想想看,一个设备可能连续运行几个月甚至几年。泄漏一点点,日积月累就是灾难。

1.3.3 调试手段有限

PC上有 Valgrind、AddressSanitizer 这些神器。嵌入式呢?

  • 很多MCU不支持MMU,没有内存保护
  • GDB 远程调试带宽有限,跑不起来
  • printf 打印会改变时序,掩盖问题
  • 堆实现各不相同,有的甚至没有标准 malloc

说白了,嵌入式调试就像戴着镣铐跳舞。你没法用PC上的那套工具链,得靠自己的经验和技巧。

1.3.4 实时性要求

很多嵌入式系统有严格的实时性要求。比如:

  • 电机控制:控制周期 100μs
  • 音频处理:缓冲区必须在 1ms 内填满
  • 通信协议:响应超时 10ms

内存泄漏会导致堆碎片化,malloc() 的执行时间越来越长。本来 50μs 能完成的操作,可能变成 200μs。实时任务就超时了。

经验之谈:我个人习惯在嵌入式项目中尽量使用静态分配。能不用堆就不用堆。实在要用,也要严格控制分配时机——只在初始化阶段分配,运行时不再动态申请。这样能从根本上避免泄漏。

1.4 知识体系总览

下面这张图,把本章的核心知识点串起来了。你仔细看看,心里有个框架。

内存泄漏知识体系 内存泄漏 申请未释放 vs 内存溢出 嵌入式特殊性 malloc/free 指针丢失 数组越界 栈溢出 资源受限 无OS兜底 调试手段有限 实时性要求 嵌入式内存泄漏 后果更严重,调试更困难

1.5 本章小结

咱们这一章,把内存泄漏的底子打好了:

  • 内存泄漏:申请了堆内存,用完后没释放。日积月累,系统崩溃。
  • 内存溢出:访问了不该访问的内存区域。瞬间崩溃,当场暴毙。
  • 嵌入式特殊性:资源少、没OS兜底、调试难、有实时性要求。同样的泄漏,在嵌入式上后果严重十倍。

记住一句话:嵌入式开发,内存管理是第一要务。后面我们会一步步深入,教你如何发现泄漏、定位泄漏、修复泄漏。这些都是我十几年实战中踩过的坑、总结的经验,希望能帮你少走弯路。