xv6系统启动——bootloader
- 分类
- 未分类
文章目录 4 节
我们以在xv6中添加一个date系统调用为例,介绍在xv6中添加系统调用的方法和原理。
date syscall介绍
date系统调用可以获取系统当前的UTC时间。
在我们完成date系统调用后,我们利用这个系统调用做一个小小的工具,可以打印当前的系统时间,工具代码如下:
date.c
#include "types.h"
#include "user.h"
#include "date.h"
int
main(int argc, char *argv[])
{
struct rtcdate r;
if (date(&r)) {
printf(2, "date failed\n");
exit();
}
// your code to print the time in any format you like...
exit();
}
变量r是我们获取到的时间,我们可以通过r打印我们想要的时间格式,结构体r的内容如下:
struct rtcdate {
uint second;
uint minute;
uint hour;
uint day;
uint month;
uint year;
};
添加系统调用
首先要注意的是,我们要添加一个系统调用,我们需要在好多地方添加对应的代码,我们很容易遗忘。因此,我们可以借鉴xv6中已经写好的系统调用,uptime系统调用是一个很好的例子。
$ grep -n uptime *.[hcS]
syscall.c:105:extern int sys_uptime(void);
syscall.c:121:[SYS_uptime] sys_uptime,
syscall.h:15:#define SYS_uptime 14
sysproc.c:83:sys_uptime(void)
user.h:25:int uptime(void);
usys.S:31:SYSCALL(uptime)
我们不难发现我们要在六个地方添加对应的代码,那么加下来的事情就是照葫芦画瓢即可。
-
extern int sys_uptime(void); extern int sys_date(void); -
static int (*syscalls[])(void) = { [SYS_fork] sys_fork, ... [SYS_close] sys_close, [SYS_date] sys_date, }; -
#define SYS_close 21 #define SYS_date 22 -
sysproc.c的内容我们留着后面完成 -
int uptime(void); int date(struct rtcdate*);
6. ```
SYSCALL(uptime)
SYSCALL(date)
第四部分的内容是系统调用的具体实现,我们在这里需要借助于系统lapic.c中给我们提供的cmostime函数,这个函数接受一个 struct rtcdate指针,将时间赋值到其中,此外,参数我们也需要借助于xv6提供的argptr来获取。
int
date(void)
{
struct rtcdate *r;
if(argptr(0, (void*)&r, sizeof(struct rtcdate)) < 0)
return -1;
cmostime(r);
return 0;
}
执行过程分析
-
我们在使用这个系统调用在用户态的形式,这个函数被定义在
user.h中。只需要引入user.h我们就可以使用系统调用。 -
这个系统调用使用汇编实现的,当然,这个实现比较简单,我们在传入参数之后,我们在
%eax寄存器中传入我们需要使用的系统调用号,然后使用int指令即可。这个实现在usys.S中。
#define SYSCALL(name) \
.globl name; \
name: \
movl $SYS_ ## name, %eax; \
int $T_SYSCALL; \
ret
SYSCALL(fork)
SYSCALL(exit)
SYSCALL(wait)
SYSCALL(pipe)
...
- 关于
movl $SYS_ ## name, %eax;在C语言中,两个#表示字符串拼接,以fork为例,这里的指令实际上是movl $SYS_ fork, %eax;,还记得我说%eax要传入系统调用号吗,那么SYS_fork是怎么转变成正确的系统调用号的呢,在syscall.h中:
#define SYS_fork 1
#define SYS_exit 2
#define SYS_wait 3
#define SYS_pipe 4
#define SYS_read 5
#define SYS_kill 6
#define SYS_exec 7
#define SYS_fstat 8
这样,在预编译后,指令就是(fork为例),movl 1,%eax。
- 然后,
int指令会陷入内核,具体的细节我们暂不关系,接下来,我们会执行syscall.c中的syscall函数
void
syscall(void)
{
int num;
struct proc *curproc = myproc();
num = curproc->tf->eax;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
curproc->tf->eax = syscalls[num]();
} else {
cprintf("%d %s: unknown sys call %d\n",
curproc->pid, curproc->name, num);
curproc->tf->eax = -1;
}
}
可以看到核心函数:
curproc->tf->eax = syscalls[num]();
其中num是系统调用号,那么syscalls是什么呢,也是在这个文件中,内容如下:
static int (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
...
[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_date] sys_date,
};
是不是巧妙地利用了C语言的宏。
- 最后,(还是以fork为例),我们会发现系统会执行
sys_fork函数,这个函数被定义在sysproc.c中(事实上可以被定义在任何位置)
int
sys_fork(void)
{
return fork();
}
可以发现有意思的一点,系统调用调用到最后竟然还是调用fork?!这个fork已经和前面的fork完全不同了,现在的fork是一个内核中的函数,被定义在proc.c中,前面的过程事实上都可以理解为对内核函数的包装,通过上面的一系列过程,我们最终实现了在用户态"调用"内核中函数。
可以发现date也是同理,date系统调用最终是调用了
cmostime函数。不过看起来fork系统调用由于用户态和内核态有着相同的名字看起来更加有趣。
将date工具加入到xv6中
我们现在已经完成了我们的系统调用,最后我们只需要make编译即可,make会帮助我们把date.c编译成_date二进制文件。最后,在编译之前,我们需要把_date加入到Makefile的UPROGS中,这样命令才能生效。
到这里我们就成功为xv6添加了一个系统调用,用利用这个系统调用制作了一个打印的时间的工具。
tips:分清楚系统调用和shell 命令
date.c和_date是我们利用date系统调用制作的,我们要注意我们在xv6 shell中使用的date命令是_date二进制文件调用的结果,而不是直接使用系统调用
UPROGS=\
_cat\
_echo\
_forktest\
_grep\
_init\
_kill\
_ln\
_ls\
_mkdir\
_rm\
_sh\
_stressfs\
_usertests\
_wc\
_zombie\
_date\
$ make
$ make qemu