Linux内核学习——内存管理

内存管理

内核的内存分配是比较复杂的,内核是不能休眠,另外内核也不好处理内存分配错误。

内核使用页作为内存管理的基本单位,这是因为内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址的硬件设备)通常以页作为单位进行处理。大多数32位体系结构支持4kb的页,而64位体系结构则支持8kb的页。

内核用以下结构表示物理页:

1
2
3
4
5
6
7
8
9
10
11
//部分域
struct page{
unsigned long flags; //存放页的状态(脏页,锁定内存的页面)
atomic_t _count; //引用计数,没有引用时为-1
atomic_t _mapcount;
unsigned long private; //作为私有数据
struct address_space *mapping; //由页缓存使用
pgofff_t index;
struct list_head lru;
void *virtual; //页的虚拟地址
};

由于硬件的显示,位于不同物理地址的页可能不能执行一些特定的任务,内核需要把页分为不同的区进行管理。linux主要使用了四种区;

  • ZONE_DMA——这里的页可以被特定硬件进行DMA操作(直接访问)
  • ZONE_DNA32——与上面的类似,但只能被32位设备访问
  • ZONE_NORMAL——正常映射的页
  • ZONE_HIGNEM——这个区为“高端内存”所在区域,不能永久映射内核地址空间

LINUX把系统的页分为不同区,形成了不同的内存池。但不是说取页就只能从特定的区域里取。例如有一些普通的内存既可以从ZONE_DMA分配,也可以从ZONE_NORMAL。

获得页

内核提供了一种请求内存的底层,有那么几种接口:

1
struct page * alloc_pages(gfp_t gfp_mask, unsigned int order);//分配2^order个连续的物理页面,指针指向第一个page结构体

也可以获得填充为0的页,避免随机产生了垃圾信息或者敏感信息,保证安全:

1
unsigned long get_zeroed_page(unsigned int gfp_mask);

释放页面,释放页需要谨慎,因为传递了错误的struct page或者地址会导致系统,因为内核是完全相信自己的:

1
2
void free_pages(unsigned long addr, unsigned int order);
void free_page(unsigned long addr);

kmalloc()

kmalloc()函数与用户控件malloc()类似,都是以字节为单位进行分配,只是多了一个flags参数。

1
2
#include <linux/slab.h>
void *kmalloc(size_t size, gfp_t flags);

在返回指针之后,需要检查返回的是不是NULL,如果是,需要处理该错误。

gfp_mask标志

这些分配器标志可以分为三类:行为修饰符、区修饰符以及类型

行为修饰符

常见的行为修饰符,比如__GFP_WAIT(分配器可以睡眠),__GFP_HIGH(分配器可以访问紧急时间缓冲池),还有一些其它的修饰符。

区修饰符

区修饰符则是表示内存区应当从何处分配:

  • __GFA_DMA:从ZONE_DMA分配;
  • __GFA_DMA32:从ZONE_DMA32分配;
  • __GFA_HIGNMEN:从ZONE_HIGNMEM或ZONE_NORMAL分配;

类型标识

类型标识更像是结合上面两种修饰符来完成特定类型的处理。

比如用于中断程序,不能睡眠时:GFP_ATOMIC;分配让分配器睡眠,交换、一些页到硬盘,可以用GFP_KERNEL。

kfree()

释放由kmalloc()分配的内存块,kfree(NULL)是安全的。

vmalloc()

vmalloc类似kmalloc,只不过kmalloc分配的是物理地址连续的内存,而vmalloc分配的是虚拟地址连续的内存。

虽然一般只有硬件设备才需要物理地址连续的内存,但内核多数也是使用kmalloc进行分配,基于性能的考虑,vmalloc获得的页必须要逐个映射到虚拟地址,这会导致大量TLB抖动。当然,为了获取大内存,一般使用vmalloc

slab层

为了避免频繁地分配和回收,linux内核提供了slab分配器,作为通用数据结构缓存层。

slab层的设计

slab层把不同的对象划分为不同的高速缓存组,因此可能一个高速缓存组存放着进程描述符,另一个高速缓存存放着索引结点。

一般来说,每个高速缓存由若干个slab组成,而slab可能仅仅由一个页面组成,因此每个slab都包含一些对象成员。对于slab来说,只有三种状态:满、部分满或者空。在分配时,先从部分满的分配,再考虑空的slab,最后再考虑创建一个slab。

每个高速缓存都使用kmem_cache结构表示,其中有三个链表:slabs_full,slabs_partial和slabs_empty。链表中含有以下的slab结构:

1
2
3
4
5
6
7
struct slab {
struct list_head list;
unsigned long colouroff; //slab着色偏移量
void *s_mem; //slab中第一个对象
unsigned int inuse; //slab中已分配的对象数
kmem_bufctl_t free; //第一个空闲对象
};

slab分配器可以创建新的slab,这是通过函数__get_free_pages()实现的,分配的页大小为2的幂次方。

slab分配器的接口

一个新的高速缓存通过函数kmem_cache_create()创建。需要指定高速缓存中每个元素的大小和第一个对象的偏移以保证对齐。

在栈上的静态分配

每个进程的内核栈大小依赖于体系结构,用户空间能够负担起非常大的栈,并且栈可以动态正增长。

单页内核栈

在2.6系列的内核早期,可以设置一个单页内核栈,即进程的内核栈只有一页大小。一方面可以减少内存消耗,另一方面可以避免因为内存碎片的增多,难以寻找未分配的连续的页。

在栈上光明正大的工作

在栈上进行大量的静态分配是很危险的,因为会发送栈溢出,导致邻堆末端的东西被覆盖。因此尽量使用动态分配,让函数所有的局部变量所占空间之和不超过几百字节。

高端内存的映射

高端内存,就是不会永久映射到内核地址空间上的内存,在x86体系上,所有高于896MB的物理内存多是高端内存。

永久映射

要映射一个给定的page结构到内核地址空间:

1
void *kmap(struct page* page);

这个函数在高端内存或者地段内存中都能使用

临时映射

临时映射(原子映射)是为了创建一个映射而当前上下文又不能睡眠时提供的,内核可以原子地把高端内存中的一个页映射到某个保留的映射中。

1
void *kmap_atomic(struct page *page, enum km_type type);

分配函数的选择

上面我们讲了多种分配函数,但如何选择呢?

如果内核需要连续的物理内存,应该使用kmalloc;

如果需要连续的虚拟内存,应该使用vmalloc,但性能相对于kmalloc会低一点;

对于高端内存,可以使用alloc_pages,返回一个struct page的结构指针;如果要得到真正的指针,应该使用kmap(),将高端内存映射到内核逻辑地址;

如果要频繁创建和撤销大的数据结构,slab高速缓存是一种更好的选择,slab层通过维持一个对象高速缓存,避免频繁地分配和释放内存,只需要根据需要从缓存中得到对象就可以了。