和共享库的思想一样,3G/1G模式下的Linux进程的地址空间对于内核空间部分是共享的,也就是说所有的进程在内核中操作的地址空间是同一个。虽然每一个进程都有单独的页表,但是这些页表的内核部分,即高1G或者2G的部分的内容是一致的。
现在的问题是,虚拟地址空间资源宝贵吗?其实你大可不必认真地对待一切虚拟的东西,画出来的饼是不能充饥的!微软深知此道,于是直接将4M的页表映射进了虚拟地址空间而不会觉得浪费了4M的内存!实际上物理内存该用多少还是多少,很难用完这4M。由此看来,虚拟地址空间是可以浪费的,只要别落实到物理内存,一切就都无所谓。
然而,Linux的虚拟内存映射方式让人不能不考虑虚拟内存的开销,把虚拟的东西带到前台的不是虚拟内存一开始便分配了物理内存,而是管理成本的开销。Linux共享内核地址空间的开销在于,所有的进程以及操作系统本身的管理机构都要使用这1G或者2G的地址空间(不考虑4G/4G模式),接下来我来说明一下Linux内核地址空间的设计,部分信息来自于早期的Maillist以及Linux早期的blog,我比较喜欢从历史中找根本原因。我不喜欢那种当有人提问为何Linux采用一一映射时信口开河“为了高效”的那种做派。
由于每一个进程的地址空间的内核部分都一样,就不能像Windows那样设计固定的地址空间区域,以页表为例,如果你将a-b这段虚拟地址空间给了进程A的页表,那么进程B就不能使用了,注意,它们的内核部分是共享的!!即使你把一个很大的范围比如X-Y给了所有的进程的页表,那么由于进程数量的不可预估性,要么会带来空间不足,要么就是严重的地址空间碎片。因此所有的地址映射都必须是离散分布,地址不固定且不分类的。也就是说根本就不能做到Windows那样的布局方式,另一方面,由于共享内核地址空间,真的不便于将每一个进程的页表虚拟地址都在该空间找一个位置,如果真的这样,1G或者2G的空间瞬间就用光了,要知道,一个进程的页表需要4M的空间啊!如何做呢?
页表本不应该被映射在虚拟地址空间里面,它属于MMU管理机构,不属于进程本身的上下文,因此Linux最初设想的方式更加自然,即不对页表进行虚拟地址映射,同样的道理,也不对诸如task_struct,page这类结构体进行映射,原因在于它们都属管理机构的成员,不属于进程地址的上下文。然而无论怎样都绕不开32位保护模式体系机构给设置的机关或者障碍。你无论如何都要映射,因为必须通过MMU来访问物理内存!现在的问题就是怎么来映射的问题。
理论上的解决方案是简单的,页目录物理页面是存在的,只是页表以及进程需要的物理页面不一定被分配,那就靠缺页异常来处理。然而事实上,有些管理数据结构是要常驻内存的,比如中断处理的代码等,另外页目录也要常驻,这样一来实际上在这些常驻的页面生命周期内,它们的映射就是固定的。很多的管理结构的生命周期就是操作系统的生命周期...该做权衡的时候到了,预分配物理页面,然后映射到连续的虚拟地址空间即可,为了不把物理内存拆散,连续的分配物理内存就成了唯一的选择,这就是Linux的一一线形映射!
关键是预分配多少连续物理页面比较合适,这个问题导致了Linux的最精彩的设计!这个设计就是将全部的前1G物理内存全部一一线性映射到高1G的内核地址空间(如果2G/2G模式则是映射2G物理内存),这样就可以随便访问了,系统陷入内核,访问内核空间的时候,实际上映射的物理地址是虚拟地址减去3G,映射这1G内存的这些页表会保存在一系列连续的物理页面上,访问它们的时候,其虚拟地址就是物理地址加上3G。因此,Linux内存映射的本质出来了。
所有的前1G(或者前2G)的物理内存全部纳入管理范畴,不足1G的按实际的算。这些物理页面线性映射到虚拟地址空间的最上面1G的范围内。这并不是说用户进程就不能使用这些物理内存了,要知道在当时Linux早期那个年代,是不可能有那么多的物理内存的。一般都不会到512M。这些物理内存可以映射在地址空间的任意地方,如果用户进程需要物理内存,比如缺页异常调页的时候,如果处在1G的范围内的物理内存没有被内核使用,它便可以被分配给用户进程,它的位置便填充了用户进程的用户态的页表项中,注意,此时并不清除其在内核页表项的映射,也就是说,通过内核一一映射的方式,还是可以访问到的,这就是妙处所在!也就是说内核态可以通过一一映射的方是访问任意前1G的物理内存,即使该物理页面被分配给了某个用户进程也是如此,靠内核的其它管理机制来判断页面是不是已经被分配给了内核关键数据结构或者用户进程,比如伙伴系统,以及伙伴系统之上的空闲内存链表等,在为进程分配页面的时候,内核本身肯定知道该页面有没有被内核数据结构使用。
内核一一线性映射的意义在于,内核空间可以用一种直接的方式来访问1G范围内的物理内存,虽然也是通过MMU,但是看样子这种例行公事敷衍得好精彩!一一映射并不影响用户进程对内存的使用,内核空间的管理机构无论如何也用不了1G的内存。在Linux这种共享内核地址空间的系统中,一一映射是一种非常巧妙的方式,注意,虽然是一一映射,感觉好像是一下子分配了1G的物理内存,太浪费了,但是分不分配是操作系统说了算的,即使这些一一映射的页表项的存在位一直都是1,只要页面没有被内核引用,这些页面还是可以分配给用户进程的,只需要填写一下进程页表的私有的用户空间部分的某个页表项即可!因此内核空间的一一映射就像是进程私用用户空间映射的更高级别的映射,一个页面可以同时被用户进程和内核的一一映射所映射。
3.1.问题和对策
好的设计都有一个微小简单的核心框架思想,然后通过局部调整来适应不同的场景。Linux内核地址空间和前1G物理内存之间一一线性映射就是这个核心,然而它有一些问题。
问题1:一一映射的结果就是连续的物理页面的虚拟地址也是连续的,但是有些内核数据结构并不需要物理内存的连续,1G空间内的连续页面数量毕竟太有限了,不需要连续页面的数据结构占据了太多的连续页面
问题2:如果严格按照一一映射来映射,就意味着在内核空间无法访问高于1G的物理内存,这些高端内存只能在用户空间访问(只需要将这些高端内存的物理地址填入页表项,而页表项通过一一映射来访问)
如何解决这些问题呢?很明确的是,内核虚拟地址的高1G空间不能全部一一映射了,需要留出一些空间来满足以上两个问题以及更多的问题的映射需求,最终就有了896M这个阀点,在虚拟地址的3G到3G+896M和物理地址的0M到896M之间,严格执行一一线性映射,1G的虚拟地址空间中空出的1G-896M,即128M的空间用于满足非一一映射的需求,这段空间又被分为了动态映射区,永久映射区,临时映射区,其中后两种我统称为PageMap空间。
动态映射区:用于映射一些不连续的物理页面,这些页面可以处在物理内存大于896M的位置。一般内核模块的虚拟地址处在该区域,由于该区域空间有限,所以对模块的大小提出了要求。
永久映射区:可以将任意的单独内存页面映射在该区域,最终的虚拟地址是接近于4G的一个值,所谓的永久映射,值得是该虚拟地址如果已经映射了一个物理页面,只能等到它被释放才能再次映射一个新的物理页面。
临时映射区:最有意思的就是它了。映射方式和永久映射一样,无非就是写一个页表项。所谓的临时就是后面的映射可以覆盖前面的映射,这就不需要锁机制来确保互斥操作,为了保证映射不被覆盖,Linux使用了另外一种机制而不是锁。该区域分为了N个子区域,每一个CPU有一个子区域,只要保证同一个CPU在使用映射的时候不会有第二个映射就可以了,由于限制在了一个CPU上,而一个CPU可以很简单得限制它执行一个执行绪,这是很好办的,保证CPU不切换即可(关中断?等等),但是有一些映射被覆盖了也无所谓,于是这个临时映射区域按照映射的目的又一次被划分。