6.s081 Lab1 System Calls


本 lab 要求我们进行系统调用代码的编写。

Preparation

切换到对应分支

$ git fetch
$ git checkout syscall
$ make clean

可以看到 Makefile 里内容都重置了,且测试脚本名也变成了 grade-lab-syscall

在做实验之前,可以先了解一下整个系统调用流程

Task1: System call tracing

实现系统调用

该任务要求我们实现 trace 命令,用于追踪特定命令的相应系统调用,并为其在 kernel/ 文件夹下实现相应的系统调用。lab 已经为我们准备好了 user/trace.c。根据上面知识,我们修改完 user/user.huser/usys.plMakefilekernel/syscall.hkernel/syscall.c,就可以正式编译了。

user/user.h: int trace(int); user/usys.pl: entry("trace"); Makefile: $U/_trace
kernel/syscall.h: #define SYS_trace 22 kernel/syscall.c: [SYS_trace] sys_trace

但编译还不能通过,是因为我们还没有实现 sys_trace()。该函数在 kernel/sys_proc.c 中定义,作用就是令当前进程记住我们传入的参数 trace mask,这里需要在 kernel/proc.h 中为 proc 结构体新增一个变量 int trae_mask,然后在 sys_trace() 中利用 argint() 获取参数并赋值即可,函数如下:

kernel/sysproc.c
uint64 sys_trace(void) { if (argint(0, &myproc()->trace_mask) < 0) { return -1; } return 0; }

因为 exec 只会改变执行的代码段,进程还是同一个,trace_mask 变量并不会被修改,所以无需担心。

为了令 tracefork 场景下也支持追踪功能,需要在 fork() 系统调用中追加子进程拷贝父进程的 trace_mask,实现略。

打印信息

现在我们已经让当前进程记住了 trace mask,接下来需要在执行命令时根据 mask 打印信息,格式为:

$ <pid>: syscall <syscall name> -> <return value>

我们需要每遇到一个被跟踪的系统调用都打印一遍信息,这就要在 kernel/syscall.c 中的 syscall() 函数中实现了,判断条件就是 (trace_mask >> num) & 1 非零.

kernel/syscall.c
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](); // 系统调用返回值 if ((p->trace_mask >> num) & 1) { printf("%d: syscall %s -> %d\n", p->pid, syscall_name[num], p->trapframe->a0); } } ... }

Task2: Sysinfo

该任务要求我们实现 sysinfo(sysinfo*) 函数,并为传入的结构体填充字段,分别为:

  1. freemem: 空闲内存字节数;
  2. nproc: 当前进程数;

由于这也是一个新建的系统调用函数,所以我们需要像上一个任务一样修改以下文件 user/user.huser/usys.plMakefilekernel/syscall.hkernel/syscall.c,并且在 kernel/sysproc.c 中添加并实现 sys_sysinfo() 函数。

user/user.h: int sysinfo(struct sysinfo *); user/usys.pl: entry("sysinfo"); Makefile: $U/_sysinfo
kernel/syscall.h: #define SYS_sysinfo 23 kernel/syscall.c: [SYS_sysinfo] sys_sysinfo

然而,lab 并没有为我们提供现成的「获取空闲字节数」与「获取当前进程数」的 API,需要我们自己实现。这两个 API 可以分别在 kernel/kalloc.ckernel/proc.c 中实现(需要在 kernel/defs.h 中添加声明)。

获取空闲内存字节数

kernel/kalloc.c 中有一个名为 kmem 的数据结构,它维护了一个空闲链表。

事实上,所有内存中未分配的页面都有一个 header,大小为 64 位(一个指针那么大),指向(逻辑上的)下一个未分配的页面,这个指针在软件层面用数据结构 struct run 表示。一旦有一个空闲页面 page 被分配,那么(逻辑上的)上一个页面 prevrun 就会指向 page 的(逻辑上的)下一个空闲页面 next;而有个物理页被 free 了,就让它成为空闲链表的表头。

这可以在 kernel/kalloc.c 中的 kalloc()kfree() 中得知。

那空闲内存的字节数就很好计算了,就是**空闲页面数*页面大小**嘛。写成代码就是

kernel/kalloc.c
int kfreemem(void) { int npage = 0; acquire(&kmem.lock); struct run *r = kmem.freelist; while (r) { r = r->next; npage++; } release(&kmem.lock); return npage * PGSIZE; }

获取当前进程数

kernel/proc.c 中为我们定义了一个名为 proc 的进程表(对的,和结构体 struct proc 同名),我们只需要遍历该表,检查进程状态即可。

kernel/proc.c
int nproc(void) { int cnt = 0; for (int i = 0; i < NPROC; i++) { if (proc[i].state != UNUSED) { cnt++; } } return cnt; }

实现 sys_sysinfo()

接下来就是实现系统函数了。由于我们在用户层调用 sysinfo() 时传入的是一个指针,所以在读取该参数时不能用 argint(),而是 argaddr()

值得注意的是,读取到的参数是一个用户态的虚拟地址,如果我们创建一个 struct sysinfo* 变量用于接收指针,然后再赋值,像下面这样:

kernel/sysproc.c
uint64 va; argaddr(0, &va); struct sysinfo* info = (struct sysinfo*)va; info->freemem = kfreemem(); info->nproc = nproc();

那肯定是不行的。对于内核而言,如果直接访问地址,那访问的就是物理地址,可我们得到的却是一个用户态下的虚拟地址,这两者是完全不能等同的。要想访问到正确的物理地址,就需要通过页表进行地址转换。然而 lab 已经为我们提供了另一个实现方法,kernel/vm.c 中的 copyout() 函数,用于拷贝一段内存到虚拟地址。这正好是我们需要的,要用它,我们只需要在内核栈中新建一个 struct sysinfo info 变量,赋值后调用 copyout() 拷贝即可。完整的 sys_sysinfo() 如下:

kernel/sysproc.c
uint64 sys_sysinfo(void) { uint64 va; if (argaddr(0, &va) < 0) { return -1; } // info is in kernel address space struct sysinfo info; info.freemem = kfreemem(); info.nproc = nproc(); // copy info to *va return copyout(myproc()->pagetable, va, (char*)&info, sizeof(info)); }

测试结果

$ make grade
...
== Test trace 32 grep ==
$ make qemu-gdb
trace 32 grep: OK (2.6s)
== Test trace all grep ==
$ make qemu-gdb
trace all grep: OK (1.0s)
== Test trace nothing ==
$ make qemu-gdb
trace nothing: OK (0.9s)
== Test trace children ==
$ make qemu-gdb
trace children: OK (13.6s)
== Test sysinfotest ==
$ make qemu-gdb
sysinfotest: OK (1.7s)
== Test time ==
time: OK
Score: 35/35

最后的工作

  1. git commit -am "" 将所有修改提交到本地;
  2. 执行 make handin。由于 lab0 保存了 APIKey,故直接成功提交;

可选的挑战再说吧,没有什么想做的欲望。


  目录