Markdown to HTML

AuroBreeze Blog

A tiny, fast Markdown blog for GitHub Pages.

xv6-2023 - Sleep Lab

xv6-2023 - Sleep Lab

Overview

Implement a user-level sleep program for xv6, along the lines of the UNIX sleep command. Your sleep should pause for a user-specified number of ticks. A tick is a notion of time defined by the xv6 kernel, namely the time between two interrupts from the timer chip. Your solution should be in the file user/sleep.c.

Some hints:

Before you start coding, read Chapter 1 of the xv6 book. Put your code in user/sleep.c. Look at some of the other programs in user/ (e.g., user/echo.c, user/grep.c, and user/rm.c) to see how command-line arguments are passed to a program. Add your sleep program to UPROGS in Makefile; once you've done that, make qemu will compile your program and you'll be able to run it from the xv6 shell. If the user forgets to pass an argument, sleep should print an error message. The command-line argument is passed as a string; you can convert it to an integer using atoi (see user/ulib.c). Use the system call sleep. See kernel/sysproc.c for the xv6 kernel code that implements the sleep system call (look for sys_sleep), user/user.h for the C definition of sleep callable from a user program, and user/usys.S for the assembler code that jumps from user code into the kernel for sleep. sleep's main should call exit(0) when it is done. Look at Kernighan and Ritchie's book The C programming language (second edition) (K&R) to learn about C.

slove it

首先我们通过查看user/echo.cuser/grep.c,我们知道,系统调用函数的两个参数分别是int argc(即参数的个数),char *argv[](参数的数组)。

假设我们的输入是sleep 10,那么argc为2,argv[0]sleepargv[1]10

同时我们根据hint,我们去查看kernel/sysproc.c中的sys_sleep(void),这是user/user.hsleep()调用的函数。

关于为什么我们点击sleep()无法找到对应的函数,是因为,所有关联的函数,都通过usys.S进行映射。

用户态进程
+--------------------+
| int r = sleep(10); |
+--------------------+
          |
          v
   usys.S 封装函数
   -----------------
   li a7, SYS_sleep   ; 把系统调用号放到 a7
   ecall              ; 触发陷入 (进入内核)
   -----------------
          |
          v
=================== 内核态 ===================
          |
          v
 trap handler 保存寄存器 → 填写 trapframe
          |
          v
 syscall() 分发器
 +------------------------------------------+
 | num = p->trapframe->a7  (取系统调用号)   |
 | if(num合法)                             |
 |    调用 syscalls[num]()                  |
 | else                                     |
 |    返回 -1                               |
 +------------------------------------------+
          |
          v
 sys_sleep()
 +------------------------------------------+
 | 解析参数 argint(0, &n)                    |  
 | 从 a0寄存器中获取参数 n                    |
 | while(ticks - ticks0 < n) {              |
 |    sleep(&ticks, &tickslock);            |
 | }                                        |
 | return 0;                                |
 +------------------------------------------+
          |
          v
 返回值写入 p->trapframe->a0
          |
          v
 usertrapret()
   恢复寄存器 (含 a0)
   sret → 切回用户态
          |
          v
用户态继续执行
+--------------------------------+
| r = (返回值在 a0,即 0 或 -1) |
+--------------------------------+

我们在这里需要知道的是,RISC-V的约定,是寄存器 a7 中保存系统调用号,所以在kernel/syscall.c中,有专门的函数的映射,也就是syscalls[],另一个RISC-V的约定是,寄存器 a0 - a5 中保存系统调用的参数,也就是我们在user/user.h中的函数的参数 ,同时处理完的返回值也会保存在寄存器 a0 中。

也就是说,在kernel/syscall.c中的这句话p->trapframe->a0 = syscalls[num]();就是调用函数,并将返回值保存在寄存器 a0 中。

uint64
sys_sleep(void)
{
  int n;
  uint ticks0;

  argint(0, &n);
  if(n < 0)
    n = 0;
  acquire(&tickslock);
  ticks0 = ticks;
  while(ticks - ticks0 < n){
    if(killed(myproc())){
      release(&tickslock);
      return -1;
    }
    sleep(&ticks, &tickslock);
  }
  release(&tickslock);
  return 0;
}

上面是系统调用sys_sleep()的具体实现,在这里的n是我们准备传入的sleep 10中的10,(当然现在只是定义了变量,10 不可能凭空传进去),我们往下看,就能看到第一个有关n的函数调用argint(0, &n);

argint()这个函数可以这样拆分名称,argintarg表示参数,int表示参数的类型,这里参数的类型是int

其中,acquire()release()用来获取和释放,用来保护共享资源。在这里我们先不深入探究。

我们在while会发现还有一个if的判断语句,if(killed(myproc())),这里的作用就是判断我们当前的进程是否被杀死,如果被杀死就将锁释放。

myproc()这个可以简单理解为获取当前进程

我们可以看一下kernel/syscall.c中的argint()的具体实现:

void
argint(int n, int *ip)
{
  *ip = argraw(n);
}

static uint64
argraw(int n)
{
  struct proc *p = myproc();
  switch (n) {
  case 0:
    return p->trapframe->a0;
  case 1:
    return p->trapframe->a1;
  case 2:
    return p->trapframe->a2;
  case 3:
    return p->trapframe->a3;
  case 4:
    return p->trapframe->a4;
  case 5:
    return p->trapframe->a5;
  }
  panic("argraw");
  return -1;
}

函数argraw()中,我们通过switch语句,将n映射到对应的寄存器中,然后返回对应的值。

也就是说,argint(int n, int *ip)函数中的n就是对应的寄存器,我们上面也说过了,我们传入的参数会依次保存在寄存器中,我们在user/user.h查看函数sleep()函数,就是int sleep(int);,仅传入一个参数,那么这个参数就会被保存到寄存器a0中。

感兴趣可以去看看kernel/proc.h中的struct proc结构体,里面有trapframe成员变量,这个结构体中保存了寄存器的值。

code

具体的代码的运行我们已经了解了,如果还想具体的深入,我建议查看使用gdb来深入查看一下运行过程,会理解很多。

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(int argc, char *argv[]){
    if(argc != 2){
        fprintf(2, "Usage: sleep <time>\n");
        exit(1);
    };
    int time = atoi(argv[1]);
    if(time < 0){
        fprintf(2, "Usage: sleep <time> > 0\n");
        exit(1);
    }
    sleep(time);
    exit(0);
}

整体的代码并不难理解,接收到参数后,使用atoi()将参数转换成数字,然后调用sleep()函数,等待指定的tick就可以了。