RISC-V关键寄存器
RISC-V中内核态寄存器:
- SIE(Supervisor Interrupt Enable):是否允许类型中断
- SIP(Supervisor Interrupt Pending):当发生中断时,处理器查看这个寄存器知道当前发生的中断类型
- SSTATUS(Supervisor Status):1bit,开/关中断
每个CPU都有独立的SIE和SSTATUS寄存器,除了通过SIE单独控制特定的中断,还可以通过SSTATUS控制所有的中断
- SATP:页表地址
- STVEC:保存中断向量的地址,是uservec/kernelvec取决于发生中断时程序运行在用户态还是内核态
- SSCRATCH:保存进程
trapframe
页的虚拟地址 - SEPC:发生中断时程序地址置,中断执行完后返回该位置继续执行
- SCAUSE:发生中断的原因
trap
xv6 会在以下情形进入usertrap:系统调用、用户指令错误、应用程序运行时发生中断(外部设备中断/软中断)
xv6 会在以下情形进入kerneltrap:内核指令错误、内核运行时发生中断(外部设备中断/软中断)
进入trap
以fork
系统调用为例,用户态程序执行fork系统调用,实际执行如下指令:
- 向a7寄存器写入系统调用编号
- 执行ecall指令,该指令在硬件层面完成:切换至内核态 + 交换关键寄存器的值(PC→SEPC,STVEC→PC)
.global fork
fork:
li a7, SYS_fork
ecall
ret
STVEC寄存器事先存储了uservec
中断向量的地址,交换后PC寄存器指向uservec
,此时系统处于内核态但未切换至内核页表,完成以下操作:
- 保存用户态上下文至
trapframe
,包括关键寄存器(SP/SATP) + 32个通用寄存器 - 恢复保存在
trapframe
的内核态上下文:SATP-内核根页表地址/SP-内核栈地址 - 跳转至
usertrap
trampoline.asm/uservec
# trap.c sets stvec to point here, so traps from user space start here,
# in supervisor mode, but with a user page table.
.globl uservec
uservec:
# sscratch stores the address of process's trapframe virtual address
# 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)
# 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
# jump to usertrap(), which does not return
jr t0
从trap返回
usertrap
最后调用usertrapret
返回用户态,usertrapret
硬件返回用户态userret
trampoline.asm/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)
# ...
# 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
补充:系统调用进入内核态后的流程(syscall)
获取系统调用号:trapframe->a7
从系统调用函数数组获取对应系统调用函数的指针
获取系统调用参数:与普通函数调用存入栈帧不同,syscall 从 trapframe 获取系统调用参数
返回系统调用结果:在trapframe->a0中记录返回值
syscall.c - syscall:
extern uint64 sys_fork(void);
extern uint64 sys_exit(void);
extern uint64 sys_wait(void);
// .....
// 系统调用指针
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
// .....
};
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;
}
}
补充:指令错误
指令错误可参考寄存器:
SCAUSE:错误类型
SEPC:造成错误的指令的虚拟地址
STVAL:对于page fault,表明造成错误的虚拟地址
usertrap
kerneltrap
内核线程调度前,保存如下数据:
kernel_satp
:存储内核页表的物理地址,切换至内核态时存储在SATPkernel_sp
:存储该进程的内核栈的地址,切换至内核态时存储在SPkernel_trap
:存储中断处理程序kerneltrap
的物理地址,kernelvec
后直接跳转至该函数
基于Page fault 附加功能
Copy on write fork
使用fork
系统调用时,先创建4个新的page,再将父进程page的内容拷贝至4个新的分配给子进程的page中,之后exec
会释放这些page,并分配新的page来包含echo相关的内容,原先创建的4个新的page
创建子进程时,与其创建分配拷贝内容到新的物理内存,不如直接共享父进程的物理内存page并设置子进程的PTE指向父进程对应的物理内存page,但父进程和子进程的PTE的标志位都设置成只读的。
父进程和子进程继续运行,当父进程或子进程执行store指令更新一些全局变量时会触发page fault,因为现在在向一个只读PTE写数据。
在得到page fault之后,先分配一个新的page,然后将父进程相应的page拷贝到新page,并将新page映射到子进程的页表中。这时,新分配的page只对子进程可见,相应的PTE设置成可读写,并且重新执行指令。对于触发page fault对应的物理page,因为现在只对父进程可见,相应的PTE对于父进程也变成可读写的了。
释放:当我们释放page时,我们将物理内存page的引用数减1。只有引用数为0时释放物理内存page。
Least Recently Used
当系统发生OOM,将部分页中的内容写回到文件系统再收回这个page,回收哪些page:access位为0,回收最近未被访问过的page。此外,dirty位为0,表明当前page被修改过