lab3 traps
1. 作业链接
https://pdos.csail.mit.edu/6.828/2020/labs/traps.html
2. 实习内容
RISC-V 汇编代码
(1)函数传参使用哪些寄存器?printf() 函数调用中 13
放在那个寄存器里?
函数传递参数使用寄存器:a0-a7
放置在 a2 寄存器里(第 3 个参数)
(2)哪一部分的代码体现了对 f(), g() 的调用?
f():编译器优化的太厉害了,直接把 f(8)+1 算出来了,结果是 12
1 2 3 # f(int) # Line 46 26: 45b1 li a1,12
g():编译器直接把 g() 函数优化成 inline 形式的,在调用 g()
的地方,直接使用 a0 = a0 + 3 代替
1 2 3 # g(int) # Line 32 14: 250d addiw a0,a0,3
(3)printf() 函数的地址
1 2 # Line 1092 0000000000000630 <printf>:
(4)在调用 printf() 之后,ra 寄存器中保存的值是多少?
0x38
1536 = 0x600
0x630 - 0x600 + 0x8 = 0x38
(5)下列代码输出是什么?
1 2 unsigned int i = 0x00646c72 ;printf ("H%x Wo%s" , 57616 , &i);
输出为:HE110 World
57616 = 0xe110
RISC-V 是小端的,因此 i 在内存中的分布为 72 6c 64
00(低地址到高地址),对应的字符串为 rld
如果 RISC-V 是大端的,为了保证输出输出不变,需要把 i 的值修改为
0x726c6400,不需要修改 57616
(6)下列代码的输出是什么?
输出结果为:x=3 y=5221
其中 5221
为一个不确定的数字,寄存器传参的时候少传了一个参数,所以这个值我们是未知的
Backtrace
要求
The compiler puts in each stack frame a frame pointer that holds the
address of the caller's frame pointer. Your backtrace
should use these frame pointers to walk up the stack and print the saved
return address in each stack frame.
在 sys_sleep() 中调用 backtrace()
kernel/defs.h
添加函数原型,从而能够让 sleep() 调用 backtrace()
kernel/riscv.h
1 2 3 4 5 static inline uint64 r_fp () { uint64 x; asm volatile ("mv %0, s0" : "=r" (x) ) ; return x; }
kernel/sysproc.c
在 sys_sleep() 函数中添加对 backtrace() 的调用
我直接加在开头了
kernel/printf.c
添加函数实现
注意栈帧结构
return address: a fixed offset (-8) from the frame pointer of a
stackframe
the saved frame pointer: fixed offset (-16) from the frame
pointer
1 2 3 4 5 6 7 8 9 10 11 12 13 void backtrace () { uint64 fp = r_fp(); uint64 down = PGROUNDDOWN(fp); uint64 up = PGROUNDUP(fp); while (fp < up && fp > down) { printptr(*(uint64 *)(fp - 8 )); consputc('\n' ); fp = *(uint64 *)(fp - 16 ); } }
backtrace 用途
Once your backtrace is working, call it from panic
in
kernel/printf.c
so that you see the kernel's backtrace when
it panics.
Alarm
问题描述
In this exercise you'll add a feature to xv6 that periodically
alerts a process as it uses CPU time. This might be useful for
compute-bound processes that want to limit how much CPU time they chew
up, or for processes that want to compute but also want to take some
periodic action. More generally, you'll be implementing a primitive form
of user-level interrupt/fault handlers; you could use something similar
to handle page faults in the application, for example. Your solution is
correct if it passes alarmtest and usertests.
添加两个系统调用
准备工作
Makefile
把 $U/_alarmtest
添加到 UPROGS
里
user/user.h
1 2 int sigalarm (int ticks, void (*handler)()) ;int sigreturn (void ) ;
user/user.pl
1 2 entry("sigalarm"); entry("sigreturn");
kernel/syscall.h
1 2 #define SYS_sigalarm 22 #define SYS_sigreturn 23
kernel/syscall.c
1 2 3 4 5 6 7 8 extern uint64 sys_sigalarm (void ) ;extern uint64 sys_sigreturn (void ) ;static uint64 (*syscalls[]) (void ) = { [SYS_sigalarm] sys_sigalarm, [SYS_sigreturn] sys_sigreturn, };
kernel/sysproc.c
1 2 3 4 5 6 uint64 sys_sigalarm (void ) { return 0 ; } uint64 sys_sigreturn (void ) { return 0 ; }
做到这里,程序能够编译成功了,但是具体功能还没有实现
test0: invoke handler
实现逻辑
在调用 sysalarm 的时候,在进程的数据结构中保存参数
handler、多久调用一次 handler、现在已经运行了多少个 ticks(初始化为
0)
在收到时钟中断信号的时候,判断是否需要调用 handler,如果需要,设置
PC 为 handler
这个是先破坏了栈帧,因此是有问题的
kernel/proc.h
添加一些中间变量辅助实现 sys_alarm() 功能
1 2 3 4 5 6 7 struct proc { int ticks_for_alarm; void (*)(alarm_handler); int ticks_used; }
kernel/proc.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static struct proc* allocproc (void ) { p->ticks_for_alarm = 0 ; p->alarm_handler = 0 ; p->ticks_used = 0 ; return p; } static void freeproc (struct proc *p) { p->ticks_for_alarm = 0 ; p->alarm_handler = 0 ; p->ticks_used = 0 ; }
kernel/sysproc.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 uint64 sys_sigalarm (void ) { struct proc *p = myproc(); int n; uint64 handler; if (argint(0 , &n) < 0 ) return -1 ; if (argaddr(1 , &handler) < 0 ) return -1 ; p->alarm_handler = (void (*)()) handler; p->ticks_for_alarm = n; p->ticks_used = 0 ; return 0 ; } uint64 sys_sigreturn (void ) { return 0 ; }
kernel/usertrap.c(错误实现)
每次接受到时钟中断的时候,计数器 ticks 自增
在从内核态返回用户态的时候判断 ticks 记录是否超标,如果是,则调用
handler 进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void usertrap (void ) { if (which_dev == 2 ){ if (p->ticks_for_alarm) { ++p->ticks_used; } yield(); } } void usertrapret (void ) { if (p->ticks_for_alarm && p->ticks_used == p->ticks_for_alarm) { p->ticks_used = 0 ; w_sepc((uint64)p->alarm_handler); } else { w_sepc(p->trapframe->epc); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void usertrap (void ) { if (which_dev == 2 ){ if (p->ticks_for_alarm) { ++p->ticks_used; if (p->ticks_used == p->ticks_for_alarm) { p->ticks_used = 0 ; p->trapframe->epc = (uint64)p->alarm_handler; } yield(); } } }
test1: resume interrupted
code
这一部分的实现除了 usertrap.c,其他部分都是基于 test0
进行修改的
kernel/usertrap.c(正确实现)
需要保存寄存器状态,通过 sysalarm 和 sigreturn 的配合实现
要求每个 hander 的最后都需要调用 sigreturn 回到内核态
test0 的实现回到用户态之前需要保存当时的寄存器状态,当 sigreturn
回到了内核态是恢复寄存器状态,返回用户态正常执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void usertrap (void ) { if (which_dev == 2 ){ if (p->ticks_for_alarm) { ++p->ticks_used; if (p->ticks_used == p->ticks_for_alarm) { p->ticks_used = 0 ; memmove(&(p->trapframe2), p->trapframe, sizeof (struct trapframe)); p->trapframe->epc = (uint64)p->alarm_handler; } } yield(); } }
kernel/proc.h
需要在 proc 数据结构中保存寄存器
这里为了方便直接使用 trapframe
的数据结构,实际上有些值是不需要保存恢复的
kernel
的相关值不用恢复:kernel_satp、kernel_sp、kernel_trap、kernel_hartid
1 2 3 4 5 struct proc { struct trapframe trapframe2 ; };
kernel/sysproc.c
当调用 sys_sigreturn 的时候恢复寄存器状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 uint64 sys_sigreturn (void ) { struct proc *p = myproc(); if (p->ticks_for_alarm) { memmove( ((uint64 *)p->trapframe) + 5 , ((uint64 *)&(p->trapframe2)) + 5 , sizeof (struct trapframe) - 5 *sizeof (uint64) ); p->trapframe->epc = p->trapframe2.epc; p->is_alarm = 0 ; } return 0 ; }
到这里为止,已经能够通过 test2,在不破坏栈帧的情况下实现 sysalarm
的效果
test2(): prevent re-entrant
calls
实现逻辑
不允许重复调用,当一个 sysalarm 已经存在的时候,没有调用 sigreturn
之前不允许调用新的 sigalarm
这一部分的实现很简单,在 proc 数据结构中添加一个标记变量
调用 sigalarm 的时候设置为 1
如果调用 sigalarm 的时候已经为 1,则该调用不生效
调用 sigreturn 的时候设置为 0
kernel/proc.h
1 2 3 4 struct proc { int is_alarm; };
kernel/proc.c
1 2 3 4 5 6 7 8 9 10 11 static struct proc* allocproc (void ) { p->is_alarm = 0 ; return p; } static void freeproc (struct proc *p) { p->is_alarm = 0 ; }
kernel/sysproc.c(错误实现)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 uint64 sys_sigalarm (void ) { if (p->is_alarm) { return 0 ; } else if (handler){ p->is_alarm = 1 ; } } uint64 sys_sigreturn (void ) { p->is_alarm = 0 ; }
这样子实现是错误的,只能阻止新的 sigalarm
的调用,但是不能够实现已经设置的 handler 在 sigreturn
之前的再次调用
输出结果
1 2 3 4 test2 start .....alarm! alarm! test2 failed: alarm handler called more than once
kernel/sysproc.c(正确实现)
此时 is_alarm 的含义变成了是否调用 handler
如果有 handler 调用,禁止新的 sigalarm 调用
恢复当然还是在调用 sigreturn 的时候恢复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 uint64 sys_sigalarm (void ) { if (p->is_alarm) { return 0 ; } } uint64 sys_sigreturn (void ) { if (p->is_alarm) { memmove( ((uint64 *)p->trapframe) + 5 , ((uint64 *)&(p->trapframe2)) + 5 , sizeof (struct trapframe) - 5 *sizeof (uint64) ); p->trapframe->epc = p->trapframe2.epc; } p->is_alarm = 0 ; }
kernel/trap.c
正确实现应该是在保存寄存器的时候进行判断
因为我们只有一块区域,不能够实现递归调用
也能解决上面的问题
1 2 3 4 5 6 7 8 9 10 void usertrap (void ) { if (!(p->is_alarm)) { p->is_alarm = 1 ; memmove(&(p->trapframe2), p->trapframe, sizeof (struct trapframe)); p->trapframe->epc = (uint64)p->alarm_handler; } }
3. 实验结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 == Test answers-traps.txt == answers-traps.txt: OK == Test backtrace test == $ make qemu-gdb backtrace test : OK (14.9s) == Test running alarmtest == $ make qemu-gdb (7.0s) == Test alarmtest: test0 == alarmtest: test0: OK == Test alarmtest: test1 == alarmtest: test1: OK == Test alarmtest: test2 == alarmtest: test2: OK == Test usertests == $ make qemu-gdb usertests: OK (231.3s) (Old xv6.out.usertests failure log removed) == Test time == time: OK Score: 85/85
4. 遇到的困难以及收获
做完整个 lab 感觉操作系统的设计很巧妙,这里的的 sysalarm
的实际就感觉很有趣,通过两次系统调用解决了一次系统调用很难解决的问题。
具体的解决方法在上面都已经提到了,主要是怎么设置
PC,以及栈帧的保存与恢复问题。
一个小问题
在这个 lab 的实现中,在 sigreturn()
未返回时,有两种实现方案
不允许 sigalarm()
重新设置 handler
允许 sigalarm()
重新设置
handler
,但是不允许其有修改保存的寄存器(我们只有一块区域,不支持递归)
这两种方法都可行,测试都能过
但似乎又都有各自的问题
第一种方法不能允许 sigalarm(0, 0)
的调用,这样的调用能够取消 handler()
第二种方法会让最终的 handler()
不太可控,可能在
sigreturn()
未返回的时候设置了奇怪的
handler()
我是按照方法 1 实现的
sys_alarm 保存的寄存器
epc(需要)
callee-saved(需要)(sig_return 之前可能没有 pop 栈)
caller-saved(不需要)
kernel_sp(不需要)
kernel_hartid(不能)
kernel_stap(不需要)
5. 对课程或 lab 的意见和建议
一个建议是最后的 usertests.c
的运行时间太长了,感觉没有必要用那么多的测试,之前已按因为一行代码写反了一直超时,浪费了太多时间。
6. 参考文献
https://pdos.csail.mit.edu/6.828/2020/reference.html
https://github.com/riscv/riscv-isa-manual/releases/download/draft-20200727-8088ba4/riscv-spec.pdf
https://pdos.csail.mit.edu/6.828/2019/xv6/book-riscv-rev0.pdf