MIT 6.S081 Lab: System calls

When the world turns its back on you, you turn your back on the world.

开始学习MIT的操作系统6.S081课程的第二个Lab。Lab内容为内核添加两个系统调用,比较简单。

增加系统调用的通用流程,

  1. 在 user/usys.pl 中增加 stub for syscalls 以便生成相应的 usys.S 代码。
  2. 在 user/user.h 中增加系统调用的函数声明,以便用户调用。
  3. 在 kernel/syscall.h 中增加系统调用号,其作为宏会易于使用。
  4. 在 kernel/syscall.c 中增加系统调用的内核实现函数(一般为 sys_xxx)的 extern 声明,增加函数指针数组syscalls的内容(系统调用号对应内核实现)。
  5. 编写系统调用的内核实现函数。

system call tracing

增加一个系统调用 trace 用于追踪系统调用的信息。系统调用 trace 接受一个指示追踪的系统调用号的参数 mask,该参数通过位指示。例如, mask == (1 << SYS_fork); 代表需要追踪系统调用 fork 的信息。如果某个系统调用 cmask 中,内核需要为系统调用 c 的每次调用打印一行,内容包括 PID,系统调用名与该次调用的返回值。系统调用 trace 需要追踪当前进程(caller)之后的系统调用,以及任何后代进程(通过fork)的系统调用。

难点在于所有通过fork得到的后代进程都需要追踪。所以需要在 kernel/proc.h 的进程结构体中增加一个字段 traceMask,用于标记当前进程需要追踪的系统调用号。我这里添加到 process private 部分中。

// Per-process state
struct proc {
  struct spinlock lock;

  // p->lock must be held when using these:
  enum procstate state;        // Process state
  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

  // wait_lock must be held when using this:
  struct proc *parent;         // Parent process

  // these are private to the process, so p->lock need not be held.
  int traceMask;               // Trace mask bits
  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)
};

简单实现就是,在 kernel/sysproc.c 中添加 sys_trace 函数,获取参数 mask 后记录到当前进程的 traceMask 字段中。同时在 kernel/proc.c 的 fork 函数实现里,复制父进程的 traceMask 字段。

// in fork function kernel/proc.c 
// copy parent's trac mask
np->traceMask = p->traceMask;

// in kernel/sysproc.c
uint64
sys_trace(void)
{
  int mask;
  if(argint(0, &mask) < 0)
    return -1;
  
  myproc()->traceMask = mask;
  return 0;
}

实现内核打印的部分在 kernel/syscall.c 的函数 syscall 中。得到系统调用的返回值后,根据系统调用号是否在 traceMask 中,决定打印 PID,系统调用名(可以用一个数组,系统调用号映射到名字字符串)与返回值。需要注意 trace 这个系统调用也可能被追踪,而且判断 traceMask 是在调用 trace 之后的,所以不能提前用局部变量记录 traceMask 的值。这部分不放代码。

Sysinfo

添加一个系统调用 sysinfo 收集当前的系统信息。系统调用 sysinfo 接受一个指向 struct sysinfo(声明在 kernel/sysinfo.h 中) 的指针作为参数,将收集的系统信息写入该指针指向的内存中。内核实现需要填充 struct sysinfo 的freemem与nproc字段。字段 freemem 指示当前系统空闲内存的字节数,字段 nproc 指示状态不是UNUSED的进程数目。

在 kernel/kallo.c 中增加 getfreemem 函数,实现上遍历 kmem.freelist 中的物理页数目,计算空闲内存字节数。在 kernel/proc.c 中增加 getnproc 函数,实现上遍历进程状态表 struct proc proc[NPROC];,计数状态不是UNUSED的进程。这两个实现比较简单,均为遍历,对于全局的数据结构小心加锁即可。这里仅放 kernel/sysfile.c 中 sys_sysinfo 函数实现代码。

uint64
sys_sysinfo(void)
{
  uint64 upsi; // user pointer to struct sysinfo
  struct sysinfo si;
  struct proc *p = myproc();

  if(argaddr(0, &upsi) < 0)
    return -1;
  
  si.freemem = getfreemem();
  si.nproc = getnproc();

  if(copyout(p->pagetable, upsi, (char *)&si, sizeof(struct sysinfo)) < 0) {
    return -1;
  }

  return 0;
}

先通过 argaddr 获取参数指针的地址(用户空间),在内核空间获取系统信息后,需要注意的是写回用户空间。这里使用函数 copyout 从用户进程的页表p->pagetable中找到虚拟地址upsi指向的物理地址,然后写入内核空间里收集的系统信息struct sysinfo si;内容。

Conclusion

Lab的内容很简单,添加系统调用的过程主要还是熟悉内核系统调用的调用流程,如何从用户空间获取数据(系统调用的参数在 trapframe 中),得到系统调用的返回值(亦在 trapframe),如何在内核空间向用户空间写数据(通过用户进程页表找到物理页),简单熟悉一些内核的数据结构如 struct proc, kmem.freelist 与进程表。

Reference

  1. MIT 6.S081 2021
  2. Lab System calls