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
+9
View File
@@ -24,6 +24,7 @@
#include <Graphics/Cursor.hpp>
#include <Libraries/Memory.hpp>
#include <Hal/MSR.hpp>
#include <Hal/SmpBoot.hpp>
#include <Api/Syscall.hpp>
using namespace Kt;
@@ -424,6 +425,14 @@ namespace Hal {
Hal::LoadTSS();
reinitProgress(0x03); // Syscalls
Montauk::InitializeSyscalls();
// Re-set GS base for BSP (lost during S3)
{
auto* bsp = Smp::GetCpuData(0);
if (bsp) {
Hal::WriteMSR(0xC0000101, (uint64_t)bsp); // IA32_GS_BASE
Hal::WriteMSR(0xC0000102, 0); // IA32_KERNEL_GS_BASE
}
}
reinitProgress(0x04); // PAT
Hal::InitializePAT();
reinitProgress(0x05); // PIC
+2 -4
View File
@@ -26,7 +26,7 @@ namespace Montauk {
}
static void Sys_SleepMs(uint64_t ms) {
Timekeeping::Sleep(ms);
Sched::BlockForSleep(ms);
}
static int Sys_GetPid() {
@@ -34,9 +34,7 @@ namespace Montauk {
}
static void Sys_WaitPid(int pid) {
while (Sched::IsAlive(pid)) {
Sched::Schedule(); // yield until the process exits
}
Sched::BlockOnPid(pid);
}
static int Sys_Spawn(const char* path, const char* args) {
+1 -1
View File
@@ -333,7 +333,7 @@ namespace Montauk {
struct ProcInfo {
int32_t pid;
int32_t parentPid;
uint8_t state; // 0=Free, 1=Ready, 2=Running, 3=Terminated
uint8_t state; // 0=Free, 1=Ready, 2=Running, 3=Blocked, 4=Terminated
uint8_t _pad[3];
char name[64];
uint64_t heapUsed; // heapNext - UserHeapBase (bytes)
+97 -24
View File
@@ -10,15 +10,24 @@
#include <Memory/HHDM.hpp>
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Spinlock.hpp>
#include <Sched/Scheduler.hpp>
namespace WinServer {
static WindowSlot g_slots[MaxWindows];
static int g_uiScale = 1;
static kcp::Mutex wsLock;
// RAII lock guard for WinServer operations
struct WsGuard {
WsGuard() { wsLock.Acquire(); }
~WsGuard() { wsLock.Release(); }
};
int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h,
uint64_t& heapNext, uint64_t& outVa) {
WsGuard guard;
// Find a free slot
int slotIdx = -1;
for (int i = 0; i < MaxWindows; i++) {
@@ -56,12 +65,12 @@ namespace WinServer {
}
slot.title[tlen] = '\0';
// Allocate physical pages and map into owner's address space
// Allocate live pixel pages (app renders here) and snapshot pages
// (compositor reads here). Present copies live -> snapshot.
uint64_t userVa = heapNext;
for (int i = 0; i < numPages; i++) {
void* page = Memory::g_pfa->AllocateZeroed();
if (page == nullptr) {
// Cleanup on failure - mark slot unused
slot.used = false;
return -1;
}
@@ -71,6 +80,14 @@ namespace WinServer {
slot.used = false;
return -1;
}
// Allocate corresponding snapshot page
void* snapPage = Memory::g_pfa->AllocateZeroed();
if (snapPage == nullptr) {
slot.used = false;
return -1;
}
slot.snapshotPhysPages[i] = Memory::SubHHDM((uint64_t)snapPage);
}
slot.ownerVa = userVa;
@@ -84,6 +101,7 @@ namespace WinServer {
}
int Destroy(int windowId, int callerPid) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return -1;
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) return -1;
@@ -113,11 +131,14 @@ namespace WinServer {
}
}
// Free physical pixel pages
// Free physical pixel pages and snapshot pages
for (int i = 0; i < slot.pixelNumPages; i++) {
if (slot.pixelPhysPages[i] != 0) {
Memory::g_pfa->Free((void*)Memory::HHDM(slot.pixelPhysPages[i]));
}
if (slot.snapshotPhysPages[i] != 0) {
Memory::g_pfa->Free((void*)Memory::HHDM(slot.snapshotPhysPages[i]));
}
}
slot.used = false;
@@ -125,15 +146,34 @@ namespace WinServer {
}
int Present(int windowId, int callerPid) {
if (windowId < 0 || windowId >= MaxWindows) return -1;
// Validate ownership under the lock (brief)
wsLock.Acquire();
if (windowId < 0 || windowId >= MaxWindows) { wsLock.Release(); return -1; }
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) return -1;
if (!slot.used || slot.ownerPid != callerPid) { wsLock.Release(); return -1; }
int numPages = slot.pixelNumPages;
wsLock.Release();
// Snapshot memcpy OUTSIDE the lock. This is safe because only the
// owning process calls Present/Resize, and a process runs on one
// CPU at a time. Holding wsLock during an 8MB copy would block
// ALL other WinServer operations (Poll, Enumerate, SendEvent)
// across all CPUs, causing convoy stalls and lockups.
for (int i = 0; i < numPages; i++) {
void* src = (void*)Memory::HHDM(slot.pixelPhysPages[i]);
void* dst = (void*)Memory::HHDM(slot.snapshotPhysPages[i]);
memcpy(dst, src, 0x1000);
}
// Set dirty under lock
wsLock.Acquire();
slot.dirty = true;
wsLock.Release();
return 0;
}
int Poll(int windowId, int callerPid, Montauk::WinEvent* outEvent) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return -1;
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) return -1;
@@ -146,6 +186,7 @@ namespace WinServer {
}
int Enumerate(Montauk::WinInfo* outArray, int maxCount) {
WsGuard guard;
int count = 0;
for (int i = 0; i < MaxWindows && count < maxCount; i++) {
if (!g_slots[i].used) continue;
@@ -164,6 +205,7 @@ namespace WinServer {
}
uint64_t Map(int windowId, int callerPid, uint64_t callerPml4, uint64_t& heapNext) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return 0;
WindowSlot& slot = g_slots[windowId];
if (!slot.used) return 0;
@@ -175,8 +217,10 @@ namespace WinServer {
uint64_t userVa = heapNext;
// Map the SNAPSHOT pages (not live pages) so the compositor
// always reads a complete frame captured at Present time.
for (int i = 0; i < slot.pixelNumPages; i++) {
if (!Memory::VMM::Paging::MapUserIn(callerPml4, slot.pixelPhysPages[i],
if (!Memory::VMM::Paging::MapUserIn(callerPml4, slot.snapshotPhysPages[i],
userVa + (uint64_t)i * 0x1000)) {
return 0;
}
@@ -189,7 +233,8 @@ namespace WinServer {
return userVa;
}
int SendEvent(int windowId, const Montauk::WinEvent* event) {
// Internal: send event without acquiring lock (caller must hold wsLock)
static int SendEventLocked(int windowId, const Montauk::WinEvent* event) {
if (windowId < 0 || windowId >= MaxWindows) return -1;
WindowSlot& slot = g_slots[windowId];
if (!slot.used) return -1;
@@ -202,8 +247,14 @@ namespace WinServer {
return 0;
}
int SendEvent(int windowId, const Montauk::WinEvent* event) {
WsGuard guard;
return SendEventLocked(windowId, event);
}
int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH,
uint64_t& heapNext, uint64_t& outVa) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return -1;
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) return -1;
@@ -217,19 +268,21 @@ namespace WinServer {
int numPages = (int)((bufSize + 0xFFF) / 0x1000);
if (numPages > MaxPixelPages) return -1;
// Unmap old pixel pages from desktop's address space (if mapped)
if (slot.desktopVa != 0 && slot.desktopPid != 0) {
auto* desktopProc = Sched::GetProcessByPid(slot.desktopPid);
if (desktopProc) {
for (int i = 0; i < slot.pixelNumPages; i++) {
Memory::VMM::Paging::UnmapUserIn(
desktopProc->pml4Phys,
slot.desktopVa + (uint64_t)i * 0x1000);
}
}
}
// Do NOT unmap/free snapshot pages from the desktop here.
// The desktop may be running on another CPU with stale TLB entries
// pointing to the old snapshot pages. Unmapping + freeing them
// causes a page fault on the desktop when the TLB entry is evicted.
// Instead, leave them mapped -- the desktop will get new snapshot
// pages on its next Map call (desktopVa is invalidated below).
// The old snapshot pages remain in the desktop's page tables and
// are reclaimed by FreeUserHalf when the desktop exits.
// Unmap old pixel pages from owner's address space, then free them
// Unmap old LIVE pixel pages from owner's address space and free them.
// Free old snapshot pages too (don't leak). The desktop's PTEs still
// point to the old physical addresses, but we don't clear them (no
// UnmapUserIn for desktop) to avoid TLB-eviction page faults. The
// desktop may read one frame of garbage from the freed pages before
// re-mapping -- acceptable during a resize.
int oldNumPages = slot.pixelNumPages;
for (int i = 0; i < oldNumPages; i++) {
Memory::VMM::Paging::UnmapUserIn(
@@ -238,9 +291,13 @@ namespace WinServer {
Memory::g_pfa->Free((void*)Memory::HHDM(slot.pixelPhysPages[i]));
slot.pixelPhysPages[i] = 0;
}
if (slot.snapshotPhysPages[i] != 0) {
Memory::g_pfa->Free((void*)Memory::HHDM(slot.snapshotPhysPages[i]));
slot.snapshotPhysPages[i] = 0;
}
}
// Allocate new pages and map into owner's address space
// Allocate new live + snapshot pages and map live into owner's address space
uint64_t userVa = heapNext;
for (int i = 0; i < numPages; i++) {
void* page = Memory::g_pfa->AllocateZeroed();
@@ -250,6 +307,10 @@ namespace WinServer {
if (!Memory::VMM::Paging::MapUserIn(ownerPml4, physAddr, userVa + (uint64_t)i * 0x1000)) {
return -1;
}
void* snapPage = Memory::g_pfa->AllocateZeroed();
if (snapPage == nullptr) return -1;
slot.snapshotPhysPages[i] = Memory::SubHHDM((uint64_t)snapPage);
}
slot.width = newW;
@@ -267,6 +328,7 @@ namespace WinServer {
}
int SetCursor(int windowId, int callerPid, int cursor) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return -1;
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) return -1;
@@ -275,6 +337,7 @@ namespace WinServer {
}
int SetScale(int scale) {
WsGuard guard;
if (scale < 0) scale = 0;
if (scale > 2) scale = 2;
g_uiScale = scale;
@@ -286,7 +349,7 @@ namespace WinServer {
ev.scale.scale = scale;
for (int i = 0; i < MaxWindows; i++) {
if (g_slots[i].used) {
SendEvent(i, &ev);
SendEventLocked(i, &ev);
}
}
return 0;
@@ -297,6 +360,7 @@ namespace WinServer {
}
void CleanupProcess(int pid) {
WsGuard guard;
for (int i = 0; i < MaxWindows; i++) {
if (g_slots[i].used && g_slots[i].ownerPid == pid) {
Kt::KernelLogStream(Kt::INFO, "WinServer") << "Cleaning up window "
@@ -314,10 +378,19 @@ namespace WinServer {
}
}
// Do NOT free physical pixel pages here they are still mapped
// Do NOT free live pixel pages here -- they are still mapped
// in the owner's page tables and FreeUserHalf() (called right
// after CleanupProcess) will free them. Freeing here would
// cause a double-free, creating a cycle in the PFA free list.
// after CleanupProcess) will free them.
// DO free snapshot pages -- they are NOT in the owner's page
// tables (they were mapped into the desktop, and we just
// unmapped them above). If we don't free them here, they leak.
for (int p = 0; p < g_slots[i].pixelNumPages; p++) {
if (g_slots[i].snapshotPhysPages[p] != 0) {
Memory::g_pfa->Free((void*)Memory::HHDM(g_slots[i].snapshotPhysPages[p]));
g_slots[i].snapshotPhysPages[p] = 0;
}
}
g_slots[i].used = false;
}
+3 -2
View File
@@ -12,14 +12,15 @@ namespace WinServer {
static constexpr int MaxWindows = 8;
static constexpr int MaxEvents = 64;
static constexpr int MaxPixelPages = 2048; // up to 2048x1024 @ 32bpp = 8MB
static constexpr int MaxPixelPages = 8192; // up to 3840x2160 @ 32bpp = 32MB
struct WindowSlot {
bool used;
int ownerPid;
char title[64];
int width, height;
uint64_t pixelPhysPages[MaxPixelPages];
uint64_t pixelPhysPages[MaxPixelPages]; // live buffer (app writes here)
uint64_t snapshotPhysPages[MaxPixelPages]; // snapshot (compositor reads here)
int pixelNumPages;
uint64_t ownerVa; // VA in owner's address space
uint64_t desktopVa; // VA in desktop's address space (0 = not yet mapped)
+16
View File
@@ -15,4 +15,20 @@ namespace kcp {
void Acquire();
void Release();
};
// Non-interrupt-disabling mutex for subsystems that are only called
// from process/syscall context (never from interrupt handlers).
// Keeps interrupts enabled while spinning and while held.
class Mutex {
std::atomic_flag flag{ATOMIC_FLAG_INIT};
public:
void Acquire() {
while (flag.test_and_set(std::memory_order_acquire)) {
asm volatile("pause");
}
}
void Release() {
flag.clear(std::memory_order_release);
}
};
};
+13
View File
@@ -197,6 +197,19 @@ namespace Drivers::PS2::Mouse {
g_MaxY = maxY;
}
void FlushState() {
g_PacketIndex = 0;
// Drain any stale bytes from the PS/2 controller data port
constexpr uint16_t StatusPort = 0x64;
constexpr uint16_t DataPort_ = 0x60;
for (int i = 0; i < 32; i++) {
uint8_t status = Io::In8(StatusPort);
if (!(status & 0x01)) break; // output buffer empty
Io::In8(DataPort_); // discard byte
}
}
void InjectMouseReport(uint8_t buttons, int8_t deltaX, int8_t deltaY, int8_t scroll) {
g_StateLock.Acquire();
+5
View File
@@ -34,6 +34,11 @@ namespace Drivers::PS2::Mouse {
void SetBounds(int32_t maxX, int32_t maxY);
// Reset packet assembly state and drain pending PS/2 data.
// Call after boot to ensure clean mouse state regardless of
// any bytes lost during interrupt-heavy boot sequence.
void FlushState();
// Inject a mouse report from an external source (e.g., USB HID mouse)
void InjectMouseReport(uint8_t buttons, int8_t deltaX, int8_t deltaY, int8_t scroll);
+52 -44
View File
@@ -6,6 +6,7 @@
#include "Vfs.hpp"
#include <Terminal/Terminal.hpp>
#include <CppLib/Spinlock.hpp>
namespace Fs::Vfs {
@@ -18,6 +19,11 @@ namespace Fs::Vfs {
static FsDriver* driveTable[MaxDrives];
static HandleEntry handleTable[MaxHandles];
// Protects handle table and driver dispatch from concurrent CPU access.
// Uses Mutex (not Spinlock) so interrupts stay enabled while held --
// VFS is never called from interrupt context.
static kcp::Mutex vfsLock;
// Parse "N:/path" into drive number and local path.
// Returns true on success, sets outDrive and outPath.
static bool ParsePath(const char* path, int& outDrive, const char*& outPath) {
@@ -74,23 +80,18 @@ namespace Fs::Vfs {
int drive;
const char* localPath;
if (!ParsePath(path, drive, localPath)) {
Kt::KernelLogStream(Kt::ERROR, "VFS") << "Invalid path format";
return -1;
}
if (!ParsePath(path, drive, localPath)) return -1;
if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) return -1;
if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) {
Kt::KernelLogStream(Kt::ERROR, "VFS") << "Drive " << drive << " not registered";
return -1;
}
vfsLock.Acquire();
int localHandle = driveTable[drive]->Open(localPath);
if (localHandle < 0) return -1;
if (localHandle < 0) { vfsLock.Release(); return -1; }
int globalHandle = AllocHandle();
if (globalHandle < 0) {
driveTable[drive]->Close(localHandle);
Kt::KernelLogStream(Kt::ERROR, "VFS") << "No free handles";
vfsLock.Release();
return -1;
}
@@ -98,61 +99,67 @@ namespace Fs::Vfs {
handleTable[globalHandle].driveNumber = drive;
handleTable[globalHandle].localHandle = localHandle;
vfsLock.Release();
return globalHandle;
}
int VfsRead(int handle, uint8_t* buffer, uint64_t offset, uint64_t size) {
if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) return -1;
vfsLock.Acquire();
if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) { vfsLock.Release(); return -1; }
HandleEntry& entry = handleTable[handle];
return driveTable[entry.driveNumber]->Read(entry.localHandle, buffer, offset, size);
int result = driveTable[entry.driveNumber]->Read(entry.localHandle, buffer, offset, size);
vfsLock.Release();
return result;
}
uint64_t VfsGetSize(int handle) {
if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) return 0;
vfsLock.Acquire();
if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) { vfsLock.Release(); return 0; }
HandleEntry& entry = handleTable[handle];
return driveTable[entry.driveNumber]->GetSize(entry.localHandle);
uint64_t result = driveTable[entry.driveNumber]->GetSize(entry.localHandle);
vfsLock.Release();
return result;
}
void VfsClose(int handle) {
if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) return;
vfsLock.Acquire();
if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) { vfsLock.Release(); return; }
HandleEntry& entry = handleTable[handle];
driveTable[entry.driveNumber]->Close(entry.localHandle);
entry.inUse = false;
vfsLock.Release();
}
int VfsWrite(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size) {
if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) return -1;
vfsLock.Acquire();
if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) { vfsLock.Release(); return -1; }
HandleEntry& entry = handleTable[handle];
if (driveTable[entry.driveNumber]->Write == nullptr) return -1;
return driveTable[entry.driveNumber]->Write(entry.localHandle, buffer, offset, size);
if (driveTable[entry.driveNumber]->Write == nullptr) { vfsLock.Release(); return -1; }
int result = driveTable[entry.driveNumber]->Write(entry.localHandle, buffer, offset, size);
vfsLock.Release();
return result;
}
int VfsCreate(const char* path) {
int drive;
const char* localPath;
if (!ParsePath(path, drive, localPath)) {
Kt::KernelLogStream(Kt::ERROR, "VFS") << "Invalid path format for Create";
return -1;
}
if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) {
Kt::KernelLogStream(Kt::ERROR, "VFS") << "Drive " << drive << " not registered";
return -1;
}
if (!ParsePath(path, drive, localPath)) return -1;
if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) return -1;
if (driveTable[drive]->Create == nullptr) return -1;
vfsLock.Acquire();
int localHandle = driveTable[drive]->Create(localPath);
if (localHandle < 0) return -1;
if (localHandle < 0) { vfsLock.Release(); return -1; }
int globalHandle = AllocHandle();
if (globalHandle < 0) {
Kt::KernelLogStream(Kt::ERROR, "VFS") << "No free handles";
vfsLock.Release();
return -1;
}
@@ -160,6 +167,7 @@ namespace Fs::Vfs {
handleTable[globalHandle].driveNumber = drive;
handleTable[globalHandle].localHandle = localHandle;
vfsLock.Release();
return globalHandle;
}
@@ -168,12 +176,13 @@ namespace Fs::Vfs {
const char* localPath;
if (!ParsePath(path, drive, localPath)) return -1;
if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) return -1;
if (driveTable[drive]->Delete == nullptr) return -1;
return driveTable[drive]->Delete(localPath);
vfsLock.Acquire();
int result = driveTable[drive]->Delete(localPath);
vfsLock.Release();
return result;
}
int VfsMkdir(const char* path) {
@@ -184,7 +193,10 @@ namespace Fs::Vfs {
if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) return -1;
if (driveTable[drive]->Mkdir == nullptr) return -1;
return driveTable[drive]->Mkdir(localPath);
vfsLock.Acquire();
int result = driveTable[drive]->Mkdir(localPath);
vfsLock.Release();
return result;
}
int VfsDriveList(int* outDrives, int maxEntries) {
@@ -201,17 +213,13 @@ namespace Fs::Vfs {
int drive;
const char* localPath;
if (!ParsePath(path, drive, localPath)) {
Kt::KernelLogStream(Kt::ERROR, "VFS") << "Invalid path format for ReadDir";
return -1;
}
if (!ParsePath(path, drive, localPath)) return -1;
if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) return -1;
if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) {
Kt::KernelLogStream(Kt::ERROR, "VFS") << "Drive " << drive << " not registered";
return -1;
}
return driveTable[drive]->ReadDir(localPath, outNames, maxEntries);
vfsLock.Acquire();
int result = driveTable[drive]->ReadDir(localPath, outNames, maxEntries);
vfsLock.Release();
return result;
}
}
+21
View File
@@ -64,6 +64,27 @@ namespace Hal {
<< " max LVT=" << base::dec << (uint64_t)((version >> 16) & 0xFF);
}
void InitializeAP() {
if (!g_apicBase) return;
// Re-assert APIC Global Enable in MSR
uint64_t msrValue = ReadMSR(MSR_APIC_BASE);
if (!(msrValue & (1ULL << 11))) {
msrValue |= (1ULL << 11);
WriteMSR(MSR_APIC_BASE, msrValue);
}
// Enable APIC software enable + set spurious vector
uint32_t svr = ReadRegister(REG_SPURIOUS);
svr |= (1 << 8);
svr = (svr & 0xFFFFFF00) | SPURIOUS_VECTOR;
WriteRegister(REG_SPURIOUS, svr);
// Accept all interrupts
WriteRegister(REG_TPR, 0);
// No log here -- APs boot in parallel and the BSP logs a summary.
}
void Reinitialize() {
if (!g_apicBase) return;
+5
View File
@@ -33,6 +33,11 @@ namespace Hal {
void Initialize(uint64_t apicBasePhys);
// Initialize the Local APIC on an AP.
// Uses the same MMIO base as the BSP (each CPU's APIC is at the same
// physical address but routes to its own hardware).
void InitializeAP();
// Re-enable the Local APIC after S3 resume.
// The MMIO mapping and base address survive (they're in page tables / RAM).
// Only the SVR and TPR hardware registers need reprogramming.
+3 -3
View File
@@ -70,9 +70,9 @@ namespace Hal {
IoApic::RouteIrq(IRQ_KEYBOARD, IRQ_VECTOR_BASE + IRQ_KEYBOARD, bspApicId);
IoApic::RouteIrq(IRQ_MOUSE, IRQ_VECTOR_BASE + IRQ_MOUSE, bspApicId);
// Step 8: Enable interrupts
asm volatile("sti");
// Note: sti is NOT called here. The caller (Main.cpp) enables
// interrupts after setting up per-CPU GS base for SMP/SWAPGS.
KernelLogStream(OK, "APIC") << "APIC subsystem initialized, interrupts enabled";
KernelLogStream(OK, "APIC") << "APIC subsystem initialized";
}
};
+9 -3
View File
@@ -42,10 +42,16 @@ namespace Hal {
// C linkage dispatch function called from assembly stubs
extern "C" void HalIrqDispatch(uint64_t irqNumber) {
// Send EOI BEFORE calling the handler. The handler may context-switch
// (via Tick -> Schedule -> SchedContextSwitch) and never return here.
// If EOI is deferred until after the handler, the LAPIC timer vector
// stays masked on this CPU -- no more timer interrupts, no more
// scheduling, and the system eventually locks up.
// This is safe because the ISR runs with IF=0; the LAPIC won't
// deliver a new interrupt until iretq re-enables them.
Hal::LocalApic::SendEOI();
if (irqNumber < Hal::IRQ_COUNT && Hal::g_irqHandlers[irqNumber] != nullptr) {
Hal::g_irqHandlers[irqNumber]((uint8_t)irqNumber);
}
// Send End of Interrupt to the Local APIC
Hal::LocalApic::SendEOI();
}
+24 -6
View File
@@ -1,7 +1,7 @@
;
; IsrStubs.asm
; Hardware interrupt (IRQ) entry stubs for APIC
; Copyright (c) 2025 Daniel Hammer
; Hardware interrupt (IRQ) entry stubs for APIC (SMP-aware with SWAPGS)
; Copyright (c) 2025-2026 Daniel Hammer
;
[bits 64]
@@ -10,18 +10,27 @@ section .text
; External C++ handler function
extern HalIrqDispatch
; Macro to define an IRQ stub
; Each stub pushes the IRQ number and jumps to the common handler
; ====================================================================
; IRQ stub macro with SWAPGS support
; Each stub checks the saved CS to determine if we came from user mode.
; If so, SWAPGS is executed to switch GS to per-CPU data.
; ====================================================================
%macro IRQ_STUB 1
global IrqStub%1
IrqStub%1:
push rax ; Save rax (we use it for the IRQ number)
mov rax, %1 ; IRQ number
cmp qword [rsp + 8], 0x08 ; check saved CS: kernel?
je %%from_kernel
swapgs ; from user mode: swap to kernel GS
%%from_kernel:
push rax ; save rax (used for IRQ number)
mov rax, %1 ; IRQ number
jmp IrqCommon
%endmacro
; ====================================================================
; Common IRQ handler: saves all general-purpose registers,
; calls the C++ dispatch function, restores registers, and returns.
; ====================================================================
IrqCommon:
; rax is already on the stack and holds the IRQ number
push rcx
@@ -63,10 +72,17 @@ IrqCommon:
pop rcx
pop rax
; SWAPGS back if returning to user mode
cmp qword [rsp + 8], 0x08 ; check saved CS: kernel?
je .from_kernel_exit
swapgs ; returning to user mode: restore user GS
.from_kernel_exit:
iretq
; ====================================================================
; Define stubs for IRQs 0..47 (vectors 32..79)
; 0-23: legacy ISA IRQs via IOAPIC, 24-47: MSI vectors
; ====================================================================
IRQ_STUB 0
IRQ_STUB 1
IRQ_STUB 2
@@ -121,7 +137,9 @@ global IrqStubSpurious
IrqStubSpurious:
iretq
; ====================================================================
; Export the stub table for C++ to reference
; ====================================================================
section .data
global IrqStubTable
IrqStubTable:
+10 -1
View File
@@ -73,10 +73,15 @@ namespace Hal {
__attribute__((interrupt)) void ExceptionHandler(System::PanicFrame* frame)
{
uint64_t cs = GetExceptionCS(i, frame);
bool fromUser = (cs & 3) == 3;
// SWAPGS: if from user mode, GS base is user-defined.
// Swap to kernel per-CPU GS base so scheduler calls work.
if (fromUser) asm volatile("swapgs");
// If the fault originated in user-mode (ring 3), kill the process
// instead of panicking the entire system.
if ((cs & 3) == 3 && Sched::GetCurrentPid() >= 0) {
if (fromUser && Sched::GetCurrentPid() >= 0) {
auto* proc = Sched::GetCurrentProcessPtr();
Kt::KernelLogStream(Kt::ERROR, "Exception")
<< ExceptionStrings[i] << " in process \""
@@ -88,6 +93,10 @@ namespace Hal {
frame->InterruptVector = i;
Panic(ExceptionStrings[i], frame);
}
// Unreachable in practice (user faults exit, kernel faults panic),
// but balance the SWAPGS for correctness.
if (fromUser) asm volatile("swapgs");
}
void LoadIDT(IDTRStruct& idtr) {
+279
View File
@@ -0,0 +1,279 @@
/*
* SmpBoot.cpp
* Symmetric Multiprocessing bootstrap and AP entry
* Copyright (c) 2026 Daniel Hammer
*/
#include "SmpBoot.hpp"
#include <Hal/Apic/Apic.hpp>
#include <Hal/Apic/Interrupts.hpp>
#include <Hal/IDT.hpp>
#include <Hal/MSR.hpp>
#include <Hal/Cpu.hpp>
#include <Memory/Paging.hpp>
#include <Memory/HHDM.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <limine.h>
// Defined in Platform/Limine.hpp (included only by Main.cpp to avoid
// duplicating the LIMINE_BASE_REVISION tag).
extern volatile limine_mp_request mp_request;
#include <Libraries/Memory.hpp>
// Verify assembly offsets match struct layout
static_assert(__builtin_offsetof(Smp::CpuData, selfPtr) == CPUDATA_SELF_PTR, "CpuData offset mismatch: selfPtr");
static_assert(__builtin_offsetof(Smp::CpuData, kernelRsp) == CPUDATA_KERNEL_RSP, "CpuData offset mismatch: kernelRsp");
static_assert(__builtin_offsetof(Smp::CpuData, userRspScratch)== CPUDATA_USER_RSP, "CpuData offset mismatch: userRspScratch");
static_assert(__builtin_offsetof(Smp::CpuData, currentSlot) == CPUDATA_CURRENT_SLOT, "CpuData offset mismatch: currentSlot");
extern "C" void SyscallEntry();
// Assembly helpers (GDT.asm)
extern "C" void LoadGDT(Hal::GDTPointer* ptr);
extern "C" void ReloadSegments();
extern "C" void LoadTR();
using namespace Kt;
namespace Smp {
static CpuData g_cpus[MaxCPUs];
static int g_cpuCount = 0;
CpuData* GetCpuData(int index) {
if (index < 0 || index >= g_cpuCount) return nullptr;
return &g_cpus[index];
}
int GetCpuCount() {
return g_cpuCount;
}
// ====================================================================
// Per-CPU GDT/TSS setup
// ====================================================================
static void SetupPerCpuGdtTss(CpuData& cpu) {
// Zero the TSS
memset(&cpu.cpuTss, 0, sizeof(Hal::TSS64));
cpu.cpuTss.iopbOffset = sizeof(Hal::TSS64);
// Copy the standard GDT layout
cpu.cpuGdt = {
{0xFFFF, 0, 0, 0x00, 0x00, 0}, // 0x00 Null
{0xFFFF, 0, 0, 0x9A, 0xA0, 0}, // 0x08 KernelCode
{0xFFFF, 0, 0, 0x92, 0xA0, 0}, // 0x10 KernelData
{0xFFFF, 0, 0, 0xF2, 0xA0, 0}, // 0x18 UserData
{0xFFFF, 0, 0, 0xFA, 0xA0, 0}, // 0x20 UserCode
{0, 0, 0, 0, 0, 0}, // 0x28 TSS low
{0, 0, 0, 0, 0, 0}, // 0x30 TSS high
};
// Encode the 16-byte TSS descriptor
uint64_t base = (uint64_t)&cpu.cpuTss;
uint32_t limit = sizeof(Hal::TSS64) - 1;
// Low 8 bytes
cpu.cpuGdt.TSS.LimitLow = limit & 0xFFFF;
cpu.cpuGdt.TSS.BaseLow = base & 0xFFFF;
cpu.cpuGdt.TSS.BaseMiddle = (base >> 16) & 0xFF;
cpu.cpuGdt.TSS.AccessByte = 0x89; // Present, 64-bit TSS Available
cpu.cpuGdt.TSS.GranularityByte = (limit >> 16) & 0x0F;
cpu.cpuGdt.TSS.BaseHigh = (base >> 24) & 0xFF;
// High 8 bytes (base[63:32])
uint32_t baseUpper = (uint32_t)(base >> 32);
cpu.cpuGdt.TSSHigh.LimitLow = baseUpper & 0xFFFF;
cpu.cpuGdt.TSSHigh.BaseLow = (baseUpper >> 16) & 0xFFFF;
cpu.cpuGdt.TSSHigh.BaseMiddle = 0;
cpu.cpuGdt.TSSHigh.AccessByte = 0;
cpu.cpuGdt.TSSHigh.GranularityByte = 0;
cpu.cpuGdt.TSSHigh.BaseHigh = 0;
cpu.tss = &cpu.cpuTss;
}
// ====================================================================
// Set GS base MSRs for per-CPU data
// ====================================================================
static constexpr uint32_t IA32_GS_BASE = 0xC0000101;
static constexpr uint32_t IA32_KERNEL_GS_BASE = 0xC0000102;
static void SetGSBase(CpuData* cpu) {
// In kernel mode, GSBASE = per-CPU data pointer.
// KernelGSBASE = 0 (user GS base, swapped in on swapgs).
Hal::WriteMSR(IA32_GS_BASE, (uint64_t)cpu);
Hal::WriteMSR(IA32_KERNEL_GS_BASE, 0);
}
// ====================================================================
// BSP initialization
// ====================================================================
void InitBsp() {
memset(g_cpus, 0, sizeof(g_cpus));
CpuData& bsp = g_cpus[0];
bsp.selfPtr = (uint64_t)&bsp;
bsp.cpuIndex = 0;
bsp.lapicId = Hal::LocalApic::GetId();
bsp.currentSlot = -1;
bsp.started = true;
// BSP uses the global TSS (already set up in PrepareGDT)
bsp.tss = &Hal::g_tss;
// Set GS base for BSP
SetGSBase(&bsp);
g_cpuCount = 1;
KernelLogStream(OK, "SMP") << "BSP initialized (LAPIC ID " << (uint64_t)bsp.lapicId << ")";
}
// ====================================================================
// AP entry point
// Called by Limine when goto_address is written.
// RDI = pointer to limine_mp_info for this CPU.
// Runs on a 64KiB Limine-provided stack.
// ====================================================================
static void ApEntry(limine_mp_info* info) {
// Find our CpuData (stored in extra_argument by BootAPs)
CpuData* cpu = (CpuData*)info->extra_argument;
// --- Load per-CPU GDT ---
Hal::GDTPointer gdtPtr {
.Size = sizeof(Hal::BasicGDT) - 1,
.GDTAddress = (uint64_t)&cpu->cpuGdt,
};
LoadGDT(&gdtPtr);
ReloadSegments();
// --- Load TSS ---
LoadTR();
// --- Load shared IDT ---
Hal::IDTReload();
// --- Switch to kernel page tables ---
Memory::VMM::LoadCR3(Memory::VMM::g_paging->PML4);
// --- Enable SSE ---
Hal::EnableSSE();
// --- Set GS base ---
SetGSBase(cpu);
// --- Initialize local APIC ---
Hal::LocalApic::InitializeAP();
// --- Program SYSCALL MSRs ---
uint64_t efer = Hal::ReadMSR(Hal::IA32_EFER);
efer |= 1; // SCE
Hal::WriteMSR(Hal::IA32_EFER, efer);
uint64_t star = (0x0010ULL << 48) | (0x0008ULL << 32);
Hal::WriteMSR(Hal::IA32_STAR, star);
Hal::WriteMSR(Hal::IA32_LSTAR, (uint64_t)SyscallEntry);
Hal::WriteMSR(Hal::IA32_FMASK, 0x200);
// --- Program PAT (entry 1 = WC) ---
Hal::InitializePAT();
// --- Calibrate and start APIC timer ---
Timekeeping::ApicTimerInitializeAP();
// --- Signal that we are online ---
cpu->started = true;
// --- Enable interrupts and enter idle loop ---
asm volatile("sti");
for (;;) {
asm volatile("hlt");
}
}
// ====================================================================
// Boot all APs
// ====================================================================
void BootAPs() {
if (mp_request.response == nullptr) {
KernelLogStream(WARNING, "SMP") << "No MP response from bootloader - single CPU mode";
return;
}
auto* resp = mp_request.response;
uint64_t cpuCount = resp->cpu_count;
KernelLogStream(INFO, "SMP") << "Bootloader reports " << cpuCount << " CPU(s), BSP LAPIC ID "
<< (uint64_t)resp->bsp_lapic_id;
if (cpuCount <= 1) {
KernelLogStream(INFO, "SMP") << "Single CPU system - no APs to boot";
return;
}
if (cpuCount > (uint64_t)MaxCPUs) {
KernelLogStream(WARNING, "SMP") << "Clamping CPU count from " << cpuCount << " to " << (uint64_t)MaxCPUs;
cpuCount = MaxCPUs;
}
// Prepare all APs, then wake them all at once. This is safe because:
// - APs use BSP's timer calibration (no PIT contention)
// - APs don't log (no terminal contention)
// - Each AP's init is purely local (GDT, TSS, APIC, MSRs)
int apIndex = 1; // BSP is index 0
for (uint64_t i = 0; i < cpuCount; i++) {
limine_mp_info* info = resp->cpus[i];
if (info->lapic_id == resp->bsp_lapic_id) continue;
if (apIndex >= MaxCPUs) break;
CpuData& ap = g_cpus[apIndex];
ap.selfPtr = (uint64_t)&ap;
ap.cpuIndex = apIndex;
ap.lapicId = info->lapic_id;
ap.currentSlot = -1;
ap.started = false;
SetupPerCpuGdtTss(ap);
info->extra_argument = (uint64_t)&ap;
// Wake this AP (it runs ApEntry in parallel with other APs)
__atomic_store_n(&info->goto_address, (limine_goto_address)ApEntry, __ATOMIC_SEQ_CST);
apIndex++;
}
g_cpuCount = apIndex;
// Wait for all APs to come online
for (int i = 1; i < g_cpuCount; i++) {
volatile bool* flag = &g_cpus[i].started;
uint64_t timeout = 100000000;
while (!*flag && timeout > 0) {
asm volatile("pause");
timeout--;
}
if (!*flag) {
KernelLogStream(ERROR, "SMP") << "AP " << i << " (LAPIC "
<< (uint64_t)g_cpus[i].lapicId << ") failed to start";
}
}
// Count how many actually started
int onlineCount = 1; // BSP
for (int i = 1; i < g_cpuCount; i++) {
if (g_cpus[i].started) onlineCount++;
}
KernelLogStream(OK, "SMP") << onlineCount << " CPU(s) online";
}
}
+64
View File
@@ -0,0 +1,64 @@
/*
* SmpBoot.hpp
* Symmetric Multiprocessing bootstrap and per-CPU data
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Hal/GDT.hpp>
// ====================================================================
// Assembly-visible offsets into CpuData
// These MUST match the struct layout below. Verified by static_assert
// in SmpBoot.cpp.
// ====================================================================
#define CPUDATA_SELF_PTR 0
#define CPUDATA_KERNEL_RSP 8
#define CPUDATA_USER_RSP 16
#define CPUDATA_CURRENT_SLOT 24
namespace Smp {
static constexpr int MaxCPUs = 64;
struct CpuData {
// === Fields accessed by assembly (offsets defined above) ===
uint64_t selfPtr; // offset 0: pointer to self
uint64_t kernelRsp; // offset 8: kernel stack top for SYSCALL
uint64_t userRspScratch; // offset 16: scratch for saving user RSP
int32_t currentSlot; // offset 24: process table slot, -1 = idle
int32_t _pad0; // offset 28: padding
// === C++-only fields ===
int cpuIndex; // 0-based CPU index
uint32_t lapicId; // local APIC ID
uint64_t idleSavedRsp; // RSP saved when switching from idle to process
volatile bool started; // set by AP after init is complete
Hal::TSS64* tss; // pointer to this CPU's TSS
// Per-CPU GDT and TSS (APs use these; BSP uses globals)
Hal::BasicGDT cpuGdt __attribute__((aligned(16)));
Hal::TSS64 cpuTss __attribute__((aligned(16)));
};
// Get the current CPU's data (reads gs:0 which holds the self-pointer)
inline CpuData* GetCurrentCpuData() {
CpuData* ptr;
asm volatile("mov %%gs:0, %0" : "=r"(ptr));
return ptr;
}
// Get CPU data by index
CpuData* GetCpuData(int index);
// Number of online CPUs
int GetCpuCount();
// Initialize BSP per-CPU data (call before interrupts are enabled)
void InitBsp();
// Boot all Application Processors (call after all subsystems ready)
void BootAPs();
}
+20 -19
View File
@@ -1,28 +1,34 @@
;
; SyscallEntry.asm
; SYSCALL/SYSRET entry point and user-mode transition
; Copyright (c) 2025 Daniel Hammer
; SYSCALL/SYSRET entry point and user-mode transition (SMP-aware)
; Copyright (c) 2025-2026 Daniel Hammer
;
[bits 64]
section .text
extern SyscallDispatch
extern g_kernelRsp
; ============================================================
; SyscallEntry — called by the SYSCALL instruction
; ====================================================================
; Per-CPU data offsets (must match CpuData in SmpBoot.hpp)
; ====================================================================
%define CPUDATA_KERNEL_RSP 8
%define CPUDATA_USER_RSP 16
; ====================================================================
; SyscallEntry -- called by the SYSCALL instruction
; RCX = user RIP, R11 = user RFLAGS, RAX = syscall number
; Args: RDI, RSI, RDX, R10, R8, R9
; Interrupts are masked (FMASK clears IF)
; ============================================================
; ====================================================================
global SyscallEntry
SyscallEntry:
mov [rel g_userRsp], rsp ; stash user RSP
mov rsp, [rel g_kernelRsp] ; switch to kernel stack
swapgs ; GS now points to per-CPU CpuData
mov [gs:CPUDATA_USER_RSP], rsp ; save user RSP to per-CPU scratch
mov rsp, [gs:CPUDATA_KERNEL_RSP] ; switch to per-CPU kernel stack
; Build SyscallFrame on kernel stack (push order matches struct)
push qword [rel g_userRsp] ; user_rsp
push qword [gs:CPUDATA_USER_RSP] ; user_rsp
push rcx ; user_rip
push r11 ; user_rflags
push rax ; syscall_nr
@@ -61,13 +67,14 @@ SyscallEntry:
pop rcx ; user RIP
pop rsp ; user RSP
swapgs ; restore user GS
o64 sysret
; ============================================================
; JumpToUserMode initial transition to ring 3 via IRETQ
; ====================================================================
; JumpToUserMode -- initial transition to ring 3 via IRETQ
; RDI = user RIP (entry point)
; RSI = user RSP (top of user stack)
; ============================================================
; ====================================================================
global JumpToUserMode
JumpToUserMode:
mov ax, 0x1B ; UserData | RPL3
@@ -79,11 +86,5 @@ JumpToUserMode:
push 0x202 ; RFLAGS (IF=1)
push 0x23 ; CS = UserCode | RPL3
push rdi ; RIP = entry point
swapgs ; switch from kernel GS to user GS
iretq
; ============================================================
; BSS: scratch space for user RSP save
; ============================================================
section .bss
global g_userRsp
g_userRsp: resq 1
+15
View File
@@ -39,6 +39,7 @@
#include <Fs/FsProbe.hpp>
#include <Sched/Scheduler.hpp>
#include <Api/Syscall.hpp>
#include <Hal/SmpBoot.hpp>
using namespace Kt;
namespace Memory {
@@ -145,6 +146,13 @@ extern "C" void kmain() {
Hal::ApicInitialize(g_acpi.GetXSDT());
// Set up BSP per-CPU data (GS base) before enabling interrupts.
// ISR stubs use SWAPGS which requires GS base to point to CpuData.
Smp::InitBsp();
// Now safe to enable interrupts (SWAPGS-aware ISR stubs are installed)
asm volatile("sti");
// Initialize ACPI events (SCI, power button) after APIC is ready
Hal::AcpiEvents::Initialize(g_acpi.GetXSDT());
@@ -220,6 +228,13 @@ extern "C" void kmain() {
Sched::Initialize();
// Boot Application Processors (all subsystems ready, APs can schedule)
Smp::BootAPs();
// Flush any stale PS/2 mouse bytes that accumulated during boot
// (edge-triggered IRQs can be lost while spinlocks disable interrupts)
Drivers::PS2::Mouse::FlushState();
Kt::SuppressKernelLog();
Sched::Spawn("0:/os/init.elf");
+27 -12
View File
@@ -2,10 +2,17 @@
#include <Memory/PageFrameAllocator.hpp>
#include <Common/Panic.hpp>
#include <Memory/HHDM.hpp>
#include <CppLib/Spinlock.hpp>
namespace Memory::VMM {
Paging* g_paging = nullptr;
// Protects user page table modifications from concurrent SMP access
static kcp::Mutex pagingLock;
void LockUserPaging() { pagingLock.Acquire(); }
void UnlockUserPaging() { pagingLock.Release(); }
extern "C" uint64_t KernelStartSymbol;
std::uint64_t GetPhysKernelAddress(std::uint64_t virtualAddress) {
@@ -184,7 +191,9 @@ namespace Memory::VMM {
}
bool Paging::MapUserIn(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
pagingLock.Acquire();
if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) {
pagingLock.Release();
Panic("Non-aligned address in Paging::MapUserIn!", nullptr);
}
@@ -211,22 +220,25 @@ namespace Memory::VMM {
PageTable* pml4 = (PageTable*)pml4Phys;
auto pml3 = walkLevel(pml4, va.GetL4Index());
if (!pml3) return false;
if (!pml3) { pagingLock.Release(); return false; }
auto pml2 = walkLevel(pml3, va.GetL3Index());
if (!pml2) return false;
if (!pml2) { pagingLock.Release(); return false; }
auto pml1 = walkLevel(pml2, va.GetL2Index());
if (!pml1) return false;
if (!pml1) { pagingLock.Release(); return false; }
PageTableEntry* pageEntry = (PageTableEntry*)Memory::HHDM(&pml1->entries[va.GetPageIndex()]);
pageEntry->Present = true;
pageEntry->Writable = true;
pageEntry->Supervisor = 1;
pageEntry->Address = physicalAddress >> 12;
pagingLock.Release();
return true;
}
bool Paging::MapUserInWC(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
pagingLock.Acquire();
if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) {
pagingLock.Release();
Panic("Non-aligned address in Paging::MapUserInWC!", nullptr);
}
@@ -251,25 +263,27 @@ namespace Memory::VMM {
PageTable* pml4 = (PageTable*)pml4Phys;
auto pml3 = walkLevel(pml4, va.GetL4Index());
if (!pml3) return false;
if (!pml3) { pagingLock.Release(); return false; }
auto pml2 = walkLevel(pml3, va.GetL3Index());
if (!pml2) return false;
if (!pml2) { pagingLock.Release(); return false; }
auto pml1 = walkLevel(pml2, va.GetL2Index());
if (!pml1) return false;
if (!pml1) { pagingLock.Release(); return false; }
PageTableEntry* pageEntry = (PageTableEntry*)Memory::HHDM(&pml1->entries[va.GetPageIndex()]);
pageEntry->Present = true;
pageEntry->Writable = true;
pageEntry->Supervisor = 1;
pageEntry->WriteThrough = true; // PWT=1, PCD=0 PAT entry 1 = WC
pageEntry->WriteThrough = true; // PWT=1, PCD=0 -> PAT entry 1 = WC
pageEntry->Address = physicalAddress >> 12;
pagingLock.Release();
return true;
}
void Paging::UnmapUserIn(std::uint64_t pml4Phys, std::uint64_t virtualAddress) {
pagingLock.Acquire();
VirtualAddress va(virtualAddress);
// Walk without allocating if any level is absent, nothing is mapped.
// Walk without allocating -- if any level is absent, nothing is mapped.
auto walkRead = [](PageTable* table, uint64_t index) -> PageTable* {
PageTableEntry* entry = (PageTableEntry*)Memory::HHDM(&table->entries[index]);
if (!entry->Present) return nullptr;
@@ -278,20 +292,21 @@ namespace Memory::VMM {
PageTable* pml4 = (PageTable*)pml4Phys;
auto pml3 = walkRead(pml4, va.GetL4Index());
if (!pml3) return;
if (!pml3) { pagingLock.Release(); return; }
auto pml2 = walkRead(pml3, va.GetL3Index());
if (!pml2) return;
if (!pml2) { pagingLock.Release(); return; }
auto pml1 = walkRead(pml2, va.GetL2Index());
if (!pml1) return;
if (!pml1) { pagingLock.Release(); return; }
PageTableEntry* pageEntry = (PageTableEntry*)Memory::HHDM(&pml1->entries[va.GetPageIndex()]);
if (!pageEntry->Present) return;
if (!pageEntry->Present) { pagingLock.Release(); return; }
// Clear the entire 8-byte PTE
*(uint64_t*)pageEntry = 0;
// Invalidate TLB for this virtual address
asm volatile("invlpg (%0)" :: "r"(virtualAddress) : "memory");
pagingLock.Release();
}
void Paging::FreeUserHalf(std::uint64_t pml4Phys) {
+7
View File
@@ -124,6 +124,13 @@ public:
extern Paging* g_paging;
// Protects user page table modifications from concurrent access.
// Required for SMP: e.g. WinServer::Resize unmaps pages from the
// desktop's page tables while the desktop may be allocating pages
// on another CPU.
void LockUserPaging();
void UnlockUserPaging();
extern "C" uint64_t GetCR3();
extern "C" void LoadCR3(PageTable* PML4);
+10
View File
@@ -76,6 +76,16 @@ namespace {
};
}
// MP request is outside the anonymous namespace so SmpBoot.cpp can
// reference it via extern.
__attribute__((used, section(".limine_requests")))
volatile limine_mp_request mp_request = {
.id = LIMINE_MP_REQUEST,
.revision = 0,
.response = nullptr,
.flags = 0
};
// Finally, define the start and end markers for the Limine requests.
// These can also be moved anywhere, to any .cpp file, as seen fit.
+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;
+15 -4
View File
@@ -1,7 +1,7 @@
/*
* Scheduler.hpp
* Preemptive process scheduler with user-mode support
* Copyright (c) 2025 Daniel Hammer
* Preemptive process scheduler with SMP support
* Copyright (c) 2025-2026 Daniel Hammer
*/
#pragma once
@@ -24,12 +24,15 @@ namespace Sched {
Free,
Ready,
Running,
Blocked, // Waiting (e.g. waitpid) -- not schedulable
Terminated
};
struct Process {
int pid;
ProcessState state;
int waitingForPid; // PID this process is blocked on (-1 if none)
uint64_t sleepUntilTick; // Tick deadline for SYS_SLEEP_MS (0 = not sleeping)
char name[64];
uint64_t savedRsp;
uint64_t stackBase; // Bottom of allocated kernel stack (lowest address)
@@ -41,6 +44,8 @@ namespace Sched {
uint64_t heapNext; // Simple bump allocator for user heap
char args[256]; // Command-line arguments (set by parent via Spawn)
int runningOnCpu; // CPU index running this process (-1 if not running)
// I/O redirection for GUI terminal
bool redirected = false;
int parentPid = -1;
@@ -67,7 +72,7 @@ namespace Sched {
int Spawn(const char* vfsPath, const char* args = nullptr);
void Schedule();
// Called from the APIC timer handler on every tick.
// Called from the APIC timer handler on every tick (per-CPU).
void Tick();
// Get the PID of the currently running process (-1 if idle)
@@ -79,9 +84,15 @@ namespace Sched {
// Called by terminated processes to mark themselves done
void ExitProcess();
// Check if a process is still alive (Ready or Running)
// Check if a process is still alive (Ready, Running, or Blocked)
bool IsAlive(int pid);
// Block the current process until the given PID exits.
void BlockOnPid(int pid);
// Block the current process for the given number of milliseconds.
void BlockForSleep(uint64_t ms);
// Find a process by PID (returns nullptr if not found or not alive)
Process* GetProcessByPid(int pid);
+13
View File
@@ -13,6 +13,7 @@
#include "../Libraries/String.hpp"
#include "../Libraries/Memory.hpp"
#include <CppLib/CString.hpp>
#include <CppLib/Spinlock.hpp>
namespace Kt {
flanterm_context *ctx;
@@ -22,6 +23,11 @@ namespace Kt {
uint32_t g_kernelLogDepth = 0;
bool g_suppressKernelLog = false;
// Protects flanterm writes from concurrent CPU access.
// Mutex (not Spinlock) so interrupts stay enabled -- prevents dropped
// PS/2 mouse/keyboard bytes during log output.
kcp::Mutex g_termLock;
// 64KB ring buffer for kernel log messages
static constexpr uint64_t KLOG_BUF_SIZE = 65536;
static char g_klogBuf[KLOG_BUF_SIZE];
@@ -228,6 +234,13 @@ namespace Kt {
}
}
// Once a graphical app takes over, suppress ALL flanterm writes
// (SYS_PRINT from user processes, etc.) to avoid painting text
// over the GUI framebuffer.
if (g_suppressKernelLog && g_kernelLogDepth == 0) {
return;
}
if (c == '\n') {
flanterm_write(ctx, "\r\n", 2);
return;
+9 -1
View File
@@ -10,6 +10,7 @@
#include <Libraries/String.hpp>
#include <CppLib/Stream.hpp>
#include <CppLib/Spinlock.hpp>
using namespace kcp;
@@ -42,6 +43,11 @@ namespace Kt
void Putchar(char c);
void Print(const char *text);
// Terminal mutex - protects flanterm from concurrent CPU access.
// Uses Mutex (not Spinlock) to keep interrupts enabled while held,
// preventing dropped PS/2 mouse/keyboard bytes during log output.
extern kcp::Mutex g_termLock;
void UpdatePanelBar(const char* panelText);
void Rescale(std::size_t font_scale_x, std::size_t font_scale_y);
@@ -138,7 +144,8 @@ public:
}
}
KernelLogStream(KernelLogLevel level, const char* desiredComponentName) { /* level for potential future rich event logging */
KernelLogStream(KernelLogLevel level, const char* desiredComponentName) {
g_termLock.Acquire();
g_kernelLogDepth++;
componentName = desiredComponentName;
@@ -148,6 +155,7 @@ public:
~KernelLogStream() {
localStream << newline;
g_kernelLogDepth--;
g_termLock.Release();
}
template<typename T>
+25 -8
View File
@@ -7,6 +7,7 @@
#include "ApicTimer.hpp"
#include <Hal/Apic/Apic.hpp>
#include <Hal/Apic/Interrupts.hpp>
#include <Hal/SmpBoot.hpp>
#include <Io/IoPort.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -40,15 +41,18 @@ namespace Timekeeping {
static bool g_schedEnabled = false;
// Timer IRQ handler: increment tick count, poll NIC, and drive scheduler
// Timer IRQ handler: BSP handles timekeeping+polling, all CPUs run scheduler
static void TimerHandler(uint8_t) {
g_tickCount = g_tickCount + 1;
auto* cpu = Smp::GetCurrentCpuData();
// In polling mode, drain the NIC's RX ring from timer context
// (equivalent to a real NIC IRQ handler, runs with interrupts disabled)
Drivers::Net::E1000E::Poll();
Drivers::USB::Xhci::ProcessDeferredWork();
Drivers::USB::HidKeyboard::Tick();
if (cpu->cpuIndex == 0) {
// BSP: increment global tick count and poll devices
g_tickCount = g_tickCount + 1;
Drivers::Net::E1000E::Poll();
Drivers::USB::Xhci::ProcessDeferredWork();
Drivers::USB::HidKeyboard::Tick();
}
if (g_schedEnabled) {
Sched::Tick();
@@ -165,12 +169,25 @@ namespace Timekeeping {
g_schedEnabled = true;
}
void ApicTimerInitializeAP() {
// Use the BSP's calibrated ticks-per-ms value directly.
// All cores share the same bus clock, so the APIC timer rate is
// identical. This avoids PIT contention during AP boot.
if (g_ticksPerMs == 0) return;
// Configure periodic timer at 1000 Hz (same vector as BSP)
uint32_t lvt = (Hal::IRQ_VECTOR_BASE + Hal::IRQ_TIMER) | LVT_PERIODIC;
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_DIVIDE, DIVIDE_BY_16);
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_LVT, lvt);
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_INITIAL, g_ticksPerMs);
}
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
// switches away (via Tick -> Schedule), EOI is never sent, so no
// more timer interrupts fire and any process doing hlt freezes.
Sched::Schedule();
}
+3
View File
@@ -11,6 +11,9 @@ namespace Timekeeping {
// Initialize the APIC timer: calibrate against PIT, start periodic interrupts
void ApicTimerInitialize();
// Initialize the APIC timer on an AP (calibrate + start, no IRQ handler registration)
void ApicTimerInitializeAP();
// Reinitialize the APIC timer after S3 resume using the previously
// calibrated tick rate. Skips PIT calibration and IRQ registration
// (both survive in RAM). Only reprograms the timer hardware registers.