两个不同的用户进程具有不同的虚拟地址空间。由于虚拟↔物理地址映射是不同的,所以在从一个用户进程切换到另一个用户进程时,TLB缓存会被使无效。这非常昂贵,因为如果TLB中未缓存地址,则任何内存访问都将导致故障和PTE的遍历。
Syscall涉及两个上下文切换:用户→内核,然后内核→用户。为了加快速度,通常会将前1GB或2GB的虚拟地址空间保留给内核使用。由于虚拟地址空间在这些上下文切换中不会改变,因此无需进行TLB清除。这是通过每个PTE中的用户/监管位实现的,该位确保内核内存仅在内核空间中可访问;即使页面表相同,用户空间也不能访问。
如果存在两个专门用于内核使用的TLBs的硬件支持,则这种优化将不再有用。但是,如果您有足够的空间可以分配,那么制作一个更大的TLB可能更值得。
在x86上,Linux曾经支持一种称为“4G/4G分裂”的模式。在此模式下,用户空间可以完全访问整个4GB虚拟地址空间,而内核也有完整的4GB虚拟地址空间。如上所述,代价是每个 syscall都需要进行TLB清除,以及更复杂的例程来在用户和内核内存之间复制数据。这已被证明会造成高达30%的性能损失。
自从这个问题最初被提出并回答以来,时代已经发生了变化:64位操作系统现在更加普遍。在x86-64上的当前操作系统中,允许用户程序使用的虚拟地址范围为0到2
47-1(即0-128TB),而内核永久驻留在虚拟地址从2
47×(2
17-1)到2
64-1的范围内(或者如果您将地址视为有符号整数,则为-2
47到-1)。
如果在64位Windows上运行32位可执行文件会发生什么?你可能认为所有的虚拟地址从0到2
32(即0-4GB)都很容易被使用,但为了避免暴露现有程序中的错误,32位可执行文件仍然限制在0-2GB之内,除非使用
/LARGEADDRESSAWARE
重新编译。对于那些做了这样的处理的程序,它们就可以访问全部的0-4GB。(这不是一个新标志;在带有
/3GB
开关的32位Windows内核中运行时也适用相同的规则,该开关将默认的2G/2G用户/内核分离变为3G/1G,尽管当然3-4GB仍然超出范围。)
可能存在哪些错误?例如,假设您正在实现快速排序并有两个指针
a
和
b
分别指向数组的开头和结尾。如果您选择中位数作为枢轴,使用表达式
(a+b)/2
,只要这两个地址都在2GB以下,它就可以正常工作,但如果它们都在2GB以上,那么加法将遇到整数溢出,并且结果将落在数组之外。(正确的表达式是
a+(b-a)/2
。)
顺便提一下,32位Linux系统默认采用3G/1G用户/内核分割方式,程序历史上运行在2-3GB堆栈范围内,因此任何这类编程错误很可能会很快被排除。64位Linux系统可让32位程序访问0-4GB内存。