feat: add threading to Scheduler, fix desktop background selection freeze issue
This commit is contained in:
+1
-1
@@ -6,7 +6,7 @@ MAKEFLAGS += -rR
|
|||||||
ARCH := x86_64
|
ARCH := x86_64
|
||||||
|
|
||||||
# Default user QEMU flags. These are appended to the QEMU command calls.
|
# 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)
|
override IMAGE_NAME := montauk-$(ARCH)
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ namespace Montauk {
|
|||||||
for (int i = 0; i < Sched::MaxProcesses && count < maxCount; i++) {
|
for (int i = 0; i < Sched::MaxProcesses && count < maxCount; i++) {
|
||||||
auto* proc = Sched::GetProcessSlot(i);
|
auto* proc = Sched::GetProcessSlot(i);
|
||||||
if (!proc || proc->state == Sched::ProcessState::Free) continue;
|
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].pid = (int32_t)proc->pid;
|
||||||
buf[count].parentPid = (int32_t)proc->parentPid;
|
buf[count].parentPid = (int32_t)proc->parentPid;
|
||||||
@@ -154,6 +156,25 @@ namespace Montauk {
|
|||||||
return i;
|
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) {
|
static int Sys_Chdir(const char* path) {
|
||||||
auto* proc = Sched::GetCurrentProcessPtr();
|
auto* proc = Sched::GetCurrentProcessPtr();
|
||||||
if (proc == nullptr || path == nullptr) return -1;
|
if (proc == nullptr || path == nullptr) return -1;
|
||||||
|
|||||||
@@ -340,6 +340,16 @@ namespace Montauk {
|
|||||||
return Sys_AudioList((AudioStreamInfo*)frame->arg1, (int)frame->arg2);
|
return Sys_AudioList((AudioStreamInfo*)frame->arg1, (int)frame->arg2);
|
||||||
case SYS_AUDIOWAIT:
|
case SYS_AUDIOWAIT:
|
||||||
return Sys_AudioWait(frame->arg1, frame->arg2);
|
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:
|
case SYS_BTSCAN:
|
||||||
if ((int64_t)frame->arg2 < 0) return -1;
|
if ((int64_t)frame->arg2 < 0) return -1;
|
||||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtScanResult), true)) return -1;
|
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtScanResult), true)) return -1;
|
||||||
|
|||||||
@@ -243,6 +243,12 @@ namespace Montauk {
|
|||||||
static constexpr uint64_t SYS_AUDIOLIST = 128;
|
static constexpr uint64_t SYS_AUDIOLIST = 128;
|
||||||
static constexpr uint64_t SYS_AUDIOWAIT = 129;
|
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 CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024;
|
||||||
|
|
||||||
static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0;
|
static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0;
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ SyscallEntry:
|
|||||||
; JumpToUserMode -- initial transition to ring 3 via IRETQ
|
; JumpToUserMode -- initial transition to ring 3 via IRETQ
|
||||||
; RDI = user RIP (entry point)
|
; RDI = user RIP (entry point)
|
||||||
; RSI = user RSP (top of user stack)
|
; RSI = user RSP (top of user stack)
|
||||||
|
; RDX = user RDI (first SystemV arg, e.g. thread arg; 0 for new processes)
|
||||||
; ====================================================================
|
; ====================================================================
|
||||||
global JumpToUserMode
|
global JumpToUserMode
|
||||||
JumpToUserMode:
|
JumpToUserMode:
|
||||||
@@ -86,5 +87,11 @@ JumpToUserMode:
|
|||||||
push 0x202 ; RFLAGS (IF=1)
|
push 0x202 ; RFLAGS (IF=1)
|
||||||
push 0x23 ; CS = UserCode | RPL3
|
push 0x23 ; CS = UserCode | RPL3
|
||||||
push rdi ; RIP = entry point
|
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
|
swapgs ; switch from kernel GS to user GS
|
||||||
iretq
|
iretq
|
||||||
|
|||||||
+402
-80
@@ -28,8 +28,11 @@
|
|||||||
extern "C" void SchedContextSwitch(uint64_t* oldRsp, uint64_t newRsp, uint64_t newCR3,
|
extern "C" void SchedContextSwitch(uint64_t* oldRsp, uint64_t newRsp, uint64_t newCR3,
|
||||||
uint8_t* oldFpuArea, uint8_t* newFpuArea);
|
uint8_t* oldFpuArea, uint8_t* newFpuArea);
|
||||||
|
|
||||||
// Assembly: jump to user mode via IRETQ
|
// Assembly: jump to user mode via IRETQ.
|
||||||
extern "C" void JumpToUserMode(uint64_t rip, uint64_t rsp);
|
// `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 {
|
namespace Sched {
|
||||||
|
|
||||||
@@ -145,8 +148,10 @@ namespace Sched {
|
|||||||
// Set up per-CPU TSS RSP0 for hardware interrupts from ring 3
|
// Set up per-CPU TSS RSP0 for hardware interrupts from ring 3
|
||||||
cpu->tss->rsp0 = proc.kernelStackTop;
|
cpu->tss->rsp0 = proc.kernelStackTop;
|
||||||
|
|
||||||
// Jump to user mode (never returns)
|
// Jump to user mode (never returns).
|
||||||
JumpToUserMode(proc.entryPoint, proc.userStackTop);
|
// 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();
|
ExitProcess();
|
||||||
@@ -196,6 +201,10 @@ namespace Sched {
|
|||||||
processTable[i].ioInHandle = -1;
|
processTable[i].ioInHandle = -1;
|
||||||
processTable[i].ioKeyHandle = -1;
|
processTable[i].ioKeyHandle = -1;
|
||||||
processTable[i].ioWaitsetHandle = -1;
|
processTable[i].ioWaitsetHandle = -1;
|
||||||
|
processTable[i].primarySlot = -1;
|
||||||
|
processTable[i].joinerSlot = -1;
|
||||||
|
processTable[i].exitCode = 0;
|
||||||
|
processTable[i].joinable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
nextPid = 0;
|
nextPid = 0;
|
||||||
@@ -347,6 +356,10 @@ namespace Sched {
|
|||||||
for (; i < 63 && vfsPath[i]; i++) proc.name[i] = vfsPath[i];
|
for (; i < 63 && vfsPath[i]; i++) proc.name[i] = vfsPath[i];
|
||||||
proc.name[i] = '\0';
|
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.savedRsp = (uint64_t)sp;
|
||||||
proc.stackBase = (uint64_t)kernelStackBase;
|
proc.stackBase = (uint64_t)kernelStackBase;
|
||||||
proc.entryPoint = entry;
|
proc.entryPoint = entry;
|
||||||
@@ -440,6 +453,266 @@ namespace Sched {
|
|||||||
return resultPid;
|
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) {
|
int StartProcess(int pid) {
|
||||||
if (pid < 0) return -1;
|
if (pid < 0) return -1;
|
||||||
|
|
||||||
@@ -688,12 +961,30 @@ namespace Sched {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int GetCurrentPid() {
|
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();
|
auto* cpu = Smp::GetCurrentCpuData();
|
||||||
int slot = cpu->currentSlot;
|
int slot = cpu->currentSlot;
|
||||||
return (slot >= 0) ? processTable[slot].pid : -1;
|
return (slot >= 0) ? processTable[slot].pid : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Process* GetCurrentProcessPtr() {
|
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();
|
auto* cpu = Smp::GetCurrentCpuData();
|
||||||
int slot = cpu->currentSlot;
|
int slot = cpu->currentSlot;
|
||||||
if (slot < 0) return nullptr;
|
if (slot < 0) return nullptr;
|
||||||
@@ -708,10 +999,86 @@ namespace Sched {
|
|||||||
return;
|
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];
|
Process& proc = processTable[slot];
|
||||||
proc.killPending = false;
|
proc.killPending = false;
|
||||||
proc.startPending = false;
|
proc.startPending = false;
|
||||||
int exitingPid = proc.pid;
|
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
|
// Clean up any windows owned by this process
|
||||||
WinServer::CleanupProcess(exitingPid);
|
WinServer::CleanupProcess(exitingPid);
|
||||||
@@ -817,98 +1184,53 @@ namespace Sched {
|
|||||||
|
|
||||||
schedLock.Acquire();
|
schedLock.Acquire();
|
||||||
|
|
||||||
// Find the process by PID
|
// pid identifies a process (main thread). Sibling-thread TIDs are
|
||||||
int slot = -1;
|
// not killable through this entry point.
|
||||||
|
int primarySlot_ = -1;
|
||||||
for (int i = 0; i < MaxProcesses; i++) {
|
for (int i = 0; i < MaxProcesses; i++) {
|
||||||
if (processTable[i].pid == pid) {
|
if (processTable[i].pid != pid) continue;
|
||||||
auto s = processTable[i].state;
|
if (processTable[i].primarySlot != i) continue;
|
||||||
if (s == ProcessState::Ready || s == ProcessState::Running ||
|
auto s = processTable[i].state;
|
||||||
s == ProcessState::Blocked) {
|
if (s == ProcessState::Ready || s == ProcessState::Running ||
|
||||||
slot = i;
|
s == ProcessState::Blocked) {
|
||||||
}
|
primarySlot_ = i;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slot < 0) {
|
if (primarySlot_ < 0) {
|
||||||
schedLock.Release();
|
schedLock.Release();
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Process& proc = processTable[slot];
|
// Flag the main thread so its next tick (or scheduler dispatch)
|
||||||
|
// routes through ExitProcess, which sweeps every sibling thread
|
||||||
if (proc.runningOnCpu >= 0) {
|
// and tears down the address space. If the main thread is parked
|
||||||
// Process is currently running on another CPU. We cannot
|
// on a blocking syscall we promote it to Ready so the scheduler
|
||||||
// safely free its resources (kernel stack, PML4, user pages)
|
// picks it up promptly.
|
||||||
// because that CPU is actively using them. Set a kill-pending
|
Process& primary = processTable[primarySlot_];
|
||||||
// flag; the target CPU's Tick() will call ExitProcess().
|
primary.killPending = true;
|
||||||
proc.killPending = true;
|
if (primary.state == ProcessState::Blocked) {
|
||||||
schedLock.Release();
|
primary.state = ProcessState::Ready;
|
||||||
return 0;
|
readyCount++;
|
||||||
|
primary.waitingForPid = -1;
|
||||||
|
primary.waitingOnObject = nullptr;
|
||||||
|
primary.sleepUntilTick = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process is Ready or Blocked (not running on any CPU).
|
// Also stamp running siblings so they exit promptly; ExitProcess on
|
||||||
// Mark it non-runnable so the scheduler won't pick it up, but do not
|
// the main thread will additionally wait for them anyway.
|
||||||
// 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
|
|
||||||
for (int i = 0; i < MaxProcesses; i++) {
|
for (int i = 0; i < MaxProcesses; i++) {
|
||||||
if (processTable[i].state == ProcessState::Blocked &&
|
if (i == primarySlot_) continue;
|
||||||
processTable[i].waitingForPid == killedPid) {
|
if (processTable[i].primarySlot != primarySlot_) continue;
|
||||||
processTable[i].state = ProcessState::Ready;
|
auto s = processTable[i].state;
|
||||||
readyCount++;
|
if (s == ProcessState::Running) {
|
||||||
processTable[i].waitingForPid = -1;
|
processTable[i].killPending = true;
|
||||||
processTable[i].waitingOnObject = nullptr;
|
|
||||||
processTable[i].sleepUntilTick = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
schedLock.Release();
|
schedLock.Release();
|
||||||
KickOneIdleCpu(Smp::GetCurrentCpuData() ? Smp::GetCurrentCpuData()->cpuIndex : -1);
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,18 @@
|
|||||||
|
|
||||||
namespace Sched {
|
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 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 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 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 uint64_t UserHeapBase = 0x40000000ULL; // User heap start VA
|
||||||
static constexpr uint32_t UserReadDirSlots = 64; // rotating scratch pages for SYS_READDIR
|
static constexpr uint32_t UserReadDirSlots = 64; // rotating scratch pages for SYS_READDIR
|
||||||
static constexpr uint64_t UserReadDirBase =
|
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 reapReady = false; // Set once teardown is complete and BSP may free slot resources
|
||||||
bool startPending = false; // Spawned but not yet made runnable
|
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
|
// I/O redirection for GUI terminal
|
||||||
bool redirected = false;
|
bool redirected = false;
|
||||||
int parentPid = -1;
|
int parentPid = -1;
|
||||||
@@ -97,15 +113,46 @@ namespace Sched {
|
|||||||
// tick interval. The BSP runs at 1 ms; APs may use a coarser interval.
|
// tick interval. The BSP runs at 1 ms; APs may use a coarser interval.
|
||||||
void Tick(uint32_t elapsedMs = 1);
|
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();
|
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();
|
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();
|
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)
|
// Check if a process is still alive (Ready, Running, or Blocked)
|
||||||
bool IsAlive(int pid);
|
bool IsAlive(int pid);
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,12 @@ namespace Montauk {
|
|||||||
// Block until the mixer state serial differs from the caller's snapshot.
|
// Block until the mixer state serial differs from the caller's snapshot.
|
||||||
static constexpr uint64_t SYS_AUDIOWAIT = 129;
|
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 CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024;
|
||||||
|
|
||||||
static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0;
|
static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0;
|
||||||
|
|||||||
@@ -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 <cstdint>
|
||||||
|
#include <Api/Syscall.hpp>
|
||||||
|
#include <montauk/syscall.h>
|
||||||
|
#include <montauk/heap.h>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -163,8 +163,16 @@ bool settings_handle_appearance_click(Window* win, SettingsState* st, int mx, in
|
|||||||
montauk::strcpy(fullpath, st->desktop->home_dir);
|
montauk::strcpy(fullpath, st->desktop->home_dir);
|
||||||
str_append(fullpath, "/", 256);
|
str_append(fullpath, "/", 256);
|
||||||
str_append(fullpath, st->wp_files.names[i], 256);
|
str_append(fullpath, st->wp_files.names[i], 256);
|
||||||
wallpaper_load(&s, fullpath,
|
// Decode and scale off the UI thread so rapid cycling
|
||||||
st->desktop->screen_w, st->desktop->screen_h);
|
// 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);
|
desktop_mark_background_dirty(st->desktop);
|
||||||
settings_persist(st);
|
settings_persist(st);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -684,6 +684,12 @@ void gui::desktop_run(DesktopState* ds) {
|
|||||||
uint64_t now = montauk::get_milliseconds();
|
uint64_t now = montauk::get_milliseconds();
|
||||||
sceneChanged |= desktop_refresh_panel_state(ds, now);
|
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);
|
uint64_t launcherBlinkToken = desktop_launcher_blink_token(ds, now);
|
||||||
if (launcherBlinkToken != lastLauncherBlinkToken) {
|
if (launcherBlinkToken != lastLauncherBlinkToken) {
|
||||||
lastLauncherBlinkToken = launcherBlinkToken;
|
lastLauncherBlinkToken = launcherBlinkToken;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
#include <montauk/heap.h>
|
#include <montauk/heap.h>
|
||||||
|
#include <montauk/thread.h>
|
||||||
#include <gui/gui.hpp>
|
#include <gui/gui.hpp>
|
||||||
#include <gui/desktop.hpp>
|
#include <gui/desktop.hpp>
|
||||||
|
|
||||||
@@ -29,66 +30,52 @@ namespace gui {
|
|||||||
// Wallpaper loading
|
// Wallpaper loading
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Load a JPEG file and scale it to cover the given screen dimensions.
|
// Decode `path` and scale it to cover (screen_w x screen_h). Returns a newly
|
||||||
// Stores the scaled ARGB pixel buffer in settings. Returns true on success.
|
// montauk::malloc'd ARGB buffer the caller owns, or nullptr on failure. This
|
||||||
inline bool wallpaper_load(DesktopSettings* s, const char* path,
|
// is the heavy lifting; it touches no shared state and is safe to run from a
|
||||||
int screen_w, int screen_h) {
|
// worker thread.
|
||||||
// Free existing wallpaper
|
inline uint32_t* wallpaper_decode_scaled(const char* path,
|
||||||
if (s->bg_wallpaper) {
|
int screen_w, int screen_h) {
|
||||||
montauk::mfree(s->bg_wallpaper);
|
|
||||||
s->bg_wallpaper = nullptr;
|
|
||||||
s->bg_wallpaper_w = 0;
|
|
||||||
s->bg_wallpaper_h = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file
|
|
||||||
int fd = montauk::open(path);
|
int fd = montauk::open(path);
|
||||||
if (fd < 0) return false;
|
if (fd < 0) return nullptr;
|
||||||
|
|
||||||
uint64_t size = montauk::getsize(fd);
|
uint64_t size = montauk::getsize(fd);
|
||||||
if (size == 0 || size > 16 * 1024 * 1024) {
|
if (size == 0 || size > 16 * 1024 * 1024) {
|
||||||
montauk::close(fd);
|
montauk::close(fd);
|
||||||
return false;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t* filedata = (uint8_t*)montauk::malloc(size);
|
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);
|
int bytes_read = montauk::read(fd, filedata, 0, size);
|
||||||
montauk::close(fd);
|
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;
|
int img_w, img_h, channels;
|
||||||
unsigned char* rgb = stbi_load_from_memory(filedata, bytes_read,
|
unsigned char* rgb = stbi_load_from_memory(filedata, bytes_read,
|
||||||
&img_w, &img_h, &channels, 3);
|
&img_w, &img_h, &channels, 3);
|
||||||
montauk::mfree(filedata);
|
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_w = screen_w;
|
||||||
int dst_h = screen_h;
|
int dst_h = screen_h;
|
||||||
|
|
||||||
uint32_t* scaled = (uint32_t*)montauk::malloc((uint64_t)dst_w * dst_h * 4);
|
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;
|
int src_crop_w, src_crop_h, src_x0, src_y0;
|
||||||
if ((int64_t)img_w * dst_h > (int64_t)img_h * dst_w) {
|
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_h = img_h;
|
||||||
src_crop_w = (int)((int64_t)img_h * dst_w / dst_h);
|
src_crop_w = (int)((int64_t)img_h * dst_w / dst_h);
|
||||||
src_x0 = (img_w - src_crop_w) / 2;
|
src_x0 = (img_w - src_crop_w) / 2;
|
||||||
src_y0 = 0;
|
src_y0 = 0;
|
||||||
} else {
|
} else {
|
||||||
// Image is taller — crop top/bottom
|
|
||||||
src_crop_w = img_w;
|
src_crop_w = img_w;
|
||||||
src_crop_h = (int)((int64_t)img_w * dst_h / dst_w);
|
src_crop_h = (int)((int64_t)img_w * dst_h / dst_w);
|
||||||
src_x0 = 0;
|
src_x0 = 0;
|
||||||
src_y0 = (img_h - src_crop_h) / 2;
|
src_y0 = (img_h - src_crop_h) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nearest-neighbor scale from cropped region to destination
|
|
||||||
for (int y = 0; y < dst_h; y++) {
|
for (int y = 0; y < dst_h; y++) {
|
||||||
int sy = src_y0 + (int)((int64_t)y * src_crop_h / dst_h);
|
int sy = src_y0 + (int)((int64_t)y * src_crop_h / dst_h);
|
||||||
if (sy < 0) sy = 0;
|
if (sy < 0) sy = 0;
|
||||||
@@ -106,14 +93,137 @@ inline bool wallpaper_load(DesktopSettings* s, const char* path,
|
|||||||
}
|
}
|
||||||
|
|
||||||
stbi_image_free(rgb);
|
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 = scaled;
|
||||||
s->bg_wallpaper_w = dst_w;
|
s->bg_wallpaper_w = screen_w;
|
||||||
s->bg_wallpaper_h = dst_h;
|
s->bg_wallpaper_h = screen_h;
|
||||||
montauk::strncpy(s->bg_image_path, path, 127);
|
montauk::strncpy(s->bg_image_path, path, 127);
|
||||||
s->bg_image = true;
|
s->bg_image = true;
|
||||||
s->bg_gradient = false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 <montauk/syscall.h>
|
||||||
|
#include <montauk/thread.h>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user