schedule tasks across all cores

Per-core GDT/TSS and AP scheduler entry; fix AP SSE + single_threaded.
This commit is contained in:
Daniel Samson
2026-07-08 12:35:30 +01:00
parent ed7f542006
commit 43afe6bf2e
11 changed files with 239 additions and 81 deletions
+41 -19
View File
@@ -9,14 +9,14 @@
//!
//! Cores are brought up **one at a time**: a single trampoline page and parameter
//! block are reused, so the BSP patches, wakes, and waits for one AP before the
//! next. The mechanism-vs-policy split matches the rest of the kernel — the generic
//! scheduler decides *what* runs where; this just gets a core executing 64-bit code.
//!
//! This is step 3a: an AP climbs to long mode, publishes its per-CPU pointer, marks
//! itself alive, and parks. Entering the scheduler (its own TSS, LAPIC timer, and
//! the run loop) is the next step.
//! next. That also lets `apEntry` pick up its dense CPU index from a plain global.
//! Once a core has its own descriptor tables, LAPIC, and timer, it calls the generic
//! scheduler entry and joins the run loop — mechanism here, policy there.
const io = @import("io.zig");
const gdt = @import("gdt.zig");
const tss = @import("tss.zig");
const idt = @import("idt.zig");
const apic = @import("apic.zig");
/// IA32_GS_BASE — the per-CPU data pointer (see cpu.zig; kept in sync here so the AP
@@ -32,6 +32,19 @@ var tramp_phys: u64 = 0;
/// one-at-a-time handshake (only one AP is being started at any moment).
var ap_alive: u32 = 0;
/// The dense CPU index of the AP currently being started. Set by the BSP before the
/// wake, read by `apEntry` (safe because bring-up is strictly one core at a time).
var boot_index: usize = 0;
/// The generic scheduler entry a woken core jumps to once its arch state is up. Set
/// by the kernel via `setSecondaryEntry`; never returns.
var secondary_entry: ?*const fn () callconv(.c) noreturn = null;
/// Register the generic entry an AP calls once its per-CPU tables/LAPIC/timer are up.
pub fn setSecondaryEntry(entry: *const fn () callconv(.c) noreturn) void {
secondary_entry = entry;
}
/// Copy the trampoline blob to its low page. Call once, after the page has been
/// allocated and made executable, before waking any AP.
pub fn prepare(phys: u64) void {
@@ -53,11 +66,13 @@ fn param(comptime name: []const u8) *align(1) volatile u64 {
return @ptrFromInt(tramp_phys + (sym - start));
}
/// Wake the core with Local APIC id `apic_id`, hand it `stack_top` and `percpu` (its
/// per-CPU pointer), and wait for it to come alive. Returns false if it doesn't
/// report in within the timeout (left parked, no harm to the running system).
/// `cr3` is the kernel page tables the AP adopts. Precondition: `prepare` has run.
pub fn startAp(apic_id: u32, stack_top: usize, percpu: usize, cr3: u64) bool {
/// Wake the core with Local APIC id `apic_id` as dense CPU `index`, hand it
/// `stack_top` and its per-CPU pointer `percpu`, and wait for it to come alive.
/// Returns false if it doesn't report in within the timeout (left parked, no harm to
/// the running system). `cr3` is the kernel page tables the AP adopts. Precondition:
/// `prepare` has run.
pub fn startAp(apic_id: u32, stack_top: usize, percpu: usize, index: usize, cr3: u64) bool {
boot_index = index;
param("ap_tramp_cr3").* = cr3;
param("ap_tramp_stack").* = stack_top;
param("ap_tramp_entry").* = @intFromPtr(&apEntry);
@@ -89,13 +104,20 @@ fn delayMicros(us: u64) void {
}
/// The 64-bit entry every AP lands on, called from the trampoline with its per-CPU
/// pointer in RDI. Adopts the shared descriptor tables, publishes its per-CPU
/// pointer, signals the BSP it's alive, and (for now) parks. Never returns.
/// pointer in RDI. Brings up this core's own descriptor tables, LAPIC and timer,
/// signals the BSP, then jumps to the generic scheduler entry. Never returns.
fn apEntry(percpu: usize) callconv(.c) noreturn {
// Step 3a: minimal. The core keeps the trampoline's descriptor tables, publishes
// its per-CPU pointer, signals the BSP, and parks with interrupts off. Loading
// this core's own kernel GDT/IDT/TSS and entering the scheduler is step 3b.
io.wrmsr(ia32_gs_base, percpu); // publish per-CPU pointer (GS base)
@atomicStore(u32, &ap_alive, 1, .release); // "I'm up" — BSP is polling this
while (true) asm volatile ("hlt"); // parked (3b enters the scheduler here)
const cpu = boot_index;
gdt.loadOnThisCpu(cpu); // this core's GDT (with its own TSS slot)
tss.setupThisCpu(cpu); // this core's TSS + IST stack, loaded into TR
idt.loadOnThisCpu(); // the shared IDT
io.wrmsr(ia32_gs_base, percpu); // per-CPU pointer — *after* the GDT reload
apic.initSecondary(); // software-enable this core's LAPIC
apic.initTimer(apic.frequencyHz()); // arm its timer (still masked: interrupts off)
@atomicStore(u32, &ap_alive, 1, .release); // "arch state up" — BSP is polling this
if (secondary_entry) |enterScheduler| enterScheduler(); // joins the run loop
while (true) asm volatile ("hlt"); // (only if no entry was registered)
}