开始日期:22.3.11
操作系统:Ubuntu20.0.4
Link:Lab Syscall
my github repository: duilec/MITS6.081-fall2021/tree/syscall
Lab Syscall
写在前面
遇到的问题或bug
如何在虚拟机中调试(debug),需要打开两个终端,过程中需要打开两个终端,一个用来启动qemu,一个用来正常debug。(课程建议debug时,启动qemu只用一个cpu)
1
2
3
4
5one windows
$ make CPUS=1 qemu-gdb
another windows
$ gdb-multiarch kernel/kernel
$ target remote localhost:26000没有切换分支到syscall lab,导致trace相关无法运行(因为只有切换到syscall分支之后才会有相关文件)
官网lab中其实已经提示了,在整个实验开始前必须完全分支切换1
2
3$ git fetch
$ git checkout syscall
$ make cleanTimeout! trace children
超时!超时bug如下所示,原因是电脑性能不够强
1
2
3
4
5
6
7
8
9
10
11== Test trace children ==
$ make qemu-gdb
Timeout! trace children: FAIL (30.2s)
...
9: syscall fork -> 56
6: syscall fork -> -1
7: syscall fork -> -1
8: syscall fork -> -1
qemu-system-riscv64: terminating on signal 15 from pid 5581 (make)
MISSING '^ALL TESTS PASSED'
QEMU output saved to xv6.out.trace_children解决方案,在.py文件
gradelib.py
中改变超时判断时间30s为50s或更长时间(在428行)- 将
timeout=30
改为timeout=50
或更长时间
1
2
3428 def run_qemu_kw(target_base="qemu", make_args=[], timeout=50):
429 return target_base, make_args, timeout
430 target_base, make_args, timeout = run_qemu_kw(**kw)- 将
学到的知识
spinlock vs mutex
- 前者(自旋锁)会不断尝试直到成功,后者(互斥锁)失败一次就放弃,然后等待启用;前者适用于等待时间短的,后者适用于等待时间长的。
- spin lock 和mutex,自旋锁(spin lock)与互斥量(mutex)的比较
系统调用函数的添加流程(一开始可能读不懂,可以先阅读后面的实验内容同时多看看材料)
user/user.h,添加用户调用声明(prototype)
user/usys.pl,添加存根(stub)到脚本文件
usys.pl
可以看到其作用与压栈相关(打印了汇编代码)
1
2
3
4
5
6
7
8
9/* 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";
}以系统调用函数trace为例,事实上是要调用时把SYS_trace(trace的系统调用编号)压入到寄存器a7当中,然后调用ecall进入kernel
1
2
3
4/* usys.S */
li a7, SYS_trace
ecall
ret
kernel/syscall.h,添加系统调用编号(syscall number)
kernel/syscall.c,添加系统调用编号对应的系统调用函数,系统函数外部调用声明以及系统调用编号对应的函数名字
- 第一个:系统调用编号对应的系统调用函数,听起来有点绕口,其实这条添加的内容是存放在函数指针表
static uint64 (*syscalls[])(void)
中的,该表的功能是:根据系统调用编号,找到并调用对应的函数 - 第二个:为了能让
static uint64 (*syscalls[])(void)
根据系统调用编号,找到并调用对应的函数,因为这些函数存放的位置都不统一,只能用外部调用的方式来声明 - 第三个:系统调用编号对应的函数名字,就是调用函数的名字,用来给
syscall.c
跟踪打印
- 第一个:系统调用编号对应的系统调用函数,听起来有点绕口,其实这条添加的内容是存放在函数指针表
实验内容
System call tracing (moderate)
任务:实现系统调用跟踪
功能:跟踪一个或多个系统调用进程,打印该进程的pid,名字和该进程返回值
return value
The line should contain the process id, the name of the system call and the return value;
eg.
3: syscall read -> 1023
推出:
<pid>: syscall <syscall_name> -> <return_value>
注意不同进程的返回值不同,需要注意的是fork()的返回值可以恰好是
pid
按照提示(hints)一步步走即可。
Add
$U/_trace
to UPROGS in Makefile即系统调用函数的添加流程,共四次添加,但提示只给到了三次添加,事实上,要结合最后一步才能更好的理解为什么要第四次添加,可以先不添加,看到最后一步再添加
trace.c
已给出,它需要一个int参数/* 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/* user/user.h */
// syscall.h
int fork(void);
...
int trace(int); /* ADD */
/* user/usys.pl */
entry("fork");
...
entry("trace"); /* ADD */
/* kernel/syscall.h */
// System call numbers
...
/* kernel/syscall.c */
extern uint64 sys_chdir(void);
...
extern uint64 sys_trace(void); /* ADD */
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
...
[SYS_trace] sys_trace, /* ADD */
在
kernel/sysproc.c
中编写sys_trace()
,它的功能是获得该程序的mask,编写时主要参考kernel/proc.h,kernel/syscall.c,kernel/sysproc.c中的部分内容我们需要使用mask,mask是用来检查当前系统函数和用户所要跟踪的系统函数mask是否对应,如果是,才打印跟踪内容。eg.
trace 32 grep hello README
,其中32
是1 << SYS_read
即1 << 5
,需要跟踪read
如何使用mask来判断是最后一步(修改
syscall()
)的内容,这里先不提我们先要解决如何获得mask的问题
首先,每个进程都有一个其相关信息的结构体,我们在这个结构体当中添加多一条mask
1
2
3
4
5
6
7
8/* kernel/proc.h */
// Per-process state
struct proc {
struct spinlock lock;
// these are private to the process, so p->lock need not be held.
...
int mask; // mask for check and trace然后,编写
sys_trace()
获得该程序的mask,这个mask其实是我们一开始给的参数(在trace.c
中放入),它存放在寄存器a0
当中,所以我们要调用0模式(argint(0, &n)
),具体内容要看4.3和4.4eg.
trace 32 grep hello README
,其中32
是1 << SYS_read
即1 << 5
,它就存放在a0
当中。当使用
myproc()->mask = n
时,是对当前进程的mask赋值eg.
trace 32 grep hello README
,该语句就是一个进程,包含了多个系统函数,但我们只跟踪read()
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 -> 0eg.
trace 2147483647 grep hello README
,该语句就是一个进程,该进程包含了多个系统函数,同时,全部调用到的系统函数我们都跟踪1
2
3
4
5
6
7
8
9$ 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
参考代码:
1
2
3
4
5
6
7
8
9
10uint64
sys_trace(void)
{
int n;
/* get mask by trap (mask in a0 of trapframe)*/
if(argint(0, &n) < 0)
return -1;
myproc()->mask = n;
return 0;
}需要理解的
argint()
和argraw()
以及user.c/trace.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
29static 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;
}
// Fetch the nth 32-bit system call argument.
int
argint(int n, int *ip)
{
*ip = argraw(n);
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22int
main(int argc, char *argv[])
{
int i;
char *nargv[MAXARG];
if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){
fprintf(2, "Usage: %s mask command\n", argv[0]);
exit(1);
}
if (trace(atoi(argv[1])) < 0) {
fprintf(2, "%s: trace failed\n", argv[0]);
exit(1);
}
for(i = 2; i < argc && i < MAXARG; i++){
nargv[i-2] = argv[i];
}
exec(nargv[0], nargv);
exit(0);
}
为了通过
trace children
,即子程序也能被跟踪,需要把父程序的mask复制到子程序当中,很简单,在fork()
中添加语句即可。1
2
3
4
5
6
7
8
9
10
11/* kernel/proc.c */
int
fork(void)
{
...
// copy saved user registers.
*(np->trapframe) = *(p->trapframe);
// copy saved mask for trace.
np->mask = p->mask;
...最后一步,我们修改
syscall()
满足功能:打印被跟踪进程的pid,名字和该进程返回值return value
首先,需要在
syscall.c
中多添加trace的系统外部调用声明以及trace的系统调用编号对应的系统函数调用,为了能够跳转并执行sys_trace(),获得它的返回值1
2
3
4
5
6
7
8
9
10/* kernel/syscall.c */
extern uint64 sys_chdir(void);
...
extern uint64 sys_trace(void);
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
...
[SYS_trace] sys_trace,
};其次,获得调用系统函数名字,我们需要一个指针数组,以便
syscall()
使用You will need to add an array of syscall names to index into.
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// an array of syscall names to index into
static char *syscall_name[] = {
[SYS_fork] "fork",
[SYS_exit] "exit",
[SYS_wait] "wait",
[SYS_pipe] "pipe",
[SYS_read] "read",
[SYS_kill] "kill",
[SYS_exec] "exec",
[SYS_fstat] "stat",
[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",
};最后,修改
syscall()
- 寄存器
a0
本身被约定用来存放返回值(c-style),打印出来即可;寄存器a7
是用来存放SYS_<syscall_name>
的数值的
位移处理后,我们用来和当前进程的mask比较,从而检查当前系统函数和用户所要跟踪的系统函数mask是否对应
用&
操作,我们可以检查一个或多个系统函数调用。 - eg.
trace 32 grep hello README
,其中32
是1 << SYS_read
即1 << 5
,需要跟踪是系统函数read
- 注意,要先判断是不是系统函数,再判断是不是需要跟踪的系统函数
- 我们实际上是用syscall()来打印跟踪内容,trace()只是用来传递mask的
参考代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
// check mask, if OK print
if(p->mask & (1 << num))
printf("%d: syscall %s -> %d\n", p->pid, syscall_name[num], p->trapframe->a0);
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}- 寄存器
参考链接
Sysinfo (moderate)
任务:实现一个系统函数
sysinfo()
功能:收集正在运行的系统信息,包括freemem(空闲内存的大小)和nproc(运行程序的数量:即程序状态不是
UNUSED
)按照提示写
Add
$U/_sysinfotest
to UPROGS in Makefile,注意添加的是**$U/_sysinfotest
** , 不是$U/_sysinfo
系统函数添加流程
这里要格外添加一个结构体sysinfo,它就是系统信息,包含了freemem和nproc
1
2
3
4
5/* kernel/sysinfo.h */
struct sysinfo {
uint64 freemem; // amount of free memory (bytes)
uint64 nproc; // number of process
};添加
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/* user/user.h */
struct stat;
...
struct sysinfo;
// syscall.h
int fork(void);
...
int sysinfo(struct sysinfo *); /* ADD */
/* user/usys.pl */
entry("fork");
...
entry("sysinfo"); /* ADD */
/* kernel/syscall.h */
// System call numbers
...
/* kernel/syscall.c */
extern uint64 sys_chdir(void);
...
extern uint64 sys_sysinfo(void); /* ADD */
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
...
[SYS_sysinfo] sys_sysinfo, /* ADD */
// an array of syscall names to index into
static char *syscall_name[] = {
[SYS_fork] "fork",
...
[SYS_sysinfo] "sys_sysinfo",
};
参考
filestat()
(kernel/file.c
),sys_fstat()
(kernel/sysfile.c
) 以及copyout()
,编写sys_sysinfo()
,从内核中获取freemem和nproc,输出给用户1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20uint64
sys_sysinfo(void)
{
struct proc *p = myproc();
struct sysinfo info;
uint64 addr;
/* get VA(virtual address)*/
if(argaddr(0, &addr) < 0)
return -1;
/* get info*/
info.freemem = get_amount_freemem();
info.nproc = get_nproc();
/* Copy len bytes from src(info) to virtual address dstva(addr) in a given page table. */
if(copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)
return -1;
return 0;
}接下来,就是要编写
get_amount_freemem()
和get_nproc()
从而获得freemem
和nproc
注意写完之后要在
defs.h
中声明1
2
3
4
5
6
7
8/* kernel/defs.h */
// kalloc.c
...
uint64 get_amount_freemem(void);
// proc.c
...
uint64 get_nproc(void);
get_amount_freemem()
主要参考
kalloc.c
,我们可以看到内核的内存是如何初始化,如何分配,从kfree()
中可以看到kmem.freelist
一直是指向空闲内存,它是一个空闲链表,而且是倒着组装的,我们只要遍历即可获得空闲内存的总量(一个链表的结点就是一个页表,一个页表的字节数为PGSIZE
即4096
),但是它没有初始化指向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
66struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
void
kinit()
{
initlock(&kmem.lock, "kmem");
freerange(end, (void*)PHYSTOP);
}
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}参考
kalloc(void)
来写,这里没有使用自旋锁,因为没有更改kmem.freelist
的指向,只是简单的查看。1
2
3
4
5
6
7
8
9
10
11
12
13uint64
get_amount_freemem()
{
uint64 num_page = 0;
struct run *page = kmem.freelist;
while(page){
/* count for number of page */
num_page++;
/* perare next page */
page = page->next;
}
return PGSIZE * num_page;
}
get_nproc()
这个编写很简单,多线程的
state
访问,需要用自旋锁保护参考
proc.c
的内容1
2
3
4
5
6
7
8
9enum procstate { UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
...统计状态不是
UNUSED
的线程即可1
2
3
4
5
6
7
8
9
10
11
12
13
14uint64
get_nproc(void)
{
uint64 nproc = 0;
struct proc *p;
/* use spinlock to avoid race(accessing 'nproc') between different threads */
for(p = &proc[0]; p < &proc[NPROC]; p++){
acquire(&p->lock);
if(p->state != UNUSED)
nproc++;
release(&p->lock);
}
return nproc;
}
参考链接
总结
- 完成日期22.3.21
- 写
sysinfo
笔者一开始误把p < proc[NPROC]
写成了p < p[NPROC]
,找了好几个小时这个bug,是抄procdump()
的时候抄错了 = ^ = - 笔者一开始把系统函数和进程弄混了,误认为一个系统函数就是一个进程,事实上,一个进程一般都会调用多个系统函数
- 倒着组装空闲链表,需要不断地改变头指针(即
freelist
)的指向 - 艾尔登法环真好玩!
- 修改了一些错误22.8.06