diff --git a/src/arch/interrupts.asm b/src/arch/interrupts.asm index d388c26..82c4bc7 100644 --- a/src/arch/interrupts.asm +++ b/src/arch/interrupts.asm @@ -7,9 +7,11 @@ global isr1_wrapper global isr8_wrapper global isr12_wrapper global isr14_wrapper +global isr_sched_ipi_wrapper extern timer_handler extern keyboard_handler extern mouse_handler +extern sched_ipi_handler extern exception_handler_c ; Helper to send EOI (End of Interrupt) to PIC @@ -85,6 +87,9 @@ isr1_wrapper: isr12_wrapper: ISR_NOERRCODE mouse_handler, 44 +isr_sched_ipi_wrapper: + ISR_NOERRCODE sched_ipi_handler, 65 + ; Common exception macro for exceptions WITHOUT error code %macro EXCEPTION_NOERRCODE 1 global exc%1_wrapper diff --git a/src/core/main.c b/src/core/main.c index 36a260d..37987f3 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -21,6 +21,7 @@ #include "wallpaper.h" #include "smp.h" #include "work_queue.h" +#include "lapic.h" // --- Limine Requests --- __attribute__((used, section(".requests"))) @@ -241,6 +242,9 @@ void kmain(void) { ps2_init(); asm("sti"); + // Initialize LAPIC for IPI support + lapic_init(); + // Initialize SMP — bring up all CPU cores if (smp_request.response != NULL) { uint32_t online = smp_init(smp_request.response); diff --git a/src/dev/ps2.c b/src/dev/ps2.c index 5aed3e1..6a11677 100644 --- a/src/dev/ps2.c +++ b/src/dev/ps2.c @@ -5,6 +5,8 @@ #include "io.h" #include "wm.h" #include "network.h" +#include "lapic.h" +#include "smp.h" #include extern void serial_print(const char *s); @@ -23,7 +25,14 @@ uint64_t timer_handler(registers_t *regs) { outb(0x20, 0x20); // EOI after processing to prevent nested timer interrupts extern uint64_t process_schedule(uint64_t current_rsp); - return process_schedule((uint64_t)regs); + uint64_t new_rsp = process_schedule((uint64_t)regs); + + // SMP: Wake AP cores to run their assigned processes + if (smp_cpu_count() > 1) { + lapic_send_ipi_all(); + } + + return new_rsp; } // --- Keyboard --- diff --git a/src/sys/idt.c b/src/sys/idt.c index 1305e56..8d562c4 100644 --- a/src/sys/idt.c +++ b/src/sys/idt.c @@ -230,6 +230,10 @@ void idt_register_interrupts(void) { idt_set_gate(30, exc30_wrapper, cs, 0x8E); idt_set_gate(31, exc31_wrapper, cs, 0x8E); + // SMP: Scheduling IPI for AP cores (vector 0x41 = 65) + extern void isr_sched_ipi_wrapper(void); + idt_set_gate(0x41, isr_sched_ipi_wrapper, cs, 0x8E); + } void idt_load(void) { diff --git a/src/sys/lapic.c b/src/sys/lapic.c new file mode 100644 index 0000000..5448b05 --- /dev/null +++ b/src/sys/lapic.c @@ -0,0 +1,50 @@ +// Copyright (c) 2023-2026 Chris (boreddevnl) +// This software is released under the GNU General Public License v3.0. See LICENSE file for details. +// This header needs to maintain in any file it is present in, as per the GPL license terms. +#include "lapic.h" +#include "platform.h" + +extern void serial_write(const char *str); + +// LAPIC is at physical 0xFEE00000. Access via HHDM. +static volatile uint32_t *lapic_base = 0; + +// LAPIC register offsets (byte offsets, divided by 4 for uint32_t* indexing) +#define LAPIC_ID (0x020 / 4) +#define LAPIC_EOI (0x0B0 / 4) +#define LAPIC_SVR (0x0F0 / 4) +#define LAPIC_ICR_LOW (0x300 / 4) +#define LAPIC_ICR_HIGH (0x310 / 4) + +void lapic_init(void) { + extern uint64_t hhdm_offset; + lapic_base = (volatile uint32_t *)(hhdm_offset + 0xFEE00000ULL); + + // Enable the LAPIC by setting the Spurious Interrupt Vector Register + // Bit 8 = APIC Software Enable, vector = 0xFF (spurious) + lapic_base[LAPIC_SVR] = 0x1FF; + + serial_write("[LAPIC] Initialized at HHDM + 0xFEE00000\n"); +} + +void lapic_eoi(void) { + if (lapic_base) { + lapic_base[LAPIC_EOI] = 0; + } +} + +void lapic_send_ipi_all(void) { + if (!lapic_base) return; + + // Send IPI to all excluding self + // ICR format: + // bits 7:0 = vector (IPI_SCHED_VECTOR = 0x41) + // bits 10:8 = delivery mode (000 = Fixed) + // bit 11 = destination mode (0 = Physical) + // bit 14 = level (1 = Assert) + // bits 19:18 = destination shorthand (11 = All Excluding Self) + uint32_t icr_low = IPI_SCHED_VECTOR | (0b11 << 18) | (1 << 14); + + // Writing ICR_LOW triggers the IPI send + lapic_base[LAPIC_ICR_LOW] = icr_low; +} diff --git a/src/sys/lapic.h b/src/sys/lapic.h new file mode 100644 index 0000000..57832fb --- /dev/null +++ b/src/sys/lapic.h @@ -0,0 +1,21 @@ +// Copyright (c) 2023-2026 Chris (boreddevnl) +// This software is released under the GNU General Public License v3.0. See LICENSE file for details. +// This header needs to maintain in any file it is present in, as per the GPL license terms. +#ifndef LAPIC_H +#define LAPIC_H + +#include + +// IPI vector used for scheduling on APs +#define IPI_SCHED_VECTOR 0x41 + +// Initialize LAPIC access (maps registers via HHDM) +void lapic_init(void); + +// Send End-of-Interrupt to the local APIC +void lapic_eoi(void); + +// Send a scheduling IPI to all APs (excludes self) +void lapic_send_ipi_all(void); + +#endif diff --git a/src/sys/process.c b/src/sys/process.c index aab6b0e..5ddc494 100644 --- a/src/sys/process.c +++ b/src/sys/process.c @@ -11,17 +11,21 @@ #include "elf.h" #include "wm.h" #include "spinlock.h" +#include "smp.h" +#include "lapic.h" extern void cmd_write(const char *str); extern void serial_write(const char *str); #define MAX_PROCESSES 16 +#define MAX_CPUS_SCHED 32 process_t processes[MAX_PROCESSES] __attribute__((aligned(16))); int process_count = 0; -static process_t* current_process = NULL; +static process_t* current_process[MAX_CPUS_SCHED] = {0}; // Per-CPU static uint32_t next_pid = 0; static void *free_kernel_stack_later = NULL; static spinlock_t runqueue_lock = SPINLOCK_INIT; +static uint32_t next_cpu_assign = 1; // Round-robin CPU assignment (start from CPU 1) void process_init(void) { for (int i = 0; i < MAX_PROCESSES; i++) { @@ -49,7 +53,8 @@ void process_init(void) { kernel_proc->used_memory = 32768; // Kernel stack kernel_proc->next = kernel_proc; // Circular linked list - current_process = kernel_proc; + kernel_proc->cpu_affinity = 0; // Kernel always on BSP + current_process[0] = kernel_proc; } void process_create(void* entry_point, bool is_user) { @@ -133,10 +138,12 @@ void process_create(void* entry_point, bool is_user) { asm volatile("fninit"); new_proc->fpu_initialized = true; + new_proc->cpu_affinity = 0; // Non-ELF processes stay on BSP + // Add to linked list (Critical Section) uint64_t rflags = spinlock_acquire_irqsave(&runqueue_lock); - new_proc->next = current_process->next; - current_process->next = new_proc; + new_proc->next = current_process[0]->next; + current_process[0]->next = new_proc; spinlock_release_irqrestore(&runqueue_lock, rflags); } @@ -316,10 +323,20 @@ process_t* process_create_elf(const char* filepath, const char* args_str) { asm volatile("fninit"); new_proc->fpu_initialized = true; + // Assign to an AP core via round-robin (if SMP is active) + uint32_t cpu_count = smp_cpu_count(); + if (cpu_count > 1) { + new_proc->cpu_affinity = next_cpu_assign; + next_cpu_assign++; + if (next_cpu_assign >= cpu_count) next_cpu_assign = 1; // Wrap, skip CPU 0 + } else { + new_proc->cpu_affinity = 0; + } + // Add to linked list (Critical Section) uint64_t rflags = spinlock_acquire_irqsave(&runqueue_lock); - new_proc->next = current_process->next; - current_process->next = new_proc; + new_proc->next = current_process[0]->next; + current_process[0]->next = new_proc; spinlock_release_irqrestore(&runqueue_lock, rflags); serial_write("[PROCESS] Spawned ELF Executable: "); @@ -329,7 +346,8 @@ process_t* process_create_elf(const char* filepath, const char* args_str) { } process_t* process_get_current(void) { - return current_process; + uint32_t cpu = smp_this_cpu_id(); + return current_process[cpu]; } uint64_t process_schedule(uint64_t current_rsp) { @@ -338,41 +356,54 @@ uint64_t process_schedule(uint64_t current_rsp) { free_kernel_stack_later = NULL; } - if (!current_process || !current_process->next || current_process == current_process->next) + uint32_t my_cpu = smp_this_cpu_id(); + process_t *cur = current_process[my_cpu]; + + if (!cur || !cur->next || cur == cur->next) return current_rsp; // Save context - current_process->rsp = current_rsp; + cur->rsp = current_rsp; - // Switch to next ready process + // Switch to next ready process assigned to this CPU extern uint32_t wm_get_ticks(void); uint32_t now = wm_get_ticks(); - process_t *start = current_process; - process_t *next_proc = current_process->next; + process_t *start = cur; + process_t *next_proc = cur->next; while (next_proc != start) { - if (next_proc->pid == 0 || next_proc->sleep_until == 0 || next_proc->sleep_until <= now) { - break; + // Only consider processes assigned to our CPU + if (next_proc->cpu_affinity == my_cpu) { + if (next_proc->pid == 0 || next_proc->sleep_until == 0 || next_proc->sleep_until <= now) { + break; + } } next_proc = next_proc->next; } - current_process = next_proc; + // If we didn't find a ready process for our CPU, stay on current + if (next_proc->cpu_affinity != my_cpu) { + return current_rsp; + } + + current_process[my_cpu] = next_proc; // Update Kernel Stack for User Mode interrupts and System Calls - if (current_process->is_user && current_process->kernel_stack) { - tss_set_stack(current_process->kernel_stack); - extern uint64_t kernel_syscall_stack; - kernel_syscall_stack = current_process->kernel_stack; + if (current_process[my_cpu]->is_user && current_process[my_cpu]->kernel_stack) { + tss_set_stack_cpu(my_cpu, current_process[my_cpu]->kernel_stack); + if (my_cpu == 0) { + extern uint64_t kernel_syscall_stack; + kernel_syscall_stack = current_process[my_cpu]->kernel_stack; + } } // Switch page table - paging_switch_directory(current_process->pml4_phys); + paging_switch_directory(current_process[my_cpu]->pml4_phys); - current_process->ticks++; + current_process[my_cpu]->ticks++; - return current_process->rsp; + return current_process[my_cpu]->rsp; } process_t* process_get_by_pid(uint32_t pid) { @@ -430,11 +461,12 @@ void process_terminate(process_t *to_delete) { // 3. Remove current from list prev->next = to_delete->next; - if (to_delete == current_process) { - current_process = to_delete->next; - // WARNING: If this was called as a regular function and not via a task switch, - // the stack might be in a weird state. But usually we call this via window manager - // or other external triggers. + // Update per-CPU current_process if this was the current on any CPU + uint32_t cpu_count = smp_cpu_count(); + for (uint32_t c = 0; c < cpu_count && c < MAX_CPUS_SCHED; c++) { + if (current_process[c] == to_delete) { + current_process[c] = to_delete->next; + } } // Mark slot as free @@ -442,11 +474,7 @@ void process_terminate(process_t *to_delete) { if (to_delete->user_stack_alloc) kfree(to_delete->user_stack_alloc); if (to_delete->kernel_stack_alloc) { - if (to_delete == current_process) { - free_kernel_stack_later = to_delete->kernel_stack_alloc; - } else { - kfree(to_delete->kernel_stack_alloc); - } + kfree(to_delete->kernel_stack_alloc); } extern void paging_destroy_user_pml4_phys(uint64_t pml4_phys); @@ -464,46 +492,49 @@ void process_terminate(process_t *to_delete) { uint64_t process_terminate_current(void) { uint64_t rflags = spinlock_acquire_irqsave(&runqueue_lock); - if (!current_process || current_process->pid == 0) { + uint32_t my_cpu = smp_this_cpu_id(); + process_t *cur = current_process[my_cpu]; + + if (!cur || cur->pid == 0) { spinlock_release_irqrestore(&runqueue_lock, rflags); return 0; } - process_cleanup_inner(current_process); + process_cleanup_inner(cur); // 2. Find previous process in circular list - process_t *prev = current_process; - while (prev->next != current_process) { + process_t *prev = cur; + while (prev->next != cur) { prev = prev->next; } // 3. Remove current from list - process_t *to_delete = current_process; + process_t *to_delete = cur; - if (prev == current_process) { + if (prev == cur) { // Only one process (should be kernel), cannot terminate. spinlock_release_irqrestore(&runqueue_lock, rflags); return to_delete->rsp; } prev->next = to_delete->next; - current_process = to_delete->next; + current_process[my_cpu] = to_delete->next; // Mark slot as free to_delete->pid = 0xFFFFFFFF; // 4. Load context for the NEXT process - if (current_process->is_user && current_process->kernel_stack) { - tss_set_stack(current_process->kernel_stack); - extern uint64_t kernel_syscall_stack; - kernel_syscall_stack = current_process->kernel_stack; + if (current_process[my_cpu]->is_user && current_process[my_cpu]->kernel_stack) { + tss_set_stack_cpu(my_cpu, current_process[my_cpu]->kernel_stack); + if (my_cpu == 0) { + extern uint64_t kernel_syscall_stack; + kernel_syscall_stack = current_process[my_cpu]->kernel_stack; + } } - paging_switch_directory(current_process->pml4_phys); + paging_switch_directory(current_process[my_cpu]->pml4_phys); - // 5. Actually free the memory (after switching state to avoid issues) - // We only safely free the user stack. Immediate freeing of the current - // kernel stack is unsafe while we are still running on it. + // 5. Free memory if (to_delete->user_stack_alloc) kfree(to_delete->user_stack_alloc); extern void paging_destroy_user_pml4_phys(uint64_t pml4_phys); @@ -511,17 +542,24 @@ uint64_t process_terminate_current(void) { paging_destroy_user_pml4_phys(to_delete->pml4_phys); } - // Clear pointers to avoid double-free during slot reuse to_delete->user_stack_alloc = NULL; free_kernel_stack_later = to_delete->kernel_stack_alloc; - to_delete->kernel_stack_alloc = NULL; // Leak the small kernel stack for safety + to_delete->kernel_stack_alloc = NULL; to_delete->pml4_phys = 0; - uint64_t next_rsp = current_process->rsp; + uint64_t next_rsp = current_process[my_cpu]->rsp; spinlock_release_irqrestore(&runqueue_lock, rflags); return next_rsp; } +// SMP: IPI handler called on AP cores when BSP broadcasts scheduling IPI +uint64_t sched_ipi_handler(registers_t *regs) { + lapic_eoi(); // Acknowledge the IPI + + // Run the scheduler for this CPU + return process_schedule((uint64_t)regs); +} + void process_push_gui_event(process_t *proc, gui_event_t *ev) { if (!proc) return; diff --git a/src/sys/process.h b/src/sys/process.h index 843a50e..4831a9c 100644 --- a/src/sys/process.h +++ b/src/sys/process.h @@ -52,6 +52,7 @@ typedef struct process { uint64_t ticks; uint64_t sleep_until; size_t used_memory; + uint32_t cpu_affinity; // Which CPU this process runs on (0 = BSP) } __attribute__((aligned(16))) process_t; typedef struct { @@ -70,6 +71,9 @@ uint64_t process_terminate_current(void); void process_terminate(process_t *proc); process_t* process_get_by_pid(uint32_t pid); +// SMP: IPI handler for AP scheduling (called from ISR) +uint64_t sched_ipi_handler(registers_t *regs); + void process_push_gui_event(process_t *proc, gui_event_t *ev); process_t* process_get_by_ui_window(void* win); diff --git a/src/sys/smp.c b/src/sys/smp.c index d9958e5..6b12719 100644 --- a/src/sys/smp.c +++ b/src/sys/smp.c @@ -89,16 +89,12 @@ static void ap_entry(struct limine_smp_info *info) { serial_write_num(cpu_states[my_id].lapic_id); serial_write(")\n"); - // 8. Enable interrupts and enter idle loop - // APs don't handle PIC interrupts (only BSP does with legacy PIC). - // But they WILL pick up work from the work queue. + // 8. Enable interrupts and enter idle halt loop. + // APs will be woken by scheduling IPIs from BSP (vector 0x41). + // The IPI handler does context switching for this CPU's processes. asm volatile("sti"); - // Import work queue drain function - extern void work_queue_drain_loop(void); - work_queue_drain_loop(); - - // Should never reach here + // Idle loop — APs halt and wait for IPI for (;;) { asm volatile("hlt"); } }