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#