3.系统调用跳转流程
系统调用跳转流程
以write()系统调用为例
1. 用户态
-
调用
write()
-
跳转到
usys.s\write
#include "kernel/syscall.h" write: li a7, SYS_write ecall ret
SYS_write
的定义在kernel/syscall.h
中#define SYS_write 16
将
SYS_write
的索引16
放到a7寄存器
中执行
ecall
,跳转到STVEC
指向的地址!!!
STVEC
涉及到中断相关知识,后续补齐!!!???
STVEC
中的地址在哪里设置的此处
STVEC
指向trampoline.S\uservec()
Q1.ECALL功能?
- 从
user mode
切换到supervisor mode
- 将
pc
的值保存到SEPC
寄存器 - 跳转到
STVEC
指向的地址
ecall的功能十分简单,主要是为了提升软件设计的灵活性,但此时ecall
做的事远远不够,对于xv6,我们还需要完成
- 保存31个用户寄存器的内容,之后用来恢复代码运行状态
ecall
不切换page table
,因此此时还在使用user page table
,需要切换到kernel page table
。- 需要创建或者找到一个
kernel stack
,并将Stack Pointer
寄存器的内容指向这个kernel stack
。代码需要使用栈执行程序。 - 需要跳转到内核代码的某些合理的位置。
使用ECALL指令时,将系统调用类型存在a7寄存器,参数存在a0-a5寄存器
2 trampoline.s\uservec()
保存32个通用寄存器
uservec:
# 需要保存所有寄存器的值,之后需要恢复执行
# 此时所有通用寄存器都不可修改
# 将`a0`存储到`sscratch`,这样`a0`就可以修改了
csrw sscratch, a0
# 将`TRAPFRAME`放到`a0`中,给后续存储提供基地址
li a0, TRAPFRAME
# 保存其他通用寄存器
sd ra, 40(a0)
sd sp, 48(a0)
sd gp, 56(a0)
sd tp, 64(a0)
....
# 将之前保存在`sscratch`中的`a0`取出并保存
csrr t0, sscratch
sd t0, 112(a0)
# 加载`TRAPFRAME`中的`kernel_sp`到`stack pointer`寄存器中
ld sp, 8(a0)
# 加载`kernel_hartid`(当前CPU编号)到`tp`寄存器
ld tp, 32(a0)
# 加载`usertrap()`地址到`t0`寄存器
ld t0, 16(a0)
# 加载`kernel page table`到`t1`寄存器中
ld t1, 0(a0)
# 等待之前内存访问完成,清空tle
sfence.vma zero, zero
# 加载`t1`到`satp`,页表转换为内核页表
csrw satp, t1
sfence.vma zero, zero
# jump to usertrap(), which does not return
jr t0
??? t0在哪里设置的
3 trap.c\usertrap()
确定trap类型并进行处理
void usertrap(void) {
int which_dev = 0;
// 获取`sstatus`寄存器的值,`SPP`位指示`trap`是来自
// 用户模式还是管理模式
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");
// 令`stvec`指向`kernelvec`而不是之前的`uservec`
// 在kernel中,trap处理不必使用user模式的逻辑
w_stvec((uint64)kernelvec);
// myproc()根据之前存储的hart_id值,查询进程数组
// 获取当前进程信息
struct proc *p = myproc();
// 将`ecall`指令存储在`sepc`中的`pc`值保存到`trapframe`中
p->trapframe->epc = r_sepc();
//获取`scause`的值,判断trap类型
if(r_scause() == 8){
// system call
// 判断当前进程是否被杀掉
if(killed(p))
exit(-1);
// pc存储的是系统调用的地址,返回时需指向下一条指令
p->trapframe->epc += 4;
// 修改`sstatus`的SIE位,打开中断,trap执行过程中可响应中断
intr_on();
// 根据之前存储在`p->trapframe->a7`中的值找到系统调用并执行
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());
setkilled(p);
}
// 如果进程已关闭,就不必恢复了
if(killed(p))
exit(-1);
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();
usertrapret();
}
Q1 为什么需要存储sepc
中的pc
值?
当前程序运行时,CPU可能切换到别的进程,而别的进程如果也进行系统调用,sepc寄存器就会被修改,这个操作可以放到之前的trampoline.s\uservec()
中,逻辑更统一。
?scause
是何时被设置的
ecall
指令用于向运行时环境发出请求,如系统调用,因此应该是ecall设置的
4.1.4 trap.c\usertrapret()
void usertrapret(void)
{
struct proc *p = myproc();
// 关闭中断,因为接下来会修改`stvec`指向uservec,此时如果发生中断,程序会跳转到用户trap处理代码,容易出错
intr_off();
// 设置STVEC指向uservec
uint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);
w_stvec(trampoline_uservec);
// 将一些内核信息存储到trapframe中方便下次使用
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();
//SPP位和SPIE位都会影响sret指令的行为
// SPP为0表示下次执行sret的时候,返回user mode
// 而不是supervisor mode
x &= ~SSTATUS_SPP;
//SPIE位控制在执行完sret之后,是否打开中断。因为我们希望打开中断
//所以这里将SPIE bit位设置为1
x |= SSTATUS_SPIE; // enable interrupts in user mode
w_sstatus(x);
// 将之前存储的sepc值写回
w_sepc(p->trapframe->epc);
// 根据user page table地址生成相应的SATP值
uint64 satp = MAKE_SATP(p->pagetable);
// 我们会在汇编代码trampoline.S\userret()中完成page table
// 的切换,切换只能在trampoline中完成,因为只有trampoline中
// 的代码是同时在用户和内核空间中映射的
// 求出trampoline.S\userret()的地址
uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline);
// satp会作为第二个参数传给userret()
((void (*)(uint64))trampoline_userret)(satp);
}
4.1.5 trampoline.S\userret()
userret:
# a0: user page table, for satp.
# switch to the user page table.
sfence.vma zero, zero
csrw satp, a0
sfence.vma zero, zero
# a0指向TRAPFRAME作为基地址
li a0, TRAPFRAME
# 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
ld a0, 112(a0)
# sret功能:
# 1.程序切换回user mode
# 2.SEPC寄存器的数值会被拷贝到PC寄存器
# 3.重新打开中断
sret
系统调用执行结束