]> Projects (at) Tadryanom (dot) Me - AdrOS.git/commitdiff
feat: AP scheduler entry (SMP Phase 3)
authorTulio A M Mendes <[email protected]>
Mon, 16 Feb 2026 19:00:38 +0000 (16:00 -0300)
committerTulio A M Mendes <[email protected]>
Mon, 16 Feb 2026 19:00:38 +0000 (16:00 -0300)
Enable scheduling on Application Processors:

- Load IDT on APs via idt_load_ap() — root cause of AP crashes was
  missing lidt, causing triple-fault when LAPIC timer fires
- Create per-CPU idle process for each AP in sched_ap_init()
- Start LAPIC timer on APs using BSP-calibrated ticks (no PIT
  recalibration needed — all CPUs share same bus clock)
- AP timer handler calls schedule() for local CPU runqueue
- BSP signals APs via ap_sched_go flag after timer_init completes
- Allocations in sched_ap_init done outside sched_lock to avoid
  ABBA deadlock with heap lock
- TSS updates restricted to CPU 0 (shared TSS, only BSP runs
  user processes)
- AP stack increased to 8KB to match kernel thread stack size

All processes still assigned to CPU 0 — Phase 4 will add load
balancing to distribute processes across CPUs.

83/83 smoke tests pass (8s), cppcheck clean.

include/arch/x86/idt.h
include/arch/x86/lapic.h
src/arch/x86/idt.c
src/arch/x86/lapic.c
src/arch/x86/smp.c
src/hal/x86/timer.c
src/kernel/main.c
src/kernel/scheduler.c

index 82d690eabdf4edf4dccc4c5cba6c9cc6e62fde9a..980f17e42a2ece52a12939f4778814d0236a87e5 100644 (file)
@@ -30,6 +30,9 @@ struct registers {
 // Initialize IDT and PIC
 void idt_init(void);
 
+// Load IDT on an AP (same IDT as BSP, just needs lidt)
+void idt_load_ap(void);
+
 // Register a custom handler for a specific interrupt
 typedef void (*isr_handler_t)(struct registers*);
 void register_interrupt_handler(uint8_t n, isr_handler_t handler);
index 9da71ea098779930524b2ffb8bc9ab41a7f24c90..da0349a8b1bf1f1a984cc05a30c26870da45a64f 100644 (file)
@@ -55,6 +55,9 @@ uint32_t lapic_get_id(void);
 /* Start the LAPIC timer at the given frequency (approximate). */
 void lapic_timer_start(uint32_t frequency_hz);
 
+/* Start LAPIC timer on an AP using BSP-calibrated ticks (no PIT recalibration). */
+void lapic_timer_start_ap(void);
+
 /* Stop the LAPIC timer. */
 void lapic_timer_stop(void);
 
index 2ca749dfdd1c559cc4f65bdbbfa686483b2dd65a..d1075b25790da7e554d5199d1ca0d8fbd74fc1b7 100644 (file)
@@ -312,6 +312,10 @@ void idt_init(void) {
     kprintf("[IDT] Loaded.\n");
 }
 
+void idt_load_ap(void) {
+    __asm__ volatile("lidt %0" : : "m"(idtp));
+}
+
 void register_interrupt_handler(uint8_t n, isr_handler_t handler) {
     uintptr_t flags = spin_lock_irqsave(&idt_handlers_lock);
 
index 0b73027a953efeaafdd71ecd3675b6f4f9c7e574..dae84578cd28b4954482534d9dfa0a72e1b12a58 100644 (file)
@@ -16,6 +16,7 @@
 
 static volatile uint32_t* lapic_base = 0;
 static int lapic_active = 0;
+static uint32_t lapic_timer_ticks_saved = 0; /* BSP-calibrated ticks for AP reuse */
 
 uint64_t rdmsr(uint32_t msr) {
     uint32_t lo, hi;
@@ -187,6 +188,9 @@ void lapic_timer_start(uint32_t frequency_hz) {
 
     if (ticks_per_interrupt == 0) ticks_per_interrupt = 1;
 
+    /* Save calibrated value for AP reuse */
+    lapic_timer_ticks_saved = ticks_per_interrupt;
+
     /* Configure periodic timer */
     lapic_write(LAPIC_TIMER_LVT, LAPIC_TIMER_PERIODIC | LAPIC_TIMER_VEC);
     lapic_write(LAPIC_TIMER_DCR, LAPIC_TIMER_DIV_16);
@@ -196,6 +200,16 @@ void lapic_timer_start(uint32_t frequency_hz) {
             (unsigned)frequency_hz, ticks_per_interrupt);
 }
 
+void lapic_timer_start_ap(void) {
+    /* Start LAPIC timer on an AP using BSP-calibrated ticks.
+     * All CPUs share the same bus clock, so the same ticks value works. */
+    if (!lapic_active || lapic_timer_ticks_saved == 0) return;
+
+    lapic_write(LAPIC_TIMER_LVT, LAPIC_TIMER_PERIODIC | LAPIC_TIMER_VEC);
+    lapic_write(LAPIC_TIMER_DCR, LAPIC_TIMER_DIV_16);
+    lapic_write(LAPIC_TIMER_ICR, lapic_timer_ticks_saved);
+}
+
 void lapic_timer_stop(void) {
     if (!lapic_active) return;
     lapic_write(LAPIC_TIMER_LVT, LAPIC_LVT_MASKED);
index fbc15f4299528a9a71dc964ecbe38e835b304702..e6db605c36ef1ba2ed1b3925bb318e9799afd054 100644 (file)
@@ -22,14 +22,19 @@ extern uint8_t ap_pm_target[];
 #define AP_TRAMPOLINE_PHYS  0x8000U
 #define AP_DATA_PHYS        0x8F00U
 
-/* Per-AP kernel stack size */
-#define AP_STACK_SIZE       4096U
+/* Per-AP kernel stack size — must be large enough for sched_ap_init()
+ * which calls kmalloc, kstack_alloc, kprintf under sched_lock. */
+#define AP_STACK_SIZE       8192U
 
 #define KERNEL_VIRT_BASE    0xC0000000U
 
 static struct cpu_info g_cpus[SMP_MAX_CPUS];
 static volatile uint32_t g_cpu_count = 0;
 
+/* Flag set by BSP after process_init + timer_init to signal APs
+ * that they can safely initialize their per-CPU schedulers. */
+volatile uint32_t ap_sched_go = 0;
+
 /* AP kernel stacks — statically allocated */
 static uint8_t ap_stacks[SMP_MAX_CPUS][AP_STACK_SIZE] __attribute__((aligned(16)));
 
@@ -51,6 +56,9 @@ static inline uint32_t read_cr3(void) {
 /* Called by each AP after it enters protected mode + paging.
  * This runs on the AP's own stack. */
 void ap_entry(void) {
+    /* Load the IDT on this AP (BSP already initialized it, APs just need lidt) */
+    idt_load_ap();
+
     /* Enable LAPIC on this AP */
     uint64_t apic_base_msr = rdmsr(0x1B);
     if (!(apic_base_msr & (1ULL << 11))) {
@@ -67,16 +75,33 @@ void ap_entry(void) {
     uint32_t my_id = lapic_get_id();
 
     /* Find our cpu_info slot, set up per-CPU GS, and mark started */
+    uint32_t my_cpu = 0;
     for (uint32_t i = 0; i < g_cpu_count; i++) {
         if (g_cpus[i].lapic_id == (uint8_t)my_id) {
+            my_cpu = i;
             percpu_setup_gs(i);
             __atomic_store_n(&g_cpus[i].started, 1, __ATOMIC_SEQ_CST);
             break;
         }
     }
 
-    /* AP is now idle — halt until needed.
-     * In the future, each AP will run its own scheduler. */
+    /* Wait for BSP to finish scheduler init (process_init sets PID 0).
+     * We check by waiting for the ap_sched_go flag set by the BSP after
+     * timer_init completes. */
+    while (!__atomic_load_n(&ap_sched_go, __ATOMIC_ACQUIRE)) {
+        __asm__ volatile("pause");
+    }
+
+    /* Initialize this AP's scheduler: create idle process + set current */
+    extern void sched_ap_init(uint32_t);
+    sched_ap_init(my_cpu);
+
+    /* Start LAPIC timer using BSP-calibrated ticks */
+    lapic_timer_start_ap();
+    kprintf("[SMP] CPU%u scheduler active.\n", my_cpu);
+    (void)my_cpu;
+
+    /* AP enters idle loop — timer interrupts will call schedule() */
     for (;;) {
         __asm__ volatile("sti; hlt");
     }
index 31c0bc050d49ed16ab303ceba6b9be762b346867..6870a69d8126ddd18809db645c4d63a3f0eb1660 100644 (file)
@@ -11,10 +11,13 @@ 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 (lapic_is_enabled() && lapic_get_id() != 0) {
+        /* AP: only run the local scheduler — tick accounting, VGA flush,
+         * UART poll, and sleep-queue wake are handled by the BSP. */
+        extern void schedule(void);
+        schedule();
+        return;
+    }
     if (g_tick_cb) g_tick_cb();
 }
 
index 343a8aa6c61ba1e4f0e61a949bc521dad4bc08bc..5eee84167dc426054a30da64e7d6f70f742ef75b 100644 (file)
@@ -79,6 +79,12 @@ void kernel_main(const struct boot_info* bi) {
     // 8. Start Timer (Preemption!) - 100Hz (like Linux CONFIG_HZ=100)
     timer_init(TIMER_HZ);
 
+    // 8b. Signal APs to start their per-CPU schedulers
+    {
+        extern volatile uint32_t ap_sched_go;
+        __atomic_store_n(&ap_sched_go, 1, __ATOMIC_RELEASE);
+    }
+
     hal_cpu_enable_interrupts();
 
     int init_ret = init_start(bi);
index 25d3d1d9faa0724cb722fad17f0fc8261cb340e1..c7215b964e7047583f094e2ac7ce9ab77228c591 100644 (file)
@@ -821,6 +821,61 @@ void process_init(void) {
     spin_unlock_irqrestore(&sched_lock, flags);
 }
 
+void sched_ap_init(uint32_t cpu) {
+    if (cpu == 0 || cpu >= SCHED_MAX_CPUS) return;
+
+    /* Allocate OUTSIDE sched_lock to avoid ABBA deadlock with heap lock:
+     * AP: sched_lock → heap_lock (kmalloc)
+     * BSP: heap_lock → timer ISR → sched_lock  ← deadlock */
+    struct process* idle = (struct process*)kmalloc(sizeof(*idle));
+    if (!idle) {
+        kprintf("[SCHED] CPU%u: OOM allocating idle process.\n", cpu);
+        return;
+    }
+    memset(idle, 0, sizeof(*idle));
+
+    void* kstack = kstack_alloc();
+    if (!kstack) {
+        kfree(idle);
+        kprintf("[SCHED] CPU%u: OOM allocating idle kstack.\n", cpu);
+        return;
+    }
+
+    /* Fill in idle process fields (no lock needed — not yet visible) */
+    idle->parent_pid = 0;
+    idle->priority = SCHED_NUM_PRIOS - 1;
+    idle->nice = 19;
+    idle->state = PROCESS_RUNNING;
+    idle->addr_space = kernel_as;
+    idle->cpu_id = cpu;
+    strcpy(idle->cwd, "/");
+    for (int i = 0; i < PROCESS_MAX_MMAPS; i++)
+        idle->mmaps[i].shmid = -1;
+    idle->kernel_stack = (uint32_t*)kstack;
+    arch_fpu_init_state(idle->fpu_state);
+
+    /* Take sched_lock only for PID assignment + list insertion */
+    uintptr_t flags = spin_lock_irqsave(&sched_lock);
+
+    idle->pid = next_pid++;
+    idle->tgid = idle->pid;
+
+    /* Insert into global process list */
+    idle->next = ready_queue_head;
+    idle->prev = ready_queue_tail;
+    ready_queue_tail->next = idle;
+    ready_queue_head->prev = idle;
+    ready_queue_tail = idle;
+
+    /* Register as this CPU's idle process and current process */
+    pcpu_rq[cpu].idle = idle;
+    percpu_set_current(idle);
+
+    spin_unlock_irqrestore(&sched_lock, flags);
+
+    kprintf("[SCHED] CPU%u idle process PID %u ready.\n", cpu, idle->pid);
+}
+
 extern spinlock_t sched_lock;
 
 void thread_wrapper(void (*fn)(void)) {
@@ -980,7 +1035,9 @@ void schedule(void) {
         hal_cpu_set_address_space(current_process->addr_space);
     }
 
-    if (current_process->kernel_stack) {
+    /* Only update TSS kernel stack on CPU 0 — the TSS is shared and
+     * only the BSP runs user processes that need ring 0 stack in TSS. */
+    if (cpu == 0 && current_process->kernel_stack) {
         hal_cpu_set_kernel_stack((uintptr_t)current_process->kernel_stack + KSTACK_SIZE);
     }