init writes /mnt/usb/DANOS.LOG at shutdown and then enters S5 — but the write sat in the USB flash controller's write cache and was lost when power was cut, because nothing issued SCSI SYNCHRONIZE CACHE. Invisible in QEMU (its backing file commits immediately); real on hardware. The boot-time flush survived only because the machine kept running afterward and the cache drained on its own. - block protocol gains a flush op; usb-storage serves it with SYNCHRONIZE CACHE (10); runtime.block gains Device.flush() - the FAT server tracks whether blocks were written and, on a file close, commits the device cache (durable-on-close — the right default for removable media, and it makes init's existing shutdown close() persist the log before S5, no init/VFS change needed) - verified the SYNCHRONIZE CACHE actually reaches the driver on each dirty close; usb-storage/fat-mount/mutations/rename/mtime/log-flush all green
45 lines
1.9 KiB
Zig
45 lines
1.9 KiB
Zig
//! The block-device wire protocol — what a filesystem (the FAT server) says to a
|
|
//! block driver (usb-storage) over its well-known `.block` endpoint. A protocol
|
|
//! module like vfs-protocol / usb-transfer-protocol: extern-struct messages, an
|
|
//! `Operation` tag, everything in one IPC message.
|
|
//!
|
|
//! 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 256-byte IPC boundary; only the small request /
|
|
//! reply headers do. (Safe while the IOMMU is unenforced; see docs/driver-model.md.)
|
|
|
|
pub const Operation = enum(u32) {
|
|
/// geometry() -> { block_size, block_count }
|
|
geometry = 0,
|
|
/// read(lba, count, physical): read `count` blocks from `lba` into the buffer
|
|
read = 1,
|
|
/// write(lba, count, physical): write `count` blocks at `lba` from the buffer
|
|
write = 2,
|
|
/// flush(): commit any device write cache to stable media (no data transfer).
|
|
/// A filesystem calls this to make prior writes durable — e.g. before power-off,
|
|
/// so a shutdown-time write isn't lost in the USB flash controller's cache.
|
|
flush = 3,
|
|
};
|
|
|
|
pub const Request = extern struct {
|
|
operation: u32,
|
|
reserved: u32 = 0,
|
|
lba: u64,
|
|
count: u32, // number of blocks (read/write)
|
|
reserved2: u32 = 0,
|
|
physical: u64, // caller's DMA buffer physical address (read/write)
|
|
};
|
|
|
|
pub const Reply = extern struct {
|
|
status: i32, // 0 on success, negative on failure
|
|
reserved: u32 = 0,
|
|
block_size: u32, // geometry: bytes per block (512)
|
|
reserved2: u32 = 0,
|
|
block_count: u64, // geometry: total blocks; read/write: blocks moved
|
|
};
|
|
|
|
pub const message_maximum: usize = 256;
|
|
pub const request_size: usize = @sizeOf(Request);
|
|
pub const reply_size: usize = @sizeOf(Reply);
|