主要分析如下问题:

  • 用户态下中断处理
  • 内核态下中断处理

1. 用户态下中断处理

1.1 进入中断前的准备工作

RISC-V 的中断处理有一个专门的寄存器 stvec ,用于存放中断处理函数的入口地址,用户进程在创建的时候,会设置该寄存器,将其值设置为 uservec 函数地址,这样在执行用户进程的时候,触发中断,就会执行 uservec 函数。
uservec 的定义如下:

  1. .globl trampoline
  2. trampoline:
  3. .align 4
  4. .globl uservec
  5. uservec:
  6. #
  7. # trap.c sets stvec to point here, so
  8. # traps from user space start here,
  9. # in supervisor mode, but with a
  10. # user page table.
  11. #
  12. # sscratch points to where the process's p->trapframe is
  13. # mapped into user space, at TRAPFRAME.
  14. #
  15. # swap a0 and sscratch
  16. # so that a0 is TRAPFRAME
  17. csrrw a0, sscratch, a0
  18. # save the user registers in TRAPFRAME
  19. sd ra, 40(a0)
  20. sd sp, 48(a0)
  21. sd gp, 56(a0)
  22. sd tp, 64(a0)
  23. sd t0, 72(a0)
  24. sd t1, 80(a0)
  25. sd t2, 88(a0)
  26. sd s0, 96(a0)
  27. sd s1, 104(a0)
  28. sd a1, 120(a0)
  29. sd a2, 128(a0)
  30. sd a3, 136(a0)
  31. sd a4, 144(a0)
  32. sd a5, 152(a0)
  33. sd a6, 160(a0)
  34. sd a7, 168(a0)
  35. sd s2, 176(a0)
  36. sd s3, 184(a0)
  37. sd s4, 192(a0)
  38. sd s5, 200(a0)
  39. sd s6, 208(a0)
  40. sd s7, 216(a0)
  41. sd s8, 224(a0)
  42. sd s9, 232(a0)
  43. sd s10, 240(a0)
  44. sd s11, 248(a0)
  45. sd t3, 256(a0)
  46. sd t4, 264(a0)
  47. sd t5, 272(a0)
  48. sd t6, 280(a0)
  49. # save the user a0 in p->trapframe->a0
  50. csrr t0, sscratch
  51. sd t0, 112(a0)
  52. # restore kernel stack pointer from p->trapframe->kernel_sp
  53. ld sp, 8(a0)
  54. # make tp hold the current hartid, from p->trapframe->kernel_hartid
  55. ld tp, 32(a0)
  56. # load the address of usertrap(), p->trapframe->kernel_trap
  57. ld t0, 16(a0)
  58. # restore kernel page table from p->trapframe->kernel_satp
  59. ld t1, 0(a0)
  60. csrw satp, t1
  61. sfence.vma zero, zero
  62. # a0 is no longer valid, since the kernel page
  63. # table does not specially map p->tf.
  64. # jump to usertrap(), which does not return
  65. jr t0

这里总体来说做了如下步骤:

  • 保存用户态寄存器上下文到 proc.trapframe,该结构为进程结构 struct proc 的成员变量,主要保存用户态与内核态的上下文
  • 切换当前栈寄存器为用户进程的内核栈寄存器(每个用户进程都有自己的内核栈)
  • 切换页表为内核页表,并清空 TLB
  • 跳转到 usertrap,这里是具体处理中断逻辑的地方

1.2 处理中断

前面准备工作处理完后,接着就是根据中断号再进一步处理中断,可以通过 r_scause 来捕获当前中断号,如下所示:

//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
void usertrap(void)
{
  int which_dev = 0;

  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);

  struct proc *p = myproc();

  // save user program counter.
  p->trapframe->epc = r_sepc();

  if(r_scause() == 8){
    // system call

    if(p->killed)
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;

    // an interrupt will change sstatus &c registers,
    // so don't enable until done with those registers.
    intr_on();

    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
  }

  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

有几点细节需要注意:

  • 进入中断后默认会关闭中断
  • 在处理系统调用时,会对指令寄存器 p->trapframe->epc += 4; 来跳过 ecall 指令,防止重复执行,因为中断返回的时候默认会重新执行触发中断时的代码。
  • 触发用户中断时,会重写 stvec 寄存器,前文说过,该寄存器为中断入口,xv6 中允许内核继续触发中断(有些简单的内核,比如操作系统真象还原,为了实现简单,会在内核态的时候屏蔽中断),设置为 kenreltrap 函数。

1.3 中断返回

usertrap 执行完毕后,其最后会执行 usertrapret,用来切换权限到用户态,并且返回继续执行用户进程的代码。其逻辑如下:

//
// return to user space
//
void
usertrapret(void)
{
  struct proc *p = myproc();

  // we're about to switch the destination of traps from
  // kerneltrap() to usertrap(), so turn off interrupts until
  // we're back in user space, where usertrap() is correct.
  intr_off();

  // send syscalls, interrupts, and exceptions to trampoline.S
  w_stvec(TRAMPOLINE + (uservec - trampoline));

  // set up trapframe values that uservec will need when
  // the process next re-enters the kernel.
  p->trapframe->kernel_satp = r_satp();         // kernel page table
  p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
  p->trapframe->kernel_trap = (uint64)usertrap;
  p->trapframe->kernel_hartid = r_tp();         // hartid for cpuid()

  // set up the registers that trampoline.S's sret will use
  // to get to user space.

  // set S Previous Privilege mode to User.
  unsigned long x = r_sstatus();
  x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
  x |= SSTATUS_SPIE; // enable interrupts in user mode
  w_sstatus(x);

  // set S Exception Program Counter to the saved user pc.
  w_sepc(p->trapframe->epc);

  // tell trampoline.S the user page table to switch to.
  uint64 satp = MAKE_SATP(p->pagetable);

  // jump to trampoline.S at the top of memory, which 
  // switches to the user page table, restores user registers,
  // and switches to user mode with sret.
  uint64 fn = TRAMPOLINE + (userret - trampoline);
  ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
}

这里整体来说做了几步操作:

  • 保存当前内核的上下文到用户进程结构 struct proc,以便下次重新进入内核态,如内核页表,内核栈指针
  • 切换特权级,开启中断
  • 更新 异常指令寄存器 ,该寄存器用于用户进程中断返回后确定继续执行的指令位置
  • 切换页表为用户进程的页表
  • 执行 userret 函数

userret 函数具体逻辑如下:

.globl userret
userret:
    # userret(TRAPFRAME, pagetable)
    # switch from kernel to user.
    # usertrapret() calls here.
    # a0: TRAPFRAME, in user page table.
    # a1: user page table, for satp.

    # switch to the user page table.
    csrw satp, a1
    sfence.vma zero, zero

    # put the saved user a0 in sscratch, so we
    # can swap it with our a0 (TRAPFRAME) in the last step.
    ld t0, 112(a0)
    csrw sscratch, t0

    # 这里因为 a0 寄存器目前还存放着 userret 的参数,所以不能直接覆盖回去
    # restore all but a0 from TRAPFRAME
    ld ra, 40(a0)
    ld sp, 48(a0)
    ld gp, 56(a0)
    ld tp, 64(a0)
    ld t0, 72(a0)
    ld t1, 80(a0)
    ld t2, 88(a0)
    ld s0, 96(a0)
    ld s1, 104(a0)
    ld a1, 120(a0)
    ld a2, 128(a0)
    ld a3, 136(a0)
    ld a4, 144(a0)
    ld a5, 152(a0)
    ld a6, 160(a0)
    ld a7, 168(a0)
    ld s2, 176(a0)
    ld s3, 184(a0)
    ld s4, 192(a0)
    ld s5, 200(a0)
    ld s6, 208(a0)
    ld s7, 216(a0)
    ld s8, 224(a0)
    ld s9, 232(a0)
    ld s10, 240(a0)
    ld s11, 248(a0)
    ld t3, 256(a0)
    ld t4, 264(a0)
    ld t5, 272(a0)
    ld t6, 280(a0)

    # restore user a0, and save TRAPFRAME in sscratch
    csrrw a0, sscratch, a0

    # return to user mode and user pc.
    # usertrapret() set up sstatus and sepc.
    sret

主要重点如下:

  • 切换页表,清空 TLB
  • 前文进入中断的时候会保存用户态时的上下文,这里需要将其恢复回来
  • 保存用户进程的 proc.trapframesscratch 寄存器
  • sret 指令返回到用户态

    这里 sscratch 寄存器的主要作用在于保存 proc.trapframe,触发中断时,我们需要做两步操作

    • 保存用户态上下文
    • 切换到内核态上下文

    这两者上下文都存放在用户进程的 proc.trapframe 结构中,由于用户进程和内核使用不同的页表,用户进程刚触发中断时,使用的还是用户进程的页表,因此是无法获取位于内核中当前进程的 proc.trapframe。 因此,xv6 对于每个进程的 proc.trapframe ,专门定义了一个虚拟地址宏 TRAPFRAME,初始化用户进程时,用户进程页表会手动映射 proc.trapframe 与该宏,这样用户进程访问 TRAPFRAME 就能够读写 proc.trapframe xv6 将 TRAPFRAME 保存在 sscratch 寄存器中,sscratch 寄存器在我们中断切换特权级,或者函数调用的时候,均不会修改它的值,因此可以用来专门保存用户进程的 proc.trapframe

1.4 小结

用户进程进入中断的触发流程为 uservec -> usertrap,主要流程如下:

  • 保存用户态寄存器上下文到 proc.trapframe,该结构为进程结构 struct proc 的成员变量,主要保存用户态与内核态的上下文
  • 切换当前栈寄存器为用户进程的内核栈寄存器(每个用户进程都有自己的内核栈)
  • 切换页表为内核页表,并清空页表缓存
  • 跳转到 usertrap,这里是具体处理中断逻辑的地方

从中断返回到用户进程的触发流程为 usertrap -> userret,主要流程如下:

  • 切换页表,清空页表缓存
  • 保存当前内核态上下文到 proc.trapframe
  • 读取 TRAPFRAME ,恢复用户进程上下文到对应寄存器
  • 保存用户进程的 TRAPFRAME 地址到 sscratch 寄存器,方便用户进程再次进入用户态时,保存用户态上下文以及切换到内核态上下文。
  • sret 指令返回到用户态

2. 内核态下中断处理

在进入内核态的时候,xv6 会将中断入口从 uservec 修改为 kernelvec

2.1 进入中断前的准备工作

内核中断的处理流程相对比较简单。
进入内核中断前,其操作与用户态中断类似,也是保存当前上下文寄存器,只是由于当前已经处于内核态了,因此主要差异如下:

  • 不需要切换栈
  • 不需要切换页表
  • 不需要切换到新的上下文
  • 寄存器直接保存在栈上

    .globl kerneltrap
    .globl kernelvec
    .align 4
    kernelvec:
      // make room to save registers.
      addi sp, sp, -256
    
      // save the registers.
      sd ra, 0(sp)
      sd sp, 8(sp)
      sd gp, 16(sp)
      sd tp, 24(sp)
      sd t0, 32(sp)
      sd t1, 40(sp)
      sd t2, 48(sp)
      sd s0, 56(sp)
      sd s1, 64(sp)
      sd a0, 72(sp)
      sd a1, 80(sp)
      sd a2, 88(sp)
      sd a3, 96(sp)
      sd a4, 104(sp)
      sd a5, 112(sp)
      sd a6, 120(sp)
      sd a7, 128(sp)
      sd s2, 136(sp)
      sd s3, 144(sp)
      sd s4, 152(sp)
      sd s5, 160(sp)
      sd s6, 168(sp)
      sd s7, 176(sp)
      sd s8, 184(sp)
      sd s9, 192(sp)
      sd s10, 200(sp)
      sd s11, 208(sp)
      sd t3, 216(sp)
      sd t4, 224(sp)
      sd t5, 232(sp)
      sd t6, 240(sp)
    
      // call the C trap handler in trap.c
      call kerneltrap
    
      // restore registers.
      ld ra, 0(sp)
      ld sp, 8(sp)
      ld gp, 16(sp)
      // not this, in case we moved CPUs: ld tp, 24(sp)
      ld t0, 32(sp)
      ld t1, 40(sp)
      ld t2, 48(sp)
      ld s0, 56(sp)
      ld s1, 64(sp)
      ld a0, 72(sp)
      ld a1, 80(sp)
      ld a2, 88(sp)
      ld a3, 96(sp)
      ld a4, 104(sp)
      ld a5, 112(sp)
      ld a6, 120(sp)
      ld a7, 128(sp)
      ld s2, 136(sp)
      ld s3, 144(sp)
      ld s4, 152(sp)
      ld s5, 160(sp)
      ld s6, 168(sp)
      ld s7, 176(sp)
      ld s8, 184(sp)
      ld s9, 192(sp)
      ld s10, 200(sp)
      ld s11, 208(sp)
      ld t3, 216(sp)
      ld t4, 224(sp)
      ld t5, 232(sp)
      ld t6, 240(sp)
    
      addi sp, sp, 256
    
      // return to whatever we were doing in the kernel.
      sret
    

    2.2 处理中断

    处理中断的接口为 kerneltrap,流程如下:

    // interrupts and exceptions from kernel code go here via kernelvec,
    // on whatever the current kernel stack is.
    void kerneltrap()
    {
    int which_dev = 0;
    uint64 sepc = r_sepc();
    uint64 sstatus = r_sstatus();
    uint64 scause = r_scause();
    
    if((sstatus & SSTATUS_SPP) == 0)
      panic("kerneltrap: not from supervisor mode");
    if(intr_get() != 0)
      panic("kerneltrap: interrupts enabled");
    
    if((which_dev = devintr()) == 0){
      printf("scause %p\n", scause);
      printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
      panic("kerneltrap");
    }
    
    // give up the CPU if this is a timer interrupt.
    if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
      yield();
    
    // the yield() may have caused some traps to occur,
    // so restore trap registers for use by kernelvec.S's sepc instruction.
    w_sepc(sepc);
    w_sstatus(sstatus);
    }
    

    这里只处理了时钟中断,流程比较简单。

    2.3 中断返回

    这里中断返回不像用户态中断那么复杂,另行封装了 usertrapret 接口,而是直接执行完 kerneltrap 之后,就直接从栈上恢复寄存器。可参考 2.1 的代码逻辑。

2.4 小结

在内核态中处理中断的流程大致就是

  • 保存当前寄存器上下文到栈
  • 执行 kerneltrap 处理中断逻辑
  • 从栈上恢复寄存器上下文

与用户态中断的差异点主要在于:

  • 不需要切换栈
  • 不需要切换页表
  • 不需要切换到新的上下文
  • 寄存器直接保存在栈上

这里主要由于特权级没有变更,依旧在内核中执行,导致步骤相对简单。