星标公众号,让嵌入式知识 “投喂” 不停歇!

嵌入式项目一旦上到现场,最怕两类问题:偶现死机跑久了就“越跑越慢”。后者十有八九和内存泄漏/碎片有关,但现场通常只有串口日志、少量 Flash、没法挂 gdb、也没 Valgrind。

之前我们也分享内存泄漏监测工具:嵌入式系统内存泄漏检测利器:MTrace

不过这是开发态的工具,有朋友问有没有运维态的内存检测工具?

这篇文章我们就分享一个运维态的内存泄漏监测方案:dlmalloc

一、为什么要在“运维态”做泄漏检测?

开发阶段的泄漏,大多能靠单元测试 + 静态分析 + PC 端工具搞定。真正棘手的是这些场景:

  • 设备跑了几天甚至几周才慢慢耗光内存
  • 某些异常网络包 / 罕见业务流程才触发泄漏
  • 线下复现成本高,客户现场环境又复杂

这类问题有几个共同特点:

  1. 只在真实负载下出现,实验室难以完整模拟
  2. 触发条件组合很多,靠人工造数据基本抓不住
  3. 设备重启后现象消失,只能从零开始积累

所以,合理的思路是:在设备上常驻一套轻量级“内存监控”和“泄漏记录机制”,一旦出问题能给你留下证据。但这个检测机制又不能太重,要求轻量级、低开销、非侵入、无需调试器 / 源码修改,且不能影响业务运行。

dlmalloc 提供的 mallinfo / malloc_stats 再加一点追踪表,就能把这套基础设施搭起来。

二、dlmalloc 能提供哪些“观察点”?

1. dlmalloc 简介

dlmalloc是一个开源轻量级内存检测库。轻量级 malloc 实现(嵌入式主流),自带内存统计接口,无依赖,仅几百行代码。

http://gee.cs./dl/html/malloc.html

https://github.com/ennorehling/dlmalloc

dlmalloc 的头文件里有两个对运维非常有用的接口:

  • struct mallinfo mallinfo(void);

    返回当前堆的统计信息:总空间、已用、空闲块数、可回收空间等

  • void malloc_stats(void);

    直接把详细统计打印到 stderr(嵌入式一般会重定向到串口/日志)

mallinfo 的几个关键字段:

  • uordblks:当前总 已分配 字节数
  • fordblks:当前总 空闲 字节数
  • ordblks空闲块数量(配合 fordblks 可以大致估计碎片情况)
  • arena:从系统拿到的总堆空间

只靠这几个数字,你就能在设备上做一件重要的事:画出“内存水位曲线”

三、基于dlmalloc的运维态泄漏检测

运维态泄漏检测,可以分两层来做:

  • 微观层:在 malloc/free 外面包一层“带来源信息”的追踪表,最后列出“谁分配了没还”

  • 宏观层:用 mallinfo 盯住堆整体健康状况(是否持续上涨、碎片是否变多)

1. 在 malloc/free 外面挂一层“追踪表”

整体水位只能告诉你“在漏”,但是谁在漏还得靠更细粒度的记录。

思路:用宏把所有分配/释放包装一下,加上“来源位置”信息,放进一个小表里

#define USE_DL_PREFIX       // 使用 dlmalloc 前缀
#include"malloc.h"

// 追踪表,用于记录未释放的分配
typedefstruct {
    void   *ptr;        // 分配得到的指针地址
    size_t  size;       // 分配的字节数
    constchar *file;   // 分配发生的源文件名
    int     line;       // 分配发生的源代码行号
alloc_record_t;

#define MAX_RECORDS 256
staticalloc_record_t g_records[MAX_RECORDS];
staticint g_record_count = 0;

void *tracked_malloc(size_t size, constchar *file, int line){
    void *p = dlmalloc(size);
    if (p && g_record_count < MAX_RECORDS) {
        g_records[g_record_count].ptr  = p;
        g_records[g_record_count].size = size;
        g_records[g_record_count].file = file;
        g_records[g_record_count].line = line;
        g_record_count++;
    }
    return p;
}

voidtracked_free(void *ptr){
    if (!ptr) return;
    for (int i = 0; i < g_record_count; i++) {
        if (g_records[i].ptr == ptr) {
            g_records[i] = g_records[g_record_count - 1];
            g_record_count--;
            break;
        }
    }
    dlfree(ptr);
}

#define EM_MALLOC(sz)  tracked_malloc((sz), __FILE__, __LINE__)
#define EM_FREE(p)     tracked_free((p))

追踪结果如:

2. 用 mallinfo 做整体水位监控

先看一个最简单的“内存看门狗”任务:

#include"malloc.h"
#include<stdio.h>

typedefstruct {
    int peak_uordblks;   // 峰值已分配
    int last_uordblks;   // 上一次记录
mem_watch_t;

staticmem_watch_t g_mem_watch;

voidmemory_watchdog_task(void){
    structmallinfoinfo = mallinfo();

    if (info.uordblks > g_mem_watch.peak_uordblks) {
        g_mem_watch.peak_uordblks = info.uordblks;
    }

    // 简单策略:每次打印当前值和相对变化
    int delta = info.uordblks - g_mem_watch.last_uordblks;
    g_mem_watch.last_uordblks = info.uordblks;

    printf("[MEM] used=%dB free=%dB blocks=%d delta=%dB peak=%dBn",
           info.uordblks, info.fordblks, info.ordblks,
           delta, g_mem_watch.peak_uordblks);
}

把这个函数每秒、每分钟调一次,就能看到:

  • 某个业务场景结束后,uordblks 是否回落到接近初始值
  • 长时间运行后,uordblks 是否存在缓慢但持续上升

四、内存泄漏检测实战

1. 完整示例

leak_demo.c:

#include<stdio.h>
#include<string.h>

#define USE_DL_PREFIX       // 使用 dlmalloc 前缀
#include"malloc.h"
嵌入式运维态内存泄漏检测方案?

// 追踪表,用于记录未释放的分配
typedefstruct {
    void   *ptr;        // 分配得到的指针地址
    size_t  size;       // 分配的字节数
    constchar *file;   // 分配发生的源文件名
    int     line;       // 分配发生的源代码行号
alloc_record_t;

#define MAX_RECORDS 256
staticalloc_record_t g_records[MAX_RECORDS];
staticint g_record_count = 0;

void *tracked_malloc(size_t size, constchar *file, int line){
    void *p = dlmalloc(size);
    if (p && g_record_count < MAX_RECORDS) {
        g_records[g_record_count].ptr  = p;
        g_records[g_record_count].size = size;
        g_records[g_record_count].file = file;
        g_records[g_record_count].line = line;
        g_record_count++;
    }
    return p;
}

voidtracked_free(void *ptr){
    if (!ptr) return;
    for (int i = 0; i < g_record_count; i++) {
        if (g_records[i].ptr == ptr) {
            g_records[i] = g_records[g_record_count - 1];
            g_record_count--;
            break;
        }
    }
    dlfree(ptr);
}

#define EM_MALLOC(sz)  tracked_malloc((sz), __FILE__, __LINE__)
#define EM_FREE(p)     tracked_free((p))

// 故意制造泄漏
voidtest1(void){
    // 正常释放的缓冲区
    char *buf1 = (char *)EM_MALLOC(64);
    strcpy(buf1, "test1: buf1");
    EM_FREE(buf1);

    // 故意不释放的缓冲区
    char *buf2 = (char *)EM_MALLOC(128);
    strcpy(buf2, "test1: buf2");
}

// 故意制造泄漏
voidtest2(void){
    void *pkt = EM_MALLOC(512);
}

// 报告函数
voidreport_leaks(void){
    printf("n========== Memory Leak Report ==========n");
    if (g_record_count == 0) {
        printf("nNo leaks foundn");
        return;
    }

    // 统计总泄漏字节数
    size_t total = 0;
    for (int i = 0; i < g_record_count; i++) {
        printf(" [%d] %zu bytes @ %p  from %s:%dn",
               i + 1,
               g_records[i].size,
               g_records[i].ptr,
               g_records[i].file,
               g_records[i].line);
        total += g_records[i].size;
    }
    printf("nTotal leaked: %zu bytes (%.2f KB)n", total, total / 1024.0);
}

intmain(void){
    printf("n========== Memory Leak Test ==========n");
    test1();
    test2();

    structmallinfoinfo = mallinfo();
    printf("nCurrently allocated: %d bytesn", info.uordblks);

    report_leaks();
    
    return0;
}

编译运行:

gcc -O2 -DUSE_DL_PREFIX leak_demo.c malloc.c -o leak_demo
./leak_demo

这就已经是一个最小可用的“泄漏定位工具”了

  • 能告诉你 泄漏大小
  • 能指向具体 文件 + 行号
  • 不依赖 OS,不依赖 glibc 动态库

2. 实际使用需要的问题

追踪表大小受限

  • MAX_RECORDS 需要根据 RAM 调整,比如 64/128/256
  • 超出后可以:停止记录 / 覆盖旧记录 / 只记录最大块

不能一直开着,影响性能

  • 建议用 #ifdef MEM_LEAK_TRACE 控制
  • 只在“疑似有问题”的版本或现场定位阶段打开
  • 或者只对某几类模块宏替换为 EM_MALLOC/EM_FREE