MIT 6.S081 Fall 2020 Lab 4

Traps

学!

不懂这题为啥是 moderate, 挺 easy 的.

根据提示, 先在 riscv.h 里写上 r_fp 函数, 然后在 sysproc.c 的 sys_sleep 中加上一句 backtrace();, 在哪都无所谓.

接着就是实现这个函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
uint64
get_ra(uint64 fp)
{
  return (*(uint64*)(fp - 0x8));
}

uint64
get_pre_fp(uint64 fp)
{
  return (*(uint64*)(fp - 0x10));
}

void
backtrace(void)
{
  for (uint64 fp = r_fp(); fp < PGROUNDUP(fp); fp = get_pre_fp(fp))
    printf("%p\n", get_ra(fp));
}

一个循环从当前 fp 一直跳就行, 直到到达 stack base. 根据提示 stack 只有一个 page, 所以 fp 向上 page round 就是 stack base 了.

也不懂这题为什么标注的 hard, 感觉 moderate

实现简单的 Alarm, 首先按照提示添加两个系统调用, 这里就不说了. 主要说 Alarm.

sigalarm(int ticks, void (*handler)()) 需要间隔 ticks 调用一下 handler(). 每个进程只有一个 alarm (当然可以实现多个, 这里只需要实现一个), 所以可以直接在 PCB 中添加 alarm 时间, 剩余 ticks, handler 函数指针. 当使用 sigalarm 系统调用时, 陷入内核态, 在内核态可以修改 PCB, 设置好 alarm.

proc.h 中定义:

1
2
3
4
5
6
7
// Per-process state
struct proc {
  ...
  int alarm_interval;          // Alarm interval ticks
  void (*alarm_handler)();     // Alarm handler function
  uint64 alarm_ticks_left;     // Ticks left until the next handler call
};

proc.callocproc() 中初始化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static struct proc*
allocproc(void)
{
  ...
found:
  ...
  // Init fields of alarm
  p->alarm_interval = 0;
  p->alarm_handler = 0;
  p->alarm_ticks_left = 0;

  return p;
}

signalalarm 的设置, 其中参数 ticks 为 0 时表示没有 alarm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
uint64
sys_sigalarm(void)
{
  int interval;
  void (*handler)();
  struct proc* p;

  argint(0, &interval);
  argaddr(1, (void *)&handler);
  p = myproc();

  if (interval == 0) {
    p->alarm_interval = 0;
    p->alarm_handler = 0;
    p->alarm_ticks_left = 0;
  } else {
    p->alarm_interval = interval;
    p->alarm_handler = handler;
    p->alarm_ticks_left = interval;
  }

  return 0;
}

每个 tick 都会产生时钟中断, 导致 trap. 所以当 trap 时, 看看是不是时钟中断, 如果是, 则需要看看是不是应该 “调用” handler. 具体来说就是剩余时间减少 1, 然后判断有没有到 0, 到了就说明需要 “调用” handler.

这里的 “调用” 不是直接 p->alarm_handler() 就可以, 因为我们还在内核态, handler 是用户态的东西, 所以需要返回到用户态. 根据 xv6 的 trampoline 机制, usertrapret 中会把 trapframe 中的 epc 写入 sepc 寄存器, 之后在 userret 的 sret 中由硬件设置 PC 到 sepc 处返回用户态. 所以, 在 trap 中判断了需要 “调用” handler, 需要把 trapframe 的 epc 设置为 handler 位置.

这就产生了另一个问题: 直接把进程的 PC 指针设置到这里, 怎么在 handler 结束后回到 alarm 前呢? 参考 trap into kernel 的方法, 它会在 trapframe page 上存一下所有的寄存器, 这样 userret 的时候再完全恢复, 就能够正确返回. 可以用相似的方法保存一下 “调用” handler 前的 regs.

xv6 在 handler 结束前必须手动调用 sigreturn 系统调用, 所以在这里可以恢复一下 regs. 同时恢复倒计时, 以实现 “间隔 ticks 触发 handler”.

首先在 PCB 中加入另一个 trapframe, 供我们保存寄存器状态:

1
2
3
4
5
// Per-process state
struct proc {
  ...
  struct trapframe *alarm_state;  // Regs state used for signalalarm and sigreturn
};

然后初始化给一段空间, 可以像 trapframe 一样 kalloc 一个 page, 也可以直接用 trapframe page 上空闲的, 这里直接用空闲的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static struct proc*
allocproc(void)
{
  ...
found:
  ...
  p->alarm_state = p->trapframe + 1;

  return p;
}

在 trap (trap.c) 的时候判断是不是时钟中断 (which_dev == 2), 以及减少定时器, 判断需不需要 “调用” handler. 如果需要, 则保存 regs, 设置 epc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void
usertrap(void)
{
  ...
  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    if (p->alarm_interval > 0 && --p->alarm_ticks_left == 0) {
      memmove(p->alarm_state, p->trapframe, sizeof(struct trapframe));
      p->trapframe->epc = (uint64)p->alarm_handler;
    }
    yield();
  }

  usertrapret();
}

最后在 sigreturn 系统调用中恢复 regs 和定时器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
uint64
sys_sigreturn(void)
{
  struct proc* p = myproc();

  memmove(p->trapframe, p->alarm_state, sizeof(struct trapframe));
  p->alarm_ticks_left = p->alarm_interval;

  return 0;
}

需要注意的是, 定时器的恢复不能在设置 epc 到 handler 的时候. 否则会出现 handler 还没执行完, 就又到时间了, trap 后又跳到了 handler 开头. 所以这里必须放在 sigreturn 处.

这不得用调试符号吗? 不会