MIT 6.S081 Lab Two

MIT 6.S081 Lab Two

  • 引言
  • system calls
    • System call tracing(moderate)
      • 实验解析
      • 实现思路小结
    • Sysinfo(moderate)
      • 实验解析
    • 可选的挑战


引言

本文为 MIT 6.S081 2020 操作系统 实验一解析。

MIT 6.S081课程前置基础参考: 基于RISC-V搭建操作系统系列


system calls

在上一个实验中,您使用系统调用编写了一些实用程序。在本实验室中,您将向xv6添加一些新的系统调用,这将帮助您了解它们是如何工作的,并使您了解xv6内核的一些内部结构。您将在以后的实验室中添加更多系统调用。

Attention:

  • 在你开始写代码之前,请阅读xv6手册《book-riscv-rev1》的第2章、第4章的第4.3节和第4.4节以及相关源代码文件:
  • 系统调用的用户空间代码在user/user.h和user/usys.pl中。
  • 内核空间代码是kernel/syscall.h、kernel/syscall.c。
  • 与进程相关的代码是kernel/proc.h和kernel/proc.c。

要开始本章实验,请将代码切换到syscall分支:

$ git fetch
$ git checkout syscall
$ make clean

如果运行make grade,您将看到测试分数的脚本无法执行trace和sysinfotest。您的工作是添加必要的系统调用和存根(stubs)以使它们工作。


System call tracing(moderate)

YOUR JOB:

  • 在本作业中,您将添加一个系统调用跟踪功能,该功能可能会在以后调试实验时对您有所帮助。
    • 您将创建一个新的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系统调用。32是1<
  • 在第二个示例中,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程序让用户进行调用:

//user/trace.c
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(int argc, char *argv[]){
  int i;
  char *nargv[MAXARG];
  //参数个数不小于3个,确保系统调用号是合法数字
  if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){
    fprintf(2, "Usage: %s mask command\n", argv[0]);
    exit(1);
  }
  //将第一个参数转换为整数,作为系统调用号传入trace函数---该系统调用函数需要我们提供对应的实现
  if (trace(atoi(argv[1])) < 0) {
    fprintf(2, "%s: trace failed\n", argv[0]);
    exit(1);
  }
  //nargv数组持有要追踪的命令
  for(i = 2; i < argc && i < MAXARG; i++){
    nargv[i-2] = argv[i];
  }
  //执行命令完成系统调用过程追踪
  exec(nargv[0], nargv);
  exit(0);
}

我们需要做的是提供trace系统调用的具体实现,步骤如下:

  1. 在Makefile的UPROGS中添加$U/_trace

  2. 将系统调用原型添加到user/user.h头文件中

MIT 6.S081 Lab Two_第1张图片
2. 将存根添加到user/usys.pl , 这段perl调用由makefile文件调用,生成实际的系统调用存根user/usys.S
MIT 6.S081 Lab Two_第2张图片
3. 将系统调用编号添加到kernel/syscall.h中
MIT 6.S081 Lab Two_第3张图片
4. 执行make clean 和 make qemu 命令,查看usys.S是否生成,是否符合我们的预期

MIT 6.S081 Lab Two_第4张图片
5. 尝试执行trace 32 grep hello README命令,此时由于我们还没有在内核中提供trace系统调用的具体实现,所以这里执行会失败
MIT 6.S081 Lab Two_第5张图片


  1. proc结构体中添加一个数据字段,用于保存trace的参数
// kernel/proc.h
struct proc {
  // ...
  int trace_mask;    // trace系统调用参数
};
  1. 在sys_trace()的实现中实现参数的保存
// kernel/sysproc.c
uint64
sys_trace(void)
{
  // 获取系统调用的参数
  argint(0, &(myproc()->trace_mask));
  return 0;
}
  1. 由于struct proc中增加了一个新的变量,当fork的时候我们也需要将这个变量传递到子进程中(提示中已说明)
//kernel/proc.c
int
fork(void)
{
  // ...

  safestrcpy(np->name, p->name, sizeof(p->name));

  //将trace_mask拷贝到子进程
  np->trace_mask = p->trace_mask;

  pid = np->pid;
  // ...

  return pid;
}
  1. 接下来应当考虑如何进行系统调用追踪了,根据提示,这将在syscall()函数中实现。下面是实现代码,需要注意的是条件判断中使用了&而不是==,这是因为在实验说明书的例子中,trace 2147483647 grep hello README将所有31个低位置为1,使得其可以追踪所有的系统调用。
void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;  // 系统调用编号,参见书中4.3节
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->trapframe->a0 = syscalls[num]();  // 执行系统调用,然后将返回值存入a0

    // 系统调用是否匹配 -- 位运算判断
    //如果我们要追踪read,那么trace_mask的值为32,也就是10000
    //假如当前系统调用号为5,那么1左移五位为: 10000
    //此时相与得到1,说明是我们需要追踪的系统调用,则进行打点记录
    if ((1 << num) & p->trace_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;
  }
}

这里需要注意一点: 我们是通过位运算来判断当前是否需要对某个系统调用进行追踪的,例如: 如果要追踪read系统调用,由于read系统调用号为5,所以我们将二进制第五位设置为1,也就是32。

  1. 在上面的代码中,我们还有一些引用的变量尚未定义,在syscall.c中定义他们
// ...
extern uint64 sys_trace(void);

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

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",
};
  • 测试输出结果如下:

MIT 6.S081 Lab Two_第6张图片


实现思路小结

实现步骤总共两步:

  1. 补齐暴露给用户库中trace系统调用相关定义项
  2. 补齐内核库中trace系统调用函数的实现

系统调用追踪思路:

MIT 6.S081 Lab Two_第7张图片
trace_mask就是一个位图,每个进程执行系统调用时,再获取当前系统调用号,通过移位得到对应的位图,与自身的trace_mask位图相与,判断得到结果是否为1,如果是说明当前系统调用号被监听了,需要输出对应的打点信息。

很重要的一点是,只要通过trace父进程创建的子进程才会被设置trace_mask。


Sysinfo(moderate)

YOUR JOB:

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

提示:

  • 在Makefile的UPROGS中添加$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中添加一个函数

实验解析

本实验中在暴露给用户的user库中已经提供好了相关的sinfo程序让用户进行调用:

//user/sysinfotest.c
void
sinfo(struct sysinfo *info) {
  if (sysinfo(info) < 0) {
    printf("FAIL: sysinfo failed");
    exit(1);
  }
}
  1. 在Makefile的UPROGS中添加$U/_sysinfotest
  2. 将系统调用原型添加到user/user.h头文件中 --> 要在user/user.h中声明sysinfo()的原型,需要预先声明struct sysinfo的存在
struct sysinfo;
int sysinfo(struct sysinfo *);
  1. 将存根添加到user/usys.pl
entry("sysinfo");
  1. 将系统调用编号添加到kernel/syscall.h中
#define SYS_sysinfo 23
  1. 执行make clean 和 make qemu 命令,查看usys.S是否生成,是否符合我们的预期
    MIT 6.S081 Lab Two_第8张图片
  2. 尝试执行sysinfotest命令,此时由于我们还没有在内核中提供trace系统调用的具体实现,所以这里执行会失败

MIT 6.S081 Lab Two_第9张图片


  1. 在kernel/kalloc.c中添加一个函数用于获取空闲内存量
struct run {
  struct run *next;
};

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

内存是使用链表进行管理的,因此遍历kmem中的空闲链表就能够获取所有的空闲内存,如下

void
freebytes(uint64 *dst)
{
  *dst = 0;
  struct run *p = kmem.freelist; // 用于遍历

  acquire(&kmem.lock);
  while (p) {
    // 统计空闲页数,乘上页大小 PGSIZE 就是空闲的内存字节数
    *dst += PGSIZE;
    p = p->next;
  }
  release(&kmem.lock);
}

xv6 中,空闲内存页的记录方式是,将空闲内存页本身直接用作链表节点,形成一个空闲页链表,每次需要分配,就把链表根部对应的页分配出去。每次需要回收,就把这个页作为新的根节点,把原来的 freelist 链表接到后面。注意这里是直接使用空闲页本身作为链表节点,所以不需要使用额外空间来存储空闲页链表,在 kalloc() 里也可以看到,分配内存的最后一个阶段,是直接将 freelist 的根节点地址(物理地址)返回出去了:

// kernel/kalloc.c
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
  struct run *r;

  acquire(&kmem.lock);
  r = kmem.freelist; // 获得空闲页链表的根节点
  if(r)
    kmem.freelist = r->next;
  release(&kmem.lock);

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r; // 把空闲页链表的根节点返回出去,作为内存页使用(长度是 4096)
}

常见的记录空闲页的方法有:空闲表法、空闲链表法、位示图法(位图法)、成组链接法。这里 xv6 采用的是空闲链表法。


  1. 在kernel/proc.c中添加一个函数获取进程数

遍历proc数组,统计处于活动状态的进程即可,循环的写法参考scheduler函数

void
procnum(uint64 *dst)
{
  *dst = 0;
  struct proc *p;
  // 不需要锁进程 proc 结构,因为我们只需要读取进程列表,不需要写
  for (p = proc; p < &proc[NPROC]; p++) {
     // 不是 UNUSED 的进程位,就是已经分配的
    if (p->state != UNUSED)
      (*dst)++;
  }
}
  1. 内核的头文件中添加函数声明 --> kernel/defs.h
void            freebytes(uint64 *dst);
void            procnum(uint64 *dst);
  1. 实现sys_sysinfo,将数据写入结构体并传递到用户空间 --> 在kernel/sysproc.c文件中编写
uint64
sys_sysinfo(void)
{
  struct sysinfo info;
  freebytes(&info.freemem);
  procnum(&info.nproc);

  // a0寄存器作为系统调用的参数寄存器,从中取出存放 sysinfo 结构的用户态缓冲区指针
  uint64 dstaddr;
  argaddr(0, &dstaddr);

  // 使用 copyout,结合当前进程的页表,获得进程传进来的指针(逻辑地址)对应的物理地址
  // 然后将 &sinfo 中的数据复制到该指针所指位置,供用户进程使用。
  if (copyout(myproc()->pagetable, dstaddr, (char *)&info, sizeof info) < 0)
    return -1;

  return 0;
}

kernel/sysproc.c中记得引入sysinfo结构体定义所在的头文件:

//sysinfo.h具体定义在kernel/sysinfo.h文件中
#include "sysinfo.h"
struct sysinfo {
  uint64 freemem;   // amount of free memory (bytes)
  uint64 nproc;     // number of process
};
  1. 在系统调用列表中补充我们的新添加的sysinfo系统调用 --> kernel/syscall.c

MIT 6.S081 Lab Two_第10张图片
11. 测试运行结果:

MIT 6.S081 Lab Two_第11张图片


可选的挑战

感兴趣的小伙伴可以去做一下可选的挑战:

  • 打印所跟踪的系统调用的参数(easy)。
  • 计算平均负载并通过sysinfo导出(moderate)。

你可能感兴趣的:(#,MIT,6.S081,git,github)