Re-organize the source tree as a monorepo mirroring the FHS

The source layout now mirrors the runtime filesystem hierarchy
(docs/danos-file-system-hierarchy-FSH.md): what lives under system/ in the
source is what a running danos represents under /system. Each service and
driver is a sub-project directory that is its own Zig module — cross-project
references go by module name, never by a path into another project's files.

Moves (all git mv, history preserved):
- src/            -> system/            (danos internals; the self-representation)
    root.zig      -> danos.zig          (the kernel<->user contract module)
    kernel/arch/  -> kernel/architecture/   (arch -> architecture)
    device/       -> devices/           (what /system/devices reflects)
    boot/         -> /boot              (the loaders, top level)
- sbin/           -> split by role:
    init, vfs     -> system/services/<name>/<name>.zig
    hpetd, busd   -> system/drivers/<name>/<name>.zig
    vfs-test      -> system/services/vfs/vfs-test.zig  (inside the vfs project)
- lib/            -> library/runtime/   (room for other libraries beside runtime)

The VFS wire protocol becomes its own module, system/services/vfs/protocol.zig
("vfs-protocol"): the vfs sub-project exposes its interface, and the runtime's
file layer imports it by name. First instance of the "protocol module" pattern
(docs/driver-model.md); usb/block will expose theirs the same way.

Also: fix a naming-standard violation in the protocol — Op -> Operation (and
req -> request, _pad -> _padding). Docs updated: /system/services added to the
FHS doc, a repository-layout section added to the docs index, and stale source
paths swept across comments and docs.

Runtime boot paths are unchanged (the bootloader still loads /sbin/init);
aligning the runtime filesystem to the FHS is a separate follow-up. Suite 35/35
plus host tests green.
This commit is contained in:
Daniel Samson
2026-07-10 12:55:56 +01:00
parent 15b70856c9
commit 8754d4e46a
83 changed files with 334 additions and 177 deletions
+48 -18
View File
@@ -116,27 +116,57 @@ other over IPC **endpoints** ([ipc.md](ipc.md)), and a **[driver](drivers.md)**
a device, maps its registers, and sleeps until the hardware interrupts it — which is
the whole reason for the arrangement ([vision.md](vision.md)).
## Repository layout
danos is a **monorepo of sub-projects**. Each service or driver is a directory that is
its own Zig module — it can hold as many files as it needs, and other sub-projects
reach it *by module name*, never by a path into its files. The source tree deliberately
**mirrors the runtime FHS** ([danos-file-system-hierarchy-FSH.md](danos-file-system-hierarchy-FSH.md)):
what you see under `system/` in the source is what a running danos represents under
`/system`.
```
system/ → /system danos's own internals (the self-representation)
danos.zig the kernel↔user ABI contract (the `danos` module)
parameters.zig initrd.zig shared contracts
kernel/ IPC, memory, scheduling, the private syscall dispatch
architecture/x86_64/ the `architecture` module (never named by generic code)
devices/ the device model /system/devices reflects (+ aml/)
drivers/ hpetd/ busd/ one sub-project per driver → /system/drivers
services/ init/ vfs/ system servers → /system/services (vfs/ holds
vfs.zig, vfs-test.zig, protocol.zig)
library/ → /lib the runtime library (the stable application ABI)
boot/ → /boot the loaders
tools/ test/ host-side build + QEMU test harness
```
A sub-project exposes its **public interface as a module**: `system/services/vfs/` owns
the VFS wire protocol (`protocol.zig`, the `vfs-protocol` module), which the runtime's
file layer imports by name. `usb`/`block` drivers will expose their protocols the same
way.
## Source map
| Area | Code |
|------|------|
| Boot methods (one per way of booting the kernel) | `src/boot/` — `efi.zig` (UEFI) → `BOOTX64.efi` |
| Kernel entry, panic, bring-up | `src/kernel/main.zig` |
| Shared loader↔kernel contract (`BootInfo`, `Framebuffer`, `MemoryMap`, `Syscall`, ABI) | `src/root.zig` |
| Physical frame allocator | `src/kernel/pmm.zig` |
| Kernel heap (`std.mem.Allocator`) | `src/kernel/heap.zig` |
| Scheduler (fixed-priority preemptive; blocking, wait queues) | `src/kernel/scheduler.zig` |
| Big kernel lock + interrupt-safe critical sections | `src/kernel/sync.zig` |
| IPC channels between kernel threads (message passing) | `src/kernel/ipc.zig` |
| IPC endpoints: cross-address-space call/reply, handles, notifications | `src/kernel/ipc-synchronous.zig` |
| User processes: ELF loading, address spaces, the syscall table | `src/kernel/process.zig` |
| Device tree + claim capability + `device_register` containment | `src/kernel/device-service.zig` |
| IRQ-as-IPC: routing a device interrupt to a driver's endpoint | `src/kernel/irq.zig` |
| Hardware discovery (ACPI/device tree) behind one neutral device model | `src/device/` |
| Framebuffer text console (mirrors to serial) | `src/kernel/console.zig` |
| In-kernel test cases | `src/kernel/tests.zig` |
| Arch-specific kernel code (`halt`, GDT/IDT/TSS, exception + interrupt stubs, page tables, APIC/IO-APIC/timer, serial, linker script) | `src/kernel/arch/x86_64/` |
| User runtime library (`rt`): syscalls, heap, stdio, IPC, device access | `lib/` |
| User-space programs shipped in the initrd (`init`, `vfs`, `hpetd` leaf driver, `busd` bus driver) | `sbin/` |
| Boot methods (one per way of booting the kernel) | `boot/` — `efi.zig` (UEFI) → `BOOTX64.efi` |
| Kernel entry, panic, bring-up | `system/kernel/main.zig` |
| Shared loader↔kernel contract (`BootInfo`, `Framebuffer`, `MemoryMap`, `Syscall`, ABI) | `system/danos.zig` |
| Physical frame allocator | `system/kernel/pmm.zig` |
| Kernel heap (`std.mem.Allocator`) | `system/kernel/heap.zig` |
| Scheduler (fixed-priority preemptive; blocking, wait queues) | `system/kernel/scheduler.zig` |
| Big kernel lock + interrupt-safe critical sections | `system/kernel/sync.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` |
| User processes: ELF loading, address spaces, the syscall table | `system/kernel/process.zig` |
| Device tree + claim capability + `device_register` containment | `system/kernel/device-service.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/` |
| Framebuffer text console (mirrors to serial) | `system/kernel/console.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/` |
| Runtime library (`runtime`): syscall wrappers, heap, stdio, IPC, device access — the stable application ABI | `library/runtime/` |
| System services (init, the VFS server + its `protocol` module) | `system/services/` |
| Device drivers, one sub-project each (`hpetd` leaf driver, `busd` bus driver) | `system/drivers/` |
| Build + `run-x86-64` (QEMU/OVMF) | `build.zig` |
| QEMU integration test harness | `test/qemu_test.py` |
+5 -5
View File
@@ -16,13 +16,13 @@ the RSDT's address is a field *inside* the RSDP. The platform follows that point
UEFI configuration table
│ the loader reads the RSDP's physical address
▼
BootInfo.acpi_rsdp (u64, in the shared `danos` module) src/root.zig
BootInfo.acpi_rsdp (u64, in the shared `danos` module) system/danos.zig
│ the kernel forwards the whole BootInfo
▼
platform.discover(boot_info, …) src/device/platform.zig
platform.discover(boot_info, …) system/devices/platform.zig
│ reads boot_info.acpi_rsdp, hands it to the ACPI backend
▼
acpi.discover(rsdp_phys, …) src/device/acpi.zig
acpi.discover(rsdp_phys, …) system/devices/acpi.zig
│ dereferences the RSDP, reads the pointer it contains
▼
RSDP ──(a field in the struct)──► RSDT / XSDT ──► SDTs (MADT, MCFG, FADT, HPET, DSDT…)
@@ -35,7 +35,7 @@ successor the **XSDT** (ACPI 2.0+) — which in turn lists every other SDT.
## Step 1 — the loader finds the RSDP
Only the firmware knows where ACPI lives, so the RSDP must be grabbed while UEFI is
still up. `acpiRootSystemDescriptorPointer()` in `src/boot/efi.zig` walks the UEFI
still up. `acpiRootSystemDescriptorPointer()` in `boot/efi.zig` walks the UEFI
**configuration table** for the ACPI GUID and returns the vendor pointer — the same
"grab it before `ExitBootServices`" pattern as the [framebuffer](framebuffer.md) and
the [memory map](memory-map.md).
@@ -48,7 +48,7 @@ module at all** (it imports only the shared `danos` module). So instead of a cal
deposits a value in the handoff struct:
```zig
// src/boot/efi.zig — while boot services are still up
// boot/efi.zig — while boot services are still up
.acpi_rsdp = if (acpiRootSystemDescriptorPointer()) |p| @intFromPtr(p) else 0,
```
+12 -12
View File
@@ -13,7 +13,7 @@ runtime dispatch. `build.zig` exposes one architecture's code as a module called
```zig
const arch_mod = b.addModule("arch", .{
.root_source_file = b.path("src/kernel/arch/x86_64/cpu.zig"),
.root_source_file = b.path("system/kernel/architecture/x86_64/cpu.zig"),
});
```
@@ -26,7 +26,7 @@ arch.halt(); // never says "x86_64"
```
Adding a second architecture is then a build-time choice: create
`src/kernel/arch/aarch64/`, and point the `arch` module at it when the target CPU is
`system/kernel/arch/aarch64/`, and point the `arch` module at it when the target CPU is
AArch64. `main.zig` and `console.zig` don't change. **That compiler-checked module
boundary _is_ the architecture interface** — when a new arch is missing a function
the generic kernel calls, the build fails and names exactly what's missing.
@@ -36,7 +36,7 @@ the generic kernel calls, the build fails and names exactly what's missing.
The split follows a simple test: does it name a CPU instruction, a hardware
register, or a memory-management structure? If so, it's arch-specific.
| Arch-specific — `src/kernel/arch/x86_64/` | Generic — kernel core |
| Arch-specific — `system/kernel/architecture/x86_64/` | Generic — kernel core |
|---|---|
| `cpu.zig`: `halt()` (`hlt`), later GDT/IDT/paging | `console.zig` — pure pixel math, works anywhere |
| `linker.ld` — link layout, load address | `main.zig` — `kmain` orchestration, panic handler |
@@ -51,9 +51,9 @@ should end up on the generic side; the arch module stays small.
There are really two independent questions, and it's worth not conflating them:
- **CPU architecture** (x86_64 vs AArch64): instructions, MMU, interrupts →
`src/kernel/arch/<cpu>/`.
`system/kernel/arch/<cpu>/`.
- **Boot protocol** (UEFI vs Raspberry Pi firmware + device tree): handled
*separately*, because loaders are their own binaries. `src/boot/efi.zig` builds
*separately*, because loaders are their own binaries. `boot/efi.zig` builds
`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
device-tree pointer, so that entry work would live in the AArch64 arch code.
@@ -61,24 +61,24 @@ There are really two independent questions, and it's worth not conflating them:
## Current x86_64 contents
- **`src/kernel/arch/x86_64/cpu.zig`** — the `arch` module root. Exposes `halt()` (see
- **`system/kernel/architecture/x86_64/cpu.zig`** — the `arch` module root. Exposes `halt()` (see
[halting.md](halting.md)), `init()` (bring up the descriptor tables),
`enablePaging()`, `setFaultHandler`, `readCr2`/`readCr3`, and the `CpuState`
trap frame.
- **`src/kernel/arch/x86_64/gdt.zig`** / **`idt.zig`** / **`tss.zig`** — the GDT, IDT and
- **`system/kernel/architecture/x86_64/gdt.zig`** / **`idt.zig`** / **`tss.zig`** — the GDT, IDT and
TSS plus CPU-exception handling (see [interrupts.md](interrupts.md)).
- **`src/kernel/arch/x86_64/paging.zig`** — the kernel's page tables (see
- **`system/kernel/architecture/x86_64/paging.zig`** — the kernel's page tables (see
[paging.md](paging.md)).
- **`src/kernel/arch/x86_64/apic.zig`** — the Local APIC and its timer, the source of
- **`system/kernel/architecture/x86_64/apic.zig`** — the Local APIC and its timer, the source of
device interrupts (see [device-interrupts.md](device-interrupts.md)).
- **`src/kernel/arch/x86_64/serial.zig`** / **`io.zig`** — the COM1 UART (the kernel's
- **`system/kernel/architecture/x86_64/serial.zig`** / **`io.zig`** — the COM1 UART (the kernel's
machine-readable log channel, see [testing.md](testing.md)) and the shared
port-I/O + MSR primitives.
- **`src/kernel/arch/x86_64/isr.s`** — the exception stubs, the `lgdt`/`lidt`/`ltr` load
- **`system/kernel/architecture/x86_64/isr.s`** — the exception stubs, the `lgdt`/`lidt`/`ltr` load
helpers, and the context switch (`switch_context` / `task_trampoline`, see
[scheduling.md](scheduling.md)) — real assembly, since Zig inline asm can't
express them.
- **`src/kernel/arch/x86_64/linker.ld`** — the kernel link layout (fixed low load
- **`system/kernel/architecture/x86_64/linker.ld`** — the kernel link layout (fixed low load
address, one PT_LOAD per permission set).
The kernel entry point `_start` currently still lives in the generic `main.zig` as
+4 -4
View File
@@ -18,7 +18,7 @@ matters for understanding why. This page maps the landscape so the
new ISA.
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 `src/kernel/arch/<name>/`.
page-table formats, and calling conventions. Each needs its own `system/kernel/arch/<name>/`.
## The Raspberry Pi models
@@ -57,16 +57,16 @@ the DTB/ACPI tells you what devices exist.
## What danos needs, layer by layer
- **One CPU arch module: `src/kernel/arch/aarch64/`** — covering the Zero 2 W and Pi 3-5,
- **One CPU arch module: `system/kernel/arch/aarch64/`** — covering the Zero 2 W and Pi 3-5,
providing the same `arch` interface as x86_64: `halt`, context switch,
interrupt/exception vectors, page tables, a UART, a timer. No `src/kernel/arch/arm/` is
interrupt/exception vectors, page tables, a UART, a timer. No `system/kernel/arch/arm/` is
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
that parses the DTB's `/memory` and `/reserved-memory` into the neutral
[`MemoryMap`](memory-map.md) — the same neutral handoff `efi.zig` produces, just
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.
- **The UEFI loader mostly carries over.** `src/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
x86-specific bits are the ELF machine check (`.X86_64`) and the SysV calling
convention for the kernel jump. So an `aarch64`-UEFI target (QEMU `virt` + AAVMF)
+117
View File
@@ -0,0 +1,117 @@
# 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).
## Directory structure
| Path | Description |
|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| / | Primary hierarchy root and root directory of the entire file system hierarchy. |
| /bin | Essential command binaries that need to be available in single-user mode, including to bring up the system or repair it, for all users (e.g., cat, ls, cp). |
| /boot | Boot loader files (e.g., EFI, initrd.img ). |
| /dev | POSIX Device files (e.g., /dev/null, /dev/disk0, /dev/tty, /dev/random). |
| /etc | Host-specific system-wide configuration files. |
| /home | Users' home directories, containing saved files, personal settings, etc. |
| /lib | Libraries essential for the binaries in /bin and /sbin. eg realtime, system, ipc etc. |
| /sbin | Essential system binaries (e.g init) |
| /srv | Site-specific data served by this system, such as data and scripts for web servers, data offered by FTP servers, and repositories for version control systems |
| /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/drivers | driver binaries, one sub-project each (e.g. /system/drivers/hpetd) |
| /system/services | system-service binaries — the VFS server, init, and other user-mode servers (e.g. /system/services/vfs, /system/services/init) |
| /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. |
| /usr | Secondary hierarchy for read-only user data; contains the majority of (multi-)user utilities and applications. Should be shareable and read-only. |
| /var | Variable files: files whose content is expected to continually change during normal operation of the system, such as logs, spool files, and temporary e-mail files. |
## File types
POSIX specifies the long format of the ls command to represent the Unix file type as the first letter for an entry.
| type | symbol | Description |
|-------------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| regular | - | An ordinary file holding an uninterpreted byte stream. Reads and writes are positional, and the file grows on demand (e.g., a binary in /bin, a config file in /etc). |
| directory | d | A container mapping names to other files. It may only be modified through directory operations, never written to directly. |
| symbolic link | l | A file whose contents are a path that is resolved in its place. The target need not exist, and may cross mount points. |
| FIFO special | p | A named pipe: an in-order byte stream between processes, where writers block until a reader opens the other end. |
| block special | b | A device node addressed in fixed-size blocks with the kernel free to buffer and reorder access (e.g., /dev/disk0). |
| character special | c | A device node addressed as an unbuffered byte stream, delivered to the driver in order (e.g., /dev/tty, /dev/null). |
| socket | s | A named endpoint for bidirectional message-passing between processes, bound to a path rather than an address. |
## /dev
`/dev` holds the names through which processes reach devices. It is deliberately not
the device tree: the tree — every node discovered by ACPI or PCI enumeration, with its
resources and its parent — lives under [/system/devices](#directory-structure) and is
addressed by device id. `/dev` is the much smaller set of devices that have a driver
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
([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
`system/services/vfs/vfs.zig` reserves for M10 and what the `Stat.kind` field is for; **none of it is
implemented today.** The current VFS is a flat, in-memory ramfs of eight nodes, with no
directories at all and `kind` hardcoded to zero. The three sections below describe the
intended shape, and are honest about which parts the kernel can already support.
### Character devices
A character device is a byte stream with no addressable position: bytes are delivered
to the driver in the order written, and a read consumes what is there. Terminals,
serial lines, keyboards and mice are all of this shape. These are the natural first
device nodes in danos, because a character driver needs nothing the kernel doesn't
already provide — it claims its device, maps its registers with `mmio_map`, and blocks
on `replyWait` for either an interrupt or a client request. `system/drivers/hpetd/hpetd.zig` is already
that program, minus the client half.
The obstacle is not the file type, it is which hardware a ring-3 driver can actually
drive. Port I/O is unavailable to user space — the TSS I/O permission bitmap is absent
and IOPL is never raised — so `in`/`out` from a driver is a #GP. That excludes the
16550 UART at `0x3F8` and PS/2 at `0x60`/`0x64`, which is to say it excludes the
obvious implementations of `/dev/tty`, `/dev/ttyS0` and a keyboard node. Until either
port I/O grants or a memory-mapped UART exist, serial output stays a kernel service
reached through the `write` system call rather than a file. A memory-mapped device such
as the framebuffer has no such problem and is the more likely first real entry here.
### Block devices
A block device is addressed in fixed-size blocks and, unlike a character device, the
layer above is free to buffer, reorder, coalesce and retry requests against it. Disks
and other persistent storage are the whole population of this class.
**danos cannot host a block driver at all today,** and the reason is worth stating
plainly because it is not a matter of unwritten code. Every storage controller worth
naming is a bus master: it is programmed by handing it the physical address of a
descriptor ring and left to read and write memory on its own. A ring-3 driver cannot
build such a ring, because `mmap` returns writeback-cached, physically discontiguous
pages and never discloses their physical address. Nor should it be allowed to: a device
programmed with an arbitrary physical address writes to arbitrary physical memory, and
page tables do not sit between a device and RAM — an IOMMU does. Granting a DMA-capable
device to a driver process, with no IOMMU programmed, is equivalent to granting ring 0,
which would forfeit the isolation that motivates user-space drivers in the first place.
Block devices therefore wait on DMA-capable memory, memory barriers, and VT-d/DMAR —
the M14–M16 work in [driver-model.md](driver-model.md). A ramdisk over the initrd is
the one block-shaped thing implementable now, and it needs no driver process.
### Pseudo-devices
A pseudo-device has the interface of a device and no hardware behind it: `/dev/null`
discarding writes and reading as end-of-file, `/dev/zero` reading as an endless run of
zero bytes, `/dev/full` failing writes with `ENOSPC`, `/dev/random` and `/dev/urandom`
yielding unpredictable bytes.
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
process, no `device_claim`, no MMIO grant and no interrupt. The VFS server answers them
out of its own address space — `null` and `zero` are a few lines each in
`system/services/vfs/vfs.zig`'s `read` and `write` handlers. Doing so forces the two pieces of
structure that every later device node depends on and that the flat ramfs currently
lacks: a directory, so that `/dev/null` is a path rather than a name; and a populated
`Stat.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
options on this kernel are `RDRAND`/`RDSEED` where CPUID advertises them, and the HPET
counter's low bits as a poor fallback. Neither is a seeded CSPRNG, and a `/dev/random`
that is merely unpredictable-looking is worse than none — nothing should be keyed from
it until it is a real one.
+1 -1
View File
@@ -18,7 +18,7 @@ Interrupt delivery on modern x86 goes through the **APIC**, not the legacy 8259
PIC. There are two halves; we only need one so far:
- The **Local APIC** (per-CPU, memory-mapped at physical `0xFEE00000`) handles the
CPU's own timer and receives interrupts routed to it. `src/kernel/arch/x86_64/apic.zig`.
CPU's own timer and receives interrupts routed to it. `system/kernel/architecture/x86_64/apic.zig`.
- The **IO-APIC** routes *external* device lines (keyboard, etc.) to LAPIC vectors.
Not needed for the timer — it'll arrive with the keyboard.
+9 -9
View File
@@ -32,7 +32,7 @@ plain bus driver with no controller — a USB hub — is also a real thing.
## The device table is the spine
danos already has the right central structure. `src/kernel/device-service.zig` holds a table of
danos already has the right central structure. `system/kernel/device-service.zig` holds a table of
`DeviceDesc`, each with a parent, a class, and a set of resources. Firmware discovery
seeds it ([discovery.md](discovery.md)); `device_register` grows it.
@@ -57,7 +57,7 @@ is not an address window. Discovery is trusted; user space is not.
### What a bus driver looks like
`sbin/busd.zig` is the smallest honest one. Its "bus" is the HPET's register block and
`system/drivers/busd/busd.zig` is the smallest honest one. Its "bus" is the HPET's register block and
its "devices" are the block's comparators:
```zig
@@ -94,8 +94,8 @@ A "family" is two modules, not one:
- **A protocol module** — the IPC message types that let a class driver talk to
*whatever* published its device. This is the part that makes class drivers portable.
danos already has one of each: `lib/device.zig` is a logic module,
[`lib/vfs-protocol.zig`](lib/vfs-protocol.zig) is a protocol module shared by `sbin/vfs.zig`
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`
and its clients. The pattern generalises directly:
```
@@ -106,7 +106,7 @@ lib/
pci.zig module "pci" — ECAM, BAR decode, capability walk
usb.zig module "usb" — descriptors, control transfers, hubs
proto/
vfs.zig module "proto.vfs" (today: lib/vfs-protocol.zig)
vfs.zig module "proto.vfs" (today: system/services/vfs/protocol.zig)
block.zig module "proto.block"
hid.zig module "proto.hid"
@@ -183,7 +183,7 @@ const dev_ep = ipc.callCap(h, // ... mint a per-device endpoint,
**The blocker.** An HCD is a DMA-engine programmer. It needs a descriptor ring the
device can read, which means memory that is (a) physically contiguous, (b) at a
physical address the driver knows, (c) of the right cacheability, and (d) pinned.
[`sysMmap`](src/kernel/process.zig) gives you *none* of the four: it calls `pmm.alloc()`
[`sysMmap`](system/kernel/process.zig) gives you *none* of the four: it calls `pmm.alloc()`
once per page, maps writeback-cached, and never reveals a physical address.
**The fix.**
@@ -217,7 +217,7 @@ doorbell.* = i; // volatile store to UC MMIO
// nothing stops the compiler reordering these; the device reads a stale descriptor
```
So the rules, which belong in `lib/mmio.zig` and behind `arch`:
So the rules, which belong in `library/mmio.zig` and behind `arch`:
| Situation | Required |
|---|---|
@@ -241,12 +241,12 @@ with a compiler barrier alone. ARM is not, and [vision.md](vision.md) makes ARM
condition. Build the abstraction while there is one caller to fix.
(Zig note: `@fence` was **removed in 0.16**. Use `@atomicRmw(..., .seq_cst)` for a full
barrier, or per-arch inline asm — which is what `lib/mmio.zig` should hide.)
barrier, or per-arch inline asm — which is what `library/mmio.zig` should hide.)
## M15 — interrupts for PCI devices
**The blocker, and it's a hard one.** No PCI device can take an interrupt today.
[`addBars`](src/device/acpi.zig) records `.memory` and `.io_port` BARs and never an
[`addBars`](system/devices/acpi.zig) records `.memory` and `.io_port` BARs and never an
`.irq`; there is no `_PRT` parsing anywhere in the tree. `hpetd` only works because the
HPET advertises its own routing options in its own registers — a privilege no ordinary
device has.
+4 -4
View File
@@ -22,7 +22,7 @@ say.*
## The capability: claim before touch
The five driver syscalls (`src/root.zig`, dispatched in `src/kernel/process.zig`):
The five driver syscalls (`system/danos.zig`, dispatched in `system/kernel/process.zig`):
| # | Call | Meaning |
|---|------|---------|
@@ -138,7 +138,7 @@ Two properties worth knowing:
## A whole driver
`sbin/hpetd.zig` is ~150 lines and does all of it. The shape:
`system/drivers/hpetd/hpetd.zig` is ~150 lines and does all of it. The shape:
```zig
const hpet = findHpet(buf) orelse return; // device_enumerate, look for
@@ -206,7 +206,7 @@ bus driver may only ever subdivide what it already owns.
A device with **no resources** is legal and common. A USB device is reached through its
controller, not by MMIO, so it gets `resource_count = 0`.
See [`sbin/busd.zig`](../sbin/busd.zig) for a complete one, and
See [`system/drivers/busd/busd.zig`](../system/drivers/busd/busd.zig) for a complete one, and
[driver-model.md](driver-model.md) for how bus drivers, class drivers and host
controller drivers fit together.
@@ -265,7 +265,7 @@ Worth knowing before you write the second driver:
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
tests can't see this. Linux flushes remote-IRR by toggling the entry to edge and
back. See the note at the top of `src/kernel/irq.zig`.
back. See the note at the top of `system/kernel/irq.zig`.
## Verifying it
+6 -6
View File
@@ -10,7 +10,7 @@ that hands us a working CPU, a memory map, and a screen, and then gets out of th
way.
The key thing to understand: **UEFI is not our OS, it's a stepping stone.** It
exists to load *us*. Our `src/boot/efi.zig` is a UEFI *application* — a normal program
exists to load *us*. Our `boot/efi.zig` is a UEFI *application* — a normal program
that the firmware runs — and its entire purpose is to gather what the kernel needs
and then jump into the kernel.
@@ -23,7 +23,7 @@ Partition (ESP)** and running a file at a well-known fallback path:
esp/EFI/BOOT/BOOTX64.efi <- the "removable media" default for x86-64
```
That's exactly the layout `build.zig` assembles. It builds `src/boot/efi.zig` for the
That's exactly the layout `build.zig` assembles. It builds `boot/efi.zig` for the
`uefi` target, installs it to `esp/EFI/BOOT/BOOTX64.efi`, and drops the kernel ELF
at `esp/kernel`. The `run-x86-64` step then points QEMU at OVMF (UEFI firmware for
virtual machines) and presents that `esp/` directory to the guest as a FAT drive.
@@ -139,7 +139,7 @@ kernel is freestanding and uses the **SysV AMD64** convention (first argument in
read garbage.
So both sides pin the convention explicitly to SysV via the shared
`danos.kernel_abi` (defined in `src/root.zig`). The loader's function-pointer type
`danos.kernel_abi` (defined in `system/danos.zig`). The loader's function-pointer type
and the kernel's `_start` both reference it, so the pointer lands in the register
the kernel expects. This is the whole reason `kernel_abi` lives in the shared
`danos` module: it's a contract both binaries must agree on. See
@@ -149,7 +149,7 @@ the kernel expects. This is the whole reason `kernel_abi` lives in the shared
The loader and kernel are two *separate* binaries built for two different targets,
so everything they exchange must have an identically-defined memory layout. That's
what `src/root.zig` provides — imported by both as the `danos` module:
what `system/danos.zig` provides — imported by both as the `danos` module:
- `BootInfo` — the top-level struct passed to the kernel (currently just the
framebuffer; this is where future handoff data like the memory map will go).
@@ -170,10 +170,10 @@ power on
-> loadKernel (read danos ELF, load PT_LOAD segments to 0x100000)
-> exitBootServices (retry until the memory-map key holds)
-> jump to e_entry, boot_info pointer in RDI
-> kernel _start (src/kernel/main.zig: framebuffer console, then halt)
-> kernel _start (system/kernel/main.zig: framebuffer console, then halt)
```
Bottom line: **UEFI's job is to give us a CPU, memory, and a framebuffer, then
disappear.** `src/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
on our own.
+2 -2
View File
@@ -3,7 +3,7 @@
Once the kernel knows what RAM exists ([memory-map.md](memory-map.md)), it needs a
way to *hand out* that RAM: give me a free page of physical memory, and later,
here's one back. That's the **physical frame allocator** (a "physical memory
manager", hence `src/kernel/pmm.zig`). It deals only in fixed 4 KiB **frames** — the
manager", hence `system/kernel/pmm.zig`). It deals only in fixed 4 KiB **frames** — the
natural unit because that's the granularity the CPU's paging hardware maps — and
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.
@@ -33,7 +33,7 @@ RAM is 32768 frames — a **4 KiB bitmap, a single frame**. Even 64 GiB needs on
## How it works
State lives in `src/kernel/pmm.zig`: the `bitmap` slice, `total_frames`, `used_frames`,
State lives in `system/kernel/pmm.zig`: the `bitmap` slice, `total_frames`, `used_frames`,
and a `next_hint` marking where the next allocation scan should start.
### init(map) — building it from the memory map
+2 -2
View File
@@ -9,10 +9,10 @@ write a 32-bit value to the right address, and a pixel changes color. That's
exactly what `Console.pixel` does:
```zig
self.rowPtr(y)[x] = color; // src/kernel/console.zig
self.rowPtr(y)[x] = color; // system/kernel/console.zig
```
Our `Framebuffer` struct (`src/root.zig`) is the four facts you need to
Our `Framebuffer` struct (`system/danos.zig`) is the four facts you need to
address it:
| Field | Meaning |
+3 -3
View File
@@ -16,7 +16,7 @@ safely, until the machine is reset or powered off.
## The core of it: `hlt`
Everything comes down to one x86 instruction. It's CPU-specific, so it lives in
the arch module, `src/kernel/arch/x86_64/cpu.zig` (see [arch.md](arch.md)), and the
the arch module, `system/kernel/architecture/x86_64/cpu.zig` (see [arch.md](arch.md)), and the
generic kernel calls it as `arch.halt()`:
```zig
@@ -83,7 +83,7 @@ treats the call:
signature for a kernel entry point — the bootloader jumps in and nothing ever
jumps back out.
You can see the chain in `src/kernel/main.zig`: `_start` is `noreturn`, it calls
You can see the chain in `system/kernel/main.zig`: `_start` is `noreturn`, it calls
`kmain` which is `noreturn`, which ends by calling `arch.halt()` which is
`noreturn`. The "never returns" property is threaded all the way down.
@@ -106,7 +106,7 @@ There are three halt sites, and they're all the same idea:
`arch.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 `src/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
loop so the message stays on screen:
+1 -1
View File
@@ -7,7 +7,7 @@ top of both to provide what the rest of the kernel actually wants: `alloc(n)` /
the thing that unlocks dynamic data structures — lists, hash maps, driver state,
eventually a process table.
It's generic kernel code (`src/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.
## A growable free-list allocator
+5 -5
View File
@@ -9,7 +9,7 @@ reboot is miserable.
This is the machinery that catches those faults and prints what happened instead.
It's all x86_64-specific, so it lives behind the [arch](arch.md) boundary in
`src/kernel/arch/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 are wired up so far;
device interrupts (timer, keyboard, via the APIC) come later, on the same IDT.
## First the GDT
@@ -20,7 +20,7 @@ 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
known selectors: `0x08` kernel code, `0x10` kernel data.
`src/kernel/arch/x86_64/gdt.zig` holds three flat descriptors — a required null entry,
`system/kernel/architecture/x86_64/gdt.zig` holds three flat descriptors — a required null entry,
plus code and data — where the only bits that matter in long mode are the access
byte and the code segment's long-mode (`L`) flag. Loading it (`gdt_flush` in
`isr.s`) does two things: `lgdt`, then reload the segment registers. The data
@@ -33,7 +33,7 @@ into CS:RIP.
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
fields, a quirk of the format), the code selector (`0x08`), and flags: `0x8E`
means present, ring 0, 64-bit interrupt gate. `src/kernel/arch/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`
(`idt_flush`).
@@ -49,7 +49,7 @@ hit a fault *while trying to deliver another fault* — very often because the
current stack pointer is bad, so pushing the exception frame itself faulted. If
the #DF handler then tried to push onto that same bad stack, it would fault a
third time and **triple-fault** — an instant reset. So the #DF gate is pointed at
**IST1**, a small dedicated stack (`src/kernel/arch/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
descriptor in the GDT (`gdt.setTss`), and load it into the task register with
@@ -60,7 +60,7 @@ which is why the GDT grew from three entries to five.
On an exception the CPU pushes a small frame (SS, RSP, RFLAGS, CS, RIP) and, for
*some* vectors, an **error code**. That inconsistency is a nuisance, so each stub
in `src/kernel/arch/x86_64/isr.s` normalises it: vectors that don't get a hardware error
in `system/kernel/architecture/x86_64/isr.s` normalises it: vectors that don't get a hardware error
code push a dummy `0`, then every stub pushes its **vector number** and jumps to a
shared tail, `isr_common`. The tail pushes all the general registers and calls the
Zig handler with a pointer to the whole thing.
+4 -4
View File
@@ -8,15 +8,15 @@ concern, not an afterthought.
There are two layers, built a milestone apart:
- **`src/kernel/ipc.zig`** — a bounded blocking channel between *kernel threads*,
- **`system/kernel/ipc.zig`** — a bounded blocking channel between *kernel threads*,
described below. The primitive, and where the blocking discipline was worked out.
- **`src/kernel/ipc-synchronous.zig`** — synchronous call/reply between *processes*, across
- **`system/kernel/ipc-synchronous.zig`** — synchronous call/reply between *processes*, across
address spaces. What user-space servers and drivers actually talk over. It's the
second half of this document.
## The channel
The first form is a **bounded blocking channel** (`src/kernel/ipc.zig`): a fixed-size
The first form is a **bounded blocking channel** (`system/kernel/ipc.zig`): a fixed-size
ring buffer of messages with a producer/consumer rendezvous, built on the
scheduler's [wait queues](scheduling.md).
@@ -54,7 +54,7 @@ expected `5050`), and neither task busy-waits — they block and wake each other
A channel connects two kernel threads sharing one address space. Real servers are
*processes*, so the payload has to cross an address-space boundary. That's
`src/kernel/ipc-synchronous.zig`, and its shape is L4's: a synchronous **rendezvous** at an
`system/kernel/ipc-synchronous.zig`, and its shape is L4's: a synchronous **rendezvous** at an
`Endpoint`, with the message copied directly from the sender's pages to the receiver's
(`copyAcross` walks both sets of page tables through the physmap — no CR3 switch, no
bounce buffer).
+3 -3
View File
@@ -13,7 +13,7 @@ kernel follows.
## The log is multi-sink
`src/kernel/log.zig` is the diagnostic log. It fans a message out to a set of
`system/kernel/log.zig` is the diagnostic log. It fans a message out to a set of
registered **sinks**, each best-effort and self-guarding:
```zig
@@ -36,7 +36,7 @@ Properties that matter:
## The framebuffer is *not* a log sink
The framebuffer is a general graphics surface, **not inherently a text terminal**.
Today `src/kernel/console.zig` paints a text grid on it as a *bootstrap* console, but
Today `system/kernel/console.zig` paints a text grid on it as a *bootstrap* console, but
that's a stop-gap: once the driver machinery exists the framebuffer becomes a proper
**graphics device driver**, and the text crutch goes away. So the log must not assume
it — routing the verbose log through a pixel console would bake in "the OS is text".
@@ -58,7 +58,7 @@ screen. `console.write` is a no-op when the firmware gave us no framebuffer.
A framebuffer is not guaranteed — a headless server exposes no UEFI Graphics Output
Protocol. That used to be *fatal* (the loader failed the boot). Now the loader hands
over a "no framebuffer" descriptor (`base == 0`) rather than failing, and
`Framebuffer.present()` (in `src/root.zig`) gates every on-screen path. A headless,
`Framebuffer.present()` (in `system/danos.zig`) gates every on-screen path. A headless,
serial-less machine boots and runs correctly — it just goes quiet.
## Last-resort channels (no text output at all)
+2 -2
View File
@@ -27,7 +27,7 @@ danos's own neutral format, and the kernel only ever sees that.**
## The neutral format
Defined in `src/root.zig`, the shared loader↔kernel contract:
Defined in `system/danos.zig`, the shared loader↔kernel contract:
```zig
pub const MemoryKind = enum(u32) {
@@ -68,7 +68,7 @@ pub const BootInfo = extern struct {
## The loader side (UEFI)
Two functions in `src/boot/efi.zig`, called from `exitBootServices`:
Two functions in `boot/efi.zig`, called from `exitBootServices`:
- **`classify`** maps each UEFI descriptor to a `MemoryKind`:
`conventional_memory` **and** `boot_services_code`/`boot_services_data → usable`;
+2 -2
View File
@@ -7,7 +7,7 @@ which live in memory we'd like to reclaim and don't control), switches CR3 onto
them, and — crucially — maps with **real permissions**.
It's x86_64-specific (the 4-level table format is an Intel/AMD thing), so it lives
behind the [arch](arch.md) boundary in `src/kernel/arch/x86_64/paging.zig`.
behind the [arch](arch.md) boundary in `system/kernel/architecture/x86_64/paging.zig`.
## The format
@@ -27,7 +27,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
`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:
`danos.physToVirt(phys)`. The layout constants live in `src/root.zig`:
`danos.physToVirt(phys)`. The layout constants live in `system/danos.zig`:
| region | virtual base | PML4 slot |
|--------|--------------|-----------|
+2 -2
View File
@@ -6,8 +6,8 @@ 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
task runs when) and its decisions are O(1), unlike a fair-share scheduler.
The scheduler proper (`src/kernel/sched.zig`) is generic; the context switch and new-task
stack setup are architecture-specific (`src/kernel/arch/x86_64/`, see [arch](arch.md)).
The scheduler proper (`system/kernel/sched.zig`) is generic; the context switch and new-task
stack setup are architecture-specific (`system/kernel/architecture/x86_64/`, see [arch](arch.md)).
## Tasks
+3 -3
View File
@@ -131,7 +131,7 @@ Whatever the top goal, the *sequence* is the same and seL4 validates starting si
model you already have (the interrupt-flag discipline in
[scheduling.md](scheduling.md)) stay largely intact: one lock around kernel entry
instead of rethinking every critical section. **Done** — see
`src/kernel/sync.zig`.
`system/kernel/sync.zig`.
4. **Later, if contention bites,** evolve toward **per-core run queues + explicit
affinity** (the Fiasco.OC direction) — also the more real-time-predictable model.
5. **Placement stays a user-space policy** — the kernel runs a thread on the core it's
@@ -151,7 +151,7 @@ next lands.
- **Core enumeration** — the MADT parse records every usable Local APIC (with its
`apic_id`, which an AP wake targets); `platform.cpus()` returns the list. See
[discovery.md](discovery.md).
- **The big kernel lock** (`src/kernel/sync.zig`) — one coarse spinlock guarding the
- **The big kernel lock** (`system/kernel/sync.zig`) — one coarse spinlock guarding the
scheduler queues and IPC, always held with local interrupts disabled. It is held
*across* a context switch and released by whichever task resumes (the hand-off
rule); `task_trampoline` releases it for a freshly-spawned task. `scheduler.zig` and
@@ -164,7 +164,7 @@ next lands.
the highest-priority ready task; per-core queues are a later optimisation.
- **AP wake to long mode** — `arch.startSecondary` drives INIT–SIPI–SIPI (via the
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](../src/kernel/arch/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`,
publishes its per-CPU pointer, and reports in. Verified in QEMU with `-smp 4`:
all four cores report `online`.
+1 -1
View File
@@ -6,7 +6,7 @@ System calls (syscalls) are the bridge between your programs and the operating s
> `isr.s` does the `swapgs` + kernel-stack switch and reuses the interrupt
> dispatcher). The `int 0x80` gate is kept alongside as a minimal test path. The
> current call set is still a placeholder — `0 = exit(code)`, `1 = ping`,
> `2 = write(ptr, len)`, `3 = sleep(ms)` (see `src/kernel/process.zig`); the
> `2 = write(ptr, len)`, `3 = sleep(ms)` (see `system/kernel/process.zig`); the
> handler dispatches on whether the caller is a scheduled process (its own address
> space) or a borrowed test thread. The microkernel set below (IPC_Call /
> IPC_ReplyWait / Yield) replaces it once a second user server exists.
+2 -2
View File
@@ -1,6 +1,6 @@
# SysV: the kernel's calling convention
Several places in danos say "the kernel is SysV" — most visibly `src/root.zig`:
Several places in danos say "the kernel is SysV" — most visibly `system/danos.zig`:
```zig
pub const kernel_abi: std.builtin.CallingConvention = .{ .x86_64_sysv = .{} };
@@ -52,7 +52,7 @@ argument arrives in **RCX**, not RDI.
danos's two binaries default to different conventions:
- `src/boot/efi.zig` is built for the UEFI target, so its default C convention is
- `boot/efi.zig` is built for the UEFI target, so its default C convention is
Microsoft x64 (first argument → RCX).
- The kernel is freestanding, so its convention is SysV (first argument → RDI).
+5 -5
View File
@@ -8,7 +8,7 @@ without a human staring at the screen.
There are two layers:
- **Host unit tests** (`zig build test`) — for pure, platform-independent logic in
the shared `danos` module (the handoff layout in `src/root.zig`). These compile
the shared `danos` module (the handoff layout in `system/danos.zig`). These compile
for the host and run natively.
- **QEMU integration tests** (`python3 test/qemu_test.py`) — boot the real kernel
and check its behaviour. This is the interesting part.
@@ -17,7 +17,7 @@ There are two layers:
The framebuffer console draws pixels, which a test can't read without
screen-scraping. So the kernel also writes everything to a **serial port**
(`src/kernel/arch/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). `Console.write` mirrors every
byte to it, so all kernel output — boot log, memory summary, exception reports —
appears on serial as plain text.
@@ -29,7 +29,7 @@ a new architecture's UART is what makes the same tests run there.
## In-kernel test cases
Building with `-Dtest-case=<name>` makes the kernel, after normal bring-up, run one
self-test from `src/kernel/tests.zig` instead of idling. Each case writes structured
self-test from `system/kernel/tests.zig` instead of idling. Each case writes structured
markers to serial:
```
@@ -108,7 +108,7 @@ firmware, boot method, serial device). The cases are architecture-neutral —
So bringing up a second architecture — an AArch64 Raspberry Pi is the motivating
one — means:
1. implement `src/kernel/arch/aarch64/` (CPU ops, its UART, exception vectors, page
1. implement `system/kernel/arch/aarch64/` (CPU ops, its UART, exception vectors, page
tables) behind the same `arch` interface,
2. add an `aarch64` entry to `ARCHES` with its `qemu-system-aarch64` invocation,
@@ -118,7 +118,7 @@ architectures".
## Writing a new case
1. Add a function to `src/kernel/tests.zig` and dispatch it in `run` on its name.
1. Add a function to `system/kernel/tests.zig` and dispatch it in `run` on its name.
2. Emit `[PASS]/[FAIL]` lines and a `DANOS-TEST-RESULT:` line (non-faulting cases),
or trigger the condition and rely on the handler's output (faulting cases).
3. Add an entry to `CASES` in `test/qemu_test.py` with the regex that proves it.