星标公众号,让嵌入式知识 “投喂” 不停歇!
嵌入式项目一旦上到现场,最怕两类问题:偶现死机和跑久了就“越跑越慢”。后者十有八九和内存泄漏/碎片有关,但现场通常只有串口日志、少量 Flash、没法挂 gdb、也没 Valgrind。
之前我们也分享内存泄漏监测工具:嵌入式系统内存泄漏检测利器:MTrace
不过这是开发态的工具,有朋友问有没有运维态的内存检测工具?
这篇文章我们就分享一个运维态的内存泄漏监测方案:dlmalloc。
一、为什么要在“运维态”做泄漏检测?
开发阶段的泄漏,大多能靠单元测试 + 静态分析 + PC 端工具搞定。真正棘手的是这些场景:
-
设备跑了几天甚至几周才慢慢耗光内存 -
某些异常网络包 / 罕见业务流程才触发泄漏 -
线下复现成本高,客户现场环境又复杂
这类问题有几个共同特点:
-
只在真实负载下出现,实验室难以完整模拟 -
触发条件组合很多,靠人工造数据基本抓不住 -
设备重启后现象消失,只能从零开始积累
所以,合理的思路是:在设备上常驻一套轻量级“内存监控”和“泄漏记录机制”,一旦出问题能给你留下证据。但这个检测机制又不能太重,要求轻量级、低开销、非侵入、无需调试器 / 源码修改,且不能影响业务运行。
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