mirror of
https://github.com/rdmsr/zag.git
synced 2026-10-27 13:58:20 +00:00
527 lines
15 KiB
Zig
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();
|
|
}
|