From 47610e8ee29ee683e58d8b542249d69435cc0a98 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:20:14 +0100 Subject: [PATCH] Device manager (increment 1): discover + match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The device manager is the ring-3 process that turns the device tree into a running system — the udev-analog. It is mechanism-vs-policy done right: the kernel enumerates the hardware and enforces the claim capability; this decides which driver serves which device, using no special privilege (the same device_enumerate any process could call). This first increment does the discovery + matching half: system/services/ device-manager enumerates /system/devices, matches each device to a driver by class (a small static policy table), and logs the decision — finding the HPET (a timer) and deciding `hpet` serves it. It does not spawn yet: spawning needs a `system_spawn` system call (the kernel spawns every initial-ramdisk binary in a loop today), which is the next increment. New `device-manager` test; suite 36/36 plus host tests. --- build.zig | 4 ++ docs/README.md | 4 +- system/kernel/tests.zig | 40 +++++++++++++ .../device-manager/device-manager.zig | 58 +++++++++++++++++++ test/qemu_test.py | 6 ++ 5 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 system/services/device-manager/device-manager.zig diff --git a/build.zig b/build.zig index f2bb3be..b75052e 100644 --- a/build.zig +++ b/build.zig @@ -257,6 +257,7 @@ pub fn build(b: *std.Build) void { const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, "vfs-test", "system/services/vfs/vfs-test.zig"); const hpet_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, "hpet", "system/drivers/hpet/hpet.zig"); const bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, "bus", "system/drivers/bus/bus.zig"); + const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, "device-manager", "system/services/device-manager/device-manager.zig"); // Pack the user binaries into the initial_ramdisk image with the host-side Python tool // (the container format is trivial, and Python sidesteps std API churn). Args: @@ -272,11 +273,14 @@ pub fn build(b: *std.Build) void { mk_run.addFileArg(hpet_exe.getEmittedBin()); mk_run.addArg("bus"); mk_run.addFileArg(bus_exe.getEmittedBin()); + mk_run.addArg("device-manager"); + mk_run.addFileArg(device_manager_exe.getEmittedBin()); // Also install the packed binaries to their FHS homes, so zig-out is a true image // of the filesystem — even though at boot they arrive inside the initial-ramdisk. for ([_]struct { *std.Build.Step.Compile, []const u8 }{ .{ vfs_exe, "system/services" }, + .{ device_manager_exe, "system/services" }, .{ hpet_exe, "system/drivers" }, .{ bus_exe, "system/drivers" }, }) |entry| { diff --git a/docs/README.md b/docs/README.md index 9941354..4e416c3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -152,7 +152,7 @@ system/ → /system danos's own internals (the self-representation) architecture/x86_64/ the `architecture` module (never named by generic code) devices/ the device model /system/devices reflects (+ aml/) drivers/ hpet/ bus/ one sub-project per driver → /system/drivers - services/ init/ vfs/ system servers → /system/services (vfs/ holds + services/ init/ vfs/ device-manager/ system servers → /system/services (vfs/ holds vfs.zig, vfs-test.zig, protocol.zig) library/ → /lib libraries, one sub-directory each runtime/ the danos-native runtime — the stable application ABI @@ -193,7 +193,7 @@ appears in the private-ABI path. | Arch-specific kernel code (`halt`, GDT/IDT/TSS, exception + interrupt stubs, page tables, APIC/IO-APIC/timer, serial, linker script) | `system/kernel/architecture/x86_64/` | | danos-native runtime (`runtime`): syscall wrappers, heap, IPC, device access — the stable application ABI | `library/runtime/` | | POSIX/C compatibility (`posix`): unistd, stdio — the one place POSIX names are allowed | `library/posix/` | -| System services (init, the VFS server + its `protocol` module) | `system/services/` | +| System services (init, the VFS server + `protocol`, the device-manager) | `system/services/` | | Device drivers, one sub-project each (`hpet` leaf driver, `bus` bus driver) | `system/drivers/` | | Build + `run-x86-64` (QEMU/OVMF) | `build.zig` | | QEMU integration test harness | `test/qemu_test.py` | diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index 9a3f6be..0e82436 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -120,6 +120,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { irqFreeTest(); } else if (eql(case, "bus")) { busTest(boot_information); + } else if (eql(case, "device-manager")) { + deviceManagerTest(boot_information); } else if (eql(case, "poweroff")) { powerTest(.off); } else if (eql(case, "reboot")) { @@ -1173,6 +1175,44 @@ fn busTest(boot_information: *const BootInformation) void { result(); } +/// The device manager (a ring-3 service) enumerates /system/devices, matches each +/// device to a driver, and — eventually — spawns it. This increment only checks the +/// discovery+matching half: it must find the HPET (a timer) and decide `hpet` serves +/// it, printing "device-manager: ok". It uses no special privilege — the same +/// `device_enumerate` any process could call. (Spawning is the next increment.) +fn deviceManagerTest(boot_information: *const BootInformation) void { + log("DANOS-TEST-BEGIN: device-manager\n", .{}); + if (boot_information.initial_ramdisk_len == 0) { + check("bootloader handed over an initial_ramdisk", false); + result(); + return; + } + const image = @as([*]const u8, @ptrFromInt(system.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len]; + const rd = initial_ramdisk.Reader.init(image) orelse { + check("initial_ramdisk image is valid", false); + result(); + return; + }; + + process.write_count = 0; + process.write_from_user = false; + check("device-manager spawned from the initial_ramdisk", spawnNamed(rd, "device-manager")); + + const prefix = "device-manager: ok"; + scheduler.setPriority(1); + const deadline = architecture.millis() + 10000; + while (architecture.millis() < deadline) { + if (process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix)) break; + scheduler.yield(); + } + scheduler.setPriority(4); + + const ok = process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix); + check("device manager enumerated the tree and matched a driver to a device", ok); + check("its syscalls came from user mode (CPL 3)", process.write_from_user); + result(); +} + /// Every child `bus` registered must have each of its resources inside a parent /// resource of the same kind — the invariant `device_register` exists to maintain, /// checked from the kernel's own table rather than the driver's word for it. diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig new file mode 100644 index 0000000..69b0bfd --- /dev/null +++ b/system/services/device-manager/device-manager.zig @@ -0,0 +1,58 @@ +//! /system/services/device-manager — the ring-3 process that turns the device +//! tree into a running system. The kernel enumerates the hardware and enforces the +//! claim capability (mechanism); this decides *which driver serves which device* +//! and, eventually, spawns it (policy). Keeping that split in user space is the +//! whole point of the microkernel: the manager is an ordinary, restartable process +//! with no special privilege — it uses the same `device_*` system calls any process +//! could ([drivers.md](../../../docs/drivers.md), [driver-model.md]). +//! +//! Increment 1 (this file): enumerate /system/devices and *match* each device to a +//! driver, logging the decision. It does not spawn anything yet — spawning needs a +//! `system_spawn` system call (the kernel spawns every initial-ramdisk binary in a +//! loop today; see system/kernel/kernel.zig). Increment 2 adds that call and turns +//! these decisions into actual spawns. + +const runtime = @import("runtime"); +const device = runtime.device; + +/// The driver that serves each device class — the policy table. In a fuller system +/// this comes from the drivers describing what they bind (or a manifest under +/// /system/drivers); for now it is a small static map, which is enough to prove the +/// manager reads the tree and decides. `null` = no driver for this class yet. +fn driverFor(class: u64) ?[]const u8 { + if (class == @intFromEnum(device.DeviceClass.timer)) return "hpet"; // the HPET + return null; +} + +pub fn main() void { + // Enumerate into a heap buffer (too big for the one-page user stack). + const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch { + _ = runtime.system.write("device-manager: out of memory\n"); + return; + }; + const total = device.enumerate(buffer); + const count = @min(total, buffer.len); + + var matched: usize = 0; + for (buffer[0..count]) |descriptor| { + const driver_name = driverFor(descriptor.class) orelse continue; + // Increment 2 will `system_spawn(driver_name)` here; for now, record the + // decision so the policy is observable and testable. + _ = runtime.system.write("device-manager: match "); + _ = runtime.system.write(driver_name); + _ = runtime.system.write(" -> would spawn it\n"); + matched += 1; + } + + if (matched == 0) { + _ = runtime.system.write("device-manager: no matchable devices\n"); + return; + } + _ = runtime.system.write("device-manager: ok\n"); + while (true) runtime.system.sleep(1000); +} + +pub const panic = runtime.panic; +comptime { + _ = &runtime.start._start; // pull the runtime entry shim into the image +} diff --git a/test/qemu_test.py b/test/qemu_test.py index f5e2e2b..922610c 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -197,6 +197,12 @@ CASES = [ {"name": "hpet", "expect": r"DANOS-TEST-RESULT: PASS", "fail": r"DANOS-TEST-RESULT: FAIL"}, + # Device manager: a ring-3 service enumerates /system/devices and matches each + # device to a driver (discovery + policy in user space). This increment logs the + # decision; spawning follows. + {"name": "device-manager", + "expect": r"DANOS-TEST-RESULT: PASS", + "fail": r"DANOS-TEST-RESULT: FAIL"}, # Bus driver: a user process claims a device, enumerates its children from the # hardware, and publishes each with dev_register — and the kernel refuses a child # whose window escapes the parent's (else dev_register maps arbitrary memory).