在开始做lab之前务必弄清楚
- 所谓的系统调用,本质上就是内核态和用户态之间的切换
- 内核态和用户态的区别本质上就是一些关键属性的区别,比如页表
而他们的运行方式都一样,就是取指执行,没有魔法
只不过内核态和用户态的执行不在一个体系上,所以切换会比较复杂,但这些切换也就只是修改一些关键属性- 进程的运行本质上就是一些值不断的变化,trapframe中的值完全可以保存并复现一个进程执行到了哪里加粗样式
kernel/trampoline.S
:在用户态和内核态进行切换的汇编代码kernel/trap.c
:处理陷入的代码有一个文件user/call.c
,通过make fs.img
可以编译并生成一个可读性很高的汇编代码user/call.asm
。
阅读这个汇编代码中的函数g
,f
,main
,并回答以下问题
1. a0-a7,a2
2. 被优化了?
3. 630
4. 38
5. He110 World,第一个是直接输出十六进制的表示,第二个是将十六进制的每个字节看做一个字符,并且是小端法
将i改成0x726c6400,57616不用改
6. 随机值,因为相当于调用printf的时候没有给出寄存器a2的值,那么这时候就会根据a2的值随机出现答案
如果能够清楚在错误发生之前的一系列函数调用,那么对debug很有帮助
在kernel/printf.c
中实现一个backtrace()
函数
是kernel
文件夹里的,不是user
文件夹里,user
文件夹里也有一个printf.c
在sys_sleep
中插入对backtrace
函数的调用
运行bttest
,它会调用sys_sleep
,你的输出应该是
backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898
在bttest
结束之后,在你的终端中运行
$ addr2line -e kernel/kernel
0x0000000080002de2
0x0000000080002f4a
0x0000000080002bfc
Ctrl-D
你会看到
kernel/sysproc.c:74
kernel/syscall.c:224
kernel/trap.c:85
编译器会给每个栈帧一个frame pointer
,你应该使用这个指针去遍历栈并且打印每个栈帧中保存的返回地址
记得在kernel/defs.h
中声明你的backtrace
函数
GCC编译器将栈指针存放在当前执行函数的s0
寄存器中,将下面这个函数添加到kernel/riscv.h
中,并且在backtrace
函数中调用它
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
返回地址位于fp-8
的固定位置
被保存的栈帧地址位于fp-16
的固定位置
你可以使用这两个宏,有助于终止backtrace
的循环
PGROUNDDOWN(fp)
计算栈顶的位置
PGROUNDUP(fp)
计算栈底的位置
如果你的backtrace
工作了,可以在kernel/printf.c
的panic
调用它,这样一旦内核出错了,你就可以看到调用栈
一行一行地打印函数调用的地址,思路比较简单
r_fp
函数获得当前的fp
具体实现上
void backtrace() {
printf("backtrace:\n");
uint64 fp = r_fp();
uint64 up_edge = PGROUNDUP(fp);
while (fp < up_edge) {
printf("%p\n", *(uint64 *)(fp - 8));
fp = *(uint64 *)(fp - 16);
}
}
在这个练习中,你将给xv6增加一个特性,即会根据CPU时间周期性地alert一个进程
如果你的解答可以通过
alarmtest
和usertests
你应该添加一个新的系统调用sigalarm(interval,handler)
如果一个应用调用了sigalarm(n,fn)
那么在这个应用消耗了n ticks个CPU时间后,内核会调用函数fn
当fn
返回后,应用会回到被打断的地方继续执行
如果一个应用调用了sigalarm(0,0)
,内核应该暂停生成周期性的alarm
在你的xv6文件中有一个文件叫user/alarmtest.c
,你需要将其加入到Makefile
只有你正确添加了sigalarm
和sigreturn
系统调用之后,才可以正确编译
首先修改内核,跳转到用户空间的alarm handler
这将让test0
打印alarm!
现在还不用管打印之后发生了什么,你的程序在打印之后崩了就行
你需要去修改Makefile,让它去编译alarmtest.c
成为用户程序
在user/user.h
中正确的声明应该如下
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
更新user/usys.pl
,kernel/syscall.h
和kernel/syscall.c
使得alarmtest
能够调用sigalarm
和sigreturn
系统调用
至此你的sys_sigreturn
应该只返回0
你的sys_sigalarm
应该存下alarm的间隔和处理函数的指针到proc
结构体的新的区域
你需要去跟踪自从上次调用alarm的处理函数到现在已经过去了多少ticks
这也需要在struct proc
中增加一个新的字段,你可以在proc.c 的 allocproc()
中初始化这个字段
每次来一个tick
,都会在kernel/trap.c
的usertrap
中被处理
你只需要在有时钟中断的时候操作
只需要在一个进程有 t i m e r o u t s t a n d i n g timer\ outstanding timer outstanding的时候调用alarm函数
小心函数地址为0的情况,因为函数地址可以为0,我是傻逼!
你将需要去修改usertrap
函数,使得当一个进程的alarm时间间隔到期时,用户进程执行处理函数
当一个陷入返回到用户空间时,是什么决定着用户空间代码继续执行的指令地址?
如果你运行make CPUS=1 qemu-gdb
,会使用用gdb查看trap的时候更容易
如果alarmtest
打印了alarm!
,你就成功了
首先根据它的提示去各个文件中把系统调用的声明给弄好
然后在struct proc
中增加如下字段,其中关键在于uint64 handler
,它是函数指针,不过终究也就是个指针,因此可以用uint64
来表示
int cur_ticks;
uint64 handler;
int ticks;
在sysprorc.c
中完成sys_sigalarm
和sys_sigreturn
uint64
sys_sigalarm(void) {
struct proc *p;
p = myproc();
argint(0, &p->ticks);
argaddr(1, &p->handler);
p->cur_ticks = 0;
return 0;
}
uint64
sys_sigreturn(void) {
return 0;
}
最后在trap.c
中完成调用
注意了,函数指针可能是0,所以用ticks是否为0判断是否需要计数
if (which_dev == 2) {
if (p->ticks != 0) {
p->cur_ticks++;
if (p->cur_ticks == p->ticks) {
p->cur_ticks = 0;
p->trapframe->epc = p->handler;
}
}
yield();
}
这里的实现是如果当前已经到了第n个时钟中断,那么会先去中断,等下一次获得cpu使用权时,再去执行handler操作
我试了一下在放弃cpu之前直接
p->handler()
,结果不允许估计是因为地址的原因,现在可是在内核态,怎么可能能够通过这个用户态的虚拟地址来执行
所以,只能等到这个进程再次获得CPU并且回归用户态用,就会用epc这个参数来初始化pc,就会从这里开始执行了
你需要在执行完
alarm
处理函数之后,正确返回程序被中断的地方,并且各种寄存器的状态也要不变xv6已经为实现提供了一种思路,即每个alarm处理函数的最后都有一个
alarmreturn
函数,你可以通过usertrap
和sys_sigreturn
合作来完成用户进程的恢复
struct proc
中保存足够多的状态,使得你可以在sigreturn
中恢复到了这一步,必须要先搞清楚系统调用的过程中对于状态的保存和恢复了
uservec
保存了各种常用的寄存器usertrap
将返回的pc地址存到了p->trapframe->epc
usertrapret
通过p->trapframe->epc
恢复pcuserret
恢复各种寄存器首先,我们希望在时钟中断之后,这个进程被调度回来的时候,去执行alarm处理函数,因此我们需要在时钟中断的处理中,将epc
置为处理函数的地址,这样就完成了task0
但是如果只是这样的话,这个进程在执行完alarm处理函数之后并不能正确的返回需要执行的地方。那如何正确的返回呢?
可以发现,alarm处理函数的最后一句通常是alarmreturn
,这是一个系统调用!如果我们能够在这个系统调用返回之前将trapframe(因为trapframe包括了所有返回用户态需要的信息,所以我们只需要这个就行了)变成在时钟中断处理之前的样子,那么就可以借用alarmreturn
这个系统调用的返回操作回到我们想去的地方
而需要注意的是,如果已经执行了alarm处理函数,那此时的trapframe肯定是不行的,因为包括pc和各种通用寄存器都被破坏的,那哪个时间点的trapframe可以呢?
答案是刚进入if (which_dev == 2)
的时候,想一想,如果我们不需要搞这个什么alarm,那么等之后这个进程再次被调度到cpu之后,那不就是继续正常执行吗?说明这个时间点的trapframe可以通过任何一个系统调用的返回过程使得进程执行到继续执行的地方
不过我们也没必要每次进入这里都保存了,只需要在确定了会去执行alarm处理函数的时候保存,在alarmreturn
中恢复即可
除此之外,题目还要求如果已经有一个alarm处理函数在执行,那么其他的必须等待,因此额外增加一个变量代表是否有在执行
具体实现如下
首先给proc结构体增加如下变量
struct trapframe *alarm_tf;
int is_runing;
并且在进程初始化和终止的时候对这两个变量进行处理
// 进程初始化,这里主要是防止申请不成功,那就学着已有的代码对进程进行销毁
p->alarm_tf = (struct trapframe *)kalloc();
if (p->alarm_tf == 0) {
freeproc(p);
release(&p->lock);
return 0;
}
p->is_runing = 0;
// 进程结束
kfree(p->alarm_tf);
trap.c
中的代码进行如下更新
p->cur_ticks >= p->ticks
,这里变成大于等于,是为了保证现在可以等,但之后如果没有正在运行的了,那就可以进入alarm处理函数的流程。如果和之前一样是等于号的好,那可能就错过了 if (which_dev == 2) {
if (p->ticks != 0) {
p->cur_ticks++;
if (p->cur_ticks >= p->ticks && p->is_runing == 0) {
memmove(p->alarm_tf, p->trapframe, sizeof(struct trapframe));
p->is_runing = 1;
p->cur_ticks = 0;
p->trapframe->epc = (uint64)p->handler;
}
}
yield();
}
最后修改sys_sigreturn
函数
uint64
sys_sigreturn(void) {
struct proc *p;
p = myproc();
memmove(p->trapframe, p->alarm_tf, sizeof(struct trapframe));
p->is_runing = 0;
return 0;
}