syscall代码在这里。不行了,今天已经快飙到极限了,写了5篇博客了……药不能停,再来一篇!
写在前面
syscall即system call,系统调用。完成本次实验可以让我们了解诸如:fork()
、write()
、exec()
之类的函数究竟是如何调用的——这些函数在user文件夹中真的找不到原型……
和往常一样,我的博客OS实验xv6 6.S081 开坑中给出了一些有用的参考资料供大家参考。
实验介绍
MIT官方指导书献上。
本次实验内容主要有三个:
- 回答给出的关于RISC-V汇编的几个问题;
- 实现Uthread: switching between threads,即线程上下文的转换;
- 实现Syscall:Alarm,即熟悉系统调用方式;
开始!
回答问题
阅读user/call.c与其汇编代码call.asm,回答下述问题:
1. 哪个寄存器用于函数传参?对于本例,那个寄存器将13传给了printf
2. f函数和g函数分别在哪里被调用了?
3. printf函数在哪个位置?
4. 执行完3a: 00000097 auipc ra,0x0后,ra中保存了什么?
通过阅读源码,我们可以给出
-
register a2
24: 4635 li a2,13
-
f: line 26 ; g: line 26
printf("%d %d\n", f(8)+1, 13); 24: 4635 li a2,13 26: 45b1 li a1,12
可以看到,f(8)+1是一个inline function,于是在
26: 45b1 li a1,12
直接调用了f与g,将值算了出来(为12); -
5c8
34: 598080e7 jalr 1432(ra) # 5c8 <printf>
-
3a
3a: 00000097 auipc ra,0x0
将当前pc的值加上0x0送入ra,故此时ra为3a。
Uthread: switching between threads
实验的任务是修改uthread.c
的代码,使之能够支持进程的切换。我们首先应该完成thread_create
来正确地创建线程;其次,应该在uthread_Switch.S
中实现进程的切换(主要是涉及的操作是保存哪些寄存器,修改哪些寄存器)
乍一看,这个实验需要写汇编,好困难,事实上我们可以参考进程上下文切换的方式来完成线程上下文的切换。在proc.h
中,对进程上下文的定义如下:
struct context {
uint64 ra;
uint64 sp;
// callee-saved
uint64 s0;
uint64 s1;
uint64 s2;
uint64 s3;
uint64 s4;
uint64 s5;
uint64 s6;
uint64 s7;
uint64 s8;
uint64 s9;
uint64 s10;
uint64 s11;
};
其切换上下文的汇编代码如下:
# Context switch
#
# void swtch(struct context *old, struct context *new);
#
# Save current registers in old. Load from new.
.globl swtch
swtch:
sd ra, 0(a0)
sd sp, 8(a0)
sd s0, 16(a0)
sd s1, 24(a0)
sd s2, 32(a0)
sd s3, 40(a0)
sd s4, 48(a0)
sd s5, 56(a0)
sd s6, 64(a0)
sd s7, 72(a0)
sd s8, 80(a0)
sd s9, 88(a0)
sd s10, 96(a0)
sd s11, 104(a0)
ld ra, 0(a1)
ld sp, 8(a1)
ld s0, 16(a1)
ld s1, 24(a1)
ld s2, 32(a1)
ld s3, 40(a1)
ld s4, 48(a1)
ld s5, 56(a1)
ld s6, 64(a1)
ld s7, 72(a1)
ld s8, 80(a1)
ld s9, 88(a1)
ld s10, 96(a1)
ld s11, 104(a1)
ret
于是我们如法炮制,首先修改uthread.c
中的thread
定义如下:
struct thread {
/** My Implementation */
uint64 ra;
uint64 sp;
// callee registers
/** thread_switch needs to save/restore only the callee-save registers. */
uint64 s0;
uint64 s1;
uint64 s2;
uint64 s3;
uint64 s4;
uint64 s5;
uint64 s6;
uint64 s7;
uint64 s8;
uint64 s9;
uint64 s10;
uint64 s11;
char stack[STACK_SIZE]; /* the thread's stack */
int state; /* FREE, RUNNING, RUNNABLE */
};
然后模仿进程上下文的切换,在uthread_Switch.S
中补充线程上下文的切换:
.text
/* Switch from current_thread to next thread_thread, and make
* next_thread the current_thread. Use t0 as a temporary register,
* which should be caller saved. */
.globl thread_switch
thread_switch:
/* YOUR CODE HERE */
sd ra, 0(a0)
sd sp, 8(a0)
sd s0, 16(a0)
sd s1, 24(a0)
sd s2, 32(a0)
sd s3, 40(a0)
sd s4, 48(a0)
sd s5, 56(a0)
sd s6, 64(a0)
sd s7, 72(a0)
sd s8, 80(a0)
sd s9, 88(a0)
sd s10, 96(a0)
sd s11, 104(a0)
ld ra, 0(a1)
ld sp, 8(a1)
ld s0, 16(a1)
ld s1, 24(a1)
ld s2, 32(a1)
ld s3, 40(a1)
ld s4, 48(a1)
ld s5, 56(a1)
ld s6, 64(a1)
ld s7, 72(a1)
ld s8, 80(a1)
ld s9, 88(a1)
ld s10, 96(a1)
ld s11, 104(a1)
ret /* return to ra */
接下来,修改thread_create
,使之能够记录线程的返回地址ra与栈地址sp。其中,返回地址意味着当切换线程时,线程应该返回到什么哪个地址。这里应该是传入的函数的入口func
。代码如下:
void
thread_create(void (*func)())
{
struct thread *t;
for (t = all_thread; t < all_thread + MAX_THREAD; t++) {
if (t->state == FREE) break;
}
t->state = RUNNABLE;
// YOUR CODE HERE
t->ra = (uint64)func;
/** 栈是倒着生长 —— addi sp, sp, -STACK_SIZE */
t->sp = (uint64)(t->stack + STACK_SIZE);
}
最后,在thread_schedule
中添加thread_switch
调用:
void
thread_schedule(void)
{
struct thread *t, *next_thread;
/* Find another runnable thread. */
next_thread = 0;
t = current_thread + 1;
for(int i = 0; i < MAX_THREAD; i++){
if(t >= all_thread + MAX_THREAD)
t = all_thread;
if(t->state == RUNNABLE) {
next_thread = t;
break;
}
t = t + 1;
}
if (next_thread == 0) {
printf("thread_schedule: no runnable threads\n");
exit(-1);
}
if (current_thread != next_thread) { /* switch threads? */
next_thread->state = RUNNING;
t = current_thread;
current_thread = next_thread;
/* YOUR CODE HERE
* Invoke thread_switch to switch from t to next_thread:
* thread_switch(??, ??);
*/
/**
* My Implementatioon
*/
thread_switch((uint64)t, (uint64)next_thread);
} else
next_thread = 0;
}
至此,我们便完成了Uthread。
Alarm
在Alarm中,我们要实现sigalarm(n, fn)
与int sigreturn(void)
的系统调用。
参考指导书的步骤:
首先,我们在user/user.h
声明Alarm系统调用接口:
/** AlarmTest */
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
然后,参考user/usys.pl
、 kernel/syscall.h
、 kernel/syscall.c
的写法,添加sigalarm
与sigreturn
的系统调用:
user/usys.pl
# AlarmTest
entry("sigalarm");
entry("sigreturn");
kernel/syscall.h
/** AlarmTest */
#define SYS_sigalarm 26
#define SYS_sigreturn 27
kernel/syscall.c
/** Alarm */
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
[SYS_pipe] sys_pipe,
[SYS_read] sys_read,
[SYS_kill] sys_kill,
[SYS_exec] sys_exec,
[SYS_fstat] sys_fstat,
[SYS_chdir] sys_chdir,
[SYS_dup] sys_dup,
[SYS_getpid] sys_getpid,
[SYS_sbrk] sys_sbrk,
[SYS_sleep] sys_sleep,
[SYS_uptime] sys_uptime,
[SYS_open] sys_open,
[SYS_write] sys_write,
[SYS_mknod] sys_mknod,
[SYS_unlink] sys_unlink,
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
[SYS_ntas] sys_ntas,
[SYS_crash] sys_crash,
/** Alarm */
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn
};
然后,我们可以在 kernel/sysfile.c
中实现sigalarm
与sigreturn
的系统调用:
/** AlarmTest */
uint64
sys_sigalarm(void){
return 0;
}
uint64
sys_sigreturn(void){
return 0;
}
下面,我们将sys_sigalarm
补充完整。首先要明确sigalarm(int ticks, void (*handler)())
函数的作用:每ticks
秒,执行一次handler
函数。现在,我们遇到了第一个问题,如何传参?参考kernel/sysfile.c
中其他系统调用函数的实现,我们可以很容易知道依靠argint
、argaddr
等函数即可实现参数传递。接下来,根据指导书给出的几个Hints:
-
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. You can initialize proc fields in allocproc() in proc.c.
-
Every tick, the hardware clock forces an interrupt, which is handled in usertrap(); you should add some code here.
-
You only want to manipulate a process’s alarm ticks if there’s a timer interrupt; you want something like
if(which_dev == 2) … -
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 alarmtest.asm, periodic is at address 0).
我们可以很快完成sys_sigalarm
的实现。
首先在在proc.h
中对proc
添加如下定义:
/** AlarmTest */
int ticks;
void (*handler)();
/** 已经过了多久tick */
int tickpassed;
接着,在proc.c
中的allocproc
初始化这几个字段:
/** AlarmTest */
p->ticks = -1;
p->tickpassed = 0;
p->handler = 0;
然后再trap.c
中的时钟中断里修改进程proc
的时钟状态,当流逝的时间等于预设的时间时,我们便调用handler函数,需要注意的是,由于中断发生在内核态,因此我们不能直接用p->handler()
调用handler函数,因为用户态与内核态的函数地址不同,这里只能通过p->tf->epc = (uint64)p->handler
将下一条指令的地址修改为handler
地址才能正常运行。
if(which_dev == 2){
// printf("got tick intr\n");
// printf("num:%d\n",p->tf->a7);
p->tickpassed++;
if(p->ticks != 0){
if(p->tickpassed == p->ticks){
//printf("hh\n");
//printf("handler: %p\n",p->handler);
/**
*
* 存疑:为什么不能直接p->handler(),
*
*
* 解决:usertrapret执行后执行userret,
* userret使用sret,所以可以直接这样写;
* 在内核态下,用户态页表被更换为内核态页表;
* */
p->tf->epc = (uint64)p->handler;
}
}
yield();
}
接下来,开始正式实现sys_sigalarm
。很简单,利用argint
与argaddr
获得参数后更新进程字段即可。
/** AlarmTest */
uint64
sys_sigalarm(void){
struct proc *p = myproc();
uint64 handler;
int ticks;
if(argint(0, &ticks) < 0)
return -1;
if(argaddr(1, &handler) < 0)
return -1;
p->ticks = ticks;
p->handler = (void *)handler;
return 0;
}
完善Alarm
在这一阶段,我们主要需要实现sigreturn
。根据实验指导书的Hints,我们需要在执行handler
之前将用户进程的地址保存起来,然后在调用sigreturn
后恢复到调用handler
之前的状态。为了实现这一点,指导书给出了一个非常重要的Hint:
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).
Hint中讲到,要保存许多寄存器。于是我的想法是干脆直接将整个trapframe
保存起来,那里面含有足够多的寄存器。于是,向proc
添加了新的字段,该字段用于保存trapframe
:
struct trapframe savedtf;
然后在trap.c
中完成trapframe
的保存:
if(which_dev == 2){
// printf("got tick intr\n");
// printf("num:%d\n",p->tf->a7);
p->tickpassed++;
if(p->ticks != 0){
if(p->tickpassed == p->ticks){
//printf("hh\n");
//printf("handler: %p\n",p->handler);
/**
*
* 存疑:为什么不能直接p->handler(),
*
*
* 解决:usertrapret执行后执行userret,
* userret使用sret,所以可以直接这样写;
* 在内核态下,用户态页表被更换为内核态页表;
* */
memmove(&p->savedtf, p->tf, sizeof(struct trapframe));
p->tf->epc = (uint64)p->handler;
}
}
yield();
}
最后,填充sigreturn
,使之能够将保存的trapframe
重新恢复回来,代码如下:
uint64
sys_sigreturn(void){
struct proc *p = myproc();
p->tickpassed = 0;
memmove(p->tf, &p->savedtf, sizeof(struct trapframe));
// printf("call return;\n");
return 0;
}
至此,我们完成了整个syscall,测试结果如下,起飞!🛫