From b8212e21277ca9153666bb891c4779f7c4c609d1 Mon Sep 17 00:00:00 2001 From: pyao12 Date: Sun, 31 May 2026 12:11:57 +0800 Subject: [PATCH] [feat] Simple Multitask --- Makefile | 14 ++- include/scheduler.h | 39 ++++++ kernel/main.cpp | 38 +++++- kernel/scheduler/context_switch.S | 30 +++++ kernel/scheduler/scheduler.cpp | 189 ++++++++++++++++++++++++++++++ 5 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 include/scheduler.h create mode 100644 kernel/scheduler/context_switch.S create mode 100644 kernel/scheduler/scheduler.cpp diff --git a/Makefile b/Makefile index c60b38c..5ea0ff7 100644 --- a/Makefile +++ b/Makefile @@ -17,9 +17,11 @@ BOOT_OBJ = build/boot.o KERNEL_CPP = kernel/entry.cpp kernel/main.cpp kernel/serial.cpp kernel/fs.cpp \ kernel/memory/heap.cpp kernel/memory/pmm.cpp \ + kernel/scheduler/scheduler.cpp \ graphics/context.cpp graphics/draw.cpp \ fonts/pixel_font.cpp -KERNEL_OBJ = $(KERNEL_CPP:%.cpp=build/%.o) +KERNEL_ASM = kernel/scheduler/context_switch.S +KERNEL_OBJ = $(KERNEL_CPP:%.cpp=build/%.o) $(KERNEL_ASM:%.S=build/%.o) EFI_TOP_C = $(wildcard efi/lib/*.c) EFI_TOP_S = $(wildcard efi/lib/*.S) @@ -44,6 +46,7 @@ all: _bd $(EFI_OBJ) $(BOOT_OBJ) $(KERNEL_OBJ) _bd: @mkdir -p build/graphics build/kernel build/fonts build/kernel/memory \ + build/kernel/scheduler \ build/efi/lib build/efi/lib/x86_64 build/efi/lib/runtime build/efi/gnuefi $(EFI_CRT0_OBJ): efi/gnuefi/crt0-efi-x86_64.S | _bd @@ -86,6 +89,15 @@ build/kernel/memory/%.o: kernel/memory/%.cpp | _bd @echo "Compile CPP $<" @g++ $(KERNEL_CXXFLAGS) -c $< -o $@ +build/kernel/scheduler/%.o: kernel/scheduler/%.cpp | _bd + @echo "Compile CPP $<" + @g++ $(KERNEL_CXXFLAGS) -c $< -o $@ + +build/kernel/scheduler/%.o: kernel/scheduler/%.S | _bd + @echo "Compile AS $<" + @gcc -Iinclude -Iefi/inc -ffreestanding -fno-stack-protector -fno-stack-check \ + -fshort-wchar -mno-red-zone -fcf-protection=none -c $< -o $@ + build/graphics/%.o: graphics/%.cpp | _bd @echo "Compile CPP $<" @g++ $(KERNEL_CXXFLAGS) -c $< -o $@ diff --git a/include/scheduler.h b/include/scheduler.h new file mode 100644 index 0000000..71ea7e1 --- /dev/null +++ b/include/scheduler.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +#define TASK_STACK_SIZE (PAGE_SIZE * 4) // 16 KB kernel stack per task +#define TASK_MAX 32 +#define TASK_NAME_LEN 32 + +typedef enum { + TASK_STATE_READY, + TASK_STATE_RUNNING, + TASK_STATE_TERMINATED, +} task_state_t; + +typedef struct task { + UINT64 rsp; // saved stack pointer (for context switch) + UINT32 id; + task_state_t state; + char name[TASK_NAME_LEN]; + void* stack_base; // base address of kernel stack + struct task* next; // circular linked list +} task_t; + +// Create a new task. Returns task pointer or NULL on failure. +task_t* task_create(const char* name, void (*entry)(void)); + +// Yield CPU to next ready task (cooperative) +void yield(void); + +// Start the scheduler — does not return. Picks first READY task and runs it. +void scheduler_run(void); + +// Called by a task when it finishes — marks as TERMINATED and yields +void task_exit(void); + +// Get current running task +task_t* scheduler_current(void); diff --git a/kernel/main.cpp b/kernel/main.cpp index afac7a3..38b41cf 100644 --- a/kernel/main.cpp +++ b/kernel/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include extern EFI_SYSTEM_TABLE *ST; @@ -105,7 +106,40 @@ extern "C" void kernel_main() { } pf_print("Welcome to Sylva OS!\n"); - serial_write(" Kernel prepared well, start loop.\n"); + serial_write(" Kernel prepared well.\n"); - while (1) ASM ("hlt"); // 《30天》看多了 (doge + // --- Multitasking demo --- + serial_write("Sylva: creating tasks...\n"); + + // Task A: prints a message 3 times, yielding between each + task_create("taskA", []() { + for (int i = 0; i < 3; i++) { + serial_write("[taskA] running iteration "); + serial_write_hex(i); + serial_write("\n"); + yield(); + } + serial_write("[taskA] done\n"); + }); + + // Task B: prints a message 5 times + task_create("taskB", []() { + for (int i = 0; i < 5; i++) { + serial_write("[taskB] hello from taskB #"); + serial_write_hex(i); + serial_write("\n"); + yield(); + } + serial_write("[taskB] done\n"); + }); + + // Task C: short task + task_create("taskC", []() { + serial_write("[taskC] quick task\n"); + yield(); + serial_write("[taskC] finished\n"); + }); + + serial_write("Sylva: starting scheduler\n"); + scheduler_run(); // never returns } \ No newline at end of file diff --git a/kernel/scheduler/context_switch.S b/kernel/scheduler/context_switch.S new file mode 100644 index 0000000..9b14b1b --- /dev/null +++ b/kernel/scheduler/context_switch.S @@ -0,0 +1,30 @@ +.intel_syntax noprefix + +// void context_switch(UINT64* old_rsp, UINT64 new_rsp) +// rdi = &old_rsp (pointer to save current RSP) +// rsi = new_rsp (value of new stack pointer) +// +// Saves/restores callee-saved registers only. +// On first switch into a new task, ret lands on task_entry_trampoline. +.global context_switch +context_switch: + push rbx + push rbp + push r12 + push r13 + push r14 + push r15 + + mov [rdi], rsp // save current RSP + mov rsp, rsi // switch to new stack + + pop r15 + pop r14 + pop r13 + pop r12 + pop rbp + pop rbx + + ret + +.section .note.GNU-stack,"",@progbits diff --git a/kernel/scheduler/scheduler.cpp b/kernel/scheduler/scheduler.cpp new file mode 100644 index 0000000..686f87a --- /dev/null +++ b/kernel/scheduler/scheduler.cpp @@ -0,0 +1,189 @@ +#include +#include +#include +#include +#include + +// Assembly: context_switch(UINT64* old_rsp, UINT64 new_rsp) +extern "C" void context_switch(UINT64* old_rsp, UINT64 new_rsp); + +static task_t g_tasks[TASK_MAX]; +static UINT32 g_task_count = 0; +static task_t* g_current = NULL; +static task_t* g_task_list = NULL; // circular linked list head + +// Trampoline: first thing a new task runs after context_switch. +// The entry function pointer is stored in the task's name field +// (we repurpose a slot — actually we store it in a simple global array). +static void (*g_task_entries[TASK_MAX])(void); + +extern "C" void task_entry_trampoline() { + task_t* cur = scheduler_current(); + if (cur && g_task_entries[cur->id]) { + g_task_entries[cur->id](); // call the user function + } + task_exit(); // clean up when done +} + +task_t* task_create(const char* name, void (*entry)(void)) { + if (g_task_count >= TASK_MAX) { + serial_write("SCHED: task limit reached\n"); + return NULL; + } + + UINT32 id = g_task_count++; + task_t* task = &g_tasks[id]; + + // Store entry function for the trampoline + g_task_entries[id] = entry; + + // Allocate kernel stack + UINTN stack_pages = TASK_STACK_SIZE / PAGE_SIZE; + void* stack = pmm_alloc_pages(stack_pages); + if (!stack) { + serial_write("SCHED: stack alloc failed for task "); + serial_write(name); + serial_write("\n"); + return NULL; + } + + // Fill task struct + task->id = id; + task->state = TASK_STATE_READY; + task->stack_base = stack; + + // Copy name + const char* s = name; + char* d = task->name; + for (int i = 0; i < TASK_NAME_LEN - 1 && *s; i++) { + *d++ = *s++; + } + *d = '\0'; + + // Set up initial stack for first context_switch into this task. + // Stack grows downward. context_switch will pop 6 regs then ret. + // + // Layout (high addr -> low addr): + // [stack + TASK_STACK_SIZE] <- top + // return addr = task_entry_trampoline (ret goes here) + // rbx = 0 + // rbp = 0 + // r12 = 0 + // r13 = 0 + // r14 = 0 + // r15 = 0 <- RSP points here initially + // + UINT64* sp = (UINT64*)((UINT8*)stack + TASK_STACK_SIZE); + + // Push return address (task_entry_trampoline) + *--sp = (UINT64)task_entry_trampoline; + + // Push callee-saved registers (all zero) + *--sp = 0; // rbx + *--sp = 0; // rbp + *--sp = 0; // r12 + *--sp = 0; // r13 + *--sp = 0; // r14 + *--sp = 0; // r15 + + task->rsp = (UINT64)sp; + + // Insert into circular linked list + if (g_task_list == NULL) { + task->next = task; // points to itself (single element circle) + g_task_list = task; + } else { + // Insert after current tail (g_task_list is the "last" in circle) + task->next = g_task_list->next; + g_task_list->next = task; + g_task_list = task; // new tail + } + + serial_write("SCHED: created task '"); + serial_write(task->name); + serial_write("' id="); + serial_write_hex(id); + serial_write("\n"); + + return task; +} + +void yield(void) { + if (g_current == NULL || g_task_list == NULL) return; + + task_t* cur = g_current; + task_t* next = cur->next; + + // Skip terminated tasks + while (next->state == TASK_STATE_TERMINATED && next != cur) { + next = next->next; + } + if (next->state == TASK_STATE_TERMINATED) return; // all terminated + + if (next == cur) return; // only one task, nothing to do + + cur->state = TASK_STATE_READY; + next->state = TASK_STATE_RUNNING; + g_current = next; + + context_switch(&cur->rsp, next->rsp); +} + +void scheduler_run(void) { + if (g_task_list == NULL) { + serial_write("SCHED: no tasks to run\n"); + return; + } + + // Find first READY task + task_t* start = g_task_list->next; // head of circle + task_t* t = start; + do { + if (t->state == TASK_STATE_READY) { + break; + } + t = t->next; + } while (t != start); + + if (t->state != TASK_STATE_READY) { + serial_write("SCHED: no READY tasks\n"); + return; + } + + g_current = t; + t->state = TASK_STATE_RUNNING; + + serial_write("SCHED: starting task '"); + serial_write(t->name); + serial_write("'\n"); + + // First context switch — no old RSP to save (we're still in scheduler_run) + // Just switch to the task's stack directly. + // We need a dummy old_rsp to satisfy the API, but we never return here. + UINT64 dummy_rsp; + context_switch(&dummy_rsp, t->rsp); + + // We only return here when ALL tasks are terminated + serial_write("SCHED: all tasks finished\n"); + while (1) ASM ("hlt"); +} + +void task_exit(void) { + if (g_current == NULL) return; + + serial_write("SCHED: task '"); + serial_write(g_current->name); + serial_write("' exited\n"); + + g_current->state = TASK_STATE_TERMINATED; + + // Yield to next task — we won't come back + yield(); + + // Should never reach here + while (1) ASM ("hlt"); +} + +task_t* scheduler_current(void) { + return g_current; +}