:本文提及的物理地址 空间可以理解为就是物理内存 ,但是在某些情况下,把他们理解为物理内存是不对的。
本文讨论的环境是NON-PAE的i386平台,内核 版本2.6.31-14
一. 什么是高端内存
linux 中内核使用3G-4G的线性地址空间,也就是说总共只有1G的地址空间可以用来映射物理地址空间。但是,如果内存大于1G的情况下呢?是不是超过1G的内存 就无法使用了呢?为此内核引入了一个高端内存的概念,把1G的线性地址空间划分为两部分:小于896M物理地址空间的称之为低端内存,这部分内存的物理地 址和3G开始的线性地址是一一对应映射的,也就是说内核使用的线性地址空间3G--(3G+896M)和物理地址空间0-896M一一对应;剩下的 128M的线性空间用来映射剩下的大于896M的物理地址空间,这也就是我们通常说的高端内存区。
所谓的建立高端内存的映射就是能用一个线性地址来访问高端内存的页。如何理解这句话呢?在开启分页后,我们要访问一个物理内存地址,需要经过MMU的转换,也就是一个32位地址vaddr的高10位用来查找该vaddr所在页目录 项,用12-21位来查找页表项,再用0-11位偏移和页的起始物理地址相加得到paddr,再把该paddr放到前端总线上,那么我们就可以访问该vaddr对应的物理内存了。在低端内存中,每一个物理内存页在系统 初始化的时候都已经存在这样一个映射了。而高端内存还不存在这样一个映射(页目录项,页表都是空的 ), 所以我们必须要在系统初始化完后,提供一系列的函数来实现这个功能,这就是所谓的高端内存的映射。那么我们为什么不再系统初始化的时候把所有的内存映射都 建立好呢?主要原因是,内核线性地址空间不足以容纳所有的物理地址空间(1G的内核线性地址空间和最多可达4G的物理地址空间),所以才需要预留一部分 (128M)的线性地址空间来动态 的映射所有的物理地址空间,于是就产生了所谓的高端内存映射。
二.内核如何管理高端内存
上面的图展示了内核如何使用3G-4G的线性地址空间,首先解释下什么是high_memory
在arch/x86/mm/init_32.c里面由如下代码:
high_memory是“具体物理内存的上限对应的虚拟 地址”,可以这么理解:
当内存内存小于896M时,那么high_memory = (void *) __va(max_low_pfn * PAGE_SIZE),max_low_pfn就是在内存中最后的一个页帧号,所以high_memory=0xc0000000+物理内存大小;
当内存大于896M时,那么 highstart_pfn = max_low_pfn, 此时max_low_pfn就不是物理内存的最后一个页帧号了,而是内存为896M时的最后一个页帧号,那么 high_memory=0xc0000000+896M.总之high_memory是不能超过0xc0000000+896M.
由于我们讨论的是物理内存大于896M的情况,所以high_memory实际上就是0xc0000000+896M,从high_memory开始的128M(4G-high_memory)就是用作用来映射剩下的大于896M的内存的,当然这128M还可以用来映射设备 的内存(MMIO)。
从 上图我们看到有VMALLOC_START,VMALLOC_END,PKMAP_BASE,FIX_ADDRESS_START等宏术语,其实这些术语 划分了这128M的线性空间,一共分为三个区域:VMALLOC区域(本文不涉及这部分内容,关注本博客的其他文章),永久映射区(permanetkernel mappings), 临时映射区(temporary kernel mappings).这三个区域都可以用来映射高端内存,本文重点阐述下后两个区域是如何映射高端内存的。
三. 永久映射区(permanet kernel mappings)
1. 介绍几个定义:
PKMAP_BASE :永久映射区的起始线性地址。
pkmap_page_table :永久内核映射允许内核建立高端页框到内核地址空间的长期映射,它们使用主内核页表中一个专门的页表,其地址存放在
pkmap_page_table 。
LAST_PKMAP : pkmap_page_table里面包含的entry的数量=1024 或512,取决于PAE是否激活。
pkmap_count[LAST_PKMAP]数组:每一个元素的值对应一个entry的引用计数。关于引用计数的值,有以下几种情况:
0:说明这个entry可用。
1:语义特殊,表示该位置的页已经映射,但由于CPU的TLB没有更新而无法使用,见下面的情景 。
N:有N-1个对象正在使用这个页面
首先,要知道这个区域的大小是4M,也就是说128M的线性地址空间里面,只有4M的线性地址空间是用来作永久映射区的。至于到底是哪4M,是由PKMAP_BASE决定的,这个变量表示用来作永久内存映射的4M区间的起始线性地址。(如果启动PAE,则只有2M)
在NON-PAE的i386上,页目录里面的每一项都指向一个4M的空间,所以永久映射区只需要一个页目录项就可以了。而一个页目录项指向一张页表,那么 永久映射区正好就可以用一张页表来表示了,于是我们就用 pkmap_page_table 来指向这张页表。
2. 具体代码分析(2.6.31)
kmap()函数就是用来建立永久映射的函数 :由于调用kmap函数有可能会导致进程 阻塞, 所 以它不能在中断处理函数等不可被阻塞的上下文下被调用, might_sleep()的作用就是当该函数在不可阻塞的上下文下被调用是,打印栈信息。
接下来判断该需要建立永久映射的页是否确实属于高端内存,因为我们知道低端内存的每个页都已经存在和线性地址的映射了,所以,就不需要再建立 了,page_address()函数返回该page对应的线性地址。(关于page_address()函数,参考本博客的专门文章有解释)。最后调用 kmap_high(page),可见kmap_high()才真正执行建立永久映射的操作。
kmap_high 函数分析:
首先获得对pkmap_page_table操作的锁,然后再调用page_address()来返回该page是否已经被映射,我们看到前面在kmap()里面已经判断过了,为什么这里还要再次判断呢?因为再获的锁的时候,有可能锁被其他CPU拿走了,而恰巧其他CPU拿了这个锁之后,也是执行这段code,而且映射的也是同一个page,那么当它把锁释放掉的时候,其实就表示该page的映射已经被建立了,我们这里就没有必要再去执行这段 code了,所以就有必要在获得锁后再判断下。
如果发现vaddr不为空,那么就是刚才说的,已经被其他cpu上执行的任务给建立了,这里只需要 把表示该页引用计数的pkmap_count[]再加一就可以了。同时调用BUG_ON来确保该引用计数确实是不小于2的,否则就是有问题的了。然后返回 vaddr,整个建立就完成了。
如果发现vaddr为空呢?调用map_new_virtual()函数,到此我们看到,其实真正进行建立映射的代码在这个函数里面
10 last_pkmap_nr 是全局变量,记录最后使用的位置,该行使 last_pkmap_nr曾1,
当 到达pkmap_count的最大索引值 LAST_PKMAP时,搜索绕 回到位置0继续.这就
实现了所谓的反向扫描pkmap_count数组。并且在绕回到位置0时顺便执行
flush_all_zero_pkmaps()来刷出CPU高速缓存,但尚未更新TLB。
15 pkmap_count[ last_pkmap_nr]=0,说明我们找到了一个位置,可以使用的。
17 执行到这里,说明我们还没找到,如果--count不为0,说明还没有搜索完一整圈,继续
搜索。如果为0,说明已经搜索了一整圈了,还没找到合适的位置。
23~37 该流程进入睡眠状态,其中33~34是希望睡眠期间有其他哥们已经帮我把page映射了。
如果没有则跳到start处再重新执行搜索过程。直到找到为止。
39 终于找到了一个位置,通过PKMAP_ADDR计算出该位置处的虚拟地址
#define PKMAP_ADDR(nr) (PKMAP_BASE + ((nr) << PAGE_SHIFT))
40 把页框的物理地址插入到pkmap_page_table的相应项中。
42 新位置的使用技术器设置为1,说明该页已分配但无法使用,因为TLB项未跟新。
43 将该页添加到持久映射的数据结构。
一.为什么引入临时内存映射(temporary kernel mappings)
在永久内存映射中我们看到,如果 pkmap_page_table页表里面没有空的entry,那么就会导致这次映射被阻塞,所以我们说不能在一些原子的上下文情况下调用kmap()函数。而在临时内存映射中,不会去判断该pte是否已经被用掉了,它采用的是覆盖的策略,所以把总是能成功的建立映射。 会不会被阻塞就是临时内存映射和永久内存映射一个最明显的区别 。二. 临时内存映射
内核地址空间布局
|
|
从FIXADDR_START到FIXADDR_TOP这段地址空间被称为固定映射线性空间,这段地址空间主要用来映射一些设备的内存和寄存器,像FIX_APIC_BASE是用来映射local apic的寄存器的。其中的FIX_KMAP_BEGIN到FIX_KMAP_END是用来供临时内存映射的使用。
这段空间的大小是 KM_TYPE_NR* NR_CPUS,也就是说,每个CPU都有独立的KM_TYPE_NR大小的空间,来看看看KM_TYPE_NR是什么
|
每个枚举项都代表了一个pte,每个CPU都有20个这样的pte供临时内核映射使用。我们先来看看内核如何来获得这些对应的枚举项所对应的pte地址:
|
这个宏返回的是固定线性映射里面枚举项所对应的线性地址,FIXADDR_TOP=0xfffff000,以FIX_APIC_BASE为例,__fix_to_virt(
FIX_APIC_BASE
),FIX_APIC_BASE等于4,那么他的线性地址就是0xfffff000-(4<<PAGE_SHIFT),这里我们看到,
线性地址和枚举项的位置关系是成反比的,越靠前的枚举项对应的线性地址越靠后。
下面的代码是完整的如何通过枚举项,获得对应的pte项所对应的线性地址。
|
注意:kmap_pte是临时内存映射起始的页表项,接下来的其他页表项根据他们的相对位置差就可以求得。比如idx对应的pte=kmap_pte - idx,为什么是减不是加,看上面的说明(越靠前的枚举项对应的线性地址越靠后
)。
接下来我们看实际的建立映射的函数(kmap_atomic)
|
解除映射也很简单,没有做什么特殊处理:
|
三. 总结
其 实,不管是那样的方式,原理都是一样的,都是在固定映射区外选定一个地址,然后再修改PTE项,使其指向相应的page。特别值得我们注意的是,因为 kmap()会引起睡眠,所以它不能用于中断处理。但每一种映射方式都有自己的优点和缺点,这需要我们在写代码的时候仔细考虑了。