add a big kernel lock for SMP

Guards the scheduler and IPC; held across the context switch.
This commit is contained in:
Daniel Samson
2026-07-08 12:35:30 +01:00
parent cf7c6df41c
commit 941ab091db
5 changed files with 129 additions and 21 deletions
+30 -15
View File
@@ -9,10 +9,18 @@
//! Switching happens both cooperatively (`yield`) and preemptively (the timer
//! calls `tick`). See docs/scheduling.md for the interrupt-flag discipline that
//! makes those two paths coexist.
//!
//! Cross-core safety is the **big kernel lock** (`sync.zig`): every critical
//! section here runs under it, and it is held across a context switch and released
//! by the task that resumes (see sync.zig's hand-off rule). On a single core the
//! lock is never contended, so the behaviour is exactly the old interrupt-flag
//! model; it's what lets a second core enter `schedule()` without corrupting the
//! shared queues.
const std = @import("std");
const arch = @import("arch");
const heap = @import("heap.zig");
const sync = @import("sync.zig");
/// Priority level: 0 (lowest) .. 7 (highest). 8 levels total.
pub const Priority = u3;
@@ -121,19 +129,19 @@ fn schedule() void {
/// Voluntarily give up the CPU to the next ready task.
pub fn yield() void {
const flags = arch.saveInterrupts();
const flags = sync.enter();
schedule();
arch.restoreInterrupts(flags);
sync.leave(flags);
}
/// 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();
const flags = sync.enter();
current.wake_at = arch.millis() + ms;
current.state = .blocked;
schedule(); // current is blocked, so schedule() won't re-enqueue it
arch.restoreInterrupts(flags);
sync.leave(flags);
}
// --- event-based blocking -------------------------------------------------
@@ -147,9 +155,10 @@ 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.
/// Block the current task on `wq` and switch away. Precondition: the big kernel
/// lock is held (so a condition can be checked and the block committed atomically;
/// it also keeps local interrupts disabled). On return — when woken — the lock is
/// still held.
pub fn waitLocked(wq: *WaitQueue) void {
current.state = .blocked;
current.next = wq.head;
@@ -158,7 +167,7 @@ pub fn waitLocked(wq: *WaitQueue) void {
}
/// Move the highest-priority waiter on `wq` (if any) to the ready queue.
/// Precondition: interrupts disabled. Does not preempt — the caller decides.
/// Precondition: the big kernel lock is held. 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;
@@ -182,20 +191,20 @@ pub fn wakeLocked(wq: *WaitQueue) void {
/// Block on `wq` (a self-contained critical section).
pub fn wait(wq: *WaitQueue) void {
const flags = arch.saveInterrupts();
const flags = sync.enter();
waitLocked(wq);
arch.restoreInterrupts(flags);
sync.leave(flags);
}
/// Wake the highest-priority waiter on `wq`, preempting if it outranks us.
pub fn wake(wq: *WaitQueue) void {
const flags = arch.saveInterrupts();
const flags = sync.enter();
wakeLocked(wq);
// If a higher-priority task is now ready, run it immediately.
if (highestReadyPriority()) |p| {
if (p > current.priority) schedule();
}
arch.restoreInterrupts(flags);
sync.leave(flags);
}
fn highestReadyPriority() ?Priority {
@@ -217,10 +226,15 @@ fn wakeExpired() void {
}
/// Called from the timer interrupt (interrupts already disabled): wake due
/// sleepers, then preempt.
/// sleepers, then preempt. Takes the kernel lock like any other critical section,
/// but releases it *without* touching the interrupt flag — the handler's `iretq`
/// restores the interrupted context's flags, so re-enabling here would open a
/// nested-interrupt window before the return.
pub fn tick() void {
_ = sync.enter();
wakeExpired();
if (preemption_enabled) schedule();
sync.leaveIsr();
}
/// Enable or disable timer-driven preemption (cooperative-only when off).
@@ -229,9 +243,10 @@ pub fn setPreemption(enabled: bool) void {
}
/// End the current task and switch away for good; never returns. The task's stack
/// is leaked for now (no reaper yet).
/// is leaked for now (no reaper yet). Acquires the kernel lock and hands it off to
/// the task we switch into (which releases it) — this frame never returns to leave.
pub fn exit() noreturn {
arch.disableInterrupts();
_ = sync.enter();
current.state = .free;
const next = dequeueHighest() orelse @panic("sched: no task left to run");
next.state = .running;