Valgrind 内存与文件描述符泄漏排查指南
Valgrind 是 Linux 平台广泛使用的动态程序分析工具,能在运行期发现 C/C++ 程序的内存泄漏、未初始化读取、越界访问、重复释放,配合 --track-fds 还能确认文件描述符泄漏。本文整理它的原理、安装、两个典型场景的完整操作流程,以及一份对照官方手册核实过的常用选项速查表,供排查内存问题时快速查阅。
概述与原理
Valgrind 在一个虚拟 CPU 上模拟执行目标程序,实现对内存和系统调用的细粒度监控,核心用途包括:
- 内存泄漏(未释放已分配内存)
- 使用未初始化的内存
- 越界访问(如数组越界)
- 错误的内存释放操作(如重复释放)
- 辅助识别文件描述符(FD)泄漏:退出时列出仍打开的 FD,并打印程序自行打开的 FD 的调用栈,便于定位遗漏的
close()
最佳实践前提:被测程序应使用
-g编译选项保留调试符号,并避免优化(建议使用-O0),以确保错误报告具备准确的源码位置信息。
Memcheck(默认工具)
Memcheck 拦截所有内存分配/释放函数(malloc / free、new / delete),跟踪每块内存的状态,程序退出时分类报告泄漏类型:
- definitely lost:无法再访问的内存,明确泄漏
- indirectly lost:因父对象泄漏导致的子对象不可达
- possibly lost:可能因指针偏移导致的泄漏
- still reachable:程序结束时仍可通过某些指针访问的内存(通常非严重问题)
FD 跟踪
--track-fds=yes(3.16 起由 Memcheck 选项升级为核心选项,对所有工具生效)在程序终止时输出仍打开的 FD 列表,并对程序自行打开的 FD 打印打开位置的调用栈。注意:stdin/stdout/stderr 等继承自父进程的标准流没有调用栈,只有程序自己打开的 FD 才能定位到代码行。
安装
| 发行版 | 安装命令 |
|---|---|
| Ubuntu / Debian | sudo apt update && sudo apt install valgrind |
| RHEL / CentOS / Fedora | sudo dnf install valgrind |
| Arch Linux | sudo pacman -S valgrind |
验证安装:valgrind --version。
使用示例
示例程序(test.c)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
// 内存泄漏:分配但未释放
char *p = malloc(1024);
p[0] = 'A';
// FD 泄漏:打开文件但未关闭
int fd = open("/etc/passwd", O_RDONLY);
if (fd >= 0) {
char buf[10];
read(fd, buf, sizeof(buf));
// 忘记 close(fd);
}
return 0;
} 编译(必须包含调试信息):
gcc -g -O0 -o test test.c 检查内存泄漏
valgrind --tool=memcheck \
--leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
./test 关键参数:
| 参数 | 作用 |
|---|---|
--leak-check=full | 显示每处泄漏的完整调用栈 |
--show-leak-kinds=all | 报告所有类型的泄漏(definite/indirect/possible/reachable) |
--track-origins=yes | 追踪未初始化值的来源(显著增加开销,按需启用) |
典型输出片段:
==12345== HEAP SUMMARY:
==12345== in use at exit: 1,024 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 1,024 bytes allocated
==12345==
==12345== 1,024 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4848899: malloc (...)
==12345== by 0x10915B: main (test.c:6) 其中 definitely lost 表示必须修复的明确泄漏。
检查文件描述符泄漏
valgrind --tool=memcheck \
--track-fds=yes \
./test 输出示例:
==12346== FILE DESCRIPTORS: 4 open at exit.
==12346== Open file descriptor 0: /dev/pts/2 # stdin
==12346== Open file descriptor 1: /dev/pts/2 # stdout
==12346== Open file descriptor 2: /dev/pts/2 # stderr
==12346== Open file descriptor 3: /etc/passwd
==12346== at 0x493E7F5: open (...)
==12346== by 0x10919D: main (test.c:11) FD 0/1/2 为标准流,属正常;FD 3 对应 /etc/passwd,表明存在 FD 泄漏。
验证修复
修改代码,添加 free(p) 和 close(fd) 后重新运行:内存检查应显示 0 bytes in 0 blocks(无泄漏);FD 列表应仅包含 0/1/2。
常用选项速查
以下按用途分组整理常用选项,默认值与新旧选项等价关系均已对照 Valgrind 官方手册核实。
泄漏报告选项(Memcheck)
| 选项 | 默认值 | 作用 |
|---|---|---|
--leak-check=<no|summary|yes|full> | summary | 退出时是否搜索泄漏:summary 只输出一行计数汇总;full 逐条输出泄漏详情与分配位置的调用栈(yes 与 full 等价);no 关闭退出时的泄漏检查 |
--show-leak-kinds=<set> | definite,possible | 控制详细报告中显示哪些泄漏类型,取值为 definite / indirect / possible / reachable 的逗号组合,也可写 all 或 none。只影响「显示」,须配合 --leak-check=full 才有意义 |
--errors-for-leak-kinds=<set> | definite,possible | 控制哪些泄漏类型计入错误数(影响 ERROR SUMMARY 统计及 --error-exitcode 是否触发),与显示选项相互独立 |
--leak-check-heuristics=<set> | all | 启发式规则(stdstring / newarray / multipleinheritance / length64):把指向内存块内部的指针(如 std::string 的内部指针)判为可达,减少 possibly lost 误报。规则依赖特定 C++ 编译器的对象布局,结果需结合代码复核 |
--show-reachable=<yes|no> | no | 旧写法,等价关系见下方说明 |
--show-possibly-lost=<yes|no> | yes | 旧写法,等价关系见下方说明 |
新旧选项等价关系(--show-leak-kinds 是 Valgrind 3.9 起的统一写法,旧选项仍被接受):
--show-reachable=yes等价于--show-leak-kinds=all--show-reachable=no --show-possibly-lost=yes等价于--show-leak-kinds=definite,possible(即默认行为)--show-reachable=no --show-possibly-lost=no等价于--show-leak-kinds=definite
网上教程常见的
--show-possibly-lost=no --show-reachable=no就是「只看明确泄漏」的降噪写法,等价于--show-leak-kinds=definite;与--show-leak-kinds=all(首次排查看全貌)是两种不同策略。两种写法不建议混用,否则最终生效的显示集合需要按上面的等价关系换算才能确定。
输出控制(核心选项,对所有工具生效)
| 选项 | 默认值 | 作用 |
|---|---|---|
--log-file=<文件名> | 无(报告输出到 stderr) | 将报告写入指定文件,与被测程序自身的输出分开。文件名支持格式符:%p(替换为进程 PID,多进程程序可避免日志互相覆盖)、%n(本进程内递增的文件序号)、%q{VAR}(替换为环境变量 VAR 的值)、%%(字面 %);% 后跟其他字符会直接导致启动失败 |
--num-callers=<n> | 12 | 错误与泄漏报告中调用栈的最大层数(取值 2–500),深调用链定位不到分配点时可调大 |
-v / --verbose | 关闭 | 输出更多过程信息:加载的共享库、生效的抑制规则、各错误类型的统计等 |
--quiet | 关闭 | 静默模式,只输出错误信息,适合接自动化测试脚本 |
其他常用选项
| 选项 | 默认值 | 作用 |
|---|---|---|
--track-origins=<yes|no> | no | 追踪未初始化值的来源。官方手册给出的代价:速度约降为原来的一半、内存至少多占 100 MB,建议只在排查未初始化问题时开启 |
--error-exitcode=<退出码> | 0 | 发现错误时以指定退出码结束(默认原样返回被测程序的退出码),供脚本/CI 判断;泄漏是否算错误由 --errors-for-leak-kinds 决定 |
--tool=<工具名> | memcheck | 选择工具(cachegrind / callgrind / helgrind / drd / massif 等),使用默认的 memcheck 时可省略 |
--track-fds=<yes|no> | no | 退出时列出仍打开的 FD |
--suppressions=<文件> / --gen-suppressions=<yes|no|all> | — | 生成/加载抑制规则,见「进阶技巧」 |
常用组合
# 全量排查:所有泄漏类型 + 未初始化来源 + 报告落盘(按 PID 区分进程)
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \
--log-file=vg_%p.log ./test
# 降噪排查:只显示明确泄漏(等价于旧写法 --show-possibly-lost=no --show-reachable=no)
valgrind --leak-check=full --show-leak-kinds=definite ./test
# 脚本/CI 集成:发现问题即返回非零退出码
valgrind --leak-check=full --error-exitcode=1 ./test 注意事项与局限性
- 性能开销大:程序运行速度通常降低 10–50 倍,仅限开发/调试环境使用,禁止用于生产或性能测试
- FD 检查在程序退出时输出:程序自行打开的 FD 附带打开位置的调用栈,可直接定位;标准流除外
- 多线程支持良好,但在高并发竞争条件下,报告可能受执行顺序影响
- 不兼容静态链接的 glibc:建议使用动态链接方式编译程序
进阶技巧
抑制第三方库误报
生成抑制规则文件以忽略已知非问题项:
valgrind --gen-suppressions=all ./test 2> my.supp 后续运行时加载该文件:
valgrind --suppressions=my.supp ./test 与 GDB 联合调试
启动 Valgrind 并启用 GDB 支持:
valgrind --vgdb=yes --vgdb-error=0 ./test 在另一终端中:
gdb ./test
(gdb) target remote | vgdb 即可在发生内存错误时中断并进行交互式调试。
要点
- 先
-g -O0编译再测,否则调用栈定位不准 - 首次全量看
--leak-check=full --show-leak-kinds=all,日常降噪用--show-leak-kinds=definite - FD 泄漏用
--track-fds=yes,调用栈只对程序自己打开的 FD 有效 - 接 CI 用
--error-exitcode=1,让泄漏直接 fail 构建
本文基于 Valgrind 的标准行为编写,适用于主流 Linux 发行版上的 Valgrind 3.15+,具体输出格式可能因版本略有差异。