This commit is contained in:
2026-07-03 15:09:16 +01:00
parent df774691c8
commit 31bf86af56
12 changed files with 492 additions and 41 deletions
+50 -9
View File
@@ -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.
+34 -4
View File
@@ -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);
+54
View File
@@ -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;
}
};
}
+1 -1
View File
@@ -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=<name>`), run that case and stop.
// Normal builds fall through to the idle halt.
+106 -6
View File
@@ -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();
}
+108 -8
View File
@@ -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");