A dead task's kernel stack was leaked (no reaper), so every process/thread death bled kernel memory. Now exit()/exitUserLocked record the dying task in a per-core reap_after_switch slot and switch away; the task that resumes on that core frees the stack in switchTo's tail (on its own stack, lock still held so the slot can't be reused). reapKillPendingLocked drains the slot on the timer tick as a safety net for the fresh-task case (a fresh task enters via the trampoline, bypassing switchTo's tail). A task killed while not running is freed directly in destroyTaskLocked. live_stack_bytes is the observable. Fixed a migration bug this exposed: the post-switchContext reap read the pc parameter, but a migrated task carries a stale pc in its saved switchTo frame -> it freed the wrong core's pending stack (a #GP under SMP). Re-fetch thisCpu() after the switch. Gate task-reap PASS (5x isolated, 2x in the 24-case batch); full guardrail 24/24 incl. fault-recovery/supervision/process-kill/smp/affinity; build + host green.
1011 lines
45 KiB
Zig
1011 lines
45 KiB
Zig
//! The scheduler: fixed-priority preemptive multitasking.
|
|
//!
|
|
//! Tasks are kernel threads (ring 0, each with its own stack). The **highest-
|
|
//! priority ready task always runs**; within a priority level, tasks round-robin.
|
|
//! Selection is O(1) — a bitmap of non-empty priority levels plus a FIFO queue per
|
|
//! level — which keeps scheduling deterministic, as a real-time kernel needs (see
|
|
//! docs/vision.md).
|
|
//!
|
|
//! 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 abi = @import("abi");
|
|
const parameters = @import("parameters");
|
|
const architecture = @import("architecture");
|
|
const heap = @import("heap.zig");
|
|
const sync = @import("sync.zig");
|
|
|
|
/// Priority level: 0 (lowest) .. 7 (highest). 8 levels total.
|
|
pub const Priority = u3;
|
|
const number_priorities = 8;
|
|
|
|
const stack_size = parameters.kernel_stack_size; // each task's kernel stack
|
|
const maximum_tasks = parameters.maximum_tasks; // maximum tasks alive at once (static pool)
|
|
|
|
const State = enum { free, ready, running, blocked };
|
|
|
|
pub const Task = struct {
|
|
id: u32 = 0,
|
|
state: State = .free,
|
|
priority: Priority = 0,
|
|
sp: usize = 0, // saved stack pointer, valid while not running
|
|
stack: []u8 = &.{},
|
|
kstack_top: usize = 0, // top of `stack` (== TSS.rsp0 for a user task); 0 = none
|
|
wake_at: u64 = 0, // uptime (ms) to wake a sleeping task; 0 = not sleeping
|
|
affinity: ?u32 = null, // null = runs on any core; else the index of its pinned core
|
|
// --- process management (process.zig) ---
|
|
// Id of the process that spawned this one (0 = the kernel). The supervision
|
|
// link is the kill authority: only the supervisor may process_kill a child.
|
|
supervisor: u32 = 0,
|
|
// Endpoint to notify when this process ends (any way: exit, fault, kill), or
|
|
// null. Holds its own reference, dropped when the notification is posted.
|
|
// Opaque here for the same reason as `handles` below.
|
|
exit_endpoint: ?*anyopaque = null,
|
|
// How this process ended — set by the death paths (exit, fault, kill) just
|
|
// before the reap records it for `process_exit_reason`. Meaningless while
|
|
// the task lives.
|
|
exit_reason: abi.ExitReason = .exited,
|
|
// Endpoint this process's signals arrive on (signal_bind), or null — same
|
|
// ownership rules as exit_endpoint (holds a reference; opaque here).
|
|
signal_endpoint: ?*anyopaque = null,
|
|
// Signals posted but not yet delivered: the coalescing pending mask
|
|
// (docs/process-lifecycle.md). Bits are abi.Signal values. Signals pend here
|
|
// until an endpoint is bound; two pending terminates are one terminate.
|
|
pending_signals: u32 = 0,
|
|
// Set by process_kill on a task that is running on another core; the kernel
|
|
// finishes the kill at that task's next system call or timer tick.
|
|
kill_pending: bool = false,
|
|
// True while this task executes its own system call — the timer tick must not
|
|
// tear a task down in the middle of a kernel operation, only while it runs
|
|
// user code (or sits at a block point, where teardown is safe).
|
|
in_system_call: bool = false,
|
|
// Where this task is parked while blocked, so a kill can unlink it: the
|
|
// WaitQueue it waits on (maintained by waitLocked/wakeLocked), or the endpoint
|
|
// whose sender FIFO it queues in (maintained by the IPC layer; opaque here).
|
|
wait_queue: ?*WaitQueue = null,
|
|
ipc_wait_endpoint: ?*anyopaque = null,
|
|
// Physical root of this task's address space, or 0 for a kernel task (which
|
|
// runs on the shared kernel page tables). A user task carries its own.
|
|
aspace: u64 = 0,
|
|
user_ip: u64 = 0, // user-mode entry point (user task only)
|
|
user_sp: u64 = 0, // user-mode stack pointer (user task only)
|
|
user_arg: u64 = 0, // value delivered in the user's rdi at first entry: 0 for a
|
|
// process (its _start ignores it), the closure pointer for a thread (docs/threading.md)
|
|
// The user address this task is blocked on in futex_wait (0 = not futex-waiting).
|
|
// Cleared to 0 by futexWakeLocked as the "woken, not timed out" signal (docs/threading.md).
|
|
futex_addr: u64 = 0,
|
|
// The mmap / MMIO grant-arena cursors moved from Task to the per-address-space object
|
|
// (`AspaceRef`, below) so threads sharing one address space hand out disjoint grants
|
|
// — see aspaceMmapNextPtr / aspaceDeviceMapNextPtr (docs/threading-plan.md M7).
|
|
// --- synchronous IPC (ipc_sync.zig) ---
|
|
// Per-process handle table: a small-int handle names a kernel capability object.
|
|
// Each entry tags its `kind` (an IPC endpoint or a shared-memory object) so the
|
|
// close/exit and cap-passing paths reclaim the right type. Kept opaque here so the
|
|
// scheduler and IPC modules don't import each other (ipc_sync.zig owns the kinds).
|
|
handles: [ipc_maximum_handles]?HandleObject = .{null} ** ipc_maximum_handles,
|
|
// A server holds the caller it currently owes a reply to (set by ReplyWait's
|
|
// receive, cleared when it replies). A client, while blocked in Call, records
|
|
// its message + reply buffers here and its result lands in `ipc_status`.
|
|
ipc_client: ?*Task = null,
|
|
ipc_send_ptr: u64 = 0, // client: outgoing message (vaddr in this task's AS)
|
|
ipc_send_len: u64 = 0,
|
|
ipc_reply_ptr: u64 = 0, // client: reply buffer (vaddr)
|
|
ipc_reply_cap: u64 = 0,
|
|
ipc_status: i64 = 0, // client: reply length / -errno, written by the replier
|
|
dma_map_next: u64 = 0, // bump pointer into this task's DMA arena (0 = unseeded)
|
|
shm_map_next: u64 = 0, // bump pointer into this task's shared-memory arena (0 = unseeded)
|
|
ipc_send_cap: u64 = ~@as(u64, 0), // handle to transfer with this message (abi.no_cap = none)
|
|
ipc_received_cap: u64 = ~@as(u64, 0), // client: handle the reply's transferred cap landed at (abi.no_cap = none)
|
|
next: ?*Task = null, // ready-queue link (also the endpoint sender-FIFO link)
|
|
// The process's name — argv[0] as it was spawned (a boot-volume path for init,
|
|
// an initial-ramdisk name for everything else); empty for kernel tasks. Fixed
|
|
// storage, so the fault path can name the dead without touching the heap.
|
|
// Zero-initialised (not `undefined`): an undefined default is materialised as
|
|
// a 0xAA fill, which would move the whole static task pool out of .bss.
|
|
name_buffer: [maximum_task_name]u8 = .{0} ** maximum_task_name,
|
|
name_length: u8 = 0,
|
|
|
|
/// The task's name (argv[0] at spawn), or empty for a kernel task.
|
|
pub fn name(self: *const Task) []const u8 {
|
|
return self.name_buffer[0..self.name_length];
|
|
}
|
|
};
|
|
|
|
/// Capacity of `Task.name_buffer` — matches the longest name `system_spawn`
|
|
/// accepts, so a spawned name is never truncated. Shared with the ABI's
|
|
/// ProcessDescriptor, so `enumerate` copies names without clipping.
|
|
pub const maximum_task_name = abi.maximum_process_name;
|
|
|
|
/// Size of each task's IPC handle table. Kept here (not in ipc_sync.zig) because
|
|
/// it dimensions a field of `Task`; ipc_sync.zig re-exports it.
|
|
pub const ipc_maximum_handles = 16;
|
|
|
|
/// One handle-table entry: a capability object plus a `kind` tag saying what `ptr` points
|
|
/// at (an ipc endpoint or a shared-memory object), so a task's exit path and the
|
|
/// capability-passing path reclaim/share the right type. The `kind` values are defined by
|
|
/// ipc_sync.zig (`handle_kind_*`); kept an opaque `u8` here so the scheduler doesn't import
|
|
/// the IPC module.
|
|
pub const HandleObject = struct { kind: u8, ptr: *anyopaque };
|
|
|
|
var tasks = [_]Task{.{}} ** maximum_tasks;
|
|
|
|
/// Address-space reference counts: one live entry per address space, counting the
|
|
/// tasks that share it. An address space is 1:1 with a process today; threads
|
|
/// (docs/threading.md) will push a count above 1, and `destroyAddressSpace` must run
|
|
/// only when the **last** task on an address space exits. All access is under the big
|
|
/// kernel lock. There can be no more live address spaces than tasks, so the table is
|
|
/// sized to the task pool and never overflows in practice.
|
|
// The per-address-space kernel object: a reference count plus the grant-arena cursors.
|
|
// One live entry per address space; threads sharing an address space share this entry,
|
|
// so their mmap/mmio grants bump one cursor and never overlap (docs/threading-plan.md M7).
|
|
// `mmap_next`/`device_map_next` are 0 until process.zig seeds them to the arena base.
|
|
const AspaceRef = struct { root: u64 = 0, count: u32 = 0, mmap_next: u64 = 0, device_map_next: u64 = 0 };
|
|
var aspace_refs = [_]AspaceRef{.{}} ** maximum_tasks;
|
|
var aspace_destroy_count: u64 = 0;
|
|
|
|
/// Total bytes of task **kernel** stacks currently allocated from the kernel heap —
|
|
/// incremented when a task is created, decremented when the reaper frees a dead task's
|
|
/// stack. A test-observable proof that the reaper reclaims every stack (docs/threading-
|
|
/// plan.md M8): with no live tasks beyond the baseline, this returns to its baseline.
|
|
var live_stack_bytes: usize = 0;
|
|
|
|
/// Test-observable: bytes of task kernel stacks currently live (see `live_stack_bytes`).
|
|
pub fn liveStackBytes() usize {
|
|
return live_stack_bytes;
|
|
}
|
|
|
|
/// Free a dead task's kernel stack and drop it from `live_stack_bytes`. The task must be
|
|
/// off that stack already (killed while not running, or reaped after it switched away).
|
|
/// Caller holds the kernel lock.
|
|
fn reapStackLocked(t: *Task) void {
|
|
if (t.stack.len == 0) return; // boot/idle tasks run on a static stack — nothing to free
|
|
live_stack_bytes -= t.stack.len;
|
|
heap.allocator().free(t.stack);
|
|
t.stack = &.{};
|
|
t.kstack_top = 0;
|
|
}
|
|
|
|
/// Take a reference to address space `root` (0 = a kernel task, which owns none).
|
|
/// Returns false only if the ref table is full — bounded by `maximum_tasks`, so in
|
|
/// practice it never is. Caller holds the kernel lock.
|
|
fn retainAspace(root: u64) bool {
|
|
if (root == 0) return true;
|
|
var free: ?*AspaceRef = null;
|
|
for (&aspace_refs) |*entry| {
|
|
if (entry.count != 0 and entry.root == root) {
|
|
entry.count += 1;
|
|
return true;
|
|
}
|
|
if (entry.count == 0 and free == null) free = entry;
|
|
}
|
|
const slot = free orelse return false;
|
|
slot.* = .{ .root = root, .count = 1 };
|
|
return true;
|
|
}
|
|
|
|
/// Drop a reference to `root`; destroy the address space when the **last** one drops.
|
|
/// A `root` with no entry — never retained, e.g. a hand-built test space — is
|
|
/// destroyed directly, preserving the pre-refcount behaviour. Caller holds the lock.
|
|
fn releaseAspace(root: u64) void {
|
|
if (root == 0) return;
|
|
for (&aspace_refs) |*entry| {
|
|
if (entry.count == 0 or entry.root != root) continue;
|
|
entry.count -= 1;
|
|
if (entry.count == 0) {
|
|
entry.root = 0;
|
|
architecture.destroyAddressSpace(root);
|
|
aspace_destroy_count += 1;
|
|
}
|
|
return;
|
|
}
|
|
architecture.destroyAddressSpace(root);
|
|
aspace_destroy_count += 1;
|
|
}
|
|
|
|
/// Test-observable: how many address spaces are live (entries with a nonzero count).
|
|
pub fn liveAspaceCount() u32 {
|
|
var live: u32 = 0;
|
|
for (&aspace_refs) |*entry| {
|
|
if (entry.count != 0) live += 1;
|
|
}
|
|
return live;
|
|
}
|
|
|
|
/// Test-observable: total address-space destructions since boot.
|
|
pub fn aspaceDestroyCount() u64 {
|
|
return aspace_destroy_count;
|
|
}
|
|
|
|
/// Pointer to the mmap grant-arena cursor for address space `root`, so the mmap syscall
|
|
/// can read-and-bump it. Per-address-space (not per-task), so sibling threads get
|
|
/// disjoint grants. **Caller holds the kernel lock** (the entry is stable while held).
|
|
/// Null only if `root` was never retained — which can't happen for a live user task.
|
|
pub fn aspaceMmapNextPtr(root: u64) ?*u64 {
|
|
for (&aspace_refs) |*entry| {
|
|
if (entry.count != 0 and entry.root == root) return &entry.mmap_next;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Pointer to the MMIO grant-arena cursor for address space `root` (see
|
|
/// `aspaceMmapNextPtr`). Caller holds the kernel lock.
|
|
pub fn aspaceDeviceMapNextPtr(root: u64) ?*u64 {
|
|
for (&aspace_refs) |*entry| {
|
|
if (entry.count != 0 and entry.root == root) return &entry.device_map_next;
|
|
}
|
|
return null;
|
|
}
|
|
var next_id: u32 = 1;
|
|
|
|
/// Per-CPU scheduler state: the task each core is running, its own idle task, and a
|
|
/// queue of tasks **pinned** to it. One entry per core; the architecture 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.
|
|
///
|
|
/// Most work stays in the **global** ready queue (below), which any idle core pulls
|
|
/// from — work-conserving. A task given an *affinity* instead goes to that core's
|
|
/// `pinned_*` queue and is only ever run there (no surprise migration — the more
|
|
/// real-time-predictable model, docs/smp.md). The two queues are merged at selection
|
|
/// time. Both are still mutated only under the big kernel lock, so one core enqueuing
|
|
/// into another core's pinned queue is safe.
|
|
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)
|
|
hw_id: u32 = 0, // the core's hardware id (Local APIC id on x86_64)
|
|
index: u32 = 0, // dense 0-based core index
|
|
online: bool = false, // has this core finished bring-up?
|
|
loaded_aspace: u64 = 0, // the address-space root currently loaded on this core
|
|
// Tasks pinned to this core (affinity == index), per priority level + bitmap.
|
|
pinned_head: [number_priorities]?*Task = .{null} ** number_priorities,
|
|
pinned_tail: [number_priorities]?*Task = .{null} ** number_priorities,
|
|
pinned_bitmap: u8 = 0,
|
|
// A task that ended while running on THIS core: it could not free the kernel stack it
|
|
// was standing on, so it recorded itself here and switched away. The next task to run
|
|
// on this core frees that stack (from its own stack, safely) in `switchTo`. The big
|
|
// lock is held continuously across the switch, so the dead task's slot can't be reused
|
|
// before it is reaped (docs/threading-plan.md M8).
|
|
reap_after_switch: ?*Task = null,
|
|
};
|
|
|
|
const maximum_cpus = parameters.maximum_cpus;
|
|
var cpus = [_]PerCpu{.{}} ** maximum_cpus;
|
|
|
|
/// This core's per-CPU state, via the architecture 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(architecture.cpuLocal());
|
|
}
|
|
|
|
/// The task running on this core — the per-CPU replacement for the old global
|
|
/// `current`. A convenience reader; writes go through `thisCpu().current`.
|
|
pub inline fn current() *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: [number_priorities]?*Task = .{null} ** number_priorities;
|
|
var ready_tail: [number_priorities]?*Task = .{null} ** number_priorities;
|
|
var ready_bitmap: u8 = 0;
|
|
|
|
var preemption_enabled = true;
|
|
|
|
/// 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, .loaded_aspace = architecture.kernelPageTable() };
|
|
architecture.setCpuLocal(0, @intFromPtr(pc));
|
|
tasks[0] = .{ .id = 0, .state = .running, .priority = boot_priority };
|
|
pc.current = &tasks[0];
|
|
pc.idle = create(idle, 0, null); // this core's idle task: always ready, lowest priority
|
|
architecture.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");
|
|
}
|
|
|
|
/// Reserve and initialise the per-CPU slot for an application processor at dense
|
|
/// `index` (1-based; 0 is the BSP) with hardware id `hw_id`, and return a
|
|
/// pointer the architecture bring-up hands to the core (it publishes it in its GS base).
|
|
/// Called on the BSP before waking each AP; the AP marks itself `online`.
|
|
pub fn prepareSecondary(index: usize, hw_id: u32) *PerCpu {
|
|
const pc = &cpus[index];
|
|
pc.* = .{ .index = @intCast(index), .hw_id = hw_id, .online = false };
|
|
return pc;
|
|
}
|
|
|
|
/// Entry for an application processor once the architecture layer has set up its per-CPU
|
|
/// tables, LAPIC, and timer. It turns this bring-up context into the core's idle task
|
|
/// (as task 0 is for the BSP), marks the core online, and enters the run loop: with
|
|
/// interrupts enabled the timer preempts this idle context into whatever the global
|
|
/// ready queue offers, so the core runs real work in parallel with the others. The
|
|
/// `.c` calling convention lets the architecture trampoline path jump here. Never returns.
|
|
pub fn secondaryMain() callconv(.c) noreturn {
|
|
const flags = sync.enter();
|
|
const pc = thisCpu();
|
|
const t = freeSlot() orelse @panic("sched: task table full (AP idle task)");
|
|
t.* = .{ .id = next_id, .state = .running, .priority = 0 };
|
|
next_id += 1;
|
|
pc.current = t;
|
|
pc.idle = t;
|
|
pc.online = true;
|
|
pc.loaded_aspace = architecture.kernelPageTable(); // the AP adopted the kernel tables at bring-up
|
|
sync.leave(flags);
|
|
|
|
architecture.enableInterrupts(); // the timer now preempts this idle context into work
|
|
while (true) asm volatile ("hlt"); // idle when this core has nothing ready
|
|
}
|
|
|
|
/// Number of cores that have finished bring-up (the BSP plus every online AP).
|
|
pub fn onlineCount() usize {
|
|
var n: usize = 0;
|
|
for (&cpus) |*pc| {
|
|
if (pc.online) n += 1;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/// Make `t` ready. A pinned task (affinity set) goes to that core's pinned queue;
|
|
/// everything else goes to the shared global queue.
|
|
fn enqueue(t: *Task) void {
|
|
if (t.affinity) |cpu| {
|
|
const pc = &cpus[cpu];
|
|
enqueueTo(&pc.pinned_head, &pc.pinned_tail, &pc.pinned_bitmap, t);
|
|
} else {
|
|
enqueueTo(&ready_head, &ready_tail, &ready_bitmap, t);
|
|
}
|
|
}
|
|
|
|
fn enqueueTo(head: *[number_priorities]?*Task, tail: *[number_priorities]?*Task, bitmap: *u8, t: *Task) void {
|
|
t.next = null;
|
|
const p: usize = t.priority;
|
|
if (tail[p]) |tl| tl.next = t else head[p] = t;
|
|
tail[p] = t;
|
|
bitmap.* |= @as(u8, 1) << t.priority;
|
|
}
|
|
|
|
/// The highest non-empty priority level in a bitmap, or -1 if empty.
|
|
fn topLevel(bitmap: u8) i32 {
|
|
if (bitmap == 0) return -1;
|
|
return @as(i32, number_priorities - 1) - @as(i32, @clz(bitmap));
|
|
}
|
|
|
|
/// Pick the highest-priority ready task for core `pc`: the better of the global queue
|
|
/// and this core's pinned queue. Still O(1) (two `clz` and a compare). A pinned task
|
|
/// wins an equal-priority tie, so it can't be starved by global work at its level.
|
|
fn dequeueHighest(pc: *PerCpu) ?*Task {
|
|
const g = topLevel(ready_bitmap);
|
|
const p = topLevel(pc.pinned_bitmap);
|
|
if (g < 0 and p < 0) return null;
|
|
if (p >= g) return dequeueFrom(&pc.pinned_head, &pc.pinned_tail, &pc.pinned_bitmap, @intCast(p));
|
|
return dequeueFrom(&ready_head, &ready_tail, &ready_bitmap, @intCast(g));
|
|
}
|
|
|
|
fn dequeueFrom(head: *[number_priorities]?*Task, tail: *[number_priorities]?*Task, bitmap: *u8, level: usize) ?*Task {
|
|
const t = head[level].?;
|
|
head[level] = t.next;
|
|
if (head[level] == null) {
|
|
tail[level] = null;
|
|
bitmap.* &= ~(@as(u8, 1) << @intCast(level));
|
|
}
|
|
t.next = null;
|
|
return t;
|
|
}
|
|
|
|
/// Create a task that runs `entry` at `priority`, runnable on any core. 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, null);
|
|
sync.leave(flags);
|
|
}
|
|
|
|
/// Like `spawn`, but **pins** the task to core `cpu` — it will only ever run there.
|
|
/// Returns true if pinned; false if `cpu` isn't a valid, online core, in which case
|
|
/// the task is still created but left unpinned (so it runs *somewhere* rather than
|
|
/// stranding in a queue no core services). Callers that require the pin (e.g. tests)
|
|
/// should check the result.
|
|
pub fn spawnOn(entry: *const fn () void, priority: Priority, cpu: u32) bool {
|
|
const flags = sync.enter();
|
|
defer sync.leave(flags);
|
|
const ok = cpu < maximum_cpus and cpus[cpu].online;
|
|
_ = create(entry, priority, if (ok) cpu else null);
|
|
return ok;
|
|
}
|
|
|
|
/// Spawn a **user** task: a task with its own address space (`aspace`) that starts
|
|
/// in user mode at `entry` on `user_sp`, recorded under `name` (its argv[0]).
|
|
/// `supervisor` is the id of the spawning process (0 = the kernel) — the kill
|
|
/// authority — and `exit_endpoint` (an *ipc.Endpoint whose reference the caller
|
|
/// has already taken, or null) is notified when this process ends.
|
|
/// It gets a fresh kernel stack for syscalls/interrupts, and its first switch-in
|
|
/// lands in `user_task_trampoline`.
|
|
/// Returns the new process id, or null (creating nothing) if the table is full or
|
|
/// out of memory.
|
|
/// **Caller must hold the kernel lock** (the loader that builds `aspace` holds it
|
|
/// across the whole spawn, so the address space and the task appear atomically).
|
|
pub fn spawnUserLocked(aspace: u64, entry: u64, user_sp: u64, user_arg: u64, priority: Priority, task_name: []const u8, supervisor: u32, exit_endpoint: ?*anyopaque) ?u32 {
|
|
const t = freeSlot() orelse return null;
|
|
const stack = heap.allocator().alloc(u8, stack_size) catch return null;
|
|
// Take this task's reference to the address space before we commit the slot, so a
|
|
// failure here leaves nothing to unwind (the caller still owns the raw `aspace`).
|
|
if (!retainAspace(aspace)) {
|
|
heap.allocator().free(stack);
|
|
return null;
|
|
}
|
|
live_stack_bytes += stack.len; // the reaper drops this when the task dies (M8)
|
|
t.* = .{
|
|
.id = next_id,
|
|
.state = .ready,
|
|
.priority = priority,
|
|
.stack = stack,
|
|
.aspace = aspace,
|
|
.user_ip = entry,
|
|
.user_sp = user_sp,
|
|
.user_arg = user_arg,
|
|
.supervisor = supervisor,
|
|
.exit_endpoint = exit_endpoint,
|
|
};
|
|
const name_length = @min(task_name.len, maximum_task_name);
|
|
@memcpy(t.name_buffer[0..name_length], task_name[0..name_length]);
|
|
t.name_length = @intCast(name_length);
|
|
next_id += 1;
|
|
const top = @intFromPtr(stack.ptr) + stack.len;
|
|
t.kstack_top = top;
|
|
// First switch-in lands in startUserTask (no register smuggling — it reads
|
|
// the user entry/stack from the Task itself).
|
|
t.sp = architecture.initTaskStack(top, @intFromPtr(&startUserTask));
|
|
enqueue(t);
|
|
return t.id;
|
|
}
|
|
|
|
/// The first thing a fresh user task runs (in ring 0, via task_trampoline). It
|
|
/// drops to ring 3 at the task's recorded entry/stack. Reading them from the
|
|
/// Task avoids smuggling values through callee-saved registers across the
|
|
/// context switch and lock release.
|
|
fn startUserTask() void {
|
|
const t = current();
|
|
// No serial chatter here: this runs on every spawn, unserialized against
|
|
// user-space writes, and its output used to shear concurrent log lines in
|
|
// half — the largest source of corrupted markers in the QEMU scenarios.
|
|
architecture.jumpToUserArg(t.user_ip, t.user_sp, t.user_arg); // noreturn (arg0 = 0 for a process)
|
|
}
|
|
|
|
/// The unlocked task-creation primitive. Caller must hold the kernel lock (or be the
|
|
/// single-threaded boot path). `affinity` pins the task to a core (null = any).
|
|
/// Returns the new task so a core can keep a handle to its idle task.
|
|
fn create(entry: *const fn () void, priority: Priority, affinity: ?u32) *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");
|
|
live_stack_bytes += stack.len; // the reaper drops this when the task dies (M8)
|
|
t.* = .{ .id = next_id, .state = .ready, .priority = priority, .stack = stack, .affinity = affinity };
|
|
next_id += 1;
|
|
const top = @intFromPtr(stack.ptr) + stack.len;
|
|
t.kstack_top = top;
|
|
t.sp = architecture.initTaskStack(top, @intFromPtr(entry));
|
|
enqueue(t);
|
|
return t;
|
|
}
|
|
|
|
fn freeSlot() ?*Task {
|
|
for (&tasks) |*t| {
|
|
if (t.state == .free) return t;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// 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 `previous` and dequeue `next`. A dequeued task is `.ready`,
|
|
/// never running elsewhere, so two cores never run the same task.
|
|
fn schedule() void {
|
|
const pc = thisCpu();
|
|
const previous = pc.current;
|
|
if (previous.state == .running) {
|
|
previous.state = .ready;
|
|
enqueue(previous); // back of its level's queue (round-robin)
|
|
}
|
|
const next = dequeueHighest(pc) orelse {
|
|
previous.state = .running; // nothing else ready — keep running
|
|
return;
|
|
};
|
|
next.state = .running;
|
|
pc.current = next;
|
|
if (next != previous) switchTo(pc, &previous.sp, next);
|
|
}
|
|
|
|
/// Make `next` this core's running task: publish its kernel stack (TSS.rsp0, so a
|
|
/// user-mode interrupt lands on a good stack) and its address space (only when
|
|
/// it differs from what's loaded — every page-table switch is a full TLB flush),
|
|
/// then switch registers/stacks. Kernel tasks (aspace == 0, no kstack_top used
|
|
/// from user mode) resolve to the shared kernel page tables and skip the kernel-
|
|
/// stack write, so this is a no-op beyond the register switch for a pure-kernel
|
|
/// workload. The big kernel lock is held and interrupts are off throughout, so no
|
|
/// interrupt can observe a half-updated (kernel stack, address space) pair.
|
|
/// `save_sp` receives the outgoing task's stack pointer.
|
|
fn switchTo(pc: *PerCpu, save_sp: *usize, next: *Task) void {
|
|
if (next.kstack_top != 0) architecture.setKernelStack(pc.index, next.kstack_top);
|
|
const want = if (next.aspace != 0) next.aspace else architecture.kernelPageTable();
|
|
if (want != pc.loaded_aspace) {
|
|
architecture.loadPageTable(want);
|
|
pc.loaded_aspace = want;
|
|
}
|
|
architecture.switchContext(save_sp, next.sp);
|
|
// Resumed now (switchContext returned into our own switchTo frame). Re-fetch the core
|
|
// via thisCpu(): the `pc` parameter is from *our* earlier switchTo call, so it names
|
|
// the core we last ran on — stale if we migrated. switchContext only swaps stacks on
|
|
// the current core, so thisCpu() is the core the just-dead task died on. If a task
|
|
// died switching to us, free its kernel stack: we're on ours so it's safe, and the big
|
|
// lock is still held so its slot can't have been reused (docs/threading-plan.md M8).
|
|
const here = thisCpu();
|
|
if (here.reap_after_switch) |dead| {
|
|
here.reap_after_switch = null;
|
|
reapStackLocked(dead);
|
|
}
|
|
}
|
|
|
|
/// Voluntarily give up the CPU to the next ready task.
|
|
pub fn yield() void {
|
|
const flags = sync.enter();
|
|
schedule();
|
|
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 = sync.enter();
|
|
const t = current();
|
|
t.wake_at = architecture.millis() + ms;
|
|
t.state = .blocked;
|
|
schedule(); // current is blocked, so schedule() won't re-enqueue it
|
|
sync.leave(flags);
|
|
}
|
|
|
|
// --- futex: block/wake on a user address (docs/threading.md) ----------------
|
|
//
|
|
// A futex waiter is not linked into any queue — it is simply a `.blocked` task
|
|
// tagged with the address it waits on (`futex_addr`). Waking scans the task table
|
|
// (bounded) for matching waiters. A timed wait also sets `wake_at`, so the timer's
|
|
// `wakeExpired` can wake it; `futex_addr` stays non-zero in that case, which is how
|
|
// the waiter tells a timeout from a real wake.
|
|
|
|
pub const FutexResult = enum { woken, timed_out };
|
|
|
|
/// Block the current task on futex `addr` until woken, or (if `timeout_ms > 0`) the
|
|
/// deadline. **Precondition:** the big kernel lock is held and the caller has already
|
|
/// checked, under this same lock, that the futex word equals the expected value — so
|
|
/// no wake can be missed. Returns with the lock still held.
|
|
pub fn futexWaitLocked(addr: u64, timeout_ms: u64) FutexResult {
|
|
const t = current();
|
|
t.futex_addr = addr;
|
|
t.wake_at = if (timeout_ms > 0) architecture.millis() + timeout_ms else 0;
|
|
t.state = .blocked;
|
|
schedule(); // woken by futexWakeLocked (clears futex_addr) or wakeExpired (timeout)
|
|
const woken = t.futex_addr == 0;
|
|
t.futex_addr = 0;
|
|
t.wake_at = 0;
|
|
return if (woken) .woken else .timed_out;
|
|
}
|
|
|
|
/// Wake up to `count` tasks blocked in `futex_wait` on `addr` in address space
|
|
/// `aspace`. Precondition: the big kernel lock is held. Returns how many woke.
|
|
pub fn futexWakeLocked(aspace: u64, addr: u64, count: u32) u32 {
|
|
var woken: u32 = 0;
|
|
for (&tasks) |*t| {
|
|
if (woken >= count) break;
|
|
if (t.state == .blocked and t.aspace == aspace and t.futex_addr == addr) {
|
|
t.futex_addr = 0; // the "woken, not timed out" signal to futexWaitLocked
|
|
t.wake_at = 0;
|
|
t.state = .ready;
|
|
enqueue(t);
|
|
woken += 1;
|
|
}
|
|
}
|
|
return woken;
|
|
}
|
|
|
|
// --- 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 `wait_queue` 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(wait_queue: *WaitQueue) void {
|
|
const t = current();
|
|
t.state = .blocked;
|
|
t.wait_queue = wait_queue; // so a kill can unlink a parked waiter
|
|
t.next = wait_queue.head;
|
|
wait_queue.head = t;
|
|
schedule();
|
|
}
|
|
|
|
/// Move the highest-priority waiter on `wait_queue` (if any) to the ready queue.
|
|
/// Precondition: the big kernel lock is held. Does not preempt — the caller decides.
|
|
pub fn wakeLocked(wait_queue: *WaitQueue) void {
|
|
// Find the highest-priority waiter (bounded scan) and unlink it.
|
|
var best_previous: ?*Task = null;
|
|
var best: ?*Task = null;
|
|
var previous: ?*Task = null;
|
|
var node = wait_queue.head;
|
|
while (node) |t| : ({
|
|
previous = t;
|
|
node = t.next;
|
|
}) {
|
|
if (best == null or t.priority > best.?.priority) {
|
|
best = t;
|
|
best_previous = previous;
|
|
}
|
|
}
|
|
const t = best orelse return;
|
|
if (best_previous) |p| p.next = t.next else wait_queue.head = t.next;
|
|
t.wait_queue = null;
|
|
t.state = .ready;
|
|
enqueue(t);
|
|
}
|
|
|
|
/// Unlink `t` from the wait queue it is parked on, if any (the kill path — a
|
|
/// killed waiter must not be woken later as a dangling pointer). Precondition:
|
|
/// the big kernel lock is held.
|
|
pub fn removeFromWaitQueueLocked(t: *Task) void {
|
|
const wait_queue = t.wait_queue orelse return;
|
|
t.wait_queue = null;
|
|
var previous: ?*Task = null;
|
|
var node = wait_queue.head;
|
|
while (node) |n| : ({
|
|
previous = n;
|
|
node = n.next;
|
|
}) {
|
|
if (n != t) continue;
|
|
if (previous) |p| p.next = t.next else wait_queue.head = t.next;
|
|
t.next = null;
|
|
return;
|
|
}
|
|
}
|
|
|
|
/// Unlink `t` from the ready queue it sits in (global, or its affinity core's
|
|
/// pinned queue) — the kill path for a task that is runnable but not running.
|
|
/// Precondition: the big kernel lock is held.
|
|
pub fn removeFromReadyQueueLocked(t: *Task) void {
|
|
if (t.affinity) |cpu| {
|
|
const pc = &cpus[cpu];
|
|
removeFrom(&pc.pinned_head, &pc.pinned_tail, &pc.pinned_bitmap, t);
|
|
} else {
|
|
removeFrom(&ready_head, &ready_tail, &ready_bitmap, t);
|
|
}
|
|
}
|
|
|
|
fn removeFrom(head: *[number_priorities]?*Task, tail: *[number_priorities]?*Task, bitmap: *u8, t: *Task) void {
|
|
const level: usize = t.priority;
|
|
var previous: ?*Task = null;
|
|
var node = head[level];
|
|
while (node) |n| : ({
|
|
previous = n;
|
|
node = n.next;
|
|
}) {
|
|
if (n != t) continue;
|
|
if (previous) |p| p.next = t.next else head[level] = t.next;
|
|
if (tail[level] == t) tail[level] = previous;
|
|
if (head[level] == null) bitmap.* &= ~(@as(u8, 1) << @intCast(level));
|
|
t.next = null;
|
|
return;
|
|
}
|
|
}
|
|
|
|
/// Find a live task by process id, or null. Ids are monotonic and never reused,
|
|
/// so a stale id misses cleanly rather than naming a recycled slot.
|
|
/// Precondition: the big kernel lock is held.
|
|
pub fn taskByIdLocked(id: u32) ?*Task {
|
|
for (&tasks) |*t| {
|
|
if (t.state != .free and t.id == id) return t;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Make every server that still holds `t` as the client it owes a reply to forget
|
|
/// it — the reply of a dead client is dropped, not delivered into freed state.
|
|
/// Precondition: the big kernel lock is held.
|
|
pub fn forgetIpcClientLocked(t: *Task) void {
|
|
for (&tasks) |*other| {
|
|
if (other.state != .free and other.ipc_client == t) other.ipc_client = null;
|
|
}
|
|
}
|
|
|
|
/// Block the current task and switch away, without putting it on any wait queue —
|
|
/// the caller has already linked it wherever it belongs (e.g. an endpoint's sender
|
|
/// FIFO). Precondition: the big kernel lock is held; still held on return (when the
|
|
/// task is made ready again). The IPC layer's counterpart to `waitLocked`.
|
|
pub fn blockCurrentLocked() void {
|
|
current().state = .blocked;
|
|
schedule();
|
|
}
|
|
|
|
/// Make a specific (currently blocked) task ready to run again. Precondition: the
|
|
/// big kernel lock is held. Used by the IPC layer to wake a specific caller/server
|
|
/// rather than "some waiter on a queue".
|
|
pub fn readyLocked(t: *Task) void {
|
|
t.state = .ready;
|
|
enqueue(t);
|
|
}
|
|
|
|
/// Block on `wait_queue` (a self-contained critical section).
|
|
pub fn wait(wait_queue: *WaitQueue) void {
|
|
const flags = sync.enter();
|
|
waitLocked(wait_queue);
|
|
sync.leave(flags);
|
|
}
|
|
|
|
/// Wake the highest-priority waiter on `wait_queue`, preempting if it outranks us.
|
|
pub fn wake(wait_queue: *WaitQueue) void {
|
|
const flags = sync.enter();
|
|
const pc = thisCpu();
|
|
wakeLocked(wait_queue);
|
|
// If a task this core would now pick outranks the running one, run it at once.
|
|
// (A waiter pinned to *another* core isn't counted — that core picks it up on its
|
|
// next tick; this core doesn't preempt for work it can't run.)
|
|
if (highestReadyPriority(pc)) |p| {
|
|
if (p > pc.current.priority) schedule();
|
|
}
|
|
sync.leave(flags);
|
|
}
|
|
|
|
/// The highest-priority task core `pc` could run right now — the better of the global
|
|
/// queue and this core's pinned queue — or null if it would fall back to idle.
|
|
fn highestReadyPriority(pc: *PerCpu) ?Priority {
|
|
const top = @max(topLevel(ready_bitmap), topLevel(pc.pinned_bitmap));
|
|
if (top < 0) return null;
|
|
return @intCast(top);
|
|
}
|
|
|
|
/// 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 = architecture.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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process-teardown hooks, registered by process.zig at init — the scheduler sits
|
|
// below the process layer, so finishing a kill (IRQ bindings, IPC handles, exit
|
|
// notification) is called *up* through these, mirroring how the architecture
|
|
// layer calls up into `tick`.
|
|
//
|
|
// `terminate_current_hook` ends the task running on THIS core (lock held, never
|
|
// returns — it switches away like `exitUserLocked`). `reap_task_hook` tears down
|
|
// a task that is NOT running on any core (lock held).
|
|
pub var terminate_current_hook: ?*const fn () noreturn = null;
|
|
pub var reap_task_hook: ?*const fn (*Task) void = null;
|
|
|
|
/// Finish any pending kills this core can see (the deferred half of process_kill;
|
|
/// the immediate half runs in the killer's own call). Precondition: the big kernel
|
|
/// lock is held, from `tick`.
|
|
///
|
|
/// - This core's *current* task, if condemned, is terminated here — but only when
|
|
/// it is not inside one of its own system calls (`in_system_call`): the tick may
|
|
/// have interrupted kernel code mid-operation, where teardown would leak or
|
|
/// corrupt what that operation holds. User-mode execution (and the system_call
|
|
/// entry/exit stubs, which hold nothing) are safe termination points. A task
|
|
/// that *is* mid-call dies at its next block, tick, or system_call entry instead.
|
|
/// The hook never returns; abandoning the interrupt frame is fine — the LAPIC
|
|
/// was acknowledged before the tick hook ran (see apic.timerTick), exactly as on
|
|
/// the fault-kill path.
|
|
/// - Condemned tasks that are ready or blocked are not running anywhere (state
|
|
/// changes need the lock we hold), so they are reaped in place.
|
|
fn reapKillPendingLocked() void {
|
|
const pc = thisCpu();
|
|
const cur = pc.current;
|
|
// Safety net for the reap-after-switch slot: if a dying task switched to a *fresh*
|
|
// task (which enters via task_trampoline, not switchTo's tail), its kernel stack is
|
|
// still pending here. The dying task switched away before this tick, so it is off its
|
|
// stack — reap it now (docs/threading-plan.md M8).
|
|
if (pc.reap_after_switch) |dead| {
|
|
pc.reap_after_switch = null;
|
|
reapStackLocked(dead);
|
|
}
|
|
if (cur.kill_pending and cur.aspace != 0 and !cur.in_system_call) {
|
|
if (terminate_current_hook) |hook| hook(); // noreturn
|
|
}
|
|
if (reap_task_hook) |hook| {
|
|
for (&tasks) |*t| {
|
|
if (!t.kill_pending) continue;
|
|
if (t.state == .ready or t.state == .blocked) hook(t);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Called from the timer interrupt (interrupts already disabled): wake due
|
|
/// sleepers, finish pending kills, 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.
|
|
/// Called from the tick with the big kernel lock held — process.zig hangs the
|
|
/// one-shot timer sweep here (timer_bind), the same call-up pattern as the
|
|
/// teardown hooks below.
|
|
pub var timer_tick_hook: ?*const fn () void = null;
|
|
|
|
pub fn tick() void {
|
|
_ = sync.enter();
|
|
wakeExpired();
|
|
if (timer_tick_hook) |hook| hook();
|
|
reapKillPendingLocked();
|
|
if (preemption_enabled) schedule();
|
|
sync.leaveIsr();
|
|
}
|
|
|
|
/// Enable or disable timer-driven preemption (cooperative-only when off).
|
|
pub fn setPreemption(enabled: bool) void {
|
|
preemption_enabled = enabled;
|
|
}
|
|
|
|
/// End the current task and switch away for good; never returns. The task's stack
|
|
/// 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 {
|
|
_ = sync.enter();
|
|
const pc = thisCpu();
|
|
pc.current.state = .free;
|
|
pc.reap_after_switch = pc.current; // the task we switch to frees this stack (M8)
|
|
const next = dequeueHighest(pc) orelse @panic("sched: no task left to run");
|
|
next.state = .running;
|
|
pc.current = next;
|
|
var discard: usize = 0;
|
|
switchTo(pc, &discard, next);
|
|
unreachable;
|
|
}
|
|
|
|
/// End the current **user** task: free its address space, then exit. Runs on the
|
|
/// dying task's kernel stack (in the shared kernel half, so it survives the CR3
|
|
/// switch to the kernel tables that must happen before we free the process's own
|
|
/// tables — we can't free the page tables we're standing on). The kernel stack
|
|
/// itself is leaked, as in `exit` (no reaper yet). Never returns.
|
|
pub fn exitUser() noreturn {
|
|
_ = sync.enter();
|
|
exitUserLocked();
|
|
}
|
|
|
|
/// The body of `exitUser` for callers that already hold the big kernel lock (the
|
|
/// tick-time terminate path, which enters with the lock held). The lock is handed
|
|
/// off through the switch and released by the task that resumes. Never returns.
|
|
pub fn exitUserLocked() noreturn {
|
|
const pc = thisCpu();
|
|
const dying = pc.current;
|
|
const as = dying.aspace;
|
|
if (as != 0) {
|
|
const kroot = architecture.kernelPageTable();
|
|
architecture.loadPageTable(kroot); // off the process tables before freeing them
|
|
pc.loaded_aspace = kroot;
|
|
releaseAspace(as); // destroys only when this was the last task on the space
|
|
}
|
|
dying.state = .free;
|
|
dying.aspace = 0;
|
|
dying.kill_pending = false;
|
|
dying.in_system_call = false;
|
|
pc.reap_after_switch = dying; // the task we switch to frees this stack (M8)
|
|
const next = dequeueHighest(pc) orelse @panic("sched: no task left to run");
|
|
next.state = .running;
|
|
pc.current = next;
|
|
var discard: usize = 0;
|
|
switchTo(pc, &discard, next);
|
|
unreachable;
|
|
}
|
|
|
|
/// Free a task that is NOT running on any core (it is ready or blocked, and the
|
|
/// caller — the kill path — has already unlinked it from every queue and released
|
|
/// what it held). Destroys its address space: safe here because no core can have
|
|
/// it loaded (every switch away from a task loads the next task's tables, and the
|
|
/// task isn't running). The kernel stack is leaked, as in `exitUser` (no reaper
|
|
/// yet). Precondition: the big kernel lock is held.
|
|
pub fn destroyTaskLocked(t: *Task) void {
|
|
if (t.aspace != 0) releaseAspace(t.aspace); // destroys only on the last reference
|
|
reapStackLocked(t); // safe to free now: `t` is not running on any core (M8)
|
|
t.aspace = 0;
|
|
t.kill_pending = false;
|
|
t.in_system_call = false;
|
|
t.wake_at = 0;
|
|
t.state = .free;
|
|
}
|
|
|
|
/// Snapshot the task table into `out` (up to its length), returning the total
|
|
/// number of live tasks — the kernel half of `process_enumerate`, mirroring
|
|
/// devices_broker.enumerate. Kernel tasks are included (empty name, supervisor 0):
|
|
/// an honest `ps` shows the idle tasks too. `out` may be user memory: the caller's
|
|
/// address space is loaded during its system call, and the same bring-up trust
|
|
/// applies as for device_enumerate (an unmapped user page faults the kernel).
|
|
pub fn enumerate(out: []abi.ProcessDescriptor) u64 {
|
|
const flags = sync.enter();
|
|
defer sync.leave(flags);
|
|
var total: u64 = 0;
|
|
for (&tasks) |*t| {
|
|
if (t.state == .free) continue;
|
|
if (total < out.len) {
|
|
const d = &out[total];
|
|
d.* = .{
|
|
.id = t.id,
|
|
.supervisor = t.supervisor,
|
|
.state = @intFromEnum(@as(abi.ProcessState, switch (t.state) {
|
|
.ready => .ready,
|
|
.running => .running,
|
|
.blocked => .blocked,
|
|
.free => unreachable,
|
|
})),
|
|
.priority = t.priority,
|
|
.name_length = t.name_length,
|
|
.name = t.name_buffer,
|
|
};
|
|
}
|
|
total += 1;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
/// Whether the running task is a user process (has its own address space).
|
|
pub fn currentIsUserProcess() bool {
|
|
return current().aspace != 0;
|
|
}
|
|
|
|
pub fn currentId() u32 {
|
|
return current().id;
|
|
}
|
|
|
|
/// The dense index of the core this task is currently running on (0 = BSP). Reads
|
|
/// per-CPU state, so a task calling it on different cores sees different values —
|
|
/// which is how a test can prove work is running in parallel. Returns 0 if the GS
|
|
/// base isn't published yet (a fault in very early boot, before `init`), so a fault
|
|
/// reporter can call it unconditionally without a second fault.
|
|
pub fn currentCpuIndex() u32 {
|
|
if (architecture.cpuLocal() == 0) return 0;
|
|
return thisCpu().index;
|
|
}
|
|
|
|
/// The running task's id, or 0 if this core's scheduler isn't up yet (early boot, no GS
|
|
/// base). Safe for a fault reporter to call unconditionally — like `currentCpuIndex`,
|
|
/// it never dereferences an unpublished per-CPU pointer and so can't fault a second time.
|
|
pub fn currentIdSafe() u32 {
|
|
if (architecture.cpuLocal() == 0) return 0;
|
|
return thisCpu().current.id;
|
|
}
|
|
|
|
/// The running task's name (argv[0]), or "" if this core's scheduler isn't up yet.
|
|
/// The companion to `currentIdSafe` for naming the culprit in a fatal fault report.
|
|
pub fn currentNameSafe() []const u8 {
|
|
if (architecture.cpuLocal() == 0) return "";
|
|
return thisCpu().current.name();
|
|
}
|
|
|
|
/// Change the running task's priority (takes effect next time it's enqueued).
|
|
pub fn setPriority(p: Priority) void {
|
|
current().priority = p;
|
|
}
|