Timers
A kernel needs to measure time for reasons ranging from preemptive scheduling to simple delay loops, and x86 hardware offers no single, obvious answer for how: four different timing facilities coexist on any modern machine, each with a different tradeoff between precision, per-CPU independence, and how much setup it needs before it’s usable at all.
The Programmable Interval Timer
Section titled “The Programmable Interval Timer”The PIT (Intel 8253, later 8254) is the oldest of the four, present in some form on every PC-compatible machine for backward compatibility even though its own hardware is long obsolete. It’s programmed through four I/O ports: 0x40, 0x41, and 0x42 access its three independent counting channels, and 0x43 is a mode/command register used to configure them. Channel 0 is the one relevant to kernel timing, conventionally wired to legacy IRQ0; channel 1 historically refreshed dynamic RAM and is unused on any modern chipset, and channel 2 drives the PC speaker. The PIT’s internal oscillator runs at approximately 1.193182 MHz, an otherwise arbitrary frequency inherited from dividing the original IBM PC’s video clock, and every interval the PIT can generate is expressed as a 16-bit divisor of that base frequency:
#define PIT_FREQUENCY 1193182
void pit_set_frequency(uint32_t hz) { uint16_t divisor = PIT_FREQUENCY / hz; outb(0x43, 0x36); // channel 0, lobyte/hibyte, mode 3 (square wave) outb(0x40, divisor & 0xFF); outb(0x40, (divisor >> 8) & 0xFF);}A 16-bit divisor against a ~1.19 MHz base caps the PIT’s lowest programmable frequency at about 18.2 Hz (the historical BIOS tick rate) and its practical usefulness at a few kilohertz before the divisor becomes too coarse to be useful; what keeps it in use is that it needs no discovery mechanism at all, a kernel can simply program it, in contrast with every other timer described below.
The Local APIC timer
Section titled “The Local APIC timer”Every CPU core with a Local APIC has its own timer built into that APIC, entirely independent of every other core’s copy, which makes it the natural choice for per-CPU scheduling ticks on a multiprocessor system where a single shared timer interrupt would have to be distributed to every core somehow. It’s configured through memory-mapped registers relative to the Local APIC’s base (conventionally mapped at physical address 0xFEE00000): the LVT Timer register (offset 0x320) sets the interrupt vector to fire and the timer’s mode, the Initial Count register (0x380) is written to arm it, the Current Count register (0x390) can be read to see how far a countdown has progressed, and the Divide Configuration register (0x3E0) scales down the bus clock the timer counts against. The LVT Timer register’s mode bits select between one-shot (count down once from the initial count to zero, then stop), periodic (reload the initial count automatically and repeat), and, on newer CPUs, TSC-deadline mode, where instead of an initial count the software writes an absolute target TSC value the timer should fire at, sidestepping the need to reprogram a countdown value for every new deadline.
A Local APIC timer’s tick rate depends on the bus clock it’s derived from, a frequency that varies by CPU model and generation and is not exposed through any single reliable register read; a kernel commonly calibrates it directly by counting how many Local APIC timer ticks occur during a fixed, independently-known interval measured against the PIT or the TSC, then using that ratio for all future scheduling.
The Time Stamp Counter
Section titled “The Time Stamp Counter”The TSC, read with the RDTSC instruction, is a 64-bit counter that increments once per some clock reference on every core, giving software an extremely cheap way to measure elapsed time as a simple difference between two reads with no I/O port access or memory-mapped register involved. Its practical usefulness hinged for years on whether it was invariant: early implementations tied the TSC’s increment rate to the CPU’s current power-management frequency, making it useless for wall-clock timing across a frequency change, while an invariant TSC (advertised via a CPUID feature bit) increments at a fixed rate regardless of throttling or sleep states, which is what makes RDTSC-based timing reliable on any reasonably modern x86 CPU. Even with an invariant TSC, the counter’s absolute frequency still isn’t given directly by any single instruction and typically needs calibration against another time source once at boot, the same way the Local APIC timer does; some newer CPUs expose the TSC frequency directly via a CPUID leaf, removing the need for that calibration step where it’s available.
The High Precision Event Timer is a memory-mapped timer block, discovered through an ACPI table of the same name rather than assumed to exist at a fixed address, containing a single main counter that runs at a fixed known frequency (reported directly in its capabilities register, unlike the Local APIC timer or an uncalibrated TSC) alongside several independent comparators that can each be programmed to raise an interrupt when the main counter reaches a target value. Because its frequency is self-describing, HPET is frequently used specifically as the reference clock a kernel calibrates its other, faster-but-uncalibrated timers (the Local APIC timer, the TSC) against during early boot, rather than as the primary scheduling tick itself; its own interrupt delivery, routed through I/O APIC pins or as message-signaled interrupts depending on configuration, adds latency an in-core Local APIC timer doesn’t have.
Choosing a time source
Section titled “Choosing a time source”No single one of these four covers every need a kernel has simultaneously, which is why a typical kernel ends up using more than one rather than picking a single winner. The Local APIC timer is the near-universal choice for the recurring scheduling tick, since it’s per-core and needs no shared-resource locking between CPUs to rearm. The TSC, once confirmed invariant, is the usual choice for fine-grained elapsed-time measurement (profiling, short delays) precisely because reading it costs nothing beyond executing one instruction. The PIT survives mainly as a calibration reference during early boot, before APIC or HPET discovery has necessarily completed, and as a fallback on hardware too old or too unusual to trust the others. HPET, where present, is frequently used as the higher-quality calibration reference in place of the PIT specifically because its frequency doesn’t need to be assumed from historical hardware trivia the way the PIT’s does.
Implementation notes
Section titled “Implementation notes”Calibrating the Local APIC timer or the TSC against the PIT requires being confident the calibration window itself was measured correctly; a common mistake is calibrating against a PIT one-shot count without accounting for the several-microsecond latency of the I/O port accesses used to read it back, which on a fast CPU can be a meaningful fraction of a short calibration window and skew the resulting frequency estimate. A non-invariant TSC is not merely slower to use but actively misleading if invariance isn’t checked first: two RDTSC reads taken across a frequency transition on such a CPU produce a difference that doesn’t correspond to any fixed unit of wall-clock time at all, silently corrupting anything timed against it. Finally, HPET’s comparators are a shared resource across the whole system in the way a Local APIC timer isn’t, so a kernel using HPET interrupts for more than pure calibration has to arbitrate access to those comparators between cores rather than treating one as freely ownable by whichever core programs it first.
References
Section titled “References”- ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, volume 3, chapter on Advanced Programmable Interrupt Controller (defines the Local APIC timer’s LVT and divide-configuration registers)
- ^ Intel/Microsoft/others, IA-PC HPET (High Precision Event Timers) Specification (defines the HPET register block and ACPI discovery table)
- ^ OSDev Wiki, “Programmable Interval Timer” (covers the 8253/8254’s other operating modes beyond the square-wave mode used above)
See also
Section titled “See also”- PIC & APIC: the interrupt controller whose Local APIC also houses the per-CPU timer described here.
- Schedulers: the scheduling tick these timers are most commonly used to drive.
- CPUID: the instruction behind the invariant-TSC feature bit mentioned above.
- Real Time Clock (RTC/CMOS): a different problem these elapsed-time counters don’t solve, reading the actual calendar date and time.