M9: user-space VFS server + client file API (open/read/write/stat + stdio)

The payoff milestone: files are served by a user-space process, reached over
IPC — the kernel never sees a path or an fd.

- lib/vfs_proto.zig: the VFS wire protocol (Op, Request/Reply fixed header +
  inline payload, Stat), shared by client and server; one message <= MSG_MAX.
- lib/ipc.zig: replyWait() — the server-side dual-return stub (length in rax,
  badge in rdx via a "+{rdx}" read-write operand), deferred from M7.
- sbin/vfs.zig: the real VFS server — an in-heap ramfs (open creates a node)
  with an IPC_ReplyWait dispatch loop serving open/read/write/stat/close.
  Registers its endpoint under the well-known vfs id at startup.
- lib/unistd.zig: POSIX-style client API in rt — a per-process fd table +
  open/close/read/write/lseek/stat, each an IPC_Call to the VFS. The kernel
  knows nothing of fds; the table lives here.
- lib/stdio.zig: C stdio over unistd — FILE + fopen/fclose/fread/fwrite/
  fseek/ftell/rewind/feof/ferror/fputs/fputc/fgetc (unbuffered for now).
- sbin/vfstest.zig: a client that opens/writes/seeks/reads a file and only
  heartbeats "vfstest: ok" if the round trip matched. Packed in the initrd.
- New `vfs` test drives it end to end. initrd test relaxed to generic
  liveness. Suite 31/31.
This commit is contained in:
Daniel Samson
2026-07-09 07:48:24 +01:00
parent 750a73f050
commit b0f894f50c
10 changed files with 583 additions and 14 deletions
+149
View File
@@ -0,0 +1,149 @@
//! POSIX-style file API for user programs — the low level under C stdio. Files
//! are named objects served by the user-space VFS server (sbin/vfs.zig); each
//! call marshals a request, IPC_Calls the VFS, and unmarshals the reply. The
//! kernel knows nothing of files or fds — the fd table lives here, per process.
const std = @import("std");
const proto = @import("vfs_proto.zig");
const ipc = @import("ipc.zig");
const danos = @import("danos");
pub const O_CREAT = proto.O_CREAT;
pub const SEEK_SET: u32 = 0;
pub const SEEK_CUR: u32 = 1;
pub const SEEK_END: u32 = 2;
// Resolve (and cache) the VFS server endpoint, looked up by well-known id.
var vfs_handle: usize = 0;
var vfs_resolved = false;
fn vfs() ?usize {
if (!vfs_resolved) {
vfs_handle = ipc.lookup(.vfs) orelse return null;
vfs_resolved = true;
}
return vfs_handle;
}
const max_fds = 32;
const Fd = struct { used: bool = false, node: u64 = 0, offset: u64 = 0 };
var fds = [_]Fd{.{}} ** max_fds;
fn allocFd() ?usize {
for (&fds, 0..) |*f, i| {
if (!f.used) {
f.* = .{ .used = true };
return i;
}
}
return null;
}
const Result = struct { reply: proto.Reply, payload: []u8 };
/// One request/reply round trip: [Request header][send payload] -> VFS ->
/// [Reply header][recv payload]. The recv payload is written into `out`.
fn transact(req: proto.Request, send: []const u8, out: []u8) ?Result {
const h = vfs() orelse return null;
var msg: [proto.msg_max]u8 = undefined;
@memcpy(msg[0..proto.req_size], std.mem.asBytes(&req));
const slen = @min(send.len, proto.max_payload);
@memcpy(msg[proto.req_size..][0..slen], send[0..slen]);
var rbuf: [proto.msg_max]u8 = undefined;
const n = ipc.call(h, msg[0 .. proto.req_size + slen], &rbuf) catch return null;
if (n < proto.reply_size) return null;
const reply = std.mem.bytesToValue(proto.Reply, rbuf[0..proto.reply_size]);
const rpl = @min(n - proto.reply_size, out.len);
@memcpy(out[0..rpl], rbuf[proto.reply_size..][0..rpl]);
return .{ .reply = reply, .payload = out[0..rpl] };
}
/// Open (or create, with O_CREAT) `path`; returns an fd or -1.
pub fn open(path: []const u8, flags: u32) i32 {
const fd = allocFd() orelse return -1;
const req = proto.Request{ .op = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = flags };
const r = transact(req, path, &.{}) orelse {
fds[fd].used = false;
return -1;
};
if (r.reply.status != 0) {
fds[fd].used = false;
return -1;
}
fds[fd] = .{ .used = true, .node = r.reply.node, .offset = 0 };
return @intCast(fd);
}
fn fdPtr(fd: i32) ?*Fd {
if (fd < 0 or fd >= max_fds) return null;
const f = &fds[@intCast(fd)];
return if (f.used) f else null;
}
/// Read up to `buf.len` bytes at the current offset; returns the count or -1.
pub fn read(fd: i32, buf: []u8) isize {
const f = fdPtr(fd) orelse return -1;
const want: u32 = @intCast(@min(buf.len, proto.max_payload));
const req = proto.Request{ .op = .read, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
const r = transact(req, &.{}, buf) orelse return -1;
if (r.reply.status != 0) return -1;
f.offset += r.reply.len;
return @intCast(r.reply.len);
}
/// Write `data` at the current offset; returns the count or -1.
pub fn write(fd: i32, data: []const u8) isize {
const f = fdPtr(fd) orelse return -1;
const want: u32 = @intCast(@min(data.len, proto.max_payload));
const req = proto.Request{ .op = .write, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
const r = transact(req, data[0..want], &.{}) orelse return -1;
if (r.reply.status != 0) return -1;
f.offset += r.reply.len;
return @intCast(r.reply.len);
}
/// Reposition the fd's offset. Returns the new offset or -1. (SEEK_END needs the
/// file size, which `stat` provides; handled by fetching it here.)
pub fn lseek(fd: i32, off: i64, whence: u32) i64 {
const f = fdPtr(fd) orelse return -1;
const base: i64 = switch (whence) {
SEEK_SET => 0,
SEEK_CUR => @intCast(f.offset),
SEEK_END => blk: {
const req = proto.Request{ .op = .stat, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
var sbuf: [@sizeOf(proto.Stat)]u8 = undefined;
const r = transact(req, &.{}, &sbuf) orelse return -1;
if (r.reply.status != 0 or r.payload.len < @sizeOf(proto.Stat)) return -1;
const st = std.mem.bytesToValue(proto.Stat, sbuf[0..@sizeOf(proto.Stat)]);
break :blk @intCast(st.size);
},
else => return -1,
};
const pos = base + off;
if (pos < 0) return -1;
f.offset = @intCast(pos);
return pos;
}
/// Stat `path`. Returns 0 or -1.
pub fn stat(path: []const u8, out: *proto.Stat) i32 {
// Open, stat by node, close — simple and enough for now.
const fd = open(path, 0);
if (fd < 0) return -1;
defer close(fd);
const f = fdPtr(fd).?;
const req = proto.Request{ .op = .stat, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
var sbuf: [@sizeOf(proto.Stat)]u8 = undefined;
const r = transact(req, &.{}, &sbuf) orelse return -1;
if (r.reply.status != 0 or r.payload.len < @sizeOf(proto.Stat)) return -1;
out.* = std.mem.bytesToValue(proto.Stat, sbuf[0..@sizeOf(proto.Stat)]);
return 0;
}
/// Close an fd (best effort — tells the VFS to release the open file).
pub fn close(fd: i32) void {
const f = fdPtr(fd) orelse return;
const req = proto.Request{ .op = .close, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
_ = transact(req, &.{}, &.{});
f.used = false;
}