feat: Symmetric Multiprocessing, text editor improvements, merge doom libc, implement math functions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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.cpuIndex = apIndex;
|
||||
ap.lapicId = info->lapic_id;
|
||||
ap.currentSlot = -1;
|
||||
ap.started = false;
|
||||
|
||||
SetupPerCpuGdtTss(ap);
|
||||
info->extra_argument = (uint64_t)≈
|
||||
|
||||
// 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";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user