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
+128 -6
View File
@@ -1,14 +1,136 @@
//! /sbin/vfs — the user-space VFS server. Shipped in the initrd and spawned as a
//! ring-3 process. This is a placeholder that only heartbeats, proving the initrd
//! pipeline ships and spawns it; the real path namespace + IPC dispatch loop
//! (open/read/write/stat forwarded to driver processes) is built in M9.
//! /sbin/vfs — the user-space VFS server. Shipped in the initrd, spawned as a
//! ring-3 process, and reached by every other process through IPC (the `rt`
//! file API marshals open/read/write/stat/close into calls to this server's
//! endpoint, published under the well-known `vfs` service id).
//!
//! For now the namespace is a small in-memory ramfs (opening a name creates it):
//! enough to prove the whole path — client file API -> IPC -> server dispatch ->
//! reply. Device nodes backed by user-space drivers (/dev) layer on top in M10,
//! where `open` on a /dev name forwards to the owning driver's endpoint.
const std = @import("std");
const rt = @import("rt");
const proto = rt.vfsproto;
const Node = struct {
used: bool = false,
name: [24]u8 = undefined,
name_len: usize = 0,
data: [512]u8 = undefined,
size: usize = 0,
};
const OpenFile = struct {
used: bool = false,
node: usize = 0,
};
var nodes = [_]Node{.{}} ** 8;
var opens = [_]OpenFile{.{}} ** 16;
fn findNode(name: []const u8) ?usize {
for (&nodes, 0..) |*n, i| {
if (n.used and std.mem.eql(u8, n.name[0..n.name_len], name)) return i;
}
return null;
}
fn createNode(name: []const u8) ?usize {
for (&nodes, 0..) |*n, i| {
if (!n.used) {
const l = @min(name.len, n.name.len);
@memcpy(n.name[0..l], name[0..l]);
n.* = .{ .used = true, .name = n.name, .name_len = l, .size = 0 };
return i;
}
}
return null;
}
fn openAt(id: u64) ?*OpenFile {
if (id >= opens.len) return null;
const o = &opens[@intCast(id)];
return if (o.used) o else null;
}
/// Serialise a reply header + payload into `out`; returns the total length.
fn writeReply(out: []u8, reply: proto.Reply, payload: []const u8) usize {
@memcpy(out[0..proto.reply_size], std.mem.asBytes(&reply));
const n = @min(payload.len, out.len - proto.reply_size);
@memcpy(out[proto.reply_size..][0..n], payload[0..n]);
return proto.reply_size + n;
}
fn fail(out: []u8) usize {
return writeReply(out, .{ .status = -1 }, &.{});
}
/// Handle one request; write the reply into `out`, return its length.
fn handle(msg: []const u8, out: []u8) usize {
if (msg.len < proto.req_size) return fail(out);
const req = std.mem.bytesToValue(proto.Request, msg[0..proto.req_size]);
const payload = msg[proto.req_size..];
switch (req.op) {
.open => {
const name = payload[0..@min(payload.len, req.len)];
const ni = findNode(name) orelse createNode(name) orelse return fail(out);
for (&opens, 0..) |*o, i| {
if (!o.used) {
o.* = .{ .used = true, .node = ni };
return writeReply(out, .{ .status = 0, .node = i }, &.{});
}
}
return fail(out);
},
.read => {
const of = openAt(req.node) orelse return fail(out);
const nd = &nodes[of.node];
const off: usize = @intCast(req.offset);
if (off >= nd.size) return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); // EOF
const n = @min(@min(nd.size - off, req.len), proto.max_payload);
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, nd.data[off .. off + n]);
},
.write => {
const of = openAt(req.node) orelse return fail(out);
const nd = &nodes[of.node];
const off: usize = @intCast(req.offset);
if (off > nd.data.len) return fail(out);
const n = @min(@min(payload.len, req.len), nd.data.len - off);
@memcpy(nd.data[off .. off + n], payload[0..n]);
if (off + n > nd.size) nd.size = off + n;
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, &.{});
},
.stat => {
const of = openAt(req.node) orelse return fail(out);
const st = proto.Stat{ .size = nodes[of.node].size, .kind = 0 };
return writeReply(out, .{ .status = 0, .len = @sizeOf(proto.Stat) }, std.mem.asBytes(&st));
},
.close => {
if (req.node < opens.len) opens[@intCast(req.node)].used = false;
return writeReply(out, .{ .status = 0 }, &.{});
},
}
}
pub fn main() void {
const ep = rt.ipc.createEndpoint() orelse {
_ = rt.sys.write("vfs: no endpoint\n");
return;
};
if (!rt.ipc.register(.vfs, ep)) {
_ = rt.sys.write("vfs: register failed\n");
return;
}
_ = rt.sys.write("vfs: ready\n");
var reply_buf: [proto.msg_max]u8 = undefined;
var reply_len: usize = 0;
var recv: [proto.msg_max]u8 = undefined;
while (true) {
_ = rt.sys.write("vfs: alive\n");
rt.sys.sleep(1000);
const got = rt.ipc.replyWait(ep, reply_buf[0..reply_len], &recv);
// Ignore notifications (none expected here); handle a request.
reply_len = handle(recv[0..got.len], &reply_buf);
}
}
+47
View File
@@ -0,0 +1,47 @@
//! /sbin/vfstest — a client that proves the VFS round trip end to end: open a
//! file through the `rt` file API, write to it, seek back, read it, and compare.
//! On success it heartbeats "vfstest: ok" so the kernel test can observe it;
//! on failure it reports what went wrong. Shipped in the initrd alongside vfs.
const std = @import("std");
const rt = @import("rt");
pub fn main() void {
const u = rt.unistd;
const payload = "hello-vfs";
// The VFS server may not have registered yet — retry open until it's up.
var fd: i32 = -1;
var tries: u32 = 0;
while (fd < 0 and tries < 200) : (tries += 1) {
fd = u.open("greeting", u.O_CREAT);
if (fd < 0) rt.sys.sleep(20);
}
if (fd < 0) {
_ = rt.sys.write("vfstest: open failed\n");
return;
}
if (u.write(fd, payload) != @as(isize, payload.len)) {
_ = rt.sys.write("vfstest: write failed\n");
return;
}
_ = u.lseek(fd, 0, u.SEEK_SET);
var buf: [32]u8 = undefined;
const n = u.read(fd, &buf);
u.close(fd);
if (n == @as(isize, payload.len) and std.mem.eql(u8, buf[0..@intCast(n)], payload)) {
while (true) {
_ = rt.sys.write("vfstest: ok\n");
rt.sys.sleep(1000);
}
}
_ = rt.sys.write("vfstest: mismatch\n");
}
pub const panic = rt.panic;
comptime {
_ = &rt.start._start;
}