当内核有内存泄漏的时候

内存泄露是一个很容易出现的问题,尤其是对于测试不太充分的代码。怎么判断出现内存泄露了呢?很简单,就跑一些简单的测试,等待足够长时间即可。内存总有耗尽的时候,这时候内核会触发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发行版上开启的,这就引入了第二个方式------如何在不修改内核代码的情况下拿到内核运行的相关内容呢。

相关推荐
IC 见路不走1 小时前
LeetCode 第91题:解码方法
linux·运维·服务器
翻滚吧键盘2 小时前
查看linux中steam游戏的兼容性
linux·运维·游戏
小能喵2 小时前
Kali Linux Wifi 伪造热点
linux·安全·kali·kali linux
汀沿河2 小时前
8.1 prefix Tunning与Prompt Tunning模型微调方法
linux·运维·服务器·人工智能
zly35002 小时前
centos7 ping127.0.0.1不通
linux·运维·服务器
小哥山水之间3 小时前
基于dropbear实现嵌入式系统ssh服务端与客户端完整交互
linux
ldj20203 小时前
2025 Centos 安装PostgreSQL
linux·postgresql·centos
翻滚吧键盘3 小时前
opensuse tumbleweed上安装显卡驱动
linux
cui_win4 小时前
【内存】Linux 内核优化实战 - net.ipv4.tcp_tw_reuse
linux·网络·tcp/ip
CodeWithMe7 小时前
【Note】《深入理解Linux内核》 Chapter 15 :深入理解 Linux 页缓存
linux·spring·缓存