内存虚拟化-分段
2023-1-10
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property
 
 
                   一个地址空间
一个地址空间
 
 
 
 
 
如果将整个地址空间放入物理内存,那么栈和堆之间的空间并没有被进程使用,却依然占用了实际的物理内存。因此,简单的通过基址寄存器和界限寄存器实现的虚拟内存很浪费。另外,如果剩余物理内存无法提供连续区域来放置完整的地址空间,进程便无法运行。
 
 
怎样支持大地址空间,同时栈和堆之间(可能)有大量空闲空间?
为了解决这个问题,分段(segmentation)的概念应运而生:在MMU中引入不止一个基址和界限寄存器对,而是给地址空间内的每个逻辑段(segment)一对。一个段只是地址空间里的一个连续定长的区域,在典型的地址空间里有3个逻辑不同的段:代码、栈和堆。分段的机制使得操作系统能够将不同的段放到不同的物理内存区域,从而避免了虚拟地址空间中的未使用部分占用物理内存。
 
               在物理内存中放置段
在物理内存中放置段
只有已用的内存才在物理内存中分配空间,因此可以容纳巨大的地址空间,其中包含大量未使用的地址空间(稀疏地址空间,sparse address spaces)
 
需要MMU中的硬件结构来支持分断:在这种情况下,需要一组3对基址和界限寄存器
notion image
 
注:段错误指的是在支持分段的机器上发生了非法的内存访问
 
来看一个堆中的地址,虚拟地址4200。如果用虚拟地址4200 加上堆的基址(34KB),得到物理地址39016,这不是正确的地址。首先应该先减去堆的偏移量,即该地址指的是这个段中的哪个字节。因为堆从虚拟地址4K(4096)开始,4200 的偏移量实际上是4200 减去4096,即104,然后用这个偏移量(104)加上基址寄存器中的物理地址(34KB),得到真正的物理地址34920。
 
 
硬件在地址转换时使用段寄存器。它如何知道段内的偏移量,以及地址引用了哪个段?
一种常见的方式,有时称为显式(explicit)方式,就是用虚拟地址的开头几位来标识不同的段,VAX/VMS 系统使用了这种技术。之前的例子中,有3 个段,因此需要两位来标识。
如果前两位是00,硬件就知道这是属于代码段的地址,因此使用代码段的基址和界限来重定位到正确的物理地址。如果前两位是01,则是堆地址,对应地,使用堆的基址和界限。下面来看一个4200 之上的堆虚拟地址,进行进制转换,确保弄清楚这些内容。虚拟地址4200 的二进制形式如下:
notion image
前两位(01)告诉硬件引用哪个段。剩下的12 位是段内偏移:0000 0110 1000(十六进制0x068 或十进制104)。因此,硬件就用前两位来决定使用哪个段寄存器,然后用后12位作为段内偏移。偏移量与基址寄存器相加,硬件就得到了最终的物理地址。
偏移量也简化了对段边界的判断。只要检查偏移量是否小于界限,大于界限的为非法地址。因此,如果基址和界限放在数组中(每个段一项),为了获得需要的物理地址,硬件会做下面这样的事:
 
硬件还有其他方法来决定特定地址在哪个段。在隐式(implicit)方式中,硬件通过地址产生的方式来确定段。例如,如果地址由程序计数器产生(即它是指令获取),那么地址在代码段。如果基于栈或基址指针,它一定在栈段。其他地址则在堆段。
 
反向增长
栈被重定位到物理地址28KB。但有一点关键区别,它反向增长。在物理内存中,它始于28KB,增长回到26KB,相应虚拟地址从16KB 到14KB。地址转换必须有所不同。
除了基址和界限外,硬件还需要知道段的增长方向(用一位区分,比如1 代表自小而大增长,0 反之)。
notion image
 
 
支持共享
要节省内存,有时候在地址空间之间共享(share)某些内存段是有用的。为了支持共享,需要一些额外的硬件支持,这就是保护位(protection bit)。基本为每个段增加了几个位,标识程序是否能够读写该段,或执行其中的代码。通过将代码段标记为只读,同样的代码可以被多个进程共享,而不用担心破坏隔离。虽然每个进程都认为自己独占这块内存,但操作系统秘密地共享了内存,进程不能修改这些内存。
notion image
硬件算法除了检查虚拟地址是否越界,还需要检查特定访问是否允许。如果用户进程试图写入只读段,或从非执行段执行指令,硬 件会触发异常,让操作系统来处理出错进程。
 
细粒度与粗粒度的分段
到目前为止,例子大多针对只有很少的几个段的系统(即代码、栈、堆)。可以认为这种分段是粗粒度的(coarse-grained),因为它将地址空间分成较大的、粗粒度的块。但是,一些早期系统(如Multics)更灵活,允许将地址空间划分为大量较小的段,这被称为细粒度(fine-grained)分段。
支持许多段需要进一步的硬件支持,并在内存中保存某种段表(segment table)。这种段表通常支持创建非常多的段,因此系统使用段的方式,可以比之前讨论的方式更灵活。例如,像Burroughs B5000 这样的早期机器可以支持成千上万的段,有了操作系统和硬件的支持,编译器可以将代码段和数据段划分为许多不同的部分。当时的考虑是,通过更细粒度的段,操作系统可以更好地了解哪些段在使用哪些没有,从而可以更高效地利用内存。
 
 
操作系统支持
分段的基本原理。系统运行时,地址空间中的不同段被重定位到物理内存中。与整个地址空间只有一个基址/界限寄存器对的方式相比,大量节省了物理内存。具体来说,栈和堆之间没有使用的区域就不需要再分配物理内存,能将更多地址空间放进物理内存。
分段也带来了一些新的问题:
  • 操作系统在上下文切换时应该做什么?
    • 各个段寄存器中的内容必须保存和恢复。显然,每个进程都有自己独立的虚拟地址空间,操作系统必须在进程运行前,确保这些寄存器被正确地赋值。
  • 管理物理内存的空闲空间
    • 新的地址空间被创建时,操作系统需要在物理内存中为它的段找到空间。之前,假设所有的地址空间大小相同,物理内存可以被认为是一些槽块,进程可以放进去。现在,每个进程都有一些段,每个段的大小也可能不同。 一般会遇到的问题是,物理内存很快充满了许多空闲空间的小洞,因而很难分配给新的段,或扩大已有的段。这种问题被称为外部碎片(external fragmentation)。
      如图所示,,一个进程需要分配一个20KB 的段。当前有24KB 空闲,但并不连续(是3 个不相邻的块)。因此,操作系统无法满足这个20KB 的请求。
                                                                             非紧凑和紧凑的内存
      非紧凑和紧凑的内存
      该问题的一种解决方案是紧凑(compact)物理内存,重新安排原有的段。例如,操作系统先终止运行的进程,将它们的数据复制到连续的内存区域中去,改变它们的段寄存器中的值,指向新的物理地址,从而得到了足够大的连续空闲空间。这样做,操作系统能让新的内存分配请求成功。但是,内存紧凑成本很高,因为拷贝段是内存密集型的,一般会占用大量的处理器时间。
      一种更简单的做法是利用空闲列表管理算法,试图保留大的内存块用于分配。相关的算法可能有成百上千种,包括传统的最优匹配(best-fit,从空闲链表中找最接近需要分配空间的空闲块返回)、最坏匹配(worst-fit)、首次匹配(first-fit)以及像伙伴算法(buddy algorithm)这样更复杂的算法。但遗憾的是,无论算法多么精妙,都无法完全消除外部碎片,因此,好的算法只是试图减小它。
       
  • 计算机基础
  • 操作系统
  • 内存虚拟化-机制:地址转换内存虚拟化-空闲空间管理
    目录