介绍

历史

第一台电脑

早期的计算机有一块内存,程序员将代码和数据放入其中,并在此环境中执行 CPU。鉴于计算机非常昂贵,不幸的是它会完成一项工作,停止并等待下一个工作加载到它中,然后处理那个工作。

多用户,多处理

因此,计算机很快就变得更加复杂,同时支持多个用户和/或程序 - 但是,当简单的一块内存想法开始出现问题时。如果计算机同时运行两个程序,或者为多个用户运行相同的程序 - 当然每个用户需要单独的数据 - 那么对该内存的管理变得至关重要。

例如:如果一个程序被编写为在内存地址 1000 工作,但另一个程序已经加载到那里,则无法加载新程序。解决这个问题的一种方法是使程序使用相对寻址 - 程序加载的位置并不重要,它只是相对于加载它的内存地址做了所有事情。但这需要硬件支持。

诡辩

随着计算机硬件变得越来越复杂,它能够支持更大的内存块,允许更多的同步程序,并且编写不干扰已经加载的程序的程序变得更加棘手。一个杂散内存引用不仅可以降低当前程序,还可以降低内存中的任何其他程序 - 包括操作系统本身!

解决方案

需要的是一种允许内存块具有动态地址的机制。这样一个程序可以被编写为在其识别的地址处使用其存储块 - 并且无法访问其他程序的其他块(除非一些合作允许)。

分割

实现这一目标的一种机制是 Segmentation。这允许定义所有不同大小的内存块,并且程序需要定义它想要一直访问哪个 Segment。

问题

这种技术很强大 - 但它的灵活性是一个问题。由于 Segments 基本上将可用内存细分为不同大小的块,因此这些段的内存管理是一个问题:分配,释放,增长,缩小,碎片 - 所有必需的复杂例程,有时需要大规模复制才能实现。

分页

一种不同的技术将所有内存划分为相同大小的块,称为页面,这使得分配和释放例程非常简单,并且消除了增长,缩小和碎片(除了内部碎片,这仅仅是一个问题浪费)。

虚拟寻址

通过将存储器分成这些块,可以根据需要将它们分配给不同的程序,无论程序需要什么地址。存储器的物理地址和程序所需地址之间的这种映射非常强大,并且是当今每个主要处理器(Intel,ARM,MIPS,Power 等人)存储器管理的基础。

硬件和操作系统支持

硬件自动且连续地执行重新映射,但需要内存来定义要执行的操作的表。当然,与这种重新映射相关的内务管理必须由某种东西控制。操作系统必须根据需要分配内存,并管理硬件所需的数据表以支持所需的程序。

分页功能

一旦硬件可以执行此重映射,它允许什么?主要的驱动因素是多处理 - 运行多个程序的能力,每个程序都有自己的内存,相互保护。但另外两个选项包括稀疏数据虚拟内存

每个程序都有自己的虚拟地址空间 - 一系列地址,它们可以将物理内存映射到所需的任何地址。只要有足够的物理内存可以到处(虽然参见下面的虚拟内存),但可以同时支持许多程序。

更重要的是,这些程序无法访问未映射到其虚拟地址空间的内存 - 程序之间的保护是自动的。如果程序需要通信,他们可以要求操作系统安排共享内存块 - 一块物理内存,同时映射到两个不同程序的地址空间。

稀疏数据

允许一个巨大的虚拟地址空间(通常为 4 GB,与这些处理器通常具有的 32 位寄存器相对应)本身并不浪费内存,如果该地址空间的大面积未映射的话。这允许创建巨大的数据结构,其中任何时候仅映射某些部分。想象一下每个方向上有一个 1000 字节的三维数组:通常需要十亿字节! 但是程序可以保留一个虚拟地址空间块以保留这些数据,但只能在填充它们时映射小部分。这样可以实现高效的编程,同时不会浪费内存来处理不需要的数据。

虚拟内存

上面我使用术语虚拟寻址来描述硬件执行的虚拟到物理寻址。这通常被称为虚拟内存 - 但该术语更准确地对应于使用虚拟寻址来支持提供比实际可用内存更多的内存的假设的技术。

它的工作原理如下:

  • 当程序被加载并请求更多内存时,操作系统会从可用内存中提供内存。除了跟踪已映射的内存之外,操作系统还会跟踪实际使用内存的时间 - 硬件支持标记已使用的页面。

  • 当操作系统耗尽物理内存时,它会查看已经分发的所有内存,无论使用哪个页面最少,或者使用时间最长。它将特定页面的内容保存到硬盘,记住它的位置,将其标记为原始所有者的硬件不存在,然后将页面归零并将其提供给新的所有者。

  • 如果原始所有者再次尝试访问该页面,则硬件会通知操作系统。操作系统然后分配一个新页面(可能需要再次执行上一步!),加载旧页面的内容,然后将新页面交给原始程序。

    需要注意的重要一点是,由于任何页面都可以映射到任何地址,并且每个页面的大小相同,因此只要内容保持不变,一页就可以与其他页面一样好!

  • 如果程序访问未映射的内存位置,则硬件会像以前一样通知操作系统。这一次,操作系统注意到它不是一个已被保存的页面,因此将其识别为程序中的错误,并终止它!

    这实际上是当你的应用程序神秘地消失在你身上时会发生的事情 - 也许是来自操作系统的 MessageBox。这也是(通常)导致臭名昭着的蓝屏或悲伤 Mac 的原因 - 错误的程序实际上是一个操作系统驱动程序访问它不应该的内存!

寻呼决定

硬件架构师需要对 Paging 做出一些重大决策,因为设计会直接影响 CPU 的设计! 一个非常灵活的系统会产生很高的开销,需要大量内存才能管理 Paging 基础架构本身。

一个页面应该有多大?

在硬件中,最简单的 Paging 实现是获取一个 Address 并将其分为两部分。上半部分是要访问哪个页面的指示符,而下半部分是所需字节的页面索引:

+-----------------+------------+
| Page index      | Byte index |
+-----------------+------------+

尽管小页面需要为每个程序提供大量索引,但很快就会变得明显:即使是未映射的内存,也需要表中的条目来指示这一点。

因此,使用多层索引。地址分为多个部分(下面的例子中显示了三个部分),顶部(通常称为目录)索引到下一部分,依此类推,直到最后一个字节索引进入最终页面:

+-----------+------------+------------+
| Dir index | Page index | Byte index |
+-----------+------------+------------+

这意味着 Directory 索引可以指示大量地址空间的未映射,而不需要大量的页面索引。

如何优化页表的使用?

必须映射 CPU 将进行的每个地址访问 - 因此虚拟到物理过程必须尽可能高效。如果要实现上述三层系统,那就意味着每次内存访问实际上都是三次访问:一次进入目录; 一进页表; 然后最终得到所需的数据。如果 CPU 也需要执行内务处理,例如指示此页面现在已被访问或写入,那么这将需要更多访问来更新字段。

内存可能很快,但这会在分页期间对所有内存访问造成三倍减速! 幸运的是,大多数程序都有范围内的位置 - 也就是说,如果它们访问内存中的一个位置,那么将来的访问可能就在附近。由于 Pages 不是太小,只需要在访问新页面时执行映射转换:不是绝对每次访问。

但更好的方法是实现最近访问过的 Pages 的缓存,而不仅仅是最新的。问题在于跟上页面的访问权限和未访问的内容 - 硬件必须在每次访问时扫描缓存以找到缓存的值。因此,缓存被实现为内容可寻址缓存:不是通过地址访问,而是由内容访问 - 如果请求的数据存在,则提供它,否则标记为空填充的位置。缓存管理所有这些。

此内容可寻址缓存通常称为转换后备缓冲区(TLB),并且需要由 OS 作为虚拟寻址子系统的一部分进行管理。当操作系统修改目录或页表时,它需要通知 TLB 更新其条目 - 或者只是使它们无效。