Lab4: traps 主要是想让我们熟悉 xv6 系统调用时进出 kernel 时的细节
在开始实验之前,一定要阅读 xv6-6.S081 的第四章节 Traps and device drivers 及
kernel/trampoline.S
和kernel/trap.c
的相关代码
主要是想让我们熟悉 RISC-V 的栈帧结构,包括如何入栈(从高地址往低地址去),每个函数调用帧内如何布局(返回地址,上一函数调用的帧地址以及寄存器的值)
首先需要了解 RISC-V 的栈帧结构,如下图,
地址从高到低扩展,fp 指向函数帧的首地址,sp 是栈指针,与函数帧无关。fp-8 指向返回地址(对应图中 Return Address ),fp-16 指向上一函数帧的首地址(对应图中 To Prev. Frame )
在清楚上述理论知识之后就可以动手完成 Backtrace 遍历工作了。首先需要在 kernel/defs.h
中添加声明,
// printf.c
...
void backtrace(void);
接着需要将读取 fp 的内联代码添加到 kernel/riscv.h
中,
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
然后就可以开始编写 kernel/printf.c:backtrace()
代码,
void
backtrace(void)
{
uint64 fp = r_fp();
uint64 top = PGROUNDUP(fp);
printf("backtrace:\n");
while(fp < top) {
uint64 retaddr = fp-8;
printf("%p\n", *(uint64*)retaddr);
fp = *((uint64*)(fp-16));
}
}
Lab: Backtrace 没有什么难点,就是想考察我们如何顺着第一个函数帧,揪出所有函数帧。需要注意,要善于使用课程代码提供的功能函数或宏,PGROUNDUP(fp)
和 PGROUNDDWON(fp)
可以通俗理解为向上或向下取整,即将小于 4KB 的值变为 4KB 或 0
手动进入 qemu ,
make qemu
$bttests
进入 xv6 来查看输出结果
主要是想让我们在 kernel 中添加一个定时功能,在时钟中断( Timer interrupts )次数达到规定数量后触发用户态的 alarm 回调函数,以示告警目的
首先需要了解 Lab4: traps 实验主页 中关于 sigalarm(interval, handler)
和 sigreturn()
的大致功能
其实,sigalarm(interval, handler)
很简单,需要提前知道的是 CPU 只要发生时钟中断就一定会进入内核(具体为跳入 kernel/trap.c:usertrap()
) ,Lab4: traps 实验主页 的原话,
Every tick, the hardware clock forces an interrupt, which is handled in
usertrap()
inkernel/trap.c
.
为了在时钟中断次数达到规定数量( interval
)后能够顺利触发用户态的 alarm 回调函数,struct proc*
需要有一个字段 ticks
用来记录发生了多少次时钟中断(具体表现为每发生一次时钟中断,ticks
加1)
要注意,kernel 会一直监视 interval
,一直在捕捉,一直在判断是否要警告用户层(一直体现在:只要进入 usertrap()
就会执行监视任务,而进入 usertrap()
这件事是一直都在发生的,因为每个时钟中断都会进入 kernel )
并且将 interval
的正负来作为判断依据,当调用 sigalarm(2, fn)
时,kernel 会以两个时钟中断单位为间隔,每过两个时间点就会回调一次 fn
;而当调用 sigalarm(0, fn)
时,kernel 就会忽略掉 alarm 任务(具体见接下来关于时钟中断的代码 kernel/trap.c:usertrap()
)
另外,为了顺利完成编译工作,需要在 Makefile
中添加 alarmtest
相关语句,具体如下,
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
$U/_grep\
$U/_init\
$U/_kill\
$U/_ln\
$U/_ls\
$U/_mkdir\
$U/_rm\
$U/_sh\
$U/_stressfs\
$U/_usertests\
$U/_grind\
$U/_wc\
$U/_zombie\
$U/_alarmtest
...
按照提示一步一步来,第一步在 Makefile
添加 alarmtest(如上);第二步在 user/user.h
中添加 sigalarm()
和 sigreturn()
声明(见 Lab2: system calls 如何添加系统调用),
int sigalarm(int, void (*handler)());
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
};
完成了上述基本配置后开始着手完善 kernel/sysproc.c:sys_sigalarm()
和 kernel/sysproc.c:sys_sigreturn()
具体业务逻辑
根据 test0 给出的提示,暂时还不需要考虑 sys_sigreturn()
的业务逻辑,直接返回 0 值就好,所以代码暂时写成这样就可以,
uint64
sys_sigreturn(void)
{
return 0;
}
sys_sigalarm()
最重要的任务就是能够准确获取用户态传来的参数,时间间隔 interval
( a0 寄存器)和回调函数 handler
( a1 寄存器),通过 argint()
和 argaddr()
读取,
uint64
sys_sigalarm(void)
{
int interval;
uint64 handler;
struct proc* p;
if(argint(0, &interval)<0 || argaddr(1, &handler)<0)
return -1;
p = myproc();
p->interval = interval;
p->handler = handler;
return 0;
}
至此,已经完成了系统调用的大致框架。将目光转移到 kernel/trap.c:usertrap()
上,我们需要通过进程的 ticks
字段来记录该进程从上次完成回调函数之后已经运行了多少个时钟中断, Lab4: traps 实验主页 的原话,
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;
在 kernel/proc.c:allocproc()
中完善具体细节,需要将 ticks
字段置 0 ,从 0 开始计时,暂时只需初始化计时器,
...
found:
p->pid = allocpid();
/** 计时器默认从0开始 */
p->ticks = 0;
...
当每个时钟中断发生时,xv6 都会从用户态进入内核(通过 kernle/trap.c:usertrap()
),在 usertrap()
中可以看到课程提供的框架中包含了时钟中断的业务代码,需要在其中添加上述的业务逻辑,
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
if(p->interval <= 0)
goto yield;
p->ticks++;
if(p->ticks == p->interval)
p->trapframe->epc = p->handler;
yield:
yield();
}
...
当进程的 interval
小于 0 时,直接忽略 alarm 任务(对应 sigalarm(0, fn)
的具体作用);反之则累积时钟中断次数,当时钟中断次数达到规定数量后触发回调函数。具体表现为将回调函数的地址存档在 trapframe 的 epc 中,当从内核返回到用户态之后,xv6 会将 trapframe 的 epc 值赋值给 pc,即 CPU 将执行从回调函数的第一条指令处开始执行
至此完成了 test0 的相关工作
该环节皆在让我们完善从内核返回用户态的相关代码,这不仅仅涉及到 kernel/sysproc.c:sigreturn()
,还有 kernel/proc.c:allocproc()
和 kernel/proc.c:freeproc()
我们需要在 kernel/proc.c:allocproc()
中为每个进程再分配一个 trapframe,用于保存执行回调函数之前的寄存器状态,
static struct proc*
allocproc(void)
{
...
found:
p->pid = allocpid();
/** 计时器默认从0开始 */
p->ticks = 0;
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);
return 0;
}
/** 分配一块内存用于恢复执行handler之前的现场情况 */
if((p->trapframe2 = (struct trapframe *)kalloc()) == 0){
release(&p->lock);
return 0;
}
...
return p;
}
相应地,需要在 kernel/proc.c:freeproc()
中释放 trapframe,对应的代码如下,
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
/** 释放trapframe */
if(p->trapframe2)
kfree((void*)p->trapframe2);
p->trapframe = 0;
p->trapframe2 = 0;
...
}
同时在 kernel/trap.c:usertrap()
中添加相应的存档寄存器代码,
void
usertrap(void)
{
...
if(p->killed)
exit(-1);
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
if(p->interval <= 0)
goto yield;
p->ticks++;
if(p->ticks == p->interval) {
*p->trapframe2 = *p->trapframe;
p->trapframe->epc = p->handler;
}
yield:
yield();
}
...
}
完成存档之后,在返回时顺理成章能够获取到执行回调函数之前的状态,
uint64
sys_sigreturn(void)
{
struct proc* p;
p = myproc();
*p->trapframe = *p->trapframe2;
/** 为下一次回调handler函数做准备 */
p->ticks = 0;
return 0;
}
需要将进程的 ticks
置 0,标明此次回调函数已经结束,下一次回调函数将会新起炉灶,重新计数
至此,完成了 Lab: Alarm 的所有环节
手动进入 qemu ,
make qemu
$alarmtests
进入 xv6 来查看输出结果