The block protocol gains a pushed medium_changed event (present + a monotonic change counter; presence only, never content). usb-storage becomes a Subscribers provider and runs a slow TEST UNIT READY poll (1 s): success is present, failure absent, and a transition bumps the counter and publishes. This is the second removal trigger — the DEVICE stays while the MEDIUM leaves (card readers, ATAPI trays) — which channel death cannot see (storage-architecture.md, two triggers one lifecycle). The subscriber is the volume manager (V3); until it exists the publish is a no-op fan-out, so this commit is behaviour-neutral, and its end-to-end test (eject -> medium_changed -> unmount/remount) lands in V4 with the real consumer rather than a throwaway subscriber fixture (recorded sequencing). Sense-key inspection to tell medium-absent from other transport errors is a noted refinement; a clean eject reads correctly as not-ready. Neutral: 12/12 across the block-serving surface, restart, confinement, conformance, and logging.
111 lines
5.3 KiB
Zig
111 lines
5.3 KiB
Zig
//! The block-device wire protocol — what a filesystem (the FAT server) says to a
|
|
//! block driver (usb-storage) over `/protocol/block`. Defined through the
|
|
//! envelope, so every packet begins with the folded `Header`.
|
|
//!
|
|
//! **`Header.target` is always 0 here**: a block driver instance serves exactly
|
|
//! one device over its own endpoint, so there is no object within the peer to
|
|
//! address. A driver that later fronts several volumes gives them target ids and
|
|
//! `enumerate` lists them; nothing else about the protocol changes.
|
|
//!
|
|
//! Data path: read and write move whole blocks to or from a **caller-owned DMA
|
|
//! buffer**, named by its physical address — the same physical-address handoff
|
|
//! usb-storage already uses toward the controller, one layer up. So a 512-byte
|
|
//! sector never has to cross the packet floor; only the small request / reply
|
|
//! parts do. Under an enforcing IOMMU the buffer's physical addresses are only
|
|
//! reachable by the device once the filesystem has `attach`ed the buffer's
|
|
//! capability (the block server forwards it to the controller); see
|
|
//! docs/driver-model.md.
|
|
|
|
const envelope = @import("envelope");
|
|
|
|
/// The answer to `geometry()`.
|
|
pub const Geometry = extern struct {
|
|
block_size: u32, // bytes per block (512)
|
|
_padding: u32 = 0,
|
|
block_count: u64, // total blocks
|
|
};
|
|
|
|
/// `read(lba, count, physical)` / `write(...)`: move `count` blocks between the
|
|
/// device and the caller's DMA buffer at `physical`.
|
|
pub const Transfer = extern struct {
|
|
lba: u64,
|
|
count: u32,
|
|
_padding: u32 = 0,
|
|
physical: u64, // caller's DMA buffer physical address
|
|
};
|
|
|
|
/// How many blocks a transfer actually moved.
|
|
pub const Transferred = extern struct { count: u32 };
|
|
|
|
/// `define_range(badge, base_lba, block_count)`: confine the sender identified by
|
|
/// `badge` to blocks `[base_lba, base_lba + block_count)`. The volume manager
|
|
/// calls this for each filesystem process it hands a channel to — the badge is
|
|
/// the filesystem's kernel-stamped task id, and the range is the partition it
|
|
/// mounts. A confined sender's read/write LBAs are then volume-relative (the
|
|
/// driver adds `base_lba`) and a transfer past `block_count` is refused. A
|
|
/// sender with no range is unconfined (the whole device), the default until the
|
|
/// volume manager defines one. The clamp lives at the provider because a channel
|
|
/// must carry exactly the authority it grants (storage-architecture.md): handing
|
|
/// a filesystem the whole disk plus a base offset would let it reach the
|
|
/// neighbouring partition. **A confined caller may not call this** — a filesystem
|
|
/// cannot redefine its own range and escape; only an unconfined party (the
|
|
/// volume manager) confines others.
|
|
pub const DefineRange = extern struct {
|
|
badge: u32,
|
|
_padding: u32 = 0,
|
|
base_lba: u64,
|
|
block_count: u64,
|
|
};
|
|
|
|
/// The `medium_changed` event payload: whether a medium is now present, and a
|
|
/// monotonic counter so a subscriber that missed an edge still sees that
|
|
/// SOMETHING changed. Pushed by a driver whose transport can tell medium from
|
|
/// device (a card reader, an ATAPI tray): the device stays, the medium comes and
|
|
/// goes. The volume manager consumes it into the same unmount/remount path it
|
|
/// runs on device death — one lifecycle, two triggers
|
|
/// (docs/file-system-development/storage-architecture.md). Presence only, never
|
|
/// content: the driver reports that the medium changed, not what is on it.
|
|
pub const MediumChanged = extern struct {
|
|
present: u8, // 1 present, 0 absent
|
|
_padding: u8 = 0,
|
|
_padding2: u16 = 0,
|
|
change_count: u32,
|
|
};
|
|
|
|
pub const Protocol = envelope.Define(.{
|
|
.name = "block",
|
|
.version = 1,
|
|
.events = &.{
|
|
.{ .name = "medium_changed", .payload = MediumChanged },
|
|
},
|
|
.operations = &.{
|
|
.{ .name = "geometry", .reply = Geometry },
|
|
.{ .name = "read", .request = Transfer, .reply = Transferred },
|
|
.{ .name = "write", .request = Transfer, .reply = Transferred },
|
|
// flush(): commit any device write cache to stable media (no data
|
|
// transfer). A filesystem calls this to make prior writes durable —
|
|
// before power-off, so a shutdown-time write isn't lost in the USB flash
|
|
// controller's cache.
|
|
.{ .name = "flush" },
|
|
// attach(): the caller's DMA-region capability rides the call's cap
|
|
// slot; the block server forwards it to the controller so the buffer's
|
|
// physical addresses (named in later read/write) are reachable by the
|
|
// device under an enforcing IOMMU. Call once per buffer before using it.
|
|
.{ .name = "attach" },
|
|
// detach(): the reverse — the same region capability rides the cap slot
|
|
// (the caller still holds its handle; the kernel matches the region) and
|
|
// the buffer leaves the device's domain. Every grant a live process
|
|
// makes is revocable by the granter while alive; death remains the
|
|
// mechanical backstop (storage-architecture.md, the lifecycle rule).
|
|
.{ .name = "detach" },
|
|
// define_range(): confine a sender to a block sub-range — the partition
|
|
// it mounts. See `DefineRange`. Appended, so every verb above keeps its
|
|
// number.
|
|
.{ .name = "define_range", .request = DefineRange },
|
|
},
|
|
});
|
|
|
|
pub const Operation = Protocol.Operation;
|
|
pub const Event = Protocol.Event;
|
|
pub const message_maximum: usize = Protocol.message_maximum;
|