type
status
date
slug
summary
tags
category
icon
password
Property
Mar 4, 2023 02:38 AM
使用分页作为核心机制来实现虚拟内存,可能会带来较高的性能开销。因为要使用分页,就要将内存地址空间切分成大量固定大小的单元(页),并且需要记录这些单元的地址映射信息。因为这些映射信息一般存储在物理内存中,所以在转换虚拟地址时,分页逻辑上需要一次额外的内存访问。每次指令获取、显式加载或保存,都要额外读一次内存以得到转换信息,这慢得无法接受。因此我们面临如下问题:
关键问题:如何加速地址转换 如何才能加速虚拟地址转换,尽量避免额外的内存访问?需要什么样的硬件支持?操作系统该如何支持?
想让某些东西更快,操作系统通常需要一些帮助。帮助常常来自操作系统的老朋友:硬件。我们要增加所谓的地址转换旁路缓冲存储器(translation-lookasidebuffer,TLB[CG68,C95]),它就是频繁发生的虚拟到物理地址转换的硬件缓存(cache)。因此,更好的名称应该是地址转换缓存(address-translation cache)。对每次内存访问,硬件先检查 TLB,看看其中是否有期望的转换映射,如果有,就完成转换(很快),不用访问页表(其中有全部的转换映射)。TLB 带来了巨大的性能提升,实际上,因此它使得虚拟内存成为可能。
TLB 的基本算法
硬件算法的大体流程如下:
首先从虚拟地址中提取页号(VPN),然后检查 TLB 是否有该 VPN 的转换映射(第 2 行)。
如果有,我们有了 TLB 命中(TLB hit),这意味着 TLB 有该页的转换映射。接下来我们就可以从相关的 TLB 项中取出页帧号(PFN),与原来虚拟地址中的偏移量组合形成期望的物理地址(PA),并访问内存,假定保护检查没有失败。
如果 CPU 没有在 TLB 中找到转换映射(TLB 未命中),在本例中,硬件访问页表来寻找转换映射,并用该转换映射更新 TLB,假设该虚拟地址有效,而且我们有相关的访问权限(第 13、15 行)。上述系列操作开销较大,主要是因为访问页表需要额外的内存引用(第 12 行)。最后,当 TLB 更新成功后,系统会重新尝试该指令,这时 TLB 中有了这个转换映射,内存引用得到很快处理。
示例:访问数组
现在考虑一个简单的循环,访问数组中的每个元素,类似下面的 C 程序:
访问 a[1]后,TLB 有了 VPN=06 的映射,下次访问 a[2]或 a[0]也能 TLB 命中.
硬件缓存背后的思想是利用指令和数据引用的局部性(locality)。通常有两种局部性:时间局部性(temporal locality)和空间局部性(spatial locality)。时间局部性是指,最近访问过的指令或数据项可能很快会再次访问。空间局部性是指,当程序访问内存地址 x 时,可能很快会访问邻近 x 的内存。
谁来处理 TLB 未命中
- 硬件:
发生未命中时, 硬件会“遍历”页表,找到正确的页表项,取出想要的转换映射,用它更新 TLB,并重试该指令
一个例子是 x86 架构,它采用固定的多级页表(multi-level page table)
- 软件:
精简指令集计算机上。
发生 TLB 未命中时,硬件系统会抛出一个
异常
(见图 19.3 第 11 行),这会暂停当前的指令流,将特权级提升至内核模式
,跳转至陷阱处理程序
(trap handler)。接下来你可能已经猜到了,这个陷阱处理程序是操作系统的一段代码,用于处理 TLB 未命中
。这段代码在运行时,会查找页表中的转换映射,然后用特别的“特权”指令更新 TLB,并从陷阱返回。此时,硬件会重试该指令(导致 TLB 命中)。可能导致
无限递归
,比如陷阱处理程序
中也有未命中
的。可以把 TLB 未命中陷阱处理程序
直接放到物理内存中 [它们没有映射过(unmapped),不用经过地址转换]。或者在 TLB 中保留一些项,记录永久有效
的地址转换
,并将其中一些永久地址转换槽块留给处理代码本身,这些被监听的(wired)地址转换总是会命中 TLB。TLB的内容
VPN | PFN | 其他位
VPN
和PFN
同时存在于TLB
中,因为一条地址映射
可能出现在任意位置
(用硬件的术语,TLB 被称为全相联
的(fully-associative)缓存)。硬件并行
地查找
这些项,看看是否有匹配。补充:TLB 的有效位!=页表的有效位
常见的错误是混淆 TLB 的有效位和页表的有效位。在页表中,如果一个页表项(PTE)被标记为无效,就意味着该页并没有被进程申请使用,正常运行的程序不应该访问该地址。当程序试图访问这样的页时,就会陷入操作系统,操作系统会杀掉该进程。
TLB 的有效位不同,只是指出 TLB 项是不是有效的地址映射。例如,系统启动时,所有的 TLB 项
通常被初始化为无效状态,因为还没有地址转换映射被缓存在这里。一旦启用虚拟内存,当程序开始运行,访问自己的虚拟地址,TLB 就会慢慢地被填满,因此有效的项很快会充满 TLB。
TLB 有效位在系统上下文切换时起到了很重要的作用,后面我们会进一步讨论。通过将所有 TLB项设置为无效,系统可以确保将要运行的进程不会错误地使用前一个进程的虚拟到物理地址转换映射。
其他位:TLB 通常有一个
有效(valid)位
,用来标识该项是不是有效地转换映射。通常还有一些保护(protection)位
,用来标识该页是否有访问权限
。例如,代码页被标识为可读和可执行,而堆的页被标识为可读和可写。还有其他一些位,包括地址空间标识符(address-space identifier)
、脏位(dirty bit)
等。上下文切换时对 TLB 的处理
TLB 中包含的虚拟到物理的地址映射只对当前进程有效,对其他进程是没有意义的。所以在发生进程切换时,硬件或操作系统(或二者)必须注意确保即将运行的进程不要误读了之前进程的地址映射。
这个问题有一些可能的解决方案。一种方法是在上下文切换时,简单地清空(flush)TLB,这样在新进程运行前 TLB 就变成了空的。
如果是软件管理 TLB 的系统,可以在发生上下文切换时,通过一条
显式(特权)指令
来完成。如果是硬件管理 TLB,则可以在
页表基址寄存器
内容发生变化时清空 TLB(注意,在上下文切换时,操作系统必须改变页表基址寄存器(PTBR)的值)。不论哪种情况,清空操作都是把全部
有效位
(valid)置为 0,本质上清空了 TLB。
每次进程运行,当它访问数据和代码页时,都会触发 TLB 未命中,有一定开销。如果操作系统频繁地切换进程,这种开销会很高。
为了减少这种开销,一些系统增加了硬件支持,实现跨上下文切换的 TLB 共享。比如有的系统在 TLB 中添加了一个地址空间标识符(Address Space Identifier,ASID)。可以把ASID 看作是进程标识符(Process Identifier,PID),但通常比 PID 位数少(PID 一般 32 位,ASID 一般是 8 位)。因此,有了地址空间标识符,TLB 可以同时缓存不同进程的地址空间映射,没有任何冲突。当然,硬件也需要知道当前是哪个进程正在运行,以便进行地址转换,
因此操作系统在
上下文切换
时,必须将某个特权寄存器
设置为当前进程的 ASID。属于两个不同进程的两项,可以将两个不同的 VPN 指向了相同的物理页
两个进程可以共享同一物理页(例如代码段的页)。共享代码页(以二进制或共享库的方式)是有用的,因为它减少了物理页的使用,从而减少了内存开销。
TLB 替换策略
TLB 和其他缓存一样,还有一个问题要考虑,即缓存替换(cache replacement)。具体来
说,向 TLB 中插入新项时,会替换(replace)一个旧项,这样问题就来了:应该替换那一个?
关键问题:如何设计 TLB 替换策略 在向 TLB 添加新项时,应该替换哪个旧项?目标当然是减小 TLB 未命中率(或提高命中率),从而改进性能。
这里我们先简单指出几个典型的策略。
- 一种常见的策略是替换
最近最少使用
(least-recently-used,LRU)的项。LRU尝试利用内存引用流中的局部性,假定最近没有用过的项,可能是好的换出候选项。
- 另一种典型策略就是
随机
(random)策略,即随机选择一项换出去。这种策略很简单,并且可 以避免一种极端情况。例如,一个程序循环访问 n+1 个页,但 TLB 大小只能存放 n 个页。 这时之前看似“合理”的 LRU 策略就会表现得不可理喻,因为每次访问内存都会触发 TLB未命中,而随机策略在这种情况下就好很多。
实际系统的 TLB 表项
最后,我们简单看一下真实的 TLB。这个例子来自 MIPS R4000[H93],它是一种现代的系统,采用软件管理 TLB。图 19.4 展示了稍微简化的 MIPS TLB 项。
MIPS R4000
支持 32 位
的地址空间,页大小为 4KB
。所以在典型的虚拟地址中,预期会看到 20 位的 VPN
和 12 位的偏移量
。但是,你可以在 TLB 中看到,只有 19 位
的 VPN。事实上,用户地址
只占地址空间的一半
(剩下的留给内核),所以只需要 19 位的 VPN。VPN 转换成最大 24 位
的物理帧号
(PFN),因此可以支持最多有 64GB 物理内存(224 个 4KB 内 存页)的系统。全局位
(Global,G),用来指示这个页是不是所有进程全局共享的。因此,如果全局位置为 1,就会忽略
ASID。3 个
一致性位
(Coherence,C),决定硬件如何缓存该页(其中一位超出了本书的范围);脏位
(dirty),表示该页是否被写入新数据(后面会介绍用法);有效位
(valid),告诉硬件该项的地址映射是否有效。还有没在图 19.4 中展示的页掩码
(page mask)字段,用来支持不同的页大小。MIPS 的 TLB 通常有
32 项
或 64 项
,大多数提供给用户进程使用,也有一小部分留给操作系统使用。操作系统
可以设置一个被监听的寄存器,告诉硬件需要为自己预留多少 TLB 槽。这些保留的转换映射
,被操作系统用于关键时候它要使用的代码和数据,在这些时候,TLB 未命中可能会导致问题(例如,在 TLB 未命中处理程序
中)。由于 MIPS 的 TLB 是软件管理的,所以系统需要提供一些更新 TLB 的指令。MIPS 提供了 4 个这样的指令:
TLBP
,用来查找指定的转换映射是否在 TLB 中;
TLBR
,用来将 TLB中的内容读取到指定寄存器中;
TLBWI
,用来替换指定的 TLB 项;
TLBWR
,用来随机替换一个 TLB 项。
操作系统可以用这些指令管理 TLB 的内容。当然这些指令是特权指令,这很关键。如果用户程序可以任意修改 TLB 的内容,你可以想象一下会发生什么可怕的事情。
小结
如果一个程序短时间内访问的页数
超过了
TLB 中的页数
,就会产生大量的 TLB 未命中
,运行速度就会变慢。这种现象被称为超出 TLB 覆盖范围
(TLB coverage)。可以用更大的页
来缩小页的数量,增加命中率访问 TLB 很容易成为 CPU 流水线的瓶颈,尤其是有所谓的
物理地址索引缓存
(physically-indexed cache),这是 CPU 内部的缓存。有了这种缓存,地址转换必须发生在访问该缓存之前,这会让操作变慢。虚拟地址索引缓存
(virtually-indexed cache)解决了一些性能问题,但也为硬件设计带来了新问题。参考
- 作者:GJJ
- 链接:https://blog.gaojj.cn/article/blog-22
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。