操作系统 内存虚拟化

本文将介绍操作系统对内存的虚拟化。

一、内存虚拟化的目标

  • 透明:操作系统虚拟内存,应该让运行的程序不可见,程序应该认为自己拥有完整的、物理的内存空间
  • 效率:操作系统应该用更少的时间、空间消耗实现虚拟化
  • 保护:操作系统应该确保各个进程的内存空间不会相互影响

二、地址转换

1. 什么是地址转换?

地址转换,又被称为基于硬件的地址转换。

利用地址转换,硬件对每次内存访问进行处理,进程进行内存访问时指定 “它所认为的地址”,由操作系统将指令中的虚拟地址转换为物理地址。

2. 硬件支持

具体来说,每个 CPU 需要两个硬件寄存器:

  • 基址寄存器:用于记录物理地址起始位置
  • 界限寄存器:用于记录可用空间范围

对于每一个访问内存的指令,CPU 会通过基址寄存器将虚拟地址转换为物理地址,并通过界限寄存器避免进程访问额外的内存空间

3. 操作系统支持

操作系统应该做的事情有:

  • 在进程创建时,为进程分配对应的内存空间

  • 在进程终止时,回收内存

  • 在进程切换时,操作系统必须保存和恢复两个硬件寄存器(基址寄存器、界限寄存器)

    每个 CPU 只有两个硬件寄存器,因此应该在进程切换时,将两个寄存器中记录的内容切换

  • 提供异常处理程序,在程序视图越界访问内存时,进行异常处理

三、分段

1. 问题说明

当进程需要空间时,如果简单地将一整块空间直接完整划分给它使用,将可能出现以下问题:

  • 内存空间可能有大量的闲置,导致物理资源的浪费
  • 每次分配内存时,都需要一片完整的内存空间,分配所需较大

2. 分段的含义

将进程所需的内存空间按照逻辑划分为若干类型。

典型的三种类型:代码、栈、堆

每种类型在物理内存中均有着各自的一片内存空间。

每种类型均有一对硬件寄存器。

当需要为进程分配内存空间时,分别分配各个类型的内存空间。

四、空闲空间管理

1. 底层机制

(1) 空闲列表

操作系统会维护一个空闲列表,说明内存中的所有空闲空间。

(2) 分割

如果所需的空间小于 “空闲空间块” 的大小,则 “空闲空间块” 将被分割走一部分,变为更小的 “空闲空间块”。

(3) 合并

空间被归还时,如果该空间与 “空闲空间块” 相邻,则 “空闲空间块” 会与该空间合并,变成更大的 “空闲空间块”。

(4) 已分配空间大小的记录

程序在释放已分配空间时,通常不会手动指定空间大小,只用将空间对应的指针交给 free() 即可。

因此,操作系统需要自己记住已分配空间的大小。

操作系统通常会在分配空间的同时额外分配一个区域,该区域在被分配空间之前,被称为头块,其中存储着被分配空间的大小。

2. 基本策略

(1) 最优匹配

遍历整个空闲列表,找到所有大于等于请求大小的空闲块,返回其中最小的一个。

(2) 最差匹配

找到最大的空闲块,将其分割,一部分满足请求,一部分继续作为空闲块。

(3) 首次匹配

找到一块足够大的空闲块,将其分割,一部分满足请求,一部分继续作为空闲块。

(4) 下次匹配

不同于首次匹配每次都从空闲列表头开始查找,下次匹配会额外维护一个指针,该指针指向上一次查找结束的位置,下次便可以从指针处开始查找。

通过这种方式,避免了每次都从空闲列表的头部开始查找。

3. 其它策略

(1) 分离空闲列表

增加一个独立的列表,用于管理几种固定大小的、经常被申请的内存空间。将固定大小的、频繁的请求都交由分离空闲列表处理,其它大小的请求再由更通用的内存分配程序处理。

(2) 伙伴系统

该方法用于简化合并操作。

将内存空间递归地二分,当块被释放时,只需要递归地检查并合并块的伙伴(二叉树兄弟结点),就可以将空闲内存空间合并。

五、分页

1. 什么是分页?

为了便于空间管理,将内存空间划分为若干物理块,同时将进程所需空间划分为若干页,块与页的大小相同,可以将进程的任一页放置在任一物理块中。

为了记录每个进程的各个页放置到哪个块中,操作系统通常会为每个进程设置一个页表,用于保存页的映射信息。

为了使进程能够访问到实际的地址,操作系统会在进程访问内存时做地址转换。

2. 页表的性能问题

如果引入分页机制,可能会带来较高的性能开销。

首先,要将内存划分为若干个块;其次,需要通过页表记录这些块的映射信息;最后,每次进行内存访问时,都需要额外访问页表(以进行地址转换)。

3. 页表的性能问题解决方案 - TLB

(1) 什么是 TLB?

TLB,translation-lookaside buffer,地址转接旁路缓冲存储器,又名地址转换缓存。TLB 是一个硬件,它是对地址映射关系的缓存。

(2) TLB 如何工作?

对每次内存访问,硬件会先检查 TLB 中是否有期望的地址映射关系,如果有,快速取出,否则,访问页表,取出信息并更新 TLB。

(3) 进程切换时的 TLB 处理

每个进程都有各自的一个页表,用于存储该进程的地址映射关系,因此,在进程切换时,TLB 也应该做相应的处理。

常见的两种处理方法是:

  • 在进程切换时,清空 TLB
  • 为 TLB 增加一个进程标识符,使其能够区分不同进程的地址映射关系

(4) TLB 的数据淘汰

与其它缓存相同,TLB 也存在空间大小限制,并且常会使用 LRU 策略进行数据淘汰。

4. 页表的存储问题

页表可以非常大,因为一个进程可以需要很大的空间,这些空间将被划分为 “巨量” 的页,这些映射关系都需要保存在页表中,将会导致页表的空间占用也十分大。

5. 页表的存储问题解决方案

(1) 更大的页

将页划分得更大,映射关系就可以变少,从而页表便可以缩小。

这一方法存在一个致命问题:更大的页会导致更多的内存浪费。

(2) 分段 + 分页

不再只会进程提供一个页表,而是为每个逻辑段分别提供一个页表。这种方式可以更好地节约内存,避免分配空间的闲置。

这一方法存在的问题是:分段要求程序对内存的使用也符合规范,这是不现实的。

(3) 多级页表

不再以一个占用一块完整空间的数组存储映射关系,而是采用类似 “索引” 的方式,维护一个树形结构,从而可以将映射关系离散、分别存储在内存的各个地方。

(4) 反向页表

反向页表是对空间的极致节省。

页表不再是每个进程一个,而是所有进程共用一个,其中的项表明哪个进程在使用这个物理页、哪个虚拟页映射到这个物理页。

(5) 交换至磁盘

允许系统在内存压力较大时,将页表中的一部分交换到磁盘。

六、模拟出更大空间

1. 为什么要模拟出更大空间?

考虑这样一个情况,同时有多个用到大量内存的进程在运行,以至于将所有 “虚拟内存” 都放入 “物理内存” 中已经不可行了,此时便需要引入额外的机制来模拟一个更大的内存空间,营造出都放在内存中的假象。

2. 交换空间

交换空间,swap space,是操作系统在磁盘中开辟的专门用于与内存做交换的空间。操作系统会将页交换到其中,又在需要的时候交换出去。

3. 存在位和页错误

操作系统会在页表项中增设一个存在位,用于表示该页是否存在于内存之中。

当进行内存访问时,如果发现页表存在位为 false,则视为发生了页错误,应该从硬盘中交换页。

页错误并不是错误,可以理解为页未命中

4. 合适进行交换?

操作系统通常不会等到内存已满后才执行交换,而是更倾向于主动预留一部分空闲内存。

绝大多数操作系统会这样做:

设置高水位和低水位;当可用内存小于低水位时,交换程序开始运行,直至可用内存大于高水位。

5. 将什么页交换出内存?

(1) FIFO

先进先出,以队列形式管理内存中的页,并在需要淘汰页时淘汰最先进入队列的页。

(2) 随机

在需要淘汰页时,随机选择一个页,淘汰。

(3) LRU

按照最近最少使用原则,将最近最不常用的页淘汰。

(4) 近似 LRU

操作系统通常会采用这个方法,不再要求完美的 LRU,而是淘汰相对近的相对不常用的页。

参考

  • 操作系统导论