fat: multi-sector block transfers (B5a)
The FAT engine read and wrote one sector per device command — one SCSI READ(10)/WRITE(10) over USB Bulk-Only Transport per 512 bytes, so every file read, log write, and cluster fill paid a full USB round trip per sector. The lower stack (runtime.block, the block protocol, usb-storage's read10/write10 + count*block_size data stage) already carried a multi-sector count; only the engine's BlockDevice interface and IpcBlock were single-sector. Widen BlockDevice to move a run of `count` contiguous sectors per call (readBlock/writeBlock kept as count=1 wrappers, so metadata call sites — FAT sectors, directory entries — are untouched). readFile and writeFile now coalesce the aligned full-sector middle of a transfer into one command (capped at the cluster boundary and the 4 KiB bounce = 8 sectors), reading straight into / writing straight from the caller's buffer with no staging copy. Full-sector writes skip the read-modify-write entirely, since they overwrite the whole sector. Partial head/tail sectors keep the per-sector RMW path. IpcBlock passes count through to the block driver and sizes its bounce from engine.max_transfer_sectors (already 4 KiB — no new allocation). A new spc=8 engine test proves runs coalesce (a 21-sector overwrite drops from >=21 writes to <=5) and that reads/writes round-trip byte-identical, including an unaligned offset spanning a cluster boundary. Full QEMU suite 92/92.
This commit is contained in:
parent
ded555961c
commit
6e261ccda1
|
|
@ -17,21 +17,37 @@
|
|||
const std = @import("std");
|
||||
const on_disk = @import("on-disk.zig");
|
||||
|
||||
/// The most sectors one multi-sector transfer moves. Runs of contiguous full
|
||||
/// sectors within a cluster are coalesced into a single device command up to this
|
||||
/// cap; it bounds the DMA bounce buffer the fat server must provide (see
|
||||
/// `IpcBlock` in fat.zig — its bounce is `max_transfer_sectors * 512` bytes).
|
||||
pub const max_transfer_sectors = 8;
|
||||
|
||||
/// A block device the engine reads and writes in fixed-size blocks. The two
|
||||
/// function pointers let the same engine run over a real `.block` driver or a
|
||||
/// RAM buffer (the tests).
|
||||
/// function pointers move a *run* of `count` contiguous sectors in one call, so a
|
||||
/// bulk file read/write costs one device round-trip per run rather than one per
|
||||
/// sector; the same engine runs over a real `.block` driver (which turns a run
|
||||
/// into one multi-sector SCSI command) or a RAM buffer (the tests). Metadata
|
||||
/// accesses (FAT sectors, directory sectors) use the single-block convenience
|
||||
/// wrappers below.
|
||||
pub const BlockDevice = struct {
|
||||
context: *anyopaque,
|
||||
block_size: u32,
|
||||
block_count: u64,
|
||||
readBlockFn: *const fn (context: *anyopaque, lba: u64, buffer: []u8) bool,
|
||||
writeBlockFn: *const fn (context: *anyopaque, lba: u64, buffer: []const u8) bool,
|
||||
readBlocksFn: *const fn (context: *anyopaque, lba: u64, count: u32, buffer: []u8) bool,
|
||||
writeBlocksFn: *const fn (context: *anyopaque, lba: u64, count: u32, buffer: []const u8) bool,
|
||||
|
||||
pub fn readBlocks(self: BlockDevice, lba: u64, count: u32, buffer: []u8) bool {
|
||||
return self.readBlocksFn(self.context, lba, count, buffer);
|
||||
}
|
||||
pub fn writeBlocks(self: BlockDevice, lba: u64, count: u32, buffer: []const u8) bool {
|
||||
return self.writeBlocksFn(self.context, lba, count, buffer);
|
||||
}
|
||||
pub fn readBlock(self: BlockDevice, lba: u64, buffer: []u8) bool {
|
||||
return self.readBlockFn(self.context, lba, buffer);
|
||||
return self.readBlocksFn(self.context, lba, 1, buffer);
|
||||
}
|
||||
pub fn writeBlock(self: BlockDevice, lba: u64, buffer: []const u8) bool {
|
||||
return self.writeBlockFn(self.context, lba, buffer);
|
||||
return self.writeBlocksFn(self.context, lba, 1, buffer);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -87,6 +103,16 @@ pub const FileSystem = struct {
|
|||
fn blockWrite(self: *FileSystem, lba: u64, buffer: []const u8) bool {
|
||||
return self.device.writeBlock(self.base_lba + lba, buffer);
|
||||
}
|
||||
// Move `count` contiguous full sectors in one device command. `buffer` must be
|
||||
// exactly count*sector_size and sector-aligned in meaning (whole sectors only);
|
||||
// callers use these for the aligned middle of a file transfer, falling back to
|
||||
// the single-sector read-modify-write path for partial head/tail sectors.
|
||||
fn blockReadRun(self: *FileSystem, lba: u64, count: u32, buffer: []u8) bool {
|
||||
return self.device.readBlocks(self.base_lba + lba, count, buffer);
|
||||
}
|
||||
fn blockWriteRun(self: *FileSystem, lba: u64, count: u32, buffer: []const u8) bool {
|
||||
return self.device.writeBlocks(self.base_lba + lba, count, buffer);
|
||||
}
|
||||
|
||||
/// Mount the filesystem on `device`: either a bare FAT with its boot sector at
|
||||
/// LBA 0, or (as QEMU's VVFAT and most real USB sticks present it) an MBR-
|
||||
|
|
@ -544,6 +570,7 @@ pub const FileSystem = struct {
|
|||
const want = @min(buffer.len, available);
|
||||
const cluster_bytes = self.geometry.sectors_per_cluster * sector_size;
|
||||
|
||||
const spc = self.geometry.sectors_per_cluster;
|
||||
var produced: usize = 0;
|
||||
var position = offset;
|
||||
while (produced < want) {
|
||||
|
|
@ -551,7 +578,23 @@ pub const FileSystem = struct {
|
|||
const in_cluster = position % cluster_bytes;
|
||||
const sector_in_cluster = in_cluster / sector_size;
|
||||
const in_sector = in_cluster % sector_size;
|
||||
if (!self.blockRead(self.clusterSector(cluster, sector_in_cluster), &self.sector)) break;
|
||||
const lba = self.clusterSector(cluster, sector_in_cluster);
|
||||
|
||||
// Aligned middle: read a run of whole sectors straight into the caller's
|
||||
// buffer in one device command (no per-sector round-trip, no staging
|
||||
// copy). Bounded by the sectors left in this cluster (the next cluster
|
||||
// may not be physically contiguous), the transfer cap, and the request.
|
||||
if (in_sector == 0 and want - produced >= sector_size) {
|
||||
const full = @min((want - produced) / sector_size, spc - sector_in_cluster);
|
||||
const run: u32 = @intCast(@min(full, max_transfer_sectors));
|
||||
if (!self.blockReadRun(lba, run, buffer[produced .. produced + run * sector_size])) break;
|
||||
produced += run * sector_size;
|
||||
position += run * sector_size;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Partial head or tail sector: read one sector, copy the slice needed.
|
||||
if (!self.blockRead(lba, &self.sector)) break;
|
||||
const n = @min(want - produced, sector_size - in_sector);
|
||||
@memcpy(buffer[produced .. produced + n], self.sector[in_sector .. in_sector + n]);
|
||||
produced += n;
|
||||
|
|
@ -573,6 +616,7 @@ pub const FileSystem = struct {
|
|||
node.first_cluster = fresh;
|
||||
}
|
||||
|
||||
const spc = self.geometry.sectors_per_cluster;
|
||||
var consumed: usize = 0;
|
||||
var position = offset;
|
||||
while (consumed < data.len) {
|
||||
|
|
@ -581,7 +625,22 @@ pub const FileSystem = struct {
|
|||
const sector_in_cluster = in_cluster / sector_size;
|
||||
const in_sector = in_cluster % sector_size;
|
||||
const lba = self.clusterSector(cluster, sector_in_cluster);
|
||||
// Read-modify-write the sector for a partial write.
|
||||
|
||||
// Aligned middle: write a run of whole sectors straight from the caller's
|
||||
// data in one device command. These sectors are fully overwritten, so no
|
||||
// read-modify-write is needed — a double saving over the per-sector RMW
|
||||
// path. Bounded by the sectors left in this (already-resolved) cluster,
|
||||
// the transfer cap, and the data remaining.
|
||||
if (in_sector == 0 and data.len - consumed >= sector_size) {
|
||||
const full = @min((data.len - consumed) / sector_size, spc - sector_in_cluster);
|
||||
const run: u32 = @intCast(@min(full, max_transfer_sectors));
|
||||
if (!self.blockWriteRun(lba, run, data[consumed .. consumed + run * sector_size])) break;
|
||||
consumed += run * sector_size;
|
||||
position += run * sector_size;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Partial head or tail sector: read-modify-write one sector.
|
||||
if (!self.blockRead(lba, &self.sector)) break;
|
||||
const n = @min(data.len - consumed, sector_size - in_sector);
|
||||
@memcpy(self.sector[in_sector .. in_sector + n], data[consumed .. consumed + n]);
|
||||
|
|
@ -1049,18 +1108,29 @@ pub const FileSystem = struct {
|
|||
|
||||
const RamDisk = struct {
|
||||
bytes: []u8,
|
||||
fn readBlock(context: *anyopaque, lba: u64, buffer: []u8) bool {
|
||||
// Instrumentation for the multi-sector tests: how many device commands were
|
||||
// issued, and the largest run (in sectors) any single command carried.
|
||||
reads: usize = 0,
|
||||
writes: usize = 0,
|
||||
max_run: u32 = 0,
|
||||
fn readBlocks(context: *anyopaque, lba: u64, count: u32, buffer: []u8) bool {
|
||||
const self: *RamDisk = @ptrCast(@alignCast(context));
|
||||
const len = @as(usize, count) * sector_size;
|
||||
const start = lba * sector_size;
|
||||
if (start + sector_size > self.bytes.len) return false;
|
||||
@memcpy(buffer[0..sector_size], self.bytes[start .. start + sector_size]);
|
||||
if (start + len > self.bytes.len or buffer.len < len) return false;
|
||||
@memcpy(buffer[0..len], self.bytes[start .. start + len]);
|
||||
self.reads += 1;
|
||||
if (count > self.max_run) self.max_run = count;
|
||||
return true;
|
||||
}
|
||||
fn writeBlock(context: *anyopaque, lba: u64, buffer: []const u8) bool {
|
||||
fn writeBlocks(context: *anyopaque, lba: u64, count: u32, buffer: []const u8) bool {
|
||||
const self: *RamDisk = @ptrCast(@alignCast(context));
|
||||
const len = @as(usize, count) * sector_size;
|
||||
const start = lba * sector_size;
|
||||
if (start + sector_size > self.bytes.len) return false;
|
||||
@memcpy(self.bytes[start .. start + sector_size], buffer[0..sector_size]);
|
||||
if (start + len > self.bytes.len or buffer.len < len) return false;
|
||||
@memcpy(self.bytes[start .. start + len], buffer[0..len]);
|
||||
self.writes += 1;
|
||||
if (count > self.max_run) self.max_run = count;
|
||||
return true;
|
||||
}
|
||||
fn device(self: *RamDisk) BlockDevice {
|
||||
|
|
@ -1068,8 +1138,8 @@ const RamDisk = struct {
|
|||
.context = self,
|
||||
.block_size = sector_size,
|
||||
.block_count = self.bytes.len / sector_size,
|
||||
.readBlockFn = readBlock,
|
||||
.writeBlockFn = writeBlock,
|
||||
.readBlocksFn = readBlocks,
|
||||
.writeBlocksFn = writeBlocks,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -1078,13 +1148,17 @@ const RamDisk = struct {
|
|||
// two reserved entries, an empty root directory. Enough for the engine to mount
|
||||
// and operate on.
|
||||
fn formatFat16(bytes: []u8) void {
|
||||
formatFat16Spc(bytes, 1);
|
||||
}
|
||||
|
||||
fn formatFat16Spc(bytes: []u8, sectors_per_cluster: u8) void {
|
||||
@memset(bytes, 0);
|
||||
const total_sectors: u16 = @intCast(bytes.len / sector_size);
|
||||
var bpb = std.mem.zeroes(on_disk.BiosParameterBlock);
|
||||
bpb.jump = .{ 0xEB, 0x3C, 0x90 };
|
||||
bpb.oem_name = "MSWIN4.1".*;
|
||||
bpb.bytes_per_sector = sector_size;
|
||||
bpb.sectors_per_cluster = 1;
|
||||
bpb.sectors_per_cluster = sectors_per_cluster;
|
||||
bpb.reserved_sector_count = 1;
|
||||
bpb.fat_count = 2;
|
||||
bpb.root_entry_count = 512;
|
||||
|
|
@ -1159,6 +1233,63 @@ test "create, write, read back a file through the engine" {
|
|||
try std.testing.expect(fs.listEntry(fs.rootNode(), 1) == null);
|
||||
}
|
||||
|
||||
test "multi-sector transfers coalesce and round-trip identically" {
|
||||
const allocator = std.testing.allocator;
|
||||
const bytes = try allocator.alloc(u8, 8000 * sector_size);
|
||||
defer allocator.free(bytes);
|
||||
// 8 sectors per cluster: aligned runs can hit the max_transfer_sectors cap.
|
||||
formatFat16Spc(bytes, 8);
|
||||
|
||||
var disk = RamDisk{ .bytes = bytes };
|
||||
var fs = FileSystem.mount(disk.device()).?;
|
||||
|
||||
// A payload that is many whole sectors plus a partial tail, all sector-aligned
|
||||
// at the start (offset 0): the aligned-run path should carry the bulk.
|
||||
const size = 20 * sector_size + 100;
|
||||
const payload = try allocator.alloc(u8, size);
|
||||
defer allocator.free(payload);
|
||||
for (payload, 0..) |*b, i| b.* = @truncate(i * 7 + 3);
|
||||
|
||||
var node = fs.createFile(fs.rootNode(), "BIG.BIN").?;
|
||||
const written = fs.writeFile(&node, 0, payload);
|
||||
try std.testing.expectEqual(size, written);
|
||||
|
||||
// Overwrite the same region (no allocation, no cluster-zeroing, no FAT writes),
|
||||
// so the command count reflects only data movement. The 21 sectors it spans
|
||||
// (three clusters of 8) coalesce into a handful of runs — far fewer than the
|
||||
// >=21 writes a per-sector engine would issue, and each full sector skips the
|
||||
// read-modify-write entirely.
|
||||
disk.writes = 0;
|
||||
disk.max_run = 0;
|
||||
_ = fs.writeFile(&node, 0, payload);
|
||||
// A per-sector engine would issue >=21 writes (one RMW per sector); coalescing
|
||||
// plus skip-RMW-on-full-sector brings it to the three aligned runs + one partial
|
||||
// tail + the directory-entry writeback.
|
||||
try std.testing.expect(disk.max_run > 1);
|
||||
try std.testing.expect(disk.writes <= 5);
|
||||
|
||||
const resolved = fs.resolve("/BIG.BIN").?;
|
||||
try std.testing.expectEqual(@as(u32, size), resolved.size);
|
||||
|
||||
// Read back the whole file and compare byte-for-byte.
|
||||
const readback = try allocator.alloc(u8, size);
|
||||
defer allocator.free(readback);
|
||||
disk.reads = 0;
|
||||
disk.max_run = 0;
|
||||
const got = fs.readFile(resolved, 0, readback);
|
||||
try std.testing.expectEqual(size, got);
|
||||
try std.testing.expectEqualSlices(u8, payload, readback);
|
||||
try std.testing.expect(disk.max_run > 1);
|
||||
try std.testing.expect(disk.reads < 21);
|
||||
|
||||
// Equivalence at an unaligned offset spanning a cluster boundary: same bytes as
|
||||
// a naive per-sector engine would produce (the payload we wrote).
|
||||
var window: [1000]u8 = undefined;
|
||||
const n = fs.readFile(resolved, 300, &window);
|
||||
try std.testing.expectEqual(@as(usize, 1000), n);
|
||||
try std.testing.expectEqualSlices(u8, payload[300 .. 300 + 1000], window[0..1000]);
|
||||
}
|
||||
|
||||
test "truncate frees the chain and zeroes the file" {
|
||||
const allocator = std.testing.allocator;
|
||||
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
||||
|
|
|
|||
|
|
@ -22,20 +22,24 @@ const mount_point = "/mnt/usb";
|
|||
// buffer the driver reads/writes by physical address.
|
||||
const IpcBlock = struct {
|
||||
device: runtime.block.Device,
|
||||
bounce: dma.Region,
|
||||
bounce: dma.Region, // engine.max_transfer_sectors * 512 bytes
|
||||
|
||||
fn readBlock(context: *anyopaque, lba: u64, buffer: []u8) bool {
|
||||
fn readBlocks(context: *anyopaque, lba: u64, count: u32, buffer: []u8) bool {
|
||||
const self: *IpcBlock = @ptrCast(@alignCast(context));
|
||||
if (!self.device.read(lba, 1, self.bounce.physical)) return false;
|
||||
if (count == 0 or count > engine.max_transfer_sectors) return false;
|
||||
const len = count * 512;
|
||||
if (!self.device.read(lba, count, self.bounce.physical)) return false;
|
||||
const source: [*]const u8 = @ptrFromInt(self.bounce.virtual);
|
||||
@memcpy(buffer[0..512], source[0..512]);
|
||||
@memcpy(buffer[0..len], source[0..len]);
|
||||
return true;
|
||||
}
|
||||
fn writeBlock(context: *anyopaque, lba: u64, buffer: []const u8) bool {
|
||||
fn writeBlocks(context: *anyopaque, lba: u64, count: u32, buffer: []const u8) bool {
|
||||
const self: *IpcBlock = @ptrCast(@alignCast(context));
|
||||
if (count == 0 or count > engine.max_transfer_sectors) return false;
|
||||
const len = count * 512;
|
||||
const destination: [*]u8 = @ptrFromInt(self.bounce.virtual);
|
||||
@memcpy(destination[0..512], buffer[0..512]);
|
||||
if (!self.device.write(lba, 1, self.bounce.physical)) return false;
|
||||
@memcpy(destination[0..len], buffer[0..len]);
|
||||
if (!self.device.write(lba, count, self.bounce.physical)) return false;
|
||||
device_dirty = true; // a block reached the device; a close will flush it
|
||||
return true;
|
||||
}
|
||||
|
|
@ -108,15 +112,15 @@ fn tryBringUp() void {
|
|||
_ = runtime.system.write("/system/services/fat: block geometry unavailable\n");
|
||||
return;
|
||||
};
|
||||
const bounce = dma.alloc(4096, dma.coherent) orelse return;
|
||||
const bounce = dma.alloc(engine.max_transfer_sectors * 512, dma.coherent) orelse return;
|
||||
ipc_block = .{ .device = device, .bounce = bounce };
|
||||
|
||||
const block_device = engine.BlockDevice{
|
||||
.context = &ipc_block,
|
||||
.block_size = geometry.block_size,
|
||||
.block_count = geometry.block_count,
|
||||
.readBlockFn = IpcBlock.readBlock,
|
||||
.writeBlockFn = IpcBlock.writeBlock,
|
||||
.readBlocksFn = IpcBlock.readBlocks,
|
||||
.writeBlocksFn = IpcBlock.writeBlocks,
|
||||
};
|
||||
filesystem = engine.FileSystem.mount(block_device) orelse {
|
||||
_ = runtime.system.write("/system/services/fat: not a FAT filesystem\n");
|
||||
|
|
|
|||
Loading…
Reference in New Issue