move the running task into per-CPU state
PerCpu.current via the GS base; ready queues stay global.
This commit is contained in:
parent
941ab091db
commit
36c29d2d6d
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue