]> 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 8d1b9e8af8e00d2d2bca85feb01688e0a3eed199..8550003bcb8f1f09344281ae2d4ad4b62faefe07 100644 (file)
@@ -54,9 +54,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 d91fc6b45a6abd877fbae772100bf39277353348..6027b16b8516c357389f39366f049593ad468b23 100644 (file)
@@ -1,15 +1,11 @@
 #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) {
@@ -17,19 +13,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 9a77215f1c89a44bb39b2d7dc3b6c947fe08b992..31c0bc050d49ed16ab303ceba6b9be762b346867 100644 (file)
@@ -3,6 +3,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"
 
@@ -10,6 +11,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();
 }
 
@@ -19,7 +24,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 334079286c16f8bba5d8fb1137f13e55718ddeb6..07f7c05d3273bda9b9f34bbac1670df3f245d0a9 100644 (file)
@@ -863,9 +863,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);
@@ -907,12 +917,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);