当内核有内存泄漏的时候

内存泄露是一个很容易出现的问题,尤其是对于测试不太充分的代码。怎么判断出现内存泄露了呢?很简单,就跑一些简单的测试,等待足够长时间即可。内存总有耗尽的时候,这时候内核会触发OOM,根据oom_score选择一个进程杀掉。这种时候,多半是有问题了。

或者在某个进程运行的时候,看/proc/meminfo 观察空闲内存部分,一直下降多半是有问题了。这就确定有内存泄露了,可能是用户态进程泄露,也可能是内核泄露。可以有多种方法来分辨:

  • 观察meminfo中的slab项,如果这一项有异常的增长或者只增不减一路狂飙,那八成是内核漏出去了
  • OOM killer会根据oom score选择一个进程杀掉,假如选择了desktop Xserver等等,就说明找不到分更高的进程了,这么front且和用户体验相关的进程nice值是很低的。这都能选上,那就是用户态进程没啥占内存特别高的进程了,差不多也能确定是内核泄露了。
  • OOM killer杀了进程之后,观察OOM打出来的内存信息,并没有收回来多少。内核的内存泄露,不管OOM选择的是哪个进程来杀,内存都收不回来的,oom完事之后系统使用起来依然觉得卡卡的,那就是内核泄露了。

现在确定了内核泄露的方法,接下来怎么找到泄漏点呢,下面会介绍几种调试的方法。在此之前,先简单实现一个有内存泄漏的内核模块。

C 复制代码
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/list.h>
#include <linux/percpu.h>
#include <linux/fdtable.h>

struct list_node {
	long header[25];
	struct list_head list;
	char name[25];
};

static LIST_HEAD(test_list);

/*
 * Some very simple testing. This function needs to be extended for
 * proper testing.
 */
static int __init module_init(void)
{
	struct list_node *list;
	int i;

	pr_info("This is test mode for memleak debug tools!\n");

	/*alloc */
	pr_info("kmalloc(32) = %p\n", kmalloc(32, GFP_KERNEL));
	pr_info("kmalloc(32) = %p\n", kmalloc(32, GFP_KERNEL));
	pr_info("kmalloc(1024) = %p\n", kmalloc(1024, GFP_KERNEL));
	pr_info("kmalloc(1024) = %p\n", kmalloc(1024, GFP_KERNEL));
	pr_info("kmalloc(2048) = %p\n", kmalloc(2048, GFP_KERNEL));
	pr_info("kmalloc(2048) = %p\n", kmalloc(2048, GFP_KERNEL));
	pr_info("kmalloc(4096) = %p\n", kmalloc(4096, GFP_KERNEL));
	pr_info("kmalloc(4096) = %p\n", kmalloc(4096, GFP_KERNEL));

	pr_info("vmalloc(64) = %p\n", vmalloc(64));
	pr_info("vmalloc(64) = %p\n", vmalloc(64));
	pr_info("vmalloc(64) = %p\n", vmalloc(64));
	pr_info("vmalloc(64) = %p\n", vmalloc(64));
	pr_info("vmalloc(64) = %p\n", vmalloc(64));

	/*
	 * create a list have 10 nodes
	 */
	for (i = 0; i < 10; i++) {
		list = kzalloc(sizeof(*list), GFP_KERNEL);
		pr_info("kzalloc(sizeof(*list)) = %p\n", list);
		if (!list)
			return -ENOMEM;
		INIT_LIST_HEAD(&list->list);
		list_add_tail(&list->list, &test_list);
	}

	return 0;
}
module_init(module_init);

static void __exit module_exit(void)
{
	struct test_node *list, *tmp;

	//delete all node in test
	list_for_each_entry_safe(list, tmp, &test_list, list)
		list_del(&list->list);
}
module_exit(module_exit);

MODULE_LICENSE("GPL");

kmemleak

第一个debug的方法也是最简单的------使用Kmemleak,这是一种用于检测内核内存泄漏的工具,可以用来检测目前内核中可能存在的泄露。kmemleak 是一种运行时的工具,它可以在内核运行时进行内存泄漏检测,但也会带来一定的性能开销。因此,通常情况下发行版并不会打开此工具。

Kmemleak使用

  • 先打开Kmemleak的相关配置:

    CONFIG_DEBUG_KMEMLEAK //kmemleak总开关
    CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF //是不是默认禁用kmemleak,非必须;
    // 打开选项之后,可以通过cmdline kmemleak=on 打开kmemleak;关闭这个选项,也可以通过cmdline kmemleak=off禁用

  • 编译安装内核

    能看到/sys/kernel/debug/kmemleak这个文件,说明kmemleak已经配置好了。

  • make 并insmod写好的test模块

  • 触发内存scan

    模块插入后等几分钟执行echo scan > /sys/kernel/debug/kmemleak触发一次内存scan去找可能存在的内存泄露,这是整个内存的scan,可能需要点时间。

  • 查看结果

    通过cat /sys/kernel/debug/kmemleak查看内存泄漏的结果,这时候可以下面的结果:

    cat /sys/kernel/debug/kmemleak

    unreferenced object 0xffff89862ca702e8 (size 32):
    comm "modprobe", pid 2088, jiffies 4294680594 (age 375.486s)
    hex dump (first 32 bytes):
    6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b kkkkkkkkkkkkkkkk
    6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b a5 kkkkkkkkkkkkkkk.
    backtrace:
    [<00000000e0a73ec7>] 0xffffffffc01d3446
    [<000000000c5d2a46>] do_one_initcall+0x41/0x1df
    [<0000000046db7e0a>] do_init_module+0x55/0x200
    [<00000000542b9814>] load_module+0x203c/0x2480
    [<00000000c2850256>] __do_sys_finit_module+0xba/0xe0
    [<000000006564e7ef>] do_syscall_64+0x43/0x110
    [<000000007c873fa6>] entry_SYSCALL_64_after_hwframe+0x44/0xa9

calltrace最上面一行就是执行malloc的位置,可以根据addr2line或者gdb把地址和代码对应起来。是不是超级简单,超级容易。打开选项重编一个内核,就知道所有可能泄漏的位置。

为什么说可能存在的泄露位置呢,因为Kmemleak存在误报的情况。

kmemleak误报

kmemleak工作流程大概分为三部分:标记分配、检测释放和报告泄露。

  1. 标记分配的内存:kmemleak 会跟踪内核中的动态内存分配,包括 kmalloc、kzalloc、vmalloc 等函数分配的内存块。标记这些分配的内存块,并记录它们的地址和大小。

  2. 检测释放:当内存块被释放时,kmemleak 会记录释放的内存块,并在一段时间后检查是否仍然存在对这些内存块的引用。

  3. 报告泄漏:扫描内存并报告这些内存泄漏,扫描方法如下:

    • 标记所有对象为白色:初始时,kmemleak 会将所有的内存对象标记为白色。在后续的扫描中,仍然被标记为白色的对象将被视为孤立对象(orphan)。

    • 扫描内存:从数据段和栈开始扫描内存,检查其中的数值是否与存储在红黑树(rbtree)中的地址相匹配。如果发现指向白色对象的指针,则将该对象添加到灰色列表中。

    • 扫描灰色对象:对灰色对象进行扫描,查找匹配的地址。一些白色对象可能会变为灰色,并被添加到灰色列表的末尾,直到灰色集合完成扫描。

    • 报告孤立对象:剩余的白色对象被视为孤立对象,因为数据段和堆栈里没有指向这块空间的变量/常量,内核认为这块空间未来不会被使用,而且这时候似乎free并不能释放这块空间,白色对象通过 /sys/kernel/debug/kmemleak 报告。

内核中本身有一些内存是不需要释放的,比如说核心相关的部分do_init_call函数,或者此块内存地址可以通过其他的什么方法计算得到(除了container_of,scan的流程是:假如现在扫到栈里面有一个指针指向结构体的某一个成员,整个结构体占的这块空间也会被标记为灰色,这个操作就是兼容container_of的意思),kmemleak仍然会把这块内存当泄露处理。

误报概率非常非常低,绝大部分情况是非常非常好用的!几乎一切内存泄露都逃不过kmemleak的检查,慧眼如炬明察秋毫!

然而接下来来了一个这样的场景:假如我现在的系统不方便换内核,比如说生产服务器或者拿不到内核源码,这下怎么办呢,因为性能问题Kmemleak是绝对不会在任何一个非debug发行版上开启的,这就引入了第二个方式------如何在不修改内核代码的情况下拿到内核运行的相关内容呢。

相关推荐
传而习乎6 分钟前
Linux:CentOS 7 解压 7zip 压缩的文件
linux·运维·centos
程序猿进阶12 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
我们的五年16 分钟前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
IT果果日记37 分钟前
ubuntu 安装 conda
linux·ubuntu·conda
Python私教40 分钟前
ubuntu搭建k8s环境详细教程
linux·ubuntu·kubernetes
羑悻的小杀马特1 小时前
环境变量简介
linux
小陈phd1 小时前
Vscode LinuxC++环境配置
linux·c++·vscode
是阿建吖!1 小时前
【Linux】进程状态
linux·运维
明明跟你说过2 小时前
Linux中的【tcpdump】:深入介绍与实战使用
linux·运维·测试工具·tcpdump
Komorebi.py3 小时前
【Linux】-学习笔记05
linux·笔记·学习