Linux内核设计与实现 十二、内存管理

内核分配内存不像其他地方分配内存那么容易,因为内核本身不能像用户空间那样奢侈地使用内存。

12.1 页

页是虚拟内存的最小单位:
 内核把物理页作为内存管理的基本单位。处理器的最小可寻址单位通常为字(甚至字节)。但是MMU(内存管理单元,将虚拟地址转换为物理地址的硬件)通常以页为单位进行处理。从虚拟内存角度来看,页就是最小单位。

物理页结构
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;
}

●flag:存放页的状态。
●_count:存放页的引用计数
●virtual:页在虚拟内存中的地址

作用
 内核用这一结构来管理系统中所有的页,因为内核需要知道一个页是否空闲(有没有被分配)。如果页已经被分配,内核还需要知道谁拥有这个页。

花费
 系统的每个物理页都要分配一个这样的结构体,就算struct page占用40字节的内存,假定系统物理页为8KB大小,系统有4GB内存,那么共有524288个页,这么多页面的page结构体也才20MB,要管理那么多物理页面,这个代价不算高。

12.2 区

原因
 由于硬件的限制,内核并不能对所有的页一视同仁。所以内核把页划分为不同的区。Linux必须处理如下两种由于硬件存在缺陷而引起的内存寻址问题:
●一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问)
●一些体系结构的内存的物理寻址范围比虚拟寻址范围大得多。这样,就有一些内存不能永久地映射到内核空间上。

解决
Linux使用了四种区:
●ZONE_DMA:这个区包含的页能用来执行DMA操作
●ZONE_DMA32:和DMA类似,该区包含的页面可以用来执行DMA操作,但是只能被32位设备访问
●ZONE_NORMAL:这个区包含的都是能正常映射的页
●ZONE_HIGHEM:这个区包含“高端内存”。其中的页并不能永久地映射到内核地址空间。

意义
 Linux把系统的页划分为区,形成不同的内存池,这样就可以根据用途进行分配了。区的划分没有任何物理意义,这只不过是内核为了管理页而采取的一种逻辑上的分组。

12.3 获得页与释放页

内核提供了一种请求内存的底层机制,并提供了对它进行访问的几个接口。所以这些接口都以页为单位分配内存

释放页时注意只能释放属于自己的页,内核是完全信赖自己的,注意检查错误。

12.4 kmalloc()

kmalloc()

函数

void *kmalloc(size_t size, gfp_t flags)

思想
 kmalloc()函数与用户空间的malloc()非常相似,只不过多了一个flags参数,大多数内核分配中kmalloc()接口用的更多。

 这个函数返回一个指向内存块的指针,内存块至少有size大小,所分配的内存区在物理上是连续的,出错时返回NULL

gfp_mask标志

应用范围
 不管是在低级页分配函数中,还是在kmalloc()中,都用到了分配器标志。

分类
 标志可以分为三类,行为修饰符、区修饰符、类型

具体见书12.4.1

kfree()

函数

void kfree(const void *ptr)

思想
 kfree是kmalloc()的另一端,kfree()函数释放由kmalloc()分配出来的内存块

12.5 vmalloc()

**函数 **

void *vmalloc(unsigned long size)

思想
 vmalloc()函数的工作方式类似于kmalloc(),kmalloc()分配的内存虚拟地址是连续的,而物理地址则无须连续。这也是用户空间分配函数的工作的工作方式:由malloc()返回的页在进程的虚拟地址空间内是连续的,但是,这并不保证它们在物理RAM中也是连续的。

物理地址连续与虚拟地址连续
 硬件设备存在于内存管理单元之外,它根本不理解什么是虚拟地址。因此,硬件设备用到的任何内存区都必须是物理上连续的块,而不仅仅是虚拟地址连续上的块。而仅供软件使用的内存块(如进程相关的缓冲区)就可以使用只有虚拟地址连续的内存块。

缺点
 vmalloc()函数为了把物理上不连续的页转换为虚拟地址空间上连续的页,必须专门建立页表项。通过vmalloc()获得的页必须一个一个进行映射(因为物理上不连续),这就导致比直接内存映射大得多的TLB抖动。
 所以vmalloc()在不得已时才会使用,如获得大块内存时。

12.6 slab层

slab层存在的原因

 分配和释放数据结构是所有内核中最普遍的操作之一。Linux内核提供了slab层。slab分配器扮演了通用数据结构缓存层的角色。slab层的关键是避免频繁分配和释放页。

高速缓存、slab、对象的关系

 每种对象类型对应一个高速缓存。例如一个高速缓存用于存放进程描述符,另一个高速缓存存放索引节点对象。这些高速缓存又被划分为slab,slab由一个或多个物理上连续的页组成。
 每个slab都包含一些对象成员,这里的对象是指被缓存的数据结构。每个slab处于三种状态之一:满、部分满、空。一个满的slab没有空闲的对象(slab中的所有对象都已被分配)。如果没有空的slab,就要创建一个slab了。
在这里插入图片描述
在这里插入图片描述

slab分配器的接口

见书12.6.2

12.7 在栈上的静态分配

 用户空间能够负担非常大的栈,而且栈空间还可以动态增长,内核栈小而且固定。当给每个进程分配一个固定大小的小栈后,不但可以减少内存的消耗,内核也无需负担太重的栈管理任务。

单页内核栈

单页的原因
 2.6内核引入了一个选项设置单页内核栈。可以让每个进程减少内存消耗。
 随着机器运行时间的增加,寻找两个未分配的、连续的页变得越来越困难,物理内存渐渐变为碎片。

中断栈
 中断栈为每个进程提供一个用于中断处理程序的栈。中断处理程序不用再和被中断进程共享一个内核栈。它们可以使用自己的栈了。

注意
 必须尽量节省栈资源,让所有局部变量所占空间之和不要超过几百字节。在栈上进行大量的静态分配是很危险的。
 栈溢出时,多出的数据就会直接溢出来,覆盖掉紧邻对战末端的东西。首先面临考验的就是thread_info结构。

12.8 高端内存的映射

https://www.jianshu.com/p/0b8e1879729a

 高端内存中的页不能永久地映射到内核地址空间上。

 有这样一个问题:内核虚拟地址空间只有1G,也就是说内核只能访问1G物理内存空间,但是如果物理内存是2G,那内核如何访问剩余的1G物理内存空间呢?按照我们刚才说的,这2G的物理内存地址被划分成了3个Zone,物理内存0到896M是内核直接可以访问的,896M到2G这一部分内核要如何访问呢?实际上,当内核想要访问高于896M的物理内存空间时,会从0xF800 0000到0xFFFF FFFF这一块线性地址空间中找一段,然后映射到想要访问的那一块超过896M部分的物理内存,用完之后就归还。由于0xF800 0000~0xFFFF FFFF这一块没有和固定的物理内存空间进行映射,也就是说,这128M的线性地址空间可以和高于896M的物理内存空间短暂的、任意的建立映射,循环使用者128M线性地址空间,这样内核就可以访问剩下的高于896M的物理内存空间了。
在这里插入图片描述
在这里插入图片描述

永久映射

 高端低端内存上都能用。如果page结构对应低端,函数返回该页的虚拟地址。如果页位于高端内存,则会建立永久映射,在返回地址。
 可以睡眠,kmap只能用在进程上下文中。

void *kmap(struct page *page)		//映射一个给定的page结构到内核地址空间

不需要高端内存时,应该解除映射

void kunmap(struct page *page)

临时映射

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

void *kmap_atomic(struct page *page, enum km_type type)	//映射
void kunmap_atomic(void *kvaddr, enum km_type type)		//取消映射

12.9 每个CPU的分配

 一般每个CPU的数据存放在一个数组中。数组中的每一项对应着系统上一个存在的处理器。按当前处理器号确定这个数组的当前元素。

//声明
unsigned long my_percpu[NR_CPUS];
//访问
int cpu;

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

 上面的代码没有出现锁,原因如下
●因为锁操作的数据对当前处理器来说是唯一的。除了当前处理器,没有其他处理器可接触到这个数据,不存在并发访问问题,所以可以在不用锁的情况下安全访问它。
●get_cpu()时已经禁止了内核抢占,调用put_cpu()时又会重新激活当前处理器号。

12.10 新的percpu接口

2.6内核为了方便创建和操作每个CPU数据,引进了新的操作接口,称为percpu

编译时的percpu数据

DEFINE_PER_CPU(type, name);		//编译时定义每个CPU变量

DECLARE_PER_CPU(type, name);	//为每个处理器创建类型为type,名字为name的变量实例

get_cpu_var(name)++;					//返回当前处理器上的指定变量,同时禁止抢占
put_cpu_var(name);					//完成,重新激活抢占

per_cpu(name, cpu)++;			//增加制定处理器上的name变量值

运行时的每个CPU数据

内核实现每个CPU数据的动态分配方法类似于kmalloc()。该例程为系统的每个处理器创建所需内存的实例。

//分配,返回一个指针,用来简介引用动态创建的每个CPU数据
void *alloc_percpu(type);
void *_alloc_percpu(size_t size, size_t align);

//释放
void free_percpu(const void *);

12.11 使用每个CPU数据的原因

●减少数据锁定。因为按照每个处理器访问每个CPU数据的逻辑,你可以不再需要任何锁。不过这只是一个编程约定,你需要确保本地处理器只会访问它自己的唯一数据。还需要禁止内核抢占。
●使用每个CPU数据可以大大减少缓存失效。如果一个处理器操作某个数据,而该数据又存放在其他处理器缓存中,那么存放该数据的那个处理器必须清理或刷新自己的缓存。持续不断的缓存失效称为缓存抖动。

12.12 分配函数的选择

●需要连续的物理页,就可以使用某个低级页分配器或kmalloc()。这是内核中内存分配的常用方式。
●从高端内存进行分配,就是用alloc_pages()。alloc_pages()函数返回一个指向struct page结构的指针。
●不需要物理上连续的页,而仅仅需要虚拟地址上连续的页,就使用vmalloc()。vmalloc()函数分配的内存虚地址是连续的,但它本身不保证物理上的连续。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值