内存分段与分页
一年前写了 “段页内存管理” 的部分章节,后面一直搁置在草稿箱中。最近发现内存相关的知识非常重要,最近几十年科技文明的巅峰,硬件侧就是 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 字符读取的流程图。
顺带提一个,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 读取内存之前,还有操作系统的干预,属于前置链路。有下面几个问题:
- 应用程序通过 CPU 操作内存,那么有没有可能 A 应用程序通过 CPU 发送的内存地址是 B 应用程序的?这会导致各个应用程序的数据窃取和乱改,非常可怕。
- A/B 应用程序都占用一段内存空间,还剩下一部分空间 M 没有使用。这时候如果关闭了 A 程序,打开了 C 程序。C 需要的内存空间大于 A 和 M,小于 A+M,这时候如果执行 C 程序?
- 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 根地址总线的最大内存检索范围。
而 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 会去拿页表里面存储的物理地址。
上面二级页表视图里面,有一个 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 高速缓存,和内存的 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 寄存器。这里的 PAE 技术,就是将 CR3 寄存器本身不再存放页目录的地址,而是指向了页目录指针表。相当于向上又增加了一个维度(增加一级)。
所以对于 16 位 CPU 操作 1M 内存空间,和 32 位 CPU 操作 64G 内存空间,都需要扩展地址总线。因为不扩展地址总线,就无法表达更大的内存地址。
扩展了地址总线宽度后,16 位 CPU 基于分段模型给了解决方案。32 位 CPU 基于分页模型给了解决方案。