|
|
|
@@ -31,7 +31,10 @@ 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 };
|
|
|
|
|
// `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,
|
|
|
|
@@ -83,6 +86,9 @@ pub const Task = struct {
|
|
|
|
|
// 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,
|
|
|
|
|
// 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).
|
|
|
|
@@ -174,6 +180,20 @@ fn reapStackLocked(t: *Task) void {
|
|
|
|
|
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.
|
|
|
|
@@ -268,12 +288,12 @@ pub const PerCpu = struct {
|
|
|
|
|
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,
|
|
|
|
|
// 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;
|
|
|
|
@@ -555,11 +575,7 @@ fn switchTo(pc: *PerCpu, save_sp: *usize, next: *Task) void {
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
drainReapListLocked(thisCpu());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Voluntarily give up the CPU to the next ready task.
|
|
|
|
@@ -623,6 +639,46 @@ pub fn futexWakeLocked(aspace: u64, addr: u64, count: u32) u32 {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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
|
|
|
|
@@ -724,7 +780,7 @@ fn removeFrom(head: *[number_priorities]?*Task, tail: *[number_priorities]?*Task
|
|
|
|
|
/// 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;
|
|
|
|
|
if (t.state != .free and t.state != .reaping and t.id == id) return t;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
@@ -734,7 +790,7 @@ pub fn taskByIdLocked(id: u32) ?*Task {
|
|
|
|
|
/// 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;
|
|
|
|
|
if (other.state != .free and other.state != .reaping and other.ipc_client == t) other.ipc_client = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -826,14 +882,10 @@ pub var reap_task_hook: ?*const fn (*Task) void = null;
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
// 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 and cur.aspace != 0 and !cur.in_system_call) {
|
|
|
|
|
if (terminate_current_hook) |hook| hook(); // noreturn
|
|
|
|
|
}
|
|
|
|
@@ -875,8 +927,11 @@ pub fn setPreemption(enabled: bool) void {
|
|
|
|
|
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)
|
|
|
|
|
// 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;
|
|
|
|
@@ -908,11 +963,14 @@ pub fn exitUserLocked() noreturn {
|
|
|
|
|
pc.loaded_aspace = kroot;
|
|
|
|
|
releaseAspace(as); // destroys only when this was the last task on the space
|
|
|
|
|
}
|
|
|
|
|
dying.state = .free;
|
|
|
|
|
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.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)
|
|
|
|
|
// 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;
|
|
|
|
@@ -935,6 +993,7 @@ pub fn destroyTaskLocked(t: *Task) void {
|
|
|
|
|
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
|
|
|
|
@@ -948,7 +1007,7 @@ pub fn enumerate(out: []abi.ProcessDescriptor) u64 {
|
|
|
|
|
defer sync.leave(flags);
|
|
|
|
|
var total: u64 = 0;
|
|
|
|
|
for (&tasks) |*t| {
|
|
|
|
|
if (t.state == .free) continue;
|
|
|
|
|
if (t.state == .free or t.state == .reaping) continue; // reaping = already exited
|
|
|
|
|
if (total < out.len) {
|
|
|
|
|
const d = &out[total];
|
|
|
|
|
d.* = .{
|
|
|
|
@@ -958,7 +1017,7 @@ pub fn enumerate(out: []abi.ProcessDescriptor) u64 {
|
|
|
|
|
.ready => .ready,
|
|
|
|
|
.running => .running,
|
|
|
|
|
.blocked => .blocked,
|
|
|
|
|
.free => unreachable,
|
|
|
|
|
.free, .reaping => unreachable,
|
|
|
|
|
})),
|
|
|
|
|
.priority = t.priority,
|
|
|
|
|
.name_length = t.name_length,
|
|
|
|
|