文章目录
- 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 TRAPFRAMEcsrrw a0, sscratch, a0# save the user registers in TRAPFRAMEsd ra, 40(a0)sd sp, 48(a0)sd gp, 56(a0).............# save the user a0 in p->trapframe->a0csrr t0, sscratchsd t0, 112(a0)# restore kernel stack pointer from p->trapframe->kernel_spld sp, 8(a0)# make tp hold the current hartid, from p->trapframe->kernel_hartidld tp, 32(a0)# load the address of usertrap(), p->trapframe->kernel_trapld t0, 16(a0)# restore kernel page table from p->trapframe->kernel_satpld t1, 0(a0)csrw satp, t1sfence.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 returnjr 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 callif(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.Sw_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 tablep->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stackp->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 modex |= SSTATUS_SPIE; // enable interrupts in user modew_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, a1sfence.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 TRAPFRAMEld ra, 40(a0)ld sp, 48(a0)ld gp, 56(a0)。。。。# restore user a0, and save TRAPFRAME in sscratchcsrrw 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 againp->calling = 1;//"re-arm" the alarm counter after each time it goes offp->ticks_count = 0;//save and restore registersmemmove(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();}