一、背景

linux 内存泄漏问题及调试方法大概如下:

这里只介绍内核内存泄漏的部分(上图右下角部分)。

二、如何判定内存泄漏的原因?

在定位问题时,通常我们需要先界定问题类型,这里可以通过/proc/meminfo来判断(这里之介绍内核相关)

MemTotal:        1947580 kB  //所有可用RAM大小(物理内存减去保留内存和内核的代码大小)
MemFree:         1810924 kB  //未使用的内存
MemAvailable:    1878920 kB  //应用程序可用内存,计算时包含了可以回收的部分,
                               MemAvailable <= MemFree + Active(file) + Inactive(file) + SReclaimable
......  
Slab:              10068 kB  //可收回Slab的大小(SUnreclaim+SReclaimable=Slab)
SReclaimable:       1508 kB
SUnreclaim:         8560 kB  //相比开机或者正常业务运行时增长较多则表示存在泄漏
KernelStack:        1616 kB  //用户进程对应的内核栈大小,每个线程16k,可以根据此值的变化计算是否存在线程创建过多或者泄漏
......
VmallocTotal:   133141626880 kB
VmallocUsed:        2164 kB  //vmalloc 使用量
VmallocChunk:          0 kB
Percpu:              800 kB  //percpu  变量占用大小
......

通常MemAvailable变小,同时发现SUnreclaim, VmallocUsed,Percpu增长过多时可以利用kmemleak定位

三、kmemleak简介

3.1 原理简介

kmemleak实现方法是一个插桩加扫描过程, 它提供一个kmemleak_alloc桩函数,这个函数会在内核slab、vmalloc、alloc_bootmem、pcpu_alloc等函数分配接口中被调用,每次调用时该函数均会创建一个kmemleak object记录分配内存的相关信息比如内存地址,大小,调用栈等,并将这个object加入到一个rbtree里面;当内核释放内存时,也会调用kmemleak_free删除对应object。这个kmemleak的rbtree相当于记录了所有内核分配的内存信息。那么当内核线程kmemleak开始扫描时,会以这个rbtree为基准对内核数据段、stack及堆等等区域进行扫描寻找和rbtree中object地址相同数据,如果数据相同代表这个内存区还有人在使用,如果没找到代表这个可能时内存泄漏。

kmemleak将内存块(object)标记为3种颜色,分别为黑色、白色、灰色, 通过count和min_count区分不同颜色的object。

黑色: min_count = -1,表示被忽略的object,此object不包含对别人的引用,也不会存在内存泄漏,比如代码段会标记为黑色。

白色: count < min_count,孤立的object,没有足够的引用指向这个object,一轮扫描结束后被认为泄漏的内存块。

灰色: min_count = 0,表示不是孤立的object,即不存在内存泄漏的object,如代码中主动标记object为灰色,防止误报; 或者count >= min_count,对该object有足够的指针引用,认为不存在内存泄漏的内存块。

这里的min_count和count进行组合,count是在内存数据中找到指针的次数,而min_count是用来判断是否出现泄漏的边界判断条件,之所以要将min_count单独设计一个变量控制,是因为不同的内存分配类型确定内存泄漏的min_count不同; 比如vmalloc 分配时,即使我们人为制造一个泄漏(返回的指针ptr存在局部变量上),vmalloc内部的实现也会在两个地方存储这个ptr(vmalloc内部会生成一个struct vm_struct 结构体,vm_struct->addr 会记录这个地址), 所以vmalloc的min_count是2 。

3.2 具体检测步骤:

  • 1、通过struct kmemleak_object(简称为object)描述kmalloc、vmalloc、kmem_cache_alloc等函数申请的内存块,记录申请内存的起始地址,大小、call trace等信息。同时把object加入到红黑树object_tree_root和双向链表object_list中,红黑树中的key值为内存块的起始地址。
  • 2、遍历双向链表object_list,把所有的object的count计数清0,即在新的一轮扫描前,尽可能的把能复位成白色的object标记为白色。然后判断object是否是灰色(默认data、bss、ro_after_init段会被标记为灰色),如果是灰色的object则把object加入到灰色链表gray_list中。
  • 3、扫描内存中可能存放指针的内存区域(per-cpu段、struct page的内容、内核栈、灰色链表),根据挂在红黑树中所有的object的地址范围进行对比。如果有指针指向某一个object(指向该object的起始地址或者指向object地址范围内),会把object对应的count字段增加1,如果object变成灰色,则会把object加入到灰色链表中。
  • 4、扫描object_list中的白色对象的object,判断object所描述的地址范围的内容的crc值是否发生变化,如果发生变化,则同样把object加入到灰色链表gray_list中。说明通过间接的方式访问了object描述的地址范围,不是内存泄漏,减少误报。
  • 5、重新扫描灰色链表,因为步骤4中,可能有些白色的object加入到了灰色链表中,需要重新扫描。
  • 6、经过上述一系列的扫描,剩余白色的object就是可疑的内存泄漏点。

四、kmemleak使用

4.1 相关宏介绍

1、CONFIG_HAVE_DEBUG_KMEMLEAK

所有kmemleak相关config的依赖

2、CONFIG_DEBUG_KMEMLEAK

kmemleak功能开关,打开后会建立/sys/kernel/debug/kmemleak接口

3、CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF

此宏打开后,kmemleak默认关闭,可以通过cmdline中通过kmemleak=on打开

4、CONFIG_DEBUG_KMEMLEAK_AUTO_SCAN

支持kmemleak自动扫描,可以设置扫描时间间隔,默认为600秒,关闭则不会自动扫描

5、CONFIG_DEBUG_KMEMLEAK_MEM_POOL_SIZE=16000

kmemleak object数据结构预留内存,在实际运行时,会先通过slub系统申请object内存,如果申请失败才会使用这里定义的静态物理内存,此宏可以设置预留静态物理内存的大小,通常不需要关注

4.2 kemeleak控制节点

通过kemeleak 调试节点完成对kmemleak控制及日志获取。

1、echo scan > /sys/kernel/debug/kmemleak

触发kmemleak扫描。

2、cat /sys/kernel/debug/kmemleak

获取当前扫描的结果,如果存在泄漏则打印泄漏size和分配调用栈

3、echo off > /sys/kernel/debug/kmemleak

关闭kmemleak功能。

4、echo stack=on/off > /sys/kernel/debug/kmemleak

打开或关闭task内存栈扫描,默认打开。

5、echo scan=on/off > /sys/kernel/debug/kmemleak

打开或关闭自动扫描,默认打开。

6、echo scan=XXX > /sys/kernel/debug/kmemleak

设置自动扫描时间间隔,单位为秒。

7、echo clear > /sys/kernel/debug/kmemleak

将当前标记为内存泄漏的object设置为gray, 并且后面忽略打印;如果kememleak功能关闭,则会释放所有的object内存占用

8、echo dump=XXX > /sys/kernel/debug/kmemleak

获取某个指针地址扫描结果。

4.3 测试代码

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/workqueue.h>
#include <linux/jiffies.h>
#include <asm/page.h>
#include <linux/vmalloc.h>
#include <linux/mm.h>

enum sample_kmemleak_test_case{
    SLAB_LEAK = 0,
    PAGE_LEAK = 1,
    VMALLOC_LEAK = 2,
    PCPU_LEAK = 3,
};

static noinline void kmalloc_leak(size_t size, int write_offset)
{
    char *ptr;

    ptr = kmalloc(size, GFP_KERNEL);
    pr_info("%s %llx\n", __func__, (unsigned long long)ptr);

}

static noinline void pagealloc_leak(size_t order)
{
	struct page *pages;
    char *ptr;

	pages = alloc_pages(GFP_KERNEL, order);
    ptr = page_address(pages);

	pr_info("%s page addr %llx, page_to_virt %llx\n", __func__, (unsigned long long)pages, (unsigned long long)ptr);

}

static noinline void vmalloc_leak(size_t size)
{
    char *v_ptr;

    v_ptr = vmalloc(size);

    OPTIMIZER_HIDE_VAR(v_ptr);

    pr_info("%s %llx", __func__, (unsigned long long)v_ptr);
    v_ptr[0] = 0;

}

static noinline void sample_kmemleak_test_case(int type)
{
    switch(type) {
        case SLAB_LEAK:
            kmalloc_leak(128, 2); //alloc 128 byte and overwrite 2 offset
            break;

        case PAGE_LEAK: //can't detect
            pagealloc_leak(0);
            break;

        case VMALLOC_LEAK:
            vmalloc_leak(2048);
            break;

        case PCPU_LEAK:
            break;

        default :
            pr_info("undef error type %d\n", type);
            break;
    }
    pr_info("%s type %d\n", __func__, type);
}

static noinline ssize_t sample_kmemleak_testcase_write(struct file *filp, const char __user *buf,
                   size_t len, loff_t *off)
{
    char *kbuf;
    int ntcase;
    kbuf = kmalloc(len + 1, GFP_KERNEL);
    if (copy_from_user(kbuf, buf, len) != 0) {
        pr_info("copy the buff failed \n");
        goto done;
    }

    ntcase = simple_strtoul(kbuf, NULL, 0);

    sample_kmemleak_test_case(ntcase);
done:
    return len;
}

static struct file_operations sample_kmemleak_fops = {
    .owner  =   THIS_MODULE,
    .write  =   sample_kmemleak_testcase_write,
    .llseek =   noop_llseek,
};

static struct miscdevice sample_kmemleak_misc = {
    .minor  = MISC_DYNAMIC_MINOR,
    .name   = "sample_kmemleak_test",
    .fops   = &sample_kmemleak_fops,
};

static int __init sample_kmemleak_start(void) 
{
    int ret;

    ret = misc_register(&sample_kmemleak_misc);
    if (ret < 0) {
        printk(KERN_EMERG " sample_kmemleak test register failed %d\n", ret);
        return ret;
    }

    printk(KERN_INFO "sample_kmemleak test register\n");
    return 0;
}

static void __exit sample_kmemleak_end(void) 
{ 
    misc_deregister(&sample_kmemleak_misc);
} 

MODULE_LICENSE("GPL");
MODULE_AUTHOR("geek");
MODULE_DESCRIPTION("A simple kmemleak test driver!");
MODULE_VERSION("0.1");
 
module_init(sample_kmemleak_start);
module_exit(sample_kmemleak_end);

4.4 验证结果

/sys/kernel/debug # echo scan > kmemleak 
[   27.860877] kmemleak: 1 new suspected memory leaks (see /sys/kernel/debug/kmemleak)
/sys/kernel/debug # echo 0 > /dev/sample_kmemleak_test 
[   38.527578] kmalloc_leak ffff000003de0000
[   38.528102] sample_kmemleak_test_case type 0

/sys/kernel/debug # echo 2 > /dev/sample_kmemleak_test 
[   48.085362] vmalloc_leak ffff800082b2d000
[   48.085574] sample_kmemleak_test_case type 2
/sys/kernel/debug # echo scan >  kmemleak 
/sys/kernel/debug # [   66.061077] kmemleak: 3 new suspected memory leaks (see /sys/kernel/debug/kmemleak)

/sys/kernel/debug # cat kmemleak 
...

unreferenced object 0xffff000003de0000 (size 128):
  comm "sh", pid 97, jiffies 4294901906 (age 96.796s)
  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:
    [<0000000065c9d44f>] __kmem_cache_alloc_node+0x1cc/0x29c
    [<00000000e79b13ac>] kmalloc_trace+0x20/0x2c
    [<00000000ebc0ac96>] kmalloc_leak.constprop.0+0x20/0x48 [kmemleak_driver]
    [<0000000075c5d47d>] sample_kmemleak_test_case+0x84/0x88 [kmemleak_driver]
    [<000000000b7610a3>] sample_kmemleak_testcase_write+0x7c/0xf4 [kmemleak_driver]
    [<00000000dd79e720>] vfs_write+0xc8/0x300
    [<00000000f52b754a>] ksys_write+0x74/0x10c
    [<0000000087192a82>] __arm64_sys_write+0x1c/0x28
    [<00000000c164c5fa>] invoke_syscall+0x48/0x110
    [<00000000af7f1aae>] el0_svc_common.constprop.0+0x40/0xe0
    [<00000000f72be685>] do_el0_svc+0x1c/0x28
    [<0000000024327bec>] el0_svc+0x40/0xe4
    [<0000000059966142>] el0t_64_sync_handler+0x120/0x12c
    [<00000000ec8342ce>] el0t_64_sync+0x190/0x194
unreferenced object 0xffff000004238030 (size 8):
  comm "sh", pid 97, jiffies 4294903204 (age 91.604s)
  hex dump (first 8 bytes):
    31 0a 23 04 00 00 ff ff                          1.#.....
  backtrace:
    [<0000000065c9d44f>] __kmem_cache_alloc_node+0x1cc/0x29c
    [<00000000746a0a3a>] __kmalloc+0x48/0x78
    [<00000000326b14a1>] sample_kmemleak_testcase_write+0x28/0xf4 [kmemleak_driver]
    [<00000000dd79e720>] vfs_write+0xc8/0x300
    [<00000000f52b754a>] ksys_write+0x74/0x10c
    [<0000000087192a82>] __arm64_sys_write+0x1c/0x28
    [<00000000c164c5fa>] invoke_syscall+0x48/0x110
    [<00000000af7f1aae>] el0_svc_common.constprop.0+0x40/0xe0
    [<00000000f72be685>] do_el0_svc+0x1c/0x28
    [<0000000024327bec>] el0_svc+0x40/0xe4
    [<0000000059966142>] el0t_64_sync_handler+0x120/0x12c
    [<00000000ec8342ce>] el0t_64_sync+0x190/0x194


unreferenced object 0xffff800082b35000 (size 4096):
  comm "sh", pid 97, jiffies 4298240980 (age 24.276s)
  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:
    [<000000009b270b1d>] __vmalloc_node_range+0x438/0x664
    [<00000000f03d11c6>] vmalloc+0x58/0x68
    [<0000000063f55c01>] vmalloc_leak.constprop.0+0x18/0x48 [kmemleak_driver]
    [<000000008a1021c9>] sample_kmemleak_test_case+0x7c/0x88 [kmemleak_driver]
    [<000000000b7610a3>] sample_kmemleak_testcase_write+0x7c/0xf4 [kmemleak_driver]
    [<00000000dd79e720>] vfs_write+0xc8/0x300
    [<00000000f52b754a>] ksys_write+0x74/0x10c
    [<0000000087192a82>] __arm64_sys_write+0x1c/0x28
    [<00000000c164c5fa>] invoke_syscall+0x48/0x110
    [<00000000af7f1aae>] el0_svc_common.constprop.0+0x40/0xe0
    [<00000000f72be685>] do_el0_svc+0x1c/0x28
    [<0000000024327bec>] el0_svc+0x40/0xe4
    [<0000000059966142>] el0t_64_sync_handler+0x120/0x12c
    [<00000000ec8342ce>] el0t_64_sync+0x190/0x194

五、小结

适用范围及注意事项

1、Kmemleak存在漏检或误检,扫描判断标准是通过数据区域是否还保存有和申请内存地址相同数据,这意味这如果地址数据有变化或者被转换可能会有误报,比如申请虚拟内存后,将其转换为物理地址保存,本身并没有泄漏; 再例如当前已出现内存泄漏问题,但是恰好内存中有一个变量其值等于之前申请内存起始地址,那么Kmemleak将会误判等等。kmemleak并不是精准检测内存泄漏,而是对内存泄漏提供有效信息。

2、Kmemleak追踪范围,kmemleak当前追踪kmalloc,vmalloc,kmem_cache_alloc及per_cpu函数,但是并不支持alloc_page、ioremap等系列函数。

3、打开CONFIG_DEBUG_SLAB或CONFIG_SLUB_DEBUG宏可以增强kmemleak检测精度。

4、如果在特殊场景需要申请内存,但是不引用,可以使用kmemleak_not_leak、kmemleak_ignore、kmemleak_no_scan等函数进行设置,避免产生误测。

5、消耗大量内存,性能较差,只在调试版本使用

参考:

https://www.eet-china.com/mp/a113356.html

https://lotabout.me/2021/Linux-Available-Memory/

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐