# MIT6.s081-2020 Lab2 System Calls
# 准备
git checkout syscall
make clean
2
# System call trace
# 引言
在本任务中,您将添加一个系统调用跟踪功能,该功能可能会在以后调试实验时帮助您。您将创建一个新的trace
将控制跟踪的系统调用。它应该采用一个参数,一个整数“掩码”,其位指定要跟踪的系统调用。例如,要跟踪 fork 系统调用,程序调用trace(1 << SYS_fork)
, 在哪里SYS_fork
是来自的系统调用号kernel/syscall.h
. 如果掩码中设置了系统调用的编号,则必须修改 xv6 内核以在每个系统调用即将返回时打印出一行。该行应包含进程id、系统调用的名称和返回值;您不需要打印系统调用参数。这trace
系统调用应该启用对调用它的进程以及它随后派生的任何子进程的跟踪,但不应影响其他进程。
# 实现
这一部分是要求我们实现一个系统调用的跟踪,按照HINT一步步做下去即可。
**HINT1:**在Makefile的UPROGS中添加$U/_trace
打开目录可以发现已经提供了一个user/trace.c
文件,先在Makefile里添加一下即可。
user/trace.c
if (trace(atoi(argv[1])) < 0) {
fprintf(2, "%s: trace failed\n", argv[0]);
exit(1);
}
2
3
4
可以看到trace.c里面调用了trace()
方法(来自user.h),trace接收一个int参数,返回int,并且如果执行失败返回负值。
然后这个trace()
函数的作用是,输入参数是掩码,因为int是32位最多能表示31个系统调用,被置为1的那个系统调用位就会被跟踪并打印信息。
**HINT2:**在user/user.h
中添加系统调用的原型,在user/usys.pl
中添加一个占位,并在kernel/syscall.h
中添加一个系统调用号。
Makefile调用perl脚本user/usys.pl
来生成user/usys.S
,usys.S
也就是实际的系统调用代码(stub),它使用RISC-V的ecall指令来进入内核态。修复了编译问题后运行trace 32 grep hello README
依然执行失败因为你还没有在内核中实现这个系统调用。
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";
}
2
3
4
5
6
7
8
ecall
: xv6是基于RISC-V指令集的,在RISV-V指令集中ecall
代表系统调用。
li
: 是一个加载立即数到寄存器的指令,格式是li rd, immediate
,表示x[rd] = immediate
,将常量加载到 x[rd]
中。
SYS_${name}
的含义是在kernel/syscall.h中#define SYS_${name} xxx
的系统调用号,这样就可以告诉ecall要调用几号系统调用。
在user/user.h
中添加函数原型。
int trace(int);
在user/usys.pl
中添加trace的占位。
entry("trace");
在syscall.h
里添加系统调用号。
#define SYS_close 21
#define SYS_trace 22 // NEW
2
**HINT3:**在kernel/syspro.c
中添加sys_trace()
函数,sys_trace()
通过在proc struct的新变量中存储参数来实现这个新的系统调用。从用户空间检索系统调用参数的函数位于kernel/syscall.c
中,可以在kernel/sysproc.c
中看到这些函数的使用示例。
查看kernel/syscall.c
中函数可以发现:
syscall.c
中的argint()
函数的实现如下,trapframe
是的用户进程陷入(trap)内核之前的寄存器等上下文信息。
// Fetch the nth 32-bit system call argument.
int argint(int n, int *ip) {
*ip = argraw(n);
return 0;
}
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
因此需要在proc
结构体(在kernel/proc.h
)里面添加一个新变量用来存储这个跟踪号,然后在fork()
的时候把结构体里新增加的变量也给复制过去,这样就达到了传参的目的。我们新的变量是不需要加锁的,因为只会被自己所在的进程使用,所以放在char name[16]
之后
修改kernel/proc.h
// Per-process state
struct proc {
// ...
// these are private to the process, so p->lock need not be held.
// ,,
char name[16]; // Process name (debugging)
int trace_mask; // trace mask for debugging
};
2
3
4
5
6
7
8
9
打开kernel/sysproc.c
该文件的其他系统调用函数的参数都是void
,因为获取参数使用的是argcint()
方法,argint()
用于读取在a0-a5
寄存器中中传递的系统调用参数。
结合这个文件里其他函数我们能看出myproc()
函数可以获取当前进程的struct proc
, 也就是PCB。添加sys_trace(void)
函数。
// add mask to proc's trace_mask
uint64 sys_trace(void) {
int n;
if(argint(0, &n) < 0) {
return -1;
}
myproc()->trace_mask = n;
return 0;
}
2
3
4
5
6
7
8
9
**HINT4:**修改fork()
来实现“从父进程复制trace mask
到子进程”的功能,fork
位于kernel/proc.c
。
因此需要在kernel/proc.c的fork()函数里加一句np->trace_mask = p -> trace_mask;
即可。
**HINT5:**修改kernel/syscall.c
中的syscall()
函数来把trace
打印出来。你需要添加一个以系统调用号为索引的存储系统调用名字的数组。
在kernel/syscall.c
里实现我们的打印操作,文档里说打印出的一行应当包括:进程id,系统调用的名字以及系统调用的返回值,不需要打印系统的调用的参数。
文档里让我们自己搞一个可以用系统调用号索引系统调用名称的数组,参考static uint64 (*syscalls[])(void)
的写法,我们写个这样的东西出来。
extern表明这个函数来自外部文件。
extern uint64 sys_trace(void);
static uint64 (*syscalls[])(void) = {
// ...
[SYS_close] sys_close,
[SYS_trace] sys_trace, // NEW
};
static char* syscall_names[] = {
[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",
[SYS_sysinfo] "info",
};
void syscall(void)
{
int num;
struct proc *p = myproc();
char* syscall_name; // NEW
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
if ((p->trace_mask & (1 << num)) != 0) { // NEW
syscall_name = syscall_names[num]; // NEW
printf("%d: syscall %s -> %d", p->pid, syscall_name, p->trapframe->a0); // NEW
} // NEW
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -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
# 结果
实验测试:
# Sysinfo
# 引言
在本次任务中,您将添加一个系统调用,sysinfo
,收集有关正在运行的系统的信息。系统调用有一个参数:一个指向struct sysinfo
(看kernel/sysinfo.h
)。内核应该填写这个结构的字段:freemem
字段应设置为可用内存的字节数,并且nproc
字段应设置为其进程的数量state
不是UNUSED
. 我们提供测试程序sysinfotest
; 如果它打印“sysinfotest:OK”,你就通过了这个任务。
# 实现
因此本部分内容也可以按上述trace流程一样,按照HINT一步步做下去即可。
**HINT1:**添加$U/_sysinfotest
到 Makefile 中的 UPROGS
**HINT2:**运行make qemu
;user/sysinfotest.c
将无法编译。添加系统调用 sysinfo
,步骤与之前的分配相同。声明 sysinfo()
的原型在user/user.h
你需要预先声明存在struct sysinfo
:
struct sysinfo;
int sysinfo(struct sysinfo *);
2
修改user/user.h
:需要在user.h
里再声明一次struct sysinfo
的原因是:在参数列表里的参数是看不见本文件外面定义的struct
的。
struct stat;
struct sysinfo;
struct rtcdate;
// system calls
// ...
int sysinfo(struct sysinfo *);
2
3
4
5
6
7
usys.pl
entry("sysinfo");
syscall.h
#define SYS_sysinfo 23
syscall.c
extern uint64 sys_info(void);
static uint64 (*syscalls[])(void) = {
// ...
[SYS_sysinfo] sys_info,
};
static char* syscall_names[] = {
// ...
[SYS_sysinfo] "sys_info",
};
2
3
4
5
6
7
8
9
10
修复编译问题后,运行 sysinfotest
;它会失败,因为你还没有在内核中实现系统调用。
**HINT3:**sysinfo 需要复制一个struct sysinfo
返回用户空间;看sys_fstat()
(kernel/sysfile.c
) 和filestat()
(kernel/file.c
) 有关如何使用的示例copyout()
.
按照HINT,阅读sys_fstat()
函数和filestat()
函数。
// **kernel/sysfile.c**
uint64
sys_fstat(void)
{
struct file *f;
uint64 st; // user pointer to struct stat
if(argfd(0, 0, &f) < 0 || argaddr(1, &st) < 0)
return -1;
return filestat(f, st);
}
// **kernel/file.c**
// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
struct proc *p = myproc();
struct stat st;
if(f->type == FD_INODE || f->type == FD_DEVICE){
ilock(f->ip);
stati(f->ip, &st);
iunlock(f->ip);
if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
return -1;
return 0;
}
return -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
由上述可以发现,当进行内核态数据到用户态数据的拷贝时,将会用到copyout
其中参数包含:线程的页表,拷贝对象的目的地址,拷贝对象地址以及对象所占用的空间大小。
**HINT4:**要收集可用内存量,请添加一个函数kernel/kalloc.c
阅读了一下了一下kernel/kalloc.c
的kfree()
和kalloc()
,内存是被用空闲列表freelist
的形式管理的,且freelist
是链表的头结点,当free
一块内存的时候,freelist
头插这块被free的内存,当申请一块内存的时候,freelist
的头部被拿走。并且会更新头部的位置为先前头部节点的下一个节点。
因此,要获得空闲内存的大小只需要遍历一下freelist
看看有多少个空闲的内存块就可以了。
uint64 free_mem(void) {
struct run *r;
uint64 count = 0;
acquire(&kmem.lock);
r = kmem.freelist;
while(r) {
r = r->next;
count++;
}
release(&kmem.lock);
return count * PGSIZE;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
**HINT5:**要收集进程数,请添加一个函数kernel/proc.c
从kernel/proc.c
的allocproc()
函数可以看出,这里的进程用的是一个进程表proc的结构来管理的,这个进程表就是struct proc
构成的数组(struct proc proc[NPROC];
)
此外在kernle/proc.h
中声明了enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
所以我们仿照allocproc()
函数来实现我们的统计进程数量的函数即可:
uint64 proc_num(void) {
struct proc *p;
uint64 count = 0;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state != UNUSED) {
count++;
}
release(&p->lock);
}
return count;
}
2
3
4
5
6
7
8
9
10
11
12
13
为了使得在kernel/sysproc.c
能够调用上述两个方法,需要将函数声明添加至kernel/defs.h
//kalloc.c
//...
void kinit(void);
uint64 free_mem(void);
//proc,c
//...
void procdump(void);
uint64 proc_num(void);
2
3
4
5
6
7
8
9
最后,需要在kernel/sysproc.c
中实现sys_info()
函数:
uint64 sys_info(void) {
struct sysinfo info;
uint64 addr;
info.freemem = kfreemem();
info.nproc = proc_num();
if(argaddr(0, &addr) < 0) {
return -1;
}
// copy info(kernel space) to addr(user space)
if (copyout(myproc()->pagetable, addr, (char *)&info, sizeof(info)) < 0) {
return -1;
} else {
return 0;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
为什么能通过argaddr()
函数获得用户空间那边被复制的地址呢?因为调用的时候的形式是sysinfo(struct sysinfo *info)
的形式,传参了info
的地址。并且argaddr()
函数输入参数中的第一个参数指的是获取寄存器a0
中数据,第二个参数指的是用户空间保存地址。
# 结果
实验测试:
# 总结
因为这个Lab2的两个部分都是moderate难度的缘故,一路顺着HINT的提示做下来很流畅。
做完System call tracing
部分粗浅地理解了xv6中系统调用的过程。在做完Sysinfo
部分后算是初步窥探了一点xv6 中内存管理和进程管理的内容,此外还意识到需要利用copyout()
函数将一块内存从内核态copy
到用户态。
在此过程中也遇到一些问题在此做出笔记:
- 现象:在测试
sysinfo
中输出painc:acquire
- 原因:请求锁未释放