[MIT6.S081]Lab2: System Calls
# [MIT6.S081]Lab2: System Calls
Lab: System calls (mit.edu) (opens new window)
# 1、System call tracing
这个 assignment 让自己更了解了 xv6 内部系统调用的实现方式,以及用户态和内核态是如何隔离的。
我们要实现一个 trace 系统调用,使得我们能够跟踪 command 中所调用的我们想要追踪的系统调用,以及其返回值。
我们正常执行指令时,如果遇到了系统调用,则会从用户态切换到内核态,提升硬件的特权,来执行系统调用,而在 xv6 中这个切换的操作就由 ecall
来执行。
要实现一个新的系统调用函数,我们就必须在用户态中提供切换到内核态并执行该系统调用的一个接口,这个接口,也就是一个跳板函数,由 usys.pl
自动生成,名字叫 SYS_trace
, 并加入到 usys.S
中。
在 usys.S
中 ,我们将 SYS_trace
放入 a7
寄存器
所以我们先在 usys.pl
中加入对应的接口
#!/usr/bin/perl -w
# Generate usys.S, the stubs for syscalls.
print "# generated by usys.pl - do not edit\n";
print "#include \"kernel/syscall.h\"\n";
sub entry {
my $name = shift;
print ".global $name\n";
print "${name}:\n";
print " li a7, SYS_${name}\n";
print " ecall\n";
print " ret\n";
}
# 生成每个系统调用从用户态 到 内核态的跳板函数,通过 ecall 从 user mode 切换到 supervisor mode,实现了用户态和内核态的隔离
entry("fork");
entry("exit");
entry("wait");
entry("pipe");
entry("read");
entry("write");
entry("close");
entry("kill");
entry("exec");
entry("open");
entry("mknod");
entry("unlink");
entry("fstat");
entry("link");
entry("mkdir");
entry("chdir");
entry("dup");
entry("getpid");
entry("sbrk");
entry("sleep");
entry("uptime");
entry("trace"); # 在这里添加跳板入口
而在用户态中,官方已经帮我们实现好了 user/trace.c
,直接在 shell 中使用即可,该文件中调用了 trace
函数,因此我们还要在 user.h
加入这个函数的声明。
上面是用户态中我们所需要执行的,而通过 ecall
我们便进入了内核态,内核态中所有的系统调用都会在 kernel/syscall.c
中处理执行,我们先要在这个文件中对前面汇编生成的 SYS_trace
进行注册。
我们先在 syscall.h
中加入宏定义,为 SYS_trace
指定了一个数字
#define SYS_trace 22
接着在 syscall.c
中的系统调用函数数组中添加 sys_trace
函数,并用 SYS_trace
作为下标来指定,另外我们并不在 syscall.c
中实现这个函数,所以还要从外部 extern 进来
extern uint64 sys_trace(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_trace] sys_trace, // C语言的语法,表明 sys_trace 的下标是 SYS_trace
};
而这个系统调用的具体实现,我们则在 sysproc.c
中进行,
首先我们要获取 trace
系统调用的参数,由于该参数是在用户态下传入的,因为用户态和内核态是相互隔离的,所以我们无法直接像 C 语言传参那样,需要从进程的 trapframe
中读取寄存器中的值,我们用 int argint(int, int)
实现,并将这个值保存到进程一个新变量中,我们在 proc.h
中的 struct proc
中添加变量 int sysc_trace
。另外记得要在分配一个新进程的时候将这个新变量的值初始化为 0,避免出现混乱的初始值,也就是在 proc.c
中的 allocproc
函数中合适的位置添加 p->sysc_trace = 0;
回到具体实现上,我们获取到的参数是一个掩码,其二进制下的每一位表示着我们想要追踪的系统调用,我们将这个信息保存到当前进程的 sysc_trace
中,实现如下:
uint64 sys_trace(void) {
int msk;
if(argint(0, &msk) < 0)
return -1;
myproc()->sysc_trace = msk;
return 0;
}
为了能够打印我们追踪的信息,我们还要在 syscall.c
中的 void syscall(void)
添加一些内容。
我们之前已经将系统调用专属的数字标号放入了 a7 寄存器中, num = p->trapframe->a7;
将该值赋给 num
。
而从 p->trapframe->a0 = syscalls[num]();
可以看出这句执行了系统调用,并将结果保存在了 a0
寄存器中。
因此我们这行下面加入打印信息
if ((p->sysc_trace >> num) & 1) {
printf("%d: syscall %s -> %d\n", p->pid, syscalls_names[num], p->trapframe->a0);
}
p->pid
是当前进程的 pid 号,syscalls_names
需要我们在 syscall.c
中额外定义一下。
const char *syscalls_names[] = {
[SYS_fork] "fork",
[SYS_exit] "exit",
[SYS_wait] "wait",
[SYS_pipe] "pipe",
[SYS_read] "read",
[SYS_kill] "kill",
[SYS_exec] "exec",
[SYS_fstat] "fstat",
[SYS_chdir] "chdir",
[SYS_dup] "dup",
[SYS_getpid] "getpid",
[SYS_sbrk] "sbrk",
[SYS_sleep] "sleep",
[SYS_uptime] "uptime",
[SYS_open] "open",
[SYS_write] "write",
[SYS_mknod] "mknod",
[SYS_unlink] "unlink",
[SYS_link] "link",
[SYS_mkdir] "mkdir",
[SYS_close] "close",
[SYS_trace] "trace",
};
最后我们还要在 fork
系统调用的时候将父进程的 sysc_trace
copy 给子进程,故在 proc.c
的 int fork(void)
中添加 np->sysc_trace = p->sysc_trace;
即可。
**PS:**由于测试点 Test trace children
耗时可能比较长(至少我是的),所以可能需要在 gradelib.py
中修改一下 timeout
,如下
def run_qemu_kw(target_base="qemu", make_args=[], timeout=50):
# 2、Sysinfo
这个 assignment 要我们实现 Sysinfo
系统调用,让我们可以得到此时内核中空闲的内存大小以及不在 UNUSED
状态下的进程个数,而以上信息用 struct sysinfo
保存。
前面声明注册系统调用的步骤和前面的任务差不多,在这里我们就直接跳过了。
首先我们要在 kalloc.c
中实现函数来获取空闲内存大小。xv6 采用的是空闲链表分配法,我们只需要遍历一下链表,统计空闲的页数,乘上每个页的内存大小即可
void get_freemem(uint64 *freemem) {
struct run *node = kmem.freelist;
uint64 res = 0;
while (node) {
res += PGSIZE;
node = node->next;
}
*freemem = res;
}
接着我们在 proc.c
中实现函数来获取状态不为 UNUSED
的进程个数,更简单了,只需要遍历一下就行
void get_nproc(uint64 *nproc) {
int cnt = 0;
struct proc *p;
for (p = proc; p < &proc[NPROC]; p++) {
if (p->state != UNUSED) {
cnt++;
}
}
*nproc = cnt;
}
另外记得在 def.h
中声明一下这两个函数。
然后我们在 sysproc.c
里实现一下 sys_sysinfo
这个函数就 ok 了
uint64 sys_sysinfo(void) {
struct proc *p = myproc();
struct sysinfo info;
uint64 addr;
get_freemem(&info.freemem);
get_nproc(&info.nproc);
if (argaddr(0, &addr) < 0) { // 获取地址参数
return -1;
}
if(copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0) { // 将 info 拷贝到对应的地方
return -1;
}
return 0;
}
这里我们调用了函数 copyout(p->pagetable, addr, (char *)&info, sizeof(info)
,需要注意的是这里的 addr
是个虚拟地址,我们将 info
的信息拷贝到给定的当前进程的页表 p->pagetable
中的 addr
处。
如此一来我们就实现了系统调用 sysinfo
,可以在用户态下获取内核里的相关信息。