trap机制:每当程序执行系统调用、程序出现了类似page fault、运算时除以0的错误、一个设备触发了中断使得当前程序运行需要响应内核设备驱动。从来源上看,陷阱可以分为用户态陷阱(系统调用、严重错误、中断)和内核态陷阱(严重错误、中断)。
关于RISC-V架构中的异常与中断涉及到的概念和相关寄存器参考:
6.S081——补充材料——RISC-V架构中的异常与中断详解_risc-v 中断设计-CSDN博客
trap代码的执行流程:以在shell中运行write系统调用为例:
首先在usys.s中,它将SYS_write加载到a7寄存器,SYS_write是常量16。这里告诉内核,我想要运行第16个系统调用,而这个系统调用正好是write。之后这个函数中执行了ecall指令,从这里开始代码执行跳转到了内核。内核完成它的工作之后,代码执行会返回到用户空间,继续执行ecall之后的指令,也就是ret,最终返回到Shell中。所以ret从write库函数返回到了Shell中。
此时用户寄存器的状态:
a0,a1,a2是Shell传递给write系统调用的参数,a7寄存器为系统调用编号
ecall指令:将代码从user mode改到supervisor mode,将程序计数器的值保存在了SEPC寄存器,跳转到STVEC寄存器指向的指令(在进入到用户空间之前,内核会将trampoline page(注意这个页是在进程页表中的,但没有设置PTE_U,因此用户无法访问,只有进入内核后才能访问)的地址存在STVEC寄存器中)。
总的来说就是:(保护现场)
汇编程序:
现在程序位于trampoline page的起始,也是uservec函数的起始。需要做的第一件事情就是保存寄存器的内容。
对于保存用户寄存器,XV6在RISC-V上的实现包括了两个部分。第一个部分是,XV6在每个user page table映射了trapframe page,这样每个进程都有自己的trapframe page。这个page包含了很多有趣的数据,但是现在最重要的数据是用来保存用户寄存器的32个空槽位。这个位置的虚拟地址总是0x3ffffffe000。
在trapframe 中保存有内核的页表、进程的内核栈、接下来要跳转到的函数指针地址usertrap()、用户程序计数器,tp寄存器,用于保存此时的CPU核。
之所以叫trampoline,是因为你某种程度在它上面“弹跳”了一下,然后从用户空间走到了内核空间。
总的来说,uservec的作用就是:
void usertrap(void)
{
int which_dev = 0;
if((r_sstatus() & SSTATUS_SPP) != 0) //判断是否来自User mode
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){ // 1.系统调用system call
if(p->killed)
exit(-1);
// 执行完系统调用后,返回到ecall的下一条指令
p->trapframe->epc += 4;
intr_on(); // 开中断
syscall();
} else if((which_dev = devintr()) != 0){ // 2.设备异常处理
} else { // 3.程序异常
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(); // 返回
}
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](); // 调用全局系统调用函数指针数组,并将返回值放入a0寄存器
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
usertrap函数处理来自用户空间的陷阱,判断陷阱原因,并进行相应处理(如系统调用/设备中断/程序异常)。
1、usertrap中先将STVEC指向了kernelvec变量,这是内核空间trap处理代码的位置,而不是用户空间trap处理代码的位置。(内核空间trap不在本节讨论范围内)
2、保存用户程序计数器到p->trapframe->epc,防止当当程序还在内核中执行时,我们可能切换到另一个进程,并进入到那个程序的用户空间,然后那个进程可能再调用一个系统调用进而导致SEPC寄存器的内容被覆盖。保存当前进程的SEPC寄存器到一个与该进程关联的内存中,这样这个数据才不会被覆盖。
3、找出我们现在会在usertrap函数的原因,根据SCAUSE寄存器判断触发trap原因,如果是8则说明是系统调用导致(查询RISC-V的内核异常码)。
4、 p->trapframe->epc += 4; 用于存储ecall的下一条指令
5、intr_on(); 显式打开中断,因为一些系统调用比较耗时,所以打开中断(此前被rise-v的trap硬件关闭了)
6.调用syscall函数(根据传入的系统调用号,去全局的系统调用的函数指针数组中进行调用)
7、syscall返回后检查进程是否被杀死,没有被杀死则恢复进程,调用usertrapret函数
总结就是:usertrap函数的作用:
设置完成在返回到用户空间之前内核要做的工作。
1、关闭中断
2、设置STVEC寄存器指向trampoline代码,在那里最终会执行sret指令返回到用户空间。
3.填入了trapframe的内容(kernel page table的指针、当前用户进程的kernel stack、usertrap函数的指针、当前的CPU核编号)来保证进程的下一次进入内核时传入的参数。
4.设置SSTATUS寄存器,控制SPPbit为0,保证下一次执行sret时,返回user mode,同时设置SPIE位为1,来使得执行完sret后打开中断
5.设置sepc寄存器为之前保存的用户程序计数器。
6、调用Userret函数,传入Trapframe和satp(用户页表地址)的值
userret也是在trampoline中。
1、切换页表,从内核ptb切换成小得多的user ptb
2、加载之前保护的用户寄存器的值(将trapframe中的值加载到各个寄存器中)
3、sret指令
之所以叫trampoline,是因为你某种程度在它上面“弹跳”了一下,然后又从内核空间回到了用户空间。
sret指令执行完后:
然后就调回用户空间了
总结来说就是:
在usertrap中有一段代码w_stvec((uint64)kernelvec);
将stvec寄存器设置为kernelvec,当内核发生陷阱时PC就会从kernelvec中开始执行。
kernelvec位于kernel/kernelvec.S中,此时已经位于内核,因此不需要切换页表,设置内核栈之类的操作,只需要保存寄存器,调用kerneltrap,恢复寄存器并返回。
#
# interrupts and exceptions while in supervisor
# mode come here.
#
# push all registers, call kerneltrap(), restore, return.
#
.globl kerneltrap
.globl kernelvec
.align 4
kernelvec:
// make room to save registers.
addi sp, sp, -256
// save the registers.,保存寄存器,保护现场
sd ra, 0(sp)
sd sp, 8(sp)
sd gp, 16(sp)
sd tp, 24(sp)
sd t0, 32(sp)
sd t1, 40(sp)
sd t2, 48(sp)
sd s0, 56(sp)
sd s1, 64(sp)
sd a0, 72(sp)
sd a1, 80(sp)
sd a2, 88(sp)
sd a3, 96(sp)
sd a4, 104(sp)
sd a5, 112(sp)
sd a6, 120(sp)
sd a7, 128(sp)
sd s2, 136(sp)
sd s3, 144(sp)
sd s4, 152(sp)
sd s5, 160(sp)
sd s6, 168(sp)
sd s7, 176(sp)
sd s8, 184(sp)
sd s9, 192(sp)
sd s10, 200(sp)
sd s11, 208(sp)
sd t3, 216(sp)
sd t4, 224(sp)
sd t5, 232(sp)
sd t6, 240(sp)
// call the C trap handler in trap.c 调用kerneltrap C函数
call kerneltrap
// restore registers.,恢复现场
ld ra, 0(sp)
ld sp, 8(sp)
ld gp, 16(sp)
// not this, in case we moved CPUs: ld tp, 24(sp)
ld t0, 32(sp)
ld t1, 40(sp)
ld t2, 48(sp)
ld s0, 56(sp)
ld s1, 64(sp)
ld a0, 72(sp)
ld a1, 80(sp)
ld a2, 88(sp)
ld a3, 96(sp)
ld a4, 104(sp)
ld a5, 112(sp)
ld a6, 120(sp)
ld a7, 128(sp)
ld s2, 136(sp)
ld s3, 144(sp)
ld s4, 152(sp)
ld s5, 160(sp)
ld s6, 168(sp)
ld s7, 176(sp)
ld s8, 184(sp)
ld s9, 192(sp)
ld s10, 200(sp)
ld s11, 208(sp)
ld t3, 216(sp)
ld t4, 224(sp)
ld t5, 232(sp)
ld t6, 240(sp)
addi sp, sp, 256
// return to whatever we were doing in the kernel.
sret
#
# machine-mode timer interrupt.
#
.globl timervec
.align 4
timervec:
# start.c has set up the memory that mscratch points to:
# scratch[0,8,16] : register save area.
# scratch[32] : address of CLINT's MTIMECMP register.
# scratch[40] : desired interval between interrupts.
csrrw a0, mscratch, a0
sd a1, 0(a0)
sd a2, 8(a0)
sd a3, 16(a0)
# schedule the next timer interrupt
# by adding interval to mtimecmp.
ld a1, 32(a0) # CLINT_MTIMECMP(hart)
ld a2, 40(a0) # interval
ld a3, 0(a1)
add a3, a3, a2
sd a3, 0(a1)
# raise a supervisor software interrupt.
li a1, 2
csrw sip, a1
ld a3, 16(a0)
ld a2, 8(a0)
ld a1, 0(a0)
csrrw a0, mscratch, a0
mret
// interrupts and exceptions from kernel code go here via kernelvec,
// on whatever the current kernel stack is.
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){ //调用devintr函数对中断类型进行判断,并进行对应处理
printf("scause %p\n", scause);
printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
panic("kerneltrap");
}
// give up the CPU if this is a timer interrupt. // 如果是定时器中断,那么发生一次调度
if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
yield();
// the yield() may have caused some traps to occur,
// so restore trap registers for use by kernelvec.S's sepc instruction.
w_sepc(sepc);
w_sstatus(sstatus);
}
kerneltrap只需要处理设备中断(串口/磁盘)和定时器中断即可(不可能是系统调用)。如果是定时器中断,则调用yield让出CPU。当kerneltrap完成后则返回到kernelvec函数,恢复寄存器,回到被中断的位置。