diff --git a/GNUmakefile b/GNUmakefile index a23668e..9f9da94 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -6,7 +6,7 @@ MAKEFLAGS += -rR ARCH := x86_64 # Default user QEMU flags. These are appended to the QEMU command calls. -QEMUFLAGS := -smp 4 -m 2G -d int -D qemu.log -no-reboot +QEMUFLAGS := -smp 32 -m 2G -d int -D qemu.log -no-reboot override IMAGE_NAME := montauk-$(ARCH) diff --git a/kernel/src/Api/Process.hpp b/kernel/src/Api/Process.hpp index a331659..1849f20 100644 --- a/kernel/src/Api/Process.hpp +++ b/kernel/src/Api/Process.hpp @@ -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; diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 9b20102..d1527cb 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -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(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; diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 72319ce..fcc8eb1 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -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; diff --git a/kernel/src/Hal/SyscallEntry.asm b/kernel/src/Hal/SyscallEntry.asm index c4caea3..01dad9c 100644 --- a/kernel/src/Hal/SyscallEntry.asm +++ b/kernel/src/Hal/SyscallEntry.asm @@ -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 diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index c082145..978eede 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -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; } diff --git a/kernel/src/Sched/Scheduler.hpp b/kernel/src/Sched/Scheduler.hpp index d09cc32..509dbef 100644 --- a/kernel/src/Sched/Scheduler.hpp +++ b/kernel/src/Sched/Scheduler.hpp @@ -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); diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 663fb96..be5ae99 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -167,6 +167,12 @@ namespace Montauk { // Block until the mixer state serial differs from the caller's snapshot. static constexpr uint64_t SYS_AUDIOWAIT = 129; + // 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; diff --git a/programs/include/montauk/thread.h b/programs/include/montauk/thread.h new file mode 100644 index 0000000..e4eb5aa --- /dev/null +++ b/programs/include/montauk/thread.h @@ -0,0 +1,134 @@ +/* + * thread.h + * Userspace threading primitives for MontaukOS + * Copyright (c) 2026 Daniel Hammer + * + * Threads share their parent's address space, IPC handles, cwd, and + * heap allocator. The caller owns the thread stack: thread_spawn + * allocates one out of the user heap and the kernel uses it verbatim. + * + * Lifetimes: + * - thread_spawn returns a positive TID on success. + * - thread_join blocks until the target TID exits, frees its kernel + * stack, then returns its exit code via *out_code. + * - thread_exit terminates only the current thread. If the main thread + * calls exit() (SYS_EXIT) the whole process tears down, killing any + * surviving sibling threads. + * - thread_self returns the calling thread's TID. +*/ + +#pragma once +#include +#include +#include +#include + +namespace montauk { + + // Default per-thread stack size (64 KiB). Enough for typical app work; + // matches the main thread's 32 KiB lower bound with headroom for + // TrueType rendering call chains. + static constexpr uint64_t DEFAULT_THREAD_STACK_BYTES = 64 * 1024; + + using ThreadEntry = int (*)(void* arg); + + // Terminate the calling thread. Never returns. If called by the main + // thread this is equivalent to exit() (the whole process exits). + [[noreturn]] inline void thread_exit(int code = 0) { + syscall1(Montauk::SYS_THREAD_EXIT, (uint64_t)code); + __builtin_unreachable(); + } + + namespace detail { + struct ThreadCtx { + ThreadEntry user_entry; + void* user_arg; + void* stack_base; + }; + + // Userspace trampoline: bridges from the raw entry the kernel jumps + // to into the typed entry, then funnels into SYS_THREAD_EXIT. We + // route the exit through libc rather than relying on a kernel-side + // exit stub on the user stack -- the kernel never writes to user + // memory on this path. + // + // The thread's stack itself is intentionally not freed here: we are + // still running on it. It is reclaimed when the process exits, or + // the joiner may free it explicitly after thread_join. + [[noreturn]] inline void thread_trampoline(detail::ThreadCtx* ctx) { + int code = ctx->user_entry(ctx->user_arg); + montauk::mfree(ctx); + thread_exit(code); + } + } + + // Spawn a new thread that begins executing `entry(arg)`. Returns the + // new TID on success, or -1 on failure. The thread's stack is + // allocated from the user heap; it is leaked on thread exit (the + // thread itself cannot free the stack it is running on). The kernel + // reclaims it on process exit. Callers that need to spawn many short- + // lived threads should pool stacks themselves. + inline int thread_spawn(ThreadEntry entry, void* arg, + uint64_t stack_bytes = 0) { + if (entry == nullptr) return -1; + if (stack_bytes == 0) stack_bytes = DEFAULT_THREAD_STACK_BYTES; + stack_bytes = (stack_bytes + 15) & ~15ULL; + + void* stack = montauk::malloc(stack_bytes); + if (stack == nullptr) return -1; + auto* ctx = (detail::ThreadCtx*)montauk::malloc(sizeof(detail::ThreadCtx)); + if (ctx == nullptr) { + montauk::mfree(stack); + return -1; + } + ctx->user_entry = entry; + ctx->user_arg = arg; + ctx->stack_base = stack; + + uint64_t stack_top = ((uint64_t)stack + stack_bytes) & ~0xFULL; + int tid = (int)syscall3(Montauk::SYS_THREAD_SPAWN, + (uint64_t)&detail::thread_trampoline, + (uint64_t)ctx, stack_top); + if (tid < 0) { + montauk::mfree(ctx); + montauk::mfree(stack); + return -1; + } + return tid; + } + + // Block until the thread identified by `tid` terminates. Returns 0 on + // success (with the thread's exit code in *out_code if non-null) or + // -1 if `tid` is not a joinable sibling. + inline int thread_join(int tid, int* out_code = nullptr) { + return (int)syscall2(Montauk::SYS_THREAD_JOIN, + (uint64_t)tid, (uint64_t)out_code); + } + + // Return the calling thread's TID (== getpid() for the main thread). + inline int thread_self() { + return (int)syscall0(Montauk::SYS_THREAD_SELF); + } + + // Lightweight mutex backed by a single atomic word + the kernel + // yield syscall. Adequate for short critical sections; a heavier + // primitive can wrap mailbox_recv when blocking semantics are needed. + struct Mutex { + volatile uint32_t locked = 0; + + void lock() { + while (__atomic_exchange_n(&locked, 1, __ATOMIC_ACQUIRE) != 0) { + montauk::yield(); + } + } + + bool try_lock() { + return __atomic_exchange_n(&locked, 1, __ATOMIC_ACQUIRE) == 0; + } + + void unlock() { + __atomic_store_n(&locked, 0, __ATOMIC_RELEASE); + } + }; + +} diff --git a/programs/libs/libprintersapplet/obj/src/libprintersapplet.o b/programs/libs/libprintersapplet/obj/src/libprintersapplet.o index 68cd0c6..7901812 100644 Binary files a/programs/libs/libprintersapplet/obj/src/libprintersapplet.o and b/programs/libs/libprintersapplet/obj/src/libprintersapplet.o differ diff --git a/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o b/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o index 307910a..48292a5 100644 Binary files a/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o and b/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o differ diff --git a/programs/src/desktop/apps/settings/appearance.cpp b/programs/src/desktop/apps/settings/appearance.cpp index ae25966..5d25af8 100644 --- a/programs/src/desktop/apps/settings/appearance.cpp +++ b/programs/src/desktop/apps/settings/appearance.cpp @@ -163,8 +163,16 @@ bool settings_handle_appearance_click(Window* win, SettingsState* st, int mx, in montauk::strcpy(fullpath, st->desktop->home_dir); str_append(fullpath, "/", 256); str_append(fullpath, st->wp_files.names[i], 256); - wallpaper_load(&s, fullpath, - st->desktop->screen_w, st->desktop->screen_h); + // Decode and scale off the UI thread so rapid cycling + // between high-res images stays responsive. The radio state + // and persisted path update immediately; the pixel buffer + // arrives asynchronously and is swapped in by + // wallpaper_poll() on the next frame. + s.bg_image = true; + s.bg_gradient = false; + montauk::strncpy(s.bg_image_path, fullpath, 127); + wallpaper_load_async(fullpath, + st->desktop->screen_w, st->desktop->screen_h); desktop_mark_background_dirty(st->desktop); settings_persist(st); return true; diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 6c80334..a74eaa1 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -684,6 +684,12 @@ void gui::desktop_run(DesktopState* ds) { uint64_t now = montauk::get_milliseconds(); sceneChanged |= desktop_refresh_panel_state(ds, now); + // Pick up any wallpaper buffer produced by a background loader. + if (wallpaper_poll(&ds->settings)) { + desktop_mark_background_dirty(ds); + sceneChanged = true; + } + uint64_t launcherBlinkToken = desktop_launcher_blink_token(ds, now); if (launcherBlinkToken != lastLauncherBlinkToken) { lastLauncherBlinkToken = launcherBlinkToken; diff --git a/programs/src/desktop/wallpaper.hpp b/programs/src/desktop/wallpaper.hpp index 303e2c6..83bd6a6 100644 --- a/programs/src/desktop/wallpaper.hpp +++ b/programs/src/desktop/wallpaper.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -29,66 +30,52 @@ namespace gui { // Wallpaper loading // ============================================================================ -// Load a JPEG file and scale it to cover the given screen dimensions. -// Stores the scaled ARGB pixel buffer in settings. Returns true on success. -inline bool wallpaper_load(DesktopSettings* s, const char* path, - int screen_w, int screen_h) { - // Free existing wallpaper - if (s->bg_wallpaper) { - montauk::mfree(s->bg_wallpaper); - s->bg_wallpaper = nullptr; - s->bg_wallpaper_w = 0; - s->bg_wallpaper_h = 0; - } - - // Read file +// Decode `path` and scale it to cover (screen_w x screen_h). Returns a newly +// montauk::malloc'd ARGB buffer the caller owns, or nullptr on failure. This +// is the heavy lifting; it touches no shared state and is safe to run from a +// worker thread. +inline uint32_t* wallpaper_decode_scaled(const char* path, + int screen_w, int screen_h) { int fd = montauk::open(path); - if (fd < 0) return false; + if (fd < 0) return nullptr; uint64_t size = montauk::getsize(fd); if (size == 0 || size > 16 * 1024 * 1024) { montauk::close(fd); - return false; + return nullptr; } uint8_t* filedata = (uint8_t*)montauk::malloc(size); - if (!filedata) { montauk::close(fd); return false; } + if (!filedata) { montauk::close(fd); return nullptr; } int bytes_read = montauk::read(fd, filedata, 0, size); montauk::close(fd); - if (bytes_read <= 0) { montauk::mfree(filedata); return false; } + if (bytes_read <= 0) { montauk::mfree(filedata); return nullptr; } - // Decode JPEG int img_w, img_h, channels; unsigned char* rgb = stbi_load_from_memory(filedata, bytes_read, &img_w, &img_h, &channels, 3); montauk::mfree(filedata); - if (!rgb) return false; + if (!rgb) return nullptr; - // Scale to cover screen (crop to fill, maintain aspect ratio) int dst_w = screen_w; int dst_h = screen_h; - uint32_t* scaled = (uint32_t*)montauk::malloc((uint64_t)dst_w * dst_h * 4); - if (!scaled) { stbi_image_free(rgb); return false; } + if (!scaled) { stbi_image_free(rgb); return nullptr; } - // Compute source crop region for "cover" scaling int src_crop_w, src_crop_h, src_x0, src_y0; if ((int64_t)img_w * dst_h > (int64_t)img_h * dst_w) { - // Image is wider — crop sides src_crop_h = img_h; src_crop_w = (int)((int64_t)img_h * dst_w / dst_h); src_x0 = (img_w - src_crop_w) / 2; src_y0 = 0; } else { - // Image is taller — crop top/bottom src_crop_w = img_w; src_crop_h = (int)((int64_t)img_w * dst_h / dst_w); src_x0 = 0; src_y0 = (img_h - src_crop_h) / 2; } - // Nearest-neighbor scale from cropped region to destination for (int y = 0; y < dst_h; y++) { int sy = src_y0 + (int)((int64_t)y * src_crop_h / dst_h); if (sy < 0) sy = 0; @@ -106,14 +93,137 @@ inline bool wallpaper_load(DesktopSettings* s, const char* path, } stbi_image_free(rgb); + return scaled; +} +// Synchronous loader retained for code paths that genuinely want to block +// (e.g. desktop boot, where there is no UI to keep responsive yet). +inline bool wallpaper_load(DesktopSettings* s, const char* path, + int screen_w, int screen_h) { + uint32_t* scaled = wallpaper_decode_scaled(path, screen_w, screen_h); + if (!scaled) return false; + + if (s->bg_wallpaper) montauk::mfree(s->bg_wallpaper); s->bg_wallpaper = scaled; - s->bg_wallpaper_w = dst_w; - s->bg_wallpaper_h = dst_h; + s->bg_wallpaper_w = screen_w; + s->bg_wallpaper_h = screen_h; montauk::strncpy(s->bg_image_path, path, 127); s->bg_image = true; s->bg_gradient = false; + return true; +} +// ============================================================================ +// Async wallpaper loader +// +// Decoding a screen-sized JPEG blocks the desktop event loop. The async +// loader spawns a worker thread per click; rapid cycling is tolerated via a +// generation counter -- only the highest-gen worker's output is published. +// The main thread calls wallpaper_poll() each frame to swap in any new +// buffer (it owns the actual handoff and the free of the previous one). +// ============================================================================ + +struct WallpaperPublishSlot { + montauk::Mutex lock; + uint64_t latest_gen; // bumped on every load request + uint64_t pending_gen; // gen of pending_pixels (0 = empty) + uint64_t applied_gen; // highest gen consumed by the main thread + uint32_t* pending_pixels; // worker-produced ARGB buffer awaiting handoff + int pending_w, pending_h; + char pending_path[128]; +}; + +inline WallpaperPublishSlot& wallpaper_slot() { + static WallpaperPublishSlot s{}; + return s; +} + +struct WallpaperJob { + uint64_t gen; + int screen_w, screen_h; + char path[256]; +}; + +inline int wallpaper_worker(void* raw) { + auto* job = (WallpaperJob*)raw; + uint32_t* scaled = wallpaper_decode_scaled(job->path, job->screen_w, job->screen_h); + + auto& slot = wallpaper_slot(); + slot.lock.lock(); + bool publish = scaled != nullptr + && job->gen > slot.applied_gen + && job->gen > slot.pending_gen; + if (publish) { + if (slot.pending_pixels) { + // A stale (lower-gen) pending buffer was never consumed -- drop it. + montauk::mfree(slot.pending_pixels); + } + slot.pending_pixels = scaled; + slot.pending_w = job->screen_w; + slot.pending_h = job->screen_h; + montauk::strncpy(slot.pending_path, job->path, 127); + slot.pending_gen = job->gen; + } + slot.lock.unlock(); + + if (!publish && scaled) montauk::mfree(scaled); + montauk::mfree(job); + return 0; +} + +// Kick off a background decode. Returns true if the worker thread was +// spawned; the actual wallpaper swap happens later when wallpaper_poll +// observes the published buffer. +inline bool wallpaper_load_async(const char* path, + int screen_w, int screen_h) { + auto* job = (WallpaperJob*)montauk::malloc(sizeof(WallpaperJob)); + if (!job) return false; + montauk::strncpy(job->path, path, 255); + job->screen_w = screen_w; + job->screen_h = screen_h; + + auto& slot = wallpaper_slot(); + slot.lock.lock(); + job->gen = ++slot.latest_gen; + slot.lock.unlock(); + + int tid = montauk::thread_spawn(&wallpaper_worker, job); + if (tid < 0) { + montauk::mfree(job); + return false; + } + return true; +} + +// If a worker has produced a newer buffer than the main thread has applied, +// swap it into `s` and free the previous wallpaper. Returns true if a new +// wallpaper was installed (caller should mark the background dirty). +inline bool wallpaper_poll(DesktopSettings* s) { + auto& slot = wallpaper_slot(); + slot.lock.lock(); + if (slot.pending_gen == 0 || slot.pending_gen <= slot.applied_gen) { + slot.lock.unlock(); + return false; + } + uint32_t* new_pixels = slot.pending_pixels; + int new_w = slot.pending_w; + int new_h = slot.pending_h; + char new_path[128]; + montauk::strncpy(new_path, slot.pending_path, 127); + new_path[127] = '\0'; + slot.applied_gen = slot.pending_gen; + slot.pending_pixels = nullptr; + slot.pending_gen = 0; + slot.lock.unlock(); + + uint32_t* old = s->bg_wallpaper; + s->bg_wallpaper = new_pixels; + s->bg_wallpaper_w = new_w; + s->bg_wallpaper_h = new_h; + montauk::strncpy(s->bg_image_path, new_path, 127); + s->bg_image = true; + s->bg_gradient = false; + if (old) montauk::mfree(old); return true; } diff --git a/programs/src/threadtest/main.cpp b/programs/src/threadtest/main.cpp new file mode 100644 index 0000000..e69f5d9 --- /dev/null +++ b/programs/src/threadtest/main.cpp @@ -0,0 +1,101 @@ +/* + * main.cpp + * Threading smoke test for MontaukOS. + * Copyright (c) 2026 Daniel Hammer + * + * Spawns two worker threads that increment a shared counter under a + * userspace mutex, joins them, and reports the result. +*/ + +#include +#include + +using montauk::Mutex; + +static constexpr int ITERS_PER_THREAD = 100000; + +struct WorkerState { + Mutex* lock; + int* counter; + int delta; +}; + +static int worker(void* raw) { + auto* s = (WorkerState*)raw; + for (int i = 0; i < ITERS_PER_THREAD; i++) { + s->lock->lock(); + *(s->counter) += s->delta; + s->lock->unlock(); + } + return s->delta; +} + +static void print_int(int v) { + char buf[16]; + int n = 0; + if (v == 0) { + buf[n++] = '0'; + } else { + bool neg = v < 0; + unsigned int u = neg ? (unsigned int)(-(long long)v) : (unsigned int)v; + char tmp[16]; int t = 0; + while (u) { tmp[t++] = (char)('0' + (u % 10)); u /= 10; } + if (neg) buf[n++] = '-'; + while (t) buf[n++] = tmp[--t]; + } + buf[n] = '\0'; + montauk::print(buf); +} + +extern "C" void _start() { + Mutex lock; + int counter = 0; + + WorkerState a{&lock, &counter, +1}; + WorkerState b{&lock, &counter, +2}; + + int tidA = montauk::thread_spawn(worker, &a); + int tidB = montauk::thread_spawn(worker, &b); + if (tidA < 0 || tidB < 0) { + montauk::print("threadtest: spawn failed\n"); + montauk::exit(1); + } + + montauk::print("threadtest: spawned tids "); + print_int(tidA); + montauk::print(" and "); + print_int(tidB); + montauk::print("\n"); + + int codeA = -1, codeB = -1; + if (montauk::thread_join(tidA, &codeA) != 0) { + montauk::print("threadtest: join A failed\n"); + montauk::exit(2); + } + if (montauk::thread_join(tidB, &codeB) != 0) { + montauk::print("threadtest: join B failed\n"); + montauk::exit(3); + } + + int expected = ITERS_PER_THREAD * (1 + 2); + montauk::print("threadtest: counter="); + print_int(counter); + montauk::print(" expected="); + print_int(expected); + montauk::print(" codes="); + print_int(codeA); + montauk::print("/"); + print_int(codeB); + montauk::print("\n"); + + if (counter != expected) { + montauk::print("threadtest: FAIL (counter mismatch)\n"); + montauk::exit(4); + } + if (codeA != 1 || codeB != 2) { + montauk::print("threadtest: FAIL (exit codes)\n"); + montauk::exit(5); + } + montauk::print("threadtest: PASS\n"); + montauk::exit(0); +}