• xv6源码阅读——中断与异常


    说明

    • 阅读的代码是 xv6-riscv 版本的

    陷入机制概述

    每个RISC-V CPU都有一组控制寄存器,内核通过向这些寄存器写入内容来告诉CPU如何处理陷阱,内核可以读取这些寄存器来明确已经发生的陷阱。RISC-V文档包含了完整的内容。riscv.h(kernel/riscv.h:1)包含在xv6中使用到的内容的定义。以下是最重要的一些寄存器概述:

    • stvec:内核在这里写入其陷阱处理程序的地址;RISC-V跳转到这里处理陷阱。
    • sepc:当发生陷阱时,RISC-V会在这里保存程序计数器pc(因为pc会被stvec覆盖)
      sret(从陷阱返回)指令会将sepc复制到pc。内核可以写入sepc来控制sret的去向。
    • scause: RISC-V在这里放置一个描述陷阱原因的数字。
    • sscratch:内核在这里放置了一个值,这个值在陷阱处理程序一开始就会派上用场。
    • sstatus:其中的SIE位控制设备中断是否启用。如果内核清空SIE,RISC-V将推迟设备中断,直到内核重新设置SIE。SPP位指示陷阱是来自用户模式还是管理模式,并控制sret返回的模式。

    上述寄存器都用于在管理模式下处理陷阱,在用户模式下不能读取或写入。在机器模式下处理陷阱有一组等效的控制寄存器,xv6仅在计时器中断的特殊情况下使用它们。

    Traps from user space

    在用户空间中,使用系统调用会触发trap机制
    例如:write()函数

    .global write
    write:
     li a7, SYS_write
     ecall
     ret
    
    • 1
    • 2
    • 3
    • 4
    • 5

    调用逻辑

    在这里插入图片描述

    ecall

    这是一个汇编指令,他会做下面操作

    1.清除SIE以禁用中断。
    2.将pc复制到sepc。
    3.将当前模式(用户或管理)保存在状态的SPP位中。
    4.设置scause以反映产生陷阱的原因。
    5.将模式设置为管理模式。
    6.将stvec复制到pc。
    7.在新的pc上开始执行。

    注意:stvec指向的地址是 uservec,将stvec复制到pc后,下面会到从uservec开始执行

    uservec

    # trampoline.S
    uservec:
        # 交换 a0 和 sscratch 寄存器的值
            # so that a0 is TRAPFRAME
            csrrw a0, sscratch, a0
    
            # 将寄存器保存到当前进程的 trapframe 中
            sd ra, 40(a0)
            # ... 保存寄存器
            sd t6, 280(a0)
    
            # 同时也保存 a0
            csrr t0, sscratch
            sd t0, 112(a0)
    
            # 从 user mode 的 traptable 中恢复一些内核的信息
            ld sp, 8(a0)
            ld tp, 32(a0)
            ld t0, 16(a0)
            ld t1, 0(a0)
            csrw satp, t1
            sfence.vma zero, zero
    
            # a0 is no longer valid, since the kernel page
            # table does not specially map p->tf.
    
            # jump to usertrap(), which does not return
            jr t0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 在进行系统调用之前,p->trapframe 的起始地址会被保存在 sscratch 寄存器中
    • usertrap 首先将所有的寄存器保存在 p->trapframe
    • 将原本保存在p->trapframe中的内核信息加载到寄存器当中
      • sp
      • tp
      • 内核页表地址
      • usertrap地址
    • 将页表切换到内核页表
    • 跳转到usertrap

    usertrap

    void usertrap(void) {
      int which_dev = 0;
      if((r_sstatus() & SSTATUS_SPP) != 0)
        panic("usertrap: not from user mode");
    
      // 设置 stvec 为 kernelvec
      w_stvec((uint64)kernelvec);
    
      struct proc *p = myproc();
    
      // 保存PC, 否则可能会有其他的 usertrap 修改它
      p->trapframe->epc = r_sepc();
    
      if(r_scause() == 8){
        // system call
    
        if(p->killed)
          exit(-1);
    
        // 系统调用返回下一条命令
        p->trapframe->epc += 4;
    
        // 做完寄存器的操作之后打开设备中断
        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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 判断是否在管理模式下
    • 设置 stvec 为 kernelvec
    • 保存PC, 否则可能会有其他的 usertrap 修改它
    • 判断trap类型
      • 系统调用
        • p->trapframe->epc += 4(系统调用返回下一条命令)
        • 打开设备中断
        • syscall()进行系统调用
      • 设备中断
        • yield
      • 异常
    • 根据trap类型执行相应操作
    • 调用usertrapret函数

    usertrapret

    该部分代码就是做一些返回用户模式的准备

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 关闭中断
    • 将内核信息保存到p->trapfarm当中
    • 设置sstatus寄存器 (用户模式)
    • 将p->trapframe->epc(用户模式下要执行的下一条命令)放到sepc寄存器当中
    • 将用户页表保存在stap(此处是定义的变量,并非是寄存器)
    • 跳转到rampoline,执行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
    
            # restore all but a0 from TRAPFRAME
            ld ra, 40(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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 切换到用户页表
    • 将之前保存的寄存器恢复
    • 将TRAPFRAME保存回ssractch当中
    • sret

    sret

    • 程序切换到用户模式
    • 将sepc存到pc当中
    • 开启中断
    • 跳到pc开始执行

    Traps from kernel mode

    • 在内核模式下,trap 只有两类:exceptions 、device interrupt
    • 当一个 trap 发生的时候,首先硬件开始工作,配置寄存器
    • 此时 stvec 指向了 kernelvec 的起始地址

    kernelvec

    因为是发生在内核状态下的,所以相较于系统调用,就简单很多

    # kernel/kernelvec.S
    .globl kerneltrap
    .globl kernelvec
    .align 4
    kernelvec:
            # 在栈上开辟一块空间用于保存寄存器
            addi sp, sp, -256
            sd ra, 0(sp)
            # ... 保存所有寄存器
            sd t6, 240(sp)
    
            # 调用 C 处理程序
            call kerneltrap
    
            # 恢复寄存器到之前的状态
            ld ra, 0(sp)
            # ... 恢复所有的寄存器(除了 tp)
            ld t6, 240(sp)
    
            # 恢复栈指针
            addi sp, sp, 256
    
            # 返回到之前的运行状态
            sret
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 在栈上开辟一段空间,用来保存寄存器
    • 保存寄存器
    • 调用 C 处理程序kerneltrap
    • 恢复寄存器到之前的状态
    • 恢复栈指针
    • 返回到之前的运行状态

    kerneltrap

    kerneltrap的处理和usertrap还是很了类似的

    // 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 保存sepc,sstatus,scause寄存器(因为 yield()可能会引起其他陷阱,修改寄存器)
    • 判断是否处于管理模式
    • 判断trap类型,并执行相应操作
    • 恢复sepc,sstatus,scause寄存器

    感言

    这部分还是卡了很久,因为出现了很多汇编,需要了解xv6的寄存器,以及RSICV指令集,还有栈桢相关的知识,后续继续一步一步完成,继续努力

    参考资料

    • http://xv6.dgs.zone/tranlate_books/book-riscv-rev1/c1/s0.html
    • xv6-riscv源码
  • 相关阅读:
    集合框架:List系列集合:特点、方法、遍历方式、ArrayList,LinkList的底层原理
    pycharm debug调试点击结束断点报错KeyboardInterrupt
    HDFS的启动流程和HA
    PHP·解决http_build_query模拟浏览器请求多选参数加下标索引的BUG| 无法模拟浏览器多选参数问题
    面渣逆袭:MySQL六十六问,两万字+五十图详解!有点六!
    2022-9-3 22点 程序爱生活 纳指这波下跌需要缓口气,但是后面更加猛烈,恒指可能有反弹, 但会继续被裹挟下跌,创出新低
    俄罗斯方块
    【算法练习Day40】打家劫舍&&打家劫舍 II&&打家劫舍 III
    vue中 el-tab-plane 如何显示使用el-badge显示小红点
    【Spark NLP】第 13 章:构建知识库
  • 原文地址:https://blog.csdn.net/m0_61705102/article/details/126931007