MIT 6.S081 Lab2 website

为了完成 Syscall 作业,需要阅读:

XV6-book, Chapter 2, Sections 4.3 and 4.4

files: user/user.h, kernel/proc.c kernel/proc.h, kernel/syscall.c kernel/syscall.h

system call tracing

system call tracing 需要我们补充 kernel 中的一些程序,将某程序中指定的 system call 打印出来。当然,这需要我们新增一个 system_trace 系统调用函数。题中,给定的 tracing 程序以 trace [system-call-number] [cmd] 的方式运行。我们首先去看 user/trace.c 中的内容,看看 system call number 是怎么传入系统调用的。

user/trace.c 的程序如下所示:

int
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 系统调用来实现。所以接下来的任务是进行 trace 系统调用的实现。再次回顾 Lab 1 中的内容,在 Lab 1 中我们分析系统调用是通过在 usys.S 的汇编程序中进入的,然后 ecall 到 kernel/syscall.c 中映射到的函数,最终执行。那么 trace 系统调用也是这个逻辑。

在汇编中,是这样实现的:

li a7, SYS_trace
ecall
ret

这个 lab 使用的是 risc-v 汇编。li a7, SYS_trace 表示把 32 位的数据 SYS_trace 加载到指定的寄存器 a7 中。

ecall 将异常的类型写在 a7 寄存器中,参数写在 a0-a5 寄存器中。syscall 场景下,使用 ecall 会把处理器的特权级别由 User-Mode 转到 Supervisor-Mode。那 ecall 跳转的地址在哪里呢?在操作系统启动的时候,会把异常表地址绑定到 stevc 寄存器中。

为了能够让每个进程知道需要打印出什么样的 syscall 调用,我们需要将 trace 调用传入的值传给进程?那如何将一个值传给进程,进程又该如何保存这个值呢?

显然,我们需要修改进程的定义,在 kernel/proc.h 中修改进程的数据结构,加上一个 mask 项来存储需要跟踪的系统调用的数值。

struct proc {
   struct spinlock lock;

   ...

   char name[16];               // Process name (debugging)
   int mask;                    // -- Modified -- Lab2, system call.
 };

接下来,我们需要考虑在哪个地方插入打印指令来打印出 trace 程序追踪的系统调用?是在 process 中加入吗?显然不能在进程中做到这一点。我们只能在 syscall 这个通用的系统调用函数里面加入处理代码。在文件 kernel/syscall.c 的函数 syscall 中加入打印代码:

void
syscall(void)
{
    ...
    if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
        ...
        // -- modified --
        if ((1 << num) & p->mask) {
        printf("%d: syscall %s -> %d\n", p->pid, sysname[num], p->trapframe->a0);
        }
    } else {
        ...
    }
}

在上述代码中,syscall 指令会将调用号(a7 寄存器)读取到 num 中,然后查找系统调用表并执行。我在这里定义了一个新的数组 sysname 来存放每个系统调用的名称。

对于系统调用,a0 寄存器中保存的是返回值

为了便利的增加新的系统调用,xv6 lab 实现了用 perl 脚本来生成 asm 的一段小程序。我们需要在 user/usys.pl 中加入:

entry("trace");

在 make qemu 的过程中,它会自动生成对应的陷入系统调用的汇编如下:

.global trace
trace:
 li a7, SYS_trace
 ecall
 ret

接下来,我们还需要给 sys_trace 函数在内核中新加一段声明,修改 kernel/syscall.c 文件,加入

extern uint64 sys_trace();

然后在 kernel/sysproc.c 中实现系统调用:

uint64
sys_trace() {
  int mask;
  argint(0, &mask);
  if(mask < 0) return -1;
  
  myproc()->mask = mask;
  return 0;
}

这里的 argint 函数会去调用 argraw 函数如下:

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;
}

因为我们需要获得 mask 值,而 mask 值在 trace() 函数中是第一个,所以我们直接方位 a0 寄存器来得到 mask 的值。

总结:

在 xv6 中实现系统调用有如下的几步:

  1. user-mode 调用 trace()trace() 的实现在 usys.S 的汇编中
  2. usys.S 将 user-mode 中调用的系统调用代号填充到 a7 寄存器中
  3. ecall 调用了 kernel/syscall.c 中的 syscall() 函数来执行系统调用,并切换到 supervisor-mode
    • syscall() 函数本身没有定义输入,其从 a7 寄存器中找到系统调用函数的代号,然后再在函数表中查找。

Notes:

为了过 fork() 测试样例,需要修改 fork() 函数,加入 np->mask = p->mask; 来继承 mask。

sysinfo

有了上面的例子,实现 sysinfo() 就非常的简单了。sysinfo 要求我们统计空闲进程的数量和空闲内存的数量。

对于进程,xv6 使用了一个数组来存储了所有的进程,每个进程的状态都保存在一个结构体(PCB)中。如果要查询每个进程的状态,我们只需要遍历所有的进程,然后 check 是否是 UNUSED 状态就行了。

每个进程的状态只能是下面的几种之一:

enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };

我们需要在 kernel/proc.c 中加入代码:

// -- modified --
// To get the number of free processes.
uint64
free_process_num() {
uint64 res = 0;
struct proc* tmp;
for (tmp = proc; tmp < &proc[NPROC]; tmp++) {
    acquire(&tmp->lock);
    if (tmp->state != UNUSED) res++;
    release(&tmp->lock);
}
return res;
}

需要注意的是: 在统计状态的时候,必须对当前的 PCB 加锁,防止出现冲突(free_process_num)运行过程中变成了 UNUSED 或者其他

而可用空间判断则是在 kernel/kalloc.c 文件中定义了一个链表,每个链表都指向上一个可用空间,这个kmem就是一个保存最后链表的变量。

struct run {
  struct run *next;
};

struct {
  struct spinlock lock;
  struct run *freelist;
} kmem;

kmem.freelist 永远指向最后一个可用页,那我们只要顺着这个链表往前走,直到 NULL 为止。

kernel/kalloc.c 中加入代码:

uint64
free_mem_num() {
uint64 res = 0;
struct run *page = kmem.freelist;
while(page != 0) {
    res ++;
    page = page->next;
}
return res * PGSIZE;
}

对于统计空闲内存和进程的代码,kernel/def.h 中声明。 之后,我们在 kernel/sysproc.c 中实现 sysinfo() 函数如下:

uint64
sys_sysinfo() {
uint64 address;

struct sysinfo info;
struct proc *p = myproc();

argaddr(0, &address);

if (address < 0) return -1;

info.freemem = free_mem_num();
info.nproc = free_process_num();

if (copyout(p->pagetable, address, (char*)&info, sizeof(info)) < 0) return -1;

return 0;
}

这里 copyout 是吧页表中的内容 copy 到 user-mode 的进程信息中去。我们通过 argaddr(0, &address); 拿到 info 的地址,把内核态的内存 copy 到用户态去。