内存分页

分页的目的:提高内存的利用率

将内存分为每 2122^{12} 字节,即 4096B,4KB 大小的页。这样,4GB 可以分成 2202^{20} 个大小为 4KB 的分页。

把页地址的高 20 位保存起来,再加上每个分页的一些属性,比如分页的计算单位,读写属性等。把页地址和页属性用 4 个字节保存起来。每 4 个字节为一个内存分页的入口,称为 PTE/Page Table Entrance。把这些分页入口都保存起来,形成一张表叫做页表/Page Table。那么,总共需要 220×4=2^{20} \times 4 = 4MB 的空间存储页表。

既然内存已经通过分页管理,这 4MB 自然也不例外,同样适用分页存放。

4MB 需要使用 1024 个大小为 4KB 的分页存放。1024 个页表,页表地址的高 20 位和页表属性再构成一张页表,称为页表目录(2 级页表)。这个 2 级页表需要 1024×41024 \times 4 = 4KB 字节的内存区域存储。而这个值,刚好等于 1 个页面的大小。

寻找某个具体的内存页,就到页表目录里找页表,再到页表里找具体的页。

有个专门的寄存器 CR3 用来保存页目录的内存地址,类似于 GDTR 的作用。

段页式内存管理

在段式内存管理的基础上,增加了分页机制后该如何进行内存寻址呢?

举例:

CodeDescriptor:Offset,
段基地址是 0x00000000,Offset 是 0x7f9a45d3。

那么最终虚拟内存地址为 0x7f9a45d3。

把 0x7f9a45d3 换成二进制并分成三份,高 10 位表示页表编号 T,中间的 10 位表示内存页的编号 P,低 12 位表示偏移地址 O。

CR3 中保存了页目录的起始内存地址 B(假设 B = 0x00010000)。

  1. 计算页表地址。

页目录的起始地址是 B,页表的编号是 T,那么页表的起始地址是 B + T * 4 = 0x000107f8。从 0x000107f8 往后的 4 个字节,存储的即是编号为 T 的页表的信息,我们把这个信息的高 20 位拿出,低 12 位补零,假设为 0x00023000。

  1. 计算内存页地址

0x00023000 + P * 4 = 0x00023690。从这个地址往后的 4 个字节,存储的即是编号为 P 的内存页的信息。把高 20 位拿出,低 12 位补零,假设为 0x00035000。

  1. 计算实际内存地址

0x00035000 + O = 0x000355d3。该地址即为最终访问的物理地址。

以上就是段页式内存管理。

段页式内存访问

分页机制可以解决内存碎片化导致资源利用不充分的问题。

对于不同的程序,每个程序都有一份自己的页目录、页表、页结构。不然按照同一份内存分页结构定义算出来的物理地址肯定就一样了。

最终要的是分页入口地址。

在 GDT 和 LDT 里,段描述符的界限你可以定的很大,反正都是虚的,页表和页目录是可以自由定义的,叶编号对应的物理地址随心定。只不过作为操作系统要时刻掌握内存物理页的使用情况以及使用次数。

物理页分配完了就没有了,如果定义的页入口超出了物理内存,肯定会报错(缺页中断?)。

段内存管理时,使用交换空间后,把使用频率较低的段先交换到硬盘上,再次被使用时再交换到内存。分页机制也有类似的功能,只不过被交换对象变成了内存页而已。

页属性的 P 位就是这个用途,P 为 1 说明在内存中,为 0 则反之。当系统或程序访问到一个 P 为 0 的分页,这个分页就从硬盘返回到内存。操作系统要统计每一个分页的使用次数,作为是否交换到硬盘的依据(最近最久未使用算法,先入先出算法等,参考操作系统书籍)

页的权限管理

之前讲段描述符的时候有 DPL,从 0-3。现在看来,段描述符似乎没有存在的必要了。

DPL 有 4 级,但是在实际应用中,一般只用 0 和 3,即要么能访问,要么不能访问。于是在内存页特权级管理的时候,就使用 S/U 一个位,只有 0 和 1 两种情况。这样段式内存就可以完全退出历史舞台了。64 位模式就是这么干的。

开启分页机制是切换到 64 位模式的强制要求,是内存管理的根本依据。分页机制共有 4 种,当前的分页方案并不是重点,我们只是借助它来理清楚分页机制。

内存分页的四种模式

逻辑地址:段内偏移地址,可以是一个多项式表达式。

最复杂的情况是 Base+(Index * Scale) + Displacement 的情况。

Base 和 Index 都是通用寄存器,两者都可以用作变量;Scale 是一个固定值,可以为 2、4 或 8,Displacement 是一个数值(逻辑地址)。

不过通常我们都是用它比较简单的形式,比如 mov ax, [0x7c00] 这种,实际上只使用了 Displacement。又如 mov ax, [BX+0x7c00],使用了 Base 和 Displacement 两项。而段地址或者段选择子与逻辑地址组合起来就叫做线性地址

64 位模式会把所有的段基地址都当作 0,并且不检查段选择子的界限,而段寄存器的 DS、ES、SS 会被直接忽略,CS、FS、GS 只关心一部分属性。

64 位模式下,虚拟地址或者线性地址通过分页转换为一个物理地址

以上就是 64 位 x86 系列 CPU 支持的四种内存分页模式。

64 位 CPU 线性地址宽度并不是 64 位,

分页模式之间的转换关系(不含 5-level)

其中,#GP 代表一般保护性错误。

从图中可以看出,要先把 PAE 和 LME 设为 1,然后再把 PG 设为 1,即可进入 4-level Paging 模式。

CR3 是个 64 位寄存器,在 32-bit 分页模式下使用说明:

只使用了前 32 位,前 12 位只有 2 位有效,是属性 PWT 和 PCD。后面的 31:12 共 20 位保存的是页目录的地址(页目录是 4K 对齐的,低 12 位补零即可)。

而 4-level 分页模式下,CR3 使用了全部的 64 位,其中前 32 位跟 32-bit 分页模式下是一样的,后面的 63:M 位是保留的,必须是 0。M 代表最大物理地址宽度,其上限是 52。M-1:12 之间的位数用于保存 4K 对齐的 PML4 或者 PML5,它们都是表,表里存储的是 4-level 或者 5-level 的最顶层的表数据结构,就是表入口。

32-bit 分页模式的页表数据结构只有 page-directory 和 page-table 这两层,而 4-level 则对多有 PML4(Page Map Level 4)、PDPT(Page Directory Pointer Table)、PD(Page Directory)、PT(Page Table) 四层结构。通过这 4 层结构,可以把一个 48 位的线性地址转换成一个 52 位的物理地址。

E 在这里是 Entry 的意思。PT 是由很多个 PTE 组成的,每个 PTE 指向一个物理内存页;PD 是由很多个 PDE 组成的,每个 PDE 指向一个 PT;PDPT 是由很多个 PDPTE 组成的,每个 PDPTE 指向一个 PD;PML4 由很多 PML4E 组成,每个 PML4E 指向一个 PDPT。

4-level 分页模式的页表结构

以页大小 4KB 为例

主干结构共5级,4个层次。

64 位模式下,所有表入口都使用 8 个字节来保存页的信息和属性。4KB 的页可以保存 512 个。

每一级表格最大保存 512 个入口,即 292^9,所以需要 9 位数据作为索引,而 4 级表格就需要 4×9=364 \times 9 = 36 位来保存索引。一个 4KB 内存寻址需要 12 位。所以,为了找到一个物理地址一共需要 48 位。48 正好是 4-level 的最大线性地址宽度。

把 48 拆成 9 + 9 + 9 + 9 + 12 这种格式。

第一个 9 位就是 PML4 的行号,找到这个条目就可以找到 PDPT 的物理地址。第二个 9 位就是 PDPT 表的行号,同理可以在找到的条目中查询 PD 的物理地址。第三个 9 是 PD 表的行号,第四个 9 是 PT 表的行号,逻辑跟上面一样。通过这 36 位最终定位到了一个物理页,在通过 12 位偏移就得到最终的物理地址。

当内存页是 2MB 大小时,有什么不同呢?由于 2MB=2212MB = 2^{21} ,因此至少需要 21 位偏移。

在 48 位 线性地址里的低 21 位就全都是偏移了,刚好吃掉了 PT 的 9 位索引,页表的数据结构就少了 PT 这一层。其余结构不变。

同理,当内存页是 1GB 大小时,偏移就吃掉了整整 30 位,PD 结构也就消失了。

定义 PTE、PDE、PTPDE、PML4E

PTE 的定义

0 位表示页是否在内存中

1、2 位是权限管理位

PWD、PCD、PAT 这三个位可以决定采用哪个 PAT(Page Attribute Table,页属性表) 的入口,一共有 8 种情况。

5、6 位管理多个程序访问同一个页时的同步问题

8 位决定通过这个内存页进行线性地址和物理地址的转换是否为全局。

PTE 的 0~7 位都是属性,常用的组合是这些位全部设置为 1,第 8 位也是 1。9~11 位是 ignored,令其全部为 0。12~M-1 是内存页物理地址,物理内存取 8GB,也就是 2332^{33} 字节,所以 M 的值就是 33。页大小取 4 KB。

以第三个物理页为例,它的起始地址为 0x0000000000003000。那么 12~32 位用于保存上述物理页的内存地址,值为 000000000000000000011。33 位到 63 全部都是 0。

这个 PTE 的完整格式为 000000000000000000000000000000 000000000000000000011 000 111111111

转化为 16 进制就是 0x00000000000031ff

一个 PT 里保存了 512 个这样的 PTE。

PDE 的定义

第 6 位 dirty 被忽略,第 7 位现在代表页大小,当 PS 为 0 则每页大小是 4KB,为 1 则是 2MB。

PDPTE 和 PML4E 的格式跟 PD 的几乎完全一致。

总结

内存分页的难度在于多种模式,多种页大小,多个属性位组合的复杂度,把每个细节都掌握是困难的,选择性的掌握在日后开发中用到的部分。不需要死记硬背,需要时翻阅官方手册查找即可。