diff options
| author | hachem <im@hachem.wtf> | 2026-09-25 21:21:09 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-09-25 21:21:09 +0200 |
| commit | 09982d80c45805715984cc1cae96c37e4fc148c3 (patch) | |
| tree | f711d6e7f4d4cb2d4dcb555d6a139aae343d7334 /boot/boot.zig | |
| parent | fd3f51eecaecc12806a573c38263df1de43ee8c2 (diff) | |
feat: scheduler + cleanup
Diffstat (limited to 'boot/boot.zig')
| -rw-r--r-- | boot/boot.zig | 202 |
1 files changed, 126 insertions, 76 deletions
diff --git a/boot/boot.zig b/boot/boot.zig index 8f8c7fe..9f384fa 100644 --- a/boot/boot.zig +++ b/boot/boot.zig @@ -11,12 +11,13 @@ const user = @import("user.zig"); const elf = @import("elf.zig"); const keyboard = @import("keyboard.zig"); const timer = @import("timer.zig"); +const sched = @import("sched.zig"); const bridge = @import("bridge"); -// We need a panic handler otherwise zig won't be happy, -// std usually formats a message through std.Io.Writer which -// emits SSE instructions which I haven't enabled. Ours -// dumps the message to serial, then halts. +// we need a panic handler otherwise zig wont be happy, +// std usually formats a message through std.io.writer which +// emits sse instructions which i havent enabled. ours +// dumps the message to serial, then halts pub const panic = std.debug.FullPanic(struct { fn halt(msg: []const u8, ret_addr: ?usize) noreturn { serial.init(); @@ -32,26 +33,26 @@ pub const panic = std.debug.FullPanic(struct { }.halt); // std.debug.print (revo hits it on a few error paths) defaults to a threaded -// stderr IO that doesn't exist on freestanding and won't even compile. This +// stderr io that doesnt exist on freestanding and wont even compile. this // just points it to the custom one we wrote in bridge tthat uses the serial // console and shit pub const std_options_debug_io: std.Io = bridge.debug_io; -// Check `linker.ld` +// check `linker.ld` export var base_revision: limine.BaseRevision linksection(".limine_requests") = limine.BaseRevision.init(3); export var requests_start: limine.RequestsStartMarker linksection(".limine_requests_start") = .{}; export var requests_end: limine.RequestsEndMarker linksection(".limine_requests_end") = .{}; // the loader fills these before we run so we read them through volatile pointers or -// the optimizer assumes they're still the null we initialized them to +// the optimizer assumes theyre still the null we initialized them to export var hhdm_request: limine.HhdmRequest linksection(".limine_requests") = .{}; export var memmap_request: limine.MemoryMapRequest linksection(".limine_requests") = .{}; -// 4 MiB stack +// 4 mib stack export var stack_size_request: limine.StackSizeRequest linksection(".limine_requests") = .{ .stack_size = 4 * 1024 * 1024 }; -// wrappers matching bridge.KernelOps (pmm/vmm return optionals; the revo-facing -// primitives use 0 as the failure sentinel). +// wrappers matching bridge.kernelops (pmm/vmm return optionals, the revo facing +// primitives use 0 as the failure sentinel) fn kPhysToVirt(p: u64) u64 { return pmm.physToVirt(p); } @@ -62,96 +63,96 @@ fn kCreateAddrspace() u64 { return vmm.createAddressSpace() orelse 0; } fn kSerialNext() i64 { - return if (serial.mirrorNext()) |c| @intCast(c) else -1; + return if (serial.mirrorNext()) |byte| @intCast(byte) else -1; } -// ENTRY(_start) +// read only kernel state exposed to revo so it can present it (mem/uptime/ps and +// the memory map dump live in revo now) +var g_memmap: ?*const limine.MemoryMapResponse = null; +fn kMemmapCount() u64 { + const mm = g_memmap orelse return 0; + return mm.entry_count; +} +fn memmapEntry(i: u64) ?*const limine.MemoryMapEntry { + const mm = g_memmap orelse return null; + if (i >= mm.entry_count) return null; + return mm.entries.?[i]; +} +fn kMemmapBase(i: u64) u64 { + return if (memmapEntry(i)) |e| e.base else 0; +} +fn kMemmapLen(i: u64) u64 { + return if (memmapEntry(i)) |e| e.length else 0; +} +fn kMemmapKind(i: u64) u64 { + return if (memmapEntry(i)) |e| @intFromEnum(e.type) else 0; +} +fn kMemFree() u64 { + return pmm.freeBytes(); +} +fn kMemTotal() u64 { + return pmm.usableBytes(); +} +fn kUptime() u64 { + return timer.now(); +} +fn kProcCount() u64 { + return sched.procCount(); +} + +// entry(_start) export fn _start() callconv(.c) noreturn { serial.init(); serial.write("if you see this, it means revel didn't shit the bed\r\n"); gdt.load(); idt.init(); - user.installSyscall(); // int 0x80 gate at DPL 3 - // NOTE: CPU interrupts stay off until the event loop's sti - // which is after the VM has run that\ + user.installSyscall(); // int 0x80 gate at dpl 3 + // note: cpu interrupts stay off until the event loops sti + // which is after the vm has run that\ serial.write("gdt + idt loaded\r\n"); - // HHDM + memory map -> physical frame allocator -> kernel heap + // hhdm + memory map -> physical frame allocator -> kernel heap const hhdm_req: *volatile limine.HhdmRequest = &hhdm_request; const memmap_req: *volatile limine.MemoryMapRequest = &memmap_request; const hhdm = hhdm_req.response orelse @panic("limine gave us no HHDM"); const memmap = memmap_req.response orelse @panic("limine gave us no memory map"); pmm.init(hhdm, memmap); - pmm.dump(memmap); + g_memmap = memmap; // revo dumps it (dump_memmap) once the vm is up heap.init(); - // NOTE: this is a sanity check - // two allocations should be distinct and non-overlapping, - // and a freed frame should come straight back on the next alloc. - const a = pmm.alloc().?; - const b = pmm.alloc().?; - serial.write("pmm test: a="); - serial.writeHex(a); - serial.write(" b="); - serial.writeHex(b); - serial.write("\r\n"); - pmm.free(a); - const c = pmm.alloc().?; - serial.write("pmm test: freed a, realloc="); - serial.writeHex(c); - serial.write(if (c == a) " (reused, good)\r\n" else " (mismatch!)\r\n"); - pmm.free(b); - pmm.free(c); - - // this is just a VMM smoke test - // we map a fresh frame at an unused higher half address in the - // active address space, write a pattern, read it back through the mapping - { - const pml4 = vmm.activePml4(); - const frame = pmm.alloc().?; - const test_virt: u64 = 0xffff_c000_0000_0000; - _ = vmm.map(pml4, test_virt, frame, vmm.WRITE); - const cell: *volatile u64 = @ptrFromInt(test_virt); - cell.* = 0xDEADBEEFCAFEBABE; - serial.write("vmm test: map+rw "); - serial.write(if (cell.* == 0xDEADBEEFCAFEBABE) "OK" else "FAIL"); - serial.write(", translate "); - const back = vmm.translate(pml4, test_virt) orelse 0; - serial.write(if (back == frame) "OK\r\n" else "FAIL\r\n"); - - // create a separate address space, switch into it (the scary part -- a - // bad CR3 triple-faults), map a lower-half user page, read/write it, - // then switch back. if the kernel keeps running, address spaces work. - const as = vmm.createAddressSpace().?; - const user_frame = pmm.alloc().?; - const user_virt: u64 = 0x0000_0000_4000_0000; // 1 GiB, lower half (user) - _ = vmm.map(as, user_virt, user_frame, vmm.WRITE | vmm.USER); - vmm.loadPml4(as); - serial.write("vmm test: switched CR3 OK\r\n"); // reached => kernel still mapped - const ucell: *volatile u64 = @ptrFromInt(user_virt); - ucell.* = 0x1234_5678_9ABC_DEF0; - serial.write("vmm test: user page rw "); - serial.write(if (ucell.* == 0x123456789ABCDEF0) "OK\r\n" else "FAIL\r\n"); - vmm.loadPml4(pml4); // back to the original address space - serial.write("vmm test: switched back OK\r\n"); - } + pmmSelfTest(); + vmmSelfTest(); const elf_pages = (elf.hello_elf.len + 4095) / 4096; const elf_phys = pmm.allocContig(elf_pages) orelse @panic("no room for the embedded ELF"); @memcpy(@as([*]u8, @ptrFromInt(pmm.physToVirt(elf_phys)))[0..elf.hello_elf.len], elf.hello_elf); + // the event loop below is scheduler task 0 (the kernel), running in the + // current cr3. user tasks spawned by `run` get time fucked against it + sched.init(vmm.activePml4()); + const kops: bridge.KernelOps = .{ .phys_to_virt = &kPhysToVirt, .alloc_frame = &kAllocFrame, .create_addrspace = &kCreateAddrspace, - .run_process = &user.runProcess, + .spawn = &sched.spawn, + .deliver_input = &sched.deliverInput, + .proc_exited = &sched.takeProcExited, .serial_next = &kSerialNext, + .memmap_count = &kMemmapCount, + .memmap_base = &kMemmapBase, + .memmap_len = &kMemmapLen, + .memmap_kind = &kMemmapKind, + .mem_free = &kMemFree, + .mem_total = &kMemTotal, + .uptime = &kUptime, + .proc_count = &kProcCount, .elf_phys = elf_phys, .elf_size = elf.hello_elf.len, }; - // hand the kernel heap + framebuffer + low-level ops to revo and bring up the VM + // hand the kernel heap + framebuffer + low level ops to revo and bring up the vm const fb: ?bridge.Fb = if (framebuffer.get()) |s| .{ .ptr = s.address, .width = s.width, @@ -160,13 +161,62 @@ export fn _start() callconv(.c) noreturn { } else null; bridge.boot(heap.allocator(), serial.write, fb, kops); - // - sleep until an interrupt - // - drain the keyboard - // - repeat. - // - // the cli/pop/sti-hlt bullshit closes the lost-wakeup race (a key - // arriving between "queue empty" and hlt would otherwise sit until - // the next keypress) + eventLoop(); +} + +// sanity check: two allocations should be distinct and non overlapping, and a +// freed frame should come straight back on the next alloc +fn pmmSelfTest() void { + const first = pmm.alloc().?; + const second = pmm.alloc().?; + serial.write("pmm test: a="); + serial.writeHex(first); + serial.write(" b="); + serial.writeHex(second); + serial.write("\r\n"); + pmm.free(first); + const refreed = pmm.alloc().?; + serial.write("pmm test: freed a, realloc="); + serial.writeHex(refreed); + serial.write(if (refreed == first) " (reused, good)\r\n" else " (mismatch!)\r\n"); + pmm.free(second); + pmm.free(refreed); +} + +// map a fresh frame at an unused higher half address, write a pattern, read it +// back through the mapping, then create a separate address space, switch into it +// (a bad cr3 triple faults), map + rw a lower half user page, and switch back +fn vmmSelfTest() void { + const pml4 = vmm.activePml4(); + const frame = pmm.alloc().?; + const test_virt: u64 = 0xffff_c000_0000_0000; + _ = vmm.map(pml4, test_virt, frame, vmm.WRITE); + const cell: *volatile u64 = @ptrFromInt(test_virt); + cell.* = 0xDEADBEEFCAFEBABE; + serial.write("vmm test: map+rw "); + serial.write(if (cell.* == 0xDEADBEEFCAFEBABE) "OK" else "FAIL"); + serial.write(", translate "); + const back = vmm.translate(pml4, test_virt) orelse 0; + serial.write(if (back == frame) "OK\r\n" else "FAIL\r\n"); + + const as = vmm.createAddressSpace().?; + const user_frame = pmm.alloc().?; + const user_virt: u64 = 0x0000_0000_4000_0000; // 1 gib, lower half (user) + _ = vmm.map(as, user_virt, user_frame, vmm.WRITE | vmm.USER); + vmm.loadPml4(as); + serial.write("vmm test: switched CR3 OK\r\n"); // reached => kernel still mapped + const ucell: *volatile u64 = @ptrFromInt(user_virt); + ucell.* = 0x1234_5678_9ABC_DEF0; + serial.write("vmm test: user page rw "); + serial.write(if (ucell.* == 0x123456789ABCDEF0) "OK\r\n" else "FAIL\r\n"); + vmm.loadPml4(pml4); // back to the original address space + serial.write("vmm test: switched back OK\r\n"); +} + +// sleep until an interrupt, then drain the keyboard / tick and repeat. this is +// scheduler task 0. the cli/pop/sti hlt dance closes the lost wakeup race (a key +// arriving between "queue empty" and hlt would otherwise sit until the next one) +fn eventLoop() noreturn { var last_ticks: u64 = 0; while (true) { asm volatile ("cli"); |
