MIT 6.S081 Lab Two
- 引言
- system calls
- System call tracing(moderate)
- 实验解析
- 实现思路小结
- Sysinfo(moderate)
- 实验解析
- 可选的挑战
引言
本文为 MIT 6.S081 2020 操作系统 实验一解析。
MIT 6.S081课程前置基础参考: 基于RISC-V搭建操作系统系列
system calls
在上一个实验中,您使用系统调用编写了一些实用程序。在本实验室中,您将向xv6添加一些新的系统调用,这将帮助您了解它们是如何工作的,并使您了解xv6内核的一些内部结构。您将在以后的实验室中添加更多系统调用。
Attention:
- 在你开始写代码之前,请阅读xv6手册《book-riscv-rev1》的第2章、第4章的第4.3节和第4.4节以及相关源代码文件:
- 系统调用的用户空间代码在user/user.h和user/usys.pl中。
- 内核空间代码是kernel/syscall.h、kernel/syscall.c。
- 与进程相关的代码是kernel/proc.h和kernel/proc.c。
要开始本章实验,请将代码切换到syscall分支:
$ git fetch
$ git checkout syscall
$ make clean
如果运行make grade,您将看到测试分数的脚本无法执行trace和sysinfotest。您的工作是添加必要的系统调用和存根(stubs)以使它们工作。
System call tracing(moderate)
YOUR JOB:
- 在本作业中,您将添加一个系统调用跟踪功能,该功能可能会在以后调试实验时对您有所帮助。
- 您将创建一个新的trace系统调用来控制跟踪。
- 它应该有一个参数,这个参数是一个整数“掩码”(mask),它的比特位指定要跟踪的系统调用。
- 例如,要跟踪
fork
系统调用,程序调用trace(1 << SYS_fork)
,其中SYS_fork
是kernel/syscall.h
中的系统调用编号。 - 如果在掩码中设置了系统调用的编号,则必须修改
xv6
内核,以便在每个系统调用即将返回时打印出一行。 - 该行应该包含进程id、系统调用的名称和返回值;
- 您不需要打印系统调用参数。
- trace系统调用应启用对调用它的进程及其随后派生的任何子进程的跟踪,但不应影响其他进程。
我们提供了一个用户级程序版本的trace,它运行另一个启用了跟踪的程序(参见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时跟踪所有系统调用;2147483647将所有31个低位置为1。
- 在第三个示例中,程序没有被跟踪,因此没有打印跟踪输出。
- 在第四个示例中,在usertests中测试的forkforkfork中所有子孙进程的fork系统调用都被追踪。
如果程序的行为如上所示,则解决方案是正确的(尽管进程ID可能不同)
提示:
- 在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
指令转换到内核。一旦修复了编译问题(注:如果编译还未通过,尝试先make clean
,再执行make qemu
),就运行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()
函数以打印跟踪输出。您将需要添加一个系统调用名称数组以建立索引。
实验解析
本实验中在暴露给用户的user库中已经提供好了相关的trace程序让用户进行调用:
//user/trace.c
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[]){
int i;
char *nargv[MAXARG];
//参数个数不小于3个,确保系统调用号是合法数字
if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){
fprintf(2, "Usage: %s mask command\n", argv[0]);
exit(1);
}
//将第一个参数转换为整数,作为系统调用号传入trace函数---该系统调用函数需要我们提供对应的实现
if (trace(atoi(argv[1])) < 0) {
fprintf(2, "%s: trace failed\n", argv[0]);
exit(1);
}
//nargv数组持有要追踪的命令
for(i = 2; i < argc && i < MAXARG; i++){
nargv[i-2] = argv[i];
}
//执行命令完成系统调用过程追踪
exec(nargv[0], nargv);
exit(0);
}
我们需要做的是提供trace系统调用的具体实现,步骤如下:
-
在Makefile的UPROGS中添加$U/_trace
-
将系统调用原型添加到user/user.h头文件中
2. 将存根添加到user/usys.pl , 这段perl调用由makefile文件调用,生成实际的系统调用存根user/usys.S
3. 将系统调用编号添加到kernel/syscall.h中
4. 执行make clean 和 make qemu 命令,查看usys.S是否生成,是否符合我们的预期
5. 尝试执行trace 32 grep hello README命令,此时由于我们还没有在内核中提供trace系统调用的具体实现,所以这里执行会失败
- proc结构体中添加一个数据字段,用于保存trace的参数
// kernel/proc.h
struct proc {
// ...
int trace_mask; // trace系统调用参数
};
- 在sys_trace()的实现中实现参数的保存
// kernel/sysproc.c
uint64
sys_trace(void)
{
// 获取系统调用的参数
argint(0, &(myproc()->trace_mask));
return 0;
}
- 由于struct proc中增加了一个新的变量,当fork的时候我们也需要将这个变量传递到子进程中(提示中已说明)
//kernel/proc.c
int
fork(void)
{
// ...
safestrcpy(np->name, p->name, sizeof(p->name));
//将trace_mask拷贝到子进程
np->trace_mask = p->trace_mask;
pid = np->pid;
// ...
return pid;
}
- 接下来应当考虑如何进行系统调用追踪了,根据提示,这将在syscall()函数中实现。下面是实现代码,需要注意的是条件判断中使用了&而不是==,这是因为在实验说明书的例子中,trace 2147483647 grep hello README将所有31个低位置为1,使得其可以追踪所有的系统调用。
void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7; // 系统调用编号,参见书中4.3节
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num](); // 执行系统调用,然后将返回值存入a0
// 系统调用是否匹配 -- 位运算判断
//如果我们要追踪read,那么trace_mask的值为32,也就是10000
//假如当前系统调用号为5,那么1左移五位为: 10000
//此时相与得到1,说明是我们需要追踪的系统调用,则进行打点记录
if ((1 << num) & p->trace_mask)
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;
}
}
这里需要注意一点: 我们是通过位运算来判断当前是否需要对某个系统调用进行追踪的,例如: 如果要追踪read系统调用,由于read系统调用号为5,所以我们将二进制第五位设置为1,也就是32。
- 在上面的代码中,我们还有一些引用的变量尚未定义,在syscall.c中定义他们
// ...
extern uint64 sys_trace(void);
static uint64 (*syscalls[])(void) = {
// ...
[SYS_trace] sys_trace,
};
static char *syscalls_name[] = {
[SYS_fork] "fork",
[SYS_exit] "exit",
[SYS_wait] "wait",
[SYS_pipe] "pipe",
[SYS_read] "read",
[SYS_kill] "kill",
[SYS_exec] "exec",
[SYS_fstat] "fstat",
[SYS_chdir] "chdir",
[SYS_dup] "dup",
[SYS_getpid] "getpid",
[SYS_sbrk] "sbrk",
[SYS_sleep] "sleep",
[SYS_uptime] "uptime",
[SYS_open] "open",
[SYS_write] "write",
[SYS_mknod] "mknod",
[SYS_unlink] "unlink",
[SYS_link] "link",
[SYS_mkdir] "mkdir",
[SYS_close] "close",
[SYS_trace] "trace",
};
- 测试输出结果如下:
实现思路小结
实现步骤总共两步:
- 补齐暴露给用户库中trace系统调用相关定义项
- 补齐内核库中trace系统调用函数的实现
系统调用追踪思路:
trace_mask就是一个位图,每个进程执行系统调用时,再获取当前系统调用号,通过移位得到对应的位图,与自身的trace_mask位图相与,判断得到结果是否为1,如果是说明当前系统调用号被监听了,需要输出对应的打点信息。
很重要的一点是,只要通过trace父进程创建的子进程才会被设置trace_mask。
Sysinfo(moderate)
YOUR JOB:
- 在这个作业中,您将添加一个系统调用sysinfo,它收集有关正在运行的系统的信息。
- 系统调用采用一个参数:
- 一个指向struct sysinfo的指针(参见kernel/sysinfo.h)。
- 内核应该填写这个结构的字段:
- freemem字段应该设置为空闲内存的字节数
- nproc字段应该设置为state字段不为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中添加一个函数
实验解析
本实验中在暴露给用户的user库中已经提供好了相关的sinfo程序让用户进行调用:
//user/sysinfotest.c
void
sinfo(struct sysinfo *info) {
if (sysinfo(info) < 0) {
printf("FAIL: sysinfo failed");
exit(1);
}
}
- 在Makefile的UPROGS中添加$U/_sysinfotest
- 将系统调用原型添加到user/user.h头文件中 --> 要在user/user.h中声明sysinfo()的原型,需要预先声明struct sysinfo的存在
struct sysinfo;
int sysinfo(struct sysinfo *);
- 将存根添加到user/usys.pl
entry("sysinfo");
- 将系统调用编号添加到kernel/syscall.h中
#define SYS_sysinfo 23
- 执行make clean 和 make qemu 命令,查看usys.S是否生成,是否符合我们的预期
- 尝试执行sysinfotest命令,此时由于我们还没有在内核中提供trace系统调用的具体实现,所以这里执行会失败
- 在kernel/kalloc.c中添加一个函数用于获取空闲内存量
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
内存是使用链表进行管理的,因此遍历kmem中的空闲链表就能够获取所有的空闲内存,如下
void
freebytes(uint64 *dst)
{
*dst = 0;
struct run *p = kmem.freelist; // 用于遍历
acquire(&kmem.lock);
while (p) {
// 统计空闲页数,乘上页大小 PGSIZE 就是空闲的内存字节数
*dst += PGSIZE;
p = p->next;
}
release(&kmem.lock);
}
xv6 中,空闲内存页的记录方式是,将空闲内存页本身直接用作链表节点,形成一个空闲页链表,每次需要分配,就把链表根部对应的页分配出去。每次需要回收,就把这个页作为新的根节点,把原来的 freelist 链表接到后面。注意这里是直接使用空闲页本身作为链表节点,所以不需要使用额外空间来存储空闲页链表,在 kalloc() 里也可以看到,分配内存的最后一个阶段,是直接将 freelist 的根节点地址(物理地址)返回出去了:
// kernel/kalloc.c
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist; // 获得空闲页链表的根节点
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r; // 把空闲页链表的根节点返回出去,作为内存页使用(长度是 4096)
}
常见的记录空闲页的方法有:空闲表法、空闲链表法、位示图法(位图法)、成组链接法。这里 xv6 采用的是空闲链表法。
- 在kernel/proc.c中添加一个函数获取进程数
遍历proc数组,统计处于活动状态的进程即可,循环的写法参考scheduler函数
void
procnum(uint64 *dst)
{
*dst = 0;
struct proc *p;
// 不需要锁进程 proc 结构,因为我们只需要读取进程列表,不需要写
for (p = proc; p < &proc[NPROC]; p++) {
// 不是 UNUSED 的进程位,就是已经分配的
if (p->state != UNUSED)
(*dst)++;
}
}
- 内核的头文件中添加函数声明 --> kernel/defs.h
void freebytes(uint64 *dst);
void procnum(uint64 *dst);
- 实现sys_sysinfo,将数据写入结构体并传递到用户空间 --> 在kernel/sysproc.c文件中编写
uint64
sys_sysinfo(void)
{
struct sysinfo info;
freebytes(&info.freemem);
procnum(&info.nproc);
// a0寄存器作为系统调用的参数寄存器,从中取出存放 sysinfo 结构的用户态缓冲区指针
uint64 dstaddr;
argaddr(0, &dstaddr);
// 使用 copyout,结合当前进程的页表,获得进程传进来的指针(逻辑地址)对应的物理地址
// 然后将 &sinfo 中的数据复制到该指针所指位置,供用户进程使用。
if (copyout(myproc()->pagetable, dstaddr, (char *)&info, sizeof info) < 0)
return -1;
return 0;
}
kernel/sysproc.c中记得引入sysinfo结构体定义所在的头文件:
//sysinfo.h具体定义在kernel/sysinfo.h文件中
#include "sysinfo.h"
struct sysinfo {
uint64 freemem; // amount of free memory (bytes)
uint64 nproc; // number of process
};
- 在系统调用列表中补充我们的新添加的sysinfo系统调用 --> kernel/syscall.c
11. 测试运行结果:
可选的挑战
感兴趣的小伙伴可以去做一下可选的挑战:
- 打印所跟踪的系统调用的参数(easy)。
- 计算平均负载并通过sysinfo导出(moderate)。