high memory只存在于32位kernel下,以下文字都针对32位kernel。
1)什么是high memory,为什么要有high memory
Linux人为的把4G虚拟地址空间(32位地址最多寻址4G)分为3G+1G,其中0~3G为用户程序地址空间,3G~4G为kernel地址空间(为什么要这么分?为什么用户程序和kernel不能各自独享4G虚拟地址空间?这两个问题下次再说吧,这里不表),这就是说kernel最多寻址1G的虚拟地址空间。
当CPU启用MMU的paging机制后,CPU访问的是虚拟地址,然后由MMU根据页表转换成物理地址。页表是由kernel维护的,所以kernel可以决定1G的虚拟地址空间具体映射到什么物理地址。但是kernel最多只有3G~4G这1G地址空间,所以不管kernel怎么映射,最多只能映射1G的物理内存。所以如果一个系统有超过1G的物理内存,在某一时刻,必然有一部分kernel是无法直接访问到的(这个一定要想清楚,不然无法明白high memory)。另外,kernel除了访问内存外,还需要访问很多IO设备。在现在的计算机体系结构下,这些IO设备的资源(比如寄存器,片上内存等)一般都是通过MMIO的方式映射到物理内存地址空间来访问的,就是说kernel的1G地址空间除了映射内存,还要考虑到映射这些IO资源--换句话说,kernel还需要预留出一部分虚拟地址空间用来映射这些IO设备(ioremap就是干这个的)。
Linux kernel采用了最简单的映射方式来映射物理内存,即把物理地址+3G按照线性关系直接映射到kernel空间。考虑到一部分kernel虚拟地址空间需要留给IO设备(以及一些其他特殊用途),Linux kernel最多直接映射896M物理内存,而预留了最高端的128M虚拟地址空间给IO设备(还有其他的用途)。所以,当系统有大于896M内存时,超过896M的内存kernel就无法直接访问到了(想明白了么?),这部分内存就是high memory。那kernel就永远无法访问到超过896M的内存了马?不是的,kernel已经预留了128M虚拟地址,我们可以用这个地址来动态的映射到high memory,从而来访问high memory。所以预留的128M除了映射IO设备外,还有一个重要的功能是提供了一种动态访问high memory的一种手段(kmap主要就是干这个的,当然还有vmalloc)。
当然,在系统物理内存<896M,比如只有512M的时候,就没有high memory了,因为512M的物理内存都已经被kernel直接映射。事实上,在物理内存<896M时,从3G+max_phy ~ 4G的空间都作为上述的预留的内核地址空间(未考证)。
要理解high memory,关键是把物理内存管理,虚拟地址空间管理,以及两者间的映射(页表管理)三个部分分开考虑,不要把物理内存管理和虚拟地址空间管理混在一起。比如high memory也参与kernel的物理内存分配,你调用get_page得到的物理页有可能是low memory,也可以是high memory,这个物理页可以被映射到kernel,同时也可以被映射到user space。再比如vmalloc,只保证返回的虚拟地址是在预留的vmalloc area里,对应的物理内存,可以是low memory,也可以是high memory。当然出于性能考虑,kernel可能会优先分配直接映射的low memory,但我们不能假设high memory就不会被分配到。
一些结论:
1)high memory针对的是物理内存,不是虚拟内存,更确切的,虚拟地址空间。
2)high memory也是被内核管理的(有对应的page结构),只是没有映射到内核虚拟地址空间。当kernel需要分配high memory时,通过kmap等从预留的地址空间中动态分配一个地址,然后映射到high memory,从而访问这个物理页。
3)high memory和low memory一样,都是参与内核的物理内存分配,都可以被映射到kernel地址空间,也都可以被映射到user space地址空间。
4)物理内存<896M时,没有high memory,因为所有的内存都被kernel直接映射了。
5)64位系统下不会有high memory,因为64位虚拟地址空间非常大(分给kernel的也很大),完全能够直接映射全部物理内存。
2)题外话1 -- 关于最高端的128M内核虚拟地址(或者当物理内存<896M时更大)的分配:
这部分地址空间被划分为4段,分别是fixed mapping,kmap area,vmalloc area,还有8M用来catch kernel指针错误。其中fixed mapping主要用在boot阶段用来永久性映射一些物理地址固定的数据结构或者硬件地址(比如ACPI表,APIC地址,等等)。kmap area是kernel用来临时建立映射来访问物理页用的,可用的地址空间也比较小。128M中绝大部分reserve了给vmalloc area,vmalloc和ioremap返回的都是这个空间里的地址。
另外,在《Understanding the Linux Virtual Memory Manager》这本书中有linux 进程地址空间划分的详细图,很不错,我就懒得画了。
3)题外话2 -- 为什么要人为划分3G/1G?为什么kernel没有自己的4G地址空间?为什么所有进程共享kernel地址空间?
为什么x86上Linux kernel没有自己独立的地址空间?
接上上次写的博文(linux里的high memory),里面提到high memory存在的原因是在32位系统下,kernel只能访问高端的1G地址空间(kernel需要和进程共享地址空间),从而导致当物理内存超过896M的时候,有一部分物理内存无法直接映射。那既然x86提供了MMU进行地址转换,为什么kernel不能有自己独立的4G地址空间?如果kernel有自己独立的4G空间,那就不需要high memory了,另外user space进程也可以有完整的4G空间。(物理内存超过4G?那是另外一个问题,和high
memory无关,x86提供了PSE(32位)以及PAE(64位)来解决这个问题。)
先来看看如果kernel要有自己独立的4G空间,需要什么样的硬件支持。进程之所以有各自独立的地址空间,是因为进程有自己的独立页表(kernel管理,保存在task_struct中),当进程切换的时候,kernel负责切换进程的页表(参考context_switch->switch_mm)。同样的道理,如果kernel想要有独立地址空间,就要有自己的独立页表,并且在切换到kernel态的时候,切换到kernel的页表。什么时候会切换到kernel态?中断发生的时候(这里的中断包括外部中断,以及INT x等软件exception,以及NMI等等所有中断)。kernel是直接管理硬件的代码,kernel不能自己load映射自己的页表,所以这个页表切换过程必须硬件实现。有人可能会问,中断发生时(切换到kernel态)的第一件事就是切换到kernel的页表,不就可以实现kernel自己切换页表了吗?不行的,首先中断向量表里的interrupt
handler里的地址本身就是虚拟地址,不是物理地址,这时页表没有ready,当然CPU就不能访问到正确的interrupt handler。那如果把IDT表的interrupt handler设计成物理地址不就行了吗?也不行,因为切换到kernel态的时候,要保存执行现场,比如代码断,数据段,比如堆栈地址等等,而这个也是虚拟地址。所以,要想让kernel有自己独立的地址空间,硬件必须支持切换到kernel态的时候,自动切换到kernel的页表。
那x86提供这样的硬件机制吗?没有。其实x86的Task switch机制可以提供自动切换页表,但这是为进程切换用的(以及让中断在自己的线程里跑),设计的远比单纯切换页表复杂,并不适用于这种目的(事实上,由于使用Task switch机制,时间开销很大,Linux并没有使用该机制去实现切换)。所以,在x86上,kernel只能和user space进程共享地址空间,在用户态切换到内核态时,不会进行页表切换,这样,只要所有的进程都共享内核空间(高端1G),kernel就可以正确寻址,无论是从哪个进程切换到内核态,因为所有进程的3G~4G这段虚拟地址的映射都是一样的。顺便提一句,SPARC的MMU提供了这样的机制,所以Solaris在SPARC上,kernel和用户态进程是各自独享64位地址空间。
总之一句话,所有地址空间的管理和分配(能力),本质上都是MMU的架构决定的,MMU提供什么样的能力,决定了对地址空间管理和分配的能力。这个适用于CPU,也适用于IO。
题外话:
1)关于x86进程切换时TLB的flush
x86规定,切换页表的时候(move页表基地址到CR3),会invalidate整个TLB。这种设计导致进程切换会影响性能,因为当进程被重新调度到CPU上的时候,原先cache在TLB里的数据早就没有了。个人觉得这是一种非常脑残的设计,因为要解决这个问题很简单,只需要为不同的进程tag一个PID(进程ID)就行了,TLB cache的数据里也包含PID,有了PID做为tag,进程切换时就不需要invalidate整个TLB。目前x86_64的paging机制里实现了类似的机制,但提供的PID只有12位,最多支持4k个PID,这对于现在的OS来说,实在是太少了。不知道Intel为什么这么设计,也许是由于兼容性的包袱,谁知道呢。