RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorTyler Fowler <tyler@localhost>2026-10-29 23:57:08 -0700
commita0fc1ab827ad7844fa38a7380fb140a1894ab230
parent866aaa3978d692723cabcfa91d2ada0517c190b8
treebrowse 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
MCargo.lock+69 -0
MCargo.toml+6 -1
MREADME.md+149 -41
Mboot.png+0 -0
Mscripts/screenshot.sh+23 -8
Msrc/allocator.rs+1 -1
Msrc/interrupts.rs+13 -43
Msrc/lib.rs+2 -0
Msrc/main.rs+71 -7
Asrc/task/executor.rs+121 -0
Asrc/task/keyboard.rs+117 -0
Asrc/task/mod.rs+71 -0
Asrc/task/simple_executor.rs+40 -0
Asrc/thread.rs+201 -0
Msrc/vga_buffer.rs+45 -26
Atests/executor.rs+73 -0
Atests/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).  ![boot screenshot](boot.png) @@ -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)+}