docs: full docs-vs-code audit — fix every stale claim across 40 docs

Every doc verified claim-by-claim against the code by parallel audit agents,
then fixed and adversarially re-verified. Two waves of staleness corrected:
the originally audited findings (higher-half boot handoff, kernel VFS
takeover, fault isolation + claim release + driver restart, AML/S5 moving to
ring 3, threading's shipped design, USB+FAT landing) and a second pass of
adjacent claims the verifiers caught (smp.md 'not built yet' intro,
system-requirements' PS/2-only and no-storage claims, halting.md's red-panic
and no-IDT text, testing.md's serial mirroring, router-era vfs-protocol
wording, capsule-first boot loading).

threading.md now documents the shared-fate gap explicitly: the design says a
process dies whole, the kernel today kills only the offending thread.

Also fixes three stale code comments (isr.s exceptionHandler, acpi.zig
sleepValue, build.zig boot-volume) — comments only, no behavior change.
This commit is contained in:
Daniel Samson 2026-07-22 09:09:53 +01:00
parent 52df2ba6f6
commit e854f65623
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
43 changed files with 821 additions and 546 deletions

View File

@ -788,8 +788,8 @@ pub fn build(b: *std.Build) void {
"/usr/local/share/qemu/edk2-i386-vars.fd", // macOS Homebrew (Intel) "/usr/local/share/qemu/edk2-i386-vars.fd", // macOS Homebrew (Intel)
}); });
// The FHS zig-out (installed above) *is* the boot volume no separate ESP to // The guest boots the self-contained FAT image (attached as USB storage below),
// assemble. QEMU presents it to the guest as a FAT drive below. // not the installed FHS zig-out see the run step's drive/device flags.
// The firmware needs to write NVRAM, so give it a writable copy of the vars. // The firmware needs to write NVRAM, so give it a writable copy of the vars.
const vars_copy = b.addSystemCommand(&.{ "cp", "-f", ovmf_vars }); const vars_copy = b.addSystemCommand(&.{ "cp", "-f", ovmf_vars });

View File

@ -54,27 +54,32 @@ rather than restate it. Roughly in the order things happen at runtime:
three primitives it proposed are long since built (M13 capability passing, three primitives it proposed are long since built (M13 capability passing,
M14 DMA + barriers, M15 MSI), and the driver *contract* on top of them — M14 DMA + barriers, M15 MSI), and the driver *contract* on top of them —
hello, supervision, restart — is built too (device-manager.md, M18). hello, supervision, restart — is built too (device-manager.md, M18).
16. **[process-management.md](process-management.md) — process management.** The 16. **[usb-hub.md](usb-hub.md) — USB hubs.** Built (M22): why hub topology is handled
*inside* the `usb-xhci-bus` driver rather than a separate hub class driver — a
device behind a hub is reached by the **controller**, programmed with a route
string in its slot context — plus the compound-hub reality (a USB 3.0 hub is
physically two hubs) and detection via the hub's status-change interrupt endpoint.
17. **[process-management.md](process-management.md) — process management.** The
microkernel's `ps`/`kill`/SIGCHLD: enumerate as a table snapshot, the microkernel's `ps`/`kill`/SIGCHLD: enumerate as a table snapshot, the
supervision link as the kill authority, and child-exit notifications over the supervision link as the kill authority, and child-exit notifications over the
same endpoints IRQs arrive on. same endpoints IRQs arrive on.
17. **[process-lifecycle.md](process-lifecycle.md) — the process lifecycle.** Built 18. **[process-lifecycle.md](process-lifecycle.md) — the process lifecycle.** Built
(M17): signals over IPC as the one lifecycle vocabulary every process speaks — the (M17): signals over IPC as the one lifecycle vocabulary every process speaks — the
POSIX.1-1990 words with message delivery instead of stack hijack, the stable POSIX.1-1990 words with message delivery instead of stack hijack, the stable
`runtime.process` interface, exit reasons, published exit events any stateful `runtime.process` interface, exit reasons, published exit events any stateful
service can subscribe to (the VFS releasing dead clients' handles), and the two service can subscribe to (the VFS releasing dead clients' handles), and the two
iron rules (cleanup is the kernel's job; kill is not a signal). iron rules (cleanup is the kernel's job; kill is not a signal).
18. **[device-manager.md](device-manager.md) — the device manager.** Built (M18, 19. **[device-manager.md](device-manager.md) — the device manager.** Built (M18,
through the app surface): the through the app surface): the
tree, the matcher, and the supervisor. Tree structure lives in the manager, tree, the matcher, and the supervisor. Tree structure lives in the manager,
authority stays in the kernel; bus drivers report what they see; drivers are authority stays in the kernel; bus drivers report what they see; drivers are
restarted through the lifecycle vocabulary — the plan that turns restarted through the lifecycle vocabulary — the plan that turns
[resilience.md](resilience.md)'s restart goal into increments. [resilience.md](resilience.md)'s restart goal into increments.
19. **[input.md](input.md) — the input module.** Broadcasting input events (keyboard, 20. **[input.md](input.md) — the input module.** Broadcasting input events (keyboard,
mouse, joystick): why a synchronous rendezvous can't fan out to many listeners, the mouse, joystick): why a synchronous rendezvous can't fan out to many listeners, the
asynchronous `ipc_send` primitive built to fix it, and the per-device subscribe/publish asynchronous `ipc_send` primitive built to fix it, and the per-device subscribe/publish
service layered on top. service layered on top.
20. **[display.md](display.md) — the display service.** The display half of the GUI 21. **[display.md](display.md) — the display service.** The display half of the GUI
track: a user-space compositor that owns the framebuffer, composes a layer stack into track: a user-space compositor that owns the framebuffer, composes a layer stack into
a double buffer, and presents it. Why GOP and the PCI display device are two views of a double buffer, and presents it. Why GOP and the PCI display device are two views of
one controller, the device-node + write-combining handoff, and what flicker-free buys one controller, the device-node + write-combining handoff, and what flicker-free buys
@ -85,7 +90,7 @@ rather than restate it. Roughly in the order things happen at runtime:
further out, two research snapshots survey what a *native* driver for real GPU silicon further out, two research snapshots survey what a *native* driver for real GPU silicon
would take as another `.scanout` backend: [nvidia-gpus.md](nvidia-gpus.md) (RTX 3060 / would take as another `.scanout` backend: [nvidia-gpus.md](nvidia-gpus.md) (RTX 3060 /
Ampere) and [intel-igpu.md](intel-igpu.md) (Intel iGPU). Ampere) and [intel-igpu.md](intel-igpu.md) (Intel iGPU).
21. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and 22. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
how `while (true) hlt` parks the CPU safely once there's nothing left to do. how `while (true) hlt` parks the CPU safely once there's nothing left to do.
Start with the north star: Start with the north star:
@ -139,7 +144,7 @@ Cutting across all of these:
hardware exists via ACPI (x86) or device tree (ARM) behind one neutral device model — hardware exists via ACPI (x86) or device tree (ARM) behind one neutral device model —
when to build it, and how to keep it architecture-agnostic. when to build it, and how to keep it architecture-agnostic.
- **[acpi.md](acpi.md) — finding the ACPI tables.** The concrete x86 locator chain: - **[acpi.md](acpi.md) — finding the ACPI tables.** The concrete x86 locator chain:
how the loader captures the **RSDP**, hands its physical address across in `BootInfo`, how the loader captures the **RSDP**, hands its physical address across in `BootInformation`,
and how the platform derives the **RSDT/XSDT** from it and walks the SDTs — plus the and how the platform derives the **RSDT/XSDT** from it and walks the SDTs — plus the
live event side (the SCI, the power button, GPE/Notify) the ring-3 acpi service runs. live event side (the SCI, the power button, GPE/Notify) the ring-3 acpi service runs.
- **[power.md](power.md) — the power service.** System power as a domain-named - **[power.md](power.md) — the power service.** System power as a domain-named
@ -210,7 +215,7 @@ addressed as **`system/services/init`** — the repeated leaf resolves away:
| `library/runtime/runtime.zig` | `library/runtime` (the `runtime` module) | | `library/runtime/runtime.zig` | `library/runtime` (the `runtime` module) |
In **source**, a sub-project is a directory so it can hold many files — the entry is In **source**, a sub-project is a directory so it can hold many files — the entry is
`init/init.zig`, beside it `vfs/vfs-test.zig`, `vfs/protocol.zig`, and so on. When `fat/fat.zig`, beside it `fat/engine.zig`, `fat/on-disk.zig`, and so on. When
**addressed or installed**, that collapses to the single canonical path: the `init` **addressed or installed**, that collapses to the single canonical path: the `init`
binary installs to `/system/services/init` (a file at that path), not binary installs to `/system/services/init` (a file at that path), not
`/system/services/init/init`. The repeated leaf exists only in source; the directory is `/system/services/init/init`. The repeated leaf exists only in source; the directory is
@ -222,24 +227,28 @@ A sub-project's extra files are reached through the module, never as separate pa
system/ → /system danos's own internals (the self-representation) system/ → /system danos's own internals (the self-representation)
boot-handoff.zig the loader↔kernel contract (the `boot-handoff` module) boot-handoff.zig the loader↔kernel contract (the `boot-handoff` module)
abi.zig the private kernel↔runtime syscall ABI (the `abi` module) abi.zig the private kernel↔runtime syscall ABI (the `abi` module)
parameters.zig initial-ramdisk.zig shared contracts parameters.zig initial-ramdisk.zig vfs-protocol.zig shared contracts
kernel/ IPC, memory, scheduling, the private syscall dispatch kernel/ IPC, memory, scheduling, the VFS root, the private syscall dispatch
architecture/x86_64/ the `architecture` module (never named by generic code) architecture/x86_64/ the `architecture` module (never named by generic code)
devices/ the device model /system/devices reflects (+ aml/) devices/ the device model /system/devices reflects (+ aml/)
device-abi.zig the device wire types (the `device-abi` module) device-abi.zig the device wire types (the `device-abi` module)
drivers/ hpet/ bus/ one sub-project per driver → /system/drivers drivers/ pci-bus/ ps2-bus/ usb-xhci-bus/ one sub-project per driver → /system/drivers
services/ init/ vfs/ device-manager/ system servers → /system/services (vfs/ holds services/ init/ fat/ device-manager/ system servers → /system/services (fat/ holds
vfs.zig, vfs-test.zig, protocol.zig) fat.zig, engine.zig, on-disk.zig)
library/ → /lib libraries, one sub-directory each library/ → /lib libraries, one sub-directory each
runtime/ the danos-native runtime + file API (fs) — the stable application ABI runtime/ the danos-native runtime + file API (fs) — the stable application ABI
boot/ → /boot the loaders boot/ → /boot the loaders
tools/ test/ host-side build + QEMU test harness tools/ test/ host-side build + QEMU test harness
``` ```
A sub-project exposes its **public interface as a module**: `system/services/vfs/` owns A sub-project exposes its **public interface as a module**: the `usb-xhci-bus` driver
the VFS wire protocol (`protocol.zig`, the `vfs-protocol` module), which the runtime's owns the USB transfer protocol (`usb-transfer-protocol.zig`, the `usb-transfer-protocol`
file API (`runtime.fs`) imports by name. `usb`/`block` drivers expose their protocols the module), which `runtime.usb` imports by name — the USB class drivers reach the
same way. protocol through that wrapper; `block` exposes its protocol (`block-protocol`) the
same way. The VFS wire protocol is the one that outgrew its
sub-project: the VFS root moved into the kernel (`system/kernel/vfs.zig`), so the
protocol lives as a shared contract at `system/vfs-protocol.zig` (the `vfs-protocol`
module), which the runtime's file API (`runtime.fs`) imports by name.
There is **no POSIX/C compatibility layer today**: danos programs do file I/O through the There is **no POSIX/C compatibility layer today**: danos programs do file I/O through the
danos-native `runtime.fs` (open/read/write/list over the VFS). A hand-rolled POSIX shim danos-native `runtime.fs` (open/read/write/list over the VFS). A hand-rolled POSIX shim
@ -254,7 +263,7 @@ exception in [coding-standards.md](coding-standards.md) applies to that seam.
|------|------| |------|------|
| Boot methods (one per way of booting the kernel) | `boot/``efi.zig` (UEFI) → `BOOTX64.efi` | | Boot methods (one per way of booting the kernel) | `boot/``efi.zig` (UEFI) → `BOOTX64.efi` |
| Kernel entry, panic, bring-up | `system/kernel/kernel.zig` | | Kernel entry, panic, bring-up | `system/kernel/kernel.zig` |
| Loader↔kernel handoff (`BootInfo`, `Framebuffer`, `MemoryMap`, VM layout) | `system/boot-handoff.zig` | | Loader↔kernel handoff (`BootInformation`, `Framebuffer`, `MemoryMap`, VM layout) | `system/boot-handoff.zig` |
| Private kernel↔runtime syscall ABI (`SystemCall`, mmap prot flags, `page_size`) — the runtime speaks it, not apps | `system/abi.zig` | | Private kernel↔runtime syscall ABI (`SystemCall`, mmap prot flags, `page_size`) — the runtime speaks it, not apps | `system/abi.zig` |
| Device wire types (`DeviceDescriptor`, `DeviceClass`, …) | `system/devices/device-abi.zig` | | Device wire types (`DeviceDescriptor`, `DeviceClass`, …) | `system/devices/device-abi.zig` |
| Physical frame allocator | `system/kernel/pmm.zig` | | Physical frame allocator | `system/kernel/pmm.zig` |
@ -264,6 +273,7 @@ exception in [coding-standards.md](coding-standards.md) applies to that seam.
| IPC channels between kernel threads (message passing) | `system/kernel/ipc.zig` | | IPC channels between kernel threads (message passing) | `system/kernel/ipc.zig` |
| IPC endpoints: cross-address-space call/reply, handles, notifications | `system/kernel/ipc-synchronous.zig` | | IPC endpoints: cross-address-space call/reply, handles, notifications | `system/kernel/ipc-synchronous.zig` |
| User processes: ELF loading, address spaces, the syscall table | `system/kernel/process.zig` | | User processes: ELF loading, address spaces, the syscall table | `system/kernel/process.zig` |
| VFS root: mount table + kernel-served nodes (`fs_resolve`/`fs_node`); wire protocol in `system/vfs-protocol.zig` | `system/kernel/vfs.zig` |
| Device tree + claim capability + `device_register` containment | `system/kernel/devices-broker.zig` | | Device tree + claim capability + `device_register` containment | `system/kernel/devices-broker.zig` |
| IRQ-as-IPC: routing a device interrupt to a driver's endpoint | `system/kernel/irq.zig` | | IRQ-as-IPC: routing a device interrupt to a driver's endpoint | `system/kernel/irq.zig` |
| Hardware discovery (ACPI/device tree) behind one neutral device model | `system/devices/` | | Hardware discovery (ACPI/device tree) behind one neutral device model | `system/devices/` |
@ -271,7 +281,7 @@ exception in [coding-standards.md](coding-standards.md) applies to that seam.
| In-kernel test cases | `system/kernel/tests.zig` | | In-kernel test cases | `system/kernel/tests.zig` |
| Arch-specific kernel code (`halt`, GDT/IDT/TSS, exception + interrupt stubs, page tables, APIC/IO-APIC/timer, serial, linker script) | `system/kernel/architecture/x86_64/` | | Arch-specific kernel code (`halt`, GDT/IDT/TSS, exception + interrupt stubs, page tables, APIC/IO-APIC/timer, serial, linker script) | `system/kernel/architecture/x86_64/` |
| danos-native runtime (`runtime`): syscall wrappers, heap, IPC, device access, the file API (`fs`) — the stable application ABI | `library/runtime/` | | danos-native runtime (`runtime`): syscall wrappers, heap, IPC, device access, the file API (`fs`) — the stable application ABI | `library/runtime/` |
| System services (init, the VFS server + `protocol`, the device-manager) | `system/services/` | | System services (init, the `fat` filesystem, the device-manager) | `system/services/` |
| Device drivers, one sub-project each (`pci-bus`, `ps2-bus`, `usb-xhci-bus` bus drivers) | `system/drivers/` | | Device drivers, one sub-project each (`pci-bus`, `ps2-bus`, `usb-xhci-bus` bus drivers) | `system/drivers/` |
| Build + `run-x86-64` (QEMU/OVMF) + `release-x86-64` (the flashable ISO) | `build.zig` | | Build + `run-x86-64` (QEMU/OVMF) + `release-x86-64` (the flashable ISO) | `build.zig` |
| QEMU integration test harness | `test/qemu_test.py` | | QEMU integration test harness | `test/qemu_test.py` |

View File

@ -16,11 +16,11 @@ the RSDT's address is a field *inside* the RSDP. The platform follows that point
UEFI configuration table UEFI configuration table
│ the loader reads the RSDP's physical address │ the loader reads the RSDP's physical address
BootInfo.acpi_rsdp (u64, in the loader↔kernel handoff) system/boot-handoff.zig BootInformation.acpi_rsdp (u64, in the loader↔kernel handoff) system/boot-handoff.zig
│ the kernel forwards the whole BootInfo │ the kernel forwards the whole BootInformation
platform.discover(boot_info, …) system/devices/platform.zig platform.discover(boot_information, …) system/devices/platform.zig
│ reads boot_info.acpi_rsdp, hands it to the ACPI backend │ reads boot_information.acpi_rsdp, hands it to the ACPI backend
acpi.discover(rsdp_phys, …) system/devices/acpi.zig acpi.discover(rsdp_phys, …) system/devices/acpi.zig
│ dereferences the RSDP, reads the pointer it contains │ dereferences the RSDP, reads the pointer it contains
@ -40,11 +40,12 @@ still up. `acpiRootSystemDescriptorPointer()` in `boot/efi.zig` walks the UEFI
"grab it before `ExitBootServices`" pattern as the [framebuffer](framebuffer.md) and "grab it before `ExitBootServices`" pattern as the [framebuffer](framebuffer.md) and
the [memory map](memory-map.md). the [memory map](memory-map.md).
## Step 2 — the handoff: a physical address in `BootInfo` ## Step 2 — the handoff: a physical address in `BootInformation`
The loader can't just call the device module: the bootloader binary and the kernel The loader can't just call the device module: the bootloader binary and the kernel
binary are compiled separately, and **the loader isn't linked against the `platform` binary are compiled separately, and **the loader isn't linked against the `platform`
module at all** (it imports only the `boot-handoff` contract). So instead of a call, it module at all** (it imports only the `boot-handoff` contract and the
`initial-ramdisk` module). So instead of a call, it
deposits a value in the handoff struct: deposits a value in the handoff struct:
```zig ```zig
@ -56,13 +57,14 @@ Two things about what crosses the boundary:
- **It's a *physical* address, not a Zig pointer.** The loader and kernel don't share - **It's a *physical* address, not a Zig pointer.** The loader and kernel don't share
an address space at the moment of the jump, so a raw `u64` physical address is the an address space at the moment of the jump, so a raw `u64` physical address is the
only thing that survives the handoff. `BootInfo.acpi_rsdp` is `0` when the firmware only thing that survives the handoff. `BootInformation.acpi_rsdp` is `0` when the firmware
exposed no ACPI (e.g. a future device-tree machine, which would fill a different exposed no ACPI (e.g. a future device-tree machine, which would fill a different
field instead — the kernel never learns which firmware booted it). field instead — the kernel never learns which firmware booted it).
- **The kernel can dereference it because it identity-maps ACPI memory.** The RSDP - **The kernel can dereference it through the physmap.** The RSDP lives in
lives in ACPI-reclaim memory, which [paging.zig](paging.md) identity-maps along with ACPI-reclaim memory, which [paging.zig](paging.md) maps — along with the rest of
the rest of RAM, so by the time discovery runs `@ptrFromInt(rsdp_phys)` is a valid RAM — into the higher-half **physmap** (there is no identity mapping; the low half
pointer. belongs to user space). So by the time discovery runs,
`@ptrFromInt(physicalToVirtual(rsdp_phys))` is a valid pointer.
This is the concrete form of the "capture the description pointer" step sketched in This is the concrete form of the "capture the description pointer" step sketched in
[discovery.md](discovery.md) — a plain `acpi_rsdp: u64` rather than a tagged handle, [discovery.md](discovery.md) — a plain `acpi_rsdp: u64` rather than a tagged handle,
@ -75,13 +77,13 @@ and then reads the root-table pointer *out of it*. Which pointer depends on the
version, because the RSDP carries **both**: version, because the RSDP carries **both**:
```zig ```zig
const rsdp: *const RootSystemDescriptionPointer = @ptrFromInt(rsdp_phys); const rsdp: *const RootSystemDescriptionPointer = @ptrFromInt(physicalToVirtual(rsdp_phys));
if (!std.mem.eql(u8, &rsdp.signature, "RSD PTR ")) return error.BadRsdpSignature; if (!std.mem.eql(u8, &rsdp.signature, "RSD PTR ")) return error.BadRsdpSignature;
if (!checksumOk(@ptrFromInt(rsdp_phys), 20)) return error.BadRsdpChecksum; if (!checksumOk(@ptrFromInt(physicalToVirtual(rsdp_phys)), 20)) return error.BadRsdpChecksum;
if (rsdp.revision >= 2) { if (rsdp.revision >= 2) {
// ACPI 2.0+: use the 64-bit XSDT pointer (the 32-bit RSDT is deprecated) // ACPI 2.0+: use the 64-bit XSDT pointer (the 32-bit RSDT is deprecated)
const xsdp: *const ExtendedSystemDescriptorPointer = @ptrFromInt(rsdp_phys); const xsdp: *const ExtendedSystemDescriptorPointer = @ptrFromInt(physicalToVirtual(rsdp_phys));
try walkRoot(u64, xsdp.extended_system_descriptor_table_address, …); try walkRoot(u64, xsdp.extended_system_descriptor_table_address, …);
} else { } else {
// ACPI 1.0: use the 32-bit RSDT pointer // ACPI 1.0: use the 32-bit RSDT pointer
@ -120,13 +122,15 @@ namespace and the port grant.
**The kernel hands the service what it needs and no more.** Reading PM1 event **The kernel hands the service what it needs and no more.** Reading PM1 event
blocks and GPE blocks requires the FADT, which the kernel already parses for its blocks and GPE blocks requires the FADT, which the kernel already parses for its
own `\_S5` poweroff. Rather than re-parse, the kernel appends the **FADT as one own power register map (feeding reboot), the PM timer, and the SCI line — the
kernel itself has no S5/poweroff path. Rather than re-parse, the kernel appends the **FADT as one
more memory resource** on the `acpi-tables` node; the service tells it apart more memory resource** on the `acpi-tables` node; the service tells it apart
from the AML blob resources by signature — the FADT keeps its intact `"FACP"` from the AML blob resources by signature — the FADT keeps its intact `"FACP"`
header, while the blob resources are header-stripped bytecode that starts with header, while the blob resources are header-stripped bytecode that starts with
no signature. The kernel's own FADT parse is untouched; the service reads the no signature. The kernel's own FADT parse is untouched; the service reads the
PM1 *event* blocks (which the kernel never parsed — it only needs PM1 *control* PM1 *event* blocks (which the kernel never parsed — it extracts only the PM1
for `\_S5`) and the GPE0/GPE1 blocks straight from its copy. The **SCI itself** *control* register, and it is the service, not the kernel, that writes it for
`\_S5`) and the GPE0/GPE1 blocks straight from its copy. The **SCI itself**
arrives as the node's one `len == 1` irq resource (distinct from the broad arrives as the node's one `len == 1` irq resource (distinct from the broad
`[0, 256)` window that covers children's legacy lines), which is how the service `[0, 256)` window that covers children's legacy lines), which is how the service
finds the line to `irq_bind`. finds the line to `irq_bind`.
@ -141,8 +145,9 @@ some firmwares boot with it already set), sets `PWRBTN_EN`, and on each SCI:
service evaluates its `\_GPE._L%02X` (level) or `_E%02X` (edge) handler service evaluates its `\_GPE._L%02X` (level) or `_E%02X` (edge) handler
method, drains the **Notify** queue that method produced, maps each notified method, drains the **Notify** queue that method produced, maps each notified
device to an event (battery, AC, lid, or a generic `notify` with its code), device to an event (battery, AC, lid, or a generic `notify` with its code),
and clears the status bit. A missing handler method is clear-and-log, not an and clears the status bit. A missing handler method is not an error: the
error. Making GPEs work required teaching the interpreter one opcode it never status bit is cleared and the event silently dropped. Making GPEs work
required teaching the interpreter one opcode it never
handled — `Notify` (`0x86`) — which it now folds into a bounded queue drained handled — `Notify` (`0x86`) — which it now folds into a bounded queue drained
per evaluation; everything else a handler needs (field access, control flow, per evaluation; everything else a handler needs (field access, control flow,
method calls) was already proven by the ring-3 `_STA`/`_CRS` work. method calls) was already proven by the ring-3 `_STA`/`_CRS` work.
@ -152,8 +157,9 @@ so GPE/Notify correctness is proven by **host unit tests** — hand-encoded AML
with a `Notify` inside a method body, run under `zig build test`. The QEMU with a `Notify` inside a method body, run under `zig build test`. The QEMU
`power-button` scenario proves the fixed-event path end to end: a QMP `power-button` scenario proves the fixed-event path end to end: a QMP
`system_powerdown` injects a real ACPI power-button press, and the service's SCI `system_powerdown` injects a real ACPI power-button press, and the service's SCI
handler must log it. Battery/AC/lid and the embedded controller's `_Qxx` queries handler must log it. Battery/AC/lid mapping is interface-complete but validated
are interface-complete but validated on real hardware later. on real hardware later; the embedded controller's `_Qxx` queries are out of
scope.
The service surface these events are *published on* — subscription, the event The service surface these events are *published on* — subscription, the event
vocabulary, and orderly shutdown — is the power service, [power.md](power.md). vocabulary, and orderly shutdown — is the power service, [power.md](power.md).

View File

@ -61,7 +61,7 @@ There are really two independent questions, and it's worth not conflating them:
`BOOTX64.efi`, a distinct executable from the kernel ELF. On a Pi there is no `BOOTX64.efi`, a distinct executable from the kernel ELF. On a Pi there is no
separate loader at all — the firmware jumps straight into the kernel with a separate loader at all — the firmware jumps straight into the kernel with a
device-tree pointer, so that entry work would live in the AArch64 architecture code. device-tree pointer, so that entry work would live in the AArch64 architecture code.
Either path converges on the same neutral [`BootInfo`](memory-map.md). Either path converges on the same neutral [`BootInformation`](memory-map.md).
## Current x86_64 contents ## Current x86_64 contents

View File

@ -18,7 +18,7 @@ matters for understanding why. This page maps the landscape so the
new ISA. new ISA.
They are as different from each other as either is from x86-64: separate registers, They are as different from each other as either is from x86-64: separate registers,
page-table formats, and calling conventions. Each needs its own `system/kernel/arch/<name>/`. page-table formats, and calling conventions. Each needs its own `system/kernel/architecture/<name>/`.
## The Raspberry Pi models ## The Raspberry Pi models
@ -57,9 +57,9 @@ the DTB/ACPI tells you what devices exist.
## What danos needs, layer by layer ## What danos needs, layer by layer
- **One CPU arch module: `system/kernel/arch/aarch64/`** — covering the Zero 2 W and Pi 3-5, - **One CPU arch module: `system/kernel/architecture/aarch64/`** — covering the Zero 2 W and Pi 3-5,
providing the same `arch` interface as x86_64: `halt`, context switch, providing the same `arch` interface as x86_64: `halt`, context switch,
interrupt/exception vectors, page tables, a UART, a timer. No `system/kernel/arch/arm/` is interrupt/exception vectors, page tables, a UART, a timer. No `system/kernel/architecture/arm/` is
planned (see the decision above), so there's a single ARM backend to write. planned (see the decision above), so there's a single ARM backend to write.
- **A device-tree boot path.** Since stock Pis boot via DTB, danos needs an entry - **A device-tree boot path.** Since stock Pis boot via DTB, danos needs an entry
that parses the DTB's `/memory` and `/reserved-memory` into the neutral that parses the DTB's `/memory` and `/reserved-memory` into the neutral
@ -67,11 +67,16 @@ the DTB/ACPI tells you what devices exist.
from a different source. This is where keeping boot-protocol knowledge on the from a different source. This is where keeping boot-protocol knowledge on the
loader side (as we did for the UEFI memory-map classification) pays off. loader side (as we did for the UEFI memory-map classification) pays off.
- **The UEFI loader mostly carries over.** `boot/efi.zig` is largely - **The UEFI loader mostly carries over.** `boot/efi.zig` is largely
boot-*protocol* code (`std.os.uefi` protocol calls), not x86 code. Its only truly boot-*protocol* code (`std.os.uefi` protocol calls), not x86 code. Its truly
x86-specific bits are the ELF machine check (`.X86_64`) and the SysV calling x86-specific bits are the ELF machine check (`.X86_64`), the SysV calling
convention for the kernel jump. So an `aarch64`-UEFI target (QEMU `virt` + AAVMF) convention for the kernel jump, the `hlt` park on failure — and, the substantial
can reuse it — which makes **aarch64-UEFI the easiest second target**, easier than one, the bootstrap page tables: `buildBootstrapTables` builds x86-64 4-level
the device-tree Pi. tables (PML4/PDPT/PD index shifts, x86 PTE bits, 2 MiB leaves) and hands the
kernel a CR3. Page-table formats are per-architecture (see above), so an
`aarch64` loader keeps the protocol code but rewrites that builder in the
aarch64 translation-table format. Even so, an `aarch64`-UEFI target (QEMU
`virt` + AAVMF) reuses most of it — which makes **aarch64-UEFI the easiest
second target**, easier than the device-tree Pi.
## Pi hardware quirks (for when we port) ## Pi hardware quirks (for when we port)

View File

@ -5,8 +5,8 @@ Conventions for danos source. The overriding one, from which most of the rest fo
> **Names are spelled out in full. An identifier is not abbreviated unless the > **Names are spelled out in full. An identifier is not abbreviated unless the
> abbreviation is an acronym.** > abbreviation is an acronym.**
`interruptDispatch`, not `intDisp`. `message_len`, not `message_len` (`msg` expands, `len` `interruptDispatch`, not `intDisp`. `message_len`, not `msg_len` (`msg` expands, `len`
is a Zig idiom — see the exceptions). `devices_broker`, not `devices_broker`. `scheduler`, not is a Zig idiom — see the exceptions). `devices_broker`, not `dev_broker`. `scheduler`, not
`sched`. The cost of a longer name is paid once, at the keyboard; the cost of a `sched`. The cost of a longer name is paid once, at the keyboard; the cost of a
cryptic one is paid every time the code is read, by everyone who reads it. In a cryptic one is paid every time the code is read, by everyone who reads it. In a
microkernel whose whole argument is that a human can hold each piece in their head, microkernel whose whole argument is that a human can hold each piece in their head,
@ -98,8 +98,8 @@ Three, and only three.
That's all — no Unix-abbreviation exception. The source directories are full words That's all — no Unix-abbreviation exception. The source directories are full words
(`system`, `library`, not `src`/`lib`), and there is no daemon `d` suffix: a driver (`system`, `library`, not `src`/`lib`), and there is no daemon `d` suffix: a driver
lives in `system/drivers/` and a service in `system/services/`, so the *location* lives in `system/drivers/` and a service in `system/services/`, so the *location*
already says what it is. Encoding the role in the name too (`busd`, `vfsd`) is already says what it is. Encoding the role in the name too (`busd`, `fatd`) is
redundant — the program is just `ps2-bus`, `vfs`. Don't put in a name what its directory redundant — the program is just `ps2-bus`, `fat`. Don't put in a name what its directory
already tells you. already tells you.
## A note on collisions ## A note on collisions

View File

@ -1,6 +1,6 @@
# DanOS Filesystem Hierarchy Standard (DFHS) # DanOS Filesystem Hierarchy Standard (DFHS)
Most modern Unix and Unix-like operating systems follow the FHS. DanOS has its own FHS structure which extends the unix FHS. This is provided by virtual file system driver (VFS). Most modern Unix and Unix-like operating systems follow the FHS. DanOS has its own FHS structure which extends the unix FHS. Root path resolution is provided by the kernel-resident VFS root (`fs_resolve`, `system/kernel/vfs.zig`); mounted filesystem servers serve the subtrees they own.
## Directory structure ## Directory structure
@ -18,7 +18,7 @@ Most modern Unix and Unix-like operating systems follow the FHS. DanOS has its o
| /system | DanOS operating system files (similar idea to C:\Windows). A true representation of danos — its layout mirrors the source tree, so `/system` is what danos *is*. | | /system | DanOS operating system files (similar idea to C:\Windows). A true representation of danos — its layout mirrors the source tree, so `/system` is what danos *is*. |
| /system/devices | danos virtual device tree e.g. similar to /sys on linux but with danos device tree conventions (the structures in the devices module) | | /system/devices | danos virtual device tree e.g. similar to /sys on linux but with danos device tree conventions (the structures in the devices module) |
| /system/drivers | driver binaries, one sub-project each (e.g. /system/drivers/pci-bus, /system/drivers/ps2-bus) | | /system/drivers | driver binaries, one sub-project each (e.g. /system/drivers/pci-bus, /system/drivers/ps2-bus) |
| /system/services | system-service binaries — the VFS server, init, and other user-mode servers (e.g. /system/services/vfs, /system/services/init) | | /system/services | system-service binaries — init, the FAT server, and other user-mode servers (e.g. /system/services/init, /system/services/fat) |
| /system/kernel | the kernel image | | /system/kernel | the kernel image |
| /tmp | Directory for temporary files (see also /var/tmp). Often not preserved between system reboots and may be severely size-restricted. | | /tmp | Directory for temporary files (see also /var/tmp). Often not preserved between system reboots and may be severely size-restricted. |
| /usr | Secondary hierarchy for read-only user data; contains the majority of (multi-)user utilities and applications. Should be shareable and read-only. | | /usr | Secondary hierarchy for read-only user data; contains the majority of (multi-)user utilities and applications. Should be shareable and read-only. |
@ -48,10 +48,13 @@ willing to serve them, addressed by name.
A device node is not a file the VFS can read. The bytes live in a driver process A device node is not a file the VFS can read. The bytes live in a driver process
([drivers.md](drivers.md)), so opening a `/dev` name has to resolve to that driver's ([drivers.md](drivers.md)), so opening a `/dev` name has to resolve to that driver's
IPC endpoint, and subsequent reads and writes are calls against it. This is what IPC endpoint, and subsequent reads and writes are calls against it. Resolve-to-endpoint
`system/services/vfs/vfs.zig` reserves for M10 and what the `Stat.kind` field is for; **none of it is is exactly what the kernel's `fs_resolve` already does for any mounted backend, and
implemented today.** The current VFS is a flat, in-memory ramfs of eight nodes, with no `FileStatus.kind` is the field that marks a device node; **what is not implemented today
directories at all and `kind` hardcoded to zero. The three sections below describe the is `/dev` itself** — no service mounts it. (The flat eight-node ramfs this section once
described is retired: the kernel-resident VFS root in `system/kernel/vfs.zig` serves the
read-only `/system` initrd mount with real directories and node kinds, and filesystem
backends such as the FAT server mount the rest.) The three sections below describe the
intended shape, and are honest about which parts the kernel can already support. intended shape, and are honest about which parts the kernel can already support.
### Character devices ### Character devices
@ -107,12 +110,13 @@ yielding unpredictable bytes.
These are the only `/dev` entries danos can implement immediately, and they are the These are the only `/dev` entries danos can implement immediately, and they are the
sensible place to start, because they are exactly the entries that need no driver sensible place to start, because they are exactly the entries that need no driver
process, no `device_claim`, no MMIO grant and no interrupt. The VFS server answers them process, no `device_claim`, no MMIO grant and no interrupt. A future pseudo-device
out of its own address space — `null` and `zero` are a few lines each in service would answer them out of its own address space — `null` and `zero` are a few
`system/services/vfs/vfs.zig`'s `read` and `write` handlers. Doing so forces the two pieces of lines each in its `read` and `write` handlers — and mount itself at `/dev` the way the
structure that every later device node depends on and that the flat ramfs currently FAT server mounts `/mnt/usb`. The two pieces of structure every later device node
lacks: a directory, so that `/dev/null` is a path rather than a name; and a populated depends on (and that the flat ramfs of the time lacked) exist now: directories, so that
`Stat.kind`, so that a caller can tell a character device from a regular file. `/dev/null` is a path rather than a name; and a populated `FileStatus.kind`, so that a
caller can tell a character device from a regular file.
`/dev/random` is the one that is not free. It needs an entropy source, and the honest `/dev/random` is the one that is not free. It needs an entropy source, and the honest
options on this kernel are `RDRAND`/`RDSEED` where CPUID advertises them, and the HPET options on this kernel are `RDRAND`/`RDSEED` where CPUID advertises them, and the HPET

View File

@ -87,7 +87,9 @@ because they decide whether we read time with a cheap `rdtsc` or fall back to th
frequency scaling — useless as wall time. Modern CPUs (all of danos's targets) provide an frequency scaling — useless as wall time. Modern CPUs (all of danos's targets) provide an
**invariant TSC**: a constant rate across P/C-states that never stops. The guarantee is a **invariant TSC**: a constant rate across P/C-states that never stops. The guarantee is a
CPUID bit — leaf `0x80000007`, EDX bit 8 — on both Intel *and* AMD. danos reads it in CPUID bit — leaf `0x80000007`, EDX bit 8 — on both Intel *and* AMD. danos reads it in
`calibrate`, and a TSC that doesn't advertise it is not used as the clocksource. AMD is `calibrate`, and a TSC that doesn't advertise it is demoted to the HPET clocksource —
provided a usable HPET exists (64-bit; a 32-bit one wraps too fast to stay monotonic).
With no such fallback the TSC stays, there being nothing steadier to switch to. AMD is
why this matters in practice: it doesn't populate the Intel leaf `0x15` that enumerates why this matters in practice: it doesn't populate the Intel leaf `0x15` that enumerates
the TSC *frequency*, so danos already measures AMD's rate against the HPET — but a the TSC *frequency*, so danos already measures AMD's rate against the HPET — but a
measured frequency without the invariance guarantee is not enough. measured frequency without the invariance guarantee is not enough.
@ -127,6 +129,10 @@ if (state.vector < 32) {
// else: spurious/unhandled — deliberately no EOI // else: spurious/unhandled — deliberately no EOI
``` ```
(A third branch has since joined for user mode, elided here:
`state.vector == system_call_vector` (128) hands the trap frame to the ring-3
syscall handler.)
Two things make device interrupts *return* where exceptions don't: Two things make device interrupts *return* where exceptions don't:
1. **The handler returns.** The timer handler just bumps a tick counter. Control 1. **The handler returns.** The timer handler just bumps a tick counter. Control
@ -151,8 +157,10 @@ needs, so only the handler can sequence it. See [drivers.md](drivers.md), where
device is quieted by a driver in ring 3, long after the ISR has returned. device is quieted by a driver in ring 3, long after the ISR has returned.
A device handler is a plain `fn () void` — a timer or keyboard handler doesn't need A device handler is a plain `fn () void` — a timer or keyboard handler doesn't need
the interrupted registers. (Note: the stubs don't save the SSE/vector registers, so the interrupted registers. (The stubs originally didn't save the SSE/vector
a handler must not use them; ours don't.) registers, so a handler couldn't use them; `isr_common` now does an
`fxsave`/`fxrstor` of the full SSE/x87 state around dispatch — see
[interrupts.md](interrupts.md).)
## Turning them on ## Turning them on

View File

@ -73,8 +73,8 @@ one world.
| Direction | Message | Purpose | | Direction | Message | Purpose |
|---|---|---| |---|---|---|
| driver → manager | `hello { version, role, device_id }` | confirms the argv assignment, starts the deadline clock | | 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_added { parent, bus_address, identity, device_id, hid }` | one node the bus discovered |
| bus → manager | `child_removed { id }` | unplug, or the bus lost it | | bus → manager | `child_removed { parent, bus_address }` | unplug, or the bus lost it |
| app → manager | `enumerate` | snapshot of the tree (read-only) | | app → manager | `enumerate` | snapshot of the tree (read-only) |
| app → manager | `subscribe` | receive published add/remove events | | app → manager | `subscribe` | receive published add/remove events |
@ -147,8 +147,10 @@ published exit events, signals + `runtime.process`). On top of those:
to a manager-internal seam. to a manager-internal seam.
8. **Discovery migration** — DONE (M19M20, 2026-07-13): enumeration moved to 8. **Discovery migration** — DONE (M19M20, 2026-07-13): enumeration moved to
ring 3 as swappable per-firmware discoverers — the pci-bus driver (M19) then 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 the acpi service (M20), see [discovery.md](discovery.md); of the enumerable
only the host bridge and the acpi-tables node. Matching moved with it: 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 `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 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 matches drivers from those **reports** rather than its boot-time snapshot. The
@ -167,6 +169,9 @@ published exit events, signals + `runtime.process`). On top of those:
- **Manager death**: drivers survive the manager; the restarted manager re-learns - **Manager death**: drivers survive the manager; the restarted manager re-learns
the world (above). Checkpointing driver state with the manager is deferred until the world (above). Checkpointing driver state with the manager is deferred until
something demonstrates the need. something demonstrates the need.
- **Matching stays code until the third bus.** `driverFor`/`pciDriverFor` are - **Matching stays code until the third bus.** `driverFor`/`pciDriverFor` were
honest at two bus types; the third triggers the manifest (a driver declares what honest at two bus types; the third was expected to trigger the manifest (a driver
it binds: a PCI class triple, a USB class triple, an ACPI `_HID`). declares what it binds: a PCI class triple, a USB class triple, an ACPI `_HID`).
(Since then: the third bus — USB — arrived and is matched in code too. Today's
matchers are `pciDriverForIdentity`, `hidDriverFor`, and `usbDriverForIdentity`;
the manifest waits until code matching actually hurts.)

View File

@ -7,19 +7,23 @@ tree** (DTB). This note is a design plan, not built yet: *when* danos should tac
it, and *how* to keep it architecture-agnostic — the same discipline the it, and *how* to keep it architecture-agnostic — the same discipline the
[memory map](memory-map.md) and [architecture split](architecture.md) already follow. [memory map](memory-map.md) and [architecture split](architecture.md) already follow.
## What the kernel assumes today ## What the kernel assumed when this plan was written
Right now danos discovers almost nothing — it coasts on legacy PC fixtures that are At the time danos discovered almost nothing — it coasted on legacy PC fixtures that
guaranteed to exist under QEMU + UEFI: are guaranteed to exist under QEMU + UEFI:
- `arch/x86_64/apic.zig` assumes the **Local APIC** at the default `0xFEE0_0000` and - `system/kernel/architecture/x86_64/apic.zig` assumed the **Local APIC** at the
calibrates its timer against the **PIT** (the legacy 8254). default `0xFEE0_0000` and calibrated its timer against the **PIT** (the legacy
- `arch/x86_64/serial.zig` hardcodes **COM1** at I/O port `0x3F8`. 8254). (Today the PIT is the *last-resort* reference: calibration prefers the
CPUID-reported TSC frequency, then the HPET, then the ACPI PM timer.)
- `system/kernel/architecture/x86_64/serial.zig` hardcoded **COM1** at I/O port
`0x3F8`. (Today `0x3F8` is only the default: the kernel loopback-probes the UART
and parses ACPI's SPCR table to target the firmware's actual debug port.)
- The framebuffer and memory map come from **UEFI** — that *is* discovery, just done - The framebuffer and memory map come from **UEFI** — that *is* discovery, just done
by the firmware and handed over, not read from ACPI. by the firmware and handed over, not read from ACPI.
This works only because PC-compatible hardware promises those legacy pieces exist at This worked only because PC-compatible hardware promises those legacy pieces exist at
those addresses. It is a crutch, and it does not travel. those addresses. It was a crutch, and it did not travel.
## The forcing functions: when to build it ## The forcing functions: when to build it
@ -53,8 +57,8 @@ it isn't really agnostic.
## The one cheap step to take sooner ## The one cheap step to take sooner
Have the **loader capture the description pointer** into `BootInfo` — a neutral Have the **loader capture the description pointer** into `BootInformation` — a
handle, no parsing: neutral handle, no parsing:
```zig ```zig
pub const HardwareInfo = extern struct { pub const HardwareInfo = extern struct {
@ -146,7 +150,7 @@ free; discovery on x86 is partly about *finding* what ARM just tells you.
## Suggested ordering ## Suggested ordering
1. **Now (cheap):** plumb the neutral `HardwareInfo` pointer through the loader into 1. **Now (cheap):** plumb the neutral `HardwareInfo` pointer through the loader into
`BootInfo`. No parser yet. `BootInformation`. No parser yet.
2. **Next milestone unchanged:** user mode + address-space isolation — needs no 2. **Next milestone unchanged:** user mode + address-space isolation — needs no
discovery. discovery.
3. **With the aarch64 port:** build the agnostic discovery layer, **DTB first** (the 3. **With the aarch64 port:** build the agnostic discovery layer, **DTB first** (the
@ -182,15 +186,20 @@ the static tables (MADT, HPET, MCFG, FADT + `\\_S5`) stay kernel-side.
The kernel no longer folds the AML namespace's Device objects into the device The kernel no longer folds the AML namespace's Device objects into the device
tree. It still parses the *static* tables (MADT for SMP, HPET for the tick, MCFG tree. It still parses the *static* tables (MADT for SMP, HPET for the tick, MCFG
for the host bridge, FADT) and still builds the AML namespace — but only to read for the host bridge, FADT); at this point it also still built the AML namespace —
the `\\_S5` sleep type for poweroff. Device discovery is the ring-3 **acpi but only to read the `\\_S5` sleep type for poweroff. (That remnant is gone too:
the kernel now runs no AML at all — soft-off belongs to the acpi service, and the
kernel keeps only the AML-free reboot path.) Device discovery is the ring-3 **acpi
service** ([device-manager.md](device-manager.md)): it claims the `acpi-tables` service** ([device-manager.md](device-manager.md)): it claims the `acpi-tables`
node the kernel publishes (the AML blobs, a broad io_port grant, the SCI), node the kernel publishes (the AML blobs, a broad io_port grant, the SCI),
re-parses the same blobs with the shared AML module, evaluates `_STA`/`_CRS`, re-parses the same blobs with the shared AML module, evaluates `_STA`/`_CRS`,
and registers + reports each `_HID` device — the device manager matches drivers and registers + reports each `_HID` device — the device manager matches drivers
(ps2-bus) from those reports. With M19's pci-bus driver, discovery now runs (ps2-bus) from those reports. With M19's pci-bus driver, discovery now runs
entirely in user space; the kernel seeds only the host bridge and the entirely in user space, anchored on two kernel-seeded nodes: the host bridge and
acpi-tables node. the acpi-tables node. (The kernel's static-table parse also seeds the processor,
interrupt-controller, and HPET timer nodes, and it publishes the boot
framebuffer as a claimable display node — but no *enumeration* happens in
ring 0.)
## Discovery is a swappable process per firmware (M19M20) ## Discovery is a swappable process per firmware (M19M20)
@ -231,11 +240,14 @@ Two consequences of neutrality bind on later work:
Two supporting decisions keep the kernel's remaining slice honest: Two supporting decisions keep the kernel's remaining slice honest:
- **The AML interpreter is a shared build module**, compiled into both the - **The AML interpreter is a single build module**
kernel and the acpi service — one source, two builds, no fork. The kernel (`system/devices/aml/aml.zig`) — one source, no fork. During the ring-3 move
links it for the `\_S5` poweroff evaluation, the service links it for it was compiled into both the kernel (which linked it just for the `\_S5`
everything else, and the `acpi-parse` test asserts the two produce the same poweroff evaluation) and the acpi service, with the `acpi-parse` test
device count across the ring-3 move. asserting the two produce the same device count. Since soft-off followed
discovery out of the kernel, only the acpi service links the module — the
kernel runs no AML — and the test now asserts a device-count *floor* for the
ring-3 parse instead, there being no kernel count left to equal.
- **Bridge apertures come from the firmware memory map, not AML.** Registered - **Bridge apertures come from the firmware memory map, not AML.** Registered
PCI functions carry BAR resources, and `device_register` containment demands PCI functions carry BAR resources, and `device_register` containment demands
the bridge own windows that cover them. Those apertures are derived the bridge own windows that cover them. Those apertures are derived

View File

@ -7,7 +7,8 @@ Read [display.md](display.md) first for the *why*; this is the *what* and the *o
## Locked decisions (do not relitigate) ## Locked decisions (do not relitigate)
- **Handoff = device node + write-combining `mmio_map`.** The kernel seeds a synthetic - **Handoff = device node + write-combining `mmio_map`.** The kernel seeds a synthetic
`display0` node from `BootInformation.framebuffer`; the service claims + WC-maps it. display node (found by class, not name) from `BootInformation.framebuffer`; the
service claims + WC-maps it.
(Not a bespoke `framebuffer_map` syscall — the device route inherits ownership, (Not a bespoke `framebuffer_map` syscall — the device route inherits ownership,
release-on-death, and re-claim-on-restart.) release-on-death, and re-claim-on-restart.)
- **v1 = the full compositor pipeline on the dumb framebuffer.** One `display` service - **v1 = the full compositor pipeline on the dumb framebuffer.** One `display` service

View File

@ -29,7 +29,7 @@ The compositor itself (layers, back buffer, damage) does not change. Only the la
└─ VirtioGpuBackend talks to a virtio-gpu driver process over a `scanout` └─ VirtioGpuBackend talks to a virtio-gpu driver process over a `scanout`
service: present via a shared resource + fenced flush, service: present via a shared resource + fenced flush,
EDID mode list, runtime mode-set. a fixed mode list, runtime mode-set, EDID refresh rate.
``` ```
A **backend** is a small interface the compositor calls: A **backend** is a small interface the compositor calls:
@ -53,10 +53,12 @@ manager brings it up after boot), and because danos is meant to be resilient:
blank screen while drivers load — the exact v1 behaviour. blank screen while drivers load — the exact v1 behaviour.
2. **Upgrade on announce.** When the virtio-gpu driver has claimed its device and set up a 2. **Upgrade on announce.** When the virtio-gpu driver has claimed its device and set up a
scanout, it **announces itself to the display service** (a `push`: the driver looks up scanout, it **announces itself to the display service** (a `push`: the driver looks up
`.display` and sends an *attach-scanout* message carrying its `scanout` endpoint as a `.display` and sends an *attach-scanout* message carrying the shared scanout **surface**
capability). The compositor switches to `VirtioGpuBackend` and re-presents the current as a capability; the compositor maps it and reaches the driver's present/mode channel by
frame full-screen. Push beats polling — the compositor doesn't know a priori which looking up the registered `scanout` service). The compositor switches to
driver, if any, exists, and danos has no service-registration pub/sub. `VirtioGpuBackend` and re-presents the current frame full-screen. Push beats polling —
the compositor doesn't know a priori which driver, if any, exists, and danos has no
service-registration pub/sub.
3. **Native is restartable, not fallback-on-crash.** Once a native driver has reprogrammed 3. **Native is restartable, not fallback-on-crash.** Once a native driver has reprogrammed
the device, the firmware's GOP framebuffer is **stale** — "native → GOP" is not a clean the device, the firmware's GOP framebuffer is **stale** — "native → GOP" is not a clean
fall-back. So a native driver that **crashes** is *restarted* by its supervisor (the fall-back. So a native driver that **crashes** is *restarted* by its supervisor (the
@ -64,9 +66,10 @@ manager brings it up after boot), and because danos is meant to be resilient:
(native → native). The screen freezes on the last frame during the gap — acceptable. (native → native). The screen freezes on the last frame during the gap — acceptable.
4. **GOP is the floor for "no driver was ever there."** On a real GPU (NVIDIA/AMD/Intel) 4. **GOP is the floor for "no driver was ever there."** On a real GPU (NVIDIA/AMD/Intel)
the class-0x03 device matches nothing in the driver table, no `scanout` is ever the class-0x03 device matches nothing in the driver table, no `scanout` is ever
announced, and the compositor stays on GOP forever — no special-casing. Only if a announced, and the compositor stays on GOP forever — no special-casing. If a native
native driver *permanently* gives up (crash-loop cap) does the compositor attempt GOP driver *permanently* gives up (the device manager's crash-loop cap), nothing tells the
again, and even then only if the LFB is still mappable. compositor and there is no path back to GOP — the screen stays frozen on the last
frame. A GOP revert (sensible only while the LFB is still mappable) is not built.
## The shared-memory primitive this needs ## The shared-memory primitive this needs
@ -91,11 +94,15 @@ reference instead of drawing by command). One piece of kernel work, two features
A new ring-3 driver process (the topology v1 anticipated — "split the driver from the A new ring-3 driver process (the topology v1 anticipated — "split the driver from the
compositor when a second backend arrives"). It claims the virtio-gpu PCI function, and: compositor when a second backend arrives"). It claims the virtio-gpu PCI function, and:
- sets up the **virtqueues** (control + cursor) and the device's config space, - sets up the **control virtqueue** (queue 0 — the only queue it uses; no cursor queue)
and the device's config space,
- creates a **2D scanout resource** backed by a shared-memory region, `attach_backing`s it, - creates a **2D scanout resource** backed by a shared-memory region, `attach_backing`s it,
`set_scanout`s it to a CRTC, and `resource_flush`es damaged rectangles, `set_scanout`s it to a CRTC, and on each present transfers and `resource_flush`es the
- reads **EDID** (the `GET_EDID` control command) for the mode list, and `set_scanout` full current-mode rectangle (damage-narrowed flushes are a later refinement),
at a chosen mode for **runtime mode-setting**, - reads **EDID** (the `GET_EDID` control command) to log the monitor's preferred timing
and derive the refresh rate it announces (the compositor's frame-clock seed); the mode
list it offers is a fixed pair — 640×480 and 800×600 — and `set_scanout` at a chosen
mode gives **runtime mode-setting**,
- registers a `scanout` service and announces to the display service. - registers a `scanout` service and announces to the display service.
Its `resource_flush` is the real **present** — and gives a **fenced, tear-free** path a Its `resource_flush` is the real **present** — and gives a **fenced, tear-free** path a
@ -111,8 +118,9 @@ the existing IRQ-as-IPC path) or the compositor's own frame clock.
## What v2 unlocks — and its honest scope ## What v2 unlocks — and its honest scope
Behind the abstraction, a native backend gives runtime **mode-setting** (resolution / Behind the abstraction, a native backend gives runtime **mode-setting** (resolution only —
refresh / bpp), **EDID** enumeration, and **fenced presents**. But only on devices we have a driver the scanout protocol carries neither refresh nor bpp), an **EDID**-derived refresh rate, and
**fenced presents**. But only on devices we have a driver
for — realistically **VMs** (virtio-gpu, and later maybe Bochs DISPI). Real discrete GPUs for — realistically **VMs** (virtio-gpu, and later maybe Bochs DISPI). Real discrete GPUs
need per-vendor KMS-class drivers that aren't getting written, so they **stay on GOP** need per-vendor KMS-class drivers that aren't getting written, so they **stay on GOP**
which is genuinely fine (v1 on the NVIDIA box is smooth). So v2's real value is twofold: which is genuinely fine (v1 on the NVIDIA box is smooth). So v2's real value is twofold:

View File

@ -25,7 +25,7 @@ which one you're holding decides what you can do.
exiting ([gop.md](gop.md)). Once the kernel runs, GOP is **gone** — no `set_mode`, no exiting ([gop.md](gop.md)). Once the kernel runs, GOP is **gone** — no `set_mode`, no
mode list, no EDID. What survives is the frozen snapshot in mode list, no EDID. What survives is the frozen snapshot in
[`BootInformation.framebuffer`](../system/boot-handoff.zig): `{base, width, height, [`BootInformation.framebuffer`](../system/boot-handoff.zig): `{base, width, height,
pitch, format}`, and nothing more. pitch, format, refresh_hz}`, and nothing more.
- **The PCI class-0x03 device is the raw controller** — BARs, config space, registers, - **The PCI class-0x03 device is the raw controller** — BARs, config space, registers,
IO ports. It is what you actually *own* after boot. On QEMU's emulated adapter IO ports. It is what you actually *own* after boot. On QEMU's emulated adapter
@ -69,28 +69,30 @@ rest of the system hasn't had to face:
[syscall](syscall.md). A user-space display service needs a **new mechanism just to [syscall](syscall.md). A user-space display service needs a **new mechanism just to
touch the pixels**. (See "The handoff" below — this is built.) touch the pixels**. (See "The handoff" below — this is built.)
2. **danos has no cross-process shared memory.** The memory syscalls are `mmap` 2. **danos had no cross-process shared memory.** At v1 the memory syscalls were `mmap`
(private, zeroed), `mmio_map` (a *claimed device's* MMIO), and `dma_alloc` (new (private, zeroed), `mmio_map` (a *claimed device's* MMIO), and `dma_alloc` (new
pinned physical). The block driver's "pass a buffer by physical address" trick pinned physical). The block driver's "pass a buffer by physical address" trick
([block/protocol.zig](../system/services/block/protocol.zig)) works *only because its ([block/protocol.zig](../system/services/block/protocol.zig)) works *only because its
consumer is DMA hardware*. A compositor that CPU-reads and blends client layers can't consumer is DMA hardware*. A compositor that CPU-reads and blends client layers can't
use it — it would have to *map* another process's memory, which nothing allows. This use it — it would have to *map* another process's memory, which nothing allowed. v1
is deferred (see "What v1 does not do"), because v1 sidesteps it entirely. sidesteps it entirely (see "What v1 does not do"); v2 has since built the primitive
(`shared_memory_create` / `shared_memory_map` / `shared_memory_physical`
[display-v2.md](display-v2.md)).
## Architecture ## Architecture
``` ```
kernel ── owns the boot framebuffer; bootstrap console only kernel ── owns the boot framebuffer; bootstrap console only
│ seeds a "display0" device node from BootInformation.framebuffer │ seeds a display-class device node from BootInformation.framebuffer
│ (ResourceKind.memory = [base, height*pitch], write-combining hint, │ (ResourceKind.memory = [base, height*pitch], write-combining hint,
│ plus DisplayInfo{width, height, pitch, format}) │ plus DisplayInfo{width, height, pitch, format, refresh_hz})
display service (system/services/display/, ServiceId.display) ← the compositor display service (system/services/display/, ServiceId.display) ← the compositor
│ device.claim(display0) → mmio_map(WRITE-COMBINING) = FRONT buffer (the LFB) │ device.claim(display node) → mmio_map(WRITE-COMBINING) = FRONT buffer (the LFB)
│ mmap(cacheable) a BACK buffer of the same geometry │ mmap(cacheable) a BACK buffer of the same geometry
│ owns: an ordered LAYER STACK + a per-frame DAMAGE list │ owns: an ordered LAYER STACK + a per-frame DAMAGE tracker (rect list or tile grid)
│ loop: composite dirty layers → back buffer → present dirty rects → front │ loop: composite dirty layers → back buffer → present dirty rects → front
│ backend is an INTERNAL interface: {gop-fb} today; {bochs-dispi, virtio-gpu} later │ backend is an INTERNAL interface: {gop-fb} at boot; {virtio-gpu} on hot-attach (v2)
▼ reached by name (ipc_lookup); clients drive it over the display protocol ▼ reached by name (ipc_lookup); clients drive it over the display protocol
┌────────────────────────────────────┬──────────────────────────────────────┐ ┌────────────────────────────────────┬──────────────────────────────────────┐
drawing clients (v1) surface clients (deferred) drawing clients (v1) surface clients (deferred)
@ -121,12 +123,15 @@ release-on-death, and re-claim-on-restart for free (the [resilience](resilience.
story: a crashed display service returns the LFB to the kernel, and its restart story: a crashed display service returns the LFB to the kernel, and its restart
re-claims it). re-claims it).
- The kernel seeds a synthetic **`display0`** node into the - The kernel seeds a synthetic **display-class** node into the
[devices-broker](../system/kernel/devices-broker.zig) at init, from [devices-broker](../system/kernel/devices-broker.zig) at init (`seedDisplay`), from
`BootInformation.framebuffer`: one `ResourceKind.memory` resource spanning `BootInformation.framebuffer`: one `ResourceKind.memory` resource spanning
`[base, height*pitch]`, tagged **write-combining**, plus a small `[base, height*pitch]`, tagged **write-combining**, plus a small
`DisplayInfo{width, height, pitch, format}` (the memory resource says *where* and *how `DisplayInfo{width, height, pitch, format, refresh_hz}` (the memory resource says *where*
big*; `DisplayInfo` says how to *interpret* the bytes). and *how big*; `DisplayInfo` says how to *interpret* the bytes — and `refresh_hz`, the
panel refresh the loader computed from EDID before `ExitBootServices`, seeds the
compositor's frame clock). The node carries no name or index; it is identified purely by
its `display` device class.
- The service `device.claim`s it and `mmio_map`s the resource. The map is - The service `device.claim`s it and `mmio_map`s the resource. The map is
**write-combining**, not the strong-uncacheable that `mmio_map` uses for register **write-combining**, not the strong-uncacheable that `mmio_map` uses for register
MMIO. The kernel already programs a WC PAT slot for its own console MMIO. The kernel already programs a WC PAT slot for its own console
@ -138,8 +143,8 @@ re-claims it).
panic on screen wins. panic on screen wins.
The display service is a **named boot service**: `init` spawns it by name alongside The display service is a **named boot service**: `init` spawns it by name alongside
`vfs`/`input`/`device-manager` ([init.zig](../system/services/init/init.zig)), and it `input`/`device-manager`/`fat` ([init.zig](../system/services/init/init.zig)), and it
self-discovers `display0` with `device.enumerate`. The [device manager](device-manager.md) self-discovers the display node with `device.enumerate` (matching on `DeviceClass.display`). The [device manager](device-manager.md)
matching path (PCI class 0x03 → a driver) is reserved for the future *native* backend, not matching path (PCI class 0x03 → a driver) is reserved for the future *native* backend, not
this singleton synthetic node. this singleton synthetic node.
@ -182,6 +187,12 @@ discovered.
The compositor holds an **ordered stack of layers**. Each layer has a rectangle, a The compositor holds an **ordered stack of layers**. Each layer has a rectangle, a
z-order, a visibility flag, and a surface. Presenting walks the stack bottom-to-top, z-order, a visibility flag, and a surface. Presenting walks the stack bottom-to-top,
painting each dirty layer into the back buffer, then flushes the damage to the front. painting each dirty layer into the back buffer, then flushes the damage to the front.
Damage is tracked by one of two interchangeable trackers behind a compile-time
`damage_mode` A/B switch ([display.zig](../system/services/display/display.zig)): a
free-form dirty-rectangle **list** (tight bounds, heuristic merging) or a fixed 64-px
**tile grid** (exact O(1) merging, tile-quantized repaints) — the grid is the default;
[compositor.zig](../system/services/display/compositor.zig) has both, with the trade-off
discussion.
In v1 the surfaces are **server-owned**, and clients draw into them with a small In v1 the surfaces are **server-owned**, and clients draw into them with a small
immediate-mode command protocol — essentially the model early X used, and enough for a immediate-mode command protocol — essentially the model early X used, and enough for a
@ -258,15 +269,16 @@ both are clean additions behind the interfaces v1 establishes.
- **Client-rendered surfaces (shared memory).** The fast path for a bitmap-heavy app is - **Client-rendered surfaces (shared memory).** The fast path for a bitmap-heavy app is
to render into its *own* buffer and hand the compositor a *reference*, not a stream of to render into its *own* buffer and hand the compositor a *reference*, not a stream of
commands. That needs the missing cross-process shared-memory primitive — best built as commands. That needs a cross-process shared-memory primitive — the natural
the natural generalization of the existing M13 [capability passing](driver-model.md) generalization of the existing M13 [capability passing](driver-model.md)
from *endpoints* to *memory objects* (`shared_memory_create(len) → {cap, virtual_address}`, pass `cap` on from *endpoints* to *memory objects* (`shared_memory_create(len) → {cap, virtual_address}`, pass `cap` on
an `ipc_call`, receiver `shared_memory_map(cap) → virtual_address`). v1 avoids it because server-owned an `ipc_call`, receiver `shared_memory_map(cap) → virtual_address`). v1 avoids it because server-owned
surfaces already prove the whole pipeline. surfaces already prove the whole pipeline; v2 has since built exactly that primitive
([display-v2.md](display-v2.md)) — the client-surface path on top of it is still open.
- **Runtime mode-setting (a native backend).** Detecting the EDID mode list and changing - **Runtime mode-setting (a native backend).** Detecting the EDID mode list and changing
resolution / bpp at runtime needs the raw PCI device. The first native backend is resolution / bpp at runtime needs the raw PCI device. The first native backend — since
Bochs DISPI — the register interface QEMU's `-device VGA` exposes — behind the same built by v2 ([display-v2.md](display-v2.md)) — is virtio-gpu, behind the same
internal backend interface the dumb framebuffer sits behind. Refresh-rate and colour internal backend interface the dumb framebuffer sits behind. Refresh-rate and colour
management (a gamma LUT) are real-GPU-KMS territory, far beyond this. management (a gamma LUT) are real-GPU-KMS territory, far beyond this.

View File

@ -33,7 +33,7 @@ plain bus driver with no controller — a USB hub — is also a real thing.
## The device table is the spine ## The device table is the spine
danos already has the right central structure. `system/kernel/devices-broker.zig` holds a table of danos already has the right central structure. `system/kernel/devices-broker.zig` holds a table of
`DeviceDesc`, each with a parent, a class, and a set of resources. Firmware discovery `DeviceDescriptor`, each with a parent, a class, and a set of resources. Firmware discovery
seeds it ([discovery.md](discovery.md)); `device_register` grows it. seeds it ([discovery.md](discovery.md)); `device_register` grows it.
Three invariants make it a capability system rather than a directory: Three invariants make it a capability system rather than a directory:
@ -67,7 +67,7 @@ const base = dev.mmioMap(bus.id, 0).?; // 2. enumerate it — from the har
const n = ((cap.* >> 8) & 0x1F) + 1; // GENERAL_CAP says how many children const n = ((cap.* >> 8) & 0x1F) + 1; // GENERAL_CAP says how many children
for (0..n) |i| { // 3. publish each child for (0..n) |i| { // 3. publish each child
var child = std.mem.zeroes(dev.DeviceDesc); var child = std.mem.zeroes(dev.DeviceDescriptor);
child.class = @intFromEnum(dev.DeviceClass.timer); child.class = @intFromEnum(dev.DeviceClass.timer);
child.resource_count = 1; child.resource_count = 1;
child.resources[0] = .{ .kind = memory, child.resources[0] = .{ .kind = memory,
@ -96,8 +96,11 @@ A "family" is two modules, not one:
*whatever* published its device. This is the part that makes class drivers portable. *whatever* published its device. This is the part that makes class drivers portable.
danos already has one of each: `library/runtime/device.zig` is a logic module, danos already has one of each: `library/runtime/device.zig` is a logic module,
[`system/services/vfs/protocol.zig`](system/services/vfs/protocol.zig) is a protocol module shared by `system/services/vfs/vfs.zig` [`system/vfs-protocol.zig`](system/vfs-protocol.zig) is a protocol module shared by the
and its clients. The pattern generalises directly: mount backends (today the fat server) and their clients. (The user-space VFS server it
was originally written against has since retired — path routing moved into the kernel,
`system/kernel/vfs.zig`'s `fs_resolve` — but the protocol module outlived it, which is
rather the point.) The pattern generalises directly:
``` ```
library/ library/
@ -107,7 +110,7 @@ library/
pci/ module "pci" — ECAM, BAR decode, capability walk pci/ module "pci" — ECAM, BAR decode, capability walk
usb/ module "usb" — descriptors, control transfers, hubs usb/ module "usb" — descriptors, control transfers, hubs
proto/ proto/
vfs/ module "vfs-protocol" (today: system/services/vfs/protocol.zig) vfs/ module "vfs-protocol" (today: system/vfs-protocol.zig)
block/ module "block-protocol" block/ module "block-protocol"
hid/ module "hid-protocol" hid/ module "hid-protocol"
@ -117,10 +120,11 @@ system/drivers/ one sub-project each → /system/drivers (no `d` suffix)
block/ class driver imports runtime, block-protocol block/ class driver imports runtime, block-protocol
``` ```
The only build change needed: [`addUserBinary`](build.zig) currently takes exactly one The build side of this has since landed: [`addUserBinary`](build.zig) injects the
module (`rt_mod`) and injects it. It should take a slice of modules. That's a default modules (`runtime`, `mmio`, `xkeyboard-config`, `acpi-ids`) into every user
five-line change, and it's the *entire* mechanism — Zig modules already give you binary, and per-binary extras — protocol modules, bus logic — are added with
everything else. `programModule(exe).addImport(...)`. That's the *entire* mechanism — Zig modules
already give you everything else.
The discipline that makes this work: **a class driver must not import a bus's logic The discipline that makes this work: **a class driver must not import a bus's logic
module.** `usbhid` imports `proto.hid` and `usb` (for descriptor types), never `pci`. module.** `usbhid` imports `proto.hid` and `usb` (for descriptor types), never `pci`.
@ -132,20 +136,24 @@ If a class driver needs `mmio`, it has become an HCD and should be one.
grants, `device_grant` teardown. grants, `device_grant` teardown.
- **M11**`irq_bind` / `irq_ack`. IRQ delivered as an IPC notification; mask before - **M11**`irq_bind` / `irq_ack`. IRQ delivered as an IPC notification; mask before
EOI; `irq_ack` is the unmask. EOI; `irq_ack` is the unmask.
- **M12**`parent` in `DeviceDesc`, `device_register` with resource containment. - **M12**`parent` in `DeviceDescriptor`, `device_register` with resource containment.
- **M13** — capability passing. `ipc_call` / `ipc_reply_wait` grew a `send_cap` argument - **M13** — capability passing. `ipc_call` / `ipc_reply_wait` grew a `send_cap` argument
and a `received_cap` return (r8): an endpoint travels with a message, installed into and a `received_cap` return (r8): an endpoint travels with a message, installed into
the receiver's handle table (shared, refcount-bumped — a copy, not a move). A full the receiver's handle table (shared, refcount-bumped — a copy, not a move). A full
table fails `-ENOSPC` and does not half-deliver. This is the "open" primitive — a bus table fails `-ENOSPC` and does not half-deliver. This is the "open" primitive — a bus
driver mints a per-device endpoint and hands it to a class driver. The runtime exposes driver mints a per-device endpoint and hands it to a class driver. The runtime exposes
`callCap` and `replyWait(..., send_cap)`; no class driver consumes it yet. `callCap` and `replyWait(..., send_cap)`, and class drivers consume them now: the
PS/2 keyboard and mouse drivers attach to ps2-bus this way, and `runtime.usb` /
`runtime.input` open their per-device and subscription channels with `callCap`.
- **M14** — DMA memory + the memory-ordering layer. `/lib/mmio` gives drivers typed - **M14** — DMA memory + the memory-ordering layer. `/lib/mmio` gives drivers typed
volatile access and `mb`/`rmb`/`wmb` (per-arch); `dma_alloc`/`dma_free` grant volatile access and `mb`/`rmb`/`wmb` (per-arch); `dma_alloc`/`dma_free` grant
physically-contiguous, pinned, uncacheable, reclaim-on-teardown buffers with the physically-contiguous, pinned, uncacheable, reclaim-on-teardown buffers with the
physical address exposed (`pmm.allocContiguous`, a DMA arena, `mapUserDmaInto`). physical address exposed (`pmm.allocContiguous`, a DMA arena, `mapUserDmaInto`).
`dma_below_4g` caps the address for legacy engines; `dma_write_combining` is accepted `dma_below_4g` caps the address for legacy engines; `dma_write_combining` is accepted
but falls back to coherent until PAT is programmed. The bus drivers use `/lib/mmio`; but falls back to coherent until PAT is programmed. The bus drivers use `/lib/mmio`,
no DMA driver consumes `dma_alloc` yet. and `dma_alloc` has real consumers now: the xHCI driver's rings and contexts,
usb-storage's command/status wrappers, virtio-gpu's virtqueue, and the fat
service's bounce buffer.
- **M15** — interrupts for PCI devices, the MSI half. Discovery now gives every PCI - **M15** — interrupts for PCI devices, the MSI half. Discovery now gives every PCI
function its 4 KiB ECAM config space as resource 0 (unblocking the capability walk function its 4 KiB ECAM config space as resource 0 (unblocking the capability walk
with no new syscall), and `msi_bind(device_id, endpoint) -> address, data` allocates a with no new syscall), and `msi_bind(device_id, endpoint) -> address, data` allocates a
@ -175,8 +183,11 @@ If a class driver needs `mmio`, it has become an HCD and should be one.
the hardware and spawns each driver ([drivers.md](drivers.md)). Ungated for now — a the hardware and spawns each driver ([drivers.md](drivers.md)). Ungated for now — a
spawn capability is future work. spawn capability is future work.
So: **bus drivers work now, and they're started by the device manager, not the kernel.** So: **all three shapes work now, and they're started by the device manager, not the
HCDs and class drivers do not work yet. Here is exactly why, and exactly what would fix it. kernel.** The xHCI driver is the HCD-and-bus proof; the USB HID/storage and PS/2 class
drivers reach their devices purely over IPC. What follows are the original design notes
for the primitives that unblocked each shape — exactly why each was the blocker, and
exactly what fixed it.
--- ---
@ -302,8 +313,10 @@ barrier, or per-arch inline asm — which is what `library/mmio.zig` should hide
rather than an out-struct. The rest of this section is the original design note.* rather than an out-struct. The rest of this section is the original design note.*
**The blocker, and it's a hard one.** No PCI device can take an interrupt today. **The blocker, and it's a hard one.** No PCI device can take an interrupt today.
[`addBars`](system/devices/acpi.zig) records `.memory` and `.io_port` BARs and never an `addBars` (then in the kernel's ACPI discovery; BAR decode has since moved to the
`.irq`; there is no `_PRT` parsing anywhere in the tree. The HPET is the one exception — ring-3 pci-bus driver, `system/drivers/pci-bus/pci-bus.zig`) records `.memory` and
`.io_port` BARs and never an `.irq`; there is no `_PRT` parsing anywhere in the tree.
The HPET is the one exception —
it advertises its own interrupt routing in its own registers, a privilege no ordinary it advertises its own interrupt routing in its own registers, a privilege no ordinary
device has. device has.

View File

@ -31,8 +31,8 @@ kernel ──spawns──► init (PID 1) ──spawns──► device-manag
| | | | | |
spawns only init, the service supervisor: the driver supervisor: enumerates spawns only init, the service supervisor: the driver supervisor: enumerates
publishes the starts the system /system/devices, matches each device publishes the starts the system /system/devices, matches each device
initial-ramdisk services (vfs, the to a driver, and system_spawn's it initial-ramdisk services (device-manager, to a driver, and system_spawn's it
so user space can device-manager). Its so user space can fat, logger, ...). Its
system_spawn from it list is init policy. system_spawn from it list is init policy.
``` ```
@ -42,15 +42,22 @@ in the initial-ramdisk as a fresh ring-3 process — `name` becoming its argv[0]
the optional arguments its argv[1..], on a SysV entry stack, see sysv.md). Everything else is a user-space decision: the optional arguments its argv[1..], on a SysV entry stack, see sysv.md). Everything else is a user-space decision:
- **init** ([system/services/init](system/services/init/init.zig)) is the **service - **init** ([system/services/init](system/services/init/init.zig)) is the **service
supervisor**. It spawns the system services danos brings up at boot — today `vfs` and supervisor**. It spawns the system services danos brings up at boot — today `input`,
the `device-manager` — from a small list. Drivers are deliberately *not* its job. the `device-manager`, `fat`, `display`, `display-demo`, and the `logger` — from a
small list. Drivers are deliberately *not* its job. (An earlier draft listed a `vfs`
service here; that service is retired — the router moved into the kernel as
`fs_resolve`.)
- **device-manager** ([system/services/device-manager](system/services/device-manager/device-manager.zig)) - **device-manager** ([system/services/device-manager](system/services/device-manager/device-manager.zig))
is the **driver supervisor**. It does the three steps a monolithic kernel would do in is the **driver supervisor**. It does the three steps a monolithic kernel would do in
its probe path, entirely from ring 3: its probe path, entirely from ring 3:
1. **Discover**`device_enumerate` snapshots the device table the kernel built from 1. **Discover**`device_enumerate` snapshots the device table the kernel built from
ACPI/PCI ([discovery](discovery.md)). ACPI/PCI ([discovery](discovery.md)).
2. **Match** — for each device it looks up a driver by `DeviceClass`. The match policy 2. **Match** — for each device it looks up a driver. The match policy is code, a few
is a table (`driverFor`): today a static `timer → hpet` map; a fuller system reads small per-bus tables: from the boot snapshot only the PCI host bridge matches
(→ `pci-bus`); everything else arrives later as bus reports and matches on
identity — `pciDriverForIdentity` (xHCI → `usb-xhci-bus`, virtio-gpu →
`virtio-gpu`), `hidDriverFor` (PNP0303/PNP0F13 → `ps2-bus`), and
`usbDriverForIdentity` (USB keyboard, mouse, storage). A fuller system reads
what each driver *binds* (a manifest under `/system/drivers`, or the driver what each driver *binds* (a manifest under `/system/drivers`, or the driver
describing its own match). describing its own match).
3. **Spawn**`system_spawn(driver_name, arguments)` starts the matched driver (the 3. **Spawn**`system_spawn(driver_name, arguments)` starts the matched driver (the
@ -84,7 +91,7 @@ names a device by id and a resource by index. That indirection is the entire sec
model. If `mmio_map` took a physical address, any process could map the kernel's model. If `mmio_map` took a physical address, any process could map the kernel's
memory; if `irq_bind` took a GSI, any process could bind the keyboard's line and memory; if `irq_bind` took a GSI, any process could bind the keyboard's line and
silently intercept it. Instead the kernel checks two things (`process.ownedGsi`, and silently intercept it. Instead the kernel checks two things (`process.ownedGsi`, and
the same check at the top of `sysMmioMap`): the same check at the top of `systemMmioMap`):
- `devices_broker.ownerOf(dev_id) == me` — you claimed it, and claims are exclusive - `devices_broker.ownerOf(dev_id) == me` — you claimed it, and claims are exclusive
- the resource at `res_idx` is of the right *kind*`memory` for `mmio_map`, `irq` - the resource at `res_idx` is of the right *kind*`memory` for `mmio_map`, `irq`
@ -95,11 +102,15 @@ The claim is the capability. Everything else follows from it.
## Registers: `mmio_map` ## Registers: `mmio_map`
`mmio_map` walks the caller's page tables and installs the device's physical frames `mmio_map` walks the caller's page tables and installs the device's physical frames
with `present | user | writable | nx | pcd | pwt` with `present | user | writable | nx | device_grant` plus a cache mode
(`arch/x86_64/paging.zig:mapUserDeviceInto`). Two of those bits are load-bearing: (`system/kernel/architecture/x86_64/paging.zig:mapUserDeviceInto`). Two of those bits
are load-bearing:
- **`pcd | pwt`** — strong-uncacheable. A device register is not memory; a cached read - **`pcd | pwt`** — strong-uncacheable, the default cache mode. A device register is
would return a stale value and a write might never leave the CPU. not memory; a cached read would return a stale value and a write might never leave
the CPU. The one exception: a resource flagged write-combining
(`resource_flag_write_combining` — today the kernel-seeded display framebuffer)
gets the PAT bit instead, so pixel writes batch into bursts.
- **`device_grant`** (bit 9, one of the PTE's available bits) — marks the leaf as MMIO - **`device_grant`** (bit 9, one of the PTE's available bits) — marks the leaf as MMIO
rather than RAM, so `freeSubtree` skips `pmm.free` on it when the address space is rather than RAM, so `freeSubtree` skips `pmm.free` on it when the address space is
destroyed. Without this, killing a driver would hand the HPET's registers back to destroyed. Without this, killing a driver would hand the HPET's registers back to
@ -143,7 +154,7 @@ interrupt fires exactly once, ever; call it before the device is quiet and you g
interrupt storm. That single fact explains why `irq_bind` and `irq_ack` are two interrupt storm. That single fact explains why `irq_bind` and `irq_ack` are two
syscalls and not one. syscalls and not one.
This is also why `interruptDispatch` (`arch/x86_64/idt.zig`) no longer issues the EOI This is also why `interruptDispatch` (`system/kernel/architecture/x86_64/idt.zig`) no longer issues the EOI
itself. It used to, before running the handler — correct for the LAPIC timer, and itself. It used to, before running the handler — correct for the LAPIC timer, and
impossible for a routed device line. Each handler now owns its EOI, because only the impossible for a routed device line. Each handler now owns its EOI, because only the
handler knows which discipline its source needs. handler knows which discipline its source needs.
@ -234,10 +245,10 @@ static capability.
A device that *contains other devices* — a PCI bridge, a USB hub, or the HPET's block A device that *contains other devices* — a PCI bridge, a USB hub, or the HPET's block
of comparators — needs a driver that enumerates it and tells the kernel what it found. of comparators — needs a driver that enumerates it and tells the kernel what it found.
That's `device_register`, and it makes the device table a tree rather than a list That's `device_register`, and it makes the device table a tree rather than a list
(`DeviceDesc.parent`). (`DeviceDescriptor.parent`).
```zig ```zig
var child = std.mem.zeroes(dev.DeviceDesc); var child = std.mem.zeroes(dev.DeviceDescriptor);
child.class = @intFromEnum(dev.DeviceClass.timer); child.class = @intFromEnum(dev.DeviceClass.timer);
child.resource_count = 1; child.resource_count = 1;
child.resources[0] = .{ .kind = memory, .start = bus_base + 0x100, .len = 0x20 }; child.resources[0] = .{ .kind = memory, .start = bus_base + 0x100, .len = 0x20 };
@ -248,10 +259,12 @@ The child is left **unclaimed**, which is the whole point: another process claim
`mmio_map`s it, and sees only that 0x20-byte window. `mmio_map`s it, and sees only that 0x20-byte window.
The rule the kernel enforces is **containment**: every resource of a child must lie The rule the kernel enforces is **containment**: every resource of a child must lie
inside a resource of the same kind on its parent. Ranges must nest; an IRQ must match inside a resource of the same kind on its parent. Ranges must nest; a child's IRQ —
exactly. This isn't bureaucracy — a `DeviceDesc` is a licence to map physical memory, so still exactly one line — must fall within the parent's IRQ range (a length-1 parent
without containment `device_register` would be a syscall for mapping any page you like. A range is the old exact-match rule). This isn't bureaucracy — a `DeviceDescriptor` is a
bus driver may only ever subdivide what it already owns. licence to map physical memory, so without containment `device_register` would be a
syscall for mapping any page you like. A 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 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`. controller, not by MMIO, so it gets `resource_count = 0`.
@ -277,8 +290,13 @@ Several things this list used to warn about are now available (see
[driver-model.md](driver-model.md)): **port I/O** (`io_read`/`io_write`, claim-gated by [driver-model.md](driver-model.md)): **port I/O** (`io_read`/`io_write`, claim-gated by
the device's `io_port` resource — direct ring-3 `in`/`out` is still a #GP, so a PS/2 or the device's `io_port` resource — direct ring-3 `in`/`out` is still a #GP, so a PS/2 or
16550 driver goes through these), **DMA memory** (`dma_alloc`: contiguous, pinned, 16550 driver goes through these), **DMA memory** (`dma_alloc`: contiguous, pinned,
uncacheable, physical address exposed), and **memory barriers** (`/lib/mmio`'s uncacheable, physical address exposed), **memory barriers** (`library/mmio`'s
`mb`/`rmb`/`wmb`). What remains: `mb`/`rmb`/`wmb`, imported as the `mmio` module), **fault isolation** (a ring-3 fault kills only the faulting
process — `killCurrentProcess` — and the machine keeps running,
[resilience](resilience.md)), and **reclaim + restart on death** (every path out of a
process releases its claims and IRQ/MSI bindings — `releaseAllOwnedBy`,
`irq.releaseOwner` — and the device manager respawns the driver with backoff,
[device-manager.md](device-manager.md)). What remains:
- **Page granularity.** `mmio_map` rounds to 4 KiB. Two devices sharing a page means - **Page granularity.** `mmio_map` rounds to 4 KiB. Two devices sharing a page means
granting one grants the other. A `device_register`ed child's *resource* can be narrower granting one grants the other. A `device_register`ed child's *resource* can be narrower
@ -289,8 +307,9 @@ uncacheable, physical address exposed), and **memory barriers** (`/lib/mmio`'s
are programmed, so `device_claim` on a DMA-capable device is still effectively are programmed, so `device_claim` on a DMA-capable device is still effectively
equivalent to granting ring 0. This is the largest gap between the design's promise and equivalent to granting ring 0. This is the largest gap between the design's promise and
what it delivers; enforcement lands with the first DMA driver. what it delivers; enforcement lands with the first DMA driver.
- **No `dev_release`.** A claim is never dropped (only IRQ/MSI bindings are, on exit), so - **No voluntary `dev_release`.** A *live* driver can't drop a claim — only exit
a device stays owned for the life of its driver — which blocks restart. releases it (any path out of a process runs `releaseAllOwnedBy`) — so handing a
device between running drivers still means exiting.
- **One endpoint per GSI**, so shared legacy PCI INTx lines can't be split between two - **One endpoint per GSI**, so shared legacy PCI INTx lines can't be split between two
drivers. MSI/MSI-X — one vector per device, edge-triggered, unshared — is the real drivers. MSI/MSI-X — one vector per device, edge-triggered, unshared — is the real
answer, and QEMU's HPET doesn't offer it (`Tn_FSB_INT_DEL_CAP = 0`). answer, and QEMU's HPET doesn't offer it (`Tn_FSB_INT_DEL_CAP = 0`).
@ -303,13 +322,6 @@ uncacheable, physical address exposed), and **memory barriers** (`/lib/mmio`'s
the ISR until `irq_ack`, so at most one badge is ever outstanding. Bind nine devices the ISR until `irq_ack`, so at most one badge is ever outstanding. Bind nine devices
to one endpoint, though, and a dropped badge leaves that line masked with nobody to one endpoint, though, and a dropped badge leaves that line masked with nobody
left to ack it. left to ack it.
- **A faulting driver still kills the machine.** There is no per-process kill path: a
ring-3 page fault halts the kernel, so `releaseIrqs` runs only on a voluntary
`exit`. Fault isolation is the whole premise ([vision](vision.md)) and it is
[not built yet](resilience.md).
- **A dead driver's device is not reclaimed.** `releaseIrqs` unbinds and masks the
line on exit, but the claim is never released — restart is
[not built](resilience.md).
- **On real hardware, the mask/EOI cycle may need a remote-IRR flush.** Masking a - **On real hardware, the mask/EOI cycle may need a remote-IRR flush.** Masking a
level-triggered redirection entry with remote-IRR set doesn't clear it on some level-triggered redirection entry with remote-IRR set doesn't clear it on some
chipsets, and the line never fires again. QEMU clears it on EOI regardless, so the chipsets, and the line never fires again. QEMU clears it on EOI regardless, so the

View File

@ -23,14 +23,19 @@ Partition (ESP)** and running a file at a well-known fallback path:
EFI/BOOT/BOOTX64.efi <- the "removable media" default for x86-64 EFI/BOOT/BOOTX64.efi <- the "removable media" default for x86-64
``` ```
The boot volume is the **FHS-shaped `zig-out`** itself (see the repository-layout note The boot volume is **FHS-shaped** (see the repository-layout note in
in [README.md](README.md)): `build.zig` installs `boot/efi.zig` (built for the `uefi` [README.md](README.md)): `build.zig` installs `boot/efi.zig` (built for the `uefi`
target) to `zig-out/EFI/BOOT/BOOTX64.efi` — the one path UEFI firmware fixes — and lays target) at `EFI/BOOT/BOOTX64.efi` — the one path UEFI firmware fixes — and lays
the rest out by FHS path: the kernel at `zig-out/system/kernel`, init at the rest out by FHS path: the kernel at `system/kernel`, init at
`zig-out/system/services/init`, the initial-ramdisk at `zig-out/boot/`. The `system/services/init`, the pre-packed boot capsule at `boot/system.img`.
`run-x86-64` step points QEMU at OVMF (UEFI firmware for virtual machines) and presents `zig-out` mirrors that tree, but what a machine actually boots is the
`zig-out` to the guest as a FAT drive. The firmware finds `BOOTX64.efi` and runs it — self-contained FAT32 image `tools/make-fat-image.py` builds from the same files
that's our `main()`, which then loads the kernel and init from their FHS paths. (`danos-usb.img`). The `run-x86-64` step points QEMU at OVMF (UEFI firmware for
virtual machines) and attaches that image (its serial-logging twin, built the
same way) as a USB mass-storage device on the xHCI bus — the guest never sees
`zig-out`. The firmware finds `BOOTX64.efi` on the image and runs it — that's
our `main()`, which then loads the kernel and the system binaries from their
FHS paths.
## Boot services: the firmware's API ## Boot services: the firmware's API
@ -56,7 +61,15 @@ The comment in `boot()` says exactly this:
## What our loader actually does ## What our loader actually does
`boot()` runs four steps in order: The four milestones below are the spine of `boot()`. Along the way it also
captures the **ACPI RSDP** from the UEFI configuration table (while boot
services are still up), loads the system binaries into an in-RAM
**initial ramdisk** (`loadSystemTree` — normally a single read of the pre-packed
`boot\system.img` capsule, which already *is* the ramdisk wire format; it falls
back to opening each manifest-listed path, and walks the `/system` tree only as
a last resort for hand-assembled sticks. Best-effort either way — a kernel-only
volume still boots), and builds the **bootstrap page tables** the kernel starts
life on (`buildBootstrapTables`), all before the jump:
### 1. Query the framebuffer (`queryFramebuffer`) ### 1. Query the framebuffer (`queryFramebuffer`)
@ -91,16 +104,20 @@ All of this *must* happen now, because after exit there's no GOP to ask. (See
may return short, so we loop.) may return short, so we loop.)
- Parse the ELF: validate the `\x7fELF` magic and the `x86_64` machine type, then - Parse the ELF: validate the `\x7fELF` magic and the `x86_64` machine type, then
walk the program headers. For every `PT_LOAD` segment we: walk the program headers. For every `PT_LOAD` segment we:
- reserve the exact physical pages it's linked at (`p_paddr`) via - reserve the exact physical pages it asks to be loaded at (`p_paddr`) via
`allocatePages`, `allocatePages`,
- `@memcpy` the file-backed bytes to that address, - `@memcpy` the file-backed bytes to that address,
- `@memset` the `.bss` tail (the part where `p_memsz > p_filesz`) to zero. - `@memset` the `.bss` tail (the part where `p_memsz > p_filesz`) to zero.
The kernel is linked to load at physical `0x100000` (1 MiB) — set by The kernel is linked to *run* in the higher half (virtual base
`exe.image_base` in `build.zig` and the linker script. UEFI identity-maps memory, `0xFFFFFFFF80000000`; `exe.image_base` in `build.zig` is the *virtual* address
so the physical address the ELF asks for is the address it actually runs at. If a `0xFFFFFFFF80100000`) but is *loaded* low: the linker script's `AT()` clauses
segment's `p_paddr` collided with firmware-reserved memory, `allocatePages` would give every segment a low physical load address (`p_paddr`, with `.text` at
fail and we'd need to move `image_base`. `0x100000`, 1 MiB), which is what the loader allocates and copies into. The
bootstrap page tables built before the jump map the high link addresses onto
those low physical pages. If a segment's `p_paddr` collided with
firmware-reserved memory, `allocatePages` would fail and we'd need to move the
load addresses.
`loadElf` returns `e_entry`, the kernel's entry-point address. `loadElf` returns `e_entry`, the kernel's entry-point address.
@ -124,13 +141,12 @@ entirely ours.
### 4. Jump to the kernel ### 4. Jump to the kernel
```zig ```zig
const kernel: *const fn (*const BootInfo) callconv(boot_handoff.kernel_abi) noreturn = handoff(cr3, entry, &boot_information);
@ptrFromInt(entry);
kernel(&boot_info);
``` ```
We cast the entry address to a function pointer and call it, passing a pointer to `handoff` is a single inline-asm block — `cli`, load the bootstrap page tables'
the `BootInfo` we filled in. This never returns. `cr3`, place the `boot_information` pointer in RDI, then `callq *entry` — so
nothing runs between the CR3 load and the jump. This never returns.
## The ABI subtlety: RCX vs RDI ## The ABI subtlety: RCX vs RDI
@ -138,14 +154,15 @@ There's a deliberate detail worth calling out. A UEFI binary is compiled with th
**Microsoft x64** calling convention (first argument in register **RCX**). Our **Microsoft x64** calling convention (first argument in register **RCX**). Our
kernel is freestanding and uses the **SysV AMD64** convention (first argument in kernel is freestanding and uses the **SysV AMD64** convention (first argument in
**RDI**). If we let each side use its target's default, the loader would place **RDI**). If we let each side use its target's default, the loader would place
`boot_info` in RCX while the kernel looked for it in RDI — and the kernel would `boot_information` in RCX while the kernel looked for it in RDI — and the kernel
read garbage. would read garbage.
So both sides pin the convention explicitly to SysV via the shared So the convention is pinned explicitly to SysV via the shared
`boot_handoff.kernel_abi` (defined in `system/boot-handoff.zig`). The loader's `boot_handoff.kernel_abi` (defined in `system/boot-handoff.zig`). The kernel's
function-pointer type and the kernel's `_start` both reference it, so the pointer lands `kmainEntry` declares `callconv(kernel_abi)`; the loader honours the same contract
in the register the kernel expects. This is the whole reason `kernel_abi` lives in the by loading RDI by hand in `handoff`'s inline asm rather than trusting its own
shared `boot-handoff` module: it's a contract both binaries must agree on. See Microsoft-x64 default. `kernel_abi` lives in the shared `boot-handoff` module
because it's a contract both binaries must agree on. See
[sysv.md](sysv.md) for what "SysV" means and where else it shows up. [sysv.md](sysv.md) for what "SysV" means and where else it shows up.
## The handoff contract ## The handoff contract
@ -156,13 +173,16 @@ what `system/boot-handoff.zig` provides — imported by both as the `boot-handof
It is *only* the handoff: the kernel↔user ABI (`system/abi.zig`) and the device types It is *only* the handoff: the kernel↔user ABI (`system/abi.zig`) and the device types
(`system/devices/device-abi.zig`) are separate contracts the bootloader never sees. (`system/devices/device-abi.zig`) are separate contracts the bootloader never sees.
- `BootInfo` — the top-level struct passed to the kernel (currently just the - `BootInformation` — the top-level struct passed to the kernel: the
framebuffer; this is where future handoff data like the memory map will go). framebuffer, the memory map, the kernel's own `PT_LOAD` segments
- `Framebuffer`, `PixelFormat`, `kernel_abi` — the shared field layouts and the (`kernel_segments` + `kernel_segment_count`, so the kernel can re-map itself
calling convention. with correct permissions), the ACPI RSDP address, and the initial-ramdisk
base/length.
- `Framebuffer`, `PixelFormat`, `MemoryMap`/`MemoryRegion`, `KernelSegment`,
`kernel_abi` — the shared field layouts and the calling convention.
Both are `extern struct`, giving them a stable, C-compatible layout so the bytes The structs are `extern struct`, giving them a stable, C-compatible layout so the
the loader writes are the bytes the kernel reads. bytes the loader writes are the bytes the kernel reads.
## The whole flow at a glance ## The whole flow at a glance
@ -170,15 +190,20 @@ the loader writes are the bytes the kernel reads.
power on power on
-> UEFI firmware initialises hardware -> UEFI firmware initialises hardware
-> finds EFI/BOOT/BOOTX64.efi on the FHS volume, runs it (our efi.zig main) -> finds EFI/BOOT/BOOTX64.efi on the FHS volume, runs it (our efi.zig main)
-> grab boot services -> grab boot services (+ the ACPI RSDP from the configuration table)
-> queryFramebuffer (via GOP: EDID native res, setMode, describe fb) -> queryFramebuffer (via GOP: EDID native res, setMode, describe fb)
-> loadKernel (read system/kernel ELF, load PT_LOAD segments to 0x100000) -> loadKernel (read system/kernel ELF, load PT_LOAD segments low, .text at 0x100000)
-> loadSystemTree (read the boot\system.img capsule as the in-RAM initial ramdisk;
fallbacks: manifest-listed paths, then a /system tree walk)
-> buildBootstrapTables (identity + physmap + higher-half kernel mappings)
-> exitBootServices (retry until the memory-map key holds) -> exitBootServices (retry until the memory-map key holds)
-> jump to e_entry, boot_info pointer in RDI -> handoff: load bootstrap CR3, jump to e_entry, boot_information pointer in RDI
-> kernel _start (system/kernel/kernel.zig: framebuffer console, then halt) -> kernel _start (architecture/x86_64/isr.s: switch to a kernel-owned stack,
call kmainEntry -> paging, heap, device discovery,
scheduler, SMP, user space)
``` ```
Bottom line: **UEFI's job is to give us a CPU, memory, and a framebuffer, then Bottom line: **UEFI's job is to give us a CPU, memory, and a framebuffer, then
disappear.** `boot/efi.zig` is the thin bridge that collects those gifts into a disappear.** `boot/efi.zig` is the thin bridge that collects those gifts into a
`BootInfo`, tears down the firmware, and jumps into the kernel — after which we're `BootInformation`, tears down the firmware, and jumps into the kernel — after
on our own. which we're on our own.

View File

@ -8,7 +8,7 @@ natural unit because that's the granularity the CPU's paging hardware maps — a
it is the primitive everything above it stands on: page tables, the kernel heap, it is the primitive everything above it stands on: page tables, the kernel heap,
per-process memory all ultimately ask the frame allocator for pages. per-process memory all ultimately ask the frame allocator for pages.
It's **generic kernel code**: it operates on the neutral `system.MemoryRegion` It's **generic kernel code**: it operates on the neutral `boot_handoff.MemoryRegion`
array, so there's no UEFI in it and nothing architecture-specific beyond the 4 KiB array, so there's no UEFI in it and nothing architecture-specific beyond the 4 KiB
page. (Contrast [architecture.md](architecture.md), which is where CPU-specific code lives.) page. (Contrast [architecture.md](architecture.md), which is where CPU-specific code lives.)
@ -38,16 +38,18 @@ and a `next_hint` marking where the next allocation scan should start.
### init(map) — building it from the memory map ### init(map) — building it from the memory map
1. **Size it.** Find the highest address across all `usable` regions; 1. **Size it.** Find the highest address across all RAM regions — every kind
`total_frames = highest / page_size`. Reserved and MMIO spans above that *except* `mmio` — so reserved and ACPI spans sit *inside* the bitmap, marked
(remember the ~12 GiB of MMIO from [memory-map.md](memory-map.md)) sit *outside* used but trackable (e.g. so the boot buffers can be freed later);
the bitmap and are simply never allocatable. `total_frames = highest / page_size`. Only MMIO (device address space —
remember the ~12 GiB of it from [memory-map.md](memory-map.md)) sits outside
the bitmap and is simply never allocatable.
2. **Place it (the bootstrap).** The bitmap needs storage before an allocator 2. **Place it (the bootstrap).** The bitmap needs storage before an allocator
exists — a chicken-and-egg. Solution: pick the first `usable` region big enough exists — a chicken-and-egg. Solution: pick the first `usable` region big enough
to hold the bitmap and put it there, addressing it directly as a pointer. That to hold the bitmap and put it there, addressing it through the **physmap**
last part relies on the firmware's **identity mapping** still being in effect (`boot_handoff.physicalToVirtual`). The loader's bootstrap page tables already
(physical address == virtual address), which holds until the kernel installs provide the physmap and the kernel's own tables keep it, so the pointer stays
its own page tables. valid across the paging switch.
3. **Mark, then free.** Set the whole bitmap to `used` (`0xff`), then walk the 3. **Mark, then free.** Set the whole bitmap to `used` (`0xff`), then walk the
`usable` regions clearing their bits. Doing it in that direction means every `usable` regions clearing their bits. Doing it in that direction means every
gap, reserved span, and hole is unallocatable *by default* — we only ever hand gap, reserved span, and hole is unallocatable *by default* — we only ever hand
@ -73,10 +75,13 @@ free, or one out of range) are ignored rather than corrupting the used count.
- **Generic walk.** Because `MemoryRegion` is danos's own type, the map is a plain - **Generic walk.** Because `MemoryRegion` is danos's own type, the map is a plain
slice — none of the variable descriptor-stride from the raw UEFI map. slice — none of the variable descriptor-stride from the raw UEFI map.
- **Identity mapping assumption.** Placing the bitmap by physical address only - **Physmap addressing.** The bitmap (and the region array) is reached through
works while the firmware's identity map is live. When danos sets up its own the physmap via `boot_handoff.physicalToVirtual`, which both the loader's
paging, the bitmap (and any other physical pointer) will need an explicit bootstrap tables and the kernel's own tables provide — no remapping is needed
mapping. This is a deliberate, documented dependency of this stage. when danos switches to its own paging. The one invariant: the bitmap must sit
under the bootstrap physmap's reach (4 GiB), which holds because the placement
scan (step 2 above) runs from the lowest usable region up and takes the first
one big enough — on the supported configurations that lands well under 4 GiB.
- **Frame 0 is reserved** so `0` stays a safe "none" sentinel — and the bitmap is - **Frame 0 is reserved** so `0` stays a safe "none" sentinel — and the bitmap is
never placed there. (An early bug did exactly that: a `usable` region at physical never placed there. (An early bug did exactly that: a `usable` region at physical
address 0 collided with a `0`-means-not-found sentinel and tripped a panic. The address 0 collided with a `0`-means-not-found sentinel and tripped a panic. The
@ -90,12 +95,15 @@ free, or one out of range) are ignored rather than corrupting the used count.
`kmain` brings the allocator up and self-tests it. Booted in QEMU with 128 MiB: `kmain` brings the allocator up and self-tests it. Booted in QEMU with 128 MiB:
``` ```
danos: frame allocator online /system/kernel: frame allocator online
free frames: 19751 (77 MiB) <- matches the map's 77 MiB usable free frames: 30520 (119 MiB) <- matches the map's 119 MiB usable
alloc x3 : 0x2000 0x3000 0x4000 <- frame 0 reserved, bitmap at 0x1000, so allocs start at 0x2000 alloc x3 : 0x3000 0x4000 0x5000 <- frame 0 reserved, bitmap at 0x1000, AP trampoline at 0x2000
after free : 19751 frames free <- three freed, count restored after free : 30520 frames free <- three freed, count restored
``` ```
(The AP-trampoline page is claimed with `allocBelow` right after `init`, before
the demo allocations — hence they start at 0x3000.)
The `free frames` MiB agreeing with the memory map's `usable RAM`, the three The `free frames` MiB agreeing with the memory map's `usable RAM`, the three
distinct consecutive addresses, and the count returning to its start after freeing distinct consecutive addresses, and the count returning to its start after freeing
are the three signals that init, alloc and free are all correct. are the three signals that init, alloc and free are all correct.
@ -103,7 +111,7 @@ are the three signals that init, alloc and free are all correct.
## Boot-services memory comes pre-reclaimed ## Boot-services memory comes pre-reclaimed
The UEFI boot-services memory (~44 MiB) is defunct and free once The UEFI boot-services memory (~44 MiB) is defunct and free once
`ExitBootServices` runs, taking usable RAM from ~76 MiB up to ~121 MiB. The frame `ExitBootServices` runs, taking usable RAM from ~76 MiB up to ~119 MiB. The frame
allocator does **nothing special** to get it: the loader already classified it as allocator does **nothing special** to get it: the loader already classified it as
`usable` (see [memory-map.md](memory-map.md)), so it's just part of the `usable` `usable` (see [memory-map.md](memory-map.md)), so it's just part of the `usable`
regions `init` frees. Keeping that boot-protocol knowledge on the loader side is regions `init` frees. Keeping that boot-protocol knowledge on the loader side is

View File

@ -89,11 +89,13 @@ it.
## Two subtleties worth noting ## Two subtleties worth noting
1. **`width` vs `pitch` in the loops.** In `fillRow`/`copyRow` we iterate `x` 1. **`width` vs `pitch` in the loops.** In `fillRow` we iterate `x` up to
up to `self.fb.width` — the *visible* count — but jump between rows with `self.fb.width` — the *visible* count — but jump between rows with
`pitch`. That's the correct pairing: touch only real pixels, but skip the `pitch`. That's the correct pairing: touch only real pixels, but skip the
full stride (including padding) to reach the next row. We never write into full stride (including padding) to reach the next row. We never write into
the padding, which is right. the padding, which is right. (A `copyRow` used to sit alongside it; it's
gone — `scroll` was since rewritten as a writes-only screen clear, because
reading VRAM back is uncached-slow on real hardware.)
2. **`volatile`.** The pointer is `volatile` because this memory is special — 2. **`volatile`.** The pointer is `volatile` because this memory is special —
it's watched by the display hardware. `volatile` tells the compiler *"don't it's watched by the display hardware. `volatile` tells the compiler *"don't

View File

@ -17,7 +17,7 @@ safely, until the machine is reset or powered off.
Everything comes down to one x86 instruction. It's CPU-specific, so it lives in Everything comes down to one x86 instruction. It's CPU-specific, so it lives in
the arch module, `system/kernel/architecture/x86_64/cpu.zig` (see [architecture.md](architecture.md)), and the the arch module, `system/kernel/architecture/x86_64/cpu.zig` (see [architecture.md](architecture.md)), and the
generic kernel calls it as `arch.halt()`: generic kernel calls it as `architecture.halt()`:
```zig ```zig
/// Park the core forever. `hlt` drops it into a low-power idle until the next /// Park the core forever. `hlt` drops it into a low-power idle until the next
@ -56,10 +56,10 @@ door: every time an interrupt wakes the core, the loop immediately runs `hlt`
again and it goes back to sleep. The net effect is a permanent halt that still again and it goes back to sleep. The net effect is a permanent halt that still
sleeps between the interrupts it can't prevent. sleeps between the interrupts it can't prevent.
(At this stage danos hasn't set up an interrupt descriptor table, so most (danos handles plenty of interrupts through its interrupt descriptor table —
interrupts aren't even something we handle — but non-maskable interrupts and the timer tick waking a halted idle core is exactly how scheduling works — and
system-management interrupts can still wake a halted core regardless. The loop non-maskable and system-management interrupts can wake a halted core regardless
makes us robust to all of them.) of what we handle. The loop makes the halt robust to all of them.)
## The `asm volatile` part ## The `asm volatile` part
@ -79,32 +79,40 @@ treats the call:
- Code *after* a `noreturn` call is unreachable, so the compiler needn't emit a - Code *after* a `noreturn` call is unreachable, so the compiler needn't emit a
return sequence, and won't warn about "missing return value" in the callers. return sequence, and won't warn about "missing return value" in the callers.
- It lets `kmain` and `_start` themselves be `noreturn`, which is the honest - It lets `kmain` and the exported entry shim `kmainEntry` themselves be
signature for a kernel entry point — the bootloader jumps in and nothing ever `noreturn`, which is the honest signature for a kernel entry point — the
jumps back out. bootloader jumps in and nothing ever jumps back out.
You can see the chain in `system/kernel/kernel.zig`: `_start` is `noreturn`, it calls You can see the chain in the code: `_start` (an assembly stub in
`kmain` which is `noreturn`, which ends by calling `arch.halt()` which is `system/kernel/architecture/x86_64/isr.s`) installs a kernel-owned stack and calls
`noreturn`. The "never returns" property is threaded all the way down. `kmainEntry` in `system/kernel/kernel.zig`, which is `noreturn`; it calls `kmain`,
also `noreturn`, which ends by calling `architecture.halt()`, again `noreturn`.
The "never returns" property is threaded all the way down.
## Where danos halts ## Where danos halts
There are three halt sites, and they're all the same idea: There are three halt sites, and they're all the same idea:
1. **Normal end of kernel work**`kmain` prints its status, then calls 1. **The BSP's idle loop**`kmain` no longer runs out of work: it spawns
`arch.halt()`: `/system/services/init` as PID 1, drops itself to priority 0, and ends as the
bootstrap core's idle task — still by calling `architecture.halt()`:
```zig ```zig
con.write("\nkernel initialised; nothing left to do, halting.\n"); scheduler.setPriority(0);
arch.halt(); status("\n/system/kernel: kernel idle; user space is running.\n");
architecture.halt();
``` ```
There's genuinely nothing more to do yet, so the kernel parks itself. The timer keeps preempting the idle context into init and whatever else is
ready; between those interrupts, the halt loop is exactly the low-power park
described above.
2. **Kernel panic** — the freestanding panic handler has no OS to report to, so 2. **Kernel panic** — the freestanding panic handler has no OS to report to, so
it prints the message in red (if the console is up) and halts via the same it prints the message to the diagnostic log and the on-screen console —
`arch.halt()`. A panic is unrecoverable here, so stopping the machine — rather forcing the console back on even if a display service had it suppressed — and
than limping on with corrupted state — is the safe response. halts via the same `architecture.halt()`. A panic is unrecoverable here, so
stopping the machine — rather than limping on with corrupted state — is the
safe response.
3. **Bootloader failure** — in `boot/efi.zig`, if `boot()` fails *before* handing 3. **Bootloader failure** — in `boot/efi.zig`, if `boot()` fails *before* handing
off to the kernel, `main` logs the error and parks the machine with the same off to the kernel, `main` logs the error and parks the machine with the same
@ -112,14 +120,14 @@ There are three halt sites, and they're all the same idea:
```zig ```zig
boot() catch |err| { boot() catch |err| {
log("\r\ndanos: boot failed: "); log("\r\nEFI: boot failed: ");
logBytes(@errorName(err)); logBytes(@errorName(err));
log("\r\n"); log("\r\n");
while (true) asm volatile ("hlt"); while (true) asm volatile ("hlt");
}; };
``` ```
(Here it's an inline loop rather than `arch.halt()` because that lives in the (Here it's an inline loop rather than `architecture.halt()` because that lives in the
kernel's arch module, and the loader is a separate binary from the kernel.) kernel's arch module, and the loader is a separate binary from the kernel.)
## Summary ## Summary
@ -132,5 +140,5 @@ There are three halt sites, and they're all the same idea:
otherwise wake the core and let execution continue. otherwise wake the core and let execution continue.
- **`asm volatile`** emits the instruction and forbids the compiler from removing - **`asm volatile`** emits the instruction and forbids the compiler from removing
it; **`noreturn`** encodes "control never comes back" into the type system. it; **`noreturn`** encodes "control never comes back" into the type system.
- danos halts on normal completion, on a kernel panic, and on a bootloader error - danos halts in the kernel's idle loop, on a kernel panic, and on a bootloader
— the same "stop safely and stay stopped" in all three. error — the same "park the core safely" in all three.

View File

@ -8,7 +8,7 @@ the thing that unlocks dynamic data structures — lists, hash maps, driver stat
eventually a process table. eventually a process table.
It's generic kernel code (`system/kernel/heap.zig`): the allocator logic is It's generic kernel code (`system/kernel/heap.zig`): the allocator logic is
architecture-neutral, using `arch.mapPage` and the frame allocator underneath. architecture-neutral, using `architecture.mapPage` and the frame allocator underneath.
## A growable free-list allocator ## A growable free-list allocator
@ -30,9 +30,10 @@ Allocations are 16-byte aligned; larger alignments aren't supported yet (the
## Growing on demand ## Growing on demand
The heap lives in the **higher half** of the address space (virtual base The heap lives in the **higher half** of the address space (virtual base
`0xFFFF_8000_0000_0000`) — unmapped, well clear of the identity-mapped low half, `0xFFFF_8000_0000_0000`) — unmapped, well clear of the low half, which belongs
and leaving the low half free for a future user address space. (That base is to user space (unmapped in the kernel's own tables; per-process user address
x86_64-canonical; another architecture would pick its own.) spaces now map into it). (That base is x86_64-canonical; another architecture
would pick its own.)
When the free list can't satisfy a request, `grow` extends the mapped region: it When the free list can't satisfy a request, `grow` extends the mapped region: it
pulls fresh frames from the [frame allocator](frame-allocator.md) and `map`s each pulls fresh frames from the [frame allocator](frame-allocator.md) and `map`s each

View File

@ -5,7 +5,7 @@ window server, a logger. None of them owns the hardware, and the driver should n
who is listening. So between the drivers and the listeners sits the **input service** who is listening. So between the drivers and the listeners sits the **input service**
(`system/services/input/`): drivers **publish** events to it, programs **subscribe**, and (`system/services/input/`): drivers **publish** events to it, programs **subscribe**, and
it fans each event out to every interested subscriber. It is an ordinary ring-3 process it fans each event out to every interested subscriber. It is an ordinary ring-3 process
reached over IPC, like the [VFS server](../system/services/vfs/vfs.zig) — no kernel knows reached over IPC, like the [FAT server](../system/services/fat/fat.zig) — no kernel knows
what a key is. what a key is.
## One service, several device classes ## One service, several device classes
@ -43,10 +43,13 @@ consequences decide the whole design:
2. **A synchronous push can hang the whole service.** If the service delivered with 2. **A synchronous push can hang the whole service.** If the service delivered with
`ipc_call`, it would block until each subscriber replied. `ipc_call` has no timeout, and `ipc_call`, it would block until each subscriber replied. `ipc_call` has no timeout, and
the kernel does **not** wake a caller parked on a *dead* peer's endpoint (it only fails a a subscriber's endpoint is an *unregistered* capability the kernel's death path cannot
peer that was mid-reply — see [process.zig](../system/kernel/process.zig) reach (since display v2's V6, `killOwnedEndpointsLocked` in
`releaseTaskResourcesLocked`). One subscriber that exits mid-delivery would wedge input [ipc-synchronous.zig](../system/kernel/ipc-synchronous.zig) marks a dead owner's
for everyone. That is the opposite of the resilience the microkernel is for. *registered* endpoints dead and wakes parked callers with `-EPEER` — but unregistered
ones just drop with the task's handle table). One subscriber that exits mid-delivery
would wedge input for everyone. That is the opposite of the resilience the microkernel
is for.
The fix is the asynchronous send that [ipc.md](ipc.md) had already earmarked as future The fix is the asynchronous send that [ipc.md](ipc.md) had already earmarked as future
work ("asynchronous / buffered send … for notifications between servers"): work ("asynchronous / buffered send … for notifications between servers"):

View File

@ -9,8 +9,10 @@ reboot is miserable.
This is the machinery that catches those faults and prints what happened instead. This is the machinery that catches those faults and prints what happened instead.
It's all x86_64-specific, so it lives behind the [architecture](architecture.md) boundary in It's all x86_64-specific, so it lives behind the [architecture](architecture.md) boundary in
`system/kernel/architecture/x86_64/`. Only the 32 CPU-defined exception vectors are wired up so far; `system/kernel/architecture/x86_64/`. Only the 32 CPU-defined exception vectors were wired up at
device interrupts (timer, keyboard, via the APIC) come later, on the same IDT. this stage; device interrupts (timer, keyboard, via the APIC) came later, on the
same IDT — today it installs 48 gates (vectors 0-47) plus the ring-3 syscall gate
at vector 128.
## First the GDT ## First the GDT
@ -20,9 +22,11 @@ IDT gate names a code-segment *selector* that must resolve in the current GDT. T
firmware left a GDT in place, but we don't control it, so we install our own with firmware left a GDT in place, but we don't control it, so we install our own with
known selectors: `0x08` kernel code, `0x10` kernel data. known selectors: `0x08` kernel code, `0x10` kernel data.
`system/kernel/architecture/x86_64/gdt.zig` holds three flat descriptors — a required null entry, `system/kernel/architecture/x86_64/gdt.zig` held three flat descriptors at this point — a required
plus code and data — where the only bits that matter in long mode are the access null entry, plus code and data — where the only bits that matter in long mode are
byte and the code segment's long-mode (`L`) flag. Loading it (`gdt_flush` in the access byte and the code segment's long-mode (`L`) flag. (The table has since
grown to seven entries: ring-3 user data and user code descriptors arrived with
user mode, and the TSS descriptor — below — spans two slots.) Loading it (`gdt_flush` in
`isr.s`) does two things: `lgdt`, then reload the segment registers. The data `isr.s`) does two things: `lgdt`, then reload the segment registers. The data
registers take a plain `mov`, but **CS can't** — so we reload it with a far registers take a plain `mov`, but **CS can't** — so we reload it with a far
return, pushing the new selector and a return address and letting `lretq` pop them return, pushing the new selector and a return address and letting `lretq` pop them
@ -34,7 +38,8 @@ The **Interrupt Descriptor Table** maps each of 256 vectors to a handler. Each
entry is a 16-byte *gate* holding the handler's address (split across three entry is a 16-byte *gate* holding the handler's address (split across three
fields, a quirk of the format), the code selector (`0x08`), and flags: `0x8E` fields, a quirk of the format), the code selector (`0x08`), and flags: `0x8E`
means present, ring 0, 64-bit interrupt gate. `system/kernel/architecture/x86_64/idt.zig` builds the means present, ring 0, 64-bit interrupt gate. `system/kernel/architecture/x86_64/idt.zig` builds the
table, points the first 32 vectors at their stubs, and loads it with `lidt` table, points the first 32 vectors at their stubs (since grown to gates 0-47,
plus the ring-3 syscall gate at vector 128), and loads it with `lidt`
(`idt_flush`). (`idt_flush`).
## The TSS and the double-fault stack ## The TSS and the double-fault stack
@ -52,9 +57,11 @@ third time and **triple-fault** — an instant reset. So the #DF gate is pointed
**IST1**, a small dedicated stack (`system/kernel/architecture/x86_64/tss.zig`) that's always valid. **IST1**, a small dedicated stack (`system/kernel/architecture/x86_64/tss.zig`) that's always valid.
Bringing it up: fill in the TSS's IST1 pointer, publish the TSS through a Bringing it up: fill in the TSS's IST1 pointer, publish the TSS through a
descriptor in the GDT (`gdt.setTss`), and load it into the task register with descriptor in the GDT (`gdt.setTssFor`), and load it into the task register with
`ltr`. The TSS descriptor is a 16-byte system descriptor spanning two GDT slots, `ltr`. The TSS descriptor is a 16-byte system descriptor spanning two GDT slots,
which is why the GDT grew from three entries to five. which is why the GDT grew from three entries to five (and later to seven, when
user mode slotted the ring-3 user data/user code descriptors between the kernel
data entry and the TSS pair).
## The stubs and the trap frame ## The stubs and the trap frame
@ -74,12 +81,12 @@ push order in `isr.s`; the two must stay in sync.
The stubs and table-loads are real assembly rather than Zig inline asm because The stubs and table-loads are real assembly rather than Zig inline asm because
they need things inline asm on this toolchain can't express: cross-symbol they need things inline asm on this toolchain can't express: cross-symbol
`jmp`/`call` (a stub jumping to `isr_common`, which calls the exported `jmp`/`call` (a stub jumping to `isr_common`, which calls the exported
`exceptionHandler`), and the `lgdt`/`lidt` memory operands (which LLVM rejects `interruptDispatch`), and the `lgdt`/`lidt` memory operands (which LLVM rejects
inline). `build.zig` adds `isr.s` to the arch module. inline). `build.zig` adds `isr.s` to the arch module.
## Reporting a fault ## Reporting a fault
`isr_common` calls `exceptionHandler`, which forwards to a swappable `on_fault` `isr_common` calls `interruptDispatch`, which forwards to a swappable `on_fault`
hook. The generic kernel installs a reporter (`onException` in `kernel.zig`) that hook. The generic kernel installs a reporter (`onException` in `kernel.zig`) that
prints the exception name and vector, the error code, the faulting RIP prints the exception name and vector, the error code, the faulting RIP
and RSP, and — for a page fault (#PF, vector 14) — the faulting address from and RSP, and — for a page fault (#PF, vector 14) — the faulting address from

View File

@ -25,7 +25,7 @@ ring buffer, a count, and two wait queues:
- **`send(msg)`** — if the channel is full, block on the *not-full* queue; otherwise - **`send(msg)`** — if the channel is full, block on the *not-full* queue; otherwise
write the message, bump the count, and wake a waiting receiver. write the message, bump the count, and wake a waiting receiver.
- **`recv()`** — if the channel is empty, block on the *not-empty* queue; otherwise - **`receive()`** — if the channel is empty, block on the *not-empty* queue; otherwise
take a message, drop the count, and wake a waiting sender. take a message, drop the count, and wake a waiting sender.
Neither side busy-waits: a full channel parks the sender, an empty one parks the Neither side busy-waits: a full channel parks the sender, an empty one parks the
@ -36,17 +36,20 @@ Two details make it correct:
- **Recheck in a loop.** A woken task re-tests the condition (`while (full) wait`) - **Recheck in a loop.** A woken task re-tests the condition (`while (full) wait`)
rather than assuming the slot is still available — another waiter may have taken rather than assuming the slot is still available — another waiter may have taken
it first. This is the standard guard against spurious or racing wakeups. it first. This is the standard guard against spurious or racing wakeups.
- **One critical section.** `send`/`recv` run under `saveInterrupts` / - **One critical section.** `send`/`receive` run under the [big kernel
`restoreInterrupts` (the composable form, see [scheduling.md](scheduling.md)), so lock](smp.md) (`sync.enter` / `sync.leave`), which disables interrupts on this
checking the condition and committing the block/enqueue happen atomically with core *and* takes the kernel's one spinlock — since SMP, the interrupt flag alone
respect to the timer preempting mid-operation. `waitLocked` / `wakeLocked` are the is not atomicity, because `cli` on one core does nothing to another. So checking
variants that assume the caller already holds that critical section. the condition and committing the block/enqueue happen atomically both with respect
to the timer preempting mid-operation and to the other side running on another
CPU. `waitLocked` / `wakeLocked` are the variants that assume the caller already
holds that critical section.
## Verifying it ## Verifying it
The `ipc` test (see [testing.md](testing.md)) runs a producer and a consumer passing The `ipc` test (see [testing.md](testing.md)) runs a producer and a consumer passing
**100 messages through a 4-slot channel**. The small buffer means the channel goes **100 messages through a 4-slot channel**. The small buffer means the channel goes
full and empty over and over, so both the blocking-send and blocking-recv paths are full and empty over and over, so both the blocking-send and blocking-receive paths are
exercised heavily. The messages arrive intact and in order (their sum is the exercised heavily. The messages arrive intact and in order (their sum is the
expected `5050`), and neither task busy-waits — they block and wake each other. expected `5050`), and neither task busy-waits — they block and wake each other.

View File

@ -5,8 +5,11 @@ and every service must run correctly with zero output channels. On top of that
rule, danos has **per-process logging** — every process's output is attributed rule, danos has **per-process logging** — every process's output is attributed
by the kernel and lands in its own file on the flash volume, which is what makes by the kernel and lands in its own file on the flash volume, which is what makes
a headless real machine (no serial port) debuggable. The display (the a headless real machine (no serial port) debuggable. The display (the
framebuffer surface) is a separate concern and deliberately not a log sink; framebuffer surface) is a separate concern with one bootstrap exception: the
`main.zig` mirrors a few user-facing status lines and panics to it explicitly. kernel's framebuffer console (`system/kernel/console.zig`) joins the log sinks
at boot, so the whole transcript shows on screen until the display service
claims the framebuffer and silences it; after that only panic/fatal messages
are mirrored to it explicitly (`system/kernel/kernel.zig`).
## The pipeline ## The pipeline
@ -37,7 +40,8 @@ kernel log.print ─┘ │
8-byte aligned. 8-byte aligned.
4. **Render.** Registered sinks (serial under `-Dserial`, the 0xE9 debug 4. **Render.** Registered sinks (serial under `-Dserial`, the 0xE9 debug
console) get a live transcript: kernel/raw output verbatim, leveled records console, and the framebuffer console until the display service claims the
screen) get a live transcript: kernel/raw output verbatim, leveled records
as `<binary path>: message` — one composed write per line, under the log's as `<binary path>: message` — one composed write per line, under the log's
own spinlock (never the big kernel lock; panic paths try-acquire with a own spinlock (never the big kernel lock; panic paths try-acquire with a
bound and fall back to sinks-only). Sinks are best-effort and self-guarding; bound and fall back to sinks-only). Sinks are best-effort and self-guarding;

View File

@ -57,22 +57,27 @@ itself, `@sizeOf` is authoritative: the kernel walks a plain `[]MemoryRegion` wi
no variable-stride subtlety (that stride problem is a UEFI-ism, and it stays in the no variable-stride subtlety (that stride problem is a UEFI-ism, and it stays in the
loader). loader).
`BootInfo` carries it alongside the framebuffer: `BootInformation` carries it alongside the framebuffer (trimmed here to the
fields this page is about — the full struct has since grown the kernel's
PT_LOAD segments, the ACPI RSDP, and the initial-ramdisk span):
```zig ```zig
pub const BootInfo = extern struct { pub const BootInformation = extern struct {
framebuffer: Framebuffer, framebuffer: Framebuffer,
memory_map: MemoryMap, memory_map: MemoryMap,
// ...kernel_segments, acpi_rsdp, initial_ramdisk_base/len
}; };
``` ```
## The loader side (UEFI) ## The loader side (UEFI)
Two functions in `boot/efi.zig`, called from `exitBootServices`: Two functions in `boot/efi.zig`: `exitBootServices` calls `convertMemoryMap`,
which runs `classify` on each descriptor:
- **`classify`** maps each UEFI descriptor to a `MemoryKind`: - **`classify`** maps each UEFI descriptor to a `MemoryKind`:
`conventional_memory` **and** `boot_services_code`/`boot_services_data → usable`; `conventional_memory` **and** `boot_services_code`/`boot_services_data → usable`;
`acpi_reclaim_memory → acpi_tables`; `acpi_memory_nvs → acpi_nvs`; **everything `acpi_reclaim_memory → acpi_tables`; `acpi_memory_nvs → acpi_nvs`;
`memory_mapped_io`/`memory_mapped_io_port_space → mmio`; **everything
else → reserved** (the safe default). Our own `loader_data` — the kernel image and else → reserved** (the safe default). Our own `loader_data` — the kernel image and
these buffers — falls into `reserved`. these buffers — falls into `reserved`.
@ -120,18 +125,25 @@ for the conversion.)
The kernel receives a plain array and reads it with zero UEFI knowledge: The kernel receives a plain array and reads it with zero UEFI knowledge:
```zig ```zig
const mm = boot_info.memory_map; const mm = boot_information.memory_map;
const regions = @as([*]const system.MemoryRegion, @ptrFromInt(mm.regions))[0..mm.len]; const regions = @as(
[*]const boot_handoff.MemoryRegion,
@ptrFromInt(boot_handoff.physicalToVirtual(mm.regions)),
)[0..mm.len];
for (regions) |r| { for (regions) |r| {
if (r.kind == .usable) usable_pages += r.pages; if (r.kind == .usable) usable_pages += r.pages;
} }
``` ```
(`mm.regions` is a physical address, so it's dereferenced through the physmap —
`physicalToVirtual` — since the kernel no longer runs under the loader's
identity map.)
`kmain` summarises the map to prove the handoff works. Booted in QEMU with `kmain` summarises the map to prove the handoff works. Booted in QEMU with
128 MiB, it reports: 128 MiB, it reports:
``` ```
danos: physical memory /system/kernel: physical memory
total RAM : 0.12 GiB (127 MiB) - RAM the firmware reported total RAM : 0.12 GiB (127 MiB) - RAM the firmware reported
usable : 121 MiB - free RAM (incl. reclaimed boot-services memory) usable : 121 MiB - free RAM (incl. reclaimed boot-services memory)
reserved : 6 MiB - kernel image, boot stack, ACPI, runtime services reserved : 6 MiB - kernel image, boot stack, ACPI, runtime services

View File

@ -14,8 +14,12 @@ behind the [architecture](architecture.md) boundary in `system/kernel/architectu
x86_64 uses **4 levels**: PML4 → PDPT → PD → PT, each a 512-entry table, with 9 x86_64 uses **4 levels**: PML4 → PDPT → PD → PT, each a 512-entry table, with 9
bits of the virtual address indexing each level and the low 12 bits the offset into bits of the virtual address indexing each level and the low 12 bits the offset into
the final 4 KiB page. Each entry holds a physical address plus flag bits — the final 4 KiB page. Each entry holds a physical address plus flag bits —
present, writable, and (bit 63) **no-execute**. danos maps everything with 4 KiB present, writable, and (bit 63) **no-execute**. danos maps nearly everything with
pages: precise, and the extra table memory is negligible against available RAM. 4 KiB pages: precise, and the extra table memory is negligible against available
RAM. (The physmap has since become the one exception: 2 MiB-aligned RAM there is
mapped with **2 MiB huge pages** — a PS-bit leaf at the PD level — with 4 KiB
pages filling the unaligned edges, so the table footprint scales sanely with big
RAM. Kernel segments, heap, user space, and on-demand MMIO stay 4 KiB.)
## Higher half: the address-space layout ## Higher half: the address-space layout
@ -27,7 +31,7 @@ address to the low load address in its bootstrap tables and jumps in). The entir
alongside a **physmap** — a straight window onto all of physical memory at alongside a **physmap** — a straight window onto all of physical memory at
`physmap_base + phys`. Wherever the kernel needs to touch a physical address (a `physmap_base + phys`. Wherever the kernel needs to touch a physical address (a
page-table frame, an ACPI table, a device register), it adds that constant: page-table frame, an ACPI table, a device register), it adds that constant:
`system.physToVirt(phys)`. The layout constants live in `system/boot-handoff.zig`: `boot_handoff.physicalToVirtual(phys)`. The layout constants live in `system/boot-handoff.zig`:
| region | virtual base | PML4 slot | | region | virtual base | PML4 slot |
|--------|--------------|-----------| |--------|--------------|-----------|
@ -46,7 +50,7 @@ builds its own precise tables below and abandons them. Because both use the same
The address space is built in four passes (`init`): The address space is built in four passes (`init`):
1. **All RAM in the physmap, RW + NX.** Every non-MMIO region from the 1. **All RAM in the physmap, RW + NX.** Every non-MMIO region from the
[memory map](memory-map.md) is mapped at `physToVirt(phys)`, read-write and [memory map](memory-map.md) is mapped at `physicalToVirtual(phys)`, read-write and
*non-executable*. There is **no low/identity mapping** — the low half is user *non-executable*. There is **no low/identity mapping** — the low half is user
space. (Frames the kernel touches while still building these tables are reached space. (Frames the kernel touches while still building these tables are reached
through the loader's bootstrap physmap, which covers the low 4 GiB; both the through the loader's bootstrap physmap, which covers the low 4 GiB; both the
@ -72,7 +76,7 @@ Blanket RW+NX is fine for data but wrong for the kernel's own code, which must b
executable — and its code must *not* be writable (W^X: no page is both). We get the executable — and its code must *not* be writable (W^X: no page is both). We get the
right permissions per region straight from the kernel ELF: the **loader already right permissions per region straight from the kernel ELF: the **loader already
parses the program headers**, so `efi.zig` records each `PT_LOAD` segment's parses the program headers**, so `efi.zig` records each `PT_LOAD` segment's
address, size and R/W/X flags into `BootInfo`. Pass 3 re-maps those ranges with address, size and R/W/X flags into `BootInformation`. Pass 3 re-maps those ranges with
flags derived from the ELF flags: flags derived from the ELF flags:
| segment | ELF flags | mapped as | | segment | ELF flags | mapped as |
@ -97,9 +101,10 @@ real memory — turning a whole class of silent bugs into an immediate, located
## Switching on, and the on-demand API ## Switching on, and the on-demand API
Loading the PML4's physical address into **CR3** switches address spaces and Loading the PML4's physical address into **CR3** switches address spaces and
flushes the TLB in one step. This works because the firmware's identity map is flushes the TLB in one step. This works because *while we build* the kernel is
still active *while we build*, so freshly allocated table frames are reachable by still running on the loader's bootstrap tables, whose 4 GiB physmap makes freshly
physical address; afterwards they're covered by pass 1. allocated table frames reachable at the same `physmap_base + phys` addresses;
afterwards they're covered by pass 1.
`init` keeps the PML4 and the frame allocator around and exposes `map(virt, phys, `init` keeps the PML4 and the frame allocator around and exposes `map(virt, phys,
writable)` / `unmap(virt)` (with `invlpg` TLB invalidation) — the primitive the writable)` / `unmap(virt)` (with `invlpg` TLB invalidation) — the primitive the

View File

@ -83,16 +83,18 @@ On a `power_button` event or a `terminate` signal, init:
3. requests `.power` `shutdown`. 3. requests `.power` `shutdown`.
The service then enters **S5** (soft off) by writing `SLP_TYP | SLP_EN` to the The service then enters **S5** (soft off) by writing `SLP_TYP | SLP_EN` to the
PM1 control register(s) from ring 3, mirroring the kernel's own PM1 control register(s) from ring 3, with the `SLP_TYP` values taken from its own
`system/devices/power.zig` `sleepValue`. If the write returns instead of powering AML parse of the `_S5` object — the kernel has no S5 path of its own. If the
the machine off, it logs loudly so a test fails rather than hangs. write returns instead of powering the machine off, it logs loudly so a test
fails rather than hangs.
**No new system call was needed for S5.** The broad io_port grant on the **No new system call was needed for S5.** The broad io_port grant on the
`acpi-tables` node ([discovery.md](discovery.md)) already put the PM1 control `acpi-tables` node ([discovery.md](discovery.md)) already put the PM1 control
ports in the acpi service's hands, so writing S5 from ring 3 is something it ports in the acpi service's hands, so writing S5 from ring 3 is something it
could physically already do; formalizing it as a protocol operation added a could physically already do; formalizing it as a protocol operation added a
contract, not authority. The kernel keeps `power.zig` for its own test paths and contract, not authority. The kernel's `system/devices/power.zig` keeps only
panic-time poweroff, where no user space is available to ask. **reboot** (the FADT reset register plus the legacy fallbacks, which need no AML);
it has no poweroff path at all — S5 is not a kernel operation.
## Verifying it ## Verifying it

View File

@ -55,10 +55,14 @@ pattern reused. Signals are the same pattern reused a third time.
- **`process_signal(id, signal)`** — posts the signal as an asynchronous - **`process_signal(id, signal)`** — posts the signal as an asynchronous
notification to the target's bound endpoint: badge = `notify_badge_bit | notification to the target's bound endpoint: badge = `notify_badge_bit |
notify_signal_bit | pending signals`. Non-blocking for the sender, always. notify_signal_bit | pending signals`. Non-blocking for the sender, always.
- **Pending signals coalesce** in a per-process bitmask until the target next waits - **Pending signals coalesce** in a per-process bitmask while the target has no
— exactly like interrupt notifications, and exactly POSIX's own semantics for signal endpoint bound, and the whole mask arrives as one notification at bind —
non-realtime signals (two pending SIGTERMs are one SIGTERM). The bitmask *is* the POSIX's own semantics for non-realtime signals (two pending SIGTERMs are one
design: signals carry no payload. Anything with a payload is a protocol message. SIGTERM). Once bound, each `process_signal` flushes the mask straight into the
endpoint's notification ring, so a busy receiver drains separate posts as
separate notifications — harmless, because the badge is a set of bits, never a
count. The bitmask *is* the design: signals carry no payload. Anything with a
payload is a protocol message.
- **Authority**: the supervisor may signal its children — the same link that is - **Authority**: the supervisor may signal its children — the same link that is
already the kill authority. A process may signal itself. Anything broader waits already the kill authority. A process may signal itself. Anything broader waits
for transferable process handles. for transferable process handles.
@ -85,7 +89,7 @@ defense below the table.
| SIGUSR1, SIGUSR2 | signals `user_1`, `user_2` | service-defined | | SIGUSR1, SIGUSR2 | signals `user_1`, `user_2` | service-defined |
| SIGCHLD | **already exists** — the exit notification | the badge carries the child id, dodging the classic coalescing bug (Unix code must loop `waitpid`) | | SIGCHLD | **already exists** — the exit notification | the badge carries the child id, dodging the classic coalescing bug (Unix code must loop `waitpid`) |
| SIGKILL | `process_kill` — kernel mechanism | its definition is "cannot be handled"; it was never really a signal | | SIGKILL | `process_kill` — kernel mechanism | its definition is "cannot be handled"; it was never really a signal |
| SIGABRT | exit reason `abort` | `abort()` is synchronous self-termination, not an event | | SIGABRT | exit reason `aborted` | synchronous self-termination is an exit, not an event; recorded for any nonzero exit code |
| SIGSEGV, SIGILL, SIGFPE | exit reasons, **never delivered** | see below | | SIGSEGV, SIGILL, SIGFPE | exit reasons, **never delivered** | see below |
| SIGPIPE | **an error return**, not a signal | see below | | SIGPIPE | **an error return**, not a signal | see below |
| SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU, SIGCONT | deferred | job control needs terminals, sessions, and process groups; stop/continue is scheduler territory | | SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU, SIGCONT | deferred | job control needs terminals, sessions, and process groups; stop/continue is scheduler territory |
@ -135,18 +139,21 @@ deadline.
1. **Cleanup is the kernel's job.** A process can die with no warning — fault, 1. **Cleanup is the kernel's job.** A process can die with no warning — fault,
kill, power. Correctness must never depend on a `terminate` handler running. On kill, power. Correctness must never depend on a `terminate` handler running. On
any death the kernel releases the address space, IPC handles, IRQ bindings, and any death the kernel releases the address space, IPC handles, IRQ bindings,
owed replies (built), and must also release **device, I/O-port, and interrupt owed replies, and **device, I/O-port, and interrupt claims and MSI vectors**
claims and MSI vectors** (the known gap in the last of these was once the known gap in
[process-management.md](process-management.md); increment 1). A signal handler is [process-management.md](process-management.md), closed by increment 1
(`releaseTaskResourcesLocked`, on every death path). A signal handler is
for *graceful* work — flushing, deregistering, saving — never for *necessary* for *graceful* work — flushing, deregistering, saving — never for *necessary*
work. work.
2. **Kill is not a signal, and exit reasons are load-bearing.** The standard stop 2. **Kill is not a signal, and exit reasons are load-bearing.** The standard stop
sequence is *terminate → deadline → `process_kill`*; the unhandleable kill stays sequence is *terminate → deadline → `process_kill`*; the unhandleable kill stays
a kernel mechanism. And a supervisor deciding whether to restart must know *how* a kernel mechanism. And a supervisor deciding whether to restart must know *how*
the child died: clean exit (meant to — don't restart), fault (restart with the child died: clean exit (meant to — don't restart), fault (restart with
backoff), killed (the supervisor did it). The exit notification today carries backoff), killed (the supervisor did it). The exit notification carries only the
only the id; it grows a reason. Restart policy cannot be written without it. id; the reason is recorded before the notification posts and read with the
supervisor-gated `process_exit_reason` query. Restart policy cannot be
written without it.
## Who learns of a death ## Who learns of a death
@ -154,7 +161,8 @@ A death has three audiences, and conflating them is how systems end up with eith
zombie state or privileged snooping: zombie state or privileged snooping:
1. **The supervisor** — gets the exit notification on the endpoint it gave at spawn 1. **The supervisor** — gets the exit notification on the endpoint it gave at spawn
(built), which grows the `ExitReason` (increment 2). The supervisor is the only (built), then reads the `ExitReason` with the `process_exit_reason` query
(increment 2). The supervisor is the only
audience that needs the *reason*, because it is the only one deciding whether to audience that needs the *reason*, because it is the only one deciding whether to
restart. restart.
2. **The peer owed a reply** — already built: a client that dies mid-request fails 2. **The peer owed a reply** — already built: a client that dies mid-request fails
@ -162,10 +170,11 @@ zombie state or privileged snooping:
the same way. This covers the *synchronous* case only. the same way. This covers the *synchronous* case only.
3. **The subscribers** — the new piece, and it is the input service's 3. **The subscribers** — the new piece, and it is the input service's
publish/subscribe shape ([input.md](input.md)) applied to exits. A stateful publish/subscribe shape ([input.md](input.md)) applied to exits. A stateful
service accumulates per-client state across many requests: the VFS holds a dead service accumulates per-client state across many requests: a filesystem server
client's open file handles, the input service holds its subscriptions, a future (FAT today) holds a dead client's open file handles, the input service holds
network stack holds its sockets. None of these are the client's supervisor, and its subscriptions, a future network stack holds its sockets. None of these
none learn anything from a failed reply if the client simply never calls again. are the client's supervisor, and none learn anything from a failed reply if
the client simply never calls again.
So the kernel **publishes every exit** to whoever subscribed: So the kernel **publishes every exit** to whoever subscribed:
`process_subscribe(endpoint)` adds a subscriber, and each death posts a `process_subscribe(endpoint)` adds a subscriber, and each death posts a
notification to every subscriber (badge = `notify_exit_bit | process id` — the notification to every subscriber (badge = `notify_exit_bit | process id` — the
@ -180,7 +189,8 @@ zombie state or privileged snooping:
non-blocking coalescing notification as everything else — a dying process never non-blocking coalescing notification as everything else — a dying process never
waits on its mourners. Subscribing is ungated, like `process_enumerate`: what is waits on its mourners. Subscribing is ungated, like `process_enumerate`: what is
running (and dying) is not a secret between cooperating processes. Subscribers running (and dying) is not a secret between cooperating processes. Subscribers
do not receive the exit reason — the VFS does not care *why* the client died. do not receive the exit reason — the filesystem server does not care *why*
the client died.
This is the service-side mirror of iron rule 1: **a service must never depend on This is the service-side mirror of iron rule 1: **a service must never depend on
its clients cleaning up after themselves.** Handle release on client death is the its clients cleaning up after themselves.** Handle release on client death is the
@ -221,7 +231,6 @@ pub const Signal = enum(u5) {
pub const SignalSet = struct { pub const SignalSet = struct {
pending: u32, pending: u32,
pub fn has(set: SignalSet, signal: Signal) bool { ... } pub fn has(set: SignalSet, signal: Signal) bool { ... }
pub fn iterate(set: SignalSet) Iterator { ... }
}; };
/// Nominate `endpoint` as this process's signal endpoint (signal_bind). The /// Nominate `endpoint` as this process's signal endpoint (signal_bind). The
@ -231,14 +240,15 @@ pub fn bindSignals(endpoint: usize) bool { ... }
/// Decode a received badge into signals, or null if the badge is not a signal /// Decode a received badge into signals, or null if the badge is not a signal
/// notification (mirrors ipc.Received.isChildExit). /// notification (mirrors ipc.Received.isChildExit).
pub fn signalsFrom(badge: usize) ?SignalSet { ... } pub fn signalsFrom(badge: u64) ?SignalSet { ... }
/// Send `signal` to process `id`. Supervisor-gated, like kill; non-blocking. /// Send `signal` to process `id`. Supervisor-gated, like kill; non-blocking.
pub fn sendSignal(id: u32, signal: Signal) bool { ... } pub fn sendSignal(id: u32, signal: Signal) bool { ... }
/// The standard stop sequence: terminate, wait up to `deadline_ms` for the exit /// The standard stop sequence: terminate, wait up to `deadline_ms` for the exit
/// notification, then process_kill. The one call a supervisor needs. /// notification on `exit_endpoint` (the endpoint the child was spawned with),
pub fn stop(id: u32, deadline_ms: u64) void { ... } /// then process_kill. The one call a supervisor needs.
pub fn stop(id: u32, deadline_ms: u64, exit_endpoint: usize) void { ... }
/// Subscribe `endpoint` to published exit events (process_subscribe). Every /// Subscribe `endpoint` to published exit events (process_subscribe). Every
/// process death posts an asynchronous notification: badge = notify_exit_bit | /// process death posts an asynchronous notification: badge = notify_exit_bit |
@ -252,7 +262,7 @@ pub fn subscribeExits(endpoint: usize) bool { ... }
/// it first, so the two never race). What restart policy reads. (Built in M17.2.) /// it first, so the two never race). What restart policy reads. (Built in M17.2.)
pub const ExitReason = enum(u8) { pub const ExitReason = enum(u8) {
exited, // returned from main / clean exit exited, // returned from main / clean exit
aborted, // abort() — deliberate self-termination (SIGABRT's ghost; reserved) aborted, // deliberate failure exit — any nonzero exit code (SIGABRT's ghost)
segmentation_fault, // SIGSEGV's ghost segmentation_fault, // SIGSEGV's ghost
illegal_instruction, // SIGILL's ghost illegal_instruction, // SIGILL's ghost
arithmetic_fault, // SIGFPE's ghost arithmetic_fault, // SIGFPE's ghost
@ -299,8 +309,10 @@ get POSIX; danos-native programs never pay for it.
kill a claiming driver, spawn it again, the claim succeeds. kill a claiming driver, spawn it again, the claim succeeds.
2. **Exit reason in the death notification** (`ExitReason` above). 2. **Exit reason in the death notification** (`ExitReason` above).
3. **Exit events**: `process_subscribe` in the kernel (bounded subscriber table, 3. **Exit events**: `process_subscribe` in the kernel (bounded subscriber table,
publishes on every death), `runtime.process.subscribeExits`; the VFS becomes the publishes on every death), `runtime.process.subscribeExits`; the userspace VFS
first subscriber — releasing a dead client's handles is its proof test. router was the first subscriber — releasing a dead client's handles was its
proof test — and the FAT server inherited the role when the router moved into
the kernel (clients now hold the filesystem server's node ids directly).
4. **Signals**: `signal_bind` + `process_signal` + the pending mask in the kernel; 4. **Signals**: `signal_bind` + `process_signal` + the pending mask in the kernel;
`runtime.process` grows the interface above; the service harness handles `runtime.process` grows the interface above; the service harness handles
`terminate` and answers the common `ping`; `stop()` for supervisors. `terminate` and answers the common `ping`; `stop()` for supervisors.

View File

@ -12,13 +12,15 @@ made the file tree the whole interface (`echo kill > /proc/n/ctl`). Microkernels
mostly abandon ambient PIDs: Minix and QNX route everything through a user-space mostly abandon ambient PIDs: Minix and QNX route everything through a user-space
process-manager server, and Fuchsia/seL4 control processes only through handles. process-manager server, and Fuchsia/seL4 control processes only through handles.
danos rules out `/proc` **as the primitive**: here a `/proc` would be served by danos rules out `/proc` **as the primitive**: the path router lives in the
the VFS server — a user process — which would put the VFS in the path of process kernel (`fs_resolve`), but what is mounted under a path is served by a
control. If the VFS (or anything under it) hangs, nothing could be listed or user-process filesystem server (the way FAT serves `/mnt/usb`) — a `/proc`
killed, *including the hung VFS*. The control plane for processes must not would be one more such server, which would put a user process in the path of
depend on a process. So the primitives are kernel system calls; a read-only process control. If that server (or anything under it) hangs, nothing could be
`/proc` rendering can be layered on later, and a POSIX-style process-manager listed or killed, *including the hung server*. The control plane for processes
server can be built *from* these primitives when one is needed. must not depend on a process. So the primitives are kernel system calls; a
read-only `/proc` rendering can be layered on later, and a POSIX-style
process-manager server can be built *from* these primitives when one is needed.
## The three primitives ## The three primitives
@ -100,7 +102,11 @@ the architecture layer calls up into `tick`.
(`releaseTaskResourcesLocked`), so a restarted driver can claim its hardware (`releaseTaskResourcesLocked`), so a restarted driver can claim its hardware
again — the cleanup half of [process-lifecycle.md](process-lifecycle.md)'s iron again — the cleanup half of [process-lifecycle.md](process-lifecycle.md)'s iron
rule 1. The `claim-release` test proves the kill → release → re-claim cycle. rule 1. The `claim-release` test proves the kill → release → re-claim cycle.
- Kernel stacks of dead tasks are leaked, as on every exit path (no reaper yet). - ~~Kernel stacks of dead tasks are leaked~~ Closed (threading-plan M8): a task
exiting on its own core queues on the core's reap list in `.reaping` state, and
the next switch away (or tick) frees its kernel stack; one killed while off-CPU
has its stack freed synchronously by the reap itself. Both paths are accounted
by `live_stack_bytes`, which returns to baseline when no extra tasks are live.
- ~~There is no exit status in the notification~~ Closed (M17.2): the kernel - ~~There is no exit status in the notification~~ Closed (M17.2): the kernel
records how every process ends — exited, a fault class, or killed — before it records how every process ends — exited, a fault class, or killed — before it
posts the exit notification, and the supervisor reads it with posts the exit notification, and the supervisor reads it with

View File

@ -6,7 +6,7 @@ ready task always runs, and tasks at the same priority take turns. That model is
chosen for [real-time](vision.md) — it's predictable (you can reason about which chosen for [real-time](vision.md) — it's predictable (you can reason about which
task runs when) and its decisions are O(1), unlike a fair-share scheduler. task runs when) and its decisions are O(1), unlike a fair-share scheduler.
The scheduler proper (`system/kernel/sched.zig`) is generic; the context switch and new-task The scheduler proper (`system/kernel/scheduler.zig`) is generic; the context switch and new-task
stack setup are architecture-specific (`system/kernel/architecture/x86_64/`, see [architecture](architecture.md)). stack setup are architecture-specific (`system/kernel/architecture/x86_64/`, see [architecture](architecture.md)).
## Tasks ## Tasks

View File

@ -1,10 +1,11 @@
# SMP: multiple cores, the microkernel way # SMP: multiple cores, the microkernel way
A design/research note, not built yet. danos runs on **one core** today (see A design/research note that predates the build — danos now runs on **multiple
[scheduling.md](scheduling.md)); this maps how microkernels — especially the L4 cores** by default (see [Implementation status](#implementation-status) and
family and seL4 — handle **symmetric multiprocessing (SMP)**, so the eventual port [scheduling.md](scheduling.md)). This maps how microkernels — especially the L4
has a plan and a reading list. It also flags where those choices depend on whether family and seL4 — handle **symmetric multiprocessing (SMP)**, the plan and
danos is chasing **real-time** or **resilience** (see the note at the end). reading list the port followed. It also flags where those choices depend on
whether danos is chasing **real-time** or **resilience** (see the note at the end).
## First, the vocabulary ## First, the vocabulary
@ -158,11 +159,15 @@ next lands.
`ipc.zig` run every critical section under it. Uncontended on one core, so behaviour `ipc.zig` run every critical section under it. Uncontended on one core, so behaviour
is identical to the old interrupt-flag model. is identical to the old interrupt-flag model.
- **Per-CPU state** — a `PerCpu` struct (running task, idle task, APIC id) per core, - **Per-CPU state** — a `PerCpu` struct (running task, idle task, APIC id) per core,
its pointer kept in the x86 **GS base** (`IA32_GS_BASE`; no `swapgs`, since there's its pointer kept in the x86 **GS base** (`IA32_GS_BASE`). No `swapgs` was needed at
no user mode yet). The old global `current` is now `thisCpu().current`. The ready the time — there was no user mode yet; with ring 3 in place, every ring transition
now swaps it against the user's own GS base under the `swapgs` discipline (see
`system/kernel/architecture/x86_64/per-cpu.zig`), and each AP enables the fast
system-call path (`initSystemCall`) for itself at bring-up. The old global
`current` is now `thisCpu().current`. The ready
queues stay **global** under the lock — work-conserving, so any idle core will pull queues stay **global** under the lock — work-conserving, so any idle core will pull
the highest-priority ready task; per-core queues are a later optimisation. the highest-priority ready task; per-core queues are a later optimisation.
- **AP wake to long mode**`arch.startSecondary` drives INITSIPISIPI (via the - **AP wake to long mode**`architecture.startSecondary` drives INITSIPISIPI (via the
LAPIC ICR) to wake each parked core one at a time. A woken core starts in 16-bit LAPIC ICR) to wake each parked core one at a time. A woken core starts in 16-bit
real mode at a low page and runs the [trampoline](../system/kernel/architecture/x86_64/trampoline.s) real mode at a low page and runs the [trampoline](../system/kernel/architecture/x86_64/trampoline.s)
up through protected mode into 64-bit long mode, then lands in `smp.zig:apEntry`, up through protected mode into 64-bit long mode, then lands in `smp.zig:apEntry`,
@ -203,8 +208,9 @@ next lands.
[scheduling.md](scheduling.md#affinity-pinning-a-task-to-a-core)). The `affinity` [scheduling.md](scheduling.md#affinity-pinning-a-task-to-a-core)). The `affinity`
test confirms a pinned task never migrates. This is the mechanism the fault-on-AP test confirms a pinned task never migrates. This is the mechanism the fault-on-AP
test rides on, and the *explicit-affinity* real-time-predictable model. test rides on, and the *explicit-affinity* real-time-predictable model.
- **Right-sized footprint** — the per-CPU ceiling (`system.max_cpus`, one constant - **Right-sized footprint** — the per-CPU ceiling (`parameters.maximum_cpus`, one
shared by discovery, the scheduler, and the per-core GDT/TSS) is generous (128), but constant shared by discovery, the scheduler, and the per-core GDT/TSS) is generous
(128), but
the *large* per-core resources — the kernel and IST (double-fault) stacks — are the *large* per-core resources — the kernel and IST (double-fault) stacks — are
**heap-allocated at bring-up**, only for cores that actually come online. Only the **heap-allocated at bring-up**, only for cores that actually come online. Only the
BSP's IST stack is static, because it must exist before the frame allocator does. BSP's IST stack is static, because it must exist before the frame allocator does.
@ -217,7 +223,7 @@ next lands.
life, but kept **inert between wakes**: zeroed and non-executable, armed (blob life, but kept **inert between wakes**: zeroed and non-executable, armed (blob
copied in, page made executable) only for the moment a core is actually climbing, copied in, page made executable) only for the moment a core is actually climbing,
then disarmed again. So there's never a dormant executable page, and a core can be then disarmed again. So there's never a dormant executable page, and a core can be
(re)woken at any time — `arch.startSecondary` is one self-contained attempt (arm → (re)woken at any time — `architecture.startSecondary` is one self-contained attempt (arm →
INITSIPISIPI → disarm), and its `INIT` resets a wedged core, so retrying just INITSIPISIPI → disarm), and its `INIT` resets a wedged core, so retrying just
works. Boot retries a non-responding core up to three times; the same primitive is works. Boot retries a non-responding core up to three times; the same primitive is
the groundwork a future **power manager** would drive to bring cores up (and, the groundwork a future **power manager** would drive to bring cores up (and,

View File

@ -81,10 +81,11 @@ they are guidance, not a tested-hardware list.)
- **Firmware must be UEFI.** Many 2011-era machines could do either UEFI or - **Firmware must be UEFI.** Many 2011-era machines could do either UEFI or
legacy BIOS — danos needs it set to UEFI. There is no BIOS boot path. legacy BIOS — danos needs it set to UEFI. There is no BIOS boot path.
- **Input is PS/2 only, for now.** danos does not yet support USB - **Internal disks are invisible.** The only block-device driver is USB mass
keyboards/mice. This is fine on most **laptops** (their built-in keyboards are storage (`usb-storage`) — there is no AHCI / NVMe / IDE driver — so danos
wired to a PS/2-style i8042 controller) but means a **desktop with only USB boots from and stores to a **USB stick**, not the machine's internal
ports** currently has no usable keyboard. USB HID input is planned. SATA/NVMe disk. Keyboards and mice are fine either way: both PS/2 (most
laptops' built-in keyboards) and USB HID work.
Virtual machines are the easiest way to meet every requirement: QEMU (with OVMF/ Virtual machines are the easiest way to meet every requirement: QEMU (with OVMF/
UEFI, a `qemu-xhci` controller, and the default Q35 machine type), or any UEFI, a `qemu-xhci` controller, and the default Q35 machine type), or any
@ -94,40 +95,41 @@ hypervisor configured for UEFI firmware and an xHCI USB controller.
| Requirement | Detail | Source | | Requirement | Detail | Source |
|---|---|---| |---|---|---|
| **x86-64, 64-bit only** | Kernel and loader are built exclusively for `x86_64`; the loader rejects any non-x86-64 kernel ELF (`error.WrongArchitecture`). | `build.zig:285`, `boot/efi.zig:418` | | **x86-64, 64-bit only** | Kernel and loader are built exclusively for `x86_64`; the loader rejects any non-x86-64 kernel ELF (`error.WrongArchitecture`). | `build.zig:481`, `boot/efi.zig:622` |
| **Long mode + PAE + NX** | AP trampoline sets `CR4.PAE`, `EFER.LME`, `EFER.NXE`; NX is used in kernel page-table entries. | `system/kernel/architecture/x86_64/trampoline.s:62` | | **Long mode + PAE + NX** | AP trampoline sets `CR4.PAE`, `EFER.LME`, `EFER.NXE`; NX is used in kernel page-table entries. | `system/kernel/architecture/x86_64/trampoline.s:62` |
| **SSE / SSE2** | Baseline: the compiler emits SSE for ordinary struct copies. Trampoline enables `CR4.OSFXSR` + `OSXMMEXCPT` and clears `CR0.EM`. | `build.zig:282`, `trampoline.s:62` | | **SSE / SSE2** | Baseline: the compiler emits SSE for ordinary struct copies. Trampoline enables `CR4.OSFXSR` + `OSXMMEXCPT` and clears `CR0.EM`. | `build.zig:477`, `trampoline.s:62` |
| **`syscall` / `sysret`** | Primary user↔kernel entry path. `EFER.SCE` enabled; `STAR`/`LSTAR`/`SFMASK` programmed per core. (`int 0x80` exists as a parallel gate.) | `architecture/x86_64/per-cpu.zig:59`, `isr.s:169` | | **`syscall` / `sysret`** | Primary user↔kernel entry path. `EFER.SCE` enabled; `STAR`/`LSTAR`/`SFMASK` programmed per core. (`int 0x80` exists as a parallel gate.) | `architecture/x86_64/per-cpu.zig:71`, `isr.s:196` |
| **Local APIC (xAPIC)** | LAPIC accessed via MMIO at `0xFEE00000`. LAPIC ID read as a `u8` — classic xAPIC. **x2APIC is not supported** (no MSR path). | `apic.zig:62`, `apic.zig:414` | | **Local APIC (xAPIC)** | LAPIC accessed via MMIO at `0xFEE00000`. LAPIC ID read as a `u8` — classic xAPIC. **x2APIC is not supported** (no MSR path). | `apic.zig:67`, `apic.zig:646` |
| **CPUID + RDTSC** | CPUID leaf `0x15` for TSC frequency; RDTSC is the monotonic clock. | `apic.zig:279`, `apic.zig:84` | | **CPUID + RDTSC** | CPUID leaf `0x15` for TSC frequency; RDTSC is the monotonic clock. | `apic.zig:333`, `apic.zig:113` |
| **SMP (optional)** | Multi-core supported via INITSIPISIPI; ceiling `maximum_cpus = 128`. Single core is fine. Cores beyond the ceiling are parked. | `system/parameters.zig:16`, `apic.zig:144` | | **SMP (optional)** | Multi-core supported via INITSIPISIPI; ceiling `maximum_cpus = 128`. Single core is fine. Cores beyond the ceiling are parked. | `system/parameters.zig:16`, `apic.zig:173` |
## Firmware / boot ## Firmware / boot
- **UEFI only.** A custom UEFI application loader is installed to - **UEFI only.** A custom UEFI application loader is installed to
`\EFI\BOOT\BOOTX64.efi`. There is **no BIOS, multiboot, or limine** path. The `\EFI\BOOT\BOOTX64.efi`. There is **no BIOS, multiboot, or limine** path. The
loader tolerates UEFI Class-3 machines with no legacy PIC/PIT. loader tolerates UEFI Class-3 machines with no legacy PIC/PIT.
(`build.zig:464`, `boot/efi.zig`) (`build.zig:246`, `boot/efi.zig`)
- **ACPI is the hardware-discovery mechanism.** The RSDP is taken from the UEFI - **ACPI is the hardware-discovery mechanism.** The RSDP is taken from the UEFI
configuration table (ACPI 2.0 GUID preferred, 1.0 fallback). Without a valid configuration table (ACPI 2.0 GUID preferred, 1.0 fallback). Without a valid
RSDP there is **no device discovery** — no SMP, no IOAPIC routing, no PCI/USB. RSDP there is **no device discovery** — no SMP, no IOAPIC routing, no PCI/USB.
(`efi.zig:578`, `boot-handoff.zig:144`) (`efi.zig:790`, `boot-handoff.zig:149`)
- **Required ACPI tables:** MADT (interrupt topology), MCFG (PCIe ECAM base), - **Required ACPI tables:** MADT (interrupt topology), MCFG (PCIe ECAM base),
FADT (power / PM timer). Optionally consumed: HPET, DMAR, SPCR. FADT (power / PM timer). Optionally consumed: HPET, DMAR, SPCR.
(`system/devices/acpi.zig:3`) (`system/devices/acpi.zig:3`)
- The loader reads `/system/kernel`, `/system/services/init`, and - The loader reads `/system/kernel` off the FAT boot volume, then loads user
`/boot/initial-ramdisk.img` off the FAT boot volume. The kernel can boot space: a prebuilt `boot\system.img` capsule when present, otherwise it walks
"kernel-only" without init or the ramdisk. (`efi.zig:14`, `efi.zig:66`) the volume's `/system` tree (init included) into the initial ramdisk. The
kernel can boot "kernel-only" without either. (`efi.zig:16`, `efi.zig:68`)
## Interrupt controller ## Interrupt controller
- **Local APIC + I/O APIC required.** I/O APIC base, GSI base, and MADT - **Local APIC + I/O APIC required.** I/O APIC base, GSI base, and MADT
interrupt-source overrides come from ACPI. (`cpu.zig:365`, `apic.zig:119`) interrupt-source overrides come from ACPI. (`cpu.zig:388`, `ioapic.zig:35`)
- **MSI supported** — edge-triggered, keyed by vector, no I/O APIC mask cycle. - **MSI supported** — edge-triggered, keyed by vector, no I/O APIC mask cycle.
Vector window 3346, timer on 32, spurious on 47. (`system/kernel/irq.zig:70`, Vector window 3346, timer on 32, spurious on 47. (`system/kernel/irq.zig:70`,
`cpu.zig:397`) `cpu.zig:427`)
- The legacy 8259 PIC is remapped and masked **only if present** (MADT - The legacy 8259 PIC is remapped and masked **only if present** (MADT
`PCAT_COMPAT`); it is not required. (`apic.zig:103`) `PCAT_COMPAT`); it is not required. (`apic.zig:149`)
## PCI / PCIe ## PCI / PCIe
@ -135,46 +137,47 @@ hypervisor configured for UEFI firmware and an xHCI USB controller.
bridge's ECAM window (1 MiB config space per bus) and computes config bridge's ECAM window (1 MiB config space per bus) and computes config
addresses directly. **There is no legacy CF8/CFC port-IO config path** — the addresses directly. **There is no legacy CF8/CFC port-IO config path** — the
driver bails if the bridge exposes no ECAM window. The ECAM base comes from driver bails if the bridge exposes no ECAM window. The ECAM base comes from
the ACPI MCFG table. (`system/drivers/pci-bus/pci-bus.zig:41`, `acpi.zig:6`) the ACPI MCFG table. (`system/drivers/pci-bus/pci-bus.zig:86`, `acpi.zig:7`)
## USB ## USB
- **xHCI only.** The sole USB driver is `usb-xhci-bus`, and the device manager - **xHCI only.** The sole USB *host-controller* driver is `usb-xhci-bus`, and
binds it strictly to PCI prog-IF `0x30` (xHCI). UHCI / OHCI / EHCI exist only the device manager binds it strictly to PCI prog-IF `0x30` (xHCI). UHCI /
as report strings with no driver behind them — **USB 1.x/2.0-only controllers OHCI / EHCI exist only as report strings with no driver behind them — **USB
are not supported.** (`system/drivers/usb-xhci-bus/`, 1.x/2.0-only controllers are not supported.** (`system/drivers/usb-xhci-bus/`,
`system/services/device-manager/device-manager.zig:34`) `system/services/device-manager/device-manager.zig:30`)
- USB input (keyboard/mouse over HID) is future work; the current input stack is - USB class drivers ride on top of it: HID keyboard + mouse (`usb-hid`, feeding
PS/2. See [Buses & devices](#buses--devices). the same input service as PS/2) and mass storage (`usb-storage`). See
[Buses & devices](#buses--devices).
## Timers ## Timers
Calibration prefers, in order: (1) CPUID leaf `0x15` TSC frequency, (2) HPET, Calibration prefers, in order: (1) CPUID leaf `0x15` TSC frequency, (2) HPET,
(3) ACPI PM timer (3.579545 MHz, from FADT), (4) legacy PIT. Any one suffices — (3) ACPI PM timer (3.579545 MHz, from FADT), (4) legacy PIT. Any one suffices —
HPET/PM-timer/PIT are optional fallbacks when CPUID `0x15` is absent. HPET/PM-timer/PIT are optional fallbacks when CPUID `0x15` is absent.
(`apic.zig:180`) (`apic.zig:209`)
- **TSC** — monotonic high-resolution clock. - **TSC** — monotonic high-resolution clock.
- **LAPIC timer** — scheduler heartbeat, periodic at `timer_hz = 1000 Hz`. - **LAPIC timer** — scheduler heartbeat, periodic at `timer_hz = 1000 Hz`.
(`parameters.zig:39`) (`parameters.zig:42`)
## Memory ## Memory
**Target: 128 MiB RAM.** The system uses 4 KiB pages and a bitmap physical-frame **Target: 128 MiB RAM.** The system uses 4 KiB pages and a bitmap physical-frame
allocator built from the firmware memory map. There is no hardcoded minimum-RAM allocator built from the firmware memory map. There is no hardcoded minimum-RAM
constant — the allocator only panics if there is no usable region, or none large constant — the allocator only panics if there is no usable region, or none large
enough to hold its own bitmap. (`system/kernel/pmm.zig:13`, `pmm.zig:77`) enough to hold its own bitmap. (`system/kernel/pmm.zig:15`, `pmm.zig:77`)
Where the budget goes: Where the budget goes:
| Consumer | Size | Source | | Consumer | Size | Source |
|---|---|---| |---|---|---|
| Kernel heap (cap, grown one page at a time) | up to **64 MiB** | `system/kernel/heap.zig:26` | | Kernel heap (cap, grown one page at a time) | up to **64 MiB** | `system/kernel/heap.zig:28` |
| Kernel stack, per CPU | 16 KiB | `parameters.zig:26` | | Kernel stack, per CPU | 16 KiB | `parameters.zig:29` |
| IST stack, per CPU | 16 KiB | `parameters.zig:36` | | IST stack, per CPU | 16 KiB | `parameters.zig:39` |
| User stack, per task | 8 pages / 32 KiB | `parameters.zig:32` | | User stack, per task | 8 pages / 32 KiB | `parameters.zig:35` |
| Max concurrent tasks | 32 | `parameters.zig:23` | | Max concurrent tasks | 48 | `parameters.zig:26` |
| Boot page-table pool | 64 frames / 256 KiB | `efi.zig:299` | | Boot page-table pool | 64 frames / 256 KiB | `efi.zig:307` |
The 64 MiB heap cap plus kernel image, per-CPU stacks, task stacks, the frame The 64 MiB heap cap plus kernel image, per-CPU stacks, task stacks, the frame
bitmap, and DMA-contiguous allocations fit comfortably within 128 MiB on a bitmap, and DMA-contiguous allocations fit comfortably within 128 MiB on a
@ -184,9 +187,9 @@ ceiling) add per-CPU stack overhead and push toward more RAM.
**Note on the 4 GiB physmap:** the loader identity-maps and physmaps the low **Note on the 4 GiB physmap:** the loader identity-maps and physmaps the low
4 GiB of address space with 2 MiB leaves. This is *virtual address* reach, not a 4 GiB of address space with 2 MiB leaves. This is *virtual address* reach, not a
RAM requirement — RAM above 4 GiB simply needs an extra mapping window and is not RAM requirement — RAM above 4 GiB simply needs an extra mapping window and is not
needed to boot. (`efi.zig:305`) needed to boot. (`efi.zig:313`)
Virtual-memory layout (`boot-handoff.zig:47`): Virtual-memory layout (`boot-handoff.zig:58`):
| Region | Base | | Region | Base |
|---|---| |---|---|
@ -201,16 +204,20 @@ Buses with real drivers today:
- **PCIe** via ECAM (`pci-bus`) - **PCIe** via ECAM (`pci-bus`)
- **xHCI USB** (`usb-xhci-bus`) - **xHCI USB** (`usb-xhci-bus`)
- **PS/2** keyboard + mouse (`ps2-bus`) — the current input stack - **PS/2** keyboard + mouse (`ps2-bus`)
- **USB HID** keyboard + mouse (`usb-hid`) — PS/2 and USB HID feed the same
input service
- **Serial UART** (16550/16450), configured from the ACPI SPCR table - **Serial UART** (16550/16450), configured from the ACPI SPCR table
**No storage driver exists yet.** AHCI / NVMe / IDE are named for reporting only; **Storage is USB mass storage only.** The block-device driver is `usb-storage`
there is no block-device driver. Persistent storage is future work. (bulk-only transport + SCSI), and the FAT server reads and writes it —
create / truncate / mkdir / unlink / rename, with mtime. AHCI / NVMe / IDE are
named for reporting only; internal SATA / NVMe / IDE disks have no driver.
## IOMMU ## IOMMU
**Detection only; enforcement deferred.** The ACPI DMAR table is parsed for the **Detection only; enforcement deferred.** The ACPI DMAR table is parsed for the
first VT-d DRHD unit and its capabilities are exposed via `PlatformInfo` first VT-d DRHD unit and its capabilities are exposed via `PlatformInformation`
(`iommu_present`, `iommu_base`, `iommu_version`). No DMA-remapping tables are (`iommu_present`, `iommu_base`, `iommu_version`). No DMA-remapping tables are
programmed and no translation is enforced. An IOMMU is therefore **not required** programmed and no translation is enforced. An IOMMU is therefore **not required**
and does not currently constrain devices. (`system/devices/acpi.zig:96`) and does not currently constrain devices. (`system/devices/acpi.zig:96`)
@ -223,5 +230,5 @@ and does not currently constrain devices. (`system/devices/acpi.zig:96`)
- Legacy port-IO (CF8/CFC) PCI configuration - Legacy port-IO (CF8/CFC) PCI configuration
- Non-xHCI USB (UHCI / OHCI / EHCI) - Non-xHCI USB (UHCI / OHCI / EHCI)
- Machines without ACPI (no device discovery) - Machines without ACPI (no device discovery)
- Persistent storage (no AHCI / NVMe / IDE driver yet) - Internal-disk storage (no AHCI / NVMe / IDE driver — persistent storage means
- USB HID input (PS/2 only for now) USB mass storage)

View File

@ -56,7 +56,7 @@ danos's two binaries default to different conventions:
Microsoft x64 (first argument → RCX). Microsoft x64 (first argument → RCX).
- The kernel is freestanding, so its convention is SysV (first argument → RDI). - The kernel is freestanding, so its convention is SysV (first argument → RDI).
When the loader jumps to the kernel passing the `BootInfo` pointer, both sides have When the loader jumps to the kernel passing the `BootInformation` pointer, both sides have
to agree *which register that pointer lands in*. Left to their defaults, the loader to agree *which register that pointer lands in*. Left to their defaults, the loader
would place it in RCX while the kernel looked in RDI — and the kernel would read would place it in RCX while the kernel looked in RDI — and the kernel would read
garbage. So both sides reference the same `system.kernel_abi` (SysV): the loader's garbage. So both sides reference the same `system.kernel_abi` (SysV): the loader's

View File

@ -7,10 +7,13 @@ without a human staring at the screen.
There are two layers: There are two layers:
- **Host unit tests** (`zig build test`) — for pure, platform-independent logic in - **Host unit tests** (`zig build test`) — for pure, platform-independent logic.
the shared contracts (`system/boot-handoff.zig`, `system/abi.zig`, What began as the three shared contracts (`system/boot-handoff.zig`,
`system/devices/device-abi.zig`), which also compile-checks the three-way split `system/abi.zig`, `system/devices/device-abi.zig`) now spans ~26 modules:
stays self-consistent. These compile for the host and run natively. protocol and on-wire definitions (VFS, USB, virtio-gpu), the FAT engine, the
display compositor, PS/2 and HID decoding, the kernel log ring, and the
runtime's `time`/`thread` — the full list is the test step in `build.zig`.
These compile for the host and run natively.
- **QEMU integration tests** (`python3 test/qemu_test.py`) — boot the real kernel - **QEMU integration tests** (`python3 test/qemu_test.py`) — boot the real kernel
and check its behaviour. This is the interesting part. and check its behaviour. This is the interesting part.
@ -18,9 +21,9 @@ There are two layers:
The framebuffer console draws pixels, which a test can't read without The framebuffer console draws pixels, which a test can't read without
screen-scraping. So the kernel also writes everything to a **serial port** screen-scraping. So the kernel also writes everything to a **serial port**
(`system/kernel/architecture/x86_64/serial.zig`, a 16550 UART on COM1). `Console.write` mirrors every (`system/kernel/architecture/x86_64/serial.zig`, a 16550 UART on COM1). The diagnostic log fans out
byte to it, so all kernel output — boot log, memory summary, exception reports to registered sinks, and the serial UART is one of them, so all kernel output
appears on serial as plain text. boot log, memory summary, exception reports — appears on serial as plain text.
QEMU captures that with `-serial file:serial.log`, giving a machine-readable QEMU captures that with `-serial file:serial.log`, giving a machine-readable
transcript. Serial is per-architecture (x86 uses port I/O; an ARM board uses a transcript. Serial is per-architecture (x86 uses port I/O; an ARM board uses a
@ -51,13 +54,18 @@ DANOS-TEST-RESULT: PASS (6 passed, 0 failed)
DANOS-TEST-DONE DANOS-TEST-DONE
``` ```
Current cases: The core kernel cases (the suite has since grown far beyond this table — SMP,
threads, processes, display, USB, FAT and more; the full list is the `CASES`
table in `test/qemu_test.py`):
| Case | What it checks | How the harness confirms it | | Case | What it checks | How the harness confirms it |
|------|----------------|-----------------------------| |------|----------------|-----------------------------|
| `smoke` | memory map has usable RAM; frame alloc/free; paging active | `DANOS-TEST-RESULT: PASS` | | `smoke` | memory map has usable RAM; frame alloc/free; paging active | `DANOS-TEST-RESULT: PASS` |
| `discovery` | ACPI discovery populated the platform facts: MADT (LAPIC base, CPU count) and FADT (PM/reset registers) | `DANOS-TEST-RESULT: PASS` |
| `wx` | W^X audit: kernel code is executable; rodata, data, heap, and stack are NX | `DANOS-TEST-RESULT: PASS` |
| `timer` | device interrupts fire and return (tick count advances) | `DANOS-TEST-RESULT: PASS` | | `timer` | device interrupts fire and return (tick count advances) | `DANOS-TEST-RESULT: PASS` |
| `clock` | LAPIC + TSC calibrated; monotonic uptime advances; `nanos()` has sub-ms resolution | `DANOS-TEST-RESULT: PASS` | | `clock` | LAPIC + TSC calibrated; monotonic uptime advances; `nanos()` has sub-ms resolution | `DANOS-TEST-RESULT: PASS` |
| `wall-clock` | the CMOS RTC read at boot yields a plausible current epoch | `DANOS-TEST-RESULT: PASS` |
| `vmm` | on-demand `map` works: a mapped page is writable and reads back | `DANOS-TEST-RESULT: PASS` | | `vmm` | on-demand `map` works: a mapped page is writable and reads back | `DANOS-TEST-RESULT: PASS` |
| `heap` | kernel heap: alloc/free, block reuse, growth, and a std container on it | `DANOS-TEST-RESULT: PASS` | | `heap` | kernel heap: alloc/free, block reuse, growth, and a std container on it | `DANOS-TEST-RESULT: PASS` |
| `sched` | preemption: three non-yielding tasks all make progress | `DANOS-TEST-RESULT: PASS` | | `sched` | preemption: three non-yielding tasks all make progress | `DANOS-TEST-RESULT: PASS` |
@ -65,11 +73,19 @@ Current cases:
| `sleep` | a task blocks for ~50 ms (real block, not a busy-wait) | `DANOS-TEST-RESULT: PASS` | | `sleep` | a task blocks for ~50 ms (real block, not a busy-wait) | `DANOS-TEST-RESULT: PASS` |
| `event` | a task blocks on a wait queue and is woken (preempting) | `DANOS-TEST-RESULT: PASS` | | `event` | a task blocks on a wait queue and is woken (preempting) | `DANOS-TEST-RESULT: PASS` |
| `ipc` | producer/consumer pass 100 messages through a 4-slot channel intact | `DANOS-TEST-RESULT: PASS` | | `ipc` | producer/consumer pass 100 messages through a 4-slot channel intact | `DANOS-TEST-RESULT: PASS` |
| `ipc-call` | synchronous IPC: client and server ping-pong 100 calls through one endpoint (rendezvous, reply routing, cross-copy) | `DANOS-TEST-RESULT: PASS` |
| `ipc-cap` | capability passing: endpoints handed over in a call and its reply arrive as the same object, shared not moved | `DANOS-TEST-RESULT: PASS` |
| `dma` | DMA memory: contiguous frame allocation, below-4G cap, coherent mapping, reclaim on teardown | `DANOS-TEST-RESULT: PASS` |
| `msi` | an MSI vector is allocated and delivered as an endpoint notification (a self-IPI stands in for the device write) | `DANOS-TEST-RESULT: PASS` |
| `iommu` | the VT-d unit is found in the DMAR table and its registers read back (detection only; boots with an emulated IOMMU) | `DANOS-TEST-RESULT: PASS` |
| `ioport` | port I/O grants: an `io_port` resource admits in-range reads, refuses out-of-range/unclaimed | `DANOS-TEST-RESULT: PASS` |
| `fault-ud` | invalid-opcode exception is caught | serial shows `invalid opcode (vector 6)` | | `fault-ud` | invalid-opcode exception is caught | serial shows `invalid opcode (vector 6)` |
| `fault-pf` | page fault caught with CR2 | `page fault (vector 14)` | | `fault-pf` | page fault caught with CR2 | `page fault (vector 14)` |
| `fault-df` | double fault caught on IST1 (not a triple-fault reset) | `double fault (vector 8)` | | `fault-df` | double fault caught on IST1 (not a triple-fault reset) | `double fault (vector 8)` |
| `fault-ap-df` | a double fault pinned to an application processor is caught by that core's own TSS/IST (boots with `-smp 4`) | `core N: double fault (vector 8)`, N ≥ 1 |
| `fault-nx` | executing a data page (NX) faults | `page fault (vector 14)` | | `fault-nx` | executing a data page (NX) faults | `page fault (vector 14)` |
| `fault-null` | dereferencing the unmapped page 0 faults | `page fault (vector 14)` | | `fault-null` | dereferencing the unmapped page 0 faults | `page fault (vector 14)` |
| `fault-recovery` | a ring-3 process that faults is killed and reaped while init keeps heartbeating — the OS survives | `DANOS-TEST-RESULT: PASS` |
The faulting cases don't print a result line — they deliberately raise a CPU The faulting cases don't print a result line — they deliberately raise a CPU
exception, and the harness asserts on the [exception report](interrupts.md) the exception, and the harness asserts on the [exception report](interrupts.md) the
@ -81,8 +97,10 @@ marker would never appear.
`test/qemu_test.py` ties it together. For each case it: `test/qemu_test.py` ties it together. For each case it:
1. builds the kernel with `-Dtest-case=<name>`, 1. builds the kernel with `-Dtest-case=<name>` (the build produces the bootable
2. assembles a fresh EFI System Partition from the built binaries, FAT32 USB image, `zig-out/danos-usb.img`),
2. copies that image to a fresh per-run boot volume, so the guest's mutations
don't dirty the build artifact,
3. boots it headless in QEMU with serial captured to a file and `-no-reboot` 3. boots it headless in QEMU with serial captured to a file and `-no-reboot`
(so a triple fault exits rather than looping), (so a triple fault exits rather than looping),
4. polls the serial log until the case's expected regex appears (**pass**), a 4. polls the serial log until the case's expected regex appears (**pass**), a
@ -118,8 +136,8 @@ firmware, boot method, serial device). The cases are architecture-neutral —
So bringing up a second architecture — an AArch64 Raspberry Pi is the motivating So bringing up a second architecture — an AArch64 Raspberry Pi is the motivating
one — means: one — means:
1. implement `system/kernel/arch/aarch64/` (CPU ops, its UART, exception vectors, page 1. implement `system/kernel/architecture/aarch64/` (CPU ops, its UART, exception vectors, page
tables) behind the same `arch` interface, tables) behind the same `architecture` interface,
2. add an `aarch64` entry to `ARCHES` with its `qemu-system-aarch64` invocation, 2. add an `aarch64` entry to `ARCHES` with its `qemu-system-aarch64` invocation,
and the *same* `smoke` / `fault-*` cases run against it: `python3 test/qemu_test.py and the *same* `smoke` / `fault-*` cases run against it: `python3 test/qemu_test.py

View File

@ -112,7 +112,7 @@ task's exit; make destruction happen on the **last** exit.
(`address_space_refs`): `retainAddressSpace` takes a reference in `spawnUserLocked` (on the (`address_space_refs`): `retainAddressSpace` takes a reference in `spawnUserLocked` (on the
success path, after the slot + stack are secured), all under the big kernel lock. success path, after the slot + stack are secured), all under the big kernel lock.
- [x] Both task-teardown paths ([scheduler.zig](../system/kernel/scheduler.zig): - [x] Both task-teardown paths ([scheduler.zig](../system/kernel/scheduler.zig):
`exitUserLocked` and `destroyTaskLocked`) call `releaseAspace`, which decrements `exitUserLocked` and `destroyTaskLocked`) call `releaseAddressSpace`, which decrements
and only `destroyAddressSpace`s at **zero**; an unretained space (hand-built test and only `destroyAddressSpace`s at **zero**; an unretained space (hand-built test
spaces) is destroyed directly, preserving prior behaviour. spaces) is destroyed directly, preserving prior behaviour.
- [x] `-Dtest-case=address-space-refcount`: spawn and reap several ring-3 processes in sequence - [x] `-Dtest-case=address-space-refcount`: spawn and reap several ring-3 processes in sequence
@ -134,9 +134,10 @@ Spawn only — no join yet. Prove a second task executes in the **caller's** add
space and exits cleanly. space and exits cleanly.
- [x] [abi.zig](../system/abi.zig): `thread_spawn = 37`, `thread_exit = 38`. Handlers in - [x] [abi.zig](../system/abi.zig): `thread_spawn = 37`, `thread_exit = 38`. Handlers in
process.zig; `thread_spawn` calls `scheduler.spawnThread` (shares the caller's process.zig; `thread_spawn` calls `scheduler.spawnThread` (today, after M3, the
handler goes `spawnThreadSupervised``scheduler.spawnUserLocked`; shares the caller's
address space, `retainAddressSpace`); `thread_exit` ends the task like a process `exit(0)` address space, `retainAddressSpace`); `thread_exit` ends the task like a process `exit(0)`
(`terminateCurrent` → `releaseAspace`). The closure pointer is delivered in the new (`terminateCurrent` → `releaseAddressSpace`). The closure pointer is delivered in the new
thread's **rdi** via a new `jump_to_user_arg` asm path (`t.user_arg`, 0 for a thread's **rdi** via a new `jump_to_user_arg` asm path (`t.user_arg`, 0 for a
process) — no naked runtime asm. process) — no naked runtime asm.
- [x] `library/runtime/thread.zig` (barrel-exported as `runtime.Thread`): `spawn` maps a - [x] `library/runtime/thread.zig` (barrel-exported as `runtime.Thread`): `spawn` maps a
@ -379,8 +380,9 @@ to the std shape and drop M3's per-thread exit endpoint:
it enters the kernel to exit — so waking at *exit* time (not reap time) is safe, and it enters the kernel to exit — so waking at *exit* time (not reap time) is safe, and
no reaper/address-space juggling or user-memory write is needed. This is equally no reaper/address-space juggling or user-memory write is needed. This is equally
std-shaped (like `pthread_join`) and much simpler/safer than the planned std-shaped (like `pthread_join`) and much simpler/safer than the planned
reaper-written completion word. `thread_spawn` no longer takes an exit endpoint (the reaper-written completion word. The runtime no longer passes `thread_spawn` an exit
runtime passes `no_cap`); the per-thread IPC endpoint is gone. endpoint (it passes `no_cap`; the kernel's 4th `exit_endpoint` arg remains and is
still honored); the runtime's per-thread IPC endpoint is gone.
- [x] `thread-join` passes on the new path, and its join mode now runs **40 spawn+join - [x] `thread-join` passes on the new path, and its join mode now runs **40 spawn+join
cycles** — under the old per-thread-endpoint scheme those leaked handles would cycles** — under the old per-thread-endpoint scheme those leaked handles would
exhaust the 16-slot handle table; here they all succeed, proving join is endpoint-free. exhaust the 16-slot handle table; here they all succeed, proving join is endpoint-free.

View File

@ -81,9 +81,11 @@ address space. Threads deliberately remove that boundary *within* a process:
- Threads share one address space, so one thread's stray write corrupts them all — - Threads share one address space, so one thread's stray write corrupts them all —
there is no isolation **between** threads. there is no isolation **between** threads.
- Threads share fate: a fault in any thread, or a "kill the process" decision, takes - Threads share fate — by contract: a fault in any thread, or a "kill the process"
down **all** of them. Restartability lives at the process level, not the thread decision, takes down **all** of them, so restartability lives at the process level,
level. not the thread level. (The kernel does not yet enforce this fan-out — see the
Lifecycle note under
[Interaction with the rest of the kernel](#interaction-with-the-rest-of-the-kernel).)
- Shared mutable state reintroduces data races — the failure class the - Shared mutable state reintroduces data races — the failure class the
isolate-and-message model was chosen to avoid. isolate-and-message model was chosen to avoid.
@ -103,16 +105,15 @@ Lives in `library/runtime/thread.zig`, re-exported as `runtime.Thread`.
pub const Thread = struct { pub const Thread = struct {
pub const Id = u32; // the kernel task id pub const Id = u32; // the kernel task id
pub const SpawnConfig = struct { pub const SpawnConfig = struct {
stack_size: usize = default_stack_size, stack_size: usize = default_stack_size, // no allocator: the closure lives at the top of the thread's own stack
allocator: ?std.mem.Allocator = null, // for the closure + stack bookkeeping
}; };
pub const SpawnError = error{ OutOfMemory, ThreadQuotaExceeded, SystemResources }; pub const SpawnError = error{SystemResources};
pub fn spawn(config: SpawnConfig, comptime function: anytype, args: anytype) SpawnError!Thread; pub fn spawn(config: SpawnConfig, comptime function: anytype, args: anytype) SpawnError!Thread;
pub fn join(self: Thread) void; // block until the thread ends, reclaim its stack pub fn join(self: Thread) void; // block until the thread ends, reclaim its stack
pub fn detach(self: Thread) void; // give up the right to join; kernel reclaims on exit pub fn detach(self: Thread) void; // give up the right to join; stack reclaimed at process exit
pub fn getCurrentId() Id; pub fn getCurrentId() Id;
pub fn yield() void; // -> existing `yield` syscall pub fn currentCore() Id; // danos extension: the calling core's dense index
pub const Mutex = struct { pub fn lock(*Mutex) void; pub fn tryLock(*Mutex) bool; pub fn unlock(*Mutex) void; }; pub const Mutex = struct { pub fn lock(*Mutex) void; pub fn tryLock(*Mutex) bool; pub fn unlock(*Mutex) void; };
pub const Condition = struct { pub fn wait(*Condition, *Mutex) void; pub fn timedWait(*Condition, *Mutex, u64) error{Timeout}!void; pub fn signal(*Condition) void; pub fn broadcast(*Condition) void; }; pub const Condition = struct { pub fn wait(*Condition, *Mutex) void; pub fn timedWait(*Condition, *Mutex, u64) error{Timeout}!void; pub fn signal(*Condition) void; pub fn broadcast(*Condition) void; };
@ -127,18 +128,21 @@ Deviations from `std.Thread`, called out honestly:
- **The thread function's return value is discarded** (as `std.Thread.join` returns - **The thread function's return value is discarded** (as `std.Thread.join` returns
`void`). Return data through shared state or a `Semaphore`/`Condition`, not the `void`). Return data through shared state or a `Semaphore`/`Condition`, not the
return. return.
- `getCpuCount()` maps to the existing SMP core count ([smp.md](smp.md)); a service - No `getCpuCount()` (a service rarely needs it) and no `Thread.yield()``yield`
rarely needs it. lives in `runtime.system`. Instead `currentCore()` exposes the calling core's dense
index ([smp.md](smp.md)), used to observe genuine cross-core parallelism.
## Kernel primitives (new private syscalls) ## Kernel primitives (new private syscalls)
Four new entries extend [abi.zig](../system/abi.zig) `SystemCall` after Five core entries extend [abi.zig](../system/abi.zig) `SystemCall` after
`shared_memory_physical = 36`, each with a `library/runtime` wrapper: `shared_memory_physical = 36` (plus small helpers `current_core`, `thread_self`, and
`set_thread_pointer`), each with a `library/runtime` wrapper:
| Syscall | Signature | Purpose | | Syscall | Signature | Purpose |
|---|---|---| |---|---|---|
| `thread_spawn` | `(entry, stack_top, arg) -> tid` | create a task sharing the **caller's** address space | | `thread_spawn` | `(entry, stack_top, arg, exit_endpoint) -> tid` | create a task sharing the **caller's** address space; the runtime passes `no_cap` for `exit_endpoint` (join is a syscall, not an endpoint) |
| `thread_exit` | `(stack_base, stack_len)` | end the calling thread; hand back its stack range for reclaim | | `thread_exit` | `()` | end the calling thread; its stack is reclaimed by the joiner's `munmap`, not the kernel |
| `thread_join` | `(tid) -> 0` | block until the thread with id `tid` has exited |
| `futex_wait` | `(addr, expected, timeout_ns) -> status` | block if `*addr == expected`, until woken or timeout | | `futex_wait` | `(addr, expected, timeout_ns) -> status` | block if `*addr == expected`, until woken or timeout |
| `futex_wake` | `(addr, count) -> woken` | wake up to `count` waiters on `addr` | | `futex_wake` | `(addr, count) -> woken` | wake up to `count` waiters on `addr` |
@ -148,56 +152,56 @@ Plus one invariant change with no new syscall: **address-space reference countin
### Address-space reference counting ### Address-space reference counting
Today an address space is 1:1 with a task: `spawnUserLocked` records `address_space` on the Before this work an address space was 1:1 with a task: `spawnUserLocked` records
Task, and teardown does `destroyAddressSpace(t.address_space)` when **any** user task exits `address_space` on the Task (as it still does), and teardown did
`destroyAddressSpace(t.address_space)` when **any** user task exited
([scheduler.zig](../system/kernel/scheduler.zig)). With threads, several tasks share ([scheduler.zig](../system/kernel/scheduler.zig)). With threads, several tasks share
one `address_space`, so the first to exit would rip the address space out from under its one `address_space`, so the first to exit would rip the address space out from under its
siblings. siblings.
Fix: a small refcount keyed by the address-space root (`createAddressSpace` in Fix: a small refcount keyed by the address-space root, kept in
[process.zig](../system/kernel/process.zig) sets it to 1). `thread_spawn` increments [scheduler.zig](../system/kernel/scheduler.zig): `retainAddressSpace` takes a
it; task teardown decrements and only calls `destroyAddressSpace` at **zero**. All of reference for every user task `spawnUserLocked` starts (count 1 on the first take, so
a thread sharing the caller's space increments it); task teardown calls
`releaseAddressSpace`, which only calls `destroyAddressSpace` at **zero**. All of
this is already under the big kernel lock, so no new locking. This is the one piece this is already under the big kernel lock, so no new locking. This is the one piece
that must land and be proven before anything shares an address space. that must land and be proven before anything shares an address space.
### `thread_spawn` and the trampoline ### `thread_spawn` and the trampoline
The scheduler already accepts an arbitrary `address_space` and does **not** smuggle values The scheduler already accepts an arbitrary `address_space` and does **not** smuggle
through registers — `startUserTask` reads the entry/stack from the Task and values through scratch registers — `startUserTask` reads the entry/stack (and the
`jumpToUser`s ([scheduler.zig](../system/kernel/scheduler.zig)). That makes the thread thread's closure arg, delivered in `rdi` via `jumpToUserArg`) from the Task
path clean: ([scheduler.zig](../system/kernel/scheduler.zig)). That makes the thread path clean:
1. The runtime's `spawn` `mmap`s a stack (syscall `4`), heap-allocates a closure — 1. The runtime's `spawn` `mmap`s a stack (syscall `4`) and writes the closure —
`{ fn_ptr, args_tuple, completion }`, the std "Instance" pattern — and writes the `{ tls_base, args }`, the std "Instance" pattern — at the **top of the new stack
closure pointer to the **top word of the new stack**. itself** (no heap allocation), with a small per-thread TLS block just below it.
2. It calls `thread_spawn(entry = &threadTrampoline, stack_top, arg = closure_ptr)`. 2. It calls `thread_spawn(entry = &Closure.entry, stack_top, arg = closure_ptr,
The kernel calls the same `spawnUserLocked` path with the **caller's address space** exit_endpoint = no_cap)`. The kernel calls the same `spawnUserLocked` path with the
(refcount++), `entry`, and `user_sp = stack_top`. **caller's address space** (refcount++), `entry`, and `user_sp = stack_top`.
3. `threadTrampoline` (a small runtime shim) reads the closure off its stack, calls 3. `Closure.entry` (a small runtime shim) receives the closure pointer in `rdi` — the
the user function, then calls `thread_exit`. No new register ABI — the closure kernel delivers `arg` as the entry's first C-ABI argument — sets the thread
pointer rides the stack the runtime set up, mirroring how `startUserTask` avoids pointer, calls the user function, then calls `thread_exit`.
register smuggling.
Unlike a process start, there is **no** System V argc/argv/auxv block Unlike a process start, there is **no** System V argc/argv/auxv block
([sysv.md](sysv.md)) — a thread stack carries only the closure pointer. ([sysv.md](sysv.md)) — a thread stack carries only the closure and its TLS block.
### Lifetime: exit, join, detach, stack reclaim ### Lifetime: exit, join, detach, stack reclaim
- **`thread_exit`** marks the task dead and hands the kernel the thread's user-stack - **`thread_exit`** (no arguments) marks the task dead. The kernel releases the
range. The kernel reaps the task on the scheduler (already running on a *kernel* task's resources, decrements the address-space refcount, and frees the task slot —
stack, so it can safely unmap the user stack), decrements the address-space refcount, and the user stack is not the kernel's to unmap; the joiner reclaims it.
frees the task slot. - **`join`** is a dedicated `thread_join(tid)` syscall: the caller blocks in the
- **`join` — Stage 1** reuses the existing exit-notification machinery kernel (`joinThreadLocked`, woken by `wakeJoinersLocked` when the thread exits),
([process-lifecycle.md](process-lifecycle.md)): `spawn` passes a per-thread then `munmap`s the stack. (The plan staged join over a per-thread `exit_endpoint`
`exit_endpoint`, and `join` blocks in `ipc_reply_wait` until the child-exit first, with a futex `completion` word as a Stage-2 refinement; neither shipped — the
notification for that `tid` arrives, then `munmap`s the stack. No futex needed to dedicated syscall replaced both. `thread_spawn` still accepts an `exit_endpoint`
land spawn/join. argument, which the runtime passes as `no_cap`.)
- **`join` — Stage 2 refinement** migrates to the std shape: a `completion` word in - **`detach`** relinquishes the join right: no one waits for the thread, and its
the closure that `thread_exit`'s trampoline `futex_wake`s and `join` `futex_wait`s stack is reclaimed at process exit — kernel-side reclaim of a detached thread's
on — dropping the per-thread endpoint. Kept as a refinement so Stage 1 ships first. user stack stays deferred (as the intro notes), since `thread_exit` passes no stack
- **`detach`** relinquishes the join right; the kernel reclaims the stack and slot on range.
`thread_exit` (a detached thread's stack range is unmapped by the reaper, since no
joiner will).
### Futex, and the sync primitives on top ### Futex, and the sync primitives on top
@ -219,21 +223,24 @@ decision, not a "maybe later."
### TLS and `getCurrentId` ### TLS and `getCurrentId`
danos sets up no thread-pointer TLS today (fine under `single_threaded`). Two scoped needs: Per-thread thread-pointer TLS is in place (the `threadlocal` *compiler* layer is not —
see the intro). Two scoped pieces, as built:
- **`getCurrentId`** returns the kernel task id — either a trivial syscall or, better, - **`getCurrentId`** returns the kernel task id via the trivial `thread_self` syscall.
a value the runtime stashes in a per-thread control block. - **The thread pointer** is per-thread: `spawn` carves a small TLS block (an `fs:0`
- **`threadlocal` variables** need a real per-thread TLS block and the thread pointer set per self-pointer plus scratch) from the top of the thread's own stack, the trampoline
thread. `thread_spawn` sets the thread pointer to a runtime-allocated per-thread block; full calls `set_thread_pointer` before any user code runs, and the scheduler saves and
`threadlocal` support is Stage 3, only if a consumer needs it. Nothing in the core restores the pointer per task across context switches. Full `threadlocal` support is
runtime+linker work on top of this, only if a consumer needs it. Nothing in the core
spawn/join/mutex path requires `threadlocal`. spawn/join/mutex path requires `threadlocal`.
### Build: multi-threaded codegen, opt-in ### Build: multi-threaded codegen, opt-in
`addUserBinary` gains a `threaded: bool = false` parameter; when set it builds that A binary opts in by being added with `addThreadedUserBinary` — as `addUserBinary`,
binary `single_threaded = false` so atomics and (later) TLS are real. Threads and but the shared implementation builds it `single_threaded = false` — so atomics and
atomics are unsound in a `single_threaded` image, so a binary must opt in **before** (later) TLS are real. Threads and atomics are unsound in a `single_threaded` image,
it may call `runtime.Thread.spawn`. Everyone else stays single-threaded and lean. so a binary must opt in **before** it may call `runtime.Thread.spawn`. Everyone else
stays single-threaded and lean.
## Interaction with the rest of the kernel ## Interaction with the rest of the kernel
@ -243,12 +250,22 @@ it may call `runtime.Thread.spawn`. Everyone else stays single-threaded and lean
process can run on different cores simultaneously — that is the point. process can run on different cores simultaneously — that is the point.
- **Halting** ([halting.md](halting.md)): futex-parked waiters keep the "idle core - **Halting** ([halting.md](halting.md)): futex-parked waiters keep the "idle core
halts" property intact under lock contention — no busy-wait. halts" property intact under lock contention — no busy-wait.
- **Lifecycle** ([process-lifecycle.md](process-lifecycle.md)): killing a process - **Lifecycle** ([process-lifecycle.md](process-lifecycle.md)): the contract is that
must kill *all* its threads and only then drop the last address-space ref. The kill path killing a process kills *all* its threads and only then drops the last address-space
already targets a process; it fans out to every task on that address space. ref. **The kernel does not implement that fan-out yet**: `process_kill` reaps only
- **Resilience** ([resilience.md](resilience.md)): a faulting thread kills its whole the one task it resolves, and no death path loops over the tasks sharing an address
process (shared fate). The supervisor restarts the **process**, which respawns its space — the refcount keeps the space (and the sibling threads) alive and running.
threads from a known-good state — restart granularity stays the process. The gap is hit in practice: of the only threaded binaries (the `display` service and
the `thread-test` harness), `display` is a boot service that handles no `.terminate`
signal, so init's stop sequence escalates to `process_kill` on every orderly
shutdown — benign only because poweroff follows. Whether to implement the fan-out or
amend the contract is a decision still to be made.
- **Resilience** ([resilience.md](resilience.md)): by the same contract, a faulting
thread kills its whole process (shared fate); the supervisor restarts the
**process**, which respawns its threads from a known-good state — restart
granularity stays the process. Today a CPU fault kills only the faulting task
(`killCurrentProcess` tears down a single task), so sibling threads keep running —
the same implementation gap as above.
- **IPC — two consequences threads forced ([ipc.md](ipc.md)):** - **IPC — two consequences threads forced ([ipc.md](ipc.md)):**
- *Handles do not cross threads.* The handle table lives on the `Task` - *Handles do not cross threads.* The handle table lives on the `Task`
([scheduler.zig](../system/kernel/scheduler.zig)), so a handle number is meaningful ([scheduler.zig](../system/kernel/scheduler.zig)), so a handle number is meaningful
@ -262,8 +279,8 @@ it may call `runtime.Thread.spawn`. Everyone else stays single-threaded and lean
mutate the global service registry, endpoint refcounts, and handle tables. Those paths mutate the global service registry, endpoint refcounts, and handle tables. Those paths
were unlocked because a single-threaded process could not race itself; a multi-threaded were unlocked because a single-threaded process could not race itself; a multi-threaded
one can, from two cores at once. They now take `sync.enter()` like `call`/`reply_wait`/ one can, from two cores at once. They now take `sync.enter()` like `call`/`reply_wait`/
`send` already did — the kernel heap has no lock of its own yet (heap.zig: "a lock comes `send` already did — the kernel heap has no lock of its own (heap.zig: "every kernel
with threads/SMP"), so the big lock is what keeps its callers serialized. entry takes the big kernel lock"), so the big lock is what keeps its callers serialized.
## Build-out plan (staged, each gate serial-checkable) ## Build-out plan (staged, each gate serial-checkable)
@ -278,11 +295,13 @@ a verifiable gate (`python3 test/qemu_test.py <case>`, asserting serial markers;
*Gate:* the full QEMU suite stays green (no regression) — proves the reframing is *Gate:* the full QEMU suite stays green (no regression) — proves the reframing is
invisible until used. invisible until used.
- **Stage 1 — spawn / join / detach.** `thread_spawn` + `thread_exit`, the trampoline, - **Stage 1 — spawn / join / detach.** `thread_spawn` + `thread_exit`, the trampoline,
stacks via `mmap`, join over the exit-endpoint, the `threaded` build flag. stacks via `mmap`, join over the exit-endpoint (as built, join became the dedicated
*Gate:* `-Dtest-case=thread-spawn` — a threaded test service spawns N threads that `thread_join` syscall instead), the `addThreadedUserBinary` build opt-in.
each `@atomicRmw`-increment a shared counter, the parent joins all N, and asserts *Gate:* two cases as built — `-Dtest-case=thread-spawn`, where a worker thread runs
the total is exactly N × iterations. Runs `smp` (multi-core) to prove real in the caller's address space (a shared-memory write, observed by the main thread),
parallelism. and `-Dtest-case=thread-join`, where N workers each atomically increment a shared
counter K times, the parent joins all N and asserts the total is exactly N × K —
the join case running multi-core (`smp` 4) to prove real parallelism.
- **Stage 2 — blocking synchronization.** `futex_wait`/`futex_wake` + `Futex`, - **Stage 2 — blocking synchronization.** `futex_wait`/`futex_wake` + `Futex`,
`Mutex`, `Condition`, `Semaphore`; optionally migrate join to a futex completion `Mutex`, `Condition`, `Semaphore`; optionally migrate join to a futex completion
word. *Gate:* `-Dtest-case=thread-mutex` — a bounded producer/consumer over a word. *Gate:* `-Dtest-case=thread-mutex` — a bounded producer/consumer over a

View File

@ -29,8 +29,9 @@ full-speed keyboard.
## Slot-context fields for a downstream device ## Slot-context fields for a downstream device
`buildAddressInputContext` fills the Slot Context. Today it hardcodes route 0 and `buildAddressInputContext` fills the Slot Context from the device record: a
the root-hub port. A downstream device additionally needs: root-port device carries route 0 and its own root-hub port. A downstream device
additionally carries:
- **Route String** (Slot Context dword 0, bits 19:0) — 5 tiers × 4 bits, each - **Route String** (Slot Context dword 0, bits 19:0) — 5 tiers × 4 bits, each
tier the downstream hub-port number. Composed as tier the downstream hub-port number. Composed as
@ -80,10 +81,12 @@ When the bus scan (or a hot-plug bring-up) finds a device of class 9:
## Testing ## Testing
QEMU's `usb-hub` is a USB 2.0 single-TT hub. A **static boot topology** QEMU's `usb-hub` is a USB 2.0 single-TT hub. A **static boot topology** on a
(`-device usb-hub,bus=xhci.0,id=h -device usb-kbd,bus=h.0`) presents the dedicated second controller (`-device qemu-xhci,id=xhci2 -device
downstream device connected from the start, so the bus reads it on the first usb-hub,bus=xhci2.0,port=1 -device usb-kbd,bus=xhci2.0,port=1.1` — isolated from
status-change report — exercising the full path (hub setup, TT slot context, the boot controller's auto-assigned devices, whose ports the hub would collide
with) presents the downstream device connected from the start, so the bus reads
it on the first status-change report — exercising the full path (hub setup, TT slot context,
downstream enumerate, class-driver bind) without needing a hot-plug event. A new downstream enumerate, class-driver bind) without needing a hot-plug event. A new
`usb-hub` QEMU case asserts the hub enumerates, the downstream keyboard `usb-hub` QEMU case asserts the hub enumerates, the downstream keyboard
enumerates behind it, and `usb-hid-keyboard` binds. enumerates behind it, and `usb-hid-keyboard` binds.

View File

@ -119,16 +119,23 @@ One table entry per kernel call, C ABI (System V AMD64), names prefixed
returns are `u64`, errors return as negative values exactly as today. returns are `u64`, errors return as negative values exactly as today.
The calls that return two values in `rax:rdx` today — `dma_alloc` The calls that return two values in `rax:rdx` today — `dma_alloc`
(virtual_address + physical_address), `msi_bind` (address + data), `shared_memory_create` (virtual_address + handle) — (virtual_address + physical_address), `msi_bind` (address + data), `shared_memory_create` (virtual_address + handle),
`fs_resolve` (route tag + node token / backend handle) —
become functions returning a two-`u64` struct. The System V ABI returns a become functions returning a two-`u64` struct. The System V ABI returns a
16-byte struct in `rax:rdx`, so the stub is a plain `syscall; ret` — the 16-byte struct in `rax:rdx`, so the stub is a plain `syscall; ret` — the
C-ABI spelling of the existing convention, at zero cost. C-ABI spelling of the existing convention, at zero cost. The one call that
returns *three* values — `ipc_reply_wait` (receive_len in `rax`, badge in
`rdx`, received capability in `r8`) — exceeds the two-register return: its
function returns a three-`u64` struct, which the ABI passes via a hidden
result pointer, so that one stub stores `rax`/`rdx`/`r8` through the pointer
after the `syscall` — a few instructions rather than one.
Grouped as `abi.zig` groups them: Grouped as `abi.zig` groups them:
| Group | Functions | | Group | Functions |
|-------|-----------| |-------|-----------|
| process | `danos_exit`, `danos_yield`, `danos_sleep`, `danos_spawn`, `danos_process_enumerate`, `danos_process_kill`, `danos_process_exit_reason`, `danos_process_subscribe`, `danos_process_signal`, `danos_signal_bind` | | process | `danos_exit`, `danos_yield`, `danos_sleep`, `danos_spawn`, `danos_process_enumerate`, `danos_process_kill`, `danos_process_exit_reason`, `danos_process_subscribe`, `danos_process_signal`, `danos_signal_bind` |
| threads | `danos_thread_spawn`, `danos_thread_exit`, `danos_current_core`, `danos_futex_wait`, `danos_futex_wake`, `danos_thread_self`, `danos_thread_join`, `danos_set_thread_pointer` |
| memory | `danos_mmap`, `danos_munmap`, `danos_dma_alloc`, `danos_dma_free`, `danos_shared_memory_create`, `danos_shared_memory_map`, `danos_shared_memory_physical` | | memory | `danos_mmap`, `danos_munmap`, `danos_dma_alloc`, `danos_dma_free`, `danos_shared_memory_create`, `danos_shared_memory_map`, `danos_shared_memory_physical` |
| ipc | `danos_endpoint_create`, `danos_ipc_register`, `danos_ipc_lookup`, `danos_ipc_call`, `danos_ipc_reply_wait`, `danos_ipc_send` | | ipc | `danos_endpoint_create`, `danos_ipc_register`, `danos_ipc_lookup`, `danos_ipc_call`, `danos_ipc_reply_wait`, `danos_ipc_send` |
| devices | `danos_device_enumerate`, `danos_device_claim`, `danos_device_register`, `danos_mmio_map`, `danos_irq_bind`, `danos_irq_ack`, `danos_msi_bind`, `danos_io_read`, `danos_io_write` | | devices | `danos_device_enumerate`, `danos_device_claim`, `danos_device_register`, `danos_mmio_map`, `danos_irq_bind`, `danos_irq_ack`, `danos_msi_bind`, `danos_io_read`, `danos_io_write` |

View File

@ -7,10 +7,11 @@
> redirects the caller to the owning backend's endpoint plus the rewritten > redirects the caller to the owning backend's endpoint plus the rewritten
> mount-relative path — after which the client speaks THIS protocol to the > mount-relative path — after which the client speaks THIS protocol to the
> backend, unchanged. The Zig source of truth is `system/vfs-protocol.zig` > backend, unchanged. The Zig source of truth is `system/vfs-protocol.zig`
> (the `vfs-protocol` module), whose unit tests pin the sizes and values > (the `vfs-protocol` module), whose unit test pins a sample of the sizes
> below. This page is the **language-neutral wire specification** of that > and values below. This page is the **language-neutral wire specification**
> contract — what a Rust or C client implements ([vdso.md](vdso.md) explains > of that contract — what a Rust or C client implements ([vdso.md](vdso.md)
> why the IPC protocols, not the syscall numbers, are danos's public ABI). > explains why the IPC protocols, not the syscall numbers, are danos's
> public ABI).
## Transport ## Transport
@ -57,16 +58,20 @@ exit events.
| 16 | 4 | `len` | reply payload length in bytes | | 16 | 4 | `len` | reply payload length in bytes |
| 20 | 4 | — | padding | | 20 | 4 | — | padding |
On failure the router replies `status = -1`; a mounted backend's negative On failure the backend replies `status = -1`, and that reply reaches the
status is forwarded to the client verbatim. A richer errno vocabulary is client directly — there is no party between them on the wire. (Kernel-served
future work — clients must treat *any* negative status as failure, not match paths produce no wire replies at all: `fs_resolve`/`fs_node` failures are
on -1. syscall register statuses.) A richer errno vocabulary is future work —
clients must treat *any* negative status as failure, not match on -1.
## Operations ## Operations
Values are append-only and never renumbered (the same evolution rule every Values are append-only and never renumbered (the same evolution rule every
danos protocol follows); an unrecognised operation gets a `status = -1` danos protocol follows). Send only values from this table: the shipped server
reply. decodes the operation into an exhaustive enum, so an out-of-range value is
not answered with a `status = -1` reply — it trips a safety check in safe
builds and is undefined otherwise. (The `-1` replies cover recognised but
refused operations, such as `mount` sent to a backend.)
| value | operation | request payload | reply | | value | operation | request payload | reply |
|------:|-----------|-----------------|-------| |------:|-----------|-----------------|-------|
@ -84,10 +89,12 @@ reply.
Notes per operation: Notes per operation:
- **open** — paths are absolute (`/mnt/usb/notes.txt`) or bare names - **open** — the path is the mount-relative path `fs_resolve` handed back
(`greeting`); bare names resolve in the VFS's flat ramfs, absolute paths (absolute-shaped: `/notes.txt` under fat's `/mnt/usb` mount). Bare names
route through the mount table (below). The returned `node` is an id in the (`greeting`) resolve nowhere — the flat ramfs is retired, and `fs_resolve`
*router's* open table; clients never see a backend's own ids. refuses non-absolute paths. The returned `node` is the *backend's* own
open-node id: with the router in the kernel there is no forwarding table,
and clients hold backend ids directly (see *Lifetimes and trust*).
- **read / write** — a single exchange moves at most 224 bytes - **read / write** — a single exchange moves at most 224 bytes
(`maximum_payload`); the client loops, advancing `offset` by the returned (`maximum_payload`); the client loops, advancing `offset` by the returned
`len`, until done (read) or the slice is written (write). A `write` reply `len`, until done (read) or the slice is written (write). A `write` reply
@ -105,8 +112,12 @@ Notes per operation:
the longest matching prefix wins, and an optional backend-side rewrite the longest matching prefix wins, and an optional backend-side rewrite
prefix maps a mount into the backend's namespace (fat serves `/mnt/usb` prefix maps a mount into the backend's namespace (fat serves `/mnt/usb`
from its volume root and `/var` from its `/var` subtree). from its volume root and `/var` from its `/var` subtree).
- **rename** — same-directory rename only (the router requires old and new to - **rename** — same-directory rename only: the backend compares the old and
resolve under one mount). new parent paths and refuses a mismatch. The client (`runtime.fs`) refuses
earlier when the two paths resolve to different backend endpoints, but that
check is coarser than "one mount" — one endpoint can serve several mounts
(fat serves `/mnt/usb` and `/var`), so a cross-mount rename reaches the
backend and fails on its same-directory check.
## Open flags ## Open flags
@ -156,11 +167,12 @@ table can grow.
## Lifetimes and trust ## Lifetimes and trust
Open-node ids live in the server. A client that dies without closing leaks Open-node ids live in the backend. A client that dies without closing leaks
nothing permanently: the VFS subscribes to the kernel's published process-exit nothing permanently: the backend (the FAT server) subscribes to the kernel's
events (docs/process-lifecycle.md) and releases a dead client's handles, published process-exit events (docs/process-lifecycle.md) and releases a dead
closing forwarded backend nodes best-effort. Ids are plain integers, not client's handles. The kernel VFS root needs no sweep at all — its node tokens
capabilities — the VFS trusts its callers with each other's ids today, which are permanent for a boot and carry no open state. Ids are plain integers, not
capabilities — a backend trusts its callers with each other's ids today, which
is acceptable while every client is part of the system image and worth is acceptable while every client is part of the system image and worth
revisiting (per-client id namespaces) before third-party binaries arrive. revisiting (per-client id namespaces) before third-party binaries arrive.
@ -169,8 +181,10 @@ revisiting (per-client id namespaces) before third-party binaries arrive.
What a non-Zig implementation may rely on, and what it must not: What a non-Zig implementation may rely on, and what it must not:
- Operation values, flag bits, `NodeKind` values, and struct layouts are - Operation values, flag bits, `NodeKind` values, and struct layouts are
**append-only and frozen once shipped** — the unit tests in `protocol.zig` **append-only and frozen once shipped**. The unit test in
pin them exactly so a refactor can't silently move them. `system/vfs-protocol.zig` pins a sample of them (the `DirectoryEntry`
size, `NodeKind` 01, `Operation` values 0, 4 and 5); this page is the
full record of the frozen values.
- The 256-byte message ceiling is a property of the current IPC transport, - The 256-byte message ceiling is a property of the current IPC transport,
not a promise; clients should read `maximum_payload`-shaped limits from the not a promise; clients should read `maximum_payload`-shaped limits from the
reply lengths they actually get (loop-until-done), not hard-code 224. reply lengths they actually get (loop-until-done), not hard-code 224.

View File

@ -90,11 +90,12 @@ own page tables — plus a [test harness](testing.md).
- **Isolation track****user mode + address-space isolation**. *Done: a - **Isolation track****user mode + address-space isolation**. *Done: a
higher-half kernel with a physmap (the low half is user space), per-process higher-half kernel with a physmap (the low half is user space), per-process
address spaces with CR3 switched on context switch, the `swapgs` discipline, address spaces with CR3 switched on context switch, the `swapgs` discipline,
`syscall`/`sysret`, a user-ELF loader, and `/system/services/init` running as a real `syscall`/`sysret`, a user-ELF loader, an address-space/stack reaper for exited
preemptive ring-3 process (PID 1). Remaining polish: an address-space/stack tasks, and `/system/services/init` running as a real preemptive ring-3 process
reaper for exited tasks, SMAP + fault-recovering copy-in/out, the real IPC (PID 1). Remaining polish: SMAP + fault-recovering copy-in/out, and TLB shootdown
syscalls (IPC_Call/IPC_ReplyWait — they arrive with the second user server), once a process has more than one thread. (The real IPC syscalls —
and TLB shootdown once a process has more than one thread.* `ipc_call`/`ipc_reply_wait` — have since been built and are the backbone every
driver and service speaks; see [ipc.md](ipc.md).)*
- **Resilience track** — fault → kill → notify, a supervisor/reincarnation server, - **Resilience track** — fault → kill → notify, a supervisor/reincarnation server,
resource cleanup on death, then a restartable driver as proof. Needs isolation. resource cleanup on death, then a restartable driver as proof. Needs isolation.
See [resilience.md](resilience.md). See [resilience.md](resilience.md).

View File

@ -137,20 +137,22 @@ be. `runtime.os` is only the interim staging ground: developed against the stock
toolchain so Phase 1 need not wait on the fork, then promoted near-verbatim into the toolchain so Phase 1 need not wait on the fork, then promoted near-verbatim into the
fork's `std/os/danos.zig`. fork's `std/os/danos.zig`.
### Retire `library/posix` ### Retire `library/posix` (done)
The `posix` compatibility layer (`unistd`, `stdio`) was the right instinct too early. The `posix` compatibility layer (`unistd`, `stdio`) was the right instinct too early.
Its whole value is POSIX *spellings* for POSIX software — and danos has no POSIX Its whole value was POSIX *spellings* for POSIX software — and danos has no POSIX
software; every current caller is danos-native code that could use `runtime.fs` software; every caller was danos-native code that could use `runtime.fs`
directly. The real POSIX story arrives later and from elsewhere (musl, or upstream directly. The real POSIX story arrives later and from elsewhere (musl, or upstream
`std`'s own posix over `std.os.danos`), which supersedes a hand-rolled shim. So it is `std`'s own posix over `std.os.danos`), which supersedes a hand-rolled shim. So it was
premature abstraction that adds a "which layer do I use?" fork with no payoff yet. premature abstraction that added a "which layer do I use?" fork with no payoff.
Its footprint is tiny: **five** call sites, all `unistd` file operations — Its footprint was tiny: **five** call sites, all `unistd` file operations —
`system/services/fat/fat.zig` (`mount`), the `vfs-test` and `fat-test` clients, and `system/services/fat/fat.zig` (`mount`), the `vfs-test` and `fat-test` clients, and
(from the boot-log work) `init.zig` and `log-flush.zig`. `stdio.zig` is dead — nothing (from the boot-log work) `init.zig` and `log-flush.zig`. `stdio.zig` was dead — nothing
imports it. The plan: build `runtime.fs`, migrate those five to it, delete imported it. The plan — build `runtime.fs`, migrate those five to it, delete
`library/posix/`, and drop the `posix` module from `build.zig`'s `addUserBinary`. `library/posix/`, and drop the `posix` module from `build.zig`'s `addUserBinary` — has
since been carried out: `library/` today holds only `mmio`, `runtime`, and
`xkeyboard-config`.
## Where danos stands: coverage vs. the gaps ## Where danos stands: coverage vs. the gaps
@ -158,7 +160,7 @@ What the seam needs, and what danos already provides:
| std need | danos today | Gap | | std need | danos today | Gap |
|----------|-------------|-----| |----------|-------------|-----|
| open / read / write / close / lseek | VFS (via the current `unistd`, → `runtime.fs`) | none — repackage | | open / read / write / close / lseek | VFS (via `runtime.fs`; the `unistd` shim is retired) | none — repackage |
| directory read (`getdents`) | VFS `readdir` | none — repackage | | directory read (`getdents`) | VFS `readdir` | none — repackage |
| mmap / munmap | native syscalls ([abi.zig](../system/abi.zig)) | none | | mmap / munmap | native syscalls ([abi.zig](../system/abi.zig)) | none |
| page allocator | over `mmap`, via `root.os.heap.page_allocator` override | ~30-line hook | | page allocator | over `mmap`, via `root.os.heap.page_allocator` override | ~30-line hook |
@ -172,7 +174,7 @@ What the seam needs, and what danos already provides:
| **cwd / chdir** | paths are absolute or bare | missing (no cwd anchor) | | **cwd / chdir** | paths are absolute or bare | missing (no cwd anchor) |
| **entropy / random** | — | missing (needed behind `vtable.random`) | | **entropy / random** | — | missing (needed behind `vtable.random`) |
| process spawn + exit status | `system_spawn` starts a *named ramdisk binary*; `ExitReason` is a *category* | no exec-of-path, no numeric `WEXITSTATUS` | | process spawn + exit status | `system_spawn` starts a *named ramdisk binary*; `ExitReason` is a *category* | no exec-of-path, no numeric `WEXITSTATUS` |
| threads | one thread per process | avoided via `-fsingle-threaded` (below) | | threads | native threads — `thread_spawn`/futex/`thread_join` (`runtime.Thread`) | `std.Thread` seam unwritten; avoided via `-fsingle-threaded` (below) |
| symlinks | `NodeKind` has the tag; unimplemented | low priority | | symlinks | `NodeKind` has the tag; unimplemented | low priority |
The clustering is clear: reads and memory are basically done; the real work is The clustering is clear: reads and memory are basically done; the real work is
@ -241,7 +243,7 @@ readdir/isatty/args/exit) exists. Those are downstream and out of scope here.
danos's biggest genuine gap, and the correctness-critical one: danos's biggest genuine gap, and the correctness-critical one:
- Add **mkdir / unlink / rename / truncate** to *both* the VFS wire protocol - Add **mkdir / unlink / rename / truncate** to *both* the VFS wire protocol
([protocol.zig](../system/services/vfs/protocol.zig)) and the FAT engine ([vfs-protocol.zig](../system/vfs-protocol.zig)) and the FAT engine
([engine.zig](../system/services/fat/engine.zig)), then expose them via `runtime.os`. ([engine.zig](../system/services/fat/engine.zig)), then expose them via `runtime.os`.
- Extend `stat` beyond `{size, kind}` to carry **mtime + inode + mode**`std`'s file - Extend `stat` beyond `{size, kind}` to carry **mtime + inode + mode**`std`'s file
stat needs them for build-cache validity — which in turn needs **wall-clock** time stat needs them for build-cache validity — which in turn needs **wall-clock** time
@ -270,8 +272,9 @@ fork):** the `runtime.os` seam, `cwd`, stdio-as-fds, and the compiler bring-up.
Build the compiler with **two load-bearing flags**: Build the compiler with **two load-bearing flags**:
- **`-fsingle-threaded`** removes `std.Thread` entirely — `Thread.spawn` is a hard - **`-fsingle-threaded`** removes `std.Thread` entirely — `Thread.spawn` is a hard
compile error under it, and `std.Io`'s threaded backend runs inline. danos being compile error under it, and `std.Io`'s threaded backend runs inline. The unwritten
one-thread-per-process is therefore **not** a blocker. Parallel codegen is a `std.Thread` seam is therefore **not** a blocker (danos has native threads now —
`thread_spawn`/futex — but the seam need not cover them yet). Parallel codegen is a
throughput optimisation, not a correctness requirement. throughput optimisation, not a correctness requirement.
- **`-fno-llvm -fno-lld`** keeps codegen and linking **in-process** (the self-hosted - **`-fno-llvm -fno-lld`** keeps codegen and linking **in-process** (the self-hosted
x86-64 backend + self-linker), so a single `build-exe` **never forks a child**. That x86-64 backend + self-linker), so a single `build-exe` **never forks a child**. That
@ -306,17 +309,18 @@ today), symlinks, and musl.
`system_spawn` only starts a *named ramdisk binary*, not exec of an arbitrary path. `system_spawn` only starts a *named ramdisk binary*, not exec of an arbitrary path.
Verify the self-hosted backend covers the target output before assuming child Verify the self-hosted backend covers the target output before assuming child
processes are optional. processes are optional.
- **The shim cannot host the compiler.** danos's current `runtime`/`posix` is fine for - **The shim cannot host the compiler.** danos's current `runtime` is fine for
danos's *own* native programs, but the compiler `import`s *upstream* `std`, which on danos's *own* native programs, but the compiler `import`s *upstream* `std`, which on
a non-target hits the void `system` stub. So the compiler forces the real target a non-target hits the void `system` stub. So the compiler forces the real target
(Phase 0's fork). Do not over-invest in extending the hand-shim for compiler (Phase 0's fork). Do not over-invest in extending the hand-shim for compiler
purposes; put that effort into `runtime.os` + the VFS/FAT operations, which both the purposes; put that effort into `runtime.os` + the VFS/FAT operations, which both the
fork *and* a future musl consume. fork *and* a future musl consume.
- **`"w"`/`O_CREAT` does not truncate — a silent-corruption bug on this road.** The FAT - **`"w"`/`O_CREAT` not truncating was a silent-corruption bug on this road — fixed in
engine's `writeFile` only *grows* `node.size`, so overwriting a shorter file leaves Phase 2.** The FAT engine's `writeFile` only *grows* `node.size`, so overwriting a
trailing garbage. Harmless for the boot log today, but for a compiler it means shorter file used to leave trailing garbage — for a compiler that means **corrupt
**corrupt `.o`/cache files that look like nondeterministic compiler bugs.** Land `.o`/cache files that look like nondeterministic compiler bugs.** `engine.truncate`
`truncate` (Phase 2) before the compiler ever writes cache. + the O_TRUNC open flag (wired through the VFS protocol and `runtime.fs`) closed
this before the compiler ever writes cache.
- **Exit status is categorical, not numeric.** `process_exit_reason` returns an - **Exit status is categorical, not numeric.** `process_exit_reason` returns an
`ExitReason` *category*, not a numeric code (`WEXITSTATUS`). Fine while spawn is `ExitReason` *category*, not a numeric code (`WEXITSTATUS`). Fine while spawn is
stubbed; the day `zig build` or external tools arrive, plan a kernel exit-record stubbed; the day `zig build` or external tools arrive, plan a kernel exit-record

View File

@ -1,6 +1,6 @@
# x86_64 low-level entry code: the CPU-exception stubs, plus the GDT/IDT load # x86_64 low-level entry code: the CPU-exception stubs, plus the GDT/IDT load
# helpers. Kept in a dedicated assembly file rather than inline asm because these # helpers. Kept in a dedicated assembly file rather than inline asm because these
# need real labels and cross-symbol jumps/calls (isr_common, exceptionHandler), # need real labels and cross-symbol jumps/calls (isr_common, interruptDispatch),
# and because `lgdt`/`lidt` memory operands aren't expressible in Zig inline asm. # and because `lgdt`/`lidt` memory operands aren't expressible in Zig inline asm.
# #
# Each exception vector normalises the stack to a uniform trap frame a dummy # Each exception vector normalises the stack to a uniform trap frame a dummy

View File

@ -400,8 +400,8 @@ fn isSubscriber(task: u32) bool {
} }
/// Enter S5 (soft off): write SLP_TYP|SLP_EN to the PM1 control register(s). /// Enter S5 (soft off): write SLP_TYP|SLP_EN to the PM1 control register(s).
/// Mirrors the kernel's power.zig sleepValue. Only reached from a PID-1 /// The kernel has no S5 path soft-off is owned here, from the service's own
/// shutdown request (M21.3). /// AML parse. Only reached from a PID-1 shutdown request (M21.3).
fn enterS5() void { fn enterS5() void {
if (!s5_valid or pm1a_cnt == 0) { if (!s5_valid or pm1a_cnt == 0) {
_ = runtime.system.write("power: S5 unavailable\n"); _ = runtime.system.write("power: S5 unavailable\n");