往期精选
『内存中的操作系统』虚拟化是什么
『内存中的操作系统』内存虚拟化又是什么
思维模型如何实现从虚拟地址到物理地址
在上文中, 我们讲解了如何虚拟化内存的大致思路, 并讲解了在设计之前的目标和准则. 那么接下来我们就要尝试进行详细设计我们的虚拟化内存方式.
我们最容易想到的就是基于硬件来直接做转换, 也就是硬件对每次内存访问进行处理(即指令获取、数据读取或写入),将指令中的虚拟(virtual)地址转换为数据实际存储的物理(physical)地址. 硬件对每次内存访问进行处理(即指令获取、数据读取或写入),将指令中的虚拟(virtual)地址转换为数据实际存储的物理(physical)地址.
这样的话, 我们同时还需要操作系统来设置好硬件, 以完成正确的地址转换, 同时需要管理内存, 记录已经使用和空闲的内存位置, 防止进程对内存的不合理使用.
1.1 地址转换这个就是在20世纪50年代后期出现的虚拟内存的设计思路, 为了实现上面的目标, CPU增加了两个硬件寄存器:基址(base)寄存器和界限(bound)寄存器,有时称为限制(limit)寄存器。这组基址和界限寄存器,让我们能够将地址空间放在物理内存的任何位置,同时又能确保进程只能访问自己的地址空间。即:
实际物理地址 = 虚拟地址 + 基址(base)
进程中使用的内存引用都是虚拟地址(virtual address),硬件接下来将虚拟地址加上基址寄存器中的内容,得到物理地址(physical address),再发给内存系统。
而界限寄存器则可以保证超过界限的内存访问都会被阻止, 系统会报错, 设置会终止进程, 从而可以有效的防止对内存的不合理使用.
这种基址寄存器配合界限寄存器的硬件结构是芯片中的(每个CPU一对)。有时我们将CPU的这个负责地址转换的部分统称为内存管理单元(MemoryManagement Unit,MMU)。这个在我们现在的硬件中都是有的, 不过它拥有的比我们这里说的更复杂的内容.
1.2 分段那么上面的设计有没有什么问题呢? 答案其实很清晰, 那就是逻辑过于简单了, 虽然很多时候设计越简单越好, 但是前提是要满足复杂的现实场景. 根据上面的设计, 我们可以很容易画出我们目前的内存结构:
这里你可能已经看到问题在哪了, 那就是栈和堆之间,有一大块“空闲”空间。如果我们将整个地址空间放入物理内存,那么栈和堆之间的空间并没有被进程使用,却依然占用了实际的物理内存。因此,简单的通过基址寄存器和界限寄存器实现的虚拟内存很浪费。
另外,如果剩余物理内存无法提供连续区域来放置完整的地址空间,进程便无法运行。这种基址加界限的方式看来并不像我们期望的那样灵活。
所以只有基址寄存器和界限寄存器并不能满足实际的内存使用需求. 我们还需要更为复杂的设计, 也就是分段(segmentation). 分段并不是一个新概念,它甚至可以追溯到20世纪60年代初期。这个想法很简单,在MMU中引入不止一个基址和界限寄存器对,而是给地址空间内的每个逻辑段(segment)一对。
一个段只是地址空间里的一个连续定长的区域,在典型的地址空间里有 3 个逻辑不同的段:代码、栈和堆。分段的机制使得操作系统能够将不同的段放到不同的物理内存区域,从而避免了虚拟地址空间中的未使用部分占用物理内存。
那么根据我们的设计, 内存结构可能就是如下的样子:
从图中可以看到,只有已用的内存才在物理内存中分配空间,因此可以容纳巨大的地址空间,其中包含大量未使用的地址空间(有时又称为稀疏地址空间,sparseaddress spaces)。你会想到,需要MMU中的硬件结构来支持分段:在这种情况下,需要一组3对基址和界限寄存器。下面是一组示例:
根据我们的示例, 我们来看一个例子:
假设现在要引用虚拟地址100(在代码段中),MMU将基址值加上偏移量(100)得到实际的物理地址:100 + 32KB = 32868。然后它会检查该地址是否在界限内(100小于2KB),发现是的,于是发起对物理地址32868的引用。
如果我们有学过Java虚拟机JVM的内存结构, 就会发现其中的: 堆内存, 方法区和栈的设计思路就是我们这里的设计思路.
有趣的知识: segmentation fault
如果我们试图访问非法的地址,例如7KB,它超出了堆的边界呢?你可以想象发生的情况:硬件会发现该地址越界,因此陷入操作系统,很可能导致终止出错进程。这就是每个C程序员都感到恐慌的术语的来源:段异常(segmentationviolation)或段错误(segmentation fault)。
来看一个堆中的地址,虚拟地址4200(同样参考图16.1)。如果用虚拟地址4200加上堆的基址(34KB),得到物理地址39016,这不是正确的地址。我们首先应该先减去堆的偏移量,即该地址指的是这个段中的哪个字节。因为堆从虚拟地址4K(4096)开始,4200的偏移量实际上是4200减去4096,即104,然后用这个偏移量(104)加上基址寄存器中的物理地址(34KB),得到真正的物理地址34920。
总结到这里, 我们已经有了虚拟化内存的基本思路, 沿着这条思路, 让我们在接下来的文章继续细化我们的设计. 来达到高效,灵活的效果.