概述
内存泄漏与内存碎片化是内存管理中的两大核心问题。内存泄漏会导致可用内存逐渐减少,而内存碎片化则会造成内存分配失败和系统性能下降。尤其是在 RTOS 环境中,这些问题会严重影响系统稳定性与运行效能。
此外,内存越界(memory overrun)和堆破坏(heap corruption)也是内存管理中潜在的隐患,这类问题通常难以检测和调试。
FreeRTOS 为开发者提供了一套高效灵活的内存管理机制。FreeRTOS 内核在 heap_4 和 heap_5 内存管理方案中支持相邻空闲内存块合并(coalesces)机制,从而有效降低内存碎片化的风险。然而,系统原生并未提供有效的方法来检测内存泄漏和堆破坏问题。
我们针对 FreeRTOS heap_5 提供了堆监控功能,包括:
堆状态统计
任务级堆使用分析
堆完整性校验(堆损坏检测)
内存泄漏检测
相关 API 头文件路径:heap_trace/heap_trace.h
堆状态统计
使能方法:默认使能
API 名称: heap_get_stats()
输出示例:
********** Heap Usage Status **********
Total Number Of Successful Allocations: 13
Total Number Of Successful Frees: 2
Current Available Heap Space: 96576 bytes
Minimum Ever Free Space Remaining: 96448 bytes
Number Of Free Blocks: 3
Size Of Largest Free Block: 65520 bytes
Size Of Smallest Free Block: 31056 bytes
输出项说明:
Total Number Of Successful Allocations : 成功调用 malloc 并返回有效内存块的次数。
Total Number Of Successful Frees : 成功调用 free 并释放内存块的次数。
Current Available Heap Space : 当前堆可用总空间。
Minimum Ever Free Space Remaining : 系统启动后堆中剩余空闲内存的最小值(所有空闲块的总和)。
Number Of Free Blocks : 堆内空闲内存块数量。
Size Of Largest Free Block : 堆内最大空闲块的字节数。
Size Of Smallest Free Block : 堆内最小空闲块的字节数。
任务级堆使用分析
获得每个任务使用的栈大小。任务级堆使用分析可用于内存泄漏检查:若某任务的堆使用率在增长,则可能存在内存泄漏。
使能方法:默认使能。需要根据实际需求在 Kconfig 中定义 CONFIG_HEAP_TRACE_MAX_TASK_NUMBER 为的最大任务数量。 如果任务 a 在被 delete 前它 malloc 的空间没有被全部 free,那么他会占一条 heap_task_info。 建议大于常驻的任务数,否则常驻任务占满 heap_task_info,新任务的 malloc 就会报 overflow。
API 名称: heap_get_per_task_info()
输出示例:
/* 1st calling heap_get_per_task_info() */
Deleted Task: main thread - task heap usage: 0x64b0
Task: TX - task heap usage: 0x180
/* 2nd calling heap_get_per_task_info() */
Deleted Task: main thread - task heap usage: 0x64b0
Task: TX - task heap usage: 0x1C0
/* 3rd calling heap_get_per_task_info() */
Deleted Task: main thread - task heap usage: 0x64b0
Task: TX - task heap usage: 0x200
本例中,通过三次调用结果分析,任务”TX”的堆内存使用量持续增长,存在内存泄漏的嫌疑。
此外,”main thread”虽已被删除却仍显示堆内存使用量,可能也存在内存泄漏。
此时可通过 set_tracing()
缩小排查范围,或人工审查”TX”及”main thread”的代码逻辑,确认是否存在未释放内存的情况。
本例的”TX”任务确实存在内存泄漏。而”main thread”在创建 TASK TX 后即被删除,其堆内存使用量源于为 TASK TX 创建任务栈(task stack)的行为,属于正常现象。
本例中内存泄漏代段如下所示:
static void prvQueueSendTask( void *pvParameters )
{
for( ;; )
{
/* Allocation but no free */
uint32_t *p = malloc(8);
uint32_t *q = malloc(8);
vTaskDelay(50);
heap_get_per_task_info();
}
}
/* Main thread is used to create all app tasks */
int main( void )
{
xTaskCreate( prvQueueSendTask,
"TX",
configMINIMAL_STACK_SIZE,
NULL,
mainQUEUE_SEND_TASK_PRIORITY,
NULL );
vTaskDelete(NULL);
for( ;; );
}
小心
堆完整性校验
检查堆数据结构和内容的完整性。我们提供三个保护模式:
默认模式(堆头数据结构保护)
轻量影响模式(边界保护器)
全面模式(内容保护器)
API 名称: heap_check_integrity()
默认模式(堆头数据结构保护)
使用默认使能的堆保护器检查,可以检测到大多数越界写入和结构体破坏。避免堆结构体破坏带来的难以定位的崩溃。几乎没有性能影响。 有关堆保护器的更多详细信息,请参阅 https://mcuoneclipse.com/2024/01/28/freertos-with-heap-protector/
使能方法:默认使能。若需要关掉,可以在 Kconfig 中关闭 CONFIG_HEAP_PROTECTOR。
检查的时机和内容:
默认检查:在每次使用堆数据结构时,都会检测堆数据结构中的地址是否是有效值。
增强检查:在 Kconfig 中使能 CONFIG_HEAP_INTEGRITY_CHECK_IN_TASK_SWITCHED_OUT 后,会在每次任务切出后都进行检查。
手动检查:用户可以使用
heap_check_integrity()
来检查所有堆块结构的完整性。
轻量影响模式(边界保护器)
轻量影响模式能够提供更精确的内存溢出检测。 在内存管理中,”canary”指的是用于检测内存溢出的技术。canary 区域在程序执行期间被填充特定值,放置在栈或内存缓冲区的末尾。 在轻量影响模式下,在每个分配的堆块的头部和尾部都放置了两个 canary 区域。头部的 canary 区域用 xHeadCanaryValue 填充,尾部的 canary 区域用 xTailCanaryValue 填充。
#define xHeadCanaryValue 0xDEADBEEF // 每个分配的堆块的头部 canary 值
#define xTailCanaryValue 0xCAFEBABE // 每个分配的堆块的尾部 canary 值
使能方法:启用前请确保 CONFIG_HEAP_PROTECTOR 为 1,然后 Kconfig 中使能 CONFIG_HEAP_CORRUPTION_DETECT_LITE。
检查的时机和内容:
默认检查:每次在轻量影响模式下调用 free 时,会验证被释放块的头部和尾部 canary 字节是否与预期值匹配。
增强检查:在 Kconfig 中使能 CONFIG_HEAP_INTEGRITY_CHECK_IN_TASK_SWITCHED_OUT 后,会在每次任务切出后都进行检查。
手动检查:
heap_check_integrity()
作为用户 API 提供,验证所有已分配堆内存块的 canary 字节是否与其预期值匹配。
负面影响: 启用轻量影响检查会增加内存使用。每个单独的分配会使用 2 * portBYTE_ALIGNMENT 字节的内存,具体取决于对齐方式。 溢出发生在程序写入超过分配块末尾时。这通常会导致堆中下一个连续块的损坏,无论它是否已分配。 下溢发生在程序写入分配块起始位置之前时。它们经常会损坏块本身的头部。
全面模式(内容保护器)
全面模式在前两个功能的基础上,增加了对未初始化访问和释放后使用的错误检查。 在这种全面模式里新分配的块会用模式 xFillAlocated 初始化,而已释放的内存会用模式 xFillFreed 填充。
#define xFillAlocated 0xCC // 新分配的块用 xFillAlocated 初始化
#define xFillFreed 0xDD // 所有释放的内存用 xFillFreed 填充
使能方法:全面模式是基于轻量影响模式实现的。启用前请确保 CONFIG_HEAP_CORRUPTION_DETECT_LITE 为 1,然后 Kconfig 中使能 CONFIG_HEAP_CORRUPTION_DETECT_COMPREHENSIVE。
检查的时机和内容:
默认检查:每次调用 malloc 时,不仅会验证 canary 区域,还会验证每个空闲块的字节是否与 xFillFreed 匹配。
手动检查:
heap_check_integrity()
作为用户 API 提供。
负面影响: 虽然全面模式更容易检测到内存损坏或者非法访问错误,但此模式会显著影响运行时性能。 因为每次调用 malloc 和 free 时都需要初始化内存为特定的值,并且检查时完整检测所有内存空间。 因此,建议仅在调试内存错误期间启用此模式,而不是在一般和生产环境中使用。 另外,全面模式使能后不可以在 task switch hook 中调用。此项检查太耗费时间,在 hook 中执行会影响系统正常调度。
内存泄漏检测
如何诊断内存泄漏:
建议先使用 heap_get_stats()
或 heap_get_per_task_info()
这两个 API 将泄漏问题缩小到某个任务或函数序列:
在这些函数或函数序列中,空闲内存始终减少且永远不会恢复。
再在较小范围内使用堆追踪功能。堆追踪的功能介绍和使用方法如下:
使能方法:
启用前请确保 CONFIG_HEAP_PROTECTOR 为 1,然后 Kconfig 中使能 CONFIG_HEAP_TRACE。
Kconfig 中配置 CONFIG_HEAP_TRACE_STACK_DEPTH 为 malloc 回溯栈的最大深度。
Kconfig 中配置 CONFIG_HEAP_TRACE_MAX_TASK_NUMBER 为的最大任务数量。 如果任务 a 在被 delete 前它 malloc 的空间没有被全部 free,那么他会占一条 heap_task_info。 建议大于常驻的任务数,否则当常驻任务占满 heap_task_info 时,新任务在 malloc 时就会报 overflow。
API 名称:
heap_trace_init()
:初始化 malloc/free 日志的结构。heap_trace_start()
:开始泄漏追踪heap_trace_stop()
:结束泄漏追踪heap_trace_record_dump()
:在开始和停止之间,有 malloc 记录但是没有 free 记录的堆信息
heap_trace_init()
初始化时传入的 num_records 可以理解成一个池子(buffer size),这里放的就是 “当前有 malloc 但没有 free 出去的内存块数量”。
假如用户使用 heap_get_stats()
统计到下面数据:
Total Number Of Successful Allocations: 371
Total Number Of Successful Frees: 314
一般情况下 num_records 配置为(371-314 = 57)就够了,除非遇到极端情况。比如连续 malloc 了 60 多次再 free 会丢失 log。 因此考虑到在 num_records 快满的时候出现连续 malloc 的情况,建议在 57 的基础上再增加一些。
heap_trace_record_dump()
将泄漏记录显示如下,包括 malloc 地址、大小和 malloc 调用栈:
====== Heap Trace Log Count: 2 records (log buffer capacity: 8) ======
560 bytes (@ 0x101086c0) allocated, caller bakctrace: 0x0e02b544 0x0e039274 0x0e03a630 0x0e032ac2 0x0e035900
1072 bytes (@ 0x10108aa0) allocated, caller bakctrace: 0x0e02943e 0x0e02b5a6 0x0e045b70 0x0e03a664 0x0e032ac2 0x0e035900
========================= Heap Trace Summary =========================
Mode: Heap Trace Leaks
1632 bytes 'leaked' in trace (2 allocations)
records: 2 (8 capacity, 3 high water mark)
内存泄漏误报:
在 trace 的这段时间区域里面,先 free 再紧接着 malloc 到相同的地址,就会留下 leak 的记录。但在调用
heap_get_stats()
检查后,发现剩余堆大小没有变化。 比如 WIFI lwip 中的 sys_check_timeouts 使用了这六块 memory,都会存在报这个问题。一般这种误报泄漏的地址或者大小是相同的。heap_trace_start()
之后动态创建的 rtos 任务或者组件,在heap_trace_stop()
之前组件没有删除。heap_trace API 尽量在某个 task 中使用。若范围太大,则 leak 记录不好分析。