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.
141 lines
4.9 KiB
Zig
141 lines
4.9 KiB
Zig
//! /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) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
pub const panic = rt.panic;
|
|
comptime {
|
|
_ = &rt.start._start;
|
|
}
|