Lec 18 虚拟内存
探讨一下另一大块关于硬件支持下,能让操作系统运行更快的方法——虚拟内存。
总览
- 回顾
- 虚拟内存概述
- 页面置换策略
- 页面置换过程
- MMU工作原理
- 上下文切换
- 虚拟地址空间组织
- MMU实现细节
回顾
操作系统的三大目标
保护和私有。进程之间相互访问到对方的数据,或者做一些硬件资源有害的事。
- 保护和私有。 每个进程都有独立的私有空间,其他正在运行的进程无法访问。
抽象。 隐藏硬件细节。比如进程可以通过open和访问文件不需要发送原始命令给到硬盘
资源管理。控制进程如何共享资源(CPU,MEM,DISK)
共享“CPU":
用户态+管理员态
通过陷入安全地进入supervisor态
共享”内存“ ——今天的主题
IMPORTANT
虚拟内存主要围绕让每个进程都有一个很大的,私有的,独享的存储空间。
内存层次结构

当前内存技术中的一个基本权衡:随着内存容量的增加,访问时间也会增加。要构建一个容量大且平均访问时间小的内存系统,需要一些架构上的巧妙设计。这种巧妙设计体现在缓存中,它是一个位于CPU和主内存之间的硬件子系统。
硬件缓存

直接映射(Direct-Mapped)缓存:每个内存块只能映射到缓存中的一个特定位置,这样会导致不同内存块争夺同一个缓存位置的问题。
全相联(Fully Associative)缓存:任何内存块可以放置在缓存中的任何位置,这样提高了缓存的灵活性,但增加了查找的复杂性。
组相联(Set-Associative)缓存:结合了上述两种方式。缓存被划分为若干组,每个内存块可以放在某一组中的任意位置。通常,组相联缓存比直接映射缓存更能有效减少缓存位置争夺的问题。
Cache的内容由硬件自动管理。Cache之所以有效,是因为局部性原理:如果CPU在时间T访问了位置X,那么它很可能在不久的将来访问附近的位置。缓存的组织方式使得附近的位置能够同时存在于缓存中。
为了增加请求地址存在于缓存中的概率,我们引入了”associativity(关联性)"的概念,增加了每次访问时检查缓存位置的数量,解决了指令和数据争夺相同缓存位置的问题。
我们还讨论了块大小(缓存行中的字数)、替换策略(如何选择在缓存未命中时重新使用哪个缓存行)和写策略(决定何时将更改的数据写回主内存)的适当选择。
我们从未讨论过主内存中的数据来自何处以及如何管理填充主内存的过程。这是今天讲座的主题。
虚拟内存概述
我们将内存用作是磁盘的缓存,与硬件缓存有两个区别
- 好消息是,二级存储的容量巨大
- 坏消息是,磁盘访问时间比主存慢100,000倍。
在讨论DRAM时,我们发现检索一个连续块的额外访问时间比第一个字的访问时间要小,因此假设我们最终会访问附加的字,获取一个块是一个正确的选择。对于磁盘而言,更是如此。并且, 也可知道,未命中的惩罚代价是很高的
鉴于我们需要
- 高关联性High Associativity,这意味着在主内存中定位和管理来自磁盘的数据时,要尽可能灵活,以便减少数据块间的冲突。
- 程序的工作集(程序在运行时需要访问的所有数据)能够完全适应在主存中。即,虚拟内存系统应能够合理地管理内存,以避免将不必要的数据从磁盘加载到内存中,从而避免频繁的未命中和磁盘访问。
- 使用较大的块大小,,一旦从磁盘读取了一个数据块,读取这个块中的其他字(即数据单元)通常成本很低, 又因为局部性原理,我们预计会访问块中的其他字,从而将
的成本分摊到许多未来的 - Write-back策略,在主内存中更改的数据需要被其他二级存储块的数据替换时,我们才会更新磁盘上的内容。
未命中的长延迟还有一个好处。我们可以在软件中管理主内存的组织和对二级存储的访问。即使处理未命中的后果需要成千上万条指令,相比于磁盘的访问时间,这些指令的执行速度非常快。因此,我们的策略是让硬件处理命中,让软件处理未命中。这将导致简单的内存管理硬件和在软件中实现非常巧妙策略的可能性,以应对未命中的处理
工作原理
下面虚拟内存系统的工作原理。CPU生成的内存地址是虚拟地址,主内存使用的是物理地址,CPU和主存之间,有个硬件叫内存管理单元(MMU),它的作用是将虚拟地址转换为物理地址。
等等! 在CPU和主内存之间不是缓存吗?
但现在,我们假设只有MMU而没有缓存。
MMU硬件通过简单的表查找将虚拟地址转换为物理地址。这个表称为页表(page table)。从概念上讲,MMU使用虚拟地址作为索引来选择表中的一个条目,该条目告诉我们相应的物理地址。这个表允许将一个虚拟地址可以被映射到主内存的任何位置。在正常操作中,我们希望确保两个虚拟地址不会映射到同一个物理地址。但如果某些虚拟地址没有对应的物理地址的翻译,这也是可以的。这将表示请求的虚拟地址的内容尚未加载到主内存中,因此MMU将向CPU发出内存管理异常信号,然后OS可以分配到主内存中的一个位置,并执行所需的I/O操作,将数据从二级存储加载到主内存中的指定位置。
页表是操作系统决定的还是CPU上的MMU决定的?
页表是操作系统管理的,硬件MMU只是执行这个转换。因此最终是由OS决定的
页表使得系统能控制程序运行时如何访问物理内存。例如,当我们在切换程序时更改页映射,能够快速连续运行多个程序(分时调度)。通过正确的管理页表,使得一个程序可以访问主存的某位置但是对于另外一个程序却不可访问。我们还可以使用内存管理异常在需要时将程序内容加载到主内存中,而不是在执行开始之前加载整个程序。
虚拟内存的分页实现

我们将虚拟和物理地址空间划分为固定大小的块,称为页(page)。页大小总是2的幂字节,例如
我们会将一个进程所需要的页,映射到物理内存的不同区域,但是不要求相邻。为了能找到一个进程的页所在的物理地址,我们需要引入页表(page table),它使用低位(lower-order bits)作为页内偏移量(offset),高位(higher-order bits)来确定具体是哪一页。每个虚拟地址现在被解释为一个有序对{页号,偏移量},页表告诉我们每个虚拟页号对应的物理页号以及相应的起始物理地址
一个典型的页大小为4KB到16KB,对应p=12和p=14。例如,假设p=12。如果CPU生成一个32位的虚拟地址,虚拟地址的低12位是页偏移量,高20位是虚拟页号,类似地,物理地址的低p位是页偏移量,其余的物理地址位是物理页号。
IMPORTANT
MMU(内存管理单元)将管理页面,而不是单个位置。
我们将整个页面从二级存储中移动到主内存中。根据局部性原则,如果一个程序访问了页面上的一个位置,我们预计它很快会访问页面上的其他相邻位置。
MMU 将虚拟页码映射到物理页码。它通过使用虚拟页码(VPN)作为索引来访问页表。页表中的每个条目指示页面是否驻留在主内存中,如果是,它会提供适当的物理页码(PPN)。PPN 与页面偏移量结合形成主内存的物理地址。
如果请求的虚拟页面不在主内存中,MMU 会向 CPU 发送一个内存管理异常,称为页面错误(page fault),以便 CPU 能够从二级存储中加载适当的页面,并在 MMU 中设置适当的映射。
我们将主内存作为page cache,这种方法称为“分页”或有时称为“按需分页”,因为页面的移动是根据程序的需求来决定的。
分页机制梳理
最初,程序的所有虚拟页面都驻留在二级存储中,MMU 是空的,即没有页面驻留在物理内存中。
CPU 开始运行程序,程序生成的每个虚拟地址,无论是用于指令提取还是数据访问,都传递给 MMU 以映射到主内存中的物理地址。
- 如果虚拟地址已驻留在物理内存中,主内存硬件可以完成访问。
如果虚拟地址未驻留在物理内存中,MMU 会发出页面错误异常。
切换到页面错误处理程序
- 处理程序分配一个物理页面来存放请求的虚拟页面,并将虚拟页面从二级存储中加载到主内存中。然后,
- 它调整请求的虚拟页面的页映射条目,标记它现在已驻留
- 并指示为新分配和初始化的物理页面提供物理页面编号。
第四步中,尝试分配物理页面时,处理程序可能会发现所有物理页面当前都在使用中。在这种情况下,它选择一个现有的页面进行替换,例如一个最近没有被访问的驻留虚拟页面。它将选择的虚拟页面的内容交换到二级存储中,并更新被替换虚拟页面的页映射条目,以指示它不再驻留。现在有一个空闲的物理页面可以重新使用,以存放缺失的虚拟页面的内容。
程序的工作集,即程序当前正在访问的页面集,通过一系列页面错误加载到主内存中。程序开始运行时,页面错误会频繁发生,之后工作集变化缓慢,因此页面错误的频率会显著降低,如果程序较小且表现良好,页面错误的频率可能接近零。也有可能编写不断生成页面错误的程序,这种现象称为“抖动”(thrashing)。由于二级存储的访问时间较长,抖动的程序运行得非常缓慢,通常慢到用户放弃并重写程序以更合理地运行。(也就是说,代码需要性能优化了)
一种简单页表的设计
页表的设计非常简单。每个虚拟页面在页表中都有一个条目。例如,如果 CPU 生成的是一个 32 位的虚拟地址,且页面大小为 2¹² 字节,那么虚拟页码就有 32−12=20 位,页表将包含
页表中的每个条目包含一个“驻留位”(R),当虚拟页面驻留在物理内存中时,该位被设置为 1。如果 R 为 0,则访问该虚拟页面会导致页面错误。如果 R 为 1,又因为条目还包含物理页码(PPN),它指示虚拟页面在主内存中的具体位置。
还有一个附加的状态位,称为“脏位”(D)。当页面刚从二级存储中加载时,它是“干净”的,即物理内存的内容与二级存储中的页面内容相同。因此,D 位设置为 0。如果随后 CPU 对该页面中的某个位置进行了存储操作,页面的 D 位将被设置为 1,表示页面是“脏的”,即内存的内容现在与二级存储的内容不同。如果一个脏页面被选中进行替换,则在页面被重用之前,必须将其内容写入二级存储以保存更改。
一些 MMU 在每个页表条目中还有其他状态位。例如,可能有一个“只读”位,当该位被设置时,如果程序试图向该页面存储数据,将产生异常。这对于保护代码页面不被错误的数据访问破坏非常有用,也是一种非常实用的调试功能。
NOTE
扩展知识: 磁盘上的一块区域叫交换空间(Swap Space),用于备份DRAM中的数据。当内存不够用时,操作系统可以将不常用的数据从内存移到此处,主要是为了扩展内存。
示例: 从虚拟地址到物理地址转换
下面出一道题:
基本设置:
- 页大小
字节 - 16个虚拟页,8个物理页
- 意味着,虚拟地址4+8=12位;物理地址11位
执行指令LD(R31, 0x2C8, R0)时,VA = 0x2C8, 那么PA = ?
Solution:我们给定了虚拟地址0x2C8,可知我们的VPN=2,接着我们就去页表中找VPN=2,通过查找到(D,R,PPN) = (0, 1, 4),查看索引为 2 的页表条目,我们看到 R 位为 1,表示虚拟页面 2 驻留在物理内存中。条目的 PPN 字段告诉我们虚拟页面 2 位于物理页面 4,将 PPN 和 8 位偏移量结合起来得到0x4C8
页错误
当 CPU 访问一个不驻留的虚拟页面时会发生什么,也就是说,该页面的驻留位被设置为 0。
在这种情况下,MMU 会发出页面错误异常,导致 CPU 暂停程序执行并切换到页面错误处理程序。处理程序首先找到一个未使用的物理页面,或者如果有必要的话,通过选择一个正在使用的页面并将其腾空。
我们可以选择哪个页面?当然,有一些限制。显然,我们不能选择存放页面错误处理程序代码的页面。免于被选择的页面称为“锁定(wired)”页面。最优策略是选择下次使用时间最远的页面。但是,这显然需要了解未来的执行路径,因此这并不是一个可行的策略。不管用什么策略进行替换。
最后,处理程序完成,原始程序的执行恢复,重新执行导致页面错误的指令。由于页映射表已更新,这次访问成功,执行得以继续。
示例: 页错误
下面出一道题:
基本设置:
- 页大小
字节 - 16个虚拟页,8个物理页
- LRU 页: VPN = 0xE
1,执行指令,ST(BP, -4, SP) -- 其中SP = 0x604, VA = 0x600, 那么PA = ?
2, 请在图中标注执行完后的映射关系,以及更正页表项
Solution: 一个存储指令,它正在访问虚拟地址 0x600,该地址位于虚拟页面 6 上,检查虚拟页面号(VPN)6 的页表条目,我们看到它的 R 位为 0,表明它没有驻留在主存中,这导致了页面错误异常。页面错误处理程序选择 VPN 0xE 进行替换,因为我们在设置中已经得知它是最近最少使用(LRU)的页面。接下来,所需的虚拟页面会从二级存储读取到选定的物理页面中,并且更新页表条目, 标记虚拟页号 0x6 已经驻留在物理页号 0x5 中。最后,处理程序完成,原始程序的执行恢复,重新执行导致页面错误的指令。由于页映射表已更新,这次访问成功,执行得以继续。

程序员角度看虚拟内存
我们可以将内存管理单元 (MMU) 的工作分为两项任务,作为计算机科学家,可以将其视为两个过程。在这个框架中,页映射信息保存在几个数组中:R 数组保存驻留位,D 数组保存脏位,PPN 数组保存物理页号,DiskAdr 数组保存每个虚拟页在二级存储中的位置。
// 硬件执行
int VtoP(int Vaddr) {
int VpageNo = Vaddr >> p;
int P0 = Vaddr & ((1 << p) - 1); // 获取页内偏移
if(R[VPageNo] == 0)
PageFault(VpageNo);
return (PPN(VPageNo) << p) | P0; // 将物理页号移到高位 然后与页内偏移 与,就得到了最终的物理地址
}
// 下面软件执行
void PageFault(int VpageNo) {
int i;
i = SelectLRUpage();
if (D[i] == 1) {
WritePage(DiskAdr[i], PPN[i]);
}
R[i] = 0;
PPN[VPageNo] = PPN[i];
ReadPage(DiskAddr[VPageNo], PPN[i]);
R[VPageNo] = 1;
D[VPageNo] = 0;
}VtoP 过程在每次内存访问时被调用,用于将虚拟地址转换为物理地址。如果请求的虚拟页不在内存中,则会调用 PageFault 过程,使该页驻留。一旦请求的页驻留,VPN(虚拟页号)就会作为索引用于查找相应的 PPN(物理页号),然后将其与页偏移量连接起来,形成物理地址。
PageFault 过程首先选择一个虚拟页进行替换,如果该页是脏的,则会将其内容写回。然后将选定的页标记为不驻留。
最后,从二级存储读取所需的虚拟页,并更新页表信息,反映该页现在已经驻留在新分配的物理页中。
软硬件平衡
我们将使用硬件来实现VtoP(虚拟地址到物理地址)的功能,因为每次内存访问时都需要进行这个转换。调用PageFault(页面错误)过程是通过页面错误异常来完成的,这会指示CPU执行相应的处理程序软件,其中包含PageFault过程。
这种策略值得在所有实现选择中推广:对于需要快速执行的操作,使用硬件来处理,而通过异常处理(希望是很少发生的)异常情况则由软件处理。由于软件是由CPU执行的,而CPU本身也是一块硬件,本质上我们是在做出使用专用硬件(如MMU)和使用通用硬件(如CPU)之间的权衡
简而言之,我们不要轻易使用专用硬件,除非操作的频率非常高且直接影响系统性能。
IMPORTANT
虚拟内存系统的架构由三个参数决定,因此也决定了MMU的架构。
- P 表示虚拟和物理地址中用于页偏移的地址位数。
- V 表示虚拟页号所占的地址位数。
- M 表示物理页号所占的地址位数。
虚拟地址的大小由指令集架构(ISA)决定。虚拟地址空间不足会限制系统能够处理的数据量和内存地址范围,这也是很多ISA消亡的主要原因
为什么典型的页大小是4KB到16KB?
Sol:这是内存存储不需要的位置的缺点和从二级存储中读取尽可能多数据优点之间的”权衡“, 将初始访问的word的高成本分摊到尽可能多的word上。
小虚拟地址的限制是许多ISA消亡的主要原因。当然,每代工程师都认为他们的过渡将是最终的!我记得我们曾经认为32位是一个无法想象的巨大地址空间。那时我们还在按兆字节购买内存,在我们的幻想中我们才会认为一个系统可以拥有数千兆字节的内存。今天的CPU架构师们对64位感到相当自豪——让我们看看他们在几十年后会有何感想!
目前物理地址的大小介于30位(嵌入式处理器,内存需求较小)到40多位(处理大数据集的服务器)之间。由于CPU实现每隔几年就会更新,物理内存大小的选择可以根据当前技术进行调整。由于程序员使用的是虚拟地址,他们与这种实现选择隔离开来。MMU确保现有软件能够在不同大小的物理内存下继续正常工作。程序员可能会注意到性能上的差异,但不会在基本功能上发现变化。
RAM驻留页表的缺点
但是为什么要使用专用的内存来存储页表,而不是使用已经存在且已经购买的主内存的一部分呢?
我们可以使用一个寄存器,称为页表指针,来保存主内存中页表数组的地址。换句话说,页表将占用一些专用的物理页面。通过将虚拟页号用作索引,硬件可以执行通常的数组访问计算,从主内存中提取所需的页表项。
这种提议的缺点是,执行一次虚拟访问现在需要两个物理内存访问:第一个访问是为了检索虚拟到物理地址转换所需的页表项,第二个访问是为了实际访问请求的内存位置。
快表TLB
大多数系统都包含一种专用的缓存,称为转换后备缓冲器(translation look-aside buffer, TLB),用于将虚拟页号映射到物理页号。TLB 通常很小且速度很快。为了确保最佳的命中率,TLB 通常是全相联的,以避免冲突。如果在 TLB 中找到了物理页号,则可以避免对主内存的页表项的访问,这样每次虚拟访问就可以恢复为单次物理访问。
TLB 的命中率相当高,通常超过 99%。这并不令人惊讶,因为局部性和工作集的概念表明,在短时间内只有少量页面处于活动使用状态。

综合来看:CPU 生成的虚拟地址首先由 TLB 处理,以查看是否缓存了从虚拟页号(VPN)到物理页号(PPN)的适当翻译。如果缓存中有,则可以直接进行主内存访问。
如果所需的映射不在 TLB 中,则会访问主内存中的相应页表项。如果页面在内存中,页表项的 PPN 字段将用于完成地址翻译。翻译结果当然会被缓存到 TLB 中,以便后续访问此页面时可以避免对页表的访问。
如果所需的页面不在内存中,MMU 会触发页面错误异常,然后页面错误处理程序代码将处理这个问题。

示例:快表
假设:
提问:
- 物理内存中可以同时存在多少个页面?
- 页表中有多少条目?
- 每个页表条目多少位(假如每个都有PPN,驻留位,脏位)?
- 页表占用多少页面?
- 给定任何时间,虚拟内存有多少比例可以在内存驻留?
- 虚拟地址0x1804的物理地址是什么? 涉及到哪些MMU组件进行翻译?
- 虚拟地址0x1080呢?
- 虚拟地址0x0FC呢?
1、2^24 / 2^10 个 2、 2^32 / 2^10个 3、24+2=26bit 4、 (2^32 / 2^10 * 26) / (8 *2^10) 页 5、页表条目/虚拟页面数量 = 2^14/2^22 6、将虚拟地址分解为 VPN 和偏移量,分别是0x6和0x004,首先在TLB找,发现VPN-to-PPN的映射被缓存了,将PPN(0x2)和10位偏移连接得到0x804
7,同上题,虚拟内存0x1080呢?对于这个地址,VPN 是 0x4,偏移量是 0x80。VPN 0x4 的翻译没有缓存到 TLB 中,因此我们需要检查页表,这告诉我们该页面驻留在物理页面 5。通过将 PPN 和偏移量连接,得到物理地址 0x1480 8、同上题,虚拟内存0x0FC呢?在这个地址中,VPN 是 0,偏移量是 0xFC。VPN 0 的映射没有在 TLB 中找到,检查页表显示 VPN 0 不在主内存中驻留,因此触发了页面错误异常。
上下文切换
页表提供了解释虚拟地址的上下文context:即它虚拟地址在主内存或辅助存储中位置所需的信息。
多个程序可以同时加载到主内存中,每个程序都有自己的上下文。需要注意的是,分开的上下文确保程序之间不会相互干扰。例如,一个程序中虚拟地址 0 的物理位置与另一个程序中虚拟地址 0 的物理位置会不同。每个程序在自己的虚拟地址空间中独立运行。正是页表提供的上下文使得它们可以共存并共享同一块物理内存。因此,在切换程序时我们需要切换上下文。这是通过重新加载Reload页表来实现的。

在时间共享系统中,CPU 会定期从运行一个程序切换到另一个程序,给人一种多个程序各自运行在自己的虚拟机上的错觉。这通过在切换到下一个程序时切换上下文来实现。
有一段特权代码称为操作系统(OS),它负责管理一个物理处理器和主内存在多个程序之间的共享,每个程序都有自己的 CPU 状态和虚拟地址空间。操作系统实际上是在使用一组共享的物理资源创建多个虚拟机并协调它们的执行。
操作系统运行在一种特殊的上下文中,我们称之为内核。操作系统包含异常处理和支持分时调度的必要代码。由于它需要管理物理内存,因此它被允许访问任何物理位置以处理页面错误等问题。运行中的程序出现异常时,硬件会切换到内核上下文,这被称为进入“内核模式”。处理完异常后,程序的执行会恢复到所谓的“用户模式”。
由于操作系统在内核模式下运行,它可以访问许多在用户模式下无法访问的硬件寄存器。这些包括 MMU 状态、I/O 设备等。需要访问磁盘等硬件的用户模式程序需要向操作系统内核发出请求,以执行操作,从而使操作系统有机会检查请求的适当权限等。
内存管理& 保护

用户模式程序(也称为应用程序)在编写时,假设它们可以访问整个虚拟地址空间。它们通常遵循相同的约定,如程序中第一条指令的地址、栈指针的初始值等。由于所有这些虚拟地址都是使用当前上下文进行解释的,通过控制上下文,操作系统可以确保程序能够无冲突地共存。
上图的图示展示了一种标准的组织应用程序虚拟地址空间的计划。通常,第一个虚拟页会被设置为不可访问,这有助于捕捉涉及引用初始化(即零值)指针的错误。接下来是一些只读页,这些页保存了应用程序的代码以及它所使用的任何共享库的代码。将代码页标记为只读可以避免难以发现的错误,这些错误可能是由于错误的数据访问无意中更改了程序!然后是读写页,这些页保存了应用程序的静态分配数据结构。
虚拟地址空间的其余部分被划分为两个可以随着时间增长的数据区域。第一个是应用程序的栈,用于保存过程调用记录。这里我们将栈放在虚拟地址空间的较低端,因为我们的约定是栈向更高的地址增长。
另一个可增长的区域是堆,用于动态分配长寿命数据结构的存储。“动态”意味着对象的分配和释放是通过显式的过程调用来完成的,而不是在应用程序运行时预先知道哪些对象会被创建。正如这里所示,随着堆的扩展,它向较低地址增长。
页面错误handler知道在这些区域增长时分配新页面。当然,如果它们在中间某个地方相遇并且需要更多空间,应用程序将没有运气——它已经用尽了虚拟内存!
多级页表
我们可以对MMU实现进行一些调整,以提高效率或功能。
在我们简单的页表实现中,完整的页表占用了一些物理页面。使用这里显示的数字,如果每个页表项占用一个主存字,我们需要
这里展示的MMU实现使用了分层页表。虚拟地址的前10位用于访问一个“页目录”,该目录指示了保存该虚拟地址空间段页表的物理页。关键思想是页表段存储在虚拟内存中,即它们不需要在任何给定时间都驻留在内存中。如果运行的应用程序只活跃地使用了其虚拟地址空间的一小部分,我们可能只需要少量页面来保存页目录和必要的页表段。当有多个应用程序,每个都有自己的上下文时,这种节省真的会累积起来。
在这个例子中,注意到页目录中的中间条目,即堆栈和堆之间尚未分配的虚拟内存对应的条目,都标记为“未驻留”。因此,不需要为持有标记为“未驻留”的大量页表项而分配资源。
现在访问页表需要两次访问主存(首先访问页目录,然后访问页表的适当段),但TLB使得这额外的访问几乎没有影响。
在更改上下文时,操作系统通常会重新加载页表指针,以指向适当的页表(或如果我们采用之前幻灯片中的方案,则指向页目录)。由于这种上下文切换实际上更改了页表中的所有条目,操作系统还必须使TLB缓存中的所有条目失效。这自然会对TLB命中率产生巨大影响,平均内存访问时间会大幅增加,因为所有页表访问现在都是必需的,直到TLB被重新填充。
为了减少上下文切换的影响,一些MMU包含一个上下文编号寄存器,其内容与虚拟页号连接在一起,形成对TLB的查询。这本质上意味着TLB缓存条目的标签字段将扩展以包括在填充TLB条目时提供的上下文编号。
切换上下文时,操作系统现在需要重新加载上下文编号寄存器和页表指针。由于有了新的上下文编号,其他上下文中的TLB条目将不再匹配,因此在上下文切换时无需刷新TLB。如果TLB具有足够的容量来缓存多个上下文的VPN到PPN的映射,则上下文切换将不会对平均内存访问时间产生实质性的影响。
最后,让我们回到如何将缓存和MMU结合到我们的内存系统中去的问题。
第一种选择是将缓存放在CPU和MMU之间,即缓存将处理虚拟地址。这看起来不错:VPN到PPN的转换只在缓存未命中时发生。困难在于上下文切换时,会改变虚拟内存的有效内容。毕竟,上下文切换的目的就是切换执行到另一个程序。这意味着操作系统在执行上下文切换时必须使缓存中的所有条目失效,这会导致缓存未命中率相当高,直到缓存被重新填充。因此,上下文切换的性能影响会非常高。
我们可以通过缓存物理地址来解决这个问题,即将缓存放在MMU和主内存之间。这样缓存的内容不会受到上下文切换的影响——请求的物理地址会有所不同,但缓存会随之处理。这个方法的缺点是,在开始缓存访问之前,我们必须承担MMU翻译的成本,这会稍微增加平均内存访问时间。
优化点: MMU和Cache并行工作

我们巧妙地设计,我们不必等待MMU完成后再开始访问缓存。一开始,缓存需要虚拟地址中的行号来提取适当的缓存行。如果用于行号的地址位完全包含在虚拟地址的页偏移中,这些位不会受到MMU翻译的影响,因此缓存查找可以与MMU操作并行进行。
一旦缓存查找完成,缓存行的标签字段可以与MMU生成的物理地址的适当位进行比较。如果MMU中发生了TLB命中,物理地址应该会在与缓存查找产生的标签字段大约相同的时间内可用。
通过并行执行MMU翻译和缓存查找,通常不会对平均内存访问时间产生影响!这样就可以实现两全其美:一个物理地址缓存,不需要为MMU翻译付出时间代价。
最后一个细节:增加缓存容量的一种方法是增加缓存行的数量,从而增加用作行号的地址位数。由于我们希望行号适配到虚拟地址的页偏移字段中,因此缓存行的数量是有限的。相同的论点适用于增加块大小。因此,要增加缓存容量,我们唯一的选择是增加缓存的关联度,这样可以在不影响用于行号的地址位的情况下增加容量。
这就是我们对虚拟内存讨论的全部内容。我们使用MMU提供虚拟地址到物理地址的映射上下文。通过切换上下文,我们可以创建多个虚拟地址空间的错觉,使许多程序可以共享单个CPU和物理内存而不互相干扰。
我们讨论了使用页表将虚拟页号翻译为物理页号。为了节省成本,我们将页表放在物理内存中,并使用TLB消除大多数虚拟内存访问的页表访问成本。访问非驻留页会导致页错误异常,允许操作系统管理许多应用程序之间公平共享物理内存的复杂性。
我们看到,提供上下文是创建虚拟机的第一步,这也是我们下一节讲座的主题。
虚拟内存的分段实现
在分段内存管理中,我们为每个进程分配一段特定的连续物理内存段。理论上,我们可以直接将物理地址暴露给程序使用,但这样会使编译过程变得非常繁琐(因为程序需要明确添加起始地址)。相比之下,让进程将起始地址视为地址0,并让操作系统灵活地移动我们的连续内存块会更为方便。
因此,我们采用了另一种方法:为每个进程分配一个基址寄存器(这是特权寄存器,只有操作系统可以控制)。实际使用的物理地址始终是虚拟地址加上段基址寄存器的值。这种方式使得进程可以以相对简单的方式处理地址,而操作系统负责将虚拟地址转换为物理地址,从而实现了地址空间的管理和保护。

这种技术被称为基址和界限(base-and-bound)地址转换,其中的“界限”部分涉及到限制内存分配的范围。因此,除了基址寄存器外,每个进程还需要一个界限寄存器(同样是特权寄存器)。操作系统通过检查虚拟地址是否在允许的范围内来确保内存访问的合法性。如果虚拟地址超出了允许的范围,就会触发一个界限违规异常(bound violation exception),因为程序试图访问其不应该访问的内存区域。

对于每个程序而言,我们会为数据和代码的分配分离,因此对于每个程序, 在内存中都有这么个数据段(包含了数据界限寄存器,数据基址寄存器),以及代码段(包含了代码界限寄存器,代码基址寄存器)。这种分离是有好处的,因为这样就不可能在执行期间意外更改我们的代码。并且多个程序可以共享一个代码段。
这种实现方式简单,但是很少用,因为他会有几个问题——最主要的就是内存碎片的问题。
下图说明了了不同的进程有有不同页表,黄色区域是OS,后面会讲。每个页表中的页(表项)表示一个物理内存的起始地址。页表的条目不需要是连续的。例如,进程1的页表条目可以分散在不同的物理内存位置

操作系统(OS)控制的页表基址寄存器(PT base register)指向当前进程的开始的页表。令VPN为虚拟页号。
如果我们想执行一次内存访问,让我们考虑一下该如何进行。我们有一个页表基址寄存器(PT base register),它告诉我们当前正在运行的进程的页表的起始位置。一旦我们完成地址转换并在页表中查找,我们将发现每个虚拟页号对应的物理页号,然后我们才能获取我们的数据。所以我们从一个虚拟页号(VPN)和偏移量(offset)拼接在一起的地址开始,我们实际上需要进行两步操作:
- 找到物理页号(PPN):物理页号位于内存地址
Mem[PT base + VPN],其中PT base的值是通过从内核页表中查找得来的。 - 计算物理地址:现在我们有了物理页号(PPN),我们可以通过计算
PPN + offset来找到物理地址。
然后,我们需要进行第二次DRAM内存访问以实际获取数据字,这意味着我们已经将内存访问延迟翻倍了(这是不好的)。

为了完成这些工作,我们还需要高级页表,叫系统页表,本质上包含了指向每个进程的开头页表的指针。所以每次我们切换进程时,我们还需要调整 PT base寄存器 的值。
当我们第一次访问某个内存地址时,需要将虚拟地址(VA)转换为物理地址(PA)。这个过程通常涉及查找页表。后续操作直接使用物理地址:一旦完成了首次转换,后续对同一地址的访问将直接使用已转换得到的物理地址,不再进行额外的地址转换。
综上,我们用符号表示如下
进行地址转换时
- PPN = Mem[PT Base Reg + VPN]
- PA =
进程切换时
- PT Base Reg := System PT Base + new process ID
IMPORTANT
Page fault 会带来数百万时钟周期的开销
总结
整个地址转换过程

目标#1: 大规模利用局部性
- 程序员希望一个大的,扁平的地址空间,但是只使用其中的一小部分
- 解决方法: 将工作集从磁盘缓存到RAM中
- 基本实现: 使用单级页表的MMU
- 通过快速硬件路径访问已加载的页面
- 按需加载虚拟内存: 页错误
- 几个优化措施
- 为了节省成本,将页表移动到RAM中
- 使用快表来重新获得高性能
- 缓存/虚拟内存交互: 可以缓存物理或者虚拟地址
目标2 & 3: 简化内存管理,保护多个上下文之间互不干扰

