说明
陷入机制概述
每个RISC-V CPU都有一组控制寄存器,内核通过向这些寄存器写入内容来告诉CPU如何处理陷阱,内核可以读取这些寄存器来明确已经发生的陷阱。RISC-V文档包含了完整的内容。riscv.h(kernel/riscv.h:1)包含在xv6中使用到的内容的定义。以下是最重要的一些寄存器概述:
stvec :内核在这里写入其陷阱处理程序的地址;RISC-V跳转到这里处理陷阱。sepc :当发生陷阱时,RISC-V会在这里保存程序计数器pc(因为pc会被stvec覆盖) sret(从陷阱返回)指令会将sepc复制到pc。内核可以写入sepc来控制sret的去向。scause : RISC-V在这里放置一个描述陷阱原因的数字。sscratch :内核在这里放置了一个值,这个值在陷阱处理程序一开始就会派上用场。sstatus :其中的SIE位控制设备中断是否启用。如果内核清空SIE,RISC-V将推迟设备中断,直到内核重新设置SIE。SPP位指示陷阱是来自用户模式还是管理模式,并控制sret返回的模式。
上述寄存器都用于在管理模式下处理陷阱,在用户模式下不能读取或写入。在机器模式下处理陷阱有一组等效的控制寄存器,xv6仅在计时器中断的特殊情况下使用它们。
Traps from user space
在用户空间中,使用系统调用会触发trap机制 例如:write()函数
.global write
write:
li a7, SYS_write
ecall
ret
调用逻辑
ecall
这是一个汇编指令,他会做下面操作
1.清除SIE以禁用中断。 2.将pc复制到sepc。 3.将当前模式(用户或管理)保存在状态的SPP位中。 4.设置scause以反映产生陷阱的原因。 5.将模式设置为管理模式。 6.将stvec复制到pc。 7.在新的pc上开始执行。
注意 :stvec指向的地址是 uservec,将stvec复制到pc后,下面会到从uservec开始执行
uservec
# trampoline.S
uservec:
# 交换 a0 和 sscratch 寄存器的值
# so that a0 is TRAPFRAME
csrrw a0, sscratch, a0
# 将寄存器保存到当前进程的 trapframe 中
sd ra, 40(a0)
# ... 保存寄存器
sd t6, 280(a0)
# 同时也保存 a0
csrr t0, sscratch
sd t0, 112(a0)
# 从 user mode 的 traptable 中恢复一些内核的信息
ld sp, 8(a0)
ld tp, 32(a0)
ld t0, 16(a0)
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
- 在进行系统调用之前,
p->trapframe 的起始地址会被保存在 sscratch 寄存器中 usertrap 首先将所有的寄存器保存在 p->trapframe 中- 将原本保存在
p->trapframe 中的内核信息加载到寄存器当中
- 将页表切换到内核页表
- 跳转到
usertrap
usertrap
void usertrap(void) {
int which_dev = 0;
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");
w_stvec((uint64)kernelvec);
struct proc *p = myproc();
p->trapframe->epc = r_sepc();
if(r_scause() == 8){
if(p->killed)
exit(-1);
p->trapframe->epc += 4;
intr_on();
syscall();
} else if((which_dev = devintr()) != 0){
} 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);
if(which_dev == 2)
yield();
usertrapret();
}
- 判断是否在管理模式下
- 设置 stvec 为 kernelvec
- 保存PC, 否则可能会有其他的 usertrap 修改它
- 判断trap类型
- 系统调用
- p->trapframe->epc += 4(系统调用返回下一条命令)
- 打开设备中断
- syscall()进行系统调用
- 设备中断
- 异常
- 根据trap类型执行相应操作
- 调用
usertrapret 函数
usertrapret
该部分代码就是做一些返回用户模式的准备
void usertrapret(void)
{
struct proc *p = myproc();
intr_off();
w_stvec(TRAMPOLINE + (uservec - trampoline));
p->trapframe->kernel_satp = r_satp();
p->trapframe->kernel_sp = p->kstack + PGSIZE;
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp();
unsigned long x = r_sstatus();
x &= ~SSTATUS_SPP;
x |= SSTATUS_SPIE;
w_sstatus(x);
w_sepc(p->trapframe->epc);
uint64 satp = MAKE_SATP(p->pagetable);
uint64 fn = TRAMPOLINE + (userret - trampoline);
((void (*)(uint64, uint64))fn)(TRAPFRAME, satp);
}
- 关闭中断
- 将内核信息保存到p->trapfarm当中
- 设置sstatus寄存器 (用户模式)
- 将p->trapframe->epc(用户模式下要执行的下一条命令)放到sepc寄存器当中
- 将用户页表保存在stap(此处是定义的变量,并非是寄存器)
- 跳转到rampoline,执行userret
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 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
- 切换到用户页表
- 将之前保存的寄存器恢复
- 将TRAPFRAME保存回ssractch当中
- sret
sret
- 程序切换到用户模式
- 将sepc存到pc当中
- 开启中断
- 跳到pc开始执行
Traps from kernel mode
- 在内核模式下,trap 只有两类:exceptions 、device interrupt
- 当一个 trap 发生的时候,首先硬件开始工作,配置寄存器
- 此时 stvec 指向了 kernelvec 的起始地址
kernelvec
因为是发生在内核状态下的,所以相较于系统调用,就简单很多
# kernel/kernelvec.S
.globl kerneltrap
.globl kernelvec
.align 4
kernelvec:
# 在栈上开辟一块空间用于保存寄存器
addi sp, sp, -256
sd ra, 0(sp)
# ... 保存所有寄存器
sd t6, 240(sp)
# 调用 C 处理程序
call kerneltrap
# 恢复寄存器到之前的状态
ld ra, 0(sp)
# ... 恢复所有的寄存器(除了 tp)
ld t6, 240(sp)
# 恢复栈指针
addi sp, sp, 256
# 返回到之前的运行状态
sret
- 在栈上开辟一段空间,用来保存寄存器
- 保存寄存器
- 调用 C 处理程序
kerneltrap - 恢复寄存器到之前的状态
- 恢复栈指针
- 返回到之前的运行状态
kerneltrap
kerneltrap的处理和usertrap还是很了类似的
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)
{
printf("scause %p\n", scause);
printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
panic("kerneltrap");
}
if (which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
yield();
w_sepc(sepc);
w_sstatus(sstatus);
}
- 保存sepc,sstatus,scause寄存器(因为 yield()可能会引起其他陷阱,修改寄存器)
- 判断是否处于管理模式
- 判断trap类型,并执行相应操作
- 恢复sepc,sstatus,scause寄存器
感言
这部分还是卡了很久,因为出现了很多汇编,需要了解xv6的寄存器,以及RSICV指令集,还有栈桢相关的知识,后续继续一步一步完成,继续努力
参考资料
- http://xv6.dgs.zone/tranlate_books/book-riscv-rev1/c1/s0.html
- xv6-riscv源码
|