操作系统MIT6.S081:陷入(trap)流程

1.big picture

 假设有一个程序需要执行write系统调用,xv6会先执行ecall进入内核空间,然后跳转到uservec保护现场,接着进入usertrap()判断是什么出发了trap,这里是write系统调用,所以会进入syscall()中处理系统调用,然后进入usertrapret()函数处理一下,最后进入userret()恢复现场,返回用户空间中。

2.用户程序调用系统调用(以write为例)

在用户程序调用write系统调用时,实际上是关联到用户程序的一个库函数--usy.s

write:
    li a7, SYS_write
    ecall
    ret

 这段代码首先将SYS_write加载到a7寄存器中,这个SYS_write实际上是一个常量,在xv6中是16.

然后执行ecall指令。

3.ecall

ecall会做下面这三件事:

        1.ecall将代码从user mode改到supervisor mode。
        2.ecall将程序计数器的值保存在了SEPC寄存器。(发生陷阱时,pc寄存器会被覆盖,所以需要SEPC寄存器保存中断发生的地方,以便陷阱处理完之后恢复现场)
        3.ecall会跳转到STVEC寄存器指向的指令。(STVEC指向的是usevec函数)

4.uservec函数

uservec在trampoline.S中,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)
        sd tp, 64(a0)
        sd t0, 72(a0)
        sd t1, 80(a0)
        sd t2, 88(a0)
        sd s0, 96(a0)
        sd s1, 104(a0)
        sd a1, 120(a0)
        sd a2, 128(a0)
        sd a3, 136(a0)
        sd a4, 144(a0)
        sd a5, 152(a0)
        sd a6, 160(a0)
        sd a7, 168(a0)
        sd s2, 176(a0)
        sd s3, 184(a0)
        sd s4, 192(a0)
        sd s5, 200(a0)
        sd s6, 208(a0)
        sd s7, 216(a0)
        sd s8, 224(a0)
        sd s9, 232(a0)
        sd s10, 240(a0)
        sd s11, 248(a0)
        sd t3, 256(a0)
        sd t4, 264(a0)
        sd t5, 272(a0)
        sd t6, 280(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

为了保护现场,我们需要把32个寄存器的数据保存起来,xv6的做法是把这些数据保存在用户虚拟地址的高地址的一个页中(trapframe page 0x3fffffe000)。SSCRATCH寄存器存储的是这个页面的虚拟地址。

csrrw a0, sscratch, a0

它通过csrrw交换指令,a0现在保存的是trapframe page的虚拟地址,然后我们就可以保存32个寄存器中的值了。

指令就接下来的是把a1寄存器保存的值保存到地址:a0+OFFSET上。

 sd a1, OFFSET(a0)

然后保存a0的值。

csrr t0, sscratch
sd t0, 112(a0)

接下来从user page table转到kernel page table。

        # 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

然后跳转到usertrap()函数中,由usertrap处理陷阱。

Q:我们经过csrw satp, t1,就已经切换到了kernel page table中,那么执行jr t0为什么没有报错?换句话说,t0里面的地址是用户虚拟页表的地址,为什么我们换成kernel page table这个地址还能用的呢?

A:因为我们还在trampoline代码中,而trampoline代码在用户空间和内核空间都映射到了同一个地址。

5.usertrap()

usertrap()函数在trap.c中。

//
// 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)
  {
    
    if(p->preticks == p->ticks){
      p->trapframecopy = p->trapframe + 512;  
      memmove(p->trapframecopy,p->trapframe,sizeof(struct trapframe));    // 复制trapframe
      p->trapframe->epc = (uint64)p->handler;
    }
    p->preticks += 1;
    yield();  
  }
  
    usertrapret();
}

usertrap()首先把STVEC寄存器的值改为指向kernelvec,在内核空间发生的trap,因为程序已经在kernel page table中了,所以流程跟用户空间发生trap不一样,需要另一套处理流程。

xv6需要知道当前运行的是什么进程,通过myproc()函数可以实现。

struct proc *p = myproc();

我们在ecall处把程序计数器的值保存在了SEPC寄存器中,但是SEPC可能会被覆盖,所以保存到trapframe page中。

p->trapframe->epc = r_sepc();

然后是找出的发生trap原因,SCAUSE寄存器中保存的正是trap发生的原因。

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

如果SCAUSE寄存器保存的是8,那么进入系统调用的处理程序。接下来第一件事情是检查是不是有其他的进程杀掉了当前进程,然后是trapframe page里面的epc寄存器加4,因为我们恢复现场时,我们希望回到现场的下一条指令,所以加4。XV6会在处理系统调用的时候使能中断,这样中断可以更快的服务,有些系统调用需要许多时间处理。中断总是会被RISC-V的trap硬件关闭,所以在这个时间点,我们需要显式的打开中断。然后就是调用syscall()函数处理系统调用。

最后是进入usertrapret()函数。

6.usertrapret()

usertrapret()函数是回到用户空间之前内核要做的工作。

    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);

首先关闭中断,把STVEC寄存器的值设置为userret的地址。userret函数会执行sret指令返回到用户空间。位于trampoline代码最后的sret指令会重新打开中断。这样,即使我们刚刚关闭了中断,当我们在执行用户代码时中断是打开的。

    intr_off();

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

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

接下来我们要设置SSTATUS寄存器,这是一个控制寄存器。这个寄存器的SPP bit位控制了sret指令的行为,该bit为0表示下次执行sret的时候,我们想要返回user mode而不是supervisor mode。这个寄存器的SPIE bit位控制了,在执行完sret之后,是否打开中断。因为我们在返回到用户空间之后,我们的确希望打开中断,所以这里将SPIE bit位设置为1。修改完这些bit位之后,我们会把新的值写回到SSTATUS寄存器。

  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);

我们在trampoline代码的最后执行了sret指令。这条指令会将程序计数器设置成SEPC寄存器的值,所以现在我们将SEPC寄存器的值设置成之前保存的用户程序计数器的值。在不久之前,我们在usertrap函数中将用户程序计数器保存在trapframe中的epc字段。

w_sepc(p->trapframe->epc);

接下来,我们根据user page table地址生成相应的SATP值,这样我们在返回到用户空间的时候才能完成page table的切换。实际上,我们会在汇编代码trampoline中完成page table的切换,并且也只能在trampoline中完成切换,因为只有trampoline中代码是同时在用户和内核空间中映射。但是我们现在还没有在trampoline代码中,我们现在还在一个普通的C函数中,所以这里我们将page table指针准备好,并将这个指针作为第二个参数传递给汇编代码,这个参数会出现在a1寄存器。

倒数第二行的作用是计算出我们将要跳转到汇编代码的地址。我们期望跳转的地址是tampoline中的userret函数,这个函数包含了所有能将我们带回到用户空间的指令。所以这里我们计算出了userret函数的地址。

倒数第一行,将fn指针作为一个函数指针,执行相应的函数(也就是userret函数)并传入两个参数,两个参数存储在a0,a1寄存器中。

((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);

7.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 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

我们首先把kernel page table切换为user page table,usertrapret函数通过((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);传递给userret,也就是a1寄存器中。然后清空页表缓存。

csrw satp, a1
sfence.vma zero, zero

usertrapret函数通过((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);把trapframe page的地址传递给userret,也就是a0寄存器中,然后trapframe page里面a0寄存器是系统调用的返回值,把他保存到sscratch寄存器中。用于恢复现场。

 ld t0, 112(a0)
 csrw sscratch, t0

然后就是把trapframe page中保存的32个寄存器的值加载到各个对应的寄存器中。

把trapframe page的地址保存在SSCRATCH寄存器中,用于下次trap的使用。同时SSCRATCH寄存器保存的是系统调用的返回值,需要保存到a0寄存器内。

csrrw a0, sscratch, a0

然后执行sret

返回到ret,执行ret,从write系统调用中返回到程序中。

li a7 SYS_write
ecall
ret

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值