一、背景
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)原理分析