自陷：CPU跳出当前指令流转而执行一些指定的代码。

#### 三种自陷

- 使用`ecall`指令来产生一个异常。XV6用它来触发系统调用。
- 指令非法执行引起的异常。
- 外设发出中断信号引起的设备中断。

#### xv6自陷处理的流程

##### 一、硬件动作

这个硬件动作不可被打断，CPU必须一次完成所有的步骤。

1. 如果当前自陷是设备中断，先判断`sstatus.SIE`的值，为1则继续向下执行，为0则终止自陷。因为，用户可以修改`SIE`位来控制是否接收中断。
2. `SIE`位置0。避免被其它中断打断。
3. 把`pc`复制到`sepc`。记录程序计数器的值。
4. 把当前的模式(用户或管理员)保存到`sstatus`寄存器的SPP位。记录当前特权级。
5. 设置`scause`来记录中断的原因。
6. 切换到管理员模式。
7. 把`stvec`保存到`pc`。这样就可以从我们指定的位置处理自陷了。
8. 从新的`pc`开始执行。

注：硬件动作做完后，页表、栈、上下文都还和自陷之前一样，如果自陷来自用户态，则需要在软件层面全部切换它们以满足内核态的环境。如果自陷来自内核态，则只需切换上下文即可。

##### 二、汇编代码

- 内核空间的自陷

  此时，`stvec`里保存的是`kernelvec`的地址。`kernelvec`保存上下文，就跳转到`kerneltrap`来分发自陷。当`kerneltrap`返回后，`kernelvec`就恢复之前保存的上下文，最后执行`sret`，这将会把`sepc`保存到`pc`从而恢复被中断的内核代码。

- 用户空间的自陷

  此时，`stvec`里保存的是`uservec`的地址。
  
  当用户进程从用户态切换到内核态的时候，它的页表还是用户态的，却要执行内核态的代码。xv6是通过把这段代码映射到同一个位置实现这一点的，这个位置即trampoline页的位置。
  
  `uservec`首先切换`a0`和`sscratch`的值。在切换之前，`a0`保存的是用户代码里使用的值，`sscratch`保存的是`trapframe`页的地址。为什么不直接使用相应寄存器的值，而是要切换一下呢？因为`sscratch`是管理和控制寄存器，它没法像通用寄存器那样直接使用，而是要先读取到一个通用寄存器里。
  
  `trapframe`页被用户态的页表映射到了指定的位置。这是为了方便用户进程在切换内核态后，在切换到内核页表之前访问它。
  
  `uservec`接下来把上下文保存在`trapframe`页，把`trapframe`页保存的内核栈、内核页表恢复到相应寄存器，就跳转到`usertrap`来分发自陷。`usertrap`并不会返回，而是调用`usertrapret`，`usertrapret`主要是保存一些控制和状态寄存器的值来为下一次用户进程的自陷做准备。
  
  `usertrapret`接下来跳转到`userret`，`userret`和`uservec`一样，也在trampoline页，也是一段汇编代码。它把用户页表恢复到`satp`寄存器，恢复之前保存的上下文，切换`a0`和`sscratch`的值，最后使用`sret`返回用户空间。

##### 三、分发自陷

- 内核空间的自陷

  `kerneltrap`用于分发内核空间的自陷，它通过`devinitr` 实现其功能。`devinitr`会进行判断，如果是管理员级外设中断，则分发给对应的中断处理例程处理，并返回1；如果是管理员级软件中断，则它来自于机器级的计时器中断，则进行一些处理并返回2；在内核空间只可能发生异常或设备中断，如果不是以上情况，则说明是异常，直接返回0.

  `kerneltrap`通过`devinitr`的返回值进行下一步的处理，如果返回值为0说明是异常，直接`panic`；如果返回值为2说明是计时器中断，这时要进一步看是不是调度器线程，如果是调度器线程则直接返回让调度器继续执行，如果是普通内核线程说明它的时间片到了，需要执行`yield`来让出CPU。

- 用户空间的自陷

  `usertrap`用于分发内核空间的自陷。它会进行判断，如果是系统调用，则用`syscall`来处理；接下来通过`devinitr`来处理设备中断；如果`devinitr`返回值为0，说明是异常，调用`exit`退出；如果`devinitr`返回值为2，说明是计时器中断，用户进程的时间片到了，则让出CPU。最后，调用`usertrapret`准备返回用户态。

##### 四、处理例程

- 计时器中断
- 系统调用
- 串口中断
- 磁盘中断