大多数现代 CPU 实现虚拟寻址/虚拟内存 - 当程序引用特定地址时,该地址是虚拟的;到物理页面的映射(如果有)由 CPU 的 MMU(内存管理单元)实现。 CPU 通过在为当前进程设置的操作系统中查找page table 将每个虚拟地址转换为物理地址。这些查找由TLB 缓存,因此大多数时候没有额外的延迟。 (在一些非 x86 CPU 设计中,TLB 未命中由操作系统在软件中处理。)
所以我的程序访问地址 0x8050,它位于虚拟页面 8 中(假设标准的 4096 字节 (0x1000) 页面大小)。 CPU 看到虚拟页面 8 映射到物理页面 200,因此在物理地址200 * 4096 + 0x50 == 0xC8050 处执行读取。
当 CPU 没有该虚拟地址的 TLB 映射时会发生什么?这种事情经常发生,因为 TLB 的大小有限。答案是 CPU 产生 page fault,由 OS 处理。
页面错误会导致几种结果:
- 第一,操作系统可以说“哦,它只是不在 TLB 中,因为我装不下它”。操作系统从 TLB 中驱逐一个条目,并使用进程的页表映射填充新条目,然后让进程继续运行。在中等负载的机器上,这种情况每秒会发生数千次。 (在具有硬件 TLB 未命中处理的 CPU 上,例如 x86,这种情况是在硬件中处理的,甚至不是“次要”页面错误。)
- 第二,操作系统可以说“哦,那个虚拟页面现在没有映射,因为它正在使用的物理页面被交换到磁盘,因为我的内存不足”。操作系统挂起进程,找到一些要使用的内存(可能通过换出一些其他虚拟映射),将磁盘读取排队等待请求的物理内存,当磁盘读取完成时,使用新填充的页表映射恢复进程。 (这是"major" page fault。)
- 三,进程正在尝试访问不存在映射的内存 - 它正在读取不应该的内存。这通常称为分段错误。
相关情况是第 3 种情况。当发生段错误时,操作系统的默认行为是中止进程并执行诸如写出核心文件之类的操作。但是,允许进程捕获自己的段错误并尝试处理它们,甚至可能不停止。这就是事情变得有趣的地方。
我们可以利用这一点来执行“硬件加速”索引检查,但我们在尝试这样做时遇到了更多的绊脚石。
首先,总体思路:对于每个数组,我们将其放在自己的虚拟内存区域中,所有包含数组数据的页面都照常映射。在真实数组数据的两侧,我们创建了不可读和不可写的虚拟页面映射。如果您尝试读取数组之外的内容,则会产生页面错误。编译器在编写程序时会插入自己的页错误处理程序,并处理页错误,将其变成索引越界异常。
第一个障碍是我们只能将整个页面标记为可读或不可读。数组大小可能不是页面大小的偶数倍,所以我们遇到了一个问题——我们不能在数组末尾之前和之后放置栅栏。我们能做的最好的事情是在数组开始之前或数组结束之后在数组和最近的“栅栏”页面之间留一个小间隙。
他们如何解决这个问题?好吧,在 Java 的情况下,编译执行负索引的代码并不容易。如果是这样,无论如何都没有关系,因为负索引被视为无符号,这将索引置于数组开头的位置,这意味着它很可能会命中未映射的内存并且无论如何都会导致错误.
所以他们所做的就是对齐数组,使数组的末尾正好与页面的末尾对接,就像这样('-' 表示未映射,'+' 表示已映射):
-----------++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
| Page 1 | Page 2 | Page 3 | Page 4 | Page 5 | Page 6 | Page 7 | ...
|----------------array---------------------------|
现在,如果索引超出了数组的末尾,它将到达未映射的第 7 页,这将导致页面错误,这将变成索引越界异常。如果索引在数组的开头之前(也就是说,它是负数),那么因为它被视为一个无符号值,它会变得非常大并且是正数,使我们再次远远超过第 7 页,导致读取未映射的内存,导致页面错误,这将再次变成索引越界异常。
第 2 个障碍 是我们确实应该在映射下一个对象之前在数组末尾留下 大量 未映射的虚拟内存,否则,如果index 超出范围,但是远远超出范围,它可能会命中有效页面并且不会导致 index-out-of-bounds 异常,而是会读取或写入任意内存。
为了解决这个问题,我们只使用了大量的虚拟内存——我们将每个数组放入自己的 4 GiB 内存区域,其中只有前 N 几页实际映射。我们可以这样做,因为我们只是在这里使用地址空间,而不是实际的物理内存。一个 64 位进程有大约 40 亿块 4 GiB 的内存区域,所以在我们用完之前我们有足够的地址空间可以使用。在 32 位 CPU 或进程上,我们可以使用的地址空间非常少,因此这种技术不太可行。事实上,今天的许多 32 位程序都用完了虚拟地址空间,只是试图访问真实内存,更不用说尝试在该空间中映射空的“栅栏”页面以尝试用作“硬件加速”索引范围检查。