

   \chapter{Traps and system calls}   
    \label{CH:TRAP}     

共有三种事件导致 CPU 搁置普通指令执行并强制将控制权转移到处理该事件的特殊代码。一种情况是系统调用，当用户程序执行  {    \tt    ecall   }  指令以要求内核为其执行某些操作时。另一种情况是    \indextext{exception}    ：指令（用户或内核）执行非法操作，例如除以零或使用无效的虚拟地址。第三种情况是设备    \indextext{interrupt}    ，当设备发出需要注意的信号时，例如当磁盘硬件完成读取或写入请求时。  

本书使用    \indextext{trap}    作为这些情况的通用术语。通常，\indextext{trap}发生时正在执行的任何代码稍后都需要恢复，并且不需要知道发生了任何特殊情况。也就是说，我们经常希望\indextext{trap}是透明的；这对于设备中断尤其重要，而被中断的代码通常不会预料到这种情况。通常的顺序是\indextext{trap}强制将控制权转移到内核中；内核保存寄存器和其他状态，以便可以恢复执行；内核执行适当的处理程序代码（例如，系统调用实现或设备驱动程序）；内核恢复保存的状态并从\indextext{trap}中返回；原始代码从中断处继续。  

xv6 处理内核中的所有\indextext{trap}；\indextext{trap}不会传递给用户代码。对于系统调用来说，处理内核中的\indextext{trap}是很自然的。这对于中断来说是有意义的，因为隔离要求仅允许内核使用设备，并且因为内核是在多个进程之间共享设备的便捷机制。它对于异常也有意义，因为 xv6 通过杀死有问题的程序来响应用户空间的所有异常。  

Xv6 \indextext{trap}处理分四个阶段进行：RISC-V CPU 采取的硬件操作、为内核 C 代码做好准备的一些汇编指令、决定如何处理\indextext{trap}的 C 函数以及系统调用或设备驱动程序服务例程。虽然三种\indextext{trap}类型之间的共性表明内核可以使用单个代码路径处理所有\indextext{trap}，但事实证明，对于三种不同的情况使用单独的代码会很方便：来自用户空间的\indextext{trap}、来自内核空间的\indextext{trap}和计时器中断。处理\indextext{trap}的内核代码（汇编程序或 C）通常称为    \indextext{handler}    ；第一个处理程序指令通常用汇编程序（而不是 C）编写，有时称为    \indextext{vector}    。  

   \section{RISC-V trap 机制  }     

每个 RISC-V CPU 都有一组控制寄存器，内核写入这些控制寄存器来告诉 CPU 如何处理\indextext{trap}，并且内核可以读取这些寄存器来找出已发生的\indextext{trap}。 RISC-V 文档包含完整的介绍~    \cite{riscv:priv}    。  {    \tt    riscv.h   } 
    \lineref{kernel/riscv.h}{1}    包含 xv6 使用的定义。以下是最重要寄存器的概述：  

   \begin{itemize}


   \item      \indexcode{stvec}    ：内核将其\indextext{trap}处理程序的地址写入此处； RISC-V 跳转到  {    \tt    stvec   }  中的地址来处理\indextext{trap}。   \item      \indexcode{sepc}    ：当\indextext{trap}发生时，RISC-V 将程序计数器保存在这里（因为  {    \tt    pc   }  随后会被  {    \tt    stvec   }  中的值覆盖）。这
  {    \tt    sret   } （从\indextext{trap}返回）指令将  {    \tt    sepc   }  复制到
  {    \tt    pc   }  。内核可以编写  {    \tt    sepc   }  来控制  {    \tt    sret   }  的去向。   \item      \indexcode{scause}   ：RISC-V 在此处放置一个数字来描述\indextext{trap}的原因。   \item      \indexcode{sscratch}    ：\indextext{trap}处理程序代码使用  {    \tt    scratch   }  来帮助避免在保存用户寄存器之前覆盖它们。   \item      \indexcode{sstatus}   ： {    \tt    sstatus   }  中的 SIE 位控制是否启用设备中断。如果内核清除 SIE，RISC-V 将推迟设备中断，直到内核设置 SIE。 SPP 位指示\indextext{trap}是来自用户模式还是管理模式，并控制  {    \tt    sret   }  返回的模式。  \end{itemize}     

上述寄存器与管理模式下处理的\indextext{trap}相关，并且不能在用户模式下读取或写入。对于机器模式下处理的\indextext{trap}，有一组类似的控制寄存器； xv6 仅将它们用于定时器中断的特殊情况。  

多核芯片上的每个 CPU 都有自己的一组寄存器，并且在任何给定时间都可能有多个 CPU 正在处理\indextext{trap}。  

当需要强制\indextext{trap}时，RISC-V 硬件会对所有\indextext{trap}类型（定时器中断除外）执行以下操作：  

   \begin{enumerate}


   \item   如果\indextext{trap}是设备中断，并且  {    \tt    sstatus   }  SIE 位清零，则不要执行以下任何操作。   \item   通过清除  {    \tt    sstatus   }  中的 SIE 位来禁用中断。   \item   将  {    \tt    pc   }  复制到  {    \tt    sepc   }  。   \item   将当前模式（用户或管理员）保存在  {    \tt    sstatus   }  的 SPP 位中。   \item   设置  {    \tt   scause   }  以反映\indextext{trap}的原因。   \item   将模式设置为主管。   \item   将  {    \tt    stvec   }  复制到  {    \tt    pc   }  。   \item   在新的  {    \tt    pc   }  处开始执行。  \end{enumerate}     

请注意，CPU 不会切换到内核页表，不会切换到内核中的堆栈，并且不会保存除  {    \tt    pc   }  之外的任何寄存器。内核软件必须执行这些任务。 CPU 在\indextext{trap}期间执行最少工作的原因之一是为软件提供灵活性。例如，某些操作系统在某些情况下省略页表切换以提高\indextext{trap}性能。  

值得思考是否可以省略上面列出的任何步骤，也许是为了寻找更快的\indextext{trap}。尽管在某些情况下可以使用更简单的顺序，但一般来说，省略许多步骤是危险的。例如，假设 CPU 没有切换程序计数器。然后，来自用户空间的\indextext{trap}可以切换到管理员模式，同时仍然运行用户指令。这些用户指令可能会破坏用户/内核隔离，例如通过修改  {    \tt    stap   }  寄存器以指向允许访问所有物理内存的页表。因此，CPU 切换到内核指定的指令地址（即  {    \tt    stvec   }  ）非常重要。  

   \section{来自用户空间的 trap  }     

Xv6 对\indextext{trap}的处理方式有所不同，具体取决于\indextext{trap}是在内核中执行还是在用户代码中执行时发生。这是来自用户代码的\indextext{trap}的描述；章节~    \ref{s:ktraps}    描述了内核代码中的\indextext{trap}。  

如果用户程序进行系统调用（ {    \tt    ecall   }  指令）、执行非法操作或者设备中断，则在用户空间执行时可能会发生\indextext{trap}。来自用户空间的\indextext{trap}的高级路径是
  {    \tt    uservec   } 
  \lineref{kernel/trampoline.S:/^uservec/}   ，然后  {    \tt    usertrap   } 
  \lineref{kernel/trap.c:/^usertrap/}    ；返回时，
  {    \tt    usertrap   } 
  \lineref{kernel/trap.c:/^usertrapret/}    然后
  {    \tt    userret   } 
  \lineref{kernel/trampoline.S:/^userret/}   。  

xv6 \indextext{trap}处理设计的一个主要限制是 RISC-V 硬件在强制\indextext{trap}时不会切换页表。这意味着  {    \tt    stvec   }  中的\indextext{trap}处理程序地址必须在用户页表中具有有效的映射，因为这是\indextext{trap}处理代码开始执行时有效的页表。此外，xv6的\indextext{trap}处理代码需要切换到内核页表；为了能够在该切换后继续执行，内核页表还必须具有  {    \tt    stvec   }  指向的处理程序的映射。  

Xv6 使用    \indextext{trampoline}    页满足这些要求。 \indextext{trampoline} 页面包含  {    \tt    uservec   }  ，即  {    \tt    stvec   }  指向的 xv6 \indextext{trap}处理代码。\indextext{trampoline}页被映射到每个进程的页表中的地址
    \indexcode{TRAMPOLINE}    位于虚拟地址空间的顶部，因此它将位于程序自身使用的内存之上。\indextext{trampoline} 页也映射到内核页表中的地址  {    \tt    TRAMPOLINE   } 。参见图~    \ref{fig:as}    和图~\ref{fig:xv6_layout}    。由于\indextext{trampoline}页面映射到用户页表中，因此没有  {    \tt    PTE\_U   }  标志，\indextext{trap}可以在管理模式下开始执行。由于\indextext{trampoline}页映射到内核地址空间中的同一地址，因此\indextext{trap}处理程序在切换到内核页表后可以继续执行。  

 {    \tt    uservec   }  \indextext{trap}处理程序的代码位于  {    \tt trampoline.S   }  中
 \lineref{kernel/trampoline.S:/^uservec/}    。当  {    \tt    uservec   }  启动时，所有 32 个寄存器都包含被中断的用户代码拥有的值。这 32 个值需要保存在内存中的某个位置，以便当\indextext{trap}返回到用户空间时可以恢复它们。存储到内存需要使用寄存器来保存地址，但目前还没有可用的通用寄存器！幸运的是，RISC-V 以  {    \tt    scratch   }  寄存器的形式提供了帮助。  {    \tt    uservec   }  开头的  {    \tt    csrw   }  指令将  {    \tt    a0   }  保存在  {    \tt    scratch   }  中。现在
  {    \tt    uservec   }  有一个寄存器（  {    \tt    a0   }  ）可供使用。  

 {    \tt    uservec   }  的下一个任务是保存 32 个用户寄存器。内核为每个进程分配一个内存页
  {    \tt    trapframe   }  结构（除其他外）有空间保存 32 个用户寄存器
    \lineref{kernel/proc.h}{43}    。因为 {    \tt    stap   } 仍然引用用户页表，所以 {    \tt    uservec   } 需要将trapframe映射到用户地址空间。 xv6将每个进程的trapframeat虚拟地址 {    \tt    trapframe   } 映射到该进程的用户页表中；
  {    \tt    trapframe   }  正好低于  {    \tt    TRAMPOLINE   }  。进程的  {    \tt    p->trapframe   }  也指向\indextext{trap}帧，尽管位于其物理地址，因此内核可以通过内核页表使用它。  

因此， {    \tt    uservec   }  将地址  {    \tt    trapframe   }  加载到  {    \tt    a0   }  中，并在那里保存所有用户寄存器，包括从  {    \tt    scratch   }  读回的用户的  {    \tt    a0   }  。  

 {    \tt    trapframe   } 包含当前进程的内核堆栈地址、当前CPU的hartid、 {    \tt    usertrap   } 函数的地址以及内核页表的地址。  {    \tt    uservec   }  检索这些值，将  {    \tt    stap   }  切换到内核页表，并调用  {    \tt    usertrap   }  。  

 {    \tt    usertrap   } 的工作是确定\indextext{trap}的原因、处理它并返回
 \lineref{kernel/trap.c:/^usertrap/}   。它首先更改  {    \tt    stvec   } ，以便内核中的\indextext{trap}将由
  {    \tt    kernelvec   }  而不是  {    \tt    uservec   }  。它保存了 {    \tt    sepc   } 寄存器（保存的用户程序计数器），因为
  {    \tt    usertrap   }  可能会调用    \lstinline{yield}    来切换到另一个进程的内核线程，并且该进程可能会返回到用户空间，在此过程中它将修改    \lstinline{sepc}    。如果\indextext{trap}是系统调用，则  {    \tt    usertrap   }  调用  {    \tt    syscall   }  来处理它；如果是设备中断，则  {    \tt    devintr   }  ；否则它是异常，内核会终止出错的进程。系统调用路径向保存的用户程序计数器添加 4，因为 RISC-V 在系统调用的情况下使程序指针指向  {    \tt    ecall   }  指令，但用户代码需要在后续指令处恢复执行。在出去时， {    \tt    usertrap   }  检查进程是否已被终止或应让出 CPU（如果此\indextext{trap}是计时器中断）。  

返回用户空间的第一步是调用  {    \tt    usertrapret   } 
\lineref{kernel/trap.c:/^usertrapret/}     。该函数设置 RISC-V 控制寄存器，为用户空间的未来\indextext{trap}做好准备。这涉及更改  {    \tt    stvec   }  以引用  {    \tt    uservec   }  ，准备\indextext{trap}帧字段
  {    \tt    uservec   }  依赖于先前保存的用户程序计数器，并将  {    \tt    sepc   }  设置为。最后， {    \tt    usertrap   }  在映射到用户页表和内核页表中的\indextext{trampoline}页上调用  {    \tt    userret   } ；原因是  {    \tt    userret   }  中的汇编代码会切换页表。  

 {    \tt    usertrap   }  对  {    \tt    userret   }  的调用将指针传递给  {    \tt    a0   }  中进程的用户页表
 \lineref{kernel/trampoline.S:/^userret/}    。
  {    \tt    userret   }  将  {    \tt    stap   }  切换到进程的用户页表。回想一下，用户页表映射了\indextext{trampoline}页和  {    \tt    trapframe   }  ，但没有映射来自内核的其他内容。用户页表和内核页表中相同虚拟地址的\indextext{trampoline}页映射允许
  {    \tt    userret   }  在更改  {    \tt    stap   }  后继续执行。从此时起， {    \tt    userret   }  唯一可以使用的数据是寄存器内容和\indextext{trap}帧的内容。
  {    \tt    userret   }  将  {    \tt    trapframe   }  地址加载到  {    \tt    a0   }  中，通过  {    \tt    a0   }  从 trapframe 中恢复保存的用户寄存器，恢复保存的用户  {    \tt    a0   }  ，并执行  {    \tt    sret   }  返回用户空间。  

   \section{代码：调用系统调用  }     

第~    \ref{CH:FIRST}    结束于
    \indexcode{initcode.S}    调用  {    \tt    exec   }  系统调用
    \lineref{user/initcode.S:/SYS_exec/}   。让我们看看用户调用如何进入内核中的  {    \tt    exec   }  系统调用的实现。  

 {    \tt    initcode.S   }  放置参数
    \indexcode{exec}    位于寄存器  {    \tt    a0   }  和  {    \tt    a1   }  中，并将系统调用号放入
    \texttt{a7}    。系统调用号与  {    \tt    syscall   }  数组（函数指针表）中的条目匹配
    \lineref{kernel/syscall.c:/syscalls/}    。    \lstinline{ecall}    指令陷入内核并导致
  {    \tt    uservec   }  ,
  {    \tt    usertrap   }  ，然后  {    \tt    syscall   }  执行，如我们上面所见。  

   \indexcode{syscall}   
    \lineref{kernel/syscall.c:/^syscall/}    从保存的系统调用号中检索
 trapframe 中的    \texttt{a7}    并使用它来索引  {    \tt    系统调用   }  。对于第一个系统调用，
    \texttt{a7}    包含
    \indexcode{SYS_exec}   
    \lineref{kernel/syscall.h:/SYS_exec/}    ,导致调用系统调用实现函数
    \lstinline{sys_exec}    。  

当    \lstinline{sys_exec}    返回时，
    \lstinline{syscall}   将其返回值记录在
    \lstinline{p->trapframe->a0}    。这将导致原始用户空间调用
  {    \tt    exec()   }  返回该值，因为 RISC-V 上的 Ccalling 约定将返回值放在  {    \tt    a0   }  中。系统调用通常返回负数来指示错误，返回零或正数来指示成功。如果系统调用号无效，
    \lstinline{syscall}    打印错误并返回    $-1$    。  

   \section{代码：系统调用参数  }     

内核中的系统调用实现需要找到用户代码传递的参数。由于用户代码调用系统调用包装函数，因此参数最初位于 RISC-V C 调用约定放置它们的位置：寄存器中。kerneltrap代码将用户寄存器保存到当前进程的\indextext{trap}框架中，内核代码可以在其中找到它们。核函数
    \lstinline{argint}    ,
    \lstinline{argaddr}    和
    \lstinline{argfd}    从\indextext{trap}帧中检索第 n 个系统调用参数作为整数、指针或文件描述符。它们都调用  {    \tt    argraw   }  来检索适当的已保存用户寄存器
    \lineref{kernel/syscall.c:/^argraw/}    。  

某些系统调用将指针作为参数传递，内核必须使用这些指针来读取或写入用户内存。例如， {    \tt    exec   }  系统调用向内核传递一个指向用户空间中字符串参数的指针数组。这些指示提出了两个挑战。首先，用户程序可能有错误或恶意，并且可能向内核传递无效指针或旨在欺骗内核访问内核内存而不是用户内存的指针。其次，xv6 内核页表映射与用户页表映射不同，因此内核无法使用普通指令从用户提供的地址加载或存储。  

内核实现了安全地与用户提供的地址之间传输数据的功能。
  {    \tt    fetchstr   }  是一个示例    \lineref{kernel/syscall.c:/^fetchstr/}     。文件系统调用例如
  {    \tt    exec   }  使用  {    \tt    fetchstr   }  从用户空间检索字符串文件名参数。
    \lstinline{fetchstr}    调用    \lstinline{copyinstr}    来完成这项艰苦的工作。  

   \indexcode{copyinstr}   
    \lineref{kernel/vm.c:/^copyinstr/}    复制最多    \lstinline{max}    字节到
    \lstinline{dst}    来自用户页表    \lstinline{pagetable}    中的虚拟地址    \lstinline{srcva}    。由于    \lstinline{pagetable}    是  {    \it    不   }  当前页表，
    \lstinline{copyinstr}    使用  {    \tt    walkaddr   }  （调用  {    \tt    walk   }  ）来查找
    \lstinline{srcva}    中
    \lstinline{pagetable}    ，产生物理地址    \lstinline{pa0}    。内核将每个物理RAM地址映射到相应的内核虚拟地址，因此
  {    \tt    copyinstr   }   可以直接将字符串字节从  {    \tt    pa0   }  复制到  {    \tt    dst   }  。
  {    \tt    walkaddr   } 
  \lineref{kernel/vm.c:/^walkaddr/}    检查用户提供的虚拟地址是否是进程用户地址空间的一部分，因此程序无法欺骗内核读取其他内存。类似的函数  {    \tt    copyin   }  将数据从内核复制到用户提供的地址。  

   \section{来自内核空间的trap}   
    \label{s:ktraps}     

Xv6 对 CPU \indextext{trap}寄存器的配置略有不同，具体取决于执行的是用户代码还是内核代码。当内核在CPU上执行时，内核将 {    \tt    stvec   } 指向 {    \tt    kernelvec   } 处的汇编代码
\lineref{kernel/kernelvec.S:/^kernelvec/}    。由于 xv6 已经在内核中，因此  {    \tt    kernelvec   }  可以依赖设置为内核页表的  {    \tt    stap   }  以及引用有效内核堆栈的堆栈指针。
  {    \tt    kernelvec   }  将所有 32 个寄存器压入堆栈，稍后将从堆栈中恢复它们，以便中断的内核代码可以不受干扰地恢复。  

 {    \tt    kernelvec   }  将寄存器保存在被中断的内核线程的堆栈上，这是有意义的，因为寄存器值属于该线程。如果\indextext{trap}导致切换到不同的线程，这一点尤其重要——在这种情况下，\indextext{trap}实际上将从新线程的堆栈中返回，将被中断线程保存的寄存器安全地保留在其堆栈上。  

 {    \tt    kernelvec   }  跳转到  {    \tt    kerneltrap   } 
 保存寄存器后的    \lineref{kernel/trap.c}{135}   。
  {    \tt    kerneltrap   }  为两种类型的\indextext{trap}做好了准备：设备中断和异常。它调用
  {    \tt    devintr   } 
  \lineref{kernel/trap.c:/^devintr/}    检查并处理前者。如果\indextext{trap}不是设备中断，那么它一定是一个异常，并且如果它发生在 xv6 内核中，则始终是致命错误；内核调用    \lstinline{panic}    并停止执行。  

如果由于计时器中断而调用  {    \tt    kerneltrap   } ，并且进程的内核线程正在运行（而不是调度程序线程），
  {    \tt    kerneltrap   }  调用  {    \tt    yield   }  为其他线程提供运行的机会。在某些时候，其中一个线程将屈服，并让我们的线程及其  {    \tt    kerneltrap   }  再次恢复。 Chapter~    \ref{CH:SCHED}    解释了  {    \tt    yield   }  中发生的情况。  

当  {    \tt    kerneltrap   }  的工作完成时，它需要返回到被\indextext{trap}中断的任何代码。因为  {    \tt    yield   }  可能会干扰  {    \tt    sepc   }  和  {    \tt    sstatus   }  中的先前模式，
  {    \tt    kerneltrap   }  在启动时保存它们。现在它恢复这些控制寄存器并返回到  {    \tt    kernelvec   } 
  \lineref{kernel/kernelvec.S:/call.kerneltrap$/}   。
  {    \tt    kernelvec   }  从堆栈中弹出保存的寄存器并执行  {    \tt    sret   }  ，它将  {    \tt    sepc   }  复制到  {    \tt    pc   }  并恢复中断的内核代码。  

值得思考的是，如果
 由于定时器中断， {    \tt    kerneltrap   }  调用了  {    \tt    yield   } 。  

当CPU从用户空间进入内核时，Xv6将CPU的 {    \tt    stvec   } 设置为 {    \tt    kernelvec   } ；你可以在  {    \tt    usertrap   }  中看到这一点
\lineref{kernel/trap.c:/stvec.*kernelvec/}    。内核开始执行时有一个时间窗口，但  {    \tt    stvec   }  仍设置为  {    \tt    uservec   }  ，并且在该窗口期间不发生设备中断至关重要。幸运的是，RISC-V 在开始捕获\indextext{trap}时始终会禁用中断，并且 xv6 在设置  {    \tt    stvec   }  之前不会再次启用中断。  

   \section{页面错误异常  }   
    \label{sec:pagefaults}     

Xv6 对异常的响应非常无聊：如果用户空间发生异常，内核就会杀死出错的进程。如果内核中发生异常，内核就会发生 \indextext{panic}。真正的操作系统通常会以更有趣的方式做出响应。  

举个例子，许多内核使用页面错误来实现
    \indextext{copy-on-write (COW) fork}    。为了解释写时复制\indextext{fork}，请考虑 xv6 的    \lstinline{fork}    ，如 Chapter~    \ref{CH:MEM}    中所述。
    \lstinline{fork}    导致子进程的初始内存内容与\indextext{fork}时父进程的初始内存内容相同。 Xv6 实现 fork
    \lstinline{uvmcopy}   
    \lineref{kernel/vm.c:/^uvmcopy/}    ，为子进程分配物理内存并将父进程的内存复制到其中。如果孩子和父母可以共享父母的物理内存，那么效率会更高。然而，直接实现这一点是行不通的，因为它会导致父进程和子进程对共享堆栈和堆的写入扰乱彼此的执行。  

父进程和子进程可以通过适当使用页表权限和页错误来安全地共享物理内存。 CPU 提出一个
    \indextext{page-fault exception}    当使用在页表中没有映射的虚拟地址时，或者具有清除    \lstinline{PTE_V}    标志的映射，或者其权限位（    \lstinline{PTE_R}    ，
    \lstinline{PTE_W}    ,
    \lstinline{PTE_X}    ,
    \lstinline{PTE_U}   ）禁止正在尝试的操作。 RISC-V 区分三种页面错误：加载页面错误（当加载指令无法翻译其虚拟地址时）、存储页面错误（当存储指令无法翻译其虚拟地址时）和指令页面错误（当程序计数器中的地址不正确时）。翻译）。这
    \lstinline{scause}    寄存器指示页错误的类型，   \indexcode{stval}    寄存器包含无法转换的地址。  

COW fork 中的基本计划是让父进程和子进程最初共享所有物理页，但每个将它们映射为只读（使用
    \lstinline{PTE_W}    标志清除）。父母和孩子可以从共享物理内存中读取数据。如果其中任何一个写入给定页面，RISC-V CPU 就会引发页面错误异常。内核的\indextext{trap}处理程序通过分配新的物理内存页并将故障地址映射到的物理页复制到其中来做出响应。内核更改故障进程页表中的相关 PTE 以指向副本并允许写入和读取，然后在导致故障的指令处恢复故障进程。由于 PTE 允许写入，因此重新执行的指令现在将正确执行。写时复制需要簿记来帮助决定何时可以释放物理页面，因为根据\indextext{fork}、页面错误、执行和退出的历史记录，每个页面都可以由不同数量的页表引用。这种簿记允许进行重要的优化：如果进程发生存储页错误并且仅从该进程的页表引用物理页，则不需要复制。  

写入时复制使    \lstinline{fork}    更快，因为    \lstinline{fork}    不需要复制内存。稍后写入时，某些内存必须被复制，但通常情况下，大多数内存永远不需要复制。一个常见的例子是
    \lstinline{fork}    后跟    \lstinline{exec}    ：在    \lstinline{fork}    之后可能会写入几页，但随后子进程的    \lstinline{exec}    会释放从父进程继承的大部分内存。写入时复制    \lstinline{fork}    无需复制该内存。此外，COW \indextext{fork}是透明的：无需对应用程序进行任何修改即可受益。  

除了 COW 分支之外，页表和页错误的组合还开辟了广泛的有趣的可能性。另一个广泛使用的功能称为    \indextext{lazy allocation}    ，它有两个部分。首先，当应用程序通过调用请求更多内存时
    \lstinline{sbrk}    ，内核注意到大小的增加，但不分配物理内存，也不为新的虚拟地址范围创建 PTE。其次，当这些新地址之一发生页面错误时，内核会分配物理内存页面并将其映射到页表中。与 COW fork 一样，内核可以对应用程序透明地实现延迟分配。  

由于应用程序经常要求比它们需要的更多的内存，因此延迟分配是一个胜利：内核不必为应用程序从不使用的页面执行任何工作。此外，如果应用程序要求大量增加地址空间，则没有延迟分配的    \lstinline{sbrk}    成本高昂：如果应用程序要求 1 GB 内存，则内核必须分配 262,144 4096 字节页面并将其归零。惰性分配允许这种成本随着时间的推移而分散。另一方面，惰性分配会带来页面错误的额外开销，这涉及内核/用户转换。操作系统可以通过为每个页面错误分配一批连续的页面而不是一个页面以及专门针对此类页面错误的内核进入/退出代码来降低此成本。  

另一个广泛使用的利用页面错误的功能是
    \indextext{demand paging}    。在    \lstinline{exec}    中，xv6 将应用程序的所有文本和数据急切地加载到内存中。由于应用程序可能很大并且从磁盘读取数据的成本很高，因此这种启动成本可能会引起用户的注意：当用户从 shell 启动大型应用程序时，用户可能需要很长时间才能看到响应。为了缩短响应时间，现代内核为用户地址空间创建页表，但将页面的 PTE 标记为无效。发生页面错误时，内核从磁盘读取页面内容并将其映射到用户地址空间。与 COW fork 和延迟分配一样，内核可以对应用程序透明地实现此功能。  

计算机上运行的程序可能需要比计算机 RAM 更多的内存。为了优雅地应对，操作系统可能会实现    \indextext{paging to disk}    。这个想法是只将一部分用户页面存储在 RAM 中，并将其余部分存储在磁盘上
    \indextext{paging area}    。内核将与存储在分页区域（因此不在 RAM 中）的内存相对应的 PTE 标记为无效。如果应用程序尝试使用已  {    \it    paged out   }  到磁盘的页面之一，则应用程序将引发页面错误，并且该页面必须是  {    \it    paged in   }  ：kerneltrap处理程序将分配物理 RAM 页面，将该页面从磁盘读取到 RAM 中，并修改相关PTE以指向RAM。  

如果需要调入一个页面，但没有可用的物理 RAM，会发生什么情况？在这种情况下，内核必须首先释放物理页，方法是将其调出或  {    \it    evicting   }  到磁盘上的分页区域，并将引用该物理页的 PTE 标记为无效。驱逐的成本很高，因此如果不频繁进行分页，则性能最佳：如果应用程序仅使用其内存页面的子集，并且子集的并集适合 RAM。该属性通常被称为具有良好的引用局部性。与许多虚拟内存技术一样，内核通常以对应用程序透明的方式实现对磁盘的分页。  

无论硬件提供多少 RAM，计算机通常都使用很少或没有  {    \it    free   }  物理内存来运行。例如，云提供商在一台机器上多路复用许多客户，以便经济高效地使用他们的硬件。另一个例子，用户在智能手机上的少量物理内存中运行许多应用程序。在这样的设置中，分配页面可能需要首先驱逐现有页面。因此，当可用物理内存稀缺时，分配成本很高。  

当可用内存稀缺时，延迟分配和请求分页特别有利。急切地分配内存
    \lstinline{sbrk}    或    \lstinline{exec}    会产生额外的驱逐成本以使内存可用。此外，还存在浪费急切工作的风险，因为在应用程序使用该页面之前，操作系统可能已将其驱逐。  

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

   \section{真实世界  }     

\indextext{trampoline}和\indextext{trap}框架可能看起来过于复杂。一个驱动力是 RISC-V 在强制\indextext{trap}时有意尽可能少地执行操作，以允许非常快速的\indextext{trap}处理，事实证明这很重要。因此，kerneltrap处理程序的前几条指令实际上必须在用户环境中执行：用户页表和用户寄存器内容。\indextext{trap}处理程序最初不知道有用的事实，例如正在运行的进程的身份或内核页表的地址。解决方案是可能的，因为 RISC-V 提供了受保护的位置，内核可以在进入用户空间之前在其中隐藏信息：  {    \tt    scratch   }  寄存器和指向内核内存但由于缺少    \lstinline{PTE_U}    而受到保护的用户页表条目。 Xv6 的 \indextext{trampoline} 和 \indextext{trapframe} 利用了这些 RISC-V 功能。  

如果将内核内存映射到每个进程的用户页表（具有适当的 PTE 权限标志），则可以消除对特殊\indextext{trampoline}页面的需求。当从用户空间捕获到内核时，这也将消除对页表切换的需要。这反过来又允许内核中的系统调用实现利用当前进程的用户内存映射，从而允许内核代码直接取消引用用户指针。许多操作系统都使用这些想法来提高效率。 Xv6 避免了它们，以减少由于无意使用用户指针而导致内核中出现安全错误的可能性，并减少确保用户和内核虚拟地址不重叠所需的复杂性。  

生产操作系统实现写时复制\indextext{fork}、延迟分配、请求分页、分页到磁盘、内存映射文件等。此外，生产操作系统将尝试使用所有物理内存，无论是用于应用程序还是缓存（例如，缓冲区缓存）文件系统的部分，我们将在后面的部分~    \ref{s:bcache}    中介绍）。 Xv6 在这方面是 na\"{i}ve：您希望操作系统使用您付费的物理内存，但 xv6 不这样做。此外，如果 xv6 内存不足，它会向正在运行的应用程序返回错误或终止它，而不是逐出另一个应用程序的页面。  

   \section{练习  }     

   \begin{enumerate}


   \item   函数  {    \tt    copyin   }  和  {    \tt    copyinstr   }   在软件中遍历用户页表。设置内核页表，以便内核映射用户程序， {    \tt    copyin   }  和  {    \tt    copyinstr   }   可以使用  {    \tt    memcpy   }  将系统调用参数复制到内核空间，依靠硬件进行页表遍历。   \item   实现惰性内存分配。   \item   实施 COW \indextext{fork}。   \item   有没有办法消除每个用户地址空间中的特殊  {    \tt    trapframe   }  页面映射？例如，可以
  {    \tt    uservec   }  是否可以修改为简单地将 32 个用户寄存器压入内核堆栈，或者将它们存储在  {    \tt    proc   }  结构中？   \item   是否可以修改 xv6 以消除特殊的  {    \tt    TRAMPOLINE   }  页面映射？  \end{enumerate}     


