diff --git a/docs/README.md b/docs/README.md index f6b40a1..e7357ec 100644 --- a/docs/README.md +++ b/docs/README.md @@ -32,9 +32,12 @@ rather than restate it. Roughly in the order things happen at runtime: the VMM, exposed as a `std.mem.Allocator` so std containers work — dynamic allocation for the kernel. 10. **[scheduling.md](scheduling.md) — the scheduler.** Fixed-priority preemptive - multitasking: kernel threads, the context switch, and O(1) priority selection — - the leap to a running system. -11. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and + multitasking: kernel threads, the context switch, O(1) priority selection, and + blocking (sleep, wait queues) — the leap to a running system. +11. **[ipc.md](ipc.md) — inter-process communication.** Bounded blocking + message-passing channels — the backbone the microkernel's isolated servers will + talk over. +12. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and how `while (true) hlt` parks the CPU safely once there's nothing left to do. Start with the north star: @@ -65,8 +68,9 @@ its **descriptor tables** so CPU faults are caught ([interrupts.md](interrupts.m builds its own **page tables** and switches onto them ([paging.md](paging.md)), brings up the **heap** for dynamic allocation ([heap.md](heap.md)), starts the **scheduler** ([scheduling.md](scheduling.md)) and the **timer** that preempts it -([device-interrupts.md](device-interrupts.md)), runs — its CPU-specific bits behind -the [arch](arch.md) boundary — and when idle, or on a panic, it **halts** +([device-interrupts.md](device-interrupts.md)) — with tasks blocking, sleeping and +passing messages over **[IPC](ipc.md)** channels — runs, its CPU-specific bits +behind the [arch](arch.md) boundary, and when idle, or on a panic, it **halts** ([halting.md](halting.md)). ## Source map @@ -78,7 +82,8 @@ the [arch](arch.md) boundary — and when idle, or on a panic, it **halts** | Shared loader↔kernel contract (`BootInfo`, `Framebuffer`, `MemoryMap`, ABI) | `src/root.zig` | | Physical frame allocator | `src/pmm.zig` | | Kernel heap (`std.mem.Allocator`) | `src/heap.zig` | -| Scheduler (fixed-priority preemptive) | `src/sched.zig` | +| Scheduler (fixed-priority preemptive; blocking, wait queues) | `src/sched.zig` | +| IPC channels (message passing) | `src/ipc.zig` | | Framebuffer text console (mirrors to serial) | `src/console.zig` | | In-kernel test cases | `src/tests.zig` | | Arch-specific kernel code (`halt`, GDT/IDT/TSS, exception + interrupt stubs, page tables, APIC/timer, serial, linker script) | `src/arch/x86_64/` | diff --git a/docs/device-interrupts.md b/docs/device-interrupts.md index 59ac8d3..982bf4d 100644 --- a/docs/device-interrupts.md +++ b/docs/device-interrupts.md @@ -46,8 +46,25 @@ it against the **PIT** (the legacy 8254, whose 1.193182 MHz is fixed): run the LAPIC timer one-shot from its maximum count while the PIT counts out a known 10 ms (polling channel 2, no interrupt needed), then see how far the LAPIC got. That yields its counts-per-millisecond, from which `initTimer(hz)` computes the reload -count for any target frequency. danos runs it at **1000 Hz** (a 1 ms tick), and the -tick count times the known period gives a monotonic `uptimeMs()`. +count for any target frequency. danos runs it at **1000 Hz** (a 1 ms tick). + +## The high-resolution clock (TSC) + +The timer tick gives *scheduling* — a 1 ms quantum — but 1 ms is coarse for a +real-time system to *measure* with (interrupt latency, jitter, timeouts). So the +same calibration also measures the **TSC** (Time Stamp Counter): a per-core cycle +counter read with `rdtsc` in a couple of cycles, giving roughly **nanosecond** +resolution — a million times finer than the tick. We snapshot the TSC across the +same 10 ms PIT window to get its frequency (measured ~3.6 GHz on the test host). + +The monotonic clock is exposed as one function per resolution — `nanos()`, +`micros()`, `millis()` — each scaling the cycle delta directly at its unit (with a +128-bit intermediate so a long uptime doesn't overflow) rather than chaining +divisions. `millis()` is what the scheduler uses for `sleep` deadlines; `nanos()` +is there for fine measurement. Note the two clocks are distinct: the **tick** drives +preemption and wakeups (1 ms granularity); the **TSC** is the resolution you read +time at. Making `sleep` itself sub-millisecond would take a tickless one-shot +timer — a later step. ## Two kinds of vector, one dispatch diff --git a/docs/ipc.md b/docs/ipc.md new file mode 100644 index 0000000..67710a1 --- /dev/null +++ b/docs/ipc.md @@ -0,0 +1,57 @@ +# IPC: message-passing channels + +Inter-process communication is the **backbone of a microkernel**. Once drivers and +services run isolated in their own address spaces ([vision](vision.md)), they can't +just call each other — a request becomes a **message**. In a microkernel, whatever +was a function call across a monolithic kernel is IPC, so it's a first-class +concern, not an afterthought. + +This first form is a **bounded blocking channel** (`src/ipc.zig`): a fixed-size +ring buffer of messages with a producer/consumer rendezvous, built on the +scheduler's [wait queues](scheduling.md). + +## The channel + +`Channel(T, capacity)` is generic over the message type and buffer size. It holds a +ring buffer, a count, and two wait queues: + +- **`send(msg)`** — if the channel is full, block on the *not-full* queue; otherwise + write the message, bump the count, and wake a waiting receiver. +- **`recv()`** — if the channel is empty, block on the *not-empty* queue; otherwise + take a message, drop the count, and wake a waiting sender. + +Neither side busy-waits: a full channel parks the sender, an empty one parks the +receiver, and each operation wakes the other side when it makes progress possible. + +Two details make it correct: + +- **Recheck in a loop.** A woken task re-tests the condition (`while (full) wait`) + rather than assuming the slot is still available — another waiter may have taken + it first. This is the standard guard against spurious or racing wakeups. +- **One critical section.** `send`/`recv` run under `saveInterrupts` / + `restoreInterrupts` (the composable form, see [scheduling.md](scheduling.md)), so + checking the condition and committing the block/enqueue happen atomically with + respect to the timer preempting mid-operation. `waitLocked` / `wakeLocked` are the + variants that assume the caller already holds that critical section. + +## Verifying it + +The `ipc` test (see [testing.md](testing.md)) runs a producer and a consumer passing +**100 messages through a 4-slot channel**. The small buffer means the channel goes +full and empty over and over, so both the blocking-send and blocking-recv paths are +exercised heavily. The messages arrive intact and in order (their sum is the +expected `5050`), and neither task busy-waits — they block and wake each other. + +## What's next (not done here) + +- **Across address spaces.** Today both endpoints are kernel threads sharing the + kernel's memory, so the message is copied within one address space. When user + mode arrives, the same channel carries messages between *isolated* processes, + copying the payload across the boundary — which is where IPC earns its place as + the microkernel's backbone. +- **Synchronous call/reply.** A request/response pattern (send-and-wait-for-reply) + on top of channels, the shape most driver/service calls take. +- **Interrupts as messages.** A hardware interrupt delivered to the driver task + that owns the device, as an IPC message. +- **Priority inheritance** through IPC, so a high-priority client blocked on a + low-priority server doesn't suffer unbounded priority inversion. diff --git a/docs/scheduling.md b/docs/scheduling.md index a6a8b05..a2459fd 100644 --- a/docs/scheduling.md +++ b/docs/scheduling.md @@ -67,21 +67,56 @@ exist, which is what a real-time scheduler needs. - **Round-robin within a level.** When a task is descheduled it goes to the *back* of its level's queue, so equal-priority tasks share the CPU fairly. +## Sleeping and the idle task + +A task can **block** — give up the CPU until an event, rather than busy-wait +(busy-waiting is the enemy of a real-time system: it wastes cycles a +higher-priority task should get). The first form is time-based: **`sleep(ms)`** +marks the task blocked with a wake deadline and switches away. On every tick the +timer wakes any task whose deadline has passed (a bounded scan, so it stays +deterministic), which makes it ready again; the scheduler then runs it when its +priority comes up. `sleep` measures its deadline on the [calibrated +clock](device-interrupts.md), so it's real time. + +When *every* task is blocked, something still has to run — so there's an **idle +task** at the lowest priority that just `hlt`s until the next interrupt (see +[halting.md](halting.md)). Because it's always runnable, the scheduler always has a +task to pick, and the "nothing to run" case never arises. + +## Event-based blocking + +The other form of blocking is waiting for an **event** rather than a duration. A +**wait queue** is a set of tasks parked until something happens: `wait(wq)` blocks +the caller on it, `wake(wq)` moves the highest-priority waiter back to ready +(preempting if it now outranks the running task). A task links into a wait queue +through the same field the ready queues use — it's in exactly one queue at a time. +These are the primitives locks, semaphores and [IPC](ipc.md) are built on. + +Blocking safely needs **composable critical sections**. A blanket `cli`/`sti` pair +doesn't nest: an IPC channel that `cli`s and then calls `wait` would have `wait`'s +`sti` re-enable interrupts too early, mid-operation. So the blocking primitives use +`saveInterrupts` / `restoreInterrupts` — capture the interrupt flag, disable, and +later restore *only if it was set* — which nests correctly. The invariant that +makes it all work: `schedule()` is always entered with interrupts disabled, so a +task always resumes from a switch with interrupts disabled and can restore its +caller's state. + ## Verifying it -Two tests (see [testing.md](testing.md)) prove the two guarantees: +Three tests (see [testing.md](testing.md)) prove the guarantees: - **`sched`** spawns three tasks that busy-loop *without ever yielding*. They all make progress — which can only happen if the timer is **preempting** between them and the context switch is correct (nothing yields voluntarily). - **`priority`** (with preemption off, for determinism) spawns tasks at three priorities; they run and exit **highest-priority first** — `[6, 4, 2]`. +- **`sleep`** blocks a task for 50 ms and checks the elapsed time on the clock — a + real block (the idle task runs meanwhile), not a busy-wait. +- **`event`** blocks a task on a wait queue; waking it (from another task) resumes + it, and since it's higher priority it preempts immediately. ## What's next (not done here) -- **Blocking and sleep.** Right now a task can only yield or exit; it can't wait for - a condition or a duration. `sleep(ms)` (on the calibrated clock) and blocking - come next, and are what a real-time task really needs. - **Priority inheritance.** Once tasks block on shared resources (locks, IPC), danos will need it to bound priority inversion — a [real-time](vision.md) requirement. diff --git a/docs/testing.md b/docs/testing.md index 4ec28ba..b80cfa9 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -47,11 +47,14 @@ Current cases: |------|----------------|-----------------------------| | `smoke` | memory map has usable RAM; frame alloc/free; paging active | `DANOS-TEST-RESULT: PASS` | | `timer` | device interrupts fire and return (tick count advances) | `DANOS-TEST-RESULT: PASS` | -| `clock` | calibrated LAPIC frequency is sane; monotonic uptime advances | `DANOS-TEST-RESULT: PASS` | +| `clock` | LAPIC + TSC calibrated; monotonic uptime advances; `nanos()` has sub-ms resolution | `DANOS-TEST-RESULT: PASS` | | `vmm` | on-demand `map` works: a mapped page is writable and reads back | `DANOS-TEST-RESULT: PASS` | | `heap` | kernel heap: alloc/free, block reuse, growth, and a std container on it | `DANOS-TEST-RESULT: PASS` | | `sched` | preemption: three non-yielding tasks all make progress | `DANOS-TEST-RESULT: PASS` | | `priority` | fixed-priority tasks run highest-first | `DANOS-TEST-RESULT: PASS` | +| `sleep` | a task blocks for ~50 ms (real block, not a busy-wait) | `DANOS-TEST-RESULT: PASS` | +| `event` | a task blocks on a wait queue and is woken (preempting) | `DANOS-TEST-RESULT: PASS` | +| `ipc` | producer/consumer pass 100 messages through a 4-slot channel intact | `DANOS-TEST-RESULT: PASS` | | `fault-ud` | invalid-opcode exception is caught | serial shows `invalid opcode (vector 6)` | | `fault-pf` | page fault caught with CR2 | `page fault (vector 14)` | | `fault-df` | double fault caught on IST1 (not a triple-fault reset) | `double fault (vector 8)` | diff --git a/src/arch/x86_64/apic.zig b/src/arch/x86_64/apic.zig index 2eca53d..aff0ce6 100644 --- a/src/arch/x86_64/apic.zig +++ b/src/arch/x86_64/apic.zig @@ -44,6 +44,23 @@ var ticks_per_ms: u32 = 0; /// The periodic-interrupt frequency the timer is armed at, once initTimer runs. var timer_hz: u32 = 0; +/// TSC (Time Stamp Counter) calibration: cycles per second, and the count at boot. +/// The TSC is a per-core cycle counter, giving a ~nanosecond high-resolution +/// monotonic clock — far finer than the millisecond timer tick. +var tsc_hz: u64 = 0; +var tsc_base: u64 = 0; + +/// Read the 64-bit Time Stamp Counter. +fn rdtsc() u64 { + var low: u32 = undefined; + var high: u32 = undefined; + asm volatile ("rdtsc" + : [low] "={eax}" (low), + [high] "={edx}" (high), + ); + return (@as(u64, high) << 32) | low; +} + fn read(reg: u32) u32 { return @as(*volatile u32, @ptrFromInt(base + reg)).*; } @@ -78,10 +95,10 @@ pub fn init() void { write(reg_spurious, 0x100 | spurious_vector); // bit 8 = software enable } -/// Measure the LAPIC timer's counting rate against the PIT (channel 2, which can -/// be polled without interrupts). We run the LAPIC timer one-shot from its max -/// count while the PIT counts out a known 10 ms, then see how far the LAPIC got. -/// This gives real time, which the RTOS quanta guarantees depend on. +/// Measure the LAPIC timer's and the TSC's rates against the PIT (channel 2, which +/// can be polled without interrupts). We run the LAPIC timer one-shot from its max +/// count and snapshot the TSC while the PIT counts out a known 10 ms, then see how +/// far each got. This gives real time, which the RTOS timing guarantees depend on. pub fn calibrate() void { const pit_hz = 1_193_182; const calib_ms = 10; @@ -99,13 +116,18 @@ pub fn calibrate() void { io.outb(0x43, 0xB0); // channel 2, lo/hi byte, mode 0 io.outb(0x42, @truncate(pit_count)); io.outb(0x42, @truncate(pit_count >> 8)); - io.outb(0x61, (io.inb(0x61) & 0xFC) | 0x01); // gate high -> start + const tsc_start = rdtsc(); + io.outb(0x61, (io.inb(0x61) & 0xFC) | 0x01); // gate high -> start while (io.inb(0x61) & 0x20 == 0) {} // poll channel-2 output until terminal count + const tsc_end = rdtsc(); const elapsed = 0xFFFFFFFF - read(reg_timer_current); write(reg_timer_initial, 0); // stop the timer + ticks_per_ms = elapsed / calib_ms; + tsc_hz = (tsc_end -% tsc_start) * (1000 / calib_ms); // cycles/10ms -> cycles/s + tsc_base = rdtsc(); // the clock's zero point (boot) } /// Arm the LAPIC timer to fire on `timer_vector` at `hz` (periodic). Requires @@ -128,10 +150,29 @@ pub fn lapicHz() u64 { return @as(u64, ticks_per_ms) * 1000; } -/// Milliseconds since the timer started (monotonic). Ticks accrue at timer_hz. -pub fn uptimeMs() u64 { - if (timer_hz == 0) return 0; - return ticks() * 1000 / timer_hz; +/// Measured TSC frequency (Hz). +pub fn tscHz() u64 { + return tsc_hz; +} + +// Monotonic high-resolution clock, from the TSC. A function per resolution, each +// scaling the cycle delta directly at its unit (the 128-bit intermediate avoids +// overflow across a long uptime). nanos() resolves to a few ns; millis() is what +// the scheduler uses for sleep deadlines. + +pub fn nanos() u64 { + if (tsc_hz == 0) return 0; + return @intCast(@as(u128, rdtsc() -% tsc_base) * 1_000_000_000 / tsc_hz); +} + +pub fn micros() u64 { + if (tsc_hz == 0) return 0; + return @intCast(@as(u128, rdtsc() -% tsc_base) * 1_000_000 / tsc_hz); +} + +pub fn millis() u64 { + if (tsc_hz == 0) return 0; + return @intCast(@as(u128, rdtsc() -% tsc_base) * 1_000 / tsc_hz); } /// Acknowledge the current interrupt so the LAPIC will deliver the next one. diff --git a/src/arch/x86_64/cpu.zig b/src/arch/x86_64/cpu.zig index 5877dab..3fdfef8 100644 --- a/src/arch/x86_64/cpu.zig +++ b/src/arch/x86_64/cpu.zig @@ -78,15 +78,24 @@ pub fn ticks() u64 { return apic.ticks(); } -/// Milliseconds since the timer started (monotonic). -pub fn uptimeMs() u64 { - return apic.uptimeMs(); +// Monotonic high-resolution clock (from the TSC), one function per resolution. +pub fn nanos() u64 { + return apic.nanos(); +} +pub fn micros() u64 { + return apic.micros(); +} +pub fn millis() u64 { + return apic.millis(); } -/// Measured LAPIC timer frequency in Hz (from calibration). +/// Measured LAPIC timer / TSC frequencies in Hz (from calibration). pub fn lapicHz() u64 { return apic.lapicHz(); } +pub fn tscHz() u64 { + return apic.tscHz(); +} /// Unmask maskable interrupts (`sti`) so device interrupts get delivered. pub fn enableInterrupts() void { @@ -98,6 +107,27 @@ pub fn disableInterrupts() void { asm volatile ("cli"); } +/// Disable interrupts and return the previous flags, so a nested critical section +/// can restore the caller's state rather than blindly re-enabling. Pairs with +/// restoreInterrupts. +pub fn saveInterrupts() u64 { + var flags: u64 = undefined; + asm volatile ( + \\pushfq + \\pop %[f] + \\cli + : [f] "=r" (flags), + : + : .{ .memory = true } + ); + return flags; +} + +/// Re-enable interrupts only if they were enabled when `flags` was captured. +pub fn restoreInterrupts(flags: u64) void { + if (flags & 0x200 != 0) asm volatile ("sti" ::: .{ .memory = true }); // bit 9 = IF +} + /// Register a callback the timer interrupt invokes each tick (e.g. the scheduler). pub fn setTickHook(hook: *const fn () void) void { apic.setTickHook(hook); diff --git a/src/ipc.zig b/src/ipc.zig new file mode 100644 index 0000000..4d7c565 --- /dev/null +++ b/src/ipc.zig @@ -0,0 +1,54 @@ +//! Inter-process communication: message-passing channels. +//! +//! IPC is the backbone of a microkernel ([vision](../docs/vision.md)): once +//! drivers and services live in separate address spaces, a message is how they +//! talk. This first form is a **bounded blocking channel** — a ring buffer of +//! messages with a producer/consumer rendezvous, built on the scheduler's +//! [wait queues](scheduling.md). `send` blocks when the channel is full, `recv` +//! blocks when it's empty; neither busy-waits. +//! +//! For now both endpoints are kernel threads sharing the kernel address space. +//! When user mode arrives, the same primitive carries messages across the +//! isolation boundary (with the payload copied between address spaces). + +const arch = @import("arch"); +const sched = @import("sched.zig"); + +/// A bounded blocking channel of `capacity` messages of type `T`. +pub fn Channel(comptime T: type, comptime capacity: usize) type { + return struct { + const Self = @This(); + + buffer: [capacity]T = undefined, + head: usize = 0, // next slot to read + tail: usize = 0, // next slot to write + count: usize = 0, + not_full: sched.WaitQueue = .{}, // senders wait here + not_empty: sched.WaitQueue = .{}, // receivers wait here + + /// Send a message, blocking while the channel is full. + pub fn send(self: *Self, msg: T) void { + const flags = arch.saveInterrupts(); + // Recheck the condition in a loop: a wakeup only means "try again" + // (another waiter may have taken the slot first). + while (self.count == capacity) sched.waitLocked(&self.not_full); + self.buffer[self.tail] = msg; + self.tail = (self.tail + 1) % capacity; + self.count += 1; + sched.wakeLocked(&self.not_empty); // a receiver can now proceed + arch.restoreInterrupts(flags); + } + + /// Receive a message, blocking while the channel is empty. + pub fn recv(self: *Self) T { + const flags = arch.saveInterrupts(); + while (self.count == 0) sched.waitLocked(&self.not_empty); + const msg = self.buffer[self.head]; + self.head = (self.head + 1) % capacity; + self.count -= 1; + sched.wakeLocked(&self.not_full); // a sender can now proceed + arch.restoreInterrupts(flags); + return msg; + } + }; +} diff --git a/src/main.zig b/src/main.zig index f4d9103..ac8fd50 100644 --- a/src/main.zig +++ b/src/main.zig @@ -106,7 +106,7 @@ fn kmain(boot_info: *const BootInfo) noreturn { // the timer preempts among tasks. arch.startTimer(); arch.enableInterrupts(); - con.print("danos: timer online ({d} Hz tick, LAPIC {d} MHz measured)\n", .{ arch.timer_hz, arch.lapicHz() / 1_000_000 }); + con.print("danos: timer online ({d} Hz tick; LAPIC {d} MHz, TSC {d} MHz measured)\n", .{ arch.timer_hz, arch.lapicHz() / 1_000_000, arch.tscHz() / 1_000_000 }); // In a test build (`zig build -Dtest-case=`), run that case and stop. // Normal builds fall through to the idle halt. diff --git a/src/sched.zig b/src/sched.zig index 2722a90..da48944 100644 --- a/src/sched.zig +++ b/src/sched.zig @@ -21,7 +21,7 @@ const num_priorities = 8; const stack_size = 16 * 1024; // per-task kernel stack const max_tasks = 16; -const State = enum { free, ready, running }; +const State = enum { free, ready, running, blocked }; const Task = struct { id: u32 = 0, @@ -29,6 +29,7 @@ const Task = struct { priority: Priority = 0, rsp: usize = 0, // saved stack pointer, valid while not running stack: []u8 = &.{}, + wake_at: u64 = 0, // uptime (ms) to wake a sleeping task; 0 = not sleeping next: ?*Task = null, // ready-queue link }; @@ -43,14 +44,21 @@ var ready_bitmap: u8 = 0; var preemption_enabled = true; -/// Register the currently-running kernel context as the first task, and hook the -/// timer for preemption. +/// Register the currently-running kernel context as the first task, spawn the +/// idle task, and hook the timer for preemption. pub fn init(boot_priority: Priority) void { tasks[0] = .{ .id = 0, .state = .running, .priority = boot_priority }; current = &tasks[0]; + spawn(idle, 0); // lowest priority, always runnable — runs when nothing else is arch.setTickHook(tick); } +/// The idle task: run when every other task is blocked or sleeping. `hlt` waits +/// for the next interrupt at near-zero power (see docs/halting.md). +fn idle() void { + while (true) asm volatile ("hlt"); +} + fn enqueue(t: *Task) void { t.next = null; const p: usize = t.priority; @@ -113,13 +121,105 @@ fn schedule() void { /// Voluntarily give up the CPU to the next ready task. pub fn yield() void { - arch.disableInterrupts(); + const flags = arch.saveInterrupts(); schedule(); - arch.enableInterrupts(); + arch.restoreInterrupts(flags); } -/// Called from the timer interrupt (interrupts already disabled) to preempt. +/// Block the current task for `ms` milliseconds, then let it become runnable +/// again. The idle task (or other work) runs in the meantime. +pub fn sleep(ms: u64) void { + const flags = arch.saveInterrupts(); + current.wake_at = arch.millis() + ms; + current.state = .blocked; + schedule(); // current is blocked, so schedule() won't re-enqueue it + arch.restoreInterrupts(flags); +} + +// --- event-based blocking ------------------------------------------------- +// +// A WaitQueue is a set of tasks blocked waiting for something (a resource, a +// message). Tasks link into it through the same `next` field the ready queues +// use — a task is in exactly one queue at a time. These are the primitive locks, +// semaphores and IPC channels are built on. + +pub const WaitQueue = struct { + head: ?*Task = null, +}; + +/// Block the current task on `wq` and switch away. Precondition: interrupts are +/// disabled (the caller holds them, so a condition can be checked and the block +/// committed atomically). On return — when woken — interrupts are still disabled. +pub fn waitLocked(wq: *WaitQueue) void { + current.state = .blocked; + current.next = wq.head; + wq.head = current; + schedule(); +} + +/// Move the highest-priority waiter on `wq` (if any) to the ready queue. +/// Precondition: interrupts disabled. Does not preempt — the caller decides. +pub fn wakeLocked(wq: *WaitQueue) void { + // Find the highest-priority waiter (bounded scan) and unlink it. + var best_prev: ?*Task = null; + var best: ?*Task = null; + var prev: ?*Task = null; + var cur = wq.head; + while (cur) |t| : ({ + prev = t; + cur = t.next; + }) { + if (best == null or t.priority > best.?.priority) { + best = t; + best_prev = prev; + } + } + const t = best orelse return; + if (best_prev) |p| p.next = t.next else wq.head = t.next; + t.state = .ready; + enqueue(t); +} + +/// Block on `wq` (a self-contained critical section). +pub fn wait(wq: *WaitQueue) void { + const flags = arch.saveInterrupts(); + waitLocked(wq); + arch.restoreInterrupts(flags); +} + +/// Wake the highest-priority waiter on `wq`, preempting if it outranks us. +pub fn wake(wq: *WaitQueue) void { + const flags = arch.saveInterrupts(); + wakeLocked(wq); + // If a higher-priority task is now ready, run it immediately. + if (highestReadyPriority()) |p| { + if (p > current.priority) schedule(); + } + arch.restoreInterrupts(flags); +} + +fn highestReadyPriority() ?Priority { + if (ready_bitmap == 0) return null; + return @intCast(num_priorities - 1 - @clz(ready_bitmap)); +} + +/// Wake any sleeping task whose deadline has passed. Bounded by the task count, +/// so it stays deterministic. Called from the timer tick (interrupts disabled). +fn wakeExpired() void { + const now = arch.millis(); + for (&tasks) |*t| { + if (t.state == .blocked and t.wake_at != 0 and now >= t.wake_at) { + t.wake_at = 0; + t.state = .ready; + enqueue(t); + } + } +} + +/// Called from the timer interrupt (interrupts already disabled): wake due +/// sleepers, then preempt. pub fn tick() void { + wakeExpired(); if (preemption_enabled) schedule(); } diff --git a/src/tests.zig b/src/tests.zig index 3932f1a..a5ec9f5 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -15,6 +15,7 @@ const arch = @import("arch"); const pmm = @import("pmm.zig"); const heap = @import("heap.zig"); const sched = @import("sched.zig"); +const ipc = @import("ipc.zig"); /// Formatted write straight to serial, independent of the framebuffer console. fn log(comptime fmt: []const u8, args: anytype) void { @@ -60,6 +61,12 @@ pub fn run(case: []const u8, boot_info: *const BootInfo) void { schedTest(); } else if (eql(case, "priority")) { priorityTest(); + } else if (eql(case, "sleep")) { + sleepTest(); + } else if (eql(case, "event")) { + eventTest(); + } else if (eql(case, "ipc")) { + ipcTest(); } else if (eql(case, "fault-ud")) { faultInvalidOpcode(); } else if (eql(case, "fault-pf")) { @@ -211,27 +218,46 @@ fn heapTest() void { result(); } -/// Verify the calibrated clock: a plausible measured LAPIC frequency, the -/// configured tick rate, and monotonic uptime that advances with real ticks. +/// Verify the calibrated clocks: sane measured frequencies, monotonic uptime that +/// advances with real ticks, and — the point of the TSC clock — nanosecond +/// resolution far finer than the 1 ms tick, with the unit functions consistent. fn clock() void { log("DANOS-TEST-BEGIN: clock\n", .{}); - // Calibration produced a sane LAPIC frequency (roughly 1 MHz .. 100 GHz). const lapic = arch.lapicHz(); check("LAPIC frequency measured", lapic > 1_000_000 and lapic < 100_000_000_000); + const tsc = arch.tscHz(); + check("TSC frequency measured", tsc > 100_000_000 and tsc < 100_000_000_000); - // Wait for ~5 real ticks and confirm uptime advanced by about that many ms - // (tick rate is 1000 Hz, so 1 tick == 1 ms). + // Uptime advances over ~5 real ticks (1000 Hz => 1 tick == 1 ms). const start_ticks = arch.ticks(); - const start_ms = arch.uptimeMs(); + const start_ms = arch.millis(); var spins: u64 = 0; while (arch.ticks() < start_ticks + 5 and spins < 5_000_000_000) spins +%= 1; - const elapsed_ms = arch.uptimeMs() - start_ms; + const elapsed_ms = arch.millis() - start_ms; check("uptime advances with ticks", elapsed_ms >= 5 and elapsed_ms < 100); + // Sub-millisecond resolution: spin until nanos() first advances, then confirm + // that first step happened within a millisecond — so nanos() resolves finer + // than the 1 ms tick (a tick clock's smallest step *is* 1 ms). Spinning to the + // first change is robust to QEMU's coarse TSC update granularity. + const n1 = arch.nanos(); + var s2: u64 = 0; + while (arch.nanos() == n1 and s2 < 10_000_000) s2 +%= 1; + const n2 = arch.nanos(); + check("nanos() has sub-millisecond resolution", n2 > n1 and (n2 - n1) < 1_000_000); + + // The unit functions agree (within rounding). + const ns = arch.nanos(); + check("nanos/micros/millis are consistent", diffWithin(arch.micros(), ns / 1000, 1000) and diffWithin(arch.millis(), ns / 1_000_000, 2)); + result(); } +fn diffWithin(a: u64, b: u64, tol: u64) bool { + return if (a > b) a - b <= tol else b - a <= tol; +} + // --- scheduler tests ------------------------------------------------------ var counters = [_]u64{0} ** 3; @@ -292,7 +318,7 @@ fn taskLow() void { fn priorityTest() void { log("DANOS-TEST-BEGIN: priority\n", .{}); sched.setPreemption(false); - sched.setPriority(0); // run this observer task last, after all workers + sched.setPriority(1); // above the idle task (0), below the workers — runs last run_n = 0; sched.spawn(taskLow, 2); @@ -308,6 +334,80 @@ fn priorityTest() void { result(); } +var event_wq: sched.WaitQueue = .{}; +var event_stage: u32 = 0; + +fn eventWaiter() void { + event_stage = 1; // reached the wait + sched.wait(&event_wq); // block until woken + event_stage = 3; // woken and resumed + sched.exit(); +} + +/// Event-based blocking: a task blocks on a wait queue and is woken. The waiter is +/// higher priority, so waking it preempts us and it runs to completion at once. +fn eventTest() void { + log("DANOS-TEST-BEGIN: event\n", .{}); + event_stage = 0; + sched.spawn(eventWaiter, 6); // higher priority than this task (4) + + var spins: u64 = 0; + while (event_stage != 1 and spins < 1_000_000_000) : (spins += 1) sched.yield(); + check("waiter reached the wait and blocked", event_stage == 1); + + sched.wake(&event_wq); + check("wake resumed the blocked waiter (preempting)", event_stage == 3); + result(); +} + +var channel: ipc.Channel(u64, 4) = .{}; +var recv_sum: u64 = 0; +var recv_count: u64 = 0; + +fn producer() void { + var i: u64 = 1; + while (i <= 100) : (i += 1) channel.send(i); + sched.exit(); +} +fn consumer() void { + var n: u64 = 0; + while (n < 100) : (n += 1) { + recv_sum += channel.recv(); + recv_count += 1; + } + sched.exit(); +} + +/// IPC: a producer and consumer pass 100 messages through a 4-slot channel. The +/// small buffer forces the channel full and empty repeatedly, exercising both the +/// blocking-send and blocking-recv paths. The messages must arrive intact. +fn ipcTest() void { + log("DANOS-TEST-BEGIN: ipc\n", .{}); + channel = .{}; + recv_sum = 0; + recv_count = 0; + sched.spawn(consumer, 5); // above this task (4) so they run and we observe after + sched.spawn(producer, 5); + + var spins: u64 = 0; + while (recv_count < 100 and spins < 2_000_000_000) : (spins += 1) sched.yield(); + + check("all 100 messages received", recv_count == 100); + check("messages arrived intact (sum 1..100 == 5050)", recv_sum == 5050); + result(); +} + +/// Blocking: sleep(50) should block this task for about 50 ms (measured on the +/// calibrated clock) — not busy-wait — while the idle task runs. +fn sleepTest() void { + log("DANOS-TEST-BEGIN: sleep\n", .{}); + const t0 = arch.millis(); + sched.sleep(50); + const elapsed = arch.millis() - t0; + check("sleep(50) blocked for ~50 ms", elapsed >= 50 and elapsed <= 70); + result(); +} + fn faultInvalidOpcode() void { log("DANOS-TEST-BEGIN: fault-ud\n", .{}); asm volatile ("ud2"); diff --git a/test/qemu_test.py b/test/qemu_test.py index 3fe595f..89152b1 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -81,6 +81,15 @@ CASES = [ {"name": "priority", "expect": r"DANOS-TEST-RESULT: PASS", "fail": r"DANOS-TEST-RESULT: FAIL"}, + {"name": "sleep", + "expect": r"DANOS-TEST-RESULT: PASS", + "fail": r"DANOS-TEST-RESULT: FAIL"}, + {"name": "event", + "expect": r"DANOS-TEST-RESULT: PASS", + "fail": r"DANOS-TEST-RESULT: FAIL"}, + {"name": "ipc", + "expect": r"DANOS-TEST-RESULT: PASS", + "fail": r"DANOS-TEST-RESULT: FAIL"}, {"name": "fault-ud", "expect": r"invalid opcode \(vector 6\)"}, {"name": "fault-pf", "expect": r"page fault \(vector 14\)"}, {"name": "fault-df", "expect": r"double fault \(vector 8\)"},