5.4: Timer_interrupts

Xv6 使用定时器中断来维护当前时间的概念,并在计算密集型进程之间进行切换。定时器中断来自连接到每个 RISC-V CPU 的时钟硬件。

  • Xv6 为每个 CPU 的时钟硬件编程,使其定期中断 CPU。

start.c(kernel/start.c:53)中的代码设置了一些控制位,允许在超级用户模式下访问定时器控制寄存器,然后请求第一次定时器中断。

// ask each hart to generate timer interrupts.
void
timerinit()
{
  // enable supervisor-mode timer interrupts.
  w_mie(r_mie() | MIE_STIE);
  
  // enable the sstc extension (i.e. stimecmp).
  w_menvcfg(r_menvcfg() | (1L << 63)); 
  
  // allow supervisor to use stimecmp and time.
  w_mcounteren(r_mcounteren() | 2);
  
  // ask for the very first timer interrupt.
  w_stimecmp(r_time() + 1000000);
}

时间控制寄存器包含一个硬件以稳定速率递增的计数器,用于表示当前时间。stimecmp 寄存器包含一个时间点,CPU 将在此时间点触发定时器中断;将 stimecmp 设置为当前时间加上 x,则会在 x 时间单位后安排中断。对于 QEMU 的 RISC-V 模拟,1000000 时间单位大约是 1/10 秒。

定时器中断通过 usertrap 或 kerneltrap 和 devintr 传入,就像其他设备中断一样。定时器中断到达时,scause 的低位被设置为 5;trap.c 中的 devintr 检测到这种情况,并调用 clockintr(kernel/trap.c:164)。

void
clockintr()
{
  if(cpuid() == 0){
    acquire(&tickslock);
    ticks++;
    wakeup(&ticks);
    release(&tickslock);
  }

  // ask for the next timer interrupt. this also clears
  // the interrupt request. 1000000 is about a tenth
  // of a second.
  w_stimecmp(r_time() + 1000000);
}

该函数增加 ticks,使内核能够跟踪时间的流逝。这个增量只发生在一个 CPU 上,以避免在多个 CPU 的情况下时间过快地流逝。clockintr 唤醒任何在 sleep 系统调用中等待的进程,并通过写入 stimecmp 安排下一个定时器中断。

devintr 在处理定时器中断时返回 2,以便告诉 kerneltrap 或 usertrap 调用 yield,使得 CPU 可以在可运行进程之间进行多路复用。

内核代码可能会被定时器中断打断,并通过 yield 强制进行上下文切换,这也是为什么 usertrap 中的早期代码在启用中断前会小心地保存诸如sepc的状态。 上下文切换还意味着内核代码必须了解,它可能会在没有警告的情况下从一个 CPU 移动到另一个 CPU。