linux kernel内存泄漏检测工具之page_owner
一、背景
page泄漏也是linux中常见的一种内存泄漏类型,本文将介绍page owner的定位方法,由于page owner存储的信息位于page_ext区域,这里也简单介绍一下page_ext的存储区域,page如何找到对应的page_ext信息,最后还是一个简单的测试驱动演示如何使用page_owner
二、page_owner配置及调试工具
1、开启方法
CONFIG_PAGE_EXTENSION=y
CONFIG_PAGE_OWNER=y
打开方法a:
CONFIG_PAGE_OWNER_ENABLE_DEFAULT=y
打开方法b:
不配置CONFIG_PAGE_OWNER_ENABLE_DEFAULT
启动时增加"page_owner=on" 到 cmdline中
2、确认是否打开成功:
是否存在节点/sys/kernel/debug/page_owner
3、调试工具
源码:tools/mm/page_owner_sort.c
交叉工具链编译生成:aarch64-none-linux-gnu-gcc -o page_owner_sort tools/mm/page_owner_sort.c
4、分析page泄漏
/test# cat /sys/kernel/debug/page_owner > pageowner.txt
/test# ./page_owner_sort -t pageowner.txt test.txt
test.txt中就是排序结果
5、page_owner_sort工具更多用法
(input指的就是cat /sys/kernel/debug/page_owner的结果,output是排序结果)
Documentation/mm/page_owner.rst中有详细说明
排序:
-a 按内存分配时间排序
-m 按总内存排序
-p 按pid排序。
-P 按tgid排序。
-n 按任务命令名称排序。
-r 按内存释放时间排序。
-s 按堆栈跟踪排序。
-t 按时间排序(默认)。
--sort <order> 指定排序顺序。排序的语法是[+|-]key[,[+|-]key[,...]]。从
**标准格式指定器**那一节选择一个键。"+"是可选的,因为默认的方向是数字或
词法的增加。允许混合使用缩写和完整格式的键。
例子:
./page_owner_sort <input> <output> --sort=n,+pid,-tgid
./page_owner_sort <input> <output> --sort=at
其它函数::
剔除:
--cull <rules>
指定剔除规则。剔除的语法是key[,key[,...]]。从**标准格式指定器**
部分选择一个多字母键。
<rules>是一个以逗号分隔的列表形式的单一参数,它提供了一种指定单个剔除规则的
方法。 识别的关键字在下面的**标准格式指定器**部分有描述。<规则>可以通过键的
序列k1,k2,...来指定,在下面的标准排序键部分有描述。允许混合使用简写和完整形
式的键。
Examples:
./page_owner_sort <input> <output> --cull=stacktrace
./page_owner_sort <input> <output> --cull=st,pid,name
./page_owner_sort <input> <output> --cull=n,f
过滤:
-f 过滤掉内存已被释放的块的信息。
选择:
--pid <pidlist> 按pid选择。这将选择进程ID号出现在<pidlist>中的块。
--tgid <tgidlist> 按tgid选择。这将选择其线程组ID号出现在<tgidlist>
中的块。
--name <cmdlist> 按任务命令名称选择。这将选择其任务命令名称出现在
<cmdlist>中的区块。
<pidlist>, <tgidlist>, <cmdlist>是以逗号分隔的列表形式的单个参数,
它提供了一种指定单个选择规则的方法。
例子:
./page_owner_sort <input> <output> --pid=1
./page_owner_sort <input> <output> --tgid=1,2,3
./page_owner_sort <input> <output> --name name1,name2
三、page_owner原理
在介绍page_owner实现原理前,需要介绍稀疏内存模型SPARSEMEM,它很好的支持了内存空洞以及内存的热插拔,已经替换掉了原先的平坦内存模型 FLATMEM; 关于sparse内存模型和flat内存模型差异,主要是管理页的差异,由于物理内存地址通常是不连续的,采用sparse内存模型能更节约内存。
3.1 sparsemem 相关config
CONFIG_ARCH_SPARSEMEM_ENABLE=y
CONFIG_SPARSEMEM=y
CONFIG_SPARSEMEM_EXTREME=y
CONFIG_SPARSEMEM_VMEMMAP_ENABLE=y
CONFIG_SPARSEMEM_VMEMMAP=y
3.2 sparsemem相关知识
定义arch/arm64/include/asm/mmzone.h
extern struct pglist_data *node_data[];
#define NODE_DATA(nid) (node_data[(nid)])
//用来管理所有page的结构体mem_section
#ifdef CONFIG_SPARSEMEM_EXTREME
extern struct mem_section **mem_section;
#else
extern struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT];
#endif
#define SECTIONS_PER_ROOT (PAGE_SIZE / sizeof (struct mem_section))
//页帧转换section,确认page所在section的index
static inline struct mem_section *__pfn_to_section(unsigned long pfn)
{
return __nr_to_section(pfn_to_section_nr(pfn));
}
static inline unsigned long pfn_to_section_nr(unsigned long pfn)
{
return pfn >> PFN_SECTION_SHIFT;
}
#define PFN_SECTION_SHIFT (SECTION_SIZE_BITS - PAGE_SHIFT)
static inline unsigned long section_nr_to_pfn(unsigned long sec)
{
return sec << PFN_SECTION_SHIFT;
}
//memstart_addr是物理地址的起始地址,这里会把VMEMMAP_START减去平台的起始物理地址
//这样做的目的是VMEMMAP_START会和平台的起始地址对齐;
#define vmemmap ((struct page *)VMEMMAP_START - (memstart_addr >> PAGE_SHIFT))
//由于我们是4kpage,所以SECTION_SIZE_BITS是27位,一个section是256M;
#ifdef CONFIG_ARM64_64K_PAGES
#define SECTION_SIZE_BITS 29
#else
#define SECTION_SIZE_BITS 27
#endif /* CONFIG_ARM64_64K_PAGES */
//页帧号与页的相互转换,vmemmap + pfn
#define __pfn_to_page(pfn) (vmemmap + (pfn))
#define __page_to_pfn(page) (unsigned long)((page) - vmemmap)
如何确认平台物理起始地址,通常lk/uboot/uefi中会有打印;
在我使用的qemu中(memstart_addr在arm64_memblock_init中赋值)
(gdb) p /x memstart_addr
$2 = 0x4000000
查看结构体大小
(gdb) p sizeof(struct page)
$3 = 64
(gdb) p sizeof(struct mem_section)
$8 = 32
(gdb) p page_owner_ops
$31 = {offset = 8, size = 56, need = 0xffff800081ac3eb8 <need_page_owner>, init = 0xffff800081ac3f70 <init_page_owner>, need_shared_flags = true}
(gdb) p page_ext_size
$32 = 64

上图是vmemmap,mem_section , page,物理地址及kernel 线性地址的关系,本文只需要理解所有物理地址都有一个struct page对应,一个mem_section关联256M大小的物理内存(64k个page, 物理内存大小和平台以及page size相关),通过mem_section 可以找到page_ext的额外信息,而page_ext存放着我们需要的page debug信息。
核心结构体描述:
struct mem_section {
......
#ifdef CONFIG_PAGE_EXTENSION
/*
* If SPARSEMEM, pgdat doesn't have page_ext pointer. We use
* section. (see page_ext.h about this.)
*/
struct page_ext *page_ext; //用来存放page泄漏的debug信息
unsigned long pad;
#endif
......
};
实际当指针使用,只是用来记录每个page_ext的起始地址,page_ext的实际大小由另外一个全局变量page_ext_size记录
struct page_ext {
unsigned long flags;
};
//page owner调试信息
struct page_owner {
unsigned short order;
short last_migrate_reason;
gfp_t gfp_mask; //page的flag信息
depot_stack_handle_t handle; //page分配的调用栈
depot_stack_handle_t free_handle; //page释放的调用栈
u64 ts_nsec;
u64 free_ts_nsec;
char comm[TASK_COMM_LEN]; //分配page的进程信息
pid_t pid;
pid_t tgid;
};
如何获取page owner
mm/page_owner.c
void __dump_page_owner(const struct page *page)
{
struct page_ext *page_ext = page_ext_get((void *)page); // 1.获取page的page_ext信息
struct page_owner *page_owner;
depot_stack_handle_t handle;
......
page_owner = get_page_owner(page_ext); //2、获取page_owner结构体信息
......
handle = READ_ONCE(page_owner->handle); //3、获取page对应的调用栈存放handle
if (!handle)
pr_alert("page_owner allocation stack trace missing\n");
else
stack_depot_print(handle); //4、打印page分配时的调用栈
......
page_ext_put(page_ext);
}
获取page_ext方法
mm/page_ext.c
-->page_ext_get(struct page* page)
-->lookup_page_ext(page)
-->pfn = page_to_pfn(page) //通过page找到对应的物理页帧号pfn
-->section = __pfn_to_section(pfn) //通过pfn找到所在的mem_section
-->page_ext = READ_ONCE(section->page_ext) //通过mm_section->page_ext + pfn* page_ext_size 获取pfn物理页的page_ext
-->get_entry(page_ext, pfn) //page_ext + page_ext_size * pfn;
获取page_owner信息
get_page_owner(struct page_ext *page_ext)
-->page_ext_data(page_ext, &page_owner_ops);
-->return (void *)(page_ext) + ops->offset;
//ops->offset在mm/page_ext.c中invoke_need_callbacks中计算
分配时记录page_owner
alloc_pages
-->__alloc_pages
-->get_page_from_freelist //这里只列了从freelist取page,partial及zone上流程相同
-->prep_new_page
-->post_alloc_hook
-->set_page_owner
-->__set_page_owner
-->handle = save_stack //存储调用栈,并返回调用栈hash计算后的标记handle
-->page_ext = page_ext_get(page) //获取page的page_ext信息
-->__set_page_owner_handle(page_ext, handle... //将进程,调用栈handle等信息记录到pageowner中
-->page_ext_put(page_ext)

上图简单介绍一下page_ext的所在区域,启动时分配并存在mm_section->page_ext中,使用时先通过page_ext_get获取页对应的page_ext,再通过函数get_page_owner提取struct page_owner信息;检查泄漏也就是一个扫描所有page的page_ext信息的过程,根据page是否分配flag获取其调用栈,并将调用栈按次数排序即可确认泄漏点。
四、测试验证
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 调试
1、加载测试驱动,触发page泄漏
由于测试驱动没有加泄露次数,直接脚本循环触发,1000次页order为0泄漏
#!/bin/sh
i=0
while [ $i -lt 1000 ];
do
echo 1 0 > /dev/sample_kmemleak_test
i=$((i+1))
echo $i
done
echo trigger $i times page leak
2、抓取泄漏日志(这个节点只是扫描所有的page并把page owner信息打印出来,确认泄漏点还需要page_owner_sort工具做个排序)
/test# cat /sys/kernel/debug/page_owner > pageowner.txt
3、按调用栈次数排序
./page_owner_sort pageowner.txt test.txt --cull=st,pid,name
4、test.txt显示泄漏信息

五、小结
5.1 slub泄漏能利用page owner发现吗?
答案是可以的,只是没有那么精准,因为slub本身就是在page的细化使用,只有触发slab_alloc->page_alloc(也就是每次slab用满,触发新的slab分配)的那次才会记录到page owner信息中。
同样做一个小实验,复用之前slub泄漏的测试用例
1、触发128M slub内存泄漏
echo 0 1048576 > /dev/sample_kmemleak_test
2、抓取泄漏日志
/test# cat /sys/kernel/debug/page_owner > pageowner.txt
3、按调用栈次数排序(也是可以抓到的,但是如果slub的泄漏量较小或者及慢可能抓取不到)
32768 times, 32768 pages, PID 95, task_comm_name: sh:
get_page_from_freelist+0xfb4/0x1140
__alloc_pages+0x170/0xe60
alloc_pages+0xac/0x160
new_slab+0x3d0/0x478
___slab_alloc+0x4a8/0x8a8
__slab_alloc.isra.0+0x34/0x68
__kmem_cache_alloc_node+0xf4/0x270
kmalloc_trace+0x20/0x2c
kmalloc_leak.constprop.0+0x54/0x80 [kmemleak_driver]
sample_kmemleak_test_case+0xa0/0xac [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
5.2 其他
内核内存泄漏的调试方法介绍完毕,实际用得比较多的还是slub debug和page owner(按需进行),另外还有KernelStack 泄漏(通常就是线程泄漏,每个task有8k内核栈大小,这个值过大说明系统存在线程泄漏,实际项目中这个也可以作为一个监控指标),vmalloc泄漏(/proc/vmallocinfo可以统计),由于这些也不需要开特殊debug,就不再展开介绍。


浙公网安备 33010602011771号