操作系统实验Lab 2:system calls(MIT 6.S081 FALL 2020)

实验要求

实验前须知

阅读 xv6 文档的第 2 章和第 4 章的 4.3 节和 4.4 节以及相关源文件:

  • 系统调用的用户空间代码在 user/user.huser/usys.pl 中。
  • 内核空间代码在 kernel/syscall.hkernel/syscall.c 中。
  • 与进程相关的代码在 kernel/proc.hkernel/proc.c 中。

使用下面的命令切换到 syscall 分支。

$ git fetch
$ git checkout syscall
$ make clean

推荐新建文件夹,重新使用下面的命令下载代码作为实验 2 工作区。

$ git clone git://g.csail.mit.edu/xv6-labs-2020
$ git checkout syscall

System call tracing (moderate)

实验目的

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

实验要求及提示

  • $U/_trace 添加到 MakefileUPROGS
  • 运行 make qemu , 你将看到编译器无法编译 user/trace.c ,因为系统调用的用户空间存根还不存在:将系统调用的原型添加到 user/user.h ,将存根添加到 user/usys.pl ,以及将系统调用号添加到 kernel/syscall.h 中。 Makefile 调用 perl 脚本 user/usys.pl ,它生成 user/usys.S ,实际的系统调用存根,它使用 RISC-V ecall 指令转换到内核。修复编译问题后,运行 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() 函数以打印跟踪输出。你将需要添加要索引的系统调用名称数组。

实验思路

  1. 首先,根据实验前须知阅读 xv6 文档的第 2 章和第 4 章的 4.3 节和 4.4 节以及相关源文件。其中第 2 章讲的是 xv6 系统的组织结构,第 4 章的 4.3 节讲的是调用 system call 的过程,第 4 章的 4.4 节讲的是调用 system call 的参数。与本实验直接相关,所以必须依照源码进行阅读。
  2. 这里补充一点,做这个实验需要对 xv6 启动过程以及调用系统调用过程有一些了解,具体可以观看上课视频的结尾部分,视频地址:https://www.bilibili.com/video/BV19k4y1C7kA?p=2
  3. 具体过程解释见下面的实验步骤。

实验步骤

作为一个系统调用,我们先要定义一个系统调用的序号。系统调用序号的宏定义在 kernel/syscall.h 文件中。我们在 kernel/syscall.h 添加宏定义,模仿已经存在的系统调用序号的宏定义,我们定义 SYS_trace 如下:

#define SYS_trace  22

查看了一下 user 目录下的文件,发现官方已经给出了用户态的 trace 函数( user/trace.c ),所以我们直接在 user/user.h 文件中声明用户态可以调用 trace 系统调用就好了,但有一个问题,该系统调用的参数和返回值分别是什么类型呢?接下来我们还是得看一看 trace.c 文件,可以看到 trace(atoi(argv[1])) < 0 ,即 trace 函数传入的是一个数字,并和 0 进行比较,结合实验提示,我们知道传入的参数类型是 int ,并且由此可以猜测到返回值类型应该是 int 。这样就可以把 trace 这个系统调用加入到内核中声明了:

// system calls
int trace(int);

接下来我们查看 user/usys.pl 文件,这里 perl 语言会自动生成汇编语言 usys.S ,是用户态系统调用接口。所以在 user/usys.pl 文件加入下面的语句:

entry("trace");

如果你编译后查看 usys.S 文件,就能可以看到存在把系统调用号放入 a7 寄存器的指令,然后就直接使用命令 ecall 进入系统内核。不信我们先查看上一次实验编译后的 usys.S 文件,可以看到如下的代码块:

.global fork
fork:
 li a7, SYS_fork
 ecall
 ret

li a7, SYS_fork 指令就是把 SYS_fork 的系统调用号放入 a7 寄存器,使用 ecall 指令进入系统内核。

那么,执行 ecall 指令会跳转到哪里呢?答案是跳转到 kernel/syscall.csyscall 那个函数处,执行此函数。下面是 syscall 函数的源码:

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]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

其中,我们可以看到, num = p->trapframe->a7; 从寄存器 a7 中读取系统调用号,所以上面的 usys.S 文件就是系统调用用户态和内核态的切换接口。接下来是 p->trapframe->a0 = syscalls[num](); 语句,通过调用 syscalls[num](); 函数,把返回值保存在了 a0 寄存器中。我们看看 syscalls[num](); 函数,这个函数在当前文件中。该函数调用了系统调用命令。

static uint64 (*syscalls[])(void) = {
  [SYS_fork]    sys_fork,
  [SYS_exit]    sys_exit,
  ...
}

所以我们把新增的 trace 系统调用添加到函数指针数组 *syscalls[] 上:

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

接下来在文件开头给内核态的系统调用 trace 加上声明,在 kernel/syscall.c 加上:

extern uint64 sys_trace(void);

在实现这个函数之前,我们可以看到实验最后要输出每个系统调用函数的调用情况,依照实验说明给的示例,可以知道最后输出的格式如下:

<pid>: syscall <syscall_name> -> <return_value>

其中, <pid> 是进程序号, <syscall_name> 是函数名称, <return_value> 是该系统调用的返回值。注意:冒号和 syscall 中间有个空格,刚开始的时候自己就踩了一个坑。

根据提示,我们的 trace 系统调用应该有一个参数,一个整数“mask(掩码)”,其指定要跟踪的系统调用。所以,我们在 kernel/proc.h 文件的 proc 结构体中,新添加一个变量 mask ,使得每一个进程都有自己的 mask ,即要跟踪的系统调用。

struct proc {
  ...
  int mask;               // Mask
};

然后我们就可以在 kernel/sysproc.c 给出 sys_trace 函数的具体实现了,只要把传进来的参数给到现有进程的 mask 就好了:

uint64
sys_trace(void)
{
  int mask;
  // 取 a0 寄存器中的值返回给 mask
  if(argint(0, &mask) < 0)
    return -1;
  
  // 把 mask 传给现有进程的 mask
  myproc()->mask = mask;
  return 0;
}

接下来我们就要把输出功能实现,因为 RISCV 的 C 规范是把返回值放在 a0 中,所以我们只要在调用系统调用时判断是不是 mask 规定的输出函数,如果是就输出。

因为 proc 结构体(见 kernel/proc.h )里的 name 是整个线程的名字,不是函数调用的函数名称,所以我们不能用 p->name ,而要自己定义一个数组,我这里直接在 kernel/syscall.c 中定义了,这里注意系统调用名字一定要按顺序,第一个为空,当然你也可以去掉第一个空字符串,但要记得取值的时候索引要减一,因为这里的系统调用号是从 1 开始的。

static char *syscall_names[] = {
  "", "fork", "exit", "wait", "pipe", 
  "read", "kill", "exec", "fstat", "chdir", 
  "dup", "getpid", "sbrk", "sleep", "uptime", 
  "open", "write", "mknod", "unlink", "link", 
  "mkdir", "close", "trace"};

然后我们就可以在 kernel/syscall.c 中的 syscall 函数中添加打印调用情况语句。 mask 是按位判断的,所以判断使用的是按位运算。进程序号直接通过 p->pid 就可以取到,函数名称需要从我们刚刚定义的数组中获取,即 syscall_names[num] ,其中 num 是从寄存器 a7 中读取的系统调用号,系统调用的返回值就是寄存器 a0 的值了,直接通过 p->trapframe->a0 语句获取即可。注意上面说的那个空格。

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]();
    // 下面是添加的部分
    if((1 << num) & p->mask) {
      printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num], p->trapframe->a0);
    }
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

然后在 kernel/proc.cfork 函数调用时,添加子进程复制父进程的 mask 的代码:

int
fork(void)
{
  ...

  pid = np->pid;

  np->state = RUNNABLE;

  // 子进程复制父进程的 mask 
  np->mask = p->mask;

  ...
}

最后在 MakefileUPROGS 中添加:

UPROGS=\
  ...
  $U/_trace\

实验结果

编译并运行 xv6 进行测试。

$ make qemu
...
init: starting sh
$ 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
...
$   

退出 xv6 ,运行单元测试检查结果是否正确。

./grade-lab-syscall trace

通过测试样例。

== Test trace 32 grep == trace 32 grep: OK (2.6s) 
== Test trace all grep == trace all grep: OK (1.0s) 
== Test trace nothing == trace nothing: OK (0.5s) 
== Test trace children == trace children: OK (10.8s) 

Sysinfo (moderate)

实验要求

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

实验提示

  • $U/_sysinfotest 添加到 MakefileUPROGS 中。
  • 运行 make qemu , 你将看到编译器无法编译 user/sysinfotest.c 。添加系统调用 sysinfo ,按照与之前实验相同的步骤。要在 user/user.h 中声明 sysinfo() 的原型,您需要预先声明 struct sysinfo
struct sysinfo;
int sysinfo(struct sysinfo *);
  • 修复编译问题后,运行 sysinfotest 会失败,因为你还没有在内核中实现系统调用。
  • sysinfo 需要复制一个 struct sysinfo 返回用户空间;有关如何使用 copyout() 执行此操作的示例,请参阅 sys_fstat() ( kernel/sysfile.c ) 和 filestat() ( kernel/file.c )。
  • 要收集空闲内存量,请在 kernel/kalloc.c 中添加一个函数。
  • 要收集进程数,请在 kernel/proc.c 中添加一个函数。

实验步骤

跟上个实验一样,首先定义一个系统调用的序号。系统调用序号的宏定义在 kernel/syscall.h 文件中。我们在 kernel/syscall.h 添加宏定义 SYS_sysinfo 如下:

#define SYS_sysinfo  23

user/usys.pl 文件加入下面的语句:

entry("sysinfo");

然后在 user/user.h 中添加 sysinfo 结构体以及 sysinfo 函数的声明:

struct stat;
struct rtcdate;
// 添加 sysinfo 结构体
struct sysinfo;

// system calls
...
int sysinfo(struct sysinfo *);

kernel/syscall.c 中新增 sys_sysinfo 函数的定义:

extern uint64 sys_sysinfo(void);

kernel/syscall.c 中函数指针数组新增 sys_trace

[SYS_sysinfo]   sys_sysinfo,

记得在 kernel/syscall.c 中的 syscall_names 新增一个 sys_trace

static char *syscall_names[] = {
  "", "fork", "exit", "wait", "pipe", 
  "read", "kill", "exec", "fstat", "chdir", 
  "dup", "getpid", "sbrk", "sleep", "uptime", 
  "open", "write", "mknod", "unlink", "link", 
  "mkdir", "close", "trace", "sysinfo"};

接下来我们就要开始写相应的函数实现了。

首先我们写获取可用进程数目的函数实现。通过阅读 kernel/proc.c 文件可以看到下面的语句:

struct proc proc[NPROC];

这是一个进程数组的定义,这里保存了所有的进程。我们再阅读 kernel/proc.h 查看进程结构体的定义:

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

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

可以看到,进程里面已经保存了当前进程的状态,所以我们可以直接遍历所有进程,获取其状态判断当前进程的状态是不是为 UNUSED 并统计数目就行了。当然,通过 proc 结构体的定义,我们知道使用进程状态时必须加锁,我们在 kernel/proc.c 中新增函数 nproc 如下,通过该函数以获取可用进程数目:

// Return the number of processes whose state is not UNUSED
uint64
nproc(void)
{
  struct proc *p;
  // counting the number of processes
  uint64 num = 0;
  // traverse all processes
  for (p = proc; p < &proc[NPROC]; p++)
  {
    // add lock
    acquire(&p->lock);
    // if the processes's state is not UNUSED
    if (p->state != UNUSED)
    {
      // the num add one
      num++;
    }
    // release lock
    release(&p->lock);
  }
  return num;
}

接下来我们来实现获取空闲内存数量的函数。可用空间的判断在 kernel/kalloc.c 文件中。
这里定义了一个链表,每个链表都指向上一个可用空间,这里的 kmem 就是一个保存最后链表的变量。

struct run {
  struct run *next;
};

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

要想更深入了解的话就详细看看当前这个文件(下面摘了部分内容):

extern char end[]; // first address after kernel.
                   // defined by kernel.ld.

void
kinit()
{
  initlock(&kmem.lock, "kmem");
  freerange(end, (void*)PHYSTOP);
}

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
    kfree(p);
}

// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc().  (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
  struct run *r;

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");

  // Fill with junk to catch dangling refs.
  memset(pa, 1, PGSIZE);

  r = (struct run*)pa;

  acquire(&kmem.lock);
  r->next = kmem.freelist;
  kmem.freelist = r;
  release(&kmem.lock);
}

这里把从 end (内核后的第一个地址)PHYSTOP (KERNBASE + 128*1024*1024) 之间的物理空间以 PGSIZE 为单位全部初始化为 1 ,然后每次初始化一个 PGSIZE 就把这个页挂在了 kmem.freelist 上,所以 kmem.freelist 永远指向最后一个可用页,那我们只要顺着这个链表往前走,直到 NULL 为止。所以我们就可以在 kernel/kalloc.c 中新增函数 free_mem ,以获取空闲内存数量:

// Return the number of bytes of free memory
uint64
free_mem(void)
{
  struct run *r;
  // counting the number of free page
  uint64 num = 0;
  // add lock
  acquire(&kmem.lock);
  // r points to freelist
  r = kmem.freelist;
  // while r not null
  while (r)
  {
    // the num add one
    num++;
    // r points to the next
    r = r->next;
  }
  // release lock
  release(&kmem.lock);
  // page multiplicated 4096-byte page
  return num * PGSIZE;
}

然后在 kernel/defs.h 中添加上述两个新增函数的声明:

// kalloc.c
...
uint64          free_mem(void);

// proc.c
...
uint64          nproc(void);

接下来我们按照实验提示,添加 sys_sysinfo 函数的具体实现,这里提到 sysinfo 需要复制一个 struct sysinfo 返回用户空间,根据实验提示使用 copyout() 执行此操作,我们查看 kernel/sysfile.c 文件中的 sys_fstat() 函数,如下:

uint64
sys_fstat(void)
{
  struct file *f;
  uint64 st; // user pointer to struct stat

  if(argfd(0, 0, &f) < 0 || argaddr(1, &st) < 0)
    return -1;
  return filestat(f, st);
}

这里可以看到调用了 filestat() 函数,该函数在 kernel/file.c 中,如下:

// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
  struct proc *p = myproc();
  struct stat st;
  
  if(f->type == FD_INODE || f->type == FD_DEVICE){
    ilock(f->ip);
    stati(f->ip, &st);
    iunlock(f->ip);
    if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
      return -1;
    return 0;
  }
  return -1;
}

我们可以知道,复制一个 struct sysinfo 返回用户空间需要调用 copyout() 函数,上面是一个例子,我们来查看一下 copyout() 函数的定义( kernel/vm.c ):

// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;

  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;
    n = PGSIZE - (dstva - va0);
    if(n > len)
      n = len;
    memmove((void *)(pa0 + (dstva - va0)), src, n);

    len -= n;
    src += n;
    dstva = va0 + PGSIZE;
  }
  return 0;
}

我们知道该函数其实就是把在内核地址 src 开始的 len 大小的数据拷贝到用户进程 pagetable 的虚地址 dstva 处,所以 sys_sysinfo 函数实现里先用 argaddr 函数读进来我们要保存的在用户态的数据 sysinfo 的指针地址,然后再把从内核里得到的 sysinfo 开始的内容以 sizeof(info) 大小的的数据复制到这个指针上。模仿上面的例子,我们在 kernel/sysproc.c 文件中添加 sys_sysinfo 函数的具体实现如下:

// add header
#include "sysinfo.h"

uint64
sys_sysinfo(void)
{
  // addr is a user virtual address, pointing to a struct sysinfo
  uint64 addr;
  struct sysinfo info;
  struct proc *p = myproc();
  
  if (argaddr(0, &addr) < 0)
	  return -1;
  // get the number of bytes of free memory
  info.freemem = free_mem();
  // get the number of processes whose state is not UNUSED
  info.nproc = nproc();

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

最后在 user 目录下添加一个 sysinfo.c 用户程序:

#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/sysinfo.h"
#include "user/user.h"

int
main(int argc, char *argv[])
{
    // param error
    if (argc != 1)
    {
        fprintf(2, "Usage: %s need not param\n", argv[0]);
        exit(1);
    }

    struct sysinfo info;
    sysinfo(&info);
    // print the sysinfo
    printf("free space: %d\nused process: %d\n", info.freemem, info.nproc);
    exit(0);
}

最后在 MakefileUPROGS 中添加:

$U/_sysinfotest\
$U/_sysinfo\

实验结果

编译并运行 xv6 进行测试。

$ make qemu
...
init: starting sh
$ sysinfo
free space: 133386240
used process: 3
$ sysinfotest
sysinfotest: start
sysinfotest: OK

退出 xv6 ,运行单元测试检查结果是否正确。

./grade-lab-syscall sysinfo

通过测试样例。

make: 'kernel/kernel' is up to date.
== Test sysinfotest == sysinfotest: OK (2.6s) 

Lab 2 所有实验测试

退出 xv6 ,使用命令 vim time.txt 新建文件写入你做该实验所花的时间(小时),运行整个 Lab 2 测试,检查结果是否正确。

$ make grade
...
== Test trace 32 grep == 
$ make qemu-gdb
trace 32 grep: OK (2.3s) 
== Test trace all grep == 
$ make qemu-gdb
trace all grep: OK (1.0s) 
== Test trace nothing == 
$ make qemu-gdb
trace nothing: OK (0.8s) 
== Test trace children == 
$ make qemu-gdb
trace children: OK (10.1s) 
== Test sysinfotest == 
$ make qemu-gdb
sysinfotest: OK (2.3s) 
== Test time == 
time: OK 
Score: 35/35
  • 40
    点赞
  • 108
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值