【MIT6.S081】Lab2 system calls

前言

这个lab开始我们就正式进入了xv6的世界了,这一次我们可以了解到内核中系统调用的注册和运行原理,这可以说是之后lab的一个基石。

trace

首先,我们要清楚这个实验的目的是什么:

In this assignment you will add a system call tracing feature that may help you when debugging later labs. You’ll create a new trace system call that will control tracing. It should take one argument, an integer “mask”, whose bits specify which system calls to trace. For example, to trace the fork system call, a program calls trace(1 << SYS_fork), where SYS_fork is a syscall number from kernel/syscall.h. You have to modify the xv6 kernel to print out a line when each system call is about to return, if the system call’s number is set in the mask. The line should contain the process id, the name of the system call and the return value; you don’t need to print the system call arguments. The trace system call should enable tracing for the process that calls it and any children that it subsequently forks, but should not affect other processes.

译:在Xv6的trace命令中,它应该有一个参数,一个整数“掩码”,其位指定要跟踪的系统调用。例如,要跟踪fork系统调用,程序调用trace(1<<SYS_fork),其中SYS_fork是kernel/syscall.h中的系统调用编号。如果系统调用的编号在掩码中设置,则必须修改xv6内核,以便在每个系统调用即将返回时打印出一行。该行应包含进程id系统调用的名称返回值;您不需要打印系统调用参数。跟踪系统调用应启用对调用它的进程及其随后分叉的任何子进程的跟踪,但不应影响其他进程。

注意,在该实验的初始阶段,xv6已经为我们提供了trace命令的用户态实现,但是其底层的系统调用需要我们自己实现。

mask是什么?

在使用trace命令时用到的掩码,是用来跟踪之后使用的命令用到了哪些系统调用。例如实验中给出的例子:

1
2
3
4
5
$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0

这个32就是掩码,其跟踪到了grep命令中使用到了read系统调用(为什么是read?马上就说到了)。在xv6的kernel/syscall.h中有所有系统调用的编号:

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
// System call numbers
#define SYS_fork 1
#define SYS_exit 2
#define SYS_wait 3
#define SYS_pipe 4
#define SYS_read 5
#define SYS_kill 6
#define SYS_exec 7
#define SYS_fstat 8
#define SYS_chdir 9
#define SYS_dup 10
#define SYS_getpid 11
#define SYS_sbrk 12
#define SYS_sleep 13
#define SYS_uptime 14
#define SYS_open 15
#define SYS_write 16
#define SYS_mknod 17
#define SYS_unlink 18
#define SYS_link 19
#define SYS_mkdir 20
#define SYS_close 21

// 添加
#define SYS_trace 22

将这个mask以二进制的形式来看待更加容易理解,如果传入的mask是32,那么其二进制为100000,这个1出现的位置是第5位(最低位按0计数),也就是去找编号为5的系统调用,也就是SYS_read

xv6内核提供给用户态的接口为trace,但是我们需要自己在xv6的用户头文件中添加函数的声明

1
2
3
4
5
6
7
// user/user.h

// ...

int trace(int);

// ...

这个trace底层其实调用的应该是sys_trace(这个函数名不是固定的,但是源码中其他的系统调用的命名都为sys_*,故trace对应的系统调用写成sys_trace更加合理)。sys_trace需要做的是将用户传入的mask再传给当前进程及其子进程。

我们这里将系统调用sys_trace的编号设置为22

进程及其子进程如何获取mask?

在xv6 book的4.3节中,有这么一段话:

1
2
3
syscall (kernel/syscall.c:132) retrieves the system call number from the saved a7 in the trapframe and uses it to index into syscalls. For the first system call, a7 contains SYS_exec (kernel/syscall.h:8),  resulting in a call to the system call implementation function sys_exec.

When sys_exec returns, syscall records its return value in p->trapframe->a0. This will cause the original user-space call to exec() to return that value, since the C calling convention on RISC-V places return values in a0. System calls conventionally return negative numbers to indicate errors, and zero or positive numbers for success. If the system call number is invalid, syscall prints an error and returns −1.

即系统调用的编号会保存在进程的trapframe中,根据a7寄存器即可获得,系统调用的返回值可通过a0寄存器获得。欸!这两个值可不就是实验实现中需要的吗!那么理所当然,实验中需要打印的语句应该就在这个函数中添加。

让我们来看看xv6中进程的数据结构:

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
// kernel/proc.h

struct proc {
struct spinlock lock;

// p->lock must be held when using these:
enum procstate state; // Process state
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

// wait_lock must be held when using this:
struct proc *parent; // Parent process

// 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 trace_mask;
}

可以看到诸如进程名、pid、上下文等信息都是保存在这个数据结构中,那么我们可以在其中加上一个成员变量 trace_mask 用于保存当前进程所对应trace命令中的掩码mask

xv6已经实现了用户态的trace命令,其位于 user/trace.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 "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];

if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){
fprintf(2, "Usage: %s mask command\n", argv[0]);
exit(1);
}

if (trace(atoi(argv[1])) < 0) {
fprintf(2, "%s: trace failed\n", argv[0]);
exit(1);
}

for(i = 2; i < argc && i < MAXARG; i++){
nargv[i-2] = argv[i];
}
exec(nargv[0], nargv);
exit(0);
}

对于这样的一条命令 trace 32 grep hello README ,假设开启的进程名为p,那么32将会传给p.trace_mask,之后的grep操作将使用exec创建子进程(假设进程名为son)执行,那么在创建子进程后应该有son.trace_mask = p.trace_mask,只有这样,grep操作所用到的系统调用才能被跟踪到。

在使用trace命令时,其后的mask参数会存到a0寄存器中,为了从其中拿到mask,可以使用argint()函数,其源码为:

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
// kernel/syscall.h

// Fetch the nth 32-bit system call argument.
void argint(int n, int *ip) {
*ip = argraw(n);
}

static uint64 argraw(int n) {
struct proc *p = myproc();
switch (n) {
case 0:
return p->trapframe->a0;
case 1:
return p->trapframe->a1;
case 2:
return p->trapframe->a2;
case 3:
return p->trapframe->a3;
case 4:
return p->trapframe->a4;
case 5:
return p->trapframe->a5;
}
panic("argraw");
return -1;
}

argint()内调用了argraw(),在查看以上源码后,由于只传入了一个参数,故应该将0传入argraw中。kernel/sysproc.c中实现sys_trace()

1
2
3
4
5
6
7
8
9
10
11
12
// kernel/sysproc.c

// ...

uint64 sys_trace(void) {
int mask;
argint(0, &mask);
if (mask < 0) return -1;
struct proc *p = myproc();
p->trace_mask = mask;
return 0;
}

这样当trace使用了底层的sys_trace时,就可以把mask参数传递给当前进程。但是只传递给当前进程还不够,还要传给当前进程的子进程。在Linux,我们创建子进程的函数为fork(),在xv6中也同样如此,fork内部先获取当前进程的proc结构体,然后新创建一个proc结构体代表子进程,并将父进程中的值拷贝过去,故传递给子进程的mask也在其中拷贝

1
2
3
4
5
6
7
8
9
10
11
// kernel/proc.c

int fork(void) {
// ...

// copy mask from father process to son process
// np为子进程,p为父进程
np->trace_mask = p->trace_mask;

...
}

要注意一个小细节,当进程结构体被释放时(进程结束或者为进程分配proc结构体),其mask也该重置

1
2
3
4
5
6
// kernel/proc.c

static void freeproc(struct proc *p) {
// ... ...
p->trace_mask = 0;
}

完成好以上内容后,就可以实现sys_trace了:

1
2
3
4
5
6
7
8
9
10
// kernel/sysproc.c

uint64 sys_trace(void) {
int mask;
argint(0, &mask);
if (mask < 0) return -1;
struct proc *p = myproc();
p->trace_mask = mask;
return 0;
}

这样,当前进程就可以获取了到mask,当其创建子进程时,子进程也可获取到mask~

如何跟踪系统调用?

刚刚我们说了mask的作用,还有进程及其子进程如何获取mask,那么我们又应该如何跟踪系统调用呢?

xv6中所用的系统调用都是在 kernel/syscall.c 中的 syscall 函数中调用的,为了能在syscall.c中调用sys_trace,需要在其中添加extern声明(其定义在刚刚已经实现,位于kernel/sysproc.c):

1
extern uint64 sys_trace(void);

同时需要在syscalls数组中添加sys_trace的编号

1
2
3
4
5
6
7
static uint64 (*syscalls[])(void) = {
// ... ...
[SYS_trace] sys_trace,
};

// 这里实际上就是 [22] = sys_trace
// 使用了gcc的一个拓展

并按照顺序添加各个系统调用的名字

1
2
3
4
5
6
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 中,xv6根据a7寄存器获取系统调用的编号,然后通过syscalls函数数组执行系统调用,那么我们的实现为:当使用的系统调用合法时,获取当前进程的mask,并通过判断(mask >> syscall_num) & 1是否为1来输出跟踪信息。

例如sys_read系统调用的编号为5,mask为32,则(32 >> 5) & 1 = 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void syscall(void) {
int num;
struct proc *p = myproc();

num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
// Use num to lookup the system call function for num, call it,
// and store its return value in p->trapframe->a0
p->trapframe->a0 = syscalls[num]();

// ===============================
int mask = p->trace_mask;
if ((mask >> num) & 1) {
printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num - 1], p->trapframe->a0);
}
// ===============================

} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}

那么,内核是如何通过trace找到sys_trace的呢?根据实验指导上的提示,可以知道 user/usys.pl 起到了一个中间人的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# user/usys.pl

...

sub entry {
my $name = shift;
print ".global $name\n";
print "${name}:\n";
print " li a7, SYS_${name}\n";
print " ecall\n";
print " ret\n";
}

...
entry("uptime");
++entry("trace"); # 这是实验中需要由我们自己添加的

通过其中的entry函数,可以生成对应的调用(xv6中为ecall)系统调用的汇编语句,即大致的流程为xv6在构建内核时,会将用户态trace命令对应到:

1
2
3
4
5
.global trace
trace:
li a7, SYS_trace
ecall
ret

这样,当前进程就可以通过a7寄存器拿到sys_trace的系统调用编号了,也就是说,syscall 函数可以调用 sys_trace 了。

OK,那么一个大致的框架就出来了:

完成上面的步骤后,最后只要在Makefile中的UPROGS加上$U/_trace即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
$U/_grep\
$U/_init\
$U/_kill\
$U/_ln\
$U/_ls\
$U/_mkdir\
$U/_rm\
$U/_sh\
$U/_stressfs\
$U/_usertests\
$U/_grind\
$U/_wc\
$U/_zombie\
++ $U/_trace

在这个lab中,我们需要添加一个系统调用sysinfo,用于收集有关正在运行的系统的信息。

系统调用的声明为:

1
int sysinfo(struct sysinfo*);

这个系统调用接受一个指向结构体sysinfo的指针,其定义为:

1
2
3
4
5
6
// kernel/sysinfo.h

struct sysinfo {
uint64 freemem; // amount of free memory (bytes)
uint64 nproc; // number of process
};

内核应填写此结构的字段:freemem字段应设置为可用内存的字节数,nproc字段应设为状态未使用的进程数。

sysinfo

在这个part中,我们需要添加一个系统调用sysinfo,用于收集有关正在运行的系统的信息。

计算可用内存字节数

我们可以通过内核中的kmem来获取可用的内存块的数量:

1
2
3
4
struct {
struct spinlock lock;
struct run *freelist;
} kmem;

kmem.freelist是一个链表,保存了所有可用的内存块的地址,我们遍历这个链表即可获取可用内存块数量,又一个内存块的大小为4KB,那么系统可用的内存字节数 = 可用内存块数量 * 4KB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// kernel/kalloc.c

uint64 freemem() {
struct run* r;
uint64 free_page = 0;
acquire(&kmem.lock);
r = kmem.freelist;
while (r) {
free_page++;
r = r->next;
}
release(&kmem.lock);
// 4K = 2^12,左移操作相当于对2的乘法
return (free_page << 12);
}

计算状态为未为使用进程数

内核中有一个全局数组,其中每一项为系统中的进程,xv6中设置最多进程数为64个:

1
struct proc proc[NPROC];

在表示进程的结构体中,有一个成员表示这个进程的状态:

1
2
3
4
5
6
7
8
9
enum procstate { UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };

// Per-process state
struct proc {
...
// p->lock must be held when using these:
enum procstate state;
...
}

我们可以遍历proc数组,找到所有state != UNUSED的进程的数量(这里一定要看清楚,是状态为未使用的进程数,而不是未使用的进程数):

1
2
3
4
5
6
7
8
9
10
11
12
13
// kernel/proc.c

uint64 nproc() {
struct proc* p;
uint64 not_unused = 0;

for (p = proc; p < &proc[NPROC]; p++) {
if (p->state != UNUSED) {
not_unused++;
}
}
return not_unused;
}

完成系统调用

如何获取用户态传递过来的参数和注册系统调用可以参考这篇博客,这里就不赘述了

我们创建了struct sysinfo结构体变量info后,使用刚刚的freememnproc函数来为结构体变量赋值,之后通过copyout函数将内核态中的info拷贝给用户态的struct sysinfo结构体变量。

这里的实现原理是:用户态下我们使用系统调用传递了一个struct sysinfo指针,其实就是传递了一个内存地址addr;内核态下我们将info中的数据原封不动地搬一份到addr处。这样当用户态访问addr处的内存时就可以获取到想要的数据了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
uint64 sys_sysinfo() {
uint64 addr;
argaddr(0, &addr);
if (addr < 0) {
return -1;
}

struct proc* p = myproc();
struct sysinfo info;

info.freemem = freemem();
info.nproc = nproc();

if (copyout(p->pagetable, addr, (char*)&info, sizeof(info))) {
return -1;
}
return 0;
}