MIT 6.S081 Lab2 System Calls
MIT 6.S081 Lab2 System Calls

MIT 6.S081 Lab2 System Calls

源码地址

System call tracing (moderate)

实验目的

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

实验要求及提示

  • $U/_trace添加到Makefile\UPROGS
  • 运行make qemu, 你将看到编译器无法编译user/trace.c,因为系统调用的用户空间存根还不存在:将系统调用的原型添加到user/user.h,将存根添加到user/usys.pl,以及将系统调用号添加到kernel/syscall.h中。Makefile调用perl脚本user/usys.pl,它生成user/usys.S,实际的系统调用存根,它使用RISC-Vecall指令转换到内核。修复编译问题后,运行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 )。完成后,您应该看到如下输出:

$ 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
...
$

实验步骤

首先按照提示在 kernel/syscall.h 中添加一个宏定义,定义一个系统调用的序号。

// kernel/syscall.h
#define SYS_trace  22

查看 user/trace.h 中的内容,Lab已经给出了用户态的trace调用方式,所以我们可以在 user/user.h 中声明trace系统调用。由Lab内容可知,trace读入一个int类型,代表想要追踪的进程的代号,同时由 user/trace.h 可知需要返回一个int判断是否成功追踪。

// user/user.h
// system calls
...
int trace(int);

接下来按照提示查看 user/usys.pl ,这里perl语言会自动生成汇编语言usys.S,是用户态系统调用接口,添加一个entry。

# user/usys.pl
...
entry("trace");

由课程视频后半段和Chapter2可以知道,汇编中会把系统调用号保存到a7寄存器,然后执行ecall

.global trace
trace:
 li a7, SYS_trace
 ecall
 ret

然后ecall指令执行 kernel/syscall.c 中的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;
  }
}

阅读代码可知,syscall先将a7寄存器中的系统调用号保存到num变量中,如果满足条件就把syscall中的相应内容(函数指针)保存到a0寄存器中去。
所以我们需要在syscalls数组中添加函数指针,并且在文件开头给内核态的系统调用trace加上声明:

// kernel/syscall.c
...
extern uint64 sys_trace(void);

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

同时我们需要给每一个进程增加一个变量mask来指定追踪的系统调用。

// kernel/proc.h
struct proc {
  ...
  int mask;                    // Mask for sys_trace
  ...
};

然后就可以在 kernel/sysproc.c 中编写sys_trace的代码。
此处为何能直接从a0寄存器中取出,个人理解是因为我们调用的trace函数第一个且只有一个参数保存在a0寄存器中,通过脚本生成的汇编文件默认放置在a0未改变,所以此处可直接取用。

// kernel/sysproc.c
...
uint64
sys_trace(void)
{
  int mask;
  // 取出a0寄存器到mask中
  if(argint(0, &mask) < 0)
    return -1;
  // 放入到相应进程中去
  myproc()->mask = mask;
  return 0;
}

接下来我们要实现相应的输出功能,由于源代码没有给出相应系统调用号对应的函数字符串,所以我们自己在代码中增加。我在 kernel/syscall.c 中添加了相应代码(0处为空因为没有为0的系统调用号):

// kernel/syscall.c
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",
};

按照提示修改fork函数,在fork时同样复制结构体中的mask变量(基本任意添加)。

// kernel/proc.c
// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int
fork(void)
{
  int i, pid;
  struct proc *np;
  struct proc *p = myproc();
  ...
  np->mask = p->mask;
  ...
  return pid;
}

然后在 kernel/syscall.c 中的syscall函数中增加输出语句(需要用到mask)。

// kernel/syscall.c
...
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 ((p->mask) & (1<<num)) { // 在追踪集中
      /* <pid>: syscall <syscall_name> -> <return_value> */
      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;
  }
}

最后记得在Makefile中的UPROGS添加编译即可:

UPROGS=\
    ...
    $U/_trace\

实验结果

本地测试用例通过。

cheeselongan@cheeselongan-virtual-machine:~/Desktop/xv6-labs-2021$ ./grade-lab-syscall trace
== Test trace 32 grep == trace 32 grep: OK (0.9s) 
== Test trace all grep == trace all grep: OK (0.9s) 
== Test trace nothing == trace nothing: OK (1.0s) 
== Test trace children == trace children: OK (11.2s) 

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
#define SYS_sysinfo  23

user/user.h 中添加sysinfo结构体以及sysinfo函数声明。

// user/user.h
...
struct sysinfo;

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

kernel/syscall.c 中新增sys_sysinfo函数的定义,同时在函数指针数组中新增sys_sysinfosyscall_names新增一个sysinfo

// kernel/syscall.c
static char *syscall_names[] = {
  ...
  "trace",
  "sysinfo",
};
...
extern uint64 sys_sysinfo(void);
...
[SYS_sysinfo]   sys_sysinfo,

完成以上步骤后开始编写函数实现。
kernel/proc.h 定义了和进程相关的结构体:

// kernel/proc.h
enum procstate { UNUSED, USED, 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
  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.
  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 for trace
};

其中有表示process state的state变量是我们需要的,但是使用时要注意加锁。加锁操作可以看一些代码,使用已经编写好的acquire函数。同时 kernel/proc.c 中前部代码可知进程限制最大为NPROC

// kernel/proc.c
uint64
cntproc(void)
{
  struct proc* p;
  uint64 tot = 0;
  for (p = proc; p < &proc[NPROC]; ++p) {
    acquire(&p->lock); // 加锁
    if (p->state != UNUSED) {
      ++tot;
    }
    release(&p->lock); // 释放
  }
  return tot;
}

kernel/kalloc.c 定义了空闲内存的一些函数和结构体,其中有一个链表run(使用时需要加锁),每个结点指向上一个可用的空间,这里的kmem.freelist是链表的最后一个节点。

// kernel/kalloc.c
struct run {
  struct run *next;
};

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

可以 kernel/kalloc.c 下面的代码大概理解一下具体的使用方法,这里简单说一个需要用到的点,可用空间按照PGSIZE byte为单位初始化为1,所以我们只要从kmem.freelist向前跳直到null为止。

// Count free memory
uint64
cntfreemem(void)
{
  struct run* r;
  uint64 tot = 0;
  acquire(&kmem.lock);
  r = kmem.freelist;
  while (r) {
    r = r->next;
    ++tot;
  }
  release(&kmem.lock);
  return tot*PGSIZE;
}

然后在 kernel/defs.h 中添加新增函数声明。

// kernel/defs.h
// kalloc.c
...
uint64          cntfreemem(void);

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

接着按照提示查看 kernel/sysfile.csys_fstat函数,其中调用的filestat函数调用的copyout函数,其功能是从内核空间复制内存从内核地址src开始的len大小数据到用户进程pagetable的一个虚拟地址dstva中去。我们此处只需要知道copyout的用法即可。

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

然后是 kernel/syscall.c 中的argaddr函数,功能是从指定寄存器中取出内容放置到指定地址中。

// kernel/syscall.c
// Retrieve an argument as a pointer.
// Doesn't check for legality, since
// copyin/copyout will do that.
int
argaddr(int n, uint64 *ip)
{
  *ip = argraw(n);
  return 0;
}

下面编写sys_sysinfo函数,还是从a0读入我们需要保存到用户态数据sysinfo的指针地址,然后调用上面编写的两个函数得到具体的空闲内存和总进程数,接着把这个结构体的内容复制到这个指针中去。

// kernel/sysproc.c
...
uint64
sys_sysinfo(void)
{
  uint64 addr;
  struct sysinfo info;
  struct proc *p = myproc();

  if (argaddr(0, &addr) < 0) {
    return -1;
  }

  info.freemem = cntfreemem();
  info.nproc   = cntproc();

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

  return 0;
}

然后在用户态添加一个sysinfo用户程序。

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

int
main(int argc, char *argv[])
{
  if(argc != 1){
    fprintf(2, "Param fail", argv[0]);
    exit(1);
  }

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

最后在Makefile中的UPROGS添加两个编译项sysinfotestsysinfo,编译并测试。

实验结果

本地测试通过。

== Test sysinfotest == sysinfotest: OK (3.9s) 

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注