Lab 2 system calls
跟实验1一样,完成一些功能。并且,
在开始编码之前,请阅读 xv6 书籍的第 2 章,以及第 4 章的 4.3 和 4.4 节,以及相关的源文件:
- 用户空间系统调用代码在
user/user.h
和user/usys.pl
中。 - 内核空间代码在
kernel/syscall.h
和kernel/syscall.c
中。 - 与进程相关的代码在
kernel/proc.h
和kernel/proc.c
中。
确保你对这些内容有足够的理解,以便顺利进行后续开发。
然后lab开始前,要切换分支
// 后面切换分支时没有提交修改会报错
//第一种方式 存到暂存区
git add.
git stash
//取出的时候使用
git stash pop
$ git fetch
$ git checkout syscall
$ make clean
果你运行 make grade
,你会看到评分脚本无法执行 trace
和 sysinfotest
。你的任务是添加必要的系统调用和存根,以使它们正常工作。
Syetem call tracing(moderate)
目的
添加一个系统调用跟踪功能,它应该接受一个参数,一个整数“mask”,其位指定要跟踪的系统调用(例如,要跟踪 fork 系统调用,程序调用 trace(1 << SYS_fork),其中 SYS_fork 是来自 kernel/syscall.h 的系统调用编号。)
必须修改 xv6 内核,以便在每个系统调用即将返回时打印一行,如果该系统调用的编号在掩码中设置。打印的行应包含进程 ID、系统调用的名称和返回值;你不需要打印系统调用的参数。该跟踪系统调用应为调用它的进程及其随后创建的任何子进程启用跟踪,但不应影响其他进程。
提示
一些提示:
- 在 Makefile 中将
$U/_trace
添加到UPROGS
。 - 运行
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()
函数,通过在进程结构中的新变量中记住其参数来实现新的系统调用(见kernel/proc.h
)。从用户空间检索系统调用参数的函数在kernel/syscall.c
中,你可以在kernel/sysproc.c
中看到它们的使用示例。 - 修改
fork()
(见kernel/proc.c
)以将跟踪掩码从父进程复制到子进程。 - 修改
kernel/syscall.c
中的syscall()
函数以打印跟踪输出。你需要添加一个系统调用名称的数组以便进行索引。
做法
先看一下user/trace.c,emmm发现已经很完整了。然后当时不知道怎么做了,就一头雾水。无奈只能看看提示。
用户部分
先根据1和2在user/user.h中添加函数原型
....
int uptime(void);
int trace(int); // 根据目的添加的函数原型
a stub to user/usys.pl??感觉有点莫名奇妙的,因为不知道是个啥,而且也不知道usys.pl是干什么的。然后看了看网上的:如果我们把这个entry宏展开后, 就可以明白其实它为我们直接以汇编语言的形式实现了新的系统调用stub,是吧SYS_trace放入a7这个寄存器中,然后用ecall命令。MIT 6.S081 Operating System - 知乎 (zhihu.com)
......
entry("uptime");
entry("trace"); // 新添加的
kernel/syscall.h
中添加系统调用编号,就顺手加成22吧
......
#define SYS_mkdir 20
#define SYS_close 21
#define SYS_trace 22
然后是trace 32 grep hello README,出现就faile说明1和2编译的部已经弄好,至于fail因为在内核中这个trace没有实现。
内核部分
这时候死去的回忆开始攻击我了,就补充材料资料那一部分,关于内核启动,这几个文件都有说明,但忘光光了qaq,只记得个大概。然后proc.h存的是状态,得加一个mask字段,因为用户传了。
# kernel/proc.h
...
struct proc{
......
uint64 tracemask;
};
然后在 kernel/sysproc.c
中添加一个 sys_trace()
函数,为该字段进行赋值,赋值的 mask 为系统调用传过来的参数,放在了 a0 寄存器中。使用 argint() 函数可以从对应的寄存器中取出参数并转成 int 类型。argint() 的函数声明在 kernel/defs.h 中,具体实现在 kernel/syscall.c 中。除了取出 int 型的数据函数外,还有取出地址参数的 argaddr 函数,就是将参数以地址的形式取出来.以在 kernel/sysproc.c
中看到它们的使用示例。
uint64 sys_trace(void)
{
int mask;
if(argint(0, &mask) < 0)
return -1;
myproc()->tracemask = tracemask;
return 0;
}
修改 fork()
(见 kernel/proc.c
)以将跟踪掩码从父进程复制到子进程。
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
// Allocate process.
if((np = allocproc()) == 0){
return -1;
}
// trace copy
np->mask = p->mask
......
}
修改 kernel/syscall.c
中的 syscall()
函数以打印跟踪输出。你需要添加一个系统调用名称的数组以便进行索引,放在syscall.c中,我之前把名字放在.h中编译报错说重复命名,一看.h,没有#ifnedef。
char* syscalls_name[] = {
"",
"fork",
"exit",
"wait",
"pipe",
"read",
"kill",
"exec",
"fstat",
"chdir",
"dup",
"getpid",
"sbrk",
"sleep",
"uptime",
"open",
"write",
"mknod",
"unlink",
"link",
"mkdir",
"close",
"trace",
};
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->tracemask & (1 << num)) {
printf("%d: syscall %s -> %d\n", p->pid, syscalls_name[num], p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
此外,还需添加系统调用入口 extern uint64 sys_trace(void);
和 [SYS_trace] sys_trace,
到 kernel/syscall.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
...
$
然后直接./grade-lab-syscall trace,发现trace部分都通过了
Sysinfo(moderate)
目的
跟前面一样,完成一系统调用,叫做sysinfo,用于收集有关正在运行的系统的信息。
该系统调用接受一个参数:指向 struct sysinfo
的指针(请参见 kernel/sysinfo.h
)内核应填写该结构体的字段:freemem
字段应设置为可用内存的字节数,而 nproc
字段应设置为状态不是 UNUSED
的进程数量。我们提供了一个测试程序 sysinfotest
;如果它打印出 “sysinfotest: OK”,你就通过了这个作业。
提示
-
在
Makefile
的UPROGS
中添加$U/_sysinfotest
。 -
运行
make qemu
;user/sysinfotest.c
会编译失败。按照上一个作业的步骤添加系统调用sysinfo
。在user/user.h
中声明sysinfo()
的原型时,需要预先声明struct sysinfo
的存在:struct sysinfo; int sysinfo(struct sysinfo *);
-
一旦修复了编译问题,运行
sysinfotest
;它会失败,因为你还没有在内核中实现这个系统调用。 -
sysinfo
需要将struct sysinfo
复制回用户空间;可以参考sys_fstat()
(在kernel/sysfile.c
中)和filestat()
(在kernel/file.c
中)的示例,使用copyout()
来实现这一点。 -
要收集可用内存的数量,请在
kernel/kalloc.c
中添加一个函数。 -
要收集进程的数量,请在
kernel/proc.c
中添加一个函数。
做法
用户部分
在 Makefile
的 UPROGS
中添加 $U/_sysinfotest
。
然后跟之前一样,在user.h中声明函数原型,需要注意的是需要预先声明 struct sysinfo
的存在,相当于前向声明。
然后是usys.pl中加入口。顺便在kernel/syscall.h中加入宏定义。
然后编译,看是否通过。
内核部分
然后就是内核部分跟前面一样的,syscall.h之前添加了宏定义定义该系统调用的标号,然后在syscall.c中加入extern uint64 sys_sysinfo(void);,在syscalls_name中加入"sysinfo",在syscalls中加入sys_sysinfo。
然后就是子啊sysproc.c中实现uint64 sys_sysinfo(void)函数。在sysinfo.h中查看struct sysinfo发现只有freemem和nproc两个uint64组成的结构体。我们的目的就是填写该结构体的字段然后返回给用户空间。
先看怎么返回的,提示4说可以参考 sys_fstat()
(在 kernel/sysfile.c
中)和 filestat()
(在 kernel/file.c
中)的示例,使用 copyout()
来实现这一点。然后copyout在def.h中原型,vm.c中实现。然后看一下就知道大概怎么使用了。就相当于传一个info。
// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int copyout(pagetable_t, uint64, char *, uint64);
接下来烦恼的是怎么填写那两个字段,结合提示,在 kernel/kalloc.c
中添加一个函数收集可用内存的数量、在 kernel/proc.c
中添加一个函数收集进程的数量,并且记得在defs.h中添加函数原型。
先添加uint64 kfreemem(void) ;先观察kalloc.h,开头三句注释分别说明这个文件有什么。是一个allocator,给用户进程分配空间。重点是最后一句注释, whole 4096-byte pages.每一个页是4096字节。然后从kmem结构体和kinit发现这个内存组织结构是单链表组织空闲页,所以得到空闲内存需要遍历一遍链表,仿造头上kalloc写的。
// Get the free memroy
uint64 kfreemem(void)
{
struct run *r;
uint64 free = 0;
acquire(&kmem.lock);
r = kmem.freelist;
while(r)
{
free += PGSIZE;
r = r->next;
}
release(&kmem.lock);
return free;
}
然后是添加uint64 count_freeproc(void)。然后同样观察proc.c这个文件,开头几个cpu、proc等数组十分引人注意,在proc.h中有enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };而struct proc又包含这个属性。就相当于遍历数组,统计不为UNUSED 状态的进程数目即可。而且最多NPROC个进程。
uint64 count_freeproc(void)
{
struct proc *p;
uint64 cnt = 0;
for(p = proc;p<&proc[NPROC];p++)
{
if(p->state!=UNUSED)
cnt += 1;
}
return cnt;
}
然后现在的唯一问题是,如何得到目标用户空间地址?就像前面argint中从寄存器中取出,肯定是从寄存器中取出。有取出地址参数的 argaddr 函数,就是将参数以地址的形式取出来.然后这个sys_info写法就明了了。
uint64
sys_sysinfo(void)
{
struct proc *p = myproc();
uint64 user_addr;
if(argaddr(0,&user_addr)<0)
return -1;
struct sysinfo info;
info.freemem = kfreememe();
info.nproc = count_freeproc();
if(copyout(p->pagetable,user_addr,(char*)&info,sizeof(info))<0)
return -1;
return 0;
}
另外,得在sysproc.c中加入#include “sysinfo.h”。