12 关于虚拟内存的对话
虚拟内存是什么
是和虚拟CPU一样的一种幻想,让进程认为自己拥有整个内存。拥有大量的连续内存。
第13章 抽象:地址空间
我们介绍了操作系统的一个重要子系统:虚拟内存。虚拟内存系统负责为程序提供一个巨大的、稀疏的、私有的地址空间的假象,其中保存了程序的所有指令和数据。操作系统在专门硬件的帮助下,通过每一个虚拟内存的索引,将其转换为物理地址,物理内存根据获得的物理地址去获取所需的信息。操作系统会同时对许多进程执行此操作,并且确保程序之间互相不会受到影响,也不会影响操作系统。整个方法需要大量的机制(很多底层机制)和一些关键的策略。
在前面,我们提到过——进程是指令和静态数据的组合,并且需要初始化堆栈,那么为什么进程需要使用内存呢
在计算机中,随着距离CPU的举例越远,读取数据的速度呈指数级上升。
如果每次都要从硬盘中读取,花费太多时间
所以进程需要有独立的内存。
如果每个进程都有内存,恶意程序能不能通过修改其他程序进程实现破坏呢
当然可以。
所以我们要提供一层抽象,让其他进程并不知道自己的地址空间。
也就是隔离
地址空间
一个进程的地址空间包含运行的程序的所有内存状态。
程序的代码(code,指令)必须在内存中,因此它们在地址空间里。当程序在运行的时候,利用栈(stack)来保存当前的函数调用信息,分配空间给局部变量,传递参数和函数返回值。最后,堆(heap)用于管理动态分配的、用户管理的内存
内存虚拟化的目标
- 透明:让每个程序都认为自己有大量的连续地址空间,从而实现内存复用。
- 隔离:操作系统应确保进程受到保护(protect),不会受其他进程影响,操作系统本身也不会受进程影响。当一个进程执行加载、存储或指令提取时,它不应该以任何方式访问或影响任何其他进程或操作系统本身的内存内容(即在它的地址空间之外的任何内容)。
- 效率(efficiency)。操作系统应该追求虚拟化尽可能高效(efficient),包括时间上(即不会使程序运行得更慢)和空间上(即不需要太多额外的内存来支持虚拟化)。
第14章:内存操作API
简单的C语言内存操作
第15章: 机制-地址转换
在虚拟CPU中,我们采用受限直接访问的思路(也就是只在需要高级权限的地方使用内核模式,绝大部分时候依靠用户空间执行指令)
同样,为了高效实现地址转换,我们希望提高更多的灵活性
我们的设计目标
- 隔离:确保应用程序仅能访问自己的内存空间
- 高效:需要硬件帮助
对于每个进程来说,自己都是从0地址开始。
但是哪来那么多初始地址,所以,每个进程都产生了一种幻觉。
动态(基于硬件)重定位
CPU通过两个硬件寄存器:基址寄存器和界限寄存器。让我们能够将地址空间放在物理内存的任何位置,同时又能确保进程只能访问自己的地址空间。
一句话:
虚拟寄存器加上基址就得到了实际地址,然后通过界限寄存器判断有没有越位
转换示例为了更好地理解基址加界限的地址转换的详细过程,我们来看一个例子。设想一个进程拥有4KB大小地址空间(是的,小得不切实际),它被加载到从16KB开始的物理内存中。一些地址转换结果见表15.1。

从例子中可以看到,通过基址加虚拟地址(可以看作是地址空间的偏移量)的方式,很容易得到物理地址。虚拟地址“过大”或者为负数时,会导致异常。
操作系统的问题

第一,在进程创建时,操作系统必须采取行动,为进程的地址空间找到内存空间。由于我们假设每个进程的地址空间小于物理内存的大小,并且大小相同,这对操作系统来说很容易。它可以把整个物理内存看作一组槽块,标记了空闲或已用。当新进程创建时,操作系统检索这个数据结构(常被称为空闲列表,free list),为新地址空间找到位置,并将其标记为已用。如果地址空间可变,那么生活就会更复杂,我们将在后续章节中讨论。
我们来看一个例子。在图15.2中,操作系统将物理内存的第一个槽块分配给自己,然后将例子中的进程重定位到物理内存地址32KB。另两个槽块(16~32KB,48~64KB)空闲,因此空闲列表(free list)就包含这两个槽块。
第二,在进程终止时(正常退出,或因行为不端被强制终止),操作系统也必须做一些工作,回收它的所有内存,给其他进程或者操作系统使用。在进程终止时,操作系统会将这些内存放回到空闲列表,并根据需要清除相关的数据结构。
第三,在上下文切换时,操作系统也必须执行一些额外的操作。每个CPU毕竟只有一个基址寄存器和一个界限寄存器,但对于每个运行的程序,它们的值都不同,因为每个程序被加载到内存中不同的物理地址。因此,在切换进程时,操作系统必须保存和恢复基础和界限寄存器。具体来说,当操作系统决定中止当前的运行进程时,它必须将当前基址和界限寄存器中的内容保存在内存中,放在某种每个进程都有的结构中,如进程结构(process structure)或进程控制块(Process Control Block,PCB)中。类似地,当操作系统恢复执行某个进程时(或第一次执行),也必须给基址和界限寄存器设置正确的值。
第四,操作系统必须提供异常处理程序(exception handler),或要一些调用的函数,像上面提到的那样。操作系统在启动时加载这些处理程序(通过特权命令)。例如,当一个进程试图越界访问内存时,CPU会触发异常。在这种异常产生时,操作系统必须准备采取行动。通常操作系统会做出充满敌意的反应:终止错误进程。操作系统应该尽力保护它运行的机器,因此它不会对那些企图访问非法地址或执行非法指令的进程客气。再见了,行为不端的进程,很高兴认识你。
第16章:分段
动态重定位存在的问题:
- 基址和界限寄存器限定的空间不适合大程序扩展
- 对于小程序堆栈之间的大部分空间被浪费
虽然有如上问题,但是动态重定位满足了我们对于进程内存的一系列要求。或许我们只需要稍加改进。
分段基址:基址/界限管理的泛化
在MMU中引入不止一个基址和界限寄存器对,而是给地址空间内的每个逻辑段(segment)一对。一个段只是地址空间里的一个连续定长的区域,在典型的地址空间里有 3 个逻辑不同的段:代码、栈和堆。分段的机制使得操作系统能够将不同的段放到不同的物理内存区域,从而避免了虚拟地址空间中的未使用部分占用物理内存。
本质上是将基址的任务给固定下来了。原本是一个进程需要一个基址寄存器来保存,现在整个地址空间被分为多个段,基址也被固定下来了。

引用哪一个段
硬件是如何知道地址引用了哪一个段,如何知道段内的偏移量
- 显式方式
- 通过设定前几位固定,比如00为代码段。
- 后面为偏移量,与界限比较即可
支持共享
我们要知道,操作系统的两大核心思路是隔离和通信。
隔离在前面的各种虚拟化已经得到了很好地实现。
但是这是一对矛盾的存在。
所以我们决定设计一部分区域为共享区。在后面我们称之为Trampline(跳板页)
在这里内核空间与用户空间可以实现通信,但是不会让恶意程序将恶意内容放入内核空间。
进程之间可以通信,相互交换标志位和各种信息。
细粒度的划分
物理内存很快充满了许多空闲空间的小洞,因而很难分配给新的段,或扩大已有的段。这种问题被称为外部碎片(external fragmentation)[R69],如图16.3(左边)所示。
分段的使用,任然是一种资源的浪费。
通过将一个连续区域,拆分为多个中间不连续的空间,会造成外部碎片问题。
这种问题在后续的内存分配中,即使有充足的空间,也难以分配出连续的足额空间来满足使用。
所以在后面我们会使用页表来实现,通过固定的段长度,同时将段不局限于一个位置来实现。
