实验前须知
阅读 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
中。
使用下面的命令切换到 syscall
分支。
$ git fetch
$ git checkout syscall
$ make clean
推荐新建文件夹,重新使用下面的命令下载代码作为实验 2 工作区。
$ git clone git://g.csail.mit.edu/xv6-labs-2020
$ git checkout syscall
System call tracing (moderate)
实验目的
- 添加一个系统调用跟踪功能,该功能可以在以后的实验中为你提供帮助。
- 你将创建一个新的
trace
系统调用来控制跟踪。 - 它应该有一个参数,一个整数“mask(掩码)”,其指定要跟踪的系统调用。例如,为了跟踪
fork
系统调用,程序调用trace (1 << SYS_fork)
,其中SYS_fork
是来自kernel/syscall.h
的系统调用号。 - 如果掩码中设置了系统调用的编号,则必须修改 xv6 内核以在每个系统调用即将返回时打印出一行。
- 该行应包含 进程 ID 、 系统调用名称 和 返回值 ;您不需要打印系统调用参数。
trace
系统调用应该为调用它的进程和它随后派生的任何子进程启用跟踪,但不应影响其他进程。
实验要求及提示
- 将
$U/_trace
添加到Makefile
的UPROGS
中 - 运行
make qemu
, 你将看到编译器无法编译user/trace.c
,因为系统调用的用户空间存根还不存在:将系统调用的原型添加到user/user.h
,将存根添加到user/usys.pl
,以及将系统调用号添加到kernel/syscall.h
中。Makefile
调用 perl 脚本user/usys.pl
,它生成user/usys.S
,实际的系统调用存根,它使用 RISC-Vecall
指令转换到内核。修复编译问题后,运行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()
函数以打印跟踪输出。你将需要添加要索引的系统调用名称数组。
实验思路
- 首先,根据实验前须知阅读 xv6 文档的第 2 章和第 4 章的 4.3 节和 4.4 节以及相关源文件。其中第 2 章讲的是 xv6 系统的组织结构,第 4 章的 4.3 节讲的是调用 system call 的过程,第 4 章的 4.4 节讲的是调用 system call 的参数。与本实验直接相关,所以必须依照源码进行阅读。
- 这里补充一点,做这个实验需要对 xv6 启动过程以及调用系统调用过程有一些了解,具体可以观看上课视频的结尾部分,视频地址:https://www.bilibili.com/video/BV19k4y1C7kA?p=2
- 具体过程解释见下面的实验步骤。
实验步骤
作为一个系统调用,我们先要定义一个系统调用的序号。系统调用序号的宏定义在 kernel/syscall.h 文件中。我们在 kernel/syscall.h 添加宏定义,模仿已经存在的系统调用序号的宏定义,我们定义 SYS_trace
如下:
#define SYS_trace 22
查看了一下 user 目录下的文件,发现官方已经给出了用户态的 trace
函数( user/trace.c ),所以我们直接在 user/user.h 文件中声明用户态可以调用 trace
系统调用就好了,但有一个问题,该系统调用的参数和返回值分别是什么类型呢?接下来我们还是得看一看 trace.c 文件,可以看到 trace(atoi(argv[1])) < 0
,即 trace
函数传入的是一个数字,并和 0
进行比较,结合实验提示,我们知道传入的参数类型是 int
,并且由此可以猜测到返回值类型应该是 int
。这样就可以把 trace
这个系统调用加入到内核中声明了:
// system calls
int trace(int);
接下来我们查看 user/usys.pl 文件,这里 perl 语言会自动生成汇编语言 usys.S ,是用户态系统调用接口。所以在 user/usys.pl 文件加入下面的语句:
entry("trace");
如果你编译后查看 usys.S 文件,就能可以看到存在把系统调用号放入 a7
寄存器的指令,然后就直接使用命令 ecall
进入系统内核。不信我们先查看上一次实验编译后的 usys.S 文件,可以看到如下的代码块:
.global fork
fork:
li a7, SYS_fork
ecall
ret
li a7, SYS_fork
指令就是把 SYS_fork
的系统调用号放入 a7
寄存器,使用 ecall
指令进入系统内核。
那么,执行 ecall
指令会跳转到哪里呢?答案是跳转到 kernel/syscall.c 中 syscall
那个函数处,执行此函数。下面是 syscall
函数的源码:
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]();
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
其中,我们可以看到, num = p->trapframe->a7;
从寄存器 a7
中读取系统调用号,所以上面的 usys.S 文件就是系统调用用户态和内核态的切换接口。接下来是 p->trapframe->a0 = syscalls[num]();
语句,通过调用 syscalls[num]();
函数,把返回值保存在了 a0
寄存器中。我们看看 syscalls[num]();
函数,这个函数在当前文件中。该函数调用了系统调用命令。
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
...
}
所以我们把新增的 trace
系统调用添加到函数指针数组 *syscalls[]
上:
static uint64 (*syscalls[])(void) = {
...
[SYS_trace] sys_trace,
};
接下来在文件开头给内核态的系统调用 trace
加上声明,在 kernel/syscall.c 加上:
extern uint64 sys_trace(void);
在实现这个函数之前,我们可以看到实验最后要输出每个系统调用函数的调用情况,依照实验说明给的示例,可以知道最后输出的格式如下:
<pid>: syscall <syscall_name> -> <return_value>
其中, <pid>
是进程序号, <syscall_name>
是函数名称, <return_value>
是该系统调用的返回值。注意:冒号和 syscall
中间有个空格,刚开始的时候自己就踩了一个坑。
根据提示,我们的 trace
系统调用应该有一个参数,一个整数“mask(掩码)”,其指定要跟踪的系统调用。所以,我们在 kernel/proc.h 文件的 proc
结构体中,新添加一个变量 mask
,使得每一个进程都有自己的 mask
,即要跟踪的系统调用。
struct proc {
...
int mask; // Mask
};
然后我们就可以在 kernel/sysproc.c 给出 sys_trace
函数的具体实现了,只要把传进来的参数给到现有进程的 mask
就好了:
uint64
sys_trace(void)
{
int mask;
// 取 a0 寄存器中的值返回给 mask
if(argint(0, &mask) < 0)
return -1;
// 把 mask 传给现有进程的 mask
myproc()->mask = mask;
return 0;
}
接下来我们就要把输出功能实现,因为 RISCV 的 C 规范是把返回值放在 a0
中,所以我们只要在调用系统调用时判断是不是 mask
规定的输出函数,如果是就输出。
因为 proc
结构体(见 kernel/proc.h )里的 name
是整个线程的名字,不是函数调用的函数名称,所以我们不能用 p->name
,而要自己定义一个数组,我这里直接在 kernel/syscall.c 中定义了,这里注意系统调用名字一定要按顺序,第一个为空,当然你也可以去掉第一个空字符串,但要记得取值的时候索引要减一,因为这里的系统调用号是从 1
开始的。
static char *syscall_names[] = {
"", "fork", "exit", "wait", "pipe",
"read", "kill", "exec", "fstat", "chdir",
"dup", "getpid", "sbrk", "sleep", "uptime",
"open", "write", "mknod", "unlink", "link",
"mkdir", "close", "trace"};
然后我们就可以在 kernel/syscall.c 中的 syscall
函数中添加打印调用情况语句。 mask
是按位判断的,所以判断使用的是按位运算。进程序号直接通过 p->pid
就可以取到,函数名称需要从我们刚刚定义的数组中获取,即 syscall_names[num]
,其中 num
是从寄存器 a7
中读取的系统调用号,系统调用的返回值就是寄存器 a0
的值了,直接通过 p->trapframe->a0
语句获取即可。注意上面说的那个空格。
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((1 << num) & p->mask) {
printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num], p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
然后在 kernel/proc.c 中 fork
函数调用时,添加子进程复制父进程的 mask
的代码:
int
fork(void)
{
...
pid = np->pid;
np->state = RUNNABLE;
// 子进程复制父进程的 mask
np->mask = p->mask;
...
}
最后在 Makefile
的 UPROGS
中添加:
UPROGS=\
...
$U/_trace\
实验结果
编译并运行 xv6 进行测试。
$ make qemu
...
init: starting sh
$ 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
...
$
退出 xv6 ,运行单元测试检查结果是否正确。
./grade-lab-syscall trace
通过测试样例。
== Test trace 32 grep == trace 32 grep: OK (2.6s)
== Test trace all grep == trace all grep: OK (1.0s)
== Test trace nothing == trace nothing: OK (0.5s)
== Test trace children == trace children: OK (10.8s)
Sysinfo (moderate)
实验要求
在本实验中,您将添加一个系统调用 sysinfo
,它收集有关正在运行的系统信息。系统调用接受一个参数:一个指向 struct sysinfo
的指针(参见 kernel/sysinfo.h )。内核应该填写这个结构体的字段: freemem
字段应该设置为空闲内存的字节数, nproc
字段应该设置为状态不是 UNUSED 的进程数。我们提供了一个测试程序 sysinfotest
;如果它打印 “sysinfotest:OK”
,则实验结果通过测试。
实验提示
- 将
$U/_sysinfotest
添加到Makefile
的UPROGS
中。 - 运行
make qemu
, 你将看到编译器无法编译user/sysinfotest.c
。添加系统调用sysinfo
,按照与之前实验相同的步骤。要在 user/user.h 中声明sysinfo()
的原型,您需要预先声明struct sysinfo
:
struct sysinfo;
int sysinfo(struct sysinfo *);
- 修复编译问题后,运行
sysinfotest
会失败,因为你还没有在内核中实现系统调用。 sysinfo
需要复制一个struct sysinfo
返回用户空间;有关如何使用copyout()
执行此操作的示例,请参阅sys_fstat()
( kernel/sysfile.c ) 和filestat()
( kernel/file.c )。- 要收集空闲内存量,请在 kernel/kalloc.c 中添加一个函数。
- 要收集进程数,请在 kernel/proc.c 中添加一个函数。
实验步骤
跟上个实验一样,首先定义一个系统调用的序号。系统调用序号的宏定义在 kernel/syscall.h 文件中。我们在 kernel/syscall.h 添加宏定义 SYS_sysinfo
如下:
#define SYS_sysinfo 23
在 user/usys.pl 文件加入下面的语句:
entry("sysinfo");
然后在 user/user.h 中添加 sysinfo
结构体以及 sysinfo
函数的声明:
struct stat;
struct rtcdate;
// 添加 sysinfo 结构体
struct sysinfo;
// system calls
...
int sysinfo(struct sysinfo *);
在 kernel/syscall.c 中新增 sys_sysinfo
函数的定义:
extern uint64 sys_sysinfo(void);
在 kernel/syscall.c 中函数指针数组新增 sys_trace
:
[SYS_sysinfo] sys_sysinfo,
记得在 kernel/syscall.c 中的 syscall_names
新增一个 sys_trace
:
static char *syscall_names[] = {
"", "fork", "exit", "wait", "pipe",
"read", "kill", "exec", "fstat", "chdir",
"dup", "getpid", "sbrk", "sleep", "uptime",
"open", "write", "mknod", "unlink", "link",
"mkdir", "close", "trace", "sysinfo"};
接下来我们就要开始写相应的函数实现了。
首先我们写获取可用进程数目的函数实现。通过阅读 kernel/proc.c 文件可以看到下面的语句:
struct proc proc[NPROC];
这是一个进程数组的定义,这里保存了所有的进程。我们再阅读 kernel/proc.h 查看进程结构体的定义:
enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
int mask; // Mask
};
可以看到,进程里面已经保存了当前进程的状态,所以我们可以直接遍历所有进程,获取其状态判断当前进程的状态是不是为 UNUSED
并统计数目就行了。当然,通过 proc
结构体的定义,我们知道使用进程状态时必须加锁,我们在 kernel/proc.c 中新增函数 nproc
如下,通过该函数以获取可用进程数目:
// Return the number of processes whose state is not UNUSED
uint64
nproc(void)
{
struct proc *p;
// counting the number of processes
uint64 num = 0;
// traverse all processes
for (p = proc; p < &proc[NPROC]; p++)
{
// add lock
acquire(&p->lock);
// if the processes's state is not UNUSED
if (p->state != UNUSED)
{
// the num add one
num++;
}
// release lock
release(&p->lock);
}
return num;
}
接下来我们来实现获取空闲内存数量的函数。可用空间的判断在 kernel/kalloc.c 文件中。
这里定义了一个链表,每个链表都指向上一个可用空间,这里的 kmem
就是一个保存最后链表的变量。
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
要想更深入了解的话就详细看看当前这个文件(下面摘了部分内容):
extern char end[]; // first address after kernel.
// defined by kernel.ld.
void
kinit()
{
initlock(&kmem.lock, "kmem");
freerange(end, (void*)PHYSTOP);
}
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
这里把从 end (内核后的第一个地址)
到 PHYSTOP (KERNBASE + 128*1024*1024)
之间的物理空间以 PGSIZE
为单位全部初始化为 1
,然后每次初始化一个 PGSIZE
就把这个页挂在了 kmem.freelist
上,所以 kmem.freelist
永远指向最后一个可用页,那我们只要顺着这个链表往前走,直到 NULL
为止。所以我们就可以在 kernel/kalloc.c 中新增函数 free_mem
,以获取空闲内存数量:
// Return the number of bytes of free memory
uint64
free_mem(void)
{
struct run *r;
// counting the number of free page
uint64 num = 0;
// add lock
acquire(&kmem.lock);
// r points to freelist
r = kmem.freelist;
// while r not null
while (r)
{
// the num add one
num++;
// r points to the next
r = r->next;
}
// release lock
release(&kmem.lock);
// page multiplicated 4096-byte page
return num * PGSIZE;
}
然后在 kernel/defs.h 中添加上述两个新增函数的声明:
// kalloc.c
...
uint64 free_mem(void);
// proc.c
...
uint64 nproc(void);
接下来我们按照实验提示,添加 sys_sysinfo
函数的具体实现,这里提到 sysinfo
需要复制一个 struct sysinfo
返回用户空间,根据实验提示使用 copyout()
执行此操作,我们查看 kernel/sysfile.c 文件中的 sys_fstat()
函数,如下:
uint64
sys_fstat(void)
{
struct file *f;
uint64 st; // user pointer to struct stat
if(argfd(0, 0, &f) < 0 || argaddr(1, &st) < 0)
return -1;
return filestat(f, st);
}
这里可以看到调用了 filestat()
函数,该函数在 kernel/file.c 中,如下:
// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
struct proc *p = myproc();
struct stat st;
if(f->type == FD_INODE || f->type == FD_DEVICE){
ilock(f->ip);
stati(f->ip, &st);
iunlock(f->ip);
if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
return -1;
return 0;
}
return -1;
}
我们可以知道,复制一个 struct sysinfo
返回用户空间需要调用 copyout()
函数,上面是一个例子,我们来查看一下 copyout()
函数的定义( kernel/vm.c ):
// 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 pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
n = PGSIZE - (dstva - va0);
if(n > len)
n = len;
memmove((void *)(pa0 + (dstva - va0)), src, n);
len -= n;
src += n;
dstva = va0 + PGSIZE;
}
return 0;
}
我们知道该函数其实就是把在内核地址 src
开始的 len
大小的数据拷贝到用户进程 pagetable
的虚地址 dstva
处,所以 sys_sysinfo
函数实现里先用 argaddr
函数读进来我们要保存的在用户态的数据 sysinfo
的指针地址,然后再把从内核里得到的 sysinfo
开始的内容以 sizeof(info)
大小的的数据复制到这个指针上。模仿上面的例子,我们在 kernel/sysproc.c 文件中添加 sys_sysinfo
函数的具体实现如下:
// add header
#include "sysinfo.h"
uint64
sys_sysinfo(void)
{
// addr is a user virtual address, pointing to a struct sysinfo
uint64 addr;
struct sysinfo info;
struct proc *p = myproc();
if (argaddr(0, &addr) < 0)
return -1;
// get the number of bytes of free memory
info.freemem = free_mem();
// get the number of processes whose state is not UNUSED
info.nproc = nproc();
if (copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)
return -1;
return 0;
}
最后在 user 目录下添加一个 sysinfo.c 用户程序:
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/sysinfo.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
// param error
if (argc != 1)
{
fprintf(2, "Usage: %s need not param\n", argv[0]);
exit(1);
}
struct sysinfo info;
sysinfo(&info);
// print the sysinfo
printf("free space: %d\nused process: %d\n", info.freemem, info.nproc);
exit(0);
}
最后在 Makefile 的 UPROGS
中添加:
$U/_sysinfotest\
$U/_sysinfo\
实验结果
编译并运行 xv6 进行测试。
$ make qemu
...
init: starting sh
$ sysinfo
free space: 133386240
used process: 3
$ sysinfotest
sysinfotest: start
sysinfotest: OK
退出 xv6 ,运行单元测试检查结果是否正确。
./grade-lab-syscall sysinfo
通过测试样例。
make: 'kernel/kernel' is up to date.
== Test sysinfotest == sysinfotest: OK (2.6s)
Lab 2 所有实验测试
退出 xv6 ,使用命令 vim time.txt
新建文件写入你做该实验所花的时间(小时),运行整个 Lab 2 测试,检查结果是否正确。
$ make grade
...
== Test trace 32 grep ==
$ make qemu-gdb
trace 32 grep: OK (2.3s)
== Test trace all grep ==
$ make qemu-gdb
trace all grep: OK (1.0s)
== Test trace nothing ==
$ make qemu-gdb
trace nothing: OK (0.8s)
== Test trace children ==
$ make qemu-gdb
trace children: OK (10.1s)
== Test sysinfotest ==
$ make qemu-gdb
sysinfotest: OK (2.3s)
== Test time ==
time: OK
Score: 35/35