一句话 + 关键问题

一句话:在 MMU 里放一个很小但极快的地址转换缓存(TLB),把最近用过的 VPN→PFN 转换存起来; 绝大多数访存都能在 TLB 里命中,从而省掉那次去内存查页表的额外访问——TLB 在真正意义上让虚拟内存变得可行。

如何加速地址转换?如何才能加速地址转换,并避免分页似乎必需的那次额外内存访问?需要什么硬件支持?操作系统又需要做什么?

How can we speed up address translation, and generally avoid the extra memory reference that paging seems to require? What hardware support is required? What OS involvement is needed?

第 18 章留下两个问题:页表太慢、页表太大。本章解决"慢":因为映射信息存放在物理内存中,分页在逻辑上要求每次取指、每次显式的 load/store 之前, 都先访问一次内存去读页表项。要让它变快,操作系统需要老朋友——硬件的帮助。本章之后,第 20 章再解决"大"。

核心概念速览

TLB

地址转换旁路缓冲存储器(translation-lookaside buffer),MMU 的一部分,是热门虚拟到物理地址转换的硬件缓存。更贴切的名字是"地址转换缓存"。

命中 / 未命中

TLB 里有这个 VPN 的转换叫命中(hit),直接得到 PFN;没有叫未命中(miss),必须去查页表。

命中率(hit rate)

命中次数 ÷ 访问总次数。只要命中率接近 100%,程序就几乎像没被虚拟化一样快。

空间 / 时间局部性

访问了 x,很快会访问 x 附近(空间);刚访问过的,很快会再访问(时间)。TLB 依靠两者取得高命中率。

硬件 vs 软件管理

未命中时由硬件遍历页表(x86,CISC)或抛出异常交给 OS 处理(MIPS、SPARC,RISC)。

全相联(fully associative)

一个转换可以放在 TLB 的任意位置,硬件并行比较所有项,所以每项必须同时存 VPN 和 PFN。

ASID

地址空间标识符(address space identifier),让 TLB 能同时存多个进程的转换,切换进程时不必清空 TLB。

TLB 覆盖范围

TLB 项数 × 页大小。程序短时间访问的页数超过它,就会大量未命中(exceeding the TLB coverage)。

TLB 基本算法

假设使用简单的线性页表,并且由硬件处理 TLB 未命中。每次虚拟内存引用,硬件的动作如下(原书图 19.1):

 1  VPN = (VirtualAddress & VPN_MASK) >> SHIFT
 2  (Success, TlbEntry) = TLB_Lookup(VPN)
 3  if (Success == True)    // TLB 命中
 4      if (CanAccess(TlbEntry.ProtectBits) == True)
 5          Offset   = VirtualAddress & OFFSET_MASK
 6          PhysAddr = (TlbEntry.PFN << SHIFT) | Offset
 7          Register = AccessMemory(PhysAddr)
 8      else
 9          RaiseException(PROTECTION_FAULT)
10  else                    // TLB 未命中
11      PTEAddr = PTBR + (VPN * sizeof(PTE))
12      PTE = AccessMemory(PTEAddr)
13      if (PTE.Valid == False)
14          RaiseException(SEGMENTATION_FAULT)
15      else if (CanAccess(PTE.ProtectBits) == False)
16          RaiseException(PROTECTION_FAULT)
17      else
18          TLB_Insert(VPN, PTE.PFN, PTE.ProtectBits)
19          RetryInstruction()
CPU 芯片 CPU 核 MMU 中的 TLB VPN 6 → PFN 23VPN 7 → PFN 91……(几十项) VA 物理内存(很慢) 页表(全部转换)未命中才来这里 程序的数据 / 代码每次都要来 ① 未命中:读 PTE ② PA:访问数据 命中:1 次访存(只有 ②)。未命中:2 次(① + 重试后的 ②),还要加上装入 TLB 的开销。 TLB 离处理核很近,设计得非常快;访存相对大多数 CPU 指令代价高得多。
TLB 是位于 MMU 中的一个小缓存。命中的访问不需要碰页表;只有未命中才付出查页表的代价。

例子:访问一个数组

原书的例子:一个有 10 个 4 字节整数的数组,从虚拟地址 100 开始。地址空间是很小的 8 位,页大小 16 字节, 于是虚拟地址 = 4 位 VPN(16 个虚拟页)+ 4 位偏移(每页 16 字节)。a[0] 位于(VPN=06,偏移=04),这一页只放得下 3 个整数; a[3]~a[6] 在 VPN 07;最后三个 a[7]~a[9] 在 VPN 08。循环如下(只考虑对数组的访问,忽略 i、sum 和指令本身):

int i, sum = 0;
for (i = 0; i < 10; i++) {
    sum += a[i];
}

结果:miss, hit, hit, miss, hit, hit, hit, miss, hit, hit,命中率 70%。即使这是程序第一次访问这个数组,TLB 也能凭空间局部性提高性能: 元素紧挨着,只有每页的第一次访问才会未命中。在下面的模拟器里亲自走一遍,再试试把页大小改成 32 字节、把数组访问两遍、换一种替换策略。

模拟器

TLB + 页表:逐次访问

输入一串虚拟地址,逐次观察:拆出 VPN → 在 TLB 中查找 → 命中直接得到 PFN;未命中去页表取 PTE,按替换策略装入 TLB 后重试。左边的网格仿照原书图 19.2,绿色行 = 当前 TLB 里有其转换的页。
数组生成器:
虚拟地址空间
TLB
页表(内存中,所有页有效)

再看两件事:

缓存是计算机系统中最基本的性能技术之一,用来"让常见情况更快"。硬件缓存利用指令和数据引用的局部性:时间局部性(循环变量、循环里的指令被反复访问)和空间局部性(顺序扫过一个数组)。 它们是经验法则而不是铁律,取决于程序本身。

既然缓存这么好,为什么不做一个超大的缓存把所有数据都放进去?因为物理定律:想要快,缓存就必须小——光速之类的物理约束会起作用,任何大缓存必然慢,也就失去了意义。 我们只能用小而快的缓存,剩下的问题是如何用好它们。

谁来处理 TLB 未命中?

两种答案:硬件,或软件(OS)。

 1  VPN = (VirtualAddress & VPN_MASK) >> SHIFT
 2  (Success, TlbEntry) = TLB_Lookup(VPN)
 3  if (Success == True)    // TLB 命中
 4      if (CanAccess(TlbEntry.ProtectBits) == True)
 5          Offset   = VirtualAddress & OFFSET_MASK
 6          PhysAddr = (TlbEntry.PFN << SHIFT) | Offset
 7          Register = AccessMemory(PhysAddr)
 8      else
 9          RaiseException(PROTECTION_FAULT)
10  else                    // TLB 未命中
11      RaiseException(TLB_MISS)

原书图 19.3:OS 处理未命中时的硬件控制流。和图 19.1 相比,第 11~19 行缩成了一行——硬件在未命中时几乎什么都不做。

模拟器

硬件管理 vs 软件管理:一次访存的控制流

选择管理方式和场景,逐步走一遍。高亮的是正在执行的代码行;下方泳道显示每一步由谁完成(用户程序、硬件、OS 内核)。重点观察:软件管理时,从陷阱返回要回到引发异常的那条指令重新执行。
硬件(MMU)
OS:TLB 未命中处理程序(示意)

软件管理有两个重要细节:

  1. 从陷阱返回的位置不同。系统调用的"从陷阱返回"应回到陷入 OS 那条指令的下一条,就像过程调用返回到调用点之后; 而 TLB 未命中处理完后,硬件必须回到引发陷阱的那条指令,让它重新执行并命中。所以根据陷阱或异常的原因不同,硬件陷入 OS 时必须保存不同的 PC。
  2. 避免无限的 TLB 未命中链。如果未命中处理程序本身的代码或数据也需要地址转换且不在 TLB 中,就会再次未命中……解决办法很多:把处理程序放在物理内存中 (不做地址转换),或者在 TLB 中保留一些永久有效的项给处理程序本身用——这些固定(wired)的转换总是命中。

软件管理的主要优点是灵活:OS 可以用任意数据结构实现页表,不需要改硬件。另一个优点是简单:硬件在未命中时几乎不做事,只抛出异常。

20 世纪 80 年代,计算机体系结构界有过一场大战。CISC(复杂指令集)一方认为指令应是高级原语(比如一条指令完成字符串拷贝),让汇编更好用、代码更紧凑; RISC(精简指令集,由伯克利的 David Patterson 和斯坦福的 John Hennessy 引领,John Cocke 因最早的 RISC 工作获得图灵奖)认为指令集其实是编译器的目标, 编译器只需要少数简单原语——那就把硬件(尤其是微代码)尽量剥掉,让剩下的部分简单、统一、快速。

早期 RISC 芯片明显更快;但 Intel 等 CISC 厂商把许多 RISC 技术吸收进处理器核心(比如在流水线前端把复杂指令转换成微指令,再以类似 RISC 的方式处理), 加上每个芯片上的晶体管越来越多,CISC 保持了竞争力。争论最终平息,今天两类处理器都能做得很快。

TLB 的有效位 ≠ 页表的有效位。PTE 被标记为无效,意味着这一页没有被进程分配,正确的程序不应该访问它,访问会陷入 OS、进程通常被杀死。 TLB 的有效位只表示"这个 TLB 项里有没有一个有效的转换":系统启动时所有 TLB 项通常都是无效的,因为还没有缓存任何转换; 上下文切换时把所有 TLB 项设为无效,也能保证即将运行的进程不会误用上一个进程的转换。

TLB 里有什么?

典型的 TLB 有 32、64 或 128 项,并且是全相联(fully associative)的:任何转换都可以放在 TLB 的任何位置,硬件并行搜索所有项。 所以每项必须同时保存 VPN 和 PFN:

要查的VPN = 7 VPNPFN其他位(valid、prot、ASID、dirty…) 6231 rw- 7911 rw- 1251 r-x ——0(无效) 同时和所有项的 VPN 比较(每项一个比较器),命中的那一项给出 PFN
全相联 TLB:VPN 不能当下标(任何转换都可能在任何位置),所以要存下 VPN 并在所有项上并行比较。

"其他位"中常见的有:有效位(这一项是否有有效转换)、保护位(代码页可能是读和执行,堆页是读和写)、 以及地址空间标识符、脏位等。

TLB 与上下文切换

TLB 里的转换只对当前运行的进程有意义。例如:进程 P1 的第 10 个虚拟页映射到物理帧 100;P2 的第 10 个虚拟页映射到物理帧 170。如果两者的转换都在 TLB 里:

VPNPFNvalidprot
101001rwx
——0—
101701rwx
——0—

问题来了:VPN 10 到底转换成 PFN 100(P1)还是 170(P2)?硬件无法区分。

上下文切换时如何管理 TLB 的内容?进程间切换时,上一个进程在 TLB 中的转换对即将运行的进程没有意义。硬件或操作系统应该怎么做?

When context-switching between processes, the translations in the TLB for the last process are not meaningful to the about-to-be-run process. What should the hardware or OS do in order to solve this problem?

模拟器

上下文切换:不处理 / 清空 / ASID

两个进程的页表:P1 的 VPN 10→100、11→101;P2 的 VPN 10→170、11→171。访问序列中进程号变化即表示一次上下文切换。比较三种策略:哪种会给出错误的转换?哪种未命中最多?
TLB
页表(每进程一份)
进程VPNPFN
P1(ASID 1)10100
P1(ASID 1)11101
P2(ASID 2)10170
P2(ASID 2)11171

还有一种情况:两个不同进程的不同 VPN 指向同一个物理页。原书的例子:

VPNPFNvalidprotASID
101011r-x1
——0——
501011r-x2
——0——

进程 1 和进程 2 共享物理页 101(比如一个代码页):P1 把它映射到自己地址空间的第 10 页,P2 映射到第 50 页。共享二进制代码或共享库的代码页很有用,可以减少物理页的使用,降低内存开销。

替换策略

如何设计 TLB 替换策略?向 TLB 添加新项时,应该替换哪一个旧项?目标当然是减小未命中率(提高命中率),从而改进性能。

Which TLB entry should be replaced when we add a new TLB entry? The goal, of course, being to minimize the miss rate (or increase hit rate) and thus improve performance.

在上面的 TLB 模拟器中点"LRU 的坏情况",分别用 LRU 和随机跑一遍,比较命中率。第 22 章会详细研究这类替换策略。

真实的 TLB 表项:MIPS R4000

MIPS R4000 使用软件管理的 TLB,支持 32 位地址空间和 4KB 页。按理 VPN 应有 20 位,但表项里只有 19 位:用户地址只占地址空间的一半(另一半留给内核),所以 19 位就够了。 VPN 转换成最多 24 位的 PFN,因此可以支持最多 64GB 的物理内存(224 个 4KB 页)。

VPN(19 位) G ASID(8 位) PFN(24 位) C D V 灰色 = 未使用的位。每行 32 位,一个表项共 64 位(简化版,省略了 page mask 字段)。
图 19.4:一个 MIPS TLB 表项。G:全局位;ASID:8 位地址空间标识符;C:3 位一致性位;D:脏位;V:有效位。
字段作用
G(global)全局共享的页(所有进程都能用,比如内核页)。G 置位时忽略 ASID。
ASID(8 位)区分地址空间。只有 8 位,最多 256 个——如果同时运行的进程超过 256 个,OS 该怎么办?(提示:回收 ASID 时必须清掉 TLB 中带这个 ASID 的项。)
C(3 位)一致性(coherence)位,决定硬件如何缓存这一页(超出本书范围)。
D / V脏位:页被写过时置位;有效位:这一项里有没有有效的转换。
page mask(图中未画)支持多种页大小——后面会看到大页为什么有用。

MIPS 的 TLB 通常有 32 或 64 项,大部分给用户进程使用,少数保留给 OS:OS 通过 wired 寄存器告诉硬件保留几个槽,用于关键时刻(例如 TLB 未命中处理程序中)绝不能未命中的代码和数据。 因为是软件管理,MIPS 提供 4 条更新 TLB 的指令:TLBP(探测某个转换是否在 TLB 中)、TLBR(把某项读到寄存器)、TLBWI(替换指定项)、TLBWR(替换随机一项)。 这些指令当然必须是特权指令——想想用户进程如果能改 TLB 会发生什么:几乎可以做任何事,包括接管机器、运行自己的恶意"OS"。

"随机存取存储器"这个名字暗示访问 RAM 的任何位置都一样快。通常这样想没错,但由于 TLB 这类软硬件特性,访问某一页可能很昂贵——尤其是该页当前没有被 TLB 映射时。 所以请记住:RAM 不总是 RAM。随机访问地址空间,特别是访问的页数超过了 TLB 覆盖范围时,可能导致严重的性能损失。原书作者以他们的导师 David Culler 的名字命名这条定律,因为他总把 TLB 指为许多性能问题的根源。

TLB 覆盖范围计算器

动手实验:测量 TLB 的大小和未命中代价

原书的作业借鉴了 Saavedra-Barrera 的方法:在一个大数组中,每页只访问一个整数,访问 NUMPAGES 个页,并把这个循环重复很多次来计时:

int jump = PAGESIZE / sizeof(int);
for (i = 0; i < NUMPAGES * jump; i += jump)
    a[i] += 1;

页数不超过 TLB 项数时,每次访问都命中,很快;一旦超过,每次访问的代价会突然跳高到 TLB 未命中的水平。原书图 19.5 中:8 页以内约 5ns,16 页以上跳到约 20ns,1024 页左右再跳到约 70ns—— 说明有两级 TLB:第一级很小(8~16 项),第二级更大但更慢(约 512 项)。

labs/19-tlb/ 里有三个文件:timer.c 回答作业第 1 题(计时器精度),tlb.c 是测量程序,run.sh 把页数从 1 扫到 24576。 tlb.c 处理了作业里提到的所有陷阱:用 volatile 防止编译器删掉循环(第 5 题)、用 sched_setaffinity 绑定到一个 CPU(第 6 题)、 计时前先把每页写一遍避免按需清零的开销(第 7 题);此外还用 madvise(MADV_NOHUGEPAGE) 禁止透明大页,并让每页访问的位置错开一个缓存行,避免 L1 缓存组冲突干扰测量。

cd labs/19-tlb && make
./timer
./run.sh          # 原书的 a[i] += 1 循环
./run.sh -p       # 指针追逐模式:测延迟

示例输出(AMD Ryzen 7 8745H,WSL2,gcc 14)。你的机器上数字会不同;WSL2 运行在虚拟机里,还有其他进程争抢 CPU,数字有噪声,所以每个点重复测 5 次并取最小值。

$ ./timer
clock_getres(CLOCK_MONOTONIC) = 1 ns
clock_gettime 连续两次读数:最小差 10 ns,平均差 17.3 ns
gettimeofday 能分辨的最小变化:1 us(= 1000 ns)
结论:单次页访问只有几纳秒,必须把它重复成百万次,让总时间达到毫秒级再求平均。
每次访问的平均耗时 vs. 访问的页数(横轴对数刻度)。蓝色:原书的 a[i] += 1 循环;橙色:指针追逐(-p)。数据来自本机实际运行。
$ ./run.sh -k 7            # a[i] += 1
$ ./run.sh -p -k 7         # 指针追逐

  页数   a[i]+=1 最小/中位(ns)   指针追逐 最小/中位(ns)
     1     0.47 /   0.50          1.26 /   1.27
     2     0.82 /   0.84          1.26 /   1.29
     3     0.56 /   0.57          1.25 /   1.29
     4     0.41 /   0.42          1.27 /   1.31
     6     0.37 /   0.41          1.28 /   1.30
     8     0.37 /   0.38          1.27 /   1.34
    12     0.36 /   0.37          1.28 /   1.30
    16     0.35 /   0.36          1.26 /   1.27
    24     0.33 /   0.33          1.28 /   1.28
    32     0.43 /   0.44          1.27 /   1.30
    48     0.32 /   0.32          1.26 /   1.31
    64     0.32 /   0.32          1.25 /   1.31
    96     0.43 /   0.44          2.74 /   2.78
   128     0.43 /   0.45          2.77 /   2.82
   192     0.43 /   0.44          2.77 /   2.83
   256     0.44 /   0.45          2.73 /   2.79
   384     0.45 /   0.46          2.81 /   2.87
   512     0.57 /   0.59          2.82 /   2.96
   768     0.56 /   0.60          5.14 /   5.28
  1024     0.56 /   0.58          5.18 /   5.31
  1536     0.56 /   0.57          5.17 /   5.24
  2048     0.53 /   0.55          5.17 /   5.32
  3072     2.50 /   2.57         11.61 /  11.81
  4096     4.04 /   4.10         16.95 /  18.24
  6144     4.91 /   4.99         20.70 /  36.37
  8192     5.22 /   5.35         26.75 /  40.20
 12288     5.38 /   5.54         31.83 /  33.13
 16384     5.46 /   5.83         33.92 /  41.80
 24576     5.45 /   5.75         37.62 /  43.11

怎么读这些数字

  1. 指针追逐(橙线)有清楚的台阶。
    • ≤ 64 页:约 1.26 ns,在约 4.5GHz 下是 5~6 个时钟周期:L1 TLB 命中 + L1 数据缓存命中。
    • 96~512 页:约 2.8 ns。拐点在 64 和 96 之间,与这颗 Zen 4 处理器的 L1 数据 TLB 为 72 项相吻合。多出来的约 1.5 ns(约 7 个周期)就是 L1 TLB 未命中、在第二级 TLB 命中的代价。
    • 768~2048 页:约 5.2 ns。这一跳不是 TLB 造成的:每页访问一个缓存行,512 页 × 64 字节 = 32KB,正好是 L1 数据缓存的大小,超过后数据要从 L2 缓存取。测量中混着缓存效应,解读时要小心。
    • ≥ 3072 页:12 → 38 ns。第二级 TLB(3072 项)也装不下了,每次访问都要遍历页表(page walk);页数越多,页表项和数据本身也越难留在缓存中。 另外 WSL2 运行在 Hyper-V 虚拟机里,TLB 未命中时硬件要同时遍历客户机页表和宿主机的嵌套页表,比裸机更贵。
  2. 原书的 a[i] += 1 循环(蓝线)几乎看不出第一个拐点:64 → 96 页只从 0.32 涨到 0.43 ns。各次访问之间没有数据依赖,乱序执行的 CPU 可以同时处理多个 TLB 查找,把第二级 TLB 的延迟重叠掩盖掉了; 只有第二级 TLB 也未命中、要真正遍历页表时(3072 页以后)才明显变慢,从约 0.5 ns 涨到约 5 ns,慢了 10 倍。这个循环测的是吞吐,指针追逐测的是延迟。
  3. 1~3 页时 a[i] += 1 反而更慢(2 页时 0.82 ns):同一个整数在相邻两趟中被连续加 1,下一次读必须等上一次写完成,页数少时这条依赖链成了瓶颈,和 TLB 无关。
  4. 和原书图 19.5 对比:结构相同,都是一个小而快的第一级 TLB 加一个大而慢的第二级 TLB;只是今天的 TLB 大得多(72 / 3072 项,原书的机器约 8~16 / 512 项)。 L1 TLB 命中和遍历页表之间差 10~30 倍——TLB 性能确实很重要。
  5. 噪声:WSL2 里还有别的进程在抢 CPU,中位数有时比最小值大很多(例如 6144 页:20.7 vs 36.4 ns),所以每个点测 7 次取最小值。 另外 tlb.c 计时前会先热身 0.2 秒:最初没加热身时,前几个测量点都在 5 ns 左右,是正常值的 4 倍——CPU 刚从空闲的低频状态醒来。

先预测再验证

在 TLB 模拟器中载入原书图 19.2,但把 TLB 项数改为 1。命中率会变吗?为什么?
看答案
不变,仍是 70%。这个访问序列是按页连续的:一旦离开 VPN 6 就再也不回来,所以任何时刻只需要 1 个 TLB 项。TLB 大小只影响"以后还会回来的页",也就是时间局部性。
点"把数组访问两遍"。TLB 为 4 项时命中率是多少?把 TLB 改成 2 项呢?
看答案
4 项:第一遍 3 次未命中,第二遍全命中,17/20 = 85%。2 项(LRU):第一遍结束时 TLB 里是 VPN 7、8,第二遍从 VPN 6 开始又未命中并踢掉 7,接着访问 7 又未命中……第二遍也是 3 次未命中,命中率回到 70%。时间局部性要求 TLB 装得下整个工作集。
"LRU 的坏情况":4 项 TLB,循环访问 VPN 0~4 共 6 遍。先预测 LRU、FIFO、随机三种策略的命中率。
看答案
LRU 和 FIFO 都是 0%:每次要访问的页恰好是刚被踢掉的那一页。随机策略有时会踢掉"不那么急需"的页,命中率明显大于 0(具体数值随种子变化,可以多换几个种子试试)。
上下文切换模拟器里选"什么都不做":第一次出错发生在哪一步?再选"清空"和"ASID",未命中各多少次?
看答案
"什么都不做"时,P2 第一次访问 VPN 10 就命中了 P1 留下的项,得到 PFN 100——读到了别的进程的内存,这比性能问题严重得多。"清空"是正确的,但每次切换后都要重新未命中;ASID 在两个进程都热身完后几乎全部命中。
(作业第 3、6 题)运行 ./run.sh 时不绑定 CPU(加 -c -1),结果会怎样?为什么要绑定?
看答案
每个 CPU 有自己的 TLB 层次。线程若被调度器从一个核迁移到另一个核,新核的 TLB 是"冷"的,而且会与该核上的其他程序竞争,测量会出现随机的尖峰、波动变大。绑定到一个核(sched_setaffinity、taskset)才能测到一个 TLB 的行为。

自测题

1. TLB 本质上是什么?
TLB 是 MMU 的一部分,只缓存热门的转换;完整的转换仍在内存中的页表里。
2. 原书数组例子中(16 字节页、10 个 4 字节整数),如果 a[0] 改为从地址 96 开始,10 次访问中哪几次 TLB 未命中?
96 正好是 VPN 6 的开头:a[0..3] 在 VPN 6,a[4..7] 在 VPN 7,a[8..9] 在 VPN 8。每页第一次访问未命中,即第 1、5、9 次,命中率仍是 70%——数组的对齐方式改变了未命中的位置。可以用模拟器的数组生成器(起始 96)验证。
3. 软件管理的 TLB 未命中处理完毕后,从陷阱返回应该回到哪里?
和系统调用不同:TLB 未命中时指令还没有完成,必须重试,所以硬件要保存引发异常的指令的 PC。
4. 下列哪个是软件管理 TLB 的主要优点?
灵活性和硬件简单是它的优点。它恰恰需要特权指令(如 MIPS 的 TLBWR)来更新 TLB。
5. 为什么 TLB 表项里既要存 VPN 也要存 PFN?
线性页表用 VPN 作下标,不用存 VPN;全相联的 TLB 没有"下标",必须存 VPN 并并行比较。
6. 上下文切换时如果既不清空 TLB 也没有 ASID,最严重的后果是?
这是正确性(和安全)问题,而不仅是性能问题:同一个 VPN 在不同进程里映射到不同的 PFN。
7. TLB 有 n 项,程序循环访问 n + 1 个页。哪种替换策略的命中率最低?
LRU 总是踢掉最久没用的页,而它恰好就是下一个要用的页,于是每次都未命中。随机策略反而能命中一部分。
8. MIPS R4000 的 TLB 表项中 PFN 有 24 位、页大小 4KB,能支持多大的物理内存?
224 个页 × 212 字节 = 236 字节 = 64GB。

常见误区与本章小结