MIT 6.S081 2020 Lab2 system calls 个人全流程
文章目录
- 零、写在前面
- 一、System call tracing
- 1.1 介绍
- 1.2 流程
- **1、添加trace系统调用的存根(stub)**
- **2、在进程控制块中添加trace 的mask**
- **3、trace 功能的实现**
- **4、systrace字段的清空**
- **5、子进程对于父进程systrace的继承**
- **6、在MakeFile 中添加 $U/_trace\ **
- **7、为什么没有添加trace的定义也能跑?**
 
 
- 二、Sysinfo
- 2.1 介绍
- 2.2 流程
- **1、获取空闲内存数**
- 2、获取已使用进程数
- 3、实现 sysinfo
 
 
 
零、写在前面
警钟长鸣:记得切换分支先
完整代码见:https://github.com/58164/MIT6.S081/tree/syscall
一、System call tracing
1.1 介绍
-  这个任务要求你添加一个tracing 系统调用,来帮你在后续lab中进行debug。 
-  它接收一个int 类型的参数 mask,每一位都代表一个系统调用, 1 代表追踪,0则反之。 
-  在 kernel/syscall.h中我们可以看到每一个二进制位对应的系统调用 
-  在系统调用返回值的时候你应该打印一行,包含如下信息: - pid 系统调用名 返回值
 
-  trace 系统调用可以跟踪调用它的进程以及它后续fork的子进程。 
为了说明,这里贴一个实例:
$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
$
解释:
- 跟踪 32 号系统调用(对应的是read)
- grep 是linux下常用的 根据正则表达式搜索并打印匹配的文本行的指令,这里是在README文件中搜索hello
- 这里即 在grep 运行过程中打印出 read 系统调用的调用情况
- 如果把 32换成 2147583647 ((1 << 31) - 1),即追踪所有系统调用
官网的一些hints:
- 添加 $U/_trace 到 Makefile 的UPROGS中
- 运行 make qemu,你会看到编译器无法编译user/trace.c,因为用户态系统调用的存根(stub)尚不存在:需要在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()函数,通过在结构体 proc 中添加新的变量来保存该系统调用的参数,从而实现新的系统调用。用于从用户态获取调用参数的函数位于kernel/syscall.c,然后你可以在kernel/sysproc.c中看到他们的使用示例。
- 修改 fork 系统调用来把trace的mask 拷贝到子进程。
- 修改 kernel/syscall.c中的syscall()来打印trace 的输出。你需要添加一个系统调用名称数组来以便索引使用。
1.2 流程
1、添加trace系统调用的存根(stub)
先在 kernel/syscall.h 中添加一个系统调用号,这里设为22

然后在 kernel/syscall.c 增加 trace() 的全局函数声明,并且,在 syscalls 这个函数指针数组中增加 sys_trace() 和 SYS_trace 的映射关系。

接着在 user/usys.pl 中,添加 trace 的跳板函数。

当然补药忘了在 user/user.h 声明 trace()

2、在进程控制块中添加trace 的mask
kernel/proc.h 的 proc 结构体中添加 systrace 字段,作为trace 的 mask,记录追踪哪些系统调用。

3、trace 功能的实现
在 kernel/sysproc.c 中,添加 sys_trace() 函数
uint64 
sys_trace(void) {int mask;argint(0, &mask);myproc()->systrace = mask;return 0;
}
关于 argint:
argint 是 xv6 内核中用来从用户栈中提取第 n 个 32 位整型系统调用参数的函数。
- xv6 内核中共有8个参数寄存器 a0 - a7,其中 a7 保存的是系统调用号。寄存器一般保存的是调用 syscall 时传入的参数。
函数原型
int argint(int n, int *ip);返回值
- 成功时返回 0,并通过 *ip存放提取到的参数值。
- 失败时返回 -1(例如参数地址越界)
参数
- n:要获取的第 n 个参数(从 0 开始)。
- ip:指向整型变量的指针,用于接收提取出的参数值。
在 kernel/syscall.c 中,我们添加相关打印的代码:
系统调用名称表:

打印代码:

4、systrace字段的清空
进程控制块proc 回收后的释放函数中,我们需要添加对于 systrace 字段的清空逻辑,即 systrace = 0。
在 kernel/proc.c 中的 freeproc 中修改:

5、子进程对于父进程systrace的继承
这是官网 明确要求的,所以我们在fork中添加对应逻辑:
同样在 kernel/proc.c 中

**6、在MakeFile 中添加 $U/_trace\ **
运行结果
root@LAPTOP-292TJJC6:~/xv6-labs-2021# ./grade-lab-syscall trace
make: 'kernel/kernel' is up to date.
== Test trace 32 grep == trace 32 grep: OK (2.9s)
== Test trace all grep == trace all grep: OK (1.0s)
== Test trace nothing == trace nothing: OK (0.9s)
== Test trace children == trace children: OK (9.9s)
root@LAPTOP-292TJJC6:~/xv6-labs-2021#
我们可以在qemu中也跑一下:
$ 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
6: syscall fork -> 7
test forkforkfork: 6: syscall fork -> 8
8: syscall fork -> 9
9: syscall fork -> 10
9: syscall fork -> 11
9: syscall fork -> 12
10: syscall fork -> 13
9: syscall fork -> 14
10: syscall fork -> 15
9: syscall fork -> 16
9: syscall fork -> 17
12: syscall fork -> 18
10: syscall fork -> 19
10: syscall fork -> 20
9: syscall fork -> 21
10: syscall fork -> 22
10: syscall fork -> 23
10: syscall fork -> 24
10: syscall fork -> 25
10: syscall fork -> 26
9: syscall fork -> 27
9: syscall fork -> 28
9: syscall fork -> 29
12: syscall fork -> 30
12: syscall fork -> 31
9: syscall fork -> 32
10: syscall fork -> 33
10: syscall fork -> 34
9: syscall fork -> 35
9: syscall fork -> 36
9: syscall fork -> 37
10: syscall fork -> 38
9: syscall fork -> 39
9: syscall fork -> 40
9: syscall fork -> 41
9: syscall fork -> 42
10: syscall fork -> 43
10: syscall fork -> 44
10: syscall fork -> 45
10: syscall fork -> 46
10: syscall fork -> 47
10: syscall fork -> 48
10: syscall fork -> 49
10: syscall fork -> 50
10: syscall fork -> 51
11: syscall fork -> 52
11: syscall fork -> 53
11: syscall fork -> 54
9: syscall fork -> 55
10: syscall fork -> 56
10: syscall fork -> 57
9: syscall fork -> 58
10: syscall fork -> 59
10: syscall fork -> 60
10: syscall fork -> 61
10: syscall fork -> 62
10: syscall fork -> 63
10: syscall fork -> 64
9: syscall fork -> 65
9: syscall fork -> 66
10: syscall fork -> 67
10: syscall fork -> 68
9: syscall fork -> -1
10: syscall fork -> -1
11: syscall fork -> -1
OK
6: syscall fork -> 69
ALL TESTS PASSED
7、为什么没有添加trace的定义也能跑?
因为 perl 脚本 usys.pl,编译期间执行脚本生成usys.S` 文件并将 trace 的实现存放其中,也就是说,我们没有trace的C语言实现反而由脚本自动生成其汇编实现。
usys.S:

关于 ecall
- RISC-V 中的“陷入” 指令,用于 用户态 向 内核态的转变。
sys.s 把宏 SYS_trace 保存在寄存器a7当中,然后执行 ecall 进入kernel mode并跳转到 syscall.c 的 syscall 函数。syscall 从a7读出宏SYS_trace至变量num,syscalls[num] 调了函数sys_trace,并把sys_trace的返回值保存在寄存器a0并返回,usys.S在 ecall 后顺序执行ret返回,至此trace函数得到返回值,一次system call结束。
二、Sysinfo
2.1 介绍
在该作业中,你需要添加一个 sysinfo 系统调用,用于收集系统运行时信息。这个系统调用接收一个参数:一个 struct sysinfo 的指针。内核填充该结构体的字段:
- freemem:设置为空闲内存的字节数
- nproc:设置为 状态不是- UNUSED的进程数
- 提供的 sysinfotest程序用来测试,如果你通过了,那么会打印:“sysinfotest: OK”.
官网的一些hints:
- 添加 $U/_sysinfotest到 Makefile 的 UPROGS
- 运行 make qemu 会编译失败。需要和前一个作业一样,在 user/user.h中声明 sysinfo(),当然也要在该函数前面添加 sysinfo 的声明
- sysinfo 需要把 struct sysinfo 拷贝回 用户空间,你可以在 kernel/sysfile.c的sys_fstat()和kernel/file.c的filestat()中来查看关于如何使用copyout()的示例
- 为了得到空闲内存数目,在 kernel/kalloc.c中添加一个函数
- 为了得到已使用进程数目,在 kernel/proc.c中添加一个函数
2.2 流程
1、获取空闲内存数
在 defs.h 中添加 kFreeMemNum() 函数声明,用于获取空闲内存数目

在 kalloc.c 中实现 kFreeMemNum
uint64 kFreeMemNum(void) {acquire(&kmem.lock);uint64 res = 0;struct run *r = kmem.freelist;while (r) {res += PGSIZE;r = r->next;}release(&kmem.lock);return res;
}
- 值得一提的是,内存块链的管理采用了侵入式链表,这也是操作系统内核中应用非常广泛的一种链表组织,将数据和结构解耦合并且拥有更好的内存友好性。
2、获取已使用进程数
在 defs.h 中添加 kProcCnt() 函数声明,用于获取已使用的进程数目

在 proc.c 中实现 kProcCnt
uint64 kProcCnt(void) {struct proc *p;uint64 res = 0;for (p = proc; p < &proc[NPROC]; p++) {res += p->state != UNUSED ? 1 : 0;}return res;
}
3、实现 sysinfo
和上一个作业一样,先去把存根添加下。
kernel/syscall.h 中添加系统调用号:

kernel/syscall.h 中添加 sys_sysinfo 的函数声明,同时记录到 调用表和调用名称表
//...
extern uint64 sys_sysinfo(void);static uint64 (*syscalls[])(void) = {
//...
[SYS_sysinfo]   sys_sysinfo, // here
};// name for syscall
const char *syscall_names[] = {// ...[SYS_sysinfo]   "sysinfo",
};
//...
user/usy.pl 中添加跳板函数

user/user.h 中添加 结构体sysinfo 的声明以及 sysinfo 的函数声明

然后在kernel/sysproc.c中 添加 sys_sysinfo 的实现
- 从参数寄存器a0读取用户传入地址
- 创建 sysinfo 结构体 s
- 填写 s 的字段
- 拷贝到进程页表的addr处
uint64 sys_sysinfo(void) {uint64 addr;// 从 参数寄存器a0 读取用户传入地址if(argaddr(0, &addr) < 0)return -1;struct sysinfo s;s.freemem = kFreeMemNum();s.nproc = kProcCnt();// copyout 从 s 处的 sizeof(s) 字节拷贝到 指定页表的虚拟内存 addr处if(copyout(myproc()->pagetable, addr, (char *)&s, sizeof(s)) < 0)return -1;return 0;
}然后在makefile 中添加 $U/_sysinfotest\

我们在qemu 中运行下:
xv6 kernel is bootinghart 1 starting
hart 2 starting
init: starting sh
$ sysinfotest
sysinfotest: start
sysinfotest: OK
$
测试脚本:
root@LAPTOP-292TJJC6:~/xv6-labs-2021# ./grade-lab-syscall sysinfo
make: 'kernel/kernel' is up to date.
== Test sysinfotest == sysinfotest: OK (2.1s)(Old xv6.out.sysinfotest failure log removed)
root@LAPTOP-292TJJC6:~/xv6-labs-2021#

