yzhe819's Blog

MIT 6.S081 Lab 2 系统调用

2022.06.14

本篇文章是第二个lab的记录

完成了基础的实验室内容

挑战练习有时间会再看看

官网链接:Lab: system calls

代码仓库:Github

系统调用追踪(moderate)

在本作业中,您将添加一个系统调用跟踪功能,该功能可能会在以后调试实验时对您有所帮助。您将创建一个新的trace系统调用来控制跟踪。它应该有一个参数,这个参数是一个整数“掩码”(mask),它的比特位指定要跟踪的系统调用。例如,要跟踪fork系统调用,程序调用trace(1 << SYS_fork),其中SYS_forkkernel/syscall.h中的系统调用编号。如果在掩码中设置了系统调用的编号,则必须修改xv6内核,以便在每个系统调用即将返回时打印出一行。该行应该包含进程id、系统调用的名称和返回值;您不需要打印系统调用参数。trace系统调用应启用对调用它的进程及其随后派生的任何子进程的跟踪,但不应影响其他进程。

我们提供了一个用户级程序版本的trace,它运行另一个启用了跟踪的程序(参见user/trace.c)。完成后,您应该看到如下输出:

$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
$
$ trace 2147483647 grep hello README
4: syscall trace -> 0
4: syscall exec -> 3
4: syscall open -> 3
4: syscall read -> 1023
4: syscall read -> 966
4: syscall read -> 70
4: syscall read -> 0
4: syscall close -> 0
$
$ grep hello README
$
$ trace 2 usertests forkforkfork
usertests starting
test forkforkfork: 407: syscall fork -> 408
408: syscall fork -> 409
409: syscall fork -> 410
410: syscall fork -> 411
409: syscall fork -> 412
410: syscall fork -> 413
409: syscall fork -> 414
411: syscall fork -> 415
...
$

在上面的第一个例子中,trace调用grep,仅跟踪了read系统调用。321<<SYS_read。在第二个示例中,trace在运行grep时跟踪所有系统调用;2147483647将所有31个低位置为1。在第三个示例中,程序没有被跟踪,因此没有打印跟踪输出。在第四个示例中,在usertests中测试的forkforkfork中所有子孙进程的fork系统调用都被追踪。如果程序的行为如上所示,则解决方案是正确的(尽管进程ID可能不同)

提示:

  • MakefileUPROGS中添加$U/_trace
  • 运行make qemu,您将看到编译器无法编译user/trace.c,因为系统调用的用户空间存根还不存在:将系统调用的原型添加到user/user.h,存根添加到user/usys.pl,以及将系统调用编号添加到kernel/syscall.hMakefile调用perl脚本user/usys.pl,它生成实际的系统调用存根user/usys.S,这个文件中的汇编代码使用RISC-V的ecall指令转换到内核。一旦修复了编译问题(注:如果编译还未通过,尝试先make clean,再执行make qemu),就运行trace 32 grep hello README;但由于您还没有在内核中实现系统调用,执行将失败。
  • kernel/sysproc.c中添加一个sys_trace()函数,它通过将参数保存到proc结构体(请参见kernel/proc.h)里的一个新变量中来实现新的系统调用。从用户空间检索系统调用参数的函数在kernel/syscall.c中,您可以在kernel/sysproc.c中看到它们的使用示例。
  • 修改fork()(请参阅kernel/proc.c)将跟踪掩码从父进程复制到子进程。
  • 修改kernel/syscall.c中的syscall()函数以打印跟踪输出。您将需要添加一个系统调用名称数组以建立索引。

实现:

首先观察题目中提到的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 32 grep hello README

根据代码可得,第一个判断是检查参数是否足够,以及第一个参数,整数“掩码”(mask),它是否有效。 然后就是将这个命令行参数(存成char array)转为整数。最后的for loop只是普通的将后续参数存一下(相当于去掉例子中开头的trace和32),然后接下来用exec调用。

了解完这个trace函数后,我们可以根据提示将需要的代码补上,首先在Makefile里面的UPROGS把**$U/_trace**加上。

	$U/_wc\
	$U/_zombie\
	$U/_trace

然后去user.h里面加调用原型加上,因为题目上了只有一个整数参数,使用直接trace(int)

int sleep(int);
int uptime(void);
int trace(int);

然后去到user/usys.pl把entry加上:

entry("sleep");
entry("uptime");
entry("trace");

最后添加system call number到kernel/syscall里面,直接加到最后:

#define SYS_mkdir  20
#define SYS_close  21
#define SYS_trace  22

到此配置就完成了,可以真正的实现功能了!

首先要修改kernel/proc.h中的proc结构,这是用来存储进程状态的数据结构,给他加一个mask用于追踪:

// 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 mask; // Process mask (for tracing)
};

然后转移到kernel/sysproc.c里面添加sys_trace():

uint64
sys_trace(void)
{
  argint(0, &(myproc()->mask));
  return 0;
}

这里的函数是获取args[1]的值,然后将它存到我们新添加的mask里面。更加完整的版本应该是还需要添加获取判断,看看有没有成功获取参数。

uint64
sys_trace(void)
{
  int n;
  if (argint(0, &n) < 0) // 判断参数是否获取成功
    return -1;
  myproc()->mask = n;    // 将argv[1]保存到当前进程的mask中
  return 0;
}

因为题目提到trace系统调用应启用对调用它的进程及其随后派生的任何子进程的跟踪,所以我们需要在fork的时候将mask值也保存到子进程里面。fork是在kernel/proc.c里面的,这里直接将原函数附上。

int
fork(void)
{
  int i, pid;
  struct proc *np;
  struct proc *p = myproc();

  // Allocate process.
  if((np = allocproc()) == 0){
    return -1;
  }

  // Copy user memory from parent to child.
  if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
    freeproc(np);
    release(&np->lock);
    return -1;
  }
  np->sz = p->sz;

  np->parent = p;

  // copy saved user registers.
  *(np->trapframe) = *(p->trapframe);

  // Cause fork to return 0 in the child.
  np->trapframe->a0 = 0;

  // increment reference counts on open file descriptors.
  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));

  np->mask = p->mask; // copy the process's signal mask

  pid = np->pid;

  np->state = RUNNABLE;

  release(&np->lock);

  return pid;
}

最后修改一下kernel/syscall.c中的syscall()函数以打印跟踪输出。这部分需要在(*syscalls[])(void)里面加一下。还有手动给他建个字符串数组给他存一下调用的方法名,方便使用。以及加一下函数声明在顶上:

extern uint64 sys_write(void);
extern uint64 sys_uptime(void);
extern uint64 sys_trace(void);

// ...

static uint64 (*syscalls[])(void) = {
// ...
[SYS_trace]   sys_trace,
}

// translate from the above system call function names
static char *syscalls_name[] = {
[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",
};

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]();

    // check whether the system call match the tracing mask
    if ((1 << num) & p->mask){
      printf("%d: syscall %s -> %d\n", p->pid, syscalls_name[num], p->trapframe->a0);
    }
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

系统信息(moderate)

在这个作业中,您将添加一个系统调用sysinfo,它收集有关正在运行的系统的信息。系统调用采用一个参数:一个指向struct sysinfo的指针(参见kernel/sysinfo.h)。内核应该填写这个结构的字段:freemem字段应该设置为空闲内存的字节数,nproc字段应该设置为state字段不为UNUSED的进程数。我们提供了一个测试程序sysinfotest;如果输出“sysinfotest: OK”则通过。

提示:

  • MakefileUPROGS中添加$U/_sysinfotest
  • 当运行make qemu时,user/sysinfotest.c将会编译失败,遵循和上一个作业一样的步骤添加sysinfo系统调用。要在user/user.h中声明sysinfo()的原型,需要预先声明struct sysinfo的存在:
struct sysinfo;
int sysinfo(struct sysinfo *);

一旦修复了编译问题,就运行sysinfotest;但由于您还没有在内核中实现系统调用,执行将失败。

  • sysinfo需要将一个struct sysinfo复制回用户空间;请参阅sys_fstat()(kernel/sysfile.c)和filestat()(kernel/file.c)以获取如何使用copyout()执行此操作的示例。
  • 要获取空闲内存量,请在kernel/kalloc.c中添加一个函数
  • 要获取可用进程数,请在kernel/proc.c中添加一个函数

实现:

重复一下之前trace里面的配置:

添加$U/_sysinfotest,添加函数声明到user.h (记得根据提示声明struct sysinfo),添加entry到usys.pl,添加syscall number到syscall.h,然后加入到syscall函数数组中。这是我的提交:sysinfo config

然后根据提示在kernel/kalloc.c中添加一个函数,用于获取空闲内存量。

直接历遍链表计数,最后乘一下内存页的大小就好,可以加个锁避免冲突。

uint64
freemem(){
  struct run *r = kmem.freelist;
  uint64 num = 0;
  acquire(&kmem.lock);
  while(r){
    num++;
    r = r->next;
  }
  release(&kmem.lock);
  return num * PGSIZE;
}

然后在kernel/proc.c中添加一个函数,用于获取可用进程数。把所有状态是UNUSED的记一下就可以。

uint64
nproc(void){
  uint64 n = 0;
  for(int i = 0; i < NPROC; i++){
    if(proc[i].state != UNUSED){
      n++;
    }
  }
  return n;
}

最后在kernel/sysproc.c里面调用:

uint64
sys_sysinfo(void)
{
  struct sysinfo info;
  info.freemem = freemem();
  info.nproc = nproc();

  // get the virtual address
  uint64 addr;
  if(argaddr(0, &addr) < 0)
    return -1;

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

  return 0;
}

重点是前三行代码,直接调用我们的代码就好。后面的那部分是用于将一个struct sysinfo复制回用户空间,就是提示里面说到的copyout用法。

到此我们的lab 2已经结束了。

测试与修复:

在提交前我们跑一下测试吧,推迟qemu,然后运行:

$ ./grade-lab-syscall
/usr/bin/env: ‘python\r’: No such file or directory

好吧,我再修复一下,然后再次运行:

$ dos2unix ./grade-lab-syscall
$ ./grade-lab-syscall

结果依然报错: 报错提示

报错提示程序没有打印3: syscall read -> 966而是打印了3: syscall read -> 986

想了想后,恍然大悟,需要给readme也调整一下格式:

$ dos2unix ./readme

再次运行,终于全部通过了: 通过