6.s081操作系统Lab4: trap

发布时间:2023年12月18日

chapter 4

概览

  1. trap的场景:系统调用,设备中断,异常
  2. trap对用户是透明的,用户不会察觉发生了1个trap:内核会保存trap前的状态,在trap后恢复

4.1 CPU trap流程

使用寄存器

stvec: 保存trap程序地址
sepc: 临时保存pc寄存器,trap结束时,sret(TODO 不知道是什么,可能是一段程序)会重新将sepc复杂到pc中
scause: trap原因
sscratch: 方便上下文切换

  1. 见userret,sscratch寄存器保存用户页表的trapframe
  2. 见uservec,trapframe页可以用来暂存用户态的寄存器,中断后切换回来;同时保存内核页表在中断时从用户页表切换到内核页表,可以认为是个中介的临时仓库

sstatus: SPP表示从用户态(0)或从内核态(1)切换过来的trap;SIE表示是否启用设备中断

如果cpu想处理1个trap

trap相关:设置scause和sstatus,保存trap原因和来源
状态保存相关:把pc暂存到sepc
执行相关:切换到监督者模式,把stvec复制到pc
cpu不会切换内核页表,不会切换内核栈。但是必须切换pc。

4.2 用户态引发的trap

4.2.1 uservec

uservec就是用户态的trap入口,即cpu的stvec会被设成uservec。
这里要完成3个事:

  1. 保存用户态的32个寄存器
  2. 切换satp寄存器,使用内核页表
  3. 调用处理中断的函数usertrap

(倒叙,写用户进程开始执行前的事情,可参见4.2.3节usertrapret和userret的功能)
在进入用户空间之前,内核会分配1页TRAPFRAME,专门用来暂存trap发生时需要的东西,这个TRAPFRAME的地址放在sscratch寄存器中,TRAPFRAME页还会预先放着开始就已经知道且在trap发生时需要用到的东西:usertrap的地址(进行trap类型判断并调用相应处理函数)、cpu的hartid(TODO,还不知道作用,可能是CPU的id,可以记录处理trap的CPU)、内核页表地址(uservec需要进行用户态页表到内核态页表的切换)。

.globl uservec
uservec:    
	#
        # trap.c sets stvec to point here, so
        # traps from user space start here,
        # in supervisor mode, but with a
        # user page table.
        #
        # sscratch points to where the process's p->trapframe is
        # mapped into user space, at TRAPFRAME.
        #
        
	# swap a0 and sscratch
        # so that a0 is TRAPFRAME
        csrrw a0, sscratch, a0

        # save the user registers in TRAPFRAME
        sd ra, 40(a0)
        sd sp, 48(a0)
        sd gp, 56(a0)
        .............

	# save the user a0 in p->trapframe->a0
        csrr t0, sscratch
        sd t0, 112(a0)

        # restore kernel stack pointer from p->trapframe->kernel_sp
        ld sp, 8(a0)

        # make tp hold the current hartid, from p->trapframe->kernel_hartid
        ld tp, 32(a0)

        # load the address of usertrap(), p->trapframe->kernel_trap
        ld t0, 16(a0)

        # restore kernel page table from p->trapframe->kernel_satp
        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

4.2.2 usertrap

usertrap函数会处理来自用户态的中断、异常或系统调用,由uservec汇编代码调用;这里会判断trap的原因,以调用合适的处理函数。最后调用usertrapret()返回用户态。

//
// 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();
}

4.2.3 usertrapret和userret

usertrapret

usertrapret:切换pc寄存器
userret:恢复寄存器,切换页表
usertrapret代码如下,

  1. 临时关闭中断功能:
    intr_off();将中断开关临时关闭(TODO:如何关闭),在从内核态到用户态的转换过程中,暂时停止中断功能,等切换完毕后再开启,可能是为了避免状态机紊乱。
  2. 改变 stvec 来引用 uservec:
    w_stvec(TRAMPOLINE + (uservec - trampoline));推测是重新写cpu的stvec寄存器为uservec地址,以保证下次中断时,cpu仍然跳转到uservec 去处理中断。
  3. 准备 uservec 所依赖的 trapframe 字段,如kernel_satp为内核页表地址等等。
  4. 写一些CPU寄存器:如设sstatus的SPP为0,表示为用户态的中断;设sstatus的SPIE为1,表示在用户态使能中断
  5. 将 sepc 设置为先前保存的用户程序计数器w_sepc(p->trapframe->epc);
  6. 调用 userret,并把TRAPFRAMEsatp作为参数传递过去,userret会切换用户态页表,重设用户态寄存器,最后切换回用户态
//
// 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);
}
userret
  1. 将 satp 切换到进程的用户页表,因为用户态和内核态的trampoline都是直接映射,因此在此时进行页表切换后,trampoline的程序仍能继续往下执行。此时a0寄存器指向用户页表的TRAPFRAME页,先将其保存到sscratch
.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 sp, 48(a0)
        ld gp, 56(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

Lab4

Backtrace (moderate)

实验内容:添加栈帧信息打印
考察点:xv6的栈结构;栈以类似链表的形式保存在1个页面中
关键提示:address lives at a fixed offset (-8) from the frame pointer of a stackframe, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.
在这里插入图片描述

关键代码:

void
backtrace(void)
{
  printf("backtrace:\n");
  uint64 fp = r_fp();
  uint64 down = PGROUNDDOWN(fp);
  uint64 up = PGROUNDUP(fp);
  while (fp >= down && fp < up)
  {
    uint64* res_addr = (uint64*)(fp - 8);
    uint64* next_fp_addr = (uint64*)(fp - 16);
    printf("%p\n", *res_addr);
    fp = *next_fp_addr;
  }
}

Alarm (hard)

实验内容:实现系统调用,在进程使用CPU时间超时时,进行回调函数调用,并能正常返回用户态
考察点:系统调用流程;usertrap的寄存器保存位置在trapframe页面;usertrap的pc计数器存储在epc寄存器;
关键提示:

  • When a trap on the RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?
  • Your solution will require you to save and restore registers—what registers do you need to save and restore to resume the interrupted code correctly? (Hint: it will be many).

关键代码:

// kernel/sysproc.c
int
sys_sigreturn(void)
{
  memmove(myproc()->trapframe, myproc()->trapframe_back, sizeof(struct trapframe));
  myproc()->calling = 0;
  return 0;
}
//kernel/trap.c
  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
  {
    p->ticks_count ++;
    if (p->alarmInterval != -1 && p->ticks_count >= p->alarmInterval && p->calling != 1)
    {
      // if a handler hasn't returned yet, the kernel shouldn't call it again
      p->calling = 1;
      //"re-arm" the alarm counter after each time it goes off
      p->ticks_count = 0;
      //save and restore registers
      memmove(p->trapframe_back, p->trapframe, sizeof(struct trapframe));
      //Q:When a trap on the RISC-V returns to user space,
      //what determines the instruction address at which user-space code resumes execution?
      //A: epc!
      p->trapframe->epc = p->alarmHandler;
    }
    yield();
  }
文章来源:https://blog.csdn.net/m0_51531927/article/details/134912408
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。