You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无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(&param1, &param2, 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

现咨询以下问题:

  1. PrintMemory的输出是否准确?
  2. 为何调用Function2会导致内存增长3页(约12KB)?
  3. 在无法单独运行程序的情况下,还有哪些内存泄漏排查方法?

问题解答

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,可包装分配函数记录调用栈、大小和分配点,定期输出未释放的分配记录。示例包装逻辑:
    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__)
    
    可通过编译替换或LD_PRELOAD注入实现(若服务器允许)。
  • 对比内存快照:处理N条消息前后分别导出/proc/self/maps和/proc/self/smaps,对比快照找到持续增长的内存区域,确定泄漏类型。
  • 针对性代码审查:重点检查Function2及后续调用函数中的动态内存操作,确认malloc、strdup、专有库分配接口等是否在所有分支(包括异常分支)中都完成了释放。
  • 系统工具辅助:用top、ps持续观察VSZ和RSS的增长趋势;用pmap -x <pid>查看内存分布;若服务器允许,用perf工具定位内存分配热点函数,找到频繁分配但未释放的代码。

内容的提问来源于stack exchange,提问作者ijzm

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 21:30:53