linux kernel内存泄漏检测工具之slub debug

发布于:2024-05-04 ⋅ 阅读:(35) ⋅ 点赞:(0)

一、背景

slub debug 是一个debug集,聚焦于kmem_cache 分配机制的slub内存(比如kmalloc),这部分内存在内核中使用最频繁,slub debug其中有相当部分是用来处理内存踩踏,内存use after free 等异常的,由于这部分的检测效果不如kasan(调试时slub前后填充不同的flag,在分配和释放时做检查,存在发现问题不及时的问题), 本文就不介绍了,本文关注slub debug当中的内存泄漏定位方法。

注意:本文中slub和slab名称有些混用,目前linux版本中实际默认都是使用slub,由于内核代码复用的缘故,有很多的函数名,结构体等还是slab命名,是slub还是slab还是以内核config是打开的CONFIG_SLUB还是CONFIG_SLAB来区分,本文所有实验和分析都是基于CONFIG_SLUB=y;

二、SLUB_DEBUG配置及调试工具

2.1 内核中相关配置

CONFIG_SLUB=y
CONFIG_SLUB_DEBUG=y
CONFIG_SLUB_DEBUG_ON=y
CONFIG_SLUB_STATS=y
#save the stack
CONFIG_STACKDEPOT=y

2.2 slub_debug命令行参数控制

这个参考 linux-6.6.1/Documentation/mm/slub.rst 描述即可
slub debug相关控制可以通过命令行方式进行:
slub_debug=<Debug-Options>
	打开debug选项,应用到所有的slub类型(具体类型可以参考/proc/slabinfo)

slub_debug=<Debug-Options>,<slab name1>,<slab name2>,...
	打开debug选项,可以选择指定的 slub name, 通过逗号隔开

debug option 当前可以选择调试类型,及开关控制字符

	F		Sanity checks on (SLAB_DEBUG_CONSISTENCY_CHECKS)
	Z		Red zoning(SLAB_RED_ZONE)
	P		Poisoning (object and padding) SLAB_POISON
	U		User tracking (free and alloc) SLAB_STORE_USER
	T		Trace (please only use on single slabs) SLAB_TRACE
	A		Enable failslab filter mark for the cache
	O		Switch debugging off for caches that would have
			caused higher minimum slab orders
	-		关闭所有slub 调试 (useful if the kernel is
			configured with CONFIG_SLUB_DEBUG_ON)

例子: slub 调试只开 sanity checks and red zoning:

	slub_debug=FZ

例子:开启调试,仅针对dentry cache

	slub_debug=,dentry

例子:调试 kmalloc-XXXX和dentry相关slub调试:

	slub_debug=P,kmalloc-*,dentry

例子: 关闭slub debug相关调试(开了CONFIG_SLUB_DEBUG_ON=y才需要)

	slub_debug=-

The state of each debug option for a slab can be found in the respective files
under::

	/sys/kernel/slab/<slab name>/

If the file contains 1, the option is enabled, 0 means disabled. 

slabinfo小工具,内核自带工具,能够方便快速的确认泄漏类型

aarch64-none-linux-gnu-gcc -o slabinfo tools/mm/slabinfo.c

slabinfo 有两个参数指令比较重要:
slabinfo -S      //按slub size占用大小排序slub类型及object个数信息等
slabinfo -T      //打印slub的统计信息

2.3 调试节点

查看slab 使用状态
/proc/slabinfo 
查看slab debug信息,统计状态等
/sys/kernel/slab/*
调试内存泄漏,踩踏等信息
/sys/kernel/debug/slab/*

三、slubdebug原理

3.1 slub 分配基本流程

创建kmem_cache
create_cache    //mm/slab-common.c
   -->struct kmem_cache* s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);
   -->__kmem_cache_create(s, flags);
      -->set_cpu_partial(s);
      -->init_kmem_cache_nodes(s)
      -->alloc_kmem_cache_cpus(s)
   -->list_add(&s->list, &slab_caches); //所有的kmem_cache都会加到slab_caches列表中

分配slub
kmem_cache_alloc  mm/slub.c
  -->__kmem_cache_alloc_lru  mm/slub.c
     -->slab_alloc   mm/slub.c
        -->slab_alloc_node mm/slub.c
           -->__slab_alloc_node
               -->object = c->freelist      //(1)如果freelist上有空闲,直接使用本cpu上cpu_slab->slab的
               -->object = __slab_alloc     //否则重新寻找
                   -->slub_percpu_partial   //(2)从cpu_slab->partial上取
                   -->freelist = get_partial(s, node, &pc);  //(3)从node上取         
                   -->slab = new_slab(s, gfpflags, node);    //(4)如果还获取不到,则从buddy中申请内存
                   -->set_track //开启slub debug时启动存储调用栈
  • 获取指定kemem_cache
  • 从percpu变量(struct kmem_cache_cpu*)cpu_slab上获取,从当前cpu cache(cpu_slab->slab)上freelist取
  • 如果cache上用完,则从cpu_slab的partial 列表上提取,并且将该slab 从partial列表删除,并添加到cpu cache上
  • 如果percpu上的cpu_slab上partial列表中用完,则从kmem_cache_node的partial上提取
  • 从page(buddy system)中分配一个struct slab (之前老版本是struct page,当前已经通过filo机制转换)

3.2 slub debug memleak 检测方法

  • 分配slub object时记录trace信息,会根据调用栈生成hash
  • 在每个slub object对应的tack区域存储hash值,记录pid,分配时间等信息
  • 提取分配信息时,扫描所有已分配object的track区域,根据hash值出现次数排序,并利用hash寻找对应的完整调用栈信息,最终排序打印出来

四、测试验证

4.1 测试代码:

#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,
    SLAB_ALLOC_FREE = 4,
};

static noinline void kmalloc_leak(size_t size, int cnt, bool bfree)
{
    char *ptr;

    int i = 0;
    for (; i < cnt; i++)
    {
        ptr = kmalloc(size, GFP_KERNEL);
        if(bfree)
            kfree(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, int param)
{
    switch(type) {
        case SLAB_LEAK:
            kmalloc_leak(128, param, false); //alloc 128 byte and repeat param times 
            break;

        case PAGE_LEAK: 
            pagealloc_leak(param);
            break;

        case VMALLOC_LEAK:
            vmalloc_leak(2048);
            break;

        case PCPU_LEAK:
            break;

        case SLAB_ALLOC_FREE:
            kmalloc_leak(128, param, true); //alloc 128 byte and free repeat param times 
            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[64] = {0};
    int ntcase;
    int nparam;
    int ret = 0;
    if(len > 64) {
        len = 64;
    }
    
    if (copy_from_user(kbuf, buf, len) != 0) {
        pr_info("copy the buff failed \n");
        goto done;
    }

    ret = sscanf(kbuf, "%d %d", &ntcase, &nparam);

    if (ret <= 0) {
        pr_err("should enter 2 param, first is test case type, second is param\n");
        goto done;
    }

    sample_kmemleak_test_case(ntcase, nparam);
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.2 定位泄漏

加载内存泄漏测试程序
/test # insmod kmemleak_driver.ko 

触发kmalloc 128 泄漏 100000次
/test # echo 0 100000 > /dev/sample_kmemleak_test 


利用工具slabinfo按的size排序查看 slub object,可以查看 space 总占用大小,及objects个数判断泄漏点 为kmalloc-128;
/test # ./slabinfo -S
Name                   Objects Objsize           Space Slabs/Part/Cpu  O/S O %Fr %Ef Flg
kmalloc-128             100331     128           25.6M       3136/1/0   32 1   0  49 U
inode_cache               6649     616            4.7M        290/1/0   23 2   0  86 aU
kernfs_node_cache        23966     128            4.6M       1142/1/0   21 0   0  65 U
dentry                    4041     192            1.0M        127/1/0   32 1   0  74 aU
kmalloc-1k                 384    1024          786.4K         24/0/0   16 3   0  50 U
kmalloc-512                417     512          458.7K         14/2/0   32 3  14  46 U
task_struct                102    3776          425.9K         13/2/0    8 3  15  90 U
kmalloc-2k                  93    2048          393.2K         12/1/0    8 3   8  48 U
kmalloc-4k                  46    4096          393.2K         12/1/0    4 3   8  47 U
radix_tree_node            596     576          393.2K         24/1/0   25 2   4  87 aU
kmalloc-192               1398     192          385.0K         47/1/0   30 1   2  69 U
kmalloc-256                472     256          245.7K         15/1/0   32 2   6  49 U
pool_workqueue             282     512          229.3K         14/1/0   21 2   7  62 U
sighand_cache              102    2080          229.3K          7/2/0   15 3  28  92 AU
biovec-max                  40    4096          196.6K          6/1/0    7 3  16  83 AU

......

如果没有工具,直接通过/proc/slabinfo节点来确认
/test # cat /proc/slabinfo 
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
......
kmalloc-4k            49     50  12288    2    8 : tunables    0    0    0 : slabdata     25     25      0
kmalloc-2k            94     95   6144    5    8 : tunables    0    0    0 : slabdata     19     19      0
kmalloc-1k           383    390   3072   10    8 : tunables    0    0    0 : slabdata     39     39      0
kmalloc-512          417    441   1536   21    8 : tunables    0    0    0 : slabdata     21     21      0
kmalloc-256          472    483    768   21    4 : tunables    0    0    0 : slabdata     23     23      0
kmalloc-192         1400   1404    296   27    2 : tunables    0    0    0 : slabdata     52     52      0
kmalloc-128       100331 100352    256   32    2 : tunables    0    0    0 : slabdata   3136   3136      0
//这里也可以通过num_objs * objsize的大小排序来确认是kmalloc-128出现泄漏
kmalloc-96           496    540    200   20    1 : tunables    0    0    0 : slabdata     27     27      0
kmalloc-64           877    928    256   32    2 : tunables    0    0    0 : slabdata     29     29      0
kmalloc-32           725    750    160   25    1 : tunables    0    0    0 : slabdata     30     30      0
kmalloc-16           927    928    128   32    1 : tunables    0    0    0 : slabdata     29     29      0
kmalloc-8           1614   1620    112   36    1 : tunables    0    0    0 : slabdata     45     45      0
kmem_cache_node      211    224    256   32    2 : tunables    0    0    0 : slabdata      7      7      0
kmem_cache           211    231    384   21    2 : tunables    0    0    0 : slabdata     11     11      0
......

查看具体的泄漏点
/test # cat /sys/kernel/debug/slab/kmalloc-128/alloc_traces
 100000 kmalloc_leak.constprop.0+0x54/0x80 [kmemleak_driver] age=12920/12945/12972 pid=95 cpus=3
        __kmem_cache_alloc_node+0xf4/0x2a4
        kmalloc_trace+0x20/0x2c
        kmalloc_leak.constprop.0+0x54/0x80 [kmemleak_driver]
        sample_kmemleak_test_case+0x9c/0xa8 [kmemleak_driver]
        sample_kmemleak_testcase_write+0xb0/0x12c [kmemleak_driver]
        vfs_write+0xc8/0x300
        ksys_write+0x74/0x10c
        __arm64_sys_write+0x1c/0x28
        invoke_syscall+0x48/0x110
        el0_svc_common.constprop.0+0x40/0xe0
        do_el0_svc+0x1c/0x28
        el0_svc+0x40/0xe4
        el0t_64_sync_handler+0x120/0x12c
        el0t_64_sync+0x190/0x194

     88 set_kthread_struct+0x38/0xc4 waste=1408/16 age=13270/17638/17797 pid=2 cpus=0-2
        __kmem_cache_alloc_node+0xf4/0x2a4
        kmalloc_trace+0x20/0x2c
        set_kthread_struct+0x38/0xc4
        copy_process+0x7a0/0x145c
        kernel_clone+0x68/0x368
        kernel_thread+0x80/0xb4
        kthreadd+0x144/0x1c4
        ret_from_fork+0x10/0x20
......

五、小结

5.1、性能分析

内存损耗比较大,从slabinfo也可以看到打开slub debug相关的config之后(/proc/slabinfo)下objsize大小变化,未开启时kmalloc-128 objsize就是128,开启debug后变成了384;

5.2 一个标准的slub泄漏定位方法

1、先确认slub存在内存泄漏

启动时记录/proc/meminfo 中SUnreclaim size 大小, 假设此时初始值为A ,然后每半小时或1小时检测SUnreclaim size 大小,假设为B, 如果B - A 增量超过600M 表示存在SUnreclaim 内存泄漏, 这个600M 可以根据实际项目,内存大小来综合设定

2、细化内存泄漏类型

如果版本存在SLUB内存泄漏, 抓取/proc/slabinfo或者使用slabinfo 工具来确认泄漏的slub 类型;

可以使用slabinfo -S 指令,或者直接通过/proc/slabinfo 中<num_objs> <objsize> 这两列信息做一个乘积,在excel 排序一下即可 (注意:这里的size 统计是不区分unreclaim的)

3、开启指定slub 类型泄漏的debug

例子:调试 kmalloc-XXXX和dentry相关slub leak调试:

slub_debug=U,kmalloc-*,dentry

或者全开:slub_debug=U

4、确认泄漏调用栈

等待内存泄漏一段时间后,执行下面即可确认slub泄漏调用栈及泄漏次数(乘上object的size即泄漏大小)

cat /sys/kernel/debug/slab/XXX/alloc_traces

5.3 优化和改进

商用版本上默认可以打开:CONFIG_SLUB_DEBUG=y, 不开CONFIG_SLUB_DEBUG_ON=y(或者参数传递slub_debug开启)对性能是无影响的;

缺陷: 无法开始时默认关闭,出现问题后动态打开,当前只能在发现版本存在slub内存泄漏后可以通过修改启动参数来控制调试开关,再来定位泄漏点;

如果问题复现概率极低,商用版本一直打开slub泄漏检测的话对用户的内存还是有较大影响,一般来说size占用是未开slub debug的2~4倍 。

不能动态开启的原因主要是 kmalloc的object 及size(是否有调试的metadata)在初始化时就确立了,后面无法修改object 和size,或者控制flag开关。

优化方案:当前android厂商针对这种情况也做了一些改进方案;利用vendor hook机制,在一个外部驱动中通过vendor hook机制动态修改kmalloc_caches,及获取 alloc trace即可完成针对kmalloc的动态调试;其核心思想是构建一套开启debug的kmem_cache, 然后替换原始的kmalloc_caches, 后续kmalloc就会走开启debug的kmem_cache,然后就可以定位问题

关键代码(从android kernel 5.15 拷贝,非android的只能仿照下自己加一下注册函数了):
mm/slab_common.c
//kmalloc分配时会根据size大小及flag类型去选择用kmalloc_caches中的哪一个
struct kmem_cache *kmalloc_slab(size_t size, gfp_t flags)
{
    unsigned int index;
    struct kmem_cache *s = NULL;

    if (size <= 192) {
        if (!size)
            return ZERO_SIZE_PTR;

        index = size_index[size_index_elem(size)];
    } else {
        if (WARN_ON_ONCE(size > KMALLOC_MAX_CACHE_SIZE))
            return NULL;
        index = fls(size - 1);
    }    

    trace_android_vh_kmalloc_slab(index, flags, &s); //1.注册这里vendor hook可以替换掉后面的kmalloc_cache
    if (s)
        return s;

    return kmalloc_caches[kmalloc_type(flags)][index];
}



mm/slub.c中
static void set_track(struct kmem_cache *s, void *object,
            enum track_item alloc, unsigned long addr)
{
    struct track *p = get_track(s, object, alloc);

    if (addr) {
#ifdef CONFIG_STACKTRACE
        unsigned int nr_entries;

        metadata_access_enable();
        nr_entries = stack_trace_save(kasan_reset_tag(p->addrs),
                          TRACK_ADDRS_COUNT, 3);
        metadata_access_disable();

        if (nr_entries < TRACK_ADDRS_COUNT)
            p->addrs[nr_entries] = 0; 
#endif
        p->addr = addr;
        p->cpu = smp_processor_id();
        p->pid = current->pid;
        p->when = jiffies;
        trace_android_vh_save_track_hash(alloc == TRACK_ALLOC, p);  //2.注册这里外部的ko即可获取到调用栈
    } else {
        memset(p, 0, sizeof(struct track));
    }    
}

ko中的实现基本就是将slub.c,slab_common.c中的依赖的结构体,函数等重写一下,注册好上面的两个hook函数,一个替换掉kmalloc_caches, 一个存储slub分配的调用栈,剩下的就是处理好编译报错,将/sys/kernel/debug/slab/XXX/alloc_traces 的实现用在新增的ko中,增加调试节点,即可完成动态开启slubdebug的功能。

参考

极致Linux内核:Linux内存:块分配器slab、slob和slub

SLUB DEBUG原理

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/include/linux/slub_def.h?h=v6.6.23&id=bb192ed9aa7191a5d65548f82c42b6750d65f569

Linux 内存管理新特性 - Memory folios 解读 | 龙蜥技术

五花肉:内存管理特性分析(七):Linux内核复合页(Compound Page)原理分析


网站公告

今日签到

点亮在社区的每一天
去签到