4.6 页面错误异常

Xv6对异常的响应相当无趣: 如果用户空间中发生异常,内核将终止故障进程。如果内核中发生异常,则内核会崩溃。真正的操作系统通常以更有趣的方式做出反应。

例如,许多内核使用页面错误来实现写时拷贝版本的fork——copy on write (COW) fork。要解释COW fork,请回忆第3章内容:xv6的fork通过调用uvmcopy(kernel/vm.c:309) 为子级分配物理内存,并将父级的内存复制到其中,使子级具有与父级相同的内存内容。如果父子进程可以共享父级的物理内存,则效率会更高。然而武断地实现这种方法是行不通的,因为它会导致父级和子级通过对共享栈和堆的写入来中断彼此的执行。

由页面错误驱动的COW fork可以使父级和子级安全地共享物理内存。当CPU无法将虚拟地址转换为物理地址时,CPU会生成页面错误异常。Risc-v有三种不同的页面错误: 加载页面错误 (当加载指令无法转换其虚拟地址时),存储页面错误 (当存储指令无法转换其虚拟地址时) 和指令页面错误 (当指令的地址无法转换时)。scause寄存器中的值指示页面错误的类型,stval寄存器包含无法翻译的地址。

COW fork中的基本计划是让父子最初共享所有物理页面,但将它们映射为只读。因此,当子级或父级执行存储指令时,risc-v CPU引发页面错误异常。为了响应此异常,内核复制了包含错误地址的页面。它在子级的地址空间中映射一个权限为读/写的副本,在父级的地址空间中映射另一个权限为读/写的副本。更新页表后,内核会在导致故障的指令处恢复故障进程的执行。由于内核已经更新了相关的PTE以允许写入,所以错误指令现在将正确执行。

COW策略对fork很有效,因为通常子进程会在fork之后立即调用exec,用新的地址空间替换其地址空间。在这种常见情况下,子级只会触发很少的页面错误,内核可以避免拷贝父进程内存完整的副本。此外,COW fork是透明的: 无需对应用程序进行任何修改即可使其受益。

COW fork以外,页表和页面错误的结合还开发出了广泛有趣的可能性。另一个广泛使用的特性叫做惰性分配——lazy allocation。它包括两部分内容:首先,当应用程序调用sbrk时,内核增加地址空间,但在页表中将新地址标记为无效。其次,对于包含于其中的地址的页面错误,内核分配物理内存并将其映射到页表中。由于应用程序通常要求比他们需要的更多的内存,惰性分配可以称得上一次胜利: 内核仅在应用程序实际使用它时才分配内存。像COW fork一样,内核可以对应用程序透明地实现此功能。

利用页面故障的另一个广泛使用的功能是从磁盘分页。如果应用程序需要比可用物理RAM更多的内存,内核可以换出一些页面: 将它们写入存储设备 (如磁盘),并将它们的PTE标记为无效。如果应用程序读取或写入被换出的页面,则CPU将触发页面错误。然后内核可以检查故障地址。如果该地址属于磁盘上的页面,则内核分配物理内存页面,将该页面从磁盘读取到该内存,将PTE更新为有效并引用该内存,然后恢复应用程序。为了给页面腾出空间,内核可能需要换出另一个页面。此功能不需要对应用程序进行更改,并且如果应用程序具有引用的地址 (即,它们在任何给定时间仅使用其内存的子集),则该功能可以很好地工作。

结合分页和页面错误异常的其他功能包括自动扩展栈空间和内存映射文件。