文章

MIT 6.S081 | Lab2: system calls

课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.html 代码地址:https://github.com/36-H/xv6-labs-2020/tree/syscall

#Lab2: system calls

##XV6系统调用全流程 在本次实验开始前,我们可能需要先去了解XV6系统调用的全流程。

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

由于内核与用户进程的页表不同,寄存器也不互通,在kernel/syscall.c中,有一系列argraw、argint函数来处理从进程的trapframe中读取用户进程寄存器中的参数。同时,页表的不同也带来了指针也不能直接互通访问的问题,也就是内核不能直接对用户态传来的指针进行解引用,而需要使用在kernel/vm.c中定义的copyin,copyout函数,才能找到对应用户态指针的物理地址

struct proc *p = myproc(); // 获取调用该 system call 的进程的 proc 结构
copyout(p->pagetable, addr, (char *)&data, sizeof(data)); // 将内核态的 data 变量(常为struct),结合进程的页表,写到进程内存空间内的 addr 地址处。

##System call tracing(moderate)

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

Step 1 在kernel/proc.h中修改proc结构体的定义,增加trace_mask参数

struct file *ofile[NOFILE];  // Open files
struct inode *cwd;           // Current directory
char name[16];               // Process name (debugging)
uint64 trace_mask;            // Mask for syscall tracing

Step 2 在kernel/proc.c中,设置trace_mask的默认参数

static struct proc*
allocproc(void)
{
  struct proc *p;
  ······
  // 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;

  //set default value to trace mask
  p->trace_mask = 0;

  return p;
}

Step 3 调整kernel/proc.c中freeproc函数

static void
freeproc(struct proc *p)
{
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  p->pagetable = 0;
  p->sz = 0;
  p->pid = 0;
  p->parent = 0;
  p->name[0] = 0;
  p->chan = 0;
  p->killed = 0;
  p->xstate = 0;
  p->state = UNUSED;
  p->trace_mask = 0; //free mask
}

Step 4 修改kernel/proc.c中的fork函数,使得子进程可以继承父进程的trace_mask

int
fork(void)
{
  int i, pid;
  struct proc *np;
  struct proc *p = myproc();
  // Allocate process.
  if((np = allocproc()) == 0){
    return -1;
  }
  // Copy user memory from parent to child.
  if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
    freeproc(np);
    release(&np->lock);
    return -1;
  }
  np->sz = p->sz;
  np->parent = p;
  // copy saved user registers.
  *(np->trapframe) = *(p->trapframe);
  // Cause fork to return 0 in the child.
  np->trapframe->a0 = 0;
  // 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));
  pid = np->pid;

  np->state = RUNNABLE;

  //copy the parent's trace_mask to child
  np->trace_mask = p->trace_mask;

  release(&np->lock);

  return pid;

}

Step 5 创建sys_trace系统调用 在kernel/sysproc.c中

uint64
sys_trace(void){
  int mask;

  //读取trapframe 获取mask参数
  if(argint(0,&mask) < 0){
    return -1;
  }
  myproc()->trace_mask = mask;
  return 0;
}

syscall.h 中加入新 system call 的序号

····
#define SYS_link   19
#define SYS_mkdir  20
#define SYS_close  21
#define SYS_trace  22

在kernel/syscall.c,用 extern 全局声明新的内核调用函数,并且在 syscalls 映射表中,加入从前面定义的编号到系统调用函数指针的映射

······
extern uint64 sys_wait(void);
extern uint64 sys_write(void);
extern uint64 sys_uptime(void);
extern uint64 sys_trace(void);

static uint64 (*syscalls[])(void) = {
[SYS_fork]    sys_fork,
······
[SYS_link]    sys_link,
[SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,
[SYS_trace]   sys_trace,
};

在user/usys.pl加入用户态到内核态的跳板函数

entry("sbrk");
entry("sleep");
entry("uptime");
entry("trace");

在user/user.h加入定义,使得用户态程序可以找到这个跳板入口函数。

······
char* sbrk(int);
int sleep(int);
int uptime(void);
int trace(int);

最后在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]();

    //判断trace
    if((p->trace_mask >> num) & 1){
      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/syscall.c中

const char *syscall_names[] = {
[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",
};

###Sysinfo(moderate)

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

Step 1 获取空余内存 在kernel/defs.h定义函数count_free_mem(void);

······
void*           kalloc(void);
void            kfree(void *);
void            kinit(void);
uint64          count_free_mem(void);

在kernel/kalloc.c

uint64
count_free_mem(void){
  //防竞态
  acquire(&kmem.lock);

  uint64 free_mem_size = 0;
  struct run *r = kmem.freelist;
  // 统计空闲页数,乘上页大小 PGSIZE 就是空闲的内存字节数
  while(r){
    free_mem_size += PGSIZE;
    r = r->next;
  }

  //释放锁
  release(&kmem.lock);

  return free_mem_size;
}

Step 2 获取运行的进程数 在kernel/defs.h声明函数count_nproc(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);
uint64          count_nproc(void);

在kernel/proc.c中实现

uint64
count_nproc(void){
  uint64 count= 0;
  for(struct proc *p = proc; p < &proc[NPROC]; p++) {
    // 判断进程块状态
    if(p->state != UNUSED) { 
        count++;
    }
  }
  return count;
}

Step 3 实现sysinfo系统调用 关键代码: 在kernel/sysproc.c

//为什么不能传参呢,因为这涉及到用户态和内核态的切换,那么从用户态过来的参数,
//我们应该从a0寄存器取出
//也就是 argraw(int n) 当n=0时会取出a0寄存器的值
uint64
sys_sysinfo(void){
  //读入用户态参数
  uint64 addr;

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

  struct sysinfo info;
  info.freemem = count_free_mem();
  info.nproc = count_nproc();

  //使用 copyout,通过当前进程的页表,获得进程传进来的指针(逻辑地址)对应的物理地址
  //然后将 &info 中的数据复制到该指针所指位置
  if(copyout(myproc()->pagetable, addr, (char *)&info, sizeof(info)) < 0){
    return -1;
  }

在user/user.h

int sleep(int);
int uptime(void);
int trace(int);
//声明sysinfo 结构,供用户态使用。
struct sysinfo;
int sysinfo(struct sysinfo *);
License:  CC BY 4.0