CHECKP: multi core scheduling

This commit is contained in:
boreddevnl 2026-03-17 22:11:32 +01:00
parent a7c3cccce7
commit 9fb307e603
9 changed files with 190 additions and 59 deletions

View file

@ -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

View file

@ -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);

View file

@ -5,6 +5,8 @@
#include "io.h"
#include "wm.h"
#include "network.h"
#include "lapic.h"
#include "smp.h"
#include <stdbool.h>
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 ---

View file

@ -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) {

50
src/sys/lapic.c Normal file
View file

@ -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;
}

21
src/sys/lapic.h Normal file
View file

@ -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 <stdint.h>
// 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

View file

@ -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) {
// 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);
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->kernel_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,12 +474,8 @@ 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);
}
}
extern void paging_destroy_user_pml4_phys(uint64_t pml4_phys);
if (to_delete->pml4_phys && to_delete->is_user) {
@ -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);
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->kernel_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;

View file

@ -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);

View file

@ -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"); }
}