MIT-6.S081 Lab-2:System Calls

上一次的lab初步熟悉了如何魔改xv6\text{xv6}以及xv6\text{xv6}的一些系统调用,从这次lab开始,就是纯度极高的OS的内容了。

本次lab的主题是系统调用,不过并不需要真的完全把系统调用的过程全部搞清楚了再来做(后面会讲清楚的)。

1. System Call Trace\text{1. System Call Trace}

为了方便日后的debug\text{debug},也为了熟悉系统调用的一些流程,这个任务要求实现一个新的系统调用trace,它接受一个整型参数表示需要跟踪哪些系统调用(以二进制位标记)。对于这些系统调用,在调用时将日志输出,包括调用者(进程)的process id\text{process id},系统调用的名称以及返回值。

实际上吧,指南上把过程都说得非常清楚了。。。就差把代码贴上来了。这里还是顺着指导走一遍。

首先在user/user.h\text{user/user.h}中加上系统调用的声明。然后在user/usys.pl\text{user/usys.pl}中添加新的系统调用entry\text{entry},就像这样

1
entry("trace");

然后在kernel/syscall.h\text{kernel/syscall.h}中加上一行新的系统调用的宏定义作为编号

1
#define SYS_trace  22

最后按照管理在Makefile\text{Makefile}里把trace程序加上。这些都是trivial\text{trivial}的。此时编译应该不会出错,但是调用封装好的trace程序应该出错,这是因为我们尚未实现trace()系统调用,在syscall()中查不到它。

接下来开始实现trace()系统调用。

首先进入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
static const char *syscall_names[32] = 
{
[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",
};

我们首先修改proc结构体,给其中增加一个成员变量trace_mask用来记录需要跟踪的系统调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// in kernel/proc.h
struct proc {
struct spinlock lock;

// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID

// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
int trace_mask; // The bitmask of syscalls to be traced.
};

所有的系统调用都要经过syscall()函数完成内核和用户之间参数和返回值的传递。在syscall()函数内部,检测系统调用的返回值,同时根据当前进程的trace_mask来判断是否要把此系统调用的信息输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void
syscall(void)
{
int num;
struct proc *p = myproc();

num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
int ret = syscalls[num]();
p->trapframe->a0 = ret;
if(p->trace_mask & (1 << num))
{
if(syscall_names[num])
printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num], ret);
else printf("%d %s: unknown sys call %d\n", p->pid, p->name, num);

}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}

子进程要复制父进程的trace_mask,因此将fork()函数加上一行

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
// in kernel/proc.c
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();

// Allocate process.
if((np = allocproc()) == 0){
return -1;
}

// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;

np->parent = p;
np->trace_mask = p->trace_mask; // 加上这一行

// copy saved user registers.
*(np->trapframe) = *(p->trapframe);

// Cause fork to return 0 in the child.
np->trapframe->a0 = 0;

// increment reference counts on open file descriptors.
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);

safestrcpy(np->name, p->name, sizeof(p->name));

pid = np->pid;

np->state = RUNNABLE;

release(&np->lock);

return pid;
}

最后来实现kernel/sysproc.c\text{kernel/sysproc.c}中的sys_trace()函数,我们仿照其它的系统调用,用argint(int, int*)获取参数。

这里可以稍微看看argintargraw是干了什么。

1
2
3
4
5
6
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
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;
}

argraw是将当前进程临时保存(因为此时已经不在用户空间了)的trapframe寄存器中对应值取出。这些寄存器保存了系统调用的参数,在调用ecall进入内核之前(进入内核之后寄存器当然就要变了)被保存在了trapframe中,取出这些寄存器的值就能知道系统调用的参数是什么。

sys_call中,取出参数,将其保存在当前进程的trace_mask中即可。

1
2
3
4
5
6
7
8
uint64 sys_trace(void)
{
int trace_mask;
if(argint(0, &trace_mask) < 0)
return -1;
myproc()->trace_mask = trace_mask;
return 0;
}

这样就做完了。

2. Sysinfo\text{2. Sysinfo}

实现一个sysinfo系统调用,该系统调用接受一个struct sysinfo的指针作为参数,并且将当前剩余内存的字节数与使用中的进程数保存在结构体里。

一些固定的流程和上面差不多,就不提了。这里说一说怎么实现这个系统调用。

首先我们要获得可用的内存字节数。指南提示我们在kalloc.c中实现一个函数来完成这个统计。其实并不难,可用内存是用链表结构管理的,遍历一遍链表,统计一下有多少空余页,然后乘上页面的大小也就是PGSIZE即可。

记得先获得锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
uint64 count_memfree(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
int cnt = 0;
while(r)
{
r = r->next;
cnt++;
}
release(&kmem.lock);
return cnt * PGSIZE;
}

然后是统计有多少正在使用的进程,这个只要把进程列表遍历一遍就行。

1
2
3
4
5
6
7
uint64 count_proc(void)
{
uint64 cnt = 0;
for (int i = 0; i < NPROC; i++)
if(proc[i].state != UNUSED) cnt++;
return cnt;
}

最后就是实现系统调用。关于copyout的用法,按着指南里给出的函数去看用法,虽然不一定能完全懂,但是可以照猫画虎(

1
2
3
4
5
6
7
8
9
10
11
uint64 sys_sysinfo(void)
{
uint64 ip;
argaddr(0, &ip);
struct sysinfo info;
info.freemem = count_memfree();
info.nproc = count_proc();
struct proc *p = myproc();
if(copyout(p->pagetable, ip, (char *)&info, sizeof(info)) < 0) return -1;
return 0;
}

这就做完了。总的来说这次的两个任务都不难,只要看着指导基本上就能写出来。