kmemleak 是 Linux 内核用于检测内存泄露的一种工具,实现的基本原理是通过对 kmalloc()、vmalloc()、kmem_cache_alloc() 等内存分配接口,跟踪其分配的指针、分配内存大小、堆栈等跟踪信息,存储在 kmemleak 数据结构中。

配置kmemleak

kmemleak功能需要在内核开启相关配置项才可以使用,主要配置项如下:

CONFIG_DEBUG_KMEMLEAK
CONFIG_DEBUG_KMEMLEAK_EARLY_LOG_SIZE
CONFIG_DEBUG_KMEMLEAK_TEST

其他的一些依赖配置如下:

CONFIG_DEBUG_KERNEL
CONFIG_DEBUG_FS
CONFIG_STACKTRACE
CONFIG_KALLSYMS
CONFIG_CRC32

配置kmemleak后,将会有内核线程间隔10分钟扫描一次内存,并打印找到泄漏的数量。

检查kmemleak

通过/sys/kernel/debug/kmemleak 节点来检查 kmemleak 功能是否生效。

root@Linux:/# mount -t debugfs nodev /sys/kernel/debug/
root@Linux:/# cat /sys/kernel/debug/kmemleak

在启动阶段,由于默认 CONFIG_DEBUG_KMEMLEAK_EARLY_LOG_SIZE 配置过小,系统启动阶段 early log 溢出,会导致 kmemleak 被自动禁用,log 如下:

[    0.000000] kmemleak: Kernel memory leak detector disabled
[    0.000000] clk r_dsp_cache0 not found in of_sunxi_periph_cpus_clk_setup
[    0.000000] clk r_dsp_cache1 not found in of_sunxi_periph_cpus_clk_setup
[    0.000000] clocksource: timer: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 79635851949 ns
[    0.000000] arm_arch_timer: Architected cp15 timer(s) running at 24.00MHz (virt).
[    0.000000] clocksource: arch_sys_counter: mask: 0xffffffffffffff max_cycles: 0x588fe9dc0, max_idle_ns: 440795202592 ns
[    0.000005] sched_clock: 56 bits at 24MHz, resolution 41ns, wraps every 4398046511097ns
[    0.008203] Console: colour dummy device 80x25
[    0.012434] kmemleak: Early log buffer exceeded (1438), please increase DEBUG_KMEMLEAK_EARLY_LOG_SIZE

遇到该情况,需要从新配置内核 CONFIG_DEBUG_KMEMLEAK_EARLY_LOG_SIZE 参数,把该参数按照 log 建议值设置或者设置最大值即可。

内存泄漏扫描

开机完成后,如果系统有内存泄漏,将会出现内存泄漏扫描的log(启动之后,等待一段时间,大概几十秒,如有泄漏将会有扫描结果),如下所示:

[   65.088222] kmemleak: 3 new suspected memory leaks (see /sys/kernel/debug/kmemleak)

通过查看/sys/kernel/debug/kmemleak 节点,可以查看疑似内存泄露的地方。log如下:

root@Linux:/# cat /sys/kernel/debug/kmemleak
unreferenced object 0xffffffc008efcc00 (size 512):
  comm "swapper/0", pid 1, jiffies 4294892779 (age 162.728s)
  hex dump (first 32 bytes):
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  backtrace:
    [<ffffff8008156eac>] __save_stack_trace+0x24/0x30
    [<ffffff80081574ac>] create_object+0x100/0x23c
    [<ffffff80085e7d28>] kmemleak_alloc+0x30/0x5c
    [<ffffff8008153cc4>] kmem_cache_alloc+0xd0/0x184
    [<ffffff800833a008>] hub_probe+0x144/0x804
    [<ffffff8008342190>] usb_probe_interface+0x1d4/0x1fc
    [<ffffff80082d18cc>] driver_probe_device+0x1b4/0x26c
    [<ffffff80082d1b04>] __device_attach_driver+0x9c/0xb0
    [<ffffff80082cff08>] bus_for_each_drv+0x84/0x94
    [<ffffff80082d1690>] __device_attach+0xa8/0x100
    [<ffffff80082d1c58>] device_initial_probe+0x10/0x18
    [<ffffff80082d0c84>] bus_probe_device+0x2c/0x8c
    [<ffffff80082cf094>] device_add+0x45c/0x51c
    [<ffffff800834063c>] usb_set_configuration+0x604/0x648
    [<ffffff800834aee0>] generic_probe+0x58/0x80
    [<ffffff8008341fa0>] usb_probe_device+0x28/0x44
unreferenced object 0xffffffc008efd000 (size 512):
  comm "swapper/0", pid 1, jiffies 4294892825 (age 162.544s)
  hex dump (first 32 bytes):
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  backtrace:
    [<ffffff8008156eac>] __save_stack_trace+0x24/0x30
    [<ffffff80081574ac>] create_object+0x100/0x23c
    [<ffffff80085e7d28>] kmemleak_alloc+0x30/0x5c
    [<ffffff8008153cc4>] kmem_cache_alloc+0xd0/0x184
    [<ffffff8008339ff8>] hub_probe+0x134/0x804
    [<ffffff8008342190>] usb_probe_interface+0x1d4/0x1fc
    [<ffffff80082d18cc>] driver_probe_device+0x1b4/0x26c
    [<ffffff80082d1b04>] __device_attach_driver+0x9c/0xb0
    [<ffffff80082cff08>] bus_for_each_drv+0x84/0x94
    [<ffffff80082d1690>] __device_attach+0xa8/0x100
    [<ffffff80082d1c58>] device_initial_probe+0x10/0x18
    [<ffffff80082d0c84>] bus_probe_device+0x2c/0x8c
    [<ffffff80082cf094>] device_add+0x45c/0x51c
    [<ffffff800834063c>] usb_set_configuration+0x604/0x648
    [<ffffff800834aee0>] generic_probe+0x58/0x80
    [<ffffff8008341fa0>] usb_probe_device+0x28/0x44
unreferenced object 0xffffffc008efd200 (size 512):
  comm "swapper/0", pid 1, jiffies 4294892825 (age 162.544s)
  hex dump (first 32 bytes):
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  backtrace:
    [<ffffff8008156eac>] __save_stack_trace+0x24/0x30
    [<ffffff80081574ac>] create_object+0x100/0x23c
    [<ffffff80085e7d28>] kmemleak_alloc+0x30/0x5c
    [<ffffff8008153cc4>] kmem_cache_alloc+0xd0/0x184
    [<ffffff800833a008>] hub_probe+0x144/0x804
    [<ffffff8008342190>] usb_probe_interface+0x1d4/0x1fc
    [<ffffff80082d18cc>] driver_probe_device+0x1b4/0x26c
    [<ffffff80082d1b04>] __device_attach_driver+0x9c/0xb0
    [<ffffff80082cff08>] bus_for_each_drv+0x84/0x94
    [<ffffff80082d1690>] __device_attach+0xa8/0x100
    [<ffffff80082d1c58>] device_initial_probe+0x10/0x18
    [<ffffff80082d0c84>] bus_probe_device+0x2c/0x8c
    [<ffffff80082cf094>] device_add+0x45c/0x51c
    [<ffffff800834063c>] usb_set_configuration+0x604/0x648
    [<ffffff800834aee0>] generic_probe+0x58/0x80
    [<ffffff8008341fa0>] usb_probe_device+0x28/0x44

实际代码的修改:

diff --git a/drivers/usb/core/hub.c b/drivers/usb/core/hub.c
index 46c99ef6bb82..04dc4a7ada46 100644
--- a/drivers/usb/core/hub.c
+++ b/drivers/usb/core/hub.c
@@ -1784,6 +1784,11 @@ static int hub_probe(struct usb_interface *intf, const struct usb_device_id *id)
        /* We found a hub */
        dev_info(&intf->dev, "USB hub found\n");

+       {
+               struct usb_hub *test;
+               test = kcalloc(1, sizeof(*test), GFP_KERNEL);
+               test = kzalloc(sizeof(*test), GFP_KERNEL);
+       }
        hub = kzalloc(sizeof(*hub), GFP_KERNEL);
        if (!hub)
                return -ENOMEM;

kmemleak 只能帮助解决一些非常简单的内存泄露问题,针对较为复杂的泄露路径,kmemleak 就无法处理了,并且 kmemleak 还存在着误检测的可能性。

kmemleak支持的操作

  • echo scan > /sys/kernel/debug/kmemleak:触发一次扫描
  • echo clear > /sys/kernel/debug/kmemleak:清除当前 kmemleak 记录的泄露信息
  • echo off > /sys/kernel/debug/kmemleak:关闭kmemleak(不可逆转的)
  • echo stack=off > /sys/kernel/debug/kmemleak:关闭task栈扫描
  • echo stack=on > /sys/kernel/debug/kmemleak:使能task栈扫描
  • echo scan=on > /sys/kernel/debug/kmemleak:启动自动内存扫描线程
  • echo scan=off > /sys/kernel/debug/kmemleak:停止自动内存扫描线程
  • echo scan=<secs> > /sys/kernel/debug/kmemleak:设置自动扫描线程扫描间隔,默认是600,设置0则是停止扫描
  • echo dump=<addr> > /sys/kernel/debug/kmemleak:dump某个地址的内存块信息,比如上面的echo dump=0xffffffc008efd200 > /sys/kernel/debug/kmemleak即可查看详细信息

另外,通过kernel command line传递kmemleak=off可关闭kmemleak,传递kmemleak=on则可开启kmemleak,这个前提是编译内核时已经配置了kmemleak。

扫描算法步骤

  1. 将所有对象标记为白色(剩下的白色对象稍后将被视为孤儿);
  2. 从数据部分和栈开始扫描内存,根据存储在rbtree中的地址检查值。如果找到一个指向白色对象的指针,则该对象被添加到灰色列表中;
  3. 扫描灰色对象寻找匹配地址(有些白色对象会变成灰色,并添加到灰色列表的末尾),直到灰色集结束;
  4. 剩下的白色对象被认为是孤立的,通过/sys/kernel/debug/kmemleak报告;

至于kmemleak的更多内容,可查看Documentation/dev-tools/kmemleak.rst内容。

Logo

华为云1024程序员节送福利,参与活动赢单人4000元礼包,更有热门技术干货免费学习

更多推荐