Ch07 虚拟存储器
由于技术和成本等因素,早期计算机的主存受限,而程序设计时人们不希望受特定计算机物理内存大小制约,如何解决这两者之间的矛盾是一个重要问题。
此外,现代操作系统都支持多任务,如何让多个程序有效而安全地共享主存是一个重要问题。
为了解决上述两个问题,计算机中采用了「虚拟存储技术」,其基本思想是:程序员在一个不受物理内存空间限制且比物理内存空间大得多的虚拟的逻辑地址空间中编写程序,就好像每个程序都独立拥有一个巨大的存储空间一样。程序执行过程中,把当前执行到的一部分程序和相应的数据调入贮存,其他未用到的部分暂时存放在硬盘上。
虚拟存储器概述
虚拟存储器的基本概念
在不采用虚拟存储机制的计算机系统中,CPU 执行指令时,取指令和存取操作数所用的地址都是主存物理地址,无须进行地址转换,因而计算机硬件结构比较简单,指令执行速度较快。实时性要求较高的嵌入式为微控制器大多不采用虚拟存储机制。
目前在各类通用计算机系统(服务器、台式计算机、笔记本计算机等)中都采用「虚拟存储器技术」。
在采用虚拟存储技术的计算机中执行指令时,CPU 通过「存储管理单元」(MMU, memory management unit)将指令给出的「逻辑地址」(VA, virtual address)(也称为「虚拟地址」或「虚地址」)转换为主存的「物理地址」(PA, physical address)(也称为「主存地址」或「实地址」)。
在地址转换过程中,MMU 还会检查访问信息是否在主存中、地址是否越界、访问是否越权等情况:若信息不在主存中,则通知操作系统将数据从外存读到主存;若地址越界或访问越权,则通知操作系统进行相应的异常处理。
【这里有一张图,图 7.1】
「虚拟存储器机制」(简称「虚存机制」)由硬件与操作系统共同协作实现,涉及计算机系统的许多层面,包括操作系统中的许多概念,如进程、存储管理、虚拟地址空间、缺页处理等等。
进程的虚拟地址空间
每个高级语言源程序经编译、汇编、链接等处理生成可执行的二进制及其目标代码时,都会被映射到一个统一的「虚拟地址空间」。所谓「统一」是指不同的可执行文件所映射的虚拟地址空间大小和区域划分结构相同。
「进程」是操作系统对处理器中程序运行过称的一种抽象。简单来说,进程是程序的一次运行过程。因此,一个进程一定可以对应一个用户程序(即樱花用程序),后者以可执行文件的方式存放在外村。可执行文件所映射的虚拟地址空间即为进程的虚拟地址空间映像。
软件约定了统一虚拟地址空间大小和布局,从而简化了程序链接和加载过程。虚存机制给进程带来一个假象,使其认为自己独占内存,并且主存空间极大。
这有三个好处:
- 每个进程的虚拟地址空间一致,从而简化存储管理。
- 虚存机制把主存看作外部存储器的缓存,在主存中仅保存当前活动的程序段和数据区,并根据需要在外村和主存之间交换信息,通过这种方式有效地利用有限的主存空间。
- 每个进程的虚拟地址空间是私有的、独立的,因此可以保护各进程的存储空间不被其他进程破坏。
Linux 中进程的虚拟地址空间
【这里有一张图,图 7.2】
上图给出了在 Intel 架构下 Linux 操作系统中的一个进程对应的虚拟地址空间,进程的虚拟地址空间由「内核空间」和「用户空间」组成。
内核空间映射到操作系统内核代码和数据、物理存储区,包括与每个进程相关的系统级上下文数据结构(如进程表示信息、进程现场信息、页表等进程控制信息以及内核栈等),内核空间大小在每个进程的地址空间中都相同,用户程序无权访问。
用户空间映射到用户进程的代码、数据、堆和栈等用户级上下文信息。每个区域都有相应的起始位置,堆区和栈区相向生长,即栈从高地址向低地址生长,堆则相反。
对于 IA32,内核空火箭在 0xC000 0000 以上的高端地址上,用户栈区从起始位置 0xBFFF FFFF 开始向低地址增长,堆栈区中的共享库映射区域从 0x4000 0000 开始向高地址增长,只读代码段从 0x804 8000 开始向高地址增长,只读代码段后是可读写数据段,要求按 4KB 对齐。
对于 x86-64,只读代码区域从 0x40 0000 开始,用户空间的最大地址为 0x7FFF FFFF FFFF,通常,共享库映射在 0x7FFF F000 0000 - 0x7FFF FFFF FFFF 的区域内,从 0x7FFF FFFF FFFF 向下是用户「运行时栈」(runtime stack),一般限定栈大小为 8MB,整个用户空间大小为 \(2^{47}\pu{Bytes}=128\pu{TB}\),内核空间在 0x8000 0000 0000 以上的高端地址上,最大地址为 0xFFFF FFFF FFFF,整个内核空间大小也是 \(2^{47}\pu{Bytes}=128\pu{TB}\).
目前较新的 Linux 发行版,其 gcc 默认会生成「位置无关可执行文件」(PIE, position independent executable),用 OBJDUMP 查看其反汇编代码,会发现其代码起始地址并不是上述的 0x804 8000 (IA32) 或 0x40 0000 (x86-64),而是在地址 0 附近,这主要是因为「地址空间随机化技术」(ASLR),此技术在 Ch04 中有介绍。
Linux 虚拟地址空间中的区域
Linux 将进程对应的虚拟空间组织成若干「区域」(area)的集合,这些区域是指虚拟地址空间中已分配的连续区块,如图 7.2 中的只读代码段、可读写数据段、运行时堆、用户栈、共享库等区域。
Linux 内核为每个进程维护一个「进程描述符」,数据类型为 task_struct 结构。
task_struct 中记录了内核运行该进程所需要的所有信息,例如进程的 PID(process ID, process identity)、只想用户栈的指针、可执行目标文件的文件名等。task_struct 结构可对进程虚拟地址空间中的区域进行描述。
【这里有一张图,图 7.3】
如上图所示,task_struct 结构中的指针指向一个 mm_struct 结构,后者描述了对应进程虚拟存储空间的当前状态,其中,字段 mmap 指向一个由 vm_area_struct 结构构成的链表表头。
每个 vm_area_struct 结构描述了对应进程虚拟地址空间中的一个区域,可通过系统调用函数 mmap() 添加一个区域,vm_area_struct 中的部分字段如下:
vm_start:指向区域的开始处。vm_end:指向区域的结束处。vm_prot:描述区域的访问权限。vm_flags:描述区域的属性,如是否与其他进程共享等。vm_next:指向链表下一个vm_area_struct.
虚拟存储器的基本类型
在 cache - 主存层次中 cache 是主存的缓存,类似地,在虚拟存储器机制中,可将主存看成是外存的缓存。
因此实现虚拟存储器机制与实现 cache 一样,也必须考虑交换块大小、映射、替换和写策略问题。
根据方案的不同,虚拟存储器分成三种不同的类型:「段式虚拟存储器」、「页式虚拟存储器」、「段页式虚拟存储器」。
段式虚拟存储器
根据程序的模块化特性,可按程序的逻辑结构将其划分成多个相对独立的部分,这些相对独立的部分称为「段」(segment)。
分段方式下,将主存空间按实际程序中的段来划分,并通过「段表」中的「段表项」记录每个段在主存中的基址、段长、访问权限、使用和装入情况等。
每个进程有一个段表,指令给出的虚拟地址即为段内偏移,其加上对应段的基址得到实际访问的主存物理地址。
段式虚拟存储器实现机制较简单,硬件实现成本低,适合简单的嵌入式系统和实时系统。由于段的粒度较大,不易管理,且易产生主存碎片,因此现代操作系统通常不使用段式虚拟存储管理方式。
页式虚拟存储器
现代操作系统主要采用页式虚拟存储管理方式。在「页式虚拟存储系统」中,虚拟地址空间被划分为大小相等的「页」(page),外存和主存之间以页为单位交换信息。
虚拟地址空间中的页称为「虚拟页」、「逻辑页」、「虚页」,简称 VP (virtual page);主存空间也被划分为同样大小的「页框」(「页帧」),也称为「物理页」或「实页」,简称 PF (page frame) 或 PP (physical page).
虚拟存储管理采用「请求分页」思想,仅将当前程序需要的页从外存调入主存,而其他不活跃的页保留在外村。当访问信息所在页不在主存中时,CPU 抛出「缺页异常」,此时操作系统从外存将缺失页装入主存。
虚拟地址空间中有一些没有内容的「空洞」,如图 7.2 所示:
- 没有和任何内容有关联的页(例如共享库应摄取和堆或栈之间的部分)称为「未分配页」。
- 对于代码和数据等有内容区域所关联页称为「已分配页」。
已分配页又分为两类:
- 已被缓存在主存中的页称为「缓存页」。
- 未调入主存而存储在外存中的页称为「未缓存页」。
故而所有页必然属于且仅属于未分配页、缓存页、未缓存页三者其一。
在主存和 cache 之间的交换单位为主存块,而在主存和外存之间的交换单位为页,通常页比主存块大得多,只因为降低主存和外存之间交换数据的频率。典型的页大小包括 4KB、8KB、1MB. 页大小近年有增大趋势,且由于外存访问速度低,在写策略时通常采用回写而非通写方式。
降低主存和外存之间交换数据频率的另一个方法是提高命中率,因此在主存页框和虚拟页之间采用全相联映射方式。因此与 cache 一样,必须要有一种方法来维护各虚拟页与所存放的主存页框或外存位置的映射关系,通常使用「页表」(page table)这种数据结构来维护这种映射关系。
段页式虚拟存储器
顾名思义,结合段式虚拟存储器和页式虚拟存储器的特点,将程序按模块分段,段内再分页,用段表和页表(每段一个页表)进行两级定位管理。
段页式虚拟存储器实现机制复杂,地址转换需查段表和页表,时空开销都较大,现代操作系统通常很少使用这种管理方式。
页式虚拟存储器的实现
页表和页表项的结构
【这里有一张图,图 7.4】
在采用页式虚拟存储器的系统中,每页进程有一个页表,进程中每个虚拟页在页表中都有一个对应的表项,称为「页表项」。
页表项内容包括「该虚拟页的存放位置」、「装入位」(valid)、「修改位」(dirty)、「使用位」、「访问权限位」、「禁止缓存位」等。
- 页表项中的「存放位置」字段用来建立虚拟页和物理页框之间的映射,用于进行虚拟地址到物理地址的转换。
- 「装入位」也称「有效位」或「存在位」,用于表示对应页是否在主存中:若为 1,则该虚拟页已从外存调入主存,是缓存页;若为 0,则该虚拟页未调入主存:若同时存放位置为 null 则为未分配页,否则为未缓存页。
- 「修改位」即「脏位」,用于说明页面是否被修改过,这是因为虚存机制中采用回写策略。
- 「使用位」用来说明页面的使用情况,通常由页面替换算法读取,因此也称「替换控制位」。例如是否最先调入(FIFO 位)、是否最近最少用(LRU 位)等。
- 「访问权限位」用来说明页面的访问权限,通常包括读、写、执(执行),用于存储保护。
- 「禁止缓存位」用来说明页面是否可以装入 cache,通常与存储器映射 I/O 编制方式配合使用,参照 Ch09.4.4.
页表属于「进程控制信息」,位于虚拟地址空间中的内核空间,页表在主存中的首地址记录在 CPU 的「页表基址寄存器」中,供 MMU 在进行地址转换时使用。
例如图 7.3 中的 pgd 字段就记录了对应进程的页表在主存中的首地址。
页表的项数由虚拟地址空间大小决定:2 的虚拟地址位数次方除以页大小就得到页数,页数即页表项数,页表项数成页表项大小为页表大小。
解决页表过大的方法有很多,可以采用限制大小的一级页表或二级、多级页表方式,也可采用哈希方式的倒置页表等方案。具体实现方案需要协同考虑指令系统和操作系统,读者可查阅指令集手册或操作系统相关书籍。
页式存储管理总体结构
【这里有一张图,图 7.5】
在分页方式下,每个区域长度应为页大小的整数倍,不足部分补 0.
页式虚拟存储地址转换
显然对于采用虚存机制的系统,CPU 需要将虚拟地址转换为主存物理地址,才能取指令和数据。这个工作称为「地址转换」(address translation),由 CPU 中的「存储器管理单元」(MMU)完成。
由于页大小是 2 的幂次,所以虚拟地址可分为以下两个字段:高位字段的「虚拟页号」(「虚页号」、「逻辑页号」)和低位字段的「页内偏移地址」(简称为「页内地址」);主存地址亦分为高位字段的「物理页号」和地位字段的「页内偏移地址」。因虚拟页和物理页大小一样,故两者的页内偏移地址总是相等。
页式虚拟存储管理方式下的地址转换过程如下:
- MMU 根据页表基址寄存器的内容,找到主存中对应的「页表起始位置」(即「页表基地址」)
- 将虚拟地址高位字段的虚拟页号作为页表的索引,找到对应的页表项。
- 若装入位为 1,则取出物理页号(即页框号)
- 将物理页号和虚拟地址的页内地址拼接,得到用于访问主存的物理地址。
若 3. 中装入位为 0,则 MMU 抛出缺页异常,并交由操作系统处理。
若访问权限位指出页为只读页但要写入,则访问越权,抛出页故障异常。
快表
为减少访存次数,MMU 通常利用程序访问的局部性,将页表中最活跃的几个页表项装入一个特殊的高速缓存中,这些高速缓存中的页表项组成了一个页表,称为「转换旁查缓冲器」(TLB, translation lookaside buffer)或「快表」,相应地称主存中的页表为「慢表」。
于是地址转换过程中加入一步:首先查快表,命中则继续;缺失则查慢表,查慢表称作「页表遍历」(page table walk, page walk)。
TLB 基本组织结构
TLB 通常由 SRAM 或触发器实现,容量比慢表小得多。为提高命中率,通常具有较高关联度,采用全相联或组相联方式。
每个表象的内容由页表项内容加上 TLB 标记字段组成,「TLB 标记」字段用于表示该表项对应哪个虚拟页。全相联 TLB 的标记是页表项对应的虚页号,组相联 TLB 的标记是页表项对应虚页号的高位部分,低位部分为「TLB 组索引」。
查找 TLB 时,先通过虚页号的 TLB 组索引字段选出 TLB 组,然后将虚页号的标记字段与各标记字段同时进行比较。若其中一行标记相等且有效位为 1,则「TLB 命中」,此时可直接取出页表项进行地址转换;否则「TLB 缺失」,进而查询慢表。
与 cache 不同,存数指令不会将数据写入 TLB,故 TLB 的设计无须考虑写策略。
目前 TLB 的一些典型指标如下:TLB 大小为 16-512 项,命中时间为 0.5-1 个时钟周期,缺失损失为 10-100 个时钟周期,命中率为 99%-99.99%。
TLB 的缺失处理
TLB 缺失时,根据指令集体系结构设计,通常有以下两种处理方式:
- 「硬件处理方式」:首先由 MMU 在 TLB 中寻找一个空闲 TLB 表项,若已满则根据替换算法进行 TLB 替换。然后由 MMU 中的「页表遍历器」(PTW, page table walker)模块自动进行页表遍历。
- 「软件处理方式」:MMU 抛出 TLB 缺失异常,然后由操作系统的「TLB 缺失异常处理程序」进行 TLB 替换、页表遍历、页表项装入等一系列操作。
采用硬件处理方式时,TLB 的内部结构和确实处理过程对软件透明,对软件的兼容性更好,但由于需要进行 TLB 替换,因此不宜使用复杂的替换算法,通常采用随即替换策略。
采用软件处理方式时,为了让操作系统管理 TLB,指令集体系结构需要提供若干特殊寄存器和 TLB 管理指令,前者用于存放造成 TLB 缺失的虚拟地址、待装入 TLB 的表项结构等,后者用于让软件对 TLB 进行装入、清除、查找等操作。
软件必须了解 TLB 的内部结构,才能按照正确的格式将页表项装入 TLB,这种方式下,TLB 表项的替换由软件决定,因此可采用较为复杂的算法来提升 TLB 的命中率。
此外,由于异常处理会打断处理器流水线,因此软件处理方式的缺失损失比硬件处理方式大,而对于乱序超标量处理器,前者对 IPC 带来的损失更大。
使用硬件处理方式的典型指令集体系结构是 IA32,使用软件处理方式的典型指令集体系结构是 MIPS. RISC-V 页采用硬件处理方式。
联合/分离 TLB 和多级 TLB
与 cache 设计类似,TLB 的设计也可以考虑指令和数据 TLB 分离以及多级 TLB 等方案。
现代处理器通常包含 L1 和 L2 TLB,且 L1 TLB 采用「分离 TLB」, 即数据 TLB 和指令 TLB 独立工作,L2 TLB 采用「联合 TLB」。
与 cache 不同,存数指令不会将数据写入 TLB,因此不会产生数据 TLB 和指令 TLB 之间的一致性问题。
TLB 和慢表之间的一致性问题
当操作系统更新慢表中的页表项时,会带来 TLB 和慢表之间的一致性问题。
为解决上述问题,需要通过额外的同步机制来保证地址翻译过程使用的是最新的页表项。
一种简单的实现方式是冲刷系统中所有 TLB 使后续 TLB 访问一定发生确实,但对性能影响太大,可用更复杂的控制逻辑来实现仅冲刷指定部分。
特别地,对于流水线处理器,因为指令分多个阶段并行,还需冲刷流水线,避免取指、取数错误。
TLB 和地址空间切换
由于每个进程的虚拟地址空间一致,相同的虚拟地址在不同进程中将映射到不同的物理地址,因此切换到新进程时,可能访问错误的物理页。
- 一种解决办法是进程进行上下文切换时直接冲刷 TLB。
- 另一种解决办法是为页表基址寄存器和 TLB 中的每个页表项添加「地址空间标识」(ASID, address space identifier)字段,前者用于标识当前运行的是哪个进程,后者用于标识该页表项属于哪个进程。比较 TLB 标记的同时,额外比较页表基址寄存器和页表项的 ASID.
具有 TLB 和 cache 的存储系统
层次化存储系统结构
【这里有一张图,图 7.7】
这张图很详细。
CPU 访存过程
【这里有一张图,图 7.8】
【这里有一张表,表 7.1】
表 7.1 列出了关于 TLB、页、cache 命中/缺失的八种情况,其中三种情况不可能发生,可能的五种情况如下。
| 序号 | TLB | page | cache |
|---|---|---|---|
| 1 | hit | hit | hit |
| 2 | hit | hit | miss |
| 3 | miss | hit | hit |
| 4 | miss | hit | miss |
| 5 | miss | miss | miss |
第一种情况最好,无需访问主存;第二、三种情况需要访问一次主存;第四种情况需要访问两次主存;第五种情况抛出缺页异常,需访问外存且至少访问主存两次。
cache 缺失由硬件处理,缺页由软件处理(操作系统通过缺页异常处理程序实现),TLB 缺失则根据指令集体系结构设计而定。
cache 的四种查找方式
在虚拟存储系统中,可选择用物理地址或虚拟地址查找 cache 行,根据标记字段和索引字段使用的不同,共分为四种:
- 「实索引实标记」(PIPT; physically indexed, physically tagged)。优点是容易实现,缺点是每次访问签都需要先由 MMU 进行地址转换,还需等待页表项装入 TLB,通常 L2, L3 cache 采用 PIPT 方式。
- 「虚索引虚标记」(VIVT; virtually indexed, virtually tagged)。优点是查找速度快,无须经 MMU 进行地址转换即可访问,缺点是存在三个重要问题。
- 「虚索引实标记」(VIPT; virtually indexed, physically tagged)。VIPT 可在进行地址转换的同时用地址索引查找 cache 行或组,但仍需等待地址转换的出物理地址后才能比较标记。实际使用中还会让索引字段完全落在页内地址字段中,使索引字段在地址转换前后结果一致。VIPT 可以避免 VIVT 的三个重大问题,但也限制了 cache 大小。故而通常 L1 cache 采用 VIPT 方式。
- 「实索引虚标记」(PIVT; physically indexed, virtually tagged)。无明显优点,但集合了 PIPT 和 VIVT 的缺点,几乎不用。
VIVT 的三个重大问题是:
- 「别名(alias)问题」:两个不同的虚拟地址可能映射到同一物理地址,此时需维护两个 cache 行之间的数据一致性。为了判断别名关系通常需要在 cache 中添加额外的逻辑。
- 「同名(homonyms)问题」:与「TLB 和地址空间切换问题」类似。VIVT 仅靠虚拟地址无法确定 cache 行属于哪个进程。解决办法是在进程上下文切换时冲刷 cache 或为每个 cache 行添加 ASID,并添加额外比较逻辑。
- 「页表项更新问题」:与「TLB 与慢表一致性问题」类似,在页表项的映射关系更新时,还需额外写回相应的 cache 行。
存储保护机制
为避免主存中多个程序相互干扰,防止因某个进程出错而破坏其他进程,或某个进程非法访问其他进程的代码或数据区,应对每个进程进行存储保护。
为支持操作系统实现存储保护,硬件必须有以下三个基本功能:
-
使部分 CPU 状态只能由操作系统内核程序访问,而用户进程只能读不能写,或干脆禁止访问。
-
至少支持两种特权模式。操作系统内核程序需要具有比用户程序更多的特权。
-
提供在不同特权模式之间相互切换的机制。
对于 1. 只有操作系统内核程序才能通过「特权指令」(或称「管态指令」)访问页表基址寄存器、TLB 内容等。用户进程执行时,CPU 将抛出非法指令异常或保护错异常。
对于 2. 内核程序和用户程序运行在不同的「特权级别」或「特权模式」。运行内核程序时处理器所处的模式称为「监管模式」(supervisor mode)、「内核模式」(kernel mode)、「超级用户模式」或「管理程序状态」,简称为「管态」、「管理态」、「内核态」或「核心态」;运行用户程序时处理器锁出的模式称为「用户模式」(user mode)、「用户状态」或「目标程序状态」,简称为「目态」或「用户态」。
x86 架构支持四个特权级,但操作系统通常只使用 0 级(内核模式)和 3 级(用户模式);RISC-V 架构支持三种特权模式:U 模式(用户模式)、S 模式(监管模式)、M 模式(机器模式)。
对于 3. 用户模式下可以通过「系统调用执行陷阱/自陷指令」转入更高特权模式执行。异常/中断的相应过程也可切换不同特权模式。
硬件通过提供相应的「控制状态寄存器」、专门的「自陷指令」和各种「特权指令」和操作系统协同实现以上三个基本功能。
存储保护包括两种情况:访问权限保护和存储区域保护。
访问权限保护
「访问权限保护」检测是否发生「访问越权」。
通常,各程序对本程序所在的存储区可读可写,对共享区或已获授权区可读不可写,对未或授权信息不可访问。
可读写数据段为可读可写,只读代码段只读或只执。
存储区域保护
「存储区域保护」检测是否发生「地址越界」或「访问越级」,通常有以下几种常用的方式:
- 「加界重定位」:有些系统用专门的一对上界寄存器和下界寄存器,检测物理地址是否超出上下界范围。
- 「键保护」:操作系统为主存的每一个页框分配一个存储键,为每个用户进程设置一个程序键,访问时进行比较。特别地,万能匹配键设置为 0.
- 「环保护」:x86 采用该方案,操作系统内核在 0 环(内核态),操作系统其他部分在 1 环,用户进程工作在 3 环(用户态)。
IA-32 + Linux 中的地址转换*
x86 系统启动后总是先进入实地址模式,对系统进行初始化后转入保护模式。
IA32 采用段页式虚拟存储器,地址转换过程涉及「逻辑地址」、「线性地址」和「物理地址」。「逻辑地址」即「虚拟地址」。
IA32 中的逻辑地址由 48 位组成,包括 16 位的「段选择符」和 32 位的「段内偏移量」(即「有效地址」)。
为方便多用户、多任务下的存储管理,IA32 采用在分段基础上的分页机制。分段过程将逻辑地址转换为线性地址,分页过程将线性地址转换为物理地址。
逻辑地址到线性地址的转换
段描述符
【这里有一张图,图 7.9】
具体看书。
段描述符表
「段描述符表」即「段表」,主要有三种类型:「全局描述符表」(GDT, global descriptor)、「局部描述符表」(LDT, local descriptor)、「中断描述符表」(IDT, interrupt descriptor)。
具体看书。
段选择符和段寄存器
【这里有一张图,图 7.10】
上图展示了段选择符的格式,其存放在「段寄存器」中,段寄存器供 6 个,3 个有专门的功能。
具体看书。
用户不可见寄存器
用户进程不可直接访问的内部寄存器,包括「描述符 cache」、「任务寄存器」(TR)、「局部描述符表寄存器」(LDTR)、「全局描述符表寄存器」(GDTR)、「中断描述符表寄存器」(IDTR)。
逻辑地址向线性地址的转换
【这里有一张图,图 7.12】
看图说话。
线性地址到物理地址的转换
控制寄存器
CRx,包括 CR0、CR2、CR3 寄存器。
两级页表方式中,32 位线性地址由 10 位「页目录索引」(DIR)、10 位「页表索引」(PAGE)和 12 位 「页内偏移量」(OFFSET)组成.
具体看书。