英语

内核内存泄漏检测器

Kmemleak 提供了一种检测可能的内核内存泄漏的方法,类似于跟踪垃圾收集器,不同之处在于孤立对象不会被释放,而只是通过 /sys/kernel/debug/kmemleak 报告。Valgrind 工具 (memcheck --leak-check) 也使用类似的方法来检测用户空间应用程序中的内存泄漏。

用法

必须启用“内核黑客”中的 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 文件在运行时修改内存扫描参数。支持以下参数

  • off

    禁用 kmemleak(不可逆)

  • stack=on

    启用任务堆栈扫描(默认)

  • stack=off

    禁用任务堆栈扫描

  • scan=on

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

  • scan=off

    停止自动内存扫描线程

  • scan=<秒数>

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

  • scan

    触发内存扫描

  • clear

    清除当前内存泄漏嫌疑人列表,方法是将所有当前报告的未引用对象标记为灰色,或者如果已禁用 kmemleak,则释放所有 kmemleak 对象。

  • dump=<addr>

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

也可以在启动时通过在内核命令行上传递 kmemleak=off 来禁用 Kmemleak。

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

如果启用了 CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF,则默认情况下禁用 kmemleak。在内核命令行上传递 kmemleak=on 会启用该功能。

如果您收到类似“写入 stdout 时出错”或“write_loop:参数无效”的错误,请确保 kmemleak 已正确启用。

基本算法

通过 kmalloc()vmalloc()kmem_cache_alloc() 等进行的内存分配会被跟踪,指针以及大小和堆栈跟踪等附加信息会存储在红黑树中。相应的释放函数调用会被跟踪,指针会从 kmemleak 数据结构中删除。

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

扫描算法步骤

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

  2. 从数据段和堆栈开始扫描内存,对照红黑树中存储的地址检查值。如果找到指向白色对象的指针,则将该对象添加到灰色列表

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

  4. 剩余的白色对象被视为孤立对象,并通过 /sys/kernel/debug/kmemleak 报告

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

使用 kmemleak 测试特定部分

在初始启动时,您的 /sys/kernel/debug/kmemleak 输出页面可能非常广泛。当您进行开发时,如果您的代码存在很多错误,也可能会出现这种情况。要解决这些情况,您可以使用“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 - 通知每个 CPU 内存块的分配

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

  • kmemleak_free - 通知内存块释放

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

  • kmemleak_free_percpu - 通知每个 CPU 内存块释放

  • 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 未扫描的位置。

页面分配和 ioremap 不会被跟踪。

使用 kmemleak-test 进行测试

要检查您是否已完成所有设置以使用 kmemleak,您可以使用 kmemleak-test 模块,该模块会故意泄漏内存。将 CONFIG_SAMPLE_KMEMLEAK 设置为模块(不能作为内置使用),并在启用 kmemleak 的情况下启动内核。加载该模块并使用以下命令执行扫描:

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

请注意,您可能不会立即或在第一次扫描时获得结果。当 kmemleak 获得结果时,它将记录 kmemleak: <泄漏数量> 个新的可疑内存泄漏。然后读取文件以查看结果。

# 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 结果。