操作系统MIT 6.S081 xv6内核(三):System calls实验
trace实验
trace目的
目的:完成一个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 -> 01
具体实现
根据提示可以轻易完成前面部分:
- 在Makefile的UPROGS中添加$U/_trace
这一步完成以后make qemu
,发现trace是报错的;
- 运行make
qemu,您将看到编译器无法编译
user/trace.c
,因为系统调用的用户空间存根还不存在:将系统调用的原型添加到user/user.h
,存根添加到user/usys.pl
,以及将系统调用编号添加到kernel/syscall.h
,Makefile调用perl脚本user/usys.pl,它生成实际的系统调用存根user/usys.S,这个文件中的汇编代码使用RISC-V的ecall指令转换到内核。也即: 在user/user.h
添加声明:1
int trace(int);
user/usys.pl
添加存根:在1
entry("trace");
kernel/syscall.h
添加编号:这几步主要是函数注册的步骤性问题,为了让编译器能够认识trace,此时的trace还没有被完全实现,因此编译通过也暂时不能实现功能。1
然后就是解决trace调用细节: -
在kernel/sysproc.c中添加一个sys_trace()
函数,它通过将参数保存到proc结构体(请参见kernel/proc.h
)里的一个新变量中来实现新的系统调用。从用户空间检索系统调用参数的函数在kernel/syscall.c
中,您可以在kernel/sysproc.c
中看到它们的使用示例。
修改
fork()
(请参阅kernel/proc.c
)将跟踪掩码从父进程复制到子进程。修改
kernel/syscall.c
中的syscall()
函数以打印跟踪输出。您将需要添加一个系统调用名称数组以建立索引。
也即:
kernel/sysproc.c
中增加系统调用,从陷入帧的寄存器获取参数并且传递到结构体,这个结构体会传递到系统调用函数。1
2
3
4
5
6
7uint64 sys_trace(void){
uint64 trace_mask;
if(argint(0,(int*)&trace_mask)<0)
return -1;
myproc()->trace_mask=trace_mask;
return 0;
}myproc()
是一个指向当前CPU进程指针的结构体,定义在proc.c
中;所以这一步代码就是把cpu进程结构体的trace_mask字段置为用户设定的字段,由于是内核操作,因此要使用argint、argaddr这类函数填充参数,他们均调用了argraw,三个函数均被定义在1
2
3
4
5
6
7struct proc* myproc(void) {
push_off(); //禁用CPU中断,防止代码被中断干扰
struct cpu *c = mycpu(); //获取cpu信息
struct proc *p = c->proc; //获取CPU执行的进程
pop_off(); //重新启用中断
return p; //返回当前cpu进程指针
}kernel/syscall.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
29
30
31
32
33static 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;
}
int argint(int n, int *ip)
{
*ip = argraw(n);
return 0;
}
int argaddr(int n, uint64 *ip)
{
*ip = argraw(n);
return 0;
}kernel/proc.h
中的proc结构体中新加入变量,新的变量存入获取到的掩码,这里说的掩码就是我们之前分析的系统调用编号。1
uint64 trace_mask;
kernel/syscall.c中增加函数声明和数组(
[SYS_trace] sys_trace
这种写法是C语言将SYS_trace
下标元素置为sys_trace
,在C++中已经弃用):1
2extern uint64 sys_trace(void);
[SYS_trace] sys_trace新增结构体,通过系统调用号寻找对应调用字符名称。并且修改系统调用函数:接收掩码,将掩码右移num(这里的num就是系统调用号),与1相与,例如接收掩码32,read调用号为5,最后就是1,与1相与就是1,就打印其pid、系统调用名称(read)、返回值。这里涉及两个寄存器
p->trapframe->a7
用于存放系统调用号,p->trapframe->a0
用于存放调用的返回值。kernel/syscall.c
中至此,make qemu可以得到trace结果,但是还不能ac,因为此时先后运行: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
29
30
31
32
33
34
35
36static char *syscalls_name[] = {
[SYS_fork] "fork",[SYS_exit] "exit",
[SYS_wait] "wait",[SYS_pipe] "pipe",
[SYS_read] "read",[SYS_kill] "kill",
[SYS_exec] "exec",[SYS_fstat] "fstat",
[SYS_chdir] "chdir",[SYS_dup] "dup",
[SYS_getpid] "getpid",[SYS_sbrk] "sbrk",
[SYS_sleep] "sleep",[SYS_uptime] "uptime",
[SYS_open] "open",[SYS_write] "write",
[SYS_mknod] "mknod",[SYS_unlink] "unlink",
[SYS_link] "link",[SYS_mkdir] "mkdir",
[SYS_close] "close",[SYS_trace] "trace",
};
void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7;
//for trace
//uint64 trace_mask=p->trace_mask; 这种写法导致trace无法追踪,见Q&A
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
//系统调用返回值,例如read返回的字节数
p->trapframe->a0 = syscalls[num]();
//for trace
if((p->trace_mask>>num)&1){
printf("%d: syscall %s -> %d\n",p->pid,syscalls_name[num],p->trapframe->a0);
}
}
else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}发现二者结果是一样的,但是我们在第二个命令根本没有指定trace 32,说明有些东西没有被初始化。发现1
2trace 32 grep hello README
grep hello READMEproc.c
文件下的初始化问题,导致重新使用了第一次的trace_mask
,我找到的所有博客都没有提及debug过程,这里简单介绍一下系统调用的入口如何寻找:
进入gdb,还记得sys_trace
这个函数吗,这个函数是从用户空间获取参数,并且传递给内核的结构体,因此我们的第一个断点要打在这里。由于我们是对内核进行调试,所以file命令也没必要了。
1
b sys_trace
- 在函数
freeproc(struct proc *p)
加入p->trace_mask=0
;
最后,我们需要修改fork,fork是子进程将父进程进行拷贝,因此也需要在子进程将父进程的变量进行拷贝:
- 在
kernel/proc.c
中fork()
函数加入np->trace_mask=p->trace_mask
。
至此整个trace实验完成了,输入./grade-lab-syscall trace
验证。
完整commit: Trace Done
Q&A
- trace无法追踪自身的调用,导致
trace 4194304 grep hello README
没有输出,而trace以外的21个调用是正常的。
gdb也试了好久找不出来,最后发现问题出在syscall时我使用了
1
int trace_mask=p->trace_mask
p->trace_mask
进行判断打印,导致二者的生命周期是不对称的,在第5步外面是更改了p->trace_mask
,因此在内核使用局部变量应该更加谨慎,所以应该直接使用p->trace_mask
进行条件判断。
Sysinfo实验
实现目的
Sysinfo实验旨在获取系统的相关消息,包括获取可用的内存、正在运行的进程数目等,与前面的lab不同的是,本次的现象通过sysinfotest这一个用户接口命令判断是否正确,测试文件解析如下:user/sysinfotest.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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142//sysinfotest.c
//系统调用sysinfo,info实际上就是用户空间参数位置,copyout会复制到这里
void
sinfo(struct sysinfo *info) {
if (sysinfo(info) < 0) {
printf("FAIL: sysinfo failed");
exit(1);
}
}
//
// use sbrk() to count how many free physical memory pages there are.
//
//获取空闲内存
int
countfree()
{
uint64 sz0 = (uint64)sbrk(0); //获取当前程序断点位置
struct sysinfo info; //sysinfo包括进程数、内存信息
int n = 0; //记录内存
//PGSIZE为4096(riscv.h)
while(1){
if((uint64)sbrk(PGSIZE) == 0xffffffffffffffff){ //sbrk分配内存失败
break;
}
n += PGSIZE; //分配成功说明有空闲内存,每次加4096;
}
sinfo(&info);
if (info.freemem != 0) {
printf("FAIL: there is no free mem, but sysinfo.freemem=%d\n",
info.freemem);
exit(1);
}
//(uint64)sbrk(0)是当前堆断点位置,sz0是开始的位置,这个命令用于撤销分配的内存
sbrk(-((uint64)sbrk(0) - sz0));
return n; //得到空闲内存数量
}
//测试两种获取内存数量是不是一致且正确的
void
testmem() {
struct sysinfo info;
uint64 n = countfree(); //获取理论内存
sinfo(&info);
if (info.freemem!= n) {
printf("FAIL: free mem %d (bytes) instead of %d\n", info.freemem, n);
exit(1);
}
if((uint64)sbrk(PGSIZE) == 0xffffffffffffffff){
printf("sbrk failed");
exit(1);
}
sinfo(&info);
if (info.freemem != n-PGSIZE) {
printf("FAIL: free mem %d (bytes) instead of %d\n", n-PGSIZE, info.freemem);
exit(1);
}
if((uint64)sbrk(-PGSIZE) == 0xffffffffffffffff){
printf("sbrk failed");
exit(1);
}
sinfo(&info);
//如果不相等,说明我们编写的内存获取是错误的
if (info.freemem != n) {
printf("FAIL: free mem %d (bytes) instead of %d\n", n, info.freemem);
exit(1);
}
}
//测试sysinfo的系统调用是否成功
void
testcall() {
struct sysinfo info;
if (sysinfo(&info) < 0) {
printf("FAIL: sysinfo failed\n");
exit(1);
}
//0xeaeb0b5b00002f5e是一个硬性编码,不大可能会有这个内存区域,所以测试sysinfo能否识别出异常,return -1
if (sysinfo((struct sysinfo *) 0xeaeb0b5b00002f5e) != 0xffffffffffffffff) {
printf("FAIL: sysinfo succeeded with bad argument\n");
exit(1);
}
}
//测试进程数量是否正确:创建子进程查看进程数量是否也+1
void testproc() {
struct sysinfo info;
uint64 nproc;
int status;
int pid;
sinfo(&info);
nproc = info.nproc; //这是我们编写的系统调用计算进程数量,获取初始进程
pid = fork(); //创建进程
if(pid < 0){
printf("sysinfotest: fork failed\n");
exit(1);
}
if(pid == 0){ //子进程
sinfo(&info);
if(info.nproc != nproc+1) { //看看进程数量是否也+1了
printf("sysinfotest: FAIL nproc is %d instead of %d\n", info.nproc, nproc+1);
exit(1);
}
exit(0);
}
//父进程
wait(&status);
sinfo(&info);
if(info.nproc != nproc) {
printf("sysinfotest: FAIL nproc is %d instead of %d\n", info.nproc, nproc);
exit(1);
}
}
int
main(int argc, char *argv[])
{
printf("sysinfotest: start\n");
testcall();
testmem();
testproc();
printf("sysinfotest: OK\n");
exit(0);
}
然后我们的目标就明晰了:实现sysinfo,使得上述test代码能够运行并验证;根据lab2的tips,我们需要在两个c文件添加代码分别获取进程数、内存数,最后实现sysinfo的系统调用。
具体实现
- 函数注册:
Makefile
加入$U/_sysinfotest
,这是一个测试文件,调用了sysinfo接口函数,但是这个函数未在系统注册,和trace一样,需要在user/user.h
加入1
2struct sysinfo;
int sysinfo(struct sysinfo *);
在user/usys.pl
中加入entry("sysinfo")
,在头文件#define SYS_sysinfo 23
- 获取空闲内存数量、进程数量:
进程数量的计算:
kernel/proc.c
定义了allocproc
,用于遍历进程表查找UNUSED状态的进行分配,因此这里我们仿效这种写法获取正在运行的进程数目:1
2
3
4
5
6
7
8
9
10
11uint64 get_procnum(void){
struct proc *p;
uint64 ret=0;
for(p = proc; p < &proc[NPROC]; p++){
acquire(&p->lock);
if(p->state!=UNUSED) //不是UNUSED就是在USE
ret++; //计数
release(&p->lock);
}
return ret;
}
获取空闲内存数量:在kernel/kalloc.c
中记载了内存管理的结构:
1
2
3
4
5
6
7struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;kmem
是一个链表,指向能够被利用的内存,lock是为了获取锁,因此只需要遍历这个链表,就能获取空闲内存数量:
1
2
3
4
5
6
7
8
9
10
11
12uint64 get_memory(void){
uint64 ret=0;
struct run*r;
acquire(&kmem.lock);
r=kmem.freelist; //获取表头结点
while(r){
ret++; //计数
r=r->next; //下一个结点
}
release(&kmem.lock);
return ret*PGSIZE; //每次分配内存是按页表分配的,在riscv.h中定义大小是PGSIZE=4096,乘上才是真正的字节数
}
添加新函数记得头文件声明:defs.h
1
2uint64 get_memory(void);
uint64 get_procnum(void);
- 最后一步只需要实现我们的syscall,还是在
kernel/syscall.c
中,填充系统在kernel/sysinfo.h
定义的结构体:为了使用户空间也能访问这个被内核空间赋值的结构体,需要通过特定的API函数将内核参数复制到用户空间(这样才能在test函数访问、比较和验证。这个函数是copyout,最后添加的代码:1
2
3
4
5struct sysinfo {
uint64 freemem; // amount of free
memory (bytes)
uint64 nproc; // number of process
};1
2
3
4
5
6
7
8
9
10
11
12uint64 sys_info(void){
struct sysinfo info; //在内核空间内存
info.freemem=get_memory();
info.nproc=get_procnum(); //填充结构体
uint64 usr_addr;
struct proc *p=myproc(); //获取当前进程,用于访问页表,映射到用户空间
if(argaddr(0,&usr_addr)!=0) //获取用户空间存储地址
return -1;
if(copyout(p->pagetable,usr_addr,(char*)&info,sizeof(info))<0) //复制变量到用户空间
return -1;
return 0;
}
记得在该文件包含#include "sysinfo.h"
,加上[SYS_sysinfo] sys_info
和[SYS_sysinfo] "info"(trace追踪用的数组)
。
从这里也可以看到有意思的现象,在用户空间使用系统调用是使用sysinfo函数,但是我们内部系统实现命名是sys_info,说明用户空间是通过系统调用号获取对应的系统调用函数,这与普通的库函数调用有着明显区别。
完整commit:Sysinfo Done(忽略swp文件)