The device manager supervises: hello, backoff, and the crash-loop cap (M18.1)
The manager is now a harness service on the well-known .device_manager endpoint. Every driver spawns supervised; drivers with an assignment must hello (device-manager-protocol, versioned) within a deadline enforced by a timer sweep. Exit reasons drive the restart decision: clean exits stay down, faults restart with 300/600/1200ms backoff, and three fast deaths mark a driver failed instead of respawning forever. usb-xhci-bus is the first conforming driver; the crash-test fixture claims a device, hellos, and faults on purpose — each respawn re-proving claim release on death through the manager's own path. maximum_tasks grows 16 -> 32: the initial-ramdisk sweep (15 binaries at once) was intermittently overflowing the static pool.
This commit is contained in:
@@ -1,20 +1,24 @@
|
||||
//! /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]).
|
||||
//! tree into a running system: **the matcher and the supervisor**
|
||||
//! (docs/device-manager.md). The kernel enumerates the hardware and enforces the
|
||||
//! claim capability (mechanism); this decides which driver serves which device,
|
||||
//! spawns it, and keeps it alive (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.
|
||||
//!
|
||||
//! Increment 2 (this file): enumerate /system/devices, *match* each device to a
|
||||
//! driver, and *spawn* it with `system_spawn` — the kernel loads the named binary
|
||||
//! from the initial-ramdisk as a fresh ring-3 process. On QEMU this discovers the
|
||||
//! HPET, decides `hpet` serves it, and brings that driver all the way up. (The
|
||||
//! kernel still auto-spawns the whole initial-ramdisk at boot; increment 3 removes
|
||||
//! that redundancy so the manager is the sole owner of driver spawning.)
|
||||
//! M18.1 (this increment): the manager is a harness service on the well-known
|
||||
//! `.device_manager` endpoint. Every driver is spawned **supervised** — exit
|
||||
//! notifications land in the same loop as protocol messages. Drivers with an
|
||||
//! assignment must `hello` within a deadline or be stopped; a driver that dies
|
||||
//! is restarted with backoff, and a crash loop (three fast deaths) marks it
|
||||
//! failed instead of respawning forever. Exit reasons (M17.2) drive the
|
||||
//! decision: a clean exit meant to stop; only faults and missed deadlines
|
||||
//! restart. Tree reports (`child_added`) land in M18.2.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const acpi_ids = @import("acpi-ids");
|
||||
const protocol = runtime.device_manager_protocol;
|
||||
const device = runtime.device;
|
||||
const system = runtime.system;
|
||||
|
||||
@@ -27,9 +31,8 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
}
|
||||
|
||||
/// The driver that serves each device — 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.
|
||||
/// this comes from a manifest (docs/device-manager.md: the third bus type
|
||||
/// triggers it); for now a static map. `null` = no driver for this class yet.
|
||||
fn driverFor(d: device.DeviceDescriptor) ?[]const u8 {
|
||||
// detect device via DeviceClass
|
||||
if (d.class == @intFromEnum(device.DeviceClass.timer)) return "hpet";
|
||||
@@ -47,10 +50,9 @@ fn driverFor(d: device.DeviceDescriptor) ?[]const u8 {
|
||||
/// pci-class.zig decodes.
|
||||
const xhci_pci_class: u64 = 0x0C_03_30;
|
||||
|
||||
/// The bus driver that serves a PCI function, or null. Unlike the singleton drivers
|
||||
/// in `driverFor`, a machine can carry several identical controllers — so the caller
|
||||
/// spawns one driver instance *per device*, passing the device id as argv[1] for the
|
||||
/// instance to claim.
|
||||
/// The bus driver that serves a PCI function, or null. A machine can carry
|
||||
/// several identical controllers — one driver instance per device, the id as
|
||||
/// argv[1]. These drivers speak the protocol: a hello is expected.
|
||||
fn pciDriverFor(d: device.DeviceDescriptor) ?[]const u8 {
|
||||
if (d.class != @intFromEnum(device.DeviceClass.pci_device)) return null;
|
||||
return switch (d.pci_class) {
|
||||
@@ -59,24 +61,169 @@ fn pciDriverFor(d: device.DeviceDescriptor) ?[]const u8 {
|
||||
};
|
||||
}
|
||||
|
||||
/// Spawn one instance of `driver_name` to serve the specific device `id` — the id
|
||||
/// arrives as argv[1]. No isProcessRunning gate here: the name alone cannot tell two
|
||||
/// instances apart, and this manager is the sole spawner of drivers.
|
||||
fn spawnForDevice(driver_name: []const u8, id: u64) void {
|
||||
var text: [20]u8 = undefined;
|
||||
const id_text = std.fmt.bufPrint(&text, "{d}", .{id}) catch return;
|
||||
if (system.spawnWithArguments(driver_name, &.{id_text}) != null) {
|
||||
writeLine("device-manager: spawned {s} for device {d}\n", .{ driver_name, id });
|
||||
// --- supervision -------------------------------------------------------------
|
||||
|
||||
/// How long a protocol driver has to hello after its spawn.
|
||||
const hello_deadline_ms: u64 = 3000;
|
||||
/// Deaths faster than this count toward the crash loop; slower ones reset it.
|
||||
const fast_death_ns: u64 = 2_000_000_000;
|
||||
/// Consecutive fast deaths before the manager gives up on a driver.
|
||||
const crash_loop_cap: u32 = 3;
|
||||
/// Restart backoff: base << (restarts - 1), so 300 ms, 600 ms, 1200 ms.
|
||||
const backoff_base_ms: u64 = 300;
|
||||
|
||||
const DriverState = enum {
|
||||
awaiting_hello, // spawned; the deadline is armed (protocol drivers only)
|
||||
running,
|
||||
restarting, // dead; respawn due at restart_due_ns
|
||||
stopped, // exited cleanly — it meant to; not restarted
|
||||
failed, // crash loop, or unspawnable; the manager gave up
|
||||
};
|
||||
|
||||
const Driver = struct {
|
||||
used: bool = false,
|
||||
name_buffer: [24]u8 = undefined,
|
||||
name_len: usize = 0,
|
||||
// The assigned device id (becomes argv[1]), or protocol.no_device.
|
||||
device_id: u64 = protocol.no_device,
|
||||
// Whether this driver speaks the protocol (hello expected, deadline
|
||||
// enforced). Legacy drivers (hpet, ps2-bus) are supervised and restarted
|
||||
// but not yet required to hello.
|
||||
speaks_protocol: bool = false,
|
||||
process_id: u32 = 0,
|
||||
state: DriverState = .running,
|
||||
restarts: u32 = 0,
|
||||
spawn_ns: u64 = 0,
|
||||
hello_deadline_ns: u64 = 0,
|
||||
restart_due_ns: u64 = 0,
|
||||
|
||||
fn name(driver: *const Driver) []const u8 {
|
||||
return driver.name_buffer[0..driver.name_len];
|
||||
}
|
||||
};
|
||||
|
||||
const maximum_drivers = 16;
|
||||
var drivers: [maximum_drivers]Driver = .{Driver{}} ** maximum_drivers;
|
||||
var manager_endpoint: runtime.ipc.Handle = 0;
|
||||
var test_restart_mode = false;
|
||||
|
||||
fn driverByProcess(process_id: u32) ?*Driver {
|
||||
for (&drivers) |*driver| {
|
||||
if (driver.used and driver.process_id == process_id) return driver;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Whether a singleton driver is already in the table (two ACPI nodes can both
|
||||
/// map to ps2-bus; one instance serves both).
|
||||
fn alreadySupervised(name: []const u8) bool {
|
||||
for (&drivers) |*driver| {
|
||||
if (driver.used and std.mem.eql(u8, driver.name(), name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Record a driver in the table and spawn its first instance.
|
||||
fn addDriver(name: []const u8, device_id: u64, speaks_protocol: bool) void {
|
||||
for (&drivers) |*driver| {
|
||||
if (driver.used) continue;
|
||||
const n = @min(name.len, driver.name_buffer.len);
|
||||
@memcpy(driver.name_buffer[0..n], name[0..n]);
|
||||
driver.name_len = n;
|
||||
driver.device_id = device_id;
|
||||
driver.speaks_protocol = speaks_protocol;
|
||||
driver.used = true;
|
||||
spawnDriver(driver);
|
||||
return;
|
||||
}
|
||||
writeLine("device-manager: driver table full; cannot supervise {s}\n", .{name});
|
||||
}
|
||||
|
||||
/// (Re)spawn a driver instance: supervised on the manager's own endpoint, the
|
||||
/// device id as argv[1] when it has one, the hello deadline armed when it
|
||||
/// speaks the protocol.
|
||||
fn spawnDriver(driver: *Driver) void {
|
||||
var id_text: [20]u8 = undefined;
|
||||
var arguments: [1][]const u8 = undefined;
|
||||
var argument_count: usize = 0;
|
||||
if (driver.device_id != protocol.no_device) {
|
||||
arguments[0] = std.fmt.bufPrint(&id_text, "{d}", .{driver.device_id}) catch return;
|
||||
argument_count = 1;
|
||||
}
|
||||
const child = system.spawnSupervised(driver.name(), arguments[0..argument_count], manager_endpoint) orelse {
|
||||
writeLine("device-manager: failed to spawn {s}\n", .{driver.name()});
|
||||
driver.state = .failed;
|
||||
return;
|
||||
};
|
||||
driver.process_id = child;
|
||||
driver.spawn_ns = system.clock();
|
||||
if (driver.speaks_protocol) {
|
||||
driver.state = .awaiting_hello;
|
||||
driver.hello_deadline_ns = driver.spawn_ns + hello_deadline_ms * 1_000_000;
|
||||
_ = system.timerOnce(manager_endpoint, hello_deadline_ms + 100);
|
||||
} else {
|
||||
writeLine("device-manager: failed to spawn {s} for device {d}\n", .{ driver_name, id });
|
||||
driver.state = .running;
|
||||
}
|
||||
if (driver.device_id != protocol.no_device) {
|
||||
writeLine("device-manager: spawned {s} for device {d}\n", .{ driver.name(), driver.device_id });
|
||||
} else {
|
||||
writeLine("device-manager: spawned {s}\n", .{driver.name()});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
/// A driver died. The exit reason (M17.2) is the whole decision: a clean exit
|
||||
/// meant to stop; anything else restarts with backoff until the crash-loop cap.
|
||||
fn onDriverExit(driver: *Driver) void {
|
||||
const reason = runtime.process.exitReason(driver.process_id) orelse .fault;
|
||||
if (reason == .exited) {
|
||||
driver.state = .stopped;
|
||||
writeLine("device-manager: {s} exited cleanly; not restarting\n", .{driver.name()});
|
||||
return;
|
||||
}
|
||||
const now = system.clock();
|
||||
const alive_ns = now - driver.spawn_ns;
|
||||
driver.restarts = if (alive_ns < fast_death_ns) driver.restarts + 1 else 1;
|
||||
if (driver.restarts >= crash_loop_cap) {
|
||||
driver.state = .failed;
|
||||
writeLine("device-manager: {s} is failing repeatedly (crash loop); giving up\n", .{driver.name()});
|
||||
return;
|
||||
}
|
||||
const delay_ms = backoff_base_ms << @intCast(driver.restarts - 1);
|
||||
driver.state = .restarting;
|
||||
driver.restart_due_ns = now + delay_ms * 1_000_000;
|
||||
writeLine("device-manager: restarting {s} in {d} ms (died: {s})\n", .{ driver.name(), delay_ms, @tagName(reason) });
|
||||
_ = system.timerOnce(manager_endpoint, delay_ms + 50);
|
||||
}
|
||||
|
||||
/// A timer landed: sweep every deadline. Overdue hellos are killed (the exit
|
||||
/// notification then routes through the normal restart policy); due restarts
|
||||
/// respawn. Timers carry no id on purpose — the table is the state, and one
|
||||
/// sweep serves every armed deadline.
|
||||
fn sweepDeadlines() void {
|
||||
const now = system.clock();
|
||||
for (&drivers) |*driver| {
|
||||
if (!driver.used) continue;
|
||||
switch (driver.state) {
|
||||
.awaiting_hello => if (now >= driver.hello_deadline_ns) {
|
||||
writeLine("device-manager: {s} missed its hello deadline\n", .{driver.name()});
|
||||
_ = system.kill(driver.process_id);
|
||||
// The exit notification finishes the job via onDriverExit.
|
||||
},
|
||||
.restarting => if (now >= driver.restart_due_ns) spawnDriver(driver),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- the harness callbacks -----------------------------------------------------
|
||||
|
||||
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
manager_endpoint = endpoint;
|
||||
|
||||
// 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;
|
||||
return false;
|
||||
};
|
||||
const total = device.enumerate(buffer);
|
||||
const count = @min(total, buffer.len);
|
||||
@@ -85,28 +232,74 @@ pub fn main() void {
|
||||
for (buffer[0..count]) |descriptor| {
|
||||
if (pciDriverFor(descriptor)) |driver_name| {
|
||||
matched += 1;
|
||||
spawnForDevice(driver_name, descriptor.id);
|
||||
addDriver(driver_name, descriptor.id, true);
|
||||
continue;
|
||||
}
|
||||
const driver_name = driverFor(descriptor) orelse continue;
|
||||
matched += 1;
|
||||
if (!system.isProcessRunning(driver_name)) {
|
||||
if (runtime.system.spawn(driver_name) != null) {
|
||||
writeLine("device-manager: spawned {s}\n", .{driver_name});
|
||||
} else {
|
||||
writeLine("device-manager: failed to spawn {s}\n", .{driver_name});
|
||||
}
|
||||
} else {
|
||||
writeLine("device-manager: already spawned {s}\n", .{driver_name});
|
||||
// Skip a singleton that is already alive (the initial-ramdisk sweep test
|
||||
// starts every bundled binary bare, this manager included) — spawning a
|
||||
// second instance would only lose the claim race and churn the log.
|
||||
if (!alreadySupervised(driver_name) and !system.isProcessRunning(driver_name)) {
|
||||
addDriver(driver_name, protocol.no_device, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (test_restart_mode) {
|
||||
// The driver-restart scenario's fixture: claims device 0 (the tree
|
||||
// root, otherwise unclaimed), hellos, then faults — driving backoff,
|
||||
// re-claim-after-death, and the crash-loop cap deterministically.
|
||||
addDriver("crash-test", 0, true);
|
||||
}
|
||||
|
||||
if (matched == 0) {
|
||||
_ = runtime.system.write("device-manager: no matchable devices\n");
|
||||
} else {
|
||||
_ = runtime.system.write("device-manager: ok\n");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn onMessage(message: []const u8, reply: []u8, sender: u32) usize {
|
||||
if (message.len < protocol.hello_size) return 0;
|
||||
const hello = std.mem.bytesToValue(protocol.Hello, message[0..protocol.hello_size]);
|
||||
if (hello.operation != @intFromEnum(protocol.Operation.hello)) return 0;
|
||||
|
||||
var status: i32 = 0;
|
||||
if (hello.version != protocol.version) {
|
||||
status = -1;
|
||||
writeLine("device-manager: refused hello (version {d}) from process {d}\n", .{ hello.version, sender });
|
||||
} else if (driverByProcess(sender)) |driver| {
|
||||
driver.state = .running;
|
||||
writeLine("device-manager: hello from {s} (device {d})\n", .{ driver.name(), hello.device_id });
|
||||
} else {
|
||||
status = -1;
|
||||
writeLine("device-manager: hello from unknown process {d}\n", .{sender});
|
||||
}
|
||||
const hello_reply = protocol.HelloReply{ .status = status };
|
||||
@memcpy(reply[0..protocol.reply_size], std.mem.asBytes(&hello_reply));
|
||||
return protocol.reply_size;
|
||||
}
|
||||
|
||||
fn onNotification(badge: u64) void {
|
||||
if (badge & runtime.ipc.notify_exit_bit != 0) {
|
||||
const dead: u32 = @intCast(badge & ~(runtime.ipc.notify_badge_bit | runtime.ipc.notify_exit_bit));
|
||||
if (driverByProcess(dead)) |driver| onDriverExit(driver);
|
||||
return;
|
||||
}
|
||||
_ = runtime.system.write("device-manager: ok\n");
|
||||
while (true) runtime.system.sleep(1000);
|
||||
if (badge & runtime.ipc.notify_timer_bit != 0) sweepDeadlines();
|
||||
}
|
||||
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
if (init.arguments.get(1)) |mode| {
|
||||
test_restart_mode = std.mem.eql(u8, mode, "test-restart");
|
||||
}
|
||||
runtime.service.run(protocol.message_maximum, .{
|
||||
.service = .device_manager,
|
||||
.init = initialise,
|
||||
.on_message = onMessage,
|
||||
.on_notification = onNotification,
|
||||
});
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
|
||||
Reference in New Issue
Block a user