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:
@@ -218,6 +218,7 @@ pub fn build(b: *std.Build) void {
|
||||
// the host-side mkinitrd tool. The bootloader ferries the image to the kernel,
|
||||
// which unpacks it and spawns each program (src/user/proto/initrd.zig).
|
||||
const vfs_exe = addUserBinary(b, kernel_target, rt_mod, "vfs", "sbin/vfs.zig");
|
||||
const vfstest_exe = addUserBinary(b, kernel_target, rt_mod, "vfstest", "sbin/vfstest.zig");
|
||||
|
||||
// Pack the user binaries into the initrd image with the host-side Python tool
|
||||
// (the container format is trivial, and Python sidesteps std API churn). Args:
|
||||
@@ -227,6 +228,8 @@ pub fn build(b: *std.Build) void {
|
||||
const initrd_img = mk_run.addOutputFileArg("initrd.img");
|
||||
mk_run.addArg("vfs");
|
||||
mk_run.addFileArg(vfs_exe.getEmittedBin());
|
||||
mk_run.addArg("vfstest");
|
||||
mk_run.addFileArg(vfstest_exe.getEmittedBin());
|
||||
|
||||
// Install the image to zig-out/bin (so the QEMU test harness picks it up like
|
||||
// the other binaries). The run-x86-64 ESP install is added below.
|
||||
|
||||
+26
-2
@@ -50,5 +50,29 @@ pub fn call(h: Handle, msg: []const u8, reply: []u8) CallError!usize {
|
||||
return if (failed(r)) error.Failed else r;
|
||||
}
|
||||
|
||||
// replyWait() (server side — returns message length + sender badge) lands with
|
||||
// the first server binary (M9), where it can be exercised end to end.
|
||||
/// The result of a `replyWait`: the request length and the sender's badge (a
|
||||
/// task id, or an IRQ notification if the high bit is set).
|
||||
pub const Received = struct {
|
||||
len: usize,
|
||||
badge: u64,
|
||||
};
|
||||
|
||||
/// Server side of IPC_ReplyWait: deliver `reply` to the client last received (if
|
||||
/// any), then block until the next request arrives in `recv`. Returns its length
|
||||
/// and the sender badge. This syscall returns two values — the length in rax and
|
||||
/// the badge in rdx — so it needs a hand-written stub: rdx is a read-write
|
||||
/// operand (input = reply length, arg #3; output = badge).
|
||||
pub fn replyWait(h: Handle, reply: []const u8, recv: []u8) Received {
|
||||
var rax: usize = undefined;
|
||||
var rdx: usize = reply.len; // in: reply_len (arg #3 -> rdx); out: badge
|
||||
asm volatile ("syscall"
|
||||
: [rax] "={rax}" (rax),
|
||||
[rdx] "+{rdx}" (rdx),
|
||||
: [n] "{rax}" (@intFromEnum(danos.Syscall.ipc_reply_wait)),
|
||||
[a0] "{rdi}" (h),
|
||||
[a1] "{rsi}" (@intFromPtr(reply.ptr)),
|
||||
[a3] "{r10}" (@intFromPtr(recv.ptr)),
|
||||
[a4] "{r8}" (recv.len),
|
||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||
return .{ .len = rax, .badge = rdx };
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ pub const sys = @import("sys.zig");
|
||||
pub const heap = @import("heap.zig");
|
||||
pub const ipc = @import("ipc.zig");
|
||||
pub const start = @import("start.zig");
|
||||
/// The VFS wire protocol (shared with the VFS server).
|
||||
pub const vfsproto = @import("vfs_proto.zig");
|
||||
/// POSIX-style file API: open/read/write/lseek/stat/close.
|
||||
pub const unistd = @import("unistd.zig");
|
||||
/// C stdio: fopen/fread/fwrite/fseek/ftell/fclose over unistd.
|
||||
pub const stdio = @import("stdio.zig");
|
||||
|
||||
/// Re-exported so a user binary can `pub const panic = rt.panic;`.
|
||||
pub const panic = start.panic;
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
//! A small C stdio layer over the POSIX-style file API (unistd.zig). Unbuffered
|
||||
//! for now — each fread/fwrite is one VFS round trip; an internal buffer (fewer
|
||||
//! IPC calls) is a later optimisation. Both a Zig-callable API and `extern "C"`
|
||||
//! symbols are provided, so Zig and future C programs share it.
|
||||
|
||||
const std = @import("std");
|
||||
const unistd = @import("unistd.zig");
|
||||
const heap = @import("heap.zig");
|
||||
|
||||
pub const SEEK_SET = unistd.SEEK_SET;
|
||||
pub const SEEK_CUR = unistd.SEEK_CUR;
|
||||
pub const SEEK_END = unistd.SEEK_END;
|
||||
|
||||
/// A C `FILE`: an fd plus sticky end-of-file / error flags. Allocated on the
|
||||
/// heap; `fclose` frees it.
|
||||
pub const FILE = extern struct {
|
||||
fd: i32,
|
||||
eof: c_int = 0,
|
||||
err: c_int = 0,
|
||||
};
|
||||
|
||||
fn flagsFor(mode: []const u8) u32 {
|
||||
if (mode.len == 0) return 0;
|
||||
return switch (mode[0]) {
|
||||
'w', 'a' => unistd.O_CREAT,
|
||||
else => 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Open `path` in `mode` ("r"/"w"/"a", '+' ignored for now). Returns null on error.
|
||||
pub fn fopen(path: []const u8, mode: []const u8) ?*FILE {
|
||||
const fd = unistd.open(path, flagsFor(mode));
|
||||
if (fd < 0) return null;
|
||||
const f = heap.allocator().create(FILE) catch {
|
||||
unistd.close(fd);
|
||||
return null;
|
||||
};
|
||||
f.* = .{ .fd = fd };
|
||||
if (mode.len > 0 and mode[0] == 'a') _ = unistd.lseek(fd, 0, unistd.SEEK_END);
|
||||
return f;
|
||||
}
|
||||
|
||||
pub fn fclose(f: *FILE) c_int {
|
||||
unistd.close(f.fd);
|
||||
heap.allocator().destroy(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Read `size*nmemb` bytes; returns the number of whole items read.
|
||||
pub fn fread(buf: []u8, size: usize, nmemb: usize, f: *FILE) usize {
|
||||
const total = size * nmemb;
|
||||
if (total == 0) return 0;
|
||||
const n = unistd.read(f.fd, buf[0..@min(buf.len, total)]);
|
||||
if (n <= 0) {
|
||||
f.eof = 1;
|
||||
return 0;
|
||||
}
|
||||
return @as(usize, @intCast(n)) / size;
|
||||
}
|
||||
|
||||
/// Write `size*nmemb` bytes; returns the number of whole items written.
|
||||
pub fn fwrite(data: []const u8, size: usize, nmemb: usize, f: *FILE) usize {
|
||||
const total = @min(data.len, size * nmemb);
|
||||
if (total == 0) return 0;
|
||||
const n = unistd.write(f.fd, data[0..total]);
|
||||
if (n <= 0) {
|
||||
f.err = 1;
|
||||
return 0;
|
||||
}
|
||||
return @as(usize, @intCast(n)) / size;
|
||||
}
|
||||
|
||||
pub fn fseek(f: *FILE, off: i64, whence: u32) c_int {
|
||||
f.eof = 0;
|
||||
return if (unistd.lseek(f.fd, off, whence) < 0) -1 else 0;
|
||||
}
|
||||
|
||||
pub fn ftell(f: *FILE) i64 {
|
||||
return unistd.lseek(f.fd, 0, unistd.SEEK_CUR);
|
||||
}
|
||||
|
||||
pub fn rewind(f: *FILE) void {
|
||||
_ = fseek(f, 0, SEEK_SET);
|
||||
}
|
||||
|
||||
pub fn feof(f: *FILE) c_int {
|
||||
return f.eof;
|
||||
}
|
||||
|
||||
pub fn ferror(f: *FILE) c_int {
|
||||
return f.err;
|
||||
}
|
||||
|
||||
pub fn fputs(s: []const u8, f: *FILE) c_int {
|
||||
return if (unistd.write(f.fd, s) < 0) -1 else 0;
|
||||
}
|
||||
|
||||
pub fn fputc(c: u8, f: *FILE) c_int {
|
||||
const b = [_]u8{c};
|
||||
return if (unistd.write(f.fd, &b) == 1) c else -1;
|
||||
}
|
||||
|
||||
pub fn fgetc(f: *FILE) c_int {
|
||||
var b: [1]u8 = undefined;
|
||||
const n = unistd.read(f.fd, &b);
|
||||
if (n <= 0) {
|
||||
f.eof = 1;
|
||||
return -1; // EOF
|
||||
}
|
||||
return b[0];
|
||||
}
|
||||
|
||||
// Real `extern "C"` symbols (fopen/fread/fseek/...) — with a C-string signature
|
||||
// distinct from the Zig slice API above — land with the first C program, wired
|
||||
// via @export so they don't collide with these Zig names.
|
||||
+149
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//! The VFS wire protocol — the message format spoken between a client (via the
|
||||
//! `rt` file API) and the user-space VFS server over IPC. A request is a fixed
|
||||
//! `Request` header followed by an inline payload (a path, or write bytes); a
|
||||
//! reply is a fixed `Reply` header followed by an inline payload (read bytes, or
|
||||
//! a Stat). Everything fits in one IPC message (<= ipc MSG_MAX = 256 bytes).
|
||||
//!
|
||||
//! This is user-space only — the kernel knows nothing of files or paths; it only
|
||||
//! moves the bytes. Shared by lib/unistd.zig (client) and sbin/vfs.zig (server).
|
||||
|
||||
pub const Op = enum(u32) {
|
||||
open, // open(path) -> node id
|
||||
close, // close(node)
|
||||
read, // read(node, offset, len) -> bytes
|
||||
write, // write(node, offset, bytes) -> count
|
||||
stat, // stat(node) -> Stat
|
||||
};
|
||||
|
||||
/// Request header. `node` is the server-side open-file id (from a prior open);
|
||||
/// for `open` the path is the payload and `len` is its length. `offset`/`len`
|
||||
/// carry the read/write position and count.
|
||||
pub const Request = extern struct {
|
||||
op: Op,
|
||||
node: u64,
|
||||
offset: u64,
|
||||
len: u32,
|
||||
flags: u32,
|
||||
};
|
||||
|
||||
/// Reply header. `status` is 0 on success or a negative errno; `node` is the new
|
||||
/// open-file id (for `open`); `len` is the payload length (bytes read, or the
|
||||
/// Stat size).
|
||||
pub const Reply = extern struct {
|
||||
status: i32,
|
||||
_pad: u32 = 0,
|
||||
node: u64 = 0,
|
||||
len: u32 = 0,
|
||||
_pad2: u32 = 0,
|
||||
};
|
||||
|
||||
pub const Stat = extern struct {
|
||||
size: u64,
|
||||
kind: u32,
|
||||
_pad: u32 = 0,
|
||||
};
|
||||
|
||||
pub const msg_max: usize = 256;
|
||||
pub const req_size: usize = @sizeOf(Request);
|
||||
pub const reply_size: usize = @sizeOf(Reply);
|
||||
/// Largest inline payload that still fits one IPC message alongside a header.
|
||||
pub const max_payload: usize = msg_max - req_size;
|
||||
|
||||
/// Open flags.
|
||||
pub const O_CREAT: u32 = 1;
|
||||
+128
-6
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+51
-6
@@ -107,6 +107,8 @@ pub fn run(case: []const u8, boot_info: *const BootInfo) void {
|
||||
processTest(boot_info);
|
||||
} else if (eql(case, "initrd")) {
|
||||
initrdTest(boot_info);
|
||||
} else if (eql(case, "vfs")) {
|
||||
vfsTest(boot_info);
|
||||
} else if (eql(case, "poweroff")) {
|
||||
powerTest(.off);
|
||||
} else if (eql(case, "reboot")) {
|
||||
@@ -970,17 +972,60 @@ fn initrdTest(boot_info: *const BootInfo) void {
|
||||
}
|
||||
check("every initrd binary spawned", spawned == rd.count);
|
||||
|
||||
// Wait for the spawned program(s) to heartbeat (the vfs stub sleeps ~1 s).
|
||||
// Wait for the spawned programs to run and make syscalls (they write + sleep).
|
||||
sched.setPriority(1);
|
||||
const deadline = arch.millis() + 8000;
|
||||
while (process.write_count < 2 and arch.millis() < deadline) sched.yield();
|
||||
sched.setPriority(4);
|
||||
|
||||
const prefix = "vfs: alive";
|
||||
const beat_ok = process.write_len >= prefix.len and eql(process.write_buf[0..prefix.len], prefix);
|
||||
check("an initrd process heartbeats (>=2)", process.write_count >= 2);
|
||||
check("heartbeat text arrived intact", beat_ok);
|
||||
check("heartbeats came from user mode (CPL 3)", process.write_from_user);
|
||||
check("initrd processes ran and made syscalls (>=2)", process.write_count >= 2);
|
||||
check("syscalls came from user mode (CPL 3)", process.write_from_user);
|
||||
result();
|
||||
}
|
||||
|
||||
/// The full VFS path: spawn the user-space VFS server and a client from the
|
||||
/// initrd. The client opens a file through the rt file API, writes, seeks, reads
|
||||
/// it back, and — only if the round trip matched — heartbeats "vfstest: ok". So
|
||||
/// seeing that marker proves client open/write/read reached the server over IPC
|
||||
/// and came back correct. (The client retries until the server registers.)
|
||||
fn vfsTest(boot_info: *const BootInfo) void {
|
||||
log("DANOS-TEST-BEGIN: vfs\n", .{});
|
||||
if (boot_info.initrd_len == 0) {
|
||||
check("bootloader handed over an initrd", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(danos.physToVirt(boot_info.initrd_base)))[0..boot_info.initrd_len];
|
||||
const rd = initrd.Reader.init(image) orelse {
|
||||
check("initrd image is valid", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
|
||||
process.write_count = 0;
|
||||
process.write_from_user = false;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
process.spawnProcess(item.blob, 4) catch |err| {
|
||||
log("DANOS-VFS-ERR: {s}: {s}\n", .{ item.name, @errorName(err) });
|
||||
};
|
||||
}
|
||||
|
||||
// Wait for the client's success heartbeat (it round-trips, then beats ~1/s).
|
||||
const prefix = "vfstest: ok";
|
||||
sched.setPriority(1);
|
||||
const deadline = arch.millis() + 10000;
|
||||
while (arch.millis() < deadline) {
|
||||
if (process.write_len >= prefix.len and eql(process.write_buf[0..prefix.len], prefix) and process.write_count >= 2) break;
|
||||
sched.yield();
|
||||
}
|
||||
sched.setPriority(4);
|
||||
|
||||
const ok = process.write_len >= prefix.len and eql(process.write_buf[0..prefix.len], prefix);
|
||||
check("client completed the VFS round trip (open/write/read matched)", ok);
|
||||
check("the round trip ran repeatedly (server stays up)", process.write_count >= 2);
|
||||
check("client syscalls came from user mode (CPL 3)", process.write_from_user);
|
||||
result();
|
||||
}
|
||||
|
||||
|
||||
@@ -183,6 +183,11 @@ CASES = [
|
||||
{"name": "initrd",
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# The user-space VFS: a client opens/writes/reads a file through the rt file
|
||||
# API, which IPCs the VFS server process; the round trip must match.
|
||||
{"name": "vfs",
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# The ACPI power path succeeds by QEMU *exiting* (S5 off / reset), so match the
|
||||
# pre-transition marker; the FAIL line only appears if the transition didn't take.
|
||||
{"name": "poweroff",
|
||||
|
||||
Reference in New Issue
Block a user