从分段与分页原理到 Linux 虚拟内存映射机制
对于普通程序员来说,使用 objdump 或者其他工具查看到的程序的内存地址其实都是虚拟地址,并不是真实的物理地址,在《操作系统原理》课程中,我们可能也学到了一些内存映射机制,但是都是从抽象层面去分析操作系统的内存管理。本文不仅介绍一些基本原理,而且使用实际的 Linux 内核实例化我们所学到的原理,更加直观和简洁。
1 基本概念
- 物理地址:就是插在主板上的内存条上的位置,32 位操作系统通常支持 4GB 寻址
- 虚拟地址:程序员可以看到的地址空间
- 线性地址:连续的,不分段的地址空间
如图:MMU (内存管理单元)把虚拟地址转换成物理地址
2 分段机制
学过《微机原理》的人应该知道,x86 架构的芯片都会有段寄存器,如 CS,DS 等(即代码段、数据段)寄存器。也就说,分段机制是与硬件密切相关的。在这里不得不说明一下 x86 芯片的两种内存寻址模式:实模式和保护模式。一般来说,系统启动时处理器使用的是实模式,这种状态下,处理器只能访问 1MB 的内存空间;经过处理进入保护模式。
2.1 实模式
在实模式下,段寄存器存放的确实是段基地址,通过段寄存器(如 DS)中存放的基地址和变址寄存器(如 DI)中存放的偏移量,就可以将虚拟地址转换成线性地址。
2.2 保护模式
但是在保护模式下,段寄存器存放的不再是基地址,因为 x86 芯片的段寄存器是 16 位的,无法存放 32 位的段基地址。那么在系统正常运行时,段寄存器存放的到底是什么呢?存放的是 Selector,有教程翻译成段选择符,也有的翻译成段选择子。Selector 到底是什么?后面会讲到。
这时段基地址需要单独放在一个数据结构中,这个数据结构应当包含段基地址(Base)、段界限(Limit)以及段的保护属性。这样的一个数据结构我们称之为段表也称之为段描述符。为了和《操作系统原理》课程中的段表对应,这里也使用段表一词。
段表占用 8 字节的存储空间,Intel 已经对它的格式作了详尽的描述,笔者不再赘述,有兴趣的同学可以查阅相关手册,大家在这里只需要知道段表的组成就行了,如下表所示。
这里又回到了本节最初的地方,16 位的段寄存器存放的段选择符到底是啥?16 位的段寄存器存放的就是如下图所示的结构,我们称之为段选择符。
这里的索引,对应的就是段表中的索引。TI 是选择域,TI = 0,代表选择 GDT (Global Descriptor Table,全局描述符表,即全局段表);TI = 1,代表选择 LDT (Local Descriptor Table,局部描述符表,即局部段表)。RPL(即 Requestor Privilege Level),占用 2 位,一共能够表示 4 个特权级,0 表示最高特权,对应内核态,3 表示最低特权,对应用户态。
上图就是一个较为完整的分段机制原理图,其中包含了虚拟地址到线性地址的转换和一些保护部件。
2.3 Linux 中的分段机制
经过上述的分析,我们知道,分段的核心之一是段寄存器,这是属于 x86 架构处理器中的硬件设备。然而,很多 RISC(精简指令)类型的处理器并没有专门的段寄存器,也就不支持分段机制。众所周知,Linux 支持多种硬件设备,尤其是应用在嵌入式方面,因此,为了使得 Linux 具有更好的移植性,需要采取一种折中的方案:让段的基地址为 0,而段界限为 4GB,也就是将整个内存空间划分为一个段,这样既照顾了 x86 等复杂指令集,又兼顾了 精简指令集类型的处理架构。换句话说,Linux 内核中的虚拟地址就是指线性地址。
3 分页机制
分页机制的作用就是将线性地址转换成物理地址。如分段机制一样,这里也需要一种数据结构,将线性地址映射到物理地址,即页表。
3.1 分页原理
分页就是将线性地址空间分为若干大小相同的页,一般取为 4KB,也就是每页大小 4K。至于为什么页面大小选择 4K,对于当前电脑常用的内存容量(4GB-32GB)来说,这是一个硬件设计人员综合评估的结果,短期内不会变化,其中考虑的因素很多,在这里不做解释。
线性:页(Page)–>> 物理:页面/物理块(Page Frame)
对于 4GB 的内存来说,页数 = 4GB / 4KB = 2 ^ 20,这么多页至少需要 20 位的二进制来表示,再加上还需要其他几位来表示属性,所以 Intel 规定 32 位描述线性地址空间和物理地址空间的映射关系。页表项结构如下图所示。
- 物理页面基地址:指的是页所对应的物理页面在内存的起始物理地址。相当于《操作系统原理》中的物理块号
- 属性:具体可参考 Intel 的用户手册。比如第 0 位为 P,P = 1,表示装入到内存中,P = 0,表示当前页面不在内存中。
3.2 实际应用
实际的操作系统,往往不会使用这样的一级页表,而会使用多级页表。
问题1:为什么 32 位的线性地址空间要使用多级页表?(通常两级页表)
3.2.1 单级页表存在的问题
很多时候,进程只需要访问几个页面即可,没有必要让整个页表的所有页面常驻内存。如果使用一级页表,页表占用的 4MB (每个页表项 32 位即 4 B,一共有 2 ^ 20 个页表项,所以页表大小为 4MB) 的连续空间就必须一直在内存中。有人可能会说,对于 4GB 或者更高的内存条来说,4MB 微不足道,但是,考虑到进程隔离,并不是说所有的进程都公用同一个页表。其实,多级页表从本质上来说,也并没有节约空间。
3.2.2 解决思路
3.2.3 实际应用
在实际应用中,Windows NT 采用两级页表,Linux 采用三级页表。当然 Windows 操作系统后来也是用的多级页表,而非仅仅是两级页表,具体要看处理器的位数。
Windows NT 采用两级页表的原因是:减少页表表目录数,当中断和异常发生并被系统捕捉后,系统将执行线程从用户态转换到核心态。
Linux 采用三级页表的原因:许多处理器都是采用的 64 位处理器,这种情况下,已不适合使用两级页表
两级页表原理
地址转换原理
需要考虑的细节
3.3 从线性地址到物理地址的转换
- 用 32 位线性地址的最高 10 位作为页目录-页表项的索引,乘以 4(因其占用 4 字节),与控制寄存器 CR3 中的页目录的起始地址合并,得到该目录项的物理地址。
- 从上述地址开始读取 32 位页目录项,取其高 20 位,底 12 位自动补 0,形成页表在内存中的起始地址。
- 将 32 位线性地址的中间 10 位作为页表-页表项的索引,同样道理,乘以 4,与页表的起始地址相加,得到二级页表项在内存中的地址。
- 从该地址开始读取页表项,加上线性地址中的底 12 位偏移量,得到最终的物理地址。
——————————————————————————
致谢:本篇博文感谢《Linux 操作系统原理与应用》一书,感谢简书博客《两级页表(MOOC)》,多图源自于此。感谢 https://bbs.****.net/topics/330019156 热心网友的回答。