From b3569a2b90eb818825c405021e18b6cb52e2707a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Wilczy=C5=84ski?= Date: Tue, 26 May 2026 13:32:43 +0200 Subject: [PATCH] better scheduler time allocation, proper quota tracking --- OVMF/OVMF_VARS_4M_d.fd | Bin 540672 -> 540672 bytes code/hal/hal_timer.c | 436 ++++++++++++++++++++-------------------- code/kernel/ke_sched.c | 115 +++++++++-- code/kernel/ke_sched.h | 18 +- code/kernel/ke_thread.c | 17 +- code/kernel/ke_thread.h | 11 +- 6 files changed, 362 insertions(+), 235 deletions(-) diff --git a/OVMF/OVMF_VARS_4M_d.fd b/OVMF/OVMF_VARS_4M_d.fd index 2e3b119c3b3b081e5e6acefe2d59ed513fbfd8ab..8d8bdfe408b14911d6377f29f23050066a4110e8 100644 GIT binary patch delta 118 zcmZo@P;6*W+~B7-T`ZkhX>x&{0!t4I1H)tk6_v>XdB-543-lLFpOwWtWwMcq()NHH s<_(LtFUVm|z)MzeZ4+R5z`T8eFpC0Dn*vLl0&ANBTbly=G6fC=0On>Xx&QzG literal 540672 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 -#include -#include -#include -#include - -#define LAPIC_TIMER_LVT 0x320 -#define LAPIC_TIMER_INIT 0x380 -#define LAPIC_TIMER_CUR 0x390 -#define LAPIC_TIMER_DIV 0x3E0 - -#define TIMER_VECTOR 0x20 // vector 0x20 = IRQ 0 slot in IDT -#define TIMER_PERIODIC (1 << 17) -#define TIMER_MASKED (1 << 16) -#define TIMER_DIV16 0x3 - -#define QUANTUM_MS 10 -#define PIT_HZ 1193182UL - -extern HAL_API gApi; -static volatile uint32_t* gLapic = NULL; -static uint32_t gApicTicksPerUs = 0; -static uint32_t gApicTicksPerQuantum = 0; -static uint64_t gHpetTicksPerUs = 0; -static uint64_t gHpetHz = 0; -static volatile uint64_t* gHpet = NULL; -#define LAPIC_REG(off) (*(volatile uint32_t *)((uint8_t *)gLapic + (off))) - -static volatile uint64_t gTicks = 0; -void (* volatile gTimerCallback)(void) = NULL; - -static inline void outb(uint16_t port, uint8_t val) { __outbyte(port, val); } -static inline uint8_t inb(uint16_t port) { return __inbyte(port); } - -static void TimerIsr(void) { - gTicks++; - if (gTimerCallback) gTimerCallback(); - // EOI is sent by KeIrqDispatch after this returns -} - -static uint64_t QueryTicks(void) { - // Prefer HPET when present: it is already a monotonically increasing main - // counter and avoids races between the LAPIC periodic tick and the current - // countdown register. - if (gHpet && gHpetHz) { - uint64_t counter = gHpet[30]; // offset 0xF0: main counter - uint64_t seconds = counter / gHpetHz; - uint64_t rem = counter % gHpetHz; - return seconds * 1000000ull + (rem * 1000000ull) / gHpetHz; - } - - // Fallback: combine the coarse periodic tick with the current LAPIC - // countdown. Read the coarse tick twice so an interrupt between gTicks and - // LAPIC_TIMER_CUR cannot make time jump backwards by almost one quantum. - uint64_t baseTicks1; - uint64_t baseTicks2; - uint32_t current; - do { - baseTicks1 = gTicks; - current = LAPIC_REG(LAPIC_TIMER_CUR); - baseTicks2 = gTicks; - } while (baseTicks1 != baseTicks2); - - uint32_t initial = gApicTicksPerQuantum; - - uint32_t elapsedApicTicks = 0; - if (current < initial) { - elapsedApicTicks = initial - current; - } - - uint64_t subQuantumUs = 0; - if (gApicTicksPerUs) - subQuantumUs = elapsedApicTicks / gApicTicksPerUs; - - return (baseTicks1 * QUANTUM_MS * 1000ull) + subQuantumUs; -} - -static void Stall(uint32_t us) { - // Priority 1: Use HPET (Highest Precision) - if (gHpet && gHpetTicksPerUs > 0) { - uint64_t start = gHpet[30]; // Offset 0xF0 is the Main Counter - uint64_t delta = (uint64_t)us * gHpetTicksPerUs; - while ((gHpet[30] - start) < delta) { - _mm_pause(); // Hint to CPU we are in a spin-loop - } - return; - } - - // Priority 2: Use APIC Timer (High Precision, but handles wrap-around) - if (gApicTicksPerUs > 0) { - uint32_t total_ticks = us * gApicTicksPerUs; - uint32_t last_val = LAPIC_REG(LAPIC_TIMER_CUR); - uint32_t elapsed = 0; - - while (elapsed < total_ticks) { - uint32_t current = LAPIC_REG(LAPIC_TIMER_CUR); - if (current < last_val) { - elapsed += (last_val - current); - } else if (current > last_val) { - // Handled the reset/wrap at the end of QUANTUM_MS - uint32_t init_val = LAPIC_REG(LAPIC_TIMER_INIT); - elapsed += (last_val + (init_val - current)); - } - last_val = current; - _mm_pause(); - } - return; - } - - // Priority 3: Fallback to Port 0x80 - for (uint32_t i = 0; i < us; i++) { - __outbyte(0x80, 0); - } -} - -// Returns HPET ticks/second, or 0 if HPET is unavailable. -static uint64_t HpetInit(uintptr_t hpetPhys) { - // HPET register block, accessed through physical window - volatile uint64_t *hpet = (volatile uint64_t *)gApi.PhysToVirt(hpetPhys); - - // General Capabilities register (offset 0): - // Bits 63:32 = counter clock period in femtoseconds - uint64_t caps = hpet[0]; - uint32_t periodFs = (uint32_t)(caps >> 32); - if (!periodFs) return 0; - - // Enable the HPET main counter (General Configuration, offset 0x10, bit 0) - hpet[2] |= 1; // offset 0x10 / 8 = index 2 - - return (uint64_t)1e15 / periodFs; // ticks per second -} - -// Use PIT channel 2 one-shot to measure how many APIC ticks elapse in 10 ms, then return ticks/ms. -static uint32_t CalibrateApicTimerViaPit(void) { - // Divide by 16 - LAPIC_REG(LAPIC_TIMER_DIV) = TIMER_DIV16; - // Mask, set vector - leave masked during calibration - LAPIC_REG(LAPIC_TIMER_LVT) = TIMER_MASKED | TIMER_VECTOR; - // Load max count - LAPIC_REG(LAPIC_TIMER_INIT) = 0xFFFFFFFF; - - // PIT channel 2: one-shot, ~10 ms - uint16_t pit_count = (uint16_t)(PIT_HZ / 100); // 11931 ≈ 10 ms - outb(0x61, (inb(0x61) & ~0x02) | 0x01); // gate on, speaker off - outb(0x43, 0xB0); // ch2, lo/hi, mode 0, binary - outb(0x42, (uint8_t)(pit_count & 0xFF)); - outb(0x42, (uint8_t)(pit_count >> 8)); - - // Wait for OUT2 (bit 5 of port 0x61) to go high - while (!(inb(0x61) & 0x20)) _mm_pause(); - - uint32_t elapsed = 0xFFFFFFFF - LAPIC_REG(LAPIC_TIMER_CUR); - return elapsed / 10; // ticks per ms -} - -// Use HPET main counter to measure how many APIC ticks elapse in 10 ms, then return ticks/ms. -static uint32_t CalibrateApicTimerViaHpet(uintptr_t hpetPhys) { - volatile uint64_t *hpet = (volatile uint64_t *)gApi.PhysToVirt(hpetPhys); - uint64_t hpetHz = HpetInit(hpetPhys); - if (!hpetHz) return 0; - - // Ticks in 10 ms = hpetHz / 100 - uint64_t ticksFor10ms = hpetHz / 100; - - LAPIC_REG(LAPIC_TIMER_DIV) = TIMER_DIV16; - LAPIC_REG(LAPIC_TIMER_LVT) = TIMER_MASKED | TIMER_VECTOR; - LAPIC_REG(LAPIC_TIMER_INIT) = 0xFFFFFFFF; - - // HPET main counter is at offset 0xF0, index 0xF0/8 = 30 - uint64_t hpetStart = hpet[30]; - while ((hpet[30] - hpetStart) < ticksFor10ms) _mm_pause(); - - uint32_t elapsed = 0xFFFFFFFF - LAPIC_REG(LAPIC_TIMER_CUR); - return elapsed / 10; // APIC ticks per ms -} - -KSTATUS HalTimerInitialize(uintptr_t lapicPhys, uintptr_t hpetPhys, HAL_TIMER *out) { - gLapic = (volatile uint32_t *)gApi.PhysToVirt(lapicPhys); - HalDbgPrint("HAL: lapicPhys=0x%llX, lapicVirt=0x%llX, hpetPhys=0x%llX\n", lapicPhys, (uintptr_t)gLapic, hpetPhys); - - uint32_t ticksPerMs = 0; - if (hpetPhys) { - HalDbgPrint("HAL: Attempting to calibrate APIC timer via HPET...\n"); - ticksPerMs = CalibrateApicTimerViaHpet(hpetPhys); - } - if (!ticksPerMs) { - HalDbgPrint("HAL: HPET unavailable or calibration failed. Falling back to PIT...\n"); - ticksPerMs = CalibrateApicTimerViaPit(); - } - if (!ticksPerMs) { - HalDbgPrint("HAL: APIC calibration failed. How?\n"); - return STATUS_DEVICE_ERROR; - } - - gApicTicksPerUs = ticksPerMs / 1000; - gApicTicksPerQuantum = ticksPerMs * QUANTUM_MS; - - if (hpetPhys) { - gHpet = (volatile uint64_t*)gApi.PhysToVirt(hpetPhys); - uint64_t hpetHz = HpetInit(hpetPhys); - gHpetHz = hpetHz; - gHpetTicksPerUs = hpetHz / 1000000; - } - - HalDbgPrint("HAL: APIC timer: %u ticks/ms\n", ticksPerMs); - - // Configure LAPIC timer in periodic mode - LAPIC_REG(LAPIC_TIMER_DIV) = TIMER_DIV16; - LAPIC_REG(LAPIC_TIMER_LVT) = TIMER_PERIODIC | TIMER_VECTOR; - LAPIC_REG(LAPIC_TIMER_INIT) = gApicTicksPerQuantum; - - out->QueryTicks = QueryTicks; - out->Stall = Stall; - out->Isr = TimerIsr; - out->IrqNumber = 0; - return STATUS_SUCCESS; -} +// hal_timer.c - Local APIC timer +#include +#include +#include +#include +#include + +#define LAPIC_TIMER_LVT 0x320 +#define LAPIC_TIMER_INIT 0x380 +#define LAPIC_TIMER_CUR 0x390 +#define LAPIC_TIMER_DIV 0x3E0 + +#define TIMER_VECTOR 0x20 // vector 0x20 = IRQ 0 slot in IDT +#define TIMER_PERIODIC (1 << 17) +#define TIMER_MASKED (1 << 16) +#define TIMER_DIV16 0x3 + +#define TIMER_PERIOD_MS 10 +#define PIT_HZ 1193182UL + +extern HAL_API gApi; +static volatile uint32_t* gLapic = NULL; +static uint32_t gApicTicksPerUs = 0; +static uint32_t gApicTicksPerQuantum = 0; +static uint64_t gHpetTicksPerUs = 0; +static uint64_t gHpetHz = 0; +static volatile uint64_t* gHpet = NULL; +#define LAPIC_REG(off) (*(volatile uint32_t *)((uint8_t *)gLapic + (off))) + +static volatile uint64_t gTicks = 0; +void (* volatile gTimerCallback)(void) = NULL; + +static inline void outb(uint16_t port, uint8_t val) { __outbyte(port, val); } +static inline uint8_t inb(uint16_t port) { return __inbyte(port); } + +static void TimerIsr(void) { + gTicks++; + if (gTimerCallback) gTimerCallback(); + // EOI is sent by KeIrqDispatch after this returns +} + +static uint64_t QueryTicks(void) { + // Prefer HPET when present: it is already a monotonically increasing main + // counter and avoids races between the LAPIC periodic tick and the current + // countdown register. + if (gHpet && gHpetHz) { + uint64_t counter = gHpet[30]; // offset 0xF0: main counter + uint64_t seconds = counter / gHpetHz; + uint64_t rem = counter % gHpetHz; + return seconds * 1000000ull + (rem * 1000000ull) / gHpetHz; + } + + // Fallback: combine the coarse periodic tick with the current LAPIC + // countdown. Read the coarse tick twice so an interrupt between gTicks and + // LAPIC_TIMER_CUR cannot make time jump backwards by almost one quantum. + uint64_t baseTicks1; + uint64_t baseTicks2; + uint32_t current; + do { + baseTicks1 = gTicks; + current = LAPIC_REG(LAPIC_TIMER_CUR); + baseTicks2 = gTicks; + } while (baseTicks1 != baseTicks2); + + uint32_t initial = gApicTicksPerQuantum; + + uint32_t elapsedApicTicks = 0; + if (current < initial) { + elapsedApicTicks = initial - current; + } + + uint64_t subQuantumUs = 0; + if (gApicTicksPerUs) + subQuantumUs = elapsedApicTicks / gApicTicksPerUs; + + return (baseTicks1 * TIMER_PERIOD_MS * 1000ull) + subQuantumUs; +} + +static void Stall(uint32_t us) { + // Priority 1: Use HPET (Highest Precision) + if (gHpet && gHpetTicksPerUs > 0) { + uint64_t start = gHpet[30]; // Offset 0xF0 is the Main Counter + uint64_t delta = (uint64_t)us * gHpetTicksPerUs; + while ((gHpet[30] - start) < delta) { + _mm_pause(); // Hint to CPU we are in a spin-loop + } + return; + } + + // Priority 2: Use APIC Timer (High Precision, but handles wrap-around) + if (gApicTicksPerUs > 0) { + uint32_t total_ticks = us * gApicTicksPerUs; + uint32_t last_val = LAPIC_REG(LAPIC_TIMER_CUR); + uint32_t elapsed = 0; + + while (elapsed < total_ticks) { + uint32_t current = LAPIC_REG(LAPIC_TIMER_CUR); + if (current < last_val) { + elapsed += (last_val - current); + } else if (current > last_val) { + // Handled the reset/wrap at the end of TIMER_PERIOD_MS + uint32_t init_val = LAPIC_REG(LAPIC_TIMER_INIT); + elapsed += (last_val + (init_val - current)); + } + last_val = current; + _mm_pause(); + } + return; + } + + // Priority 3: Fallback to Port 0x80 + for (uint32_t i = 0; i < us; i++) { + __outbyte(0x80, 0); + } +} + +// Returns HPET ticks/second, or 0 if HPET is unavailable. +static uint64_t HpetInit(uintptr_t hpetPhys) { + // HPET register block, accessed through physical window + volatile uint64_t *hpet = (volatile uint64_t *)gApi.PhysToVirt(hpetPhys); + + // General Capabilities register (offset 0): + // Bits 63:32 = counter clock period in femtoseconds + uint64_t caps = hpet[0]; + uint32_t periodFs = (uint32_t)(caps >> 32); + if (!periodFs) return 0; + + // Enable the HPET main counter (General Configuration, offset 0x10, bit 0) + hpet[2] |= 1; // offset 0x10 / 8 = index 2 + + return (uint64_t)1e15 / periodFs; // ticks per second +} + +// Use PIT channel 2 one-shot to measure how many APIC ticks elapse in 10 ms, then return ticks/ms. +static uint32_t CalibrateApicTimerViaPit(void) { + // Divide by 16 + LAPIC_REG(LAPIC_TIMER_DIV) = TIMER_DIV16; + // Mask, set vector - leave masked during calibration + LAPIC_REG(LAPIC_TIMER_LVT) = TIMER_MASKED | TIMER_VECTOR; + // Load max count + LAPIC_REG(LAPIC_TIMER_INIT) = 0xFFFFFFFF; + + // PIT channel 2: one-shot, ~10 ms + uint16_t pit_count = (uint16_t)(PIT_HZ / 100); // 11931 ≈ 10 ms + outb(0x61, (inb(0x61) & ~0x02) | 0x01); // gate on, speaker off + outb(0x43, 0xB0); // ch2, lo/hi, mode 0, binary + outb(0x42, (uint8_t)(pit_count & 0xFF)); + outb(0x42, (uint8_t)(pit_count >> 8)); + + // Wait for OUT2 (bit 5 of port 0x61) to go high + while (!(inb(0x61) & 0x20)) _mm_pause(); + + uint32_t elapsed = 0xFFFFFFFF - LAPIC_REG(LAPIC_TIMER_CUR); + return elapsed / 10; // ticks per ms +} + +// Use HPET main counter to measure how many APIC ticks elapse in 10 ms, then return ticks/ms. +static uint32_t CalibrateApicTimerViaHpet(uintptr_t hpetPhys) { + volatile uint64_t *hpet = (volatile uint64_t *)gApi.PhysToVirt(hpetPhys); + uint64_t hpetHz = HpetInit(hpetPhys); + if (!hpetHz) return 0; + + // Ticks in 10 ms = hpetHz / 100 + uint64_t ticksFor10ms = hpetHz / 100; + + LAPIC_REG(LAPIC_TIMER_DIV) = TIMER_DIV16; + LAPIC_REG(LAPIC_TIMER_LVT) = TIMER_MASKED | TIMER_VECTOR; + LAPIC_REG(LAPIC_TIMER_INIT) = 0xFFFFFFFF; + + // HPET main counter is at offset 0xF0, index 0xF0/8 = 30 + uint64_t hpetStart = hpet[30]; + while ((hpet[30] - hpetStart) < ticksFor10ms) _mm_pause(); + + uint32_t elapsed = 0xFFFFFFFF - LAPIC_REG(LAPIC_TIMER_CUR); + return elapsed / 10; // APIC ticks per ms +} + +KSTATUS HalTimerInitialize(uintptr_t lapicPhys, uintptr_t hpetPhys, HAL_TIMER *out) { + gLapic = (volatile uint32_t *)gApi.PhysToVirt(lapicPhys); + HalDbgPrint("HAL: lapicPhys=0x%llX, lapicVirt=0x%llX, hpetPhys=0x%llX\n", lapicPhys, (uintptr_t)gLapic, hpetPhys); + + uint32_t ticksPerMs = 0; + if (hpetPhys) { + HalDbgPrint("HAL: Attempting to calibrate APIC timer via HPET...\n"); + ticksPerMs = CalibrateApicTimerViaHpet(hpetPhys); + } + if (!ticksPerMs) { + HalDbgPrint("HAL: HPET unavailable or calibration failed. Falling back to PIT...\n"); + ticksPerMs = CalibrateApicTimerViaPit(); + } + if (!ticksPerMs) { + HalDbgPrint("HAL: APIC calibration failed. How?\n"); + return STATUS_DEVICE_ERROR; + } + + gApicTicksPerUs = ticksPerMs / 1000; + gApicTicksPerQuantum = ticksPerMs * TIMER_PERIOD_MS; + + if (hpetPhys) { + gHpet = (volatile uint64_t*)gApi.PhysToVirt(hpetPhys); + uint64_t hpetHz = HpetInit(hpetPhys); + gHpetHz = hpetHz; + gHpetTicksPerUs = hpetHz / 1000000; + } + + HalDbgPrint("HAL: APIC timer: %u ticks/ms\n", ticksPerMs); + + // Configure LAPIC timer in periodic mode + LAPIC_REG(LAPIC_TIMER_DIV) = TIMER_DIV16; + LAPIC_REG(LAPIC_TIMER_LVT) = TIMER_PERIODIC | TIMER_VECTOR; + LAPIC_REG(LAPIC_TIMER_INIT) = gApicTicksPerQuantum; + + out->QueryTicks = QueryTicks; + out->Stall = Stall; + out->Isr = TimerIsr; + out->IrqNumber = 0; + return STATUS_SUCCESS; +} diff --git a/code/kernel/ke_sched.c b/code/kernel/ke_sched.c index a334060..2fb5d07 100644 --- a/code/kernel/ke_sched.c +++ b/code/kernel/ke_sched.c @@ -10,8 +10,6 @@ #include #include -#define DEMOTION_QUANTA 4 /* ticks at a level before demotion */ - static LIST_ENTRY gRunQueue[PriorityCount]; static SPINLOCK gSchedLock; @@ -22,6 +20,39 @@ static uint32_t gAgingCounter = 0; static volatile long gReschedulePending = 0; static KE_SCHED_STATS gSchedStats; +static uint32_t KiBaseQuantumForPriority(THREAD_PRIORITY priority) { + switch (priority) { + case PriorityHigh: return SCHED_HIGH_QUANTUM_TICKS; + case PriorityNormal: return SCHED_NORMAL_QUANTUM_TICKS; + case PriorityLow: return SCHED_LOW_QUANTUM_TICKS; + default: return SCHED_NORMAL_QUANTUM_TICKS; + } +} + +static uint32_t KiComputeQuantumLocked(THREAD* thread) { + if (!thread || thread == gIdleThread) + return 1; + + uint32_t quantum = KiBaseQuantumForPriority(thread->Priority); + + // Low-priority CPU-bound threads get longer batches once they have proven + // they mostly burn CPU. Higher-priority work still preempts them as soon + // as it becomes ready, so this reduces switch overhead without hurting + // interactive wake latency. + if (thread->Priority == PriorityLow && thread->CpuBurstTicks >= + (SCHED_DEMOTE_AFTER_BURST_TICKS * 2u)) { + quantum += 4; + } + + return quantum ? quantum : 1; +} + +static void KiRefillQuantumLocked(THREAD* thread) { + if (!thread) return; + thread->QuantumTicks = KiComputeQuantumLocked(thread); + thread->QuantumRemaining = thread->QuantumTicks; +} + THREAD* KeGetCurrentThread(void) { return gCurrentThread; } void KeSetCurrentThread(THREAD* t) { gCurrentThread = t; } @@ -55,6 +86,10 @@ void KeInitializeScheduler(void) { idle->State = ThreadReady; idle->Priority = PriorityLow; idle->QuantaConsumed = 0; + idle->QuantumTicks = 1; + idle->QuantumRemaining = 1; + idle->CpuBurstTicks = 0; + idle->LastRunTicks = 0; idle->Owner = idleProc; idle->WaitObject = (OBJECT *)0; idle->WaitStatus = STATUS_SUCCESS; @@ -119,6 +154,15 @@ void KeReadyThread(THREAD* thread) { uintptr_t flags = KeAcquireSpinlock(&gSchedLock); ListInsertTail(&gRunQueue[p], &thread->SchedulerLink); + + THREAD* cur = gCurrentThread; + if (!cur || cur == gIdleThread) { + KiRequestReschedule(); + } else if (cur->State == ThreadRunning && thread->Priority > cur->Priority) { + gSchedStats.PriorityPreemptions++; + KiRequestReschedule(); + } + KeReleaseSpinlock(&gSchedLock, flags); } @@ -170,6 +214,8 @@ static void KiSchedule(void) { // lone CPU-bound thread gets alternated with the idle thread on every timer // tick, effectively turning a periodic preemption check into a 50% throttle. if (cur != gIdleThread && cur->State == ThreadRunning && !KiAnyRunnableLocked()) { + if (cur->QuantumRemaining == 0) + KiRefillQuantumLocked(cur); gSchedStats.KeepCurrentDecisions++; KeReleaseSpinlock(&gSchedLock, flags); return; @@ -188,6 +234,7 @@ static void KiSchedule(void) { } next->State = ThreadRunning; + KiRefillQuantumLocked(next); gCurrentThread = next; // Only re-queue the outgoing thread if it is still in the running state. @@ -211,33 +258,76 @@ void KiScheduleFromInterrupt(void) { KiSchedule(); } +void KiSchedulerNoteVoluntaryBlock(THREAD* thread) { + if (!thread) return; + uintptr_t flags = KeAcquireSpinlock(&gSchedLock); + gSchedStats.VoluntaryBlocks++; + thread->QuantumRemaining = 0; + KeReleaseSpinlock(&gSchedLock, flags); +} + void KeSchedulerTick(void) { THREAD* cur = gCurrentThread; if (!cur) return; uintptr_t flags = KeAcquireSpinlock(&gSchedLock); - cur->SchedLockFlags = flags; // Issue 5 fix + cur->SchedLockFlags = flags; gSchedStats.Ticks++; - cur->QuantaConsumed++; - if (cur != gIdleThread && cur->QuantaConsumed >= DEMOTION_QUANTA) { - cur->QuantaConsumed = 0; - if (cur->Priority > PriorityLow) { - cur->Priority = (THREAD_PRIORITY)(cur->Priority - 1); - gSchedStats.Demotions++; + int requestReschedule = 0; + + if (cur == gIdleThread) { + if (KiAnyRunnableLocked()) + requestReschedule = 1; + } else { + cur->CpuBurstTicks++; + cur->QuantaConsumed = cur->CpuBurstTicks; + + if (cur->CpuBurstTicks >= SCHED_DEMOTE_AFTER_BURST_TICKS) { + cur->CpuBurstTicks = 0; + cur->QuantaConsumed = 0; + if (cur->Priority > PriorityLow) { + cur->Priority = (THREAD_PRIORITY)(cur->Priority - 1); + gSchedStats.Demotions++; + gSchedStats.CpuBurstDemotions++; + } + } + + if (cur->QuantumRemaining == 0) + KiRefillQuantumLocked(cur); + + if (cur->QuantumRemaining > 0) + cur->QuantumRemaining--; + + if (cur->QuantumRemaining == 0) { + if (KiAnyRunnableLocked()) { + gSchedStats.QuantumExpirations++; + requestReschedule = 1; + } else { + // No peer is ready, so keep the CPU hot and refill the budget. + // This avoids reintroducing the old every-other-tick idle dance + // as a pointless reschedule-pending chatter loop. + KiRefillQuantumLocked(cur); + gSchedStats.KeepCurrentDecisions++; + } } } - // Periodic aging pass to prevent indefinite starvation of PriorityLow threads + // Periodic aging pass to prevent indefinite starvation of PriorityLow and + // PriorityNormal threads. Ready threads promoted by aging get a fresh CPU + // burst history; they earned the boost by waiting, not by burning CPU. gAgingCounter++; - if (gAgingCounter >= 100) { + if (gAgingCounter >= SCHED_AGING_INTERVAL_TICKS) { gAgingCounter = 0; for (int p = 0; p < PriorityHigh; p++) { while (!LIST_IS_EMPTY(&gRunQueue[p])) { LIST_ENTRY* node = ListPopHead(&gRunQueue[p]); THREAD* t = CONTAINING_RECORD(node, THREAD, SchedulerLink); t->Priority = PriorityHigh; + t->CpuBurstTicks = 0; + t->QuantaConsumed = 0; + t->QuantumRemaining = 0; gSchedStats.AgingPromotions++; ListInsertTail(&gRunQueue[PriorityHigh], &t->SchedulerLink); } @@ -246,7 +336,8 @@ void KeSchedulerTick(void) { KeReleaseSpinlock(&gSchedLock, flags); - KiRequestReschedule(); + if (requestReschedule) + KiRequestReschedule(); } __declspec(dllexport) void KeYield(void) { diff --git a/code/kernel/ke_sched.h b/code/kernel/ke_sched.h index e3ef0bb..b23a0a1 100644 --- a/code/kernel/ke_sched.h +++ b/code/kernel/ke_sched.h @@ -3,7 +3,12 @@ // Three run queues (Low / Normal / High). IRQ 0 drives KeSchedulerTick. #include -#define QUANTUM_MS 10 // base time slice in milliseconds +#define SCHED_TICK_MS 10u +#define SCHED_HIGH_QUANTUM_TICKS 2u +#define SCHED_NORMAL_QUANTUM_TICKS 4u +#define SCHED_LOW_QUANTUM_TICKS 8u +#define SCHED_DEMOTE_AFTER_BURST_TICKS 16u +#define SCHED_AGING_INTERVAL_TICKS 100u typedef struct KE_SCHED_STATS { uint64_t Ticks; @@ -12,6 +17,10 @@ typedef struct KE_SCHED_STATS { uint64_t IdleSelections; uint64_t Demotions; uint64_t AgingPromotions; + uint64_t QuantumExpirations; + uint64_t PriorityPreemptions; + uint64_t VoluntaryBlocks; + uint64_t CpuBurstDemotions; } KE_SCHED_STATS; // Called once by KernelMain before any threads are created. @@ -21,8 +30,10 @@ void KeInitializeScheduler(void); void KeReadyThread(THREAD* thread); void KeUnreadyThread(THREAD* thread); -// Called from the timer ISR every QUANTUM_MS. This only performs timer -// accounting and requests a reschedule; the actual context switch happens from +// Called from the timer ISR every SCHED_TICK_MS. This performs timer +// accounting and requests a reschedule only when the current thread has +// exhausted its scheduler budget, or when idle work becomes available. The +// actual context switch happens from // KiExitInterrupt after the IRQ handler has returned and EOI has been sent. void KeSchedulerTick(void); @@ -32,6 +43,7 @@ void KeSchedulerTick(void); void KiRequestReschedule(void); int KiIsReschedulePending(void); void KiScheduleFromInterrupt(void); +void KiSchedulerNoteVoluntaryBlock(THREAD* thread); // Voluntarily yield the CPU. Picks the next runnable thread immediately. __declspec(dllexport) void KeYield(void); diff --git a/code/kernel/ke_thread.c b/code/kernel/ke_thread.c index 78f62f3..56dd527 100644 --- a/code/kernel/ke_thread.c +++ b/code/kernel/ke_thread.c @@ -87,6 +87,10 @@ __declspec(dllexport) KSTATUS KeCreateThread(PROCESS* owner, uintptr_t entry, ui thread->State = ThreadReady; thread->Priority = priority; thread->QuantaConsumed = 0; + thread->QuantumTicks = 0; + thread->QuantumRemaining = 0; + thread->CpuBurstTicks = 0; + thread->LastRunTicks = 0; thread->WaitObject = (OBJECT *)0; thread->WaitStatus = STATUS_SUCCESS; thread->WaitListHead = (LIST_ENTRY*)0; @@ -241,6 +245,10 @@ __declspec(dllexport) KSTATUS KeCreateKernelThread(uintptr_t fn, uintptr_t arg, thread->State = ThreadReady; thread->Priority = priority; thread->QuantaConsumed = 0; + thread->QuantumTicks = 0; + thread->QuantumRemaining = 0; + thread->CpuBurstTicks = 0; + thread->LastRunTicks = 0; thread->WaitObject = (OBJECT*)0; thread->WaitStatus = STATUS_SUCCESS; thread->WaitListHead = (LIST_ENTRY*)0; @@ -344,6 +352,7 @@ KSTATUS KeCommitBlockThread(THREAD* thread, uint8_t alertable) { return STATUS_USER_APC; } + KiSchedulerNoteVoluntaryBlock(thread); KeYield(); // Kernel APCs are delivered before an alertable wait returns @@ -369,6 +378,9 @@ void KeUnblockThreadWithStatus(THREAD* thread, KSTATUS status) { // instead of inserting the running thread into the ready queue. if (thread == KeGetCurrentThread()) { KiFinishWait(thread, status); + thread->CpuBurstTicks = 0; + thread->QuantaConsumed = 0; + thread->QuantumRemaining = 0; if (thread->State == ThreadWaiting) thread->State = ThreadRunning; return; @@ -385,9 +397,12 @@ void KeUnblockThreadWithStatus(THREAD* thread, KSTATUS status) { KiFinishWait(thread, status); thread->State = ThreadReady; + thread->CpuBurstTicks = 0; + thread->QuantaConsumed = 0; + thread->QuantumRemaining = 0; + if (thread->Priority < PriorityHigh) { thread->Priority = (THREAD_PRIORITY)(thread->Priority + 1); - thread->QuantaConsumed = 0; } KeReadyThread(thread); diff --git a/code/kernel/ke_thread.h b/code/kernel/ke_thread.h index ad1adcc..0f82e4b 100644 --- a/code/kernel/ke_thread.h +++ b/code/kernel/ke_thread.h @@ -41,7 +41,8 @@ typedef struct THREAD { THREAD_STATE State; THREAD_PRIORITY Priority; - // Quanta consumed without blocking - used by the MLFQ for demotion. + // CPU-burst mirror kept here for the assembly-stable THREAD layout. + // Dynamic quantum state lives later in the structure. uint32_t QuantaConsumed; PROCESS* Owner; @@ -80,6 +81,14 @@ typedef struct THREAD { void* FpuContext; uint8_t FpuStorage[528]; + // Scheduler accounting. QuantaConsumed is kept in the assembly-stable + // portion of THREAD for layout compatibility; the dynamic scheduler uses + // these fields for per-dispatch budget and CPU-burst tracking. + uint32_t QuantumTicks; + uint32_t QuantumRemaining; + uint32_t CpuBurstTicks; + uint32_t LastRunTicks; + // Set by wait primitives before blocking and completed by the object // that wakes the thread. Keep this field after the assembly-visible // context-switch fields: ke_switch.asm hard-codes offsets through