time: read the clock from the TSC, not from counting interrupts
A timer interrupt that arrives while interrupts are disabled is not delayed,
it is lost -- the LAPIC in periodic mode holds one pending interrupt and drops
the rest. So anything the kernel does with interrupts off for longer than
10 ms makes `ticks` run slow, and it never catches up.
That has now presented twice as "uptime is wrong": once as an interrupt storm
taking 83% of the CPU, once as heavy console output on a display that is
expensive to draw to. Both times the tick was the messenger, and both times it
looked like a timekeeping bug.
monotonic() reads the TSC instead. It is a counter in the silicon that
advances whether or not this kernel is paying attention, it is already
measured against the PIT at boot, and reading it is one instruction and no
lock. wallclock(), /proc/uptime, sysinfo, getrusage and times() all go through
it now.
`ticks` keeps the job it is good at -- the scheduler preempts on it, sleep()
counts it -- where losing one means a slightly longer sleep rather than a
wrong answer about what time it is.
Lost ticks are no longer harmful, which makes them invisible, which is worse.
So /proc/interrupts now says
timer: 3383 ticks taken, 3387 expected
and names the gap when it grows, because a machine dropping timer interrupts
is spending too long with interrupts disabled, and that is worth knowing
before it turns into a clock that lies.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
65906639da
commit
bbc512a51c
7 changed files with 192 additions and 37 deletions
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
#include "x86.h"
|
||||
#include "date.h"
|
||||
#include "param.h"
|
||||
#include "spinlock.h"
|
||||
|
|
@ -91,26 +92,72 @@ time_init(void)
|
|||
// Subtract the uptime so far, so boot_epoch is the moment of boot rather than
|
||||
// the moment this function ran. It is only a fraction of a second, but it
|
||||
// keeps boot_epoch meaning exactly what its name says.
|
||||
uint64 up = ticks / HZ;
|
||||
uint64 up, dummy;
|
||||
monotonic(&up, &dummy);
|
||||
boot_epoch = now > up ? now - up : now;
|
||||
|
||||
cprintf("time: %d-%d-%d %d:%d:%d UTC from the hardware clock\n",
|
||||
(int)year, r.month, r.day, r.hour, r.minute, r.second);
|
||||
}
|
||||
|
||||
// How long the machine has been running, from the TSC.
|
||||
//
|
||||
// **Not from `ticks`.** This is the important line in this file.
|
||||
//
|
||||
// `ticks` counts timer interrupts, and a timer interrupt that arrives while
|
||||
// interrupts are disabled is not delayed, it is *lost* -- the LAPIC in periodic
|
||||
// mode holds one pending interrupt and drops the rest. So anything the kernel
|
||||
// does with interrupts off for longer than 10 ms makes the clock run slow, and
|
||||
// it never catches up. A long console write, a framebuffer scroll on a display
|
||||
// that is expensive to draw to, a driver spinning on a register: all of them
|
||||
// silently steal time from every program that asks what time it is.
|
||||
//
|
||||
// That has now happened twice. The first was an interrupt storm taking 83% of
|
||||
// the CPU (docs/history/kernel.md §13) and the clock ran seven times slow; the
|
||||
// second was heavy console output, and it presented the same way. Both times
|
||||
// the tick was the messenger.
|
||||
//
|
||||
// The TSC cannot be starved. It is a counter in the silicon that advances at a
|
||||
// fixed rate whether or not this kernel is paying attention, and it is already
|
||||
// measured against the PIT at boot. Reading it is one instruction and no lock.
|
||||
//
|
||||
// `ticks` is still what the scheduler preempts on and what sleep() counts,
|
||||
// which is the right job for it: losing a tick there means a slightly longer
|
||||
// sleep, not a wrong answer about what time it is.
|
||||
void
|
||||
monotonic(uint64 *sec, uint64 *usec)
|
||||
{
|
||||
// Before calibration there is no TSC frequency to divide by. Fall back to
|
||||
// the tick count, which is what this used to do always.
|
||||
if (tsc_hz == 0) {
|
||||
uint xticks;
|
||||
acquire(&tickslock);
|
||||
xticks = ticks;
|
||||
release(&tickslock);
|
||||
if (sec) *sec = xticks / HZ;
|
||||
if (usec) *usec = (uint64)(xticks % HZ) * (1000000 / HZ);
|
||||
return;
|
||||
}
|
||||
|
||||
uint64 d = rdtsc() - boot_tsc;
|
||||
uint64 s = d / tsc_hz;
|
||||
// The remainder first, then scale. `d * 1000000` would overflow 64 bits
|
||||
// after about 80 minutes at 3.8 GHz; the remainder is always below tsc_hz,
|
||||
// so this cannot.
|
||||
uint64 rem = d - s * tsc_hz;
|
||||
|
||||
if (sec) *sec = s;
|
||||
if (usec) *usec = (rem * 1000000) / tsc_hz;
|
||||
}
|
||||
|
||||
// Seconds and microseconds since the epoch. One place, so gettimeofday,
|
||||
// clock_gettime and anything added later cannot disagree about what time it is.
|
||||
void
|
||||
wallclock(uint64 *sec, uint64 *usec)
|
||||
{
|
||||
uint xticks;
|
||||
uint64 s, us;
|
||||
|
||||
acquire(&tickslock);
|
||||
xticks = ticks;
|
||||
release(&tickslock);
|
||||
|
||||
if (sec)
|
||||
*sec = boot_epoch + xticks / HZ;
|
||||
if (usec)
|
||||
*usec = (uint64)(xticks % HZ) * (1000000 / HZ);
|
||||
monotonic(&s, &us);
|
||||
if (sec) *sec = boot_epoch + s;
|
||||
if (usec) *usec = us;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue