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
+40 -29
View File
@@ -22,12 +22,12 @@ say.*
## How a driver gets started: discover, match, spawn
Nothing in the kernel decides that the HPET needs the `hpet` driver — that is policy,
and policy lives in user space. Boot brings user space up as a three-level supervision
hierarchy, each level owning one job:
Nothing in the kernel decides that the PCI host bridge needs the `pci-bus` driver — that
is policy, and policy lives in user space. Boot brings user space up as a three-level
supervision hierarchy, each level owning one job:
```
kernel ──spawns──► init (PID 1) ──spawns──► device-manager ──spawns──► hpet
kernel ──spawns──► init (PID 1) ──spawns──► device-manager ──spawns──► pci-bus
| | |
spawns only init, the service supervisor: the driver supervisor: enumerates
publishes the starts the system /system/devices, matches each device
@@ -184,7 +184,11 @@ Two properties worth knowing:
## A whole driver
`system/drivers/hpet/hpet.zig` is ~150 lines and does all of it. The shape:
A minimal leaf driver is only ~150 lines and does all of it. danos ships **no such
example binary** — the driver model is proven by the real drivers (`pci-bus`, `ps2-bus`,
`usb-xhci-bus`), and a teaching example belongs here, in the docs, rather than as a
compiled program nobody runs. Illustrated with a hypothetical HPET timer driver, the
shape is:
```zig
const hpet = findHpet(buf) orelse return; // device_enumerate, look for
@@ -209,8 +213,8 @@ while (...) {
}
```
The HPET is a good first driver for a reason that isn't obvious. Its *counter* is a
clocksource — the only way to use it is to read it, so it proved `mmio_map` without
The HPET makes a good illustration for a reason that isn't obvious. Its *counter* is a
clocksource — the only way to use it is to read it, so it exercises `mmio_map` without
needing interrupts at all. Its *comparators* are a clockevent, and can be configured
**level-triggered** (`Tn_INT_TYPE_CNF`), which asserts a bit in `GENERAL_INT_STATUS`
that the driver must write-1-to-clear. That's a genuine deassert step, so the full
@@ -252,9 +256,10 @@ bus driver may only ever subdivide what it already owns.
A device with **no resources** is legal and common. A USB device is reached through its
controller, not by MMIO, so it gets `resource_count = 0`.
See [`system/drivers/bus/bus.zig`](../system/drivers/bus/bus.zig) for a complete one, and
[driver-model.md](driver-model.md) for how bus drivers, class drivers and host
controller drivers fit together.
See [`system/drivers/pci-bus/pci-bus.zig`](../system/drivers/pci-bus/pci-bus.zig) for a
real one — it claims a PCI host bridge, maps its ECAM window, and publishes each function
it finds as a child — and [driver-model.md](driver-model.md) for how bus drivers, class
drivers and host controller drivers fit together.
## What the kernel does not do for you
@@ -313,32 +318,38 @@ uncacheable, physical address exposed), and **memory barriers** (`/lib/mmio`'s
## Verifying it
The `hpet` test spawns `hpet` from the initial ramdisk and watches the serial log. The driver
prints `hpet: ok` only after being woken five times, and its loop's only exit is
through `replyWait` returning a notification — it cannot reach that line by polling.
No demo driver ships to prove this end to end; the *real* drivers do, so the tests
target them and the kernel primitives directly:
The last check doesn't trust the driver's self-report at all: the kernel reads the I/O
APIC redirection entry back and asserts the line really is routed to a device vector,
really is level-triggered, and really was left unmasked by the driver's final
`irq_ack`.
- **`device-manager`** — boots only the device manager, which discovers the PCI host
bridge, matches `pci-bus`, and `system_spawn`s it. The test reads kernel state — the
process table and the device tree — to confirm pci-bus came up and registered the
functions it enumerated: the whole discover → match → spawn → driver-up chain.
- **`acpi-ps2`** — a user-space driver (`ps2-bus`) is woken by its device's IRQ,
delivered as an IPC notification, and attaches the keyboard: IRQ-as-IPC, end to end.
- **`pci-scan`** — a user-space driver (`pci-bus`) maps its device's MMIO (the ECAM
window) and walks it: `mmio_map`, end to end.
- **`containment`** — the kernel refuses a `device_register` whose child window escapes
the parent's grant (else it would be a syscall for mapping arbitrary memory), while an
identical re-register stays idempotent. Asserted in-kernel, straight against the broker.
- **`irqfree`** — the teardown path. Binds two owners to one shared endpoint, releases
one, and reads the I/O APIC back: the departing owner's line is masked, the sibling's
is not. That second half is why bindings are keyed on the owning *task* and not on the
endpoint pointer — endpoints are shared, so releasing "everything pointing at this
endpoint" would silently mask a live driver's device.
- **`iopass`** — the `device_grant` teardown rule, so destroying a driver's address
space never returns MMIO frames to the RAM pool.
```
$ python3 test/qemu_test.py hpet irqfree iopass
hpet ... PASS (matched 'DANOS-TEST-RESULT: PASS')
$ python3 test/qemu_test.py device-manager acpi-ps2 pci-scan containment irqfree iopass
device-manager ... PASS (matched 'DANOS-TEST-RESULT: PASS')
acpi-ps2 ... PASS
pci-scan ... PASS (matched 'DANOS-TEST-RESULT: PASS')
containment ... PASS (matched 'DANOS-TEST-RESULT: PASS')
irqfree ... PASS (matched 'DANOS-TEST-RESULT: PASS')
iopass ... PASS (matched 'DANOS-TEST-RESULT: PASS')
```
Two companions cover what `hpet` can't, because it never exits:
- **`irqfree`** — the teardown path. Binds two owners to one shared endpoint, releases
one, and reads the I/O APIC back: the departing owner's line is masked, the sibling's
is not. That second half is why bindings are keyed on the owning *task* and not on
the endpoint pointer — endpoints are shared, so releasing "everything pointing at
this endpoint" would silently mask a live driver's device.
- **`iopass`** — the `device_grant` teardown rule, so destroying a driver's address
space never returns MMIO frames to the RAM pool.
## What's next (not done here)
The big driver-model pieces — capability passing (class drivers), DMA + barriers, MSI,