MIT 6.S081 Lab2:System Calls | 系统调用
前置准备
主要内容为自己实现系统调用,或者优化已有的系统调用。
根据课程官网的要求,需要阅读完教材的第二章\(Operating \space system \space
organization\)。并且读懂下列源代码:kernel/proc.h,
kernel/defs.h, kernel/entry.S,
kernel/main.c, user/initcode.S,
user/init.c, 简略了解kernel/proc.c,
kernel/exec.c。
相关笔记参考: lecture 2 Notes
将util分支所有内容都上传仓库后,执行下列命令来切换分支
1
2
3git fetch
git checkout syscall
make clean
此外,新建一个syscall_dev分支来进行实际实验。
1 | |
在syscall_dev中每通过一个作业的测试,提交(git
commit)你的代码,并将所做的修改合并(git merge)到util中,然后提交(git
push)到github。
1 | |
System call tracing (moderate)
Statement
在本作业中,你将添加一项系统调用跟踪功能,这可能会在以后的实验调试中有所帮助。你将创建一个新的跟踪系统调用来控制跟踪。它应该接受一个参数,即一个"掩码", 该掩码的位数指定了要跟踪的系统调用。例如,要跟踪 fork 系统调用,程序会调用
trace(1<<SYS_fork),其中 SYS_fork 是 kernel/syscall.h 中的系统调用编号。你必须修改 xv6 内核,以便在每个系统调用即将返回时,如果掩码中设置了系统调用编号,就打印出一行。该行应包含进程 ID、系统调用名称和返回值;无需打印系统调用参数。跟踪系统调用应启用对调用该调用的进程及其随后分叉的子进程的跟踪,但不应影响其他进程。
1 | |
在第一个样例中,trace 只在 grep 中追踪
read 系统调用。\(32\) 是
\(1 << sys_read\) 的结果。
在第二个样例中,trace
追踪所有在grep中的系统调用,\(2147483647\) 是 \(31\) 位二进制位都置1。 在第三个样例中,
进程并没有进行追踪。 在第四个样例中,trace
最终所有后续的fork系统调用。
如果程序正确,应该得到一致的输出(进程号可能有所出入,但其他都应该一致)。
Hints
- 在
Makefile中的UPROGS添加$U/_trace。 - 运行
make qemu,你会发现编译器无法编译user/trace.c,因为系统调用的用户空间存根还不存在:在user/user.h中添加一个系统调用原型,在user/usys.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()函数,以打印跟踪输出。您需要添加一个系统调用名称数组作为索引。
Analysis & Solution
前文末尾(传送门)已经说过了 xv6 进行系统调用的过程,这里就略过了。
提示\(1\)和提示\(2\)没什么好说的。直接看kernel/sysproc.c文件。可以看到其他系统调用是怎么实现的。下面以exit来分析。
1 | |
可以看到,首先从trapfram->a0寄存器拿出第一个参数(由argint()完成)。然后把更多实现细节放到了exit函数中来实现。可以看出,系统调用基本遵循这个流程:在sys_xxxx中处理参数、互斥锁等失误。如果还有复杂的功能,则放到具体xxxx函数里面实现。
我们的sys_trace只需要将p->trace_mask置为p->trapframe->a0的值即可。所以有:在kernel/sysproc.c中添加下面代码:
1
2
3
4
5
6
7int sys_trace(void) {
int mask;
if (argint(0, &mask) < 0) return -1;
myproc()->trace_mask = mask;
return 0;
}
然后打开kernel/syscall.c,
找到syscall()这个函数。所有系统调用都是通过syscall函数来调用的。根据提示,我们只需要将p->trace_mask拿出来,然后看系统调用号这一位是否为\(1\),如果为\(1\),则输出信息即可。
1 | |
运行结果:

sysinfo (moderate)
Statement
在本作业中,你将添加一个系统调用
sysinfo,用于收集运行系统的信息。系统调用需要一个参数:指向struct sysinfo的指针(参见kernel/sysinfo.h)。内核应填写该结构体的字段:freemem字段应设置为可用内存的字节数## Submit lab
新建time.txt文件,输入一个整数表明完成所有实验的耗时。
运行make grade, 得分为100。 
运行make handin,
根据提示将API KEY填入。,nproc
字段应设置为状态不是 UNUSED
的进程数。我们提供了一个测试程序 sysinfotest;如果打印出
sysinfotest: OK,则视为通过。
Hints
- 在 Makefile 的 UPROGS 中添加 $U/_sysinfotest。
- 运行
make qemu。将提示user/sysinfotest.c无法编译。添加系统调sysinfo,步骤与前面的作业相同。在user/user.h中声明sysinfo()的原型,需## Submit lab
新建time.txt文件,输入一个整数表明完成所有实验的耗时。
运行make grade, 得分为100。 
运行make handin, 根据提示将API KEY填入。
解决编译问题后,运行
sysinfotest;由于内核中尚未实现系统调用,所以会失败。 -
sysinfo 需要将 struct sysinfo
复制到用户空间;有关如何使用 copyout()
进行复制的示例,请参见 sys_fstat()(kernel/sysfile.c) 和
filestat()(kernel/file.c) - 要收集可用内存量,请在
kernel/kalloc.c 中添加一个函数。 - 要收集进程数,请在
kernel/proc.c 中添加一个函数。
Analysis & Solution
先按照上一个任务的方法加入一个 sysinfo 系统调用. 这里在
kernel 文件夹下再写一个 sysinfo.c 来实现这个系统调用. 在
Makefile 的 OBJS 加入
$K/sysinfo.o \(因为我在sysinfo.c中实现,所以要链接这个文件。如果直接在kernel/proc.c中实现就不用).
看filestat()函数能得知copyout函数的功能,加上sys_fstat()中虚拟地址是从a0读出来的。所以很容易能写出下面代码。
sysinfo.c: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28#include "types.h"
#include "riscv.h"
#include "defs.h"
#include "date.h"
#include "param.h"
#include "memlayout.h"
#include "spinlock.h"
#include "proc.h"
#include "sysinfo.h"
extern uint64 freemem();
extern uint32 num_proc();
uint64 sys_sysinfo(void) {
struct sysinfo info;
struct proc* p = myproc();
uint64 addr;
if (argaddr(0, &addr) < 0) return -1;
info.freemem = freemem();
info.nproc = num_proc();
if (copyout(p->pagetable, addr, (char*)&info, sizeof(info)) < 0)
return -1;
return 0;
}
然后是freemem()和num_proc()。
在kernel/kalloc.c中添加,看一下前面的kalloc()函数,在看一下struct kmem。
1 | |
其中freelisk是一个链表,存储所有空闲内存页面的地址。所以我们只需要遍历这个链表,每一项都加上PGSIZE即可。
freemem: 1
2
3
4
5
6
7
8
9
10
11
12uint64 freemem() {
struct run* r;
acquire(&kmem.lock);
r = kmem.freelist;
uint64 tot = 0;
for (;r;r = r->next) tot += PGSIZE;
release(&kmem.lock);
return tot;
}
然后翻kernel/proc.c中的procdump函数可以知道,proc数组中存放的是所有进程控制块的地址,所以遍历proc即可。
在kernel/proc.c中添加: 1
2
3
4
5
6
7
8uint32 num_proc() {
uint32 tot = 0;
struct proc* p;
for (p = proc;p < &proc[NPROC];p ++) {
if (p->state != UNUSED) tot ++;
}
return tot;
}
运行结果: 
Submit lab
新建time.txt文件,输入一个整数表明完成所有实验的耗时。
运行make grade, 得分为100。 
运行make handin,
根据提示将API KEY填入。