本 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.h
,user/usys.pl
,Makefile
,kernel/syscall.h
,kernel/syscall.c
,就可以正式编译了。
user/user.h
: int trace(int);user/usys.pl
: entry("trace");Makefile
: $U/_trace
kernel/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()
获取参数并赋值即可,函数如下:
uint64
sys_trace(void)
{
if (argint(0, &myproc()->trace_mask) < 0) {
return -1;
}
return 0;
}
因为 exec 只会改变执行的代码段,进程还是同一个,
trace_mask
变量并不会被修改,所以无需担心。
为了令 trace
在 fork
场景下也支持追踪功能,需要在 fork()
系统调用中追加子进程拷贝父进程的 trace_mask
,实现略。
打印信息
现在我们已经让当前进程记住了 trace mask,接下来需要在执行命令时根据 mask 打印信息,格式为:
$ <pid>: syscall <syscall name> -> <return value>
我们需要每遇到一个被跟踪的系统调用都打印一遍信息,这就要在 kernel/syscall.c
中的 syscall()
函数中实现了,判断条件就是 (trace_mask >> num) & 1
非零.
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*)
函数,并为传入的结构体填充字段,分别为:
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/_sysinfo
kernel/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()
中得知。
那空闲内存的字节数就很好计算了,就是**空闲页面数*页面大小**嘛。写成代码就是
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
同名),我们只需要遍历该表,检查进程状态即可。
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*
变量用于接收指针,然后再赋值,像下面这样:
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()
如下:
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
最后的工作
git commit -am ""
将所有修改提交到本地;- 执行
make handin
。由于 lab0 保存了 APIKey,故直接成功提交;
可选的挑战再说吧,没有什么想做的欲望。