按照官方指南准备环境,即下面三行命令
$ git fetch
$ git checkout syscall
$ make clean
如果git checkout syscall的时候提示makefile文件的修改没有提交的话,把makefile文件改回原来的样子即可,反正也只是添加了几句编译新加的文件。
添加trace系统调用以跟踪程序的系统调用情况。具体见官方文档
要完成这个实验,首先要对xv6的系统调用过程尤其是有关的几个文件的作用搞清楚。在这个实验中主要用到四个文件
syscall.c:
通过ecall指令后系统执行syscall中的代码,其中a7寄存器存储的是系统调用编号,根据系统调用编号来执行syscalls函数指针数组中相应的系统调用函数。
sysproc.c:
里面的代码就是具体的系统调用函数,执行具体的系统调用过程,其中对进程的操作多通过proc.c中的代码完成。
proc.c:
有关进程的一些操作函数。
proc.h:
有关进程的一些结构体。
所以要编写trace系统的调用,首先是添加新的声明和系统调用编号,在user.h和syscall.c里面添加声明,在syscall.h里面添加编号,在sysproc.c里面添加具体函数。
trace系统调用函数的具体逻辑是什么呢,也就是说如何做到跟踪系统调用情况的,当我们在用户空间中使用trace程序(即user/trace.c),我们传入了三类参数,即系统调用编号,后面要运行的程序及参数,后面运行的程序是通过exec在当前进程运行的,所以在前面使用trace系统调用的时候一定修改了进程的状态以保留传入的要跟踪的系统调用编号,而存储进程状态的就是proc结构体,所以我们要在proc结构体中新加一个变量来存储trace要跟踪的系统调用编号,同时在sysproc.c文件中将trace系统调用传入的编号存储到新变量中。最后在syscall.c的syscall函数的每次返回前判断当前系统调用是否和之前trace系统调用传入的编号相同,若相同则需要记录。
proc结构体:
// Per-process state
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 tracenumber;
};
sys_trace函数:
uint64
sys_trace(void)
{
int number;
if (argint(0, &number) < 0) {
myproc()->tracenumber = 0;
return -1;
}
myproc()->tracenumber = number;
return 0;
}
syscall函数:
void
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]();
if ((1 << num) & p->tracenumber) {
printf("%d: syscall %s -> %d\n", p->pid, sysname[num], p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
注意syscall函数中要打印系统调用名称,所以还需要一个新数组来映射名称
static char* sysname[] = {
[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",
};
在命令行中运行
sudo ./grade-lab-syscall trace
可看到
make: 'kernel/kernel' is up to date.
== Test trace 32 grep == trace 32 grep: OK (1.2s)
== Test trace all grep == trace all grep: OK (0.8s)
== Test trace nothing == trace nothing: OK (1.0s)
== Test trace children == trace children: OK (10.1s)
编写成功!
添加一个系统调用sysinfo,它收集有关正在运行的系统的信息。具体见官方文档。
首先还是之前的步骤,加这个系统调用的相关声明定义添加到一系列文件中。
其实这个系统调用的意义也很简明,就是收集当前可用字节数和非UNUSED的进程数。怎么收集可用字节数呢,在kalloc.c中我们看到kmem有一个freelist,所以我们直接看这个链表有多少个节点就是有多少页,然后乘以每页的大小PGSIZE即可,这个我觉得还是要加锁的,防止读的时候freelist变了。而非UNUSED的进程数,同样的在proc.c中其实已经有遍历所有进程的函数了,我们直接模仿着遍历,每次遍历判断状态是否为UNUSED,不是则统计变量加1,最后返回统计变量即可。
注意内核空间和用户空间的页表不同,即内存不同,所以不能直接读写,要么通过寄存器读写,要么通过特定的函数读写,所以在这里我们要用copyout来将数据传输到用户空间中。调用sysinfo的时候我们传入了一个地址,通过copyout向这个地址写入信息。
代码如下:
// kalloc.c中kfreemem获取空闲字节数:
uint64
kfreemem(void)
{
struct run *r;
uint64 pages = 0;
uint64 bytes = 0;
acquire(&kmem.lock);
r = kmem.freelist;
while (r) {
++pages;
r = r->next;
}
release(&kmem.lock);
bytes = pages * PGSIZE;
return bytes;
}
// proc.c中nproc获取非UNUSED的进程数
int
nproc(void)
{
struct proc *p;
int sum = 0;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if (p->state != UNUSED) {
++sum;
}
release(&p->lock);
}
return sum;
}
// sysproc中的sys_sysinfo函数执行系统调用,使用copyout传递数据
uint64
sys_sysinfo(void)
{
struct proc *p = myproc();
struct sysinfo temp;
temp.freemem = kfreemem();
temp.nproc = nproc();
uint64 addr;
if (argaddr(0, &addr) < 0) {
return -1;
}
if (copyout(p->pagetable, addr, (char*)&temp, sizeof(temp)) < 0) {
return -1;
}
return 0;
}
在命令行运行
sudo ./grade-lab-syscall sysinfo
得到
make: 'kernel/kernel' is up to date.
== Test sysinfotest == sysinfotest: OK (2.1s)
编写成功!
其实这两个实验都不是很难,根据提示来基本能在1~2小时内做出来。两个实验让我更加理解系统调用的过程,同时还让我明白了一件很重要的事情:用户空间和内核空间各有各的页表,不能互相读写内存空间。