5.4 Timer interrupts
Xv6 uses timer interrupts to maintain its idea of the current time and to switch among compute-bound processes. Timer interrupts come from clock hardware attached to each RISC-V CPU. Xv6 programs each CPU’s clock hardware to interrupt the CPU periodically.
Code in start.c (kernel/start.c:53) sets some control bits that allow supervisor-mode access to the timer control registers, and then asks for the first timer interrupt. The time control register contains a count that the hardware increments at a steady rate; this serves as a notion of the current time. The stimecmp register contains a time at which the the CPU will raise a timer interrupt; setting stimecmp to the current value of time plus x will schedule an interrupt x time units in the future. For qemu’s RISC-V emulation, 1000000 time units is roughly a tenth of second.
Timer interrupts arrive via usertrap or kerneltrap and devintr, like other device interrupts. Timer interrupts arrive with scause’s low bits set to five; devintr in trap.c detects this situation and calls clockintr (kernel/trap.c:164). The latter function increments ticks, allowing the kernel to track the passage of time. The increment occurs on only one CPU, to avoid time passing faster if there are multiple CPUs. clockintr wakes up any processes waiting in the sleep system call, and schedules the next timer interrupt by writing stimecmp.
devintr returns 2 for a timer interrupt in order to indicate to kerneltrap or usertrap that they should call yield so that CPUs can be multiplexed among runnable processes.
The fact that kernel code can be interrupted by a timer interrupt that forces a context switch via yield is part of the reason why early code in usertrap is careful to save state such as sepc before enabling interrupts. These context switches also mean that kernel code must be written in the knowledge that it may move from one CPU to another without warning.