操作系统xv6实验Lab2

227 阅读5分钟

Lab2:system calls

System call tracing(moderate)

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

image.png 在上面的第一个例子中,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()函数以打印跟踪输出。您将需要添加一个系统调用名称数组以建立索引。

解答

题目要求添加一个新的trace系统调用,用来跟踪其他的系统调用。需要为其他每一个系统调用设定一个位mask,用mask设定的位来指定跟踪哪一个系统调用,并输出所要求的调试信息

先在syscall.h中添加trace的mask:

#define SYS_fork    1
#define SYS_exit    2
#define SYS_wait    3
#define SYS_pipe    4
#define SYS_read    5
#define SYS_kill    6
#define SYS_exec    7
#define SYS_fstat   8
#define SYS_chdir   9
#define SYS_dup    10
#define SYS_getpid 11
#define SYS_sbrk   12
#define SYS_sleep  13
#define SYS_uptime 14
#define SYS_open   15
#define SYS_write  16
#define SYS_mknod  17
#define SYS_unlink 18
#define SYS_link   19
#define SYS_mkdir  20
#define SYS_close  21
#define SYS_trace  22       //trace系统调用号

在syscall.c中全局声明trace系统调用处理函数,并且把系统调用号与处理函数关联:

extern uint64 sys_chdir(void);
extern uint64 sys_close(void);
extern uint64 sys_dup(void);
extern uint64 sys_exec(void);
extern uint64 sys_exit(void);
extern uint64 sys_fork(void);
extern uint64 sys_fstat(void);
extern uint64 sys_getpid(void);
extern uint64 sys_kill(void);
extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_mknod(void);
extern uint64 sys_open(void);
extern uint64 sys_pipe(void);
extern uint64 sys_read(void);
extern uint64 sys_sbrk(void);
extern uint64 sys_sleep(void);
extern uint64 sys_unlink(void);
extern uint64 sys_wait(void);
extern uint64 sys_write(void);
extern uint64 sys_uptime(void);
extern uint64 sys_trace(void);      //全局声明trace系统调用处理函数

static uint64 (*syscalls[])(void) = {
[SYS_fork]    sys_fork,
[SYS_exit]    sys_exit,
[SYS_wait]    sys_wait,
[SYS_pipe]    sys_pipe,
[SYS_read]    sys_read,
[SYS_kill]    sys_kill,
[SYS_exec]    sys_exec,
[SYS_fstat]   sys_fstat,
[SYS_chdir]   sys_chdir,
[SYS_dup]     sys_dup,
[SYS_getpid]  sys_getpid,
[SYS_sbrk]    sys_sbrk,
[SYS_sleep]   sys_sleep,
[SYS_uptime]  sys_uptime,
[SYS_open]    sys_open,
[SYS_write]   sys_write,
[SYS_mknod]   sys_mknod,
[SYS_unlink]  sys_unlink,
[SYS_link]    sys_link,
[SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,
[SYS_trace]   sys_trace,        //系统调用号与处理函数关联
};

在 proc.h 中修改 进程类proc 结构的定义,添加 syscall_trace,用 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)
  uint64 syscall_trace;        //存储进程的系统调用跟踪掩码,用于控制哪些系统调用需要被跟踪
};

在 proc.c 中,创建新进程的时候,为syscall_trace 设置默认值 0(否则会是随机数据):

static struct proc* 
allocproc(void) {

    ...... 
    
    // Set up new context to start executing at forkret, 
    // which returns to user space. 
    memset(&p->context, 0, sizeof(p->context)); 
    p->context.ra = (uint64)forkret; 
    p->context.sp = p->kstack + PGSIZE; 
    p->syscall_trace = 0; 

    syscall_trace //创建新进程的时候,设置为默认值0 

    return p; 
}

在sysproc.c 中,实现跟踪的具体代码,也就是设置当前进程的 syscall_trace:

//当前进程的系统调用跟踪掩码
uint64
sys_trace(void)
{
    int mask;

    if(argint(0, &mask) < 0)                // 通过读取进程的trapframe,获得 mask 参数
        return -1;

    myproc()->syscall_trace = mask;         // 设置调用进程的syscall_trace掩码mask
    return 0;
}

在proc.c中,修改 fork 函数,使得子进程可以继承父进程的 syscall_trace mask:

int 
fork(void) {
    ...... 
    // 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->syscall_trace = p->syscall_trace; //子进程继承父进程的syscall_trace
    
    pid = np->pid; 
    
    np->state = RUNNABLE; 
    
    release(&np->lock); 
    
    return pid; 
}

所有的系统调用到达内核态后,都会进入到 syscall() 这个函数进行处理,因此在syscall函数中做出修改(在syscall.c中):

//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]();                           // 通过系统调用编号,获取系统调用处理函数的指针,调用并将返回值存到用户进程的 a0 寄存器中

      if ((p->syscall_trace >> num) & 1) {				// 如果当前进程设置了对该编号系统调用的 trace
          printf("%d: syscall %s -> %d\n",p->pid, syscalls_name[num], p->trapframe->a0); // syscall_names[num]: 从 syscall 编号到 syscall 名的映射表
      }
  }
  else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

打印上述信息需要知道系统调用对应的名称,可以定义一个字符串数组映射:

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",  //对应名称
};

内核部分配置完毕,接下来是用户态部分。

在 usys.pl 中,加入用户态到内核态的跳板函数:

entry("fork");
entry("exit");
entry("wait");
entry("pipe");
entry("read");
entry("write");
entry("close");
entry("kill");
entry("exec");
entry("open");
entry("mknod");
entry("unlink");
entry("fstat");
entry("link");
entry("mkdir");
entry("chdir");
entry("dup");
entry("getpid");
entry("sbrk");
entry("sleep");
entry("uptime");
entry("trace");		//用户态下的程序通过调用trace函数来使用跟踪系统调用功能

该脚本运行后生成汇编文件,其中定义了每一个用户态下每一个系统调用的跳板函数

在用户态的头文件user.h加入函数声明,这用用户态可以找到这个跳板入口函数:

// system calls
int fork(void);
int exit(int) __attribute__((noreturn));
int wait(int*);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
int exec(char*, char**);
int open(const char*, int);
int mknod(const char*, short, short);
int unlink(const char*);
int fstat(int fd, struct stat*);
int link(const char*, const char*);
int mkdir(const char*);
int chdir(const char*);
int dup(int);
int getpid(void);
char* sbrk(int);
int sleep(int);
int uptime(void);
int trace(int);         //用户态程序可以找到trace系统调用的跳板入口函数

到此可以执行./grade-lab-syscall trace验证该实验是否正确

全流程

user/user.h:		用户态程序调用跳板函数 trace()
user/usys.S:		跳板函数 trace() 使用 CPU 提供的 ecall 指令,调用到内核态
kernel/syscall.c	        到达内核态统一系统调用处理函数 syscall(),所有系统调用都会跳到这里来处理。
kernel/syscall.c	        syscall() 根据跳板传进来的系统调用编号,查询 syscalls[] 表,找到对应的内核函数并调用。
kernel/sysproc.c	        到达 sys_trace() 函数,执行具体内核操作

这么繁琐的调用流程的主要目的是实现用户态和内核态的良好隔离。

并且由于内核与用户进程的页表不同,寄存器也不互通,所以参数无法直接通过 C 语言参数的形式传过来,而是需要使用 argaddr、argint、argstr 等系列函数,从进程的 trapframe 中读取用户进程寄存器中的参数。

同时由于页表不同,指针也不能直接互通访问(也就是内核不能直接对用户态传进来的指针进行解引用),而是需要使用 copyin、copyout 方法结合进程的页表,才能顺利找到用户态指针(逻辑地址)对应的物理内存地址。

Sysinfo(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中添加一个函数

解答

题目要求添加一个新的系统调用,获取 空闲内存量 和 已经创建的进程数量 并返回。

先在syscall.h中添加系统调用号:

// System call numbers
#define SYS_fork    1
#define SYS_exit    2
#define SYS_wait    3
#define SYS_pipe    4
#define SYS_read    5
#define SYS_kill    6
#define SYS_exec    7
#define SYS_fstat   8
#define SYS_chdir   9
#define SYS_dup    10
#define SYS_getpid 11
#define SYS_sbrk   12
#define SYS_sleep  13
#define SYS_uptime 14
#define SYS_open   15
#define SYS_write  16
#define SYS_mknod  17
#define SYS_unlink 18
#define SYS_link   19
#define SYS_mkdir  20
#define SYS_close  21
#define SYS_trace  22       //trace系统调用号
#define SYS_sysinfo 23  // 为sysinfo分配一个新的编号

在内存相关的kalloc.c中添加计算空闲内存的函数:

//获取空闲内存
void freebytes(uint64* dst) {
    *dst = 0;
    struct run* p = kmem.freelist;

    acquire(&kmem.lock);		//添加锁,防止竞态
    while (p) {
        *dst += PGSIZE;			//统计空闲字节数
        p = p->next;
    }
    release(&kmem.lock);
}

然后在内核头文件中defs.h声明这个函数:

// kernel/defs.h 
// kalloc.c
void*           kalloc(void);
void            kfree(void *);
void            kinit(void);
void            freebytes(uint64* dst);         //获取空闲内存

xv6 中,空闲内存页的记录方式是,将空虚内存页本身直接用作链表节点,形成一个空闲页链表,每次需要分配,就把链表根部对应的页分配出去。每次需要回收,就把这个页作为新的根节点,把原来的 freelist 链表接到后面。注意这里是直接使用空闲页本身作为链表节点,所以不需要使用额外空间来存储空闲页链表。

在进程相关的proc.c中添加计算进程数量的函数:

//统计处于活动状态的进程
void
procnum(uint64* dst) {
    *dst = 0;
    struct proc* p;
    for (p = proc;p < &proc[NPROC];p++) {
        if (p->state != UNUSED)
            (*dst)++;
    }
}

在内核头文件中声明函数:

// kernel/defs.h 
// proc.c
int             cpuid(void);
void            exit(int);
int             fork(void);
int             growproc(int);
pagetable_t     proc_pagetable(struct proc *);
void            proc_freepagetable(pagetable_t, uint64);
int             kill(int);
struct cpu*     mycpu(void);
struct cpu*     getmycpu(void);
struct proc*    myproc();
void            procinit(void);
void            scheduler(void) __attribute__((noreturn));
void            sched(void);
void            setproc(struct proc*);
void            sleep(void*, struct spinlock*);
void            userinit(void);
int             wait(uint64);
void            wakeup(void*);
void            yield(void);
int             either_copyout(int user_dst, uint64 dst, void *src, uint64 len);
int             either_copyin(void *dst, int user_src, uint64 src, uint64 len);
void            procdump(void);
void            procnum(uint64* dst);       //统计处于活动状态的进程

在sysproc.c中引入头文件sysinfo.h:

#include "types.h"
#include "riscv.h"
#include "defs.h"
#include "date.h"
#include "param.h"
#include "memlayout.h"
#include "spinlock.h"
#include "proc.h"
#include "sysinfo.h"

在sysproc.c中实现sysinfo:

//收集系统信息
uint64
sys_sysinfo(void) {
    struct sysinfo info;
    freebytes(&info.freemem);
    procnum(&info.nproc);

    //获取虚拟地址
    uint64 dstaddr;
    argaddr(0, &dstaddr);

    //从内核空间拷贝数据到用户空间
    if (copyout(myproc()->pagetable, dstaddr, (char*)&info, sizeof info) < 0)
        return -1;

    return 0;
}

syscall.c中添加对应的系统调用映射:

extern uint64 sys_chdir(void);
extern uint64 sys_close(void);
extern uint64 sys_dup(void);
extern uint64 sys_exec(void);
extern uint64 sys_exit(void);
extern uint64 sys_fork(void);
extern uint64 sys_fstat(void);
extern uint64 sys_getpid(void);
extern uint64 sys_kill(void);
extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_mknod(void);
extern uint64 sys_open(void);
extern uint64 sys_pipe(void);
extern uint64 sys_read(void);
extern uint64 sys_sbrk(void);
extern uint64 sys_sleep(void);
extern uint64 sys_unlink(void);
extern uint64 sys_wait(void);
extern uint64 sys_write(void);
extern uint64 sys_uptime(void);
extern uint64 sys_trace(void);      //全局声明trace系统调用处理函数
extern uint64 sys_sysinfo(void);    //全局声明sysinfo系统调用处理函数

static uint64 (*syscalls[])(void) = {
[SYS_fork]    sys_fork,
[SYS_exit]    sys_exit,
[SYS_wait]    sys_wait,
[SYS_pipe]    sys_pipe,
[SYS_read]    sys_read,
[SYS_kill]    sys_kill,
[SYS_exec]    sys_exec,
[SYS_fstat]   sys_fstat,
[SYS_chdir]   sys_chdir,
[SYS_dup]     sys_dup,
[SYS_getpid]  sys_getpid,
[SYS_sbrk]    sys_sbrk,
[SYS_sleep]   sys_sleep,
[SYS_uptime]  sys_uptime,
[SYS_open]    sys_open,
[SYS_write]   sys_write,
[SYS_mknod]   sys_mknod,
[SYS_unlink]  sys_unlink,
[SYS_link]    sys_link,
[SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,
[SYS_trace]   sys_trace,        //系统调用号与处理函数关联
[SYS_sysinfo]   sys_sysinfo,    //sysinfo映射
};

在用户态user.h和usys.pl添加跳板函数:

//user.h 
struct sysinfo;                       //预先声明sysinfo结构体 
int sysinfo(struct sysinfo*);         //用户态程序可以找到sysinfo系统调用的跳板入口函数 

//usys.pl 
entry("sysinfo");

执行./grade-lab-syscall sysinfotest验证该实验是否正确