216 lines
14 KiB
Markdown
216 lines
14 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](../os-development/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](../os-development/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](../os-development/process-lifecycle.md), signals over IPC and the stable
|
||
`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](../os-development/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](../os-development/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, defined through the
|
||
[envelope](../os-development/protocol-namespace.md): every packet — request,
|
||
reply, and pushed event alike — begins with the folded `Header`, and **the device
|
||
id is `Header.target`**, the manager's object addressing. The contract is bound at
|
||
`/protocol/device-manager`; the kernel-stamped badge tells the manager who is
|
||
talking; the same endpoint receives its children's exit notifications — one loop,
|
||
one world.
|
||
|
||
| Direction | Packet | Purpose |
|
||
|---|---|---|
|
||
| driver → manager | `hello { role, version }` @ the assigned device | confirms the argv assignment, starts the deadline clock |
|
||
| bus → manager | `child_added { parent, bus_address, identity, bus, vendor, device, subsystem, hid }` @ the registered device id | one node the bus discovered |
|
||
| bus → manager | `child_removed { parent, bus_address }` | unplug, or the bus lost it |
|
||
| app → manager | `enumerate` (reserved verb 1) | snapshot of the tree: one `ChildEntry` per record in the reply's tail |
|
||
| app → manager | `subscribe` (reserved verb 2) | receive published add/remove events; the subscriber's endpoint rides as the call's capability |
|
||
| manager → app | `child_added` / `child_removed` events | the same two structs, pushed rather than called |
|
||
|
||
The watcher table behind those last two rows is the **service harness's**
|
||
(`service.Subscribers`, shared with input and power), not the manager's: it
|
||
answers `subscribe`/`unsubscribe`, frames each event once for the fan-out, and
|
||
sweeps a watcher on its exit notification — where the manager previously had no
|
||
sweep for watchers at all. Its own supervised-driver exits are a different thing
|
||
and unchanged, except that a driver's death now arrives twice (the manager is
|
||
both its supervisor and a subscriber to published exits), so the manager retires
|
||
a dead driver's process id as it handles the first and the second finds nothing
|
||
to act on.
|
||
|
||
Two of what used to be the manager's own operations are the envelope's **reserved**
|
||
verbs, which mean the same thing at every provider in the system, so this protocol
|
||
numbers only three of its own (`hello` = 16, `child_added` = 17,
|
||
`child_removed` = 18) and its two events in their own space (`child_added` = 16,
|
||
`child_removed` = 17). No reply carries a status field: that is the `Status` every
|
||
reply begins with.
|
||
|
||
`child_added` is the one struct that travels both ways — a bus *calls* it, the
|
||
manager *pushes* it — which is why the operation and event numbering spaces are
|
||
separate: one encoding, both directions, told apart by which way the packet went.
|
||
Folding the operation byte and the device id out of it is also what makes it fit:
|
||
a pushed event is 64 bytes at most, header included, and this one lands exactly on
|
||
that floor. `child_removed` is the single message whose target stays 0, because it
|
||
is addressed by the composite (parent, bus address) and no single `u64` carries a
|
||
pair.
|
||
|
||
`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](../os-development/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.
|
||
(Since the registry landed, `child_added` also carries a `bus` discriminator and
|
||
the numeric `vendor`/`device`/`subsystem` ids the finer match levels need —
|
||
see [/etc/devices.csv](devices-csv.md).)
|
||
|
||
## 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](../os-development/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](../os-development/process-lifecycle.md) (claim cleanup on death, exit reasons,
|
||
published exit events, signals + `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](../os-development/discovery.md); of the enumerable
|
||
devices, the kernel seeds only the host bridge and the acpi-tables node (the
|
||
non-enumerable platform nodes — processors, interrupt controllers, the HPET,
|
||
the loader's framebuffer — stay kernel-seeded too). 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 is a registry, not code (resolved 2026-07-26).** `driverFor`/
|
||
`pciDriverFor` were honest at two bus types; the third (USB) was matched in code
|
||
too, and then the switch tables started to hurt — they keyed PCI matches on the
|
||
class triple alone, so a virtio-gpu could only be matched as a generic display
|
||
function and the driver had to re-confirm its `1AF4:1050` identity from config
|
||
space after being spawned. The manifest the earlier note anticipated landed as a
|
||
human-readable registry: **[/etc/devices.csv](devices-csv.md)**, parsed by the
|
||
pure `device-registry` module and read by the manager at boot. A row binds a
|
||
driver to a device by any of base / subclass / prog-IF / vendor / device /
|
||
subsystem / `_HID`, most-specific match winning; it is authoritative (no
|
||
compiled-in fallback — an unmatched device is logged, never guessed).
|
||
`pciDriverForIdentity`, `hidDriverFor`, and `usbDriverForIdentity` are gone.
|