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

26

序言

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

从 80386 开始,英特尔和各种克隆处理器家族的 x86 处理器已经支持带分页的虚拟内存。众所周知,当 CR0 中的 PG 位被设置时,处理器使用虚拟地址转换。然后,CR3 寄存器指向顶层页目录,即将虚拟地址映射到物理地址的 2-4 级页表结构的根目录。

处理器不会为每个生成的虚拟地址查询这些表,而是将它们缓存在一个称为 翻译后援缓存 或 TLB 的结构中。但是,当对页表进行更改时,需要刷新 TLB。在 80386 处理器上,可以通过重新加载(MOVCR3 与顶级页目录地址或任务切换来完成此操作。据我了解,虚拟内存系统始终在更改后重新加载 CR3 是完全有效的方法。
这是浪费的,因为 TLB 现在会丢弃完全良好的条目,因此在 80486 处理器中引入了 INVLPG 指令。 INVLPG 将使与源操作数地址匹配的 TLB 条目无效化。

然而,从Pentium Pro开始,我们还有全局页,它们不会随着移动到CR3或任务切换而被清除;而AMD x86-64 ISA称,某些上层页表结构可能会被缓存并且不能通过INVLPG使其失效。为了对每个ISA需要和不需要什么有一个连贯的认识,人们实际上需要下载一个多达1000页的数据手册,以阅读其中的一些页面,即使那些文档似乎在TLB失效方面也特别模糊。

问题

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

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

  1. 假设ISA支持INVLPG,在进行了哪些更改后可以安全地使用它而不是重新加载CR3?例如,“如果取消映射一个页面帧(将相应的表项设置为未出现),则始终可以使用INVLPG”?

  2. 在不触及CR3或执行INVLPG的情况下,可以对表和目录进行哪些更改?例如,“如果根本没有映射页面(未出现),则可以写入一个具有Present=1的PTE,而无需刷新TLB”?

即使阅读了大量ISA文档以及在Stack Overflow上与INVLPG相关的所有内容,我个人仍然不确定我所提出的任何一个示例。事实上,一个notable post立即指出:“我不知道你应该在什么时候使用它,什么时候不应该使用它。”因此,您可以提供任何确定、正确的示例,最好是有文献支持的,适用于IA32或x86-64。


2
一些x86微架构保证对于更改映射的有效页面的一致性页行走,而这些页面不在TLB中。例如,在英特尔SnB系列CPU上,如果在使用它的指令之前发生PTE更改,则会拒绝推测性TLB加载。显然,Win95依赖此功能,但AMD Bulldozer系列不支持此功能。 - Peter Cordes
@PeterCordes 你可以把这些内容加入到回答中。 - Antti Haapala -- Слава Україні
2个回答

28
在最简单的术语中,要求在任何依赖更改发生之前,必须使CPU的TLB无法记住的任何东西失效。
CPU可以记住的东西包括:
- 页的最终权限(来自页面表条目、页面目录条目等的读/写/执行权限的组合);其中包括页面是否存在(请参见下面的警告) - 页的物理地址 - “访问”和“脏”标志 - 影响缓存的标志 - 是否为普通页或大(2或4 MiB)页或巨大(1 GiB)页
警告:由于英特尔CPU不记住“不存在”的页面,因此英特尔的文档可能会说,在将页面从“不存在”更改为“存在”时,不需要使其失效。英特尔的文档仅适用于英特尔CPU,而不适用于所有80x86 CPU。一些CPU(主要是Cyrix)确实记住了页面何时“不存在”,因此由于这些CPU,当将页面从“不存在”更改为“存在”时,您确实需要使其失效。
注意,由于推测执行,您不能省略步骤。例如,如果您知道某个页面从未被访问过,您不能假设它不在TLB中,因为TLB可能已被推测性地获取。
我非常谨慎地选择了“在任何依赖更改发生之前”这些词。现代CPU(特别是长模式)确实缓存更高级别的分页结构(例如PDPT条目),而不仅仅是最终页面。这意味着,如果您更改了更高级别的分页结构但页面表条目本身保持不变,则仍需使其失效。这也意味着,如果没有任何东西依赖于更改,那么可以跳过失效。这方面的一个简单例子是使用了"accessed"和"dirty"标志-如果您没有依赖这些标志(以确定"最近最少使用"和要发送到交换空间的页面),那么如果CPU没有意识到您对它们进行了更改,那么这并不重要。在某些情况下,可以跳过TLB失效(不建议单CPU使用,但非常建议多CPU使用),例如,在CPU使用旧/陈旧的TLB信息时会发生页面故障的情况下,其中页面故障处理程序仅在实际需要时使其失效。
此外,"CPU的TLB可以记住的任何内容"有点棘手。通常,操作系统会将分页结构本身映射到虚拟地址空间中,以便快速/轻松地访问它们(例如,常见的"递归映射"技巧,其中假装页面目录是页面表)。在这种情况下,当您更改页面目录条目时,您需要使受影响的普通页面失效(正如您所期望的那样),但您还需要失效更改对任何映射产生的影响。
对于INVLPG或重新加载CR3的使用,存在几个问题。对于单个页面,INVLPG将更快。如果更改页面目录(影响1024页或512页,具体取决于分页的类型),则使用循环中的INVLPG可能比仅重新加载CR3更昂贵(这取决于CPU /硬件以及失效后代码访问模式)。
还有两个问题需要解决。第一个是任务切换。在切换使用不同虚拟地址空间的任务时,必须更改CR3。这意味着,如果更改影响大面积(例如,页面目录),则可以通过尽早进行任务切换来提高整体性能,而不是现在重新加载CR3(用于失效)然后不久重新加载CR3(用于任务切换)。基本上,它是"一石二鸟"的优化。另一个重点是“全局页面”。通常,在所有虚拟地址空间中都有相同的页面(例如内核)。当您重新加载CR3时(例如在任务切换期间),您不希望为保持相同的页面使TLB无效,因为那样会比必要的影响性能。为了解决这个问题并提高性能,(对于Pentium及更高版本)有一个名为“全局页面”的功能,您可以将这些公共页面标记为全局页面,当您重新加载CR3时它们不会被清除。在这种情况下,如果您需要使全局页面无效,则需要使用INVPLG或更改CR4(例如禁用然后重新启用全局页面功能)。对于较大的区域(例如更改页面目录而不仅仅是一个页面),与以前相同(在循环中调整CR4可能比INVLPG更快或更慢)。

2
太好了,特别是Cyrix部分,这正是我认为在Stack Overflow上提出这个问题的好处所在。 - Antti Haapala -- Слава Україні

6
对于你的第一个问题:
  1. 你可以始终使用 INVLPG,并且可以进行任何可能的更改。使用 INVLPG 总是安全的。
  2. 重新加载 CR3 不会使 TLB 中的全局页面无效。因此,有时必须使用 INVLPG,因为重新加载 CR3 没有影响。
  3. 必须为涉及到的每个页面使用 INVLPG。如果您同时更改多个页面,则在某一点上,重新加载 CR3 比大量调用 INVLPG 更快。
  4. 不要忘记现代 CPU 上的 ASID 扩展 (Address Space Identifier extension)
对于你的第二个问题:

未映射的页面不能被缓存在 TLB 中(假设您在之前取消映射时正确地使其无效)。因此,从不存在变化不需要 INVLPGCR3 重新加载。


1
哦,不要忘记SMP,你还需要在其他核心中关闭该条目。 - Goswin von Brederlow
我增加了单处理器系统约束,所以现在不需要考虑SMP。 - Antti Haapala -- Слава Україні

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接