sylveos

Toy Operating System
Log | Files | Refs

commit 22bc90e3895b4c87ed47c8d4e2ea839a0c627abd
parent 04326f45a3f35809f042c2fc1971fcf325d23c21
Author: Sylvia Ivory <git@sivory.net>
Date:   Thu, 19 Mar 2026 11:05:09 -0700

Use VFS in syscalls

Diffstat:
Mpi/fs/fatvfs.zig | 7+++++++
Mpi/fs/vfs.zig | 12++++++++++++
Asylveos/files.zig | 236+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msylveos/loader.zig | 23+++++++++++++++++++++--
Msylveos/root.zig | 29++++++++++++++++++++++++++---
Msylveos/syscall.zig | 132+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
6 files changed, 388 insertions(+), 51 deletions(-)

diff --git a/pi/fs/fatvfs.zig b/pi/fs/fatvfs.zig @@ -72,6 +72,13 @@ pub const File = struct { return file; } + // TODO; + pub fn write(self: *File, bytes: []const u8) Error!u32 { + _ = self; + _ = bytes; + return Error.WriteFailed; + } + pub fn reset(self: *File) Error!void { self.current_cluster = self.first_cluster; self.remaining = self.size; diff --git a/pi/fs/vfs.zig b/pi/fs/vfs.zig @@ -21,6 +21,8 @@ pub const Error = error{ InitFailed, ReadFailed, + SeekFailed, + WriteFailed, } || std.mem.Allocator.Error; fn TPtr(T: type, opaque_ptr: *anyopaque) T { @@ -174,6 +176,7 @@ pub const File = struct { impl: *anyopaque, v_close: *const fn (*anyopaque) void, v_read: *const fn (*anyopaque) Error!?[]u8, + v_write: *const fn (*anyopaque, []const u8) Error!u32, v_reset: *const fn (*anyopaque) Error!void, pub fn impl_by(impl_obj: anytype) File { @@ -182,6 +185,7 @@ pub const File = struct { .impl = impl_obj, .v_close = delegate.close, .v_read = delegate.read, + .v_write = delegate.write, .v_reset = delegate.reset, }; } @@ -194,6 +198,10 @@ pub const File = struct { return try self.v_read(self.impl); } + pub fn write(self: File, bytes: []const u8) Error!u32 { + return try self.v_write(self.impl, bytes); + } + pub fn reset(self: File) Error!void { return try self.v_reset(self.impl); } @@ -210,6 +218,10 @@ inline fn FileDelegate(impl_obj: anytype) type { return try TPtr(ImplType, impl).read(); } + fn write(impl: *anyopaque, bytes: []const u8) Error!u32 { + return try TPtr(ImplType, impl).write(bytes); + } + fn reset(impl: *anyopaque) Error!void { return try TPtr(ImplType, impl).reset(); } diff --git a/sylveos/files.zig b/sylveos/files.zig @@ -0,0 +1,236 @@ +const std = @import("std"); +const pi = @import("pi"); + +const vfs = pi.fs.vfs; +const uart = pi.devices.mini_uart; + +pub const Error = error{NotFound} || vfs.Error; + +// stdout is stateless so we don't care about self +pub const Stdout = struct { + pub fn close(self: *Stdout) void { + _ = self; + } + pub fn read(self: *Stdout) vfs.Error!?[]u8 { + _ = self; + return Error.ReadFailed; + } + pub fn reset(self: *Stdout) vfs.Error!void { + _ = self; + return Error.SeekFailed; + } + pub fn write(self: *Stdout, bytes: []const u8) vfs.Error!u32 { + _ = self; + // TODO; create some protocol that embeds PID + uart.print("{s}", .{bytes}); + + return bytes.len; + } +}; + +// Stderr is an alias for stdout +pub const Stderr = Stdout; + +// stdin needs an allocator to keep track of read bytes +pub const Stdin = struct { + alloc: std.mem.Allocator, + pub fn close(self: *Stdin) void { + _ = self; + } + pub fn read(self: *Stdin) vfs.Error!?[]u8 { + // read until newline or buffer is full + // TODO; should buffer come from user + var buffer = try self.alloc.alloc(u8, 1024); + var buf_read: u32 = 0; + for (0..buffer.len) |i| { + const byte = uart.read_byte_sync(); + buffer[i] = byte; + buf_read += 1; + if (byte == '\n') break; + } + return buffer[0..buf_read]; + } + pub fn reset(self: *Stdin) vfs.Error!void { + _ = self; + return Error.SeekFailed; + } + pub fn write(self: *Stdin, bytes: []const u8) vfs.Error!u8 { + _ = self; + _ = bytes; + return Error.WriteFailed; + } +}; + +pub fn resolve_path_relative(fs: vfs.FileSystem, directory: vfs.Directory, path: []const u8) Error!?vfs.DirectoryEntry { + // POSIX cannot fail init + var it = std.fs.path.componentIterator(path) catch unreachable; + var first_cwd = true; + var cwd = directory; + + while (it.next()) |component| { + if (try cwd.open_entry(component.name)) |entry| { + const stat = entry.stat(); + if (it.peekNext() != null and !stat.directory) { + // Still remaining components and yet not a directory + entry.close(); + return Error.ExpectedDirectory; + } + // Swap in entry and continue + if (first_cwd) { + // Don't close what was given to us + first_cwd = false; + } else { + cwd.close(); + } + + cwd = try fs.open_directory(@constCast(&entry)); + } else { + return null; + } + } + + return try cwd.open_entry("."); +} + +pub fn resolve_path_absolute(fs: vfs.FileSystem, path: []const u8) Error!?vfs.DirectoryEntry { + const root = try fs.open_root(); + defer root.close(); + return try resolve_path_relative(fs, root, path); +} + +pub fn resolve_path(fs: vfs.FileSystem, cwd: vfs.Directory, path: []const u8) Error!?vfs.DirectoryEntry { + if (std.fs.path.isAbsolute(path)) { + return try resolve_path_absolute(fs, path); + } else { + return try resolve_path_relative(fs, cwd, path); + } +} + +pub const FileResolver = struct { + fd_list: std.ArrayList(vfs.File), + read_cache: std.AutoHashMap(u32, []u8), + + alloc: std.mem.Allocator, + fs: vfs.FileSystem, + cwd: vfs.Directory, + + pub fn init(fs: vfs.FileSystem, cwd: vfs.Directory, alloc: std.mem.Allocator) !FileResolver { + uart.print("initializing std(in/err/in)\n", .{}); + const stdout = try alloc.create(Stdout); + const stderr = try alloc.create(Stderr); + var stdin = try alloc.create(Stdin); + uart.print("created std(out/err)\n", .{}); + stdin.alloc = alloc; + + var fd_list: std.ArrayList(vfs.File) = try .initCapacity(alloc, 3); + fd_list.appendAssumeCapacity(vfs.File.impl_by(stdin)); + fd_list.appendAssumeCapacity(vfs.File.impl_by(stdout)); + fd_list.appendAssumeCapacity(vfs.File.impl_by(stderr)); + uart.print("initialized fd_list\n", .{}); + + return .{ + .fd_list = fd_list, + .read_cache = .init(alloc), + .alloc = alloc, + .fs = fs, + .cwd = cwd, + }; + } + + // Would use close but uh, already used unfortunately + pub fn deinit(self: *FileResolver) void { + var cache_iter = self.read_cache.iterator(); + while (cache_iter.next()) |entry| { + self.alloc.free(entry.value_ptr.*); + } + self.read_cache.deinit(); + + for (self.fd_list.items) |*file| { + file.close(); + } + self.fd_list.deinit(); + } + + pub fn open(self: *FileResolver, path: []const u8) !u32 { + var entry = try resolve_path(self.fs, self.cwd, path) orelse return Error.NotFound; + + const stat = entry.stat(); + if (stat.directory) { + entry.close(); + return Error.ExpectedFile; + } + + const file = try self.fs.open_file(&entry); + + try self.fd_list.append(self.alloc, file); + return self.fd_list.items.len - 1; + } + + pub fn close(self: *FileResolver, fd: u32) !void { + if (fd >= self.fd_list.items.len) return Error.NotFound; + self.fd_list.items[fd].close(); + } + + pub fn write(self: *FileResolver, fd: u32, bytes: []const u8) Error!u32 { + if (fd >= self.fd_list.items.len) return Error.NotFound; + + return try self.fd_list.items[fd].write(bytes); + } + + pub fn read(self: *FileResolver, fd: u32, buffer: []u8) Error!u32 { + if (fd > self.fd_list.items.len) return Error.NotFound; + + if (buffer.len == 0) { + return 0; + } + + var bytes_copied: u32 = 0; + + // Is there data from an earlier call + if (self.read_cache.get(fd)) |cached_data| { + // If so, grab it + const bytes_from_cache = @min(buffer.len, cached_data.len); + @memcpy(buffer[0..bytes_from_cache], cached_data[0..bytes_from_cache]); + bytes_copied = bytes_from_cache; + + // Update or remove cache + if (bytes_from_cache < cached_data.len) { + // Still have leftover data, keep the remainder + const remaining = cached_data[bytes_from_cache..]; + const new_cache = try self.alloc.alloc(u8, remaining.len); + @memcpy(new_cache, remaining); + self.alloc.free(cached_data); + try self.read_cache.put(fd, new_cache); + + // Buffer is full, return + return @intCast(bytes_copied); + } else { + // Used all cached data + self.alloc.free(cached_data); + _ = self.read_cache.remove(fd); + } + } + + // If buffer still has space, read from file + if (bytes_copied < buffer.len) { + const file = self.fd_list.items[fd]; + const data = try file.read() orelse return @intCast(bytes_copied); + defer self.alloc.free(data); + + const remaining_buffer = buffer[bytes_copied..]; + const bytes_from_file = @min(remaining_buffer.len, data.len); + @memcpy(remaining_buffer[0..bytes_from_file], data[0..bytes_from_file]); + bytes_copied += bytes_from_file; + + // Cache any excess data + if (bytes_from_file < data.len) { + const excess = data[bytes_from_file..]; + const cache_data = try self.alloc.alloc(u8, excess.len); + @memcpy(cache_data, excess); + try self.read_cache.put(fd, cache_data); + } + } + + return bytes_copied; + } +}; diff --git a/sylveos/loader.zig b/sylveos/loader.zig @@ -5,10 +5,12 @@ const process = @import("./process.zig"); const memory = @import("./memory.zig"); const kuser = @import("./kuser.zig"); const Pages = @import("./pages.zig"); +const files = @import("./files.zig"); const Page = process.Page; const mailbox = pi.devices.mailbox; const uart = pi.devices.mini_uart; +const vfs = pi.fs.vfs; const mmu = pi.mmu; const pt = pi.pt; @@ -21,10 +23,21 @@ pub const Program = struct { elf_header: std.elf.Header, header_location: u32, heap_start: u32, + + // TODO; these should be in Process heap_current: u32, + files: files.FileResolver, }; -pub fn init(pt_alloc: std.mem.Allocator, heap_alloc: std.mem.Allocator, root: []mmu.FirstLevelDescriptor, elf: []const u8) !Program { +pub fn init( + fs: vfs.FileSystem, + cwd: vfs.Directory, + kalloc: std.mem.Allocator, + pt_alloc: std.mem.Allocator, + heap_alloc: std.mem.Allocator, + root: []mmu.FirstLevelDescriptor, + elf: []const u8, +) !Program { // TODO; proper allocator // TODO; asid // TODO; pid @@ -136,7 +149,14 @@ pub fn init(pt_alloc: std.mem.Allocator, heap_alloc: std.mem.Allocator, root: [] .elf_header = header, .header_location = header_location orelse unreachable, .heap_start = heap_start, + .heap_current = heap_start, + // TODO; use a different allocator + // ; aka create an allocator that automatically maps + // ; addresses into kernel space + // ; probably just uses heap_alloc under the hood and + // ; automatically map_4kb into current PT + .files = try .init(fs, cwd, kalloc), }; } @@ -144,7 +164,6 @@ pub fn init(pt_alloc: std.mem.Allocator, heap_alloc: std.mem.Allocator, root: [] pub var current_program: *Program = undefined; pub fn execute(self: *Program, pid: u24, args: []const []const u8, env: []const []const u8) !noreturn { - // TODO; args, env self.pages.switch_into(pid); // Temporarily store env/arg pointers for future reference diff --git a/sylveos/root.zig b/sylveos/root.zig @@ -57,13 +57,15 @@ fn abort_handler(regs: interrupts.Registers, ev: interrupts.ExceptionVector) voi if (ev == .PrefetchAbort and ifsr.status == .InstructionDebugEventFault) { journal.warn("[breakpoint] PC = 0x{X:0>8}\n", .{regs.pc}); + journal.append_registers(&regs); } else if (ev == .PrefetchAbort) { journal.err("[prefetch abort] {s}: got fault on 0x{X:0>8} from 0x{X:0>8}\n", .{ @tagName(ifsr.status), far, regs.pc, }); - interrupts.dump_registers(&regs); + journal.append_registers(&regs); + pi.reboot(); } else if (ev == .DataAbort) { const status = if (dfsr.status_kind == 0) @tagName(dfsr.status.@"0") else @tagName(dfsr.status.@"1"); @@ -74,7 +76,8 @@ fn abort_handler(regs: interrupts.Registers, ev: interrupts.ExceptionVector) voi regs.pc, @tagName(dfsr.abort_source), }); - interrupts.dump_registers(&regs); + journal.append_registers(&regs); + pi.reboot(); } } @@ -130,14 +133,34 @@ fn main() !void { interrupts.set_exception_handler(.SoftwareInterrupt, syscall.syscall_handler); interrupts.set_exception_handler(.UndefinedInstruction, undef); + var buffer: [1024 * 64]u8 = undefined; + var heap_fba = memory.get_allocator(); const heap_alloc = heap_fba.allocator(); var pt_fba = memory.get_pt_allocator(); const pt_alloc = pt_fba.allocator(); + var kalloc_fba: std.heap.FixedBufferAllocator = .init(&buffer); + const kalloc = kalloc_fba.allocator(); + const pt = memory.get_page_table(); - var lua = try loader.init(pt_alloc, heap_alloc, pt, lua_binary); + var sd_card = try pi.devices.sd.init(kalloc); + var fat_vfs: pi.fs.FatVFS = .init(); + + var vfs_disk = pi.fs.vfs.Disk.impl_by(&sd_card); + const vfs_fat = pi.fs.vfs.FileSystem.impl_by(&fat_vfs); + + const mbr: pi.fs.mbr.MasterBootRecord = @bitCast(try vfs_disk.read_sector(0)); + + try mbr.debug_print(&uart.writer); + + try vfs_fat.open(kalloc, &vfs_disk, mbr.pte1.first_lba); + defer vfs_fat.close(); + + const root = try vfs_fat.open_root(); + + var lua = try loader.init(vfs_fat, root, kalloc, pt_alloc, heap_alloc, pt, lua_binary); try loader.execute(&lua, 0, &[_][]const u8{"lua"}, &[_][]u8{}); } diff --git a/sylveos/syscall.zig b/sylveos/syscall.zig @@ -89,24 +89,26 @@ pub fn syscall_handler(registers: interrupts.Registers, _: interrupts.ExceptionV journal.trace("[syscall] read({d}, 0x{X}, {d})", .{ fd, @intFromPtr(buffer), count }); - if (fd == 0) { - // stdin - var read: u32 = 0; - for (0..count) |i| { - const byte = uart.read_byte_sync(); - buffer[i] = byte; - read += 1; - if (byte == '\n') break; - } - regs.gp[0] = @bitCast(@as(i32, @bitCast(read))); - } else if (fd == 3) { - // temporary buffer - const code = "print('hello world')"; - - @memcpy(buffer, code); - - regs.gp[0] = @bitCast(@as(i32, @bitCast(code.len))); - } + regs.gp[0] = loader.current_program.files.read(fd, buffer[0..count]) catch @bitCast(@as(i32, -1)); + + // if (fd == 0) { + // // stdin + // var read: u32 = 0; + // for (0..count) |i| { + // const byte = uart.read_byte_sync(); + // buffer[i] = byte; + // read += 1; + // if (byte == '\n') break; + // } + // regs.gp[0] = @bitCast(@as(i32, @bitCast(read))); + // } else if (fd == 3) { + // // temporary buffer + // const code = "print('hello world')"; + + // @memcpy(buffer, code); + + // regs.gp[0] = @bitCast(@as(i32, @bitCast(code.len))); + // } }, // write 4 => { @@ -116,22 +118,27 @@ pub fn syscall_handler(registers: interrupts.Registers, _: interrupts.ExceptionV journal.trace("[syscall] write({d}, 0x{X}, {d})", .{ fd, @intFromPtr(buffer), count }); + regs.gp[0] = loader.current_program.files.write(fd, buffer[0..count]) catch @bitCast(@as(i32, -1)); + // stdout - if (fd == 1) { - uart.print("{s}", .{buffer[0..count]}); - regs.gp[0] = @bitCast(@as(i32, @bitCast(count))); - } else { - regs.gp[0] = @bitCast(@as(i32, -9)); - } + // if (fd == 1) { + // uart.print("{s}", .{buffer[0..count]}); + // regs.gp[0] = @bitCast(@as(i32, @bitCast(count))); + // } else { + // regs.gp[0] = @bitCast(@as(i32, -9)); + // } }, // open 5 => { const path: [*:0]u8 = @ptrFromInt(regs.gp[0]); const flags = regs.gp[1]; + const len = std.mem.len(path); journal.trace("[syscall] open({s}, 0x{X})", .{ path, flags }); - regs.gp[0] = @bitCast(@as(i32, 3)); + regs.gp[0] = loader.current_program.files.open(path[0..len]) catch @bitCast(@as(i32, -1)); + + // regs.gp[0] = @bitCast(@as(i32, 3)); }, // close 6 => { @@ -166,22 +173,39 @@ pub fn syscall_handler(registers: interrupts.Registers, _: interrupts.ExceptionV journal.trace("[syscall] readv({d}, 0x{X}, {d})", .{ fd, @intFromPtr(iov_ptr), count }); const iov = iov_ptr[0..count]; + var total: u32 = 0; + + for (iov) |vec| { + if (vec.iov_len == 0) continue; - if (fd == 0) { - var total: u32 = 0; - for (iov) |vec| { - if (vec.iov_len == 0) continue; + const bytes_read = loader.current_program.files.read(@intCast(fd), vec.iov_base[0..vec.iov_len]) catch { + regs.gp[0] = @bitCast(@as(i32, -1)); + break; + }; + total += bytes_read; - uart.print("{s}", .{vec.iov_base[0..vec.iov_len]}); - total += vec.iov_len; + if (bytes_read < vec.iov_len) { + break; } - regs.gp[0] = total; - } else if (fd == 3) { - // done by first read - regs.gp[0] = 0; } else { - regs.gp[0] = @bitCast(@as(i32, -1)); + regs.gp[0] = total; } + + // if (fd == 0) { + // var total: u32 = 0; + // for (iov) |vec| { + // if (vec.iov_len == 0) continue; + + // uart.print("{s}", .{vec.iov_base[0..vec.iov_len]}); + // total += vec.iov_len; + // } + // regs.gp[0] = total; + // } else if (fd == 3) { + // // done by first read + // regs.gp[0] = 0; + // } else { + // regs.gp[0] = @bitCast(@as(i32, -1)); + // } }, // writev 146 => { @@ -192,19 +216,35 @@ pub fn syscall_handler(registers: interrupts.Registers, _: interrupts.ExceptionV journal.trace("[syscall] writev({d}, 0x{X}, {d})", .{ fd, @intFromPtr(iov_ptr), count }); const iov = iov_ptr[0..count]; - - if (fd == 1 or fd == 2) { - var total: u32 = 0; - for (iov) |vec| { - if (vec.iov_len == 0) continue; - - uart.print("{s}", .{vec.iov_base[0..vec.iov_len]}); - total += vec.iov_len; + var total: u32 = 0; + for (iov) |vec| { + if (vec.iov_len == 0) continue; + + const bytes_written = loader.current_program.files.write(@intCast(fd), vec.iov_base[0..vec.iov_len]) catch { + regs.gp[0] = @bitCast(@as(i32, -1)); + break; + }; + total += bytes_written; + + if (bytes_written < vec.iov_len) { + break; } - regs.gp[0] = total; } else { - regs.gp[0] = @bitCast(@as(i32, -1)); + regs.gp[0] = total; } + + // if (fd == 1 or fd == 2) { + // var total: u32 = 0; + // for (iov) |vec| { + // if (vec.iov_len == 0) continue; + + // uart.print("{s}", .{vec.iov_base[0..vec.iov_len]}); + // total += vec.iov_len; + // } + // regs.gp[0] = total; + // } else { + // regs.gp[0] = @bitCast(@as(i32, -1)); + // } }, // mmap2 192 => {