内存分段与分页

一年前写了 “段页内存管理” 的部分章节,后面一直搁置在草稿箱中。最近发现内存相关的知识非常重要,最近几十年科技文明的巅峰,硬件侧就是 CPU,软件侧就围绕着操作系统对内存的控制了。就又整理补齐,形成内存分段分页较完整说明。
硬件 CPU 的不断革新,从 X86 和 ARM 架构的铺天盖地,到 RISC-V 新星崛起和 MIPS 的消退。还有各种流水线优化、APU-ZPU 的推成出新,以及 Apple M1 的展露头脚。虽然 CPU 本身短时间内看不到跨越式的提升,但 CPU 周边是玩出了花。
而已经趋于稳定的操作系统,最近好多年都没有啥惊世骇俗的壮举出现。依托着 CPU 侧的大腿,操作系统躺着进行升级。但换一个角度,也可以认为操作系统已经设计的足够完备,在商业化的时代,有需求就一定会有满足,或许对于当前操作系统来说,目前的设计应对有余。
操作系统的很多设计,都摆脱不了内存这尊大神。首先,操作系统本身和上层应用,就是放在内存里运行的。其次,操作系统的基石进程和线程,就贴着内存进行设计。多核心后的 L0-2 级缓存同步也是为内存定制,当然这个算 CPU 对内存的依赖。IO / 文件系统 / 网络也都离不开内存的影子。
内存本身的制作工艺门槛不高,有 N 多厂商做这个事情。但内存在科技长河中的位置,绝对举足轻重。整个计算机的发展史,内存表现不多,但是中流砥柱。
本文可以顺带解决如下几个问题:

  • 地址总线、数据总线、控制总线是什么?CPU 如何通过地址总线找到内存地址?
  • CPU 和内存之间的高速缓存引发的缓存一致性问题是怎么回事?锁 - 共享数据安全指↑
  • 16 位 CPU 是如何操作 1M 内存空间的 (2^16=64kb)?32 位 CPU 是如何操作 64G 内存空间的 (2^32=4G)?他们的原理一样吗?
  • 分段内存管理,里面的段指的是什么?
  • 除了虚拟地址、物理地址,还有线性地址和逻辑地址,它们是什么?
  • 两个进程的虚拟地址相同,是如何指向不同的物理地址的?虚拟地址的页目录 & 页偏移均一致,如何通过 m = f(n) 函数,以相同的 n 输入,返回不同的 m 输出。

推荐书:《汇编语言》- 王爽、《程序员的自我修养 - 链接装载与库》- 俞甲子
推荐网文:《深入浅出计算机组成原理》- 徐文浩、《操作系统实战》-LMOS、
一定要读的另一篇内存文章:锁 - 共享数据安全指↑
内存相关的知识非常重要,需要很大篇幅和示例图来阐述。在锁 - 共享数据安全指↑中文字数超过了 4W,本文的文字数也超过了 1W,均需要多张配图。重要性和难理解程度,都很高。

0x00 内存是如何被 CPU 消费的

虽然操作系统和各类应用程序,都需要使用内存。但是对内存的访问,却是 CPU 做的。软件层能直接通信的硬件对象,只有 CPU。CPU 是集合了运算器 (ALU)、控制器、存储器的大部件,其中存储器,指的就是内存 & 磁盘等信息存储设备。
CPU 对内存有一套统一的读取控制,上层软件对内存的操作皆通过 CPU 进行收口。操作系统的进程设计,如数据隔离 & 共享,就是通过 CPU 对内存空间进行增删改查。
CPU 和内存都是硬件,硬件之间相互通信就得依靠有线和无线传输,显然 CPU 和内存通信需要有线。这里有一个南北桥的历史,CPU 通过北桥和高速设备连接,通过南桥和低速设备连接,南北桥就是 CPU 和外部设备通信收口的地方。现在高速设备的连接,都是集成在 CPU 内部,不再通过北桥来做了,但是南桥还是依旧在的。但不管有没有北桥,CPU 和内存通信,都需要三根总线,即地址总线、数据总线、控制总线
地址总线,就是对内存区域进行定位的。内存中每个内存地址代表一个字节,共 8 位。一个 256 字节的内存条,内存地址从 0x00 (00000000) 到 0xFF (11111111)。其中每个字节都有一个地址,如 0x03 表示第 3 字节。
控制总线,就是 CPU 对内存的控制指令。是读 (load) 数据,还是写 (store) 数据等。
数据总线,就是用于 CPU 和内存之间双向传输数据用的。如果有 8 根总线,那么一次只能传输 1 个字节的数据。如果 CPU 想写 2 个字节的数据到内存中,就需要传输 2 次。
下图是 8086 CPU 对内存地址 0x03 进行 c 字符读取的流程图。

8086 CPU 对内存地址 0x03 进行 c 字符读取的流程图

顺带提一个,CPU 其实不喜欢所有的外部存储设备。因为即使内存速度已经比较快,但相比 CPU 来说,还是太慢。
CPU 内的运算器、控制器都是晶体三极管制作,通过各种门电路进行电信号传输,寄存器内部元件的运行速度是非常快的,速度和 CPU 同步
内存是动态随机存储器 (DRAM),通过一个晶体管和一个电容来保存一个比特的数据。如果要表示 1,就需要输入高电压,往电容里面充电。当开关关闭,就需要输入低电压,电容开始放电,表示 0。电容还会漏电,所以每隔一段时间,就需要往所有开关打开的电容里面补充电量。电容的这个机制,就是为什么内存不能做断电存储的原因,也是为什么叫动态随机存储器里面动态两个字的原因
所以 CPU 在和内存进行数据读取的时候,时间差就会非常大,CPU 发出指令,需要在 100ns 后拿到内存数据。如果把 ns 作为数量级,那么 CPU 和内存之间的速度差有 2 个数量级 (100ns)。当然磁盘更大,达到 6 个数量级 (150us)。
所以 CPU 就在它和内存之间架起了一个中间层,即 L0-2 高速缓存。
高速缓存是静态随机存储器 (SRAM),需要 6-8 个晶体管来保存一个比特的数据。通过晶体管的组合可以形成锁存器,对 1&0 进行记忆,就不需要电容来保持高低电平的状态了,速度也会更快。CPU 和高速缓存之间速度上有 0 个数量级 (1ns)。
高速缓存是必须的,因为 CPU 和内存的速度鸿沟很大。但是高速缓存也引发了缓存一致性问题,在我的另一个 blog 里面有详细说明和解决方案,都是干货。锁 - 共享数据安全指↑

好,这里就把 CPU 如何与内存通信的硬件链路说通了。但在 CPU 读取内存之前,还有操作系统的干预,属于前置链路。有下面几个问题:

  1. 应用程序通过 CPU 操作内存,那么有没有可能 A 应用程序通过 CPU 发送的内存地址是 B 应用程序的?这会导致各个应用程序的数据窃取和乱改,非常可怕。
  2. A/B 应用程序都占用一段内存空间,还剩下一部分空间 M 没有使用。这时候如果关闭了 A 程序,打开了 C 程序。C 需要的内存空间大于 A 和 M,小于 A+M,这时候如果执行 C 程序?
  3. A 程序会释放部分它不再使用的内存空间,这些内存空洞该如何处理?

围绕这三个问题,就展开了内存分段分页黄金追逐时代

0x01 内存蛮荒时代 - 分段

在 8086 芯片被 intel 破土成功的时候,内存是被程序员直接控制使用的。那时候还没有虚拟内存、MMU、分页等等这些概念 (这个时候叫逻辑地址,后面会说)。简单来说,那时候内存控制是低级别控制,得开发人员自行汇编控制。想读取 0x?? 地址的内存数据,得汇编写出来这个地址。
8086 的硬件配置是数据总线宽度 16、地址总线宽度 20。而寄存器的位数一般都是和数据总线宽度相同,即 16 位寄存器。
CPU 的运算器、控制器都是通过寄存器表达的,这时候就会有个问题,向内存地址总线传递的地址如果直接从寄存器来,那么范围不够。16 位寄存器只能确定 0-2^16 字节 (64kb) 的内存空间范围,而 20 根地址总线可以确定 0-2^20 字节 (1M) 的内存空间(开头提到的第三个问题,这里就是 16 位 CPU 能操作 1M 内存空间的原因,因为有 20 根地址总线。32 位 CPU 后面再说)。所以 64kb-1M 的空间都只能被浪费掉,这显然是不行的。
所以 intel 想了一个办法,增加了 4 个段寄存器,分别是 cs、ds、ss、rs。通过 cs:ip 两个寄存器来确定 pc 寄存器的地址,即 cs*16+ip,其他段寄存器也是一样的计算方式。
cs*16,即将 cs 的值左移 4 位。这样就通过段寄存器 * 16 + 段偏移量的方式,增加到 20 位,也满足了 20 根地址总线的最大内存检索范围。

段寄存器使用1
段寄存器使用1

而 cs、ds、ss、rs 也分别表示代码段、数据段、栈段、其他段,可以做数据安全。比如数据段,就需要 ds 来做段基地址,可读可写。如果写成了 cs 代码段寄存器,代码段权限只读,那么写数据的时候就会异常。
虽然话是这么说,但是 8086 CPU 并没有做这层校验,ds*16+ip (ds 是数据段,ip 是代码区) 这样的写法,也不会报错,能够正常读取到指令。但也没人刻意这么做
这些段寄存器,就表示了内存的分段模型
这里有一个误区,即可执行文件的二进制格式,也是通过代码区、数据区这些区来表达的,是不是和分段模型有关联?
对于可执行文件,不管是 window 的 PE,还是 Linux 的 ELF,都是从 COFF 发展而来。COFF 格式的可执行文件,就是将指令和数据分开存储,即我们的代码在代码区,只读。一些 const 变量则放置于常量区,也是只读。一个全局变量则放置于静态区,可读可写。函数方法中的局部变量,则放置于栈区,可读可写。
COFF 这套二进制可执行文件的区域划分,本身是和内存分段没有关系。如果没有 8086 CPU 的 16 位寄存器和 20 位地址总线的约束,比如有一个 CPU 正好地址总线也是 16 位,那么内存访问就不需要段寄存器了,可以直接寻址。这就不是内存分段模型了。
主要是内存分段模型,通过段寄存器解决了 16 位寄存器和 20 根地址总线不对应的 CPU 内存数据互通问题,我们把这种 CPU 和 内存不对应的关系的解决方案,叫做分段模型。其实现在的 64 位 CPU,可执行文件还是 COFF 格式,可他们已经不在是分段模型了。

8086 时期,CPU 访问内存的这种方式,也导致了不少问题,有内存安全、内存大空洞 (应用关闭)、内存小空洞 (碎片化)。
大多人对这几个问题的理解有出入,他们认为导致这些问题的原因是分段模型,我认为非常不准确。当然,如果在 8086 时期,就搞出了分页模型,当然不会有这些问题。但当时为什么没有分页模型出现?我认为是当时的环境,使用分段模型就已经能够解决问题了。毕竟那时一个 CPU 大多只跑一个应用程序,都不需要操作系统的计算机发展的初级阶段。那个时候,能把 8086 CPU 做出来,就已经是科技发展的巅峰了。所以分页能够解决的问题,在那个时候,可能仅仅是一小戳人的需求。

内存安全

这个阶段还没有虚拟地址的概念,也没有分页内存的概念。所以应用程序是全部加在到内存中后再执行。基于分段模型,应用程序可以直接使用物理内存地址进行数据的增删改查。
这时候会产生非常严重的破坏性操作,即 A 程序修改或者读取了 B 程序的数据。如下图所示:
内存安全

内存空洞

内存空洞使得这个阶段的应用程序执行非常艰难,需要提前规划好内存分配情况。
内存大空洞&小空洞
操作系统这个时候也可以将部分内存空间 swap 到磁盘中,从而空出一块比较连续的内存空间给合适的应用程序执行。但磁盘的换入换出性能消耗也非常大。因为磁盘和 CPU 之间,有 6 个数量级的时间差。

所以本质上来说,这时候的内存分段是比较混乱的,段寄存器更多的用于 16 位寄存器和 20 根地址总线之间的不协调的适配。仅仅是因为 8086 CPU 的寄存器位数和地址总线不一致,所以才有了段寄存器这种内存定位的方式
但也不能把仅仅两个字说的这么轻描淡写,因为 8086 这套规则,影响实在太大了。
8086 是计算机初期发展使用范围最广的 CPU 版本。其 x86 架构现在还在征服着 PC 机市场,而它的分段模型,也直接影响着后续的 CPU 升级,即兼容
Inter 的 CPU 发展,一直在做架构指令集的兼容。这种兼容是 Inter 必须要走的路线,因为 8086 太成功,有太多商业应用了。从兼容了第一个版本开始,就要一直兼容下去。其中有一款和 AMD 竞争的酷睿处理器,就是因为没有做兼容,导致 AMD 崛起,预先做出了 64 位指令集。这套指令集兼容了 x86,又叫做 x86-64,基本上现在的 PC 机,所使用的 CPU 都是这套指令集。
在 Inter 发展 32 位 CPU 的时候,因为兼容,段寄存器的概念也无法丢弃,又开始做改造了。

0x02 内存青铜时代 - 分段分页共存

说多少位 CPU,其实就是寄存器是多少位的。如果说一个 CPU 是 N 位,那么 CPU 的寄存器一定也是 N 位,数据总线一般的宽度一般也是 N,地址总线的宽度就不确定了(8086 16 位 CPU,地址总线宽度 20。32 位 CPU,地址总线宽度也是 32。64 位 CPU,地址总线宽度是 46,因为没有那么大的内存条,更宽的总线是浪费)。
到了 Inter 32 位 CPU 发布的时候,这个时候寄存器是 32 位,数据总线、地址总线也都是 32 根。32 位 CPU 使用的还是分段模型,但是 32 位 CPU 还有一个平坦模式可以切换,平坦模式使用的就是分页模型了。
32 位 CPU 叫做保护模式了,因为增加了很多安全控制,比如 R0 - 3 四个特权级。Windows 和 Linux 只使用了其中的两个特权级。相比 16 位 CPU,那个时候叫做实模式

保护模式未开启平坦模式

虽然寄存器是 32 位,但 32 位本身也可以作为独立的 16 位使用,比如 32 位的 AX 寄存器,高 16 位用作 AH,低 16 位用作 AL,可以独立使用,这也是兼容。
但是段寄存器还是 16 位。至于为什么这么做,我猜有一定原因是为了弱化内存分段模型,毕竟 CPU 的晶体三极管,也是寸土寸金。
在 16 位 CPU 的时候,段寄存器存放的是段基地址,把段基地址左移 4 位和段偏移量做和运算,即最终的物理地址。
那么 16 位的段寄存器还怎么存储 32 位的段基地址呢?
这个时候多了一个段描述符,段描述符是一个列表。运行程序的时候,先将程序需要的各个区的大小整理好,写入程序内存中,生成段描述符列表。并将段描述符列表的地址和长度保存在 GDTR 寄存器中 (后面再说)。
每个表项长度是 64 个字节,存储有段基地址和段长度,用于表示代码区的基地址是多少,代码区共有多大这样子。还存储了其他一些信息,比如有个 G 标志位,表示当前段长度的单位是 1 字节还是 4 kb。因为段长度总共分配了 20 位,如果单位是 1 字节,那么该段长度就是 1M。如果单位是 4kb,那么该段段长度就是 4G。除了 G 标志位,还有 T 表示代码段或者数据段,R 表示是否可读,C 表示是否可执行等等。
段描述符兼容了 16 位的段寄存器,所以 64 位的长度里做了很多取舍和兼容。
有了段描述符列表,只需要在 16 位的段寄存器里面,指定当前需要段描述符列表的第几位,拿到列表项后,列表项里面存储着段基地址和段长度等信息。
段寄存器里面只存放段描述符列表的索引,还需要一个地址标记着段描述符列表在哪里,这样才能找到对应的列表项。段描述符列表的地址就存放在 GDTR 寄存器中,上面已经说到。

这样,32 位保护模式未开启平坦模式的时候,内存读取操作的表现就是和 16 位 CPU 一样的。只是多了段描述符和 GDTR 寄存器这些中间层。

保护模式开启平坦模式

CPU 不会主动开启平坦模式,当然,操作系统会帮我们开启。当开启了平坦模式后,就从分段模型切换到分页模型了。这里就说下什么是分页模型。

首先,根据时间 / 空间局部性,程序运行过程中,同一段时间只会有一部分代码区指令在执行,其他代码区指令都没有被执行。而执行的这些指令,一般都在一块。
那么我们可以在某块代码区被执行的时候,再把这些指令加在到内存中,其他的指令依旧保存在磁盘中,这样可以减少应用程序对内存的占用。
刚才说到某块代码区,如果我们可以标记这个代码区的大小,就可以对代码区的加载做自动化。即需要某块代码区的时候,就加载固定大小的磁盘空间到内存中来。过一会这些指令执行完了,那就从磁盘再加载下一个区块的指令。
那,这个代码区的大小,设置多少合适呢?目前主流的操作系统都设置为 4kb,也有设置 4M 的
刚才说的某块代码区 4kb,说的还是二进制可执行文件。这个时候文件还是存储在磁盘上的。我们是按照 4kb 对文件进行了分割。那分割并加载这 4kb 的文件到内存后,内存也理应有 4kb 大小的区域对这 4kb 文件进行存储。
所以我们将内存也分割成 N 个 4kb 大小的虚拟区块,注意,这是虚拟区块,物理上内存区块是连续的。4kb 虚拟区块,就是分页模式的基石。
有了这 4kb 的虚拟区块,因为颗粒度很细且固定,所以可以完成很多分段模式不好完成的事情。比如分配和回收,而分段模型最难的地方就在于分配和回收,因为段长度不固定,颗粒度太大

分页模型,主要的三个理解名词,一个是 4kb,一个是虚拟地址,还有一个就是页表
在上面 16 位 CPU 实模式的时候,以及 32 位未开启平坦模式的时候,使用的都是物理地址。就是开发人员在汇编里面写入内存的物理地址,或者编译高级语言的时候链接器指定物理地址,然后程序通过物理地址进行内存读取。其实分段模型最大的问题就是使得虚拟地址无法实施。使得分段模型下没有虚拟地址。
虚拟地址,就完全不需要开发人员来做了,开发人员根本做不来了。可执行文件的虚拟地址都是通过链接器来实现的(非编译器)。
虚拟地址,就是给一个应用程序错觉,让每个可执行文件都认为自己拥有内存的所有区域的使用权限。所以对于 32 位 CPU 来说,每个可执行文件的虚拟地址范围都是 0x00000000 - 0xFFFFFFFF。
然后趁应用程序不注意的时候,通过页表这个数据结构,将虚拟地址转换成物理地址,并进行真实的内存读取。
虚拟地址到物理地址到转换,肯定是要牺牲一些性能的。为了更加的高效,就在 CPU 里面配了一个 MMU 硬件。
MMU 默认会去读页表的数据。页表里面一开始是空白的,如果发现空白,就会发出缺页异常,去磁盘加载对应的 4kb 大小的指令到物理内存中,并把物理内存的地址更新到页表中。然后,后续这 4kb 指令的读取,MMU 会去拿页表里面存储的物理地址。

磁盘、虚拟内存、物理内存
4kb 内存分页
二级页表

上面二级页表视图里面,有一个 CR3 寄存器,存放着当前应用程序的页目录地址。拿到这个一级页目录地址后,再用虚拟地址里面的页目录索引做偏移,就可以拿到二级页表的地址。
当应用程序切换的时候,只需要更新 CR3 寄存器的值为当前应用程序的页目录地址,就可以使得每个应用程序都有独立的页表了
这个 CR3 寄存器,在一项 PAE 的技术里面也有使用。末尾彩蛋会说。

32 位 CPU,地址总线也是 32 位,所以最大的内存空间就是 4G 了。这里使用了二级页表。整体来说,页表的级数越多,页表本身会越省空间。但是这个中间层也是耗时的,级数越多,操作耗时也就越多。所以一般 32 位 CPU 使用的是二级页表,64 位 CPU 就使用的四级页表了。64 位 CPU 在后面的长模式里面说。

操作系统会帮我们开启平坦模式,所以大家使用的一般都是保护模式的平坦模式分页模型。使用了平坦模型,上面的段寄存器不是说就丢掉了,因为要兼容。
兼容的办法就是将所有的段描述符列表里面的所有项,段基地址都修改成 0x00000000,段长度都修改成 0xFFFFFFFF,然后把 G 标志位 (段长度单位) 修改成 4kb。这样,所有的段基地址都是 0,段长度都是 4G (2^20^4kb) 了
这样,就可以当作断寄存器不存在了,完全又分页模型来控制。但实际上这层段地址的运算还是存在的,因为要兼容。

对于虚拟地址,开头有个题目 6,可能有同学会不理解。两个应用程序的虚拟地址是一样的,那么 MMU 如果通过页表找到的物理地址不一样?因为按照上面二级页表的寻址规则,入参一样,出参应该也一样。
其实上面的图示里面,是已经发生缺页异常后的场景。因为发生了缺页异常,于是从磁盘里面加载了 4kb 放置到内存中,并把放置该 4kb 的内存地址写入到了页表中。
对于不同的应用程序,在发生缺页异常的时候,放置 4kb 的内存区块肯定是不一样的,这是操作系统来维护的,不需要我们担心。所以缺页异常后不同的应用即使虚拟地址相同,写入到页表中的物理地址也是不同的。
当然,这也有一个专业的名词,叫做页表管理

还有为什么多级页表会节省空间,这个就自己画一下整理一下,就能理解了。

保护模式到底属于什么内存模型

i386,就是 32 位 CPU。我们把这种 CPU 叫做段式管理和页式管理混合模式。其实没啥新意,就通过平坦模式来进行区分。他们并没有过多的混合,更准确的说法,应该叫段页隔离模式

0x03 内存文明时代 - 分页

前面说到 Inter 在弄一个酷睿处理器的时候,不仅高性能的酷睿处理器没弄好,还错失先机,让 AMD 弄出了 x86 兼容 64 位,即 x86-64。
32 位 CPU 能支持的最大内存就是 4G。当然通过 PAE 技术也能使用到 64G 的内存,但需要特别改造,后面的加餐会说。
在内存快速发展的时间点,64 位 CPU 可以搭配更大的地址总线,毕竟更大的内存谁会不喜欢呢。
这就来到了 Inter x86 架构的长模式
在实模式的时候,只能使用分段模型的物理地址。在保护模式的时候,分段和分页模型可以并存,但是操作系统会修改成平坦模式,大多使用的都是分页模型。到了长模式,就只有分页模型了。
一方面只能使用分页模型,一方面 x86 的分段模型还要兼容。32 位平坦模式的兼容办法是把段基地址修改成 0x00000000,段长度修改成 0xFFFFFFFF。长模式的兼容办法是把段基地址和段长度都设置为无效位,避免了多余的运算。

因为整体的分页模型没有改变,所以对内存的操作上,大的方向没有改变。这里额外说明另外两个知识点,一个是线性地址,一个是 TLB

线性地址 & 逻辑地址

虚拟地址是给应用程序看的,物理地址是给内存看的。从上面的兼容可以看到,段寄存器一直存在,那么虚拟地址还会经过分段模型走一遍,产生另一个地址 N。然后 N 被 MMU 硬解成物理地址。
这里的 N,就是线性地址 (有小彩蛋), 即分页模型下经过段寄存器处理过后的地址。
对于 32 位保护模式下的平坦模式,段基地址为 0,段长度为 4G,所以 N 等于虚拟地址,即段寄存器空运算了一次。
对于 64 位长模式,段基地址和段长度都是无效位,所以 N 等于虚拟地址,即段寄存器没有参与运算。
所以分页模型下,虚拟地址和线性地址是一样的值。
!!因为虚拟地址和线性地址是一样的,而我们一直都习惯叫虚拟地址,其实是先入为主了。
!!其实,虚拟地址和线性地址的定义刚好反过来。即程序里面看到的,其实是线性地址,经过分段模型后产生的 N,其实是虚拟地址。
!!更宽泛点来说,线性地址就是虚拟地址,虚拟地址就是线性地址。虚拟地址和线性地址相互是 alias,毕竟他们之间完全相等。但上面的分段模型运算就是他们之间的差异。

把同样的逻辑,搬到 16 位 CPU 实模式分段模型的场景,那时我们是通过 cs*16+ip 这种形式来确定物理地址的。cs 和 ip 寄存器的值。我们可以提前计算这个值,而最后也通过这个值进行物理内存地址寻址。
我们认为我们通过物理地址来寻址,其实我们用的是 cs 和 ip。这种通过段基地址左移加偏移的方式,即 base*16+offset,就是逻辑地址。逻辑地址不是计算后的值,而是指代 cs 和 ip 本身,是一对。
逻辑地址通过分段模型后,生成的值就是物理地址。
所以分段模型下,逻辑地址和物理地址虽然定义不一样,但实际上也可以说是一样的值。

TLB

TLB 高速缓存

TLB 高速缓存,和内存的 L0-2 三级缓存是一样的。也是硬件集成在 CPU 中。是对 MMU 页表运算的高速缓存。
TLB 的 L0 缓存,分为指令地址缓存和数据地址缓存。L1-2 级缓存,就是全地址缓存了。
基本和 CPU 对内存的 L0-2 高速缓存一模一样。脏数据也同样需要被标记和写回。
这一块就去看锁 - 共享数据安全指↑就可以了。

0x04 实模式、保护模式、长模式的开启

对于目前所有市场上的 Inter 的 CPU,不管 32 位还是 64 位,实模式都是一直存在的。在给 CPU 加电的时候,就是实模式。开启电脑后进入的 BIOS 系统,就是实模式。
保护模式和长模式都是需要主动开启的。这个操作系统帮我们做了。
其中,如果要开启长模式,必须要先开启保护模式。所以实模式、保护模式、长模式,每一次开机过程中,都会体验一次。这是操作系统都帮我们做了

在开启保护模式和长模式之前,都需要配置全局段符号表,这个也是操作系统帮我们做了。
如果要开启保护模式的平坦模式和长模式,还需要配置成分页模式,这个也是操作系统帮我们做了,

0x05 加餐 - 文章开头的问题 3

问题 3:
16 位 CPU 是如何操作 1M 内存空间的 (2^16=64kb)?32 位 CPU 是如何操作 64G 内存空间的 (2^32=4G)?他们的原理一样吗?

答:
0x01 内存蛮荒时代 - 分段章节,已经说了 16 位 CPU 是如何操作 1M 内存空间的。主要依靠 20 根地址总线。所以这 1M 的内存空间,都是实打实的可以访问的。逻辑地址完全能定位所有内存地址。

32 位 CPU 的地址总线也是 32 根,但是对于特殊场景如大型研究院等,32 位 CPU 仅支持 4G 内存可能不够用。所以 Windows 系统自身做了特殊处理,以使得 32 位 CPU 也可能使用超过 4G 的内存。

微软的这个技术,叫 PAE
其实和 16 位支持 1M 内存空间大同小异,也是扩展了地址总线。从 32 根扩展到了 36 根。最大寻址从 4G 扩展到了 64G。
但不再通过逻辑地址的分段模型,而是在分页模型的基础上调整页表实现的。

from wiki

CR3 指向页目录指针表

保护模式开启平坦模式章节,我们说的二级页表中,有一个 CR3 寄存器。这里的 PAE 技术,就是将 CR3 寄存器本身不再存放页目录的地址,而是指向了页目录指针表。相当于向上又增加了一个维度(增加一级)。

所以对于 16 位 CPU 操作 1M 内存空间,和 32 位 CPU 操作 64G 内存空间,都需要扩展地址总线。因为不扩展地址总线,就无法表达更大的内存地址。
扩展了地址总线宽度后,16 位 CPU 基于分段模型给了解决方案。32 位 CPU 基于分页模型给了解决方案。