六:操作系统虚拟内存之缺页中断
深入理解操作系统:缺页中断 (Page Fault) 的处理流程
在上一篇文章中,我们介绍了虚拟内存和按需调页 (Demand Paging) 的概念。虚拟内存为每个进程提供了巨大的、独立的虚拟地址空间,并通过页表 (Page Table) 将虚拟页面 (Virtual Page) 映射到物理内存中的物理帧 (Physical Frame)。按需调页的核心在于:只有当程序试图访问一个尚未加载到物理内存中的页面时,操作系统才会将其从磁盘调入。
那么,当程序访问的页面不在内存中时,具体会发生什么?这就是我们今天要深入探讨的——缺页中断 (Page Fault)。
1. 什么是缺页中断?
缺页中断是一种特殊的中断 (Interrupt) 或更准确地说,是陷阱 (Trap) 或异常 (Exception)。它发生在当程序试图访问一个虚拟地址时,内存管理单元 (MMU) 在该进程的页表中查找对应的页表条目 (Page Table Entry, PTE),发现该页面的存在位 (Present Bit) 为 0。存在位为 0 表示该虚拟页面当前不在物理内存中。
此时,MMU 无法完成虚拟地址到物理地址的翻译,硬件会立即停止当前指令的执行,并产生一个内部中断信号,将控制权交给操作系统内核中预设的缺页中断处理程序 (Page Fault Handler)。
可以将其类比为:你去图书馆(物理内存)找一本图书(页面),但发现这本书(页面)不在架上(不在内存)。于是你向图书管理员(操作系统)求助,由图书管理员负责找到这本书(从磁盘调入)并带给你。
2. 缺页中断的处理流程
当缺页中断发生后,操作系统的缺页中断处理程序会接管控制权,执行一系列步骤来解决这个“页面不在内存”的问题,以便程序能够继续执行。这个过程是操作系统内存管理的核心部分之一。
以下是典型的缺页中断处理流程:
-
陷阱到操作系统内核 (Trap to the Operating System):
- 当 MMU 检测到访问的页面的存在位为 0 时,它会生成一个硬件中断(通常称为 Page Fault Exception)。
- 硬件将导致缺页的虚拟地址以及其他相关信息(如访问类型:读/写)压入进程的栈中,并切换 CPU 的执行模式到内核态。
- CPU 跳转到操作系统预设的缺页中断处理程序的入口地址。
-
保存进程状态 (Save Process State):
- 操作系统中断处理程序会保存当前正在执行的进程的上下文(寄存器、程序计数器等),以便稍后能够恢复其执行。
-
验证虚拟地址和权限 (Validate Virtual Address and Permissions):
- 操作系统首先检查导致缺页的虚拟地址是否合法。也就是说,这个地址是否属于当前进程的有效地址空间(例如,它是否在一个分配给进程的代码、数据或栈段内)。如果地址无效,说明这是一个非法内存访问(例如,访问了不属于自己的内存区域),这通常会导致一个段错误 (Segmentation Fault),操作系统会向该进程发送一个信号,通常会终止进程。
- 如果地址合法,操作系统还会检查进程对该页面的访问权限(例如,是试图写入只读页面吗?)。权限不匹配也会导致保护错误并可能终止进程。
- 如果地址合法且权限匹配,处理程序继续执行。
-
确定页面在磁盘上的位置 (Determine Page Location on Disk):
- 操作系统需要知道这个缺失的页面存储在磁盘的哪个位置。对于程序代码和静态数据,它通常位于程序的可执行文件中。对于堆 (heap) 或栈 (stack) 数据,它可能位于系统的交换空间 (Swap Space) 或分页文件 (Paging File)。操作系统会查找内部的数据结构(例如,页表的扩展信息或单独的结构)来确定磁盘地址。
-
查找空闲物理帧 (Find a Free Physical Frame):
- 操作系统需要在物理内存中找到一个空闲的物理帧,用于加载所需的页面。
- 操作系统维护着一个空闲物理帧列表。它会首先尝试从这个列表中获取一个空闲帧。
- 如果存在空闲帧: 直接分配一个空闲帧给该页面。
-
执行页面置换 (Page Replacement - If No Free Frame):
- 如果没有空闲物理帧: 这是内存紧张的情况。操作系统必须选择一个当前已经在物理内存中的页面作为“牺牲者”,将其从内存中移除,以便为新页面腾出空间。这个过程称为页面置换 (Page Replacement)。
- 操作系统会使用某种页面置换算法(如 LRU - Least Recently Used, FIFO - First-In First-Out, Clock 等)来选择要置换的页面。选择的目标通常是希望置换一个将来最不可能被访问的页面。
- 处理“脏页” (Handle “Dirty” Pages): 在置换页面之前,操作系统会检查被选中的“牺牲者”页面的页表条目中的修改位/脏位 (Dirty Bit)。如果脏位为 1,表示该页面在加载到内存后被修改过,其内容与磁盘上的原始副本不一致。为了保存这些修改,操作系统必须将该页面写回磁盘(通常是写回交换空间)后再释放其物理帧。这是一个额外的、开销较大的 I/O 操作。
- 如果脏位为 0,表示页面未被修改,可以直接丢弃内存中的副本,因为磁盘上的副本是最新的。
- 更新被置换页面的页表条目:将被置换页面的存在位设置为 0,清除其物理帧号信息。
-
将所需页面从磁盘读入物理内存 (Read Required Page from Disk to Frame):
- 操作系统发起一个磁盘 I/O 请求,将第 4 步中确定的页面从磁盘位置读取到第 5 或 6 步中找到的物理帧中。这是一个耗时较长的操作(相对于 CPU 指令执行速度)。在等待磁盘 I/O 完成期间,操作系统通常会将 CPU 分配给其他处于就绪状态的进程执行。
-
更新页表 (Update Page Table):
- 磁盘 I/O 完成后,所需页面已成功加载到物理内存的某个物理帧中。
- 操作系统更新当前进程的页表,找到导致缺页的虚拟页面对应的页表条目。
- 将该条目的存在位设置为 1。
- 将该条目的物理帧号更新为页面实际加载到的物理帧号。
- 根据程序的访问类型,设置页面的访问权限位(读、写、执行)。
- 将脏位和访问位清零(初始状态)。
-
恢复进程执行 (Restart or Continue Process Execution):
- 操作系统恢复之前保存的进程状态。
- 最关键的一步是:操作系统会修改程序计数器,使其指向导致缺页中断的那条指令的开头。
- 进程在用户态下恢复执行。当 CPU 再次尝试执行那条指令时,MMU 会再次进行地址翻译。这一次,由于操作系统已经将所需的页面加载到内存并更新了页表,MMU 查找页表时会发现存在位为 1,能够成功翻译虚拟地址为物理地址,指令得以正常完成。
3. 缺页中断处理的例子
假设一个程序试图访问数组 arr[i]
。在编译后的机器码中,这可能对应于一条加载(Load)或存储(Store)指令,其操作数使用虚拟地址 &arr + i * sizeof(element)
。
- CPU 执行指令,计算出要访问的虚拟地址
VA
。 - CPU 将
VA
发送给 MMU 进行翻译。 - MMU 查找当前进程的页表,发现
VA
所属的虚拟页面的页表条目中,存在位是 0。 - MMU 触发一个缺页中断。
- CPU 切换到内核态,跳转到操作系统的缺页中断处理程序。
- 操作系统:
- 保存当前进程的上下文。
- 检查
VA
是否合法地址,以及访问权限(是读还是写,进程是否有权限)。假设合法。 - 确定
VA
所在的虚拟页面对应于arr
数组的某个部分,这个页面在磁盘上的某个位置(比如在程序的原始数据段或交换空间)。 - 查找物理内存中的空闲帧。假设没有空闲帧,需要进行页面置换。
- 选择一个页面
P_victim
进行置换(例如,使用了 LRU 算法选了一个很久没用的页面)。 - 检查
P_victim
的脏位。假设P_victim
的脏位是 1(它被修改过)。操作系统发起一个 I/O 操作,将P_victim
的内容写回磁盘的交换空间。 - 操作系统更新
P_victim
对应的页表条目,将其存在位设为 0。 P_victim
占用的物理帧现在变为空闲。操作系统发起第二个 I/O 操作,将导致缺页的那个页面(包含arr[i]
)从磁盘读入这个刚刚释放的物理帧。- I/O 操作完成。操作系统更新
VA
所在页面的页表条目,设置存在位为 1,记录新的物理帧号。 - 操作系统恢复进程上下文,并将程序计数器设置回导致缺页中断的那条 Load/Store 指令的开头。
- 进程在用户态下恢复执行。CPU 再次执行 Load/Store 指令,尝试访问
VA
。 - MMU 再次翻译
VA
。这一次,页表中对应的存在位是 1,MMU 成功翻译出物理地址。 - CPU 使用物理地址访问内存,Load/Store 指令成功完成。程序继续正常执行。
整个过程从程序执行的角度来看,除了由于磁盘 I/O 导致的延迟之外,是透明的。程序并不知道它刚刚经历了一次缺页中断和页面加载。
4. 缺页中断的开销
缺页中断处理是一个复杂的、涉及硬件和软件协作的过程,其中最耗时的部分是磁盘 I/O 操作。与 CPU 执行一条指令(纳秒级别)相比,磁盘访问需要数毫秒甚至更长时间,这是几个数量级的差异。
频繁的缺页中断(称为颠簸 (Thrashing))会导致系统大部分时间都在忙于页面调入调出,而用户进程实际执行指令的时间很少,系统性能急剧下降。这是物理内存不足以支持当前进程的工作集(Work Set,即进程在一段时间内频繁访问的页面集合)时常发生的情况。
然而,在大多数正常情况下,由于程序的局部性原理 (Principle of Locality)(包括时间局部性和空间局部性),程序在一段时间内只会访问其工作集中的页面。按需调页能够有效地利用物理内存,只加载这些活跃页面,从而在有限的物理内存下支持更多或更大的程序运行。缺页中断虽然有开销,但其带来的内存利用率和多道程序度(Multiprogramming Degree)的提升通常是值得的。
总结
缺页中断是虚拟内存和按需调页机制中不可避免的一部分。它是一个由 MMU 触发的硬件异常,通知操作系统某个被访问的虚拟页面不在物理内存中。操作系统的缺页中断处理程序负责定位缺失页面、寻找物理内存空间(可能需要置换其他页面)、将页面从磁盘加载到内存,并更新页表,最终让导致缺页的指令重新执行成功。
理解缺页中断的处理流程,对于理解虚拟内存的工作原理、内存性能瓶颈以及操作系统的复杂性至关重要。它是现代操作系统能够高效管理内存、运行大型程序和支持多任务的关键技术之一。