Fork me on GitHub
侧边栏

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

image

上图是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)

image

上图简单介绍一下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显示泄漏信息

image

五、小结

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,就不再展开介绍。

posted @ 2025-04-08 14:45  yooooooo  阅读(344)  评论(0)    收藏  举报