操作系统 MIT6.S081 Lab4 Trap
实验原理
① Trap:系统调用、异常和中断会导致CPU停止当前工作,这三种情况统称为trap
Xv6 将所有的 Traps 都放在内核中处理:
- 对于系统调用,则执行相应的系统调用
- 对于中断,则调用相应的设备驱动程序
- 对于异常,直接杀死抛出异常的进程
② RISC-V 的栈和寄存器
每一个 RISC-V CPU 都有一组寄存器,内核可以从中读取 Trap 的信息,也可以往寄存器中写值以告知 CPU 如何处理 Trap,比较重要的有:
- stvec:处理 Trap 的 handler 入口
- sepc:保存当前进程的 PC
- scause:其中保存的数值反映了 Trap 的类型
Trampoline:当 trap 发生时,RISCV并不会切换页表,因此用户空间到stvec应当有着稳定的映射(即trampoline)所有用户的 trampoline 都映射到同一片区域,而且只能由内核访问。
Trapframe:保存了当前进程所使用的寄存器的所有值
RISC-V 的寄存器:
reg | name | saver | description
-------+-------+--------+------------
x0 | zero | | hardwired zero
x1 | ra | caller | return address
x2 | sp | callee | stack pointer
x3 | gp | | global pointer
x4 | tp | | thread pointer
x5-7 | t0-2 | caller | temporary registers
x8 | s0/fp | callee | saved register / frame pointer
x9 | s1 | callee | saved register
x10-11 | a0-1 | caller | function arguments / return values
x12-17 | a2-7 | caller | function arguments
x18-27 | s2-11 | callee | saved registers
x28-31 | t3-6 | caller | temporary registers
pc | | | program counter
RISC-V 的栈结构:
+-> | ... | |
| +-----------------+ |
| | return address | |
| | previous fp ------+
| | saved registers |
| | local variables |
| | ... | <-+
| +-----------------+ |
| | return address | |
+------ previous fp | |
| saved registers | |
| local variables | |
$fp --> | ... | |
+-----------------+ |
| return address | |
| previous fp ------+
| saved registers |
$sp --> | local variables |
+-----------------+
Part 1 Backtrace
实验目的: 实现 backtrace 功能,用于打印出栈上调用链的所有返回地址
实验步骤:
① 已知当前函数的帧指针存在寄存器 s0 中,因此在 kernel/riscv.h 中添加获得该值的函数:
// kernel/riscv.h
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
② 实现 backtrace() ,并在 defs.h 中声明:
// kernel/defs.h
// printf.c
void backtrace(void);
// kernel/printf.c
void backtrace(void)
{
printf("backtrace:\n");
uint64 retAddr = r_fp();
while (retAddr < PGROUNDUP(retAddr)) {
printf("%p\n", *((uint64*)(retAddr - 8)));
retAddr = *((uint64*)(retAddr - 16));
}
}
使用 PGUPGROUND() 获得当前栈的顶部地址;由 RISC-V 的栈结构可知,返回地址存放在帧指针的 -8 偏移量处,保存的帧指针存放在帧指针的 -16 偏移量处
③ 在 sys_sleep() 中调用 backtrace() ,也可以在 panic() 中调用 backtrace() :
// kernel/sysproc.c
uint64
sys_sleep(void)
{
...
backtrace();
return 0;
}
// kernel/printf.c
void
panic(char *s)
{
...
backtrace();
for(;;) ;
}
实验结果:
执行 bttest ,可以看到打印出了三个地址:

执行 addr2line -e kernel/kernel ,查找地址对应的代码:

查看源码,可以观察到调用链为:usertrap() →\to→ syscall() →\to→ sys_sleep()
Part 2 Alarm
实验目标: 实现进程时间分片的功能
实验步骤:
① 在 struct proc 中添加相关变量,并添加构造和析构动作,用于辅助 sigalarm() 和 sigreturn() 系统调用实现:
// kernel/proc.h
struct proc {
...
int interval; // 从 sigalarm 开始,该进程能运行多少个 ticks
void (*handler)(); // 指向处理函数的函数指针
int handler_lock; // 自旋锁,防止对处理程序的重入调用(有点像关中断的动作)
int ticks; // 从 sigalarm 开始,该进程移进运行了多少个 ticks
struct trapframe *trapframe_copy; // 保存现场的一份 copy
};
// kernel/proc.c
static struct proc*
allocproc(void)
{
...
p->interval = 0;
p->handler = 0;
p->handler_lock = 0;
p->ticks = 0;
if ((p->trapframe_copy = (struct trapframe*)kalloc()) == 0) {
freeproc(p);
release(&p->lock);
}
return p;
}
static void
freeproc(struct proc *p)
{
...
if (p->trapframe_copy) // 释放现场的copy的空间
kfree((void*)p->trapframe_copy);
}
② 每个 tick 都会产生一个中断,因此需要在处理这个中断时判断时间片是否到期,如果已经到了的话就要保存现场,并且调用处理函数(即把 PC 值改为 handler 的地址):
// kernel/trap.c
void
usertrap(void)
{
...
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
++(p->ticks);
if (p->interval != 0 && p->ticks == p->interval) {
if (p->handler_lock == 0) {
p->handler_lock = 1;
copy_trapframe(p->trapframe, p->trapframe_copy);
p->trapframe->epc = (uint64)p->handler;
}
}
yield();
}
}
其中 copy_trapframe() 是个辅助函数,只是简单拷贝所有寄存器的值,之后恢复寄存器的值时也会用到,因此抽象成了一个函数;也要在 defs.h 中声明以供外界调用:
// kernel/trap.c
void copy_trapframe(struct trapframe* from, struct trapframe* to) {
to->kernel_satp = from->kernel_satp;
to->kernel_sp = from->kernel_sp;
to->kernel_trap = from->kernel_trap;
to->epc = from->epc;
to->kernel_hartid = from->kernel_hartid;
to->ra = from->ra;
to->sp = from->sp;
to->gp = from->gp;
to->tp = from->tp;
to->t0 = from->t0;
to->t1 = from->t1;
to->t2 = from->t2;
to->s0 = from->s0;
to->s1 = from->s1;
to->a0 = from->a0;
to->a1 = from->a1;
to->a2 = from->a2;
to->a3 = from->a3;
to->a4 = from->a4;
to->a5 = from->a5;
to->a6 = from->a6;
to->a7 = from->a7;
to->s2 = from->s2;
to->s3 = from->s3;
to->s4 = from->s4;
to->s5 = from->s5;
to->s6 = from->s6;
to->s7 = from->s7;
to->s8 = from->s8;
to->s9 = from->s9;
to->s10 = from->s10;
to->s11 = from->s11;
to->t3 = from->t3;
to->t4 = from->t4;
to->t5 = from->t5;
to->t6 = from->t6;
}
// kernel/defs.h
// trap.c
void copy_trapframe(struct trapframe*, struct trapframe*);
③ 为 sigalarm() 和 sigreturn() 系统调用添加相关声明:
// user/user.h
int sigalarm(int, void(*)(void));
int sigreturn(void);
# user.usys.pl
entry("sigalarm");
entry("sigreturn");
// kernel/syscall.h
#define SYS_sigalarm 22
#define SYS_sigreturn 23
// kernel/syscall.c
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);
static uint64 (*syscalls[])(void) = {
...
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,
};
④ 实现 sigalarm() 和 sigreturn() 系统调用
// sys_proc.c
uint64
sys_sigalarm(void) {
int interval;
uint64 handler;
argint(0, &interval);
argaddr(1, &handler);
myproc()->interval = interval;
myproc()->handler = (void(*)(void))handler;
return 0;
}
uint64
sys_sigreturn(void) {
copy_trapframe(myproc()->trapframe_copy, myproc()->trapframe);
myproc()->ticks = 0;
myproc()->handler_lock = 0;
return myproc()->trapframe->a0;
}
sys_sigalarm() 从 a0 和 a1 获得用户调用系统调用时传入的 ticks 数和处理函数,并且用这些值更新 struct proc 里的成员;
sys_sigreturn() 需要恢复现场、清除刚刚设置的成员、解除对处理函数的锁;由于中断处理对用户来说应当是透明的,当 sys_sigreturn() 返回值时会更新 a0 寄存器的值,因此直接让其返回当前 a0 的值以保证不变。
⑤ 在 Makefile 中添加 alarmtest 的指令声明:
# Makefile
UPROGS=\
...
$U/_alarmtest\
实验结果: 通过了 alarmtest 和 usertests -q 测试:
![]() | ![]() |
|---|
本文详细介绍了RISC-V架构下操作系统如何处理陷阱(包括系统调用、异常和中断),并展示了如何实现回溯功能来打印栈上的调用链。此外,还阐述了如何通过trapframe和自旋锁实现进程时间分片,详细描述了sys_sigalarm和sys_sigreturn系统调用的实现过程,以及它们在时间片到期时如何保存和恢复现场。最后,通过实验结果验证了backtrace和时间分片功能的正确性。




被折叠的 条评论
为什么被折叠?



