//! 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) // `reaping` = the task has exited and is queued on its core's reap list; its slot must not // be reused (freeSlot skips it) until the reaper has freed its kernel stack and set it // `free` (docs/threading-plan.md M8/M9). const State = enum { free, ready, running, blocked, reaping }; 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, // Id of this task's process leader — the main task's own id, copied to every // thread it (transitively) spawns; 0 for kernel tasks and never followed. // Equal `leader` is what makes two tasks one process; the leader's id is the // id `system_spawn` returned, so it is the process id the supervisor speaks // (docs/shared-fate-plan.md). leader: 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 (or a group fan-out) on a task that is running on // another core; the kernel finishes the kill at that task's next system call // or timer tick. Atomic because the syscall-entry check reads it without the // lock while another core writes it under the lock — monotonic is enough: a // missed read is caught at the next delivery point (x86-TSO or not). kill_pending: std.atomic.Value(bool) = .init(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. address_space: 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 first argument register at first entry // (rdi on x86_64, via architecture.jumpToUserArg): 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 task id this task is blocked in `thread_join` on (0 = not joining). Woken by // `wakeJoinersLocked` when that task exits (docs/threading-plan.md M9). join_target: u32 = 0, // This task's user-space TLS thread pointer — 0 until set via `set_thread_pointer`. // Architecture-neutral: the arch layer maps it to the FS base on x86_64, `TPIDR_EL0` on // aarch64. Restored on every context switch to this task (docs/threading-plan.md M10). thread_pointer: u64 = 0, // The mmap / MMIO grant-arena cursors moved from Task to the per-address-space object // (`AddressSpaceRef`, below) so threads sharing one address space hand out disjoint grants // — see addressSpaceMmapNextPtr / addressSpaceDeviceMapNextPtr (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 (virtual_address in this task's address space) ipc_send_len: u64 = 0, ipc_reply_ptr: u64 = 0, // client: reply buffer (virtual_address) ipc_reply_cap: u64 = 0, ipc_status: i64 = 0, // client: reply length / -errno, written by the replier // The DMA and shared-memory arena cursors moved to the per-address-space object // (`AddressSpaceRef`) like the mmap/MMIO cursors before them — per-TASK cursors made // sibling threads hand out overlapping windows of the one shared arena // (docs/shared-fate-plan.md M4 tripped exactly that). 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. // Raised from 16 with DMA-region capabilities: a driver now holds its per-device // channel endpoints plus received DMA-region handles (a storage driver forwards several // buffer caps), and repeated cap-passing consumes slots until handle_close. pub const ipc_maximum_handles = 32; /// 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 AddressSpaceRef = struct { root: u64 = 0, count: u32 = 0, mmap_next: u64 = 0, device_map_next: u64 = 0, dma_next: u64 = 0, // bump pointer into this space's DMA arena (0 = unseeded) shared_memory_next: u64 = 0, // bump pointer into this space's shared-memory arena (0 = unseeded) // Group-death state (docs/shared-fate-plan.md), set once by the first kill // trigger and never cleared while the entry lives. `dying` gates // retainAddressSpace — no new member may join a dying group (closing the // thread_spawn escape) — and the stash is what `group_exit_hook` posts when // the last reference drops: the leader's identity, the group reason, and the // leader's counted exit-endpoint reference (moved off its Task under the // same lock hold that set the latch). dying: bool = false, group_leader: u32 = 0, group_supervisor: u32 = 0, group_reason: abi.ExitReason = .exited, group_exit_endpoint: ?*anyopaque = null, // Shared-memory objects mapped into this space (docs/shared-fate-plan.md M3). // Each mapping holds one reference to its object, dropped through // `space_mapping_release_hook` when the space is destroyed — so "last // reference" means no handles AND no mappings, and frames can never be freed // while a live space still maps them. Opaque: the object type is the IPC // layer's. mappings: [maximum_space_mappings]?*anyopaque = .{null} ** maximum_space_mappings, }; /// Shared-memory mappings one address space can hold — matches the per-task /// handle table's order of magnitude; `shared_memory_map` fails when full. const maximum_space_mappings = 16; var address_space_refs = [_]AddressSpaceRef{.{}} ** maximum_tasks; var address_space_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; } /// Free every `.reaping` task queued on this core's reap list and mark each `.free` (now /// its slot may be reused). The tasks are all off their stacks (they switched away), and /// the caller holds the lock, so freeing is safe (docs/threading-plan.md M8/M9). fn drainReapListLocked(pc: *PerCpu) void { var node = pc.reap_list; pc.reap_list = null; while (node) |t| { node = t.next; // save the link before we clear it t.next = null; reapStackLocked(t); t.state = .free; // reusable only now, after the stack is freed } } /// 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 retainAddressSpace(root: u64) bool { if (root == 0) return true; var free: ?*AddressSpaceRef = null; for (&address_space_refs) |*entry| { if (entry.count != 0 and entry.root == root) { // A dying group admits no new member: a thread_spawn that was already // past its syscall-entry kill check when the group was condemned // fails here, under the same lock that would have created the task // (docs/shared-fate-plan.md). if (entry.dying) return false; 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 releaseAddressSpace(root: u64) void { if (root == 0) return; for (&address_space_refs) |*entry| { if (entry.count == 0 or entry.root != root) continue; entry.count -= 1; if (entry.count == 0) { // Copy the group-death stash out, then fully reset the slot before // the hook runs — a stale stash must never survive into an unrelated // process's reused entry (docs/shared-fate-plan.md). const was_dying = entry.dying; const leader = entry.group_leader; const supervisor = entry.group_supervisor; const reason = entry.group_reason; const endpoint = entry.group_exit_endpoint; const mappings = entry.mappings; entry.* = .{}; architecture.destroyAddressSpace(root); address_space_destroy_count += 1; // Release the mapping references now that no mapping exists — // `device_grant`-tagged leaves kept destroyAddressSpace's sweep off // the frames, so this drop is what may actually free them (M3). if (space_mapping_release_hook) |release| { for (mappings) |slot| if (slot) |object| release(object); } // The group-death moment: the space is gone, every member is dead. // process.zig posts the leader's deferred exit publication here — // last, so the supervisor's notification postdates every release. if (was_dying) if (group_exit_hook) |hook| hook(leader, supervisor, reason, endpoint); } return; } // Never retained (a hand-built test space): destroyed directly, out of the // group-death hook's scope, preserving the pre-refcount behaviour. architecture.destroyAddressSpace(root); address_space_destroy_count += 1; } /// Called (lock held) at the group-death moment — the last reference to a dying /// group's address space dropped and the space was destroyed. Registered by /// process.zig, which posts the leader's deferred exit notification and re-stamps /// its exit record (docs/shared-fate-plan.md). Runs under the lock at both /// release sites (`exitUserLocked`, `destroyTaskLocked`). pub var group_exit_hook: ?*const fn (leader: u32, supervisor: u32, reason: abi.ExitReason, exit_endpoint: ?*anyopaque) void = null; /// Latch `root`'s group as dying and stash the group-death payload for the hook. /// Returns false — changing nothing — if the group is already dying (a concurrent /// trigger lost the race) or `root` has no live entry. Caller holds the lock. pub fn markGroupDyingLocked(root: u64, leader: u32, supervisor: u32, reason: abi.ExitReason, exit_endpoint: ?*anyopaque) bool { for (&address_space_refs) |*entry| { if (entry.count == 0 or entry.root != root) continue; if (entry.dying) return false; entry.dying = true; entry.group_leader = leader; entry.group_supervisor = supervisor; entry.group_reason = reason; entry.group_exit_endpoint = exit_endpoint; return true; } return false; } /// Called (lock held) once per recorded shared-memory mapping when an address /// space is destroyed — drops the mapping's object reference. Registered by /// process.zig (the object type lives in the IPC layer). pub var space_mapping_release_hook: ?*const fn (*anyopaque) void = null; /// Record a shared-memory mapping on `root`'s space; its reference is dropped /// via `space_mapping_release_hook` at space destruction. Returns false — /// recording nothing — if the space has no live entry or its mapping table is /// full. On true, the caller has transferred one object reference to the space. /// Caller holds the lock. pub fn recordSpaceMappingLocked(root: u64, object: *anyopaque) bool { for (&address_space_refs) |*entry| { if (entry.count == 0 or entry.root != root) continue; for (&entry.mappings) |*slot| { if (slot.* == null) { slot.* = object; return true; } } return false; } return false; } /// Whether `root`'s group is already dying. Caller holds the lock. pub fn groupDyingLocked(root: u64) bool { for (&address_space_refs) |*entry| { if (entry.count != 0 and entry.root == root) return entry.dying; } return false; } /// The stashed supervisor of `root`'s DYING group, or null if the group is not /// dying. Answers the process_kill authority question during the condemned /// window, when the leader's task slot may already be reaped. Lock held. pub fn groupSupervisorLocked(root: u64) ?u32 { for (&address_space_refs) |*entry| { if (entry.count != 0 and entry.root == root) { return if (entry.dying) entry.group_supervisor else null; } } return null; } /// Undo a `recordSpaceMappingLocked` — the rollback half for a caller whose /// later step failed. Removes one matching slot; the caller drops the reference /// it had transferred. Lock held. pub fn removeSpaceMappingLocked(root: u64, object: *anyopaque) void { for (&address_space_refs) |*entry| { if (entry.count == 0 or entry.root != root) continue; for (&entry.mappings) |*slot| { if (slot.* == object) { slot.* = null; return; } } return; } } /// The whole static task pool, for process.zig's group fan-out — which must scan /// members under the lock it already holds. Slots may be `.free`/`.reaping`; /// callers filter by state and must not hold pointers past the lock. pub fn allTasksLocked() []Task { return tasks[0..]; } /// Test-observable: how many address spaces are live (entries with a nonzero count). pub fn liveAddressSpaceCount() u32 { var live: u32 = 0; for (&address_space_refs) |*entry| { if (entry.count != 0) live += 1; } return live; } /// Test-observable: total address-space destructions since boot. pub fn addressSpaceDestroyCount() u64 { return address_space_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 addressSpaceMmapNextPtr(root: u64) ?*u64 { for (&address_space_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 /// `addressSpaceMmapNextPtr`). Caller holds the kernel lock. pub fn addressSpaceDeviceMapNextPtr(root: u64) ?*u64 { for (&address_space_refs) |*entry| { if (entry.count != 0 and entry.root == root) return &entry.device_map_next; } return null; } /// Pointer to the DMA arena cursor for address space `root` (see /// `addressSpaceMmapNextPtr`). Caller holds the kernel lock. pub fn addressSpaceDmaNextPtr(root: u64) ?*u64 { for (&address_space_refs) |*entry| { if (entry.count != 0 and entry.root == root) return &entry.dma_next; } return null; } /// Pointer to the shared-memory arena cursor for address space `root` (see /// `addressSpaceMmapNextPtr`). Caller holds the kernel lock. pub fn addressSpaceSharedMemoryNextPtr(root: u64) ?*u64 { for (&address_space_refs) |*entry| { if (entry.count != 0 and entry.root == root) return &entry.shared_memory_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_address_space: u64 = 0, // the address-space root currently loaded on this core loaded_thread_pointer: u64 = 0, // the TLS thread pointer currently loaded on this core (docs/threading-plan.md M10) // 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, // Tasks that ended while running on THIS core: they could not free the kernel stack // they were standing on, so each pushed itself onto this list (`.reaping` state, linked // via `Task.next`) and switched away. The next task to run on this core — or the timer // tick — frees their stacks from its own stack, safely (docs/threading-plan.md M8). A // *list* (not one slot) so a second death before the first is drained can't lose it. reap_list: ?*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_address_space = 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_address_space = 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 (`address_space`) 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. `leader` is /// the process leader's id for a thread, or 0 to make the new task its own /// leader (a process spawn). /// 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 `address_space` holds it /// across the whole spawn, so the address space and the task appear atomically). pub fn spawnUserLocked(address_space: u64, entry: u64, user_sp: u64, user_arg: u64, priority: Priority, task_name: []const u8, supervisor: u32, exit_endpoint: ?*anyopaque, leader: u32) ?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 `address_space`). if (!retainAddressSpace(address_space)) { 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, .address_space = address_space, .user_ip = entry, .user_sp = user_sp, .user_arg = user_arg, .supervisor = supervisor, .exit_endpoint = exit_endpoint, .leader = if (leader == 0) next_id else leader, }; 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 (address_space == 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.address_space != 0) next.address_space else architecture.kernelPageTable(); if (want != pc.loaded_address_space) { architecture.loadPageTable(want); pc.loaded_address_space = want; } // Restore the next task's user TLS thread pointer — only on change, the same // conditional-load discipline as CR3 above (docs/threading-plan.md M10). if (next.thread_pointer != pc.loaded_thread_pointer) { architecture.setThreadPointer(next.thread_pointer); pc.loaded_thread_pointer = next.thread_pointer; } 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). drainReapListLocked(thisCpu()); } /// 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 /// `address_space`. Precondition: the big kernel lock is held. Returns how many woke. pub fn futexWakeLocked(address_space: u64, addr: u64, count: u32) u32 { var woken: u32 = 0; for (&tasks) |*t| { if (woken >= count) break; if (t.state == .blocked and t.address_space == address_space 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; } // --- thread join (docs/threading-plan.md M9) -------------------------------- // // join needs no per-thread IPC endpoint: `thread_join(tid)` blocks the caller until the // task with id `tid` has exited, and the exit paths wake any joiner. The caller only ever // reclaims the joined thread's *user* stack (which the thread vacated the moment it entered // the kernel to exit), so waking at exit time — not reap time — is safe. /// True if a task with id `tid` is still live (has not exited). Caller holds the lock. fn aliveTid(tid: u32) bool { for (&tasks) |*t| { if (t.id == tid and t.state != .free and t.state != .reaping) return true; } return false; } /// Block the current task until the task with id `tid` exits (or return at once if it /// already has / never existed). **Precondition:** the big kernel lock is held; returns /// with it still held. Woken by `wakeJoinersLocked`. pub fn joinThreadLocked(tid: u32) void { while (aliveTid(tid)) { const t = current(); t.join_target = tid; t.state = .blocked; schedule(); // woken when the joined task exits; lock handed off across the switch t.join_target = 0; } } /// Set the calling task's user TLS thread pointer and load it now. Persisted on the Task so /// context switches restore it (docs/threading-plan.md M10). Caller holds the kernel lock. pub fn setThreadPointerLocked(addr: u64) void { const pc = thisCpu(); pc.current.thread_pointer = addr; architecture.setThreadPointer(addr); pc.loaded_thread_pointer = addr; } /// Wake every task blocked in `thread_join` on `tid` — called from the exit paths once the /// exiting task's state is `.free`. Caller holds the lock. fn wakeJoinersLocked(tid: u32) void { for (&tasks) |*t| { if (t.state == .blocked and t.join_target == tid) { t.join_target = 0; t.state = .ready; enqueue(t); } } } // --- 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.state != .reaping 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.state != .reaping 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: if a dying task switched to a *fresh* task (which enters via // task_trampoline, not switchTo's tail), its stack is still queued here. The dying // task switched away before this tick, so it is off its stack — drain now (M8). drainReapListLocked(pc); if (cur.kill_pending.load(.monotonic) and cur.address_space != 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.load(.monotonic)) 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 goes on /// its core's reap list; the tick-time reaper frees the stack and recycles the /// slot. 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(); // Queue this task for reaping: `.reaping` keeps its slot out of freeSlot until its // stack is freed; `next` links it on the core's reap list (M8/M9). pc.current.state = .reaping; pc.current.next = pc.reap_list; pc.reap_list = pc.current; 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 reaped later, as in `exit`. 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.address_space; if (as != 0) { const kroot = architecture.kernelPageTable(); architecture.loadPageTable(kroot); // off the process tables before freeing them pc.loaded_address_space = kroot; releaseAddressSpace(as); // destroys only when this was the last task on the space } dying.state = .reaping; // dead but its slot stays reserved until the stack is freed wakeJoinersLocked(dying.id); // let any thread_join(dying.id) return (M9) dying.address_space = 0; dying.kill_pending.store(false, .monotonic); dying.in_system_call = false; // Queue for reaping: the task we switch to (or the next tick) frees this stack (M8/M9). dying.next = pc.reap_list; pc.reap_list = dying; 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.address_space != 0) releaseAddressSpace(t.address_space); // destroys only on the last reference reapStackLocked(t); // safe to free now: `t` is not running on any core (M8) t.address_space = 0; t.kill_pending.store(false, .monotonic); t.in_system_call = false; t.wake_at = 0; t.state = .free; wakeJoinersLocked(t.id); // a killed thread's joiners must return too (M9) } /// 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` is always KERNEL memory: the /// system call bounces it out to the caller through the checked copy layer /// (system/kernel/user-memory.zig), so a bad user pointer fails the call instead /// of faulting ring 0. pub fn enumerate(out: []abi.ProcessDescriptor) u64 { var total: u64 = 0; var cursor: usize = 0; while (true) { // Past the buffer, keep walking with an empty chunk: the total is the // whole live count, however few descriptors the caller had room for. const room = if (total < out.len) out[@intCast(total)..] else out[out.len..]; const chunk = enumerateFrom(&cursor, room); total += chunk.live; if (chunk.done) return total; } } /// What one chunk of the task-table walk found. pub const TaskChunk = struct { /// Live tasks passed in this chunk, whether or not they fit in `out` — this /// is what the running total (and hence `process_enumerate`'s result) counts. live: usize, /// How many of those were written into `out` (`@min(live, out.len)`). filled: usize, /// The cursor reached the end of the table: this was the last chunk. done: bool, }; /// One chunk of the task table: starting at slot `cursor` (advanced past /// everything scanned), describe up to `out.len` live tasks into `out` — or, with /// an empty `out`, just count the rest. `cursor == tasks.len` ends the walk. /// /// The chunked form exists so `process_enumerate` can stage each chunk in a small /// kernel buffer and copy it out with `user_memory.copyToUser`, rather than /// handing a user pointer to the kernel's own stores. A *slot* cursor, rather /// than a "skip the first N live tasks" count, keeps chunks from duplicating or /// losing an entry when a task exits between them. pub fn enumerateFrom(cursor: *usize, out: []abi.ProcessDescriptor) TaskChunk { const flags = sync.enter(); defer sync.leave(flags); var live: usize = 0; var filled: usize = 0; const limit = if (out.len == 0) tasks.len else out.len; // always makes progress while (cursor.* < tasks.len and live < limit) { const t = &tasks[cursor.*]; cursor.* += 1; if (t.state == .free or t.state == .reaping) continue; // reaping = already exited live += 1; if (filled == out.len) continue; out[filled] = .{ .id = t.id, .supervisor = t.supervisor, .leader = t.leader, .state = @intFromEnum(@as(abi.ProcessState, switch (t.state) { .ready => .ready, .running => .running, .blocked => .blocked, .free, .reaping => unreachable, })), .priority = t.priority, .name_length = t.name_length, .name = t.name_buffer, }; filled += 1; } return .{ .live = live, .filled = filled, .done = cursor.* >= tasks.len }; } /// Whether the running task is a user process (has its own address space). pub fn currentIsUserProcess() bool { return current().address_space != 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; }