Xv6 Lab2

发布时间 2023-07-01 16:22:23作者: zwyyy456

系统调用

Lab1 主要是基于提供的系统调用接口来编写一些小工具程序,而 Lab2 则是要我们自己实现系统调用,并提供系统调用的接口。

以本次 Lab 要我们实现的 trace 调用为例,说明一下系统调用的流程:

user/trace.c 的第 $15$ 行,调用了属于 system call 的 trace 函数,当前执行 make qemu 是无法成功的,因为我们还没有给用户提供接口。因此,我们需要在 user/user.h 里面添加系统调用 trace 的函数声明(prototype)。

我们需要在 user/usys.pl 中追加 entry("trace"); 这一行,从而添加一个 trap entry,从而实现调用 trace 时会发生 trap,从而会执行 ECALL 指令,并且会将系统调用的接口的参数(这里就是 trace 的参数)的存入寄存器 a0a1 等(从左往右第一个参数的地址存入 a0,依次类推),此外,还会将 trace 对应的系统调用号存入寄存器 a7

之后控制权来到 kernel 中的 syscall 函数,它从 a7 中取出系统调用号,并执行系统调用号对应的 sys_func

这里的系统调用号可以理解为数组索引,在本次 Lab 中需要我们修改 kernel/syscall.ckenel/syscall.h 从而添加 trace 对应的系统调用号,以及内核中 trace 对应的 sys_trace 的实现)。

System call tracing

官网的提示其实是比较全面了,按提示处理,即可添加 trace 的系统调用接口:

Run make qemu and you will see that the compiler cannot compile user/trace.c, because the user-space stubs for the system call don't exist yet: add a prototype for the system call to user/user.h, a stub to user/usys.pl, and a syscall number to kernel/syscall.h. The Makefile invokes the perl script user/usys.pl, which produces user/usys.S, the actual system call stubs, which use the RISC-V ecall instruction to transition to the kernel. Once you fix the compilation issues, run trace 32 grep hello README; it will fail because you haven't implemented the system call in the kernel yet.

之后,则需要我们自己在 kernel/syspro.c 中实现真正执行系统调用 trace 所需要执行的操作了:

Lab 给出的提示中提到,我们需要将 trace(int mask) 的参数 mask 参数存放到 proc 结构体中的一个新成员变量中,这里我们直接用 mask 表示,于是,sys_trace 要做的其实很简单,就是取出寄存器 a0 中的地址然后解引用,得到 mask,然后将它赋给 proc 结构体中的 mask

对于取出寄存器 a0 的参数,Xv6 已经给我们封装好了一个函数 argint,参照一下 sys_exit 的实现,以及 int argint(int n, int *p) 的实现,就能知道 argint 如何使用了,第一个参数表示从哪个寄存器中取值,取出来的值放在地址 p 中。

// 修改 syscall.h,添加 #define SYS_trace 22
static uint64 (*syscalls[])(void) = {
    [SYS_fork] sys_fork,
    [SYS_exit] sys_exit,
    [SYS_wait] sys_wait,
    [SYS_pipe] sys_pipe,
    [SYS_read] sys_read,
    [SYS_kill] sys_kill,
    [SYS_exec] sys_exec,
    [SYS_fstat] sys_fstat,
    [SYS_chdir] sys_chdir,
    [SYS_dup] sys_dup,
    [SYS_getpid] sys_getpid,
    [SYS_sbrk] sys_sbrk,
    [SYS_sleep] sys_sleep,
    [SYS_uptime] sys_uptime,
    [SYS_open] sys_open,
    [SYS_write] sys_write,
    [SYS_mknod] sys_mknod,
    [SYS_unlink] sys_unlink,
    [SYS_link] sys_link,
    [SYS_mkdir] sys_mkdir,
    [SYS_close] sys_close,
    [SYS_trace] sys_trace,
};

// syscall for trace
uint64 sys_trace(void) {
    struct proc *to_trace = myproc();
    int mask;
    if (argint(0, &mask) < 0) { // 取出 trace 之后的 mask 参数
        return -1;
    }
    to_trace->mask = mask;
    return 0;
}

然后需要修改 fork(),从而使得子进程也共享同一个 mask,这里很简单,在 fork() 的实现中添加 np->mask = p->mask; 即可。

最后还需要修改 syscall() 函数,从而当系统调用号属于 mask 使能的系统调用号时,按 Lab 要求进行 printf

void syscall(void) {
    int num;
    struct proc *p = myproc();

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

Sysinfo

添加接口的操作同上,我们先实现 sys_sysinfo,由于 sysinfo 接口的参数是个指针(或者说地址),尽管该参数的地址依旧存放于 a0 寄存器中,但是我们不能再使用 argint 了,这里 Lab 给出了提示,参照 sys_fstat 的实现,我们知道了可以使用 argaddr 实现这一操作,a0 中的值被存放于 uint64 addr 中。

之后,我们需要创建一个新的 struct sysinfo 结构体变量 info,然后将统计的 amount of free memory 和 number of processes 存放于这个结构体变量中。

之后,Lab 提示我们使用 copyoutinfo 传递到 user space,其实参照 copyout 的注释就能清楚明白 copyout 是干嘛的,怎么用了。

uint64 sys_sysinfo(void) {
    uint64 addr; // user pointer to struct sysinfo
    if (argaddr(0, &addr) < 0) {
        return -1;
    }
    struct proc *p = myproc();
    struct sysinfo info;
    info.nproc = get_proc_num();
    info.freemem = freemem_size();
    if (copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0) {
        return -1;
    }
    return 0;
}

defs.h 中添加 get_proc_numfreemem_size 的 prototype。

分别在 kernel/kalloc.ckernel/proc.c 中实现。统计 amount of free memory,从 kalloc.c 中可以发现,Xv6 是使用 external free memory list 的形式来进行 malloc 的,所以我们只需要统计 free memory list 的结点数就能知道了;而对统计 number of processes 来说,从 proc.cstruct proc proc[NPROC]; 中可以得知,进程是以数组形式管理的,我们统计该数组中状态不为 unused 即可知 number of processes。

// kernel/kalloc.c
int freemem_size(void) {
    struct run *r;
    int num = 0;
    for (r = kmem.freelist; r; r = r->next) {
        ++num;
    }
    return num * PGSIZE;
}

// kernel/proc
int get_proc_num(void) {
    struct proc *p;
    int num = 0;
    for (p = proc; p < &proc[NPROC]; ++p) {
        if (p->state != UNUSED) {
            ++num;
        }
    }
    return num;
}

总结

这个 Lab 设计得非常精妙,做了 System call tracing 之后,对系统调用的流程就有了比较深刻的理解了,Sysinfo 则是加深理解,两个实验一起,对系统调用时,参数的传入、结果的传出,都有了大致概念。