feat: add threading to Scheduler, fix desktop background selection freeze issue

This commit is contained in:
2026-05-23 16:08:25 +02:00
parent a44a65d432
commit cd159235ca
15 changed files with 895 additions and 117 deletions
+21
View File
@@ -98,6 +98,8 @@ namespace Montauk {
for (int i = 0; i < Sched::MaxProcesses && count < maxCount; i++) {
auto* proc = Sched::GetProcessSlot(i);
if (!proc || proc->state == Sched::ProcessState::Free) continue;
// Skip sibling-thread slots; only show one entry per process.
if (proc->primarySlot != i) continue;
buf[count].pid = (int32_t)proc->pid;
buf[count].parentPid = (int32_t)proc->parentPid;
@@ -154,6 +156,25 @@ namespace Montauk {
return i;
}
// ====================================================================
// Threading
// ====================================================================
static int Sys_ThreadSpawn(uint64_t entry, uint64_t arg, uint64_t userStackTop) {
return Sched::SpawnThread(entry, arg, userStackTop);
}
[[noreturn]] static void Sys_ThreadExit(int exitCode) {
Sched::ExitCurrentThread(exitCode);
}
static int Sys_ThreadJoin(int tid, int* outExitCode) {
return Sched::JoinThread(tid, outExitCode);
}
static int Sys_ThreadSelf() {
return Sched::GetCurrentTid();
}
static int Sys_Chdir(const char* path) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr || path == nullptr) return -1;
+10
View File
@@ -340,6 +340,16 @@ namespace Montauk {
return Sys_AudioList((AudioStreamInfo*)frame->arg1, (int)frame->arg2);
case SYS_AUDIOWAIT:
return Sys_AudioWait(frame->arg1, frame->arg2);
case SYS_THREAD_SPAWN:
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
case SYS_THREAD_EXIT:
Sys_ThreadExit((int)frame->arg1);
return 0;
case SYS_THREAD_JOIN:
if (frame->arg2 != 0 && !UserMemory::Writable<int>(frame->arg2)) return -1;
return Sys_ThreadJoin((int)frame->arg1, (int*)frame->arg2);
case SYS_THREAD_SELF:
return Sys_ThreadSelf();
case SYS_BTSCAN:
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtScanResult), true)) return -1;
+6
View File
@@ -243,6 +243,12 @@ namespace Montauk {
static constexpr uint64_t SYS_AUDIOLIST = 128;
static constexpr uint64_t SYS_AUDIOWAIT = 129;
/* Process.hpp -- threading */
static constexpr uint64_t SYS_THREAD_SPAWN = 130;
static constexpr uint64_t SYS_THREAD_EXIT = 131;
static constexpr uint64_t SYS_THREAD_JOIN = 132;
static constexpr uint64_t SYS_THREAD_SELF = 133;
static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024;
static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0;
+7
View File
@@ -74,6 +74,7 @@ SyscallEntry:
; JumpToUserMode -- initial transition to ring 3 via IRETQ
; RDI = user RIP (entry point)
; RSI = user RSP (top of user stack)
; RDX = user RDI (first SystemV arg, e.g. thread arg; 0 for new processes)
; ====================================================================
global JumpToUserMode
JumpToUserMode:
@@ -86,5 +87,11 @@ JumpToUserMode:
push 0x202 ; RFLAGS (IF=1)
push 0x23 ; CS = UserCode | RPL3
push rdi ; RIP = entry point
mov rdi, rdx ; SystemV arg #1 -> user RDI
xor rsi, rsi ; zero argv-ish registers for hygiene
xor rdx, rdx
xor rcx, rcx
xor r8, r8
xor r9, r9
swapgs ; switch from kernel GS to user GS
iretq
+402 -80
View File
@@ -28,8 +28,11 @@
extern "C" void SchedContextSwitch(uint64_t* oldRsp, uint64_t newRsp, uint64_t newCR3,
uint8_t* oldFpuArea, uint8_t* newFpuArea);
// Assembly: jump to user mode via IRETQ
extern "C" void JumpToUserMode(uint64_t rip, uint64_t rsp);
// Assembly: jump to user mode via IRETQ.
// `arg` is delivered as the user-mode RDI (SystemV first argument).
// For freshly spawned processes this is 0; for SpawnThread it is the
// user-supplied entry argument.
extern "C" void JumpToUserMode(uint64_t rip, uint64_t rsp, uint64_t arg);
namespace Sched {
@@ -145,8 +148,10 @@ namespace Sched {
// Set up per-CPU TSS RSP0 for hardware interrupts from ring 3
cpu->tss->rsp0 = proc.kernelStackTop;
// Jump to user mode (never returns)
JumpToUserMode(proc.entryPoint, proc.userStackTop);
// Jump to user mode (never returns).
// For main threads threadArg is 0 (libc _start ignores RDI);
// for sibling threads it carries the user-supplied argument.
JumpToUserMode(proc.entryPoint, proc.userStackTop, proc.threadArg);
}
ExitProcess();
@@ -196,6 +201,10 @@ namespace Sched {
processTable[i].ioInHandle = -1;
processTable[i].ioKeyHandle = -1;
processTable[i].ioWaitsetHandle = -1;
processTable[i].primarySlot = -1;
processTable[i].joinerSlot = -1;
processTable[i].exitCode = 0;
processTable[i].joinable = true;
}
nextPid = 0;
@@ -347,6 +356,10 @@ namespace Sched {
for (; i < 63 && vfsPath[i]; i++) proc.name[i] = vfsPath[i];
proc.name[i] = '\0';
}
proc.primarySlot = slot; // main thread owns per-process state
proc.joinerSlot = -1;
proc.exitCode = 0;
proc.joinable = false; // main thread is reaped by BSP, not joined
proc.savedRsp = (uint64_t)sp;
proc.stackBase = (uint64_t)kernelStackBase;
proc.entryPoint = entry;
@@ -440,6 +453,266 @@ namespace Sched {
return resultPid;
}
// ====================================================================
// Threading
//
// Every thread is a slot in processTable. The main thread's primarySlot
// points to itself and owns process-level state (PML4, IPC handles,
// cwd/user/args, redirected I/O). A sibling thread copies pml4Phys from
// the primary so context-switch is cheap, but per-process getters use
// primarySlot to reach the canonical state.
//
// Sibling-thread lifecycle:
// * SpawnThread: allocate slot + kernel stack; user provides user stack.
// * ExitCurrentThread: slot -> Terminated, kernel stack kept alive for
// the joiner; joiner reads exitCode and frees the kernel stack.
// * ExitProcess from the main thread sweeps any still-Terminated
// sibling slots (no joiner ever arrived) and tears the rest down.
// ====================================================================
int SpawnThread(uint64_t entry, uint64_t arg, uint64_t userStackTop) {
auto* cpu = Smp::GetCurrentCpuData();
if (cpu == nullptr || cpu->currentSlot < 0) return -1;
if (entry == 0 || userStackTop < 16) return -1;
int callerSlot = cpu->currentSlot;
int primarySlot_ = processTable[callerSlot].primarySlot;
if (primarySlot_ < 0) primarySlot_ = callerSlot;
uint64_t sharedPml4 = processTable[primarySlot_].pml4Phys;
int processPid = processTable[primarySlot_].pid;
if (sharedPml4 == 0) return -1;
// We do not write to the user stack from kernel mode (that would
// require validating the user VA against the process page tables).
// Userspace is responsible for ensuring the thread entry calls
// SYS_THREAD_EXIT; the libc trampoline does this. The kernel only
// enforces SystemV alignment by handing the entry RSP = 16n - 8.
uint64_t userRsp = (userStackTop & ~0xFULL) - 8;
// Allocate kernel stack.
void* stackMem = Memory::g_pfa->ReallocConsecutive(nullptr, StackPages);
if (stackMem == nullptr) return -1;
memset(stackMem, 0, StackSize);
uint8_t* kernelStackBase = (uint8_t*)stackMem;
uint64_t kernelStackTop = (uint64_t)kernelStackBase + StackSize;
// Initial kernel frame -- SchedContextSwitch "returns" into ProcessStartup.
uint64_t* sp = (uint64_t*)kernelStackTop;
*(--sp) = (uint64_t)ProcessStartup;
*(--sp) = 0; *(--sp) = 0; *(--sp) = 0;
*(--sp) = 0; *(--sp) = 0; *(--sp) = 0;
schedLock.Acquire();
int slot = -1;
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].state == ProcessState::Free) { slot = i; break; }
}
if (slot < 0) {
schedLock.Release();
Memory::g_pfa->Free(stackMem, StackPages);
return -1;
}
Process& thr = processTable[slot];
thr.pid = nextPid++;
thr.state = ProcessState::Ready;
readyCount++;
thr.runningOnCpu = -1;
thr.killPending = false;
thr.reapReady = false;
thr.startPending = false;
thr.waitingForPid = -1;
thr.sleepUntilTick = 0;
thr.waitingOnObject = nullptr;
thr.primarySlot = primarySlot_;
thr.joinerSlot = -1;
thr.exitCode = 0;
thr.joinable = true;
thr.threadArg = arg;
thr.savedRsp = (uint64_t)sp;
thr.stackBase = (uint64_t)kernelStackBase;
thr.entryPoint = entry;
thr.userStackTop = userRsp;
thr.kernelStackTop = kernelStackTop;
thr.pml4Phys = sharedPml4; // shared with primary, NOT owned
thr.sliceRemaining = TimeSliceMs;
thr.cpuTimeMs = 0;
thr.heapNext = 0;
thr.readdirCursor = 0;
thr.parentPid = processPid;
thr.redirected = false;
thr.outBuf = nullptr; thr.outHead = 0; thr.outTail = 0;
thr.inBuf = nullptr; thr.inHead = 0; thr.inTail = 0;
thr.keyHead = 0; thr.keyTail = 0;
thr.termCols = 0; thr.termRows = 0;
thr.ioOutHandle = -1; thr.ioInHandle = -1;
thr.ioKeyHandle = -1; thr.ioWaitsetHandle = -1;
thr.args[0] = '\0'; thr.user[0] = '\0'; thr.cwd[0] = '\0';
// Derive a debug name from the primary's name.
{
const char* base = processTable[primarySlot_].name;
int i = 0;
for (; i < 58 && base[i]; i++) thr.name[i] = base[i];
thr.name[i++] = ':';
thr.name[i++] = 't';
thr.name[i] = '\0';
}
memset(thr.fpuState, 0, 512);
*(uint16_t*)&thr.fpuState[0] = 0x037F;
*(uint32_t*)&thr.fpuState[24] = 0x1F80;
int tid = thr.pid;
schedLock.Release();
KickOneIdleCpu(cpu->cpuIndex);
return tid;
}
// Switch away from a slot we have just marked non-runnable while holding
// schedLock. Mirrors SwitchAwayFromBlockedCurrentLocked but does NOT
// assume the caller will be resumed -- used by ExitCurrentThread.
static void SwitchAwayFromExitingThreadLocked(int slot, Process& thr) {
auto* cpu = Smp::GetCurrentCpuData();
int next = -1;
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].state == ProcessState::Ready) { next = i; break; }
}
if (next >= 0) {
cpu->currentSlot = next;
processTable[next].state = ProcessState::Running;
readyCount--;
processTable[next].runningOnCpu = cpu->cpuIndex;
processTable[next].sliceRemaining = TimeSliceMs;
cpu->kernelRsp = processTable[next].kernelStackTop;
cpu->tss->rsp0 = processTable[next].kernelStackTop;
if (readyCount > 0) {
KickOneIdleCpu(cpu->cpuIndex);
}
SchedContextSwitch(&thr.savedRsp, processTable[next].savedRsp,
processTable[next].pml4Phys,
thr.fpuState, processTable[next].fpuState);
} else {
cpu->currentSlot = -1;
SchedContextSwitch(&thr.savedRsp, cpu->idleSavedRsp,
GetKernelCR3(),
thr.fpuState, nullptr);
}
schedLock.Release();
(void)slot;
}
[[noreturn]] void ExitCurrentThread(int exitCode) {
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
if (slot < 0) {
for (;;) asm volatile("hlt");
}
Process& thr = processTable[slot];
int primarySlot_ = thr.primarySlot;
if (primarySlot_ < 0 || primarySlot_ == slot) {
// This is the main thread (or an orphan): full process exit.
thr.exitCode = exitCode;
ExitProcess();
__builtin_unreachable();
}
thr.killPending = false;
thr.startPending = false;
thr.exitCode = exitCode;
schedLock.Acquire();
thr.state = ProcessState::Terminated;
thr.runningOnCpu = -1;
thr.waitingForPid = -1;
thr.sleepUntilTick = 0;
thr.waitingOnObject = nullptr;
// Wake any joiner blocked on this slot.
void* joinObj = &processTable[slot];
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].state == ProcessState::Blocked &&
processTable[i].waitingOnObject == joinObj) {
processTable[i].state = ProcessState::Ready;
readyCount++;
processTable[i].waitingOnObject = nullptr;
processTable[i].sleepUntilTick = 0;
processTable[i].waitingForPid = -1;
}
}
SwitchAwayFromExitingThreadLocked(slot, thr);
// Unreachable: the slot is Terminated, nobody resumes us.
for (;;) asm volatile("hlt");
}
int JoinThread(int tid, int* outExitCode) {
auto* cpu = Smp::GetCurrentCpuData();
if (cpu == nullptr || cpu->currentSlot < 0) return -1;
int callerSlot = cpu->currentSlot;
int callerPrimary = processTable[callerSlot].primarySlot;
if (callerPrimary < 0) callerPrimary = callerSlot;
if (tid <= 0) return -1;
// Locate the sibling slot under lock.
schedLock.Acquire();
int target = -1;
for (int i = 0; i < MaxProcesses; i++) {
if (i == callerSlot) continue;
if (processTable[i].pid != tid) continue;
if (processTable[i].primarySlot != callerPrimary) continue;
auto s = processTable[i].state;
if (s == ProcessState::Free) continue;
target = i;
break;
}
if (target < 0) {
schedLock.Release();
return -1;
}
// Fast path: target already Terminated -- harvest immediately.
if (processTable[target].state == ProcessState::Terminated) {
int code = processTable[target].exitCode;
void* stackBase = (void*)processTable[target].stackBase;
processTable[target].stackBase = 0;
processTable[target].pml4Phys = 0;
processTable[target].joinerSlot = -1;
processTable[target].primarySlot = -1;
processTable[target].state = ProcessState::Free;
schedLock.Release();
if (stackBase) Memory::g_pfa->Free(stackBase, StackPages);
if (outExitCode) *outExitCode = code;
return 0;
}
// Block until the sibling terminates and wakes us.
processTable[target].joinerSlot = callerSlot;
processTable[callerSlot].state = ProcessState::Blocked;
processTable[callerSlot].waitingOnObject = &processTable[target];
processTable[callerSlot].waitingForPid = -1;
processTable[callerSlot].sleepUntilTick = 0;
processTable[callerSlot].runningOnCpu = -1;
SwitchAwayFromBlockedCurrentLocked();
// Resumed -- harvest the sibling.
schedLock.Acquire();
int code = processTable[target].exitCode;
void* stackBase = (void*)processTable[target].stackBase;
processTable[target].stackBase = 0;
processTable[target].pml4Phys = 0;
processTable[target].joinerSlot = -1;
processTable[target].primarySlot = -1;
processTable[target].state = ProcessState::Free;
schedLock.Release();
if (stackBase) Memory::g_pfa->Free(stackBase, StackPages);
if (outExitCode) *outExitCode = code;
return 0;
}
int StartProcess(int pid) {
if (pid < 0) return -1;
@@ -688,12 +961,30 @@ namespace Sched {
}
int GetCurrentPid() {
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
if (slot < 0) return -1;
int primary = processTable[slot].primarySlot;
if (primary < 0) primary = slot;
return processTable[primary].pid;
}
int GetCurrentTid() {
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
return (slot >= 0) ? processTable[slot].pid : -1;
}
Process* GetCurrentProcessPtr() {
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
if (slot < 0) return nullptr;
int primary = processTable[slot].primarySlot;
if (primary < 0) primary = slot;
return &processTable[primary];
}
Process* GetCurrentThreadPtr() {
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
if (slot < 0) return nullptr;
@@ -708,10 +999,86 @@ namespace Sched {
return;
}
// If we're a sibling thread, this is a per-thread exit, not a
// process exit. ExitCurrentThread does the right thing and never
// returns.
{
int primary = processTable[slot].primarySlot;
if (primary >= 0 && primary != slot) {
ExitCurrentThread(0);
}
}
Process& proc = processTable[slot];
proc.killPending = false;
proc.startPending = false;
int exitingPid = proc.pid;
int primarySlot_ = slot;
// Drain sibling threads: any thread sharing our PML4 must stop using
// it before we call FreeUserHalf. Ready/Blocked siblings can be
// terminated immediately; siblings running on other CPUs are tagged
// killPending and we spin until their tick handler routes them
// through ExitCurrentThread.
for (;;) {
bool anyRunning = false;
schedLock.Acquire();
for (int i = 0; i < MaxProcesses; i++) {
if (i == primarySlot_) continue;
if (processTable[i].primarySlot != primarySlot_) continue;
auto st = processTable[i].state;
if (st == ProcessState::Free || st == ProcessState::Terminated) {
continue;
}
if (st == ProcessState::Running) {
processTable[i].killPending = true;
anyRunning = true;
continue;
}
// Ready or Blocked -- terminate inline, wake any joiner.
if (st == ProcessState::Ready) readyCount--;
processTable[i].state = ProcessState::Terminated;
processTable[i].killPending = false;
processTable[i].runningOnCpu = -1;
processTable[i].waitingForPid = -1;
processTable[i].sleepUntilTick = 0;
processTable[i].waitingOnObject = nullptr;
void* obj = &processTable[i];
for (int j = 0; j < MaxProcesses; j++) {
if (processTable[j].state == ProcessState::Blocked &&
processTable[j].waitingOnObject == obj) {
processTable[j].state = ProcessState::Ready;
readyCount++;
processTable[j].waitingOnObject = nullptr;
processTable[j].sleepUntilTick = 0;
processTable[j].waitingForPid = -1;
}
}
}
schedLock.Release();
if (!anyRunning) break;
// Wait for the sibling CPU(s) to observe killPending on their
// next timer tick. Briefly busy-wait; this path is rare.
for (int spin = 0; spin < 1024; spin++) {
asm volatile("pause");
}
}
// All siblings are quiescent. Reap any terminated sibling slots
// (the process is gone, no joiner will arrive).
for (int i = 0; i < MaxProcesses; i++) {
if (i == primarySlot_) continue;
if (processTable[i].primarySlot != primarySlot_) continue;
if (processTable[i].state != ProcessState::Terminated) continue;
void* stackBase = (void*)processTable[i].stackBase;
processTable[i].stackBase = 0;
processTable[i].pml4Phys = 0;
processTable[i].primarySlot = -1;
processTable[i].joinerSlot = -1;
processTable[i].state = ProcessState::Free;
if (stackBase) Memory::g_pfa->Free(stackBase, StackPages);
}
// Clean up any windows owned by this process
WinServer::CleanupProcess(exitingPid);
@@ -817,98 +1184,53 @@ namespace Sched {
schedLock.Acquire();
// Find the process by PID
int slot = -1;
// pid identifies a process (main thread). Sibling-thread TIDs are
// not killable through this entry point.
int primarySlot_ = -1;
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].pid == pid) {
auto s = processTable[i].state;
if (s == ProcessState::Ready || s == ProcessState::Running ||
s == ProcessState::Blocked) {
slot = i;
}
break;
if (processTable[i].pid != pid) continue;
if (processTable[i].primarySlot != i) continue;
auto s = processTable[i].state;
if (s == ProcessState::Ready || s == ProcessState::Running ||
s == ProcessState::Blocked) {
primarySlot_ = i;
}
break;
}
if (slot < 0) {
if (primarySlot_ < 0) {
schedLock.Release();
return -1;
}
Process& proc = processTable[slot];
if (proc.runningOnCpu >= 0) {
// Process is currently running on another CPU. We cannot
// safely free its resources (kernel stack, PML4, user pages)
// because that CPU is actively using them. Set a kill-pending
// flag; the target CPU's Tick() will call ExitProcess().
proc.killPending = true;
schedLock.Release();
return 0;
// Flag the main thread so its next tick (or scheduler dispatch)
// routes through ExitProcess, which sweeps every sibling thread
// and tears down the address space. If the main thread is parked
// on a blocking syscall we promote it to Ready so the scheduler
// picks it up promptly.
Process& primary = processTable[primarySlot_];
primary.killPending = true;
if (primary.state == ProcessState::Blocked) {
primary.state = ProcessState::Ready;
readyCount++;
primary.waitingForPid = -1;
primary.waitingOnObject = nullptr;
primary.sleepUntilTick = 0;
}
// Process is Ready or Blocked (not running on any CPU).
// Mark it non-runnable so the scheduler won't pick it up, but do not
// expose it to the BSP reaper until teardown is complete.
int killedPid = proc.pid;
if (proc.state == ProcessState::Ready)
readyCount--;
proc.state = ProcessState::Terminated;
proc.killPending = false;
proc.startPending = false;
proc.reapReady = false;
proc.waitingForPid = -1;
proc.sleepUntilTick = 0;
proc.waitingOnObject = nullptr;
// Wake any processes blocked on this PID
// Also stamp running siblings so they exit promptly; ExitProcess on
// the main thread will additionally wait for them anyway.
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].state == ProcessState::Blocked &&
processTable[i].waitingForPid == killedPid) {
processTable[i].state = ProcessState::Ready;
readyCount++;
processTable[i].waitingForPid = -1;
processTable[i].waitingOnObject = nullptr;
processTable[i].sleepUntilTick = 0;
if (i == primarySlot_) continue;
if (processTable[i].primarySlot != primarySlot_) continue;
auto s = processTable[i].state;
if (s == ProcessState::Running) {
processTable[i].killPending = true;
}
}
schedLock.Release();
KickOneIdleCpu(Smp::GetCurrentCpuData() ? Smp::GetCurrentCpuData()->cpuIndex : -1);
// Safe to clean up resources now -- process is not running anywhere.
WinServer::CleanupProcess(killedPid);
Drivers::Audio::Mixer::CleanupProcess(killedPid);
Ipc::CleanupProcessSlot(slot, killedPid, proc.pml4Phys);
Montauk::CleanupHeapForSlot(slot, proc.pml4Phys);
Montauk::CleanupLibTable(slot);
proc.redirected = false;
proc.parentPid = -1;
proc.outBuf = nullptr;
proc.outHead = 0;
proc.outTail = 0;
proc.inBuf = nullptr;
proc.inHead = 0;
proc.inTail = 0;
proc.keyHead = 0;
proc.keyTail = 0;
proc.termCols = 0;
proc.termRows = 0;
proc.ioOutHandle = -1;
proc.ioInHandle = -1;
proc.ioKeyHandle = -1;
proc.ioWaitsetHandle = -1;
Ipc::ProcessExitedInSlot(slot, killedPid);
Memory::VMM::Paging::FreeUserHalf(proc.pml4Phys);
schedLock.Acquire();
proc.reapReady = true;
schedLock.Release();
// Kernel stack and PML4 freed by ReclaimTerminated on BSP tick.
return 0;
}
+53 -6
View File
@@ -10,12 +10,18 @@
namespace Sched {
// Process and thread slots share the same table: the main thread of a
// process owns the per-process resources (PML4, IPC handles, args/cwd/user,
// redirected I/O), while sibling threads occupy additional slots whose
// `primarySlot` points back to the main thread. The scheduler treats
// every slot uniformly -- the primary/sibling distinction matters only
// for resource ownership on exit.
static constexpr int MaxProcesses = 256;
static constexpr uint64_t StackPages = 4; // 16 KiB kernel stack per process
static constexpr uint64_t StackPages = 4; // 16 KiB kernel stack per thread
static constexpr uint64_t StackSize = StackPages * 0x1000;
static constexpr uint64_t UserStackPages = 8; // 32 KiB user stack
static constexpr uint64_t UserStackPages = 8; // 32 KiB user stack (main thread)
static constexpr uint64_t UserStackSize = UserStackPages * 0x1000;
static constexpr uint64_t UserStackTop = 0x7FFFFFF000ULL; // User stack top VA
static constexpr uint64_t UserStackTop = 0x7FFFFFF000ULL; // Main-thread user stack top VA
static constexpr uint64_t UserHeapBase = 0x40000000ULL; // User heap start VA
static constexpr uint32_t UserReadDirSlots = 64; // rotating scratch pages for SYS_READDIR
static constexpr uint64_t UserReadDirBase =
@@ -57,6 +63,16 @@ namespace Sched {
bool reapReady = false; // Set once teardown is complete and BSP may free slot resources
bool startPending = false; // Spawned but not yet made runnable
// Threading. Every slot is a schedulable thread. The "main thread" of a
// process has primarySlot == own slot and owns per-process resources
// (pml4Phys, IPC handles, libs, heap, redirected I/O, cwd/user/args).
// Sibling threads share those resources via primarySlot.
int primarySlot = -1; // Index into processTable; -1 if Free
int joinerSlot = -1; // Slot blocked in JoinThread on this slot
int exitCode = 0; // Returned to joiner / accumulated for the process
bool joinable = true; // Sibling thread keeps Terminated state for the joiner
uint64_t threadArg = 0; // Argument passed into entryPoint on first dispatch
// I/O redirection for GUI terminal
bool redirected = false;
int parentPid = -1;
@@ -97,15 +113,46 @@ namespace Sched {
// tick interval. The BSP runs at 1 ms; APs may use a coarser interval.
void Tick(uint32_t elapsedMs = 1);
// Get the PID of the currently running process (-1 if idle)
// Get the PID of the currently running process (-1 if idle).
// For sibling threads this returns the primary slot's PID -- i.e. the
// process-level identifier, which is what userspace sees.
int GetCurrentPid();
// Get a pointer to the currently running process (nullptr if idle)
// Get the per-thread id of the currently running thread (-1 if idle).
// For the main thread this equals GetCurrentPid().
int GetCurrentTid();
// Get a pointer to the currently running process (primary slot, nullptr if idle).
// Always returns the slot that owns per-process state -- never a sibling thread.
Process* GetCurrentProcessPtr();
// Called by terminated processes to mark themselves done
// Get a pointer to the currently running thread's slot (may be a sibling).
Process* GetCurrentThreadPtr();
// Called by terminated processes to mark themselves done.
// Tears down the entire process (kills all sibling threads, frees address space).
void ExitProcess();
// ====================================================================
// Threading
// ====================================================================
// Spawn a sibling thread inside the current process.
// `entry` is a user-mode RIP, `arg` is delivered to entry in %rdi, and
// `userStackTop` points to the top-of-stack the caller (libc) allocated
// out of the user heap. Returns the new TID, or -1 on failure.
int SpawnThread(uint64_t entry, uint64_t arg, uint64_t userStackTop);
// Terminate the currently executing thread. If this is the main thread,
// the entire process exits (equivalent to ExitProcess).
[[noreturn]] void ExitCurrentThread(int exitCode);
// Block the current thread until `tid` (a sibling of the same process)
// terminates. On success, writes the exit code via outExitCode (may be
// null) and frees the joined thread's slot. Returns 0 on success, -1 if
// tid is invalid or not joinable by the caller.
int JoinThread(int tid, int* outExitCode);
// Check if a process is still alive (Ready, Running, or Blocked)
bool IsAlive(int pid);