MIT 6.828 操作系统课程系列4 traps#
https://pdos.csail.mit.edu/6.828/2022/labs/traps.html
本次lab需要读xv6手册第4章。
riscv的calling convention文档在
riscv-non-isa/riscv-elf-psabi-doc
本来是在riscv-spec文档的,后来被挪出。
或https://pdos.csail.mit.edu/6.828/2022/readings/riscv-calling.pdf
或https://wiki.riscv.org/display/HOME/RISC-V+Technical+Specifications
trap相关内容在lab0和lab2里已经看了不少。
再次强烈建议先完成CS143编译原理,这些call stack的东西就很熟了。
做题1:RISC-V assembly#
看看call.asm。挺难看的。
做题2:Backtrace#
calling convention和CS143有不同。寄存器的保存方式不同可对代码逻辑有影响。
c中嵌入asm的语法https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Extended-Asm
例如
asm volatile("mv %0, sp" : "=r" (x) );
是AssemblerTemplate : OutputOperands
的形式。%0
代表右边OutputOperands的第1项。=r
表示register。
结果就是把sp的值返回到x。r_fp读出fp。fp-8位置是ra。-16位置是saved fp。用saved fp往上回溯即可。
限定在一页。超出退出。
做题3:Alarm#
要做一个syscall实现定时调用user函数
先看看时钟相关流程
// 时钟初始化
// 每个cpu都会执行
// 此时cpu为M模式
uint64 timer_scratch[NCPU][5];
// core local interruptor (CLINT), which contains the timer.
#define CLINT 0x2000000L
#define CLINT_MTIMECMP(hartid) (CLINT + 0x4000 + 8*(hartid))
#define CLINT_MTIME (CLINT + 0xBFF8) // cycles since boot.
timerinit()
// each CPU has a separate source of timer interrupts.
int id = r_mhartid();
// ask the CLINT for a timer interrupt.
int interval = 1000000; // cycles; about 1/10th second in qemu.
*(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;
// 详见lab7
// #define CLINT_MTIME (CLINT + 0xBFF8)这个地址存的是开机以来的tick数。
// CLINT_MTIMECMP是每个cpu的timecmp地址。
// 从CLINT + 0x4000开始每8字节存一个cpu的timecmp。
// 当系统的tick数大于timecmp值,就触发该cpu的timer中断。这样实现定时。
// 见riscv-privileged文档
// 参考
// https://github.com/riscv/riscv-aclint/blob/main/riscv-aclint.adoc#2-machine-level-timer-device-mtimer
// https://github.com/qemu/qemu/blob/master/hw/riscv/virt.c
// https://danielmangum.com/posts/risc-v-bytes-timer-interrupts/
// prepare information in scratch[] for timervec.
// scratch[0..2] : space for timervec to save registers.
// scratch[3] : address of CLINT MTIMECMP register.
// scratch[4] : desired interval (in cycles) between timer interrupts.
uint64 *scratch = &timer_scratch[id][0];
scratch[3] = CLINT_MTIMECMP(id);
scratch[4] = interval;
w_mscratch((uint64)scratch);
// set the machine-mode trap handler.
w_mtvec((uint64)timervec);
// 配置M模式的trap处理
// enable machine-mode interrupts.
w_mstatus(r_mstatus() | MSTATUS_MIE);
// enable machine-mode timer interrupts.
w_mie(r_mie() | MIE_MTIE);
// 使能M模式timer中断
# 时钟中断处理
# 再次设定interval的定时。并且转发到S模式。
timervec:
# start.c has set up the memory that mscratch points to:
# scratch[0,8,16] : register save area.
# scratch[24] : address of CLINT's MTIMECMP register.
# scratch[32] : 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, 24(a0) # CLINT_MTIMECMP(hart)
ld a2, 32(a0) # interval
ld a3, 0(a1)
add a3, a3, a2
sd a3, 0(a1)
# arrange for a supervisor software interrupt
# after this handler returns.
li a1, 2
csrw sip, a1
ld a3, 16(a0)
ld a2, 8(a0)
ld a1, 0(a0)
csrrw a0, mscratch, a0
mret # start()中已设置代理。mret会跳转到kernelvec或uservec
# 对于设备中断,kerneltrap和usertrap都会走到devintr进行检测。
# 如果是时钟中断。会走clockintr把全局的ticks+1。
上面的流程维护好了全局的ticks
。
看一个相关的应用sleep
。
uint64
sys_sleep(void)
{
int n;
uint ticks0;
argint(0, &n); // 等待n个tick
acquire(&tickslock); // 获取ticks锁
ticks0 = ticks;
while(ticks - ticks0 < n){ // 检查是否达到n
if(killed(myproc())){
release(&tickslock);
return -1;
}
sleep(&ticks, &tickslock);
struct proc *p = myproc();
acquire(&p->lock);
release(lk); // 释放ticks锁
// Go to sleep.
p->chan = chan; // sleep的标志
p->state = SLEEPING; // 设置为SLEEPING
sched();
swtch(&p->context, &mycpu()->context);
// 当前进程一定是从scheduler函数切换过来的。
// 所以这里一定是切换回scheduler函数。让kernel继续调度。
// 此进程开始卡在此处
// 之后会发生时钟中断
clockintr()
acquire(&tickslock);
ticks++; // ticks被更新
wakeup(&ticks);
// 让SLEEPING的进程都醒来
if(p->state == SLEEPING && p->chan == chan) {
p->state = RUNNABLE;
}
release(&tickslock);
// 醒来后。等kernel调度,切换回此地。
p->chan = 0;
// Reacquire original lock.
release(&p->lock);
acquire(lk);
// 到此经过了一轮sleep。ticks已更新。再次检查释放等待了n个tick。如果超过n,说明完成sleep,跳出循环返回。
}
release(&tickslock);
return 0;
}
可以看到这种sleep是不准的。完全看调度的心情,如果有很多其他进程,cpu可能先跑其他进程。
等再次检查ticks时,可能早超过了n。
而且必须有个wakeup来配合,感觉不好。
起两个system call。proc里起相关的数据。
usertrap里判断tick。如果到时,设置epc为传入的函数。此时回到user空间,能进入alarm回调函数。
但是回不到timer中断时user空间主函数的位置。因为场景已经乱了。
进中断时保存了某个场景的寄存器,而只改一个epc返回后,造成在旧的场景下执行回调函数的指令,完全混乱。课程给了解决方案,用户必须在回调里调一个sigreturn。
那么在timer回调之前先另外保存一份原始的trapframe。sigreturn里把它恢复就行了。
之前的本质问题就是原始的trapframe被损坏。syscall默认a0会被写成返回值。针对sigreturn处理一下即可。