上一次的lab初步熟悉了如何魔改xv6 \text{xv6} xv6 以及xv6 \text{xv6} xv6 的一些系统调用,从这次lab开始,就是纯度极高的OS的内容了。
本次lab的主题是系统调用,不过并不需要真的完全把系统调用的过程全部搞清楚了再来做(后面会讲清楚的)。
1. System Call Trace \text{1. System Call Trace} 1. System Call Trace
为了方便日后的debug \text{debug} debug ,也为了熟悉系统调用的一些流程,这个任务要求实现一个新的系统调用trace
,它接受一个整型参数表示需要跟踪哪些系统调用(以二进制位标记)。对于这些系统调用,在调用时将日志输出,包括调用者(进程)的process id \text{process id} process id ,系统调用的名称以及返回值。
实际上吧,指南上把过程都说得非常清楚了。。。就差把代码贴上来了。这里还是顺着指导走一遍。
首先在user/user.h \text{user/user.h} user/user.h 中加上系统调用的声明。然后在user/usys.pl \text{user/usys.pl} user/usys.pl 中添加新的系统调用entry \text{entry} entry ,就像这样
然后在kernel/syscall.h \text{kernel/syscall.h} kernel/syscall.h 中加上一行新的系统调用的宏定义作为编号
最后按照管理在Makefile \text{Makefile} Makefile 里把trace
程序加上。这些都是trivial \text{trivial} 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 struct proc { struct spinlock lock ; enum procstate state ; struct proc *parent ; void *chan; int killed; int xstate; int pid; uint64 kstack; uint64 sz; pagetable_t pagetable; struct trapframe *trapframe ; struct context context ; struct file *ofile [NOFILE ]; struct inode *cwd ; char name[16 ]; int trace_mask; };
所有的系统调用都要经过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 int fork (void ) { int i, pid; struct proc *np ; struct proc *p = myproc(); if ((np = allocproc()) == 0 ){ return -1 ; } 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; *(np->trapframe) = *(p->trapframe); np->trapframe->a0 = 0 ; 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} kernel/sysproc.c 中的sys_trace()
函数,我们仿照其它的系统调用,用argint(int, int*)
获取参数。
这里可以稍微看看argint
和argraw
是干了什么。
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 uint64argraw (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} 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 ; }
这就做完了。总的来说这次的两个任务都不难,只要看着指导基本上就能写出来。