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
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
83 changed files with 334 additions and 177 deletions

View File

@ -2,8 +2,8 @@
Codename: Shodan
Version: 1
A small operating system, written from scratch in Zig — a bootloader (`src/boot/`)
and a microkernel (`src/kernel/`), sharing a neutral handoff contract (`src/root.zig`).
A small operating system, written from scratch in Zig — a bootloader (`boot/`)
and a microkernel (`system/kernel/`), sharing a neutral handoff contract (`system/danos.zig`).
It boots x86-64 via UEFI, and so far has a framebuffer console, a physical frame
allocator, its own paging with W^X permissions, interrupt/exception handling, a
LAPIC timer, a kernel heap, a fixed-priority preemptive scheduler, and in-kernel IPC

View File

@ -77,7 +77,7 @@ fn addUserBinary(
},
}),
});
exe.setLinkerScript(b.path("lib/user.ld"));
exe.setLinkerScript(b.path("library/runtime/user.ld"));
exe.entry = .{ .symbol_name = "_start" };
exe.image_base = 0x7000_0000_0000;
exe.use_llvm = true;
@ -95,21 +95,21 @@ pub fn build(b: *std.Build) void {
// so the module inherits the target of whichever binary imports it the
// freestanding kernel or the UEFI bootloader.
const danos_module = b.addModule("danos", .{
.root_source_file = b.path("src/root.zig"),
.root_source_file = b.path("system/danos.zig"),
});
// Kernel tunables (maximum_cpus, stack sizes, tick rate). A dependency-free module of
// compile-time constants, imported wherever a knob is read; keeps the trade-offs
// in one place instead of scattered across the tree. See src/configuration.zig.
// in one place instead of scattered across the tree. See system/parameters.zig.
const parameters_module = b.addModule("parameters", .{
.root_source_file = b.path("src/parameters.zig"),
.root_source_file = b.path("system/parameters.zig"),
});
// Architecture-specific kernel code (CPU ops, entry, later GDT/IDT/paging).
// The generic kernel imports this as "architecture" and never names x86_64, so a new
// architecture is a matter of pointing this module at a different directory.
const architecture_module = b.addModule("architecture", .{
.root_source_file = b.path("src/kernel/arch/x86_64/cpu.zig"),
.root_source_file = b.path("system/kernel/architecture/x86_64/cpu.zig"),
.imports = &.{
.{ .name = "danos", .module = danos_module }, // paging uses the shared BootInformation/memory-map types
.{ .name = "parameters", .module = parameters_module }, // maximum_cpus, ist_stack_size, timer_hz
@ -117,45 +117,55 @@ pub fn build(b: *std.Build) void {
});
// CPU-exception stubs real assembly, since they need cross-symbol
// jumps/calls that Zig inline asm can't express (see the file's header).
architecture_module.addAssemblyFile(b.path("src/kernel/arch/x86_64/isr.s"));
architecture_module.addAssemblyFile(b.path("system/kernel/architecture/x86_64/isr.s"));
// The AP bring-up trampoline: 16-/32-/64-bit mode-switch code that can't be
// inline asm (it runs relocated to a low page, not at its link address).
architecture_module.addAssemblyFile(b.path("src/kernel/arch/x86_64/trampoline.s"));
architecture_module.addAssemblyFile(b.path("system/kernel/architecture/x86_64/trampoline.s"));
// Firmware-agnostic device discovery. The generic kernel imports this as
// "platform" and asks it to enumerate hardware into a backend-neutral device
// tree, never naming ACPI (or, later, device-tree) the same discipline the
// architecture module applies to CPU code. The backend is selected at runtime from
// the boot handoff (see src/device/platform.zig).
// the boot handoff (see system/devices/platform.zig).
const platform_module = b.addModule("platform", .{
.root_source_file = b.path("src/device/platform.zig"),
.root_source_file = b.path("system/devices/platform.zig"),
.imports = &.{
.{ .name = "danos", .module = danos_module }, // BootInformation (carries the ACPI RSDP)
.{ .name = "parameters", .module = parameters_module }, // maximum_cpus (the discovery pool)
},
});
// The VFS wire protocol: the vfs sub-project's public interface, exposed as its
// own module. Both the vfs server and the runtime's file layer (unistd/stdio)
// depend on this contract by name neither reaches into the other's files. This
// is the first "protocol module" (see docs/driver-model.md); usb/block will
// expose theirs the same way.
const vfs_protocol_module = b.addModule("vfs-protocol", .{
.root_source_file = b.path("system/services/vfs/protocol.zig"),
});
// The user-space runtime library (a nascent libc): system_call wrappers, the
// C-convention heap, IPC helpers, the process start shim. Compiled into every
// user binary (see addUserBinary), so it inherits each exe's `.large` code
// model do NOT set a target/code_model here. It imports `danos` for the
// shared SystemCall numbers.
// shared SystemCall numbers and `vfs-protocol` for the file API.
const runtime_module = b.addModule("runtime", .{
.root_source_file = b.path("lib/runtime.zig"),
.root_source_file = b.path("library/runtime/runtime.zig"),
.imports = &.{
.{ .name = "danos", .module = danos_module },
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
},
});
// The initrd container format, shared by the kernel (unpacks it) and the
// build-time packer tools/mkinitrd.zig (produces it). No dependencies.
const initrd_module = b.addModule("initrd", .{
.root_source_file = b.path("src/user/protocol/initrd.zig"),
.root_source_file = b.path("system/initrd.zig"),
});
// Compile-time configuration the kernel reads as `@import("build_options")`. The
// QEMU test harness sets -Dtest-case=<name> to run one self-test at boot.
const test_case = b.option([]const u8, "test-case", "Kernel self-test case to run at boot (see src/kernel/tests.zig)");
const test_case = b.option([]const u8, "test-case", "Kernel self-test case to run at boot (see system/kernel/tests.zig)");
const build_options = b.addOptions();
build_options.addOption(?[]const u8, "test_case", test_case);
const build_options_module = build_options.createModule();
@ -173,7 +183,7 @@ pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "kernel",
.root_module = b.createModule(.{
.root_source_file = b.path("src/kernel/main.zig"),
.root_source_file = b.path("system/kernel/main.zig"),
.target = kernel_target,
.optimize = optimize,
.code_model = .kernel, // kernel runs in the top 2 GiB (higher half)
@ -192,7 +202,7 @@ pub fn build(b: *std.Build) void {
},
}),
});
exe.setLinkerScript(b.path("src/kernel/arch/x86_64/linker.ld"));
exe.setLinkerScript(b.path("system/kernel/architecture/x86_64/linker.ld"));
exe.entry = .{ .symbol_name = "_start" };
// The self-hosted linker ignores parts of the linker script (PHDRS,
// /DISCARD/, AT(), section order); the higher-half layout depends on the
@ -210,17 +220,17 @@ pub fn build(b: *std.Build) void {
// Built by the shared user-binary recipe (see addUserBinary): freestanding,
// linked into the kernel's user region against the `runtime` runtime library, and
// started in ring 3 by the kernel's user-ELF loader.
const init_exe = addUserBinary(b, kernel_target, runtime_module, "init", "sbin/init.zig");
const init_exe = addUserBinary(b, kernel_target, runtime_module, "init", "system/services/init/init.zig");
b.installArtifact(init_exe);
// --- initrd: a bundle of extra user binaries (VFS server + drivers) ---
// Each is built by the same user-binary recipe, then packed into one image by
// the host-side mkinitrd tool. The bootloader ferries the image to the kernel,
// which unpacks it and spawns each program (src/user/protocol/initrd.zig).
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, "vfs", "sbin/vfs.zig");
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, "vfs-test", "sbin/vfs-test.zig");
const hpetd_exe = addUserBinary(b, kernel_target, runtime_module, "hpetd", "sbin/hpetd.zig");
const busd_exe = addUserBinary(b, kernel_target, runtime_module, "busd", "sbin/busd.zig");
// which unpacks it and spawns each program (system/initrd.zig).
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, "vfs", "system/services/vfs/vfs.zig");
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, "vfs-test", "system/services/vfs/vfs-test.zig");
const hpetd_exe = addUserBinary(b, kernel_target, runtime_module, "hpetd", "system/drivers/hpetd/hpetd.zig");
const busd_exe = addUserBinary(b, kernel_target, runtime_module, "busd", "system/drivers/busd/busd.zig");
// Pack the user binaries into the initrd image with the host-side Python tool
// (the container format is trivial, and Python sidesteps std API churn). Args:
@ -242,13 +252,13 @@ pub fn build(b: *std.Build) void {
const initrd_install = b.addInstallFile(initrd_img, "bin/initrd.img");
b.getInstallStep().dependOn(&initrd_install.step);
// Boot methods live in src/boot/, one per way of getting the kernel running.
// Boot methods live in boot/, one per way of getting the kernel running.
// Each is its own binary/entry (a loader is built for its own target); today
// that's UEFI for x86-64, with room for e.g. a device-tree path for the Pis.
const efiexe = b.addExecutable(.{
.name = "BOOTX64",
.root_module = b.createModule(.{
.root_source_file = b.path("src/boot/efi.zig"),
.root_source_file = b.path("boot/efi.zig"),
.target = b.resolveTargetQuery(.{
.cpu_arch = .x86_64,
.os_tag = .uefi,
@ -362,7 +372,7 @@ pub fn build(b: *std.Build) void {
// here (compiled for the host rather than inheriting a freestanding target).
const mod_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/root.zig"),
.root_source_file = b.path("system/danos.zig"),
.target = target,
.optimize = optimize,
}),

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` |

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,
```

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

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)

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 M14M16 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.

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.

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.

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

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.

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

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 |

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:

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

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.

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).

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)

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`;

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 |
|--------|--------------|-----------|

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

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 INITSIPISIPI (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`.

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.

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).

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.

View File

@ -3,7 +3,7 @@
//! and Zig `std` containers share one heap.
//!
//! The algorithm is a straight port of the kernel's first-fit free list
//! (src/kernel/heap.zig): an address-ordered singly linked list of free blocks,
//! (system/kernel/heap.zig): an address-ordered singly linked list of free blocks,
//! split on allocation and coalesced with neighbours on free. The only thing
//! that changes on this side of the system_call boundary is where memory comes from
//! `grow` asks the kernel for pages via `mmap` instead of mapping frames

View File

@ -15,7 +15,7 @@ pub const heap = @import("heap.zig");
pub const ipc = @import("ipc.zig");
pub const start = @import("start.zig");
/// The VFS wire protocol (shared with the VFS server).
pub const vfs_protocol = @import("vfs-protocol.zig");
pub const vfs_protocol = @import("vfs-protocol");
/// POSIX-style file API: open/read/write/lseek/stat/close.
pub const unistd = @import("unistd.zig");
/// C stdio: fopen/fread/fwrite/fseek/ftell/fclose over unistd.

View File

@ -1,10 +1,10 @@
//! POSIX-style file API for user programs the low level under C stdio. Files
//! are named objects served by the user-space VFS server (sbin/vfs.zig); each
//! are named objects served by the user-space VFS server (system/services/vfs/vfs.zig); each
//! call marshals a request, IPC_Calls the VFS, and unmarshals the reply. The
//! kernel knows nothing of files or fds the fd table lives here, per process.
const std = @import("std");
const protocol = @import("vfs-protocol.zig");
const protocol = @import("vfs-protocol");
const ipc = @import("ipc.zig");
const danos = @import("danos");
@ -42,15 +42,15 @@ const Result = struct { reply: protocol.Reply, payload: []u8 };
/// One request/reply round trip: [Request header][send payload] -> VFS ->
/// [Reply header][receive payload]. The receive payload is written into `out`.
fn transact(req: protocol.Request, send: []const u8, out: []u8) ?Result {
fn transact(request: protocol.Request, send: []const u8, out: []u8) ?Result {
const h = vfs() orelse return null;
var message: [protocol.message_maximum]u8 = undefined;
@memcpy(message[0..protocol.req_size], std.mem.asBytes(&req));
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
const slen = @min(send.len, protocol.maximum_payload);
@memcpy(message[protocol.req_size..][0..slen], send[0..slen]);
@memcpy(message[protocol.request_size..][0..slen], send[0..slen]);
var rbuf: [protocol.message_maximum]u8 = undefined;
const n = ipc.call(h, message[0 .. protocol.req_size + slen], &rbuf) catch return null;
const n = ipc.call(h, message[0 .. protocol.request_size + slen], &rbuf) catch return null;
if (n < protocol.reply_size) return null;
const reply = std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]);
const rpl = @min(n - protocol.reply_size, out.len);
@ -61,8 +61,8 @@ fn transact(req: protocol.Request, send: []const u8, out: []u8) ?Result {
/// Open (or create, with O_CREAT) `path`; returns an fd or -1.
pub fn open(path: []const u8, flags: u32) i32 {
const fd = allocFd() orelse return -1;
const req = protocol.Request{ .op = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = flags };
const r = transact(req, path, &.{}) orelse {
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = flags };
const r = transact(request, path, &.{}) orelse {
fds[fd].used = false;
return -1;
};
@ -84,8 +84,8 @@ fn fdPtr(fd: i32) ?*Fd {
pub fn read(fd: i32, buffer: []u8) isize {
const f = fdPtr(fd) orelse return -1;
const want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload));
const req = protocol.Request{ .op = .read, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
const r = transact(req, &.{}, buffer) orelse return -1;
const request = protocol.Request{ .operation = .read, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
const r = transact(request, &.{}, buffer) orelse return -1;
if (r.reply.status != 0) return -1;
f.offset += r.reply.len;
return @intCast(r.reply.len);
@ -95,8 +95,8 @@ pub fn read(fd: i32, buffer: []u8) isize {
pub fn write(fd: i32, data: []const u8) isize {
const f = fdPtr(fd) orelse return -1;
const want: u32 = @intCast(@min(data.len, protocol.maximum_payload));
const req = protocol.Request{ .op = .write, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
const r = transact(req, data[0..want], &.{}) orelse return -1;
const request = protocol.Request{ .operation = .write, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
const r = transact(request, data[0..want], &.{}) orelse return -1;
if (r.reply.status != 0) return -1;
f.offset += r.reply.len;
return @intCast(r.reply.len);
@ -110,9 +110,9 @@ pub fn lseek(fd: i32, off: i64, whence: u32) i64 {
SEEK_SET => 0,
SEEK_CURRENT => @intCast(f.offset),
SEEK_END => blk: {
const req = protocol.Request{ .op = .stat, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
const request = protocol.Request{ .operation = .stat, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
var sbuf: [@sizeOf(protocol.Stat)]u8 = undefined;
const r = transact(req, &.{}, &sbuf) orelse return -1;
const r = transact(request, &.{}, &sbuf) orelse return -1;
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.Stat)) return -1;
const st = std.mem.bytesToValue(protocol.Stat, sbuf[0..@sizeOf(protocol.Stat)]);
break :blk @intCast(st.size);
@ -132,9 +132,9 @@ pub fn stat(path: []const u8, out: *protocol.Stat) i32 {
if (fd < 0) return -1;
defer close(fd);
const f = fdPtr(fd).?;
const req = protocol.Request{ .op = .stat, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
const request = protocol.Request{ .operation = .stat, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
var sbuf: [@sizeOf(protocol.Stat)]u8 = undefined;
const r = transact(req, &.{}, &sbuf) orelse return -1;
const r = transact(request, &.{}, &sbuf) orelse return -1;
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.Stat)) return -1;
out.* = std.mem.bytesToValue(protocol.Stat, sbuf[0..@sizeOf(protocol.Stat)]);
return 0;
@ -143,7 +143,7 @@ pub fn stat(path: []const u8, out: *protocol.Stat) i32 {
/// Close an fd (best effort tells the VFS to release the open file).
pub fn close(fd: i32) void {
const f = fdPtr(fd) orelse return;
const req = protocol.Request{ .op = .close, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
_ = transact(req, &.{}, &.{});
const request = protocol.Request{ .operation = .close, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
_ = transact(request, &.{}, &.{});
f.used = false;
}

View File

@ -1,5 +1,5 @@
//! Shared definitions that form the contract between a bootloader
//! (src/boot/, e.g. efi.zig built as BOOTX64.efi) and the kernel (src/kernel/main.zig).
//! (boot/, e.g. efi.zig built as BOOTX64.efi) and the kernel (system/kernel/main.zig).
//!
//! Both binaries import this as the "danos" module, so the handoff layout is
//! defined in exactly one place.
@ -59,7 +59,7 @@ pub const physmap_base: u64 = 0xFFFF_8800_0000_0000;
pub const kernel_virt_base: u64 = 0xFFFF_FFFF_8000_0000;
/// The kernel system_call numbers the single source of truth shared by the kernel
/// dispatcher (src/kernel/process.zig) and the user runtime library, so the two
/// dispatcher (system/kernel/process.zig) and the user runtime library, so the two
/// can never drift. The set is deliberately microkernel-minimal: file/device I/O
/// is not here it lives in user-space servers reached through the IPC calls.
/// The table grows one milestone at a time; see docs/syscall.md.
@ -91,7 +91,7 @@ pub const SystemCall = enum(u64) {
/// event loop can't disagree about which bit means "the hardware spoke".
pub const notify_badge_bit: u64 = 1 << 63;
/// A device class, mirroring src/device/device-model.zig's `DeviceClass` **in order**
/// A device class, mirroring system/devices/device-model.zig's `DeviceClass` **in order**
/// (its `@intFromEnum` values cross the system_call boundary in `DeviceDescriptor.class`).
/// Keep the two in sync.
pub const DeviceClass = enum(u32) {
@ -105,7 +105,7 @@ pub const DeviceClass = enum(u32) {
unknown,
};
/// A resource kind, mirroring src/device/device-model.zig's `ResourceKind` in order.
/// A resource kind, mirroring system/devices/device-model.zig's `ResourceKind` in order.
pub const ResourceKind = enum(u32) {
memory,
io_port,
@ -250,7 +250,7 @@ pub const BootInformation = extern struct {
init_len: u64 = 0,
/// The initrd image (a bundle of extra user binaries the VFS server and
/// device drivers), read off the boot volume into memory that survives the
/// handoff, same as `init` above. 0/0 = no initrd. See src/user/protocol/initrd.zig.
/// handoff, same as `init` above. 0/0 = no initrd. See system/initrd.zig.
initrd_base: u64 = 0,
initrd_len: u64 = 0,
};

View File

@ -64,7 +64,7 @@ fn findHpet(buffer: []device.DeviceDescriptor) ?Found {
for (buffer[0..n]) |d| {
if (d.class != @intFromEnum(device.DeviceClass.timer)) continue;
// Skip comparator children a bus driver may have published below the block
// (see sbin/busd.zig) we want the register block itself.
// (see system/drivers/busd/busd.zig) we want the register block itself.
if (d.parent != device.no_parent) continue;
var mmio: ?u64 = null;
var irq: ?u64 = null;

View File

@ -373,7 +373,7 @@ pub fn irqRouteRaw(n: u32) u32 {
return ioapic.entryLow(n);
}
// --- device-IRQ plumbing, for src/kernel/irq.zig -----------------------------
// --- device-IRQ plumbing, for system/kernel/irq.zig -----------------------------
//
// The generic IRQ layer speaks GSIs and vectors; everything below hides the fact
// that on x86_64 those mean "I/O APIC redirection entry" and "IDT gate". The

View File

@ -4,7 +4,7 @@
//!
//! `init` maps the I/O APIC and **masks every input** the correct quiescent state
//! on a legacy-free machine. Lines are then unmasked one at a time, as user-space
//! drivers bind them (`routeGsi`/`unmaskGsi`, driven by src/kernel/irq.zig).
//! drivers bind them (`routeGsi`/`unmaskGsi`, driven by system/kernel/irq.zig).
//!
//! Two entry points, for two kinds of caller. `routeIrq` takes a legacy **ISA IRQ**
//! and resolves it through the MADT overrides for in-kernel use, and still without
@ -96,7 +96,7 @@ pub fn routeIrq(irq: u8, vector: u8, apic_id: u8) void {
// `routeIrq` above takes an *ISA IRQ* and resolves it through the MADT overrides.
// A driver-bound interrupt is already a **GSI** (the device told us so, e.g. the
// HPET's `Tn_INT_ROUTE_CAP`), so it needs no override lookup just the redirection
// entry. These three are what `src/kernel/irq.zig` drives.
// entry. These three are what `system/kernel/irq.zig` drives.
//
// Callers must serialise: the I/O APIC is reached through an index/data register
// pair, so two cores interleaving `registerWrite` would corrupt each other. The kernel

View File

@ -48,9 +48,9 @@ pub const ENOMEM: i64 = 6; // out of memory
/// A badge with this bit set is an asynchronous notification (e.g. an IRQ), not a
/// message from a client there is no reply owed. The low bits carry the source
/// (a GSI for IRQs). Posted by `notifyFromIsr`, from the ISR in src/kernel/irq.zig;
/// (a GSI for IRQs). Posted by `notifyFromIsr`, from the ISR in system/kernel/irq.zig;
/// the message path uses a plain task-id badge with this bit clear. Defined in the
/// shared contract (src/root.zig), because ring 3 has to test the same bit.
/// shared contract (system/danos.zig), because ring 3 has to test the same bit.
pub const notify_badge_bit: u64 = danos.notify_badge_bit;
/// End of the user (low) canonical half user buffers must lie below it.

View File

@ -22,7 +22,7 @@
//!
//! Binding is capability-gated exactly like `mmio_map`: the caller must have
//! `device_claim`ed the device, and the GSI must come from one of that device's `irq`
//! resources in the discovered device table (src/kernel/device-service.zig). A driver can
//! resources in the discovered device table (system/kernel/device-service.zig). A driver can
//! therefore never bind an interrupt it doesn't own a raw-GSI system_call would let
//! any process steal the keyboard's line.
//!

View File

@ -291,7 +291,7 @@ fn ownedGsi(t: *scheduler.Task, device_id: u64, resource_index: u64) ?u32 {
/// irq_bind(device_id, resource_index, endpoint) -> 0/-1: deliver that device's IRQ to the
/// endpoint as an asynchronous IPC notification. The driver then blocks in
/// IPC_ReplyWait and is woken by the ISR; see src/kernel/irq.zig for the cycle.
/// IPC_ReplyWait and is woken by the ISR; see system/kernel/irq.zig for the cycle.
fn systemIrqBind(state: *architecture.CpuState) void {
const t = scheduler.current();
if (t.aspace == 0) return fail(state);

View File

@ -1,7 +1,7 @@
//! /sbin/init the first user-space program, PID 1. Built as its own
//! freestanding binary (see build.zig), shipped on the boot volume at sbin/init,
//! loaded by the bootloader, and started in ring 3 as a scheduled process by the
//! kernel (src/kernel/process.zig). It links against the shared user runtime
//! kernel (system/kernel/process.zig). It links against the shared user runtime
//! library `runtime` and talks to the kernel only through `runtime`'s system_call wrappers.
//!
//! Today it proves the C-convention heap works, then settles into a heartbeat:

View File

@ -5,9 +5,9 @@
//! a Stat). Everything fits in one IPC message (<= ipc MESSAGE_MAXIMUM = 256 bytes).
//!
//! This is user-space only the kernel knows nothing of files or paths; it only
//! moves the bytes. Shared by lib/unistd.zig (client) and sbin/vfs.zig (server).
//! moves the bytes. Shared by library/runtime/unistd.zig (client) and system/services/vfs/vfs.zig (server).
pub const Op = enum(u32) {
pub const Operation = enum(u32) {
open, // open(path) -> node id
close, // close(node)
read, // read(node, offset, len) -> bytes
@ -19,7 +19,7 @@ pub const Op = enum(u32) {
/// for `open` the path is the payload and `len` is its length. `offset`/`len`
/// carry the read/write position and count.
pub const Request = extern struct {
op: Op,
operation: Operation,
node: u64,
offset: u64,
len: u32,
@ -31,23 +31,23 @@ pub const Request = extern struct {
/// Stat size).
pub const Reply = extern struct {
status: i32,
_pad: u32 = 0,
_padding: u32 = 0,
node: u64 = 0,
len: u32 = 0,
_pad2: u32 = 0,
_padding2: u32 = 0,
};
pub const Stat = extern struct {
size: u64,
kind: u32,
_pad: u32 = 0,
_padding: u32 = 0,
};
pub const message_maximum: usize = 256;
pub const req_size: usize = @sizeOf(Request);
pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);
/// Largest inline payload that still fits one IPC message alongside a header.
pub const maximum_payload: usize = message_maximum - req_size;
pub const maximum_payload: usize = message_maximum - request_size;
/// Open flags.
pub const O_CREAT: u32 = 1;

View File

@ -67,13 +67,13 @@ fn fail(out: []u8) usize {
/// Handle one request; write the reply into `out`, return its length.
fn handle(message: []const u8, out: []u8) usize {
if (message.len < protocol.req_size) return fail(out);
const req = std.mem.bytesToValue(protocol.Request, message[0..protocol.req_size]);
const payload = message[protocol.req_size..];
if (message.len < protocol.request_size) return fail(out);
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
const payload = message[protocol.request_size..];
switch (req.op) {
switch (request.operation) {
.open => {
const name = payload[0..@min(payload.len, req.len)];
const name = payload[0..@min(payload.len, request.len)];
const ni = findNode(name) orelse createNode(name) orelse return fail(out);
for (&opens, 0..) |*o, i| {
if (!o.used) {
@ -84,30 +84,30 @@ fn handle(message: []const u8, out: []u8) usize {
return fail(out);
},
.read => {
const of = openAt(req.node) orelse return fail(out);
const of = openAt(request.node) orelse return fail(out);
const nd = &nodes[of.node];
const off: usize = @intCast(req.offset);
const off: usize = @intCast(request.offset);
if (off >= nd.size) return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); // EOF
const n = @min(@min(nd.size - off, req.len), protocol.maximum_payload);
const n = @min(@min(nd.size - off, request.len), protocol.maximum_payload);
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, nd.data[off .. off + n]);
},
.write => {
const of = openAt(req.node) orelse return fail(out);
const of = openAt(request.node) orelse return fail(out);
const nd = &nodes[of.node];
const off: usize = @intCast(req.offset);
const off: usize = @intCast(request.offset);
if (off > nd.data.len) return fail(out);
const n = @min(@min(payload.len, req.len), nd.data.len - off);
const n = @min(@min(payload.len, request.len), nd.data.len - off);
@memcpy(nd.data[off .. off + n], payload[0..n]);
if (off + n > nd.size) nd.size = off + n;
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, &.{});
},
.stat => {
const of = openAt(req.node) orelse return fail(out);
const of = openAt(request.node) orelse return fail(out);
const st = protocol.Stat{ .size = nodes[of.node].size, .kind = 0 };
return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.Stat) }, std.mem.asBytes(&st));
},
.close => {
if (req.node < opens.len) opens[@intCast(req.node)].used = false;
if (request.node < opens.len) opens[@intCast(request.node)].used = false;
return writeReply(out, .{ .status = 0 }, &.{});
},
}