Lab: system calls
开始写代码之前,需要先读 xv6 文档的第二章,还有第四章的 4.3 和 4.4 ,还需要看源码 user/user.h user/usys.pl kernel/syscall.h kernel/syscall.c kernel/proc.h kernel/proc.c
开始之前,切换到系统调用分支
git fetch
git checkout syscall
make clean
如果运行 make grade
,不能正常执行 trace 和 sysinfotest ,那就说明可以开始了,这个 Lab 的任务就是实现这两个功能
System call tracing(moderate)
这个功能是一项系统调用的跟踪功能,后续可能会用到。创建一个新的系统调用 trace 用来控制跟踪程序。它应该接收一个参数,是一个整数 "mask" 掩码,掩码的位数制定了要跟踪的系统调用。例如:追踪 fork 的调用,一个程序调用 trace(1 << SYS_fork) ,其中的 SYS_fork 就是 kernel/syscall.h 中的系统调用的编号。需要修改 xv6 的内核,以便在每个系统调用即将返回时,如果掩码中设置了系统调用编号,就打印出一行输出。这行输出包括进程的 ID 、系统调用的名称和返回值;不需要打印系统掉的参数。trace 系统调用需要能对调的进程以及 fork 出的子进程进行跟踪,但是不能影响其他进程
可以参考跟踪用户程序的代码 (user/trace.c)
实现完成的输出:
$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
$
$ trace 2147483647 grep hello README
4: syscall trace -> 0
4: syscall exec -> 3
4: syscall open -> 3
4: syscall read -> 1023
4: syscall read -> 966
4: syscall read -> 70
4: syscall read -> 0
4: syscall close -> 0
$
$ grep hello README
$
$ trace 2 usertests forkforkfork
usertests starting
test forkforkfork: 407: syscall fork -> 408
408: syscall fork -> 409
409: syscall fork -> 410
410: syscall fork -> 411
409: syscall fork -> 412
410: syscall fork -> 413
409: syscall fork -> 414
411: syscall fork -> 415
...
$
在第一个例子中, trace 值调用了 grep 来跟踪 read 系统调用。32 是指 1<<SYS_read
在第二个例子中, trace 运行 grep ,同时跟踪所有的系统调用; 2147583647 设置了全部 31 个低位 在第三个例子中,运行的程序不是 trace ,所以没有打印出 trace 输出 在第四个例子中, trace 了 usertests 中的 forkforkfork 测试的所有子进程的系统调用 如果测试代码和上面一样,那么就是正确的 (进程 ID 可能会出现不一样)
实验提示
- 添加 $U/_trace 到 Makefile 的 UPROGS 里
- 运行 make qemu 你将看到不能正确编译 user/trace.c ,这是因为用户空间存根还不存在:将系统调用的原型添加到 user/user.h ,将存根添加到 user/user.pl ,以及将系统调用号添加到 kernel/syscall.h 中。 Makefile 会调用 perl 脚本 (user/usys.pl) ,生成 user/usys.S ,时间的系统调用存根,它使用的是 RISC-V 的
ecall
指令转换到内核。修复编译问题后,运行trace 32 grep hello README
会失败,因为还没有在内核中实现系统调用 - 在 kernel/sysproc.c 中添加一个
sys_trace()
的方法,这个方法通过在proc
(参见 kernel/proc.h) 中的一个新变量记住新系统调用的参数来调用新的系统调用。从用户空间获取系统调用参数的函数在 kernel/syscall.c 中,你可以在 kernel/sysproc.c 中看到使用这些函数的示例。 - 修改
fork()
(参见 kernel/proc.c) 用来跟踪掩码从父进程复制到子进程 - 修改 kernel/syscall.c 中的
syscall()
用来打印 trace 的输出,需要添加一个名为 "syscall" 的数组,用来做索引
实验思路
- 首先根据要求,切换分支,然后根据提示,修改 Makefile , user/user.h, user/user.pl, kernel/syscall.h,这时候外面的环境就已经配置好了。可以进入 xv6 运行一下
trace 32 grep hello README
,虽然还是会报错,但是以及可以进入 xv6 且可以调用 trace 了。 - 根据提示,在 kernel/sysproc.c 中添加一个
sys_trace()
的函数,这里无从下手,但是提示说在 proc 的结构中一个新变量,加上后面的提示有一个掩码,推测应该是要在 proc 结构体中新加一个掩码变量,然后给它赋值,观看kernel/sysproc.c
的函数,发现一个myproc()
函数,返回的是指针,因此直接用myproc()
获取proc
,然后使用argint()
获取参数,传入刚刚新添加的变量中。 - 接着就是根据提示,修改
fork()
函数,阅读代码能看出,变量np
是子程序,直接在最后的return
前面给np
的掩码赋值。 - 最后就是要打印 trace ,修改 kernel/syspro.c ,先看代码,看到一个 a7 ,通过 xv6 book 了解到, a7 是系统调用号,做了判断之后,通过一个函数指针执行了一个函数,函数指针指向的是一个函数,然后 xv6 book 上说,系统调用函数返回的值在 a0 中,因此应该需要在这条函数指针后面编写打印语句。
- 由于系统调用函数很多,需要判断一下,直接使用位运算就可以判断,然后就是按照实例的格式打印,PID 存储在 proc 中,syscall name 这里没有,需要自己写一个数组,返回值就是 a0 。
踩到的坑
- 在 user/user.h 中,看实例以为应该填写的是
int trace(int, char*);
,但是make qemu
还是报错了,但是看报错信息,发现trace()
的参数应该是一个int
。 - syscall name 在 xv6 中没有,想了好久都不知道怎么办,一问 ai 才知道要自己写一个数组。
Sysinfo (moderate)
实验思路
- 实验提示非常简单,描述也非常简单。根据提示修改一些文件,要注意看前一个 lab 的提示。然后看提示中的代码,可以知道如何使用
copyout()
传输数据。 - 在 sysproc.c 中,修改新建一个
sysinfo()
函数,在这个函数里使用copyout()
将输出传输到用户态下。 - 根据提示,实现两个函数,一个是获取到所有剩余内存,看一下前面的代码,可以看到一个
freelist
变量,这个变量是用来记录未使用的内存的,因此只需要遍历以此就可以了;第二个是查看当前存在的进程,提示说只要进程的状态不是UNUSED
都属于存在,观看前面的代码,可以看到一个数组proc
,这类似于一个进程池,将它遍历一边,进行判断就可以了。
扩展: 为什么 xv6 使用链表实现内存?
xv6 的内存使用链表实现,主要是为了可读性,同时实现简单