rust-kernel
Rustbelt: a hobby x86_64 kernel written in Rust
| author | Tyler Fowler <tyler@localhost> | 2026-10-29 23:57:08 -0700 |
|---|---|---|
| commit | a0fc1ab827ad7844fa38a7380fb140a1894ab230 | |
| parent | 866aaa3978d692723cabcfa91d2ada0517c190b8 | |
| tree | browse files at this commit | |
Async/await executor + async keyboard task (Backspace), preemptive kernel threads - task/: Task, SimpleExecutor, waker-based Executor (hlt when idle), lock-free ScancodeStream; keyboard IRQ handler now only queues scancodes - thread.rs: heap-allocated stacks, global_asm context switch, round-robin scheduler driven by the timer interrupt; demo threads A/B/C + VGA status line - tests: executor, threads; heap 256 KiB; screenshot.sh types Backspace - README: multitasking section, roadmap update; new boot.png
| M | Cargo.lock | +69 -0 |
| M | Cargo.toml | +6 -1 |
| M | README.md | +149 -41 |
| M | boot.png | +0 -0 |
| M | scripts/screenshot.sh | +23 -8 |
| M | src/allocator.rs | +1 -1 |
| M | src/interrupts.rs | +13 -43 |
| M | src/lib.rs | +2 -0 |
| M | src/main.rs | +71 -7 |
| A | src/task/executor.rs | +121 -0 |
| A | src/task/keyboard.rs | +117 -0 |
| A | src/task/mod.rs | +71 -0 |
| A | src/task/simple_executor.rs | +40 -0 |
| A | src/thread.rs | +201 -0 |
| M | src/vga_buffer.rs | +45 -26 |
| A | tests/executor.rs | +73 -0 |
| A | tests/threads.rs | +76 -0 |
17 files changed, 1078 insertions, 127 deletions
@@ -27,12 +27,66 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e22d5a0b9e11dd5bee9d24a68885de6dc7ed367f897323b1c1286150fd469374" [[package]]+name = "conquer-once"+version = "0.4.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "5d008a441c0f269f36ca13712528069a86a3e60dffee1d98b976eb3b0b2160b4"+dependencies = [+ "conquer-util",+]++[[package]]+name = "conquer-util"+version = "0.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e763eef8846b13b380f37dfecda401770b0ca4e56e95170237bd7c25c7db3582"++[[package]] name = "const_fn" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413d67b29ef1021b4d60f4aa1e925ca031751e213832b4b1d588fae623c05c60" [[package]]+name = "crossbeam-queue"+version = "0.3.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae"+dependencies = [+ "crossbeam-utils",+]++[[package]]+name = "crossbeam-utils"+version = "0.8.23"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"++[[package]]+name = "futures-core"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"++[[package]]+name = "futures-task"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"++[[package]]+name = "futures-util"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"+dependencies = [+ "futures-core",+ "futures-task",+ "pin-project-lite",+ "slab",+]++[[package]] name = "lazy_static" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -75,6 +129,12 @@ dependencies = [ ] [[package]]+name = "pin-project-lite"+version = "0.2.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"++[[package]] name = "raw-cpuid" version = "10.7.0" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -88,6 +148,9 @@ name = "rust_kernel" version = "0.1.0" dependencies = [ "bootloader",+ "conquer-once",+ "crossbeam-queue",+ "futures-util", "lazy_static", "linked_list_allocator", "pc-keyboard",@@ -111,6 +174,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]]+name = "slab"+version = "0.4.12"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"++[[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -18,13 +18,18 @@ pic8259 = "0.11" # driver for the two chained 8259 interrupt controllers pc-keyboard = "0.7" # turns PS/2 scancodes into key events / characters linked_list_allocator = "0.9" # ready-made heap allocator (fallback for ours) +# Async/await support without std (all with default features off):+crossbeam-queue = { version = "0.3.11", default-features = false, features = ["alloc"] } # lock-free ArrayQueue+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+ [dependencies.lazy_static] version = "1.0" features = ["spin_no_std"] [features] # Build with `--features headless` to make the kernel shut QEMU down after-# printing its greeting (used by scripts/run-headless.sh). Without it the+# ~5 seconds of timer ticks (used by scripts/run-headless.sh). Without it the # kernel just halts forever so you can look at the QEMU window. headless = [] # Build with `--features stack-overflow` to deliberately overflow the kernel
@@ -7,11 +7,14 @@ Right now it boots in QEMU, prints to the VGA text screen and the serial port stack), runs the timer interrupt (uptime clock top-right), echoes what you type on the PS/2 keyboard, walks and edits its own page tables, and has a kernel heap (`Box`, `Vec`, `Rc` work) backed by a fixed-size-block allocator.+It multitasks two ways: an **async/await executor** (cooperative tasks; the+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 has a `cargo test` suite that runs inside QEMU. Most code follows the blog_os sources (MIT / Apache-2.0) closely, with extra-comments; small additions are the serial mirroring, uptime clock, demo-features and scripts.+comments; additions are the serial mirroring, uptime clock, demo features,+scripts, and the preemptive threads (`src/thread.rs`, not in the blog series).  @@ -53,8 +56,7 @@ cargo run # build + boot in a QEMU window; serial outp **Trying the keyboard:** with `cargo run`, click into the QEMU window and type; characters appear on the bottom line of the screen *and* in your terminal (via serial). Shift works; Enter starts a new line; arrows/F-keys show as e.g.-`<ArrowUp>`. Backspace isn't handled yet (shows a block) -- a good first-exercise. To get the mouse/keyboard back from QEMU press Ctrl+Alt+G; close the+`<ArrowUp>`; Backspace erases the previous character (on the current line). To get the mouse/keyboard back from QEMU press Ctrl+Alt+G; close the window or press Ctrl+C in the terminal to quit. (Over SSH without X, use `cargo run -- -display curses` to see the VGA screen right in your terminal; quit from another terminal with `pkill qemu-system`.)@@ -62,22 +64,24 @@ quit from another terminal with `pkill qemu-system`.) Headless (no window, good for SSH / scripts): ```sh-./scripts/run-headless.sh # boot, breakpoint, ~3 s of timer ticks, then QEMU exits (script exit 0)+./scripts/run-headless.sh # boot, breakpoint, demos, ~5 s of threads + timer ticks, then QEMU exits (script exit 0) ./scripts/run-headless.sh stack-overflow # deliberately overflow the stack; passes if the double-fault handler catches it ./scripts/run-headless.sh page-fault # write to unmapped 0xdeadbeaf; passes if the page-fault handler catches it cargo run --features headless -- -display none # same as the first, via cargo cargo run --features headless,stack-overflow -- -display none # same as the second # (QEMU's exit status is 33 = success; cargo just reports it as non-zero) cargo run -- -display none # headless but kernel keeps running; Ctrl+C to stop-./scripts/screenshot.sh [out.png] [text] # boot, "type" text via QEMU monitor `sendkey`, check it echoes- # on serial, save the VGA screen to boot.png+./scripts/screenshot.sh [out.png] [text] [expected]+ # boot, "type" text via QEMU monitor `sendkey` ("<" = Backspace;+ # default "Hello, kernex<l!"), check serial shows the expected+ # result ("Hello, kernel!"), save the VGA screen to boot.png ``` Cargo features (in `Cargo.toml`): | Feature | Effect | |---|---|-| `headless` | print a `.` per timer tick to serial and exit QEMU after 54 ticks (~3 s); panics exit with failure |+| `headless` | print a `.` per timer tick to serial and exit QEMU after 90 ticks (~5 s); panics exit with failure | | `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) | @@ -89,26 +93,47 @@ EXCEPTION: BREAKPOINT InterruptStackFrame { instruction_pointer: ..., ... } Survived the breakpoint, still running. [paging] CR3 (level 4 table) at phys 0x1000-[paging] physical memory mapped at virt 0x18000000000-[memory] 16 memory map regions, 123732 KiB usable RAM-[paging] translate VirtAddr(0xb8000) -> Some(PhysAddr(0xb8000))-[paging] translate VirtAddr(0x201008) -> Some(PhysAddr(0x401008))-[paging] translate VirtAddr(0x10000201a10) -> Some(PhysAddr(0x27ea10))-[paging] translate VirtAddr(0x18000000000) -> Some(PhysAddr(0x0))+... (paging + memory lines as before) [paging] mapped page 0xdeadbeaf000 -> VGA frame, wrote "New!" through it-[heap] 100 KiB heap at 0x444444440000 (fixed-size-block allocator)+.[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-Type something! (keyboard echo)-Timer + keyboard interrupts enabled.-.................. 18 ticks-.................. 36 ticks-.................. 54 ticks+[thread] spawned threads A, B, C (status on the top line)+Type something! (keyboard echo, Backspace works)+[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)+.[thread A] count 1+.[thread B] count 1+.......[thread A] count 2+.[thread B] count 2+.....[thread C] busy loop round 1 (count = 2000000)+.. 18 ticks+[thread A] count 3+.[thread B] count 3+.......[thread A] count 4+.[thread B] count 4+.....[thread C] busy loop round 2 (count = 4000000)+..[thread A] count 5+.[thread B] count 5+. 36 ticks+............[thread C] busy loop round 3 (count = 6000000)+...... 54 ticks+..........[thread C] busy loop round 4 (count = 8000000)+........ 72 ticks+....[thread C] busy loop round 5 (count = 10000000)+[thread C] done, exiting+.............. 90 ticks [ok] timer works, exiting ``` +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.+ ## Tests ```sh@@ -137,6 +162,8 @@ heap_allocation::simple_allocation... [ok] | `tests/heap_allocation.rs` | Box, big Vec, 100 K short-lived boxes (memory reuse), same with one long-lived box, >2 KiB fallback allocation | | `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/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]` function and passes them to `rust_kernel::test_runner` (in `src/lib.rs`), which@@ -158,12 +185,18 @@ src/lib.rs the kernel library: module list, init(), kprintln!, te src/main.rs the bootable binary: entry_point!(kernel_main), paging + heap demos, panic handler src/gdt.rs GDT + TSS with a separate IST stack for the double-fault handler src/interrupts.rs IDT; breakpoint / page fault / double fault handlers; 8259 PIC;- timer (uptime + ticks) and keyboard (echo) interrupt handlers+ timer handler (uptime, ticks, calls the thread scheduler),+ keyboard handler (just queues the scancode) src/memory.rs OffsetPageTable setup from CR3, example mapping, BootInfoFrameAllocator src/allocator.rs heap location/size, init_heap (maps heap pages), #[global_allocator], Locked<A> src/allocator/bump.rs bump allocator (not active, for learning) src/allocator/fixed_size_block.rs fixed-size-block allocator (ACTIVE), falls back to linked_list_allocator-src/vga_buffer.rs VGA text-mode writer, print!/println!, uptime display+src/task/mod.rs async: Task (pinned boxed future + id), yield_now()+src/task/simple_executor.rs first executor: polls every task in a loop (no wakers)+src/task/executor.rs real executor: task queue + wakers, sleeps with hlt when idle+src/task/keyboard.rs lock-free scancode queue, ScancodeStream, async keyboard echo task+src/thread.rs preemptive kernel threads: spawn, round-robin schedule(), asm context switch+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 scripts/runner.sh `cargo run`/`cargo test` runner: `bootimage runner` + stale-image fix@@ -187,8 +220,8 @@ boot.png screenshot of the VGA screen - **Hardware interrupts**: the two 8259 PICs are remapped to vectors 32-47 (0-31 are CPU exceptions) and then `sti` enables interrupts. The timer (IRQ 0, ~18.2 Hz) bumps an atomic tick counter and redraws the uptime clock; the keyboard (IRQ 1)- reads the scancode from port 0x60, decodes it with `pc-keyboard` (US layout,- scancode set 1) and echoes it. Each handler must send End-Of-Interrupt to the+ reads the scancode from port 0x60 and queues it; an async task decodes it with+ `pc-keyboard` (US layout, scancode set 1) and echoes it (see Multitasking below). Each handler must send End-Of-Interrupt to the PIC or no further interrupts arrive. `print!`/`serial_print!` disable interrupts while holding their lock, otherwise an interrupt handler printing while `_start` holds the lock would deadlock. The idle loop uses `hlt`, so QEMU@@ -218,7 +251,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, 100 KiB (we map it in `allocator::init_heap`) |+| `0x4444_4444_0000` | kernel heap, 256 KiB (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,@@ -242,9 +275,80 @@ one free list per size, so alloc/free are O(1) pops/pushes; bigger requests and lists fall back to the linked-list allocator. Switch allocators by editing one line in `src/allocator.rs`. +## Multitasking (async/await + threads)++### Cooperative: async/await (`src/task/`, phil-opp chapter 12)++An `async fn` compiles into a state machine (a `Future`). An *executor* calls+`poll` on it; the future runs until it has to wait, then returns `Pending` and+remembers a `Waker`. Whoever makes progress possible later (here: the keyboard+interrupt) calls `waker.wake()`, which puts the task's id back into the+executor's queue. When the queue is empty, the executor `hlt`s -- carefully:+it disables interrupts, checks the queue, and then uses `sti; hlt` as one step+(`enable_and_hlt`), so a wakeup arriving in between can't be missed.++The keyboard interrupt handler now does almost nothing: read the scancode from+port 0x60 and push it into a fixed-size lock-free queue (`crossbeam_queue::ArrayQueue`),+then wake the keyboard task (`futures_util::task::AtomicWaker`). No locks in the+interrupt handler means no deadlock if the interrupt arrives while the task+holds one. The task (`print_keypresses`) is an `async` loop over a+`ScancodeStream` that decodes and echoes characters.++"Cooperative" means a task only gives up the CPU at an `.await`. A task that+loops forever without awaiting freezes every other task. Upside: switching is+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+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!`):++```+push rbp, rbx, r12, r13, r14, r15 ; callee-saved regs of the OLD thread, onto its stack+mov [rdi], rsp ; remember where the old stack ends+mov rsp, rsi ; ---- now on the NEW thread's stack ----+pop r15, r14, r13, r12, rbx, rbp ; its callee-saved regs+ret ; return to wherever IT called switch_context+```++Why only those six registers? `switch_context` is called like a normal+function, so the compiler already saved the caller-saved registers, and the+`x86-interrupt` handler around it saved everything the interrupted code was+using (plus the CPU pushed RIP/RSP/RFLAGS). So a paused thread's complete+state sits on its own stack; the scheduler only stores its `rsp`. The new+thread "returns" from `schedule()`, from its timer handler, and `iretq`s back to+where it was interrupted.++A brand-new thread was never interrupted, so `spawn` hand-builds a stack that+looks as if `switch_context` had saved it: six register slots (r12 = the+thread's function) and a return address pointing to `thread_trampoline`, which+moves r12 into rdi and calls `thread_start(entry)`. That re-enables interrupts+(we arrive from inside the timer interrupt, where they're off), runs the+function, and marks the thread finished when it returns. The code that booted+the kernel becomes thread 0, "main", which runs the async executor.++Preemption means even `loop {}` can't hog the CPU (thread C in the demo is a+pure busy loop). The price: any code can be interrupted anywhere, so shared+data needs locks, and a lock that the timer handler might also want must be+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.+ ## Roadmap -Done: chapters 1-11 of the 2nd edition (everything up to Async/Await).+Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads. - [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)@@ -260,29 +364,33 @@ Done: chapters 1-11 of the 2nd edition (everything up to Async/Await). - [x] Heap allocation — `alloc`, heap mapping, global allocator. [phil-opp](https://os.phil-opp.com/heap-allocation/), [OSDev: Memory Allocation](https://wiki.osdev.org/Memory_Allocation) - [x] Allocator designs — bump, linked list (crate), fixed-size block (active). [phil-opp](https://os.phil-opp.com/allocator-designs/) -Next stages:--1. **Async/await multitasking** — a cooperative executor; turn the keyboard handler- into a task that reads scancodes from a lock-free queue (also gives you a place to- handle Backspace properly). [phil-opp: Async/Await](https://os.phil-opp.com/async-await/),- [OSDev: Multitasking Systems](https://wiki.osdev.org/Multitasking_Systems)-2. **Preemptive threads** — timer-driven context switches between kernel threads with- their own stacks. [OSDev: Brendan's Multi-tasking Tutorial](https://wiki.osdev.org/Brendan%27s_Multi-tasking_Tutorial),- [OSDev: Context Switching](https://wiki.osdev.org/Context_Switching)-3. **User mode & syscalls** — per-process page tables, ring 3 code/data segments in- the GDT, `syscall`/`sysret`, loading ELF programs. (Beyond the phil-opp series.)+- [x] Async/await — Task, simple executor, waker-based executor, lock-free async keyboard task (with Backspace). [phil-opp: Async/Await](https://os.phil-opp.com/async-await/), [OSDev: Multitasking Systems](https://wiki.osdev.org/Multitasking_Systems)+- [x] Preemptive kernel threads — own stacks, timer-driven round-robin, asm context switch. [OSDev: Brendan's Multi-tasking Tutorial](https://wiki.osdev.org/Brendan%27s_Multi-tasking_Tutorial), [OSDev: Context Switching](https://wiki.osdev.org/Context_Switching)++Next stages (beyond the phil-opp series):++1. **User mode (ring 3) & syscalls** — ring 3 code/data segments in the GDT, set+ `TSS.rsp0` per thread so interrupts from user mode land on the kernel stack,+ enter user mode with `iretq`, then `syscall`/`sysret` (or `int 0x80` first) for a+ few calls (`write`, `exit`, `yield`). Per-process page tables (a new level-4+ table that shares the kernel half). Threads become processes. [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), [OSDev: ELF](https://wiki.osdev.org/ELF)-4. **Filesystem** — a VFS layer, a RAM disk / initrd first, then FAT on an ATA/virtio disk.+ [OSDev: SYSENTER/SYSCALL](https://wiki.osdev.org/SYSENTER)+2. **ELF loading** — parse a static ELF (e.g. with the `xmas-elf` or `goblin` crate),+ map its segments into a fresh address space, jump to the entry point in ring 3.+ [OSDev: ELF](https://wiki.osdev.org/ELF), [OSDev: ELF Tutorial](https://wiki.osdev.org/ELF_Tutorial)+3. **Filesystem** — a VFS layer, a RAM disk / initrd first, then FAT on an ATA/virtio disk. [OSDev: File Systems](https://wiki.osdev.org/File_Systems), [OSDev: VFS](https://wiki.osdev.org/VFS), [OSDev: Initrd](https://wiki.osdev.org/Initrd), [OSDev: FAT](https://wiki.osdev.org/FAT), [OSDev: ATA PIO Mode](https://wiki.osdev.org/ATA_PIO_Mode)-5. **Shell** — line editing on top of the keyboard task, a few built-in commands+4. **Shell** — line editing on top of the keyboard task, a few built-in commands (`help`, `mem`, `uptime`, `ls`, `cat`), later launching user programs. [OSDev: Shell](https://wiki.osdev.org/Shell) -Small exercises along the way: handle Backspace, speed up the PIT+Small exercises along the way: a *blocked* state + sleep queue for threads,+free finished threads' stacks, 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 `mem` stat (bytes allocated) to the allocator.
Binary file changed.
@@ -4,7 +4,8 @@ # a PNG. Doubles as an automated keyboard-interrupt test: the typed text is # echoed on the serial port too, so we can grep for it. #-# Usage: ./scripts/screenshot.sh [out.png] [text-to-type]+# Usage: ./scripts/screenshot.sh [out.png] [text-to-type] [expected-result]+# In text-to-type, "<" means Backspace. The default types a typo and fixes it. set -euo pipefail cd "$(dirname "$0")/.." @@ -14,21 +15,22 @@ touch target/x86_64-rust_kernel/debug/rust_kernel cargo bootimage IMG=target/x86_64-rust_kernel/debug/bootimage-rust_kernel.bin OUT="${1:-$PWD/boot.png}"-TEXT="${2:-Hello, kernel!}"+TEXT="${2:-Hello, kernex<l!}"+EXPECT="${3:-Hello, kernel!}" SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX) SERIAL_LOG=$(mktemp /tmp/serial.XXXXXX) qemu-system-x86_64 -drive format=raw,file="$IMG" \ -serial file:"$SERIAL_LOG" -display none -monitor unix:"$SOCK",server,nowait & QPID=$!-sleep 3 # give the BIOS + bootloader + kernel time to boot+sleep 6 # boot, and let the demo threads finish their serial output # Talk to the QEMU monitor over its UNIX socket (python avoids needing socat). python3 - "$SOCK" "$OUT" "$TEXT" <<'PY' import socket, sys, time sock, out, text = sys.argv[1:] # QEMU key names for the characters we might type.-names = {' ': 'spc', ',': 'comma', '.': 'dot', '!': 'shift-1', '?': 'shift-slash',+names = {'<': 'backspace', ' ': 'spc', ',': 'comma', '.': 'dot', '!': 'shift-1', '?': 'shift-slash', '-': 'minus', '\n': 'ret'} def key(c): if c in names: return names[c]@@ -43,12 +45,25 @@ s.sendall(b"quit\n"); time.sleep(0.5) PY wait $QPID || true echo "---- serial output ----"-cat "$SERIAL_LOG"; echo+cat -v "$SERIAL_LOG"; echo # cat -v shows Backspace as ^H echo "-----------------------"-if grep -qF -- "$TEXT" "$SERIAL_LOG"; then- echo "[screenshot] keyboard echo OK: '$TEXT'"+# Replay the serial output like a terminal would ("\b \b" = erase one+# character) and check the expected text is there.+if python3 - "$SERIAL_LOG" "$EXPECT" <<'PY'+import sys+data = open(sys.argv[1], 'rb').read().decode(errors='replace').replace('\x08 \x08', '\x08')+out = []+for ch in data:+ if ch == '\x08':+ if out: out.pop()+ else:+ out.append(ch)+sys.exit(0 if sys.argv[2] in ''.join(out) else 1)+PY+then+ echo "[screenshot] keyboard echo OK: typed '$TEXT' -> '$EXPECT'" else- echo "[screenshot] keyboard echo NOT found"+ echo "[screenshot] keyboard echo NOT found (expected '$EXPECT')" fi rm -f "$SERIAL_LOG" echo "[screenshot] saved $OUT"
@@ -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 = 100 * 1024; // 100 KiB+pub const HEAP_SIZE: usize = 256 * 1024; // 256 KiB (thread stacks live here too) #[global_allocator] static ALLOCATOR: Locked<fixed_size_block::FixedSizeBlockAllocator> =
@@ -8,7 +8,7 @@ //! Handlers use the special `extern "x86-interrupt"` calling convention: the //! compiler saves/restores all registers and returns with `iretq`. -use crate::{gdt, print, println, serial_print, serial_println};+use crate::{gdt, println, serial_println}; use core::sync::atomic::{AtomicU64, Ordering}; use lazy_static::lazy_static; use pic8259::ChainedPics;@@ -139,71 +139,41 @@ extern "x86-interrupt" fn timer_interrupt_handler(_stack_frame: InterruptStackFr } // Headless runs: print a dot per tick to serial (proof the timer fires),- // then stop after ~3 seconds so scripts finish on their own. (Not done in+ // then stop after ~5 seconds so scripts finish on their own. (Not done in // normal runs, so the dots don't get mixed into your keyboard echo.) #[cfg(feature = "headless")] {- serial_print!(".");+ crate::serial_print!("."); if ticks % 18 == 0 { serial_println!(" {} ticks", ticks); } #[cfg(not(any(feature = "stack-overflow", feature = "page-fault")))]- if ticks >= 54 {+ if ticks >= 90 { serial_println!("[ok] timer works, exiting"); crate::exit_qemu(crate::QemuExitCode::Success); } } // Tell the PIC we're done, or it will never send another timer interrupt.+ // Must happen *before* switching threads: the thread we switch to might+ // be brand new and never return through this handler. unsafe { PICS.lock().notify_end_of_interrupt(InterruptIndex::Timer.as_u8()); }++ // Preemption: maybe switch to another kernel thread (see thread.rs).+ crate::thread::schedule(); } -/// IRQ 1. The keyboard controller has a scancode waiting at I/O port 0x60;-/// we must read it (or no more keyboard interrupts arrive), decode it with-/// `pc-keyboard`, and echo the character to the screen and serial port.+/// IRQ 1. The keyboard controller has a scancode waiting at I/O port 0x60.+/// We must read it (or no more keyboard interrupts arrive), then just queue+/// it for the async keyboard task (task/keyboard.rs) to decode and print. extern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: InterruptStackFrame) {- use pc_keyboard::{DecodedKey, HandleControl, KeyCode, Keyboard, ScancodeSet1, layouts};- use spin::Mutex; use x86_64::instructions::port::Port; - // Decoder state (shift held? multi-byte scancode in progress?) persists- // between interrupts, so it lives in a static.- static KEYBOARD: Mutex<Keyboard<layouts::Us104Key, ScancodeSet1>> = Mutex::new(Keyboard::new(- ScancodeSet1::new(),- layouts::Us104Key,- HandleControl::Ignore,- ));-- let mut keyboard = KEYBOARD.lock(); let mut port = Port::new(0x60); let scancode: u8 = unsafe { port.read() };-- // add_byte: scancode -> key event (press/release); process_keyevent:- // key event -> character, applying shift/caps-lock etc.- if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {- if let Some(key) = keyboard.process_keyevent(key_event) {- match key {- DecodedKey::Unicode(character) => {- print!("{}", character);- serial_print!("{}", character);- }- // Modifier keys only change how *other* keys decode; the- // decoder already tracks them, so don't print anything.- DecodedKey::RawKey(- KeyCode::LShift | KeyCode::RShift | KeyCode::LControl- | KeyCode::RControl | KeyCode::LAlt | KeyCode::RAltGr- | KeyCode::CapsLock,- ) => {}- DecodedKey::RawKey(key) => {- // Other keys without a character (arrows, F1, ...)- print!("<{:?}>", key);- serial_print!("<{:?}>", key);- }- }- }- }+ crate::task::keyboard::add_scancode(scancode); unsafe { PICS.lock().notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
@@ -26,6 +26,8 @@ pub mod gdt; // GDT + TSS: gives the double-fault handler its own stack pub mod interrupts; // IDT, exception handlers, PIC, timer + keyboard pub mod memory; // paging: page tables, address translation, frame allocator pub mod serial; // COM1 serial port+pub mod task; // cooperative multitasking: async tasks + executor+pub mod thread; // preemptive kernel threads + context switch pub mod vga_buffer; // 80x25 VGA text mode screen /// Set up everything interrupt-related. Order matters:
@@ -13,7 +13,8 @@ extern crate alloc; // Box, Vec, Rc... once the heap is set up use alloc::{boxed::Box, rc::Rc, vec, vec::Vec}; use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo;-use rust_kernel::{allocator, kprintln, memory, println, serial_println};+use rust_kernel::task::{Task, executor::Executor, keyboard};+use rust_kernel::{allocator, kprintln, memory, println, serial_println, thread, vga_buffer}; use x86_64::{VirtAddr, structures::paging::{Page, Translate}}; // `entry_point!` defines the real `_start` for us and checks at compile time@@ -36,14 +37,77 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { stack_overflow(); } - paging_demo(boot_info);+ paging_demo(boot_info); // also sets up the heap, needed from here on++ // --- Preemptive threads (thread.rs) -----------------------------------+ // From now on the timer interrupt switches between these and the code+ // below (the "main" thread) every tick (~55 ms).+ thread::spawn("A", thread_a);+ thread::spawn("B", thread_b);+ thread::spawn("C", thread_c);+ kprintln!("[thread] spawned threads A, B, C (status on the top line)");++ // --- Cooperative async tasks (task/) ------------------------------------+ // The main thread runs the executor; the keyboard is just another task.+ let mut executor = Executor::new();+ executor.spawn(Task::new(example_task()));+ executor.spawn(Task::new(keyboard::print_keypresses()));+ kprintln!("Type something! (keyboard echo, Backspace works)");+ executor.run(); // never returns: runs tasks, `hlt`s when idle+} - kprintln!("Type something! (keyboard echo)");- serial_println!("Timer + keyboard interrupts enabled.");+// --- async demo ------------------------------------------------------------- - // Nothing else to do: sleep until the next interrupt, forever. The timer- // and keyboard handlers do their work in between.- rust_kernel::hlt_loop();+async fn async_number() -> u32 {+ 42+}++async fn example_task() {+ let number = async_number().await;+ kprintln!("[async] async number: {}", number);+ for i in 1..=3 {+ rust_kernel::task::yield_now().await; // let other tasks run+ serial_println!("[async] example task step {} (after yield)", i);+ }+}++// --- thread demo ------------------------------------------------------------++/// Threads A and B: sleep a bit, count, show the count on the top line.+/// They print their first 5 counts to serial too (more would drown out the+/// keyboard echo in the terminal).+fn counter_thread(name: &str, period_ticks: u64, col: usize) {+ for n in 1u64.. {+ if n <= 5 {+ serial_println!("[thread {}] count {}", name, n);+ }+ vga_buffer::print_at(0, col, format_args!(" {}:{:>5} ", name, n));+ thread::sleep_ticks(period_ticks); // hlt -> other threads run meanwhile+ }+}++fn thread_a() {+ counter_thread("A", 5, 0);+}++fn thread_b() {+ counter_thread("B", 7, 10);+}++/// Thread C never sleeps or yields: a pure busy loop. It still only gets+/// its share of the CPU, because the timer preempts it. It finishes after a+/// while to show that threads can exit.+fn thread_c() {+ let mut count: u64 = 0;+ for round in 1..=5 {+ for _ in 0..2_000_000 {+ count = core::hint::black_box(count + 1); // don't let the compiler skip the work+ }+ serial_println!("[thread C] busy loop round {} (count = {})", round, count);+ vga_buffer::print_at(0, 20, format_args!(" C:{:>5} ", round));+ }+ serial_println!("[thread {}] done, exiting", thread::current_name());+ vga_buffer::print_at(0, 20, format_args!(" C: done ")); } /// Chapters "Introduction to Paging", "Paging Implementation", "Heap Allocation".
@@ -0,0 +1,121 @@+//! The real executor: only polls tasks that were *woken*.+//!+//! * `tasks`: all live tasks, by id+//! * `task_queue`: ids of tasks ready to run. Wakers push onto it -- possibly+//! from an interrupt handler, so it is a lock-free queue and+//! pushing never allocates.+//! * `waker_cache`: one Waker per task, created once and reused.+//!+//! When nothing is ready, the CPU sleeps with `hlt` until the next interrupt.++use super::{Task, TaskId};+use alloc::{collections::BTreeMap, sync::Arc, task::Wake};+use core::task::{Context, Poll, Waker};+use crossbeam_queue::ArrayQueue;++pub struct Executor {+ tasks: BTreeMap<TaskId, Task>,+ task_queue: Arc<ArrayQueue<TaskId>>,+ waker_cache: BTreeMap<TaskId, Waker>,+}++impl Executor {+ pub fn new() -> Self {+ Executor {+ tasks: BTreeMap::new(),+ task_queue: Arc::new(ArrayQueue::new(100)),+ waker_cache: BTreeMap::new(),+ }+ }++ pub fn spawn(&mut self, task: Task) {+ let task_id = task.id;+ if self.tasks.insert(task.id, task).is_some() {+ panic!("task with same ID already in tasks");+ }+ self.task_queue.push(task_id).expect("queue full");+ }++ /// Run tasks forever (the kernel's main loop).+ pub fn run(&mut self) -> ! {+ loop {+ self.run_ready_tasks();+ self.sleep_if_idle();+ }+ }++ /// Run until every task has finished (used by tests).+ pub fn run_until_done(&mut self) {+ while !self.tasks.is_empty() {+ self.run_ready_tasks();+ self.sleep_if_idle();+ }+ }++ fn run_ready_tasks(&mut self) {+ // Destructure `self` so we can borrow the fields separately.+ let Self { tasks, task_queue, waker_cache } = self;++ while let Some(task_id) = task_queue.pop() {+ let task = match tasks.get_mut(&task_id) {+ Some(task) => task,+ None => continue, // task already finished (stale wake-up)+ };+ let waker = waker_cache+ .entry(task_id)+ .or_insert_with(|| TaskWaker::new(task_id, task_queue.clone()));+ let mut context = Context::from_waker(waker);+ if let Poll::Ready(()) = task.poll(&mut context) {+ tasks.remove(&task_id);+ waker_cache.remove(&task_id);+ }+ }+ }++ /// `hlt` until the next interrupt, but only if the queue is still empty.+ /// Interrupts are disabled for the check: otherwise an interrupt could+ /// wake a task *between* the check and `hlt`, and we'd sleep with work+ /// waiting. `enable_and_hlt` is `sti; hlt`, which the CPU executes+ /// atomically (interrupts are only recognised after the `hlt` starts).+ fn sleep_if_idle(&self) {+ use x86_64::instructions::interrupts::{self, enable_and_hlt};+ interrupts::disable();+ if self.task_queue.is_empty() {+ enable_and_hlt();+ } else {+ interrupts::enable();+ }+ }+}++impl Default for Executor {+ fn default() -> Self {+ Self::new()+ }+}++/// Waking = pushing the task id back onto the ready queue.+struct TaskWaker {+ task_id: TaskId,+ task_queue: Arc<ArrayQueue<TaskId>>,+}++impl TaskWaker {+ fn new(task_id: TaskId, task_queue: Arc<ArrayQueue<TaskId>>) -> Waker {+ // `Wake` trait + Arc -> Waker conversion from the alloc crate.+ Waker::from(Arc::new(TaskWaker { task_id, task_queue }))+ }++ fn wake_task(&self) {+ self.task_queue.push(self.task_id).expect("task_queue full");+ }+}++impl Wake for TaskWaker {+ fn wake(self: Arc<Self>) {+ self.wake_task();+ }+ fn wake_by_ref(self: &Arc<Self>) {+ self.wake_task();+ }+}
@@ -0,0 +1,117 @@+//! Async keyboard input.+//!+//! The interrupt handler does the bare minimum: read the scancode and push it+//! into a lock-free queue (`add_scancode`), then wake whoever is waiting.+//! Decoding and printing happen later in a normal async task+//! (`print_keypresses`), outside interrupt context. That keeps the handler+//! short and means it never takes a lock (no deadlock risk).++use crate::{print, serial_print, vga_buffer};+use conquer_once::spin::OnceCell;+use core::{+ pin::Pin,+ task::{Context, Poll},+};+use crossbeam_queue::ArrayQueue;+use futures_util::{+ stream::{Stream, StreamExt},+ task::AtomicWaker,+};+use pc_keyboard::{DecodedKey, HandleControl, KeyCode, Keyboard, ScancodeSet1, layouts};++/// Scancodes waiting to be decoded. `OnceCell` because ArrayQueue::new+/// allocates, so it can only be created after the heap exists.+static SCANCODE_QUEUE: OnceCell<ArrayQueue<u8>> = OnceCell::uninit();+/// The waker of the task waiting for scancodes (if any).+static WAKER: AtomicWaker = AtomicWaker::new();++/// Called by the keyboard interrupt handler. Must not block or allocate.+pub(crate) fn add_scancode(scancode: u8) {+ if let Ok(queue) = SCANCODE_QUEUE.try_get() {+ if queue.push(scancode).is_err() {+ serial_print!("WARNING: scancode queue full; dropping keyboard input\n");+ } else {+ WAKER.wake();+ }+ }+ // Queue not created yet (no keyboard task): just drop the key.+}++/// A `Stream` (async iterator) of scancodes. Only one may exist.+pub struct ScancodeStream {+ _private: (), // prevent construction without `new`+}++impl ScancodeStream {+ pub fn new() -> Self {+ SCANCODE_QUEUE+ .try_init_once(|| ArrayQueue::new(100))+ .expect("ScancodeStream::new should only be called once");+ ScancodeStream { _private: () }+ }+}++impl Default for ScancodeStream {+ fn default() -> Self {+ Self::new()+ }+}++impl Stream for ScancodeStream {+ type Item = u8;++ fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<u8>> {+ let queue = SCANCODE_QUEUE.try_get().expect("scancode queue not initialized");++ // Fast path: something is already there.+ if let Some(scancode) = queue.pop() {+ return Poll::Ready(Some(scancode));+ }++ // Register our waker *then* check again: a key could arrive between+ // the first pop and the registration, and we'd miss its wake-up.+ WAKER.register(cx.waker());+ match queue.pop() {+ Some(scancode) => {+ WAKER.take();+ Poll::Ready(Some(scancode))+ }+ None => Poll::Pending,+ }+ }+}++/// The keyboard task: decode scancodes and echo characters forever.+pub async fn print_keypresses() {+ let mut scancodes = ScancodeStream::new();+ let mut keyboard = Keyboard::new(ScancodeSet1::new(), layouts::Us104Key, HandleControl::Ignore);++ while let Some(scancode) = scancodes.next().await {+ if let Ok(Some(key_event)) = keyboard.add_byte(scancode)+ && let Some(key) = keyboard.process_keyevent(key_event)+ {+ match key {+ // Backspace: erase the previous character on screen. On+ // serial, the uart_16550 crate already turns a 0x08 byte into+ // "\x08 \x08" (back, overwrite with space, back) for us.+ DecodedKey::Unicode('\u{8}') => {+ vga_buffer::backspace();+ serial_print!("\x08");+ }+ DecodedKey::Unicode(character) => {+ print!("{}", character);+ serial_print!("{}", character);+ }+ // Modifiers only change how other keys decode: print nothing.+ DecodedKey::RawKey(+ KeyCode::LShift | KeyCode::RShift | KeyCode::LControl | KeyCode::RControl+ | KeyCode::LAlt | KeyCode::RAltGr | KeyCode::CapsLock,+ ) => {}+ DecodedKey::RawKey(key) => {+ print!("<{:?}>", key);+ serial_print!("<{:?}>", key);+ }+ }+ }+ }+}
@@ -0,0 +1,71 @@+//! Cooperative multitasking with async/await.+//!+//! An `async fn` compiles to a state machine implementing `Future`. Calling+//! `poll` on it runs it until it either finishes (`Poll::Ready`) or has to+//! wait (`Poll::Pending`) -- e.g. for a key press. Before returning Pending,+//! the future hands the `Waker` from its `Context` to whoever will make+//! progress possible (here: the keyboard interrupt). When that happens,+//! `waker.wake()` tells the executor to poll the task again.+//!+//! "Cooperative": a task only gives up the CPU at an `.await`. A task that+//! loops forever without awaiting blocks every other task. (Compare the+//! preemptive threads in `thread.rs`.)++use alloc::boxed::Box;+use core::{+ future::Future,+ pin::Pin,+ sync::atomic::{AtomicU64, Ordering},+ task::{Context, Poll},+};++pub mod executor;+pub mod keyboard;+pub mod simple_executor;++/// A task = a heap-allocated, pinned future with no output.+/// `Pin` because async state machines may contain pointers into themselves,+/// so they must never move in memory once polled.+pub struct Task {+ id: TaskId,+ future: Pin<Box<dyn Future<Output = ()>>>,+}++impl Task {+ pub fn new(future: impl Future<Output = ()> + 'static) -> Task {+ Task { id: TaskId::new(), future: Box::pin(future) }+ }++ fn poll(&mut self, context: &mut Context) -> Poll<()> {+ self.future.as_mut().poll(context)+ }+}++/// Unique id per task, so wakers can say *which* task to run again.+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]+struct TaskId(u64);++impl TaskId {+ fn new() -> Self {+ static NEXT_ID: AtomicU64 = AtomicU64::new(0);+ TaskId(NEXT_ID.fetch_add(1, Ordering::Relaxed))+ }+}++/// A future that returns Pending once (waking itself straight away), then+/// Ready. `yield_now().await` lets other tasks run -- handy for demos/tests.+pub async fn yield_now() {+ struct YieldNow(bool);+ impl Future for YieldNow {+ type Output = ();+ fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> {+ if self.0 {+ return Poll::Ready(());+ }+ self.0 = true;+ cx.waker().wake_by_ref(); // "poll me again soon"+ Poll::Pending+ }+ }+ YieldNow(false).await+}
@@ -0,0 +1,40 @@+//! The first, naive executor from the chapter: keep a FIFO of tasks and+//! poll them round-robin, over and over, with a waker that does nothing.+//! Works, but busy-polls tasks that can't make progress. Kept for learning+//! (and used in the executor test).++use super::Task;+use alloc::collections::VecDeque;+use core::task::{Context, Poll, Waker};++pub struct SimpleExecutor {+ task_queue: VecDeque<Task>,+}++impl SimpleExecutor {+ pub fn new() -> SimpleExecutor {+ SimpleExecutor { task_queue: VecDeque::new() }+ }++ pub fn spawn(&mut self, task: Task) {+ self.task_queue.push_back(task)+ }++ /// Run until every task has finished.+ pub fn run(&mut self) {+ while let Some(mut task) = self.task_queue.pop_front() {+ let waker = Waker::noop(); // a waker whose wake() does nothing+ let mut context = Context::from_waker(waker);+ match task.poll(&mut context) {+ Poll::Ready(()) => {} // done+ Poll::Pending => self.task_queue.push_back(task), // try again later+ }+ }+ }+}++impl Default for SimpleExecutor {+ fn default() -> Self {+ Self::new()+ }+}
@@ -0,0 +1,201 @@+//! Preemptive kernel threads (beyond the phil-opp series).+//!+//! Each thread has its own stack. The timer interrupt calls `schedule()`,+//! which picks the next thread round-robin and calls `switch_context` to swap+//! stacks. Threads don't have to cooperate: even `loop {}` gets interrupted.+//!+//! How a switch works (all inside the timer interrupt, interrupts off):+//!+//! thread A running -> timer IRQ -> CPU pushes A's RIP/RSP/RFLAGS (the+//! interrupt frame) on A's stack -> the `x86-interrupt` handler saves the+//! caller-saved registers (rax, rcx, rdx, rsi, rdi, r8-r11) on A's stack ->+//! `schedule()` -> `switch_context(&A.rsp, B.rsp)`:+//! push the callee-saved registers (rbx, rbp, r12-r15) on A's stack+//! save rsp into A.rsp+//! load rsp from B.rsp <- now we're on B's stack+//! pop B's callee-saved registers+//! ret <- returns into B's earlier schedule()+//! ... which returns into B's timer handler, which restores B's caller-saved+//! registers and `iretq`s back to wherever B was interrupted.+//!+//! So a sleeping thread's whole CPU state is simply sitting on its own stack,+//! and all we have to remember per thread is its stack pointer.+//!+//! 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+//! thread's function.++use alloc::{boxed::Box, vec, vec::Vec};+use core::arch::global_asm;+use spin::Mutex;+use x86_64::instructions::interrupts;++/// 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;++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>>,+ finished: bool,+}++struct Scheduler {+ /// Boxed so each Thread has a fixed address even if the Vec grows: we+ /// hand a raw pointer to `rsp` to `switch_context`.+ threads: Vec<Box<Thread>>,+ current: usize,+}++static SCHEDULER: Mutex<Scheduler> = Mutex::new(Scheduler { threads: Vec::new(), current: 0 });++/// Start a new kernel thread running `entry`. Needs the heap.+pub fn spawn(name: &'static str, entry: fn()) {+ let mut stack = vec![0u8; STACK_SIZE];++ // Build the initial stack, top down (stacks grow towards lower addresses):+ // [return address = thread_trampoline]+ // rbp, rbx, r12 (= entry), r13, r14, r15 <- rsp points at r15+ // exactly what `switch_context` pops before its `ret`.+ let top = (stack.as_mut_ptr() as u64 + STACK_SIZE as u64) & !0xf; // 16-byte aligned+ let initial: [u64; 7] = [+ 0, // r15+ 0, // r14+ 0, // r13+ entry as *const () as u64, // r12: picked up by the trampoline+ 0, // rbx+ 0, // rbp+ thread_trampoline as *const () as u64, // return address+ ];+ let rsp = top - 8 * initial.len() as u64;+ unsafe { core::ptr::copy_nonoverlapping(initial.as_ptr(), rsp as *mut u64, initial.len()) };++ let thread = Box::new(Thread { name, rsp, _stack: Some(stack), finished: false });++ // 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,+ finished: false,+ }));+ }+ scheduler.threads.push(thread);+ });+}++/// 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.+pub fn schedule() {+ let (old_rsp, new_rsp) = {+ let mut s = SCHEDULER.lock();+ let n = s.threads.len();+ if n < 2 {+ 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);+ let next = match next {+ Some(next) if next != current => next,+ _ => return, // nobody else to run+ };+ s.current = next;+ let old_rsp: *mut u64 = &mut s.threads[current].rsp;+ (old_rsp, s.threads[next].rsp)+ // The lock is released here, *before* switching: the next thread+ // will never come back to release a lock we still held.+ };+ unsafe { switch_context(old_rsp, new_rsp) };+}++/// Name of the running thread (for printing).+pub fn current_name() -> &'static str {+ interrupts::without_interrupts(|| {+ let s = SCHEDULER.lock();+ s.threads.get(s.current).map_or("main", |t| t.name)+ })+}++/// Sleep for `ticks` timer ticks (~55 ms each), letting other threads run.+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+ }+}++// --- assembly ---------------------------------------------------------------++unsafe extern "C" {+ /// Save callee-saved registers + rsp into `*old_rsp`, load `new_rsp`,+ /// restore that thread's registers and return into it.+ fn switch_context(old_rsp: *mut u64, new_rsp: u64);+ /// First "return address" of every new thread.+ fn thread_trampoline();+}++// System V calling convention: 1st argument in rdi, 2nd in rsi. A normal+// function call already saved the caller-saved registers, so only the+// callee-saved ones (rbx, rbp, r12-r15) need saving here.+global_asm!(+ ".global switch_context",+ "switch_context:",+ " push rbp",+ " push rbx",+ " push r12",+ " push r13",+ " push r14",+ " push r15",+ " mov [rdi], rsp", // old thread's rsp -> *old_rsp+ " mov rsp, rsi", // switch stacks!+ " pop r15",+ " pop r14",+ " pop r13",+ " pop r12",+ " pop rbx",+ " pop rbp",+ " ret", // pops the new thread's return address+ "",+ ".global thread_trampoline",+ "thread_trampoline:",+ " mov rdi, r12", // entry function pointer -> 1st argument+ " and rsp, -16", // ABI: stack 16-byte aligned before `call`+ " call {start}",+ " ud2", // thread_start never returns+ start = sym thread_start,+);++/// Rust side of a new thread's life.+extern "C" fn thread_start(entry: usize) -> ! {+ // We arrived here from inside the timer interrupt, where interrupts are+ // off. Turn them back on, or this thread could never be preempted.+ interrupts::enable();++ let entry: fn() = unsafe { core::mem::transmute(entry) };+ entry();++ // The thread function returned: mark it finished so the scheduler skips+ // it, then idle until we're switched away for good. (Its stack is kept;+ // freeing it while we still run on it would be a bad idea.)+ interrupts::without_interrupts(|| {+ let mut s = SCHEDULER.lock();+ let current = s.current;+ s.threads[current].finished = true;+ });+ crate::hlt_loop();+}
@@ -122,36 +122,55 @@ lazy_static! { }); } -/// Draw " uptime: N s " right-aligned on the top row (row 0), in a different-/// colour. The normal Writer only writes the bottom row and scrolls up, so if-/// text scrolls over this, the next update simply redraws it. Called from the-/// timer interrupt handler (interrupts already off), so taking the lock is safe.-pub fn show_uptime(seconds: u64) {- // Format the number by hand, right to left: no heap, so no `format!`.- let mut digits = [0u8; 20]; // u64 has at most 20 decimal digits- let mut start = digits.len();- let mut n = seconds;- loop {- start -= 1;- digits[start] = b'0' + (n % 10) as u8;- n /= 10;- if n == 0 {- break;+/// Write formatted text at a fixed screen position (row, col) in black on+/// light grey, without moving the normal cursor. Used for status lines.+/// Text that runs past the end of the row is cut off.+pub fn print_at(row: usize, col: usize, args: fmt::Arguments) {+ /// A tiny fmt::Write target that writes cells left to right on one row.+ struct At<'a> {+ writer: &'a mut Writer,+ row: usize,+ col: usize,+ }+ impl fmt::Write for At<'_> {+ fn write_str(&mut self, s: &str) -> fmt::Result {+ let color_code = ColorCode::new(Color::Black, Color::LightGray);+ for byte in s.bytes() {+ if self.col < BUFFER_WIDTH {+ let ascii_character = byte;+ self.writer.buffer.chars[self.row][self.col]+ .write(ScreenChar { ascii_character, color_code });+ self.col += 1;+ }+ }+ Ok(()) } }+ use core::fmt::Write;+ x86_64::instructions::interrupts::without_interrupts(|| {+ let mut writer = WRITER.lock();+ let _ = At { writer: &mut writer, row, col }.write_fmt(args);+ });+}++/// Draw " uptime: N s " right-aligned on the top row. Called from the timer+/// interrupt handler about once a second.+pub fn show_uptime(seconds: u64) {+ print_at(0, BUFFER_WIDTH - 15, format_args!(" uptime: {:>4}s ", seconds));+} - let color = ColorCode::new(Color::Black, Color::LightGray);- let mut writer = WRITER.lock();- // Write the pieces one after another, ending at the right screen edge.- let parts: [&[u8]; 3] = [b" uptime: ", &digits[start..], b"s "];- let len: usize = parts.iter().map(|p| p.len()).sum();- let mut col = BUFFER_WIDTH - len;- for part in parts {- for &byte in part {- writer.buffer.chars[0][col].write(ScreenChar { ascii_character: byte, color_code: color });- col += 1;+/// Erase the character before the cursor on the bottom row (Backspace).+/// Stops at the start of the line (no going back to the previous row).+pub fn backspace() {+ x86_64::instructions::interrupts::without_interrupts(|| {+ let mut writer = WRITER.lock();+ if writer.column_position > 0 {+ writer.column_position -= 1;+ let col = writer.column_position;+ let blank = ScreenChar { ascii_character: b' ', color_code: writer.color_code };+ writer.buffer.chars[BUFFER_HEIGHT - 1][col].write(blank); }- }+ }); } #[doc(hidden)]
@@ -0,0 +1,73 @@+//! Async/await: both executors run tasks to completion, and `yield_now`+//! makes tasks take turns.+#![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::{rc::Rc, vec, vec::Vec};+use bootloader::{BootInfo, entry_point};+use core::cell::RefCell;+use core::panic::PanicInfo;+use rust_kernel::allocator;+use rust_kernel::memory::{self, BootInfoFrameAllocator};+use rust_kernel::task::{Task, executor::Executor, simple_executor::SimpleExecutor, yield_now};+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");++ test_main();+ rust_kernel::hlt_loop();+}++async fn add_one(x: u32) -> u32 {+ x + 1+}++#[test_case]+fn simple_executor_runs_tasks() {+ let result = Rc::new(RefCell::new(0));+ let r = result.clone();+ let mut executor = SimpleExecutor::new();+ executor.spawn(Task::new(async move {+ *r.borrow_mut() = add_one(41).await;+ }));+ executor.run();+ assert_eq!(*result.borrow(), 42);+}++/// Each task logs a step, then yields. With a FIFO task queue they alternate.+async fn logger(name: char, log: Rc<RefCell<Vec<(char, u32)>>>) {+ for step in 1..=3 {+ log.borrow_mut().push((name, step));+ yield_now().await;+ }+}++#[test_case]+fn executor_interleaves_yielding_tasks() {+ let log = Rc::new(RefCell::new(Vec::new()));+ let mut executor = Executor::new();+ executor.spawn(Task::new(logger('a', log.clone())));+ executor.spawn(Task::new(logger('b', log.clone())));+ executor.run_until_done();+ assert_eq!(+ *log.borrow(),+ vec![('a', 1), ('b', 1), ('a', 2), ('b', 2), ('a', 3), ('b', 3)]+ );+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+ rust_kernel::test_panic_handler(info)+}
@@ -0,0 +1,76 @@+//! Preemptive threads: busy-looping threads that never yield still all make+//! progress, because the timer interrupt switches between them.+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rust_kernel::test_runner)]+#![reexport_test_harness_main = "test_main"]++use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};+use rust_kernel::allocator;+use rust_kernel::interrupts::TICKS;+use rust_kernel::memory::{self, BootInfoFrameAllocator};+use rust_kernel::thread;+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");++ test_main();+ rust_kernel::hlt_loop();+}++static COUNT_1: AtomicU64 = AtomicU64::new(0);+static COUNT_2: AtomicU64 = AtomicU64::new(0);+static DONE_2: AtomicBool = AtomicBool::new(false);++/// Never yields, never returns.+fn spinner_1() {+ loop {+ COUNT_1.fetch_add(1, Ordering::Relaxed);+ }+}++/// Never yields, but returns after a while (tests thread exit).+fn spinner_2() {+ for _ in 0..1_000_000 {+ COUNT_2.fetch_add(1, Ordering::Relaxed);+ }+ DONE_2.store(true, Ordering::Relaxed);+}++#[test_case]+fn busy_threads_are_preempted() {+ thread::spawn("spin1", spinner_1);+ thread::spawn("spin2", spinner_2);++ // The main thread busy-waits too (no hlt): if the timer didn't preempt+ // us, the other threads would never run and we'd hit the timeout.+ let deadline = TICKS.load(Ordering::Relaxed) + 18 * 20; // ~20 s+ while !(DONE_2.load(Ordering::Relaxed) && COUNT_1.load(Ordering::Relaxed) > 1000) {+ assert!(TICKS.load(Ordering::Relaxed) < deadline, "threads made no progress");+ core::hint::spin_loop();+ }+ assert_eq!(COUNT_2.load(Ordering::Relaxed), 1_000_000);++ // spinner_2 has exited; spinner_1 must keep running alongside us.+ let before = COUNT_1.load(Ordering::Relaxed);+ let wait_until = TICKS.load(Ordering::Relaxed) + 5;+ while TICKS.load(Ordering::Relaxed) < wait_until {+ core::hint::spin_loop();+ }+ assert!(COUNT_1.load(Ordering::Relaxed) > before, "spinner_1 stopped running");+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+ rust_kernel::test_panic_handler(info)+}