rust-kernel
Rustbelt: a hobby x86_64 kernel written in Rust
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 00:52:14 -0700 |
|---|---|---|
| commit | 20756a688a45abfec809ec6ee710c35307350834 | |
| parent | 176e770e4a2ec63a4e05a273a139907cc241fb0d | |
| tree | browse files at this commit | |
Stage 6: PCI, virtio-blk, partitions, FAT32, spawn/wait and shell syscalls - PCI bus scan (printed at boot); BlockDevice trait + device registry - legacy virtio-blk driver (polled virtqueue, bounce buffer); RamDisk for tests - MBR + GPT parsing, partitions exposed as block devices (vda1...) - FAT32 via fatfs (git, no_std+alloc) behind a BlockIo adapter; mounted at /mnt/disk0; build.rs builds target/disk.img (sfdisk + mkfs.vfat + mtools) with text files and the user program ELFs; programs now load from disk - syscalls: stat, fstat, lseek, pipe, sleep_ms, wait, getcwd, chdir, mkdir, rmdir, unlink, getdents64, spawn(path, argv, stdio, trace) - real blocking: blocked threads, wake_at, WaitQueue; console read, pipes and wait block instead of spinning - 64 KiB thread stacks with a canary, finished stacks freed, 4 MiB heap, allocator takes its lock with interrupts off - demo + child user programs; tests for block, partitions, FAT, spawn/wait - README: disks section, syscall table, spawn vs fork/exec, shell design
| M | Cargo.lock | +16 -0 |
| M | Cargo.toml | +13 -0 |
| M | README.md | +329 -109 |
| M | boot.png | +0 -0 |
| M | build.rs | +122 -22 |
| A | disk/notes/todo.txt | +2 -0 |
| A | disk/readme.txt | +2 -0 |
| M | scripts/run-headless.sh | +1 -0 |
| M | scripts/screenshot.sh | +1 -0 |
| M | src/allocator.rs | +1 -1 |
| M | src/allocator/bump.rs | +14 -1 |
| M | src/allocator/fixed_size_block.rs | +14 -1 |
| A | src/block/mod.rs | +127 -0 |
| A | src/block/partition.rs | +158 -0 |
| A | src/block/virtio_blk.rs | +281 -0 |
| M | src/errno.rs | +11 -0 |
| A | src/fs/fat.rs | +360 -0 |
| M | src/fs/file.rs | +99 -8 |
| M | src/fs/mod.rs | +144 -31 |
| A | src/fs/pipe.rs | +102 -0 |
| M | src/fs/ramfs.rs | +23 -3 |
| M | src/lib.rs | +22 -8 |
| M | src/main.rs | +31 -12 |
| M | src/memory.rs | +22 -0 |
| A | src/pci.rs | +121 -0 |
| M | src/process.rs | +174 -52 |
| M | src/syscall.rs | +226 -21 |
| M | src/thread.rs | +179 -24 |
| M | src/tty.rs | +26 -17 |
| A | tests/block.rs | +133 -0 |
| A | tests/fat.rs | +131 -0 |
| M | tests/user_mode.rs | +115 -23 |
| M | tests/vfs.rs | +81 -12 |
| A | user/child.rs | +41 -0 |
| A | user/demo.rs | +152 -0 |
| M | user/ulib.rs | +167 -2 |
36 files changed, 3441 insertions, 347 deletions
@@ -63,6 +63,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]]+name = "fatfs"+version = "0.4.0"+source = "git+https://github.com/rafalh/rust-fatfs?rev=38b3a7950bf0e765e34286d1dfe76c519ee347e2#38b3a7950bf0e765e34286d1dfe76c519ee347e2"+dependencies = [+ "bitflags 2.13.2",+ "log",+]++[[package]] name = "futures-core" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -114,6 +123,12 @@ dependencies = [ ] [[package]]+name = "log"+version = "0.4.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"++[[package]] name = "pc-keyboard" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -150,6 +165,7 @@ dependencies = [ "bootloader", "conquer-once", "crossbeam-queue",+ "fatfs", "futures-util", "lazy_static", "linked_list_allocator",
@@ -23,6 +23,11 @@ crossbeam-queue = { version = "0.3.11", default-features = false, features = ["a conquer-once = { version = "0.4", default-features = false } # OnceCell: init a static at runtime, once futures-util = { version = "0.3.4", default-features = false, features = ["alloc"] } # Stream, AtomicWaker +# FAT12/16/32 file system library. The crates.io release (0.3.6) needs the+# abandoned `core_io` crate for no_std, so we use the git version, pinned to+# an exact commit so builds are reproducible. Only `alloc` + long file names.+fatfs = { git = "https://github.com/rafalh/rust-fatfs", rev = "38b3a7950bf0e765e34286d1dfe76c519ee347e2", default-features = false, features = ["alloc", "lfn"] }+ [dependencies.lazy_static] version = "1.0" features = ["spin_no_std"]@@ -64,11 +69,19 @@ harness = false # Extra QEMU arguments used by `cargo run` (via `bootimage runner`). [package.metadata.bootimage] run-args = [+ # Second disk: the FAT32 image build.rs makes (target/disk.img), as a+ # virtio-blk PCI device. snapshot=on: writes go to a temporary overlay,+ # so the image is the same at every boot (and tests can't corrupt it).+ "-drive", "file=target/disk.img,format=raw,if=virtio,snapshot=on", "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", # lets the kernel exit QEMU "-serial", "stdio", # serial port -> your terminal ] # Used by `cargo test`: headless, results on serial, exit code 33 = pass. test-args = [+ # Second disk: the FAT32 image build.rs makes (target/disk.img), as a+ # virtio-blk PCI device. snapshot=on: writes go to a temporary overlay,+ # so the image is the same at every boot (and tests can't corrupt it).+ "-drive", "file=target/disk.img,format=raw,if=virtio,snapshot=on", "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio", "-display", "none",
@@ -12,14 +12,18 @@ keyboard is one of them) and **preemptive kernel threads** switched by the timer interrupt (threads A/B/C count on the top line while you type). It runs **user programs in ring 3** (small Rust ELF binaries) as processes with their own address spaces, which talk to the kernel via `syscall`-(`read`, `write`, `open`, `close`, `getpid`, `sched_yield`, `exit`) and read-files from an in-memory filesystem behind a Linux-style **VFS**.+(about 20 calls, numbered like Linux: `read`, `write`, `open`, `stat`, `lseek`,+`getdents64`, `chdir`, `pipe`, plus our own `spawn` and `wait`...). Files live+behind a Linux-style **VFS**: an in-memory ramfs at `/` and `/tmp`, and a+**FAT32** partition on a second **virtio-blk** disk (found by **PCI**+enumeration, **GPT/MBR** partitions parsed) mounted at `/mnt/disk0`. The user+programs themselves are loaded from that disk. It has a `cargo test` suite that runs inside QEMU. Most code follows the blog_os sources (MIT / Apache-2.0) closely, with extra comments; additions are the serial mirroring, uptime clock, demo features, scripts, and everything past chapter 12: preemptive threads, user mode,-syscalls, ELF loading and the VFS.+syscalls, ELF loading, the VFS, PCI, virtio-blk, partitions and FAT32.  @@ -33,6 +37,7 @@ syscalls, ELF loading and the VFS. | `llvm-tools-preview` component | – | `llvm-objcopy` etc., used by `bootimage` | | `bootimage` (cargo subcommand) | 0.10.5 | links kernel + bootloader into a bootable disk image | | QEMU (`qemu-system-x86`, `qemu-system-gui`) | 10.0.13 | the emulator we boot in (`-gui` adds the GTK/SDL window) |+| `dosfstools`, `mtools`, `fdisk` | Debian packages | `build.rs` makes the FAT32 test disk with `sfdisk` + `mkfs.vfat` + `mmd`/`mcopy` | `rust-toolchain.toml` pins the project to nightly with the two components, so `cargo` inside this directory picks them up automatically. It also installs@@ -41,7 +46,8 @@ the built-in `x86_64-unknown-none` target, used to compile the user programs. Crates: `bootloader` 0.9.35 (BIOS bootloader used by the 2nd edition), `volatile` 0.2, `spin` 0.5, `x86_64` 0.15, `uart_16550` 0.3, `lazy_static` 1.5, `pic8259` 0.11 (interrupt controller), `pc-keyboard` 0.7 (scancode decoding),-`linked_list_allocator` 0.9 (fallback heap allocator).+`linked_list_allocator` 0.9 (fallback heap allocator), `fatfs` (git, `no_std` ++`alloc`; FAT12/16/32 reading and writing). Fresh setup on another machine: @@ -49,7 +55,7 @@ Fresh setup on another machine: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # rustup rustup toolchain install nightly -c rust-src -c llvm-tools-preview -t x86_64-unknown-none cargo install bootimage-sudo apt install qemu-system-x86 qemu-system-gui+sudo apt install qemu-system-x86 qemu-system-gui dosfstools mtools fdisk ``` ## Build and run@@ -93,75 +99,90 @@ Cargo features (in `Cargo.toml`): | `stack-overflow` | after boot, recurse forever to test the double-fault handler (with `headless`: exit success when caught) | | `page-fault` | after reading CR3, write to unmapped address `0xdeadbeaf` (with `headless`: exit success when caught) | -Expected serial output of `./scripts/run-headless.sh` (stack frame dump trimmed):+Expected serial output of `./scripts/run-headless.sh` (stack frame dump and+some timer dots / thread lines trimmed): ``` Hello from Tyler's Rust kernel! EXCEPTION: BREAKPOINT-InterruptStackFrame { instruction_pointer: ..., ... }-Survived the breakpoint, still running.-[paging] CR3 (level 4 table) at phys 0x1000-... (paging + memory lines as before)-[paging] mapped page 0xdeadbeaf000 -> VGA frame, wrote "New!" through it-.[heap] 256 KiB heap at 0x444444440000 (fixed-size-block allocator)-[heap] Box::new(41) = 41 at 0x444444440000-[heap] Vec of 500 numbers at 0x444444441000, sum = 124750-[heap] Rc count after clone = 2-[heap] Rc count after drop = 1+... (breakpoint, paging and memory lines as before)+[heap] 4096 KiB heap at 0x444444440000 (fixed-size-block allocator)+... (heap demo lines) [thread] spawned threads A, B, C (status on the top line)-.[thread A] count 1-.[thread B] count 1-..[vfs] mounted ramfs at /+[pci] 00:00.0 8086:1237 host bridge+[pci] 00:01.0 8086:7000 ISA bridge+[pci] 00:01.1 8086:7010 IDE controller+[pci] 00:01.3 8086:7113 bridge (other)+[pci] 00:02.0 1234:1111 display controller+[pci] 00:03.0 8086:100e network controller+[pci] 00:04.0 1af4:1001 SCSI storage (virtio-blk)+[pci] 7 devices on the PCI bus+[disk] vda: virtio-blk, 131072 sectors (64 MiB)+[disk] vda: GPT partition table, 1 partition(s)+[disk] vda1: start 2048 sectors 126976 type Microsoft basic data (FAT/exFAT/NTFS) name "fat32"+[disk] vda1: FAT32 volume "TYLERDISK" mounted at /mnt/disk0+[vfs] mounted ramfs at / [vfs] mounted ramfs at /tmp+[vfs] mounted fat32 at /mnt/disk0 [process] started pid 1 (hello), entry 0x8000400180-[process] started pid 2 (evil), entry 0x80004000f0-[process] started pid 3 (spin), entry 0x80004000e0-[process] started pid 4 (echo), entry 0x8000400180-Type a line + Enter: the echo program (ring 3) reads it-[async] async number: 42-[async] example task step 1 (after yield)-[async] example task step 2 (after yield)-[async] example task step 3 (after yield)-....Hello from user mode! I'm pid 1.+Hello from user mode! I'm pid 1. [hello] /hello.txt says: Hello from the ramfs! This is /hello.txt. [hello] wrote and read back /tmp/note.txt [hello] all syscall checks passed, exiting with 0 [process] pid 1 (hello) exited with code 0+[process] started pid 2 (evil), entry 0x80004000f0+[process] started pid 3 (spin), entry 0x80004000e0 [evil] pid 2: reading kernel memory at 0x200000... [kernel] user page fault: address Ok(VirtAddr(0x200000)), error PageFaultErrorCode(PROTECTION_VIOLATION | USER_MODE) [kernel] page fault in user mode at rip 0x800040017e [process] pid 2 (evil) killed: page fault [spin] pid 3: spinning without syscalls...-.[echo] pid 4: type a line and press Enter-..[thread A] count 2-.[thread B] count 2-.....[thread A] count 3-. 18 ticks-.....[thread A] count 4-.[thread B] count 3-.[thread C] busy loop round 1 (count = 2000000)-.[spin] round 1 done (count = 30000000)-...[thread A] count 5-....... 36 ticks-[thread B] count 4-..[spin] round 2 done (count = 60000000)-..........[thread B] count 5-.[thread C] busy loop round 2 (count = 4000000)-.[spin] round 3 done (count = 90000000)+[process] started pid 4 (demo), entry 0x8000400320+[process] started pid 5 (echo), entry 0x8000400180+Type a line + Enter: the echo program (ring 3) reads it+[async] async number: 42+...+[demo] pid 4 started from /mnt/disk0/bin/demo+[demo] readme.txt from FAT32: Hello from the FAT32 disk! This file lives on a virtio-blk disk, in a GPT+[demo] /mnt/disk0/bin: hello evil spin echo demo child+[echo] pid 5: type a line and press Enter+[spin] round 1 done (count = 30000000)+[demo] mkdir/unlink/rmdir work on ramfs and FAT32+[process] started pid 6 (child), entry 0x8000400240+[process] pid 6 (child) exited with code 0+[demo] read from pipe: "hello through a pipe from pid 6"+[process] started pid 7 (child), entry 0x8000400240+[child] pid 7 got 3 args: "child" "with" "three args"+[process] pid 7 (child) exited with code 0+[process] started pid 8 (child), entry 0x8000400240+[process] pid 8 (child) exited with code 42+[spin] round 3 done (count = 90000000) [process] pid 3 (spin) exited with code 0-.... 54 ticks-.................. 72 ticks-..[thread C] busy loop round 3 (count = 6000000)-...............[thread C] busy loop round 4 (count = 8000000)-. 90 ticks+[demo] spawned child 42 (pid 8), wait -> exit code 42+[process] started pid 9 (evil), entry 0x80004000f0+[evil] pid 9: reading kernel memory at 0x200000...+...+[process] pid 9 (evil) killed: page fault+[demo] all stage 6 checks passed, exiting with 0+[process] pid 4 (demo) exited with code 0+[thread C] done, exiting+.................. 90 ticks [ok] timer works, exiting+[run-headless] kernel exited OK ``` Each `.` is a timer tick. Threads A and B only print their first 5 counts to serial (so the keyboard echo in your terminal stays readable); after that they keep counting on the VGA top line. (Exact interleaving and dot counts vary a little from run to run -- that's preemption at work.) Spin's rounds interleave-with threads A/B/C: that's preemption of ring-3 code by the timer.+with threads A/B/C and the demo: that's preemption of ring-3 code by the timer.+The `[pci]` lines only go to serial (the VGA screen gets the count).++The kernel boots from the bootimage on the first (IDE) disk; the scripts and+`Cargo.toml` attach a **second** disk, `target/disk.img`, as+`-drive file=target/disk.img,format=raw,if=virtio,snapshot=on`. `snapshot=on`+makes QEMU throw away writes when it exits, so every boot and every test sees+the same fresh image even though the kernel writes to it. ## Tests @@ -192,8 +213,10 @@ heap_allocation::simple_allocation... [ok] | `tests/should_panic.rs` | a failing `assert_eq!` reaches the panic handler (`harness = false`) | | `tests/stack_overflow.rs` | stack overflow lands in the double-fault handler on the IST stack (`harness = false`) | | `tests/executor.rs` | `SimpleExecutor` runs an async fn to completion; the waker-based `Executor` interleaves two tasks that `yield_now().await` (a1 b1 a2 b2 a3 b3) |-| `tests/user_mode.rs` | `hello` exits 0 (all its syscall checks pass) and its `/tmp/note.txt` is visible to the kernel; `evil` is `Killed("page fault")`; `spin` finishes while the test thread keeps running (preemption of ring 3); the ELF loader rejects garbage/truncated files; user pointers to kernel/unmapped/overflowing ranges give `EFAULT` |-| `tests/vfs.rs` | read `/hello.txt` to EOF, path normalisation, `ENOENT`/`ENOTDIR`/`EINVAL`/`EISDIR`/`EEXIST`, `/tmp` is a separate ramfs, a third mount wins by longest prefix, sparse writes zero-fill, directory listing |+| `tests/user_mode.rs` | programs loaded from the FAT disk: `hello` exits 0, `demo` passes all its checks (exit 0), `evil` is killed with code 139, `spin` is preempted; spawn with arguments + trace + exit code 7, then reaped; a child's stdout into a pipe; spawn errors (`ENOENT`, `ENOEXEC`, `EISDIR`, `ECHILD`); ELF loader rejects garbage; user pointers give `EFAULT` (length 0 is fine); `sleep` blocks for the right number of ticks; a `WaitQueue` wakes its waiter |+| `tests/vfs.rs` | read `/hello.txt` to EOF, path normalisation (`.`, `..`, relative to a cwd), `ENOENT`/`ENOTDIR`/`EINVAL`/`EISDIR`/`EEXIST`, `/tmp` is a separate ramfs, longest-prefix mounts, sparse writes zero-fill, listing, remove (`ENOTEMPTY`, `EBUSY` on mount points), seek/stat/truncate, `getdents64` record layout, pipes pass bytes and report EOF / `EPIPE` |+| `tests/block.rs` | PCI scan finds the virtio disk; sector 1 of the image is the GPT header; multi-sector reads equal single-sector reads; write + read back; out-of-range and partial-sector requests fail; the GPT partition is registered as `vda1` (with a FAT32 boot sector); MBR parsing on a RAM disk |+| `tests/fat.rs` | the FAT32 volume is mounted at `/mnt/disk0` with the expected files; read `readme.txt`; every program in `bin/` is a valid ELF; create/write/read/truncate/remove a 10 KB file; directories with long names, `ENOTEMPTY`; the `BlockIo` adapter handles unaligned reads and writes | | `tests/threads.rs` | two busy-looping threads that never yield both make progress while the main thread busy-waits too (only possible with preemption); a thread can exit and the others keep running | How it works: `#![feature(custom_test_frameworks)]` collects every `#[test_case]`@@ -230,18 +253,27 @@ src/task/executor.rs real executor: task queue + wakers, sleeps with hlt wh src/task/keyboard.rs lock-free scancode queue, ScancodeStream, async keyboard echo task src/thread.rs preemptive threads: spawn, round-robin schedule(), asm context switch, per-thread kernel stack (TSS.rsp0) + CR3 switching-src/syscall.rs syscall/sysret MSR setup, asm entry stub, dispatch, read/write/open/close/...-src/process.rs Process (pid, fd table, exit status), spawn from ELF, iretq into ring 3, kill+src/syscall.rs syscall/sysret MSR setup, asm entry stub, dispatch + all syscalls (table at the top)+src/process.rs Process (pid, parent, cwd, args, fd table, exit status), spawn from a path or+ ELF bytes, iretq into ring 3, exit/kill, wait (reaping)+src/pci.rs PCI bus scan through config ports 0xCF8/0xCFC, device names+src/block/mod.rs BlockDevice trait, device registry (vda, vda1...), RamDisk (for tests)+src/block/virtio_blk.rs legacy virtio-blk driver: one virtqueue, polled, bounce buffer+src/block/partition.rs MBR + GPT parsing, Partition = a BlockDevice window onto its disk src/elf.rs minimal ELF64 loader (PT_LOAD segments -> user pages) src/errno.rs Linux errno numbers src/tty.rs keyboard line buffer + Console (the File behind fds 0/1/2)-src/fs/mod.rs VFS: Inode + FileSystem traits, mount table, path lookup, init (boot files)-src/fs/file.rs File trait (what an fd points to), InodeFile (inode + offset)+src/fs/mod.rs VFS: Inode + FileSystem traits, mount table, path normalize/lookup/create/remove,+ init (boot files), mount_disks (FAT partitions -> /mnt/diskN)+src/fs/file.rs File trait (what an fd points to), InodeFile (inode + offset), Stat, getdents64 src/fs/ramfs.rs in-memory filesystem-build.rs compiles user/*.rs to ELF files in OUT_DIR+src/fs/fat.rs FAT32 via the fatfs crate: BlockIo adapter + FatFs/FatInode for the VFS+src/fs/pipe.rs pipes: a byte queue with a blocking read end and a write end+build.rs compiles user/*.rs to ELF files, then builds target/disk.img (GPT + FAT32)+disk/ files copied onto the FAT32 disk (readme.txt, notes/todo.txt) user/ulib.rs tiny user-space "libc": syscall wrappers, println!, _start, panic handler user/link.ld linker script for user programs (base 0x80_0040_0000)-user/{hello,evil,spin,echo}.rs the user programs+user/{hello,evil,spin,echo,demo,child}.rs the user programs (copied to /mnt/disk0/bin) src/vga_buffer.rs VGA text-mode writer, print!/println!, print_at (status line), backspace src/serial.rs COM1 UART writer + serial_print!/serial_println! tests/ integration tests, one QEMU-booted kernel each@@ -297,7 +329,7 @@ physical RAM is also mapped at `physical_memory_offset` (`0x18000000000` here), | `0x200000`... | kernel code + data (the ELF; maps to phys around `0x400000`) | | `0x0100_0000_0000`... | kernel stack (80 pages, guard page below it) | | `0x0180_0000_0000`... | all physical memory (`physical_memory_offset`) |-| `0x4444_4444_0000` | kernel heap, 256 KiB (also holds thread stacks) (we map it in `allocator::init_heap`) |+| `0x4444_4444_0000` | kernel heap, 4 MiB (also holds thread stacks) (we map it in `allocator::init_heap`) | | `0xdead_beaf_000` | demo page we map to the VGA frame (writes "New!" on screen) | The "New!" demo writes to screen row 20 through the new mapping; by the time you look,@@ -307,7 +339,7 @@ later lines have scrolled it up, so in `boot.png` it overwrote the start of the the BIOS memory map marks `Usable` (~121 MiB of QEMU's default 128 MiB). It never frees; fine for now. -**Heap.** `allocator::init_heap` maps the 25 pages of `HEAP_START..+HEAP_SIZE` to fresh+**Heap.** `allocator::init_heap` maps the 1024 pages (4 MiB) of `HEAP_START..+HEAP_SIZE` to fresh frames, then gives the range to the `#[global_allocator]`. From then on the `alloc` crate (`Box`, `Vec`, `Rc`, `String`, `BTreeMap`...) works. `alloc` is compiled for our target by `build-std` in `.cargo/config.toml`.@@ -346,7 +378,7 @@ cheap (just return from `poll`), and all tasks share one stack. ### Preemptive: kernel threads (`src/thread.rs`, beyond the blog series) -Each thread has its own 16 KiB stack (a `Vec<u8>` on the heap). On every timer+Each thread has its own 64 KiB stack (a `Vec<u8>` on the heap). On every timer interrupt (~18 times a second) the handler sends the EOI to the PIC and then calls `thread::schedule()`, which picks the next thread round-robin and calls `switch_context(&mut old.rsp, new.rsp)` (a dozen lines of `global_asm!`):@@ -382,19 +414,50 @@ taken with interrupts disabled. Here: the print macros and `print_at` already hold their spinlocks with interrupts off (`without_interrupts`), so a thread can't be switched away while holding them; the scheduler lock is only taken with interrupts off and is released *before* switching (the next thread would-otherwise wait forever for a lock held by a paused thread). The heap allocator's-lock is never taken in an interrupt handler, so a thread preempted while-holding it only makes others spin until it runs again -- slow, not a deadlock.--Known simplifications (good exercises): sleeping threads (`sleep_ticks`) just-`hlt` and still get scheduled -- a real scheduler has a *blocked* state and a-sleep queue; finished threads' stacks are never freed; no guard page under-thread stacks (an overflow silently corrupts the heap); no priorities; a single-CPU, so `spin::Mutex` is enough.+otherwise wait forever for a lock held by a paused thread).++**The locking rule:** a lock that is *ever* taken with interrupts off must+*always* be taken with interrupts off. Otherwise: thread T takes the lock with+interrupts on, gets preempted, and some code running with interrupts off (so+it can never be preempted back to T) spins on that lock forever. Stage 6 hit+exactly this with the heap allocator: wait-queue code runs with interrupts off+and allocates, so `GlobalAlloc::alloc`/`dealloc` now take the heap lock inside+`without_interrupts`.++**Blocking (stage 6).** A thread can be *blocked*: the scheduler skips it until+someone wakes it. Two ways to be woken:++- `sleep_ticks(n)` sets `wake_at = now + n`; `schedule()` (run on every timer+ tick) unblocks it once that tick has passed (the `sleep_ms` syscall rounds+ up to 55 ms ticks).+- A `WaitQueue` (a list of waiting thread ids). `wait_until(|| condition)`+ turns interrupts off, checks the condition, and if it's false puts the+ thread on the queue, marks it blocked and switches away; when woken it checks+ again. `wake_all()` unblocks everyone on the queue. Checking with interrupts+ off is what prevents a *lost wakeup* (the event firing between "check" and+ "sleep"). Users: the console (`read(0)` waits for Enter; the keyboard path+ wakes it), pipes (reader waits for data or EOF) and `wait(pid)` (parent+ waits on the child's `exited` queue).++If every thread is blocked, the scheduler idles with `sti; hlt` until an+interrupt makes something runnable.++**Stack size and the canary.** Stage 6 first used 16 KiB stacks and crashed+with a ring-0 jump to address 0: the syscall -> VFS -> FAT -> ELF loader path in+an unoptimized debug build used more than that, overflowed into the heap and+smashed a neighbour. Now stacks are 64 KiB, and the lowest 8 bytes of each+hold a canary value (`0x5AFE_57AC_C0DE_CAFE`). `schedule()` checks it on every+switch and panics with "kernel stack overflow in thread" if it changed: a+cheap stand-in for a real guard page. Finished threads' stacks are freed (the+next `spawn` drops them, since a thread can't free the stack it's running on).++Known simplifications (good exercises): no guard page under thread stacks (the+canary only catches overflows after the fact); no priorities; a single CPU,+so `spin::Mutex` is enough. ## User mode, syscalls and processes -Four tiny Rust programs in `user/` run in **ring 3** as separate processes, each+Six tiny Rust programs in `user/` run in **ring 3** as separate processes, each with its own address space and file-descriptor table: | Program | What it does |@@ -402,7 +465,12 @@ with its own address space and file-descriptor table: | `hello` | prints "Hello from user mode!", `getpid`, reads `/hello.txt` from the ramfs, creates and reads back `/tmp/note.txt`, checks error cases (`ENOENT`, `EBADF`, `EFAULT`, `ENOSYS`), exits with the number of failed checks (0) | | `evil` | reads kernel memory at `0x200000` -> page fault in ring 3 -> the kernel kills it and keeps going | | `spin` | busy-loops ~2 s with no syscalls; it still gets preempted (threads A/B/C keep printing) |-| `echo` | `read(0)` a line from the keyboard, prints `[echo] you typed: ...`; type `exit` to end it |+| `echo` | `read(0)` a line from the keyboard (the thread is blocked until Enter), prints `[echo] you typed: ...`; type `exit` to end it |+| `demo` | stage 6 checks: cwd, reads `/mnt/disk0/readme.txt`, `stat`/`fstat`/`lseek`, lists `bin/` with `getdents64`, `chdir` errors, `mkdir`/`unlink`/`rmdir` on ramfs and FAT, a pipe from a child, arguments, `wait` exit codes (42, 139 for killed `evil`), `ECHILD`/`ENOENT`/`ENOEXEC`, `sleep_ms`; exits with the number of failed checks |+| `child` | helper for `demo` and tests: `child pipe` writes a line to stdout, `child cat` copies stdin to stdout, `child 42` exits with 42, otherwise prints its arguments |++The kernel starts them all from `/mnt/disk0/bin/` -- they're read from the FAT32+partition on the virtio disk, not compiled into the kernel any more. ### Rings in one paragraph @@ -460,15 +528,54 @@ One call, step by step: Single CPU, so the "current kernel stack" and "saved user RSP" are plain globals; SMP kernels use per-CPU data reached with `swapgs`. +Numbers are Linux's wherever Linux has the same call, so man pages and+`strace` habits mostly apply. Args 4 and 5 go in `r10` and `r8` like Linux.+ | nr | name | args | notes | |---|---|---|---|-| 0 | `read` | fd, buf, len | fd 0 = keyboard (blocks until Enter), others = files |-| 1 | `write` | fd, buf, len | fd 1/2 = console (VGA + serial) |-| 2 | `open` | path, path_len, flags | **differs from Linux**: path is ptr+len, not a C string; `O_CREAT` = 0o100 |+| 0 | `read` | fd, buf, len | console: blocks until Enter; pipe: blocks until data or EOF |+| 1 | `write` | fd, buf, len | console = VGA + serial; pipe: `EPIPE` once the reader is gone |+| 2 | `open` | path, path_len, flags | `O_CREAT` 0o100, `O_TRUNC` 0o1000; directories can be opened (for `getdents64`) | | 3 | `close` | fd | |+| 4 | `stat` | path, path_len, *Stat | |+| 5 | `fstat` | fd, *Stat | |+| 8 | `lseek` | fd, offset, whence | `SEEK_SET/CUR/END` = 0/1/2; `ESPIPE` on console/pipes |+| 22 | `pipe` | *[u32; 2] | [read end, write end] | | 24 | `sched_yield` | | |+| 35 | `sleep_ms` | ms | **ours**: Linux nr 35 is `nanosleep(timespec)` | | 39 | `getpid` | | | | 60 | `exit` | code | |+| 61 | `wait` | pid | **simpler than Linux `wait4`**: blocks, returns the exit code (139 = killed, like a shell shows SIGSEGV), reaps the child; `ECHILD` if not your child |+| 79 | `getcwd` | buf, len | returns the length (no NUL); `ERANGE` if too small |+| 80 | `chdir` | path, path_len | per-process cwd, inherited by spawned children |+| 83 | `mkdir` | path, path_len | |+| 84 | `rmdir` | path, path_len | empty directories only (`ENOTEMPTY`); `EBUSY` on a mount point |+| 87 | `unlink` | path, path_len | files only (`EISDIR`) |+| 217 | `getdents64` | fd, buf, len | real Linux `struct linux_dirent64` records (ino, off, reclen, type, name\0) |+| 500 | `spawn` | path, path_len, argv, argv_len, *SpawnOptions | **ours**: start a program, returns its pid |++Deviations from Linux worth knowing:++- **Paths are pointer + length**, not NUL-terminated C strings (easier to+ validate, and Rust `&str`s aren't NUL-terminated). Relative paths start at+ the process's cwd; `.` and `..` are resolved in the kernel (`fs::normalize`).+- **`Stat` is tiny**: `#[repr(C)] struct Stat { kind: u64, size: u64 }`, kind+ 1 = file, 2 = directory, 3 = console, 4 = pipe. No inode numbers, owners,+ modes or times yet.+- **spawn instead of fork + exec.** `fork` copies the whole address space (or+ needs copy-on-write page tables to be fast) and `exec` immediately throws the+ copy away. `spawn` does the useful part in one step, like Windows+ `CreateProcess` or POSIX `posix_spawn`. What Unix shells do between fork and+ exec -- pointing fds 0/1/2 at a pipe or a file -- is spawn's `SpawnOptions`:+ `{ stdin, stdout, stderr: i64 (one of *my* fds, -1 = same as mine), flags }`.+ `flags = SPAWN_TRACE` (1) prints every syscall the child makes, as+ `[trace pid] name(a1, a2, a3) = result` -- that's what the shell's+ `run --trace` will use. fork + exec could still be added later.+- **argv** is passed as one buffer with NUL-separated arguments+ (`"child\0with\0three args"`; empty = just the program name). The kernel+ copies the strings onto the new process's stack and starts `_start` with+ `rdi = argc`, `rsi = pointer to argc [ptr, len] pairs` (Rust-friendly+ slices instead of C's `char **argv`). `ulib` wraps that as `argc()`/`arg(i)`. Errors are negative errno values like Linux (`-2` ENOENT, `-9` EBADF, `-14` EFAULT, `-38` ENOSYS...). Unknown numbers return `-ENOSYS`.@@ -521,8 +628,10 @@ and fails with "relocation overflow"), `-T user/link.ld` (one PT_LOAD segment per permission, page-aligned). The large code model names its sections `.ltext`/`.lrodata`/`.ldata`/`.lbss`, so the linker script lists those too -- forgetting that put the code into a non-executable segment and-every program died at its first instruction. The ELFs (~15-20 KB) are embedded in the-kernel with `include_bytes!` (`rust_kernel::user_programs`).+every program died at its first instruction. The ELFs (~15-20 KB) used to be+embedded in the kernel with `include_bytes!`; since stage 6 `build.rs` copies+them onto the FAT32 disk image instead and `process::spawn_path` reads them+through the VFS (see "Disks" below). `user/ulib.rs` is a tiny libc-equivalent included by each program: the `syscall` inline asm, wrappers, `print!/println!`, `_start` (calls `main`,@@ -539,19 +648,22 @@ physical-memory map and leaves the rest (`.bss`) zero. Designed like Linux's so real filesystems can plug in later: - `trait Inode` (`fs/mod.rs`): a file or directory: `kind`, `size`,- `read_at(offset, buf)`, `write_at`, `lookup(name)`, `create(name, kind)`, `list`.+ `read_at(offset, buf)`, `write_at`, `truncate`, `lookup(name)`,+ `create(name, kind)`, `list` (names + kinds), `remove(name)`. Methods have default impls returning an error, so a read-only fs implements only what it supports. - `trait FileSystem`: `name()` and `root() -> Arc<dyn Inode>`. A disk filesystem will hold its block device and parsed superblock. - **Mount table**: `mount("/tmp", fs)`; `lookup(path)` picks the longest mount point that prefixes the path, then walks the rest with `Inode::lookup`.- Boot mounts a ramfs on `/` (`/hello.txt`, `/etc/motd`, `/tmp`) and a- second, separate ramfs on `/tmp`.+ Boot mounts a ramfs on `/` (`/hello.txt`, `/etc/motd`, `/tmp`, `/mnt`), a+ second, separate ramfs on `/tmp`, and each FAT partition at `/mnt/diskN`.+ `fs::normalize(cwd, path)` turns any path into a clean absolute one first. - `trait File` (`fs/file.rs`): what a file descriptor points to: `read(buf)`,- `write(buf)`. `InodeFile` = an inode + its own offset (two opens of the same+ `write(buf)`, `seek`, `stat`, `getdents`. `InodeFile` = an inode + its own offset (two opens of the same file have separate offsets, like Linux's `struct file`). `tty::Console` is- another `File`: writes go to VGA+serial, reads come from the keyboard line buffer.+ another `File`: writes go to VGA+serial, reads come from the keyboard line+ buffer. `fs/pipe.rs` gives two more: a pipe's read and write ends. - **fd table**: `Vec<Option<Arc<dyn File>>>` per process (max 16), fds 0-2 = Console. `open` = `fs::lookup` (or `fs::create` with `O_CREAT`) + `InodeFile::open` + first free slot.@@ -561,16 +673,125 @@ Keyboard input for `read(0)` goes through a minimal tty line discipline current line, Enter makes the line (with `\n`) available to readers. Known simplifications (exercises): a process's page tables and frames are-never freed on exit; `read(0)` waits with `hlt` instead of blocking the-thread; no `fork`/`exec`/`wait` syscalls (the kernel spawns programs and-`process::wait` is kernel-only); no `lseek`, `stat`, `readdir` yet; user-pointer checks walk the page table but don't guard against another thread-unmapping the page mid-syscall (there's no `munmap` yet, so that's fine for now).+never freed on exit; orphans aren't reparented; no signals; user pointer+checks walk the page table but don't guard against another thread unmapping+the page mid-syscall (there's no `munmap` yet, so that's fine for now).++## Disks: PCI, virtio-blk, partitions, FAT32 (stage 6)++```+/mnt/disk0/readme.txt+ VFS (fs/mod.rs) longest mount prefix -> FatFs, rest "readme.txt"+ FatInode (fs/fat.rs) fatfs crate: clusters, FAT chains, directory entries+ BlockIo adapter byte offsets -> whole 512-byte sectors+ Partition vda1 sector + 2048, bounds-checked+ VirtioBlk vda virtqueue request to QEMU+ PCI found the device and its I/O port at boot+```++**PCI** (`src/pci.rs`). Every PCI device has a 256-byte configuration space+(vendor/device id, class code, BARs = where its registers live). On a PC you+read it by writing `bus/device/function/offset` to port `0xCF8` and reading+port `0xCFC`. `pci::scan()` tries every bus/device/function, keeps the ones+whose vendor id isn't `0xFFFF`, and prints them with a class name. QEMU's+default machine has 7: host bridge, ISA/IDE/ACPI parts of the PIIX chipset,+VGA, the e1000 network card, and our disk, `1af4:1001`.++**Why virtio-blk and not ATA PIO?** ATA PIO is simpler (a few port writes,+then 256 `in` instructions per sector) but it's a 1980s interface that only+emulators and old hardware have. virtio is what QEMU/KVM and cloud VMs+actually use, and it teaches the pattern every modern device uses: the driver+and device share **queues of descriptors in RAM**. The legacy (virtio 0.9.5)+interface is used, through I/O port BAR0, because it needs no MMIO mapping or+PCI capability parsing; QEMU's device is "transitional" and speaks both.++**virtio-blk in one paragraph** (`src/block/virtio_blk.rs`, ~280 lines).+Reset the device, set ACKNOWLEDGE + DRIVER, accept no optional features,+read the queue size, allocate the virtqueue (descriptor table + available+ring + used ring, physically contiguous: `memory::alloc_contiguous_frames`)+and give its page number to the device, set DRIVER_OK. A request is a chain+of three descriptors: a 16-byte header (`IN` = read or `OUT` = write, sector+number), the data buffer, and a 1-byte status the device fills in. Put the+chain's head in the available ring, write the queue number to the notify+port, then **poll** the used ring until the device hands it back. Data goes+through a one-page **bounce buffer** whose physical address we know (heap+buffers are virtually contiguous but not physically). Interrupts are switched+off for the device (`VRING_AVAIL_F_NO_INTERRUPT`); polling is fine because QEMU+completes requests almost instantly. Exercises: IRQ-driven completion (block+the thread on a wait queue), several requests in flight, virtio 1.0 "modern"+MMIO.++**`BlockDevice`** (`src/block/mod.rs`): `sector_count()`,+`read_sectors(lba, buf)`, `write_sectors(lba, buf)`, buffers a whole number of+512-byte sectors. Devices get Linux-style names in a registry+(`block::devices()`, `block::get("vda1")`). `RamDisk` is a `Vec<u8>` disk used+by tests.++**Partitions** (`src/block/partition.rs`). Sector 0 is the MBR: four 16-byte+entries at offset 446 and the `55 AA` signature. A single entry of type `0xEE`+is a "protective MBR" meaning *look for a GPT*: sector 1 has the `EFI PART`+header, which says where the partition entry array is (128 bytes each: type+GUID, unique GUID, first/last LBA, UTF-16 name). Each partition becomes a+`Partition` -- itself a `BlockDevice` that adds its start sector and refuses+to go past its end -- registered as `vda1`, `vda2`... CRCs aren't checked yet.++**FAT32 via the `fatfs` crate** (`src/fs/fat.rs`). `fatfs` does the+filesystem logic; it wants something implementing its `Read + Write + Seek`+traits on *bytes*. `BlockIo` is that adapter: it keeps a byte position, sends+whole aligned sectors straight to the device, and handles partial sectors+through a one-sector buffer (read the sector, change the bytes, write it back).+Writes go straight through, so `flush` has nothing to do. (`fatfs` 0.3 from+crates.io depends on the abandoned `core_io` crate, which doesn't build on+current nightlies, so `Cargo.toml` pins a git revision of the 0.4 branch with+its own `no_std` IO traits.) For the VFS, `FatFs` holds the `fatfs::FileSystem`+in an `Arc<Mutex<..>>`, and a `FatInode` is just *(that filesystem, a path, a+kind)*: each call reopens its path from the root. Slower than caching+directory entries, but simple and never stale. Lookups are case-insensitive,+like FAT itself. No timestamps (`NullTimeProvider`) and long file names on.++**The test disk** (`build.rs`). After compiling the user programs,+`build.rs` makes `target/disk.img` (64 MiB): `sfdisk` writes a GPT with one+"Microsoft basic data" partition at sector 2048, `mkfs.vfat -F 32 -s 1 -n+TYLERDISK` formats a separate partition image (1 sector per cluster so 62 MiB+is enough clusters for FAT32), `mmd`/`mcopy` (mtools, which edit FAT images+without mounting, so no root) copy in `disk/` and `bin/<programs>`, and the+partition image is spliced into the disk at sector 2048. If a tool is missing+the build stops with `sudo apt install dosfstools mtools fdisk`+(`mkfs.vfat` lives in `/usr/sbin`, which isn't in a normal user's `PATH` on+Debian, so `build.rs` looks there too). Look inside it from Linux:++```sh+/usr/sbin/sfdisk -l target/disk.img # not in a normal user's PATH on Debian+export MTOOLS_SKIP_CHECK=1 # mtools: don't complain about the small-cluster FAT32+mdir -i target/disk.img@@1M ::/bin # @@1M = partition offset (2048 * 512)+mtype -i target/disk.img@@1M ::/readme.txt+```++## Next stage: the shell (Tyler's design)++A **hybrid** shell: Unix navigation with a DOS flavour on top. All the+syscalls it needs exist now (`read(0)` blocks, `spawn` + `wait`, `pipe`,+`getdents64`, `stat`, `chdir`/`getcwd`, `mkdir`/`rmdir`/`unlink`, `lseek`,+`SPAWN_TRACE`).++- **Unix navigation**: `cd`, `ls`, `pwd`, `cat`, `mkdir`, `rm`, `echo`.+- **DOS-style program running**: `run prog args` (foreground: spawn + wait,+ show the exit code) and `start prog args` (background: spawn, don't wait),+ plus aliases `dir` (= ls), `type` (= cat), `cls` (clear screen), `ver`.+- **Pipes and redirection**: `a | b` (pipe + two spawns with stdout/stdin set)+ and `> file` (open with `O_CREAT|O_TRUNC`, pass it as the child's stdout).+- `help`, `exit`.+- **`run --trace prog`**: spawn with `SPAWN_TRACE` to print every syscall the+ program makes (a built-in mini `strace`).+- **Drive letters as aliases over the single tree**: `C:` = `/`, and each+ mount gets the next letter automatically (`D:` = `/mnt/disk0`, ...). It's+ only shell syntax: the kernel keeps one Unix-style tree. ## Roadmap Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads,-user mode + syscalls, ELF loading and a VFS with a ramfs.+user mode + syscalls, ELF loading, a VFS with a ramfs, PCI, virtio-blk,+MBR/GPT partitions, FAT32, blocking threads, pipes, spawn/wait. - [x] Freestanding binary — [phil-opp](https://os.phil-opp.com/freestanding-rust-binary/) - [x] Minimal kernel booting in QEMU — [phil-opp](https://os.phil-opp.com/minimal-rust-kernel/), [OSDev: Bare Bones](https://wiki.osdev.org/Bare_Bones)@@ -592,6 +813,11 @@ user mode + syscalls, ELF loading and a VFS with a ramfs. - [x] User mode (ring 3) & syscalls — GDT user segments, TSS.rsp0 per thread, `syscall`/`sysret`, per-process page tables, user faults kill the process. [OSDev: Getting to Ring 3](https://wiki.osdev.org/Getting_to_Ring_3), [OSDev: System Calls](https://wiki.osdev.org/System_Calls), [OSDev: SYSENTER/SYSCALL](https://wiki.osdev.org/SYSENTER) - [x] ELF loading — hand-written PT_LOAD loader, user programs built by `build.rs`. [OSDev: ELF](https://wiki.osdev.org/ELF), [OSDev: ELF Tutorial](https://wiki.osdev.org/ELF_Tutorial) - [x] VFS skeleton + ramfs — Inode/FileSystem/File traits, mount table, fd table, `open/read/write/close`. [OSDev: VFS](https://wiki.osdev.org/VFS), [Linux VFS overview](https://www.kernel.org/doc/html/latest/filesystems/vfs.html)+- [x] PCI enumeration + virtio-blk driver + `BlockDevice` trait — [OSDev: PCI](https://wiki.osdev.org/PCI), [OSDev: Virtio](https://wiki.osdev.org/Virtio), [virtio spec (legacy interface)](https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html)+- [x] MBR + GPT partitions — [OSDev: MBR](https://wiki.osdev.org/MBR_(x86)), [OSDev: GPT](https://wiki.osdev.org/GPT)+- [x] FAT32 through `fatfs`, mounted at `/mnt/disk0`; programs loaded from disk — [OSDev: FAT](https://wiki.osdev.org/FAT)+- [x] Blocking threads + wait queues, pipes, `spawn`/`wait`, cwd, `stat`/`lseek`/`getdents64` — [OSDev: Blocking Process](https://wiki.osdev.org/Blocking_Process)+- [ ] Shell (next stage, design above) — [OSDev: Shell](https://wiki.osdev.org/Shell) ### Filesystem roadmap (Tyler's plan) @@ -599,19 +825,12 @@ Everything below plugs into the VFS above: a filesystem is a `FileSystem` whose `root()` returns `Inode`s; mounting it is one `fs::mount` call. 1. **ramfs / tmpfs** — done (`src/fs/ramfs.rs`).-2. **Block device driver** — a `BlockDevice` trait (`read_blocks(lba, buf)`,- `write_blocks`, `block_size`) with a first driver: **virtio-blk** (modern,- fast, what QEMU/KVM use; needs PCI enumeration + virtqueues) or **ATA PIO**- (dead simple port I/O, good first step). Then a small block cache.- [OSDev: ATA PIO Mode](https://wiki.osdev.org/ATA_PIO_Mode),- [OSDev: Virtio](https://wiki.osdev.org/Virtio), [OSDev: PCI](https://wiki.osdev.org/PCI),- [virtio spec](https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html)-3. **Partition tables** — MBR and GPT parsing so a disk can hold several filesystems.- [OSDev: GPT](https://wiki.osdev.org/GPT)-4. **FAT32** — the easiest real on-disk format, using the `fatfs` crate (`no_std`- with `alloc`) behind an adapter that implements `FileSystem`/`Inode`. Test- images are easy to make with `mkfs.vfat` + `mtools`.- [OSDev: FAT](https://wiki.osdev.org/FAT), [fatfs crate](https://crates.io/crates/fatfs)+2. **Block device driver** — done: `BlockDevice` trait + legacy virtio-blk+ (stage 6). Still to do: a block cache, IRQ-driven completion, maybe an+ ATA PIO driver for comparison ([OSDev: ATA PIO Mode](https://wiki.osdev.org/ATA_PIO_Mode)).+3. **Partition tables** — done: MBR + GPT (no CRC checks yet).+4. **FAT32** — done via the `fatfs` crate (`src/fs/fat.rs`). A good exercise+ later: a hand-written read-only FAT32 to see what the crate does. 5. **ext2** — written by hand: superblock, block groups, inode table, direct/indirect blocks, directory entries. The classic "learn how Unix filesystems work" step, and the base for understanding ext4 later. [OSDev: Ext2](https://wiki.osdev.org/Ext2),@@ -633,15 +852,10 @@ whose `root()` returns `Inode`s; mounting it is one `fs::mount` call. Other next stages: -- **Shell** — a user-mode shell using `read(0)`/`write`; needs `fork`/`exec`- (or a simpler `spawn(path)`) + `wait` syscalls and `readdir`/`stat` for `ls`/`cat`.- [OSDev: Shell](https://wiki.osdev.org/Shell)-- **Process cleanup** — free a process's page tables, frames and kernel stack on exit.-- **Real blocking** — a blocked thread state + wait queues so `read(0)` and- `sleep` don't spin with `hlt`.+- **Shell** — next stage; design in "Next stage: the shell" above.+- **Process cleanup** — free a process's page tables and frames on exit. -Small exercises along the way: a *blocked* state + sleep queue for threads,-free finished threads' stacks, guard pages under thread stacks, a timer-based+Small exercises along the way: guard pages under thread stacks, a timer-based async `sleep` future, speed up the PIT ([OSDev: PIT](https://wiki.osdev.org/Programmable_Interval_Timer)), add handlers for divide error / general protection fault, make `BootInfoFrameAllocator` O(1), add a@@ -670,3 +884,9 @@ and for a UEFI/framebuffer setup later, the newer [`bootloader` 0.11](https://gi and boots the wrong build. `scripts/runner.sh` and the scripts `touch` the ELF first to avoid this. If you call `cargo bootimage --features ...` by hand after switching features, `touch target/x86_64-rust_kernel/debug/rust_kernel` first.+- `build.rs` reruns when `user/`, `disk/` or `build.rs` change and rebuilds+ `target/disk.img`. QEMU opens it with `snapshot=on`, so the kernel's writes+ (the tests create and delete files) never reach the file on disk.+- Debug builds use a lot of stack: deep call chains (syscall -> VFS -> fatfs+ -> ELF loader) needed more than 16 KiB. If you see a weird ring-0 fault at+ address 0 or "kernel stack overflow in thread", suspect the stack first.
Binary file changed.
@@ -1,33 +1,133 @@-//! Build script: compile the user programs in user/ into ELF executables in-//! OUT_DIR. The kernel embeds them with `include_bytes!` (src/lib.rs).+//! Build script, two jobs: //!-//! They're compiled with plain `rustc` for the built-in `x86_64-unknown-none`-//! target (prebuilt `core`, so no build-std needed), as static executables-//! (`relocation-model=static`) linked at a high user address -- which needs-//! the `large` code model (the default ones assume code in the low/high 2 GiB).+//! 1. Compile the user programs in user/ into ELF executables in OUT_DIR.+//! They're compiled with plain `rustc` for the built-in+//! `x86_64-unknown-none` target (prebuilt `core`, so no build-std+//! needed), as static executables (`relocation-model=static`) linked at a+//! high user address -- which needs the `large` code model (the default+//! ones assume code in the low/high 2 GiB).+//!+//! 2. Build the second QEMU disk, `target/disk.img` (64 MiB):+//! * a GPT partition table (made with `sfdisk`) with one partition+//! starting at sector 2048 (1 MiB, the usual alignment),+//! * in it a FAT32 file system (`mkfs.vfat`), filled with `mtools`+//! (`mmd`/`mcopy` write into an image file directly -- no root, no+//! loop-mounting needed): everything in disk/ plus the user programs+//! in /bin.+//! The kernel finds the disk on the PCI bus, reads the GPT, mounts the+//! FAT32 partition at /mnt/disk0 and loads programs from /mnt/disk0/bin.+//! QEMU opens it with `snapshot=on`, so the kernel's writes never change+//! this file (every boot and every test starts from the same disk).+//!+//! Needs dosfstools (mkfs.vfat), mtools (mmd, mcopy) and fdisk (sfdisk):+//! sudo apt install dosfstools mtools fdisk++use std::{env, fs, io::Write, path::{Path, PathBuf}, process::{Command, Stdio}}; -use std::{env, path::PathBuf, process::Command};+const PROGRAMS: &[&str] = &["hello", "evil", "spin", "echo", "demo", "child"]; -const PROGRAMS: &[&str] = &["hello", "evil", "spin", "echo"];+const DISK_SECTORS: u64 = 64 * 1024 * 1024 / 512;+const PART_START: u64 = 2048;+/// Leave 1 MiB at the end too (the backup GPT lives in the last 33 sectors).+const PART_SECTORS: u64 = DISK_SECTORS - 2 * PART_START; fn main() {+ let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());- let user_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("user");- let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into());+ let user_dir = manifest.join("user"); println!("cargo:rerun-if-changed=user");+ println!("cargo:rerun-if-changed=disk");+ println!("cargo:rerun-if-changed=build.rs");++ for program in PROGRAMS {+ build_program(&user_dir, &out_dir, program);+ }+ build_disk(&manifest, &out_dir);+}++fn build_program(user_dir: &Path, out_dir: &Path, program: &str) {+ let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into());+ let status = Command::new(&rustc)+ .args(["--edition", "2024", "--target", "x86_64-unknown-none"])+ .args(["-C", "panic=abort", "-C", "opt-level=2", "-C", "debuginfo=0", "-C", "strip=debuginfo"])+ .args(["-C", "relocation-model=static", "-C", "code-model=large"])+ .arg("-C")+ .arg(format!("link-arg=-T{}", user_dir.join("link.ld").display()))+ .arg("-o")+ .arg(out_dir.join(program))+ .arg(user_dir.join(format!("{program}.rs")))+ .status()+ .expect("failed to run rustc for a user program");+ assert!(status.success(), "building user program {program} failed");+}++/// Find a tool in PATH or the sbin directories (often not in a user's PATH).+fn tool(name: &str) -> PathBuf {+ let path = env::var("PATH").unwrap_or_default();+ for dir in path.split(':').chain(["/usr/sbin", "/sbin", "/usr/bin"]) {+ let candidate = Path::new(dir).join(name);+ if candidate.is_file() {+ return candidate;+ }+ }+ panic!("`{name}` not found: install it with `sudo apt install dosfstools mtools fdisk`");+}++fn run(cmd: &mut Command, stdin: Option<&str>) {+ cmd.stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() });+ let mut child = cmd.spawn().unwrap_or_else(|e| panic!("can't run {cmd:?}: {e}"));+ if let Some(input) = stdin {+ child.stdin.take().unwrap().write_all(input.as_bytes()).unwrap();+ }+ let status = child.wait().unwrap();+ assert!(status.success(), "{cmd:?} failed");+}++fn build_disk(manifest: &Path, out_dir: &Path) {+ let target_dir = env::var("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|_| manifest.join("target"));+ fs::create_dir_all(&target_dir).unwrap();+ let disk = target_dir.join("disk.img");+ let part = out_dir.join("part.img"); + // 1. The FAT32 file system, as its own image first. -s 1: one sector per+ // cluster (FAT32 needs >= 65525 clusters, so small disks need small+ // clusters). -n: volume label.+ let _ = fs::remove_file(&part);+ run(+ Command::new(tool("mkfs.vfat"))+ .args(["-F", "32", "-s", "1", "-n", "TYLERDISK", "-C"])+ .arg(&part)+ .arg((PART_SECTORS / 2).to_string()), // size in KiB+ None,+ );+ let mtools = |name: &str, args: &[&str]| {+ let mut cmd = Command::new(tool(name));+ cmd.env("MTOOLS_SKIP_CHECK", "1").arg("-i").arg(&part).args(args);+ run(&mut cmd, None);+ };+ // Copy disk/ (text files, subdirectories) to the root of the volume...+ for entry in fs::read_dir(manifest.join("disk")).unwrap() {+ let path = entry.unwrap().path();+ mtools("mcopy", &["-s", path.to_str().unwrap(), "::/"]);+ }+ // ...and the user programs to /bin.+ mtools("mmd", &["::/bin"]); for program in PROGRAMS {- let status = Command::new(&rustc)- .args(["--edition", "2024", "--target", "x86_64-unknown-none"])- .args(["-C", "panic=abort", "-C", "opt-level=2", "-C", "debuginfo=0", "-C", "strip=debuginfo"])- .args(["-C", "relocation-model=static", "-C", "code-model=large"])- .arg("-C")- .arg(format!("link-arg=-T{}", user_dir.join("link.ld").display()))- .arg("-o")- .arg(out_dir.join(program))- .arg(user_dir.join(format!("{program}.rs")))- .status()- .expect("failed to run rustc for a user program");- assert!(status.success(), "building user program {program} failed");+ mtools("mcopy", &[out_dir.join(program).to_str().unwrap(), &format!("::/bin/{program}")]); }++ // 2. The whole disk: zeros + GPT partition table (sfdisk works on image+ // files too), then the FAT32 image copied into the partition.+ let _ = fs::remove_file(&disk);+ fs::File::create(&disk).unwrap().set_len(DISK_SECTORS * 512).unwrap();+ let layout = format!(+ "label: gpt\nstart={PART_START}, size={PART_SECTORS}, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name=\"fat32\"\n"+ );+ run(Command::new(tool("sfdisk")).arg("--quiet").arg(&disk), Some(&layout));+ let fat = fs::read(&part).unwrap();+ assert_eq!(fat.len() as u64, PART_SECTORS * 512);+ let mut file = fs::OpenOptions::new().write(true).open(&disk).unwrap();+ use std::io::{Seek, SeekFrom};+ file.seek(SeekFrom::Start(PART_START * 512)).unwrap();+ file.write_all(&fat).unwrap(); }
@@ -0,0 +1,2 @@+Next stage: the shell (cd, ls, pwd, cat, run, start, pipes, > redirection).+Later: ext2, exFAT, btrfs.
@@ -0,0 +1,2 @@+Hello from the FAT32 disk! This file lives on a virtio-blk disk, in a GPT+partition, in a FAT32 file system -- read through the kernel's VFS.
@@ -17,6 +17,7 @@ IMG=target/x86_64-rust_kernel/debug/bootimage-rust_kernel.bin set +e timeout 30 qemu-system-x86_64 \ -drive format=raw,file="$IMG" \+ -drive file=target/disk.img,format=raw,if=virtio,snapshot=on \ -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ -serial stdio -display none -no-reboot code=$?
@@ -23,6 +23,7 @@ SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX) SERIAL_LOG=$(mktemp /tmp/serial.XXXXXX) qemu-system-x86_64 -drive format=raw,file="$IMG" \+ -drive file=target/disk.img,format=raw,if=virtio,snapshot=on \ -serial file:"$SERIAL_LOG" -display none -monitor unix:"$SOCK",server,nowait & QPID=$! sleep 6 # boot, and let the demo threads finish their serial output
@@ -28,7 +28,7 @@ pub mod fixed_size_block; /// Where the heap lives in *virtual* memory. Any unused, easy-to-recognise /// address works; this one stands out in a debugger. pub const HEAP_START: usize = 0x_4444_4444_0000;-pub const HEAP_SIZE: usize = 256 * 1024; // 256 KiB (thread stacks live here too)+pub const HEAP_SIZE: usize = 4 * 1024 * 1024; // 4 MiB (thread stacks live here too) #[global_allocator] static ALLOCATOR: Locked<fixed_size_block::FixedSizeBlockAllocator> =
@@ -26,8 +26,21 @@ impl BumpAllocator { } } +// The heap lock is taken with interrupts off: code that runs with+// interrupts off (the scheduler, wait queues, ...) may allocate, and if it+// found the lock held by a thread that was preempted mid-allocation, it+// would spin forever -- that thread can't run again until interrupts are on. unsafe impl GlobalAlloc for Locked<BumpAllocator> { unsafe fn alloc(&self, layout: Layout) -> *mut u8 {+ x86_64::instructions::interrupts::without_interrupts(|| unsafe { self.alloc_inner(layout) })+ }+ unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {+ x86_64::instructions::interrupts::without_interrupts(|| unsafe { self.dealloc_inner(ptr, layout) })+ }+}++impl Locked<BumpAllocator> {+ unsafe fn alloc_inner(&self, layout: Layout) -> *mut u8 { let mut bump = self.lock(); let alloc_start = align_up(bump.next, layout.align()); let alloc_end = match alloc_start.checked_add(layout.size()) {@@ -43,7 +56,7 @@ unsafe impl GlobalAlloc for Locked<BumpAllocator> { } } - unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {+ unsafe fn dealloc_inner(&self, _ptr: *mut u8, _layout: Layout) { let mut bump = self.lock(); bump.allocations -= 1; if bump.allocations == 0 {
@@ -56,8 +56,21 @@ impl FixedSizeBlockAllocator { } } +// The heap lock is taken with interrupts off: code that runs with+// interrupts off (the scheduler, wait queues, ...) may allocate, and if it+// found the lock held by a thread that was preempted mid-allocation, it+// would spin forever -- that thread can't run again until interrupts are on. unsafe impl GlobalAlloc for Locked<FixedSizeBlockAllocator> { unsafe fn alloc(&self, layout: Layout) -> *mut u8 {+ x86_64::instructions::interrupts::without_interrupts(|| unsafe { self.alloc_inner(layout) })+ }+ unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {+ x86_64::instructions::interrupts::without_interrupts(|| unsafe { self.dealloc_inner(ptr, layout) })+ }+}++impl Locked<FixedSizeBlockAllocator> {+ unsafe fn alloc_inner(&self, layout: Layout) -> *mut u8 { let mut allocator = self.lock(); match list_index(&layout) { Some(index) => match allocator.list_heads[index].take() {@@ -77,7 +90,7 @@ unsafe impl GlobalAlloc for Locked<FixedSizeBlockAllocator> { } } - unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {+ unsafe fn dealloc_inner(&self, ptr: *mut u8, layout: Layout) { let mut allocator = self.lock(); match list_index(&layout) { Some(index) => {
@@ -0,0 +1,127 @@+//! Block devices: things that store fixed-size *sectors* (512 bytes) you can+//! read and write by number (LBA = logical block address). Disks, partitions+//! of disks, and a RAM disk for tests all implement [`BlockDevice`], so file+//! systems don't care which one they sit on.+//!+//! Layers, bottom to top:+//! virtio_blk (a QEMU disk) -> partition (a slice of a disk) -> fs::fat (FAT32)+//!+//! Devices are registered by name ("vda", "vda1", like Linux) in a global+//! list, see [`register`] and [`devices`].++pub mod partition;+pub mod virtio_blk;++use crate::errno::Errno;+use alloc::{string::String, sync::Arc, vec, vec::Vec};+use spin::Mutex;++pub const SECTOR_SIZE: usize = 512;++pub trait BlockDevice: Send + Sync {+ /// Number of sectors.+ fn sector_count(&self) -> u64;+ /// Read `buf.len() / 512` sectors starting at `lba` (buf.len() must be a+ /// multiple of 512).+ fn read_sectors(&self, lba: u64, buf: &mut [u8]) -> Result<(), Errno>;+ /// Write `buf.len() / 512` sectors starting at `lba`.+ fn write_sectors(&self, lba: u64, buf: &[u8]) -> Result<(), Errno>;+}++/// Bounds check shared by all devices: whole sectors, inside the device.+pub fn check_range(dev: &dyn BlockDevice, lba: u64, len: usize) -> Result<u64, Errno> {+ if len % SECTOR_SIZE != 0 {+ return Err(Errno::EINVAL);+ }+ let count = (len / SECTOR_SIZE) as u64;+ match lba.checked_add(count) {+ Some(end) if end <= dev.sector_count() => Ok(count),+ _ => Err(Errno::EIO), // like reading past the end of a real disk+ }+}++/// A disk in RAM (for tests, and handy for experiments).+pub struct RamDisk {+ data: Mutex<Vec<u8>>,+}++impl RamDisk {+ pub fn new(sectors: usize) -> Self {+ RamDisk { data: Mutex::new(vec![0; sectors * SECTOR_SIZE]) }+ }+}++impl BlockDevice for RamDisk {+ fn sector_count(&self) -> u64 {+ (self.data.lock().len() / SECTOR_SIZE) as u64+ }+ fn read_sectors(&self, lba: u64, buf: &mut [u8]) -> Result<(), Errno> {+ check_range(self, lba, buf.len())?;+ let start = lba as usize * SECTOR_SIZE;+ buf.copy_from_slice(&self.data.lock()[start..start + buf.len()]);+ Ok(())+ }+ fn write_sectors(&self, lba: u64, buf: &[u8]) -> Result<(), Errno> {+ check_range(self, lba, buf.len())?;+ let start = lba as usize * SECTOR_SIZE;+ self.data.lock()[start..start + buf.len()].copy_from_slice(buf);+ Ok(())+ }+}++static DEVICES: Mutex<Vec<(String, Arc<dyn BlockDevice>)>> = Mutex::new(Vec::new());++pub fn register(name: &str, dev: Arc<dyn BlockDevice>) {+ DEVICES.lock().push((String::from(name), dev));+}++/// All registered devices, (name, device), in registration order.+pub fn devices() -> Vec<(String, Arc<dyn BlockDevice>)> {+ DEVICES.lock().clone()+}++pub fn get(name: &str) -> Option<Arc<dyn BlockDevice>> {+ DEVICES.lock().iter().find(|(n, _)| n == name).map(|(_, d)| d.clone())+}++/// Boot-time storage setup: find virtio disks on the PCI bus ("vda", "vdb"...),+/// register them, and register their partitions ("vda1", ...). Returns+/// lines describing what was found, for printing.+pub fn init(pci_devices: &[crate::pci::PciDevice]) -> Vec<String> {+ use alloc::format;+ let mut log = Vec::new();+ let mut letter = b'a';+ for pci in pci_devices.iter().filter(|d| virtio_blk::is_virtio_blk(d)) {+ let name = format!("vd{}", letter as char);+ match virtio_blk::VirtioBlk::new(pci) {+ Ok(disk) => {+ let disk: Arc<dyn BlockDevice> = Arc::new(disk);+ log.push(format!(+ "{}: virtio-blk, {} sectors ({} MiB)",+ name,+ disk.sector_count(),+ disk.sector_count() * SECTOR_SIZE as u64 / (1024 * 1024)+ ));+ register(&name, disk.clone());+ match partition::scan(&disk) {+ Ok((scheme, parts)) => {+ log.push(format!("{}: {} partition table, {} partition(s)", name, scheme, parts.len()));+ for (i, p) in parts.into_iter().enumerate() {+ let pname = format!("{}{}", name, i + 1);+ log.push(format!(+ "{}: start {} sectors {} type {}{}",+ pname, p.start, p.sectors, p.type_name,+ if p.name.is_empty() { String::new() } else { format!(" name \"{}\"", p.name) }+ ));+ register(&pname, Arc::new(partition::Partition::new(disk.clone(), p.start, p.sectors)));+ }+ }+ Err(e) => log.push(format!("{}: no partition table ({:?})", name, e)),+ }+ }+ Err(e) => log.push(format!("{}: virtio-blk init failed: {:?}", name, e)),+ }+ letter += 1;+ }+ log+}
@@ -0,0 +1,158 @@+//! Partition tables: MBR (1983, max 4 primary partitions, 2 TiB) and GPT+//! (UEFI era, 128 partitions, 64-bit sizes). A [`Partition`] is itself a+//! `BlockDevice` -- a window onto part of the disk -- so a file system on+//! "vda1" doesn't know (or care) that it starts at sector 2048 of "vda".+//!+//! MBR (sector 0): boot code, then 4 entries of 16 bytes at offset 446, then+//! the signature 0x55 0xAA. Each entry: type byte (offset 4), first sector+//! (u32 at 8), sector count (u32 at 12). Type 0xEE = "this disk is GPT, the+//! MBR is just a protective placeholder".+//!+//! GPT: header in sector 1 ("EFI PART"; where the entry array is, how many+//! entries, how big each is), entry array usually in sectors 2-33. Each+//! entry: type GUID (all zero = unused), unique GUID, first/last LBA, flags,+//! UTF-16 name. A backup copy lives at the end of the disk; we don't check+//! it, nor the CRC32 checksums (exercise!). Extended/logical MBR partitions+//! aren't supported either. References: wiki.osdev.org/MBR_(x86),+//! wiki.osdev.org/GPT++use super::{BlockDevice, SECTOR_SIZE, check_range};+use crate::errno::Errno;+use alloc::{string::String, sync::Arc, vec, vec::Vec};++/// What the partition table says about one partition.+#[derive(Debug, Clone, PartialEq, Eq)]+pub struct PartitionInfo {+ pub start: u64, // first sector+ pub sectors: u64, // length+ pub type_name: &'static str,+ pub name: String, // GPT only+}++#[derive(Debug, PartialEq, Eq)]+pub enum PartitionError {+ NoSignature, // sector 0 doesn't end in 55 AA: not partitioned+ BadGpt,+ Io(Errno),+}++fn u32_at(b: &[u8], at: usize) -> u32 {+ u32::from_le_bytes(b[at..at + 4].try_into().unwrap())+}+fn u64_at(b: &[u8], at: usize) -> u64 {+ u64::from_le_bytes(b[at..at + 8].try_into().unwrap())+}++/// Read the partition table of `disk`: ("MBR" or "GPT", partitions).+pub fn scan(disk: &Arc<dyn BlockDevice>) -> Result<(&'static str, Vec<PartitionInfo>), PartitionError> {+ let mut mbr = [0u8; SECTOR_SIZE];+ disk.read_sectors(0, &mut mbr).map_err(PartitionError::Io)?;+ if mbr[510..512] != [0x55, 0xaa] {+ return Err(PartitionError::NoSignature);+ }+ let entries: Vec<&[u8]> = (0..4).map(|i| &mbr[446 + 16 * i..446 + 16 * (i + 1)]).collect();+ if entries.iter().any(|e| e[4] == 0xee) {+ return Ok(("GPT", scan_gpt(disk)?));+ }+ let parts = entries+ .iter()+ .filter(|e| e[4] != 0 && u32_at(e, 12) != 0) // type 0 = empty slot+ .map(|e| PartitionInfo {+ start: u32_at(e, 8) as u64,+ sectors: u32_at(e, 12) as u64,+ type_name: mbr_type_name(e[4]),+ name: String::new(),+ })+ .collect();+ Ok(("MBR", parts))+}++fn scan_gpt(disk: &Arc<dyn BlockDevice>) -> Result<Vec<PartitionInfo>, PartitionError> {+ let mut header = [0u8; SECTOR_SIZE];+ disk.read_sectors(1, &mut header).map_err(PartitionError::Io)?;+ if &header[0..8] != b"EFI PART" {+ return Err(PartitionError::BadGpt);+ }+ let entries_lba = u64_at(&header, 72);+ let count = u32_at(&header, 80) as usize;+ let entry_size = u32_at(&header, 84) as usize;+ if entry_size < 128 || count > 1024 || entry_size > SECTOR_SIZE || SECTOR_SIZE % entry_size != 0 {+ return Err(PartitionError::BadGpt);+ }+ let sectors = (count * entry_size).div_ceil(SECTOR_SIZE);+ let mut table = vec![0u8; sectors * SECTOR_SIZE];+ disk.read_sectors(entries_lba, &mut table).map_err(PartitionError::Io)?;++ let mut parts = Vec::new();+ for e in table.chunks(entry_size).take(count) {+ let type_guid = &e[0..16];+ if type_guid.iter().all(|&b| b == 0) {+ continue; // unused entry+ }+ let (first, last) = (u64_at(e, 32), u64_at(e, 40));+ if last < first {+ return Err(PartitionError::BadGpt);+ }+ // Name: UTF-16LE, up to 36 code units, zero-padded.+ let units: Vec<u16> = e[56..128].chunks(2).map(|c| u16::from_le_bytes([c[0], c[1]])).take_while(|&u| u != 0).collect();+ parts.push(PartitionInfo {+ start: first,+ sectors: last - first + 1,+ type_name: gpt_type_name(type_guid),+ name: char::decode_utf16(units).map(|c| c.unwrap_or('?')).collect(),+ });+ }+ Ok(parts)+}++fn mbr_type_name(t: u8) -> &'static str {+ match t {+ 0x0b | 0x0c => "FAT32",+ 0x07 => "NTFS/exFAT",+ 0x83 => "Linux",+ 0x05 | 0x0f => "extended (unsupported)",+ _ => "unknown",+ }+}++/// GUIDs are stored with their first three fields little-endian, so the+/// bytes look scrambled compared to the usual text form.+fn gpt_type_name(guid: &[u8]) -> &'static str {+ const BASIC_DATA: [u8; 16] = [0xa2, 0xa0, 0xd0, 0xeb, 0xe5, 0xb9, 0x33, 0x44, 0x87, 0xc0, 0x68, 0xb6, 0xb7, 0x26, 0x99, 0xc7]; // EBD0A0A2-B9E5-4433-87C0-68B6B72699C7+ const LINUX_FS: [u8; 16] = [0xaf, 0x3d, 0xc6, 0x0f, 0x83, 0x84, 0x72, 0x47, 0x8e, 0x79, 0x3d, 0x69, 0xd8, 0x47, 0x7d, 0xe4]; // 0FC63DAF-8483-4772-8E79-3D69D8477DE4+ const EFI_SYSTEM: [u8; 16] = [0x28, 0x73, 0x2a, 0xc1, 0x1f, 0xf8, 0xd2, 0x11, 0xba, 0x4b, 0x00, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b]; // C12A7328-F81F-11D2-BA4B-00A0C93EC93B+ match guid {+ g if g == BASIC_DATA => "Microsoft basic data (FAT/exFAT/NTFS)",+ g if g == LINUX_FS => "Linux filesystem",+ g if g == EFI_SYSTEM => "EFI system (FAT)",+ _ => "unknown",+ }+}++/// A partition as a block device: sector numbers are shifted by `start` and+/// checked against `sectors`, so a file system can't scribble outside it.+pub struct Partition {+ disk: Arc<dyn BlockDevice>,+ start: u64,+ sectors: u64,+}++impl Partition {+ pub fn new(disk: Arc<dyn BlockDevice>, start: u64, sectors: u64) -> Self {+ Partition { disk, start, sectors }+ }+}++impl BlockDevice for Partition {+ fn sector_count(&self) -> u64 {+ self.sectors+ }+ fn read_sectors(&self, lba: u64, buf: &mut [u8]) -> Result<(), Errno> {+ check_range(self, lba, buf.len())?;+ self.disk.read_sectors(self.start + lba, buf)+ }+ fn write_sectors(&self, lba: u64, buf: &[u8]) -> Result<(), Errno> {+ check_range(self, lba, buf.len())?;+ self.disk.write_sectors(self.start + lba, buf)+ }+}
@@ -0,0 +1,281 @@+//! Driver for virtio-blk: the paravirtual disk QEMU/KVM offer+//! (`-drive file=disk.img,if=virtio`).+//!+//! Why virtio and not ATA PIO? ATA PIO is simpler (copy 256 words through an+//! I/O port per sector) but it's a 1980s interface that real machines barely+//! have any more. virtio is what VMs actually use, and it teaches the core+//! idea of every modern device: *shared memory queues + DMA*. We use the+//! **legacy** (virtio 0.9.5) interface, which QEMU's "transitional" virtio+//! devices still offer: its registers are a handful of I/O ports (BAR0), and+//! setting it up is ~40 lines. The modern (1.0) interface uses PCI+//! capabilities + MMIO; the queue format is the same.+//!+//! The virtqueue: three arrays in physical memory that driver and device share.+//! * descriptor table: {physical addr, length, flags, next} -- buffers+//! * available ring: driver writes "descriptor chain N is ready"+//! * used ring: device writes "I finished chain N"+//! A block request is a chain of 3 descriptors:+//! header {type: read/write, sector} -> data (512*n bytes) -> status byte+//! We put the chain in the available ring, write the queue number to the+//! "notify" port, and wait until the used ring's index moves.+//!+//! Simplifications: one request at a time; we *poll* the used ring instead of+//! using the interrupt (we even ask the device not to interrupt us), and we+//! copy data through one page-sized "bounce buffer", so a request moves at+//! most 8 sectors. Reference: virtio spec 1.2 sections 2.7 (split virtqueues),+//! 4.1.4.8 (legacy PCI interface), 5.2 (block device); wiki.osdev.org/Virtio++use super::{BlockDevice, SECTOR_SIZE, check_range};+use crate::errno::Errno;+use crate::memory;+use crate::pci::PciDevice;+use core::ptr::{read_volatile, write_volatile};+use core::sync::atomic::{Ordering, fence};+use spin::Mutex;+use x86_64::PhysAddr;+use x86_64::instructions::port::Port;++// Legacy virtio PCI registers (offsets into the I/O BAR).+const REG_DEVICE_FEATURES: u16 = 0x00;+const REG_GUEST_FEATURES: u16 = 0x04;+const REG_QUEUE_ADDRESS: u16 = 0x08; // physical page number of the queue+const REG_QUEUE_SIZE: u16 = 0x0c;+const REG_QUEUE_SELECT: u16 = 0x0e;+const REG_QUEUE_NOTIFY: u16 = 0x10;+const REG_DEVICE_STATUS: u16 = 0x12;+const REG_ISR_STATUS: u16 = 0x13;+const REG_CAPACITY: u16 = 0x14; // device config: disk size in sectors (u64)++// Device status bits (we tell the device how far initialization got).+const STATUS_ACKNOWLEDGE: u8 = 1; // "I see you"+const STATUS_DRIVER: u8 = 2; // "I know how to drive you"+const STATUS_DRIVER_OK: u8 = 4; // "ready to go"++// Descriptor flags.+const DESC_NEXT: u16 = 1; // chain continues at `next`+const DESC_WRITE: u16 = 2; // device writes into this buffer (vs. reads it)+const AVAIL_NO_INTERRUPT: u16 = 1; // "don't interrupt me, I'm polling"++const REQ_READ: u32 = 0; // VIRTIO_BLK_T_IN+const REQ_WRITE: u32 = 1; // VIRTIO_BLK_T_OUT++/// Max sectors per request = what fits in our one-page bounce buffer.+const MAX_SECTORS: usize = 4096 / SECTOR_SIZE;++#[repr(C)]+struct Descriptor {+ addr: u64,+ len: u32,+ flags: u16,+ next: u16,+}++#[derive(Debug)]+pub enum VirtioError {+ NoIoBar,+ QueueUnavailable,+ OutOfMemory,+}++/// PCI vendor 0x1af4 (Red Hat/virtio), device 0x1001 = transitional block device.+pub fn is_virtio_blk(dev: &PciDevice) -> bool {+ dev.vendor_id == 0x1af4 && dev.device_id == 0x1001+}++pub struct VirtioBlk {+ io_base: u16,+ sectors: u64,+ queue: Mutex<Queue>,+}++struct Queue {+ size: u16,+ /// Physical address of the queue memory (descriptors, then available+ /// ring, then -- at the next page boundary -- the used ring).+ phys: PhysAddr,+ used_offset: usize,+ /// Our copy of the used ring index: how many requests we've seen finish.+ last_used: u16,+ /// One page for the request header (offset 0) and status byte (offset 16).+ header_page: PhysAddr,+ /// One page bounce buffer for the data.+ data_page: PhysAddr,+}++impl VirtioBlk {+ pub fn new(pci: &PciDevice) -> Result<Self, VirtioError> {+ let bar0 = pci.bar(0);+ if bar0 & 1 == 0 {+ return Err(VirtioError::NoIoBar); // legacy interface lives in I/O space+ }+ pci.enable_io_and_bus_master(); // bus master = allowed to DMA into our RAM+ let io_base = (bar0 & !0x3) as u16;+ let dev = VirtioBlk { io_base, sectors: 0, queue: Mutex::new(Queue::empty()) };++ // 1. Reset, then announce ourselves.+ dev.write8(REG_DEVICE_STATUS, 0);+ dev.write8(REG_DEVICE_STATUS, STATUS_ACKNOWLEDGE);+ dev.write8(REG_DEVICE_STATUS, STATUS_ACKNOWLEDGE | STATUS_DRIVER);+ // 2. Feature negotiation: the device lists what it can do; we accept+ // nothing optional (plain read/write is always supported).+ let _offered = dev.read32(REG_DEVICE_FEATURES);+ dev.write32(REG_GUEST_FEATURES, 0);+ // 3. Set up queue 0 (block devices have one request queue).+ dev.write16(REG_QUEUE_SELECT, 0);+ let size = dev.read16(REG_QUEUE_SIZE); // the device decides (QEMU: 256)+ if size == 0 {+ return Err(VirtioError::QueueUnavailable);+ }+ // Legacy layout: descriptors (16 bytes each) + available ring+ // (2+2+2*size+2), then the used ring (2+2+8*size+2) page-aligned.+ let n = size as usize;+ let used_offset = align_up(16 * n + 6 + 2 * n, 4096);+ let total = used_offset + align_up(6 + 8 * n, 4096);+ let phys = memory::alloc_contiguous_frames(total / 4096).ok_or(VirtioError::OutOfMemory)?;+ let header_page = memory::alloc_contiguous_frames(1).ok_or(VirtioError::OutOfMemory)?;+ let data_page = memory::alloc_contiguous_frames(1).ok_or(VirtioError::OutOfMemory)?;+ dev.write32(REG_QUEUE_ADDRESS, (phys.as_u64() / 4096) as u32);+ // 4. Read the disk size from the device config area, then go.+ let lo = dev.read32(REG_CAPACITY) as u64;+ let hi = dev.read32(REG_CAPACITY + 4) as u64;+ dev.write8(REG_DEVICE_STATUS, STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_DRIVER_OK);++ let queue = Queue { size, phys, used_offset, last_used: 0, header_page, data_page };+ unsafe { write_volatile(queue.avail_flags(), AVAIL_NO_INTERRUPT) };+ Ok(VirtioBlk { io_base, sectors: hi << 32 | lo, queue: Mutex::new(queue) })+ }++ /// Do one request of up to MAX_SECTORS. For writes, the data must+ /// already be in the bounce buffer; for reads, it's there afterwards.+ fn request(&self, q: &mut Queue, kind: u32, lba: u64, sectors: usize) -> Result<(), Errno> {+ let header = memory::phys_to_virt(q.header_page).as_mut_ptr::<u8>();+ unsafe {+ write_volatile(header as *mut u32, kind); // type+ write_volatile(header.add(4) as *mut u32, 0); // reserved+ write_volatile(header.add(8) as *mut u64, lba); // first sector+ write_volatile(header.add(16), 0xff); // status: device sets 0 = OK+ }+ // The chain: descriptors 0 -> 1 -> 2 (always the same three, since we+ // only ever have one request in flight).+ let data_flags = if kind == REQ_READ { DESC_WRITE } else { 0 };+ q.set_desc(0, q.header_page.as_u64(), 16, DESC_NEXT, 1);+ q.set_desc(1, q.data_page.as_u64(), (sectors * SECTOR_SIZE) as u32, data_flags | DESC_NEXT, 2);+ q.set_desc(2, q.header_page.as_u64() + 16, 1, DESC_WRITE, 0);++ // Publish chain 0 in the available ring, then bump its index.+ unsafe {+ let idx = read_volatile(q.avail_idx());+ write_volatile(q.avail_ring(idx % q.size), 0);+ fence(Ordering::SeqCst); // ring entry visible before the index+ write_volatile(q.avail_idx(), idx.wrapping_add(1));+ fence(Ordering::SeqCst);+ }+ self.write16(REG_QUEUE_NOTIFY, 0); // "queue 0 has work" -> QEMU does the I/O++ // Poll the used ring until the device reports completion.+ while unsafe { read_volatile(q.used_idx()) } == q.last_used {+ core::hint::spin_loop();+ }+ fence(Ordering::SeqCst);+ q.last_used = q.last_used.wrapping_add(1);+ let _ = self.read8(REG_ISR_STATUS); // reading acknowledges (clears) it++ match unsafe { read_volatile(header.add(16)) } {+ 0 => Ok(()),+ _ => Err(Errno::EIO),+ }+ }++ fn read8(&self, reg: u16) -> u8 {+ unsafe { Port::<u8>::new(self.io_base + reg).read() }+ }+ fn write8(&self, reg: u16, v: u8) {+ unsafe { Port::<u8>::new(self.io_base + reg).write(v) }+ }+ fn read16(&self, reg: u16) -> u16 {+ unsafe { Port::<u16>::new(self.io_base + reg).read() }+ }+ fn write16(&self, reg: u16, v: u16) {+ unsafe { Port::<u16>::new(self.io_base + reg).write(v) }+ }+ fn read32(&self, reg: u16) -> u32 {+ unsafe { Port::<u32>::new(self.io_base + reg).read() }+ }+ fn write32(&self, reg: u16, v: u32) {+ unsafe { Port::<u32>::new(self.io_base + reg).write(v) }+ }+}++impl BlockDevice for VirtioBlk {+ fn sector_count(&self) -> u64 {+ self.sectors+ }++ fn read_sectors(&self, lba: u64, buf: &mut [u8]) -> Result<(), Errno> {+ check_range(self, lba, buf.len())?;+ // Interrupts off while we hold the queue lock (see thread.rs locking+ // rule): a request takes microseconds in QEMU.+ x86_64::instructions::interrupts::without_interrupts(|| {+ let mut q = self.queue.lock();+ for (i, chunk) in buf.chunks_mut(MAX_SECTORS * SECTOR_SIZE).enumerate() {+ let sectors = chunk.len() / SECTOR_SIZE;+ self.request(&mut q, REQ_READ, lba + (i * MAX_SECTORS) as u64, sectors)?;+ let src = memory::phys_to_virt(q.data_page).as_ptr::<u8>();+ unsafe { core::ptr::copy_nonoverlapping(src, chunk.as_mut_ptr(), chunk.len()) };+ }+ Ok(())+ })+ }++ fn write_sectors(&self, lba: u64, buf: &[u8]) -> Result<(), Errno> {+ check_range(self, lba, buf.len())?;+ x86_64::instructions::interrupts::without_interrupts(|| {+ let mut q = self.queue.lock();+ for (i, chunk) in buf.chunks(MAX_SECTORS * SECTOR_SIZE).enumerate() {+ let dst = memory::phys_to_virt(q.data_page).as_mut_ptr::<u8>();+ unsafe { core::ptr::copy_nonoverlapping(chunk.as_ptr(), dst, chunk.len()) };+ self.request(&mut q, REQ_WRITE, lba + (i * MAX_SECTORS) as u64, chunk.len() / SECTOR_SIZE)?;+ }+ Ok(())+ })+ }+}++impl Queue {+ const fn empty() -> Self {+ Queue {+ size: 0,+ phys: PhysAddr::zero(),+ used_offset: 0,+ last_used: 0,+ header_page: PhysAddr::zero(),+ data_page: PhysAddr::zero(),+ }+ }+ fn base(&self) -> *mut u8 {+ memory::phys_to_virt(self.phys).as_mut_ptr()+ }+ fn set_desc(&self, i: usize, addr: u64, len: u32, flags: u16, next: u16) {+ let desc = unsafe { (self.base() as *mut Descriptor).add(i) };+ unsafe { write_volatile(desc, Descriptor { addr, len, flags, next }) };+ }+ fn avail_base(&self) -> *mut u16 {+ unsafe { self.base().add(16 * self.size as usize) as *mut u16 }+ }+ fn avail_flags(&self) -> *mut u16 {+ self.avail_base()+ }+ fn avail_idx(&self) -> *mut u16 {+ unsafe { self.avail_base().add(1) }+ }+ fn avail_ring(&self, i: u16) -> *mut u16 {+ unsafe { self.avail_base().add(2 + i as usize) }+ }+ fn used_idx(&self) -> *const u16 {+ unsafe { (self.base().add(self.used_offset) as *const u16).add(1) }+ }+}++fn align_up(x: usize, align: usize) -> usize {+ (x + align - 1) & !(align - 1)+}
@@ -5,13 +5,24 @@ #[repr(i64)] pub enum Errno { ENOENT = 2, // no such file or directory+ EIO = 5, // input/output error (disk trouble)+ E2BIG = 7, // argument list too long+ ENOEXEC = 8, // not a valid executable EBADF = 9, // bad file descriptor+ ECHILD = 10, // no such child process (wait)+ ENOMEM = 12, // out of memory EFAULT = 14, // bad address (pointer not in user space / not mapped)+ EBUSY = 16, // busy (e.g. removing a mount point) EEXIST = 17, // file exists ENOTDIR = 20, // a path component isn't a directory EISDIR = 21, // is a directory EINVAL = 22, // invalid argument EMFILE = 24, // too many open files+ ENOSPC = 28, // no space left on device+ ESPIPE = 29, // can't seek (pipe, console)+ EPIPE = 32, // writing to a pipe nobody reads+ ERANGE = 34, // result doesn't fit (getcwd buffer too small) ENAMETOOLONG = 36, // path too long ENOSYS = 38, // no such system call+ ENOTEMPTY = 39, // directory not empty }
@@ -0,0 +1,360 @@+//! FAT32 support, using the `fatfs` crate (no_std + alloc mode).+//!+//! Two adapters glue it in:+//!+//! 1. [`BlockIo`]: `fatfs` wants something that looks like a `std::fs::File`+//! (byte-addressed Read + Write + Seek). Our disks only do whole 512-byte+//! sectors, so `BlockIo` keeps a byte position and turns byte requests+//! into sector requests: whole aligned sectors go straight to the device;+//! partial sectors go through a one-sector cache (read-modify-write for+//! writes). Writes are write-through, so nothing is ever lost on "power+//! off" because of the cache.+//!+//! 2. [`FatInode`]: our VFS `Inode` for one FAT file/directory. `fatfs`'s+//! own `File`/`Dir` objects *borrow* the FileSystem (they carry a lifetime)+//! so we can't store them in a long-lived `Arc<dyn Inode>`. Instead a+//! FatInode just remembers its **path** and re-opens it from the root on+//! every operation. That's slower (each read walks the directories again)+//! but very simple and obviously correct -- fine for a learning kernel.+//! A faster design would cache directory entries (like Linux's dcache).+//!+//! Locking: the whole FileSystem sits behind one Mutex, so FAT operations+//! are serialized. The virtio driver underneath polls with interrupts off;+//! this lock is only taken from thread context, never from an interrupt.++use super::{DirEntry, FileSystem, Inode, InodeKind};+use crate::block::{BlockDevice, SECTOR_SIZE};+use crate::errno::Errno;+use alloc::{format, string::String, sync::Arc, vec::Vec};+use fatfs::{IoBase, Read, Seek, SeekFrom, Write};+use spin::Mutex;++// ---------------------------------------------------------------------------+// 1. Byte-stream view of a block device+// ---------------------------------------------------------------------------++/// The error type `fatfs` reports back to us: just our errno.+#[derive(Debug)]+pub struct IoErr(pub Errno);++impl fatfs::IoError for IoErr {+ fn is_interrupted(&self) -> bool {+ false+ }+ fn new_unexpected_eof_error() -> Self {+ IoErr(Errno::EIO)+ }+ fn new_write_zero_error() -> Self {+ IoErr(Errno::EIO)+ }+}++pub struct BlockIo {+ dev: Arc<dyn BlockDevice>,+ pos: u64,+ size: u64,+ /// One cached sector: (lba, contents).+ cache: Option<(u64, [u8; SECTOR_SIZE])>,+}++impl BlockIo {+ pub fn new(dev: Arc<dyn BlockDevice>) -> Self {+ let size = dev.sector_count() * SECTOR_SIZE as u64;+ BlockIo { dev, pos: 0, size, cache: None }+ }++ /// Get sector `lba` into the cache and return it.+ fn cached(&mut self, lba: u64) -> Result<&mut [u8; SECTOR_SIZE], IoErr> {+ if self.cache.map(|(l, _)| l) != Some(lba) {+ let mut data = [0u8; SECTOR_SIZE];+ self.dev.read_sectors(lba, &mut data).map_err(IoErr)?;+ self.cache = Some((lba, data));+ }+ Ok(&mut self.cache.as_mut().unwrap().1)+ }+}++impl IoBase for BlockIo {+ type Error = IoErr;+}++impl Read for BlockIo {+ fn read(&mut self, buf: &mut [u8]) -> Result<usize, IoErr> {+ let len = buf.len().min(self.size.saturating_sub(self.pos) as usize);+ if len == 0 {+ return Ok(0);+ }+ let lba = self.pos / SECTOR_SIZE as u64;+ let offset = (self.pos % SECTOR_SIZE as u64) as usize;+ let n = if offset == 0 && len >= SECTOR_SIZE {+ // Fast path: whole sectors straight into the caller's buffer.+ let n = len / SECTOR_SIZE * SECTOR_SIZE;+ self.dev.read_sectors(lba, &mut buf[..n]).map_err(IoErr)?;+ // Writes keep the cache up to date, so it can't be stale here.+ n+ } else {+ // Part of one sector, via the cache.+ let n = len.min(SECTOR_SIZE - offset);+ let sector = self.cached(lba)?;+ buf[..n].copy_from_slice(§or[offset..offset + n]);+ n+ };+ self.pos += n as u64;+ Ok(n)+ }+}++impl Write for BlockIo {+ fn write(&mut self, buf: &[u8]) -> Result<usize, IoErr> {+ let len = buf.len().min(self.size.saturating_sub(self.pos) as usize);+ if len == 0 {+ return Ok(0); // fatfs turns this into WriteZero (disk full)+ }+ let lba = self.pos / SECTOR_SIZE as u64;+ let offset = (self.pos % SECTOR_SIZE as u64) as usize;+ let n = if offset == 0 && len >= SECTOR_SIZE {+ let n = len / SECTOR_SIZE * SECTOR_SIZE;+ self.dev.write_sectors(lba, &buf[..n]).map_err(IoErr)?;+ // Drop the cached sector if we just overwrote it.+ if let Some((cached, _)) = self.cache {+ if cached >= lba && cached < lba + (n / SECTOR_SIZE) as u64 {+ self.cache = None;+ }+ }+ n+ } else {+ // Read-modify-write one sector.+ let n = len.min(SECTOR_SIZE - offset);+ let sector = self.cached(lba)?;+ sector[offset..offset + n].copy_from_slice(&buf[..n]);+ let copy = *sector;+ self.dev.write_sectors(lba, ©).map_err(IoErr)?;+ n+ };+ self.pos += n as u64;+ Ok(n)+ }++ fn flush(&mut self) -> Result<(), IoErr> {+ Ok(()) // write-through: nothing buffered+ }+}++impl Seek for BlockIo {+ fn seek(&mut self, pos: SeekFrom) -> Result<u64, IoErr> {+ let new = match pos {+ SeekFrom::Start(n) => Some(n),+ SeekFrom::End(d) => self.size.checked_add_signed(d),+ SeekFrom::Current(d) => self.pos.checked_add_signed(d),+ };+ match new {+ Some(n) if n <= self.size => {+ self.pos = n;+ Ok(n)+ }+ _ => Err(IoErr(Errno::EINVAL)),+ }+ }+}++/// Translate a fatfs error into an errno.+fn errno(e: fatfs::Error<IoErr>) -> Errno {+ match e {+ fatfs::Error::Io(IoErr(e)) => e,+ fatfs::Error::NotFound => Errno::ENOENT,+ fatfs::Error::AlreadyExists => Errno::EEXIST,+ fatfs::Error::DirectoryIsNotEmpty => Errno::ENOTEMPTY,+ fatfs::Error::NotEnoughSpace | fatfs::Error::WriteZero => Errno::ENOSPC,+ fatfs::Error::InvalidFileNameLength => Errno::ENAMETOOLONG,+ fatfs::Error::InvalidInput | fatfs::Error::UnsupportedFileNameCharacter => Errno::EINVAL,+ _ => Errno::EIO, // corrupted file system, unexpected EOF, ...+ }+}++// ---------------------------------------------------------------------------+// 2. The VFS side+// ---------------------------------------------------------------------------++type Fs = fatfs::FileSystem<BlockIo, fatfs::NullTimeProvider, fatfs::LossyOemCpConverter>;+type FatDir<'a> = fatfs::Dir<'a, BlockIo, fatfs::NullTimeProvider, fatfs::LossyOemCpConverter>;++/// A mounted FAT volume. The fatfs `FileSystem` sits behind one Mutex+/// (shared via Arc with every FatInode), so FAT operations are serialized.+pub struct FatFs {+ fs: Arc<Mutex<Fs>>,+}++impl FatFs {+ /// Read the boot sector of `dev` and mount it if it's FAT.+ pub fn mount(dev: Arc<dyn BlockDevice>) -> Result<Arc<FatFs>, Errno> {+ // No real-time clock driver yet, so new files get the FAT epoch date+ // (1980-01-01) via NullTimeProvider.+ let options = fatfs::FsOptions::new().time_provider(fatfs::NullTimeProvider::new());+ let fs = fatfs::FileSystem::new(BlockIo::new(dev), options).map_err(errno)?;+ Ok(Arc::new(FatFs { fs: Arc::new(Mutex::new(fs)) }))+ }++ /// The volume label (from `mkfs.vfat -n`).+ pub fn label(&self) -> String {+ String::from(self.fs.lock().volume_label().trim_end())+ }+}++impl FileSystem for FatFs {+ fn name(&self) -> &'static str {+ "fat32"+ }+ fn root(&self) -> Arc<dyn Inode> {+ Arc::new(FatInode { fs: self.fs.clone(), path: String::new(), kind: InodeKind::Directory })+ }+}++/// A file or directory on a FAT volume, identified by its path from the+/// volume root ("" = the root, "bin/hello" = a file).+pub struct FatInode {+ fs: Arc<Mutex<Fs>>,+ path: String,+ kind: InodeKind,+}++impl FatInode {+ fn child_path(&self, name: &str) -> String {+ if self.path.is_empty() { String::from(name) } else { format!("{}/{}", self.path, name) }+ }++ /// Run `f` on this inode's directory (re-opened from the root).+ fn with_dir<T>(&self, f: impl FnOnce(&FatDir) -> Result<T, Errno>) -> Result<T, Errno> {+ let fs = self.fs.lock();+ let root = fs.root_dir();+ if self.path.is_empty() {+ f(&root)+ } else {+ f(&root.open_dir(&self.path).map_err(errno)?)+ }+ }++ /// Run `f` on this inode's file (re-opened from the root).+ fn with_file<T>(&self, f: impl FnOnce(&mut fatfs::File<BlockIo, fatfs::NullTimeProvider, fatfs::LossyOemCpConverter>) -> Result<T, Errno>) -> Result<T, Errno> {+ if self.kind == InodeKind::Directory {+ return Err(Errno::EISDIR);+ }+ let fs = self.fs.lock();+ let mut file = fs.root_dir().open_file(&self.path).map_err(errno)?;+ f(&mut file)+ }+}++impl Inode for FatInode {+ fn kind(&self) -> InodeKind {+ self.kind+ }++ fn size(&self) -> usize {+ match self.kind {+ InodeKind::File => self.with_file(|f| f.seek(SeekFrom::End(0)).map_err(errno)).unwrap_or(0) as usize,+ InodeKind::Directory => self.list().map(|l| l.len()).unwrap_or(0),+ }+ }++ fn read_at(&self, offset: usize, buf: &mut [u8]) -> Result<usize, Errno> {+ self.with_file(|file| {+ let size = file.seek(SeekFrom::End(0)).map_err(errno)? as usize;+ if offset >= size {+ return Ok(0);+ }+ file.seek(SeekFrom::Start(offset as u64)).map_err(errno)?;+ let len = buf.len().min(size - offset);+ // fatfs reads at most one cluster per call; loop to fill buf.+ let mut done = 0;+ while done < len {+ match file.read(&mut buf[done..len]).map_err(errno)? {+ 0 => break,+ n => done += n,+ }+ }+ Ok(done)+ })+ }++ fn write_at(&self, offset: usize, buf: &[u8]) -> Result<usize, Errno> {+ self.with_file(|file| {+ let size = file.seek(SeekFrom::End(0)).map_err(errno)? as usize;+ if offset > size {+ // Writing past the end: fill the gap with zeros (FAT has no+ // sparse files).+ let zeros = [0u8; 512];+ let mut gap = offset - size;+ while gap > 0 {+ let n = gap.min(zeros.len());+ file.write_all(&zeros[..n]).map_err(errno)?;+ gap -= n;+ }+ } else {+ file.seek(SeekFrom::Start(offset as u64)).map_err(errno)?;+ }+ file.write_all(buf).map_err(errno)?;+ Ok(buf.len())+ })+ }++ fn truncate(&self) -> Result<(), Errno> {+ self.with_file(|file| file.truncate().map_err(errno)) // truncates at the position: 0+ }++ fn lookup(&self, name: &str) -> Result<Arc<dyn Inode>, Errno> {+ if self.kind != InodeKind::Directory {+ return Err(Errno::ENOTDIR);+ }+ let kind = self.with_dir(|dir| {+ for entry in dir.iter() {+ let entry = entry.map_err(errno)?;+ // FAT names are case-insensitive (README.TXT == readme.txt).+ if entry.file_name().eq_ignore_ascii_case(name) {+ return Ok(if entry.is_dir() { InodeKind::Directory } else { InodeKind::File });+ }+ }+ Err(Errno::ENOENT)+ })?;+ Ok(Arc::new(FatInode { fs: self.fs.clone(), path: self.child_path(name), kind }))+ }++ fn create(&self, name: &str, kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {+ if self.lookup(name).is_ok() {+ return Err(Errno::EEXIST);+ }+ self.with_dir(|dir| {+ match kind {+ InodeKind::File => dir.create_file(name).map(|_| ()),+ InodeKind::Directory => dir.create_dir(name).map(|_| ()),+ }+ .map_err(errno)+ })?;+ Ok(Arc::new(FatInode { fs: self.fs.clone(), path: self.child_path(name), kind }))+ }++ fn list(&self) -> Result<Vec<DirEntry>, Errno> {+ if self.kind != InodeKind::Directory {+ return Err(Errno::ENOTDIR);+ }+ self.with_dir(|dir| {+ let mut out = Vec::new();+ for entry in dir.iter() {+ let entry = entry.map_err(errno)?;+ let name = entry.file_name();+ if name == "." || name == ".." {+ continue; // the VFS doesn't list these (like our ramfs)+ }+ let kind = if entry.is_dir() { InodeKind::Directory } else { InodeKind::File };+ out.push(DirEntry { name, kind });+ }+ Ok(out)+ })+ }++ fn remove(&self, name: &str) -> Result<(), Errno> {+ if self.kind != InodeKind::Directory {+ return Err(Errno::ENOTDIR);+ }+ self.with_dir(|dir| dir.remove(name).map_err(errno))+ }+}
@@ -1,23 +1,65 @@ //! Open files: what a file descriptor refers to. //!-//! `File` is the interface the read/write syscalls use. It's separate from-//! `Inode` because the same inode can be open several times with different-//! offsets, and because some files aren't in any file system at all (the-//! console). Linux calls this `struct file` + `file_operations`.+//! `File` is the interface the read/write/lseek/fstat/getdents syscalls use.+//! It's separate from `Inode` because the same inode can be open several+//! times with different offsets, and because some files aren't in any file+//! system at all (the console, pipes). Linux calls this `struct file` ++//! `file_operations`. use super::{Inode, InodeKind}; use crate::errno::Errno; use alloc::sync::Arc; use spin::Mutex; +/// What `stat`/`fstat` return. Our own small layout (Linux's `struct stat`+/// has ~15 fields we have no use for yet). `#[repr(C)]` because it's copied+/// to user space as is; user/ulib.rs has the same struct.+#[repr(C)]+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]+pub struct Stat {+ pub kind: u64, // one of the KIND_* constants+ pub size: u64, // bytes (files), entries (directories)+}++pub const KIND_FILE: u64 = 1;+pub const KIND_DIR: u64 = 2;+pub const KIND_CONSOLE: u64 = 3;+pub const KIND_PIPE: u64 = 4;++impl Stat {+ pub fn of(inode: &dyn Inode) -> Stat {+ let kind = match inode.kind() {+ InodeKind::File => KIND_FILE,+ InodeKind::Directory => KIND_DIR,+ };+ Stat { kind, size: inode.size() as u64 }+ }+}++/// `whence` values for `seek`, as in Linux's lseek.+pub const SEEK_SET: u64 = 0;+pub const SEEK_CUR: u64 = 1;+pub const SEEK_END: u64 = 2;+ pub trait File: Send + Sync { /// Read up to `buf.len()` bytes; 0 means end of file. fn read(&self, buf: &mut [u8]) -> Result<usize, Errno>; /// Write bytes; returns how many were written. fn write(&self, buf: &[u8]) -> Result<usize, Errno>;+ /// Move the offset; returns the new offset. Consoles and pipes can't.+ fn seek(&self, _offset: i64, _whence: u64) -> Result<u64, Errno> {+ Err(Errno::ESPIPE)+ }+ fn stat(&self) -> Result<Stat, Errno>;+ /// Fill `buf` with Linux `dirent64` records for the next directory+ /// entries; returns bytes used, 0 at the end.+ fn getdents(&self, _buf: &mut [u8]) -> Result<usize, Errno> {+ Err(Errno::ENOTDIR)+ } } -/// A regular file opened through the VFS: inode + current position.+/// A file or directory opened through the VFS: inode + current position.+/// For directories the "position" is the index of the next entry to list. pub struct InodeFile { inode: Arc<dyn Inode>, offset: Mutex<usize>,@@ -25,9 +67,6 @@ pub struct InodeFile { impl InodeFile { pub fn open(inode: Arc<dyn Inode>) -> Result<Self, Errno> {- if inode.kind() == InodeKind::Directory {- return Err(Errno::EISDIR); // no getdents yet- } Ok(InodeFile { inode, offset: Mutex::new(0) }) } }@@ -46,4 +85,56 @@ impl File for InodeFile { *offset += n; Ok(n) }++ fn seek(&self, offset: i64, whence: u64) -> Result<u64, Errno> {+ if self.inode.kind() == InodeKind::Directory {+ return if offset == 0 && whence == SEEK_SET { *self.offset.lock() = 0; Ok(0) } else { Err(Errno::EINVAL) };+ }+ let mut current = self.offset.lock();+ let base = match whence {+ SEEK_SET => 0,+ SEEK_CUR => *current as i64,+ SEEK_END => self.inode.size() as i64,+ _ => return Err(Errno::EINVAL),+ };+ let new = base.checked_add(offset).filter(|&n| n >= 0).ok_or(Errno::EINVAL)?;+ *current = new as usize;+ Ok(new as u64)+ }++ fn stat(&self) -> Result<Stat, Errno> {+ Ok(Stat::of(&*self.inode))+ }++ fn getdents(&self, buf: &mut [u8]) -> Result<usize, Errno> {+ // Re-list the directory each call and skip what we've returned+ // already (simple; a real fs would keep a cursor into the directory).+ let entries = self.inode.list()?;+ let mut index = self.offset.lock();+ let mut used = 0;+ for entry in entries.iter().skip(*index) {+ // struct linux_dirent64 { u64 ino; i64 off; u16 reclen; u8 type; char name[]; }+ // padded to a multiple of 8 bytes.+ let reclen = (19 + entry.name.len() + 1).next_multiple_of(8);+ if used + reclen > buf.len() {+ if used == 0 {+ return Err(Errno::EINVAL); // buffer too small for even one entry+ }+ break;+ }+ let rec = &mut buf[used..used + reclen];+ rec.fill(0);+ rec[0..8].copy_from_slice(&(*index as u64 + 1).to_le_bytes()); // fake inode number+ rec[8..16].copy_from_slice(&(*index as i64 + 1).to_le_bytes()); // offset of next+ rec[16..18].copy_from_slice(&(reclen as u16).to_le_bytes());+ rec[18] = match entry.kind {+ InodeKind::Directory => 4, // DT_DIR+ InodeKind::File => 8, // DT_REG+ };+ rec[19..19 + entry.name.len()].copy_from_slice(entry.name.as_bytes());+ used += reclen;+ *index += 1;+ }+ Ok(used)+ } }
@@ -3,26 +3,31 @@ //! Modelled (very loosely) on Linux: //! //! * [`Inode`] -- one file or directory *inside some file system*. Knows how to-//! read/write its bytes at an offset and (for directories) look up / create-//! children. A FAT32, ext2, exFAT or btrfs driver implements this trait.-//! * [`FileSystem`] -- a mounted instance (e.g. "the FAT32 volume on disk 0");+//! read/write its bytes at an offset and (for directories) look up, create,+//! list and remove children. ramfs and FAT32 implement it today; ext2, exFAT+//! and btrfs will later.+//! * [`FileSystem`] -- a mounted instance (e.g. "the FAT32 volume on vda1"); //! gives you its root inode. //! * the **mount table** -- which file system is attached at which path ("/",-//! "/tmp", ...). `lookup("/tmp/x")` finds the longest matching mount point-//! and walks the rest of the path from that file system's root.+//! "/tmp", "/mnt/disk0"). `lookup("/tmp/x")` finds the longest matching+//! mount point and walks the rest of the path from that file system's root. //! * [`file::File`] -- an *open* file (what a file descriptor points to): an-//! inode plus a current offset, or a device like the console. The per-process+//! inode plus a current offset, or the console, or a pipe. The per-process //! fd table holds `Arc<dyn File>`s, so syscalls don't care what's behind it. //!-//! Today there is one implementation, [`ramfs`] (everything in RAM). Later: a-//! block device layer (virtio-blk / ATA PIO), then FAT32, ext2, exFAT and-//! btrfs (read-only first) as further `FileSystem`s.+//! Paths: every path is first turned into a clean absolute path by+//! [`normalize`] (relative paths are joined to the process's current+//! directory; ".", ".." and "//" are resolved by plain string processing).+//! That makes ".." work across mount points for free -- real kernels track+//! parent dentries instead, which also handles symlinks (we have none). +pub mod fat; pub mod file;+pub mod pipe; pub mod ramfs; use crate::errno::Errno;-use alloc::{string::String, sync::Arc, vec::Vec};+use alloc::{format, string::String, sync::Arc, vec::Vec}; use spin::Mutex; #[derive(Debug, Clone, Copy, PartialEq, Eq)]@@ -31,6 +36,13 @@ pub enum InodeKind { Directory, } +/// One entry of a directory listing.+#[derive(Debug, Clone, PartialEq, Eq)]+pub struct DirEntry {+ pub name: String,+ pub kind: InodeKind,+}+ /// A file or directory inside a file system. /// /// Default method bodies return the error a "wrong kind" of inode would give@@ -48,6 +60,10 @@ pub trait Inode: Send + Sync { fn write_at(&self, _offset: usize, _buf: &[u8]) -> Result<usize, Errno> { Err(Errno::EISDIR) }+ /// Cut the file to length 0 (for `open(..., O_TRUNC)`, i.e. shell `>`).+ fn truncate(&self) -> Result<(), Errno> {+ Err(Errno::EISDIR)+ } /// Find a child of this directory by name. fn lookup(&self, _name: &str) -> Result<Arc<dyn Inode>, Errno> { Err(Errno::ENOTDIR)@@ -56,8 +72,12 @@ pub trait Inode: Send + Sync { fn create(&self, _name: &str, _kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> { Err(Errno::ENOTDIR) }- /// Names of all children of this directory.- fn list(&self) -> Result<Vec<String>, Errno> {+ /// All children of this directory (without "." and "..").+ fn list(&self) -> Result<Vec<DirEntry>, Errno> {+ Err(Errno::ENOTDIR)+ }+ /// Delete a child (a file, or an *empty* directory).+ fn remove(&self, _name: &str) -> Result<(), Errno> { Err(Errno::ENOTDIR) } }@@ -78,12 +98,12 @@ struct Mount { /// I/O), so it's fine as a spinlock. Not used from interrupt handlers. static MOUNTS: Mutex<Vec<Mount>> = Mutex::new(Vec::new()); -/// Attach `fs` at `path` (must be absolute, like "/" or "/tmp").+/// Attach `fs` at `path` (absolute, like "/" or "/tmp"). pub fn mount(path: &str, fs: Arc<dyn FileSystem>) -> Result<(), Errno> { if !path.starts_with('/') { return Err(Errno::EINVAL); }- let path = String::from(if path.len() > 1 { path.trim_end_matches('/') } else { path });+ let path = normalize("/", path)?; let mut mounts = MOUNTS.lock(); if mounts.iter().any(|m| m.path == path) { return Err(Errno::EEXIST);@@ -105,45 +125,107 @@ fn mount_contains(mount: &str, path: &str) -> bool { || (path.starts_with(mount) && path.as_bytes().get(mount.len()) == Some(&b'/')) } +/// Turn `path` into a clean absolute path. Relative paths start at `cwd`+/// (which must be absolute). "." is dropped, ".." removes the previous+/// component (".." of "/" is "/", like Unix).+/// normalize("/home", "../etc//motd") == "/etc/motd"+pub fn normalize(cwd: &str, path: &str) -> Result<String, Errno> {+ if path.is_empty() {+ return Err(Errno::ENOENT); // like Linux: "" is not a path+ }+ let mut parts: Vec<&str> = Vec::new();+ let joined = if path.starts_with('/') { String::from(path) } else { format!("{}/{}", cwd, path) };+ for component in joined.split('/') {+ match component {+ "" | "." => {}+ ".." => {+ parts.pop();+ }+ name if name.len() > 255 => return Err(Errno::ENAMETOOLONG),+ name => parts.push(name),+ }+ }+ let result = format!("/{}", parts.join("/"));+ if result.len() > 4096 {+ return Err(Errno::ENAMETOOLONG);+ }+ Ok(result)+}+ /// Resolve an absolute path to an inode. pub fn lookup(path: &str) -> Result<Arc<dyn Inode>, Errno> { if !path.starts_with('/') {- return Err(Errno::EINVAL); // no current directory yet- }- if path.len() > 255 {- return Err(Errno::ENAMETOOLONG);+ return Err(Errno::EINVAL); // callers resolve relative paths first }+ let path = normalize("/", path)?; // Longest mount point that contains the path wins. let (root, rest) = { let mounts = MOUNTS.lock(); let m = mounts .iter()- .filter(|m| mount_contains(&m.path, path))+ .filter(|m| mount_contains(&m.path, &path)) .max_by_key(|m| m.path.len()) .ok_or(Errno::ENOENT)?;- let rest = if m.path == "/" { path } else { &path[m.path.len()..] };+ let rest = String::from(if m.path == "/" { &path[..] } else { &path[m.path.len()..] }); (m.fs.root(), rest) }; // Walk the remaining components ("a/b/c") from that file system's root. let mut inode = root;- for name in rest.split('/').filter(|c| !c.is_empty() && *c != ".") {- if name == ".." {- return Err(Errno::EINVAL); // not supported yet- }+ for name in rest.split('/').filter(|c| !c.is_empty()) { inode = inode.lookup(name)?; } Ok(inode) } -/// Create a file at `path` (its parent directory must exist).-pub fn create(path: &str, kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {- let path = path.trim_end_matches('/');+/// Split a normalized path into (parent directory inode, last component).+fn parent_of(path: &str) -> Result<(Arc<dyn Inode>, String), Errno> {+ let path = normalize("/", path)?; let (parent, name) = path.rsplit_once('/').ok_or(Errno::EINVAL)?; if name.is_empty() {- return Err(Errno::EINVAL);+ return Err(Errno::EEXIST); // "/" itself }- let parent = lookup(if parent.is_empty() { "/" } else { parent })?;- parent.create(name, kind)+ Ok((lookup(if parent.is_empty() { "/" } else { parent })?, String::from(name)))+}++/// Create a file or directory at absolute `path` (parent must exist).+pub fn create(path: &str, kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {+ let (parent, name) = parent_of(path)?;+ parent.create(&name, kind)+}++/// Delete the file (`want_dir` false, like `unlink`) or empty directory+/// (`want_dir` true, like `rmdir`) at absolute `path`.+pub fn remove(path: &str, want_dir: bool) -> Result<(), Errno> {+ let normalized = normalize("/", path)?;+ if MOUNTS.lock().iter().any(|m| m.path == normalized) {+ return Err(Errno::EBUSY); // can't delete a mount point+ }+ let inode = lookup(&normalized)?;+ match (inode.kind(), want_dir) {+ (InodeKind::Directory, false) => return Err(Errno::EISDIR),+ (InodeKind::File, true) => return Err(Errno::ENOTDIR),+ _ => {}+ }+ let (parent, name) = parent_of(&normalized)?;+ parent.remove(&name)+}++/// Read a whole file into memory (used to load programs).+pub fn read_all(path: &str) -> Result<Vec<u8>, Errno> {+ let inode = lookup(path)?;+ if inode.kind() == InodeKind::Directory {+ return Err(Errno::EISDIR);+ }+ let mut data = alloc::vec![0u8; inode.size()];+ let mut done = 0;+ while done < data.len() {+ match inode.read_at(done, &mut data[done..])? {+ 0 => break,+ n => done += n,+ }+ }+ data.truncate(done);+ Ok(data) } /// Boot-time file system setup: a ramfs as "/", with a few files, and a@@ -155,8 +237,39 @@ pub fn init() { hello.write_at(0, b"Hello from the ramfs! This is /hello.txt.\n").unwrap(); let etc = dir.create("etc", InodeKind::Directory).unwrap(); let motd = etc.create("motd", InodeKind::File).unwrap();- motd.write_at(0, b"Welcome to Tyler's kernel. Next up: FAT32, ext2, exFAT, btrfs.\n").unwrap();+ motd.write_at(0, b"Welcome to Tyler's kernel. Next up: ext2, exFAT, btrfs.\n").unwrap(); dir.create("tmp", InodeKind::Directory).unwrap(); // mount point+ dir.create("mnt", InodeKind::Directory).unwrap(); // disks go to /mnt/diskN mount("/", root).unwrap(); mount("/tmp", ramfs::RamFs::new()).unwrap(); }++/// Try to mount every registered partition (and unpartitioned disk) that+/// holds a FAT file system at /mnt/disk0, /mnt/disk1, ... Returns log lines.+/// (Later: also probe for ext2, exFAT, btrfs superblocks here.)+pub fn mount_disks() -> Vec<String> {+ let mut log = Vec::new();+ let devices = crate::block::devices();+ let mut n = 0;+ for (name, dev) in &devices {+ // Skip a whole disk if it has partitions ("vda" when "vda1" exists).+ let has_partitions = devices.iter().any(|(other, _)| other.len() > name.len() && other.starts_with(name.as_str()));+ if has_partitions {+ continue;+ }+ match fat::FatFs::mount(dev.clone()) {+ Ok(fs) => {+ let path = format!("/mnt/disk{}", n);+ let _ = create(&path, InodeKind::Directory); // mount point (may exist)+ let label = fs.label();+ match mount(&path, fs) {+ Ok(()) => log.push(format!("{}: FAT32 volume \"{}\" mounted at {}", name, label, path)),+ Err(e) => log.push(format!("{}: mount at {} failed: {:?}", name, path, e)),+ }+ n += 1;+ }+ Err(e) => log.push(format!("{}: not a FAT file system ({:?})", name, e)),+ }+ }+ log+}
@@ -0,0 +1,102 @@+//! Pipes: a byte queue with a read end and a write end, each a `File`.+//! `pipe()` puts both in the caller's fd table; the shell will hand the+//! write end to one program as stdout and the read end to the next as stdin.+//!+//! Semantics like Unix: reading an empty pipe *blocks* until data arrives or+//! every write end is closed (then read returns 0 = EOF); writing when every+//! read end is closed fails with EPIPE. Closing = dropping the last `Arc`+//! of an end (fd closed, process exited), which our `Drop` impls notice.+//! Simplification: the buffer is unbounded, so writers never block.++use super::file::{File, KIND_PIPE, Stat};+use crate::errno::Errno;+use crate::thread::WaitQueue;+use alloc::{collections::VecDeque, sync::Arc};+use spin::Mutex;+use x86_64::instructions::interrupts::without_interrupts;++struct Pipe {+ // Locked with interrupts off only (it's touched in `wait_until`'s+ // condition, see the locking rule in thread.rs).+ state: Mutex<PipeState>,+ readers_wait: WaitQueue,+}++struct PipeState {+ buffer: VecDeque<u8>,+ write_closed: bool,+ read_closed: bool,+}++pub struct PipeReader(Arc<Pipe>);+pub struct PipeWriter(Arc<Pipe>);++/// A new pipe: (read end, write end).+pub fn new() -> (PipeReader, PipeWriter) {+ let pipe = Arc::new(Pipe {+ state: Mutex::new(PipeState { buffer: VecDeque::new(), write_closed: false, read_closed: false }),+ readers_wait: WaitQueue::new(),+ });+ (PipeReader(pipe.clone()), PipeWriter(pipe))+}++impl File for PipeReader {+ fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {+ if buf.is_empty() {+ return Ok(0);+ }+ // Block until there's data or no writer is left.+ Ok(self.0.readers_wait.wait_until(|| {+ let mut state = self.0.state.lock();+ if state.buffer.is_empty() {+ return if state.write_closed { Some(0) } else { None };+ }+ let n = buf.len().min(state.buffer.len());+ for (dst, src) in buf.iter_mut().zip(state.buffer.drain(..n)) {+ *dst = src;+ }+ Some(n)+ }))+ }+ fn write(&self, _buf: &[u8]) -> Result<usize, Errno> {+ Err(Errno::EBADF) // wrong end+ }+ fn stat(&self) -> Result<Stat, Errno> {+ let size = without_interrupts(|| self.0.state.lock().buffer.len());+ Ok(Stat { kind: KIND_PIPE, size: size as u64 })+ }+}++impl File for PipeWriter {+ fn read(&self, _buf: &mut [u8]) -> Result<usize, Errno> {+ Err(Errno::EBADF)+ }+ fn write(&self, buf: &[u8]) -> Result<usize, Errno> {+ without_interrupts(|| {+ let mut state = self.0.state.lock();+ if state.read_closed {+ return Err(Errno::EPIPE);+ }+ state.buffer.extend(buf);+ Ok(buf.len())+ })?;+ self.0.readers_wait.wake_all();+ Ok(buf.len())+ }+ fn stat(&self) -> Result<Stat, Errno> {+ Ok(Stat { kind: KIND_PIPE, size: 0 })+ }+}++impl Drop for PipeWriter {+ fn drop(&mut self) {+ without_interrupts(|| self.0.state.lock().write_closed = true);+ self.0.readers_wait.wake_all(); // readers see EOF+ }+}++impl Drop for PipeReader {+ fn drop(&mut self) {+ without_interrupts(|| self.0.state.lock().read_closed = true);+ }+}
@@ -2,7 +2,7 @@ //! tmpfs without the size limit). The simplest possible `FileSystem`: a tree //! of nodes; files are a `Vec<u8>`, directories a sorted map name -> node. -use super::{FileSystem, Inode, InodeKind};+use super::{DirEntry, FileSystem, Inode, InodeKind}; use crate::errno::Errno; use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; use spin::Mutex;@@ -91,8 +91,28 @@ impl Inode for RamInode { Ok(node) } - fn list(&self) -> Result<Vec<String>, Errno> {+ fn list(&self) -> Result<Vec<DirEntry>, Errno> { let RamInode::Dir(entries) = self else { return Err(Errno::ENOTDIR) };- Ok(entries.lock().keys().cloned().collect())+ let entries = entries.lock();+ Ok(entries.iter().map(|(name, node)| DirEntry { name: name.clone(), kind: node.kind() }).collect())+ }++ fn truncate(&self) -> Result<(), Errno> {+ let RamInode::File(data) = self else { return Err(Errno::EISDIR) };+ data.lock().clear();+ Ok(())+ }++ fn remove(&self, name: &str) -> Result<(), Errno> {+ let RamInode::Dir(entries) = self else { return Err(Errno::ENOTDIR) };+ let mut entries = entries.lock();+ let node = entries.get(name).ok_or(Errno::ENOENT)?;+ if let RamInode::Dir(children) = &**node {+ if !children.lock().is_empty() {+ return Err(Errno::ENOTEMPTY);+ }+ }+ entries.remove(name);+ Ok(()) } }
@@ -22,12 +22,14 @@ extern crate alloc; use core::panic::PanicInfo; pub mod allocator; // heap: maps heap pages + the #[global_allocator]+pub mod block; // block devices: BlockDevice trait, virtio-blk, partitions pub mod elf; // ELF64 loader for user programs pub mod errno; // Linux-compatible error numbers pub mod fs; // VFS: Inode/FileSystem/File traits, mount table, ramfs pub mod gdt; // GDT (kernel + user segments) + TSS (RSP0, double-fault stack) pub mod interrupts; // IDT, exception handlers, PIC, timer + keyboard pub mod memory; // paging, frame allocator, per-process address spaces+pub mod pci; // PCI bus enumeration (config space via ports 0xCF8/0xCFC) pub mod process; // processes: address space + thread + fd table, ring 3 entry pub mod serial; // COM1 serial port pub mod syscall; // syscall/sysret entry + system call implementations@@ -36,14 +38,6 @@ pub mod thread; // preemptive kernel threads + context switch pub mod tty; // console: keyboard line discipline + fd 0/1/2 pub mod vga_buffer; // 80x25 VGA text mode screen -/// The user programs (user/*.rs), compiled to ELF by build.rs and embedded.-pub mod user_programs {- pub static HELLO: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/hello"));- pub static EVIL: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/evil"));- pub static SPIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/spin"));- pub static ECHO: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/echo"));-}- /// Set up everything interrupt-related. Order matters: /// GDT/TSS first (the IDT refers to the IST stack), then the IDT, then the /// PICs, and only then enable interrupts (`sti`).@@ -55,6 +49,26 @@ pub fn init() { x86_64::instructions::interrupts::enable(); } +/// Paging + heap + the global frame allocator, in one call (what the test+/// kernels need; src/main.rs does the same steps one by one with demos).+pub fn init_memory(boot_info: &'static bootloader::BootInfo) {+ let phys_mem_offset = x86_64::VirtAddr::new(boot_info.physical_memory_offset);+ let mut mapper = unsafe { memory::init(phys_mem_offset) };+ let mut frame_allocator = unsafe { memory::BootInfoFrameAllocator::init(&boot_info.memory_map) };+ allocator::init_heap(&mut mapper, &mut frame_allocator).expect("heap initialization failed");+ memory::init_frame_allocator(frame_allocator);+}++/// Storage bring-up: scan the PCI bus, start the disk drivers, read the+/// partition tables and mount FAT volumes under /mnt. Needs the heap, the+/// frame allocator and `fs::init()`. Returns (PCI devices, log lines).+pub fn init_storage() -> (alloc::vec::Vec<pci::PciDevice>, alloc::vec::Vec<alloc::string::String>) {+ let devices = pci::scan();+ let mut log = block::init(&devices);+ log.extend(fs::mount_disks());+ (devices, log)+}+ /// Print a line to *both* the VGA screen and the serial port. #[macro_export] macro_rules! kprintln {
@@ -15,7 +15,7 @@ use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo; use rust_kernel::task::{Task, executor::Executor, keyboard}; use rust_kernel::{allocator, fs, kprintln, memory, println, process, serial_println, thread};-use rust_kernel::{user_programs, vga_buffer};+use rust_kernel::vga_buffer; use x86_64::{VirtAddr, structures::paging::{Page, Translate}}; // `entry_point!` defines the real `_start` for us and checks at compile time@@ -48,22 +48,41 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { thread::spawn("C", thread_c); kprintln!("[thread] spawned threads A, B, C (status on the top line)"); - // --- VFS + user mode (fs/, process.rs, syscall.rs) ----------------------+ // --- VFS, PCI, disks (fs/, pci.rs, block/) ------------------------------ fs::init();+ let (pci_devices, storage_log) = rust_kernel::init_storage();+ for d in &pci_devices {+ serial_println!(+ "[pci] {:02x}:{:02x}.{} {:04x}:{:04x} {}",+ d.bus, d.device, d.function, d.vendor_id, d.device_id, d.class_name()+ );+ }+ kprintln!("[pci] {} devices on the PCI bus", pci_devices.len());+ for line in &storage_log {+ kprintln!("[disk] {}", line);+ } for (path, fs_type) in fs::mounts() { serial_println!("[vfs] mounted {} at {}", fs_type, path); }- // Each program is an ELF file built from user/*.rs and embedded in the- // kernel. Each gets its own address space and runs in ring 3.- let programs: [(&'static str, &'static [u8]); 4] = [- ("hello", user_programs::HELLO), // syscalls + ramfs demo, exits 0- ("evil", user_programs::EVIL), // touches kernel memory -> killed- ("spin", user_programs::SPIN), // busy loop, gets preempted- ("echo", user_programs::ECHO), // reads keyboard lines via read(0)++ // --- User mode (process.rs, syscall.rs) ---------------------------------+ // The programs (built from user/*.rs) are ELF files on the FAT32 disk.+ // Each gets its own address space and runs in ring 3.+ const BIN: &str = "/mnt/disk0/bin";+ if fs::lookup(BIN).is_err() {+ kprintln!("[process] no {} -- was QEMU started with the disk image?", BIN);+ }+ let programs = [+ "hello", // syscalls + ramfs demo, exits 0+ "evil", // touches kernel memory -> killed+ "spin", // busy loop, gets preempted+ "demo", // stage 6: FAT file, cwd, getdents, spawn + wait, pipes+ "echo", // reads keyboard lines via read(0) ];- for (name, elf) in programs {- if let Err(e) = process::spawn(name, elf) {- kprintln!("[process] could not start {}: {:?}", name, e);+ for name in programs {+ let path = alloc::format!("{}/{}", BIN, name);+ if let Err(e) = process::spawn_path(&path, process::SpawnArgs::kernel(name)) {+ kprintln!("[process] could not start {}: {:?}", path, e); } }
@@ -170,6 +170,28 @@ pub fn nx_enabled() -> bool { Efer::read().contains(EferFlags::NO_EXECUTE_ENABLE) } +/// `count` physically *contiguous*, zeroed frames (device DMA buffers like+/// virtio queues must be contiguous in physical memory). Our allocator hands+/// out frames in address order, so consecutive frames are usually adjacent;+/// if a run gets broken (end of a memory region) we start over and simply+/// leak the frames we skipped. Fine for a few boot-time allocations.+pub fn alloc_contiguous_frames(count: usize) -> Option<PhysAddr> {+ let mut start = alloc_frame()?.start_address();+ let mut got = 1;+ while got < count {+ let frame = alloc_frame()?.start_address();+ if frame == start + (got as u64) * 4096 {+ got += 1;+ } else {+ start = frame; // not adjacent: restart the run here+ got = 1;+ }+ }+ let ptr: *mut u8 = phys_to_virt(start).as_mut_ptr();+ unsafe { core::ptr::write_bytes(ptr, 0, count * 4096) };+ Some(start)+}+ /// A frame filled with zeros (new page tables and user pages must not leak /// old data). fn alloc_zeroed_frame() -> Option<PhysFrame> {
@@ -0,0 +1,121 @@+//! PCI bus enumeration (the legacy "configuration mechanism #1").+//!+//! Every PCI function has a 256-byte *configuration space*: vendor/device+//! ids, class code, BARs (where its registers live), the command register...+//! On x86 we reach it through two I/O ports: write the address+//! (bus, device, function, register) to 0xCF8, then read/write the data at+//! 0xCFC. Enumerating = asking every possible (bus, device, function) for its+//! vendor id; 0xFFFF means "nobody here".+//!+//! Modern machines also offer memory-mapped config space (PCIe ECAM, found+//! via ACPI's MCFG table); the port method is simpler and QEMU supports both.+//! Reference: https://wiki.osdev.org/PCI++use alloc::vec::Vec;+use x86_64::instructions::port::Port;++const CONFIG_ADDRESS: u16 = 0xCF8;+const CONFIG_DATA: u16 = 0xCFC;++#[derive(Debug, Clone, Copy)]+pub struct PciDevice {+ pub bus: u8,+ pub device: u8,+ pub function: u8,+ pub vendor_id: u16,+ pub device_id: u16,+ pub class: u8,+ pub subclass: u8,+}++/// Read a 32-bit register (`offset` must be 4-byte aligned).+pub fn read_config(bus: u8, device: u8, function: u8, offset: u8) -> u32 {+ let address = 1u32 << 31 // enable bit+ | (bus as u32) << 16+ | (device as u32) << 11+ | (function as u32) << 8+ | (offset as u32 & 0xfc);+ unsafe {+ Port::<u32>::new(CONFIG_ADDRESS).write(address);+ Port::<u32>::new(CONFIG_DATA).read()+ }+}++pub fn write_config(bus: u8, device: u8, function: u8, offset: u8, value: u32) {+ let address = 1u32 << 31 | (bus as u32) << 16 | (device as u32) << 11 | (function as u32) << 8 | (offset as u32 & 0xfc);+ unsafe {+ Port::<u32>::new(CONFIG_ADDRESS).write(address);+ Port::<u32>::new(CONFIG_DATA).write(value);+ }+}++impl PciDevice {+ fn read(&self, offset: u8) -> u32 {+ read_config(self.bus, self.device, self.function, offset)+ }++ /// Base address register `n` (0-5), raw. Bit 0 set = I/O port space.+ pub fn bar(&self, n: u8) -> u32 {+ self.read(0x10 + 4 * n)+ }++ /// Let the device respond to I/O port accesses and do DMA (bus mastering).+ pub fn enable_io_and_bus_master(&self) {+ let command = self.read(0x04);+ write_config(self.bus, self.device, self.function, 0x04, command | 0b101);+ }++ /// Human-readable class (just the common ones).+ pub fn class_name(&self) -> &'static str {+ match (self.class, self.subclass) {+ (0x01, 0x01) => "IDE controller",+ (0x01, 0x00) => "SCSI storage (virtio-blk)",+ (0x01, 0x06) => "SATA controller",+ (0x01, _) => "storage controller",+ (0x02, _) => "network controller",+ (0x03, _) => "display controller",+ (0x06, 0x00) => "host bridge",+ (0x06, 0x01) => "ISA bridge",+ (0x06, 0x80) => "bridge (other)",+ (0x06, _) => "bridge",+ (0x0c, 0x03) => "USB controller",+ _ => "other",+ }+ }+}++fn probe(bus: u8, device: u8, function: u8) -> Option<PciDevice> {+ let id = read_config(bus, device, function, 0x00);+ let vendor_id = id as u16;+ if vendor_id == 0xffff {+ return None; // no such device+ }+ let class = read_config(bus, device, function, 0x08);+ Some(PciDevice {+ bus,+ device,+ function,+ vendor_id,+ device_id: (id >> 16) as u16,+ class: (class >> 24) as u8,+ subclass: (class >> 16) as u8,+ })+}++/// Every PCI function on every bus (brute force: 256 buses x 32 devices --+/// a few thousand port reads, instant in QEMU).+pub fn scan() -> Vec<PciDevice> {+ let mut found = Vec::new();+ for bus in 0..=255u8 {+ for device in 0..32u8 {+ let Some(first) = probe(bus, device, 0) else { continue };+ found.push(first);+ // Header type bit 7: multi-function device (functions 1-7 exist).+ let header_type = (read_config(bus, device, 0, 0x0c) >> 16) as u8;+ if header_type & 0x80 != 0 {+ found.extend((1..8).filter_map(|f| probe(bus, device, f)));+ }+ }+ }+ found+}
@@ -1,29 +1,42 @@-//! Processes = a thread + its own address space + a file descriptor table.+//! Processes = a thread + its own address space + a file descriptor table+//! (+ arguments, a current directory and a parent). //!-//! Starting one (`spawn`):+//! Starting one (`spawn_image`): //! 1. new level-4 page table sharing the kernel mappings (memory.rs) //! 2. load the ELF program's segments into its user part (elf.rs) //! 3. map a user stack just below `USER_END`-//! 4. fd table: 0, 1, 2 = the console (tty.rs)-//! 5. spawn a thread with that CR3. The thread starts in the kernel-//! (`user_thread_entry`) and "returns" into ring 3 with `iretq`.+//! 4. fd table: 0, 1, 2 = whatever the parent chose (console by default,+//! or e.g. a pipe -- that's how the shell will do `a | b` and `> file`)+//! 5. put it in the process table and spawn a thread with that CR3. The+//! thread starts in the kernel (`user_thread_entry`), copies the+//! arguments onto the user stack and "returns" into ring 3 with `iretq`. //!-//! From then on the process only comes back into the kernel through-//! interrupts (timer -> maybe another thread runs), exceptions (page fault in-//! user mode -> the process is killed, see interrupts.rs) and syscalls.+//! Why `spawn(path, argv)` instead of Unix `fork()` + `exec()`? fork copies+//! the whole address space (or needs copy-on-write page tables to be fast),+//! and then exec throws the copy away again. spawn ("create a new process+//! running this program") is one simple step, and it's what Windows+//! (CreateProcess) and POSIX's own `posix_spawn` offer. Redirection, which+//! Unix shells do between fork and exec, is done with spawn's stdio options.+//!+//! Exit and wait: a process's exit status is stored in it and its `exited`+//! wait queue is woken; the parent's `wait(pid)` blocks on that queue, then+//! removes the child from the process table ("reaps" it -- until then it's+//! what Unix calls a zombie). //! //! Not done yet (exercises): freeing a dead process's frames and page tables,-//! fork/exec/wait syscalls, passing arguments, more than one thread per process.+//! reparenting orphans, signals/kill, more than one thread per process. use crate::elf::{self, ElfError}; use crate::errno::Errno;-use crate::fs::file::File;+use crate::fs::{self, file::File}; use crate::memory::{self, USER_END};-use crate::{serial_println, thread, tty::Console};-use alloc::{sync::Arc, vec, vec::Vec};+use crate::thread::{self, WaitQueue};+use crate::{serial_println, tty::Console};+use alloc::{boxed::Box, collections::BTreeMap, string::String, sync::Arc, vec, vec::Vec}; use core::sync::atomic::{AtomicU64, Ordering}; use spin::Mutex; use x86_64::VirtAddr;+use x86_64::instructions::interrupts::without_interrupts; use x86_64::structures::paging::{Page, PageTableFlags, Size4KiB}; /// The user stack: 16 KiB, ending one (unmapped) page below USER_END.@@ -31,6 +44,11 @@ pub const USER_STACK_TOP: u64 = USER_END - 0x1000; pub const USER_STACK_SIZE: u64 = 16 * 1024; /// Max open files per process. const MAX_FDS: usize = 16;+/// Max total size of a program's arguments (they go on its 16 KiB stack).+pub const MAX_ARGS_SIZE: usize = 2048;+/// Exit code `wait` reports for a process the kernel killed (like a shell+/// shows 128 + SIGSEGV(11) for a segfault).+pub const KILLED_EXIT_CODE: i64 = 139; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ExitStatus {@@ -40,18 +58,77 @@ pub enum ExitStatus { Killed(&'static str), } +impl ExitStatus {+ /// The number `wait` returns.+ pub fn code(self) -> i64 {+ match self {+ ExitStatus::Exited(code) => code,+ ExitStatus::Killed(_) => KILLED_EXIT_CODE,+ }+ }+}+ pub struct Process { pub pid: u64,- pub name: &'static str,+ /// Parent pid; 0 = started by the kernel itself.+ pub parent: u64,+ pub name: String,+ /// Print every syscall this process makes (`spawn` flag; `run --trace`).+ pub trace: bool,+ /// Current directory (always a normalized absolute path).+ pub cwd: Mutex<String>, entry: u64,+ args: Vec<String>, fds: Mutex<Vec<Option<Arc<dyn File>>>>,+ /// Locked with interrupts off only (it's read in a wait_until condition). status: Mutex<Option<ExitStatus>>,+ exited: WaitQueue, } static NEXT_PID: AtomicU64 = AtomicU64::new(1);+/// All processes that haven't been reaped yet, by pid.+static PROCESSES: Mutex<BTreeMap<u64, Arc<Process>>> = Mutex::new(BTreeMap::new());++/// Everything needed to start a process besides the program itself.+pub struct SpawnArgs {+ pub args: Vec<String>, // argv; args[0] is the program name by convention+ pub stdio: [Arc<dyn File>; 3],+ pub cwd: String,+ pub parent: u64,+ pub trace: bool,+}++impl SpawnArgs {+ /// Defaults for a process started by the kernel: console stdio, cwd "/".+ pub fn kernel(name: &str) -> SpawnArgs {+ let console: Arc<dyn File> = Arc::new(Console);+ SpawnArgs {+ args: vec![String::from(name)],+ stdio: [console.clone(), console.clone(), console],+ cwd: String::from("/"),+ parent: 0,+ trace: false,+ }+ }+}++/// Load the program at `path` (absolute) from the VFS and start it.+pub fn spawn_path(path: &str, spawn: SpawnArgs) -> Result<Arc<Process>, Errno> {+ let image = fs::read_all(path)?; // ENOENT, EISDIR, EIO, ...+ let name = path.rsplit('/').next().unwrap_or(path);+ spawn_image(name, &image, spawn).map_err(|e| match e {+ ElfError::OutOfMemory => Errno::ENOMEM,+ _ => Errno::ENOEXEC, // "Exec format error"+ })+}++/// Start ELF program `elf_image` from the kernel (console stdio, no args).+pub fn spawn(name: &str, elf_image: &[u8]) -> Result<Arc<Process>, ElfError> {+ spawn_image(name, elf_image, SpawnArgs::kernel(name))+} -/// Load ELF program `elf` into a new address space and start it.-pub fn spawn(name: &'static str, elf_image: &[u8]) -> Result<Arc<Process>, ElfError> {+/// Load `elf_image` into a new address space and start it.+pub fn spawn_image(name: &str, elf_image: &[u8], spawn: SpawnArgs) -> Result<Arc<Process>, ElfError> { let l4 = memory::new_address_space().ok_or(ElfError::OutOfMemory)?; let entry = elf::load(elf_image, l4)?; @@ -65,16 +142,24 @@ pub fn spawn(name: &'static str, elf_image: &[u8]) -> Result<Arc<Process>, ElfEr memory::map_user_page(l4, page, stack_flags).ok_or(ElfError::OutOfMemory)?; } - let console: Arc<dyn File> = Arc::new(Console);+ let [stdin, stdout, stderr] = spawn.stdio; let process = Arc::new(Process { pid: NEXT_PID.fetch_add(1, Ordering::Relaxed),- name,+ parent: spawn.parent,+ name: String::from(name),+ trace: spawn.trace,+ cwd: Mutex::new(spawn.cwd), entry,- fds: Mutex::new(vec![Some(console.clone()), Some(console.clone()), Some(console)]),+ args: spawn.args,+ fds: Mutex::new(vec![Some(stdin), Some(stdout), Some(stderr)]), status: Mutex::new(None),+ exited: WaitQueue::new(), });+ PROCESSES.lock().insert(process.pid, process.clone()); serial_println!("[process] started pid {} ({}), entry {:#x}", process.pid, name, entry);- thread::spawn_with(name, user_thread_entry, l4, Some(process.clone()));+ // Thread names are &'static str; leaking a few bytes per process is fine here.+ let thread_name: &'static str = Box::leak(String::from(name).into_boxed_str());+ thread::spawn_with(thread_name, user_thread_entry, l4, Some(process.clone())); Ok(process) } @@ -82,15 +167,37 @@ pub fn spawn(name: &'static str, elf_image: &[u8]) -> Result<Arc<Process>, ElfEr /// with the process's CR3 and kernel stack). fn user_thread_entry() { let process = thread::current_process().expect("process thread without process");+ // Copy the arguments onto the top of the user stack (we can write there+ // directly: this process's address space is active). Layout, top down:+ // the argument strings' bytes (not NUL-terminated)+ // argv: [ptr, len] pairs, one per argument <- rsi+ // (stack continues below)+ // and rdi = argc. Rust's `&str` is ptr+len, so pairs are handier than+ // C's NUL-terminated `char **argv`.+ let mut sp = USER_STACK_TOP;+ let mut pairs: Vec<[u64; 2]> = Vec::with_capacity(process.args.len());+ for arg in &process.args {+ sp -= arg.len() as u64;+ unsafe { core::ptr::copy_nonoverlapping(arg.as_ptr(), sp as *mut u8, arg.len()) };+ pairs.push([sp, arg.len() as u64]);+ }+ sp &= !0xf; // 16-byte align+ sp -= 16 * pairs.len() as u64;+ let argv = sp;+ unsafe { core::ptr::copy_nonoverlapping(pairs.as_ptr(), argv as *mut [u64; 2], pairs.len()) }; // Functions expect rsp % 16 == 8 on entry (as if a return address had- // just been pushed), so start 8 bytes below the 16-aligned top.- unsafe { enter_user_mode(process.entry, USER_STACK_TOP - 8) }+ // just been pushed), so start 8 bytes below the 16-aligned spot.+ let (entry, argc) = (process.entry, pairs.len() as u64);+ drop(pairs);+ drop(process); // enter_user_mode never returns: don't leak the Arc+ unsafe { enter_user_mode(entry, sp - 8, argc, argv) } } /// Jump to ring 3. `iretq` pops RIP, CS, RFLAGS, RSP, SS -- we push a fake /// interrupt frame with user selectors (RPL 3), so "returning" from it drops /// the privilege level. RFLAGS = 0x202: interrupts enabled (+ the always-1 bit).-unsafe fn enter_user_mode(rip: u64, rsp: u64) -> ! {+/// rdi/rsi carry `_start(argc, argv)`'s arguments.+unsafe fn enter_user_mode(rip: u64, rsp: u64, argc: u64, argv: u64) -> ! { let s = crate::gdt::selectors(); unsafe { core::arch::asm!(@@ -101,7 +208,7 @@ unsafe fn enter_user_mode(rip: u64, rsp: u64) -> ! { "push {rip}", // Don't leak kernel values to user space in registers. "xor eax, eax", "xor ebx, ebx", "xor ecx, ecx", "xor edx, edx",- "xor esi, esi", "xor edi, edi", "xor ebp, ebp",+ "xor ebp, ebp", "xor r8d, r8d", "xor r9d, r9d", "xor r10d, r10d", "xor r11d, r11d", "xor r12d, r12d", "xor r13d, r13d", "xor r14d, r14d", "xor r15d, r15d", "iretq",@@ -110,6 +217,8 @@ unsafe fn enter_user_mode(rip: u64, rsp: u64) -> ! { rflags = in(reg) 0x202u64, cs = in(reg) s.user_code.0 as u64, rip = in(reg) rip,+ in("rdi") argc,+ in("rsi") argv, options(noreturn) ); }@@ -137,50 +246,63 @@ impl Process { } pub fn close(&self, fd: u64) -> Result<(), Errno> {- let mut fds = self.fds.lock();- match fds.get_mut(fd as usize) {- Some(slot @ Some(_)) => {- *slot = None; // dropping the Arc closes the file- Ok(())+ let file = {+ let mut fds = self.fds.lock();+ match fds.get_mut(fd as usize) {+ Some(slot @ Some(_)) => slot.take(),+ _ => return Err(Errno::EBADF), }- _ => Err(Errno::EBADF),- }+ };+ drop(file); // dropping the last Arc closes the file (after unlocking)+ Ok(()) } pub fn status(&self) -> Option<ExitStatus> {- *self.status.lock()+ without_interrupts(|| *self.status.lock()) }++ /// Make `path` absolute, relative to this process's current directory.+ pub fn resolve(&self, path: &str) -> Result<String, Errno> {+ let cwd = self.cwd.lock().clone();+ fs::normalize(&cwd, path)+ }+}++/// Look up a live (or not yet reaped) process.+pub fn get(pid: u64) -> Option<Arc<Process>> {+ PROCESSES.lock().get(&pid).cloned() } /// End the current process (exit syscall, or killed by an exception). pub fn exit_current(status: ExitStatus) -> ! { if let Some(process) = thread::current_process() {- process.fds.lock().clear(); // close all files- *process.status.lock() = Some(status);+ // Close all files (a pipe's reader sees EOF once its writers are gone).+ let files = core::mem::take(&mut *process.fds.lock());+ drop(files); match status {- ExitStatus::Exited(code) => crate::kprintln!(- "[process] pid {} ({}) exited with code {}",- process.pid,- process.name,- code- ),- ExitStatus::Killed(why) => crate::kprintln!(- "[process] pid {} ({}) killed: {}",- process.pid,- process.name,- why- ),+ ExitStatus::Exited(code) => {+ crate::kprintln!("[process] pid {} ({}) exited with code {}", process.pid, process.name, code)+ }+ ExitStatus::Killed(why) => {+ crate::kprintln!("[process] pid {} ({}) killed: {}", process.pid, process.name, why)+ } }+ without_interrupts(|| *process.status.lock() = Some(status));+ process.exited.wake_all(); // the parent may be blocked in wait() } thread::exit_current() } -/// Block (the calling kernel thread) until `process` has ended.+/// Block (the calling thread) until `process` has ended, then remove it from+/// the process table. Used by the kernel and by the `wait` syscall. pub fn wait(process: &Process) -> ExitStatus {- loop {- if let Some(status) = process.status() {- return status;- }- x86_64::instructions::hlt(); // the timer lets the process run meanwhile- }+ let status = process.exited.wait_until(|| *process.status.lock());+ PROCESSES.lock().remove(&process.pid);+ status+}++/// The `wait(pid)` syscall: only a process's parent may wait for it.+pub fn wait_child(parent: u64, pid: u64) -> Result<ExitStatus, Errno> {+ let child = get(pid).filter(|c| c.parent == parent).ok_or(Errno::ECHILD)?;+ Ok(wait(&child)) }
@@ -5,15 +5,37 @@ //! `syscall` instruction itself clobbers rcx (return address) and r11 (saved //! RFLAGS); every other register is preserved for the caller. //!-//! | nr | name | arguments | note |-//! |----|-------------|--------------------------------|------|-//! | 0 | read | fd, buf, len | fd 0 blocks until a line is typed |-//! | 1 | write | fd, buf, len | fd 1/2 = console |-//! | 2 | open | path_ptr, path_len, flags | Linux takes a C string; we take ptr+len. flags: O_CREAT = 0o100 |-//! | 3 | close | fd | |-//! | 24 | sched_yield | | |-//! | 39 | getpid | | |-//! | 60 | exit | code | |+//! Numbers are Linux's where Linux has the same call, so `strace` output and+//! man pages mostly apply. Paths are passed as pointer + length (Linux uses+//! NUL-terminated C strings); relative paths start at the process's cwd.+//!+//! | nr | name | arguments | note |+//! |-----|-------------|-------------------------------------|------|+//! | 0 | read | fd, buf, len | blocks: console until Enter, pipe until data/EOF |+//! | 1 | write | fd, buf, len | |+//! | 2 | open | path, path_len, flags | O_CREAT 0o100, O_TRUNC 0o1000; directories too (for getdents) |+//! | 3 | close | fd | |+//! | 4 | stat | path, path_len, *Stat | our own small `Stat {kind, size}` |+//! | 5 | fstat | fd, *Stat | |+//! | 8 | lseek | fd, offset, whence | SEEK_SET/CUR/END = 0/1/2 |+//! | 22 | pipe | *[u32; 2] | [read end, write end] |+//! | 24 | sched_yield | | |+//! | 35 | sleep_ms | milliseconds | Linux's nr 35 is nanosleep(timespec); ours is simpler |+//! | 39 | getpid | | |+//! | 60 | exit | code | |+//! | 61 | wait | pid | returns the exit code (139 if killed). Linux wait4 is fancier |+//! | 79 | getcwd | buf, len | returns the length (no NUL) |+//! | 80 | chdir | path, path_len | |+//! | 83 | mkdir | path, path_len | |+//! | 84 | rmdir | path, path_len | only empty directories |+//! | 87 | unlink | path, path_len | files only |+//! | 217 | getdents64 | fd, buf, len | Linux `struct linux_dirent64` records |+//! | 500 | spawn | path, path_len, argv, argv_len, *SpawnOptions | ours: start a program, returns its pid (see process.rs for why not fork/exec) |+//!+//! spawn's `argv` is all arguments separated by NUL bytes ("ls\0-l\0/bin");+//! empty means just [program name]. `SpawnOptions` (or a null pointer for+//! the defaults) picks the child's stdin/stdout/stderr from the caller's fds+//! (-1 = same as mine) and flags (SPAWN_TRACE = print its syscalls). //! //! What happens on `syscall` (the CPU does steps 1-3 in one instruction): //! 1. rcx = user RIP (next instruction), r11 = user RFLAGS@@ -33,9 +55,10 @@ //! OS use, and it's faster. use crate::errno::Errno;-use crate::fs::{self, file::InodeFile};+use crate::fs::file::{InodeFile, Stat};+use crate::fs::{self, InodeKind}; use crate::{memory, process, thread};-use alloc::sync::Arc;+use alloc::{string::String, sync::Arc, vec::Vec}; use core::arch::global_asm; use core::sync::atomic::{AtomicU64, Ordering}; @@ -43,11 +66,63 @@ pub const SYS_READ: u64 = 0; pub const SYS_WRITE: u64 = 1; pub const SYS_OPEN: u64 = 2; pub const SYS_CLOSE: u64 = 3;+pub const SYS_STAT: u64 = 4;+pub const SYS_FSTAT: u64 = 5;+pub const SYS_LSEEK: u64 = 8;+pub const SYS_PIPE: u64 = 22; pub const SYS_SCHED_YIELD: u64 = 24;+pub const SYS_SLEEP_MS: u64 = 35; pub const SYS_GETPID: u64 = 39; pub const SYS_EXIT: u64 = 60;+pub const SYS_WAIT: u64 = 61;+pub const SYS_GETCWD: u64 = 79;+pub const SYS_CHDIR: u64 = 80;+pub const SYS_MKDIR: u64 = 83;+pub const SYS_RMDIR: u64 = 84;+pub const SYS_UNLINK: u64 = 87;+pub const SYS_GETDENTS64: u64 = 217;+pub const SYS_SPAWN: u64 = 500; pub const O_CREAT: u64 = 0o100;+pub const O_TRUNC: u64 = 0o1000;+pub const SPAWN_TRACE: u64 = 1;++/// Name of a syscall number (for tracing).+pub fn name(nr: u64) -> &'static str {+ match nr {+ SYS_READ => "read",+ SYS_WRITE => "write",+ SYS_OPEN => "open",+ SYS_CLOSE => "close",+ SYS_STAT => "stat",+ SYS_FSTAT => "fstat",+ SYS_LSEEK => "lseek",+ SYS_PIPE => "pipe",+ SYS_SCHED_YIELD => "sched_yield",+ SYS_SLEEP_MS => "sleep_ms",+ SYS_GETPID => "getpid",+ SYS_EXIT => "exit",+ SYS_WAIT => "wait",+ SYS_GETCWD => "getcwd",+ SYS_CHDIR => "chdir",+ SYS_MKDIR => "mkdir",+ SYS_RMDIR => "rmdir",+ SYS_UNLINK => "unlink",+ SYS_GETDENTS64 => "getdents64",+ SYS_SPAWN => "spawn",+ _ => "?",+ }+}++/// spawn's options, as laid out in user memory.+#[repr(C)]+#[derive(Clone, Copy)]+pub struct SpawnOptions {+ pub stdin: i64, // caller's fd to use as the child's fd 0; -1 = caller's fd 0+ pub stdout: i64,+ pub stderr: i64,+ pub flags: u64,+} /// Kernel stack top of the running thread (set by the scheduler). #[unsafe(no_mangle)]@@ -133,24 +208,52 @@ global_asm!( ); /// Rust side of every system call. Returns the value for rax.-extern "C" fn syscall_dispatch(nr: u64, a1: u64, a2: u64, a3: u64, _a4: u64, _a5: u64) -> i64 {+extern "C" fn syscall_dispatch(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 {+ let process = thread::current_process();+ let trace = process.as_ref().is_some_and(|p| p.trace);+ if trace && nr == SYS_EXIT {+ crate::kprintln!("[trace {}] exit({})", process.as_ref().unwrap().pid, a1 as i64);+ }+ drop(process); // don't hold an Arc while blocking/exiting let result = match nr { SYS_READ => sys_read(a1, a2, a3), SYS_WRITE => sys_write(a1, a2, a3), SYS_OPEN => sys_open(a1, a2, a3),- SYS_CLOSE => sys_close(a1),+ SYS_CLOSE => current().and_then(|p| p.close(a1)).map(|_| 0),+ SYS_STAT => sys_stat(a1, a2, a3),+ SYS_FSTAT => sys_fstat(a1, a2),+ SYS_LSEEK => current().and_then(|p| p.file(a1)).and_then(|f| f.seek(a2 as i64, a3)),+ SYS_PIPE => sys_pipe(a1), SYS_SCHED_YIELD => { thread::yield_now(); Ok(0) }+ SYS_SLEEP_MS => {+ // The PIT fires ~18.2 times a second: 1 tick = ~55 ms. Round up.+ thread::sleep_ticks(a1.div_ceil(55));+ Ok(0)+ } SYS_GETPID => current().map(|p| p.pid), SYS_EXIT => process::exit_current(process::ExitStatus::Exited(a1 as i64)),+ SYS_WAIT => current().and_then(|p| process::wait_child(p.pid, a1)).map(|s| s.code() as u64),+ SYS_GETCWD => sys_getcwd(a1, a2),+ SYS_CHDIR => sys_chdir(a1, a2),+ SYS_MKDIR => user_path(a1, a2).and_then(|p| fs::create(&p, InodeKind::Directory)).map(|_| 0),+ SYS_RMDIR => user_path(a1, a2).and_then(|p| fs::remove(&p, true)).map(|_| 0),+ SYS_UNLINK => user_path(a1, a2).and_then(|p| fs::remove(&p, false)).map(|_| 0),+ SYS_GETDENTS64 => sys_getdents64(a1, a2, a3),+ SYS_SPAWN => sys_spawn(a1, a2, a3, a4, a5), _ => Err(Errno::ENOSYS), };- match result {+ let ret = match result { Ok(value) => value as i64, Err(errno) => -(errno as i64),+ };+ if trace {+ let pid = current().map(|p| p.pid).unwrap_or(0);+ crate::kprintln!("[trace {}] {}({:#x}, {:#x}, {:#x}) = {}", pid, name(nr), a1, a2, a3, ret); }+ ret } fn current() -> Result<Arc<process::Process>, Errno> {@@ -160,6 +263,9 @@ fn current() -> Result<Arc<process::Process>, Errno> { /// Turn a user pointer + length into a slice, after checking that the user /// may really access that memory. Never trust pointers from user space! pub fn user_slice(ptr: u64, len: u64) -> Result<&'static [u8], Errno> {+ if len == 0 {+ return Ok(&[]); // nothing to access, so any pointer is fine (like Linux)+ } if !memory::user_range_ok(ptr, len, false) { return Err(Errno::EFAULT); }@@ -168,6 +274,9 @@ pub fn user_slice(ptr: u64, len: u64) -> Result<&'static [u8], Errno> { /// Like `user_slice`, but the user must be allowed to *write* the memory. pub fn user_slice_mut(ptr: u64, len: u64) -> Result<&'static mut [u8], Errno> {+ if len == 0 {+ return Ok(&mut []);+ } if !memory::user_range_ok(ptr, len, true) { return Err(Errno::EFAULT); }@@ -186,19 +295,115 @@ fn sys_write(fd: u64, buf: u64, len: u64) -> Result<u64, Errno> { file.write(buf).map(|n| n as u64) } -fn sys_open(path_ptr: u64, path_len: u64, flags: u64) -> Result<u64, Errno> {- if path_len > 255 {+/// Read a path argument and make it absolute (relative to the cwd).+fn user_path(ptr: u64, len: u64) -> Result<String, Errno> {+ if len > 4096 { return Err(Errno::ENAMETOOLONG); }- let path = core::str::from_utf8(user_slice(path_ptr, path_len)?).map_err(|_| Errno::EINVAL)?;- let inode = match fs::lookup(path) {- Err(Errno::ENOENT) if flags & O_CREAT != 0 => fs::create(path, fs::InodeKind::File)?,+ let path = core::str::from_utf8(user_slice(ptr, len)?).map_err(|_| Errno::EINVAL)?;+ current()?.resolve(path)+}++/// Copy a plain-data struct out to user memory.+fn copy_to_user<T: Copy>(ptr: u64, value: &T) -> Result<(), Errno> {+ let size = core::mem::size_of::<T>();+ let dst = user_slice_mut(ptr, size as u64)?;+ let src = unsafe { core::slice::from_raw_parts(value as *const T as *const u8, size) };+ dst.copy_from_slice(src);+ Ok(())+}++fn sys_open(path_ptr: u64, path_len: u64, flags: u64) -> Result<u64, Errno> {+ let path = user_path(path_ptr, path_len)?;+ let inode = match fs::lookup(&path) {+ Err(Errno::ENOENT) if flags & O_CREAT != 0 => fs::create(&path, InodeKind::File)?, other => other?, };+ if flags & O_TRUNC != 0 && inode.kind() == InodeKind::File {+ inode.truncate()?;+ } let file = Arc::new(InodeFile::open(inode)?); current()?.add_file(file) } -fn sys_close(fd: u64) -> Result<u64, Errno> {- current()?.close(fd).map(|_| 0)+fn sys_stat(path_ptr: u64, path_len: u64, stat_ptr: u64) -> Result<u64, Errno> {+ let inode = fs::lookup(&user_path(path_ptr, path_len)?)?;+ copy_to_user(stat_ptr, &Stat::of(&*inode))?;+ Ok(0)+}++fn sys_fstat(fd: u64, stat_ptr: u64) -> Result<u64, Errno> {+ let stat = current()?.file(fd)?.stat()?;+ copy_to_user(stat_ptr, &stat)?;+ Ok(0)+}++fn sys_pipe(fds_ptr: u64) -> Result<u64, Errno> {+ user_slice_mut(fds_ptr, 8)?; // check before creating anything+ let process = current()?;+ let (reader, writer) = fs::pipe::new();+ let read_fd = process.add_file(Arc::new(reader))?;+ let write_fd = match process.add_file(Arc::new(writer)) {+ Ok(fd) => fd,+ Err(e) => {+ process.close(read_fd)?;+ return Err(e);+ }+ };+ copy_to_user(fds_ptr, &[read_fd as u32, write_fd as u32])?;+ Ok(0)+}++fn sys_getcwd(buf: u64, len: u64) -> Result<u64, Errno> {+ let cwd = current()?.cwd.lock().clone();+ if (len as usize) < cwd.len() {+ return Err(Errno::ERANGE);+ }+ user_slice_mut(buf, cwd.len() as u64)?.copy_from_slice(cwd.as_bytes());+ Ok(cwd.len() as u64)+}++fn sys_chdir(path_ptr: u64, path_len: u64) -> Result<u64, Errno> {+ let path = user_path(path_ptr, path_len)?;+ if fs::lookup(&path)?.kind() != InodeKind::Directory {+ return Err(Errno::ENOTDIR);+ }+ *current()?.cwd.lock() = path;+ Ok(0)+}++fn sys_getdents64(fd: u64, buf: u64, len: u64) -> Result<u64, Errno> {+ let file = current()?.file(fd)?;+ file.getdents(user_slice_mut(buf, len)?).map(|n| n as u64)+}++fn sys_spawn(path_ptr: u64, path_len: u64, argv_ptr: u64, argv_len: u64, opts_ptr: u64) -> Result<u64, Errno> {+ let parent = current()?;+ let path = user_path(path_ptr, path_len)?;+ if argv_len as usize > process::MAX_ARGS_SIZE {+ return Err(Errno::E2BIG);+ }+ let blob = core::str::from_utf8(user_slice(argv_ptr, argv_len)?).map_err(|_| Errno::EINVAL)?;+ let mut args: Vec<String> = blob.split('\0').filter(|a| !a.is_empty()).map(String::from).collect();+ if args.is_empty() {+ args.push(String::from(path.rsplit('/').next().unwrap_or("")));+ }+ let opts = if opts_ptr == 0 {+ SpawnOptions { stdin: -1, stdout: -1, stderr: -1, flags: 0 }+ } else {+ let bytes = user_slice(opts_ptr, core::mem::size_of::<SpawnOptions>() as u64)?;+ unsafe { core::ptr::read_unaligned(bytes.as_ptr() as *const SpawnOptions) }+ };+ // Child fd n = the caller's fd chosen in the options (-1: the caller's fd n).+ let pick = |n: u64, choice: i64| parent.file(if choice < 0 { n } else { choice as u64 });+ let stdio = [pick(0, opts.stdin)?, pick(1, opts.stdout)?, pick(2, opts.stderr)?];+ let spawn = process::SpawnArgs {+ args,+ stdio,+ cwd: parent.cwd.lock().clone(),+ parent: parent.pid,+ trace: opts.flags & SPAWN_TRACE != 0,+ };+ drop(parent);+ process::spawn_path(&path, spawn).map(|child| child.pid) }
@@ -27,6 +27,18 @@ //! syscall entry at its kernel stack, so interrupts and syscalls from ring 3 //! land on the right stack. //!+//! Blocking: a thread waiting for something (keyboard input, a child+//! process, a pipe, a timeout) is marked `blocked` and the scheduler skips it+//! until someone calls `wake` (or its sleep deadline passes). That's what+//! `WaitQueue` is for. If *every* thread is blocked, the CPU idles with `hlt`+//! until an interrupt wakes somebody up.+//!+//! Locking rule (single CPU!): any lock that is also taken with interrupts+//! *off* (the scheduler's, a wait queue's, a pipe buffer's...) must *always*+//! be taken with interrupts off. Otherwise a thread could be preempted while+//! holding it, and the next thread would spin on it forever, unable to be+//! preempted itself.+//! //! A *new* thread has never been interrupted, so `spawn` builds a fake stack //! that looks like `switch_context` saved it, with the "return address" //! pointing at `thread_trampoline`, which enables interrupts and calls the@@ -40,16 +52,21 @@ use x86_64::instructions::interrupts; use x86_64::registers::control::Cr3; use x86_64::structures::paging::PhysFrame; -/// 16 KiB per thread stack (no guard page! a deep recursion in a thread-/// silently overwrites whatever is below its stack on the heap).-const STACK_SIZE: usize = 16 * 1024;+/// 64 KiB per thread stack. There's no guard page (stacks are plain heap+/// memory), so overflowing one silently overwrites whatever lies below it.+/// We at least *detect* that afterwards: the lowest 8 bytes hold+/// `STACK_CANARY`, checked whenever the scheduler switches away from a+/// thread. (16 KiB was enough until syscalls went through the FAT driver:+/// unoptimized debug builds use big stack frames.)+const STACK_SIZE: usize = 64 * 1024;+const STACK_CANARY: u64 = 0x5AFE_57AC_C0DE_CAFE; struct Thread { name: &'static str, /// Saved stack pointer while the thread is not running. rsp: u64, /// The thread's stack. `None` for the boot thread (uses the bootloader's stack).- _stack: Option<Vec<u8>>,+ stack: Option<Vec<u8>>, /// Top of that stack (0 for the boot thread). Becomes TSS.RSP0 and the /// syscall stack while this thread runs. kernel_stack_top: u64,@@ -58,6 +75,10 @@ struct Thread { /// The process this thread belongs to (None = pure kernel thread). process: Option<Arc<Process>>, finished: bool,+ /// Waiting for something; the scheduler skips blocked threads.+ blocked: bool,+ /// If blocked by `sleep_ticks`: wake up at this tick.+ wake_at: Option<u64>, } struct Scheduler {@@ -78,6 +99,7 @@ pub fn spawn(name: &'static str, entry: fn()) { /// process's address space `cr3` active; `entry` then drops to ring 3. pub fn spawn_with(name: &'static str, entry: fn(), cr3: PhysFrame, process: Option<Arc<Process>>) { let mut stack = vec![0u8; STACK_SIZE];+ stack[..8].copy_from_slice(&STACK_CANARY.to_ne_bytes()); // Build the initial stack, top down (stacks grow towards lower addresses): // [return address = thread_trampoline]@@ -99,35 +121,51 @@ pub fn spawn_with(name: &'static str, entry: fn(), cr3: PhysFrame, process: Opti let thread = Box::new(Thread { name, rsp,- _stack: Some(stack),+ stack: Some(stack), kernel_stack_top: top, cr3, process, finished: false,+ blocked: false,+ wake_at: None, }); // The timer handler also takes this lock, so keep interrupts off while we // hold it -- otherwise the handler would spin forever on our lock. interrupts::without_interrupts(|| { let mut scheduler = SCHEDULER.lock();- if scheduler.threads.is_empty() {- // First spawn: register the code that's running right now (the- // boot/main thread) as thread 0. Its rsp is filled in the first- // time we switch away from it.- scheduler.threads.push(Box::new(Thread {- name: "main",- rsp: 0,- _stack: None,- kernel_stack_top: 0,- cr3: Cr3::read().0,- process: None,- finished: false,- }));+ register_main_thread(&mut scheduler);+ // Free the stacks of threads that have finished (we can't do that in+ // exit_current: the thread is still running on its stack there).+ let current = scheduler.current;+ for (i, t) in scheduler.threads.iter_mut().enumerate() {+ if t.finished && i != current {+ t.stack = None;+ t.process = None;+ } } scheduler.threads.push(thread); }); } +/// On first use, register the code that's running right now (the boot/main+/// thread) as thread 0. Its rsp is filled in the first time we switch away.+fn register_main_thread(scheduler: &mut Scheduler) {+ if scheduler.threads.is_empty() {+ scheduler.threads.push(Box::new(Thread {+ name: "main",+ rsp: 0,+ stack: None,+ kernel_stack_top: 0,+ cr3: Cr3::read().0,+ process: None,+ finished: false,+ blocked: false,+ wake_at: None,+ }));+ }+}+ /// Called from the timer interrupt (interrupts disabled, EOI already sent). /// Switches to the next runnable thread, round-robin. Returns when this /// thread is scheduled again.@@ -139,12 +177,27 @@ pub fn schedule() { return; // no threads spawned yet } let current = s.current;- // Find the next thread that hasn't finished.- let next = (1..=n).map(|i| (current + i) % n).find(|&i| !s.threads[i].finished);+ // Wake up sleepers whose time has come.+ let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed);+ for t in s.threads.iter_mut() {+ if t.wake_at.is_some_and(|at| now >= at) {+ t.wake_at = None;+ t.blocked = false;+ }+ }+ // Find the next thread that can run.+ let next = (1..=n)+ .map(|i| (current + i) % n)+ .find(|&i| !s.threads[i].finished && !s.threads[i].blocked); let next = match next { Some(next) if next != current => next, _ => return, // nobody else to run };+ if let Some(stack) = &s.threads[current].stack {+ if stack[..8] != STACK_CANARY.to_ne_bytes() {+ panic!("kernel stack overflow in thread '{}'", s.threads[current].name);+ }+ } s.current = next; // Prepare the CPU for the next thread *before* switching stacks.@@ -202,13 +255,115 @@ pub fn current_name() -> &'static str { }) } -/// Sleep for `ticks` timer ticks (~55 ms each), letting other threads run.+/// Id of the running thread (its index in the thread list; ids are never reused).+pub fn current_id() -> usize {+ interrupts::without_interrupts(|| {+ let mut s = SCHEDULER.lock();+ register_main_thread(&mut s);+ s.current+ })+}++/// Make thread `id` runnable again (no-op if it isn't blocked).+pub fn wake(id: usize) {+ interrupts::without_interrupts(|| {+ if let Some(t) = SCHEDULER.lock().threads.get_mut(id) {+ t.blocked = false;+ t.wake_at = None;+ }+ });+}++fn is_blocked(id: usize) -> bool {+ SCHEDULER.lock().threads[id].blocked+}++/// Mark the running thread blocked, then run other threads until somebody+/// wakes it. Must be called with interrupts *off* (so a wake-up can't slip in+/// between the caller checking its condition and us blocking); returns with+/// interrupts still off.+fn block_current(wake_at: Option<u64>) {+ let me = {+ let mut s = SCHEDULER.lock();+ register_main_thread(&mut s);+ let me = s.current;+ s.threads[me].blocked = true;+ s.threads[me].wake_at = wake_at;+ me+ };+ loop {+ schedule(); // returns once we're picked again -- or if nobody else can run+ if !is_blocked(me) {+ return;+ }+ // Every thread is blocked: idle until an interrupt (timer, keyboard)+ // possibly wakes someone. `enable_and_hlt` = `sti; hlt` with no gap,+ // so the interrupt can't sneak in before the hlt.+ interrupts::enable_and_hlt();+ interrupts::disable();+ if !is_blocked(me) {+ return;+ }+ }+}++/// Sleep for `ticks` timer ticks (~55 ms each). The thread is *blocked*+/// meanwhile, so it costs no CPU time; the scheduler wakes it up. pub fn sleep_ticks(ticks: u64) { use crate::interrupts::TICKS; use core::sync::atomic::Ordering;- let wake_at = TICKS.load(Ordering::Relaxed) + ticks;- while TICKS.load(Ordering::Relaxed) < wake_at {- x86_64::instructions::hlt(); // the timer interrupt may switch threads here+ let was_enabled = interrupts::are_enabled();+ interrupts::disable();+ let wake_at = TICKS.load(Ordering::Relaxed) + ticks.max(1);+ block_current(Some(wake_at));+ if was_enabled {+ interrupts::enable();+ }+}++/// Threads waiting for some event (like a Linux `wait_queue_head_t`).+///+/// Usage: `queue.wait_until(|| condition)` blocks until the closure returns+/// `Some(value)`; whoever makes the condition true calls `queue.wake_all()`.+/// The closure runs with interrupts off, so it must be quick and only touch+/// locks that follow the locking rule at the top of this file.+pub struct WaitQueue {+ waiting: Mutex<Vec<usize>>,+}++impl WaitQueue {+ pub const fn new() -> Self {+ WaitQueue { waiting: Mutex::new(Vec::new()) }+ }++ pub fn wait_until<T>(&self, mut condition: impl FnMut() -> Option<T>) -> T {+ let was_enabled = interrupts::are_enabled();+ let result = loop {+ interrupts::disable();+ // Check and go to sleep without interrupts in between: otherwise+ // a wake_all() could happen right after the check, and we'd+ // sleep forever (the classic "lost wake-up" bug).+ if let Some(value) = condition() {+ break value;+ }+ let me = current_id();+ self.waiting.lock().push(me);+ block_current(None);+ // Woken up: loop and check the condition again (somebody else may+ // have consumed what we were waiting for).+ };+ if was_enabled {+ interrupts::enable();+ }+ result+ }++ pub fn wake_all(&self) {+ interrupts::without_interrupts(|| {+ for id in self.waiting.lock().drain(..) {+ wake(id);+ }+ }); } }
@@ -7,7 +7,8 @@ //! `read(0, ...)` gets a complete, already-edited line. use crate::errno::Errno;-use crate::fs::file::File;+use crate::fs::file::{File, KIND_CONSOLE, Stat};+use crate::thread::WaitQueue; use crate::{print, serial_print, vga_buffer}; use alloc::string::String; use conquer_once::spin::OnceCell;@@ -19,6 +20,9 @@ static LINE: Mutex<String> = Mutex::new(String::new()); /// Finished lines, as bytes, waiting for `read(0, ...)`. static INPUT: OnceCell<ArrayQueue<u8>> = OnceCell::uninit(); +/// Threads blocked in `Console::read`, waiting for a line.+static INPUT_WAIT: WaitQueue = WaitQueue::new();+ fn input() -> &'static ArrayQueue<u8> { INPUT.get_or_init(|| ArrayQueue::new(4096)) }@@ -41,6 +45,7 @@ pub fn input_char(c: char) { let _ = input().push(b); // queue full: drop (nobody is reading) } line.clear();+ INPUT_WAIT.wake_all(); // a reader may be blocked in Console::read } c => { print!("{}", c);@@ -61,23 +66,23 @@ impl File for Console { if buf.is_empty() { return Ok(0); }- // Sleep until a line arrives. Syscalls run with interrupts enabled,- // so `hlt` wakes on the next interrupt (and the timer may switch to- // other threads meanwhile). Lines are queued whole, so once the- // first byte is there, the rest of the line is too.- while input().is_empty() {- x86_64::instructions::hlt();- }- let mut n = 0;- while n < buf.len() {- let Some(b) = input().pop() else { break };- buf[n] = b;- n += 1;- if b == b'\n' {- break;+ // Block until a line arrives: the thread is parked on INPUT_WAIT+ // (using no CPU) and `input_char` wakes it when Enter is pressed.+ // Lines are queued whole, so once the first byte is there the rest+ // of the line is too. (The closure runs with interrupts off; the+ // ArrayQueue is lock-free, so that's fine.)+ Ok(INPUT_WAIT.wait_until(|| {+ let mut n = 0;+ while n < buf.len() {+ let Some(b) = input().pop() else { break };+ buf[n] = b;+ n += 1;+ if b == b'\n' {+ break;+ } }- }- Ok(n)+ if n > 0 { Some(n) } else { None }+ })) } fn write(&self, buf: &[u8]) -> Result<usize, Errno> {@@ -92,4 +97,8 @@ impl File for Console { } Ok(buf.len()) }++ fn stat(&self) -> Result<Stat, Errno> {+ Ok(Stat { kind: KIND_CONSOLE, size: 0 })+ } }
@@ -0,0 +1,133 @@+//! Block layer: PCI enumeration, the virtio-blk driver (against the+//! target/disk.img QEMU attaches), partition table parsing, RAM disks.+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rust_kernel::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++use alloc::{sync::Arc, vec};+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use rust_kernel::block::{self, BlockDevice, RamDisk, SECTOR_SIZE, partition};+use rust_kernel::errno::Errno;+use rust_kernel::pci;++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+ rust_kernel::init();+ rust_kernel::init_memory(boot_info);+ block::init(&pci::scan());+ test_main();+ rust_kernel::hlt_loop();+}++#[test_case]+fn pci_scan_finds_the_virtio_disk() {+ let devices = pci::scan();+ // QEMU's i440FX machine always has the host bridge at 00:00.0.+ assert_eq!((devices[0].bus, devices[0].device, devices[0].vendor_id), (0, 0, 0x8086));+ assert!(devices.iter().any(|d| d.vendor_id == 0x1af4 && d.device_id == 0x1001));+}++#[test_case]+fn virtio_disk_reads_the_gpt_image() {+ let disk = block::get("vda").expect("no vda: is target/disk.img attached?");+ assert_eq!(disk.sector_count(), 64 * 1024 * 1024 / 512);+ let mut sector = [0u8; SECTOR_SIZE];+ disk.read_sectors(0, &mut sector).unwrap();+ assert_eq!(§or[510..], &[0x55, 0xAA]); // protective MBR signature+ assert_eq!(sector[446 + 4], 0xEE); // partition type "GPT protective"+ disk.read_sectors(1, &mut sector).unwrap();+ assert_eq!(§or[..8], b"EFI PART");+}++#[test_case]+fn multi_sector_reads_match_single_sector_reads() {+ // 20 sectors = more than one 4 KiB bounce-buffer page per request.+ let disk = block::get("vda").unwrap();+ let mut big = vec![0u8; 20 * SECTOR_SIZE];+ disk.read_sectors(2048, &mut big).unwrap();+ let mut one = [0u8; SECTOR_SIZE];+ for i in 0..20 {+ disk.read_sectors(2048 + i as u64, &mut one).unwrap();+ assert_eq!(&big[i * SECTOR_SIZE..(i + 1) * SECTOR_SIZE], &one[..]);+ }+}++#[test_case]+fn virtio_write_then_read_back() {+ // QEMU runs the disk with snapshot=on, so this doesn't touch the file.+ let disk = block::get("vda").unwrap();+ let data: alloc::vec::Vec<u8> = (0..2 * SECTOR_SIZE).map(|i| (i % 251) as u8).collect();+ disk.write_sectors(100, &data).unwrap();+ let mut back = vec![0u8; 2 * SECTOR_SIZE];+ disk.read_sectors(100, &mut back).unwrap();+ assert_eq!(back, data);+}++#[test_case]+fn out_of_range_and_partial_sectors_fail() {+ let disk = block::get("vda").unwrap();+ let mut sector = [0u8; SECTOR_SIZE];+ assert_eq!(disk.read_sectors(disk.sector_count(), &mut sector), Err(Errno::EIO));+ assert_eq!(disk.read_sectors(0, &mut sector[..100]), Err(Errno::EINVAL));+}++#[test_case]+fn gpt_partition_is_found_and_registered() {+ let disk = block::get("vda").unwrap();+ let (scheme, parts) = partition::scan(&disk).unwrap();+ assert_eq!(scheme, "GPT");+ assert_eq!(parts.len(), 1);+ assert_eq!((parts[0].start, parts[0].sectors), (2048, 126976));+ assert_eq!(parts[0].name, "fat32");+ // vda1 is a block device whose sector 0 is the disk's sector 2048:+ // the FAT boot sector (ends in 55 AA, says "FAT32 " at offset 82).+ let vda1 = block::get("vda1").unwrap();+ assert_eq!(vda1.sector_count(), 126976);+ let mut sector = [0u8; SECTOR_SIZE];+ vda1.read_sectors(0, &mut sector).unwrap();+ assert_eq!(§or[82..90], b"FAT32 ");+ assert_eq!(vda1.read_sectors(126976, &mut sector), Err(Errno::EIO)); // past its end+}++#[test_case]+fn mbr_partitions_on_a_ram_disk() {+ // Build a classic MBR by hand: two primary partitions.+ let disk: Arc<dyn BlockDevice> = Arc::new(RamDisk::new(1000));+ let mut mbr = [0u8; SECTOR_SIZE];+ let entry = |mbr: &mut [u8; SECTOR_SIZE], i: usize, kind: u8, start: u32, len: u32| {+ let e = 446 + 16 * i;+ mbr[e + 4] = kind;+ mbr[e + 8..e + 12].copy_from_slice(&start.to_le_bytes());+ mbr[e + 12..e + 16].copy_from_slice(&len.to_le_bytes());+ };+ entry(&mut mbr, 0, 0x0c, 63, 400); // FAT32 (LBA)+ entry(&mut mbr, 1, 0x83, 500, 300); // Linux+ // No signature yet -> not a partition table.+ assert!(matches!(partition::scan(&disk), Err(partition::PartitionError::NoSignature)));+ mbr[510] = 0x55;+ mbr[511] = 0xAA;+ disk.write_sectors(0, &mbr).unwrap();+ let (scheme, parts) = partition::scan(&disk).unwrap();+ assert_eq!(scheme, "MBR");+ assert_eq!(parts.len(), 2);+ assert_eq!((parts[0].start, parts[0].sectors), (63, 400));+ assert_eq!((parts[1].start, parts[1].sectors), (500, 300));++ // A partition device maps its sector 0 to the partition's start.+ let part = partition::Partition::new(disk.clone(), 500, 300);+ part.write_sectors(0, &[7u8; SECTOR_SIZE]).unwrap();+ let mut back = [0u8; SECTOR_SIZE];+ disk.read_sectors(500, &mut back).unwrap();+ assert_eq!(back, [7u8; SECTOR_SIZE]);+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+ rust_kernel::test_panic_handler(info)+}
@@ -0,0 +1,131 @@+//! FAT32 through the VFS: the partition on target/disk.img is mounted at+//! /mnt/disk0 (writes are fine: QEMU uses snapshot=on).+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rust_kernel::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++use alloc::{string::String, sync::Arc, vec, vec::Vec};+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use fatfs::{Read, Seek, SeekFrom, Write};+use rust_kernel::block::{BlockDevice, RamDisk, SECTOR_SIZE};+use rust_kernel::errno::Errno;+use rust_kernel::fs::fat::BlockIo;+use rust_kernel::fs::file::{File, InodeFile};+use rust_kernel::fs::{self, InodeKind};+use rust_kernel::elf;++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+ rust_kernel::init();+ rust_kernel::init_memory(boot_info);+ fs::init();+ rust_kernel::init_storage();+ test_main();+ rust_kernel::hlt_loop();+}++#[test_case]+fn fat_volume_is_mounted() {+ assert!(fs::mounts().iter().any(|(path, kind)| path == "/mnt/disk0" && *kind == "fat32"));+ let mut names: Vec<String> = fs::lookup("/mnt/disk0").unwrap().list().unwrap().into_iter().map(|e| e.name).collect();+ names.sort(); // FAT keeps directory entries in creation order+ assert_eq!(names, vec!["bin", "notes", "readme.txt"]);+}++#[test_case]+fn read_text_file() {+ let file = InodeFile::open(fs::lookup("/mnt/disk0/readme.txt").unwrap()).unwrap();+ let mut buf = [0u8; 256];+ let n = file.read(&mut buf).unwrap();+ assert!(buf[..n].starts_with(b"Hello from the FAT32 disk!"));+ assert_eq!(n as u64, file.stat().unwrap().size);+ assert_eq!(file.read(&mut buf), Ok(0));+ // FAT is case-insensitive; subdirectories work.+ assert!(fs::lookup("/mnt/disk0/README.TXT").is_ok());+ assert_eq!(fs::lookup("/mnt/disk0/notes/todo.txt").unwrap().kind(), InodeKind::File);+ assert_eq!(fs::lookup("/mnt/disk0/nope").err(), Some(Errno::ENOENT));+ assert_eq!(fs::lookup("/mnt/disk0/readme.txt/x").err(), Some(Errno::ENOTDIR));+}++#[test_case]+fn programs_on_disk_are_valid_elf_files() {+ let bin = fs::lookup("/mnt/disk0/bin").unwrap();+ for entry in bin.list().unwrap() {+ let path = alloc::format!("/mnt/disk0/bin/{}", entry.name);+ let image = fs::read_all(&path).unwrap();+ assert_eq!(image.len(), fs::lookup(&path).unwrap().size());+ assert!(elf::check(&image).is_ok(), "{} is not a valid program", entry.name);+ }+}++#[test_case]+fn create_write_read_remove() {+ // 10 000 bytes = 20 clusters (1 sector each): exercises the FAT chain.+ let data: Vec<u8> = (0..10_000u32).map(|i| (i * 7 % 256) as u8).collect();+ let inode = fs::create("/mnt/disk0/notes/big.bin", InodeKind::File).unwrap();+ assert_eq!(inode.write_at(0, &data), Ok(data.len()));+ let mut back = vec![0u8; 12_000];+ assert_eq!(inode.read_at(0, &mut back), Ok(10_000));+ assert_eq!(&back[..10_000], &data[..]);+ // Overwrite in the middle, read across a sector boundary.+ inode.write_at(510, b"XYZW").unwrap();+ let mut four = [0u8; 4];+ inode.read_at(510, &mut four).unwrap();+ assert_eq!(&four, b"XYZW");+ // Writing past the end fills the gap with zeros.+ inode.write_at(10_010, b"!").unwrap();+ assert_eq!(inode.size(), 10_011);+ inode.read_at(10_000, &mut four).unwrap();+ assert_eq!(four, [0; 4]);+ // A fresh lookup sees the same data (it really went to the disk).+ assert_eq!(fs::lookup("/mnt/disk0/notes/big.bin").unwrap().size(), 10_011);+ inode.truncate().unwrap();+ assert_eq!(inode.size(), 0);+ fs::remove("/mnt/disk0/notes/big.bin", false).unwrap();+ assert_eq!(fs::lookup("/mnt/disk0/notes/big.bin").err(), Some(Errno::ENOENT));+}++#[test_case]+fn directories_on_fat() {+ fs::create("/mnt/disk0/newdir", InodeKind::Directory).unwrap();+ assert_eq!(fs::create("/mnt/disk0/newdir", InodeKind::Directory).err(), Some(Errno::EEXIST));+ fs::create("/mnt/disk0/newdir/a long file name.txt", InodeKind::File).unwrap();+ let list = fs::lookup("/mnt/disk0/newdir").unwrap().list().unwrap();+ assert_eq!(list.len(), 1);+ assert_eq!(list[0].name, "a long file name.txt"); // long file names (VFAT)+ assert_eq!(fs::remove("/mnt/disk0/newdir", true), Err(Errno::ENOTEMPTY));+ fs::remove("/mnt/disk0/newdir/a long file name.txt", false).unwrap();+ fs::remove("/mnt/disk0/newdir", true).unwrap();+ assert_eq!(fs::remove("/mnt/disk0", true), Err(Errno::EBUSY)); // mount point+}++#[test_case]+fn block_io_adapter_handles_unaligned_access() {+ // The byte-stream adapter fatfs uses, on a RAM disk.+ let disk: Arc<dyn BlockDevice> = Arc::new(RamDisk::new(4));+ let mut io = BlockIo::new(disk.clone());+ io.seek(SeekFrom::Start(508)).unwrap();+ io.write_all(b"spans two sectors").unwrap(); // bytes 508..525+ let mut sector = [0u8; SECTOR_SIZE];+ disk.read_sectors(1, &mut sector).unwrap();+ assert_eq!(§or[..13], b"s two sectors"); // "span" went to sector 0+ disk.read_sectors(0, &mut sector).unwrap();+ assert_eq!(§or[508..], b"span");+ io.seek(SeekFrom::Start(508)).unwrap();+ let mut back = [0u8; 17];+ io.read_exact(&mut back).unwrap();+ assert_eq!(&back, b"spans two sectors");+ assert_eq!(io.seek(SeekFrom::End(0)).unwrap(), 4 * SECTOR_SIZE as u64);+ assert!(io.seek(SeekFrom::End(1)).is_err()); // past the end of the disk+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+ rust_kernel::test_panic_handler(info)+}
@@ -1,60 +1,79 @@-//! User mode: real ring-3 programs (user/*.rs) run as processes, make-//! syscalls, get killed when they misbehave, and get preempted when they hog-//! the CPU. Plus the ELF loader and user-pointer checks.+//! User mode: real ring-3 programs (user/*.rs, loaded from the FAT32 disk)+//! run as processes, make syscalls, get killed when they misbehave, get+//! preempted when they hog the CPU, spawn children and wait for them.+//! Plus the ELF loader, user-pointer checks and blocking primitives. #![no_std] #![no_main] #![feature(custom_test_frameworks)] #![test_runner(rust_kernel::test_runner)] #![reexport_test_harness_main = "test_main"] +extern crate alloc;++use alloc::{string::String, sync::Arc, vec, vec::Vec}; use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo;-use core::sync::atomic::Ordering;+use core::sync::atomic::{AtomicBool, Ordering}; use rust_kernel::errno::Errno;+use rust_kernel::fs::file::File; use rust_kernel::interrupts::TICKS;-use rust_kernel::memory::{self, BootInfoFrameAllocator, USER_START};-use rust_kernel::process::{self, ExitStatus};-use rust_kernel::{allocator, elf, fs, syscall, user_programs};-use x86_64::VirtAddr;+use rust_kernel::memory::{self, USER_START};+use rust_kernel::process::{self, ExitStatus, SpawnArgs};+use rust_kernel::thread::{self, WaitQueue};+use rust_kernel::{elf, fs, syscall}; entry_point!(main); fn main(boot_info: &'static BootInfo) -> ! { rust_kernel::init();- let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);- let mut mapper = unsafe { memory::init(phys_mem_offset) };- let mut frame_allocator = unsafe { BootInfoFrameAllocator::init(&boot_info.memory_map) };- allocator::init_heap(&mut mapper, &mut frame_allocator).expect("heap initialization failed");- memory::init_frame_allocator(frame_allocator);+ rust_kernel::init_memory(boot_info); fs::init();-+ rust_kernel::init_storage(); test_main(); rust_kernel::hlt_loop(); } +const BIN: &str = "/mnt/disk0/bin";++/// Start /mnt/disk0/bin/<name> with these arguments (after the name).+fn run(name: &str, extra_args: &[&str]) -> Arc<process::Process> {+ let mut spawn = SpawnArgs::kernel(name);+ spawn.args.extend(extra_args.iter().map(|a| String::from(*a)));+ process::spawn_path(&alloc::format!("{}/{}", BIN, name), spawn).expect("spawn failed")+}+ /// hello does ~10 syscalls (open/read/write/close on the ramfs, error cases) /// and exits with the number of failed checks. #[test_case] fn hello_program_passes_its_syscall_checks() {- let p = process::spawn("hello", user_programs::HELLO).expect("spawn hello");+ let p = run("hello", &[]); assert_eq!(process::wait(&p), ExitStatus::Exited(0)); // It created /tmp/note.txt through the syscalls; the kernel sees it too.- let inode = fs::lookup("/tmp/note.txt").expect("hello's file is missing");- assert_eq!(inode.size(), 19);+ assert_eq!(fs::lookup("/tmp/note.txt").expect("hello's file is missing").size(), 19);+}++/// demo checks cwd, FAT reads, stat, lseek, getdents64, mkdir/rmdir/unlink,+/// pipes, spawn + wait (incl. a killed child) -- exit code = failures.+#[test_case]+fn demo_program_passes_its_checks() {+ let p = run("demo", &[]);+ assert_eq!(process::wait(&p), ExitStatus::Exited(0)); } /// evil reads kernel memory -> page fault in ring 3 -> killed. We're still here. #[test_case] fn kernel_memory_access_kills_only_the_process() {- let p = process::spawn("evil", user_programs::EVIL).expect("spawn evil");- assert_eq!(process::wait(&p), ExitStatus::Killed("page fault"));+ let p = run("evil", &[]);+ let status = process::wait(&p);+ assert_eq!(status, ExitStatus::Killed("page fault"));+ assert_eq!(status.code(), 139); } /// spin never makes a syscall while it loops. If preemption didn't work, /// this kernel thread couldn't run again until spin finished. #[test_case] fn spinning_process_is_preempted() {- let p = process::spawn("spin", user_programs::SPIN).expect("spawn spin");+ let p = run("spin", &[]); let start = TICKS.load(Ordering::Relaxed); let mut samples_while_spinning = 0; while p.status().is_none() {@@ -66,18 +85,62 @@ fn spinning_process_is_preempted() { assert!(TICKS.load(Ordering::Relaxed) < start + 18 * 30, "spin didn't finish"); core::hint::spin_loop(); }- assert_eq!(p.status(), Some(ExitStatus::Exited(0)));+ assert_eq!(process::wait(&p), ExitStatus::Exited(0)); assert!(samples_while_spinning > 0, "never ran while spin was running"); } +/// Arguments arrive in the child; its exit code comes back through wait.+/// (Tracing is on: its syscalls get printed to the serial log.)+#[test_case]+fn arguments_and_exit_codes() {+ let mut spawn = SpawnArgs::kernel("child");+ spawn.args.push(String::from("7"));+ spawn.trace = true;+ let p = process::spawn_path("/mnt/disk0/bin/child", spawn).unwrap();+ assert_eq!(process::wait(&p), ExitStatus::Exited(7));+ // Reaped: the pid is gone from the process table.+ assert!(process::get(p.pid).is_none());+}++/// A child's stdout can be a pipe: we read what it printed.+#[test_case]+fn child_stdout_into_a_pipe() {+ let (reader, writer) = fs::pipe::new();+ let mut spawn = SpawnArgs::kernel("child");+ spawn.args.push(String::from("pipe"));+ spawn.stdio[1] = Arc::new(writer);+ let p = process::spawn_path("/mnt/disk0/bin/child", spawn).unwrap();+ let mut out = Vec::new();+ let mut buf = [0u8; 32];+ loop {+ // Blocks until the child writes or exits (its exit closes the pipe).+ match reader.read(&mut buf).unwrap() {+ 0 => break,+ n => out.extend_from_slice(&buf[..n]),+ }+ }+ assert!(out.starts_with(b"hello through a pipe from pid"));+ assert_eq!(process::wait(&p), ExitStatus::Exited(0));+}++#[test_case]+fn spawn_errors() {+ let spawn = || SpawnArgs::kernel("x");+ assert_eq!(process::spawn_path("/mnt/disk0/bin/nope", spawn()).err(), Some(Errno::ENOENT));+ assert_eq!(process::spawn_path("/mnt/disk0/readme.txt", spawn()).err(), Some(Errno::ENOEXEC));+ assert_eq!(process::spawn_path("/mnt/disk0/bin", spawn()).err(), Some(Errno::EISDIR));+ assert_eq!(process::wait_child(12345, 1).err(), Some(Errno::ECHILD));+}+ #[test_case] fn elf_loader_rejects_garbage() {+ let hello = fs::read_all("/mnt/disk0/bin/hello").unwrap(); assert_eq!(elf::check(&[b'x'; 128]), Err(elf::ElfError::NotElf)); assert_eq!(elf::check(b"\x7fELF"), Err(elf::ElfError::TooShort));- let entry = elf::check(user_programs::HELLO).expect("hello should parse");+ let entry = elf::check(&hello).expect("hello should parse"); assert!(entry >= USER_START); // A truncated program: the header is fine, the program headers are cut off.- assert!(elf::check(&user_programs::HELLO[..70]).is_err());+ assert!(elf::check(&hello[..70]).is_err()); } #[test_case]@@ -87,6 +150,35 @@ fn user_pointers_are_validated() { assert_eq!(syscall::user_slice(USER_START, 16).err(), Some(Errno::EFAULT)); assert_eq!(syscall::user_slice(u64::MAX - 4, 16).err(), Some(Errno::EFAULT)); assert!(!memory::user_range_ok(USER_START - 8, 16, false));+ // Zero bytes: any pointer is fine.+ assert_eq!(syscall::user_slice(1, 0).map(|s| s.len()), Ok(0));+}++/// sleep_ticks really blocks: the thread is off the run queue meanwhile.+#[test_case]+fn sleep_blocks_for_the_requested_time() {+ let start = TICKS.load(Ordering::Relaxed);+ thread::sleep_ticks(4);+ let slept = TICKS.load(Ordering::Relaxed) - start;+ assert!((4..8).contains(&slept), "slept {} ticks", slept);+}++static FLAG: AtomicBool = AtomicBool::new(false);+static QUEUE: WaitQueue = WaitQueue::new();++/// A thread blocked on a WaitQueue wakes when another thread signals it.+#[test_case]+fn wait_queue_wakes_the_waiter() {+ thread::spawn("waker", || {+ thread::sleep_ticks(2);+ FLAG.store(true, Ordering::SeqCst);+ QUEUE.wake_all();+ });+ let start = TICKS.load(Ordering::Relaxed);+ let value = QUEUE.wait_until(|| FLAG.load(Ordering::SeqCst).then_some(42));+ assert_eq!(value, 42);+ assert!(TICKS.load(Ordering::Relaxed) - start >= 2);+ let _ = vec![0u8; 1]; // the heap still works after all the switching } #[panic_handler]
@@ -7,24 +7,18 @@ extern crate alloc; -use alloc::{string::String, vec};+use alloc::{string::String, vec, vec::Vec}; use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo; use rust_kernel::errno::Errno;-use rust_kernel::fs::file::{File, InodeFile};+use rust_kernel::fs::file::{File, InodeFile, KIND_FILE, SEEK_CUR, SEEK_END, SEEK_SET, Stat}; use rust_kernel::fs::{self, InodeKind, ramfs::RamFs};-use rust_kernel::memory::{self, BootInfoFrameAllocator};-use rust_kernel::allocator;-use x86_64::VirtAddr; entry_point!(main); fn main(boot_info: &'static BootInfo) -> ! { rust_kernel::init();- let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);- let mut mapper = unsafe { memory::init(phys_mem_offset) };- let mut frame_allocator = unsafe { BootInfoFrameAllocator::init(&boot_info.memory_map) };- allocator::init_heap(&mut mapper, &mut frame_allocator).expect("heap initialization failed");+ rust_kernel::init_memory(boot_info); fs::init(); test_main();@@ -48,7 +42,9 @@ fn lookup_errors() { assert_eq!(fs::lookup("/nope").err(), Some(Errno::ENOENT)); assert_eq!(fs::lookup("/hello.txt/x").err(), Some(Errno::ENOTDIR)); assert_eq!(fs::lookup("relative").err(), Some(Errno::EINVAL));- assert_eq!(InodeFile::open(fs::lookup("/etc").unwrap()).err(), Some(Errno::EISDIR));+ // Directories can be opened (for getdents) but not read like files.+ let dir = InodeFile::open(fs::lookup("/etc").unwrap()).unwrap();+ assert_eq!(dir.read(&mut [0u8; 8]), Err(Errno::EISDIR)); } #[test_case]@@ -75,8 +71,81 @@ fn write_grow_and_list() { let mut buf = vec![0xffu8; 10]; assert_eq!(inode.read_at(0, &mut buf), Ok(7)); assert_eq!(&buf[..7], b"\0\0\0\0abc");- let names = fs::lookup("/").unwrap().list().unwrap();- assert_eq!(names, vec![String::from("etc"), String::from("hello.txt"), String::from("tmp")]);+ let names: Vec<String> = fs::lookup("/").unwrap().list().unwrap().into_iter().map(|e| e.name).collect();+ assert_eq!(names, vec!["etc", "hello.txt", "mnt", "tmp"]);+}++#[test_case]+fn paths_are_normalized() {+ assert_eq!(fs::normalize("/", "a/b").unwrap(), "/a/b");+ assert_eq!(fs::normalize("/home/tyler", "../etc//./motd").unwrap(), "/home/etc/motd");+ assert_eq!(fs::normalize("/x", "/../../y/").unwrap(), "/y"); // ".." of "/" is "/"+ assert_eq!(fs::normalize("/x", "..").unwrap(), "/");+ assert_eq!(fs::normalize("/", "").err(), Some(Errno::ENOENT));+ // ".." works across mount points: /tmp/.. is the root ramfs again.+ assert_eq!(fs::lookup("/tmp/../etc/motd").unwrap().kind(), InodeKind::File);+}++#[test_case]+fn remove_files_and_directories() {+ fs::create("/tmp/rm", InodeKind::Directory).unwrap();+ fs::create("/tmp/rm/f", InodeKind::File).unwrap();+ assert_eq!(fs::remove("/tmp/rm", true), Err(Errno::ENOTEMPTY));+ assert_eq!(fs::remove("/tmp/rm", false), Err(Errno::EISDIR)); // unlink a dir+ assert_eq!(fs::remove("/tmp/rm/f", true), Err(Errno::ENOTDIR)); // rmdir a file+ fs::remove("/tmp/rm/f", false).unwrap();+ fs::remove("/tmp/rm", true).unwrap();+ assert_eq!(fs::lookup("/tmp/rm").err(), Some(Errno::ENOENT));+ assert_eq!(fs::remove("/tmp", true), Err(Errno::EBUSY)); // mount point+}++#[test_case]+fn seek_stat_and_truncate() {+ let inode = fs::create("/tmp/seek.txt", InodeKind::File).unwrap();+ let file = InodeFile::open(inode.clone()).unwrap();+ file.write(b"0123456789").unwrap();+ assert_eq!(file.seek(-3, SEEK_END), Ok(7));+ let mut buf = [0u8; 8];+ assert_eq!(file.read(&mut buf), Ok(3));+ assert_eq!(&buf[..3], b"789");+ assert_eq!(file.seek(2, SEEK_SET), Ok(2));+ assert_eq!(file.seek(1, SEEK_CUR), Ok(3));+ assert_eq!(file.seek(-4, SEEK_CUR), Err(Errno::EINVAL)); // before the start+ assert_eq!(file.stat(), Ok(Stat { kind: KIND_FILE, size: 10 }));+ inode.truncate().unwrap();+ assert_eq!(inode.size(), 0);+}++#[test_case]+fn getdents_returns_linux_dirent64_records() {+ let dir = InodeFile::open(fs::lookup("/etc").unwrap()).unwrap();+ let mut buf = [0u8; 64];+ let n = dir.getdents(&mut buf).unwrap();+ // One entry, "motd": 19-byte header + "motd\0" = 24 bytes (already 8-aligned).+ assert_eq!(n, 24);+ assert_eq!(u16::from_le_bytes([buf[16], buf[17]]), 24); // d_reclen+ assert_eq!(buf[18], 8); // DT_REG+ assert_eq!(&buf[19..24], b"motd\0");+ assert_eq!(dir.getdents(&mut buf), Ok(0)); // end of directory+ assert_eq!(dir.seek(0, SEEK_SET), Ok(0)); // rewind+ assert_eq!(dir.getdents(&mut [0u8; 10]), Err(Errno::EINVAL)); // too small+}++#[test_case]+fn pipes_pass_bytes_and_report_eof() {+ let (reader, writer) = fs::pipe::new();+ assert_eq!(writer.write(b"through the pipe"), Ok(16));+ let mut buf = [0u8; 7];+ assert_eq!(reader.read(&mut buf), Ok(7));+ assert_eq!(&buf, b"through");+ assert_eq!(reader.seek(0, SEEK_SET), Err(Errno::ESPIPE));+ drop(writer); // last writer gone: remaining data, then EOF (no blocking)+ let mut rest = [0u8; 32];+ assert_eq!(reader.read(&mut rest), Ok(9));+ assert_eq!(reader.read(&mut rest), Ok(0));+ let (reader, writer) = fs::pipe::new();+ drop(reader);+ assert_eq!(writer.write(b"x"), Err(Errno::EPIPE)); } #[panic_handler]
@@ -0,0 +1,41 @@+//! Helper program that `demo` (and the tests) spawn:+//! child -> print my arguments, exit 0+//! child <number> -> exit with that number (tests wait() exit codes)+//! child pipe -> write a line to stdout (demo reads it via a pipe)+//! child cat -> copy stdin to stdout until EOF+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+ match arg(1) {+ Some("pipe") => {+ println!("hello through a pipe from pid {}", getpid());+ 0+ }+ Some("cat") => {+ let mut buf = [0u8; 64];+ loop {+ let n = read(0, &mut buf);+ if n <= 0 {+ return n;+ }+ write(1, &buf[..n as usize]);+ }+ }+ Some(number) if number.parse::<i64>().is_ok() => number.parse().unwrap(),+ _ => {+ print!("[child] pid {} got {} args:", getpid(), argc());+ for i in 0..argc() {+ print!(" {:?}", arg(i).unwrap_or("?"));+ }+ println!();+ 0+ }+ }+}
@@ -0,0 +1,152 @@+//! Stage 6 demo: this program was itself loaded from the FAT32 disk.+//! It exercises the syscalls the shell (next stage) will need: cwd, reading+//! a file on the FAT disk, stat, lseek, getdents64, mkdir/unlink/rmdir,+//! pipes, and spawn + wait. Exit code = number of failed checks.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++static mut FAILURES: i64 = 0;++fn check(ok: bool, what: &str) {+ if !ok {+ println!("[demo] CHECK FAILED: {}", what);+ unsafe { FAILURES += 1 };+ }+}++fn cwd_is(expected: &str) -> bool {+ let mut buf = [0u8; 64];+ let n = getcwd(&mut buf);+ n >= 0 && &buf[..n as usize] == expected.as_bytes()+}++fn main() -> i64 {+ println!("[demo] pid {} started from /mnt/disk0/bin/demo", getpid());++ // --- current directory --------------------------------------------------+ check(cwd_is("/"), "cwd starts at /");+ check(chdir("/mnt/disk0") == 0, "chdir /mnt/disk0");+ check(cwd_is("/mnt/disk0"), "getcwd after chdir");++ // --- read a file from the FAT disk (relative path!) ---------------------+ let fd = open("readme.txt", 0);+ check(fd >= 3, "open readme.txt");+ let mut buf = [0u8; 256];+ let n = read(fd, &mut buf).max(0) as usize;+ let text = core::str::from_utf8(&buf[..n]).unwrap_or("?");+ print!("[demo] readme.txt from FAT32: {}", text.lines().next().unwrap_or(""));+ println!();+ check(text.starts_with("Hello from the FAT32 disk!"), "readme.txt contents");++ // --- stat / fstat / lseek ------------------------------------------------+ let mut st = Stat::default();+ check(stat("readme.txt", &mut st) == 0 && st.kind == KIND_FILE && st.size == n as u64, "stat readme.txt");+ check(fstat(1, &mut st) == 0 && st.kind == KIND_CONSOLE, "fstat stdout = console");+ check(stat("bin", &mut st) == 0 && st.kind == KIND_DIR, "stat bin is a directory");+ check(lseek(fd, 6, SEEK_SET) == 6, "lseek SEEK_SET");+ let mut word = [0u8; 4];+ check(read(fd, &mut word) == 4 && &word == b"from", "read after lseek");+ check(lseek(fd, 0, SEEK_END) == n as i64, "lseek SEEK_END = size");+ check(lseek(1, 0, SEEK_SET) == -ESPIPE, "can't seek the console");+ close(fd);++ // --- list a directory with getdents64 -----------------------------------+ let dir = open("bin", 0);+ let mut dents = [0u8; 512];+ let len = getdents64(dir, &mut dents);+ check(len > 0, "getdents64 bin");+ let (mut saw_demo, mut saw_child) = (false, false);+ print!("[demo] /mnt/disk0/bin:");+ for_each_dirent(&dents, len.max(0) as usize, |name, d_type| {+ print!(" {}{}", name, if d_type == 4 { "/" } else { "" });+ saw_demo |= name == "demo";+ saw_child |= name == "child";+ });+ println!();+ check(saw_demo && saw_child, "bin contains demo and child");+ check(getdents64(dir, &mut dents) == 0, "getdents64 at end returns 0");+ close(dir);++ // --- relative paths, "..", errors ----------------------------------------+ check(chdir("notes") == 0 && cwd_is("/mnt/disk0/notes"), "chdir notes");+ check(chdir("../..") == 0 && cwd_is("/mnt"), "chdir ../..");+ check(chdir("disk0/readme.txt") == -ENOTDIR, "chdir to a file -> ENOTDIR");+ check(chdir("/nope") == -ENOENT, "chdir to nothing -> ENOENT");++ // --- create and delete (ramfs /tmp and the FAT disk) ---------------------+ for base in ["/tmp", "/mnt/disk0/notes"] {+ let mut path_buf = [0u8; 64];+ let dir = join(&mut path_buf, base, "/newdir");+ check(mkdir(dir) == 0, "mkdir");+ check(mkdir(dir) == -EEXIST, "mkdir again -> EEXIST");+ let mut file_buf = [0u8; 64];+ let file = join(&mut file_buf, dir, "/f.txt");+ let fd = open(file, O_CREAT);+ check(write(fd, b"bytes") == 5, "write new file");+ close(fd);+ check(rmdir(dir) == -ENOTEMPTY, "rmdir non-empty -> ENOTEMPTY");+ check(unlink(dir) == -EISDIR, "unlink a directory -> EISDIR");+ check(unlink(file) == 0, "unlink file");+ check(rmdir(dir) == 0, "rmdir empty dir");+ check(stat(dir, &mut st) == -ENOENT, "gone after rmdir");+ }+ check(rmdir("/tmp") == -EBUSY, "rmdir a mount point -> EBUSY");+ println!("[demo] mkdir/unlink/rmdir work on ramfs and FAT32");++ // --- spawn a child with its stdout connected to a pipe -------------------+ chdir("/mnt/disk0");+ let (r, w) = pipe().unwrap_or((-1, -1));+ let opts = SpawnOptions { stdout: w, ..SpawnOptions::INHERIT };+ let pid = spawn("bin/child", "child\0pipe", &opts);+ check(pid > 0, "spawn child (pipe)");+ close(w); // only the child holds the write end now: EOF when it exits+ let mut got = [0u8; 128];+ let mut total = 0;+ loop {+ let n = read(r, &mut got[total..]); // blocks until data or EOF+ if n <= 0 {+ break;+ }+ total += n as usize;+ }+ close(r);+ let line = core::str::from_utf8(&got[..total]).unwrap_or("?").trim_end();+ println!("[demo] read from pipe: {:?}", line);+ check(line.starts_with("hello through a pipe"), "pipe contents");+ check(wait(pid) == 0, "child (pipe) exit code 0");++ // --- spawn + wait: exit codes ---------------------------------------------+ let pid = spawn("bin/child", "child\0with\0three args", &SpawnOptions::INHERIT);+ check(wait(pid) == 0, "child with args exits 0");+ let pid = spawn("bin/child", "child\042", &SpawnOptions::INHERIT);+ let code = wait(pid);+ println!("[demo] spawned child 42 (pid {}), wait -> exit code {}", pid, code);+ check(code == 42, "wait returns the exit code");+ check(wait(pid) == -ECHILD, "second wait -> ECHILD");+ let pid = spawn("/mnt/disk0/bin/evil", "", &SpawnOptions::INHERIT);+ check(wait(pid) == 139, "killed child -> 139");+ check(spawn("bin/nope", "", &SpawnOptions::INHERIT) == -ENOENT, "spawn missing -> ENOENT");+ check(spawn("readme.txt", "", &SpawnOptions::INHERIT) == -8, "spawn non-ELF -> ENOEXEC");+ check(wait(1) == -ECHILD, "wait for a non-child -> ECHILD");++ sleep_ms(100); // blocks in the kernel (no busy loop)++ let failures = unsafe { FAILURES };+ if failures == 0 {+ println!("[demo] all stage 6 checks passed, exiting with 0");+ }+ failures+}++/// Concatenate two strings into `buf` (no heap in user programs).+fn join<'a>(buf: &'a mut [u8], a: &str, b: &str) -> &'a str {+ buf[..a.len()].copy_from_slice(a.as_bytes());+ buf[a.len()..a.len() + b.len()].copy_from_slice(b.as_bytes());+ core::str::from_utf8(&buf[..a.len() + b.len()]).unwrap()+}
@@ -18,14 +18,65 @@ pub const SYS_READ: u64 = 0; pub const SYS_WRITE: u64 = 1; pub const SYS_OPEN: u64 = 2; pub const SYS_CLOSE: u64 = 3;+pub const SYS_STAT: u64 = 4;+pub const SYS_FSTAT: u64 = 5;+pub const SYS_LSEEK: u64 = 8;+pub const SYS_PIPE: u64 = 22; pub const SYS_SCHED_YIELD: u64 = 24;+pub const SYS_SLEEP_MS: u64 = 35; pub const SYS_GETPID: u64 = 39; pub const SYS_EXIT: u64 = 60;+pub const SYS_WAIT: u64 = 61;+pub const SYS_GETCWD: u64 = 79;+pub const SYS_CHDIR: u64 = 80;+pub const SYS_MKDIR: u64 = 83;+pub const SYS_RMDIR: u64 = 84;+pub const SYS_UNLINK: u64 = 87;+pub const SYS_GETDENTS64: u64 = 217;+pub const SYS_SPAWN: u64 = 500; pub const O_CREAT: u64 = 0o100;+pub const O_TRUNC: u64 = 0o1000;+pub const SEEK_SET: u64 = 0;+pub const SEEK_CUR: u64 = 1;+pub const SEEK_END: u64 = 2;+pub const SPAWN_TRACE: u64 = 1;+ pub const ENOENT: i64 = 2; pub const EBADF: i64 = 9;+pub const ECHILD: i64 = 10; pub const EFAULT: i64 = 14;+pub const EBUSY: i64 = 16;+pub const EEXIST: i64 = 17;+pub const ENOTDIR: i64 = 20;+pub const EISDIR: i64 = 21;+pub const ESPIPE: i64 = 29;+pub const ENOTEMPTY: i64 = 39;++/// Same layout as the kernel's `fs::file::Stat`.+#[repr(C)]+#[derive(Default, Clone, Copy)]+pub struct Stat {+ pub kind: u64, // KIND_*+ pub size: u64,+}+pub const KIND_FILE: u64 = 1;+pub const KIND_DIR: u64 = 2;+pub const KIND_CONSOLE: u64 = 3;+pub const KIND_PIPE: u64 = 4;++/// Same layout as the kernel's `syscall::SpawnOptions`.+#[repr(C)]+pub struct SpawnOptions {+ pub stdin: i64, // my fd to give the child as its fd 0 (-1 = my fd 0)+ pub stdout: i64,+ pub stderr: i64,+ pub flags: u64, // SPAWN_TRACE+}++impl SpawnOptions {+ pub const INHERIT: SpawnOptions = SpawnOptions { stdin: -1, stdout: -1, stderr: -1, flags: 0 };+} /// Raw system call: number in rax, args in rdi/rsi/rdx, result in rax. /// The `syscall` instruction overwrites rcx and r11.@@ -46,6 +97,26 @@ pub fn syscall3(nr: u64, a1: u64, a2: u64, a3: u64) -> i64 { ret } +/// 5-argument version (4th argument goes in r10, 5th in r8).+pub fn syscall5(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 {+ let ret: i64;+ unsafe {+ asm!(+ "syscall",+ inlateout("rax") nr as i64 => ret,+ in("rdi") a1,+ in("rsi") a2,+ in("rdx") a3,+ in("r10") a4,+ in("r8") a5,+ lateout("rcx") _,+ lateout("r11") _,+ options(nostack),+ );+ }+ ret+}+ pub fn read(fd: i64, buf: &mut [u8]) -> i64 { syscall3(SYS_READ, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64) }@@ -58,9 +129,29 @@ pub fn open(path: &str, flags: u64) -> i64 { pub fn close(fd: i64) -> i64 { syscall3(SYS_CLOSE, fd as u64, 0, 0) }+pub fn stat(path: &str, st: &mut Stat) -> i64 {+ syscall3(SYS_STAT, path.as_ptr() as u64, path.len() as u64, st as *mut Stat as u64)+}+pub fn fstat(fd: i64, st: &mut Stat) -> i64 {+ syscall3(SYS_FSTAT, fd as u64, st as *mut Stat as u64, 0)+}+pub fn lseek(fd: i64, offset: i64, whence: u64) -> i64 {+ syscall3(SYS_LSEEK, fd as u64, offset as u64, whence)+}+/// Returns (read end, write end), or the negative errno.+pub fn pipe() -> Result<(i64, i64), i64> {+ let mut fds = [0u32; 2];+ match syscall3(SYS_PIPE, fds.as_mut_ptr() as u64, 0, 0) {+ 0 => Ok((fds[0] as i64, fds[1] as i64)),+ e => Err(e),+ }+} pub fn sched_yield() { syscall3(SYS_SCHED_YIELD, 0, 0, 0); }+pub fn sleep_ms(ms: u64) {+ syscall3(SYS_SLEEP_MS, ms, 0, 0);+} pub fn getpid() -> i64 { syscall3(SYS_GETPID, 0, 0, 0) }@@ -68,6 +159,75 @@ pub fn exit(code: i64) -> ! { syscall3(SYS_EXIT, code as u64, 0, 0); loop {} // not reached }+/// Wait for child `pid` to end; returns its exit code (139 = killed).+pub fn wait(pid: i64) -> i64 {+ syscall3(SYS_WAIT, pid as u64, 0, 0)+}+/// Current directory into `buf`; returns its length (or -errno).+pub fn getcwd(buf: &mut [u8]) -> i64 {+ syscall3(SYS_GETCWD, buf.as_mut_ptr() as u64, buf.len() as u64, 0)+}+pub fn chdir(path: &str) -> i64 {+ syscall3(SYS_CHDIR, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn mkdir(path: &str) -> i64 {+ syscall3(SYS_MKDIR, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn rmdir(path: &str) -> i64 {+ syscall3(SYS_RMDIR, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn unlink(path: &str) -> i64 {+ syscall3(SYS_UNLINK, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn getdents64(fd: i64, buf: &mut [u8]) -> i64 {+ syscall3(SYS_GETDENTS64, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64)+}+/// Start program `path` with arguments `argv` (NUL-separated, e.g.+/// "child\0hi"). Returns the child's pid (or -errno).+pub fn spawn(path: &str, argv: &str, opts: &SpawnOptions) -> i64 {+ syscall5(+ SYS_SPAWN,+ path.as_ptr() as u64,+ path.len() as u64,+ argv.as_ptr() as u64,+ argv.len() as u64,+ opts as *const SpawnOptions as u64,+ )+}++/// Walk the `linux_dirent64` records getdents64 put in `buf[..len]`,+/// calling `f(name, d_type)` for each (d_type: 4 = directory, 8 = file).+pub fn for_each_dirent(buf: &[u8], len: usize, mut f: impl FnMut(&str, u8)) {+ let mut pos = 0;+ while pos + 19 < len {+ let reclen = u16::from_le_bytes([buf[pos + 16], buf[pos + 17]]) as usize;+ let d_type = buf[pos + 18];+ let name = &buf[pos + 19..pos + reclen];+ let name = &name[..name.iter().position(|&b| b == 0).unwrap_or(name.len())];+ f(core::str::from_utf8(name).unwrap_or("?"), d_type);+ pos += reclen;+ }+}++// --- program arguments ---------------------------------------------------++/// `argv` as the kernel passes it: (pointer, length) pairs.+static mut ARGC: usize = 0;+static mut ARGV: *const [usize; 2] = core::ptr::null();++/// The program's arguments; `arg(0)` is the program name.+pub fn argc() -> usize {+ unsafe { ARGC }+}+pub fn arg(i: usize) -> Option<&'static str> {+ unsafe {+ if i >= ARGC {+ return None;+ }+ let [ptr, len] = *ARGV.add(i);+ core::str::from_utf8(core::slice::from_raw_parts(ptr as *const u8, len)).ok()+ }+} pub struct Stdout; @@ -92,9 +252,14 @@ macro_rules! println { }}; } -/// The kernel jumps here (see process.rs). Run main, exit with its result.+/// The kernel jumps here (see process.rs) with rdi = argc, rsi = argv.+/// Save the arguments, run main, exit with its result. #[unsafe(no_mangle)]-pub extern "C" fn _start() -> ! {+pub extern "C" fn _start(argc: usize, argv: *const [usize; 2]) -> ! {+ unsafe {+ ARGC = argc;+ ARGV = argv;+ } exit(crate::main()) }