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).
173 lines
10 KiB
Markdown
173 lines
10 KiB
Markdown
# The device manager
|
||
|
||
**Status: the protocol and supervision are built** (M18.1, 2026-07-13): `hello`
|
||
with its deadline, supervised spawn, restart with backoff, and the crash-loop
|
||
cap are in — usb-xhci-bus is the first conforming driver, and the
|
||
`driver-restart` scenario proves fault → backoff → re-claim → cap end to end.
|
||
Tree reports are built too (M18.2, 2026-07-13): the xHCI driver scans its
|
||
root-hub ports and reports each connected device (`child_added`); the manager
|
||
mirrors them and prunes a dead reporter's children, and the `usb-report`
|
||
scenario proves report → prune → respawn → re-report. The application surface is built (M18.3, 2026-07-13):
|
||
`enumerate` and `subscribe` over IPC, with `device-list` as the first client —
|
||
the manager is now the one answer to "what devices exist" for applications.
|
||
The primitives underneath are real ([process-management.md](process-management.md):
|
||
spawn/supervise/kill/exit-notification; [driver-model.md](driver-model.md): the device
|
||
table as a capability system; [drivers.md](drivers.md): claim/map/IRQ), and the first
|
||
per-device driver spawn works (the device manager matches the xHCI controller by PCI
|
||
class and spawns `usb-xhci-bus` with the device id as argv[1]). This document designs
|
||
the rest: the device manager as **the tree, the matcher, and the supervisor** — the
|
||
policy process that turns [resilience.md](resilience.md)'s restart goal into practice
|
||
for drivers.
|
||
|
||
How processes stop, reload, and report their deaths is deliberately **not** in this
|
||
document: that is the universal lifecycle every danos process speaks —
|
||
[process-lifecycle.md](process-lifecycle.md), signals over IPC and the stable
|
||
`runtime.process` interface. The device manager is that design's first serious
|
||
customer, not its owner. Its own protocol contains nothing lifecycle-shaped; a
|
||
driver is stopped, health-checked, and buried exactly like any other process.
|
||
|
||
## The tree: structure in the manager, authority in the kernel
|
||
|
||
The device tree is two things fused: *information* (what exists, how it nests) and
|
||
*authority* (a descriptor is a licence to map physical memory). They separate:
|
||
|
||
- The **kernel keeps the capability system** — device, I/O-port, and interrupt
|
||
claims, resource containment on `device_register`, the
|
||
`mmio_map`/`irq_bind`/`msi_bind` gates — and **cleans all of it up when a process
|
||
dies** (settled; it is increment 1 of
|
||
[process-lifecycle.md](process-lifecycle.md)). The three invariants in
|
||
[driver-model.md](driver-model.md) stay exactly where they are. A device manager
|
||
that could mint MMIO mappings by its own say-so would be a second kernel, and a
|
||
buggy one would un-earn everything the microkernel bought.
|
||
- The **device manager owns the tree as data** — identity, topology, naming, driver
|
||
matching, hotplug events, and being the one process everything else asks about
|
||
devices. Firmware discovery seeds it (today via the kernel's snapshot); **bus
|
||
drivers grow it** by reporting what they see; applications query and watch it.
|
||
`device_enumerate` fades to a manager-internal (then deleted) seam.
|
||
|
||
Long-term, discovery itself leaves the kernel — but not *into* the manager. PCI
|
||
enumeration is a **pci-bus driver**: the manager spawns it against the host bridge
|
||
(already a device with the ECAM window as a resource), it scans, it reports functions
|
||
like any bus reports children. ACPI becomes an **acpi service** that interprets the
|
||
tables and reports the namespace. The manager only orchestrates and merges. Moving
|
||
AML interpretation out of ring 0 is its own project on its own track; nothing here
|
||
depends on when it lands. (It landed: [discovery.md](discovery.md), M19–M20.)
|
||
|
||
`device_register` is **idempotent on exact match**: a re-registration with an
|
||
identical (parent, class, identity, resources) tuple returns the existing id
|
||
instead of appending a duplicate. The kernel table has no unregister, so without
|
||
this a restarted registering bus would re-report its children as fresh nodes on
|
||
every respawn. Idempotence is what makes restart-and-re-report sound for *every*
|
||
reporting bus — pci-bus, the acpi service, a future fdt service — not just one,
|
||
and it is why supervision (below) can prune a dead bus's subtree and trust the
|
||
restarted instance to rebuild exactly the same ids.
|
||
|
||
## The protocol
|
||
|
||
A `device-manager-protocol` module (the vfs-protocol pattern): extern-struct
|
||
messages, a version in the handshake, reserved fields everywhere. The manager is a
|
||
well-known endpoint (`ipc.register(.device_manager)`); the badge tells it who is
|
||
talking; the same endpoint receives its children's exit notifications — one loop,
|
||
one world.
|
||
|
||
| Direction | Message | Purpose |
|
||
|---|---|---|
|
||
| driver → manager | `hello { version, role, device_id }` | confirms the argv assignment, starts the deadline clock |
|
||
| bus → manager | `child_added { parent, identity, resources }` | one node the bus discovered |
|
||
| bus → manager | `child_removed { id }` | unplug, or the bus lost it |
|
||
| app → manager | `enumerate` | snapshot of the tree (read-only) |
|
||
| app → manager | `subscribe` | receive published add/remove events |
|
||
|
||
`hello` is the one deadline the manager enforces itself: spawned and silent past the
|
||
deadline means wrong binary, wrong protocol version, or wedged before main — apply
|
||
the stop sequence and the restart policy. Everything else lifecycle-shaped
|
||
(terminate, the common `ping` liveness call, exit reasons) arrives through
|
||
[process-lifecycle.md](process-lifecycle.md)'s vocabulary, not this protocol.
|
||
|
||
Assignment stays argv (`usb-xhci-bus <device id>`) for now — simple, and it works.
|
||
The step after `hello` exists is delegation: the manager claims (or is granted) the
|
||
devices and passes the claim to the driver over IPC (the M13 capability-transfer
|
||
mechanism), replacing first-come-first-served `device_claim` with policy. Identity in
|
||
`child_added` is per-bus: PCI children carry the class triple (`pci_class`, as the
|
||
xHCI match already uses); USB children carry the (class, subclass, protocol) triple
|
||
from usb-ids.zig — each bus's native language, decoded by the shared ids modules.
|
||
|
||
## Supervision and restart
|
||
|
||
Every driver is spawned with the manager's exit endpoint (`spawnSupervised` — built).
|
||
On a death notification:
|
||
|
||
1. **Read the reason** ([process-lifecycle.md](process-lifecycle.md) increment 2).
|
||
Clean exit → it meant to; don't restart. Fault or missed `hello` deadline →
|
||
restart with **backoff**, and a crash-loop cap (three fast deaths → mark failed,
|
||
stop respawning, log loudly; a later `reload` to the manager can retry).
|
||
2. **Prune the subtree** the dead bus driver reported. Its children describe
|
||
protocol state (xHCI slot ids, transfer rings) that died with the process;
|
||
keeping the nodes would be keeping a lie. Watchers receive `child_removed` — the
|
||
input service losing, then regaining, a keyboard is the *honest* description of
|
||
what happened. The restarted instance rediscovers and re-reports.
|
||
3. **The claim is already free** because the kernel released it at death — the
|
||
restarted instance claims the same controller and comes up.
|
||
|
||
Who supervises the supervisor: **init** (PID 1), which already supervises the
|
||
services it starts. If the manager dies, drivers keep running (they hold their
|
||
claims; the kernel doesn't care who their supervisor was — though their exit
|
||
notifications now dangle harmlessly). The restarted manager re-learns the world:
|
||
kernel snapshot, then a re-`hello` round — drivers answer a broadcast or are stopped
|
||
and respawned. Full state handoff is deliberately not attempted.
|
||
|
||
## Thin drivers, class protocols
|
||
|
||
The [driver-model.md](driver-model.md) three-shape split, restated as processes:
|
||
|
||
- A **bus driver** (usb-xhci-bus) owns its controller — claim, MMIO, IRQ/MSI, DMA
|
||
rings — and offers a *transfer* protocol ("submit a control transfer to device N",
|
||
built from the usb-abi request constructors) plus tree reports to the manager.
|
||
- A **class driver** (usb-hid, usb-storage) owns nothing: it is matched to a reported
|
||
child by its identity triple, speaks the bus's transfer protocol downward and its
|
||
service's protocol upward — HID reports to the input service, blocks to the block
|
||
service. It works unchanged over any controller.
|
||
- **Services** (input, display, block) aggregate class drivers and face applications.
|
||
|
||
Each arrow is a protocol module. The manager routes none of the data plane — it
|
||
introduces the parties (matching), supervises them (lifecycle), and gets out of the
|
||
way.
|
||
|
||
## Increments
|
||
|
||
Increments 1–4 are the lifecycle prerequisites and live in
|
||
[process-lifecycle.md](process-lifecycle.md) (claim cleanup on death, exit reasons,
|
||
published exit events, signals + `runtime.process`). On top of those:
|
||
|
||
5. **device-manager-protocol**: `hello`, supervised spawn with restart policy;
|
||
usb-xhci-bus becomes the first conforming driver.
|
||
6. **Tree reports**: `child_added`/`child_removed`; the manager mirrors; xHCI reports
|
||
the mouse and keyboard QEMU already hangs off it.
|
||
7. **App surface**: `enumerate`/`subscribe` over IPC; `device_enumerate` retreats
|
||
to a manager-internal seam.
|
||
8. **Discovery migration** — DONE (M19–M20, 2026-07-13): enumeration moved to
|
||
ring 3 as swappable per-firmware discoverers — the pci-bus driver (M19) then
|
||
the acpi service (M20), see [discovery.md](discovery.md); the kernel seeds
|
||
only the host bridge and the acpi-tables node. Matching moved with it:
|
||
`child_added` grew a `device_id` (the kernel-registered id, `no_device` for
|
||
unregistered leaves like USB ports) and a firmware `hid`, and the manager now
|
||
matches drivers from those **reports** rather than its boot-time snapshot. The
|
||
PCI arm flipped in M19.3, the ACPI arm (ps2-bus matched from `_HID`) in M20.3
|
||
— each in a single phase so no device is ever matched from both sources at
|
||
once. The acpi service reports only the non-PCI `_HID` devices, since pci-bus
|
||
already reports PCI functions (M20.2).
|
||
|
||
## Settled questions (2026-07-12)
|
||
|
||
- **Stateful buses**: pruning the subtree on bus-driver death is right for USB. A
|
||
future storage bus with in-flight writes wants drain-before-terminate — which is
|
||
exactly the `deadline_ms` parameter `stop()` already has; a per-driver deadline
|
||
is one value in the manager's policy table when such a bus arrives. No design
|
||
change.
|
||
- **Manager death**: drivers survive the manager; the restarted manager re-learns
|
||
the world (above). Checkpointing driver state with the manager is deferred until
|
||
something demonstrates the need.
|
||
- **Matching stays code until the third bus.** `driverFor`/`pciDriverFor` are
|
||
honest at two bus types; the third triggers the manifest (a driver declares what
|
||
it binds: a PCI class triple, a USB class triple, an ACPI `_HID`).
|