【MIT6.S081】Lab2: syscall(详细解答版)

371 阅读5分钟

实验内容网址:xv6.dgs.zone/labs/requir…

System call tracing

关键点:系统函数调用的整个流程

思路:

解题步骤按照提示来,比较清晰。下面先以read系统调用来描述整个过程。 用户程序调用read函数,接着进入usys.S(实现了用户访问系统调用的接口)中,将SYS_read指令码存入a7寄存器中,然后调用ecall指令,切换到内核栈,在内核栈中调用usertrap函数接着调用syscall函数,将指令码从a7寄存器中取出并通过函数数组定位到系统调用sys_read执行和返回,如 p->trapframe->a0 = syscallsnum; a0存储的是系统调用的返回值。当系统调用完成时,内核切换回用户栈,并通过调用sret指令返回用户空间,该指令降低了硬件特权级别,并在系统调用指令刚结束时恢复执行用户指令。

步骤&代码:

  • 在Makefile的UPROGS中添加$U/_trace
  • 将系统调用的原型添加到user/user.h
...
int getpid(void);
char* sbrk(int);
int sleep(int);
int uptime(void);
// 新添加
int trace(int);
  • 存根添加到user/usys.pl
....
entry("getpid");
entry("sbrk");
entry("sleep");
entry("uptime");
#新添加
entry("trace");
  • 以及将系统调用编号添加到kernel/syscall.h
...
#define SYS_mkdir  20
#define SYS_close  21
// 新添加
#define SYS_trace  22
  • kernel/syscall.c中补充syscalls数组
...
[SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,
// 新添加
[SYS_trace]   sys_trace,
};
  • kernel/sysproc.c中添加一个sys_trace()函数,这个函数通过argint从用户空间检索系统调用参数。将掩码参数保存到proc结构体中(proc结构体新定义了mask变量)。
uint64
sys_trace(void){
  int mask;
  if(argint(0, &mask) < 0)
    return -1;
  // 将mask参数存入进程的proc结构体中
  myproc()->mask = mask;
  return 0;
}
  • 修改fork()(请参阅kernel/proc.c)将跟踪掩码从父进程复制到子进程。很简单,只需要copy父进程的mask到子进程,在fork函数中添加一句。
 np->sz = p->sz;

  np->parent = p;
  // 新添加copy父进程的mask到子进程
  np->mask = p->mask;
  // copy saved user registers.
  *(np->trapframe) = *(p->trapframe);

  // Cause fork to return 0 in the child.
  np->trapframe->a0 = 0;
  • 修改kernel/syscall.c中的syscall()函数以打印跟踪输出。添加一个系统调用名称数组以建立索引,需要注意系统调用编号是从1开始的,所以数组第0个以空字符串填充,以便后续访问。syscall函数中要注意的是条件判断中使用了&而不是==,这是因为在实验说明书的例子中,trace 2147483647 grep hello README将所有31个低位置为1,使得其可以追踪所有的系统调用。
// 新添加系统调用名称数组
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",
};

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

Sysinfo

关键点:系统函数调用的整个流程、内核空间与用户空间之间的数据拷贝

思路&步骤:

  • 仿照System call tracing实验,添加新的系统调用。包括kernel/sysproc.c文件中定义sys_sysinfo()函数,kernel/syscall.c文件中修改syscalls数组及系统调用号宏定义,在user/usys.pl文件中添加存根。以上这部分代码就不展示了,具体改动可以进代码仓库参考。Dragon/xv6-labs-2020
  • 根据提示在kernel/kalloc.c中添加一个函数获取空闲内存量。仿照kalloc函数。kalloc函数返回一个指向空闲内存的指针。所有空闲内存都通过freelist链表串起来。我们通过遍历链表中空闲内存的数量便可以得到空闲内存的字节大小,每一也空闲物理内存的大小是4096字节(PGSIZE)。写完getFreeMem函数记得在kernel/defs.h中进行声明,以便其他文件的函数进行调用。注意获取锁后要及时释放掉,不然会造成系统崩溃。
/*
获取空闲内存量
*/
uint64 
getFreeMem(void){

  struct run *r;
  uint64 count = 0;
  // 获取锁
  acquire(&kmem.lock);
  r = kmem.freelist;
  while(r){
    count += 4096;
    r = r->next;
  }
  release(&kmem.lock);
  return count;
}
  • 根据提示在kernel/proc.c中添加一个函数获取进程数。xv6通过一个进程数组(proc[NPROC])来记录操作系统的进程。仿照allocproc函数来编写函数获取进程数。思路是遍历进程数组,判断进程的状态,通过一个count变量记录状态不为unused 的进程数量。同样获取锁后记得释放掉。
/*
获得state字段不为UNUSED的进程数
*/
uint64
getUsedProcNum(){
  struct proc *p;
  uint64 count = 0;
  // 遍历进程数组
  for(p = proc; p < &proc[NPROC]; p++) {
    acquire(&p->lock);
    if(p->state != UNUSED) {
      count++;
    } 
    release(&p->lock);
  }
  return count;
}
  • 接下来编写kernel/sysproc.c文件中的sys_sysinfo()函数。获取系统调用的参数这里使用argaddr函数,因为传进来的是一个结构体指针(struct sysinfo*)。根据提示,从内核空间复制数据到用户空间使用copyout函数,内核空间中的源地址是&info,用户空间的目标地址是argaddr函数获得的地址。
uint64
sys_sysinfo(void){
  
  uint64 p;
  struct sysinfo info;

  if(argaddr(0, &p) < 0)
    return -1;
  info.freemem = getFreeMem();
  info.nproc = getUsedProcNum();
  //printf("freemem:%d,nproc:%d\n",info.freemem,info.nproc);
  // 从内核空间复制数据到用户空间
  if(copyout(myproc()->pagetable, p, (char *)&info, sizeof(info)) < 0)
    return -1;
  return 0;
}

写这个函数的时候,本来没有使用copyout函数进行数据的内核空间和用户空间转换,直接将数据赋值给p->freemem和p->nproc。但这样子是错误的。为什么呢?