序幕
我是操作系统爱好者,并且我的内核运行在80486+上,并且已经支持虚拟内存。
从80386开始,Intel的x86处理器家族及其各种克隆都支持带有分页的虚拟内存。众所周知,当设置了PG
中的CR0
位时,处理器将使用虚拟地址转换。然后,CR3
寄存器指向顶级页面目录,该目录是2-4级页面表结构的根,这些页面表结构将虚拟地址映射到物理地址。
处理器不会为生成的每个虚拟地址查询这些表,而是将它们缓存在称为Translation Lookaside Buffer或TLB的结构中。但是,对页表进行更改时,需要刷新TLB。在80386处理器上,将通过以下方式完成刷新:
使用顶级页面目录地址或任务开关重新加载(MOV
)CR3
。据推测,这将无条件刷新所有TLB条目。据我了解,虚拟存储系统总是在更改任何之后始终重新加载CR3 是完全有效的。
这很浪费,因为TLB现在会抛出完全正确的条目,因此在80486处理器中引入了 INVLPG
指令。 INVLPG
将使与源操作数地址匹配的TLB条目无效。
但是从Pentium Pro开始,我们还拥有一些全局页面,这些页面不会随着CR3
或任务开关的移动而刷新。 AMD x86-64 ISA表示某些上层页面表结构可能会被缓存,并且不会因INVLPG
无效。要获得每个ISA所需和不需要的内容的连贯图片,实际上需要下载1000页的数据表,以了解自80年代以来发布的大量ISA,以阅读其中的几页,即使如此,这些文档似乎对于TLB无效以及如果未正确使TLB无效会发生什么,请特别模糊。
题
为了简单起见,可以假设是我们在谈论的单处理器系统。此外,可以假定更改页面结构之后不需要任务切换。 (因此,据说INVLPG
总是至少与重新加载CR3
寄存器一样好)。
基本假设是,每次对页表和页目录进行更改后,都需要重新加载CR3
,这样的系统是正确的。但是,如果要避免不必要地刷新TLB,则需要回答以下两个问题:
INVLPG
,那么在进行了哪些更改之后,可以安全地使用它而不用重新加载CR3
吗?例如。 “如果一个人取消映射一个页面框架(将相应的表条目设置为不存在),则始终可以使用INVLPG
”。 CR3
或执行INVLPG
的情况下,可以对表和目录进行哪些更改?例如。 “如果一个页面根本没有映射(不存在),则可以为它编写一个带有Present=1
的PTE,而根本不刷新TLB”? 即使在Stack Overflow上阅读了相当多的ISA文档和与
INVLPG
相关的所有内容之后,我个人也不知道在此提供的任何示例。确实,一个notable post立刻指出:“我不知道您何时应该使用它,何时不该使用。”因此,对于您可以提供的,对于IA32或x86-64而言,最好是有文献记载的某些正确正确的示例,我们深表感谢。 最佳答案
用最简单的术语要求是在依赖于更改的所有内容发生之前,CPU的TLB可能已经记住的所有更改都必须无效。
CPU可能记得的事情包括:
警告:因为Intel CPU不会记住“不存在”页面,所以来自英特尔的文档可能会说,将页面从“不存在”更改为“当前”时,您不需要无效。英特尔文档仅适用于英特尔CPU。并非所有80x86 CPU都正确。某些CPU(主要是Cyrix)确实会记住页面“不存在”的时间,由于这些CPU,在将页面从“不存在”更改为“当前”时,您确实必须使它们无效。
请注意,由于投机执行,您不能偷工减料。例如,如果您知道从未访问过某个页面,则不能认为该页面不在TLB中,因为该TLB可能是通过推测方式获取的。
我非常仔细地选择了“在任何依赖变化的事情发生之前”一词。现代CPU(尤其是长模式)确实缓存了更高级别的分页结构(例如PDPT条目),而不仅仅是最终页面。这意味着,如果您更改了更高级别的分页结构,但页表条目本身保持不变,则仍然需要使它们无效。
这也意味着,如果不依赖更改,则可以跳过失效。一个简单的例子就是访问标志和脏标志-如果您不依赖这些标志(确定“最近最少使用”以及要发送到交换空间的页面),那么CPU并不需要没有意识到您已经更改了它们。如果CPU使用的是旧的/过时的TLB信息,则在出现页面错误的情况下,也可能跳过TLB无效(不建议用于单CPU,但建议用于多CPU)处理程序仅在实际必要时才无效。
此外; “CPU的TLB可能还记得的任何东西”有些棘手。通常,操作系统会将分页结构本身映射到虚拟地址空间中,以允许快速/轻松地访问它们(例如,假装页面目录是页表的常见“递归映射”技巧)。在这种情况下,当您更改页面目录条目时,您需要使受影响的普通页面无效(如您所期望的),但是您还需要使任何在映射中影响的更改无效。
要使用哪些工具(INVLPG或重新加载CR3),存在几个问题。对于单个页面,INVLPG将更快。如果更改页面目录(影响1024页或512页,具体取决于分页的样式),则在循环中使用INVLPG可能比仅重新加载CR3更为昂贵(或取决于它)(取决于CPU /硬件和访问模式)无效后的代码)。
还有另外两个问题。首先是任务切换。在使用不同虚拟地址空间的任务之间切换时,必须更改CR3。这意味着,如果您更改了会影响较大区域的内容(例如页面目录),则可以通过尽早执行任务切换来提高整体性能,而不是立即重新加载CR3(以使无效),然后再重新加载CR3(对于任务切换) )。基本上,这是“用一只石头杀死2只鸟”的优化。
另一件事是“全局页面”。通常,所有虚拟地址空间(例如内核)中的页面都是相同的。当您重新加载CR3时(例如在任务切换过程中),您不希望无故使那些保持不变的页面的TLB无效,因为这会对性能造成不必要的损害。为了解决这个问题并提高性能,(对于Pentium和更高版本)有一个称为“全局页面”的功能,您可以将这些通用页面标记为全局页面,并且在重新加载CR3时它们不会失效。在这种情况下,如果您需要使全局页面无效,则需要使用INVPLG或更改CR4(例如,禁用然后重新启用全局页面功能)。对于较大的区域(例如,更改页面目录,而不仅仅是更改页面),该操作与以前相同(循环中使用CR4的速度可能比INVLPG更快或更慢)。
关于x86 - 何时执行或不执行INVLPG,将MOV移至CR3以最小化TLB冲洗,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/28384234/