内存追踪
内存追踪
📚 本节导读
学习时长: 约 20 分钟
难度级别: ⭐⭐☆☆☆
前置知识: 4.4.1 堆内存管理、4.4.4 内存池
内存追踪是 OneOS 提供的内存调试工具,用于检测内存泄漏、定位内存越界和验证堆完整性。通过启用 OS_USING_MEM_TRACE 宏,可以在每个分配的内存块中记录分配该内存的任务信息,从而追踪内存的来源和归属。
🎯 学习目标
- 理解内存追踪的工作原理
- 掌握内存追踪 API 的使用方法
- 了解堆完整性检查的机制
- 掌握内存调试的最佳实践
一、配置
#define OS_USING_HEAP /* 必须:启用堆内存 */
#define OS_USING_MEM_TRACE /* 必须:启用内存追踪 */内存追踪功能依赖于堆内存管理,它会在每个内存块的头部额外记录分配该块的任务信息。
二、内存追踪原理
2.1 记录机制
启用 OS_USING_MEM_TRACE 后,分配内存时会在块头部额外记录分配该块的任务指针(TASK_ID)。
各算法如何记录(块头字段的完整讲解见 4.4.2 FIRSTFIT 的头部字节、4.4.3 BUDDY 的 BLK_HEAD_SIZE,此处聚焦内存追踪视角):
| 算法 | 未启用追踪 | 启用追踪 | 额外开销 |
|---|---|---|---|
| FIRSTFIT | chunk 头 LEFT_SIZE+SIZE_AND_USED | 增加 TASK_ID 字段 | +sizeof(os_task_t*) |
| BUDDY | 块头 4 字节(level/used/duplicate/magic_tag) | 头部增至 4+sizeof(os_base_t) | +sizeof(os_base_t) |
FIRSTFIT 启用追踪的 chunk:
┌──────────┬───────────────┬──────────┬───────────────┐
│ LEFT_SIZE│ SIZE_AND_USED │ TASK_ID │ 用户数据 │
└──────────┴───────────────┴──────────┴───────────────┘
◄───────────────────────►
TASK_ID = 分配该块的任务指针为什么需要 TASK_ID:内存追踪的核心目的是定位内存泄漏。每个分配块记录"是谁分配的",trace_mem 输出归属任务,用户就能知道某块内存是哪个任务分配后一直没释放。
代价:每个块额外占用
sizeof(os_task_t*)(4 或 8 字节)元数据。这是调试期开销,发布时可关闭OS_USING_MEM_TRACE节省。
2.2 追踪输出
追踪功能会逐块输出所有已分配内存的信息:
mem_trace for memory addr: 0x20001000 ~ 0x20011000
Used Addr Size Task ID
---- ---------- ---------- --------
* 0x20001028 128 0x20001234
* 0x20001500 256 0x20001500
- 0x20001600 512 ← 空闲块
* 0x20001a00 1024 0x20001800*表示已用块-表示空闲块Task ID显示分配该内存的任务指针
三、API 详解
3.1 默认堆追踪
os_err_t os_default_heap_trace(void);完整源码(kernel/source/os_memory.c,与项目逐字一致):
来源:kernel/source/os_memory.c
os_err_t os_default_heap_trace(void)
{
return os_heap_trace(gs_default_heap);
}- 功能:追踪默认堆所有内存块的分配信息。
- 本质:简单转发,调用
os_heap_trace(gs_default_heap)(见 3.2)。 - 返回值:
OS_SUCCESS成功;其他值表示异常。 - 注意:仅在
OS_USING_MEM_TRACE启用时编译。
3.2 自定义堆追踪
os_err_t os_heap_trace(os_heap_id heap_id);完整源码(kernel/source/os_memory.c,与项目逐字一致):
来源:kernel/source/os_memory.c
os_err_t os_heap_trace(os_heap_id heap_id)
{
struct heap_mem *h_mem;
os_err_t ret;
os_heap_t *heap;
heap = OS_TYPE_CONVERT(os_heap_t *, heap_id);
OS_ASSERT(heap);
OS_ASSERT(OS_KOBJ_INITED == heap->object_inited);
ret = OS_SUCCESS;
h_mem = heap->h_mem;
while (h_mem)
{
if (h_mem->k_mem_trace)
{
h_mem->k_mem_trace(h_mem);
if (ret != OS_SUCCESS)
{
break;
}
}
h_mem = h_mem->next;
}
return ret;
}分步详解:
- 第一步:校验
heap_id有效、堆已初始化(OS_ASSERT)。 - 第二步:
ret = OS_SUCCESS(默认成功)。 - 第三步:从
heap->h_mem开始,遍历该堆的所有内存区域(h_mem是单向链表)。 - 第四步(条件
h_mem->k_mem_trace):若该区域注册了追踪函数(函数指针多态),调用它输出该区域的分配信息;若ret != OS_SUCCESS则break停止。 - 第五步:
h_mem = h_mem->next移到下一个区域,直到链表尾。
if-else 条件走向总结(树状图):
os_heap_trace(heap_id)
│
├─ 校验 heap / object_inited(否则断言)
├─ ret = OS_SUCCESS
│
└─ while (h_mem)? /* 遍历所有内存区域 */
├─ if (h_mem->k_mem_trace)? /* 有追踪函数? */
│ ├─ 是 → 调用追踪函数,输出该区域分配信息
│ │ └─ if (ret != OS_SUCCESS)? → 是则 break
│ └─ 否 → 跳过该区域
├─ h_mem = h_mem->next(下一个区域)
└─ (循环结束)
返回 ret关键:
k_mem_trace是函数指针(os_memory.h里的多态接口),FIRSTFIT/BUDDY 各自实现了追踪函数(如_k_firstfit_mem_trace)。所以os_heap_trace通过它统一驱动不同算法的追踪。
3.3 默认堆完整性检查
os_err_t os_default_heap_check(void);完整源码(kernel/source/os_memory.c,与项目逐字一致):
来源:kernel/source/os_memory.c
os_err_t os_default_heap_check(void)
{
return os_heap_check(gs_default_heap);
}- 功能:检查默认堆完整性。
- 本质:转发到
os_heap_check(gs_default_heap)(见 3.4)。 - 返回值:
OS_SUCCESS堆正常;其他值表示发现错误。
3.4 自定义堆完整性检查
os_err_t os_heap_check(os_heap_id heap_id);完整源码(kernel/source/os_memory.c,与项目逐字一致):
来源:kernel/source/os_memory.c
os_err_t os_heap_check(os_heap_id heap_id)
{
struct heap_mem *h_mem;
os_err_t ret;
os_heap_t *heap;
heap = OS_TYPE_CONVERT(os_heap_t *, heap_id);
OS_ASSERT(heap);
OS_ASSERT(OS_KOBJ_INITED == heap->object_inited);
ret = OS_SUCCESS;
h_mem = heap->h_mem;
while (h_mem)
{
if (h_mem->k_mem_check)
{
ret = h_mem->k_mem_check(h_mem);
if (ret != OS_SUCCESS)
{
break;
}
}
h_mem = h_mem->next;
}
return ret;
}分步详解:
- 第一步:校验
heap_id、堆已初始化。 - 第二步:
ret = OS_SUCCESS。 - 第三步:遍历所有内存区域
h_mem。 - 第四步(条件
h_mem->k_mem_check):若注册了检查函数,调用它检查该区域的完整性(chunk 邻居关系、used/free 统计一致性);若ret != OS_SUCCESS则break(发现错误立即停止)。 - 第五步:移到下一个区域。
if-else 条件走向总结(树状图):
os_heap_check(heap_id)
│
├─ 校验 heap / object_inited(否则断言)
├─ ret = OS_SUCCESS
│
└─ while (h_mem)? /* 遍历所有内存区域 */
├─ if (h_mem->k_mem_check)? /* 有检查函数? */
│ ├─ 是 → ret = k_mem_check(h_mem) 检查该区域
│ │ └─ if (ret != OS_SUCCESS)? → 是则 break(发现错误)
│ └─ 否 → 跳过该区域
├─ h_mem = h_mem->next
└─ (循环结束)
返回 ret底层检查:FIRSTFIT 的
_k_firstfit_mem_check会遍历 chunk 验证左右邻居关系、校验 used/free 统计,发现错误即断言/返回错误码(详见 4.4.2)。
数值演算(默认堆含 2 个区域,第 1 个正常、第 2 个 chunk 邻居损坏):
| 步骤 | 操作 | 结果 |
|---|---|---|
| ① 区域 1 | k_mem_check(区域1) | OS_SUCCESS,继续 |
| ② 区域 2 | k_mem_check(区域2) | 发现 r_chunk->left != c → 返回错误 |
| ③ 判断 | ret != OS_SUCCESS | break 停止遍历 |
| ④ 返回 | ret | 错误码(非成功) |
四、Shell 命令
OneOS 提供了便捷的 Shell 命令用于内存调试:
| 命令 | 功能 | 依赖 |
|---|---|---|
show_mem | 显示默认堆使用情况 | OS_USING_HEAP |
show_heap | 显示所有堆信息 | OS_USING_HEAP |
check_mem | 检查默认堆完整性 | OS_USING_HEAP |
trace_mem | 追踪默认堆内存分配 | OS_USING_HEAP + OS_USING_MEM_TRACE |
show_mempool | 显示内存池信息 | OS_USING_MEM_POOL |
4.1 show_mem
show_mem输出示例:
Name Total Used Used rate Max Used Max rate Range
---------- ---------- ---------- ---------- ---------- ---------- -----
DEFAULT_HEAP 65536 12480 19.04% 16256 24.80% [0x20001000~0x20011000]4.2 check_mem
check_mem输出示例(正常):
mem_check for memory addr: 0x20001000 ~ 0x20011000
free block info:
bucketid chunkid chunk_size mem_size
3 128 16 0x00000038
5 230 32 0x00000078
memory addr : 0x20001000
memory total : 65536
memory used : 12480
memory max used : 16256
mem_check ok!输出示例(异常):
mem_check err:chunk:42, r_chunk:58, r_chunk's left:40
the addr:0x20001528 or 0x20001290 maybe overwrited! please check.4.3 trace_mem
trace_mem输出示例:
mem_trace for memory addr: 0x20001000 ~ 0x20011000
Used Addr Size Task ID
---- ---------- ---------- --------
* 0x20001028 128 0x20001300
* 0x20001500 256 0x20001300
- 0x20001600 512
* 0x20001a00 1024 0x20001800五、代码示例
5.1 使用内存追踪定位泄漏
#include <os_memory.h>
void task_with_leak(void)
{
void *buf1;
void *buf2;
buf1 = os_malloc(256);
buf2 = os_malloc(128);
/* 使用 buf1 和 buf2 ... */
/* 只释放了 buf1,忘记释放 buf2 → 内存泄漏! */
os_free(buf1);
}
void debug_memory(void)
{
/* 运行一段时间后,检查内存状态 */
os_meminfo_t info;
os_default_heap_info(&info);
os_kprintf("当前内存: 总计=%lu 已用=%lu 峰值=%lu\r\n",
info.mem_total, info.mem_used, info.mem_maxused);
/* 如果已用内存持续增长,可能存在泄漏 */
/* 使用追踪功能查看每个块的归属 */
#ifdef OS_USING_MEM_TRACE
os_default_heap_trace();
#endif
}5.2 定期检查堆完整性
void periodic_check_task(void)
{
os_err_t ret;
while (1)
{
os_task_sleep(1000); /* 每秒检查一次 */
ret = os_default_heap_check();
if (ret != OS_SUCCESS)
{
os_kprintf("堆完整性检查失败!错误码: %d\r\n", ret);
/* 发现异常后,可以进一步追踪 */
#ifdef OS_USING_MEM_TRACE
os_default_heap_trace();
#endif
}
}
}5.3 多堆追踪
void multi_heap_debug(void)
{
os_err_t ret;
/* 检查 SRAM 堆 */
ret = os_heap_check(sram_heap_id);
if (ret != OS_SUCCESS)
{
os_kprintf("SRAM 堆异常!\r\n");
}
/* 检查 SDRAM 堆 */
ret = os_heap_check(sdram_heap_id);
if (ret != OS_SUCCESS)
{
os_kprintf("SDRAM 堆异常!\r\n");
}
#ifdef OS_USING_MEM_TRACE
/* 追踪两个堆 */
os_heap_trace(sram_heap_id);
os_heap_trace(sdram_heap_id);
#endif
}六、内存调试最佳实践
6.1 开发阶段
| 实践 | 说明 |
|---|---|
| 启用 OS_USING_MEM_TRACE | 记录每个分配的任务信息,便于定位泄漏源 |
| 启用 OS_USING_MEMPOOL_CHECK_TAG | 检测内存池块的越界写入 |
| 定期调用 check_mem | 在关键路径上检查堆完整性 |
| 使用 Shell 命令 | show_mem、trace_mem、check_mem 辅助调试 |
6.2 常见问题排查
内存泄漏:
- 使用
show_mem查看mem_used是否持续增长 - 使用
trace_mem查看所有已分配块及其归属任务 - 确认每个
os_malloc/os_calloc都有对应的os_free
内存越界:
- 使用
check_mem检查堆完整性,定位越界写入位置 - 启用
OS_USING_MEMPOOL_CHECK_TAG检测内存池越界 - 出错的地址范围可以帮助定位越界的代码
Double-Free:
- 启用断言(
OS_ASSERT),FIRSTFIT 和 BUDDY 都会检测 double-free - 错误信息会明确指出 double-free 的地址
6.3 发布阶段
| 实践 | 说明 |
|---|---|
| 关闭 OS_USING_MEM_TRACE | 减少每个块的元数据开销 |
| 关闭 OS_USING_MEMPOOL_CHECK_TAG | 减少内存池块开销 |
| 保留 check_mem | 可定期调用作为运行时健康检查 |
📝 本节小结
核心要点回顾
- 内存追踪:通过
OS_USING_MEM_TRACE启用,在每个块头部记录分配任务指针(TASK_ID),用于定位内存泄漏。 - 追踪原理(2.1):FIRSTFIT 加
TASK_ID字段、BUDDY 头部增至 4+sizeof(os_base_t),每块额外开销一个指针。 os_heap_trace(3.2):遍历所有内存区域,通过函数指针k_mem_trace统一驱动各算法的追踪输出。os_heap_check(3.4):遍历所有内存区域,通过k_mem_check检查 chunk 邻居关系、统计一致性,发现错误即break并返回错误码。os_default_heap_*:都是对gs_default_heap的简单转发。- 调试手段:
show_mem(用量)、check_mem(完整性)、trace_mem(定位泄漏)。
关键易错点速查表
| 易错点 | 说明 | 对应章节 |
|---|---|---|
| 追踪需开宏 | os_default_heap_trace 仅 OS_USING_MEM_TRACE 时编译 | 3.1 |
| 额外开销 | 每个块多占 sizeof(os_task_t*)(4/8 字节) | 2.1 |
| 发布关追踪 | 调试期开、发布期关可省元数据开销 | 6.3 |
| 多区域遍历 | 一个堆含多个区域,os_heap_trace/check 会全部遍历 | 3.2/3.4 |
| check 遇错即停 | 发现第一个错误就 break,返回错误码 | 3.4 |
| 泄漏排查顺序 | 先看 mem_used 趋势,再 trace_mem 定位归属任务 | 6.2 |
下一步
回到 4.4 内存管理总览,回顾堆(FIRSTFIT/BUDDY)、内存池、内存追踪的整体脉络,结合本节调试工具,能系统地定位内存泄漏与越界问题。