星標公眾號,讓嵌入式知識 “投喂” 不停歇!
嵌入式項目一旦上到現場,最怕兩類問題:偶現死機和跑久了就“越跑越慢”。后者十有八九和內存泄漏/碎片有關,但現場通常只有串口日志、少量 Flash、沒法掛 gdb、也沒 Valgrind。
之前我們也分享內存泄漏監測工具:嵌入式系統內存泄漏檢測利器:MTrace
不過這是開發態的工具,有朋友問有沒有運維態的內存檢測工具?

這篇文章我們就分享一個運維態的內存泄漏監測方案:dlmalloc。
開發階段的泄漏,大多能靠單元測試 + 靜態分析 + PC 端工具搞定。真正棘手的是這些場景:
這類問題有幾個共同特點:
所以,合理的思路是:在設備上常駐一套輕量級“內存監控”和“泄漏記錄機制”,一旦出問題能給你留下證據。但這個檢測機制又不能太重,要求輕量級、低開銷、非侵入、無需調試器 / 源碼修改,且不能影響業務運行。
dlmalloc 提供的 mallinfo / malloc_stats 再加一點追蹤表,就能把這套基礎設施搭起來。
dlmalloc是一個開源輕量級內存檢測庫。輕量級 malloc 實現(嵌入式主流),自帶內存統計接口,無依賴,僅幾百行代碼。
http://gee.cs.oswego.edu/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:從系統拿到的總堆空間只靠這幾個數字,你就能在設備上做一件重要的事:畫出“內存水位曲線”。
運維態泄漏檢測,可以分兩層來做:
微觀層:在 malloc/free 外面包一層“帶來源信息”的追蹤表,最后列出“誰分配了沒還”
宏觀層:用 mallinfo 盯住堆整體健康狀況(是否持續上漲、碎片是否變多)
整體水位只能告訴你“在漏”,但是誰在漏還得靠更細粒度的記錄。
思路:用宏把所有分配/釋放包裝一下,加上“來源位置”信息,放進一個小表里。
#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))
追蹤結果如:

先看一個最簡單的“內存看門狗”任務:
#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=%dB\n",
info.uordblks, info.fordblks, info.ordblks,
delta, g_mem_watch.peak_uordblks);
}
把這個函數每秒、每分鐘調一次,就能看到:
uordblks 是否回落到接近初始值uordblks 是否存在緩慢但持續上升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 found\n");
return;
}
// 統計總泄漏字節數
size_t total = 0;
for (int i = 0; i < g_record_count; i++) {
printf(" [%d] %zu bytes @ %p from %s:%d\n",
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 bytes\n", info.uordblks);
report_leaks();
return0;
}
編譯運行:
gcc -O2 -DUSE_DL_PREFIX leak_demo.c malloc.c -o leak_demo
./leak_demo


這就已經是一個最小可用的“泄漏定位工具”了:
追蹤表大小受限
MAX_RECORDS 需要根據 RAM 調整,比如 64/128/256不能一直開著,影響性能
#ifdef MEM_LEAK_TRACE 控制EM_MALLOC/EM_FREE
以上就是本次的分享,如果文章對你有幫助,歡迎轉發!
猜你喜歡:
嵌入式中代碼執行時間測量的幾種方法!
一個可應用于嵌入式的輕量級單元測試框架!
嵌入式設備通信序列化的正確打開方式!
嵌入式輕量級命令行shell庫!
點擊閱讀原文,查看我給大家篩選的熱門嵌入式書籍!