内核内存分配的基础分析

本文阅读量 Posted by Kird on 2020-09-12

内核内存分配的基础分析

通常,内存泄漏会发生在用户态,比如开发者使用malloc()分配一部分空间后,使用完后没有通过free()进行回收,那么就会出现内存泄漏问题,导致内存占用越来越大。

如果内存泄漏发生在内核或者设备驱动中,可能应用开发者排查起来相对陌生,那么怎么分析内存泄漏是否是内核或者设备驱动导致的呢?

本文将会通过简单的实验测试流程为你分析内核中内核泄漏,通过systemstap等工具进行观察具体的tracepoint,来判断是否发生内核内存分配。

kmalloc和vmalloc

malloc()函数是用户态中的内存分配函数,在内核态中,使用kmalloc和vmalloc函数进行动态内存空间分配。kmalloc和vmalloc的区别是:kmalloc分配的虚拟地址对应的物理空间是连续的,而vmalloc申请的内存对应的物理空间是不连续的。通过vmalloc分配的地址,由于物理空间不连续,所以获得的页需要逐个进行映射,相比之下效率不高,一般是在获取大内存的时候使用。

简单的总结下几点区别:

  • vmalloc分配的地址一般为高地址,kmalloc则从低地址开始分配
  • vmalloc分配大内存,kmalloc分配小内存
  • vmalloc分配的空间对应的物理地址不连续,kmalloc分配的空间对应的物理地址连续
  • kmalloc常用于设备DMA操作,vmalloc不能用于DMA

在操作系统中,/proc/meminfo文件中提供了kmalloc和vmalloc的分配情况,相关信息如下:

1
2
3
4
5
6
7
8
9
#cat /proc/meminfo |grep -E 'Slab|SReclaimable|SUnreclaim|Vmalloc'

Slab: 178836 kB #kmalloc申请
SReclaimable: 113084 kB
SUnreclaim: 65752 kB

VmallocTotal: 34359738367 kB
VmallocUsed: 34860 kB #vmalloc申请
VmallocChunk: 34359644636 kB

上面输出中,Slab为kmalloc申请的内存信息,其中包括SReclaimable可回收的部分和SUnreclaim不可回收的部分。VmallocUsed为vmalloc申请的内存信息。

观察内核内存分配

上节描述了可以通过系统提供的meminfo文件来观察内核中内存的分配情况,下面通过编写简单的内核模块来进行vmalloc内存分配和释放。

内核模块核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//vmalloc 分配
char *mem_alloc(unsigned long size)
{
char *p = vmalloc(size);
if (!p)
{
pr_info("[kernel vmalloc test ] alloc fail !");
}
return p;
}
//vfree 释放
void mem_free(const void *p)
{
if (p)
{
vfree(p);
}
}

编写内核模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <linux/init.h>
#include <linux/vmalloc.h>
#include <linux/module.h>
#define SIZE (1024 * 1024 * 1024)

char *kaddr;
//vmalloc 分配
char *mem_alloc(unsigned long size)
{
char *p = vmalloc(size);
if (!p)
{
pr_info("[mem_alloc] alloc fail !");
}
return p;
}
//vfree 释放
void mem_free(const void *p)
{
if (p != NULL)
{
vfree(p);
}
}
static int __init kmem_init(void)
{
pr_info("[kmem_test]: kernel memory init\n");
kaddr = mem_alloc(SIZE);
return 0;
}

static void __exit kmem_exit(void)
{
mem_free(kaddr);
pr_info("[kmem_test]: kernel memory exit\n");
}

module_init(kmem_init)
module_exit(kmem_exit)

MODULE_LICENSE("GPLv2");

使用makefile进行内核模块编译:

1
2
3
4
5
obj-m = kmem_test.o
all:
make -C /lib/modules/`uname -r`/build M=`pwd`
clean:
rm -f *.o *.ko *.mod.c *.mod *.a modules.order Module.symvers

编译成内核模块:

看下加载模块后的内存情况:

卸载模块后的内存情况:

上面的测试能便于你更好的理解meminfo文件中的统计信息。那么如果内核发生内存泄漏会怎么样呢?

内存发生内存泄漏,即未释放申请过的内存空间。由于内核空间内存的生命周期和内核生命周期是一致的,所以当发生内核模块导致的内存泄漏时,单单卸载有问题的模块是不能解决问题的,必须通过重启操作系统才能够释放掉这些空间。

可见,内核内存泄漏检查是一个很重要的事,尽量避免第三方模块和设备驱动的内存泄漏。

内核内存泄漏的分析工具-kmemleak

如果我们想要对内核内存泄漏做些基础的分析,最好借助一些内核内存泄漏分析工具,其中最常用的分析工具就是kmemleak。
kmemleak 是内核内存泄漏检查的利器,但是,它的使用也存在一些不便性,因为打开该特性会给性能带来一些损耗,所以生产环境中的内核都会默认关闭该特性。该特性我们一般只用在测试环境中,然后在测试环境中运行需要分析的驱动程序以及其他内核模块。
与其他内存泄漏检查工具类似,kmemleak 也是通过检查内核内存的申请和释放,来判断是否存在申请的内存不再使用也不释放的情况。如果存在,就认为是内核内存泄漏,然后把这些泄漏的信息通过 /sys/kernel/debug/kmemleak 这个文件导出给用户分析。

读者可以自行研究。

使用systemtap动态追踪内存分配

systemtap在之前的文章中有介绍到,具体使用也可以参考使用Systemtap监视TCP连接队列溢出这篇文章。

使用systemtap分析kernel或模块的内存分配和释放,编写stp脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// kmem_test.stp 
// usage: stap -x pid -v kmem_test.stp
// Author: 0xFE

global free = 0;
global alloc = 0;
global module_test_alloc = 0;
global module_test_free = 0;
probe kernel.function("vfree")
{
free++;
//println("vfree occur!")
}
probe kernel.function("vmalloc").return
{
alloc++;
//println("vmalloc occur!")
}
probe module("kmem_test").function("mem_alloc").return
{
module_test_alloc++;
println("module.kmem_test->func.mem_alloc occur!")
}
probe module("kmem_test").function("mem_free").return
{
module_test_free++;
println("module.kmem_test->func.mem_free occur!")
}
probe end
{
printf("kernel alloc %d free %d\n", alloc, free);
printf("module_test alloc %d free %d\n", module_test_alloc, module_test_free);
}

运行stap后进行模块加载,观察打印出来的信息:

image-20200912122020551

可以看到运行stp脚本后能够追踪到内存分配。但是卸载模块没看到free,不知道是哪里的问题?

疑问:测试了几次还是只有malloc,没抓到free,如果知道原因的作者可以给我留言或者邮件,多谢!



支付宝打赏 微信打赏

赞赏支持一下