【MIT 6.S081】2020, 实验记录(4),Lab: Traps

在学完 Traps 一节课后,了解了在 OS 中,用户态是如何转换到内核态,再转回用户态的。

目录

    • Task: Backtrace
    • Task: Alarm

Task: Backtrace

这个任务目标是实现 backtrace() 函数,它用来打印当前堆栈中的所有函数调用信息。

为了完成这个任务,我们需要遍历函数调用栈中的每个栈帧(frame),并打印每个 frame 中的 Return Address 信息。因此,我们需要看一下函数调用堆栈的结构:

【MIT 6.S081】2020, 实验记录(4),Lab: Traps_第1张图片

上图展示了一个堆栈结构,堆栈从上向下生长,其中每个 stack frame 中有两个关键字段:

  • Return Address:这个函数的返回地址,也是在 backtrace() 函数中需要打印的信息;
  • To Prev Frame:指向上一个 frame 的一个指针

为了遍历所有 frames,我们需要认识两个关键的寄存器:

  • SP 寄存器:指向当前函数栈的底部并代表了当前栈帧的位置
  • FP 寄存器:它指向当前函数栈帧的顶部

在这里,我们可以借助 FP 的值来遍历所有栈帧,而且我们所需要的两个关键字段都相对于栈帧顶部具有固定的 offset。

根据实验提示,我们现在 kernel/riscv.h 文件中添加一个用于获取 FP 寄存器值的函数:

static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}

在 kernel/printf.c 中添加 backtrace() 函数的实现:

void
backtrace()
{
  printf("backtrace:\n");
  uint64 fp = r_fp();   // FP 寄存器值
  uint64 base = PGROUNDUP(fp);  // 栈底地址
  while (fp < base) {  // 向前遍历 frame,直到达到 base
    printf("%p\n", *((uint64*)(fp - 8)));  // 打印 Return Address 字段值
    fp = *((uint64*)(fp - 16));  // prev frame
  }
}

在 kernel/defs.h 添加 backtrace 函数的声明:

void            backtrace(void);

在 sysproc.c 中的 sys_sleep 函数中添加对 backtrace 函数的调用:

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

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

这样,就可以在 make qemu 后执行 bttest 来测试:

【MIT 6.S081】2020, 实验记录(4),Lab: Traps_第2张图片
按照官网的提示测试后,如果 backtrace() 函数没问题,就可以在 kernel/printf.c 的 panic() 中加入对 backtrace 的函数调用了,这样当程序 panic 时会打印当前的堆栈信息:

void
panic(char *s)
{
  pr.locking = 0;
  printf("panic: ");
  printf(s);
  printf("\n");
  backtrace();
  panicked = 1; // freeze uart output from other CPUs
  for(;;)
    ;
}

Task: Alarm

这个实验需要实现两个 system call:

  • sigalarm:用户代码可以传入两个参数:tickshandler,代表每过 ticks 个 tick,内核就回调一次 handler 函数来执行
  • sigreturn:用户代码需要在传入 sigalarm() 中的 handler 回调函数的函数结尾中加入一个对 sigreturn 的系统调用,这样内核可以恢复之前的正常用户程序并继续运行。

一个示例过程如下图所示:

【MIT 6.S081】2020, 实验记录(4),Lab: Traps_第3张图片

这个图并不十分准确,在正常程序的运行过程中,每次一个 CPU 的 tick 都会让程序陷入内核中。当调用 sysalarm() 这个系统调用后,用户告诉内核,每过 ticks 个 CPU tick 就要执行一次 handler,于是就有了上图,用户的正常程序在运行着,当某一次的 CPU tick 让程序陷入内核时,内核检查到刚好 ticks 超时,于是就恢复用户程序并让其执行 handler,在 handler 函数的结尾有一个对 sysreturn() 的系统调用,这个系统调用在陷入内核后,会恢复之前用户正常程序的程序上下文,并在恢复用户空间后让其继续执行用户的正常程序。

这个 task 的测试程序是 alarmtest.c,我们需要将其加入到 Makefile 中让其编译为一个 user program,这一步只需要修改 Makefile 的 UPROGS 就可以,也可以参考 lab 1。

然后在用户空间的代码中(user/user.h)声明这两个新添加的系统调用的声明:

int sigalarm(int ticks, void (*handler)());
int sigreturn(void);

接下来,参考 Lab 2,将 sys_sigalarmsys_sigreturn 这两个系统调用添加到 kernel 的代码中。

kernel 为了记录用户所调用的 alarm 的相关信息,需要在 struct proc 中(kernel/proc.h)添加几个字段:

struct proc {
  ...
  // for alarm syscall
  uint64 alarm_callback;  // 每过 ticks 就回调的函数地址
  uint64 alarm_interval;  // 也就是 ticks
  uint64 passed_ticks;    // 从上次执行完 callback handler 后,已经过了多少个 ticks
  int is_running_callback;   // 指示当前用户程序是否正在运行 alarm callback 函数
  struct trapframe interupted_trapframe;  // 用于保存因需要执行 alarm_callback 而保存的 trapframe
}

在 allocproc() 函数中(kernel/proc.c)初始化这个 proc:

static struct proc*
allocproc(void)
{
  ...
  // initilize fields for alarm syscalls
  p->alarm_callback = 0;
  p->alarm_interval = 0;
  p->passed_ticks = 0;
  p->is_running_callback = 0;
  ...
}

实现 sys_sigalarm() 函数(kernel/sysproc.c),这个函数主要负责解析用户在系统调用时传入的参数,以及将这些信息记录到当前 proc 的 struct proc 中:

uint64
sys_sigalarm(void)
{
  int interval;
  uint64 handler;

  if (argint(0, &interval) < 0 || argaddr(1, &handler) < 0) {
    return -1;
  }
  acquire(&tickslock);
  struct proc *p = myproc();
  // 记录相关信息
  p->alarm_callback = handler;  // 需要回调的函数
  p->alarm_interval = interval; // ticks
  p->passed_ticks = 0;          // 已经过了 0 ticks
  p->is_running_callback = 0;   // 用户空间没有在运行 callback handler
  release(&tickslock);

  return 0;
}

每过一个 CPU tick,用户程序就会陷入内核态,这个过程中必然会经过 kernel 的 usertrap() 函数,所以我们为了实现这个 task 的功能,可以修改 usertrap() 函数。在 usertrap 函数中,我们需要添加的功能是:如果本次 trap 是因为 CPU tick 导致的话,那么就检查 alarm ticks 是否超时,如果超时就让用户程序执行 callback handler 函数。

内核可以通过修改在恢复用户空间时 PC 寄存器的值来控制用户空间执行哪段代码。根据之前学的 trap 的知识,我们知道在恢复用户空间时,是将 myproc()->trapframe->epc 的填充到 PC 寄存器中,所以我们在 kernel 中就只需要修改这个字段就可以控制下一次恢复用户空间时用户所执行的程序。

为了在 alarm 之后恢复用户的正常程序,我们在想要执行 callback handler 前需要保存用户正常程序的执行上下文(各个寄存器的值),这样在恢复用户正常程序时才能让各个寄存器恢复原值。

usertrap() 函数(kernel/trap.c)实现如下:

void
usertrap(void)
{
  ...
  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {  // 如果是因为 CPU tick 而 trap 的
    if (p->alarm_interval > 0) {  // 用户设定过 alarm
      if (p->passed_ticks >= p->alarm_interval && p->is_running_callback == 0) {  // 超时且没有在运行 callback
        p->passed_ticks = 0;
        p->is_running_callback = 1;
        p->interupted_trapframe = *(p->trapframe);  // 保存各寄存器的值,便于之后在用户空间中恢复
        p->trapframe->epc = p->alarm_callback;  // 返回用户空间时,PC 值就变成了 alarm callback 的地址
      }
      p->passed_ticks++;
    }
    yield();
  }
  ...
}

在执行了 callback handler 后,用户空间需要恢复执行 callback 前的运行上下文,这部分逻辑实现在 sys_sigreturn() 这个系统调用的代码实现中:

uint64
sys_sigreturn(void)
{
  struct proc *p = myproc();
  p->is_running_callback = 0;
  p->passed_ticks = 0;
  *(p->trapframe) = p->interupted_trapframe;  // 用户正常程序的所有寄存器值

  return 0;
}

完成以上代码后,运行 alarmtest 测试:

【MIT 6.S081】2020, 实验记录(4),Lab: Traps_第4张图片
测试通过,本 Lab 完成!

你可能感兴趣的:(MIT6.S081,操作系统,c语言)