读书笔记
💴《Operatring System:Three Easy Pieces》第十六章 分段
00 分钟
2022-12-16
2023-3-4
type
status
date
slug
summary
tags
category
icon
password
Property
Mar 4, 2023 02:37 AM
利用基址和界限寄存器,操作系统很容易将不同进程重定位到不同的物理内存区域。但是,对于这些 内存区域,栈和堆之间,有一大块“空闲”空间。
如果我们将整个地址空间放入物理内存,那么栈和堆之间的空间并没有被进程使用,却依然占用了实际的物理内存。因此,简单的通过基址寄存器和界限寄存器实现的虚拟内存很浪费。另外,如果剩余物理内存无法提供连续区域来放置完整的地址空间,进程便无法运行。这种基址加界限的方式看来并不像我们期望的那样灵活。
因此:关键问题:怎样支持大地址空间怎样支持大地址空间,同时栈和堆之间(可能)有大量空闲空间?

分段:泛化的基址/界限

为了解决这个问题,分段(segmentation)的概念应运而生。在 MMU 中的每个逻辑段(segment)一对基址和界限寄存器对。一个段只是地址空间里的一个连续定长的区域,在典型的地址空间里有 3 个逻辑不同的段:代码、栈和堆。分段的机制使得操作系统能够将不同的段放到不同的物理内存区域,从而避免了虚拟地址空间中的未使用部分占用物理内存 只有已用的内存才在物理内存中分配空间,因此可以容纳巨大的地址空间,其中包含大量未使用的地址空间(有时又称为稀疏地址空间,sparse address spaces)。需要 MMU 中的硬件结构来支持分断:在这种情况下,需要一组 3 对基址和界限寄存器。
notion image
比如访问 100,是在代码段中,物理地址则是 32KB+100=32868,然后判断是否在界限 32KB+2KB 内,合法时发起对物理地址的访问
比如访问 4200,是在堆段中,先找到相对于堆段起始位置偏移量 4200-4096=104,物理地址是 34KB+104=34920
补充:段错误 段错误指的是在支持分段的机器上发生了非法的内存访问。有趣的是,即使在不支持分段的机器上这个术语依然保留。但如果你弄不清楚为什么代码老是出错,就没那么有趣了。

引用哪个段

  1. 显式方式
    1. 用虚拟地址的开头几位来标识不同的段。
notion image
如果基址和界限放在数组中(每个段一项),为了获得需要的物理地址,硬件会做下面这样的事:
  1. 隐式方式
    1. 硬件通过地址产生的方式来确定段。
      例如,如果地址由程序计数器产生(即它是指令获取),那么地址在代码段。如果基于栈或基址指针,它一定在栈段。其他地址则在堆段。
因此有些系统中会将堆和栈当作同一个段,因此只需要一位来做标识。

栈怎么办

栈的增长方向和堆相反。
需要一点硬件支持。除了基址和界限外,硬件还需要知道段的增长方向(用一位区分,比如 1 代表自小而大增长,0 反之)。在表 16.2 中,我们更新了硬件记录的视图。
notion image
硬件理解段可以反向增长后,这种虚拟地址的地址转换必须有点不同。下面来看一个栈虚拟地址的例子,将它进行转换,以理解这个过程: 假设要访问虚拟地址 15KB,它应该映射到物理地址 27KB。该虚拟地址的二进制形式是:11 1100 0000 0000(十六进制 0x3C00)。硬件利用前两位(11)来指定段,但然后我们要处理偏移量 3KB。为了得到正确的反向偏移,我们必须从 3KB 中减去最大的段地址:在这个例子中,段可以是 4KB,因此正确的偏移量是 3KB 减去 4KB,即−1KB。 只要用这个反向偏移量(−1KB)加上基址(28KB),就得到了正确的物理地址 27KB。用户可以进行界限检查,确保反向偏移量的绝对值小于段的大小。

支持共享

要节省内存,有时候在地址空间之间共享(share)某些内存段是有用的。尤其是,代码共享很常见,今天的系统仍然在使用。
为了支持共享,需要一些额外的硬件支持,这就是保护位(protection bit)。基本为每个段增加了几个位,标识程序是否能够读写该段,或执行其中的代码。
通过将代码段标记为只读,同样的代码可以被多个进程共享,而不用担心破坏隔离。虽然每个进程都认为自己独占这块内存,但操作系统秘密地共享了内存,进程不能修改这些内存,所以假象得以保持。
有了保护位,前面描述的硬件算法也必须改变。除了检查虚拟地址是否越界,硬件还需要检查特定访问是否允许。如果用户进程试图写入只读段,或从非执行段执行指令,硬件会触发异常,让操作系统来处理出错进程。

细粒度与粗粒度的分段

粗粒度的(coarse-grained)分段,因为它将地址空间分成较大的、粗粒度的块。大多针对只有很少的几个段的系统(即代码、栈、堆)。
但是,一些早期系统(如 Multics[CV65, DD68])更灵活,允许将地址空间划分为大量较小的段,这被称为细粒度(fine-grained)分段。
支持许多段需要进一步的硬件支持,并在内存中保存某种段表(segment table)。这种段表通常支持创建非常多的段,因此系统使用段的方式,可以比之前讨论的方式更灵活。
通过更细粒度的段,操作系统可以更好地了解哪些段在使用哪些没有,从而可以更高效地利用内存。

操作系统支持

系统运行时,地址空间中的不同段被重定位到物理内存中。与整个地址空间只有一个基址/界限寄存器对的方式相比,大量节省了物理内存。具体来说,栈和堆之间没有使用的区域就不需要再分配物理内存,让我们能将更多地址空间放进物理内存。
操作系统在上下文切换时,各个段寄存器中的内容必须保存和恢复。每个进程都有自己独立的虚拟地址空间,操作系统必须在进程运行前,确保这些寄存器被正确地赋值。 新的地址空间被创建时,操作系统需要在物理内存中为它的段找到空间。一般会遇到的问题是会产生外部碎片(external fragmentation)。
notion image
解决方式有两种:
  • 紧凑(compact)物理内存,重新安排原有的段
    • 操作系统先终止运行的进程,将它们的数据复制到连续的内存区域中去,改变它们的段寄存器中的值,指向新的物理地址,从而得到了足够大的连续空闲空间。
      样做,操作系统能让新的内存分配请求成功。但是,内存紧凑成本很高,因为拷贝段是内存密集型的,一般会占用大量的处理器时间。
  • 空闲列表管理算法
    • 试图保留大的内存块用于分配。相关的算法可能有成百上千种,包括传统的最优匹配(best-fit,从空闲链表中找最接近需要分配空间的空闲块返回)、最坏匹配(worst-fit)、首次匹配(first-fit)以及像伙伴算法(buddy algorithm)这样更复杂的算法

小结

分段帮助我们实现了更高效的虚拟内存。
不只是动态重定位,通过避免地址空间的逻辑段之间的大量潜在的内存浪费,分段能更好地支持稀疏地址空间。它还很快,因为分段要求的算法很容易,很适合硬件完成,地址转换的开销极小。
分段还有一个附加的好处:代码共享。如果代码放在独立的段中,这样的段就可能被多个运行的程序共享。
在内存中分配不同大小的段会导致外部碎片。
分段还是不足以支持更一般化的稀疏地址空间。例如,如果有一个很大但是稀疏的堆,都在一个逻辑段中,整个堆仍然必须完整地加载到内存中。换言之,如果使用地址空间的方式不能很好地匹配底层分段的设计目标,分段就不能很好地工作。

参考

上一篇
动态规划:最简单的数位DP问题
下一篇
绿导师一段代码逼我学会宏定义😥

评论
Loading...