前言
本篇博客是6.S081课程的第四次Lab分析,做这次Lab之前,需要理解Xv6操作系统系统调用时用户空间和内核空间之间的切换过程,还需要清楚函数嵌套调用时运行时栈的栈帧的结构。Lab的地址:https://pdos.csail.mit.edu/6.828/2021/labs/traps.html
RISC-V assembly
Xv6操作系统的系统调用通过trampoline来实现用户空间和内核空间之间的跳转,trampoline页面是在内核地址空间和用户地址空间均有相同的地址映射,也就是说:trampoline页面在物理地址空间只有一份 (且是可读可执行的代码),而在内核地址空间和用户地址空间中,均将MAXVA - PGSIZE
的地址(也就是第一个页面)映射到了这段代码。
这样做的好处就是,在执行trampoline中的代码时,执行内核页表和用户进程页表不会使得程序崩溃,这也就是为什么这个页面叫做trampoline(蹦床)的原因了。
为了理解Xv6的代码,需要了解部分risc-v的汇编,如果不深究的话,我认为只需要知道函数调用参数传递通过的是a0-a7
寄存器,函数调用返回值通过a0
寄存器返回。其他很多指令和x86汇编类似。
Backtrace
这道题要求打印出当前函数调用栈的情况,这里需要知道在Xv6中,函数调用的栈帧如下图所示:
我们知道函数的调用栈是向低地址 增长,在每一个函数调用的栈帧中,首先是函数调用的返回地址,然后是一个指向上一个栈帧的指针fp
,然后是一些需要被保护的寄存器(callee saved register)。这个栈帧和别的操作系统可能会有些区别,比如我之前有一篇文章说函数调用过程:机器级解释函数调用过程 。
因此这里为了将函数调用栈所有的返回地址Return Address
打印出来,只需要遍历当前的栈就行了。
首先需要在kernel/riscv.h
中加入获取当前栈指针fp
的函数:
1 2 3 4 5 6 7 8 9 static inline uint64r_fp () { uint64 x; asm volatile ("mv %0, s0" : "=r" (x)) ; return x; }
然后在kernel/defs.h
中添加backtrace
函数声明:
最重要的是在kernel/printf.c
中添加函数实现,系统级编程中存在大量的强制类型转换,需要头脑清醒的知道自己在干什么!这里是遍历当前的栈,通过fp
指针在栈帧之间跳转,并打印出栈帧中的函数返回地址。
1 2 3 4 5 6 7 8 9 10 void backtrace (void ) { printf ("backtrace:\n" ); uint64 fp = r_fp(); while (fp < PGROUNDUP(fp)) { printf ("%p\n" , *(uint64*)(fp-8 )); fp = *(uint64*)(fp - 16 ); } }
最后不要忘了在kernel/sysproc.c
中的sys_sleep
函数中调用backtrace
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 uint64 sys_sleep (void ) { int n; uint ticks0; if (argint(0 , &n) < 0 ) return -1 ; acquire(&tickslock); ticks0 = ticks; while (ticks - ticks0 < n){ if (myproc()->killed){ release(&tickslock); return -1 ; } sleep(&ticks, &tickslock); } release(&tickslock); backtrace(); return 0 ; }
Alarm
我们知道,操作系统每隔一个时间片(tick)就会触发定时器中断,从而操作系统重新获得对CPU的控制权,这样可以防止恶意程序一直占着CPU不释放。这道题要求我们实现一个简易的周期执行函数。
我们通过系统调用sigalarm(interval, handler)
通知内核,内核每interval
个ticks之后调用handler
函数。 当程序调用sigalarm(0, 0)
时,内核需要终止上述行为。
此外,需要要求handler
函数在返回时调用sigreturn()
系统调用,我们需要借助这一点来实现断点的恢复,也就是恢复到执行handler
之前的地方。
最后还要避免handler
函数的重入 ,因此我们也需要采取相应的措施来判断当前是否在执行handler
函数。
首先添加系统调用,在user/usys.pl
中加入:
1 2 entry("sigalarm" ); entry("sigreturn" );
在user/user.h
中加入函数声明:
1 2 3 int sigalarm (int ticks, void (*handler)()) ;int sigreturn (void ) ;
在kernel/syscall.h
中加入系统调用号:
1 2 #define SYS_sigalarm 22 #define SYS_sigreturn 23
在kernel/syscall.c
中加入以下代码:
1 2 3 4 5 extern uint64 sys_sigalarm (void ) ;extern uint64 sys_sigreturn (void ) ;[SYS_sigalarm] sys_sigalarm, [SYS_sigreturn] sys_sigreturn,
为了实现上面的功能,需要在进程结构中存储相关的状态,在kernel/proc.h
中加入以下5个变量:
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 26 27 28 29 30 struct proc { struct spinlock lock ; enum procstate state ; void *chan; int killed; int xstate; int pid; struct proc *parent ; uint64 kstack; uint64 sz; pagetable_t pagetable; struct trapframe *trapframe ; struct context context ; struct file *ofile [NOFILE ]; struct inode *cwd ; char name[16 ]; int alarm_interval; void (*alarm_handler)(); int ticks; struct trapframe *saved_trapframe ; int alarm_handling; };
alarm_interval
和alarm_handler
存储用户sigalarm
调用的参数,ticks
记录当前进程使用的时间片总数;saved_trapframe
用于在执行alarm_handler
函数之前,保存当前的寄存器,便于后面断点恢复。最后一个变量alarm_handling
用于标志当前是否正在执行alarm_handler
,以防止函数重入。
不要忘了在进程初始化时对这些变量初始化,在kernel/proc.c
中的allocproc
函数中加入:
1 2 3 4 5 6 7 8 9 10 p->alarm_handler = 0 ; p->alarm_interval = 0 ; p->ticks = 0 ; p->alarm_handling = 0 ; if ((p->saved_trapframe = (struct trapframe *)kalloc()) == 0 ) { freeproc(p); release(&p->lock); return 0 ; }
在kernel/proc.c
的preeproc
函数中加入以下代码以销毁saved_trapframe:
1 2 if (p->saved_trapframe) kfree((void *)p->saved_trapframe);
最后修改kernel/trap.c
中的usertrap
函数:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 void usertrap (void ) { int which_dev = 0 ; if ((r_sstatus() & SSTATUS_SPP) != 0 ) panic("usertrap: not from user mode" ); w_stvec((uint64)kernelvec); struct proc *p = myproc(); p->trapframe->epc = r_sepc(); if (r_scause() == 8 ){ if (p->killed) exit (-1 ); p->trapframe->epc += 4 ; intr_on(); syscall(); } else if ((which_dev = devintr()) != 0 ){ if (which_dev == 2 ) { p->ticks ++; if (p->alarm_interval != 0 && p->ticks % p->alarm_interval == 0 ) { if (p->alarm_handling == 0 ) { p->alarm_handling = 1 ; cpytrapframe(p->saved_trapframe, p->trapframe); p->trapframe->epc = (uint64)p->alarm_handler; } } } } 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 ); if (which_dev == 2 ) yield(); usertrapret(); }
基本逻辑是,首先每次定时器中断我们都让p->ticks ++
,当用户设置了定时器任务并到期之后,设置p->alarm_handling
并将trapframe
备份至saved_trapframe
,然后将epc
寄存器的值改为用户提供的handler
,这样系统调用返回的时候,就会去执行用户提供的handler
函数了。
上面的cpytrapframe
函数实现如下,就是将trapframe
中的寄存器拷贝一份:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 void cpytrapframe (struct trapframe* dst, struct trapframe* src) { dst->kernel_hartid = src->kernel_hartid; dst->kernel_satp = src->kernel_satp; dst->kernel_sp = src->kernel_sp; dst->kernel_trap = src->kernel_trap; dst->a0 = src->a0; dst->a1 = src->a1; dst->a2= src->a2; dst->a3 = src->a3; dst->a4 = src->a4; dst->a5 = src->a5; dst->a6 = src->a6; dst->a7 = src->a7; dst->epc = src->epc; dst->ra = src->ra; dst->sp = src->sp; dst->gp = src->gp; dst->tp = src->tp; dst->t0 = src->t0; dst->t1 = src->t1; dst->t2 = src->t2; dst->s0 = src->s0; dst->s1 = src->s1; dst->s2 = src->s2; dst->s3 = src->s3; dst->s4 = src->s4; dst->s5 = src->s5; dst->s6 = src->s6; dst->s7 = src->s7; dst->s8 = src->s8; dst->s9 = src->s9; dst->s10 = src->s10; dst->s11 = src->s11; dst->t3 = src->t3; dst->t4 = src->t4; dst->t5 = src->t5; dst->t6 = src->t6; }
最后补上两个系统调用的实现,在kernel/sysproc.c
中加入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 uint64 sys_sigalarm (void ) { int interval; uint64 handler; if (argint(0 , &interval) < 0 ) return -1 ; if (argaddr(1 , &handler) < 0 ) return -1 ; struct proc * proc = myproc(); proc->alarm_interval = interval; proc->alarm_handler = (void (*)(void ))handler; return 0 ; } uint64 sys_sigreturn (void ) { struct proc * p = myproc(); cpytrapframe(p->trapframe, p->saved_trapframe); p->alarm_handling = 0 ; return 0 ; }
其中sigalarm
只是简单的将用户的参数保存到proc中,sigreturn
将trapframe恢复,然后将handler执行标志赋0。
测试结果
2022/6/14日测试通过
参考文献
https://pdos.csail.mit.edu/6.828/2021/schedule.html