linux内核设计与实现 - 内存管理

第12章 内存管理

小结:

  1. 内存空间的不同描述单位(字节、页、区(DMA、normal、highmem)
  2. 各种内存分配机制(页分配器、slab分配器
  3. 注意分配的状态约束(是否可阻塞?访问文件系统约束?),因此有各种gfp标识
  4. 各种内存分配接口(alloc_pages、__get_free_pages、kmalloc、slab)
  5. 高端内存、每个CPU变量

文章目录

  • 第12章 内存管理
    • 12.1 页
    • 12.2 区
    • 12.3 获得页
    • 12.4 kmalloc()
      • 12.4.1 kamalloc简介
      • 12.4.2 gfp_mask标志符
      • 12.4.3 kfree()
      • 12.4.5 vmalloc()
    • 12.6 slab层
      • 12.6.1 slab层的设计
    • 12.7 在栈上的静态分配
    • 12.8 高端内存的映射
    • 12.9 每个CPU的分配
    • 12.10 新的每个cpu接口
    • 12.11 使用每个CPU数据的原因
    • 12.12 分配函数的选择

内核中分配内存不容易,根本原因在于内核本身不能奢侈地使用内存,一般内核不能睡眠,且其分配机制不能太复杂。与用户空间中的内存分配不太一样。

12.1 页

内存管理单元(MMU)通常以页为单位进行处理。大多数32位体系结构支持4KB的页,而64位体系结构一般会支持8KB的页。内核用struct page结构表示系统中的每个物理页。内核用这个结构来管理系统中所有的页,需要知道页是否空闲、谁拥有这个页,用户空间进程?动态分配的内核数据?静态内核代码?页高速缓存?

struct page{
	unsigned long flags;		//页状态,是不是脏,是不是锁定在内存中
	atomic_t	_count;			// 引用计数
	atomic_t	_mapcount;		
	unsigned long private;		// 私有数据
	struct address_space *mapping;		//指向的页高速缓存
	pgoff_t index;	
	struct list_head lru;
	void *virtual;				// 页的虚拟地址,但高端内存并不永久地映射到内核地址空间,这个值为NULL
}

一个页可以由页缓存使用(此时,mapping域指向和这个页关联的address_space对象),或者作为私有数据(由private指向),或者作为进程页表中的映射。

12.2 区

  1. 区引入的原因:
    由于硬件的限制,内核并不能对所有页一视同仁。linux必须处理由如下2种由硬件存在缺陷而引起的内存寻址问题:
  • 一些硬件只能用某些特定的内存地址来执行DMA
  • 一些体系结构的内存的物理寻址方位比虚拟寻址范围大得多。使得一些内存不能永久地映射到内核空间上。
  1. linux主要使用四种区(以32位x86为例)
  • ZONE_DMA - 这个包含的页能用来执行DMA操作(<16MB)
  • ZONE_DMA32 - 可用来执行DMA操作,但与ZONE_DMA不同这些页面只能被32位设备访问
  • ZONE_NORMAL - 这个区包括的都是能正常映射的页(16MB - 896MB)
  • ZONE_HIGHEM - 这个区包含高端内存,其中的页不能永久映射到内核地址空间(>896MB的所有物理内存)
  1. 注意:

    1. 区的划分没有任何物理意义,只是内核为了管理页而采取的一种逻辑上的分组。可以从不同区分配页,但不能同时从两个区分配。
    2. x86-64没有ZONE_HIGHMEM,应为可以映射所有物理空间,它只有ZONE_DMA和ZONE_NORMAL区。
  2. struct zone

struct zone{
	unsigned long watermark[NR_WMARK];	// 该区的最小值、最低和最高水平值
	unsigned long lowmem_reserve[MAX_NR_ZONES];
	struct per_cpu_pageset pageset[NR_CPUS];
	spintlock_t lock;			// 防止该结构被并发访问,注意这个锁只保护结构,不保护这个区的所有页
	struct free_area free_area[MAX_ORDER];
	spinlock_t lru_lock;
	struct zone_lru{
		struct list_head list;
		unsigned long nr_saved_scan;
	} lru[NR_LRU_LISTS];
	const char *name;	//是一个以NULL结束的字符串表示这个区的名字。内核启动时初始化,三个区的名字为DMA、Normal、HighMem
};

12.3 获得页

我们已经对内核如何管理内存(页、区等)有所了解,现在看看内核实现分配和释放内存的接口。所有接口都以页为单位分配内存。

(1). 分配内存,最核心的函数:struct page* alloc_pages(gfp_t gfp_mask, unsigned int order)
该函数分配2order(1< (2). 物理到逻辑地址:void *page_address(struct page *page)返回指向物理页当前所在的逻辑地址
(3). 或者结合(1)和(2): unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)返回第一个页的逻辑地址,因为是连续的,所以其他页也会紧随其后。
(4). 如果你只需要一个页:
struct page * alloc_page(gfp_t gfp_mask);
unsigned long __get_free_page(gfp_t gfp_mask)
(5). 获得填充为0的页:unsigned long get_zeroed_page(unsigned int gfp_mask)
(6). 释放页:
void __free_pages(struct page *page, unsigned int order)
void free_pages(unsigned long addr, unsigned int order)
void free_page(unsigned long addr)
释放页时要谨慎,只能释放属于你的页,传递错误会导致系统崩溃。记住内核是完全信赖自己的。

分配页 释放页
alloc_page(gfp_mask) __free_pages(page, order)
alloc_pages(mask, order) free_pages(addr, order)
__get_free_page(mask) free_page(addr)
__get_free_pages(mask, order)
get_zeroed_page(mask)

分配页时用上面方法,分配字节为单位的通常用kmalloc()

12.4 kmalloc()

12.4.1 kamalloc简介

kmalloc()可以获得以字节为单位的一块内核内存,所分配的内存区在物理上是连续的。出错时返回NULL。最终可能调用alloc_pages()。
void * kmalloc(size_t size, gfp_t flags)
返回一个指向内存块的指针,其内存块大小至少有size大小。

12.4.2 gfp_mask标志符

可以分为三类:行为标识符、区标识符、类型标识符。一般使用类型标识符即可。
行为标识符:表示内核应当如何分配所需的内存,如是否可睡眠
区标识符:表示从哪里分配内存
类型标识符:组合了行为标识符和区标识符,将各种可能的组合归纳为不同类型,简化使用,如GFP_KERNEL,在内核中进程上下文可以使用
(1). 行为修饰符

标志 描述
__GFP_WAIT 分配器可睡眠
__GFP_HIGH 分配器可以访问紧急事件缓冲池
__GFP_IO 分配器可以启动磁盘I/O
__GFP_FS 分配器可以启动文件系统I/O
__GFP_COLD 分配器应该使用高速缓存中快要淘汰出去的页
__GFP_NOWARN 分配器将不打印失败警告
__GFP_REPEAT 分配器将在分配失败时重复进行分配,但还是存在失败的可能
__GFP_NOFALL 分配器将无限地重复进行分配,分配不能失败
__GFP_NORETRY 分配器在分配失败时绝对不会重新分配
__FGP_NO_GROW 由slab层内部使用
__GFP_COMP 添加混合页元数据,在hugetlb的代码中使用

(2). 区修饰符

标志 描述
__GFP_DMA 从ZONE_DMA分配
__GFP_DMA32 只在ZONE_DMA32分配
__GFP_HIGHMEM 从ZONE_HIGHMEM或ZONE_NORMAL分配

不能给_get_free_pages()或kalloc()指定ZONE_HIGHMEM,因为这两个函数返回的都是逻辑地址,而不是page结构,这两个函数分配的内存肯能还没有映射到内存的虚拟地址空间。只有alloc_pages()才能分配高端内存。

(3). 类型标识符

标识类型 描述
GFP_ATOMIC 用在中断处理、下半部、持有自旋锁以及其他不能睡眠的地方
GFP_NOWAIT 与GFP_ATOMIC类似,但调用不会退给紧急内存池
GFP_NOIO 分配可以阻塞,但不会启动磁盘I/O
GFP_NOFS 在必要时可能阻塞,也可能启动磁盘I/O,但不会启动文件系统操作,这个标志在你不能再启动另一个文件系统的操作时,用在文件系统部分的代码
GFP_KERNEL 常规分配方式,可能会阻塞,内核会尽力而为
GFP_USER 常规分配方式,可能会阻塞,用于为用户空间进程分配内存时
GFP_HIGHUSER 从ZONE_HIGHMEM进行分配,可能会阻塞。用于为用户空间进程分配内存
GFP_DMA 从ZONE_DMA进行分配,用于获取能供DMA使用的内存的设备驱动使用,一般会结合GFP_ATOMIC和GFP_KERNEL

(4). 何时使用哪种标志

情形 相应标志
进程上下文,可以睡眠 使用GFP_KERNEL
进程上下文,不可以睡眠 使用GFP_ATOMIC,在你睡眠之前或之后以GFP_KERNEL执行内存分配
中断处理程序 使用GFP_ATOMIC
软中断 使用GFP_ATOMIC
tasklet 使用GFP_ATOMIC
需要用于DMA的内存,可以睡眠 使用(GFP_DMA|GFP_KERNEL)
需要用于DMA的内存,不可以睡眠 使用(GFP_DMA

12.4.3 kfree()

kfree()释放由kmalloc()分配出来的内存块。调用kfree(NULL)是安全的。

12.4.5 vmalloc()

vmalloc()类似kmalloc(), 不过vmalloc()分配的内存虚拟地址是连续的,而物理地址则无须连续。大多数情况下,只有硬件设备需要得到物理地址连续的内存,而仅供软件使用的内存块(例如与进程相关的缓冲区)就可以使用只有虚拟地址连续的内存块。

许多内核代码通过kmalloc()获得内存,这是出于性能考虑。 vmalloc()为了把物理上不连续的页转换为虚拟地址空间上连续的页,必须专门建立页表项。糟糕的是,通过vmalloc()获得的页必须一个一个地映射,会导致比直接内存大得多的TLB抖动,所以vmalloc()只会在为了获得大块内存时才会使用,如模块被动态插入到内核时。

  1. 声明
    void* vmalloc(unsigned long size)
    错误返回NULL,函数可能睡眠
  2. 释放
    void vfree(const void* addr)
    函数可睡眠

12.6 slab层

slab分配器扮演通用数据结构缓存层的角色。slab分配器从以下几个方面考虑:
(1). 频繁使用的数据结构也会被频繁分配和释放,应当缓存
(2). 频繁分配和回收会导致内存碎片
(3). 回收的对象可立即投入下一次分配。
(4). 如果分配器知道对象大小、页大小和总的高速缓存大小等,会做出更睿智的决策。
(5). 如果让部分缓存专属单个处理器,那么分配和回收就可以在不加SMP锁。
(6). 对存放的对象进行着色,防止多个对象映射到相同的高速缓存行

12.6.1 slab层的设计

slab层把不同的对象划分为高速缓存组,每个高速缓存组都存放不同类型的对象。kmalloc()建立在slab层之上,使用了一组通用高速缓存。

这些高速缓存划分为slab,slab由一个或多个物理连续的页组成,一般是一个页。每个slab都包含一些对象成员。分配新对象时,从部分满的slab中分配,否则从空的slab中分配,若没有则创建。

例:inode结构,他们会被频繁创建和释放,用slab分配器来管理他们十分必要,因而struct inode就由inode_cachep高速缓存进行分配。分配时,内核从部分满的slab或空的slab返回一个指向已分配但未使用的结构的指针,当用完后,slab分配器将对象标记为空闲

  1. 结构
    每个高速缓存都使用kmem_cache结构表示,包含三个链表:slabs_full、slabs_partial和slabs_empty。这些链表包含高速缓存中的所有slab,slab描述符如下:
struct slab{
	struct list_head list;		//满、部门满或空链表
	unsigned long colouroff;	//slab着色的偏移量
	void* s_mem;				//slab中的第一个对象
	unsigned int inuse;			//slab中已分配的对象数
	kmem_bufctl_t free;			//第一个空闲对象
}

slab描述符要么在slab之外另行分配,要么放在slab自身开始的地方。
slab分配器可以创建新的slab,这是通过__get_free_pages()低级内核页分配器进行的
( alloc_pages、__get_free_pages()是最底层的分配方式,kmalloc–>slab–>__get_free_pages()
2. 方法
kmem_getpages():空间满时,用于创建新的slab
kmem_freepages():释放slab,最终调用free_pages()

  1. slab分配器的接口
    (1). 创建新的高速缓存:kmem_cache_create(name, size, align, flags, void(*ctor)(void*));
    失败返回NULL,此函数可能睡眠。
    (2). flags参数:
    SLAB_HWCACHE_ALIGN - slab内对象按高速缓存行对齐。
    SLAB_POISON:使用slab层已知的值(a5a5a5)填充slab,有利于对未初始化内存的访问。
    SLAB_RED_ZONE:在已分配内存周围插入红色警戒区以探测缓冲越界。
    SLAB_PANIC: 分配失败时提醒slab层。
    SLAB_CACHE_DMA:使用DMA的内存给slab分配空间。
    (3). 撤销高速缓存:kmem_cache_destroy()
    函数可能睡眠。

  2. 高速缓存的使用
    (1). 分配:void * kmem_cache_alloc(cachep, flags)
    (2). 释放:void kmem_cache_free(cachep, objp) 标记为空闲

总之,流程是先用kmem_cache_create创建高速缓存,用kmem_cache_alloc分配对象,用完后kmem_cache_free返回,全都不用了之后用kmem_cache_destroy销毁。

12.7 在栈上的静态分配

节省栈资源,所有局部变量之和不要超过几百字节,因为栈溢出消无声息。因此,进行动态分配是一种明智的选择

12.8 高端内存的映射

高端内存页不能永久映射到内核地址空间,因此通过alloc_pages()以__GFP_HIGHMEM标志的页不可能有逻辑地址。

  1. 永久映射
    void *kmap(struct page* page):将给定page的结构映射到内核地址空间,高端、低端内存都可以用。
    函数可睡眠
  2. 解除映射
    void kunmap(struct page page)*
  3. 临时映射
    用在不可以睡眠的地方。有一组保留的映射,可以存放新创建的临时映射。
    void *kmap_atomic(struct page* page, enum km_type type)
    void kunmap_atomic(void *kvaddr, enum km_type type)
    此函数不会阻塞,但也禁止内核抢占。因为映射对每个处理器都是唯一的???(调度可能对哪个处理器执行哪个进程会有变动)
    内核可完全忘记kmap_atomic()映射,kunmap_atomic()也无须做什么事情,下一个原子映射将自动覆盖前一个映射???

12.9 每个CPU的分配

unsigned long my_percpu[NR_CPUS];
cpu = get_cpu();
my_percpu[cpu]++;	//获得当前处理器,并禁止内核抢占
my_percpu[cpu]++;		
put_cpu();	//激活内核抢占

上述代码不用锁,(1). 操作数据对当前处理器来说是唯一的; (2). get_cpu()时就已经禁止内核抢占

12.10 新的每个cpu接口

  1. 编译时的每个CPU变量(静态分配)
    DEFINE_PER_CPU(type, name);
    get_cpu_var()和put_cpu_var()操作变量:返回当前处理器上的指定变量,同时禁止抢占
    per_cpu(name, cpu)++:获得别的处理器上的每个CPU数据,但不会禁止内核抢占,也不会提供锁保护。
    静态创建的每个CPU变量不能在模块内使用,动态创建还是有可能的。

  2. 运行时每个CPU数据(动态分配)
    void * alloc_percpu(type);
    void *__alloc_percpu(size, align);
    void free_percpu(const void *);

利用两个宏获取每个CPU数据,同时禁止内核抢占
get_cpu_var(ptr)
put_cpu_var(ptr)

12.11 使用每个CPU数据的原因

  1. 减少数据锁定,每CPU,不再需要任何锁。
  2. 每个CPU数据可以大大减少缓存失效。缓存一致性保证带来的。
  3. 唯一的安全要求就是禁止抢占,且不能在访问每个CPU数据过程中睡眠。

12.12 分配函数的选择

  1. 需要连续的物理页,可以使用低级页分配器或者kmalloc(),然后选择GFP_ATOMIC和GFP_KERNEL.
  2. 如果想从高端内存分配,就使用alloc_pages(),再调用kmap()
  3. 如果不需要物理上连续的页,就使用vmalloc(),虽然有一定的性能损失。
  4. 如果要创建和撤销很多大的数据结构,考虑建立slab高速缓存。

你可能感兴趣的:(操作系统)