文章目录
- chapter 4
- 概览
- 4.1 CPU trap流程
- 使用寄存器
- 如果cpu想处理1个trap
- 4.2 用户态引发的trap
- 4.2.1 uservec
- 4.2.2 usertrap
- 4.2.3 usertrapret和userret
- usertrapret
- userret
- Lab4
- Backtrace (moderate)
- Alarm (hard)
chapter 4
概览
- trap的场景:系统调用,设备中断,异常
- trap对用户是透明的,用户不会察觉发生了1个trap:内核会保存trap前的状态,在trap后恢复
4.1 CPU trap流程
使用寄存器
stvec: 保存trap程序地址
sepc: 临时保存pc寄存器,trap结束时,sret(TODO 不知道是什么,可能是一段程序)会重新将sepc复杂到pc中
scause: trap原因
sscratch: 方便上下文切换
- 见userret,sscratch寄存器保存用户页表的
trapframe
页 - 见uservec,
trapframe
页可以用来暂存用户态的寄存器,中断后切换回来;同时保存内核页表在中断时从用户页表切换到内核页表,可以认为是个中介的临时仓库
sstatus: SPP表示从用户态(0)或从内核态(1)切换过来的trap;SIE表示是否启用设备中断
如果cpu想处理1个trap
trap相关:设置scause和sstatus,保存trap原因和来源
状态保存相关:把pc暂存到sepc
执行相关:切换到监督者模式,把stvec复制到pc
cpu不会切换内核页表,不会切换内核栈。但是必须切换pc。
4.2 用户态引发的trap
4.2.1 uservec
uservec就是用户态的trap入口,即cpu的stvec会被设成uservec。
这里要完成3个事:
- 保存用户态的32个寄存器
- 切换satp寄存器,使用内核页表
- 调用处理中断的函数usertrap
(倒叙,写用户进程开始执行前的事情,可参见4.2.3节usertrapret和userret的功能)
在进入用户空间之前,内核会分配1页TRAPFRAME,专门用来暂存trap发生时需要的东西,这个TRAPFRAME的地址放在sscratch寄存器中,TRAPFRAME页还会预先放着开始就已经知道且在trap发生时需要用到的东西:usertrap的地址(进行trap类型判断并调用相应处理函数)、cpu的hartid(TODO,还不知道作用,可能是CPU的id,可以记录处理trap的CPU)、内核页表地址(uservec需要进行用户态页表到内核态页表的切换)。
.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
# 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
# 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
4.2.2 usertrap
usertrap函数会处理来自用户态的中断、异常或系统调用,由uservec汇编代码调用;这里会判断trap的原因,以调用合适的处理函数。最后调用usertrapret()返回用户态。
//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
void
usertrap(void)
{
int which_dev = 0;
if((r_sstatus() & SSTATUS_SPP) != 0)
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){
// 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();
} 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();
}
4.2.3 usertrapret和userret
usertrapret
usertrapret:切换pc寄存器
userret:恢复寄存器,切换页表
usertrapret代码如下,
- 临时关闭中断功能:
intr_off();
将中断开关临时关闭(TODO:如何关闭),在从内核态到用户态的转换过程中,暂时停止中断功能,等切换完毕后再开启,可能是为了避免状态机紊乱。 - 改变 stvec 来引用 uservec:
w_stvec(TRAMPOLINE + (uservec - trampoline));
推测是重新写cpu的stvec
寄存器为uservec
地址,以保证下次中断时,cpu
仍然跳转到uservec
去处理中断。 - 准备 uservec 所依赖的 trapframe 字段,如
kernel_satp
为内核页表地址等等。 - 写一些CPU寄存器:如设
sstatus
的SPP为0,表示为用户态的中断;设sstatus
的SPIE为1,表示在用户态使能中断 - 将 sepc 设置为先前保存的用户程序计数器
w_sepc(p->trapframe->epc);
- 调用 userret,并把
TRAPFRAME
和satp
作为参数传递过去,userret
会切换用户态页表,重设用户态寄存器,最后切换回用户态
//
// return to user space
//
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();
// send syscalls, interrupts, and exceptions to trampoline.S
w_stvec(TRAMPOLINE + (uservec - trampoline));
// 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()
// 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);
// 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 切换到进程的用户页表,因为用户态和内核态的trampoline都是直接映射,因此在此时进行页表切换后,trampoline的程序仍能继续往下执行。此时a0寄存器指向用户页表的
TRAPFRAME
页,先将其保存到sscratch
.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)
。。。。
# 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
Lab4
Backtrace (moderate)
实验内容:添加栈帧信息打印
考察点:xv6的栈结构;栈以类似链表的形式保存在1个页面中
关键提示:address lives at a fixed offset (-8) from the frame pointer of a stackframe, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.
关键代码:
void
backtrace(void)
{
printf("backtrace:\n");
uint64 fp = r_fp();
uint64 down = PGROUNDDOWN(fp);
uint64 up = PGROUNDUP(fp);
while (fp >= down && fp < up)
{
uint64* res_addr = (uint64*)(fp - 8);
uint64* next_fp_addr = (uint64*)(fp - 16);
printf("%p\n", *res_addr);
fp = *next_fp_addr;
}
}
Alarm (hard)
实验内容:实现系统调用,在进程使用CPU时间超时时,进行回调函数调用,并能正常返回用户态
考察点:系统调用流程;usertrap的寄存器保存位置在trapframe页面;usertrap的pc计数器存储在epc寄存器;
关键提示:
- When a trap on the RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?
- Your solution will require you to save and restore registers—what registers do you need to save and restore to resume the interrupted code correctly? (Hint: it will be many).
关键代码:
// kernel/sysproc.c
int
sys_sigreturn(void)
{
memmove(myproc()->trapframe, myproc()->trapframe_back, sizeof(struct trapframe));
myproc()->calling = 0;
return 0;
}
//kernel/trap.c
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
{
p->ticks_count ++;
if (p->alarmInterval != -1 && p->ticks_count >= p->alarmInterval && p->calling != 1)
{
// if a handler hasn't returned yet, the kernel shouldn't call it again
p->calling = 1;
//"re-arm" the alarm counter after each time it goes off
p->ticks_count = 0;
//save and restore registers
memmove(p->trapframe_back, p->trapframe, sizeof(struct trapframe));
//Q:When a trap on the RISC-V returns to user space,
//what determines the instruction address at which user-space code resumes execution?
//A: epc!
p->trapframe->epc = p->alarmHandler;
}
yield();
}