Add runtime.time, drop demo drivers, harden TSC timekeeping

Time is a kernel concern in danos: the kernel owns the scheduling timer and
already exposes monotonic time via the clock/sleep/timer_bind syscalls, so a
userspace time service would be a redundant, slower path. This adds the generic
runtime.time module over those syscalls, retires the two demonstration drivers,
reorganizes the milestone docs, and makes the monotonic clock correct on Intel,
AMD, and inside any VM.

runtime.time (library/runtime/time.zig)
- Instant/Duration interface: now, sleep, spin, after, monotonicNanos, available
- a thin layer over system.clock/sleep/timerOnce; unit-tested arithmetic

Remove the demo drivers hpet and bus (a teaching example belongs in the docs,
not shipped in the tree)
- system/drivers/ now holds only real drivers: pci-bus, ps2-bus, usb-xhci-bus
- device-manager end-to-end test repointed to pci-bus (asserts on kernel state:
  the process table and the device tree, not a racy serial marker)
- device_register containment moved to a new in-kernel `containment` test
- the driver-model worked example moved inline into docs/drivers.md

Reorganize milestone docs into topic docs
- m17-m18 / m19-m20 / m21 plans dissolved into process-lifecycle, device-manager,
  discovery, and acpi docs; new docs/power.md and docs/timers.md; ~20 citations
  repointed; plan docs deleted

TSC reliability (apic.zig, smp.zig, cpu.zig, kernel.zig)
- check the invariant-TSC bit (CPUID 0x80000007 EDX[8]) on Intel and AMD
- cross-core "warp" check at SMP bring-up, pairwise BSP<->AP as each core comes up
- fall back to the HPET clocksource when the TSC is not invariant (a bare VM) or
  not synchronized (a warp), switched continuously so time never jumps
- boot log reports the outcome; new tsc-sync test exercises the TSC + warp path

Verified: zig build; zig build test; 60/60 QEMU cases (incl. new containment and
tsc-sync).
This commit is contained in:
Daniel Samson
2026-07-13 11:56:43 +01:00
parent 5b63a841ba
commit 452080e997
36 changed files with 1150 additions and 1267 deletions
+4 -1
View File
@@ -12,6 +12,9 @@
//! (arguments arrive via `init`).
pub const system = @import("system.zig");
/// Monotonic time, delays, and deadlines over the kernel clock/sleep/timer syscalls
/// — an `Instant`/`Duration` front door, no time service (docs/timers.md).
pub const time = @import("time.zig");
pub const heap = @import("heap.zig");
pub const ipc = @import("ipc.zig");
pub const start = @import("start.zig");
@@ -21,7 +24,7 @@ pub const vfs_protocol = @import("vfs-protocol");
/// The device-manager protocol: hello + tree reports (docs/device-manager.md).
pub const device_manager_protocol = @import("device-manager-protocol");
/// The power protocol: events (button, lid, battery) + shutdown (docs/m21-plan.md).
/// The power protocol: events (button, lid, battery) + shutdown (docs/power.md).
pub const power_protocol = @import("power-protocol");
/// Keyboard-event listening (subscribe/next) and broadcasting (publish), over the input
/// service. See library/runtime/input.zig and system/services/input/.
+169
View File
@@ -0,0 +1,169 @@
//! The danos time interface — monotonic time, delays, and deadlines for user space.
//!
//! There is no time *service*: the kernel already owns the scheduling timer and
//! surfaces it directly, so reading the clock is one system call (an `rdtsc` and a
//! scale), never an IPC round trip (docs/timers.md explains why). This module is a
//! thin, generic layer over the `clock`/`sleep`/`timer_bind` wrappers in `system.zig`
//! — an ergonomic `Instant`/`Duration` front door, not new mechanism.
//!
//! It is **monotonic** time only: nanoseconds since boot, moving forward, no date or
//! timezone. Wall-clock/calendar time is a separate user-space service (an RTC-backed
//! CLOCK_REALTIME) layered on top later.
const std = @import("std");
const system = @import("system.zig");
const nanos_per_micro: u64 = 1_000;
const nanos_per_milli: u64 = 1_000_000;
const nanos_per_second: u64 = 1_000_000_000;
/// A span of time, held as nanoseconds. Constructors name their unit; accessors
/// truncate toward zero. `ceilMillis` rounds *up*, since `sleep`/`after` land on the
/// kernel's millisecond granularity and rounding down could return early.
pub const Duration = struct {
ns: u64,
pub fn fromNanos(n: u64) Duration {
return .{ .ns = n };
}
pub fn fromMicros(n: u64) Duration {
return .{ .ns = n *| nanos_per_micro };
}
pub fn fromMillis(n: u64) Duration {
return .{ .ns = n *| nanos_per_milli };
}
pub fn fromSeconds(n: u64) Duration {
return .{ .ns = n *| nanos_per_second };
}
pub fn asNanos(d: Duration) u64 {
return d.ns;
}
pub fn asMicros(d: Duration) u64 {
return d.ns / nanos_per_micro;
}
pub fn asMillis(d: Duration) u64 {
return d.ns / nanos_per_milli;
}
pub fn asSeconds(d: Duration) u64 {
return d.ns / nanos_per_second;
}
/// Whole milliseconds, rounded up — the argument `sleep`/`after` pass the kernel.
/// A non-zero sub-millisecond duration becomes 1 ms rather than 0.
pub fn ceilMillis(d: Duration) u64 {
return (d.ns +| (nanos_per_milli - 1)) / nanos_per_milli;
}
pub fn plus(a: Duration, b: Duration) Duration {
return .{ .ns = a.ns +| b.ns };
}
};
/// A point on the monotonic clock — nanoseconds since boot. Compare and subtract
/// instants to measure elapsed time; it never runs backward, so `since` is safe to
/// saturate at zero rather than wrap.
pub const Instant = struct {
ns: u64,
/// The span from `earlier` to `self`, saturating at zero if `earlier` is later
/// (which the monotonic clock should never produce, but callers may pass any pair).
pub fn since(self: Instant, earlier: Instant) Duration {
return .{ .ns = self.ns -| earlier.ns };
}
/// How long since this instant, sampled now.
pub fn elapsed(self: Instant) Duration {
return now().since(self);
}
/// This instant advanced by `d` (a deadline, `d` from here).
pub fn plus(self: Instant, d: Duration) Instant {
return .{ .ns = self.ns +| d.ns };
}
/// Whether the monotonic clock has reached this instant (used as a deadline).
pub fn reached(deadline: Instant) bool {
return now().ns >= deadline.ns;
}
};
/// The current monotonic time.
pub fn now() Instant {
return .{ .ns = system.clock() };
}
/// Monotonic nanoseconds since boot — the raw `clock()` reading, for callers that
/// want a plain integer instead of an `Instant`.
pub fn monotonicNanos() u64 {
return system.clock();
}
/// Whether the monotonic clock is usable. The kernel returns 0 until the TSC is
/// calibrated (`tsc_hz == 0`); a caller that needs real time can treat that as
/// "unavailable" instead of assuming the clock advances.
pub fn available() bool {
return system.clock() != 0;
}
/// Block the caller for at least `d`, rounded up to the kernel's millisecond
/// granularity. For sub-millisecond precision the scheduler cannot express, use
/// `spin`.
pub fn sleep(d: Duration) void {
system.sleep(d.ceilMillis());
}
/// Block the caller for `ms` milliseconds — the coarse, allocation-free form.
pub fn sleepMillis(ms: u64) void {
system.sleep(ms);
}
/// Busy-wait until `d` has elapsed, polling the monotonic clock. This burns the CPU
/// on purpose, to hit sub-millisecond delays the scheduler's millisecond tick cannot.
/// Prefer `sleep` for anything at or above a millisecond.
pub fn spin(d: Duration) void {
const deadline = now().plus(d);
while (!deadline.reached()) {}
}
/// Arm a one-shot timer against `endpoint` (a handle from `ipc.createIpcEndpoint`):
/// after `d` the kernel posts a timer notification (`ipc.Received.isTimer`) there.
/// Unlike `sleep`, this does not block — a service can keep serving IPC on the same
/// endpoint while the deadline is pending. Rounds `d` up to milliseconds; returns
/// false if the timer could not be armed. See `system.timerOnce`.
pub fn after(endpoint: usize, d: Duration) bool {
return system.timerOnce(endpoint, d.ceilMillis());
}
test "Duration unit conversions round toward zero" {
try std.testing.expectEqual(@as(u64, 1_000_000_000), Duration.fromSeconds(1).asNanos());
try std.testing.expectEqual(@as(u64, 1_500), Duration.fromNanos(1_500).asNanos());
try std.testing.expectEqual(@as(u64, 2), Duration.fromMillis(2).asMillis());
try std.testing.expectEqual(@as(u64, 1), Duration.fromNanos(1_999_999).asMillis());
try std.testing.expectEqual(@as(u64, 250), Duration.fromMicros(250).asMicros());
}
test "ceilMillis rounds up, and never turns a nonzero span into zero" {
try std.testing.expectEqual(@as(u64, 0), Duration.fromNanos(0).ceilMillis());
try std.testing.expectEqual(@as(u64, 1), Duration.fromNanos(1).ceilMillis());
try std.testing.expectEqual(@as(u64, 1), Duration.fromMillis(1).ceilMillis());
try std.testing.expectEqual(@as(u64, 2), Duration.fromNanos(nanos_per_milli + 1).ceilMillis());
try std.testing.expectEqual(@as(u64, 5), Duration.fromMillis(5).ceilMillis());
}
test "Instant arithmetic: since saturates, plus/reached form deadlines" {
const t0 = Instant{ .ns = 1_000 };
const t1 = Instant{ .ns = 4_000 };
try std.testing.expectEqual(@as(u64, 3_000), t1.since(t0).asNanos());
// earlier-than-self can't happen on a monotonic clock; saturate rather than wrap.
try std.testing.expectEqual(@as(u64, 0), t0.since(t1).asNanos());
const deadline = t0.plus(Duration.fromNanos(2_500));
try std.testing.expectEqual(@as(u64, 3_500), deadline.ns);
}
test "saturating arithmetic does not overflow at the u64 ceiling" {
const big = Duration.fromSeconds(std.math.maxInt(u64));
try std.testing.expectEqual(@as(u64, std.math.maxInt(u64)), big.asNanos());
const late = Instant{ .ns = std.math.maxInt(u64) };
try std.testing.expectEqual(@as(u64, std.math.maxInt(u64)), late.plus(Duration.fromSeconds(10)).ns);
}