From: Tulio A M Mendes Date: Mon, 16 Feb 2026 19:00:38 +0000 (-0300) Subject: feat: AP scheduler entry (SMP Phase 3) X-Git-Url: https://projects.tadryanom.me/?a=commitdiff_plain;h=da6ce662266f54c673350e20087c0ba6db8c2383;p=AdrOS.git feat: AP scheduler entry (SMP Phase 3) 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. --- diff --git a/include/arch/x86/idt.h b/include/arch/x86/idt.h index 82d690e..980f17e 100644 --- a/include/arch/x86/idt.h +++ b/include/arch/x86/idt.h @@ -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); diff --git a/include/arch/x86/lapic.h b/include/arch/x86/lapic.h index 9da71ea..da0349a 100644 --- a/include/arch/x86/lapic.h +++ b/include/arch/x86/lapic.h @@ -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); diff --git a/src/arch/x86/idt.c b/src/arch/x86/idt.c index 2ca749d..d1075b2 100644 --- a/src/arch/x86/idt.c +++ b/src/arch/x86/idt.c @@ -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); diff --git a/src/arch/x86/lapic.c b/src/arch/x86/lapic.c index 0b73027..dae8457 100644 --- a/src/arch/x86/lapic.c +++ b/src/arch/x86/lapic.c @@ -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); diff --git a/src/arch/x86/smp.c b/src/arch/x86/smp.c index fbc15f4..e6db605 100644 --- a/src/arch/x86/smp.c +++ b/src/arch/x86/smp.c @@ -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"); } diff --git a/src/hal/x86/timer.c b/src/hal/x86/timer.c index 31c0bc0..6870a69 100644 --- a/src/hal/x86/timer.c +++ b/src/hal/x86/timer.c @@ -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(); } diff --git a/src/kernel/main.c b/src/kernel/main.c index 343a8aa..5eee841 100644 --- a/src/kernel/main.c +++ b/src/kernel/main.c @@ -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); diff --git a/src/kernel/scheduler.c b/src/kernel/scheduler.c index 25d3d1d..c7215b9 100644 --- a/src/kernel/scheduler.c +++ b/src/kernel/scheduler.c @@ -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); }