何时做或不做 INVLPG,MOV 到 CR3 以最小化 TLB 刷新

When to do or not do INVLPG, MOV to CR3 to minimize TLB flushing

序言

我是一个操作系统爱好者,我的内核运行在80486+,已经支持虚拟内存

从 80386 开始,Intel 的 x86 处理器系列及其各种克隆都支持带分页的虚拟内存。众所周知,当设置CR0中的PG位时,处理器使用虚拟地址转换。然后,CR3 寄存器指向顶级页面目录,即 2-4 级页面 table 结构的根,将虚拟地址映射到物理地址。

处理器不会为每个生成的虚拟地址查询这些 table,而是将它们缓存在称为 Translation Lookaside Buffer 或 TLB 的结构中。但是,当对页面 tables 进行更改时,需要刷新 TLB。在 80386 处理器上,这个刷新将由 使用顶级页面目录地址或任务切换重新加载 (MOV) CR3。这应该无条件地刷新所有 TLB 条目。据我了解,对于虚拟内存系统来说,总是在 any 更改后重新加载 CR3 是完全有效的。

这是浪费,因为 TLB 现在会抛出完全正确的条目,因此在 80486 处理器中引入了 INVLPG 指令。 INVLPG 将使与源操作数地址匹配的 TLB 条目无效。

但是从 Pentium Pro 开始,我们还有一些全局页面不会随着移动到 CR3 或任务切换而刷新; AMD x86-64 ISA 表示某些上层页面 table 结构可能会被缓存并且不会被 INVLPG 无效。为了清楚地了解每个 ISA 需要什么和不需要什么,我们真的需要为 80 年代以来发布的大量 ISA 下载一份 1000 页的数据表来阅读其中的几页,即便如此,这些文件似乎对于 TLB 失效以及如果 TLB 未正确失效会发生什么情况要特别模糊。

问题

为简单起见,可以假设我们正在谈论单处理器系统。此外,可以假设更改页面结构后不需要任务切换。 (因此 INVLPG 总是被认为至少与重新加载 CR3 寄存器一样好。

基本假设是每次更改页面 table 和页面目录后都需要重新加载 CR3,这样的系统是正确的。但是,如果想避免不必要地刷新 TLB,则需要回答以下 2 个问题:

  1. 如果ISA支持INVLPG,经过什么样的改动才能安全使用而不是重新加载CR3?例如。 "If one unmaps one page frame (set the corresponding table entry to not present), one can always use INVLPG"?

  2. 在不触及 CR3 或执行 INVLPG 的情况下,可以对 table 和目录进行哪些更改?例如。 "If a page is not mapped at all (not present), one can write a PTE with Present=1 for it without flushing the TLB at all"?

即使在 Stack Overflow 上阅读了大量 ISA 文档和与 INVLPG 相关的所有内容之后,我个人也不确定我在那里展示的任何一个示例。事实上,一个 notable post 马上就指出了这一点:"I don't know exactly when you should use it and when you shouldn't." 因此,如果您可以提供任何特定的、正确的示例,最好有文档记录,并且适用于 IA32 或 x86-64,我们将不胜感激。

第一个问题:

  1. 您可以随时使用 INVLPG,并且可以进行任何可能的更改。使用 INVLPG 总是安全的。
  2. 重新加载CR3 不会使 TLB 中的全局页面失效。所以有时你必须使用 INVLPG 因为重新加载 CR3 没有效果。
  3. INVLPG 必须用于每个涉及的页面。如果您一次更改多个页面,那么重新加载 CR3 比大量 INVLPG 调用更快。
  4. 不要忘记现代 CPU 上的 Aaddress Space Identifier 扩展.

关于你的第二个问题:

未映射的页面无法缓存在 TLB 中(假设您之前取消映射时正确地使其无效)。因此,不存在的任何更改都不需要 INVLPGCR3 重新加载。

用最简单的术语;要求是 CPU 的 TLB 可以记住的任何已更改的内容都必须在依赖更改的任何内容发生之前失效。

CPU 可能记得的事情包括:

  • 页面的最终权限(来自页面 table 条目、页面目录条目等的 read/write/execute 权限的组合); 包括页面是否存在(见下面的警告)
  • 页面的物理地址
  • "accessed" 和 "dirty" 标志
  • 影响缓存的标志
  • 无论是普通页面还是大页面(2 或 4 MiB)还是大页面(1 GiB)

警告:因为 Intel CPUs 不记得 "not present" 页,Intel 的文档可能会说当从 "not present" 到 "present"。 Intel 的文档仅适用于 Intel CPUs。它不适用于所有 80x86 CPUs。某些 CPUs(主要是 Cyrix)确实记得页面何时为 "not present",并且由于这些 CPUs,当将页面从 "not present" 更改为 "present".

请注意,由于推测执行,您不能偷工减料。例如,如果您知道某个页面从未被访问过,您就不能假设它不在 TLB 中,因为 TLB 可能已被推测性提取。

"before anything that relies on the change happens"这个词我选得很仔细。现代 CPUs(特别是对于长模式)确实缓存了更高级别的分页结构(例如 PDPT 条目),而不仅仅是最终页面。这意味着,如果您更改了更高级别的分页结构,但页面 table 条目本身保持不变,您仍然需要无效。

这也意味着如果没有依赖于更改,则可以跳过失效。一个简单的例子是访问标志和脏标志——如果你不依赖这些标志(确定 "least recently used" 以及发送哪些页面进行交换 space)那么它并不重要如果 CPU 没有意识到您已经更改了它们。如果 CPU 为使用 old/stale TLB 信息,当且仅当确实需要时页面错误处理程序才会失效。

此外; "anything the CPU's TLB could have remembered" 有点棘手。通常 OS 会将分页结构本身映射到虚拟地址 space 以允许 fast/easy 访问它们(例如常见的 "recursive mapping" 技巧,你假装页面目录是一个页 table)。在这种情况下,当您更改页面目录条目时,您需要使受影响的普通页面无效(正如您所期望的那样),但您还需要使任何映射中受到更改影响的任何内容无效。

使用哪个(INVLPG 或重新加载 CR3)存在几个问题。对于单页 INVLPG 会更快。如果您更改页面目录(影响 1024 页或 512 页,具体取决于分页的类型),那么在循环中使用 INVLPG 可能会或可能不会比仅重新加载 CR3 更昂贵(这取决于 CPU/hardware,并且失效后代码的访问模式)。

这里还有 2 个问题。首先是任务切换。在使用不同虚拟地址 space 的任务之间切换时,您必须更改 CR3。这意味着,如果您更改影响大面积的内容(例如页面目录),您可以通过尽早执行任务切换来提高整体性能,而不是现在重新加载 CR3(用于无效)然后在不久之后重新加载 CR3(用于任务切换) ).基本上,这是一个 "kill 2 birds with one stone" 优化。

另一件事是"global pages"。通常在所有虚拟地址 space 中都有相同的页面(例如内核)。当您重新加载 CR3 时(例如,在任务切换期间),您不希望保持不变的页面的 TLB 无缘无故地失效,因为这会不必要地损害性能。为了解决这个问题并提高性能,(对于 Pentium 和更高版本)有一个名为 "global pages" 的功能,您可以在其中将这些公共页面标记为全局页面,并且在您重新加载 CR3 时它们不会失效。在这种情况下,如果您需要使全局页面无效,您需要使用 INVPLG 或更改 CR4(例如,禁用然后重新启用全局页面功能)。对于更大的区域(例如,更改页面目录而不仅仅是一页),它与以前相同(在循环中弄乱 CR4 可能比 INVLPG 更快或更慢)。