Skip to content

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) ​

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;
}

编译(必须包含调试信息):

bash
gcc -g -O0 -o test test.c

检查内存泄漏 ​

bash
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 表示必须修复的明确泄漏。

检查文件描述符泄漏 ​

bash
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> — 生成/加载抑制规则,见「进阶技巧」

常用组合 ​

bash
# 全量排查:所有泄漏类型 + 未初始化来源 + 报告落盘(按 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:建议使用动态链接方式编译程序

进阶技巧 ​

抑制第三方库误报 ​

生成抑制规则文件以忽略已知非问题项:

bash
valgrind --gen-suppressions=all ./test 2> my.supp

后续运行时加载该文件:

bash
valgrind --suppressions=my.supp ./test

与 GDB 联合调试 ​

启动 Valgrind 并启用 GDB 支持:

bash
valgrind --vgdb=yes --vgdb-error=0 ./test

在另一终端中:

bash
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+,具体输出格式可能因版本略有差异。

最近更新

基于 VitePress 构建