6.4 KiB
Timers and time
Two different needs hide under the word "timer", and danos keeps them apart:
- Reading the clock — what time is it? A read of a free-running counter.
- Waiting — wake me in N milliseconds, or notify me when a deadline passes.
Both are answered by the kernel, because the kernel already owns a timer: it has to, to preempt tasks. The LAPIC heartbeat and the calibrated TSC that back all of this are built in device-interrupts.md; the scheduler's blocking and wait queues are in scheduling.md. This page is about the surface a ring-3 program actually uses, and one deliberate absence: there is no user-space time service.
Why time is a syscall, not a service
The tempting microkernel move is to put a timer driver in user space and have
applications ask it for the time over IPC. For a monotonic clock that is wrong —
reading now() should never cost an IPC round trip. The kernel is already holding the
answer: it computes the current time every time it schedules, from the TSC, in a couple
of instructions. Surfacing that as a system call is pure mechanism; routing it through a
message to another process would be slower and redundant, and a device like the HPET
(uncacheable MMIO reads) is a particularly bad thing to read on every now().
This is the same conclusion every serious system reaches: Linux and Zircon read the counter in the vDSO, L4 exposes a clock field in a shared kernel page, seL4 reads the cycle counter directly. None of them make a clock read an IPC. danos makes it a syscall.
That "from the TSC" hides a portability question, because the TSC is only a valid clock
when the CPU guarantees it is invariant and when every core's TSC is synchronized.
danos checks both — the invariant-TSC CPUID bit (0x80000007 EDX[8], set on Intel and
AMD), and a cross-core "warp" check as the cores come up — and falls back to the HPET
counter when either fails. So now() stays accurate on a real Intel box, a real AMD box,
and inside a VM alike; only the source behind it differs. The mechanism is in
device-interrupts.md.
So the timer hardware lives in the kernel, and there is no hpet driver and no time
server to consume. (An earlier HPET driver existed only to demonstrate the driver
model; that role now lives in drivers.md, as documentation.) The one place
a user-space time service is justified — wall-clock / calendar time — is discussed
at the end; it is deliberately not built yet.
The three system calls
Time and waiting are three entries in the small syscall table (syscall.md):
clock(#23) → monotonic nanoseconds since boot. It only moves forward. Not wall-clock: no date, no timezone. Backed byarchitecture.nanos()(TSC, scaled with a 128-bit intermediate so a long uptime can't overflow) — a few nanoseconds of resolution, and just anrdtscplus a multiply.sleep(#3) → block the caller for N milliseconds. The scheduler records a wake deadline and the tick sweep wakes it (scheduler.sleep).timer_bind(#31) → arm a one-shot timer that, after N milliseconds, posts a timer notification to an IPC endpoint. Unlikesleepit does not block: a service can keep answering messages on the same endpoint while a deadline is pending. This is the timed wait that stop-sequence escalation, hello deadlines, and restart backoff are built from (process-lifecycle.md, device-manager.md).
The kernel's own scheduling timer (the LAPIC, vector 32) is never exposed to user space;
programs read the TSC through clock and get timed wakeups through sleep/timer_bind,
both riding the scheduler tick.
time — the generic interface
Applications don't call the syscalls directly; they use time
(library/kernel/time.zig), a thin Instant/Duration layer over them — an ergonomic
front door, not new mechanism.
const time = @import("time");
const start = time.now(); // Instant — monotonic
doWork();
const took = start.elapsed(); // Duration
time.sleep(time.Duration.fromMillis(5)); // block ~5 ms
// A deadline delivered as a notification, so a service keeps serving meanwhile:
_ = time.after(endpoint, time.Duration.fromMillis(200));
Durationis nanoseconds under the hood, withfromNanos/fromMicros/fromMillis/ fromSecondsandasNanos/asMillis.ceilMillisrounds up to the kernel's millisecond granularity, so a sub-millisecondsleepnever rounds down to zero and returns early. All arithmetic saturates rather than wraps.Instantis a point on the monotonic clock:since,elapsed,plus,reached— built for deadline loops (while (!deadline.reached()) …).now()/monotonicNanos()wrapclock.available()reports whether the clock is calibrated at all (the kernel returns 0 until the TSC frequency is known, so a caller that needs real time can treat 0 as "unavailable" rather than assume it advances).sleep(d)wrapssleep;spin(d)busy-pollsnow()for the sub-millisecond delays the millisecond tick can't express;after(endpoint, d)wrapstimer_bind.
The raw wrappers (clock, sleepMillis, timerOnce) and the ergonomic
Instant/Duration layer both live in the time module
(library/kernel/time.zig); the latter is what everyday code uses.
Wall-clock time (not built)
Everything above is monotonic: elapsed time since boot, perfect for timeouts and
measurement, useless for "what is the date?" Calendar time — a real-time clock, time
zones, leap seconds — is genuinely a user-space concern, and it is the case a time
service is for. It would be backed by an RTC driver (the CMOS real-time clock), not
the HPET, and exposed as a CLOCK_REALTIME-style service alongside the monotonic
syscall. It is deferred until something needs it; the monotonic clock the kernel already
owns covers every current use.
Verifying it
time's Instant/Duration arithmetic has unit tests that run on the host:
$ zig build test # includes library/kernel/time.zig
End to end, the proof the clock is real is that it advances: read now(), sleep a
Duration, read now() again, and the second reading is later — the kernel's timer
driving a ring-3 program with no service in between.