trap机制

在程序执行系统调用、程序出现类似page fault、除零错误时、一个设备触发了中断使得当前程序运行需要响应内核设备驱动,就会发生用户空间和内核空间的切换,这样的切换称之为trap。

trap代码执行流程

在一开始内核空间返回用户空间时,内核会设置好STVEC寄存器指向内核希望的trap代码运行的位置。内核会设置好STVEC寄存器为0x3ffffff000,这是trampoline page的起始位置。执行完ecall指令之后,pc寄存器就会被设置成STVEC寄存器中的值,然后开始执行trampoline page中代码。

即使trampoline page在用户空间进行页表映射,但是用户代码不能使用它,因为这个page的PTE_U被设置为0

uservec函数

trampoline page 的第一个函数就是uservec,这也是trap机制执行的第一个函数

sscratch 寄存器中保存了进程trapframe的地址,第一条指令是交换a0寄存器和sscratch寄存器中值,所以当前a0寄存器中保存着trapframe地址。

asm 复制代码
.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

为什么sscratch寄存器中保存的就是trapframe page地址? 这里可以看userret函数实现

接下来是将用户寄存器保存到trapframe中,除了a0寄存器(因为a0寄存器和sscratch寄存器交换了值,此时保存的不是进入trap前的值。)

asm 复制代码
# 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)

a0因为之前和sscratch交换了值,所以此时sscratch保存的是trap之前a0寄存器的值。csrr指令是用于读取控制和状态寄存器的指令,所以下面就是将sscratch寄存器值读取保存到t0寄存器中,再将t0寄存器中值保存到trapframe中,完成a0寄存器值的保存。

bash 复制代码
# save the user a0 in p->trapframe->a0
csrr t0, sscratch
sd t0, 112(a0)

接着是从trapframe中读取内核栈地址作为sp寄存器值,即将sp指向进程内核栈。读取CPU_ID保存到tp寄存器中,读取usertrap函数地址保存到t0寄存器中。

asm 复制代码
# 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)

下面代码是将内核页表地址读取到t1寄存器中,然后再通过csrw指令写入到satp寄存器中,再调用sfence.vma指令清空页表缓存。页表的切换是在uservec函数中完成的

css 复制代码
# restore kernel page table from p->trapframe->kernel_satp
ld t1, 0(a0)
csrw satp, t1
sfence.vma zero, zero

最后调用jr t0 跳转指令,跳转执行usertrap函数(在trap.c)中

asm 复制代码
# 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

一开始检查该trap是否来自用户空间,不是则直接panic。

c 复制代码
void usertrap(void) {
  int which_dev = 0;
  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

修改stvec寄存器,将内核状态下处理trap的函数kernelvec的地址保存到stvec寄存器中,这样如果在处理用户trap时(此时处于内核态)发生中断,则可以通过stvec寄存器获取到内核态下发生中断的处理函数代码。

c 复制代码
  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);

用户trap之前会将当前pc寄存器内容保存到sepc寄存器中,所以在usertrap函数中还会读sepc寄存器,将其保存在trapframe中。

c 复制代码
  struct proc *p = myproc();
  // save user program counter.
  p->trapframe->epc = r_sepc();

scause寄存器保存着中断原因,8表示系统调用:

  • 先检查进程是否因为一些原因被杀死,如果已经被杀死,则直接退出
  • 将保存在trapframe中的pc值加4,指向下一条指令,因为执行完系统调用之后,我们希望继续执行的是系统调用包装函数的下一条指令
  • 打开中断:使得中断可以更快服务,因为有些系统调用可能会需要很多时间进行处理,中断总是会被RISC-V的trap硬件关闭,所以我们需要在这里显示打开
  • 调用syscall函数,开始执行系统调用相关处理
c 复制代码
  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();
  }

devintr函数可以获取发出中断的硬件号,2表示是CPU时钟发出的中断,若是时钟中断,则调用yield函数进行进程调用。进程管理 最后会执行usertrapret函数,为返回用户空间做准备

c 复制代码
  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();
}

usertrapret

第一步是调用intr_off函数关中断,这里关闭中断是因为我们要更新stvec寄存器来指向用户空间的trap处理代码,而此时在内核中,如果修改完stvec寄存器之后发生了中断,则可能导致在内核中调用了处理用户态trap处理函数,会出现问题。

c 复制代码
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();

第二步是修改stvec寄存器,将其值修改为uservec函数地址,这里呼应前面为什么在用户态trap时可以进入到uservec函数中

c 复制代码
  // send syscalls, interrupts, and exceptions to trampoline.S
  w_stvec(TRAMPOLINE + (uservec - trampoline));

接下来是保存内核页表、内核栈、usertrap函数地址、cpuid到trapframe中,这里呼应uservec函数中从trapframe中读取这些值,并恢复这些状态

c 复制代码
  // 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()

下面代码是设置sstatus寄存器,这个寄存器的SPP bit用于控制sret指令行为,该bit为0表示下次执行sret时,我们想要返回user mode而不是supervisor mode。这个寄存器的SPIE bit控制了执行完sret之后是否打开中断,1表示打开。

c 复制代码
  // 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);

接下来将保存在trapframe中的epc值载入到sepc寄存器中 最后satp变量中保存用户页表,fn指向trampoline中的userret函数,将trapframe和satp变量作为传输传给userret函数,这两个参数分别保存在a0和a1寄存器中。

c 复制代码
  // 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

一开始设置页表寄存器satp为进程用户页表,并清空页表缓存

asm 复制代码
.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

接下来将trapframe中a0寄存器的值保存到sscratch寄存器中

asm 复制代码
# 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

接着将trapframe中除了a0之外的用户寄存器值都载入到对应寄存器中。

asm 复制代码
# 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)

交换a0和sscratch寄存器值,此时a0中存储的是第一个参数,也就是trapframe page地址,sscratch保存到的是进入trap前a0寄存器值,所以交换完之后,a0寄存器恢复到了trap之前状态,sscratch中也保存了trapframe地址,这里也就呼应了uservec函数中sscratch寄存器值为trapframe地址了。 执行sret返回用户空间。

asm 复制代码
# 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

系统调用

用户进程可以通过调用系统调用的包装函数来调用系统调用,系统调用包装函数(以fork为例)的汇编代码如下:

asm 复制代码
fork: 
	li a7, SYS_fork 
	ecall 
	ret

首先将系统调用号存入 a7 寄存器中,然后调用ecall指令触发软中断,通过trap机制进入内核进行真正的系统调用处理。

系统调用的中断处理程序是 kernel/syscall.c 中的 syscall 函数。

c 复制代码
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;
  }
}

先从进程的trapframe(保存寄存器信息的结构体)中获取a7寄存器保存的值,也就是拿到系统调用号,然后再通过这个系统调用号在系统调用表syscalls中获取系统调用函数。

supervisor mode可以做的事情:

  • 可以访问 PTE_U = 0 的页
  • 可以读写控制寄存器,例如SATP寄存器、STVEC寄存器等 supervisor mode并不能读写任意物理地址
相关推荐
Tech Synapse36 分钟前
Java根据前端返回的字段名进行查询数据的方法
java·开发语言·后端
.生产的驴37 分钟前
SpringCloud OpenFeign用户转发在请求头中添加用户信息 微服务内部调用
spring boot·后端·spring·spring cloud·微服务·架构
微信-since811921 小时前
[ruby on rails] 安装docker
后端·docker·ruby on rails
代码吐槽菌3 小时前
基于SSM的毕业论文管理系统【附源码】
java·开发语言·数据库·后端·ssm
豌豆花下猫3 小时前
Python 潮流周刊#78:async/await 是糟糕的设计(摘要)
后端·python·ai
YMWM_3 小时前
第一章 Go语言简介
开发语言·后端·golang
码蜂窝编程官方3 小时前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
hummhumm4 小时前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
J老熊4 小时前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程
AuroraI'ncoding4 小时前
时间请求参数、响应
java·后端·spring