三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

实战:用 memleax 揪出 C 程序内存泄漏的 4 个真实案例

实战:用 memleax 揪出 C 程序内存泄漏的 4 个真实案例

实战:用 memleax 揪出 C 程序内存泄漏的 4 个真实案例

【免费下载链接】memleaxdebugs memory leak of running process. Not maintained anymore, try `libleak` please.项目地址: https://gitcode.com/gh_mirrors/me/memleax

C 程序的内存泄漏(memory leak)是让无数开发者头疼的老大难问题:程序跑得越久,内存占用越高,最后要么 OOM 被杀,要么把服务器拖垮。传统做法是用 Valgrind 重跑一遍程序,但对于线上运行中的进程,重编译、重启的代价实在太大。memleax正是为解决这个痛点而生——它可以直接 attach(附加)到正在运行的进程上,无需重新编译、无需重启服务,就能实时揪出内存泄漏的调用栈。这篇文章通过 4 个真实案例,带你快速上手这款 C 程序内存泄漏检测利器。

memleax 是什么?凭什么能"贴"在运行中的进程上

memleax 的核心思路非常巧妙:它通过 ptrace 在目标进程的mallocfreecallocrealloc等内存分配/释放函数入口处动态设置断点(相关逻辑见源码 breakpoint.c),从而 hook 住每一次内存操作,并在内存块"存活"超过阈值后,把它的分配调用栈实时打印出来。

特性memleaxValgrind
是否重启进程不需要,直接 attach 运行中的进程需要由它重新启动目标程序
报告时机实时输出,边监控边看程序退出后统一报告
初始化阶段跳过,只统计 attach 之后的行为全部统计
运行速度取决于内存调用频率,通常更快虚拟 CPU 上执行,明显变慢
功能范围专注内存泄漏检测多种内存错误检测

简单说:memleax 轻量、实时、适合生产环境,而 Valgrind 更全面、更强大。两者不是替代关系,而是互补。

⚠️ 注意:memleax 会跟随新创建的线程,但不会跟随 fork 出来的子进程。要调试多个进程,请分别启动多个 memleax 实例。

案例一:HTTP 服务 keepalive 连接引发的"假泄漏"

问题现象

一位同学监控一个带 keepalive 的 HTTP 服务,刚跑起来 memleax,屏幕上立刻刷出一大片"memory expires"报告,看起来泄漏非常严重。

真实原因

keepalive 连接本身会存活较长时间,比如超过 5 分钟。而 memleax 的默认过期阈值(expire threshold)只有 10 秒——任何存活超过 10 秒的内存块都会被判定为"疑似泄漏"。连接缓冲区的内存块存活时间远超 10 秒,自然被误报。

解决方案

-e参数把阈值调大,覆盖连接的最长存活时间:

memleax -e 360 <target-pid>

如果连接最长 5 分钟,就设-e 360;如果程序预期 1 秒内释放所有内存,则设-e 2快速出结果。官方 README 反复强调:永远要根据你的业务场景主动设置-e,不要依赖默认值。

案例二:多线程下载器里"只分配不释放"的缓冲块

问题现象

一个多线程下载程序,每个线程为下载任务分配固定大小的缓冲,任务结束后却没有释放,程序内存随任务数线性增长。

定位过程

以 root 权限运行:

memleax <target-pid>

当缓冲块存活超过阈值后,memleax 实时输出这样的报告:

CallStack[3]: memory expires with 101 bytes, backtrace: 0x00007fd322bd8220 libc-2.17.so malloc()+0 0x000000000040084e test foo()+14 foo.c:12 0x0000000000400875 test bar()+37 bar.c:20 0x0000000000400acb test main()+364 test.c:80

报告解读

  • CallStack[3]是泄漏调用栈的编号;
  • 每一行是栈帧:地址、所属模块、函数名、文件:行号;
  • 如果同一调用栈再次泄漏,会简化为CallStack[3]: memory expires with 101 bytes, 2 times again,避免刷屏;
  • 若某块"过期内存"之后被释放,会提示expired-memory frees after 10 seconds——说明是"晚释放"而非真泄漏,可以放心排除。

直接把目光锁定到foo.c:12这一行,问题一目了然。这一眼能定位到源码行号的能力,来自 memleax 对 DWARF 调试行信息的解析(见 debug_line.c),前提是目标程序编译时带-g调试信息。

案例三:HTTPS 服务 OpenSSL 高频 malloc 的双重麻烦

问题现象

同样的服务,HTTP 场景下 memleax 对性能影响轻微;换成 HTTPS 后,程序明显变慢,同时出现大量泄漏报告。

原因分析

OpenSSL 在 TLS 握手和加解密过程中会极其频繁地调用 malloc()。每次内存调用都会触发一次断点陷阱(TRAP),性能开销与内存调用频率强相关(详见 memblock.c 的分配记录逻辑)。

应对策略

  1. -l限制回溯深度:-l默认 50(也是上限),调小可以显著降低开销,比如memleax -l 20 <target-pid>
  2. 合理设置-e,区分"OpenSSL 内部正常缓存"与"业务真泄漏";
  3. 观察"过期后又释放"的报告占比,判断是否为真泄漏。

另外提一句:如果生产环境对性能极度敏感,可以关注作者后续推出的libleak(基于 LD_PRELOAD hook 内存函数,性能影响小得多)——memleax 作者已明确表示不再维护 memleax,新项目推荐尝试 libleak。

案例四:长期运行服务"越用越卡"的隐性泄漏

问题现象

一个常驻守护进程,内存占用缓慢但持续攀升,几天后逼近上限。这类泄漏单靠top很难定位到代码位置。

实战步骤

第一步,attach 并设定合理阈值:

memleax -e 300 <target-pid>

第二步,等待足够长的时间。如果监控时间太短,memleax 退出时会提示:

== Your monitoring time is too short. 300 seconds is need.

所以监控时长至少要覆盖一个完整的业务周期,否则统计毫无意义。

第三步,按 Ctrl-C 停止监控,查看退出统计:

CallStack[3]: may-leak=20 (2020 bytes) expired=20 (2020 bytes), free_expired=0 (0 bytes) alloc=20 (2020 bytes), free=0 (0 bytes) freed memory live time: min=0 max=0 average=0 un-freed memory live time: max=20 ...backtrace...
  • may-leak:可能泄漏的块数 = expired − free_expired,这是最需要关注的数字;
  • expired/free_expired:过期总数与过期后被释放的数量,两者接近则多为"晚释放";
  • freed memory live time:已释放内存的存活时间统计,max 接近阈值说明释放偏晚;
  • un-freed memory live time: max:未释放内存的最长存活时间,越大越可疑。

如果某些调用栈泄漏过快,memleax 还会根据-m(单调用栈泄漏块数上限,默认 1000)和-c(泄漏调用栈数量上限,默认 1000)自动停止监控,防止失控。

从零安装:最简单的上手路径

方式一:编译安装(推荐)

memleax 依赖libunwindlibelflibdw(或libdwarf,没有的话可禁用调试行功能,但回溯将看不到文件:行号)。依赖装好后:

git clone https://gitcode.com/gh_mirrors/me/memleax cd memleax mkdir build && cd build cmake .. make sudo make install

源码结构很清晰:主流程在 memleax.c,内存块管理在 memblock.c,调用栈处理在 callstack.c。

方式二:发行版包

部分发行版提供现成包:Arch Linux 用户可在 AUR 安装,FreeBSD 用户可在 Ports Collection 安装,另有 DEB/RPM 包可用。

环境兼容性

  • GNU/Linux:x86、x86_64、armv7、aarch64(CentOS 7.2、Ubuntu 16.04 等实测);
  • FreeBSD:i386、amd64(10.3 实测)。

💡 小贴士:如果 aarch64 上无法显示函数回溯,可用 GCC 的-funwind-tables重新编译目标程序。

总结:什么场景下该选 memleax

你的处境推荐工具
生产环境、进程不能重启、追求实时报告memleax ✅
开发阶段、需要全面内存错误检测Valgrind
追求极致性能、可接受预加载libleak

memleax 的价值在于"不改代码、不重启、实时出报告",尤其适合线上排障。记住三条铁律:始终根据场景设置-e监控时长覆盖完整业务周期关注 may-leak 与 free_expired 的对比。掌握了这 4 个案例,下次遇到 C 程序内存泄漏,你就能沉着、快速地找到真凶了。🎯

【免费下载链接】memleaxdebugs memory leak of running process. Not maintained anymore, try `libleak` please.项目地址: https://gitcode.com/gh_mirrors/me/memleax

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

← 返回列表