type
status
date
slug
summary
tags
category
icon
password
Property
Mar 28, 2023 12:55 AM
数字设备公司(DEC)在 20 世纪 70 年代末推出了 VAX-11 小型机体系结构。VMS 是软件创新的很好例子,用于隐藏架构的一些固有缺陷。尽管操作系统通常依靠硬件来构建高效的抽象和假象,但有时硬件设计人员并没有把所有事情都做好。在 VAX 硬件中,我们会看到一些例子,也会看到尽管存在这些硬件缺陷,VMS 操作系统如何构建一个有效的工作系统。
内存管理硬件
VAX-11 为每个进程提供了一个 32 位的虚拟地址空间,分为 512 字节的页。因此,虚拟地址由 23 位 VPN 和 9 位偏移组成。此外,VPN 的高两位用于区分页所在的段。该系统是
分页和分段的混合体
。地址空间的下半部分称为“
进程空间
”,对于每个进程都是唯一的。在进程空间的前半部分(称为 P0)中,有
用户程序
和一个向下增长的堆
。在进程空间的后半部分(P1),有
向上增长
的栈。地址空间的上半部分称为
系统空间
(S),尽管只有一半被使用。受保护的操作系统代码
和数据
驻留在此处,操作系统以这种方式跨进程共享。VMS 设计人员的一个主要关注点是 VAX 硬件中的页大小
非常小
(512 字节)。由于历史原因选择的这种尺寸,存在一个根本性问题,即简单的线性页表过大。因此,VMS 设计人员的首要目标之一是确保 VMS 不会用页表占满内存。系统通过两种方式,减少了页表对内存的压力:
- 首先,通过将用户地址空间分成两部分,VAX-11 为每个进程的每个区域(P0 和 P1)提供了一个页表。因此,栈和堆之间未使用的地址空间部分不需要页表空间。。一个基址寄存器保存该段的页表的地址,界限寄存器保存其大小(即页表项的数量)。
- 其次,通过在内核虚拟内存中放置用户页表(对于 P0 和 P1,因此每个进程两个),操作系统进一步降低了内存压力。因此,在分配或增长页表时,内核在段 S 中分配自己的虚拟内存空间。如果内存受到严重压力,内核可以将这些页表的页面
交换到磁盘
,从而使物理内存可以用于其他用途。
一个真实的地址空间
补充:为什么空指针访问会导致段错误 你现在应该很好地理解一个空指针引用会发生什么。通过这样做,进程生成了一个虚拟地址 0:int *p = NULL; // set p = 0 *p = 10; // try to store value 10 to virtual address 0
硬件试图在 TLB 中查找 VPN(这里也是 0),遇到 TLB 未命中。查询页表,并且发现 VPN 0 的条目被标记为无效。因此,我们遇到无效的访问,将控制权交给操作系统,这可能会终止进程(在 UNIX 系统上,会向进程发出一个信号,让它们对这样的错误做出反应。但是如果信号未被捕获,则会终止进程)。
代码段永远不会从第 0 页开始。相反,该页被标记为不可访问,以便为检测空指针(null-pointer)访问提供一些支持。
内核虚拟地址空间(即其数据结构和代码)是每个用户地址空间的一部分。在上下文切换时,操作系统改变 P0 和 P1 寄存器以指向即将运行的进程的适当页表。但是,它不会更改 S 基址和界限寄存器,并因此将“相同的”内核结构映射到每个用户的地址空间。
内核映射到每个地址空间,如果操作系统收到用户程序(例如,在 write()系统调用中)递交的指针,很容易将数据从该指针处复制到它自己的结构。操作系统自然是写好和编译好的,无须担心它访问的数据来自哪里。
如果内核完全位于
物理内存
中,那么将页表的交换页切换到磁盘是非常困难的。如果内核被赋予了自己的地址空间,那么在用户应用程序和内核之间移动数据将再次变得复杂和痛苦。通过这种构造(现在广泛使用),内核几乎就像应用程序库一样,尽管是受保护的。操作系统不希望用户应用程序读取或写入操作系统数据或代码。因此,硬件必须支持页面的不同保护级别才能启用该功能。VAX通过在页表中的保护位中指定
CPU访问特定页面所需的特权级别
来实现此目的。因此,系统数据和代码被设置为比用户数据和代码更高的保护级别。试图从用户代码访问这些信息,将会在操作系统中产生一个陷阱,并且可能会终止违规进程。页替换
VAX 中的页表项(PTE)包含以下位:一个
有效位
,一个保护字段
(4 位),一个修改
(或脏位)位,为 OS 使用保留的字段
(5 位),最后是一个物理帧号码
(PFN)将页面的位置存储在物理内存中。敏锐的读者可能会注意到:没有引用位(no reference bit)!因此,VMS 替换算法必须在没有硬件支持的情况下,确定哪些页是活跃的。
开发人员也担心会有“自私贪婪的内存”(memory hog)——一些程序占用大量内存,使其他程序难以运行。到目前为止,我们所看到的大部分策略都容易受到这种内存的影响。例如,LRU 是一种全局策略,不会在进程之间公平分享内存。分段的 FIFO
防止
有“自私贪婪的内存”(memory hog)—— 一些程序占用大量内存
, 使其他程序难以运行和在没有硬件支持
的情况下,确定哪些页是活跃
的。每个进程都有一个可以保存在内存中的最大页数,称为
驻留集大小
(Resident Set Size,RSS
)。每个页都保存在 FIFO 列表中。当一个进程超过
其 RSS
时,“先入”的页被驱逐
。决定了进程不能占用过大内存。纯粹的 FIFO 并不是特别好。为了提高 FIFO 的性能,VMS 引入了
两个
二次机会列表
( second-chance list,两个队列),页在从内存中被踢出之前被放在其中。具体来说, 是全局
的干净页空闲列表
和脏页列表
。当进程 P
超过其 RSS 时,将从其每个进程的 FIFO 中移除
一个页。如果干净(未修改)
,则将其放在干净页列表
的末尾。如果脏(已修改)
,则 将其放在脏页列表
的末尾。如果另一个
进程 Q
需要一个空闲页,它会从全局干净列表
中取出第一个
空闲页。但是, 如果原来的进程 P
在回收之前在该页上出现页错误
(不在物理内存中,在磁盘中),则 P 会从空闲(或脏)列表中回收
,从而避免昂贵的磁盘访问。这些全局二次机会列表越大,分段的 FIFO 算法越接近 LRU页聚集
通过聚集,VMS将
大批量
的页从全局脏列表中分组
到一起,并将它们一举写入磁盘(从而使它们变干净)。聚集用于大多数现代系统,因为可以在交换空间的任意位置放置页,所以操作系统对页分组,执行更少和更大的写入,从而提高性能。补充:模拟引用位 事实证明,你不需要硬件引用位,就可以了解系统中哪些页在用。事实上,在 20 世纪 80 年代早期,Babaoglu 和 Joy 表明,VAX 上的保护位可以用来模拟引用位[BJ81]。其基本思路是:如果你想了解哪些页在系统中被活跃使用,请将页表中的所有页标记为不可访问(但请注意关于哪些页可以被进程真正访问的信息,也许在页表项的“保留的操作系统字段”部分)。当一个进程访问一页时,它会在操作系统中产生一个陷阱。操作系统将检查页是否真的可以访问,如果是,则将该页恢复为正常保护(例如,只读或读写)。在替换时,操作系统可以检查哪些页仍然标记为不可用,从而了解哪些页最近没有被使用过。 这种引用位“模拟”的关键是减少开销,同时仍能很好地了解页的使用。标记页不可访问时,操作系统不应太激进,否则开销会过高。同时,操作系统也不能太被动,否则所有页面都会被引用,操作系统又无法知道踢出哪一页。
其他漂亮的虚拟内存技巧
VMS 有另外两个现在成为标准的技巧:按需置零和写入时复制。我们现在描述这些惰性(lazy)优化。
按需置零(demand zeroing)
利用按需置零,当页添加到你的地址空间时,操作系统会在页表中放入一个标记页不可访问的条目。如果进程读取或写入页,则会向操作系统
发送陷阱
。在处理陷阱时,操作系统注意到(通常通过页表项中“保留的操作系统字段”部分标记的一些位),这实际上是一个按需置零页。此时,操作系统会完成寻找物理页的必要工作,将它置零,并映射到进程的地址空间。如果该进程从不访问该页,则所有这些工作都可以避免,从而体现按需置零的好处。写时复制(copy-on-write,COW)
如果操作系统需要将一个页面从一个地址空间
复制
到另一个地址空间,不是实际复制
它,而是将其映射到目标地址空间,并在两个地址空间中将其标记为只读
。如果两个地址空间都只读取页面,则不会采取进一步的操作,因此操作系统已经实现了快速复制而不实际移动任何数据。但是,如果其中一个地址空间确实尝试写入页面,就会
陷入
操作系统。操作系统会注意到该页面是一个 COW 页面,因此(惰性地)分配一个新页,填充数据,并将这个新页映射到错误处理的地址空间。该进程然后继续,现在有了该页的私人副本。COW 有用有一些原因。任何类型的共享库都可以通过写时复制,映射到许多进程的地址空间中,从而节省宝贵的内存空间。
在 UNIX 系统中,由于 fork()和 exec()的语义,COW 更加关键。fork()会创建调用者地址空间的
精确副本
。对于大的地址空间,这样的复制过程很慢,并且是数据密集的。更糟糕的是,大部分地址空间会被随后的exec()调用立即覆盖,它用即将执行的程序覆盖调用进程的地址空间。通过改为执行写时复制的 fork(),操作系统避免了大量不必要的复制,从而保留了正确的语义,同时提高了性能。参考
- 作者:GJJ
- 链接:https://blog.gaojj.cn/article/blog-38
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。