Linux的内存管理应该是Linux最核心和最复杂的部分之一。因为个人理解水平有限,所以只能根据自己的思路概述,细节还需要更多的去相关参考文献(《深入理解Linux内核》第三版中文版,以下简称《深入》,我买了一本纸版的)中探寻。(英文版可以从网上搜到,我的skydrive中有一份chm的版本,点这里)。
(1)逻辑地址、线性地址(虚拟地址)与物理地址
下面的图表示三者之间的转化关系。
逻辑地址通过分段转化为线性地址,而线性地址通过分页转化为物理地址。分段单元和分页单元都是内存控制单元(MMU)的组成部分。
这三种地址中的核心是线性地址。线性地址永远是32位的无符号整形,可以用来表示4GB的地址空间(2的32次方=4GB),用16进制表示即为0x00000000到0xffffffff(16的8次方=2的32次方)。我们用C语言编程的时候,如果打印出某个变量的地址,这里说的地址就是线性地址。
(2)分页和分段的区别
(3)Linux偏爱分页
援引《深入》p.47的说法,Linux除了在80x86芯片(古老的东西。。。)上以外不使用分段系统。
(4)为什么要使用页?
“为了效率起见,线性地址被分成以固定长度为单位的组,成为页。”页实际上是线性地址的单位(未启用PAE或者PSE扩展的情况下1页默认为4KB)。(为什么选择了4KB大小而不是一个字节或者其他的作单位后面再说。)
而线性地址又只是被用户所使用,所以,页实际上是面向用户进程的一种封装机制。用户的进程在使用线性地址时可以不用关心这个线性地址到底在什么地方(是在内存里还是在硬盘上)或者是否连续,而只管放心使用它的4GB的连续的线性地址空间即可。这也就是虚拟内存机制的基础(linux中的swap分区就是虚拟内存机制的一部分;windows平台上实际也有—我的电脑右键>属性>高级>性能选项>设置>高级>虚拟内存),把所有的能够存储的地方都看成内存的一部分。
(5)为什么要分页?
用摘自网络的一个回答就是:房子小的时候,不需要隔开;房子大了,自然就得隔开用。
假设,我们使用最小的字节(而不是页)作为内存管理的基本单位。再假设我们现在所使用的最大的内存只有128KB,那么我们管理时所使用的表项就有128K个;也就是说如果一个进程要占用所有的内存,它自己需要维护一个有128K个表项的表。每个表项中我们需要一些标志位来提供一些内存管理功能(Present/Dirty等,详见《深入》p.52),假设每个表项只占用1字节,那么一个进程至少要使用128KB来提供内存管理功能。当我们的内存有4GB时,那么一个进程本身就要占用4GB,这显然是不可想象的。
所以,为了减少表项,内存管理的基本单位要大一些,因此使用了页。
(6)为什么要使用分级页表?
实际上这个问题与上个问题是异曲同工的。4GB的内存在使用4KB的页时,仍然需要2的20次方个表项,如果一个表项是1B,那么仍然需要1MB的内存去仅仅存储一个页表。如果分成两级,则需要的空间会少很多。
从2.6.11的内核开始,Linux使用了四级分页模型。但并不是所有时候这四级的页表都被使用了:没有启用PAE(物理地址扩展)的32位操作系统中只需要二级页表;启用了PAE的32位系统使用了三级;而64位系统使用三级还是四级需要视情况而定。(《深入》p.62-63)
(7)用户空间和内核空间
对操作系统有一定了解的读者都知道,内存空间一般是被分配成用户空间和内核空间的,并且由内核提供了一种内存管理的保护模式,这样用户就无法访问内核的内存,这一点对于一个操作系统是很重要的。那么就需要一种映射机制巧妙地将不同的两个空间划分开。
由于Linux给每个进程的地址是4GB,硬性的规定了前3GB是用户空间(当然,虽然叫用户空间,但是内核仍然是可以访问的,因为特权高嘛),地址范围是0x00000000到0xbfffffff;后1GB是内核空间,地址范围是从0xc0000000到0xffffffff,用户是不能访问的(编程时如果某个用户变量的地址不小心被写成了这个范围,或者你在这个范围内进行操作,就会报错的)。
用户空间由于可以被用户进程自由使用,这属于程序员负责的事情;下面将继续介绍内核空间的分配情况。
(8)内核空间的管理和分配
内核的1GB线性地址空间分配表(引自《深入》英文版section 8.3.1)如下所示:
在详细叙述分布的时候,需要先解决关于物理内存的两个问题。用户空间多大并不重要,也可以压缩;但是如果所有的物理内存不到1G时,内核空间映射到哪里?与此相对的一个问题就是如果物理内存大于4G,内核如何管理多出来的部分?带着这两个问题我们去看下面的内核空间。
a)我们先从解决第一个问题和分配表的最左端开始。当物理内存不到1G时(实际上是896MB时,具体原因下面会说到),采取的是“物理内存映射”(图中的Physical memory mapping)方式,即线性地址和物理地址一一对应的方式(线性地址X映射物理地址X-PAGE_OFFSET宏)。因此第一个区叫做“物理内存映射区”。到high_memory变量截止。因此,物理内存如果为x(x<896MB),那么high_memory = 0xc0000000+x。
由于只有896M内存是映射在内核空间中的,也就是说大于896M的物理内存是不能被内核直接寻址的。但我们不能让这些物理内存就被浪费掉不能使用。但是内核采取了比较灵活的办法三种办法来综合利用大于896M的内存,称为高端内存。三种方法分别是:永久内核映射、临时内核映射和非连续内存分配。由于预留了128M的内核线性地址来进行这几种映射,所以这就是为什么实际内核能直接映射的线性地址只有896M(1G-128M=896M)。
b)图中的Fix-mapped linear addresses(固定映射线性地址,从FIXADDR_START宏开始的,详见《深入》中文p.78)。映射的具体方式的话,并不是像前面一一对应的“物理内存映射”方式,而是使用了一个fix_to_vir()函数来进行计算。这个区的主要用途是用来存储一些固定的指针,如果不保存指针的地址的话,我们实际上需要两次寻址才能完成一次间接引用指针的操作(c语言中的*操作符);然而固定映射之后就只需要一次寻址即可完成,提高了效率。这个区被多个CPU分割,每个CPU拥有包含13个窗口(每个窗口是一个页表项,用于映射高端内存的一个页框)的一个集合km_type。但是这个映射比较短暂,并由CPU负责这一部分映射的刷新。这个区叫做“固定线性地址映射区”或者“临时内核映射”。使用kmap_atomic()函数创建。
c)PKMAP_BASE宏开始到FIXADDR_START宏结束的一段是部分高端内存空间的固定映射,使用了一个page_address_htable散列表保持这一映射,它映射的时间相对比较长期一些(具体参见《深入》中文版p.306或者英文版8.1.6节),所以可以叫做“永久内核映射”。使用kmap()函数创建。
d)剩下的就是从high_memory变量到PKMAP_BASE宏之间的空间了。这部分就是非连续内存区,这个区是分成一个个的小块。至于两端的8MB、8KB和块之间的4KB的隔断是用来防止越界操作的保护区。内核使用vmalloc()函数来分配一个块,每个块都对应着一个vm_struct的描述符。