往期精选
『内存中的操作系统』虚拟化是什么
『内存中的操作系统』内存虚拟化又是什么
『内存中的操作系统』如何高效, 灵活的虚拟化内存(1)
思维模型1. 硬件在地址转换时使用段寄存器。它如何知道段内的偏移量,以及地址引用了哪个段?2. 分段之后, 我们有没有新的功能可以实现?
3. 分段有没有什么问题?
1. 硬件如何知道引用了哪个段?在前一篇文章中, 我们介绍了分段的设计, 那么硬件在地址转换时, 是如何知道引用了哪个段, 又是如何知道段内的偏移量呢?
我们可以想到的是直接显式地标识出来, 我们可以用虚拟地址的前几位来标识不同的段, 还是拿我们上面所说的三个段(代码,堆,栈)来标识, 因为有3个栈, 所以我们用两位字节来表示, 如下:
在我们的例子中,如果前两位是00,硬件就知道这是属于代码段的地址,因此使用代码段的基址和界限来重定位到正确的物理地址。如果前两位是01,则是堆地址,对应地,使用堆的基址和界限。下面来看一个4200之上的堆虚拟地址,进行进制转换,确保弄清楚这些内容。虚拟地址4200的二进制形式如下:
从图中可以看到,前两位(01)告诉硬件我们引用哪个段。剩下的12位是段内偏移:0000 0110 1000(即十六进制0x068或十进制104)。因此,硬件就用前两位来决定使用哪个段寄存器,然后用后12位作为段内偏移。偏移量与基址寄存器相加,硬件就得到了最终的物理地址。
当然除了这种方式, 我们当然还可以通过地址是如何产生的来判断, 比如地址由程序计数器产生(即它是指令获取),那么地址在代码段。如果基于栈或基址指针,它一定在栈段。其他地址则在堆段。这种被称为隐式推断.
2. 我们有没有新的功能可以实现?随着分段机制的不断改进,系统设计人员很快意识到,通过再多一点的硬件支持,就能实现新的效率提升。具体来说,要节省内存,有时候在地址空间之间共享(share)某些内存段是有用的。尤其是,代码共享很常见,今天的系统仍然在使用。
为了支持共享,需要一些额外的硬件支持,这就是保护位(protection bit)。基本为每个段增加了几个位,标识程序是否能够读写该段,或执行其中的代码。通过将代码段标记为只读,同样的代码可以被多个进程共享,而不用担心破坏隔离。虽然每个进程都认为自己独占这块内存,但操作系统秘密地共享了内存,进程不能修改这些内存,所以假象得以保持。
从而我们在分段的基础上实现了共享内存的功能, 而这项功能能够有效的节省内存的占用.
3. 分段有没有什么问题?在我们实现任何一项设计之后, 除了高兴, 我们还要去思考我们目前的设计有没有什么问题, 有没有哪些地方还可以完善. 有没有带来什么新的副作用. 这里我们简单来说几个思考方向:
我们目前只考虑了硬件上面的改动, 即在内存管理单元MMU中增加寄存器的数量之外, 同时修改了其中的逻辑代码, 包括内存访问和使用. 那么我们操作系统需不需要做些改动呢? 比如操作系统在上下文切换时应该做什么?你可能已经猜到了:各个段寄存器中的内容必须保存和恢复。显然,每个进程都有自己独立的虚拟地址空间,操作系统必须在进程运行前,确保这些寄存器被正确地赋值。
另一个问题其实是我们没解决的问题, 那就是管理物理内存的空闲空间, 新的地址空间被创建时,操作系统需要在物理内存中为它的段找到空间。之前,我们假设所有的地址空间大小相同,物理内存可以被认为是一些槽块,进程可以放进去。现在,每个进程都有一些段,每个段的大小也可能不同。
一般会遇到的问题是,物理内存很快充满了许多空闲空间的小洞,因而很难分配给新的段,或扩大已有的段。这种问题被称为外部碎片(external fragmentation).如下图:
在这个例子中,一个进程需要分配一个20KB的段。当前有24KB空闲,但并不连续(是3个不相邻的块)。因此,操作系统无法满足这个20KB的请求。这个问题显然是需要解决的, 那么如何解决, 我们在下一篇文章将继续分析.