volume-manager: adopt every device, a filesystem per partition (S3)

Lift the one-device/one-volume cap. bringUpVolume now probes the whole
partition table (allVolumes uncapped) and spawns a confined filesystem
per volume; pollTick loops it to adopt every present, not-yet-adopted
device each tick.

The subtlety is adopt-once-and-keep: a device is recorded in the table
the first time it is seen and kept until it leaves the tree, even when it
carries no servable volume or its geometry cannot be read. Dropping an
unservable device would make openAnyStorage hand back the same one every
tick and starve the devices behind it; keeping it lets the scan advance
past it. A genuine removal frees the slot; a re-insert (fresh device id)
is probed anew.

The boot image is a single bare-FAT volume, so the full suite is
unchanged at 128/128.
This commit is contained in:
Daniel Samson
2026-08-10 01:26:47 +01:00
parent 7efe7b72d8
commit b2a5a0a3c6
@@ -9,12 +9,11 @@
//! supervises drivers.
//!
//! The manager holds a table of adopted storage DEVICES and a table of the
//! VOLUMES on them: one filesystem process per volume, each confined to its
//! partition's badge-scoped block range, each supervised with its own budget. A
//! device leaving the tree takes its volumes with it. (This S3 increment lays the
//! tables in; multi-device adoption and per-partition spawn land in the next
//! step — for now it adopts one device and its first volume, behavior-identical
//! to before.)
//! VOLUMES on them: it adopts every storage device the device-manager tree
//! carries, probes each one's whole partition table, and spawns one filesystem
//! process per volume — each confined to its partition's badge-scoped block
//! range, each supervised with its own budget. A device leaving the tree takes
//! its volumes with it.
const std = @import("std");
const channel = @import("channel");
@@ -129,7 +128,6 @@ var service_endpoint: ipc.Handle = 0;
var manager_handle: ?ipc.Handle = null;
var bounce: memory.DmaRegion = undefined;
var bounce_ready = false;
var logged_no_volume = false;
/// How often the poll checks device presence and fires due restarts. Fast enough
/// that an unplug unmounts promptly; the poll is a bare device-manager enumerate,
/// no channel work, so it is cheap to run continuously.
@@ -152,10 +150,6 @@ fn claimDevice() ?*StorageDevice {
for (&devices) |*d| if (!d.used) return d;
return null;
}
fn anyDeviceUsed() bool {
for (&devices) |*d| if (d.used) return true;
return false;
}
fn volumeById(id: u64) ?*Volume {
for (&volumes) |*v| if (v.used and v.id == id) return v;
return null;
@@ -288,36 +282,44 @@ fn composeMountPrefix(slot: usize, identity: partition.Identity) []const u8 {
(std.fmt.bufPrint(&mount_prefix_bufs[slot], "/volumes/{s}", .{id}) catch "/volumes/unknown");
}
/// A storage device appeared: adopt its channel, probe its partition table, and
/// spawn a filesystem per volume it carries. On any failure the channel is
/// dropped (so a present-but-unreadable device does not leak a handle every poll)
/// and the device stays unadopted — the next poll retries. This increment probes
/// only the first volume (behavior-identical to before); the next step lifts the
/// cap.
fn bringUpVolume() void {
/// Adopt the next present, not-yet-adopted storage device: take its channel,
/// probe its whole partition table, and spawn a filesystem per volume it carries.
/// Returns true when it consumed a device (so the caller can loop to adopt every
/// present device in one tick), false when none remain or the device table is full.
///
/// A device is adopted exactly once and kept until it leaves the tree — even when
/// it carries no volume we can serve, or its geometry cannot be read. Keeping the
/// empty/unreadable device adopted (rather than dropping and re-probing) is what
/// lets openAnyStorage advance PAST it to the devices behind it; dropping it would
/// make openAnyStorage hand back the same unservable device every tick and starve
/// the rest. A genuine removal frees the slot (removeDevice); a re-insert gets a
/// fresh device id and is probed anew.
fn bringUpVolume() bool {
if (!bounce_ready) {
bounce = memory.dmaAlloc(512, memory.dma_coherent | memory.dma_shareable) orelse return;
bounce = memory.dmaAlloc(512, memory.dma_coherent | memory.dma_shareable) orelse return false;
bounce_ready = true;
}
const opened = openAnyStorage() orelse return;
const opened = openAnyStorage() orelse return false;
const dev = claimDevice() orelse {
_ = logging.write("volume-manager: device table full; a storage device is left unadopted\n");
_ = ipc.close(opened.device.endpoint);
return;
return false;
};
dev.* = .{ .used = true, .device_id = opened.device_id, .channel = opened.device };
const device = opened.device;
// Attach the read buffer to THIS device (a no-op without an enforcing IOMMU).
// The handle is kept, not closed, so it can be re-attached after a replug.
// The handle is kept, not closed, so it can be re-attached after a replug. A
// failed attach or geometry read leaves the device adopted but empty — we just
// cannot read it, and the slot still watches it for removal.
if (bounce.handle) |handle| {
if (!device.attach(handle)) {
dropDevice(dev);
return;
_ = logging.write("volume-manager: could not attach the read buffer to a storage device; no volume served\n");
return true;
}
}
const geometry = device.geometry() orelse {
dropDevice(dev);
return;
_ = logging.write("volume-manager: could not read a storage device's geometry; no volume served\n");
return true;
};
const ProbeReader = struct {
device: block.Device,
@@ -332,17 +334,11 @@ fn bringUpVolume() void {
var probe = ProbeReader{ .device = device };
const reader = partition.SectorReader{ .context = &probe, .readFn = ProbeReader.readSector };
var found: [maximum_volumes]partition.Volume = undefined;
const n = partition.allVolumes(reader, geometry.block_count, found[0..1]); // cap 1 this step
const n = partition.allVolumes(reader, geometry.block_count, found[0..]);
if (n == 0) {
if (!logged_no_volume) {
_ = logging.write("volume-manager: storage present but no recognizable volume\n");
logged_no_volume = true;
}
dropDevice(dev);
return;
std.log.info("device {d} present but carries no recognizable volume", .{dev.device_id});
return true;
}
logged_no_volume = false;
var spawned = false;
for (found[0..n]) |fv| {
// Pick the service binary from the volume's content signature. A signature
// no filesystems.csv row serves goes unserved (logged), like an unbound
@@ -367,10 +363,8 @@ fn bringUpVolume() void {
};
next_volume_id += 1;
spawnFilesystem(&volumes[slot]);
spawned = true;
}
// The device carried nothing we could serve — drop it so a re-poll retries.
if (!spawned) dropDevice(dev);
return true;
}
/// Close a device's channel and free its slot. No volumes are touched (the caller
@@ -415,7 +409,12 @@ fn pollTick() void {
spawnFilesystem(v);
}
}
if (!anyDeviceUsed()) bringUpVolume();
// Adopt every present, not-yet-adopted storage device. Each call consumes at
// most one device (openAnyStorage skips the adopted), so the loop terminates
// once none remain; the maximum_devices guard is insurance against a logic
// slip, never the normal exit.
var adopted: usize = 0;
while (adopted < maximum_devices and bringUpVolume()) : (adopted += 1) {}
}
/// A filesystem announces itself for the volume it was spawned to serve. Reply