diff --git a/src/kernel/arch/x86_64/cpu.zig b/src/kernel/arch/x86_64/cpu.zig index f585391..9757635 100644 --- a/src/kernel/arch/x86_64/cpu.zig +++ b/src/kernel/arch/x86_64/cpu.zig @@ -81,6 +81,24 @@ pub fn readCr3() u64 { ); } +/// IA32_GS_BASE: the hidden base of the GS segment. We repurpose it as the per-CPU +/// data pointer (there's no user mode yet, so no `swapgs` dance — GS base is always +/// the running core's per-CPU block). Set once per core during bring-up, after the +/// GDT is loaded (loading a GS *selector* would otherwise clobber this base). +const ia32_gs_base = 0xC000_0101; + +/// Publish this core's per-CPU data pointer so `cpuLocal` can retrieve it. Each +/// core calls this once, after its GDT is in place. +pub fn setCpuLocal(ptr: usize) void { + io.wrmsr(ia32_gs_base, ptr); +} + +/// This core's per-CPU data pointer (the value `setCpuLocal` stored). Reads the GS +/// base MSR — a per-core register, so each core sees its own without any locking. +pub fn cpuLocal() usize { + return io.rdmsr(ia32_gs_base); +} + /// Kernel tick rate: 1000 Hz (1 ms), the scheduler's time quantum. pub const timer_hz = 1000; diff --git a/src/kernel/scheduler.zig b/src/kernel/scheduler.zig index c1f4d91..39ccd2c 100644 --- a/src/kernel/scheduler.zig +++ b/src/kernel/scheduler.zig @@ -42,22 +42,57 @@ const Task = struct { }; var tasks = [_]Task{.{}} ** max_tasks; -var current: *Task = undefined; var next_id: u32 = 1; -// Per-priority FIFO ready queues, and a bitmap of which levels are non-empty. +/// Per-CPU scheduler state: the task each core is running, plus its own idle task. +/// One entry per core; the arch layer stashes a pointer to the *running* core's +/// entry in the GS base, so `thisCpu()` fetches it with a single read and no lock. +/// This is the only state that's genuinely per-core — the ready queues below stay +/// **global** under the big kernel lock, so any idle core pulls the highest-priority +/// ready task (work-conserving). Per-core ready queues are a later optimisation if +/// the global queue's lock contention ever bites (docs/smp.md). +pub const PerCpu = struct { + current: *Task = undefined, // the task running on this core + idle: *Task = undefined, // this core's idle task (always ready, lowest priority) + apic_id: u32 = 0, // the core's Local APIC id + index: u32 = 0, // dense 0-based core index + online: bool = false, // has this core finished bring-up? +}; + +const max_cpus = 64; // matches the discovery pool (src/device/acpi.zig) +var cpus = [_]PerCpu{.{}} ** max_cpus; + +/// This core's per-CPU state, via the arch layer's GS-base pointer. Valid only once +/// this core has run its scheduler bring-up (BSP in `init`, AP in `secondaryInit`). +inline fn thisCpu() *PerCpu { + return @ptrFromInt(arch.cpuLocal()); +} + +/// The task running on this core — the per-CPU replacement for the old global +/// `current`. A convenience reader; writes go through `thisCpu().current`. +inline fn cur() *Task { + return thisCpu().current; +} + +// Per-priority FIFO ready queues, and a bitmap of which levels are non-empty. These +// are shared across all cores and mutated only under the big kernel lock. var ready_head: [num_priorities]?*Task = .{null} ** num_priorities; var ready_tail: [num_priorities]?*Task = .{null} ** num_priorities; var ready_bitmap: u8 = 0; var preemption_enabled = true; -/// Register the currently-running kernel context as the first task, spawn the -/// idle task, and hook the timer for preemption. +/// Bring up scheduling on the bootstrap processor: register the currently-running +/// kernel context as task 0, publish this core's per-CPU state (via the GS base), +/// give the core an idle task, and hook the timer for preemption. Runs once, at +/// boot, before interrupts are enabled — so no lock is needed here. pub fn init(boot_priority: Priority) void { + const pc = &cpus[0]; + pc.* = .{ .index = 0, .online = true }; + arch.setCpuLocal(@intFromPtr(pc)); tasks[0] = .{ .id = 0, .state = .running, .priority = boot_priority }; - current = &tasks[0]; - spawn(idle, 0); // lowest priority, always runnable — runs when nothing else is + pc.current = &tasks[0]; + pc.idle = create(idle, 0); // this core's idle task: always ready, lowest priority arch.setTickHook(tick); } @@ -93,7 +128,18 @@ fn levelBit(p: Priority) u8 { } /// Create a task that runs `entry` at `priority`. It becomes ready immediately. +/// Takes the kernel lock: it mutates the shared task table and ready queues and +/// allocates from the (non-thread-safe) heap, so on SMP it must be serialised. pub fn spawn(entry: *const fn () void, priority: Priority) void { + const flags = sync.enter(); + _ = create(entry, priority); + sync.leave(flags); +} + +/// The unlocked task-creation primitive. Caller must hold the kernel lock (or be +/// the single-threaded boot path). Returns the new task so a core can keep a handle +/// to its idle task. +fn create(entry: *const fn () void, priority: Priority) *Task { const t = freeSlot() orelse @panic("sched: task table full"); const stack = heap.allocator().alloc(u8, stack_size) catch @panic("sched: no memory for task stack"); t.* = .{ .id = next_id, .state = .ready, .priority = priority, .stack = stack }; @@ -101,6 +147,7 @@ pub fn spawn(entry: *const fn () void, priority: Priority) void { const top = @intFromPtr(stack.ptr) + stack.len; t.rsp = arch.initTaskStack(top, @intFromPtr(entry)); enqueue(t); + return t; } fn freeSlot() ?*Task { @@ -110,10 +157,14 @@ fn freeSlot() ?*Task { return null; } -/// Pick the highest-priority ready task and switch to it. Interrupts must be -/// disabled by the caller. +/// Pick the highest-priority ready task and switch this core to it. The big kernel +/// lock must be held by the caller (which also keeps local interrupts disabled); +/// it serialises every core's scheduling, so no other core can touch the shared +/// queues while we requeue `prev` and dequeue `next`. A dequeued task is `.ready`, +/// never running elsewhere, so two cores never run the same task. fn schedule() void { - const prev = current; + const pc = thisCpu(); + const prev = pc.current; if (prev.state == .running) { prev.state = .ready; enqueue(prev); // back of its level's queue (round-robin) @@ -123,7 +174,7 @@ fn schedule() void { return; }; next.state = .running; - current = next; + pc.current = next; if (next != prev) arch.switchContext(&prev.rsp, next.rsp); } @@ -138,8 +189,9 @@ pub fn yield() void { /// again. The idle task (or other work) runs in the meantime. pub fn sleep(ms: u64) void { const flags = sync.enter(); - current.wake_at = arch.millis() + ms; - current.state = .blocked; + const t = cur(); + t.wake_at = arch.millis() + ms; + t.state = .blocked; schedule(); // current is blocked, so schedule() won't re-enqueue it sync.leave(flags); } @@ -160,9 +212,10 @@ pub const WaitQueue = struct { /// 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; - wq.head = current; + const t = cur(); + t.state = .blocked; + t.next = wq.head; + wq.head = t; schedule(); } @@ -173,10 +226,10 @@ pub fn wakeLocked(wq: *WaitQueue) void { var best_prev: ?*Task = null; var best: ?*Task = null; var prev: ?*Task = null; - var cur = wq.head; - while (cur) |t| : ({ + var node = wq.head; + while (node) |t| : ({ prev = t; - cur = t.next; + node = t.next; }) { if (best == null or t.priority > best.?.priority) { best = t; @@ -202,7 +255,7 @@ pub fn wake(wq: *WaitQueue) void { wakeLocked(wq); // If a higher-priority task is now ready, run it immediately. if (highestReadyPriority()) |p| { - if (p > current.priority) schedule(); + if (p > cur().priority) schedule(); } sync.leave(flags); } @@ -247,20 +300,21 @@ pub fn setPreemption(enabled: bool) void { /// the task we switch into (which releases it) — this frame never returns to leave. pub fn exit() noreturn { _ = sync.enter(); - current.state = .free; + const pc = thisCpu(); + pc.current.state = .free; const next = dequeueHighest() orelse @panic("sched: no task left to run"); next.state = .running; - current = next; + pc.current = next; var discard: usize = 0; arch.switchContext(&discard, next.rsp); unreachable; } pub fn currentId() u32 { - return current.id; + return cur().id; } /// Change the running task's priority (takes effect next time it's enqueued). pub fn setPriority(p: Priority) void { - current.priority = p; + cur().priority = p; }