【6.S081|Lab2】实验笔记

断断续续花了一周的时间完成了Lab1,最大的感受就是对于数组、指针等概念不太熟悉。

Lab

实现Lab2需要对Xv6的kernel部分深入的了解,这里记录一些源码的阅读和解析

源码解析

什么事Trapframe?当异常或者系统调用产生时,寄存器的状态会被保存到Trapframe当中,此时会进入内核态处理异常或者执行系统调用,当处理完毕之后,就会根据Trapframe中的数据重新设置CPU的状态,返回到之前的执行状态中。

什么是Caller saved和Callee saved寄存器?看这里

struct proc

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

每个进程的状态都由该结构体管理。该结构体包含了 互斥锁lock进程态(Unused, Sleeping, Runnable, Running, Zombie)父进程的状态pid页表Trapframe 等重要信息,这就是 PCB (Process Control Block)

System call tracing

在这个实验中你需要加一个叫做trace的系统调用,它能够帮助你在未来的实验中debug。你将创建一个叫做trace的系统调用。这个系统调用接受一个整型的“掩码”参数,这个参数指代你将要追踪哪一个系统调用。例如为了追踪fork,程序应当调用trace(1 << SYS_fork),这里的SYS_fork是来自kernel/syscall.h的一个系统调用的序号。你应当修改xv6的内核,使它能够在系统调用返回时打印一行。这一行数据应当帮憨进程id,系统调用的名字以及系统调用的返回值;你不需要打印系统调用的参数。Trace系统调用应当能追踪调用它的进程以及其fork的子进程,但不应当影响其他进程。

例子如下:

$ trace 32 grep hello README # 32对应的2^5,所以是第五个系统调用
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
$
$ trace 2147483647 grep hello README # 32bit的低31bit全置1,追踪当前进程所有系统调用
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,什么都不输出
$
$ 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
...
$  

提示:

  1. 在Makefile的UPROGS加入$U/_trace
  2. 运行make qemu,你将会看到编译器不能编译user/trace.c,因为系统调用的用户空间声明还没有;在user/user.h加入函数声明,user/usys.pl加入stub,以及kernel/syscall.h加入系统调用的序号。makefile会打开user/usys.pl的perl脚本,生成user/usys.S,即实际意义上的系统调用stub,此时RISC-V可以调用ecall去运行。当你修复了编译问题,运行trace 32 grep hello README,还是会失败。因为你还没有在内核实现系统调用。
  3. kernel/sysproc.c中加入sys_trace()函数,这个函数的功能是:将trace的参数加载到proc结构体下的一个新的变量中(自己创建)。实现提取系统调用的参数的函数定义在kernel/syscall.c中。你可以在kernel/sysproc.c看到一些例子。
  4. 修改fork(),使其能从父进程复制trace mask的值。
  5. 修改kernel/syscall.csyscall()函数,让他能够打印出trace的输出。你可能需要加一个存储系统调用名字的数组。

首先通过阅读题目,理解题目的意思。目的就是我们可以根据trace+num+用户进程,追踪在这个用户进程中,序号为num的系统调用的调用记录。刚开始读了题目必然没有思路,没关系,我们先根据提示来。首先在Makefile中加入对应的值,使得我们能够将trace.c文件编译进来。

STEP 1

打开Makefile,下滑找到UPROGS,最后一行加入

UPROGS=\
	...
	$U/_wc\
	$U/_zombie\
	$U/_trace\

STEP 2

user/user.h加入函数声明

// system calls
...
int uptime(void);
int trace(int);

user/usys.pl加入对应stub

...
entry("uptime");
entry("trace");

kernel/syscall.h加入trace的序号

// System call numbers
...
#define SYS_close  21
#define SYS_trace  22

STEP 3

我们接着第三步,这里阅读英文版就会遇到一些卡壳的地方,不知道提示的第三点到底要干什么,如果仔细理解一下,我们如果要在syscall()函数层面追踪不同的系统调用,那么我们就需要理解syscall()在干什么。通过阅读Xv6的参考手册,我们知道系统调用是通过syscall()这一个接入点实现的。为了搜寻不同的系统调用,就需要以序号作为索引,而这个序号被保存在了Trapframe的a7寄存器中。下面代码块标注了大致意思。

void
syscall(void)
{
  int num;
  struct proc *p = myproc(); // 获取当前CPU的进程

  num = p->trapframe->a7;    // 从Trapframe中的a7寄存器获取系统调用序号
  
  // 合法性检查,如果通过就执行对应索引的syscall,并将返回值保存在a0,否则返回异常
  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;
  }
}

根据题意,如果我们要在这样的函数内调用一些值,我们必须在proc结构体中新加一个参数,这个参数用来记录这个进程需要trace的mask,方便我们这里进行判断并且打印。

所以先修改一下kernel/proc.h

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)
  
  //tracing mask
  uint32 tmask;
};

kernel/sysproc.c中加入sys_trace()

// 实现思路很简单,主要问题就是我们从哪获得trace的参数,并且把这个参数存放在什么地方
uint64
sys_trace(void)
{
  struct proc *p = myproc();
  int n;
  // 从寄存器a0获取参数
  if(argint(0, &n) < 0)
    return -1;
  // 存放在进程的结构体中
  p->tmask = n;
  return 0;
}

STEP 4

打开kernel/proc.c,找到fork(),通过观察源代码,发现基本做的就是np子进程和p父进程的信息复制,我们只需要在函数中加入np->tmask = p->tmask即可。

STEP 5

这里需要在执行完系统调用之后对该系统调用的序号做个逻辑判断,如果当前的系统调用序号等于tmask,那么打印信息,否则不打印。打开syscall.c,实现函数trace

// trace the corresponding system call during the process
void
trace(int nsyscall, struct proc *p)
{
  static char *sysCallname[22]={
    "fork", "exit", "wait", "pipe", "read", "kill",
    "exec", "fstat", "chdir", "dup", "getpid", "sbrk",
    "sleep", "uptime", "open", "write", "mknod", "unlink",
    "link", "mkdir", "close", "trace"
  };
  // 追踪全部系统调用
  if(2147483647 == p->tmask){
    printf("%d: syscall %s -> %d\n", p->pid, sysCallname[nsyscall-1], p->trapframe->a0);    
  }
  // 这里需要注意tmask实际是32bit的二进制数,输入的时候按照十进制输入的,比如00010000,
  // 就是第五个,输入32, 但nsyscall确确实实是1-22的十进制数,需要转化成二进制,采用移位操作
  else if((1<<(uint32)nsyscall) == p->tmask){
    printf("%d: syscall %s -> %d\n", p->pid, sysCallname[nsyscall-1], p->trapframe->a0);
  }
}

STEP 6

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

System call Sysinfo

这个实验中你将加入系统调用sysinfo。它可以收集运行系统有关的信息。这个系统调用需要一个参数:指向struct sysinfo(在kernel/sysinfo.h)的指针。内核应当把这个结构体内的数值赋值:freemem域应该被设置为空余内存的字节数量,nproc域应当被设置为状态不是UNUSED的进程数量。我们提供了测试程序sysinfotest,如果打印sysinfotest OK说明你通过了实验。

提示:

  1. 在Makefile的UPROGS加入$U/_sysinfotest

  2. 和实验1一样配置系统调用,解决编译问题。在user/user.h中声明

    struct sysinfo;

    int sysinfo(struct sysinfo *);

  3. sysinfo需要把struct sysinfo赋值到用户空间;查看sys_fstat()kernel/sysfile.c)以及filestat()kernel/file.c)如何使用copyout()

  4. 为了计算有多少空余内存,在kernel/kalloc.c加入一个函数。

  5. 为了计算有多少进程,在kernel/proc.c加入一个函数。

STEP 1

Makefile中加入对应的user代码,以便编译

UPROGS=\
	...
	$U/_wc\
	$U/_zombie\
	$U/_trace\
	$U/_sysinfotest\

STEP 2

user/user.h加入函数声明

// system calls
struct sysinfo;
...
int uptime(void);
int trace(int);
int sysinfo(struct sysinfo *);

user/usys.pl加入对应stub

...
entry("uptime");
entry("trace");
entry("sysinfo");

kernel/syscall.h加入trace的序号

// System call numbers
...
#define SYS_close  21
#define SYS_trace  22
#define SYS_sysinfo  23

STEP 3

接着我们阅读一下sys_fstat()

uint64
sys_fstat(void)
{
  struct file *f;
  uint64 st; // user pointer to struct stat
	
  // 根据Xv6手册,发现fstat应用程序接收两个参数,硬件自动保存到a0和a1
  // 这里先从a0 a1寄存器获取数据
  // 总结实验1和实验2可以发现argint获取整数数据
  //                     argfd获取文件数据
  //                     argaddr获取地址数据
  // 数据一般会按顺序存放在从a0开始的寄存器中
  // 做实验时这里有个bug,我实现的时候直接调用argaddr(1, &st) < 0
  // 导致数据读也读不出来,后来参考网上的程序才发现数据存在了a0
  if(argfd(0, 0, &f) < 0 || argaddr(1, &st) < 0)
    return -1;
  return filestat(f, st);
}

看一下filestat()

// 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){
    // 先根据文件数据把st设置好
    ilock(f->ip);
    stati(f->ip, &st);
    iunlock(f->ip);
    // copyout函数定义在vm.c,功能是将sizeof(st)长度的数据,从源目标st复制到进程的页表的addr虚拟地址中
    // 这个addr实际是该进程页表中trapframe里的a0的虚拟地址
    if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
      return -1;
    return 0;
  }
  return -1;
}

那么我们的逻辑也基本和函数filestat()一样,首先配置好struct sysinfo,然后复制回用户的页表中去。在sysproc.c添加如下代码。

uint64
sys_sysinfo(void)
{
  uint64 addr;  // pointer to struct sysinfo
  struct sysinfo si;
  struct proc *p = myproc();
	
  // 获取当前进程下的页表下的trapframe的寄存器a0的虚拟地址
  if(argaddr(0, &addr) < 0)
    return -1;
	
  // 根据计算好的数据把si配置好
  si.freemem = freemem();
  si.nproc = collectproc();
	
  // 内核的数据copy到对应进程下
  if(copyout(p->pagetable, addr, (char *)&si, sizeof(si)) < 0)
      return -1;
  // uint64 s = collectproc();
  // printf("i am sysinfo, num of proc is: %d\n", s);
  return 0;

}

STEP 4

接下来就是实现freemem()collectproc()了。

我们先找到kernel/kalloc.c,在不同文件下的代码一定是有相似性的,我们可以从这个文件下的代码思考我们如何实现。

struct run {
  struct run *next;
};

// 空余内存的抽象,由互斥锁保护,带有类似于链表的结构
struct {
  struct spinlock lock;
  struct run *freelist;
} kmem;

上述数据结构可以观察到有类似于链表的结构,可以猜测系统根据链接对应空闲区域的地址来将空闲区域连接起来的。找一个函数进一步分析,这里选kalloc()

// 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);
	
  // 把新申请的空间的数据全部设置为5
  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}

根据这样的分析,我们可以将问题转化为,这个freelist链表有多长的问题,所以实现freemem()的思路就有了。

uint64
freemem(void)
{
  struct run *r;
  uint64 num=0; //计数器
	
  // 锁住内存
  acquire(&kmem.lock);
  // 找到表头
  r = kmem.freelist;
	
  // 遍历链表
  while(r)
  {
    num++;
    r = r->next;
  }
  // 解锁
  release(&kmem.lock);
  // 注意内存分配是按页大小分配的,即4096个字节,所以我们输出的时候要乘4096,这样才是空闲的字节数
  return num*PGSIZE;
}

接下来实现collectproc,跳到kernel/proc.c文件下,我们同样需要观察带有proc的函数的模式。直接看allocproc()

// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, or a memory allocation fails, return 0.
static struct proc*
allocproc(void)
{
  struct proc *p;
	
  // 这是汲取灵感的部分,直接摘抄
  for(p = proc; p < &proc[NPROC]; p++) {
    acquire(&p->lock);
    if(p->state == UNUSED) {
      goto found;
    } else {
      release(&p->lock);
    }
  }
  return 0;
  ...
}

collectproc()实现如下

// Collect the number of not UNUSED proc.
uint64
collectproc()
{
  uint64 n=0;
  struct proc *p;

  for(p = proc; p < &proc[NPROC]; p++){
    acquire(&p->lock);
    // 简单的统计并返回就好了
    if(p->state != UNUSED){
      n++;
    }
    release(&p->lock);
  }
  return n;
}

以上就是实验的全部记录啦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值