内核内存泄漏检测器

Kmemleak 提供了一种检测潜在内核内存泄漏的方法,其方式类似于追踪垃圾回收器(tracing garbage collector),不同之处在于孤儿对象不会被释放,而是仅通过 /sys/kernel/debug/kmemleak 进行报告。Valgrind 工具(memcheck --leak-check)也使用了类似的方法来检测用户空间应用程序中的内存泄漏。

用法

必须启用“Kernel hacking”中的 CONFIG_DEBUG_KMEMLEAK。一个内核线程会每 10 分钟(默认情况下)扫描一次内存,并打印出找到的新未引用对象的数量。如果 debugfs 尚未挂载,请使用以下命令挂载

# mount -t debugfs nodev /sys/kernel/debug/

要显示所有可能扫描到的内存泄漏的详细信息

# cat /sys/kernel/debug/kmemleak

要触发一次临时的内存扫描

# echo scan > /sys/kernel/debug/kmemleak

要清除当前所有可能内存泄漏的列表

# echo clear > /sys/kernel/debug/kmemleak

再次读取 /sys/kernel/debug/kmemleak 时,将会显示新的泄漏。

请注意,孤儿对象是按照它们被分配的顺序列出的,位于列表前部的某个对象可能会导致其后面的其他对象也被报告为孤儿。

可以通过向 /sys/kernel/debug/kmemleak 文件写入内容,在运行时修改内存扫描参数。支持以下参数

  • 关闭

    禁用 kmemleak(不可逆)

  • stack=on

    启用任务栈扫描(默认)

  • stack=off

    禁用任务栈扫描

  • scan=on

    启动自动内存扫描线程(默认)

  • scan=off

    停止自动内存扫描线程

  • scan=<secs>

    以秒为单位设置自动内存扫描周期(默认 600,设置为 0 可停止自动扫描)

  • scan

    触发内存扫描

  • 清除

    清除当前疑似内存泄漏列表,通过将当前所有报告的未引用对象标记为灰色来完成,如果 kmemleak 已被禁用,则释放所有 kmemleak 对象。

  • dump=<addr>

    转储在 <addr> 处找到的对象的信息

也可以通过在内核命令行中传递 kmemleak=off 在引导时禁用 kmemleak。

内存可能会在 kmemleak 初始化之前被分配或释放,这些操作会存储在一个早期日志缓冲区中。该缓冲区的大小通过 CONFIG_DEBUG_KMEMLEAK_MEM_POOL_SIZE 选项配置。

如果启用了 CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF,kmemleak 默认处于禁用状态。在内核命令行中传递 kmemleak=on 可以启用该功能。

如果您遇到诸如 “Error while writing to stdout” 或 “write_loop: Invalid argument”之类的错误,请确保已正确启用 kmemleak。

基本算法

通过 kmalloc()vmalloc()kmem_cache_alloc() 及其同类函数进行的内存分配会被追踪,指针连同大小和栈回溯等附加信息一起存储在一个红黑树(rbtree)中。系统会追踪相应的释放函数调用,并从 kmemleak 数据结构中移除对应的指针。

如果在扫描内存(包括保存的寄存器)时找不到指向已分配内存块起始地址或块内任何位置的指针,则该内存块被认为是孤儿对象。这意味着内核可能无法将已分配块的地址传递给释放函数,因此该块被认为是内存泄漏。

扫描算法步骤

  1. 将所有对象标记为白色(剩余的白色对象稍后将被视为孤儿)

  2. 从数据段和栈开始扫描内存,将这些值与 rbtree 中存储的地址进行核对。如果找到指向白色对象的指针,则将该对象添加到灰色列表中

  3. 扫描灰色对象以查找匹配的地址(一些白色对象可能变为灰色并被添加到灰色列表末尾),直到灰色集合扫描完毕

  4. 其余的白色对象被视为孤儿,并通过 /sys/kernel/debug/kmemleak 进行报告

某些已分配的内存块在内核的内部数据结构中存储了指针,它们无法被检测为孤儿。为了避免这种情况,kmemleak 还可以存储需要找到的、指向块地址范围内某个地址的值的数量,以便该块不被认为是泄漏。一个例子是 __vmalloc()

使用 kmemleak 测试特定代码段

初次启动时,您的 /sys/kernel/debug/kmemleak 输出页面可能会非常长。在开发过程中如果编写了包含大量 Bug 的代码,也会出现这种情况。为了解决这些情况,您可以使用 ‘clear’ 命令从 /sys/kernel/debug/kmemleak 输出中清除所有已报告的未引用对象。在执行 ‘clear’ 后发出 ‘scan’ 可以找到新的未引用对象;这应该有助于测试特定的代码段。

要在干净的 kmemleak 环境中按需测试临界区,请执行

# echo clear > /sys/kernel/debug/kmemleak
... test your kernel or modules ...
# echo scan > /sys/kernel/debug/kmemleak

然后像往常一样通过以下方式获取您的报告

# cat /sys/kernel/debug/kmemleak

释放 kmemleak 内部对象

为了在用户禁用 kmemleak 或由于致命错误禁用 kmemleak 后仍允许访问先前发现的内存泄漏,禁用 kmemleak 时不会释放 kmemleak 内部对象,这些对象可能会占用物理内存的大部分。

在这种情况下,您可以使用以下命令回收内存

# echo clear > /sys/kernel/debug/kmemleak

Kmemleak API

有关函数原型,请参见 include/linux/kmemleak.h 头文件。

  • kmemleak_init - 初始化 kmemleak

  • kmemleak_alloc - 通知内存块分配

  • kmemleak_alloc_percpu - 通知 percpu 内存块分配

  • kmemleak_vmalloc - 通知 vmalloc() 内存分配

  • kmemleak_free - 通知内存块释放

  • kmemleak_free_part - 通知部分内存块释放

  • kmemleak_free_percpu - 通知 percpu 内存块释放

  • kmemleak_update_trace - 更新对象分配栈回溯

  • kmemleak_not_leak - 将对象标记为非泄漏

  • kmemleak_transient_leak - 将对象标记为瞬态泄漏

  • kmemleak_ignore - 不扫描或不将对象报告为泄漏

  • kmemleak_scan_area - 在内存块内添加扫描区域

  • kmemleak_no_scan - 不扫描某个内存块

  • kmemleak_erase - 擦除指针变量中的旧值

  • kmemleak_alloc_recursive - 与 kmemleak_alloc 类似,但会检查递归性

  • kmemleak_free_recursive - 与 kmemleak_free 类似,但会检查递归性

以下函数接受物理地址作为对象指针,并且仅当该地址具有低端内存(lowmem)映射时才执行相应的操作

  • kmemleak_alloc_phys

  • kmemleak_free_part_phys

  • kmemleak_ignore_phys

处理误报/漏报

漏报是指真正的内存泄漏(孤儿对象),但由于在内存扫描期间找到的值指向了这些对象,因此 kmemleak 没有报告它们。为了减少漏报数量,kmemleak 提供了 kmemleak_ignore、kmemleak_scan_area、kmemleak_no_scan 和 kmemleak_erase 函数(见上文)。任务栈也会增加漏报的数量,并且默认不启用对任务栈的扫描。

误报是被错误地报告为内存泄漏(孤儿)的对象。对于已知不是泄漏的对象,kmemleak 提供了 kmemleak_not_leak 函数。如果已知内存块不包含其他指针且不再需要扫描,也可以使用 kmemleak_ignore。

所报告的一些泄漏只是暂时的,特别是在 SMP 系统上,因为指针临时存储在 CPU 寄存器或栈中。Kmemleak 定义了 MSECS_MIN_AGE(默认为 1000),表示被报告为内存泄漏的对象的最小存在时间(毫秒)。

局限性与缺点

主要的缺点是内存分配和释放性能的降低。为了避免其他性能惩罚,内存扫描仅在读取 /sys/kernel/debug/kmemleak 文件时执行。无论如何,该工具旨在用于调试目的,此时性能可能不是最重要的要求。

为了保持算法简单,kmemleak 会扫描指向块地址范围内任何地址的值。这可能会导致漏报数量增加。不过,真正的内存泄漏最终很可能会显现出来。

漏报的另一个来源是非指针值中存储的数据。在未来的版本中,kmemleak 可以仅扫描已分配结构中的指针成员。此功能将解决上述许多漏报情况。

该工具可能会报告误报。这些情况包括:已分配的块不需要释放(init_call 函数中的某些情况)、指针通过常规 container_of 宏之外的其他方法计算得出,或者指针存储在 kmemleak 未扫描的位置。

页面分配(Page allocations)和 ioremap 不会被追踪。

使用 kmemleak-test 进行测试

要检查您是否已做好使用 kmemleak 的所有准备工作,您可以使用 kmemleak-test 模块,这是一个故意泄漏内存的模块。将 CONFIG_SAMPLE_KMEMLEAK 设置为模块(不能作为内置模块使用),并在启用 kmemleak 的情况下启动内核。加载模块并执行扫描

# modprobe kmemleak-test
# echo scan > /sys/kernel/debug/kmemleak

请注意,您可能不会立即或在第一次扫描时就得到结果。当 kmemleak 获得结果时,它会记录 kmemleak: <count of leaks> new suspected memory leaks。然后读取该文件以查看

# cat /sys/kernel/debug/kmemleak
unreferenced object 0xffff89862ca702e8 (size 32):
  comm "modprobe", pid 2088, jiffies 4294680594 (age 375.486s)
  hex dump (first 32 bytes):
    6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
    6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b a5  kkkkkkkkkkkkkkk.
  backtrace:
    [<00000000e0a73ec7>] 0xffffffffc01d2036
    [<000000000c5d2a46>] do_one_initcall+0x41/0x1df
    [<0000000046db7e0a>] do_init_module+0x55/0x200
    [<00000000542b9814>] load_module+0x203c/0x2480
    [<00000000c2850256>] __do_sys_finit_module+0xba/0xe0
    [<000000006564e7ef>] do_syscall_64+0x43/0x110
    [<000000007c873fa6>] entry_SYSCALL_64_after_hwframe+0x44/0xa9
...

使用 rmmod kmemleak_test 卸载模块也应该会触发一些 kmemleak 结果。