Files
Zag/src/kern/thread.zig
T
2026-10-10 09:37:20 -04:00

527 lines
15 KiB
Zig

//! Kernel thread management.
const rtl = @import("rtl");
const config = @import("config");
const r = @import("root");
const ke = r.ke;
const kep = ke.private;
const std = @import("std");
const assert = std.debug.assert;
pub const Priority = enum(u8) {
/// Reserved for the idle thread.
IdleThread = 0,
/// Background kernel work.
Idle = 1,
/// Low priority CPU-bound work
LowBatch = 2,
/// High priority CPU-bound work
HighBatch = 137,
/// Low priority interactive work.
LowInteractive = 138,
/// High priority interactive work.
HighInteractive = 223,
/// The default thread priority.
Default = 117,
/// Low priority real-time work.
LowRealtime = 224,
/// Mid priority real-time work.
MidRealtime = 239,
/// High priority real-time work.
HighRealtime = 255,
_,
pub const Class = enum(u8) {
Realtime,
Timeshare,
Idle,
};
pub const max = 255;
pub const nice_max = 20;
// The low and top 20 priorities from the batch range are reserved
// for nice.
pub const cpu_range = @backingInt(Priority.HighBatch) - (nice_max * 2) - 1;
pub fn class_from_prio(prio: u8) Class {
if (prio >= @backingInt(Priority.LowRealtime) and
prio <= @backingInt(Priority.HighRealtime))
return .Realtime;
if (prio >= @backingInt(Priority.LowBatch) and
prio < @backingInt(Priority.LowRealtime))
return .Timeshare;
if (prio <= @backingInt(Priority.Idle))
return .Idle;
unreachable;
}
};
/// Structure representing a kernel thread.
pub const Thread = struct {
/// Thread state.
pub const State = enum(u8) {
/// The thread is ready to run.
Ready,
/// The thread is currently running.
Running,
/// The thread was selected to run.
Selected,
/// The thread is currently sleeping.
Blocked,
/// The thread has exited and is waiting to be reaped.
Zombie,
/// The thread has exited.
Terminated,
};
pub const Context = extern struct {
/// Implementation-dependent context.
impl: kep.impl.ThreadContext,
stack_top: usize,
pub fn init_with_stack(
stack: r.VAddr,
) Context {
return .{
.impl = .init_with_stack(
stack,
),
.stack_top = stack,
};
}
pub fn reset(
self: *@This(),
stack_top: r.VAddr,
entry: *const fn (?*anyopaque) void,
arg: ?*anyopaque,
) void {
self.stack_top = stack_top;
_ = self.impl.reset(stack_top, entry, arg);
}
};
context: Context,
/// Thread lock.
lock: ke.SpinLock,
/// Niceness value.
nice: i8,
/// Effective priority value of the thread: `max(base, inherited)`.
priority: u8,
/// Base priority value of the thread.
base_priority: u8,
/// Priority inherited via priority donation.
inherited_prio: u8,
/// Whether the thread is pinned to this CPU.
/// If it is pinned, then it can't be moved across another CPU.
pinned: bool,
/// Current state of the thread.
state: std.atomic.Value(State),
/// Linkage into a scheduler run queue.
runq_link: rtl.List.Entry,
/// Last CPU this thread ran on.
last_cpu: ?u32,
/// CPU this thread is enqueued on.
cpu: ?u32,
/// Run queue this thread is in
runq: ?*kep.sched.RunQueue,
/// Index into the run queue this thread is currently in.
/// Only valid when `runq` is non-null.
runq_idx: u8,
/// Current wait status.
wait_status: std.atomic.Value(kep.wait.Status),
inner_waitblocks: [4]kep.wait.WaitBlock,
waitblocks: []kep.wait.WaitBlock,
/// Reason for the wait, if any.
wait_reason: ?[]const u8,
/// The number of objects the thread is waiting on.
wait_count: usize,
/// Timer used for timeouts.
timer: ke.Timer,
/// Whether or not a timeout was set.
has_timeout: bool,
/// Timeout waitblock index.
timeout_block: u8,
/// Turnstile.
turnstile: *kep.turnstile.Turnstile,
turnstile_waiter: ?*kep.turnstile.Waiter,
turnstiles_owned: rtl.List,
/// Object this thread is currently blocked on, or null.
waiting_on: ?*anyopaque,
/// Queue this thread is associated with.
queue: ?*ke.Queue,
queue_item: ?*rtl.List.Entry,
/// PELT load average,
avg: kep.sched.Average,
/// Accounting statistics.
acct: kep.sched.Accounting,
/// Set whenever the thread is switching off its stack.
/// This is used to avoid taking thread next lock to wait for switch off
/// to complete.
switching: std.atomic.Value(bool),
smr_sections: rtl.List,
hard_affinity: ke.CpuMask,
continuation: ?Continuation,
warden_data: kep.warden.HolderData,
const InitOpts = struct {
/// Entry point of the thread.
entry: ke.Continuation,
/// Top of the stack for the thread.
stack: r.VAddr,
/// Turnstile associated with the thread.
turnstile: *kep.turnstile.Turnstile,
/// Thread's priority.
priority: Priority,
};
/// Initialize a thread.
pub fn init(
thread: *Thread,
opts: InitOpts,
) void {
thread.* = .{
.context = .init_with_stack(opts.stack),
.lock = ke.SpinLock.init("thread"),
.nice = 0,
.priority = @backingInt(opts.priority),
.base_priority = @backingInt(opts.priority),
.inherited_prio = 0,
.pinned = false,
.state = std.atomic.Value(State).init(.Ready),
.runq_link = .{},
.last_cpu = null,
.cpu = null,
.runq = null,
.runq_idx = 0,
.wait_status = std.atomic.Value(kep.wait.Status).init(.Satisfied),
.inner_waitblocks = undefined,
.waitblocks = &thread.inner_waitblocks,
.wait_reason = null,
.timer = undefined,
.has_timeout = false,
.timeout_block = 0,
.wait_count = 0,
.turnstile = opts.turnstile,
.turnstile_waiter = null,
.turnstiles_owned = undefined,
.waiting_on = null,
.queue = null,
.queue_item = null,
.continuation = opts.entry,
.switching = std.atomic.Value(bool).init(false),
.avg = .{},
.acct = .{},
.smr_sections = undefined,
.hard_affinity = ke.CpuMask.init(true),
.warden_data = .{},
};
thread.turnstiles_owned.init();
thread.timer.init();
thread.smr_sections.init();
if (opts.stack != 0) {
thread.context.reset(
opts.stack,
call_continuation,
thread,
);
}
}
pub fn priority_class(self: *Thread) Priority.Class {
return Priority.class_from_prio(self.priority);
}
/// Class of the thread's *base* priority, ignoring any inherited boost.
pub fn base_priority_class(self: *Thread) Priority.Class {
return Priority.class_from_prio(self.base_priority);
}
/// The priority the thread should run at.
pub fn effective_priority(self: *Thread) u8 {
return @max(self.base_priority, self.inherited_prio);
}
pub fn is_interactive(self: *Thread) bool {
return self.priority >= @backingInt(Priority.LowInteractive) and
self.priority <= @backingInt(Priority.HighInteractive);
}
pub fn can_relinquish_stack(self: *Thread) bool {
const state = self.state.load(.monotonic);
// Stack donation for realtime threads is disabled, unless they are
// exiting.
return self.base_priority_class() != .Realtime or
state == .Zombie or
state == .Terminated;
}
pub fn can_receive_stack(self: *Thread) bool {
// Realtime threads shouldn't donate their stack, so they can't receive
// them.
return self.base_priority_class() != .Realtime;
}
};
/// HandoffList of threads waiting to be reaped.
/// This is managed by the process subsystem.
pub var reaper_list: rtl.HandoffList = undefined;
/// Terminate the currently running thread.
/// This does not return.
pub fn exit() void {
_ = ke.ipl.raise(.Dispatch);
const curtd = current();
curtd.lock.acquire_no_ipl();
curtd.state.store(.Zombie, .monotonic);
if (curtd.queue) |q| {
curtd.queue = null;
kep.queue.signal_wait(q);
}
if (config.warden) {
// There is normally a dispatch object -> thread ordering, but here we
// have a thread -> dispatch object ordering because we insert into a
// kernel queue (ex.enqueue is called as an activation to the reaper
// list) while holding the current thread's lock. This would normally be
// a deadlock, and the warden rightfully flags it; but this is safe
// because the thread that's currently exiting won't ever block or get
// woken up by this queue. Get sneaky under the warden because it can't
// understand this.
kep.warden.released(&curtd.lock, .Spin);
}
// Reuse the runq linkage to put on reaper list.
reaper_list.insert(@ptrCast(&curtd.runq_link.next));
if (config.warden) {
// Now we're good.
kep.warden.acquired(&curtd.lock, curtd.lock.wd, .Spin);
}
kep.sched.detach_load_avg(kep.sched.percpu.local(), curtd);
kep.sched.yield_locked(Continuation.dummy);
}
/// Return the currently running thread.
pub fn current() *ke.Thread {
return kep.sched.percpu.local().current_thread orelse unreachable;
}
// -- Stack allocation and continuations ---------------------------------------
pub const Continuation = struct {
func: *const fn (_: ?*anyopaque) void,
arg: ?*anyopaque,
pub const dummy: Continuation = .{
.func = dummy_fn,
.arg = null,
};
fn dummy_fn(_: ?*anyopaque) void {}
};
const Depot = struct {
stacks: ?*Stack = null,
count: u32 = 0,
min: u32 = 0,
wma: u32 = 0,
};
pub const Stack = struct {
next: ?*Stack,
prev: ?*Stack,
};
const Cpu = struct {
stack_alloc_dpc: ke.Dpc,
depot: Depot,
};
/// Number of excess stacks allowed in the global stack depot before trimming.
/// Note that this is in addition to the global minimum of `ncpus` stacks.
/// Keep this low for minimal memory overhead, but potentially
/// increased latency.
const excess_stacks = ke.TunableType(u32, 1, "ke.thread.excess_stacks");
var global_depot_lock: ke.SpinLock = undefined;
var global_depot: Depot = .{};
const percpu = ke.CpuLocalType(Cpu, undefined);
const percpu_count_max = 2;
const wma_unit = 256;
pub var thread_stack_queue = rtl.HandoffList.init(stack_activation);
inline fn wma_mix(old: u32, new: u32) u32 {
// Keep 75% of old sample and 25% of new.
return (3 * old + new * wma_unit) / 4;
}
fn init_cpu() linksection(r.init) void {
const cpu = percpu.local();
cpu.* = .{
.stack_alloc_dpc = ke.Dpc.init(stack_alloc_handler),
.depot = .{},
};
}
comptime {
_ = r.percpu_init_set.insert(&init_cpu);
}
pub fn init() void {
global_depot_lock = ke.SpinLock.init("stack_cache");
}
pub fn call_continuation(ptr: ?*anyopaque) noreturn {
const td: *ke.Thread = @ptrCast(@alignCast(ptr));
assert(td.continuation != null);
// Clean things that were set up by the wait (e.g timeout).
_ = kep.wait.post_wait(td) catch {
// Don't care about the status.
};
const cont = td.continuation orelse unreachable;
td.continuation = null;
kep.impl.call_continuation(&td.context, cont);
}
/// Try to get a stack from the stack cache.
pub fn stack_cache_pop() ?usize {
const ipl = ke.ipl.raise(.Dispatch);
defer ke.ipl.lower(ipl);
const cpu = &percpu.local().depot;
// Try to get a stack from the per-cpu depot.
if (cpu.count != 0) {
const entry = cpu.stacks.?;
cpu.stacks = entry.next;
cpu.count -= 1;
return @intFromPtr(entry) + @sizeOf(ke.Stack);
}
// Otherwise, try and get it from the global depot.
global_depot_lock.acquire_no_ipl();
defer global_depot_lock.release_no_ipl();
if (global_depot.count != 0) {
const entry = global_depot.stacks.?;
global_depot.stacks = entry.next;
global_depot.count -= 1;
global_depot.min = @min(global_depot.min, global_depot.count);
return @intFromPtr(entry) + @sizeOf(ke.Stack);
}
return null;
}
/// Free a stack back to the depot.
pub fn stack_cache_free(stack: usize) void {
const entry: *Stack = @ptrFromInt(stack - @sizeOf(Stack));
const ipl = ke.ipl.raise(.Dispatch);
defer ke.ipl.lower(ipl);
const cpu = &percpu.local().depot;
if (cpu.count < percpu_count_max) {
entry.next = cpu.stacks;
cpu.stacks = entry;
cpu.count += 1;
return;
}
global_depot_lock.acquire_no_ipl();
defer global_depot_lock.release_no_ipl();
entry.next = global_depot.stacks;
global_depot.stacks = entry;
global_depot.count += 1;
}
/// Periodic updates to the stack depot.
/// Keeps track of the number of alloc / free that happened
/// in the last time span and reaps the excess stacks, if necessary.
pub fn stack_cache_update() ?*Stack {
const ipl = global_depot_lock.acquire();
defer global_depot_lock.release(ipl);
global_depot.wma = wma_mix(global_depot.wma, global_depot.min);
var excess = @min(
global_depot.min * wma_unit,
global_depot.wma,
) / wma_unit;
global_depot.min = global_depot.count;
// If we have more than `excess_stacks` sitting unused, then trim.
// Ensure there are still `ncpus` stacks sitting in the depot.
if (excess > excess_stacks.load()) {
assert(global_depot.count >= excess);
// We want every CPU to have at least one stack sitting in the depot.
const floor: u32 = @truncate(ke.ncpus);
excess = @min(excess, global_depot.count -| floor);
const new_count = global_depot.count - excess;
global_depot.min = new_count;
if (excess == 0) return null;
// Splice the list from the tail to get the coldest stacks.
var prev = global_depot.stacks.?;
for (1..new_count) |_| prev = prev.next.?;
const head = prev.next;
prev.next = null;
global_depot.count = new_count;
global_depot.wma -= excess * wma_unit;
return head;
}
return null;
}
pub fn stacks_count() usize {
var ret: usize = 0;
for (0..ke.ncpus) |i| {
const cpu = percpu.remote(@truncate(i));
ret += @atomicLoad(u32, &cpu.depot.count, .monotonic);
}
const ipl = global_depot_lock.acquire();
ret += global_depot.count;
global_depot_lock.release(ipl);
return ret;
}
fn stack_activation(_: ?*rtl.HandoffList) void {
const cpu = percpu.local();
ke.dpc.enqueue(&cpu.stack_alloc_dpc, null);
}
fn stack_alloc_handler(_: *ke.Dpc, _: ?*anyopaque) void {
r.ps.private.thread.stack_activation();
}