better scheduler time allocation, proper quota tracking

This commit is contained in:
maksw authored and Maksymilian Wilczynski committed 2026-05-26 14:46:52 +02:00
1 parent 03bf767ac2
commit b3569a2b90
6 files changed
+143 -16

No files matched your search

Binary file not shown.
+4 -4
View File
@@ -15,7 +15,7 @@
#define TIMER_MASKED (1 << 16) #define TIMER_MASKED (1 << 16)
#define TIMER_DIV16 0x3 #define TIMER_DIV16 0x3
#define QUANTUM_MS 10 #define TIMER_PERIOD_MS 10
#define PIT_HZ 1193182UL #define PIT_HZ 1193182UL
extern HAL_API gApi; extern HAL_API gApi;
@@ -73,7 +73,7 @@ static uint64_t QueryTicks(void) {
if (gApicTicksPerUs) if (gApicTicksPerUs)
subQuantumUs = elapsedApicTicks / gApicTicksPerUs; subQuantumUs = elapsedApicTicks / gApicTicksPerUs;
return (baseTicks1 * QUANTUM_MS * 1000ull) + subQuantumUs; return (baseTicks1 * TIMER_PERIOD_MS * 1000ull) + subQuantumUs;
} }
static void Stall(uint32_t us) { static void Stall(uint32_t us) {
@@ -98,7 +98,7 @@ static void Stall(uint32_t us) {
if (current < last_val) { if (current < last_val) {
elapsed += (last_val - current); elapsed += (last_val - current);
} else if (current > last_val) { } else if (current > last_val) {
// Handled the reset/wrap at the end of QUANTUM_MS // Handled the reset/wrap at the end of TIMER_PERIOD_MS
uint32_t init_val = LAPIC_REG(LAPIC_TIMER_INIT); uint32_t init_val = LAPIC_REG(LAPIC_TIMER_INIT);
elapsed += (last_val + (init_val - current)); elapsed += (last_val + (init_val - current));
} }
@@ -194,7 +194,7 @@ KSTATUS HalTimerInitialize(uintptr_t lapicPhys, uintptr_t hpetPhys, HAL_TIMER *o
} }
gApicTicksPerUs = ticksPerMs / 1000; gApicTicksPerUs = ticksPerMs / 1000;
gApicTicksPerQuantum = ticksPerMs * QUANTUM_MS; gApicTicksPerQuantum = ticksPerMs * TIMER_PERIOD_MS;
if (hpetPhys) { if (hpetPhys) {
gHpet = (volatile uint64_t*)gApi.PhysToVirt(hpetPhys); gHpet = (volatile uint64_t*)gApi.PhysToVirt(hpetPhys);
+98 -7
View File
@@ -10,8 +10,6 @@
#include <ke_list.h> #include <ke_list.h>
#include <mm_virt.h> #include <mm_virt.h>
#define DEMOTION_QUANTA 4 /* ticks at a level before demotion */
static LIST_ENTRY gRunQueue[PriorityCount]; static LIST_ENTRY gRunQueue[PriorityCount];
static SPINLOCK gSchedLock; static SPINLOCK gSchedLock;
@@ -22,6 +20,39 @@ static uint32_t gAgingCounter = 0;
static volatile long gReschedulePending = 0; static volatile long gReschedulePending = 0;
static KE_SCHED_STATS gSchedStats; 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; } THREAD* KeGetCurrentThread(void) { return gCurrentThread; }
void KeSetCurrentThread(THREAD* t) { gCurrentThread = t; } void KeSetCurrentThread(THREAD* t) { gCurrentThread = t; }
@@ -55,6 +86,10 @@ void KeInitializeScheduler(void) {
idle->State = ThreadReady; idle->State = ThreadReady;
idle->Priority = PriorityLow; idle->Priority = PriorityLow;
idle->QuantaConsumed = 0; idle->QuantaConsumed = 0;
idle->QuantumTicks = 1;
idle->QuantumRemaining = 1;
idle->CpuBurstTicks = 0;
idle->LastRunTicks = 0;
idle->Owner = idleProc; idle->Owner = idleProc;
idle->WaitObject = (OBJECT *)0; idle->WaitObject = (OBJECT *)0;
idle->WaitStatus = STATUS_SUCCESS; idle->WaitStatus = STATUS_SUCCESS;
@@ -119,6 +154,15 @@ void KeReadyThread(THREAD* thread) {
uintptr_t flags = KeAcquireSpinlock(&gSchedLock); uintptr_t flags = KeAcquireSpinlock(&gSchedLock);
ListInsertTail(&gRunQueue[p], &thread->SchedulerLink); 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); KeReleaseSpinlock(&gSchedLock, flags);
} }
@@ -170,6 +214,8 @@ static void KiSchedule(void) {
// lone CPU-bound thread gets alternated with the idle thread on every timer // lone CPU-bound thread gets alternated with the idle thread on every timer
// tick, effectively turning a periodic preemption check into a 50% throttle. // tick, effectively turning a periodic preemption check into a 50% throttle.
if (cur != gIdleThread && cur->State == ThreadRunning && !KiAnyRunnableLocked()) { if (cur != gIdleThread && cur->State == ThreadRunning && !KiAnyRunnableLocked()) {
if (cur->QuantumRemaining == 0)
KiRefillQuantumLocked(cur);
gSchedStats.KeepCurrentDecisions++; gSchedStats.KeepCurrentDecisions++;
KeReleaseSpinlock(&gSchedLock, flags); KeReleaseSpinlock(&gSchedLock, flags);
return; return;
@@ -188,6 +234,7 @@ static void KiSchedule(void) {
} }
next->State = ThreadRunning; next->State = ThreadRunning;
KiRefillQuantumLocked(next);
gCurrentThread = next; gCurrentThread = next;
// Only re-queue the outgoing thread if it is still in the running state. // Only re-queue the outgoing thread if it is still in the running state.
@@ -211,33 +258,76 @@ void KiScheduleFromInterrupt(void) {
KiSchedule(); KiSchedule();
} }
void KiSchedulerNoteVoluntaryBlock(THREAD* thread) {
if (!thread) return;
uintptr_t flags = KeAcquireSpinlock(&gSchedLock);
gSchedStats.VoluntaryBlocks++;
thread->QuantumRemaining = 0;
KeReleaseSpinlock(&gSchedLock, flags);
}
void KeSchedulerTick(void) { void KeSchedulerTick(void) {
THREAD* cur = gCurrentThread; THREAD* cur = gCurrentThread;
if (!cur) return; if (!cur) return;
uintptr_t flags = KeAcquireSpinlock(&gSchedLock); uintptr_t flags = KeAcquireSpinlock(&gSchedLock);
cur->SchedLockFlags = flags; // Issue 5 fix cur->SchedLockFlags = flags;
gSchedStats.Ticks++; gSchedStats.Ticks++;
cur->QuantaConsumed++;
if (cur != gIdleThread && cur->QuantaConsumed >= DEMOTION_QUANTA) { 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; cur->QuantaConsumed = 0;
if (cur->Priority > PriorityLow) { if (cur->Priority > PriorityLow) {
cur->Priority = (THREAD_PRIORITY)(cur->Priority - 1); cur->Priority = (THREAD_PRIORITY)(cur->Priority - 1);
gSchedStats.Demotions++; gSchedStats.Demotions++;
gSchedStats.CpuBurstDemotions++;
} }
} }
// Periodic aging pass to prevent indefinite starvation of PriorityLow threads 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 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++; gAgingCounter++;
if (gAgingCounter >= 100) { if (gAgingCounter >= SCHED_AGING_INTERVAL_TICKS) {
gAgingCounter = 0; gAgingCounter = 0;
for (int p = 0; p < PriorityHigh; p++) { for (int p = 0; p < PriorityHigh; p++) {
while (!LIST_IS_EMPTY(&gRunQueue[p])) { while (!LIST_IS_EMPTY(&gRunQueue[p])) {
LIST_ENTRY* node = ListPopHead(&gRunQueue[p]); LIST_ENTRY* node = ListPopHead(&gRunQueue[p]);
THREAD* t = CONTAINING_RECORD(node, THREAD, SchedulerLink); THREAD* t = CONTAINING_RECORD(node, THREAD, SchedulerLink);
t->Priority = PriorityHigh; t->Priority = PriorityHigh;
t->CpuBurstTicks = 0;
t->QuantaConsumed = 0;
t->QuantumRemaining = 0;
gSchedStats.AgingPromotions++; gSchedStats.AgingPromotions++;
ListInsertTail(&gRunQueue[PriorityHigh], &t->SchedulerLink); ListInsertTail(&gRunQueue[PriorityHigh], &t->SchedulerLink);
} }
@@ -246,6 +336,7 @@ void KeSchedulerTick(void) {
KeReleaseSpinlock(&gSchedLock, flags); KeReleaseSpinlock(&gSchedLock, flags);
if (requestReschedule)
KiRequestReschedule(); KiRequestReschedule();
} }
+15 -3
View File
@@ -3,7 +3,12 @@
// Three run queues (Low / Normal / High). IRQ 0 drives KeSchedulerTick. // Three run queues (Low / Normal / High). IRQ 0 drives KeSchedulerTick.
#include <ke_thread.h> #include <ke_thread.h>
#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 { typedef struct KE_SCHED_STATS {
uint64_t Ticks; uint64_t Ticks;
@@ -12,6 +17,10 @@ typedef struct KE_SCHED_STATS {
uint64_t IdleSelections; uint64_t IdleSelections;
uint64_t Demotions; uint64_t Demotions;
uint64_t AgingPromotions; uint64_t AgingPromotions;
uint64_t QuantumExpirations;
uint64_t PriorityPreemptions;
uint64_t VoluntaryBlocks;
uint64_t CpuBurstDemotions;
} KE_SCHED_STATS; } KE_SCHED_STATS;
// Called once by KernelMain before any threads are created. // Called once by KernelMain before any threads are created.
@@ -21,8 +30,10 @@ void KeInitializeScheduler(void);
void KeReadyThread(THREAD* thread); void KeReadyThread(THREAD* thread);
void KeUnreadyThread(THREAD* thread); void KeUnreadyThread(THREAD* thread);
// Called from the timer ISR every QUANTUM_MS. This only performs timer // Called from the timer ISR every SCHED_TICK_MS. This performs timer
// accounting and requests a reschedule; the actual context switch happens from // 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. // KiExitInterrupt after the IRQ handler has returned and EOI has been sent.
void KeSchedulerTick(void); void KeSchedulerTick(void);
@@ -32,6 +43,7 @@ void KeSchedulerTick(void);
void KiRequestReschedule(void); void KiRequestReschedule(void);
int KiIsReschedulePending(void); int KiIsReschedulePending(void);
void KiScheduleFromInterrupt(void); void KiScheduleFromInterrupt(void);
void KiSchedulerNoteVoluntaryBlock(THREAD* thread);
// Voluntarily yield the CPU. Picks the next runnable thread immediately. // Voluntarily yield the CPU. Picks the next runnable thread immediately.
__declspec(dllexport) void KeYield(void); __declspec(dllexport) void KeYield(void);
+16 -1
View File
@@ -87,6 +87,10 @@ __declspec(dllexport) KSTATUS KeCreateThread(PROCESS* owner, uintptr_t entry, ui
thread->State = ThreadReady; thread->State = ThreadReady;
thread->Priority = priority; thread->Priority = priority;
thread->QuantaConsumed = 0; thread->QuantaConsumed = 0;
thread->QuantumTicks = 0;
thread->QuantumRemaining = 0;
thread->CpuBurstTicks = 0;
thread->LastRunTicks = 0;
thread->WaitObject = (OBJECT *)0; thread->WaitObject = (OBJECT *)0;
thread->WaitStatus = STATUS_SUCCESS; thread->WaitStatus = STATUS_SUCCESS;
thread->WaitListHead = (LIST_ENTRY*)0; thread->WaitListHead = (LIST_ENTRY*)0;
@@ -241,6 +245,10 @@ __declspec(dllexport) KSTATUS KeCreateKernelThread(uintptr_t fn, uintptr_t arg,
thread->State = ThreadReady; thread->State = ThreadReady;
thread->Priority = priority; thread->Priority = priority;
thread->QuantaConsumed = 0; thread->QuantaConsumed = 0;
thread->QuantumTicks = 0;
thread->QuantumRemaining = 0;
thread->CpuBurstTicks = 0;
thread->LastRunTicks = 0;
thread->WaitObject = (OBJECT*)0; thread->WaitObject = (OBJECT*)0;
thread->WaitStatus = STATUS_SUCCESS; thread->WaitStatus = STATUS_SUCCESS;
thread->WaitListHead = (LIST_ENTRY*)0; thread->WaitListHead = (LIST_ENTRY*)0;
@@ -344,6 +352,7 @@ KSTATUS KeCommitBlockThread(THREAD* thread, uint8_t alertable) {
return STATUS_USER_APC; return STATUS_USER_APC;
} }
KiSchedulerNoteVoluntaryBlock(thread);
KeYield(); KeYield();
// Kernel APCs are delivered before an alertable wait returns // 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. // instead of inserting the running thread into the ready queue.
if (thread == KeGetCurrentThread()) { if (thread == KeGetCurrentThread()) {
KiFinishWait(thread, status); KiFinishWait(thread, status);
thread->CpuBurstTicks = 0;
thread->QuantaConsumed = 0;
thread->QuantumRemaining = 0;
if (thread->State == ThreadWaiting) if (thread->State == ThreadWaiting)
thread->State = ThreadRunning; thread->State = ThreadRunning;
return; return;
@@ -385,9 +397,12 @@ void KeUnblockThreadWithStatus(THREAD* thread, KSTATUS status) {
KiFinishWait(thread, status); KiFinishWait(thread, status);
thread->State = ThreadReady; thread->State = ThreadReady;
thread->CpuBurstTicks = 0;
thread->QuantaConsumed = 0;
thread->QuantumRemaining = 0;
if (thread->Priority < PriorityHigh) { if (thread->Priority < PriorityHigh) {
thread->Priority = (THREAD_PRIORITY)(thread->Priority + 1); thread->Priority = (THREAD_PRIORITY)(thread->Priority + 1);
thread->QuantaConsumed = 0;
} }
KeReadyThread(thread); KeReadyThread(thread);
+10 -1
View File
@@ -41,7 +41,8 @@ typedef struct THREAD {
THREAD_STATE State; THREAD_STATE State;
THREAD_PRIORITY Priority; 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; uint32_t QuantaConsumed;
PROCESS* Owner; PROCESS* Owner;
@@ -80,6 +81,14 @@ typedef struct THREAD {
void* FpuContext; void* FpuContext;
uint8_t FpuStorage[528]; 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 // Set by wait primitives before blocking and completed by the object
// that wakes the thread. Keep this field after the assembly-visible // that wakes the thread. Keep this field after the assembly-visible
// context-switch fields: ke_switch.asm hard-codes offsets through // context-switch fields: ke_switch.asm hard-codes offsets through