feat: Symmetric Multiprocessing, text editor improvements, merge doom libc, implement math functions

This commit is contained in:
2026-03-23 20:09:11 +01:00
parent a805b06406
commit 63d9270613
46 changed files with 3004 additions and 2404 deletions
+351 -96
View File
@@ -1,7 +1,7 @@
/*
* Scheduler.cpp
* Preemptive process scheduler with user-mode support
* Copyright (c) 2025 Daniel Hammer
* Preemptive process scheduler with SMP support
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include "Scheduler.hpp"
@@ -12,8 +12,11 @@
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <CppLib/Spinlock.hpp>
#include <Hal/Apic/Apic.hpp>
#include <Hal/GDT.hpp>
#include <Hal/SmpBoot.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Api/WinServer.hpp>
// Assembly: context switch with CR3 and FPU state parameters
@@ -23,16 +26,16 @@ extern "C" void SchedContextSwitch(uint64_t* oldRsp, uint64_t newRsp, uint64_t n
// Assembly: jump to user mode via IRETQ
extern "C" void JumpToUserMode(uint64_t rip, uint64_t rsp);
// Global kernel RSP for SYSCALL entry (written by scheduler, read by SyscallEntry.asm)
extern "C" uint64_t g_kernelRsp;
uint64_t g_kernelRsp = 0;
namespace Sched {
static Process processTable[MaxProcesses];
static int currentPid = -1; // -1 = idle (kernel main loop)
static int nextPid = 0;
static uint64_t idleSavedRsp = 0;
// The scheduler lock MUST be a Spinlock (interrupt-disabling).
// It is held ACROSS context switches to prevent the race where
// another CPU picks up a process whose RSP hasn't been saved yet.
// The resumed process releases it.
static kcp::Spinlock schedLock;
// The idle loop runs in the kernel PML4
static uint64_t GetKernelCR3() {
@@ -41,18 +44,22 @@ namespace Sched {
// Startup function for newly spawned processes.
// SchedContextSwitch "returns" here on first schedule.
// The schedLock is held (acquired by the switching-from CPU's Schedule).
static void ProcessStartup() {
// Send EOI for the timer IRQ that triggered the context switch
Hal::LocalApic::SendEOI();
// Release the schedLock that the switching-from CPU held
schedLock.Release();
if (currentPid >= 0) {
Process& proc = processTable[currentPid];
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
// Set up kernel RSP for SYSCALL entry
g_kernelRsp = proc.kernelStackTop;
if (slot >= 0) {
Process& proc = processTable[slot];
// Set up TSS RSP0 for hardware interrupts from ring 3
Hal::g_tss.rsp0 = proc.kernelStackTop;
// Set up per-CPU kernel RSP for SYSCALL entry
cpu->kernelRsp = proc.kernelStackTop;
// 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);
@@ -78,6 +85,9 @@ namespace Sched {
processTable[i].userStackTop = 0;
processTable[i].heapNext = 0;
processTable[i].args[0] = '\0';
processTable[i].runningOnCpu = -1;
processTable[i].waitingForPid = -1;
processTable[i].sleepUntilTick = 0;
processTable[i].redirected = false;
processTable[i].parentPid = -1;
processTable[i].outBuf = nullptr;
@@ -92,15 +102,15 @@ namespace Sched {
processTable[i].termRows = 0;
}
currentPid = -1;
nextPid = 0;
idleSavedRsp = 0;
Kt::KernelLogStream(Kt::OK, "Sched") << "Initialized (" << MaxProcesses
<< " process slots, " << (uint64_t)TimeSliceMs << " ms time slice)";
}
int Spawn(const char* vfsPath, const char* args) {
schedLock.Acquire();
int slot = -1;
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].state == ProcessState::Free) {
@@ -110,19 +120,29 @@ namespace Sched {
}
if (slot < 0) {
schedLock.Release();
Kt::KernelLogStream(Kt::ERROR, "Sched") << "No free process slots";
return -1;
}
// Reserve the slot so another Spawn doesn't claim it.
// Use Running (not Ready!) so the scheduler doesn't try to
// dispatch this half-initialized process.
processTable[slot].state = ProcessState::Running;
processTable[slot].runningOnCpu = -1;
schedLock.Release();
// Create per-process PML4 with kernel-half copied
uint64_t pml4Phys = Memory::VMM::Paging::CreateUserPML4();
// Load ELF into the process's address space
uint64_t entry = ElfLoad(vfsPath, pml4Phys);
if (entry == 0) {
// Free the PML4 and any pages allocated during ELF load
Memory::VMM::Paging::FreeUserHalf(pml4Phys);
Memory::g_pfa->Free((void*)Memory::HHDM(pml4Phys));
schedLock.Acquire();
processTable[slot].state = ProcessState::Free;
schedLock.Release();
return -1;
}
@@ -132,6 +152,9 @@ namespace Sched {
Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for kernel stack";
Memory::VMM::Paging::FreeUserHalf(pml4Phys);
Memory::g_pfa->Free((void*)Memory::HHDM(pml4Phys));
schedLock.Acquire();
processTable[slot].state = ProcessState::Free;
schedLock.Release();
return -1;
}
void* stackMem = Memory::g_pfa->ReallocConsecutive(firstPage, StackPages);
@@ -140,6 +163,9 @@ namespace Sched {
Memory::g_pfa->Free(firstPage);
Memory::VMM::Paging::FreeUserHalf(pml4Phys);
Memory::g_pfa->Free((void*)Memory::HHDM(pml4Phys));
schedLock.Acquire();
processTable[slot].state = ProcessState::Free;
schedLock.Release();
return -1;
}
@@ -151,6 +177,9 @@ namespace Sched {
Memory::VMM::Paging::FreeUserHalf(pml4Phys);
Memory::g_pfa->Free((void*)Memory::HHDM(pml4Phys));
Memory::g_pfa->Free(stackMem, StackPages);
schedLock.Acquire();
processTable[slot].state = ProcessState::Free;
schedLock.Release();
};
// Allocate user stack pages and map them in the process PML4
@@ -174,7 +203,6 @@ namespace Sched {
}
// Allocate and map a user-space exit stub page.
// When _start() returns, it jumps here and calls SYS_EXIT(0).
{
void* stubPage = Memory::g_pfa->AllocateZeroed();
if (stubPage == nullptr) {
@@ -198,7 +226,6 @@ namespace Sched {
}
// Push exit stub address as the return address on the user stack.
// UserStackTop - 8 falls at offset 0xFF8 within the top stack page.
{
uint8_t* topPage = (uint8_t*)Memory::HHDM(topStackPagePhys);
*(uint64_t*)(topPage + 0xFF8) = ExitStubAddr;
@@ -216,6 +243,8 @@ namespace Sched {
*(--sp) = 0; // r14
*(--sp) = 0; // r15
schedLock.Acquire();
Process& proc = processTable[slot];
proc.pid = nextPid++;
proc.state = ProcessState::Ready;
@@ -230,8 +259,11 @@ namespace Sched {
proc.sliceRemaining = TimeSliceMs;
proc.pml4Phys = pml4Phys;
proc.kernelStackTop = kernelStackTop;
proc.userStackTop = UserStackTop - 8; // account for pushed exit stub return address
proc.userStackTop = UserStackTop - 8;
proc.heapNext = UserHeapBase;
proc.runningOnCpu = -1;
proc.waitingForPid = -1;
proc.sleepUntilTick = 0;
// Copy arguments string into process
proc.args[0] = '\0';
@@ -261,29 +293,60 @@ namespace Sched {
*(uint16_t*)&proc.fpuState[0] = 0x037F; // FCW: default x87 control word
*(uint32_t*)&proc.fpuState[24] = 0x1F80; // MXCSR: default SSE control/status
return proc.pid;
int resultPid = proc.pid;
schedLock.Release();
return resultPid;
}
// ====================================================================
// Schedule -- core context switch logic
//
// The schedLock is held ACROSS the context switch. This is critical:
// setting the old process to Ready and saving its RSP must be atomic
// with respect to other CPUs. If we released the lock before saving
// RSP, another CPU could pick up the process with a stale savedRsp.
//
// The RESUMED process releases the lock (it was acquired by whatever
// CPU called Schedule() and switched away from that process).
// New processes release it in ProcessStartup().
// ====================================================================
// Reclaim terminated process slots. Called from BSP's Tick only,
// NOT from every Schedule() call on every CPU. This avoids holding
// schedLock (with interrupts disabled) during PFA::Free on the
// hot scheduling path.
static void ReclaimTerminated() {
schedLock.Acquire();
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].state == ProcessState::Terminated) {
// Grab the pointers, mark Free, then free memory after releasing lock
void* stackBase = (processTable[i].stackBase != 0)
? (void*)processTable[i].stackBase : nullptr;
void* pml4 = (processTable[i].pml4Phys != 0)
? (void*)Memory::HHDM(processTable[i].pml4Phys) : nullptr;
processTable[i].stackBase = 0;
processTable[i].pml4Phys = 0;
processTable[i].state = ProcessState::Free;
// Release lock during PFA::Free to minimize hold time
schedLock.Release();
if (stackBase) Memory::g_pfa->Free(stackBase, StackPages);
if (pml4) Memory::g_pfa->Free(pml4);
schedLock.Acquire();
}
}
schedLock.Release();
}
void Schedule() {
// Reclaim terminated process slots — free deferred resources first
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].state == ProcessState::Terminated) {
// Free kernel stack (deferred from ExitProcess — can't free while running on it)
if (processTable[i].stackBase != 0) {
Memory::g_pfa->Free((void*)processTable[i].stackBase, StackPages);
processTable[i].stackBase = 0;
}
// Free the PML4 page (deferred from ExitProcess — can't free while it's CR3)
if (processTable[i].pml4Phys != 0) {
Memory::g_pfa->Free((void*)Memory::HHDM(processTable[i].pml4Phys));
processTable[i].pml4Phys = 0;
}
processTable[i].state = ProcessState::Free;
}
}
auto* cpu = Smp::GetCurrentCpuData();
schedLock.Acquire();
// Find the next Ready process (round-robin from after current slot)
int next = -1;
int start = (currentPid >= 0) ? currentPid + 1 : 0;
int start = (cpu->currentSlot >= 0) ? cpu->currentSlot + 1 : 0;
for (int i = 0; i < MaxProcesses; i++) {
int idx = (start + i) % MaxProcesses;
@@ -294,83 +357,146 @@ namespace Sched {
}
if (next < 0) {
// No ready processes. If we were running one, return to idle.
if (cpu->currentSlot >= 0) {
int oldSlot = cpu->currentSlot;
processTable[oldSlot].state = ProcessState::Ready;
processTable[oldSlot].runningOnCpu = -1;
cpu->currentSlot = -1;
// Lock held across context switch -- the idle loop
// doesn't release it; the next Schedule() that resumes
// a process from idle will release it via the resumed
// process path. But idle is special: we release here
// because idle doesn't go through ProcessStartup.
// The RSP save is safe because interrupts are disabled
// (Spinlock), so no timer can fire between Ready and save.
SchedContextSwitch(&processTable[oldSlot].savedRsp, cpu->idleSavedRsp,
GetKernelCR3(), processTable[oldSlot].fpuState, nullptr);
// Resumed from idle -- lock was held by whoever switched to us
schedLock.Release();
} else {
schedLock.Release();
}
return;
}
if (next == currentPid) {
if (next == cpu->currentSlot) {
// Same process, just reset time slice
processTable[next].sliceRemaining = TimeSliceMs;
schedLock.Release();
return;
}
// Prepare the context switch
uint64_t* oldRspPtr;
uint64_t oldCR3;
int oldSlot = cpu->currentSlot;
if (currentPid >= 0) {
processTable[currentPid].state = ProcessState::Ready;
oldRspPtr = &processTable[currentPid].savedRsp;
if (oldSlot >= 0) {
processTable[oldSlot].state = ProcessState::Ready;
processTable[oldSlot].runningOnCpu = -1;
oldRspPtr = &processTable[oldSlot].savedRsp;
} else {
oldRspPtr = &idleSavedRsp;
oldRspPtr = &cpu->idleSavedRsp;
}
int oldPid = currentPid;
currentPid = next;
cpu->currentSlot = next;
processTable[next].state = ProcessState::Running;
processTable[next].runningOnCpu = cpu->cpuIndex;
processTable[next].sliceRemaining = TimeSliceMs;
uint64_t newCR3 = processTable[next].pml4Phys;
// Disable interrupts while updating global kernel RSP and TSS,
// preventing an interrupt from using stale values mid-update.
asm volatile("cli");
// Update per-CPU kernel RSP and TSS RSP0
cpu->kernelRsp = processTable[next].kernelStackTop;
cpu->tss->rsp0 = processTable[next].kernelStackTop;
// Update kernel RSP for SYSCALL entry
g_kernelRsp = processTable[next].kernelStackTop;
// Update TSS RSP0 for hardware interrupts from ring 3
Hal::g_tss.rsp0 = processTable[next].kernelStackTop;
asm volatile("sti");
uint8_t* oldFpu = (oldPid >= 0) ? processTable[oldPid].fpuState : nullptr;
uint8_t* oldFpu = (oldSlot >= 0) ? processTable[oldSlot].fpuState : nullptr;
uint8_t* newFpu = processTable[next].fpuState;
// DO NOT release schedLock here! It is held across the context
// switch so that setting Ready + saving RSP is atomic. The
// resumed process releases it.
SchedContextSwitch(oldRspPtr, processTable[next].savedRsp, newCR3, oldFpu, newFpu);
// We reach here when this process is resumed by another CPU's
// Schedule(). That CPU held the lock across its context switch.
// Release it now.
schedLock.Release();
}
void Tick() {
if (currentPid < 0) {
// Idle — check if any process became ready
Schedule();
auto* cpu = Smp::GetCurrentCpuData();
// BSP: wake sleeping processes and reclaim terminated slots
if (cpu->cpuIndex == 0) {
uint64_t now = Timekeeping::GetTicks();
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].state == ProcessState::Blocked &&
processTable[i].sleepUntilTick != 0 &&
now >= processTable[i].sleepUntilTick) {
processTable[i].sleepUntilTick = 0;
processTable[i].state = ProcessState::Ready;
}
}
// Reclaim terminated process memory (BSP only, once per tick)
ReclaimTerminated();
}
int slot = cpu->currentSlot;
if (slot < 0) {
// Idle CPU. Do a quick lockless scan before taking the
// expensive schedLock path. On a 32-core system with 5
// active processes, 27 CPUs are idle -- without this check,
// they'd each acquire schedLock 1000x/sec to find nothing.
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].state == ProcessState::Ready) {
Schedule();
return;
}
}
// Nothing ready -- stay halted, don't touch schedLock
return;
}
if (processTable[currentPid].sliceRemaining > 0) {
processTable[currentPid].sliceRemaining--;
if (processTable[slot].sliceRemaining > 0) {
processTable[slot].sliceRemaining--;
}
if (processTable[currentPid].sliceRemaining == 0) {
if (processTable[slot].sliceRemaining == 0) {
Schedule();
}
}
int GetCurrentPid() {
return (currentPid >= 0) ? processTable[currentPid].pid : -1;
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
return (slot >= 0) ? processTable[slot].pid : -1;
}
Process* GetCurrentProcessPtr() {
if (currentPid < 0) return nullptr;
return &processTable[currentPid];
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
if (slot < 0) return nullptr;
return &processTable[slot];
}
void ExitProcess() {
if (currentPid < 0) {
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
if (slot < 0) {
return;
}
Process& proc = processTable[currentPid];
Process& proc = processTable[slot];
// Clean up any windows owned by this process (unmaps pixel pages from desktop)
// Clean up any windows owned by this process
WinServer::CleanupProcess(proc.pid);
// Free I/O redirect buffers (kernel-allocated pages)
// Free I/O redirect buffers
if (proc.outBuf) {
Memory::g_pfa->Free(proc.outBuf);
proc.outBuf = nullptr;
@@ -380,12 +506,139 @@ namespace Sched {
proc.inBuf = nullptr;
}
// Free all user-space physical pages and page table structures (entries 0-255).
// This covers ELF code/data, user stack, exit stub, heap, and window pixel buffers.
// The PML4 page itself is freed later in Schedule() (can't free while it's CR3).
// Free all user-space physical pages and page table structures
Memory::VMM::Paging::FreeUserHalf(proc.pml4Phys);
schedLock.Acquire();
int exitingPid = proc.pid;
proc.state = ProcessState::Terminated;
proc.runningOnCpu = -1;
// Wake any processes blocked on this PID
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].state == ProcessState::Blocked &&
processTable[i].waitingForPid == exitingPid) {
processTable[i].state = ProcessState::Ready;
processTable[i].waitingForPid = -1;
}
}
// Find next ready process
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;
processTable[next].runningOnCpu = cpu->cpuIndex;
processTable[next].sliceRemaining = TimeSliceMs;
uint64_t newCR3 = processTable[next].pml4Phys;
cpu->kernelRsp = processTable[next].kernelStackTop;
cpu->tss->rsp0 = processTable[next].kernelStackTop;
// Lock held across context switch -- resumed process releases it
SchedContextSwitch(&processTable[slot].savedRsp, processTable[next].savedRsp, newCR3,
processTable[slot].fpuState, processTable[next].fpuState);
schedLock.Release();
} else {
cpu->currentSlot = -1;
// Switch to idle -- release after resuming from idle
SchedContextSwitch(&processTable[slot].savedRsp, cpu->idleSavedRsp, GetKernelCR3(),
processTable[slot].fpuState, nullptr);
schedLock.Release();
}
for (;;) {
asm volatile("hlt");
}
}
void BlockOnPid(int pid) {
// If the target is already dead, return immediately
if (!IsAlive(pid)) return;
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
if (slot < 0) return;
schedLock.Acquire();
// Double-check under lock (target might have exited between
// the lockless IsAlive check and acquiring the lock)
bool stillAlive = false;
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].pid == pid) {
auto s = processTable[i].state;
stillAlive = (s == ProcessState::Ready ||
s == ProcessState::Running ||
s == ProcessState::Blocked);
break;
}
}
if (!stillAlive) {
schedLock.Release();
return;
}
// Mark current process as Blocked -- scheduler will skip it.
// ExitProcess will wake us when the target terminates.
processTable[slot].state = ProcessState::Blocked;
processTable[slot].waitingForPid = pid;
processTable[slot].runningOnCpu = -1;
// Find next ready process to switch to
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;
processTable[next].runningOnCpu = cpu->cpuIndex;
processTable[next].sliceRemaining = TimeSliceMs;
cpu->kernelRsp = processTable[next].kernelStackTop;
cpu->tss->rsp0 = processTable[next].kernelStackTop;
SchedContextSwitch(&processTable[slot].savedRsp, processTable[next].savedRsp,
processTable[next].pml4Phys,
processTable[slot].fpuState, processTable[next].fpuState);
schedLock.Release();
} else {
// No ready process -- go idle
cpu->currentSlot = -1;
SchedContextSwitch(&processTable[slot].savedRsp, cpu->idleSavedRsp,
GetKernelCR3(), processTable[slot].fpuState, nullptr);
schedLock.Release();
}
}
void BlockForSleep(uint64_t ms) {
if (ms == 0) return;
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot;
if (slot < 0) return;
schedLock.Acquire();
processTable[slot].state = ProcessState::Blocked;
processTable[slot].sleepUntilTick = Timekeeping::GetTicks() + ms;
processTable[slot].runningOnCpu = -1;
int next = -1;
for (int i = 0; i < MaxProcesses; i++) {
@@ -396,34 +649,34 @@ namespace Sched {
}
if (next >= 0) {
int old = currentPid;
currentPid = next;
cpu->currentSlot = next;
processTable[next].state = ProcessState::Running;
processTable[next].runningOnCpu = cpu->cpuIndex;
processTable[next].sliceRemaining = TimeSliceMs;
uint64_t newCR3 = processTable[next].pml4Phys;
g_kernelRsp = processTable[next].kernelStackTop;
Hal::g_tss.rsp0 = processTable[next].kernelStackTop;
cpu->kernelRsp = processTable[next].kernelStackTop;
cpu->tss->rsp0 = processTable[next].kernelStackTop;
SchedContextSwitch(&processTable[old].savedRsp, processTable[next].savedRsp, newCR3,
processTable[old].fpuState, processTable[next].fpuState);
SchedContextSwitch(&processTable[slot].savedRsp, processTable[next].savedRsp,
processTable[next].pml4Phys,
processTable[slot].fpuState, processTable[next].fpuState);
schedLock.Release();
} else {
int old = currentPid;
currentPid = -1;
SchedContextSwitch(&processTable[old].savedRsp, idleSavedRsp, GetKernelCR3(),
processTable[old].fpuState, nullptr);
}
cpu->currentSlot = -1;
for (;;) {
asm volatile("hlt");
SchedContextSwitch(&processTable[slot].savedRsp, cpu->idleSavedRsp,
GetKernelCR3(), processTable[slot].fpuState, nullptr);
schedLock.Release();
}
}
bool IsAlive(int pid) {
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].pid == pid) {
return processTable[i].state == ProcessState::Ready
|| processTable[i].state == ProcessState::Running;
auto s = processTable[i].state;
return s == ProcessState::Ready
|| s == ProcessState::Running
|| s == ProcessState::Blocked;
}
}
return false;
@@ -431,10 +684,12 @@ namespace Sched {
Process* GetProcessByPid(int pid) {
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].pid == pid &&
(processTable[i].state == ProcessState::Ready ||
processTable[i].state == ProcessState::Running)) {
return &processTable[i];
if (processTable[i].pid == pid) {
auto s = processTable[i].state;
if (s == ProcessState::Ready || s == ProcessState::Running ||
s == ProcessState::Blocked) {
return &processTable[i];
}
}
}
return nullptr;