IPC
This commit is contained in:
parent
df774691c8
commit
31bf86af56
|
|
@ -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
|
the VMM, exposed as a `std.mem.Allocator` so std containers work — dynamic
|
||||||
allocation for the kernel.
|
allocation for the kernel.
|
||||||
10. **[scheduling.md](scheduling.md) — the scheduler.** Fixed-priority preemptive
|
10. **[scheduling.md](scheduling.md) — the scheduler.** Fixed-priority preemptive
|
||||||
multitasking: kernel threads, the context switch, and O(1) priority selection —
|
multitasking: kernel threads, the context switch, O(1) priority selection, and
|
||||||
the leap to a running system.
|
blocking (sleep, wait queues) — the leap to a running system.
|
||||||
11. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
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.
|
how `while (true) hlt` parks the CPU safely once there's nothing left to do.
|
||||||
|
|
||||||
Start with the north star:
|
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)),
|
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
|
brings up the **heap** for dynamic allocation ([heap.md](heap.md)), starts the
|
||||||
**scheduler** ([scheduling.md](scheduling.md)) and the **timer** that preempts it
|
**scheduler** ([scheduling.md](scheduling.md)) and the **timer** that preempts it
|
||||||
([device-interrupts.md](device-interrupts.md)), runs — its CPU-specific bits behind
|
([device-interrupts.md](device-interrupts.md)) — with tasks blocking, sleeping and
|
||||||
the [arch](arch.md) boundary — and when idle, or on a panic, it **halts**
|
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)).
|
([halting.md](halting.md)).
|
||||||
|
|
||||||
## Source map
|
## 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` |
|
| Shared loader↔kernel contract (`BootInfo`, `Framebuffer`, `MemoryMap`, ABI) | `src/root.zig` |
|
||||||
| Physical frame allocator | `src/pmm.zig` |
|
| Physical frame allocator | `src/pmm.zig` |
|
||||||
| Kernel heap (`std.mem.Allocator`) | `src/heap.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` |
|
| Framebuffer text console (mirrors to serial) | `src/console.zig` |
|
||||||
| In-kernel test cases | `src/tests.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/` |
|
| Arch-specific kernel code (`halt`, GDT/IDT/TSS, exception + interrupt stubs, page tables, APIC/timer, serial, linker script) | `src/arch/x86_64/` |
|
||||||
|
|
|
||||||
|
|
@ -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
|
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
|
(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
|
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
|
count for any target frequency. danos runs it at **1000 Hz** (a 1 ms tick).
|
||||||
tick count times the known period gives a monotonic `uptimeMs()`.
|
|
||||||
|
## 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
|
## Two kinds of vector, one dispatch
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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*
|
- **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.
|
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
|
## 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
|
- **`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
|
make progress — which can only happen if the timer is **preempting** between them
|
||||||
and the context switch is correct (nothing yields voluntarily).
|
and the context switch is correct (nothing yields voluntarily).
|
||||||
- **`priority`** (with preemption off, for determinism) spawns tasks at three
|
- **`priority`** (with preemption off, for determinism) spawns tasks at three
|
||||||
priorities; they run and exit **highest-priority first** — `[6, 4, 2]`.
|
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)
|
## 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),
|
- **Priority inheritance.** Once tasks block on shared resources (locks, IPC),
|
||||||
danos will need it to bound priority inversion — a [real-time](vision.md)
|
danos will need it to bound priority inversion — a [real-time](vision.md)
|
||||||
requirement.
|
requirement.
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,14 @@ Current cases:
|
||||||
|------|----------------|-----------------------------|
|
|------|----------------|-----------------------------|
|
||||||
| `smoke` | memory map has usable RAM; frame alloc/free; paging active | `DANOS-TEST-RESULT: PASS` |
|
| `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` |
|
| `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` |
|
| `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` |
|
| `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` |
|
| `sched` | preemption: three non-yielding tasks all make progress | `DANOS-TEST-RESULT: PASS` |
|
||||||
| `priority` | fixed-priority tasks run highest-first | `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-ud` | invalid-opcode exception is caught | serial shows `invalid opcode (vector 6)` |
|
||||||
| `fault-pf` | page fault caught with CR2 | `page fault (vector 14)` |
|
| `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)` |
|
| `fault-df` | double fault caught on IST1 (not a triple-fault reset) | `double fault (vector 8)` |
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,23 @@ var ticks_per_ms: u32 = 0;
|
||||||
/// The periodic-interrupt frequency the timer is armed at, once initTimer runs.
|
/// The periodic-interrupt frequency the timer is armed at, once initTimer runs.
|
||||||
var timer_hz: u32 = 0;
|
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 {
|
fn read(reg: u32) u32 {
|
||||||
return @as(*volatile u32, @ptrFromInt(base + reg)).*;
|
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
|
write(reg_spurious, 0x100 | spurious_vector); // bit 8 = software enable
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Measure the LAPIC timer's counting rate against the PIT (channel 2, which can
|
/// Measure the LAPIC timer's and the TSC's rates against the PIT (channel 2, which
|
||||||
/// be polled without interrupts). We run the LAPIC timer one-shot from its max
|
/// 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.
|
/// count and snapshot the TSC while the PIT counts out a known 10 ms, then see how
|
||||||
/// This gives real time, which the RTOS quanta guarantees depend on.
|
/// far each got. This gives real time, which the RTOS timing guarantees depend on.
|
||||||
pub fn calibrate() void {
|
pub fn calibrate() void {
|
||||||
const pit_hz = 1_193_182;
|
const pit_hz = 1_193_182;
|
||||||
const calib_ms = 10;
|
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(0x43, 0xB0); // channel 2, lo/hi byte, mode 0
|
||||||
io.outb(0x42, @truncate(pit_count));
|
io.outb(0x42, @truncate(pit_count));
|
||||||
io.outb(0x42, @truncate(pit_count >> 8));
|
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
|
while (io.inb(0x61) & 0x20 == 0) {} // poll channel-2 output until terminal count
|
||||||
|
const tsc_end = rdtsc();
|
||||||
|
|
||||||
const elapsed = 0xFFFFFFFF - read(reg_timer_current);
|
const elapsed = 0xFFFFFFFF - read(reg_timer_current);
|
||||||
write(reg_timer_initial, 0); // stop the timer
|
write(reg_timer_initial, 0); // stop the timer
|
||||||
|
|
||||||
ticks_per_ms = elapsed / calib_ms;
|
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
|
/// 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;
|
return @as(u64, ticks_per_ms) * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Milliseconds since the timer started (monotonic). Ticks accrue at timer_hz.
|
/// Measured TSC frequency (Hz).
|
||||||
pub fn uptimeMs() u64 {
|
pub fn tscHz() u64 {
|
||||||
if (timer_hz == 0) return 0;
|
return tsc_hz;
|
||||||
return ticks() * 1000 / timer_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.
|
/// Acknowledge the current interrupt so the LAPIC will deliver the next one.
|
||||||
|
|
|
||||||
|
|
@ -78,15 +78,24 @@ pub fn ticks() u64 {
|
||||||
return apic.ticks();
|
return apic.ticks();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Milliseconds since the timer started (monotonic).
|
// Monotonic high-resolution clock (from the TSC), one function per resolution.
|
||||||
pub fn uptimeMs() u64 {
|
pub fn nanos() u64 {
|
||||||
return apic.uptimeMs();
|
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 {
|
pub fn lapicHz() u64 {
|
||||||
return apic.lapicHz();
|
return apic.lapicHz();
|
||||||
}
|
}
|
||||||
|
pub fn tscHz() u64 {
|
||||||
|
return apic.tscHz();
|
||||||
|
}
|
||||||
|
|
||||||
/// Unmask maskable interrupts (`sti`) so device interrupts get delivered.
|
/// Unmask maskable interrupts (`sti`) so device interrupts get delivered.
|
||||||
pub fn enableInterrupts() void {
|
pub fn enableInterrupts() void {
|
||||||
|
|
@ -98,6 +107,27 @@ pub fn disableInterrupts() void {
|
||||||
asm volatile ("cli");
|
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).
|
/// Register a callback the timer interrupt invokes each tick (e.g. the scheduler).
|
||||||
pub fn setTickHook(hook: *const fn () void) void {
|
pub fn setTickHook(hook: *const fn () void) void {
|
||||||
apic.setTickHook(hook);
|
apic.setTickHook(hook);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -106,7 +106,7 @@ fn kmain(boot_info: *const BootInfo) noreturn {
|
||||||
// the timer preempts among tasks.
|
// the timer preempts among tasks.
|
||||||
arch.startTimer();
|
arch.startTimer();
|
||||||
arch.enableInterrupts();
|
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=<name>`), run that case and stop.
|
// In a test build (`zig build -Dtest-case=<name>`), run that case and stop.
|
||||||
// Normal builds fall through to the idle halt.
|
// Normal builds fall through to the idle halt.
|
||||||
|
|
|
||||||
112
src/sched.zig
112
src/sched.zig
|
|
@ -21,7 +21,7 @@ const num_priorities = 8;
|
||||||
const stack_size = 16 * 1024; // per-task kernel stack
|
const stack_size = 16 * 1024; // per-task kernel stack
|
||||||
const max_tasks = 16;
|
const max_tasks = 16;
|
||||||
|
|
||||||
const State = enum { free, ready, running };
|
const State = enum { free, ready, running, blocked };
|
||||||
|
|
||||||
const Task = struct {
|
const Task = struct {
|
||||||
id: u32 = 0,
|
id: u32 = 0,
|
||||||
|
|
@ -29,6 +29,7 @@ const Task = struct {
|
||||||
priority: Priority = 0,
|
priority: Priority = 0,
|
||||||
rsp: usize = 0, // saved stack pointer, valid while not running
|
rsp: usize = 0, // saved stack pointer, valid while not running
|
||||||
stack: []u8 = &.{},
|
stack: []u8 = &.{},
|
||||||
|
wake_at: u64 = 0, // uptime (ms) to wake a sleeping task; 0 = not sleeping
|
||||||
next: ?*Task = null, // ready-queue link
|
next: ?*Task = null, // ready-queue link
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -43,14 +44,21 @@ var ready_bitmap: u8 = 0;
|
||||||
|
|
||||||
var preemption_enabled = true;
|
var preemption_enabled = true;
|
||||||
|
|
||||||
/// Register the currently-running kernel context as the first task, and hook the
|
/// Register the currently-running kernel context as the first task, spawn the
|
||||||
/// timer for preemption.
|
/// idle task, and hook the timer for preemption.
|
||||||
pub fn init(boot_priority: Priority) void {
|
pub fn init(boot_priority: Priority) void {
|
||||||
tasks[0] = .{ .id = 0, .state = .running, .priority = boot_priority };
|
tasks[0] = .{ .id = 0, .state = .running, .priority = boot_priority };
|
||||||
current = &tasks[0];
|
current = &tasks[0];
|
||||||
|
spawn(idle, 0); // lowest priority, always runnable — runs when nothing else is
|
||||||
arch.setTickHook(tick);
|
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 {
|
fn enqueue(t: *Task) void {
|
||||||
t.next = null;
|
t.next = null;
|
||||||
const p: usize = t.priority;
|
const p: usize = t.priority;
|
||||||
|
|
@ -113,13 +121,105 @@ fn schedule() void {
|
||||||
|
|
||||||
/// Voluntarily give up the CPU to the next ready task.
|
/// Voluntarily give up the CPU to the next ready task.
|
||||||
pub fn yield() void {
|
pub fn yield() void {
|
||||||
arch.disableInterrupts();
|
const flags = arch.saveInterrupts();
|
||||||
schedule();
|
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 {
|
pub fn tick() void {
|
||||||
|
wakeExpired();
|
||||||
if (preemption_enabled) schedule();
|
if (preemption_enabled) schedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
116
src/tests.zig
116
src/tests.zig
|
|
@ -15,6 +15,7 @@ const arch = @import("arch");
|
||||||
const pmm = @import("pmm.zig");
|
const pmm = @import("pmm.zig");
|
||||||
const heap = @import("heap.zig");
|
const heap = @import("heap.zig");
|
||||||
const sched = @import("sched.zig");
|
const sched = @import("sched.zig");
|
||||||
|
const ipc = @import("ipc.zig");
|
||||||
|
|
||||||
/// Formatted write straight to serial, independent of the framebuffer console.
|
/// Formatted write straight to serial, independent of the framebuffer console.
|
||||||
fn log(comptime fmt: []const u8, args: anytype) void {
|
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();
|
schedTest();
|
||||||
} else if (eql(case, "priority")) {
|
} else if (eql(case, "priority")) {
|
||||||
priorityTest();
|
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")) {
|
} else if (eql(case, "fault-ud")) {
|
||||||
faultInvalidOpcode();
|
faultInvalidOpcode();
|
||||||
} else if (eql(case, "fault-pf")) {
|
} else if (eql(case, "fault-pf")) {
|
||||||
|
|
@ -211,27 +218,46 @@ fn heapTest() void {
|
||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify the calibrated clock: a plausible measured LAPIC frequency, the
|
/// Verify the calibrated clocks: sane measured frequencies, monotonic uptime that
|
||||||
/// configured tick rate, and monotonic uptime that advances with real ticks.
|
/// 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 {
|
fn clock() void {
|
||||||
log("DANOS-TEST-BEGIN: clock\n", .{});
|
log("DANOS-TEST-BEGIN: clock\n", .{});
|
||||||
|
|
||||||
// Calibration produced a sane LAPIC frequency (roughly 1 MHz .. 100 GHz).
|
|
||||||
const lapic = arch.lapicHz();
|
const lapic = arch.lapicHz();
|
||||||
check("LAPIC frequency measured", lapic > 1_000_000 and lapic < 100_000_000_000);
|
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
|
// Uptime advances over ~5 real ticks (1000 Hz => 1 tick == 1 ms).
|
||||||
// (tick rate is 1000 Hz, so 1 tick == 1 ms).
|
|
||||||
const start_ticks = arch.ticks();
|
const start_ticks = arch.ticks();
|
||||||
const start_ms = arch.uptimeMs();
|
const start_ms = arch.millis();
|
||||||
var spins: u64 = 0;
|
var spins: u64 = 0;
|
||||||
while (arch.ticks() < start_ticks + 5 and spins < 5_000_000_000) spins +%= 1;
|
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);
|
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();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn diffWithin(a: u64, b: u64, tol: u64) bool {
|
||||||
|
return if (a > b) a - b <= tol else b - a <= tol;
|
||||||
|
}
|
||||||
|
|
||||||
// --- scheduler tests ------------------------------------------------------
|
// --- scheduler tests ------------------------------------------------------
|
||||||
|
|
||||||
var counters = [_]u64{0} ** 3;
|
var counters = [_]u64{0} ** 3;
|
||||||
|
|
@ -292,7 +318,7 @@ fn taskLow() void {
|
||||||
fn priorityTest() void {
|
fn priorityTest() void {
|
||||||
log("DANOS-TEST-BEGIN: priority\n", .{});
|
log("DANOS-TEST-BEGIN: priority\n", .{});
|
||||||
sched.setPreemption(false);
|
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;
|
run_n = 0;
|
||||||
|
|
||||||
sched.spawn(taskLow, 2);
|
sched.spawn(taskLow, 2);
|
||||||
|
|
@ -308,6 +334,80 @@ fn priorityTest() void {
|
||||||
result();
|
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 {
|
fn faultInvalidOpcode() void {
|
||||||
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
|
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
|
||||||
asm volatile ("ud2");
|
asm volatile ("ud2");
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,15 @@ CASES = [
|
||||||
{"name": "priority",
|
{"name": "priority",
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"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-ud", "expect": r"invalid opcode \(vector 6\)"},
|
||||||
{"name": "fault-pf", "expect": r"page fault \(vector 14\)"},
|
{"name": "fault-pf", "expect": r"page fault \(vector 14\)"},
|
||||||
{"name": "fault-df", "expect": r"double fault \(vector 8\)"},
|
{"name": "fault-df", "expect": r"double fault \(vector 8\)"},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue