Lab3:Traps
参考文章:
6.S081 & 操作系统内核
操作系统MIT6.S081:Lab4->Trap
Xv6操作系统的系统调用通过 trampoline 来实现用户空间和内核空间之间的跳转,trampoline页面是在内核地址空间和用户地址空间均有相同的地址映射,也就是说:trampoline页面在物理地址空间只有一份(且是可读可执行的代码),而在内核地址空间和用户地址空间中,均将MAXVA - PGSIZE的地址(也就是第一个页面)映射到了这段代码。
这样做的好处就是,在执行trampoline中的代码时,执行内核页表和用户进程页表不会使得程序崩溃,这也就是为什么这个页面叫做trampoline(蹦床)的原因了。
为了理解Xv6的代码,需要了解部分risc-v的汇编,如果不深究的话,我认为只需要知道函数调用参数传递通过的是a0-a7寄存器,函数调用返回值通过a0寄存器返回。其他很多指令和x86汇编类似。
- RISC-V的Trap机制:(来自xv6book Chapter3)
Xv6 trap 处理分为四个阶段:RISC-V CPU采取的硬件行为,为内核C代码准备的汇编入口,处理trap的C 处理程序,以及系统调用或设备驱动服务。
每个RISC-V CPU都有一组控制寄存器,内核写入这些寄存器来告诉CPU如何处理trap,内核可以通过读取这些寄存器来发现已经发生的trap。RISC-V文档包含了完整的叙述[1]。riscv.h(kernel/riscv.h:1)包含了xv6使用的定义。这里是最重要的寄存器的概述。
stvec:内核在这里写下trap处理程序的地址;RISC-V跳转到这里来处理trap。
sepc:当trap发生时,RISC-V会将程序计数器保存在这里(因为PC会被stvec覆盖)。sret(从trap中返回)指令将sepc复制到pc中。内核可以写sepc来控制sret的返回到哪里。
scause:RISC -V在这里放了一个数字,描述了trap的原因。
sscratch:内核在这里放置了一个值,在trap处理程序开始时可以方便地使用(trapframe的地址)。
sstatus:sstatus中的SIE位控制设备中断是否被启用,如果内核清除SIE,RISC-V将推迟设备中断,直到内核设置SIE。SPP位表示trap是来自用户模式还是supervisor模式,并控制sret返回到什么模式。
上述寄存器与在特权态模式下处理的trap有关,在用户模式下不能读或写。
对于机器模式下处理的trap,有一组等效的控制寄存器;xv6只在定时器中断的特殊情况下使用它们。
【来自用户空间的trap的处理路径是uservec(kernel/trampoline.S:16),然后是usertrap(kernel/trap.c:37);返回时是usertrapret(kernel/trap.c:90),然后是userret(kernel/trampoline.S:16)】
(Chapter4.2: Trap from user space 清楚地介绍了整个过程,包括trapframe的概念和作用,可以作为最好的最详细的参考资料)
参考文章:
小切的博客
xv6中的trap处理机制
xv6的trap机制
xv6陷入
本 lab 的任务是添加系统调用,理解 traps 的实现。
阅读指路:
kernel/trampoline.S:用户态和内核态相互切换的汇编代码(寄存器的取出和写入)
kernel/trap.c:处理所有中断的代码
1. RISC-V Assembly
参考文章:
操作系统MIT6.S081:Lab4->Trap
一文学懂riscv汇编操作
操作系统MIT6.S081:P4->RISC-V calling conventions and stack frames
2. Backtrace
在 kernel/printf.c 中实现函数 backtrace(),发生错误时用于打印堆栈上的函数调用列表。
(useful for debugging)
原理见下图:
准备工作:
- Add the prototype for backtrace to kernel/defs.h so that you can invoke backtrace in sys_sleep().
void backtrace(void);
主要工作:
- The GCC compiler stores the frame pointer of the currently executing function in the register s0 【GCC编译器将当前运行的函数的 frame pointer 存储在寄存器 s0】. Add the following function to kernel/riscv.h:
static inline uint64
r_fp()
{
uint64 x;
asm volatile(“mv %0, s0” : “=r” (x) );
return x;
}
and call this function in backtrace to read the current frame pointer. 【在 backtrace() 函数中调用 r_fp() 读取当前 frame pointer】.This function uses in-line assembly to read s0. - Note that the return 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.【当前函数的返回地址在栈帧-8的地址存储,上一个栈帧的 frame pointer 在当前栈帧-16的地址存储】
(见上图原理) - Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address. You can compute the top and bottom address of the stack page by using PGROUNDDOWN(fp) and PGROUNDUP(fp) 【用定义的两个宏,计算栈的地址上下界,判断backtrace循环迭代的结束点】 (see kernel/riscv.h. These number are helpful for backtrace to terminate its loop.
// [kernel/printf.c]
void
backtrace()
{
printf("backtrace:\n");
uint64 fp = r_fp(); // 获取当前栈帧
uint64 bottom = PGROUNDUP(fp); // 栈由高地址向低地址增长, 栈底是当前页的最高地址!
while(fp < bottom){ // 当帧指针在有效地址范围内:
printf("%p\n", *((uint64*)(fp - 8)));
// 当前fp-8的地址空间存储着函数返回地址(指针, %p形式输出)
fp = *((uint64*)(fp - 16));
// 当前fp-16的地址空间存储着上一个调用函数的栈帧的fp, 循环迭代输出
}
}
#define PGROUNDUP(sz) (((sz)+PGSIZE-1) & ~(PGSIZE-1))
// [helpful for backtrace to terminate its loop]
#define PGROUNDDOWN(a) (((a)) & ~(PGSIZE-1))
//uint64 sz: Size of process memory (bytes)
在系统调用中加入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.【在kernel/printf.c的panic()函数中加入backtrace()调用;在kernel/sysproc.c的sys_sleep()函数中加入backtrace()调用】
void panic(char *s)
{
// ......
printf("\n");
backtrace(); //调用backtrace
panicked = 1; // freeze uart output from other CPUs
// ......
}
uint64
sys_sleep(void)
{
// ......
release(&tickslock);
backtrace();// 调用backtrace
return 0;
}
3. Alarm
目标:
添加一个新的系统调用,实现警报功能,定期向使用 CPU 时间的进程发出警报。
介绍:
添加一个新的 sigalarm(interval, handler) 系统调用:
如果应用程序调用sigalarm(n, fn),那么在程序消耗每n个CPU时间“tick”之后,内核调用应用程序函数fn。当fn返回时,应用程序应该从中断的地方继续。
在xv6中,tick是一个任意的时间单位,由硬件定时器产生中断的频率决定。
如果应用程序调用sigalarm(0, 0),内核停止该系统调用。
test0: invoke handler
准备工作:
- You’ll need to modify the Makefile to cause alarmtest.c to be compiled as an xv6 user program.
- The right declarations to put in user/user.h are:
int sigalarm(int ticks, void (*handler)());
int sigreturn(void); - Update user/usys.pl (which generates user/usys.S), kernel/syscall.h, and kernel/syscall.c to allow alarmtest to invoke the sigalarm and sigreturn system calls.
- For now, your sys_sigreturn should just return zero.
主要工作:
- Your sys_sigalarm() should store the alarm interval and the pointer to the handler function in new fields in the proc structure (in kernel/proc.h).
【在proc.h中添加新的数据结构,由sys_sigalarm()存储 alarm间隔 & 指向handler函数的指针】 - You’ll need to keep track of how many ticks have passed since the last call (or are left until the next call) to a process’s alarm handler; you’ll need a new field in struct proc for this too.
【对进程的每一个alarm handler,维护当前经过了多少ticks(或者距离下次调用还有多少ticks),维护proc结构体内部新定义的数据结构实现】 - You can initialize proc fields in allocproc() in proc.c.
// [kernel/proc.h]
// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
//[store the alarm interval and the pointer to the handler function in new fields in the proc structure]
int alarm_interval; // alarm interval
uint64 pointer; // pointer to the handler function
int ticks; // ticks passed
struct trapframe alarm_trapframe;
int alarm_flag; // a flag for alarm
};
// [kernel/proc.c]
static struct proc*
allocproc(void)
{
struct proc *p;
......
// [Lab3:Trap:alarm] initialize proc fields addings
p->alarm_interval = 0;
p->pointer = 0;
p->ticks = 0;
p->alarm_flag = 1; // admit alarm
return p;
}
- Every tick, the hardware clock forces an interrupt, which is handled in usertrap() in kernel/trap.c.
【每个tick时间,硬件产生一次中断,由 kernel/trap.c 中的 usertrap() 函数处理】 - You only want to manipulate a process’s alarm ticks if there’s a timer interrupt; you want something like
if(which_dev == 2) … 【usertrap(): 通过 if 语句判断 timer interrupt 并特殊处理】 - Only invoke the alarm function if the process has a timer outstanding. Note that the address of the user’s alarm function might be 0 (e.g., in user/alarmtest.asm, periodic is at address 0).
- You’ll need to modify usertrap() so that when a process’s alarm interval expires, the user process executes the handler function. When a trap on the RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?
(sepc, 当系统调用发生时PC存放到此处,以便系统调用返回时能从下一条指令开始执行sret: sepc -> pc)
【6.S081 & 操作系统内核】这篇文章 “test1&test2” 部分有介绍:
(在handler函数内部的最后会调用sigreturn系统调用,随后在syscall函数执行完sys_sigreturn之后,跳转到usertrapret,usertrapret中会在spec寄存器中存入用户进程被打断的指令的地址,usertrapret最后会调用userret函数,在userret中会使用alarm_trapframe恢复用户寄存器,并且在执行完最后的sret之后,安全返回用户进程之前的状态与位置)
// [kernel/trap.c]
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
void
usertrap(void)
{
......
// a timer interrupt.
if(which_dev == 2){
if(p->alarm_interval > 0){ // 进程存在interval机制:
//(要求interval > 0, 由题目中描述, interval = 0时要求停止生成定期警报)
p->ticks++; // ticks数量+1
if(p->ticks == p->alarm_interval && p->alarm_flag){
// 进程到达 interval 周期且允许中断并执行 handler function:
p->alarm_flag = 0; // 【禁止新的中断】
p->ticks = 0; // 重设ticks【handler function返回后ticks从0开始计数】
memmove(&(p->alarm_trapframe), p->trapframe, sizeof(struct trapframe));
// 【存储进程当前栈帧信息】
p->trapframe->epc = p->pointer;
// 【进程的程序计数器(下一条指令所在单元的地址)设为 handler function 的函数指针】
}
}
yield();
}
usertrapret();
}
// [kernel/sysproc.c]
// 系统调用sigalarm:
uint64
sys_sigalarm(void)
{
// 收到系统调用参数, 存储在进程状态中
int interval;
uint64 pointer;
// 从s0寄存器中取参数, 拿到间隔ticks时长, 存储为interval
if(argint(0, &interval) < 0)
return -1;
/* 检查到最后发现是argaddr函数写错了(pointer有错误)
第一个参数应该是1 */
// 从s1寄存器中取参数, 拿到handler function函数指针, 存储为pointer)
if(argaddr(1, &pointer) < 0)
return -1;
struct proc *p = myproc();
p->alarm_interval = interval; //【interval > 0, 在 trap.c 中处理时钟中断时进行更新和检查】
p->pointer = pointer;
return 0;
}
test1/test2(): resume interrupted code
user alarm handlers are required to call the sigreturn system call when they have finished.
【上述任务还没有完成与sigalarm相配合的sigreturn系统调用 ,只是简单地让sigreturn函数返回0】
This means that you can add code to usertrap and sys_sigreturn that cooperate to cause the user process to resume properly after it has handled the alarm.
- 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).
【我选择的策略是,在调用handler function前,将进程的整个p->trapframe进行复制保存】 - Prevent re-entrant calls to the handler----if a handler hasn’t returned yet, the kernel shouldn’t call it again. test2 tests this.
【proc.h中添加flag设置是否允许interval中断调用handler function】
【注意到任何handler function的最后语句必须调用sigreturn进行返回,返回时允许下一次调用】
// [kernel/sysproc.c]
uint64
sys_sigreturn(void)
{
struct proc *p = myproc();
if(p->alarm_flag == 0){ // 如果当前禁止中断
memmove(p->trapframe, &(p->alarm_trapframe), sizeof(struct trapframe));
// 【重新加载trapframe】 memmove(dst, src, size)
p->alarm_flag = 1; // 设置为允许中断
}
return 0;
}