Trap (陷入/中断) 源码解析

用户空间和内核空间之间的切换通常称为trap

trap的三种形式
  1. 系统调用引发
  2. 异常发生
  3. 设备中断 (时间中断、IO中断、网络中断等)
supervise mode的权限

用户态和内核态之间的到底有什么区别?其实区别很小:

其中的一件事情是,你现在可以读写控制寄存器了。比如说,当你在supervisor mode时,你可以:读写SATP寄存器,也就是page table的指针;STVEC,也就是处理trap的内核指令地址;SEPC,保存当发生trap时的程序计数器;SSCRATCH等等。在supervisor mode你可以读写这些寄存器,而用户代码不能做这样的操作。

另一件事情supervisor mode可以做的是,它可以使用PTE_U标志位为0的PTE(页表项 详见页表)。当PTE_U标志位为1的时候,表明用户代码可以使用这个页表;如果这个标志位为0,则只有supervisor mode可以使用这个页表。

这也是supervise mode唯二可以做的事情了。

ecall指令

系统使用ecall指令来执行系统调用

第一,ecall将代码从user mode改到supervisor mode

第二,ecall将程序计数器的值保存在了SEPC寄存器。

第三,ecall会跳转到STVEC寄存器指向的指令 即trampoline.S

trap的全过程

exec系统调用为例。

首先,用户程序中调用exec函数,编译器解析exec函数时,并不会调用其源码,而是将exec系统调用的编号写入A7寄存器中,执行ecall指令,ecall指令将跳转到trampoline.S。

系统从用户态进入了内核态,但目前页表依旧是user page table。trampoline.S主要是使用trapFrame(附录部分)保存了系统的状态以及用户寄存器,用于恢复用户进程,通过这个操作也实现用户进程对于进程切换的无感知。在将需要存储的状态存入user page table后,系统将切换内核页表,执行相应的内核代码,即usertrap函数。

usertrap根据trap的不同情况(scause寄存器中存储着中断原因),调用相应的处理函数。

代码解析

下面的代码是操作系统的源代码,里面包含了很多的细节,你需要里了解的是整个流程、体系,不要过多的关注细节

trap的保存流程
  1. 首先,我们需要保存32个用户寄存器。因为很显然我们需要恢复用户应用程序的执行,尤其是当用户程序随机的被设备中断所打断时。(存储用户寄存器)
  2. 程序计数器也需要在某个地方保存,它几乎跟一个用户寄存器的地位是一样的,我们需要能够在用户程序运行中断的位置继续执行用户程序。(存储一些系统寄存器 如PC)
  3. 我们需要将mode改成supervisor mode,因为我们想要使用内核中的各种各样的特权指令。(修改运行模式)
  4. SATP(Supervisor Address Translation and Protection,存储了根页表的地址)寄存器现在正指向user page table,而user page table只包含了用户程序所需要的内存映射和一两个其他的映射,它并没有包含整个内核数据的内存映射。所以在运行内核代码之前,我们需要将SATP指向kernel page table。(修改SATP,获得内核的页表)
  5. 我们需要将堆栈寄存器指向位于内核的一个地址,因为我们需要一个堆栈来调用内核的C函数。(转换到内核堆栈)
  6. 一旦我们设置好了,并且所有的硬件状态都适合在内核中使用, 我们需要跳入内核的C代码usertrap函数。
trampoline.S
	#
        # code to switch between user and kernel space.
        #
        # this code is mapped at the same virtual address
        # (TRAMPOLINE) in user and kernel space so that
        # it continues to work when it switches page tables.
	#
	# kernel.ld causes this to be aligned
        # to a page boundary.
        #
.section trampsec
.globl trampoline
trampoline:
.align 4
.globl uservec
//trap状态保存
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
//trap状态恢复
.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

用户trap处理程序
void
usertrap(void)
{
  int which_dev = 0;
	
  //判断系统mode状态
  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.
  // 将中断处理程序设置为kernelvec
  // 即将kernelvec函数地址写入到stvec寄存器
  // 和user model下的中断对应,kernelvec用来处理内核态下的中断
  // 两个流程很相似,kernelvec更简单一些,就不做解释了
  w_stvec((uint64)kernelvec);

  struct proc *p = myproc();
  
  //保存用户的PC值
  // save user program counter.
  p->trapframe->epc = r_sepc();
  
  //根据scause寄存器的值判断异常原因
  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();
	
  //trap处理完毕,恢复用户进程状态
  usertrapret();
}

附:

系统调用函数 (根据A7寄存器存储的内容,调用不同的函数指针)

extern uint64 sys_chdir(void);
extern uint64 sys_close(void);
extern uint64 sys_dup(void);
extern uint64 sys_exec(void);
extern uint64 sys_exit(void);
extern uint64 sys_fork(void);
extern uint64 sys_fstat(void);
extern uint64 sys_getpid(void);
extern uint64 sys_kill(void);
extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_mknod(void);
extern uint64 sys_open(void);
extern uint64 sys_pipe(void);
extern uint64 sys_read(void);
extern uint64 sys_sbrk(void);
extern uint64 sys_sleep(void);
extern uint64 sys_unlink(void);
extern uint64 sys_wait(void);
extern uint64 sys_write(void);
extern uint64 sys_uptime(void);

static uint64 (*syscalls[])(void) = {
[SYS_fork]    sys_fork,
[SYS_exit]    sys_exit,
[SYS_wait]    sys_wait,
[SYS_pipe]    sys_pipe,
[SYS_read]    sys_read,
[SYS_kill]    sys_kill,
[SYS_exec]    sys_exec,
[SYS_fstat]   sys_fstat,
[SYS_chdir]   sys_chdir,
[SYS_dup]     sys_dup,
[SYS_getpid]  sys_getpid,
[SYS_sbrk]    sys_sbrk,
[SYS_sleep]   sys_sleep,
[SYS_uptime]  sys_uptime,
[SYS_open]    sys_open,
[SYS_write]   sys_write,
[SYS_mknod]   sys_mknod,
[SYS_unlink]  sys_unlink,
[SYS_link]    sys_link,
[SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,
};

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]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}