本 lab 要求我们进行系统调用代码的编写。
Preparation
切换到对应分支
1$ git fetch
2$ git checkout syscall
3$ make clean
可以看到
Makefile里内容都重置了,且测试脚本名也变成了grade-lab-syscall
在做实验之前,可以先了解一下整个系统调用流程。
Task1: System call tracing
实现系统调用
该任务要求我们实现 trace 命令,用于追踪特定命令的相应系统调用,并为其在 kernel/ 文件夹下实现相应的系统调用。lab 已经为我们准备好了 user/trace.c。根据上面知识,我们修改完 user/user.h,user/usys.pl,Makefile,kernel/syscall.h,kernel/syscall.c,就可以正式编译了。
user/user.h: int trace(int);user/usys.pl: entry(“trace”);Makefile: $U/_tracekernel/syscall.h: #define SYS_trace 22kernel/syscall.c: [SYS_trace] sys_trace
但编译还不能通过,是因为我们还没有实现 sys_trace()。该函数在 kernel/sys_proc.c 中定义,作用就是令当前进程记住我们传入的参数 trace mask,这里需要在 kernel/proc.h 中为 proc 结构体新增一个变量 int trae_mask,然后在 sys_trace() 中利用 argint() 获取参数并赋值即可,函数如下:
1uint64
2sys_trace(void)
3{
4 if (argint(0, &myproc()->trace_mask) < 0) {
5 return -1;
6 }
7 return 0;
8}
因为 exec 只会改变执行的代码段,进程还是同一个,
trace_mask变量并不会被修改,所以无需担心。
为了令 trace 在 fork 场景下也支持追踪功能,需要在 fork() 系统调用中追加子进程拷贝父进程的 trace_mask,实现略。
打印信息
现在我们已经让当前进程记住了 trace mask,接下来需要在执行命令时根据 mask 打印信息,格式为:
1$ <pid>: syscall <syscall name> -> <return value>
我们需要每遇到一个被跟踪的系统调用都打印一遍信息,这就要在 kernel/syscall.c 中的 syscall() 函数中实现了,判断条件就是 (trace_mask >> num) & 1 非零.
1void
2syscall(void)
3{
4 int num;
5 struct proc *p = myproc();
6
7 num = p->trapframe->a7;
8 if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
9 p->trapframe->a0 = syscalls[num](); // 系统调用返回值
10 if ((p->trace_mask >> num) & 1) {
11 printf("%d: syscall %s -> %d\n", p->pid, syscall_name[num], p->trapframe->a0);
12 }
13 }
14 ...
15}
Task2: Sysinfo
该任务要求我们实现 sysinfo(sysinfo*) 函数,并为传入的结构体填充字段,分别为:
freemem: 空闲内存字节数;nproc: 当前进程数;
由于这也是一个新建的系统调用函数,所以我们需要像上一个任务一样修改以下文件 user/user.h,user/usys.pl,Makefile,kernel/syscall.h,kernel/syscall.c,并且在 kernel/sysproc.c 中添加并实现 sys_sysinfo() 函数。
user/user.h: int sysinfo(struct sysinfo *);user/usys.pl: entry(“sysinfo”);Makefile: $U/_sysinfokernel/syscall.h: #define SYS_sysinfo 23kernel/syscall.c: [SYS_sysinfo] sys_sysinfo
然而,lab 并没有为我们提供现成的「获取空闲字节数」与「获取当前进程数」的 API,需要我们自己实现。这两个 API 可以分别在 kernel/kalloc.c 与 kernel/proc.c 中实现(需要在 kernel/defs.h 中添加声明)。
获取空闲内存字节数
kernel/kalloc.c 中有一个名为 kmem 的数据结构,它维护了一个空闲链表。
事实上,所有内存中未分配的页面都有一个 header,大小为 64 位(一个指针那么大),指向(逻辑上的)下一个未分配的页面,这个指针在软件层面用数据结构 struct run 表示。一旦有一个空闲页面 page 被分配,那么(逻辑上的)上一个页面 prev 的 run 就会指向 page 的(逻辑上的)下一个空闲页面 next;而有个物理页被 free 了,就让它成为空闲链表的表头。
这可以在
kernel/kalloc.c中的kalloc()与kfree()中得知。
那空闲内存的字节数就很好计算了,就是空闲页面数*页面大小嘛。写成代码就是
1int
2kfreemem(void)
3{
4 int npage = 0;
5 acquire(&kmem.lock);
6 struct run *r = kmem.freelist;
7 while (r) {
8 r = r->next;
9 npage++;
10 }
11 release(&kmem.lock);
12 return npage * PGSIZE;
13}
获取当前进程数
kernel/proc.c 中为我们定义了一个名为 proc 的进程表(对的,和结构体 struct proc 同名),我们只需要遍历该表,检查进程状态即可。
1int
2nproc(void)
3{
4 int cnt = 0;
5 for (int i = 0; i < NPROC; i++) {
6 if (proc[i].state != UNUSED) {
7 cnt++;
8 }
9 }
10 return cnt;
11}
实现 sys_sysinfo()
接下来就是实现系统函数了。由于我们在用户层调用 sysinfo() 时传入的是一个指针,所以在读取该参数时不能用 argint(),而是 argaddr()。
值得注意的是,读取到的参数是一个用户态的虚拟地址,如果我们创建一个 struct sysinfo* 变量用于接收指针,然后再赋值,像下面这样:
1uint64 va;
2argaddr(0, &va);
3struct sysinfo* info = (struct sysinfo*)va;
4info->freemem = kfreemem();
5info->nproc = nproc();
那肯定是不行的。对于内核而言,如果直接访问地址,那访问的就是物理地址,可我们得到的却是一个用户态下的虚拟地址,这两者是完全不能等同的。要想访问到正确的物理地址,就需要通过页表进行地址转换。然而 lab 已经为我们提供了另一个实现方法,kernel/vm.c 中的 copyout() 函数,用于拷贝一段内存到虚拟地址。这正好是我们需要的,要用它,我们只需要在内核栈中新建一个 struct sysinfo info 变量,赋值后调用 copyout() 拷贝即可。完整的 sys_sysinfo() 如下:
1uint64
2sys_sysinfo(void)
3{
4 uint64 va;
5 if (argaddr(0, &va) < 0) {
6 return -1;
7 }
8
9 // info is in kernel address space
10 struct sysinfo info;
11 info.freemem = kfreemem();
12 info.nproc = nproc();
13
14 // copy info to *va
15 return copyout(myproc()->pagetable, va, (char*)&info, sizeof(info));
16}
测试结果
1$ make grade
2...
3== Test trace 32 grep ==
4$ make qemu-gdb
5trace 32 grep: OK (2.6s)
6== Test trace all grep ==
7$ make qemu-gdb
8trace all grep: OK (1.0s)
9== Test trace nothing ==
10$ make qemu-gdb
11trace nothing: OK (0.9s)
12== Test trace children ==
13$ make qemu-gdb
14trace children: OK (13.6s)
15== Test sysinfotest ==
16$ make qemu-gdb
17sysinfotest: OK (1.7s)
18== Test time ==
19time: OK
20Score: 35/35
最后的工作
git commit -am ""将所有修改提交到本地;- 执行
make handin。由于 lab0 保存了 APIKey,故直接成功提交;
可选的挑战再说吧,没有什么想做的欲望。