MIT 6s081 blog2.xv6 Trap(陷阱)机制

发布时间:2024年01月17日

xv6 Trap(陷阱)机制

trap机制:每当程序执行系统调用程序出现了类似page fault、运算时除以0的错误、一个设备触发了中断使得当前程序运行需要响应内核设备驱动。从来源上看,陷阱可以分为用户态陷阱(系统调用、严重错误、中断)和内核态陷阱(严重错误、中断)。

关于RISC-V架构中的异常与中断涉及到的概念和相关寄存器参考:

6.S081——补充材料——RISC-V架构中的异常与中断详解_risc-v 中断设计-CSDN博客

系统调用过程

trap代码执行流程

trap代码的执行流程:以在shell中运行write系统调用为例:

在这里插入图片描述

ecall之前

首先在usys.s中,它将SYS_write加载到a7寄存器,SYS_write是常量16。这里告诉内核,我想要运行第16个系统调用,而这个系统调用正好是write。之后这个函数中执行了ecall指令,从这里开始代码执行跳转到了内核。内核完成它的工作之后,代码执行会返回到用户空间,继续执行ecall之后的指令,也就是ret,最终返回到Shell中。所以ret从write库函数返回到了Shell中。

在这里插入图片描述

此时用户寄存器的状态:

在这里插入图片描述

a0,a1,a2是Shell传递给write系统调用的参数,a7寄存器为系统调用编号

ecall之后

ecall指令:将代码从user mode改到supervisor mode,将程序计数器的值保存在了SEPC寄存器,跳转到STVEC寄存器指向的指令(在进入到用户空间之前,内核会将trampoline page(注意这个页是在进程页表中的,但没有设置PTE_U,因此用户无法访问,只有进入内核后才能访问)的地址存在STVEC寄存器中)。

总的来说就是:(保护现场)

  • 提升提权模式至supervisor
  • 保存当前pc值
  • 保存产生陷阱的原因
  • 关闭中断
  • 保存产生异常的地址
  • 更新pc值,准备进入trampoline程序段

uservec(保护现场,进入内核)

在这里插入图片描述

汇编程序:

现在程序位于trampoline page的起始,也是uservec函数的起始。需要做的第一件事情就是保存寄存器的内容。

对于保存用户寄存器,XV6在RISC-V上的实现包括了两个部分。第一个部分是,XV6在每个user page table映射了trapframe page,这样每个进程都有自己的trapframe page。这个page包含了很多有趣的数据,但是现在最重要的数据是用来保存用户寄存器的32个空槽位。这个位置的虚拟地址总是0x3ffffffe000。

在这里插入图片描述

在trapframe 中保存有内核的页表、进程的内核栈、接下来要跳转到的函数指针地址usertrap()、用户程序计数器,tp寄存器,用于保存此时的CPU核。

  • uservec中首先使用csrrrw指令交换a0和sscratch两个寄存器的内容,sscratch中保存了trapframe 页的虚拟地址。然后将用户寄存器保存在trapframe 中。(保护现场
  • 然后加载sp寄存器、写入tp寄存器,然后写入t0寄存器(保存将要执行的第一个C函数的指针:usertrap),然后写t1寄存器,写入kernel page table的地址,然后交换SATP和t1寄存器,从而实现页表从user page table切换到kernel page table(此时代码仍然在trampoline(相同的页)中,因此程序并没有崩溃)。最后jr t0,跳转到内核的C代码usertrap中。(保护现场,进行切换

之所以叫trampoline,是因为你某种程度在它上面“弹跳”了一下,然后从用户空间走到了内核空间。

总的来说,uservec的作用就是:

  • 保存用户态的cpu通用寄存器到trapframe页(进程独有的内存页中,参见blog1的进程页表),保存内核栈指针地址、内核页表。
  • 将页表从进程页表切换至内核页表,刷新快表
  • 跳转到内核的C函数usertrap中

usertrap(进而调用syscall)

void usertrap(void)
{
  int which_dev = 0;

  if((r_sstatus() & SSTATUS_SPP) != 0) //判断是否来自User mode
    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){   // 1.系统调用system call
    if(p->killed)
      exit(-1);

    // 执行完系统调用后,返回到ecall的下一条指令
    p->trapframe->epc += 4;

    intr_on(); // 开中断

    syscall();
  } else if((which_dev = devintr()) != 0){ // 2.设备异常处理
   
  } else { // 3.程序异常
    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);

  // 定时器中断,发生一次调度
  if(which_dev == 2)
    yield();

  usertrapret(); // 返回
}

void syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->trapframe->a0 = syscalls[num](); // 调用全局系统调用函数指针数组,并将返回值放入a0寄存器
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

usertrap函数处理来自用户空间的陷阱,判断陷阱原因,并进行相应处理(如系统调用/设备中断/程序异常)。

1、usertrap中先将STVEC指向了kernelvec变量,这是内核空间trap处理代码的位置,而不是用户空间trap处理代码的位置。(内核空间trap不在本节讨论范围内)

2、保存用户程序计数器到p->trapframe->epc,防止当当程序还在内核中执行时,我们可能切换到另一个进程,并进入到那个程序的用户空间,然后那个进程可能再调用一个系统调用进而导致SEPC寄存器的内容被覆盖。保存当前进程的SEPC寄存器到一个与该进程关联的内存中,这样这个数据才不会被覆盖。

3、找出我们现在会在usertrap函数的原因,根据SCAUSE寄存器判断触发trap原因,如果是8则说明是系统调用导致(查询RISC-V的内核异常码)。

4、 p->trapframe->epc += 4; 用于存储ecall的下一条指令

5、intr_on(); 显式打开中断,因为一些系统调用比较耗时,所以打开中断(此前被rise-v的trap硬件关闭了)

6.调用syscall函数(根据传入的系统调用号,去全局的系统调用的函数指针数组中进行调用

7、syscall返回后检查进程是否被杀死,没有被杀死则恢复进程,调用usertrapret函数

总结就是:usertrap函数的作用:

  • 根据陷阱类型进行分发,根据类型为系统调用/设备中断/程序异常进行具体的处理

usertrapret

设置完成在返回到用户空间之前内核要做的工作。

1、关闭中断

2、设置STVEC寄存器指向trampoline代码,在那里最终会执行sret指令返回到用户空间。

3.填入了trapframe的内容(kernel page table的指针、当前用户进程的kernel stack、usertrap函数的指针、当前的CPU核编号)来保证进程的下一次进入内核时传入的参数。

4.设置SSTATUS寄存器,控制SPPbit为0,保证下一次执行sret时,返回user mode,同时设置SPIE位为1,来使得执行完sret后打开中断

5.设置sepc寄存器为之前保存的用户程序计数器。

6、调用Userret函数,传入Trapframe和satp(用户页表地址)的值

userret

userret也是在trampoline中。

1、切换页表,从内核ptb切换成小得多的user ptb

2、加载之前保护的用户寄存器的值(将trapframe中的值加载到各个寄存器中)

3、sret指令

之所以叫trampoline,是因为你某种程度在它上面“弹跳”了一下,然后又从内核空间回到了用户空间。

sret指令执行完后:

  • 程序会切换回user mode
  • SEPC寄存器的数值会被拷贝到PC寄存器(程序计数器)
  • 重新打开中断

然后就调回用户空间了

总结来说就是:

  • 从内核页表切换回进程页表
  • cpu恢复user mode
  • 回到之前发生陷阱的程序位置

内核陷阱

在usertrap中有一段代码w_stvec((uint64)kernelvec);将stvec寄存器设置为kernelvec,当内核发生陷阱时PC就会从kernelvec中开始执行。

kernelvec

kernelvec位于kernel/kernelvec.S中,此时已经位于内核,因此不需要切换页表,设置内核栈之类的操作,只需要保存寄存器,调用kerneltrap,恢复寄存器并返回。

	#
        # interrupts and exceptions while in supervisor
        # mode come here.
        #
        # push all registers, call kerneltrap(), restore, return.
        #
.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 调用kerneltrap 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

        #
        # machine-mode timer interrupt.
        #
.globl timervec
.align 4
timervec:
        # start.c has set up the memory that mscratch points to:
        # scratch[0,8,16] : register save area.
        # scratch[32] : address of CLINT's MTIMECMP register.
        # scratch[40] : desired interval between interrupts.
        
        csrrw a0, mscratch, a0
        sd a1, 0(a0)
        sd a2, 8(a0)
        sd a3, 16(a0)

        # schedule the next timer interrupt
        # by adding interval to mtimecmp.
        ld a1, 32(a0) # CLINT_MTIMECMP(hart)
        ld a2, 40(a0) # interval
        ld a3, 0(a1)
        add a3, a3, a2
        sd a3, 0(a1)

        # raise a supervisor software interrupt.
	li a1, 2
        csrw sip, a1

        ld a3, 16(a0)
        ld a2, 8(a0)
        ld a1, 0(a0)
        csrrw a0, mscratch, a0

        mret

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){ //调用devintr函数对中断类型进行判断,并进行对应处理
    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);
}

kerneltrap只需要处理设备中断(串口/磁盘)和定时器中断即可(不可能是系统调用)。如果是定时器中断,则调用yield让出CPU。当kerneltrap完成后则返回到kernelvec函数,恢复寄存器,回到被中断的位置。

文章来源:https://blog.csdn.net/weixin_45988024/article/details/135643876
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。