再次重申我们追求的内存虚拟化:

  • 高效, 因此要利用硬件支持.
  • 控制, 因此要做好隔离, 应用程序只能访问自己的内存空间.
  • 灵活, 程序要能以任何方式访问自己的内存空间, 从而让系统更易于编程.

因此我们利用了基于硬件的地址转换, 通称地址转换, 作为受限直接执行机制的补充
由硬件对每次内存访问管理, 并把虚拟地址转化为物理地址.
操作系统在关键时刻介入, 设置硬件, 记录内存占用情况等, 保持对内存使用的控制.

动态重定位机制(基址加界限机制)

每个 CPU 需要两个寄存器: 基址(base)寄存器和界限(bound)寄存器(或者称之为限制(limit)寄存器).

当程序执行的时候, 操作系统会决定其在物理内存的实际加载地址, 并且将起始地址记录于基址寄存器.
随后, 通过以下方式将虚拟地址转化为物理地址: .

进程中引用的都是虚拟地址, 作为偏移量, 由硬件加上基址, 得到物理地址, 再发给内存系统.

界限寄存器则是用来提供访问保护, 有两种方式:

  • 记录地址空间大小, 在硬件将虚拟地址和基址寄存器内容求和前检查界限.
  • 记录地址空间结束的物理地址, 在硬件将虚拟地址转化为物理地址后检查接线.

两者等价. 而一旦进程尝试访问超过这个界限的地址, CPU 触发异常, 进入内核态, 由操作系统处理.

Memory map of single relocated process

操作系统的任务

  1. 在进程创建时, 为进程的地址空间找到内存空间; 在进程终止时, 回收他的所有空间.
  2. 在上下文切换时, 在 PCB 保存和恢复进程的基址寄存器和界限寄存器.
    特别地, 进程没有运行的时候, 操作系统可以通过拷贝其地址空间并修改基址寄存器来改变进程地址空间的物理地址.
  3. 操作系统必须提供异常处理程序与一些调用的函数, 在启动的时候加载这些处理程序. 这样, 当程序试图越界访问内存, CPU 可触发异常.

不足

这个机制效率比较低下: 重定位的进程使用了固定大小的物理内存, 但是其堆栈并不一定很大,
会导致分配的内存单元内部有大量未使用空间, 也就是内碎片.

分段

正因有以上的不足, 我们采用分段的理念:
在 MMU 中引入不止一对基址和界限寄存器, 而是给内存空间每个逻辑段一对.
一个段只是地址空间中的一个连续定长的部分, 而典型的地址空间有 3 个逻辑不同的段: 代码, 栈和堆.

这是一种泛化的基址/界限. 不同的段放在不同的物理内存部分, 只有已用内存才在物理内存分配空间.
因此可以容纳巨大的地址空间, 其中包含大量未使用的地址空间(稀疏地址空间).

Segment In Physical Memory

而对于三个段, 需要三对基址/界限寄存器.

Segment Register

给定一个虚拟地址, 先分析其属于哪一段, 基址+偏移, 和界限检查是否越界.
如果越界就会产生 C 语言中熟悉的 Segment Fault.

引用哪一段?

一种常见的方式是显式方式: 用虚拟地址开头几位来标识不同的段. 后面作为段偏移量.

Segment Address

00 为代码段, 01 为堆段, 11 为栈段.

An Address Space

实际上, 这个是很合理的, 因为代码段从 0 开始, 堆从 4KB()开始, 栈从 16KB()开始(逆向生长).
因此实际上映射关系就是他们的虚拟地址高 2 位, 不需要任何额外转换.

当然, 这样会造成 10 对应的那个段被浪费, 因此也有的系统会把栈和堆视为同一个段, 因此只有最高位作为标识.

除此之外, 还有隐式方式: 通过地址产生的方式来确定段.
例如 PC 产生的地址, 则地址在代码段. 如果是基于 SP 的地址, 那么就在栈段.

栈的处理方法

由于栈是反向生长的, 地址转换会略微有些差异.

Negative-Growth Support

具体地, 多一位标志位, 表明是否是反向生长的.
而后, 地址转化时, 用 , 得到反向偏移量, 与基址相加, 和界限比较.

支持共享

为了节省内存, 可以在地址空间之间共享某些内存段.
这需要额外一些保护位, 标识程序是否可以读写该段, 或执行其中的代码.

Protection

通过将代码标记为只读, 可以把同样的代码给多个进程共享, 而不担心破坏隔离.

额外地, 现在再访问内存, 除了检查虚拟地址是否越界, 硬件还要检查访问是否允许.

操作系统支持

为了支持分段, 操作系统需要一些额外的工作.

  • 操作系统在切换上下文时, 应该保存和恢复段寄存器.
  • 操作系统需要管理物理内存的空闲空间.
    一般会遇到问题: 物理内存充满了空闲空间的小洞, 因此很难分配给新的段, 或者扩大已有的段. 这就是外碎片的问题.
    有两种方法解决:
    • 紧凑物理内存, 重新安排已有段. 但是成本较高.
    • 使用空闲列表管理算法, 试图保留大的内存块用于分配. 这种做法更简单.

Compact

优缺点

分段可以低成本地实现更高效地虚拟内存, 支持稀疏地址空间, 并且实现代码共享.

但是分断可能造成外部碎片, 并且不足以支持更一般化的稀疏地址空间.
例如一个大而稀疏的堆, 还是会被完整加载到内存中, 造成空间浪费.