]> Projects (at) Tadryanom (dot) Me - AdrOS.git/commitdiff
refactor: proper time-slice scheduler + fix arch contamination + mask PIT
authorTulio A M Mendes <[email protected]>
Sat, 14 Feb 2026 08:24:36 +0000 (05:24 -0300)
committerTulio A M Mendes <[email protected]>
Sat, 14 Feb 2026 08:24:36 +0000 (05:24 -0300)
Three fixes for the 100Hz timer upgrade:

1. **Arch contamination removed from drivers/timer.c**
   - Moved BSP-only guard (lapic_get_id check) from generic
     src/drivers/timer.c into src/hal/x86/timer.c where it belongs
   - drivers/timer.c now has zero #ifdef or arch-specific includes

2. **Proper time-slice scheduling replaces tick%2 hack**
   - Added time_slice field to struct process (SCHED_TIME_SLICE=2)
   - schedule() skips preemption while time_slice > 0, decrementing
     each tick. Voluntary yields (sleep/waitpid/sem) bypass the
     check entirely — only timer-driven preemption is rate-limited
   - Effective preemption rate: TIMER_HZ/SCHED_TIME_SLICE = 50Hz
   - Sleep/wake resolution remains at full 100Hz via process_wake_check

3. **PIT IRQ 0 masked when LAPIC timer is active**
   - ioapic_mask_irq(0) called before lapic_timer_start()
   - Eliminates ~18 extra ticks/sec from PIT double-ticking BSP
   - Tick counter now advances at exactly 100Hz, fixing ~18% timing
     error in all sleep/timing calculations

Tests: 20/20 smoke (8s), 16/16 battery, cppcheck clean

include/process.h
src/drivers/timer.c
src/hal/x86/timer.c
src/kernel/scheduler.c

index 4a621a11a2e509071abf568f60abc978b52e0061..5d5d4ad8dd8847ef678d2c0283a93d53cf0d99ad 100644 (file)
@@ -63,9 +63,11 @@ struct process {
     uint32_t* kernel_stack;
 #define SCHED_NUM_PRIOS 32
 #define SCHED_DEFAULT_PRIO 16
+#define SCHED_TIME_SLICE   2    /* ticks before forced preemption (20ms at 100Hz) */
 
     uint8_t priority;           // 0 = highest, 31 = lowest
     int8_t  nice;               // -20 to +19 (maps to priority)
+    uint8_t time_slice;         // ticks remaining in current quantum
     process_state_t state;
     uint32_t wake_at_tick;
     uint32_t alarm_tick;
index b3496f79c758710facea4dd1ba350eb8c977b155..5434de7676180aa0abb832caae15ae829c729d1e 100644 (file)
@@ -9,16 +9,12 @@
 
 #include "timer.h"
 #include "console.h"
-#include "process.h" 
+#include "process.h"
 #include "vdso.h"
 #include "vga_console.h"
 
 #include "hal/timer.h"
 
-#if defined(__i386__)
-#include "arch/x86/lapic.h"
-#endif
-
 static uint32_t tick = 0;
 
 uint32_t get_tick_count(void) {
@@ -26,19 +22,11 @@ uint32_t get_tick_count(void) {
 }
 
 static void hal_tick_bridge(void) {
-#if defined(__i386__)
-    if (lapic_is_enabled() && lapic_get_id() != 0) return;
-#endif
     tick++;
     vdso_update_tick(tick);
     vga_flush();
     process_wake_check(tick);
-    /* Preempt every SCHED_DIVISOR ticks to reduce context-switch
-     * overhead in emulated environments (QEMU TLB flush on CR3
-     * reload is expensive).  Sleeping processes still wake at full
-     * TIMER_HZ resolution via process_wake_check above. */
-    if (tick % 2 == 0)
-        schedule();
+    schedule();
 }
 
 void timer_init(uint32_t frequency) {
index 3736060638efc76970f3e5e99e85e6c03eaf87d5..2e413b74a041143daa87369e3976ccdf744fbd14 100644 (file)
@@ -12,6 +12,7 @@
 #if defined(__i386__)
 #include "arch/x86/idt.h"
 #include "arch/x86/lapic.h"
+#include "arch/x86/ioapic.h"
 #include "io.h"
 #include "console.h"
 
@@ -19,6 +20,10 @@ static hal_timer_tick_cb_t g_tick_cb = 0;
 
 static void timer_irq(struct registers* regs) {
     (void)regs;
+    /* Only the BSP (LAPIC ID 0) drives the global tick, scheduling,
+     * and VGA refresh.  APs have no processes to run and would only
+     * add spinlock contention on sched_lock / vga_lock. */
+    if (lapic_is_enabled() && lapic_get_id() != 0) return;
     if (g_tick_cb) g_tick_cb();
 }
 
@@ -28,7 +33,11 @@ void hal_timer_init(uint32_t frequency_hz, hal_timer_tick_cb_t tick_cb) {
     register_interrupt_handler(32, timer_irq);
 
     if (lapic_is_enabled()) {
-        /* Use LAPIC timer — more precise and per-CPU capable */
+        /* Use LAPIC timer — more precise and per-CPU capable.
+         * Mask PIT IRQ 0 via IOAPIC so only the LAPIC timer drives
+         * vector 32.  Without this, PIT adds ~18 extra ticks/sec,
+         * making all timing calculations off by ~18%. */
+        ioapic_mask_irq(0);
         lapic_timer_start(frequency_hz);
     } else {
         /* Fallback to legacy PIT */
index 7fbf3e0af1d50f0a4a634a143b8eb613810a1506..4c8b02047074ead9b6b76f76bc7e9e88c0980566 100644 (file)
@@ -872,9 +872,19 @@ void schedule(void) {
 
     struct process* prev = current_process;
 
-    // Put prev back into expired runqueue if it's still runnable.
-    // Priority decay: penalize CPU-bound processes that exhaust their slice.
+    // Time-slice preemption: if the process is still running (timer
+    // preemption, not a voluntary yield) and has quantum left, do NOT
+    // preempt.  Woken processes accumulate in rq_active and get their
+    // turn when the slice expires.  This limits context-switch rate to
+    // TIMER_HZ/SCHED_TIME_SLICE while keeping full tick resolution for
+    // sleep/wake timing.
     if (prev->state == PROCESS_RUNNING) {
+        if (prev->time_slice > 0) {
+            prev->time_slice--;
+            spin_unlock_irqrestore(&sched_lock, irq_flags);
+            return;
+        }
+        // Slice exhausted — enqueue to expired with priority decay.
         prev->state = PROCESS_READY;
         if (prev->priority < SCHED_NUM_PRIOS - 1) prev->priority++;
         rq_enqueue(rq_expired, prev);
@@ -916,12 +926,14 @@ void schedule(void) {
 
     if (prev == next) {
         prev->state = PROCESS_RUNNING;
+        prev->time_slice = SCHED_TIME_SLICE;
         spin_unlock_irqrestore(&sched_lock, irq_flags);
         return;
     }
 
     current_process = next;
     current_process->state = PROCESS_RUNNING;
+    current_process->time_slice = SCHED_TIME_SLICE;
 
     if (current_process->addr_space && current_process->addr_space != prev->addr_space) {
         hal_cpu_set_address_space(current_process->addr_space);