在嵌入式项目部署到现场后,最常见的问题包括偶发性死机和运行时间长了后性能下降。后者往往与内存泄漏或内存碎片有关,但现场环境通常只有串口日志和有限的Flash存储,无法使用gdb或Valgrind等工具进行调试。
之前我们分享过开发阶段的内存泄漏检测工具:嵌入式系统内存泄漏检测利器:MTrace。但许多开发者询问是否有适用于运维阶段的检测方案。

本文将分享一个运维态的内存泄漏监测方案:dlmalloc,它能够在不影响系统运行的前提下实现轻量级内存监控。
一、为什么需要在运维阶段进行泄漏检测?
开发阶段的泄漏问题通常可以通过单元测试、静态分析和PC端工具解决。真正棘手的是以下场景:
- 设备运行数天甚至数周后才逐渐耗尽内存
- 特定异常网络包或罕见业务流程触发泄漏
- 线下复现成本高,客户现场环境复杂
这类问题具有共同特征:
- 仅在真实负载下出现,实验室环境难以完整模拟
- 触发条件组合多样,人工造数据难以捕捉
- 设备重启后现象消失,需要重新积累数据
因此,合理方案是在设备上常驻一套轻量级内存监控和泄漏记录机制,能够在问题发生时保留证据。这个检测机制需要满足轻量级、低开销、非侵入、无需调试器或源码修改的要求,且不能影响业务正常运行。
dlmalloc提供的mallinfo/malloc_stats接口结合追踪表,可以搭建这样的基础设施。
二、dlmalloc能提供哪些监控能力?
1. dlmalloc简介
dlmalloc是一个开源轻量级内存分配库,专为嵌入式内存管理设计。它具有内存统计接口、无外部依赖、代码量仅几百行的特点。
官方资源:
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:从系统获取的总堆空间
通过这些数据,可以在设备上实现内存水位曲线的绘制和监控。
三、基于dlmalloc的运维态泄漏检测方案
运维态泄漏检测可分为两个层次:
- 微观层:在malloc/free外层包装带来源信息的追踪表,最终列出未释放的分配
- 宏观层:使用mallinfo监控堆的整体健康状况(是否持续增长、碎片是否增加)
1. 实现malloc/free的追踪表
整体水位监控只能提示存在泄漏,但要定位具体泄漏源需要更细粒度的记录。
方案:使用宏包装所有分配/释放操作,添加来源位置信息并存入小型追踪表。
#define USE_DL_PREFIX // 使用 dlmalloc 前缀
#include "malloc.h"
// 追踪表,用于记录未释放的分配
typedef struct {
void *ptr; // 分配得到的指针地址
size_t size; // 分配的字节数
const char *file; // 分配发生的源文件名
int line; // 分配发生的源代码行号
} alloc_record_t;
#define MAX_RECORDS 256
static alloc_record_t g_records[MAX_RECORDS];
static int g_record_count = 0;
void *tracked_malloc(size_t size, const char *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;
}
void tracked_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>
typedef struct {
int peak_uordblks; // 峰值已分配
int last_uordblks; // 上一次记录
} mem_watch_t;
static mem_watch_t g_mem_watch;
void memory_watchdog_task(void)
{
struct mallinfo info = 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是否存在缓慢但持续上升的趋势
四、内存泄漏检测实战演示
1. 完整代码示例
leak_demo.c:
#include <stdio.h>
#include <string.h>
#define USE_DL_PREFIX // 使用 dlmalloc 前缀
#include "malloc.h"
// 追踪表,用于记录未释放的分配
typedef struct {
void *ptr; // 分配得到的指针地址
size_t size; // 分配的字节数
const char *file; // 分配发生的源文件名
int line; // 分配发生的源代码行号
} alloc_record_t;
#define MAX_RECORDS 256
static alloc_record_t g_records[MAX_RECORDS];
static int g_record_count = 0;
void *tracked_malloc(size_t size, const char *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;
}
void tracked_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))
// 故意制造泄漏
void test1(void)
{
// 正常释放的缓冲区
char *buf1 = (char *)EM_MALLOC(64);
strcpy(buf1, "test1: buf1");
EM_FREE(buf1);
// 故意不释放的缓冲区
char *buf2 = (char *)EM_MALLOC(128);
strcpy(buf2, "test1: buf2");
}
// 故意制造泄漏
void test2(void)
{
void *pkt = EM_MALLOC(512);
}
// 报告函数
void report_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);
}
int main(void)
{
printf("\n========== Memory Leak Test ==========\n");
test1();
test2();
struct mallinfo info = mallinfo();
printf("\nCurrently allocated: %d bytes\n", info.uordblks);
report_leaks();
return 0;
}
编译运行命令:
gcc -O2 -DUSE_DL_PREFIX leak_demo.c malloc.c -o leak_demo
./leak_demo
运行结果示例:

这已经构成了一个最小可用的泄漏定位工具:
- 能够报告泄漏大小
- 能够定位到具体文件和行号
- 不依赖操作系统或glibc动态库
2. 实际应用中的注意事项
追踪表大小限制
- MAX_RECORDS需要根据可用RAM调整,如64/128/256
- 超出限制后可选择:停止记录、覆盖旧记录或仅记录最大块
性能影响控制
- 建议使用
#ifdef MEM_LEAK_TRACE控制开关
- 仅在疑似问题版本或现场定位阶段启用
- 或仅对特定模块使用EM_MALLOC/EM_FREE宏替换

通过这套系统监控方案,可以在不影响嵌入式设备正常运行的前提下,有效检测和定位内存泄漏问题,为现场故障排查提供有力工具。