establishment: block stops being a name, and enumerate learns to page
P2 of docs/establishment-planes-plan.md. usb-storage serves nameless — one process per stick cannot share an exclusive bind, and a second stick used to die silently on -EBUSY before ever helloing. Its one hello now moves both directions at once: the block-serving endpoint up, its controller channel down. fat finds its volume through the manager — a new `.consumer` role asks for the channel of the driver BOUND TO a device (distinct from the device's reporter), found by enumerating the tree for the mass-storage identity. fat stays single-volume; the boot-volume-by-content choice is M21. The conversion immediately caught a live truncation of exactly the audit's shape: ChildEntry grew to 32 bytes, one enumerate reply holds ~7, and a real tree carries a dozen ACPI nodes before the first USB child — the storage entry silently never fit (the protocol comment already said "paging joins the protocol if a tree ever outgrows one packet"). enumerate is now paged: Header.target is the start cursor, a short page is the end; device-list's page-0 read is unchanged. Grant rows move with the code: the block bind and fat's block open die, fat gains open device-manager. Gate: 18 cases green including the registry trio, device-list, and both IOMMU storage variants.
This commit is contained in:
@@ -7,10 +7,8 @@
|
||||
//! `runtime.dma.alloc`), so whole sectors move without crossing the IPC size
|
||||
//! limit — the same handoff usb-storage uses toward the controller.
|
||||
|
||||
const channel = @import("channel");
|
||||
const envelope = @import("envelope");
|
||||
const ipc = @import("ipc");
|
||||
const time = @import("time");
|
||||
const block_protocol = @import("block-protocol");
|
||||
|
||||
const Protocol = block_protocol.Protocol;
|
||||
@@ -75,26 +73,7 @@ pub const Device = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// One open attempt, no waiting — for a server that retries on its own
|
||||
/// timer (the fat service) instead of blocking its harness in here.
|
||||
pub fn tryOpen() ?Device {
|
||||
if (channel.openEndpoint("block")) |handle| return .{ .endpoint = handle };
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Open `/protocol/block`, retrying generously while the USB storage chain
|
||||
/// (controller reset, enumeration, mass-storage bring-up) comes up.
|
||||
pub fn open() ?Device {
|
||||
// Patient: the whole USB storage chain (firmware discovery, xHCI reset and
|
||||
// enumeration, mass-storage bring-up) must complete first, which can take
|
||||
// tens of seconds under emulation.
|
||||
var attempts: usize = 0;
|
||||
// 30 s covers the slowest observed healthy chain (a flaky QEMU enumeration
|
||||
// completed at ~24 s); a machine whose stick genuinely failed setup should
|
||||
// not sit a further minute pretending otherwise.
|
||||
while (attempts < 600) : (attempts += 1) {
|
||||
if (channel.openEndpoint("block")) |handle| return .{ .endpoint = handle };
|
||||
time.sleepMillis(50);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// There is deliberately no open-by-name here: `block` is not a registry name.
|
||||
// One storage process serves each volume, and a consumer receives its volume's
|
||||
// channel from the device manager (establishment by lineage, communication.md
|
||||
// "Establishment: two planes"), then wraps it: `block.Device{ .endpoint = c }`.
|
||||
|
||||
@@ -270,18 +270,21 @@ pub const Exchange = struct {
|
||||
};
|
||||
|
||||
/// A consumer's whole establishment step: hello until the channel to this
|
||||
/// device's provider arrives. The manager acks a hello whose provider is not
|
||||
/// there yet (mid-restart, re-report on the way) with no channel — retryable
|
||||
/// by design — so this re-hellos on the ONE manager handle, on the same
|
||||
/// cadence the old name lookup used, and gives up on a refusal or a vanished
|
||||
/// manager. Re-hello is benign: the manager just re-marks the entry running.
|
||||
pub fn helloForChannel(role: Role, device_id: u64) ?ipc.Handle {
|
||||
const first = helloExchange(role, device_id, null, true) orelse return null;
|
||||
/// device's provider arrives. `serving` (a provider-and-consumer like
|
||||
/// usb-storage: block endpoint up, bus channel down) rides the FIRST exchange
|
||||
/// only — the manager keeps it, so retries need not resend it. The manager
|
||||
/// acks a hello whose provider is not there yet (mid-restart, re-report on
|
||||
/// the way) with no channel — retryable by design — so this re-hellos on the
|
||||
/// ONE manager handle, on the same cadence the old name lookup used, and
|
||||
/// gives up on a refusal or a vanished manager. Re-hello is benign: the
|
||||
/// manager just re-marks the entry running.
|
||||
pub fn helloForChannel(role: Role, device_id: u64, serving: ?ipc.Handle) ?ipc.Handle {
|
||||
const first = helloExchange(role, device_id, serving, true) orelse return null;
|
||||
if (first.channel) |bus| return bus;
|
||||
var attempts: u32 = 0;
|
||||
while (attempts < lookup_attempts) : (attempts += 1) {
|
||||
time.sleepMillis(lookup_pause_ms);
|
||||
const again = exchangeOn(first.manager, role, device_id, null, true) orelse return null;
|
||||
const again = helloOn(first.manager, role, device_id, null, true) orelse return null;
|
||||
if (again.channel) |bus| return bus;
|
||||
}
|
||||
std.log.info("no provider channel for device {d}", .{device_id});
|
||||
@@ -303,12 +306,14 @@ pub fn helloExchange(role: Role, device_id: u64, serving: ?ipc.Handle, want_chan
|
||||
std.log.info("no device manager to hello", .{});
|
||||
return null;
|
||||
};
|
||||
return exchangeOn(manager, role, device_id, serving, want_channel);
|
||||
return helloOn(manager, role, device_id, serving, want_channel);
|
||||
}
|
||||
|
||||
/// One hello on an already-open manager handle — the exchange without the
|
||||
/// lookup, so a retry loop never spends a handle-table slot per attempt.
|
||||
fn exchangeOn(manager: ipc.Handle, role: Role, device_id: u64, serving: ?ipc.Handle, want_channel: bool) ?Exchange {
|
||||
/// Public for parties that keep their own manager handle across a long retry
|
||||
/// cadence (fat polls for its volume on a timer).
|
||||
pub fn helloOn(manager: ipc.Handle, role: Role, device_id: u64, serving: ?ipc.Handle, want_channel: bool) ?Exchange {
|
||||
var packet: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||
const framed = device_manager_protocol.Protocol.encodeRequest(
|
||||
.hello,
|
||||
|
||||
@@ -63,6 +63,11 @@ pub const Role = enum(u8) {
|
||||
bus = 1,
|
||||
/// Serves one device, reached through a bus's transfer protocol.
|
||||
device = 2,
|
||||
/// Not a spawned driver at all: a party asking for the channel of the
|
||||
/// driver BOUND TO the target device (a service consuming a driver-layer
|
||||
/// contract — fat asking for its volume's block provider). No deadline, no
|
||||
/// state, no driver entry; just establishment by lineage.
|
||||
consumer = 3,
|
||||
};
|
||||
|
||||
// --- the per-operation request parts ----------------------------------------
|
||||
@@ -160,10 +165,18 @@ pub const ChildEntry = extern struct {
|
||||
parent: u64,
|
||||
bus_address: u64,
|
||||
identity: u64,
|
||||
/// The kernel device id this child was registered as (`no_device` for an
|
||||
/// unregistered leaf) — what a `.consumer` hello names as its target to be
|
||||
/// routed to the driver bound to this child.
|
||||
device_id: u64,
|
||||
};
|
||||
|
||||
/// How many `ChildEntry` records one `enumerate` reply can carry. Paging joins
|
||||
/// the protocol if a tree ever outgrows one packet.
|
||||
/// How many `ChildEntry` records one `enumerate` reply can carry. The verb is
|
||||
/// PAGED: the request's `Header.target` is the start index (skip that many
|
||||
/// known children), and a short or empty page means the tree is exhausted — a
|
||||
/// real tree outgrew one packet the day ACPI reported a dozen nodes before
|
||||
/// the first USB child, and an unpaged reply silently truncated exactly the
|
||||
/// entries a consumer was looking for.
|
||||
pub const entries_per_reply: usize = (envelope.packet_maximum - envelope.prefix_size) / @sizeOf(ChildEntry);
|
||||
|
||||
pub const Protocol = envelope.Define(.{
|
||||
|
||||
@@ -63,12 +63,11 @@
|
||||
/system/services/discovery, /system/services/device-manager, bind, power
|
||||
|
||||
# --- the drivers, which the device manager spawns ---------------------------
|
||||
# usb-transfer has NO bind row: several bus processes provide it (one per
|
||||
# controller), so it is never a registry name — consumers get their controller's
|
||||
# channel from the device manager's hello, routed by lineage (communication.md
|
||||
# "Establishment: two planes, one namespace").
|
||||
# usb-transfer and block have NO bind rows: several processes provide each (one
|
||||
# per controller, one per volume), so neither is ever a registry name —
|
||||
# consumers get their provider's channel from the device manager's hello,
|
||||
# routed by lineage (communication.md "Establishment: two planes").
|
||||
/system/drivers/ps2-bus, /system/services/device-manager, bind, ps2-bus
|
||||
/system/drivers/usb-storage, /system/services/device-manager, bind, block
|
||||
/system/drivers/virtio-gpu, /system/services/device-manager, bind, scanout
|
||||
|
||||
# --- the same providers when the kernel test harness starts them directly ---
|
||||
@@ -95,11 +94,12 @@
|
||||
# ============================================================================
|
||||
|
||||
# --- init's own services ----------------------------------------------------
|
||||
# fat reaches the block device behind the volume it mounts; the compositor
|
||||
# reaches the scanout its driver announced, its own endpoint (the mouse-listener
|
||||
# thread opens /protocol/display like any other client — threads share no
|
||||
# handles), and the input stream that moves the cursor.
|
||||
/system/services/fat, /system/services/init, open, block
|
||||
# fat reaches the device manager to be routed to its volume's block provider
|
||||
# (block is not a name — see the bind section); the compositor reaches the
|
||||
# scanout its driver announced, its own endpoint (the mouse-listener thread
|
||||
# opens /protocol/display like any other client — threads share no handles),
|
||||
# and the input stream that moves the cursor.
|
||||
/system/services/fat, /system/services/init, open, device-manager
|
||||
/system/services/display, /system/services/init, open, scanout
|
||||
/system/services/display, /system/services/init, open, display
|
||||
/system/services/display, /system/services/init, open, input
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 12 and column 15.
|
@@ -77,7 +77,7 @@ pub fn main(init: process.Init) void {
|
||||
|
||||
// The hello both meets the spawn deadline and brings back the channel to
|
||||
// THIS device's controller — routed by lineage, never found by name.
|
||||
const bus = device_manager.helloForChannel(.device, device_id) orelse return;
|
||||
const bus = device_manager.helloForChannel(.device, device_id, null) orelse return;
|
||||
var device = usb.open(bus, device_id) orelse {
|
||||
std.log.info("could not open device {d}", .{device_id});
|
||||
return;
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn main(init: process.Init) void {
|
||||
|
||||
// The hello both meets the spawn deadline and brings back the channel to
|
||||
// THIS device's controller — routed by lineage, never found by name.
|
||||
const bus = device_manager.helloForChannel(.device, device_id) orelse return;
|
||||
const bus = device_manager.helloForChannel(.device, device_id, null) orelse return;
|
||||
var device = usb.open(bus, device_id) orelse {
|
||||
std.log.info("could not open device {d}", .{device_id});
|
||||
return;
|
||||
|
||||
@@ -82,10 +82,13 @@ fn transact(cdb: []const u8, direction_in: bool, data_physical: u64, data_length
|
||||
var bring_up_failed = false;
|
||||
|
||||
fn initialise(endpoint: ipc.Handle) bool {
|
||||
_ = endpoint;
|
||||
// The hello both meets the spawn deadline and brings back the channel to
|
||||
// THIS device's controller — routed by lineage, never found by name.
|
||||
const bus = device_manager.helloForChannel(.device, device_id) orelse return false;
|
||||
// One hello, both directions: the block-serving endpoint goes UP (the
|
||||
// manager routes fat's consumer hello here — this driver serves one
|
||||
// volume, one process per stick, so `block` is never a registry name),
|
||||
// and the channel to THIS device's controller comes DOWN, routed by
|
||||
// lineage. A second stick used to die silently on the exclusive bind;
|
||||
// now every instance is reachable through its lineage.
|
||||
const bus = device_manager.helloForChannel(.device, device_id, endpoint) orelse return false;
|
||||
device = usb.open(bus, device_id) orelse {
|
||||
std.log.info("could not open device {d}", .{device_id});
|
||||
return false;
|
||||
@@ -224,8 +227,10 @@ pub fn main(init: process.Init) void {
|
||||
std.log.info("malformed device id '{s}'", .{argument});
|
||||
return;
|
||||
};
|
||||
// No `.service` name: several instances provide the block contract (one
|
||||
// per stick), so consumers are routed here by the device manager's
|
||||
// lineage, never by a registry bind — the endpoint goes up in the hello.
|
||||
service.run(block_protocol.message_maximum, .{
|
||||
.service = "block",
|
||||
.init = initialise,
|
||||
.on_message = onMessage,
|
||||
});
|
||||
|
||||
@@ -99,10 +99,19 @@ fn identityFromReport(report: device_manager_protocol.ChildAdded) registry.Ident
|
||||
/// Whether some driver entry already serves registered device `device_id` —
|
||||
/// a re-report after a bus restart must not spawn a second instance.
|
||||
fn driverForDevice(device_id: u64) bool {
|
||||
for (&drivers) |*driver| {
|
||||
if (driver.used and driver.device_id == device_id) return true;
|
||||
return driverEntryForDevice(device_id) != null;
|
||||
}
|
||||
return false;
|
||||
|
||||
/// The driver entry BOUND TO a device — matched and spawned for it. Distinct
|
||||
/// from the device's reporter: a mouse's provider is the bus that reported it
|
||||
/// (lineage via `children[].reporter`), while a volume's provider is the
|
||||
/// storage driver spawned FOR it — which is what a `.consumer` hello asks for.
|
||||
fn driverEntryForDevice(device_id: u64) ?*Driver {
|
||||
if (device_id == device_manager_protocol.no_device) return null;
|
||||
for (&drivers) |*driver| {
|
||||
if (driver.used and driver.device_id == device_id) return driver;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- supervision -------------------------------------------------------------
|
||||
@@ -509,6 +518,24 @@ fn onHello(_: void, invocation: Invocation(device_manager_protocol.Hello), _: An
|
||||
std.log.info("refused hello (version {d}) from process {d}", .{ invocation.request.version, invocation.sender });
|
||||
return -envelope.EPROTO;
|
||||
}
|
||||
// A consumer is not a spawned driver: no entry, no deadline, no state —
|
||||
// just establishment. It asks for the channel of the driver BOUND TO its
|
||||
// target (fat asking for its volume's block provider). No channel is a
|
||||
// retryable ack, exactly as for a device-role consumer.
|
||||
//
|
||||
// The residual, stated plainly: any process granted `open device-manager`
|
||||
// can ask. The grant rows are the gate today, as they were when the block
|
||||
// name was open-granted; a finer per-channel policy belongs to the same
|
||||
// future as the spawn capability (device-authority.md).
|
||||
if (invocation.request.role == @intFromEnum(device_manager_protocol.Role.consumer)) {
|
||||
if (invocation.request.wants_channel != 0) {
|
||||
if (driverEntryForDevice(invocation.target)) |provider| {
|
||||
if (provider.endpoint) |serving| service.replyWithCapability(serving);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const driver = driverByProcess(invocation.sender) orelse {
|
||||
std.log.info("hello from unknown process {d}", .{invocation.sender});
|
||||
return -envelope.EPERM;
|
||||
@@ -664,14 +691,24 @@ fn onChildRemoved(_: void, invocation: Invocation(device_manager_protocol.ChildR
|
||||
/// The reserved `enumerate` verb: the mirror, one `ChildEntry` per known child,
|
||||
/// packed into the reply's tail. How many arrived is the reply's own length —
|
||||
/// `Status.len` — so no count header is spent saying it twice.
|
||||
fn onEnumerate(_: void, _: Invocation(void), answer: Answer(void)) isize {
|
||||
fn onEnumerate(_: void, invocation: Invocation(void), answer: Answer(void)) isize {
|
||||
const entry_size = @sizeOf(device_manager_protocol.ChildEntry);
|
||||
const tail = answer.tail();
|
||||
// `target` is the page cursor: skip that many known children first. One
|
||||
// reply holds only a handful of entries, and a real tree (a dozen ACPI
|
||||
// nodes before the first USB child) outgrew one packet — the storage
|
||||
// child silently never fit, which is precisely the truncation shape the
|
||||
// bounds audit exists to forbid. A caller pages until a short page.
|
||||
var skip = invocation.target;
|
||||
var written: usize = 0;
|
||||
for (&children) |*child| {
|
||||
if (!child.used) continue;
|
||||
if (skip > 0) {
|
||||
skip -= 1;
|
||||
continue;
|
||||
}
|
||||
if (written + entry_size > tail.len) break;
|
||||
const entry = device_manager_protocol.ChildEntry{ .parent = child.parent, .bus_address = child.bus_address, .identity = child.identity };
|
||||
const entry = device_manager_protocol.ChildEntry{ .parent = child.parent, .bus_address = child.bus_address, .identity = child.identity, .device_id = child.device_id };
|
||||
@memcpy(tail[written..][0..entry_size], std.mem.asBytes(&entry));
|
||||
written += entry_size;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ pub fn build(b: *std.Build) void {
|
||||
.name = "fat",
|
||||
.root_source_file = b.path("fat.zig"),
|
||||
.imports = &.{
|
||||
"block", "envelope", "file-system", "ipc", "logging", "memory", "process",
|
||||
"service", "time", "vfs-protocol",
|
||||
"block", "channel", "device-manager-protocol", "driver",
|
||||
"envelope", "file-system", "ipc", "logging",
|
||||
"memory", "process", "service", "time",
|
||||
"vfs-protocol",
|
||||
},
|
||||
});
|
||||
b.installArtifact(exe);
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
//! it.
|
||||
|
||||
const std = @import("std");
|
||||
const channel = @import("channel");
|
||||
const device_manager_protocol = @import("device-manager-protocol");
|
||||
const driver = @import("driver");
|
||||
const ipc = @import("ipc");
|
||||
const process = @import("process");
|
||||
const service = @import("service");
|
||||
@@ -116,6 +119,56 @@ const mount_retry_ms = 500;
|
||||
|
||||
var mounted = false;
|
||||
var service_endpoint: ipc.Handle = 0;
|
||||
/// The one channel to the device manager, opened on first need and kept — the
|
||||
/// poll retries on it, never spending a handle-table slot per attempt.
|
||||
var manager_handle: ?ipc.Handle = null;
|
||||
|
||||
/// Find the volume's provider through the device manager (establishment by
|
||||
/// lineage, communication.md "Establishment: two planes" — `block` is not a
|
||||
/// registry name; one storage process serves each stick): enumerate the
|
||||
/// manager's tree, take the FIRST usb mass-storage child by enumeration order
|
||||
/// (deterministic within a boot; single-volume by construction, and choosing
|
||||
/// the BOOT volume by content when two sticks are present is the M21 remount
|
||||
/// track), and consumer-hello for the channel of the driver bound to it.
|
||||
/// Null until the chain is up — the caller's poll retries.
|
||||
fn acquireVolume() ?block.Device {
|
||||
const manager = manager_handle orelse opened: {
|
||||
const handle = channel.openEndpoint("device-manager") orelse return null;
|
||||
manager_handle = handle;
|
||||
break :opened handle;
|
||||
};
|
||||
|
||||
// The envelope's reserved `enumerate` verb, PAGED: one reply carries only
|
||||
// a handful of entries and a real tree (a dozen ACPI nodes before the
|
||||
// first USB child) is bigger, so `Header.target` is the start cursor and
|
||||
// a short page is the end. Identity is the bus's native triple, for USB
|
||||
// (base << 16) | (class << 8) | protocol — mass storage is base 0x08,
|
||||
// subclass 0x06 (SCSI transparent), the same key devices.csv matches on.
|
||||
const Entry = device_manager_protocol.ChildEntry;
|
||||
var start: u64 = 0;
|
||||
while (true) {
|
||||
const enumerate = envelope.Header{ .operation = envelope.operation_enumerate, .target = start };
|
||||
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||
const length = ipc.call(manager, std.mem.asBytes(&enumerate), &reply) catch return null;
|
||||
const status = envelope.statusOf(reply[0..length]) orelse return null;
|
||||
if (status.status != 0) return null;
|
||||
|
||||
const carried = @min(@as(usize, status.len), length -| envelope.prefix_size);
|
||||
const tail = reply[envelope.prefix_size..][0..carried];
|
||||
const count = tail.len / @sizeOf(Entry);
|
||||
if (count == 0) return null; // the tree is exhausted; no volume yet
|
||||
var index: usize = 0;
|
||||
while (index < count) : (index += 1) {
|
||||
const entry = std.mem.bytesToValue(Entry, tail[index * @sizeOf(Entry) ..][0..@sizeOf(Entry)]);
|
||||
if (entry.device_id == device_manager_protocol.no_device) continue;
|
||||
if ((entry.identity >> 16) & 0xff != 0x08 or (entry.identity >> 8) & 0xff != 0x06) continue;
|
||||
const exchanged = driver.helloOn(manager, .consumer, entry.device_id, null, true) orelse return null;
|
||||
const provider = exchanged.channel orelse continue; // its driver not up yet — next tick
|
||||
return .{ .endpoint = provider };
|
||||
}
|
||||
start += count;
|
||||
}
|
||||
}
|
||||
|
||||
fn initialise(endpoint: ipc.Handle) bool {
|
||||
service_endpoint = endpoint;
|
||||
@@ -133,7 +186,7 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
/// `mounted` on success; a failure leaves everything untouched for the next tick.
|
||||
fn tryBringUp() void {
|
||||
if (mounted) return;
|
||||
const device = block.tryOpen() orelse return;
|
||||
const device = acquireVolume() orelse return;
|
||||
const geometry = device.geometry() orelse {
|
||||
_ = logging.write("/system/services/fat: block geometry unavailable\n");
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user