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
+22 -19
View File
@@ -177,6 +177,15 @@ CASES = [
"smp": 4,
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# TSC clocksource + cross-core warp check (real Intel/AMD / KVM path). TCG won't
# advertise an invariant TSC, so the kernel forces the TSC clocksource on for this
# case (gated in kernel.zig) and runs the per-AP warp check across the 4 cores;
# their TSCs are synchronized, so it stays on the TSC (no HPET fallback). The rest
# of the suite exercises the HPET fallback instead. See docs/timers.md.
{"name": "tsc-sync",
"smp": 4,
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
{"name": "fault-ud", "expect": r"invalid opcode \(vector 6\)"},
{"name": "fault-pf", "expect": r"page fault \(vector 14\)"},
{"name": "fault-df", "expect": r"double fault \(vector 8\)"},
@@ -284,7 +293,7 @@ CASES = [
"fail": r"acpi-parse: mismatch|DANOS-TEST-RESULT: FAIL"},
# M20.3: the flip — ps2-bus now comes up from the acpi service's report, not
# a kernel-built node. Ordered: report -> spawn -> the driver attaches its
# keyboard, proving discovery runs entirely in ring 3 (docs/m19-m20-plan.md).
# keyboard, proving discovery runs entirely in ring 3 (docs/discovery.md).
{"name": "acpi-ps2",
"smp": 4,
"timeout": 150,
@@ -294,7 +303,7 @@ CASES = [
"fail": r"DANOS-TEST-RESULT: FAIL"},
# M21.1: the SCI + power button. Boot the manager (which spawns the acpi
# service); ~4s in, QMP system_powerdown raises the ACPI power-button fixed
# event; the service's SCI handler must log the press (docs/m21-plan.md).
# event; the service's SCI handler must log the press (docs/acpi.md).
{"name": "power-button",
"smp": 4,
"timeout": 60,
@@ -305,7 +314,7 @@ CASES = [
# ~5s in, QMP system_powerdown raises the power button; the acpi service
# publishes it, init stops its children then requests S5, and QEMU exits.
# The ordered regex proves button -> shutting-down -> entering-S5; the case
# passes on QEMU's self-exit through S5 (docs/m21-plan.md).
# passes on QEMU's self-exit through S5 (docs/power.md).
{"name": "orderly-shutdown",
"smp": 4,
"timeout": 90,
@@ -316,7 +325,7 @@ CASES = [
"fail": r"power: S5 write did not take|DANOS-TEST-RESULT: FAIL"},
# M20.2: the acpi service evaluates _CRS/_STA in ring 3 and registers +
# reports its _HID devices — the two PS/2 nodes must appear with resources
# (keyboard: io 0x60/0x64 + IRQ = 3; mouse: IRQ = 1) (docs/m19-m20-plan.md).
# (keyboard: io 0x60/0x64 + IRQ = 3; mouse: IRQ = 1) (docs/discovery.md).
{"name": "acpi-report",
"smp": 4,
"timeout": 150,
@@ -378,27 +387,21 @@ CASES = [
{"name": "input",
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# IO passthrough + IRQ-as-IPC: a user-space HPET driver maps device MMIO into
# its own address space, binds the device's interrupt to an IPC endpoint, and
# is woken by the hardware five times while blocked (never polling).
{"name": "hpet",
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# Device manager: a ring-3 service enumerates /system/devices and matches each
# device to a driver (discovery + policy in user space). This increment logs the
# decision; spawning follows.
# Device manager: a ring-3 service enumerates /system/devices, matches the PCI host
# bridge to pci-bus, and spawns it — end-to-end proof of discover -> match -> spawn
# -> driver-up (the spawned pci-bus logs "<N> functions found").
{"name": "device-manager",
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# Bus driver: a user process claims a device, enumerates its children from the
# hardware, and publishes each with dev_register — and the kernel refuses a child
# whose window escapes the parent's (else dev_register maps arbitrary memory).
{"name": "bus",
# device_register containment (in-kernel): registering a child whose MMIO window
# escapes its parent's grant is refused (NotContained) — else dev_register would map
# arbitrary physical memory — while an identical re-register stays idempotent.
{"name": "containment",
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# IRQ teardown: an exiting driver's line is masked and its slot cleared (so no
# ISR notifies a freed endpoint), and a sibling owner sharing that endpoint
# keeps its own binding. The path hpet never takes, since it runs forever.
# keeps its own binding. A long-running driver never reaches this teardown path.
{"name": "irqfree",
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
@@ -450,7 +453,7 @@ def qmp_send(path, command):
"""One QMP command: connect, capabilities handshake, execute. Raises on any
failure — the caller retries until the guest's socket is ready. This is how
a case injects a host-side event (system_powerdown = the ACPI power button)
into the running guest (docs/m21-plan.md)."""
into the running guest (docs/power.md)."""
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(5)
try: