MIT 6.s081 Lab2: System Calls

这个 lab 是让我们在 xv6 中添加两个新的系统调用,来熟悉系统调用是怎么工作的,还有阅读内核代码,后续的 lab 中会添加更多的系统调用。

System call tracing (moderate)

trace 系统调用用于追踪程序执行的过程中的系统调用。trace 的使用方法是 trace + mask + 用户程序及参数,比如:

控制台输入 trace 32 grep hello README,就会出现以下内容:

1
2
3
4
5
$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0

为什么只 trace 了 read 呢?是因为参数 mask 为 32,二进制为 100000,从 0 开始第 5 位为 1,对应着read 系统调用号 5,所以 mask 用 bitmap 的方式来标识要追踪哪些系统调用。

根据任务提示,在 user/user.h 中添加系统调用的 prototype,在 user/usys.pl 中添加 stub,在 kernel/syscall.h 中添加新的 syscall number,仿照原有的系统调用来写即可。

接下来是 trace 的实现逻辑,其实就是在进程的数据结构 struct proc 中添加一个新的成员变量 trace_mask 来记录要对哪些系统调用进行 trace。

1
2
3
4
struct proc {
// ...
int trace_mask; // To remember trace mask
}

系统调用的入口处在 kernel/syscall.c#syscall 函数中,在这里根据寄存器 a7 中存储的系统调用号,从 syscalls 数组中取出对应的系统调用进行执行,所以我们要在上面的声明中加入我们在 sysproc.c 中新实现的 sys_trace 函数。其实它干的事情就仅仅是在进程中设置 trace_mask 而已。

1
2
3
4
5
6
7
8
9
10
11
uint64
sys_trace(void)
{
int mask;

if (argint(0, &mask) < 0) {
return -1;
}
myproc()->trace_mask = mask;
return 0;
}

在 syscalls 取出一个系统调用函数并执行完毕后,我们就可以通过存储在 proc 中的 trace_mask 来判断这个系统调用是否需要进行 trace 了,也就是判断 trace_mask 中这个系统调用号这一位是不是 1。如果是的话就按照 lab 的格式需要打印 trace 信息。我们需要一个数组来存储系统调用号和系统调用名的对应关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
int pid = p->pid;
int trace_mask = p->trace_mask;
if (trace_mask != 0 && ((trace_mask >> (num)) & 1) == 1) {
printf("%d: %s -> %d\n", pid, syscall_name[num-1],p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}

如果需要 trace 的系统调用使用了 fork 创建了子进程,我们也需要能够 trace 子进程进行的系统调用,所以要在 kernel/proc.c#fork 中将 trace_mask 复制给子进程。

1
np->trace_mask = p->trace_mask;

trace 的用户程序,xv6 中已经给出,接下来只要将 $U/_trace 添加到 Makefile 中就可以运行了。

Sysinfo (moderate)

sysinfo 系统调用用于打印剩余的内存还有多少和打印当前有多少进程处于运行状态,它接受一个 struct sysinfo 结构体参数,然后内核会将信息填入到这个参数中。

添加 prototype、stub、syscall number、系统调用声明这里就省略了,和上面的操作是一样的。重点说一下 kernel/sysproc.c#sys_sysinfo 函数的实现。

想要获得有多少空余的内存,需要在 kernel/kalloc.c 中添加函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
uint64
free_mem_size()
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
int page_num = 0;
while(r) {
page_num++;
r = r->next;
}
release(&kmem.lock);
return page_num * PGSIZE;
}

在 kmem 结构体中存储了一个 freelist 来记录空闲的页数,freelist 中每一个节点都是一个 run 结构体的指针,所以遍历 freelist 并统计剩余的空闲页数即可。

想要获取有多少进程正在运行,需要在 kernel/proc.c 中添加函数:

1
2
3
4
5
6
7
8
9
10
11
12
uint64
used_proc_num()
{
struct proc *p;
uint64 proc_num = 0;
for (p = proc; p < &proc[NPROC]; p++) {
if (p -> state != UNUSED) {
proc_num++;
}
}
return proc_num;
}

进程都被保存在 proc 数组中,遍历这个数组并判断进程状态再统计数量即可。

然后我们在 kernel/sysproc.c#sys_sysinfo 中调用这两个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
uint64
sys_sysinfo(void) {

struct proc *p = myproc();

uint64 fm = free_mem_size();
uint64 np = used_proc_num();


struct sysinfo info = {
fm,np
};


uint64 addr;


if(argaddr(0, &addr) < 0) return -1;


if(copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)
return -1;
return 0;
}

根据提示,参考 kernel/sysfile.c#sys_fstat 和 kernel/file.c#filestat 是如何使用 copyout 来讲数据拷贝回用户空间的。

最后把 $U/_sysinfotest 加入到 Makefile 的 UPROGS 中即可运行。

测试结果

总结

这个 lab 乍一看比较难,但是把整个相关的源码看过一遍之后就还好了,第一个 trace 系统调用,将在进程中保存 trace_mask 和系统调用入口是怎么调用系统调用的关联起来其实就知道怎么实现了。第二个也是阅读 kalloc.c 和 proc.c 的相关代码,就知道怎么操作 kmem 和 proc 了。