xv6-labs-2020.lab1.syscall

lab1 syscall

作业链接

  • https://pdos.csail.mit.edu/6.828/2020/labs/syscall.html

xv6-RISCV 的系统调用

  • 首先用户态程序通过系统调用 ecall 进入内核态,硬件做好一些简单的环境配置,然后进入 trampoline.S/uservec() 程序段,接着通过 syscall() 函数选择具体的系统调用,接着是硬件实现或者软件实现(我们这里做的功能)具体功能。

实验 1: 添加 trace 系统调用

  • 添加一个系统调用 trace

1. 配置

  • Makefile 中把 $U/_trace 添加到 UPROGS

2. 用户态准备工作

2.1 user/user.h

  • user/user.h 中添加函数原型
1
int trace(int);

2.2 user/usys.pl

  • user/usys.pl 中添加入口
1
entry("trace");
  • usys.pl 的作用是生成 usys.S 汇编代码
  • 生成用户态的系统调用汇编代码
    • 如下是生成的 trace 系统调用汇编代码
1
2
3
4
5
.global trace
trace:
li a7, SYS_trace
ecall
ret

2.3 kernel/syscall.h

  • kernel/syscall.h 中添加系统调用号
1
#define SYS_trace  22

3. 内核代码准备工作

3.1 kernel/syscall.c

  • 添加 sys_trace() 的定义和入口
1
2
3
4
5
6
extern uint64 sys_trace(void);

static uint64 (*syscalls[])(void) = {
// 省略原来的入口地址
[SYS_trace] sys_trace,
};

3.2 kernel/sysproc.c

  • 添加 sys_trace() 的具体实现
1
2
3
4
5
6
7
8
uint64 sys_trace(void) {
// 这里没有做具体的实现
// 只是测试
// 具体实现见后面
struct proc *p = myproc();
printf("syscall: trace(pid: %d)\n", p->pid);
return 0;
}
  • 做到这个时候,trace 系统调用已经可以执行了
1
$ trace 32 grep hello README
1
syscall: trace(pid: 3)

4. 内核代码功能实现

4.1 实现方式

  • 在调用 trace 的时候,在进程的 proc 结构中保存一个新的变量 trace_mask,从而在该进程进行系统调用的产生系统调用的输出

4.2 kernel/proc.h

  • proc 数据结构中加入新的变量
1
2
// 添加在最后面, 感觉不需要加锁
int trace_mask;

4.3 kernel/sysproc.c

  • 记录 trace 的需要记录的系统调用掩码
1
2
3
4
5
6
7
8
9
uint64 sys_trace(void) {
struct proc *p = myproc();
// 简单在 proc 结构中记录一个参数掩码
int trace_mask;
if(argint(0, &trace_mask) < 0)
return -1;
p->trace_mask = trace_mask;
return 0;
}

4.4 kernel/syscall.c

  • 加入一个字符数组,通过系统调用号来找到名字
1
2
3
4
5
static char* syscall_names[] = {
[SYS_fork] "fork",
// ...
[SYS_trace] "trace",
};
  • 在 syscall 函数中做是否需要输出的判断
1
2
3
4
5
6
7
8
9
10
11
12
13
void syscall(void) {
// ...
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
// 返回的时候输出 trace_mask 对应的系统调用
if((1 << num) & p->trace_mask) {
printf("%d: syscall %s -> %d\n",
p->pid, syscall_names[num], p->trapframe->a0);
}
} else {
// ...
}
}

4.5 kernel/proc.c

  • fork() 后,trace 同时要跟踪子进程
1
2
3
4
5
6
7
int fork(void) {
// ...
// 复制 trace_mask
np->trace_mask = p->trace_mask;
release(&np->lock);
return pid;
}
  • freeproc() 函数中要把 p->trace_mask 置为 0
1
p->trace_mask = 0;

5. 一些问题

  • 怎么保证了初始化的结果为全 0
    • 怎么保证 trace_mask 初始化的时候为 0

实验 2: 添加 sysinfo 系统调用

  • 添加一个系统调用 sysinfo

1. 配置

  • Makefile 中把 $U/_sysinfotest 添加到 UPROGS

2. 用户态准备工作

2.1 user/user.h

  • user/user.h 中添加函数原型
1
2
struct sysinfo;
int sysinfo(struct sysinfo *);

2.2 user/usys.pl

  • user/usys.pl 中添加入口
1
entry("sysinfo");

2.3 kernel/syscall.h

  • kernel/syscall.h 中添加系统调用号
1
#define SYS_trace  23

3. 内核代码准备工作

3.1 kernel/syscall.c

  • 添加 sys_sysinfo() 的定义和入口
1
2
3
4
5
6
extern uint64 sys_sysinfo(void);

static uint64 (*syscalls[])(void) = {
// 省略原来的入口地址
[SYS_sysinfo] sys_sysinfo,
};

3.2 kernel/sysproc.c

  • 添加 sys_sysinfo() 的具体实现
1
2
3
4
uint64 sys_sysinfo(void) {
// 具体实现见后面
return 0;
}

4. 内核代码功能实现

4.1 kernel/sysproc.c

  • 主要逻辑就是下列代码注释中所讲的
  • 通过调用 kernel/kalloc.c 中的函数 get_freemen() 得到空闲的内存大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "sysinfo.h"
uint64 sys_sysinfo(void) {
// 需要写入 struct sysinfo 的地址
uint64 addr;
// 获取指针
if(argaddr(0, &addr) < 0)
return -1;

// 获取数据
struct sysinfo info;
info.freemem = get_freemen();
info.nproc = get_nproc();
// 复制内存内容
struct proc* p = myproc();
// file.c
// 将页表 p->pagetable 中起始地址为 &info 长度为 sizeof(info) 的内存
// 复制到地址 addr
if(copyout(p->pagetable, addr, (char*)(&info), sizeof(info)) < 0)
return -1;
return 0;
}

4.2 kernel/defs.h

  • 加入新添加的函数定义
1
2
uint64          get_freemen(void);
uint64 get_nproc(void);

4.2 kernel/kalloc.c

  • 实现函数 get_freemen(),获取当前空闲内存的字节数
1
2
3
4
5
6
7
8
9
10
11
uint64 get_freemen() {
uint64 num = 0;
struct run *r;
r = kmem.freelist;
// 遍历列表, 获得空闲的 page 数目
while(r){
++num;
r = r->next;
}
return num * PGSIZE;
}
  • struct kmem 记录当前进程的空闲内存信息
  • struct run 是一个链表,记录空闲的内存
1
2
3
4
5
6
7
8
struct run {
struct run *next;
};

struct {
struct spinlock lock;
struct run *freelist;
} kmem;

4.3 kernel/proc.c

  • 实现函数 get_nproc(),获取当前状态为 UNUSED 的进程数
1
2
3
4
5
6
7
8
9
10
11
12
13
uint64 get_nproc(void){
uint num = 0;
uint i = 0;
for(; i < NPROC; ++i) {
// 需要加锁, 详情见 proc.h
acquire(&(proc[i].lock));
if(proc[i].state != UNUSED) {
++num;
}
release(&(proc[i].lock));
}
return num;
}

实验结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ make qemu-gdb
trace 32 grep: OK (11.5s)
== Test trace all grep ==
$ make qemu-gdb
trace all grep: OK (1.7s)
== Test trace nothing ==
$ make qemu-gdb
trace nothing: OK (1.8s)
== Test trace children ==
$ make qemu-gdb
trace children: OK (21.7s)
== Test sysinfotest ==
$ make qemu-gdb
sysinfotest: OK (5.1s)
== Test time ==
time: OK
Score: 35/35

遇到的困难以及收获

  • 做完这个 lab 之后对系统调用的认识更加深入了
  • 困难主要是在于第一次写 lab,刚开始读源代码还是有点吃力,但是读完了收获还是很大的

对课程或 lab 的意见和建议

  • mit 的 lab 想到写的很清晰,循循善诱,让我们一步一步深入 xv6 的内核设计
  • 希望中文版也能跟上,毕竟我们也是自己开课的

参考文献

  • https://pdos.csail.mit.edu/6.828/2019/xv6/book-riscv-rev0.pdf