9、内存泄漏排查:Valgrind与AddressSanitizer实战
内存泄漏,说白了就是程序申请了内存,用完后没还给系统。
你想想看,一个长期运行的服务,每天漏一点,一周后可能就崩了。我在项目中遇到过好几次线上事故,最后定位都是内存泄漏。今天我们就来聊聊怎么用 Valgrind 和 AddressSanitizer 这两个利器,把泄漏揪出来。
9.1 内存泄漏的本质
先看一个最简单的泄漏例子:
void leak_example() {
int *p = (int *)malloc(10 * sizeof(int));
// 忘记 free(p)
return;
}
每次调用这个函数,就泄漏 40 字节。如果每秒调用一次,一天下来就是 3.3MB。嗯,看起来不多?但如果是嵌入式设备,这点内存可能就要命了。
核心要点:内存泄漏 = 申请 + 丢失指针 + 无法释放
9.2 Valgrind 实战
Valgrind 是我最早接触的内存检测工具。它通过模拟 CPU 来检测内存操作,精度很高,但代价是程序会跑得慢很多。
9.2.1 基本用法
// test_leak.c
#include <stdlib.h>
void func1() {
char *buf = (char *)malloc(100);
// 泄漏 100 字节
}
void func2() {
int *arr = (int *)malloc(200 * sizeof(int));
// 泄漏 800 字节
}
int main() {
func1();
func2();
return 0;
}
编译时记得加 -g 选项,保留调试信息:
gcc -g -o test_leak test_leak.c
valgrind --leak-check=full ./test_leak
输出结果会告诉你:
- 哪里申请的内存
- 泄漏了多少字节
- 调用栈信息
个人习惯:我一般会加上 --show-leak-kinds=all 参数,这样能显示所有类型的泄漏,包括"仍可访问"的泄漏。
9.2.2 常见泄漏类型
| 类型 | 含义 | 严重程度 |
|---|---|---|
| definitely lost | 确定泄漏,指针丢失 | 高 |
| indirectly lost | 间接泄漏,结构体内部泄漏 | 高 |
| possibly lost | 可能泄漏,指针被覆盖 | 中 |
| still reachable | 指针还在,但未释放 | 低 |
我曾经在一个网络协议栈里遇到 indirectly lost 的问题。链表节点释放了,但节点里的数据缓冲区没释放。Valgrind 直接帮我定位到了具体行号。
9.2.3 Valgrind 的局限性
说实话,Valgrind 有两个痛点:
- 慢。 程序运行速度会下降 10-20 倍。我有个项目跑一次要 3 小时,用 Valgrind 得等一天。
- 内存占用高。 它本身就需要大量内存来记录信息。
所以,对于大型项目,我更喜欢用 AddressSanitizer。
9.3 AddressSanitizer 实战
AddressSanitizer(简称 ASan)是 Google 开发的工具,编译时插入检测代码,运行时检查。速度比 Valgrind 快很多,大概只慢 2-3 倍。
9.3.1 基本用法
gcc -g -fsanitize=address -o test_asan test_leak.c
./test_asan
就这么简单。运行时如果发生泄漏,程序会直接报错退出,并打印详细信息。
注意:ASan 默认会在程序退出时报告泄漏。如果想在运行过程中就检测,可以设置环境变量 ASAN_OPTIONS=detect_leaks=1。
9.3.2 实战案例:循环泄漏
void loop_leak() {
for (int i = 0; i < 100; i++) {
char *p = (char *)malloc(1024);
// 每次循环泄漏 1KB
}
}
int main() {
loop_leak();
return 0;
}
用 ASan 跑一下,输出会告诉你:
- 泄漏总大小:100KB
- 泄漏发生的位置:loop_leak 函数
- 调用栈:main -> loop_leak -> malloc
嗯,这里要注意:ASan 的泄漏报告默认只显示一次。如果你想看每次泄漏的详细信息,可以加 ASAN_OPTIONS=leak_check_at_exit=false。
9.3.3 与 Valgrind 的对比
| 特性 | Valgrind | AddressSanitizer |
|---|---|---|
| 速度 | 慢 10-20 倍 | 慢 2-3 倍 |
| 内存占用 | 高 | 中等 |
| 检测精度 | 极高 | 高 |
| 使用方式 | 直接运行 | 重新编译 |
| 适用场景 | 小规模、精确分析 | 大规模、持续集成 |
我的建议:开发阶段用 ASan,因为它快,能快速反馈。发布前再用 Valgrind 做一次全面检查,确保万无一失。
9.4 避坑指南
我曾经踩过的坑:
- 在信号处理函数里 malloc,但没 free。信号处理函数执行频率低,泄漏不明显,但积累久了就出问题。
- 第三方库的内存泄漏。用 Valgrind 时记得加
--suppressions参数过滤掉库的泄漏,只看自己的代码。 - 多线程环境下的泄漏。ASan 默认支持多线程,但 Valgrind 需要加
--tool=helgrind或--tool=drd。
9.5 知识体系图
下面这张图总结了内存泄漏排查的核心流程:
9.6 实战总结
内存泄漏排查,说白了就是两件事:
- 找到泄漏点。 Valgrind 和 ASan 都能帮你做到。
- 修复泄漏。 这需要你理解代码逻辑,确保每个 malloc 都有对应的 free。
我个人习惯在项目的 Makefile 里加一个 asan 编译目标,平时开发就用它。发布前再用 Valgrind 跑一遍回归测试。这样既能保证效率,又能确保质量。
记住:内存泄漏不是 bug,是债务。今天不还,明天连本带利。
公众号:蓝海资料掘金营,微信deep3321