fix: improve scheduling, memory, timer implementations

This commit is contained in:
2026-03-24 07:40:37 +01:00
parent 195028d994
commit f902ab48a1
14 changed files with 239 additions and 94 deletions
+16 -7
View File
@@ -12,17 +12,26 @@ namespace Montauk {
return Sched::GetProcessByPid(proc->parentPid);
}
static void RingWrite(uint8_t* buf, uint32_t& head, uint32_t /*tail*/, uint32_t size, uint8_t byte) {
buf[head] = byte;
head = (head + 1) % size;
// SPSC ring buffer helpers. On x86 TSO, atomic-width aligned
// loads/stores are naturally atomic. The compiler barrier ensures
// the data write is visible before the head update.
static void RingWrite(uint8_t* buf, volatile uint32_t& head, uint32_t /*tail*/, uint32_t size, uint8_t byte) {
uint32_t h = head;
buf[h] = byte;
asm volatile("" ::: "memory"); // compiler barrier
head = (h + 1) % size;
}
static int RingRead(uint8_t* buf, uint32_t& head, uint32_t& tail, uint32_t size, uint8_t* out, int maxLen) {
static int RingRead(uint8_t* buf, volatile uint32_t& head, volatile uint32_t& tail, uint32_t size, uint8_t* out, int maxLen) {
int count = 0;
while (tail != head && count < maxLen) {
out[count++] = buf[tail];
tail = (tail + 1) % size;
uint32_t t = tail;
uint32_t h = head;
while (t != h && count < maxLen) {
out[count++] = buf[t];
t = (t + 1) % size;
}
asm volatile("" ::: "memory"); // compiler barrier
tail = t;
return count;
}
}
+1 -38
View File
@@ -97,43 +97,6 @@ namespace Montauk {
}
static int Sys_Kill(int pid) {
// Refuse to kill PID 0 (init)
if (pid == 0) return -1;
// Refuse to kill the caller's own process
if (pid == Sched::GetCurrentPid()) return -1;
auto* proc = Sched::GetProcessByPid(pid);
if (!proc) return -1;
// Clean up any windows owned by this process (unmaps pixel pages from desktop)
WinServer::CleanupProcess(pid);
// Free I/O redirect buffers
if (proc->outBuf) {
Memory::g_pfa->Free(proc->outBuf);
proc->outBuf = nullptr;
}
if (proc->inBuf) {
Memory::g_pfa->Free(proc->inBuf);
proc->inBuf = nullptr;
}
// Free all user-space pages and page table structures
Memory::VMM::Paging::FreeUserHalf(proc->pml4Phys);
// Free kernel stack (safe — killed process isn't running on single-core)
if (proc->stackBase != 0) {
Memory::g_pfa->Free((void*)proc->stackBase, Sched::StackPages);
proc->stackBase = 0;
}
// Free the PML4 page
if (proc->pml4Phys != 0) {
Memory::g_pfa->Free((void*)Memory::HHDM(proc->pml4Phys));
proc->pml4Phys = 0;
}
proc->state = Sched::ProcessState::Terminated;
return 0;
return Sched::KillProcess(pid);
}
};
+14 -3
View File
@@ -181,20 +181,31 @@ namespace Drivers::PS2::Mouse {
}
int32_t GetX() {
return g_State.X;
g_StateLock.Acquire();
int32_t x = g_State.X;
g_StateLock.Release();
return x;
}
int32_t GetY() {
return g_State.Y;
g_StateLock.Acquire();
int32_t y = g_State.Y;
g_StateLock.Release();
return y;
}
uint8_t GetButtons() {
return g_State.Buttons;
g_StateLock.Acquire();
uint8_t b = g_State.Buttons;
g_StateLock.Release();
return b;
}
void SetBounds(int32_t maxX, int32_t maxY) {
g_StateLock.Acquire();
g_MaxX = maxX;
g_MaxY = maxY;
g_StateLock.Release();
}
void FlushState() {
+2
View File
@@ -200,12 +200,14 @@ namespace Fs::Vfs {
}
int VfsDriveList(int* outDrives, int maxEntries) {
vfsLock.Acquire();
int count = 0;
for (int i = 0; i < MaxDrives && count < maxEntries; i++) {
if (driveTable[i] != nullptr) {
outDrives[count++] = i;
}
}
vfsLock.Release();
return count;
}
+19
View File
@@ -12,6 +12,25 @@ namespace Hal {
// Enable SSE/SSE2 — required for userspace programs compiled with SSE.
// CR0: clear EM (bit 2), set MP (bit 1)
// CR4: set OSFXSR (bit 9) and OSXMMEXCPT (bit 10)
// Check if MONITOR/MWAIT is supported (CPUID.01H:ECX bit 3)
inline bool HasMwait() {
uint32_t ecx;
asm volatile("cpuid" : "=c"(ecx) : "a"(1) : "ebx", "edx");
return (ecx & (1 << 3)) != 0;
}
// Idle using MWAIT if available, otherwise HLT.
// MWAIT can enter deeper C-states (C1E/C3/C6) for better
// power and thermal efficiency than HLT (C1 only).
// The monitored address is arbitrary -- we just need MONITOR
// to arm the wake trigger; any interrupt wakes MWAIT.
inline void IdleWait(volatile uint64_t* monitorAddr) {
// MONITOR: set up the address monitoring range
asm volatile("monitor" :: "a"(monitorAddr), "c"(0), "d"(0));
// MWAIT: hint=0x00 (C1 state, platform-dependent deeper states)
asm volatile("mwait" :: "a"(0x00), "c"(0));
}
inline void EnableSSE() {
uint64_t cr0;
asm volatile("mov %%cr0, %0" : "=r"(cr0));
+6 -3
View File
@@ -118,7 +118,7 @@ namespace Hal {
return result;
}
void IDTEncodeInterrupt(size_t i, void* handler, uint8_t type_attr) {
void IDTEncodeInterrupt(size_t i, void* handler, uint8_t type_attr, uint8_t ist) {
uint64_t offset = (uint64_t)handler;
auto ptr = GetInterruptDescriptor(i);
@@ -126,7 +126,7 @@ namespace Hal {
.Offset1 = (uint16_t)(offset & 0x000000000000ffff),
.Selector = 0x08,
.IST = 0x00,
.IST = ist,
.TypeAttributes = type_attr,
.Offset2 = (uint16_t)((offset & 0x00000000ffff0000) >> 16),
@@ -139,7 +139,10 @@ namespace Hal {
template<int I, int N>
struct SetHandler {
static void run() {
IDTEncodeInterrupt(I, (void*)ExceptionHandler<I>, TrapGate);
// Use IST1 for NMI (2) and Double Fault (8) so they get a
// known-good stack even if the kernel stack has overflowed.
uint8_t ist = (I == 2 || I == 8) ? 1 : 0;
IDTEncodeInterrupt(I, (void*)ExceptionHandler<I>, TrapGate, ist);
SetHandler<I+1,N>::run();
}
};
+1 -1
View File
@@ -25,6 +25,6 @@ namespace Hal {
}__attribute__((packed));
void IDTInitialize();
void IDTEncodeInterrupt(std::size_t i, void* handler, uint8_t type_attr);
void IDTEncodeInterrupt(std::size_t i, void* handler, uint8_t type_attr, uint8_t ist = 0);
void IDTReload();
};
+27
View File
@@ -11,6 +11,7 @@
#include <Hal/MSR.hpp>
#include <Hal/Cpu.hpp>
#include <Memory/Paging.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Memory/HHDM.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -60,6 +61,14 @@ namespace Smp {
memset(&cpu.cpuTss, 0, sizeof(Hal::TSS64));
cpu.cpuTss.iopbOffset = sizeof(Hal::TSS64);
// Allocate a 4KB IST1 stack for Double Fault and NMI.
// These exceptions need a known-good stack to avoid triple
// faults when the normal kernel stack overflows.
void* istPage = Memory::g_pfa->AllocateZeroed();
if (istPage) {
cpu.cpuTss.ist1 = (uint64_t)istPage + 0x1000; // top of stack
}
// Copy the standard GDT layout
cpu.cpuGdt = {
{0xFFFF, 0, 0, 0x00, 0x00, 0}, // 0x00 Null
@@ -122,10 +131,17 @@ namespace Smp {
bsp.lapicId = Hal::LocalApic::GetId();
bsp.currentSlot = -1;
bsp.started = true;
bsp.hasMwait = Hal::HasMwait();
// BSP uses the global TSS (already set up in PrepareGDT)
bsp.tss = &Hal::g_tss;
// Allocate IST1 stack for the BSP (for Double Fault / NMI)
void* bspIstPage = Memory::g_pfa->AllocateZeroed();
if (bspIstPage) {
Hal::g_tss.ist1 = (uint64_t)bspIstPage + 0x1000;
}
// Set GS base for BSP
SetGSBase(&bsp);
@@ -187,16 +203,27 @@ namespace Smp {
// --- Calibrate and start APIC timer ---
Timekeeping::ApicTimerInitializeAP();
// --- Check MWAIT support ---
cpu->hasMwait = Hal::HasMwait();
// --- Signal that we are online ---
cpu->started = true;
// --- Enable interrupts and enter idle loop ---
asm volatile("sti");
// Use MWAIT for deeper C-states if available, otherwise HLT.
static volatile uint64_t s_idleMonitor = 0;
if (cpu->hasMwait) {
for (;;) {
Hal::IdleWait(&s_idleMonitor);
}
} else {
for (;;) {
asm volatile("hlt");
}
}
}
// ====================================================================
// Boot all APs
+1
View File
@@ -37,6 +37,7 @@ namespace Smp {
volatile bool started; // set by AP after init is complete
Hal::TSS64* tss; // pointer to this CPU's TSS
bool hasMwait; // CPU supports MONITOR/MWAIT
// Per-CPU GDT and TSS (APs use these; BSP uses globals)
Hal::BasicGDT cpuGdt __attribute__((aligned(16)));
+11 -2
View File
@@ -241,8 +241,17 @@ extern "C" void kmain() {
// Enable preemptive scheduling via the APIC timer
Timekeeping::EnableSchedulerTick();
// Main loop: halt until next interrupt
// Main loop: idle until next interrupt.
// Use MWAIT for deeper C-states if available, otherwise HLT.
auto* bspCpu = Smp::GetCpuData(0);
if (bspCpu && bspCpu->hasMwait) {
static volatile uint64_t s_bspIdleMonitor = 0;
for (;;) {
asm volatile ("hlt");
Hal::IdleWait(&s_bspIdleMonitor);
}
} else {
for (;;) {
asm volatile("hlt");
}
}
}
+6 -7
View File
@@ -56,11 +56,12 @@ namespace Memory
void* HeapAllocator::Request(size_t size) {
Lock.Acquire();
size_t sizeNeeded = size + sizeof(Header);
retry:
Node* current = head.next;
Node* prev = &head;
size_t sizeNeeded = size + sizeof(Header);
while (current != nullptr) {
if (current->size >= sizeNeeded) {
// Unlink the node
@@ -89,13 +90,11 @@ namespace Memory
current = current->next;
}
Lock.Release();
// First pass allocation failed -- grow the heap
// No suitable block found -- grow the heap under the lock so
// InsertToFreelist is protected from concurrent modification.
size_t pagesNeeded = (sizeNeeded + 0xFFF) / 0x1000;
InsertPagesToFreelist(pagesNeeded);
return Request(size);
goto retry;
}
void* HeapAllocator::Realloc(void* ptr, size_t size) {
+108 -10
View File
@@ -37,6 +37,11 @@ namespace Sched {
// The resumed process releases it.
static kcp::Spinlock schedLock;
// Approximate count of Ready processes. Incremented/decremented
// under schedLock. Idle CPUs check this to avoid scanning all 256
// process slots on every timer tick.
static volatile int readyCount = 0;
// The idle loop runs in the kernel PML4
static uint64_t GetKernelCR3() {
return (uint64_t)Memory::VMM::g_paging->PML4;
@@ -86,6 +91,7 @@ namespace Sched {
processTable[i].heapNext = 0;
processTable[i].args[0] = '\0';
processTable[i].runningOnCpu = -1;
processTable[i].killPending = false;
processTable[i].waitingForPid = -1;
processTable[i].sleepUntilTick = 0;
processTable[i].redirected = false;
@@ -248,6 +254,7 @@ namespace Sched {
Process& proc = processTable[slot];
proc.pid = nextPid++;
proc.state = ProcessState::Ready;
readyCount++;
{
int i = 0;
for (; i < 63 && vfsPath[i]; i++) proc.name[i] = vfsPath[i];
@@ -262,6 +269,7 @@ namespace Sched {
proc.userStackTop = UserStackTop - 8;
proc.heapNext = UserHeapBase;
proc.runningOnCpu = -1;
proc.killPending = false;
proc.waitingForPid = -1;
proc.sleepUntilTick = 0;
@@ -361,6 +369,7 @@ namespace Sched {
if (cpu->currentSlot >= 0) {
int oldSlot = cpu->currentSlot;
processTable[oldSlot].state = ProcessState::Ready;
readyCount++;
processTable[oldSlot].runningOnCpu = -1;
cpu->currentSlot = -1;
@@ -394,6 +403,7 @@ namespace Sched {
if (oldSlot >= 0) {
processTable[oldSlot].state = ProcessState::Ready;
readyCount++;
processTable[oldSlot].runningOnCpu = -1;
oldRspPtr = &processTable[oldSlot].savedRsp;
} else {
@@ -402,6 +412,7 @@ namespace Sched {
cpu->currentSlot = next;
processTable[next].state = ProcessState::Running;
readyCount--;
processTable[next].runningOnCpu = cpu->cpuIndex;
processTable[next].sliceRemaining = TimeSliceMs;
@@ -430,6 +441,7 @@ namespace Sched {
// BSP: wake sleeping processes and reclaim terminated slots
if (cpu->cpuIndex == 0) {
schedLock.Acquire();
uint64_t now = Timekeeping::GetTicks();
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].state == ProcessState::Blocked &&
@@ -437,8 +449,10 @@ namespace Sched {
now >= processTable[i].sleepUntilTick) {
processTable[i].sleepUntilTick = 0;
processTable[i].state = ProcessState::Ready;
readyCount++;
}
}
schedLock.Release();
// Reclaim terminated process memory (BSP only, once per tick)
ReclaimTerminated();
@@ -447,17 +461,21 @@ namespace Sched {
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) {
// Idle CPU. Check the approximate ready count to avoid
// scanning 256 process slots on every tick. On a 32-core
// system with 27 idle CPUs, this avoids ~7M cache-line
// reads/sec from the process table.
if (readyCount > 0) {
Schedule();
}
return;
}
}
// Nothing ready -- stay halted, don't touch schedLock
// Check if another CPU requested this process be killed.
// We are on the CPU running it, so ExitProcess is safe here.
if (processTable[slot].killPending) {
processTable[slot].killPending = false;
ExitProcess();
return;
}
@@ -492,9 +510,11 @@ namespace Sched {
}
Process& proc = processTable[slot];
proc.killPending = false;
int exitingPid = proc.pid;
// Clean up any windows owned by this process
WinServer::CleanupProcess(proc.pid);
WinServer::CleanupProcess(exitingPid);
// Free I/O redirect buffers
if (proc.outBuf) {
@@ -511,7 +531,6 @@ namespace Sched {
schedLock.Acquire();
int exitingPid = proc.pid;
proc.state = ProcessState::Terminated;
proc.runningOnCpu = -1;
@@ -520,6 +539,7 @@ namespace Sched {
if (processTable[i].state == ProcessState::Blocked &&
processTable[i].waitingForPid == exitingPid) {
processTable[i].state = ProcessState::Ready;
readyCount++;
processTable[i].waitingForPid = -1;
}
}
@@ -536,6 +556,7 @@ namespace Sched {
if (next >= 0) {
cpu->currentSlot = next;
processTable[next].state = ProcessState::Running;
readyCount--;
processTable[next].runningOnCpu = cpu->cpuIndex;
processTable[next].sliceRemaining = TimeSliceMs;
@@ -561,6 +582,81 @@ namespace Sched {
}
}
int KillProcess(int pid) {
// Refuse to kill PID 0 (init) or caller's own process
if (pid == 0) return -1;
if (pid == GetCurrentPid()) return -1;
schedLock.Acquire();
// Find the process by PID
int slot = -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 (slot < 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;
}
// Process is Ready or Blocked (not running on any CPU).
// Mark it Terminated so the scheduler won't pick it up.
int killedPid = proc.pid;
if (proc.state == ProcessState::Ready)
readyCount--;
proc.state = ProcessState::Terminated;
proc.killPending = false;
// Wake any processes blocked on this PID
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;
}
}
schedLock.Release();
// Safe to clean up resources now -- process is not running anywhere.
WinServer::CleanupProcess(killedPid);
if (proc.outBuf) {
Memory::g_pfa->Free(proc.outBuf);
proc.outBuf = nullptr;
}
if (proc.inBuf) {
Memory::g_pfa->Free(proc.inBuf);
proc.inBuf = nullptr;
}
Memory::VMM::Paging::FreeUserHalf(proc.pml4Phys);
// Kernel stack and PML4 freed by ReclaimTerminated on BSP tick.
return 0;
}
void BlockOnPid(int pid) {
// If the target is already dead, return immediately
if (!IsAlive(pid)) return;
@@ -607,6 +703,7 @@ namespace Sched {
if (next >= 0) {
cpu->currentSlot = next;
processTable[next].state = ProcessState::Running;
readyCount--;
processTable[next].runningOnCpu = cpu->cpuIndex;
processTable[next].sliceRemaining = TimeSliceMs;
@@ -651,6 +748,7 @@ namespace Sched {
if (next >= 0) {
cpu->currentSlot = next;
processTable[next].state = ProcessState::Running;
readyCount--;
processTable[next].runningOnCpu = cpu->cpuIndex;
processTable[next].sliceRemaining = TimeSliceMs;
+12 -6
View File
@@ -45,19 +45,20 @@ namespace Sched {
char args[256]; // Command-line arguments (set by parent via Spawn)
int runningOnCpu; // CPU index running this process (-1 if not running)
bool killPending = false; // Set by Sys_Kill when target is running on another CPU
// I/O redirection for GUI terminal
bool redirected = false;
int parentPid = -1;
uint8_t* outBuf = nullptr; // 4KB ring: child writes (print/putchar), parent reads
uint32_t outHead = 0;
uint32_t outTail = 0;
volatile uint32_t outHead = 0;
volatile uint32_t outTail = 0;
uint8_t* inBuf = nullptr; // 4KB ring: parent writes, child reads (getchar)
uint32_t inHead = 0;
uint32_t inTail = 0;
volatile uint32_t inHead = 0;
volatile uint32_t inTail = 0;
Montauk::KeyEvent keyBuf[64]; // parent injects, child reads (getkey/iskeyavailable)
uint32_t keyHead = 0;
uint32_t keyTail = 0;
volatile uint32_t keyHead = 0;
volatile uint32_t keyTail = 0;
static constexpr uint32_t IoBufSize = 4096;
// GUI terminal dimensions (set by desktop, read by SYS_TERMSIZE)
@@ -93,6 +94,11 @@ namespace Sched {
// Block the current process for the given number of milliseconds.
void BlockForSleep(uint64_t ms);
// Kill a process by PID. If the process is running on another CPU,
// sets a kill-pending flag checked on the next timer tick.
// Returns 0 on success, -1 on failure.
int KillProcess(int pid);
// Find a process by PID (returns nullptr if not found or not alive)
Process* GetProcessByPid(int pid);
+10 -12
View File
@@ -5,6 +5,7 @@
*/
#include "ApicTimer.hpp"
#include <atomic>
#include <Hal/Apic/Apic.hpp>
#include <Hal/Apic/Interrupts.hpp>
#include <Hal/SmpBoot.hpp>
@@ -36,7 +37,7 @@ namespace Timekeeping {
static constexpr uint32_t TIMER_HZ = 1000;
// Global state
static volatile uint64_t g_tickCount = 0;
static std::atomic<uint64_t> g_tickCount{0};
static uint32_t g_ticksPerMs = 0;
static bool g_schedEnabled = false;
@@ -47,7 +48,7 @@ namespace Timekeeping {
if (cpu->cpuIndex == 0) {
// BSP: increment global tick count and poll devices
g_tickCount = g_tickCount + 1;
g_tickCount.fetch_add(1, std::memory_order_relaxed);
Drivers::Net::E1000E::Poll();
Drivers::USB::Xhci::ProcessDeferredWork();
@@ -158,11 +159,11 @@ namespace Timekeeping {
}
uint64_t GetTicks() {
return g_tickCount;
return g_tickCount.load(std::memory_order_relaxed);
}
uint64_t GetMilliseconds() {
return g_tickCount; // 1 tick = 1 ms at 1000 Hz
return g_tickCount.load(std::memory_order_relaxed); // 1 tick = 1 ms at 1000 Hz
}
void EnableSchedulerTick() {
@@ -183,13 +184,10 @@ namespace Timekeeping {
}
void Sleep(uint64_t ms) {
uint64_t target = g_tickCount + ms;
while (g_tickCount < target) {
// Yield to other processes instead of hlt. Using hlt here causes
// a deadlock: if the timer ISR preempts us during hlt and context-
// switches away (via Tick -> Schedule), EOI is never sent, so no
// more timer interrupts fire and any process doing hlt freezes.
Sched::Schedule();
}
if (ms == 0) return;
// Use BlockForSleep to properly deschedule the process instead
// of busy-waiting with Schedule(). This frees the CPU for real
// work and avoids unnecessary scheduling overhead.
Sched::BlockForSleep(ms);
}
};