无Valgrind情况下排查与调试内存泄漏问题
内存泄漏排查问题
我们有一个基于IPC等待消息的程序,收到消息后会处理数据、调用数据库存储过程并返回结果。客户反馈每次处理消息内存会增长20KB,该程序每日需处理超百万条消息,因此必须修复此内存泄漏。
但该程序运行在专有应用服务器中,依赖大量专有库,无法单独运行,故无法使用Valgrind等工具。我编写了读取/proc/self/statm的PrintMemory函数来监控内存:
void PrintMemory(char *msg) { PREPARELOG(PrintMemory); FILE *fp = fopen("/proc/self/statm", "r"); if(fp == NULL) { LOG(FAI, "[PrintMemory] Cant open /proc/self/statm"); return; } long rss; fscanf(fp, "%*lu %ld", &rss); fclose(fp); LOG(NOT, "[PrintMemory] [%-30s] [%ld]", msg, rss); }
其中PREPARELOG是为LOG宏设置变量的宏。
日志显示:
[...] [PrintMemory] [Function 1 end ] [32633]. [PrintMemory] [Function 2 init ] [32637]. [...]
对应的代码:
ret = Function1(param); PrintMemory("Function 1 end"); if (ret == 0) { Function2(¶m1, ¶m2, param3, param4); }
int Function2(int *param1, int *param2, char *param3, struct name* param4) { PREPARELOG(Function2); PrintMemory("Function 2 init"); [...] }
对应的汇编代码:
call PrintMemory #Function 1 end .loc 1 282 0 movl -28(%rbp), %eax # ret, ret.1 testl %eax, %eax # ret.1 jne .L37 #, .loc 1 283 0 movq -72(%rbp), %rcx # tx_switch, tmp80 leaq -64(%rbp), %rdx #, tmp81 leaq -28(%rbp), %rsi #, tmp82 leaq -24(%rbp), %rax #, tmp83 movq %rax, %rdi # tmp83, call Function2 #
Function2: .LFB9: .loc 1 965 0 .cfi_startproc pushq %rbp # .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp #, .cfi_def_cfa_register 6 subq $64, %rsp #, movq %rdi, -24(%rbp) # param1, param1 movq %rsi, -32(%rbp) # param2, param2 movq %rdx, -40(%rbp) # param3, param3 movq %rcx, -48(%rbp) # param4, param4 .loc 1 969 0 movl $.LC56, %edi #, call PrintMemory # Function 2 init
现咨询以下问题:
- PrintMemory的输出是否准确?
- 为何调用Function2会导致内存增长3页(约12KB)?
- 在无法单独运行程序的情况下,还有哪些内存泄漏排查方法?
问题解答
1. PrintMemory的输出是否准确?
基本准确但存在局限性:
/proc/self/statm的第二个字段是RSS(常驻集大小),单位为内存页(通常每页4KB),读取逻辑正确,能反映进程实际占用的物理内存变化。- 但RSS包含进程共享的库内存、内核页等非独占内存,且内存分配后可能被系统缓存,不会立即释放,因此单次增长不能直接判定为泄漏,需观察多次消息处理后的趋势——若每次处理后内存持续增长且无回落,才是真正的泄漏。
- 格式匹配方面,
%*lu跳过总虚拟内存字段、读取第二个字段到rss的逻辑,在32位和64位系统中都能兼容,不会出现类型不匹配问题。
2. 为何调用Function2会导致内存增长3页(约12KB)?
从汇编代码看,Function2刚进入时仅分配了64字节栈空间(subq $64, %rsp),远达不到12KB,因此增长的内存并非来自栈:
- 最可能的原因是
PREPARELOG(Function2)宏的初始化操作。该宏内部可能存在动态内存分配(比如创建日志上下文、缓存日志字符串等),首次调用时触发分配,后续可能复用,但首次调用会产生内存增长。 - 其次是堆内存的首次触发生长:若
Function2或其依赖的代码首次调用malloc类函数,malloc会向系统申请更大的内存块(比如12KB),后续小分配从该块中取用,因此首次调用会导致RSS增长。 - 还有可能是专有库的懒加载:
Function2依赖的某个库函数首次被调用时,系统加载对应代码页到内存,导致RSS增长,这种增长是一次性的,后续调用不会再增加。
3. 在无法单独运行程序的情况下,还有哪些内存泄漏排查方法?
- 细化内存监控:在
Function2内部关键步骤前后添加PrintMemory,定位到具体代码行;同时解析/proc/self/smaps,它能拆分进程的内存区域(堆、栈、共享库、匿名映射等),可快速定位增长的内存类型,比如匿名映射持续增长则指向堆泄漏。 - 自定义内存分配钩子:若程序使用标准
malloc/free,可包装分配函数记录调用栈、大小和分配点,定期输出未释放的分配记录。示例包装逻辑:
可通过编译替换或LD_PRELOAD注入实现(若服务器允许)。void* my_malloc(size_t size, const char* file, int line) { void* ptr = malloc(size); // 将ptr、size、file、line记录到全局链表 return ptr; } #define malloc(s) my_malloc(s, __FILE__, __LINE__) - 对比内存快照:处理N条消息前后分别导出
/proc/self/maps和/proc/self/smaps,对比快照找到持续增长的内存区域,确定泄漏类型。 - 针对性代码审查:重点检查
Function2及后续调用函数中的动态内存操作,确认malloc、strdup、专有库分配接口等是否在所有分支(包括异常分支)中都完成了释放。 - 系统工具辅助:用
top、ps持续观察VSZ和RSS的增长趋势;用pmap -x <pid>查看内存分布;若服务器允许,用perf工具定位内存分配热点函数,找到频繁分配但未释放的代码。
内容的提问来源于stack exchange,提问作者ijzm
相关产品推荐
相关产品推荐

