概述

内存泄漏与内存碎片化是内存管理中的两大核心问题。内存泄漏会导致可用内存逐渐减少,而内存碎片化则会造成内存分配失败和系统性能下降。尤其是在 RTOS 环境中,这些问题会严重影响系统稳定性与运行效能。

此外,内存越界(memory overrun)和堆破坏(heap corruption)也是内存管理中潜在的隐患,这类问题通常难以检测和调试。

FreeRTOS 为开发者提供了一套高效灵活的内存管理机制。FreeRTOS 内核在 heap_4 和 heap_5 内存管理方案中支持相邻空闲内存块合并(coalesces)机制,从而有效降低内存碎片化的风险。然而,系统原生并未提供有效的方法来检测内存泄漏和堆破坏问题。

我们针对 FreeRTOS heap_5 提供了堆监控功能,包括:

  1. 堆状态统计

  2. 任务级堆使用分析

  3. 堆完整性校验(堆损坏检测)

  4. 内存泄漏检测

相关 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( ;; );
}

小心

用户应该在单个任务的生命周期内维护堆内存:即在任务 1 中通过 malloc 分配的内存应该在任务 1 被删除之前使用 free 释放。
不建议在任务 1 被删除后在任务 2 中释放该内存,这将导致任务堆使用计算错误。因为当任务 1 被删除时,任务名称和 TCB(任务控制块)已经不再有效。

堆完整性校验

检查堆数据结构和内容的完整性。我们提供三个保护模式:

  • 默认模式(堆头数据结构保护)

  • 轻量影响模式(边界保护器)

  • 全面模式(内容保护器)

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 记录不好分析。