虚拟内存

225 阅读11分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情

为了更加有效的管理内存并减少出错,现在操作系统提供了一种对主存的抽象概念,叫做虚拟内存。虚拟内存是计算机系统内存管理的一种重要技术,它为每个进程提供了一个大的、一致的和私有的地址空间,从而简化了内存管理。它总是沉默的、自动的工作,不需要应用程序员去做什么,特别是对 JAVA 这种有自动内存回收机制的语言来说,跟内存直接打交道的机会就更少了。但是虚拟内存系统是一个设计非常巧妙、有意思的系统,很值得我们去学习,很多思想可以借鉴。

虚拟内存主要提供了以下三个重要能力:

  1. 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,它高效的使用了主存
  2. 它为每个进程提供了一致的地址空间,从而简化了内存管理
  3. 它包含了每个进程的地址空间不被其他进程破坏

虚拟寻址

计算机的主存可以被看成是一个由 M 个连续的字节大小的单元组成的数组,每个字节都有一个唯一的编号被称为物理地址(Physical Address,PA)。早先的计算机都是使用物理寻址,即直接根据物理地址读取相应的数据,现在的一些嵌入式系统仍然使用物理寻址方式。现代的计算机都使用的是一种虚拟寻址的寻址方式,如下图所示。

虚拟寻址

使用虚拟寻址方式,CPU 生成一个虚拟地址(Virtual Address,VA)来访问主存,这个虚拟地址在被送到主存之前会先转换成物理地址。CPU 芯片上有一个叫内存管理单元(Memory Management Unit,MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址。

虚拟内存组织

和存储器层次结构中其他缓存一样,磁盘上的数据被分割成大小相同块,这些块作为磁盘和主存之间的传输单元。计算机系统将虚拟内存分割成被称为虚拟页(Virtual Page,VP)的大小固定的块来处理此问题,同样物理内存也被分割成物理页(Physical Page,PP)大小的固定块,大小与虚拟页一致,物理页也被称为页帧。

虚拟页存在三种状态:

  • 未分配的:系统还未分配的页,为分配的块没有任何数据与他们想关联。
  • 缓存的:缓存了物理内存中的已分配页。
  • 未缓存的:未缓存在物理内存中的已分配页。

组织结构

在存储层次结构中,我们知道 DRAM 比 SRAM 慢了大约 10 倍,而磁盘比 DRAM 慢了大于 10W 倍,因此,DRAM 缓存的不命中要比 SRAM 的缓存不命中代价大的多。从磁盘的一个扇区读取第一个字节的时间开销比起读取这个扇区中连续字节要慢大约 10W 倍。

因为不命中代价较大及访问第一个字节的大开销,虚拟页也往往比较大,通常是 4KB~2MB。同时由于不命中代价较大,DRAM 缓存策略是全相连的,因为相比较不命中的代价,搜索的复杂度是可容忍的。

页表

与其他缓存系统一样,虚拟内存系统需要用某种方法来判断一个虚拟页是否已经缓存在 DRAM 中,同时也需要知道确切的物理页位置。如果未缓存的,还需要知道这个虚拟页对应磁盘的哪个位置。

在物理内存中存放着一个数据结构被称为页表(Page Table,PT),页表将虚拟页映射到物理页。每次地址翻译的时候就需要读取页表。页表中的每个条目被称为页表条目(Page Table Entry,PTE)。

页命中

页命中

当 CPU 要读取包含在 VP2 虚拟页中的数据时候,地址翻译器将虚拟地址作为索引来定位到 PTE2,因为设置了有效位 1,那么地址翻译器就知道 VP2 已经缓存在 DRAM 中,然后就可以使用 PTE 中的物理内存地址进行数据读取。

缺页

在虚拟内存系统中,DRAM 缓存不命中被称为缺页。

例如当 CPU 需要读取包含在 VP3 虚拟页中的数据时候,由于有效位为 0,所以可以判定 VP3 未被缓存到 DRAM 中。也是就触发了缺页异常,缺页异常调用内核的缺页异常处理程序,如果 DRAM 已满,该程序会选择一个牺牲页,例如下图将 VP4 作为牺牲页。如果 VP4 已经被修改了,那么内核会将它复制到磁盘。

缺页

接下来,内核会从磁盘复制 VP3 到内存中的 PP3,更新 PTE3,随后返回。现在 VP3 已经缓存在 DRAM 中了,地址翻译硬件也可以继续正常处理。

分配页面

当未分配的页面需要分配一个新的虚拟页面时候。如下图所示,需要进行 VP5 分配,就是在磁盘上创建空间并更新 PTE5,使它指向磁盘上这个新创建的页面。

分配页面

多级页表

现代 64 位操作系统一般都采用 48 位的虚拟地址空间,假设每个虚拟页大小为 4KB,每个 PTE 大小为 8byte,那么整个页表的大小将是:24821223=239byte=512GB2^{48}*2^{-12}*2^{3}=2^{39}byte=512GB 。这对任何系统来说都是难以承受的。

于是就引入了多级页表的概念,多级页表将 VPN (VPN,VPO 概念可见下面内容)划分为多个层级,地址查询的时候,需要从一级页表逐层进行查询。只有一级页表需要常驻内存中,虚拟内存系统可以在需要的时候才调出次级页表,这样就减少了对主存的压力。同时对一个普通应用程序而言,在使用一个 48 位的虚拟地址空间时候,绝大部分虚拟地址空间应该都是未分配的,如果一级页面的地址空间是未分配的,后面的次级页表肯定也都是空的。

多级页表

地址翻译

地址翻译的时候,CPU 中一个控制寄存器--页表基址寄存器(Page Table Base Register, PTBR)指向当前页表。虚拟地址被分为两部分:

  • 虚拟页面偏移:Virtual Page Offset,VPO,位数为:log2虚拟页大小log_2虚拟页大小
  • 虚拟页号:Virtual Page Number,VPN,虚拟页的唯一标识

MMU 使用 VPN 找到对应的 PTE,可以采取一些放置策略来加速查找的速度,例如让 VPN 0 对应 PTE 0,VPN 1 对应 PTE 1,以此类推。如果有效位是 1 ,将从 PTE 中获取的物理页号(Physical Page Number,PPN)与虚拟地址中的 VPO 拼合起来就可以得到需要的物理地址(因为物理页跟虚拟页的大小是一样的,所以物理页面偏移(Physical Page Offset,PPO)与 VPO 是一样的)。查找过程如下图所示。

地址翻译

TLB

CPU 运行是非常快的,如果每次 CPU 产生虚拟地址的时候,MMU 都需要从页表中进行查阅,最坏情况下会要求从内存中多取一次数据,代价是几十到几百个时钟周期。即使 PTE 被缓存在了高速缓存中,依然会有损耗。为了消除这样的损耗,在 MMU 中加入了一个 PTE 的小缓存,被称为翻译后备缓冲器(Translation Lookaside Buffer,TLB)。

TLB 是一个小的,虚拟寻址的缓存,每行保存着一个 PTE。为了快速在 TLB 中找到 PTE,虚拟地址被划分为如下形式,跟之前讲解的缓存划分一样,只不过由于 TLB 每行只有一条数据,所以块偏移是 0 位。

TLB

VPN 被划分成了如下两部分:

  • TLB索引:TLBI,位数为log2TLB组数log_2TLB组数,用于快速定位缓存组
  • TLB标记:TLBT,位数为:VPN位数-TLBI位数,在一个缓存组有多行时候,查询正确的 PTE

TLB查找

上图展示了利用 TLB 进行地址转换时候的 TLB 命中跟不命中情况。

  1. CPU 产生一个虚拟地址
  2. MMU 利用 VPN 从 TLB 中获取 PTE,命中的话就直接返回 PTE。否则就需要通过高速缓存/内存获取 PTE,并在 TLB 中进行缓存
  3. MMU 将虚拟地址翻译成为物理地址,并将其发送至高速缓存/内存
  4. 高速缓存/内存将所请求的数据返回给 CPU

虚拟地址作为内存包含工具

现代计算机必须为操作系统提供手段来控制内存的访问。

  • 不应该允许一个用户进程修改它的只读代码段
  • 不允许它读或者修改任何内核中的代码和数据
  • 不允许它读或者写其他进程的私有内存
  • 不允许它修改任何与其他进程共享的虚拟页面,除非所有共享者都显式的允许

通过在 PTE 上添加一些额外的许可位,来实现对一个虚拟页面的访问控制变的十分简单。如下图所示,可以添加三个许可位, SUP 位表示必须允许在超级用户模式下才能访问该页,READ 和 WRITE 位控制对页的读、写权限。

权限控制

例如,进程 i 允许在用户模式下的话,那么它有读 VP0 和读写 VP1 的权限,但是不允许访问 VP2。

在多级页表的情况下,父页表跟底层页表的标识也是不同的,如下图给出了一个实际系统中的页表条目格式。

第一、第二、第三级页表条目格式

第四级页表条目格式

实例分析

Core i7 cpu 封装

上图是 Intel Core i7(Haswall)的 CPU 封装,用于内存访问的 Core i7 的核心参数如下:

  • L1 d-TLB 为 64 个条目,4路,所以有 16 组
  • 64 byte CacheLine,4KB PageSize
  • L1 d-cache 为 32KB,8路,所以有 64 组
  • 支持 48 位虚拟地址,52 位物理地址
  • 4 级页表

内存翻译访问过程

上图是 Core i7 结合了虚拟地址、高速缓存的实际内存访问过程实例。

  1. 48 位虚拟地址被划分为 12 位 VPO(PageSize 为 4KB)及 36 位 VPN
  2. 进行虚拟地址翻译,首先查找 TLB 中是否存在缓存 a. VPN 根据 TLB 缓存情况被划分为 4 位 TLBI(16组)及 32 位 TLBT,首先使用 TLBI 定位组,再使用 TLBT 逐行匹配,图中 1 行 4 块其实是代表的 1 组 4 路 b. 如果命中 TLB 缓存,从获取的 PTE 中取出 PPN 与 VPO 组成物理地址 c. 如果未命中 TLB 缓存,需要从页表进行查询,36 位 VPN 被按照 9 位一组划分成了 4 级页表,查询到 PTE 后,取出 PPN 与 VPO 组成物理地址,同时也会缓存 PTE 到 TLB 中
  3. 获取到物理地址后,首先查询 L1 缓存中是否有缓存 a. 物理地址被划分为 3 部分,6位 CO(CacheLine 为 64 byte),6位 CI(L1 缓存为 64组)及 40 位 CT。首先使用 CI 定位缓存组,再使用 CT 逐行匹配 b. 如果命中 L1 缓存,取出缓存行后使用 CO 定位数据位置,获取数据内容 c. 如果未命中 L1 缓存,则按照既定规则,查询 L2、L3、主存,直至获取到数据内容

Linux 虚拟内存系统

这块看了好几遍其实还是比较迷糊,先记录下来吧,也希望有比较明白的同学能留言给指教。

Linux 为每个进程维护了一个单独的虚拟地址空间,如图所示

Linux虚拟内存系统

内核虚拟内存包含内核中的代码和数据结构,内核虚拟内存的某些区域被映射到所有进程共享的物理页面(这样就不需每个进程将运行所必须的内核代码都加载一遍)。Linux 也将一组连续的虚拟页面(大小等于系统 DRAM 的总量)映射到一组连续的物理页面,这样为内核提供一种可以便利的方法来访问物理内存中的任何特定的位置。

内核虚拟内存的其他区域包含每个进程都不相同的数据,比如,页表、内核在进程的上下文执行代码是使用的栈,以及记录虚拟地址空间当前组织的各种数据结构。

深入理解计算机系统

2015 CMU 15-213 CSAPP 深入理解计