From d449150169370e1399d778a646a2c593d79c58ef Mon Sep 17 00:00:00 2001 From: JimmyBinoculars Date: Thu, 26 Feb 2026 21:08:06 +0000 Subject: [PATCH] Task scheduler --- Makefile | 12 +- README.md | 201 +++++++++++++++++++-------- boot_info.h | 1 + commands.c | 213 ++++++++++++++++++++++++++++ context_switch.S | 44 ++++++ kernel.c | 14 +- main.c | 6 + task.c | 351 +++++++++++++++++++++++++++++++++++++++++++++++ task.h | 56 ++++++++ 9 files changed, 836 insertions(+), 62 deletions(-) create mode 100644 context_switch.S create mode 100644 task.c create mode 100644 task.h diff --git a/Makefile b/Makefile index 1dd7d87..de40e2f 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ TARGET = BOOTX64.EFI TARGET_SO = bootx64.so OBJ = $(BUILD_DIR)/main.o KERNEL_TARGET = kernel.elf -KERNEL_OBJS = $(BUILD_DIR)/kernel.o $(BUILD_DIR)/string_utils.o $(BUILD_DIR)/commands.o $(BUILD_DIR)/idt.o $(BUILD_DIR)/isr.o $(BUILD_DIR)/memory.o +KERNEL_OBJS = $(BUILD_DIR)/kernel.o $(BUILD_DIR)/string_utils.o $(BUILD_DIR)/commands.o $(BUILD_DIR)/idt.o $(BUILD_DIR)/isr.o $(BUILD_DIR)/memory.o $(BUILD_DIR)/task.o $(BUILD_DIR)/context_switch.o KERNEL_LD = kernel.ld # QEMU settings @@ -166,6 +166,16 @@ $(BUILD_DIR)/memory.o: $(SRC_DIR)/memory.c $(CC) -ffreestanding -fno-stack-protector -fno-pic -fshort-wchar \ -mno-red-zone -Wall -Wextra $(EFI_INCLUDES) -c $< -o $@ +$(BUILD_DIR)/task.o: $(SRC_DIR)/task.c + @echo "Compiling task.c..." + $(CC) -ffreestanding -fno-stack-protector -fno-pic -fshort-wchar \ + -mno-red-zone -Wall -Wextra $(EFI_INCLUDES) -c $< -o $@ + +$(BUILD_DIR)/context_switch.o: $(SRC_DIR)/context_switch.S + @echo "Assembling context_switch.S..." + $(CC) -ffreestanding -fno-stack-protector -fno-pic -fshort-wchar \ + -mno-red-zone -Wall -Wextra -c $< -o $@ + # Link kernel ELF $(BUILD_DIR)/$(KERNEL_TARGET): $(KERNEL_OBJS) $(KERNEL_LD) @echo "Linking kernel ELF..." diff --git a/README.md b/README.md index 2e1477e..1434193 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ # Simple UEFI Operating System -A minimal bootable UEFI operating system written in C that demonstrates basic UEFI functionality. +A minimal bootable UEFI operating system written in C that demonstrates UEFI boot loading, memory management, interrupt handling, and cooperative multitasking. ## Features -- **UEFI Boot**: Boots directly on UEFI firmware +- **UEFI Boot**: Boots directly on UEFI firmware via a PE32+ loader +- **ELF64 Kernel Loader**: Reads and maps a standalone kernel from the EFI partition - **Console I/O**: Interactive keyboard input and screen output -- **System Information**: Displays firmware details -- **Kernel Loader**: Loads an ELF64 kernel from the EFI partition -- **Simple Commands**: Minimal interactive command interface +- **Interrupt Handling**: IDT setup with CPU exception handlers (vectors 0–31) and hardware IRQ dispatch +- **Memory Management**: Bitmap-based physical memory manager (PMM), 4-level x86-64 paging, and a first-fit heap allocator with block coalescing +- **Cooperative Multitasking**: Process control blocks, assembly context switching, and a round-robin task scheduler +- **Interactive Shell**: Command registry with `help`, `man`, built-in commands, and test utilities ## Requirements @@ -28,30 +30,30 @@ make install-deps Or manually: ```bash -sudo apt-get install gnu-efi qemu-system-x86 ovmf gcc binutils make +sudo apt-get install gnu-efi qemu-system-x86 ovmf gcc binutils make xorriso mtools ``` ### On Arch Linux: ```bash -sudo pacman -S gnu-efi qemu-system-x86 edk2-ovmf gcc binutils make +sudo pacman -S gnu-efi qemu-system-x86 edk2-ovmf gcc binutils make xorriso mtools ``` ### On Fedora/RHEL: ```bash -sudo dnf install gnu-efi qemu-system-x86 edk2-ovmf gcc binutils make +sudo dnf install gnu-efi qemu-system-x86 edk2-ovmf gcc binutils make xorriso mtools ``` ## Building -Build the UEFI application and kernel: +Build the UEFI loader and kernel: ```bash make ``` -This will create `build/EFI/BOOT/BOOTX64.EFI` and `build/kernel.elf`. +This creates `build/EFI/BOOT/BOOTX64.EFI` (UEFI loader) and `build/kernel.elf` (kernel binary). ## Running @@ -61,63 +63,151 @@ Test with QEMU (no graphics mode): make run ``` +To build and run from an ISO image: + +```bash +make runiso +``` + The loader expects `kernel.elf` at the root of the EFI partition (next to the `EFI/` directory). ### QEMU Controls: - **Exit QEMU**: Press `Ctrl+A`, then `X` - **Shutdown OS**: Type `shutdown` in the OS -## Commands in the OS +## Commands Once the OS boots, you can use these commands: -- `help` - Display all available commands -- `man ` - Display detailed help for a command -- `shutdown` - Shutdown the system -- `clear` - Clear the screen -- `about` - Display system information +| Command | Description | +|---------|-------------| +| `help` | Display all available commands | +| `man ` | Display detailed help for a command | +| `shutdown` | Shutdown the system | +| `clear` | Clear the screen | +| `about` | Display system information | +| `mem` | Display memory statistics (PMM, heap, paging) | +| `ps` | List all active tasks with PID, state, and name | +| `spawn [name]` | Create a demo background task | +| `memtest` | Run memory allocation/deallocation tests | +| `tasktest` | Spawn multiple concurrent tasks to test the scheduler | + +### Example Session -Example usage: ``` -> help --> man shutdown --> clear -> about +-> mem +-> memtest +-> spawn worker1 +-> spawn worker2 +-> ps +-> tasktest -> shutdown ``` +### Testing Memory Management + +The `memtest` command runs through four test phases: + +1. **Heap allocation** — allocates 8 blocks of increasing size (16 to 4096 bytes) via `kmalloc()` +2. **Heap free** — frees all allocated blocks via `kfree()` and verifies coalescing +3. **PMM single page** — allocates and frees a single 4 KB page +4. **PMM multi-page** — allocates and frees 4 contiguous pages + +Each phase reports addresses, success/failure, and usage statistics. + +### Testing Task Scheduling + +The `tasktest` command: + +1. Spawns 3 worker tasks (`worker-A`, `worker-B`, `worker-C`) +2. Each worker prints 3 progress steps, yielding between each +3. The shell yields to let workers run in round-robin order +4. Displays the task list after completion + +You can also manually test with `spawn` and `ps`: + +``` +-> spawn myTask +Created task 'myTask' (PID 1) +-> ps + PID STATE SWITCHES NAME + --- ---------- -------- ---- + 0 RUNNING 5 kernel + 1 READY 0 myTask + + Active tasks: 2 / 32 +``` + +Background tasks run whenever the shell yields (which happens automatically while waiting for keyboard input). + ## Project Structure ``` . -├── main.c # UEFI loader (reads and loads kernel ELF) -├── kernel.c # Kernel entry point and main loop -├── commands.c # Command registry and handlers -├── commands.h # Command interface definitions -├── string_utils.c # String utility functions -├── string_utils.h # String utility declarations -├── kernel.ld # Kernel linker script -├── boot_info.h # Shared boot interface -├── Makefile # Build configuration -└── README.md # This file +├── main.c # UEFI loader (reads and loads kernel ELF) +├── kernel.c # Kernel entry point and interactive shell loop +├── kernel.ld # Kernel linker script (base address 0x100000) +├── boot_info.h # Shared boot interface (BootInfo struct) +├── commands.c # Command registry and handler implementations +├── commands.h # Command interface definitions +├── string_utils.c # String utility functions (trim, compare, etc.) +├── string_utils.h # String utility declarations +├── idt.c # IDT setup, exception handlers, PIC EOI +├── idt.h # IDT types (ISRFrame) and init declaration +├── isr.S # ISR stub table (vectors 0–255) with common handler +├── memory.c # PMM (bitmap), paging (4-level), heap (first-fit) +├── memory.h # Memory subsystem declarations and constants +├── task.c # Task manager: PCB pool, scheduler, yield/exit +├── task.h # Task types (Task, TaskState) and API +├── context_switch.S # Assembly cooperative context switch (x86-64) +├── Makefile # Build configuration +└── README.md # This file ``` -## How It Works +## Architecture -1. **UEFI Entry Point**: The `efi_main` function is the entry point called by UEFI firmware -2. **Initialization**: GNU-EFI library is initialized with the system table -3. **Console Setup**: Output is configured and screen is cleared -4. **Kernel Load**: The loader reads `kernel.elf` and maps its loadable segments -5. **Main Loop**: The kernel enters a loop waiting for keyboard input -6. **Command Processing**: User commands are parsed and dispatched to registered handlers -7. **Command Execution**: Each command is executed via its handler function -8. **Shutdown**: UEFI runtime services are used to shutdown the system +### Boot Flow -**Command System Architecture:** -- Commands are registered in a static array with metadata (name, description, usage, handler) -- The `execute_command()` function parses input, splits command and arguments, and dispatches to handlers -- The `man` command provides detailed help by looking up command metadata -- New commands can be easily added by implementing a handler and registering it +1. **UEFI Entry** — `efi_main()` in `main.c` is called by firmware +2. **GNU-EFI Init** — library initialized with the system table +3. **Kernel Load** — `kernel.elf` is read from the EFI partition, ELF64 PT_LOAD segments are mapped into memory +4. **BootInfo Setup** — function pointers for console I/O, shutdown, and page allocation are packaged into a `BootInfo` struct +5. **Kernel Entry** — `kmain()` is called with the `BootInfo` pointer + +### Kernel Initialization + +1. **IDT** — CPU exception vectors (0–31) are installed; firmware IRQ handlers are preserved +2. **Memory** — PMM allocates a 16 MB page pool via UEFI, bitmap tracks free/used pages; paging reads CR3; heap carves 256 KB from the PMM +3. **Tasks** — scheduler initializes; the running kernel becomes task 0 +4. **Shell** — interactive loop with non-blocking input and cooperative yielding + +### Memory Management + +| Component | Description | +|-----------|-------------| +| **PMM** | Bitmap-based page-frame allocator managing a 16 MB pool (4096 pages); supports single and contiguous multi-page allocation | +| **Paging** | Walks/creates 4-level x86-64 page tables (PML4 → PDPT → PD → PT); supports map, unmap, and virtual-to-physical translation | +| **Heap** | First-fit free-list allocator with block splitting and bidirectional coalescing; 16-byte alignment; magic number corruption detection | + +### Task System + +| Component | Description | +|-----------|-------------| +| **PCB** | `Task` struct with PID, state, name, saved RSP, stack, entry function, and scheduling metadata | +| **States** | `FREE` → `READY` → `RUNNING` → `TERMINATED` → `FREE` | +| **Scheduler** | Round-robin selection among `READY` tasks via `task_yield()` | +| **Context Switch** | Assembly routine saves/restores callee-saved registers (`rbp`, `rbx`, `r12`–`r15`) and `RFLAGS`, swaps RSP | +| **Stack** | Each task gets 32 KB (8 pages) allocated from PMM; freed on `task_exit()` | +| **Limits** | Up to 32 concurrent tasks; cooperative (voluntary yield) only | + +### Command System + +- Commands are registered in a static array with metadata (name, description, usage, handler function) +- `execute_command()` parses input, splits command and arguments, and dispatches to the matching handler +- `man` provides detailed help by looking up command metadata +- New commands are added by writing a handler and appending to the `commands[]` array ## Building for Real Hardware @@ -166,30 +256,23 @@ Ensure you have QEMU with x86_64 support and OVMF firmware installed. - **Architecture**: x86_64 - **Firmware Interface**: UEFI 2.x - **Development Library**: GNU-EFI -- **Executable Format**: PE32+ (Portable Executable for EFI) +- **Loader Format**: PE32+ (Portable Executable for EFI) +- **Kernel Format**: ELF64 - **Boot Protocol**: UEFI Boot Services +- **Memory Model**: Identity-mapped (UEFI), 4-level paging (kernel) +- **Scheduling**: Cooperative round-robin multitasking ## License This is a minimal educational example. Feel free to use and modify as needed. -## Further Development +## Adding New Commands -The codebase is now modular and easy to extend: - -**Adding New Commands:** 1. Open `commands.c` -2. Create a new handler function: `static void cmd_yourcommand(BootInfo *Boot, CHAR16 *Args)` -3. Add your command to the `commands[]` array with name, description, usage, and handler -4. Rebuild with `make` - -**Other Ideas for expansion:** -- File system support -- Memory management -- Graphics output -- Network stack -- Device drivers -- Multi-tasking +2. Add a forward declaration: `static void cmd_yourcommand(BootInfo *Boot, CHAR16 *Args);` +3. Add an entry to the `commands[]` array (before the sentinel) +4. Implement the handler function +5. Rebuild with `make` ## Resources diff --git a/boot_info.h b/boot_info.h index 8b8bf27..219bccb 100644 --- a/boot_info.h +++ b/boot_info.h @@ -11,6 +11,7 @@ typedef struct { EFI_STATUS (*clear_screen)(void); EFI_STATUS (*set_attribute)(UINTN Attribute); EFI_STATUS (*read_key)(EFI_INPUT_KEY *Key); + EFI_STATUS (*try_read_key)(EFI_INPUT_KEY *Key); /* non-blocking */ void (*shutdown)(void); EFI_STATUS (*alloc_pages)(UINTN pages, EFI_PHYSICAL_ADDRESS *addr); EFI_STATUS (*free_pages)(EFI_PHYSICAL_ADDRESS addr, UINTN pages); diff --git a/commands.c b/commands.c index 97105ad..8ce2ebd 100644 --- a/commands.c +++ b/commands.c @@ -2,6 +2,7 @@ #include "commands.h" #include "string_utils.h" #include "memory.h" +#include "task.h" #define SAFE_PRINT(Boot, ...) \ do { \ @@ -17,6 +18,10 @@ static void cmd_man(BootInfo *Boot, CHAR16 *Args); static void cmd_clear(BootInfo *Boot, CHAR16 *Args); static void cmd_about(BootInfo *Boot, CHAR16 *Args); static void cmd_mem(BootInfo *Boot, CHAR16 *Args); +static void cmd_ps(BootInfo *Boot, CHAR16 *Args); +static void cmd_spawn(BootInfo *Boot, CHAR16 *Args); +static void cmd_memtest(BootInfo *Boot, CHAR16 *Args); +static void cmd_tasktest(BootInfo *Boot, CHAR16 *Args); // Command registry static Command commands[] = { @@ -56,6 +61,30 @@ static Command commands[] = { L"Usage: mem\n\r Shows physical memory, heap, and paging information.", cmd_mem }, + { + L"ps", + L"List running tasks", + L"Usage: ps\n\r Displays all active tasks with PID, state, and name.", + cmd_ps + }, + { + L"spawn", + L"Spawn a demo background task", + L"Usage: spawn [name]\n\r Creates a cooperative demo task.\n\r Optional argument sets the task name.", + cmd_spawn + }, + { + L"memtest", + L"Test memory allocation and deallocation", + L"Usage: memtest\n\r Allocates and frees heap and page memory to verify\n\r the memory manager is working correctly.", + cmd_memtest + }, + { + L"tasktest", + L"Test task scheduler with multiple tasks", + L"Usage: tasktest\n\r Spawns several concurrent tasks that run cooperatively\n\r and report their progress, demonstrating context switching.", + cmd_tasktest + }, {NULL, NULL, NULL, NULL} // Sentinel }; @@ -153,6 +182,7 @@ static void cmd_about(BootInfo *Boot, CHAR16 *Args) SAFE_PRINT(Boot, L" - Console I/O\n\r"); SAFE_PRINT(Boot, L" - ELF64 Kernel Loader\n\r"); SAFE_PRINT(Boot, L" - Memory Management (PMM + Heap)\n\r"); + SAFE_PRINT(Boot, L" - Cooperative Multitasking\n\r"); SAFE_PRINT(Boot, L" - Interactive Command Interface\n\r"); SAFE_PRINT(Boot, L"\n\r"); SAFE_PRINT(Boot, L"Type 'help' for available commands.\n\r"); @@ -165,6 +195,189 @@ static void cmd_mem(BootInfo *Boot, CHAR16 *Args) memory_print_stats(Boot); } +/* ---------------------------------------------------------------- + * Demo task – runs cooperatively, prints progress, then exits + * ---------------------------------------------------------------- */ + +static void demo_task_fn(void *arg) +{ + BootInfo *Boot = (BootInfo *)arg; + Task *self = task_current(); + UINTN i; + + SAFE_PRINT(Boot, L"[%s:%d] started\n\r", self->name, self->pid); + + for (i = 0; i < 5; i++) { + SAFE_PRINT(Boot, L"[%s:%d] tick %d/5\n\r", + self->name, self->pid, i + 1); + task_yield(); + } + + SAFE_PRINT(Boot, L"[%s:%d] finished\n\r", self->name, self->pid); +} + +static void cmd_ps(BootInfo *Boot, CHAR16 *Args) +{ + (void)Args; + task_print_list(Boot); +} + +static void cmd_spawn(BootInfo *Boot, CHAR16 *Args) +{ + Task *t; + const CHAR16 *name; + + if (Args != NULL && Args[0] != L'\0') { + trim_spaces_inplace(Args); + name = Args; + } else { + name = L"demo"; + } + + t = task_create(name, demo_task_fn, Boot); + if (t == NULL) { + SAFE_PRINT(Boot, L"Failed to create task (out of slots or memory).\n\r"); + return; + } + + SAFE_PRINT(Boot, L"Created task '%s' (PID %d)\n\r", t->name, t->pid); +} + +/* ---------------------------------------------------------------- + * memtest – exercise the heap and PMM allocators + * ---------------------------------------------------------------- */ + +static void cmd_memtest(BootInfo *Boot, CHAR16 *Args) +{ + void *ptrs[8]; + UINTN sizes[] = { 16, 64, 128, 256, 512, 1024, 2048, 4096 }; + UINTN i; + UINT64 page; + UINTN h_total, h_used, h_free, h_blocks; + (void)Args; + + SAFE_PRINT(Boot, L"\n\r"); + SAFE_PRINT(Boot, L"Memory Test\n\r"); + SAFE_PRINT(Boot, L"================================================\n\r"); + SAFE_PRINT(Boot, L"\n\r"); + + /* --- Heap allocation test --- */ + SAFE_PRINT(Boot, L"[1] Heap allocation test\n\r"); + for (i = 0; i < 8; i++) { + ptrs[i] = kmalloc(sizes[i]); + if (ptrs[i] != NULL) { + SAFE_PRINT(Boot, L" kmalloc(%4d) = 0x%lx OK\n\r", + sizes[i], (UINT64)(UINTN)ptrs[i]); + } else { + SAFE_PRINT(Boot, L" kmalloc(%4d) = NULL FAIL\n\r", sizes[i]); + } + } + + heap_get_stats(&h_total, &h_used, &h_free, &h_blocks); + SAFE_PRINT(Boot, L" Heap after alloc: used=%d free=%d blocks=%d\n\r", + h_used, h_free, h_blocks); + + /* --- Heap free test --- */ + SAFE_PRINT(Boot, L"\n\r[2] Heap free test\n\r"); + for (i = 0; i < 8; i++) { + if (ptrs[i] != NULL) { + kfree(ptrs[i]); + SAFE_PRINT(Boot, L" kfree(0x%lx) OK\n\r", + (UINT64)(UINTN)ptrs[i]); + } + } + + heap_get_stats(&h_total, &h_used, &h_free, &h_blocks); + SAFE_PRINT(Boot, L" Heap after free: used=%d free=%d blocks=%d\n\r", + h_used, h_free, h_blocks); + + /* --- PMM page allocation test --- */ + SAFE_PRINT(Boot, L"\n\r[3] PMM page allocation test\n\r"); + SAFE_PRINT(Boot, L" Free pages before: %d\n\r", pmm_get_free_pages()); + + page = pmm_alloc_page(); + if (page != 0) { + SAFE_PRINT(Boot, L" pmm_alloc_page() = 0x%lx OK\n\r", page); + SAFE_PRINT(Boot, L" Free pages after alloc: %d\n\r", pmm_get_free_pages()); + pmm_free_page(page); + SAFE_PRINT(Boot, L" pmm_free_page() OK\n\r"); + SAFE_PRINT(Boot, L" Free pages after free: %d\n\r", pmm_get_free_pages()); + } else { + SAFE_PRINT(Boot, L" pmm_alloc_page() = 0 FAIL (out of pages)\n\r"); + } + + /* --- Multi-page allocation test --- */ + SAFE_PRINT(Boot, L"\n\r[4] PMM multi-page allocation test (4 pages)\n\r"); + page = pmm_alloc_pages(4); + if (page != 0) { + SAFE_PRINT(Boot, L" pmm_alloc_pages(4) = 0x%lx OK\n\r", page); + SAFE_PRINT(Boot, L" Free pages after alloc: %d\n\r", pmm_get_free_pages()); + pmm_free_pages(page, 4); + SAFE_PRINT(Boot, L" pmm_free_pages() OK\n\r"); + SAFE_PRINT(Boot, L" Free pages after free: %d\n\r", pmm_get_free_pages()); + } else { + SAFE_PRINT(Boot, L" pmm_alloc_pages(4) = 0 FAIL\n\r"); + } + + SAFE_PRINT(Boot, L"\n\rAll memory tests completed.\n\r\n\r"); +} + +/* ---------------------------------------------------------------- + * tasktest – spawn multiple concurrent tasks to exercise scheduler + * ---------------------------------------------------------------- */ + +static void worker_task_fn(void *arg) +{ + BootInfo *Boot = (BootInfo *)arg; + Task *self = task_current(); + UINTN i; + + for (i = 0; i < 3; i++) { + SAFE_PRINT(Boot, L" [%s:%d] working... step %d/3\n\r", + self->name, self->pid, i + 1); + task_yield(); + } + + SAFE_PRINT(Boot, L" [%s:%d] done\n\r", self->name, self->pid); +} + +static void cmd_tasktest(BootInfo *Boot, CHAR16 *Args) +{ + Task *t1, *t2, *t3; + UINTN i; + (void)Args; + + SAFE_PRINT(Boot, L"\n\r"); + SAFE_PRINT(Boot, L"Task Scheduler Test\n\r"); + SAFE_PRINT(Boot, L"================================================\n\r"); + SAFE_PRINT(Boot, L"\n\r"); + + SAFE_PRINT(Boot, L"Spawning 3 worker tasks...\n\r\n\r"); + + t1 = task_create(L"worker-A", worker_task_fn, Boot); + t2 = task_create(L"worker-B", worker_task_fn, Boot); + t3 = task_create(L"worker-C", worker_task_fn, Boot); + + if (t1 == NULL || t2 == NULL || t3 == NULL) { + SAFE_PRINT(Boot, L"Failed to create one or more tasks.\n\r"); + return; + } + + SAFE_PRINT(Boot, L"Created: %s (PID %d), %s (PID %d), %s (PID %d)\n\r", + t1->name, t1->pid, t2->name, t2->pid, t3->name, t3->pid); + SAFE_PRINT(Boot, L"\n\rYielding to let workers run:\n\r\n\r"); + + /* Yield enough times for all workers to complete (3 tasks x 3 steps) */ + for (i = 0; i < 12; i++) { + task_yield(); + } + + SAFE_PRINT(Boot, L"\n\rTask list after test:\n\r"); + task_print_list(Boot); + + SAFE_PRINT(Boot, L"Task scheduler test completed.\n\r\n\r"); +} + void show_help(BootInfo *Boot) { UINTN i = 0; diff --git a/context_switch.S b/context_switch.S new file mode 100644 index 0000000..dfafcd7 --- /dev/null +++ b/context_switch.S @@ -0,0 +1,44 @@ +/* + * context_switch.S – cooperative context switch for x86_64 + * + * void context_switch(UINT64 *old_rsp, UINT64 new_rsp); + * + * %rdi = pointer to save current RSP (old task's PCB field) + * %rsi = RSP value to restore (new task's saved RSP) + * + * Saves callee-saved registers and RFLAGS on the current stack, + * stores RSP, loads the new stack, restores registers, and returns + * into the new task's execution context. + */ + +.text +.global context_switch + +context_switch: + /* Save callee-saved registers and flags on the current stack */ + pushq %rbp + pushq %rbx + pushq %r12 + pushq %r13 + pushq %r14 + pushq %r15 + pushfq + + /* Save current stack pointer into *old_rsp */ + movq %rsp, (%rdi) + + /* Load new task's stack pointer */ + movq %rsi, %rsp + + /* Restore callee-saved registers and flags from the new stack */ + popfq + popq %r15 + popq %r14 + popq %r13 + popq %r12 + popq %rbx + popq %rbp + + ret + +.section .note.GNU-stack,"",@progbits diff --git a/kernel.c b/kernel.c index 26333b8..dadcbc7 100644 --- a/kernel.c +++ b/kernel.c @@ -4,6 +4,7 @@ #include "commands.h" #include "idt.h" #include "memory.h" +#include "task.h" #define SAFE_PRINT(Boot, ...) \ do { \ @@ -38,6 +39,7 @@ void kmain(BootInfo *Boot) idt_init(Boot); memory_init(Boot); + task_init(Boot); SAFE_PRINT(Boot, L"================================================\n\r"); SAFE_PRINT(Boot, L" Welcome to Simple UEFI Operating System!\n\r"); @@ -74,12 +76,20 @@ void kmain(BootInfo *Boot) SAFE_PRINT(Boot, L"-> "); while (TRUE) { - if (Boot->read_key == NULL) { + /* Try non-blocking read first; yield to other tasks while idle */ + if (Boot->try_read_key != NULL) { + Status = Boot->try_read_key(&Key); + if (Status == EFI_NOT_READY) { + task_yield(); + continue; + } + } else if (Boot->read_key != NULL) { + Status = Boot->read_key(&Key); + } else { SAFE_PRINT(Boot, L"Console input unavailable.\n\r"); break; } - Status = Boot->read_key(&Key); if (EFI_ERROR(Status)) { read_errors++; if (read_errors == 1 || (read_errors % 64) == 0) { diff --git a/main.c b/main.c index 6d240bd..e67ef89 100644 --- a/main.c +++ b/main.c @@ -202,6 +202,11 @@ static EFI_STATUS loader_read_key(EFI_INPUT_KEY *Key) return uefi_call_wrapper(ST->ConIn->ReadKeyStroke, 2, ST->ConIn, Key); } +static EFI_STATUS loader_try_read_key(EFI_INPUT_KEY *Key) +{ + return uefi_call_wrapper(ST->ConIn->ReadKeyStroke, 2, ST->ConIn, Key); +} + static void loader_shutdown(void) { uefi_call_wrapper(RT->ResetSystem, 4, EfiResetShutdown, EFI_SUCCESS, 0, NULL); @@ -251,6 +256,7 @@ efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) Boot.clear_screen = loader_clear_screen; Boot.set_attribute = loader_set_attribute; Boot.read_key = loader_read_key; + Boot.try_read_key = loader_try_read_key; Boot.shutdown = loader_shutdown; Boot.alloc_pages = loader_alloc_pages; Boot.free_pages = loader_free_pages; diff --git a/task.c b/task.c new file mode 100644 index 0000000..4b74231 --- /dev/null +++ b/task.c @@ -0,0 +1,351 @@ +#include "task.h" +#include "memory.h" + +#define SAFE_PRINT(Boot, ...) \ + do { \ + if ((Boot) != NULL && (Boot)->print != NULL) { \ + (Boot)->print(__VA_ARGS__); \ + } \ + } while (0) + +/* ================================================================ + * Task / Process Control Block pool + * ================================================================ */ + +static Task tasks[TASK_MAX]; +static Task *current_task = NULL; +static UINT32 next_pid = 0; +static BootInfo *task_boot = NULL; +static BOOLEAN task_ready = FALSE; + +/* Forward declaration */ +static void task_trampoline(void); + +/* ---------------------------------------------------------------- + * Helpers + * ---------------------------------------------------------------- */ + +static void wstrcpy16(CHAR16 *dst, const CHAR16 *src, UINTN max) +{ + UINTN i = 0; + while (i < max - 1 && src != NULL && src[i] != L'\0') { + dst[i] = src[i]; + i++; + } + dst[i] = L'\0'; +} + +/* ---------------------------------------------------------------- + * Initialisation – make the current (kernel) thread task 0 + * ---------------------------------------------------------------- */ + +void task_init(BootInfo *Boot) +{ + UINTN i; + + task_boot = Boot; + + /* Clear all PCB slots */ + for (i = 0; i < TASK_MAX; i++) { + tasks[i].state = TASK_STATE_FREE; + tasks[i].pid = 0; + tasks[i].saved_rsp = 0; + tasks[i].stack_base = 0; + tasks[i].stack_pages = 0; + tasks[i].entry = NULL; + tasks[i].arg = NULL; + tasks[i].switches = 0; + tasks[i].name[0] = L'\0'; + } + + /* + * Task 0 = the currently running kernel thread (the shell). + * It already has a stack (the kernel's boot stack), so we don't + * allocate one. Its saved_rsp will be filled in during the + * first context_switch call in task_yield(). + */ + tasks[0].pid = next_pid++; + tasks[0].state = TASK_STATE_RUNNING; + tasks[0].switches = 1; + wstrcpy16(tasks[0].name, L"kernel", TASK_NAME_LEN); + + current_task = &tasks[0]; + task_ready = TRUE; + + SAFE_PRINT(Boot, L" Tasks: scheduler ready (max %d tasks)\n\r", + (UINTN)TASK_MAX); +} + +/* ---------------------------------------------------------------- + * Trampoline – first code a new task executes after context_switch + * returns into it. It calls the real entry function and then + * performs a clean task_exit(). + * ---------------------------------------------------------------- */ + +static void task_trampoline(void) +{ + Task *t = task_current(); + if (t != NULL && t->entry != NULL) { + t->entry(t->arg); + } + task_exit(); + /* Should never reach here, but just in case: */ + for (;;) { + __asm__ __volatile__("hlt"); + } +} + +/* ---------------------------------------------------------------- + * Create a new task + * ---------------------------------------------------------------- */ + +Task *task_create(const CHAR16 *name, TaskEntryFn entry, void *arg) +{ + Task *t = NULL; + UINTN i; + UINT64 stack_phys; + UINT64 *sp; + + if (!task_ready || entry == NULL) { + return NULL; + } + + /* Find a free PCB slot */ + for (i = 0; i < TASK_MAX; i++) { + if (tasks[i].state == TASK_STATE_FREE) { + t = &tasks[i]; + break; + } + } + if (t == NULL) { + return NULL; /* out of slots */ + } + + /* Allocate stack pages from the physical memory manager */ + stack_phys = pmm_alloc_pages(TASK_STACK_PAGES); + if (stack_phys == 0) { + return NULL; /* out of memory */ + } + + /* Fill in the PCB */ + t->pid = next_pid++; + t->state = TASK_STATE_READY; + t->entry = entry; + t->arg = arg; + t->switches = 0; + t->stack_base = stack_phys; + t->stack_pages = TASK_STACK_PAGES; + wstrcpy16(t->name, name != NULL ? name : L"unnamed", TASK_NAME_LEN); + + /* + * Set up the initial stack frame so that context_switch() can + * "return" into task_trampoline(). + * + * context_switch saves/restores (low → high on stack): + * flags, r15, r14, r13, r12, rbx, rbp (pushes) + * then `ret` pops the return address (→ trampoline) + * + * Above the return address we place a safety-net address + * (task_exit) so that if the trampoline or entry function does + * a bare `ret`, it lands in task_exit(). + */ + sp = (UINT64 *)(stack_phys + TASK_STACK_SIZE); + + /* Align stack top to 16 bytes */ + sp = (UINT64 *)((UINT64)sp & ~0xFULL); + + /* Safety-net return address for the trampoline */ + *(--sp) = (UINT64)(UINTN)task_exit; + + /* Return address for context_switch's `ret` → trampoline */ + *(--sp) = (UINT64)(UINTN)task_trampoline; + + /* Callee-saved registers – all zero for fresh task */ + *(--sp) = 0; /* rbp */ + *(--sp) = 0; /* rbx */ + *(--sp) = 0; /* r12 */ + *(--sp) = 0; /* r13 */ + *(--sp) = 0; /* r14 */ + *(--sp) = 0; /* r15 */ + + /* RFLAGS – interrupts enabled (IF = bit 9) */ + *(--sp) = 0x202; /* flags */ + + t->saved_rsp = (UINT64)(UINTN)sp; + + return t; +} + +/* ---------------------------------------------------------------- + * Schedule – pick the next READY task (round-robin) + * ---------------------------------------------------------------- */ + +static Task *schedule_next(void) +{ + UINTN start, idx, i; + + if (current_task == NULL) { + return &tasks[0]; + } + + /* Find current task's index in the array */ + start = (UINTN)(current_task - tasks); + + /* Round-robin: scan from (current+1) wrapping around */ + for (i = 1; i <= TASK_MAX; i++) { + idx = (start + i) % TASK_MAX; + if (tasks[idx].state == TASK_STATE_READY) { + return &tasks[idx]; + } + } + + /* No other ready task – stay with current if still runnable */ + if (current_task->state == TASK_STATE_RUNNING || + current_task->state == TASK_STATE_READY) { + return current_task; + } + + /* Fallback to task 0 (kernel / shell) */ + return &tasks[0]; +} + +/* ---------------------------------------------------------------- + * Yield – voluntarily give up the CPU to the next ready task + * ---------------------------------------------------------------- */ + +void task_yield(void) +{ + Task *prev, *next; + + if (!task_ready) { + return; + } + + prev = current_task; + next = schedule_next(); + + if (next == prev) { + return; /* nothing else to switch to */ + } + + /* Mark the previous task as READY (still runnable) */ + if (prev->state == TASK_STATE_RUNNING) { + prev->state = TASK_STATE_READY; + } + + next->state = TASK_STATE_RUNNING; + next->switches++; + current_task = next; + + /* + * context_switch saves callee-saved regs + flags on prev's stack, + * stores prev's RSP into prev->saved_rsp, loads next->saved_rsp + * into RSP, restores regs + flags, and `ret`s into next's code. + */ + context_switch(&prev->saved_rsp, next->saved_rsp); +} + +/* ---------------------------------------------------------------- + * Exit – terminate the current task and switch away + * ---------------------------------------------------------------- */ + +void task_exit(void) +{ + Task *prev, *next; + + if (!task_ready) { + return; + } + + prev = current_task; + prev->state = TASK_STATE_TERMINATED; + + /* Free the stack memory back to the PMM */ + if (prev->stack_base != 0 && prev->stack_pages != 0) { + pmm_free_pages(prev->stack_base, prev->stack_pages); + prev->stack_base = 0; + prev->stack_pages = 0; + } + + /* Mark the PCB slot as free for reuse */ + prev->state = TASK_STATE_FREE; + + next = schedule_next(); + if (next == prev) { + /* Shouldn't happen if task 0 (kernel) is always alive */ + next = &tasks[0]; + } + + next->state = TASK_STATE_RUNNING; + next->switches++; + current_task = next; + + /* One-way switch: we never return to the exited task */ + context_switch(&prev->saved_rsp, next->saved_rsp); + + /* Should never reach here */ + for (;;) { + __asm__ __volatile__("hlt"); + } +} + +/* ---------------------------------------------------------------- + * Accessors + * ---------------------------------------------------------------- */ + +Task *task_current(void) +{ + return current_task; +} + +UINTN task_count(void) +{ + UINTN i, count = 0; + for (i = 0; i < TASK_MAX; i++) { + if (tasks[i].state != TASK_STATE_FREE) { + count++; + } + } + return count; +} + +/* ---------------------------------------------------------------- + * Print task list (implements the `ps` command) + * ---------------------------------------------------------------- */ + +static const CHAR16 *state_str(TaskState s) +{ + switch (s) { + case TASK_STATE_FREE: return L"FREE"; + case TASK_STATE_READY: return L"READY"; + case TASK_STATE_RUNNING: return L"RUNNING"; + case TASK_STATE_TERMINATED: return L"ENDED"; + default: return L"???"; + } +} + +void task_print_list(BootInfo *Boot) +{ + UINTN i; + + SAFE_PRINT(Boot, L"\n\r"); + SAFE_PRINT(Boot, L" PID STATE SWITCHES NAME\n\r"); + SAFE_PRINT(Boot, L" --- ---------- -------- ----\n\r"); + + for (i = 0; i < TASK_MAX; i++) { + if (tasks[i].state == TASK_STATE_FREE) { + continue; + } + + SAFE_PRINT(Boot, L" %3d %-10s %8d %s\n\r", + tasks[i].pid, + state_str(tasks[i].state), + tasks[i].switches, + tasks[i].name); + } + + SAFE_PRINT(Boot, L"\n\r"); + SAFE_PRINT(Boot, L" Active tasks: %d / %d\n\r", + task_count(), (UINTN)TASK_MAX); + SAFE_PRINT(Boot, L"\n\r"); +} diff --git a/task.h b/task.h new file mode 100644 index 0000000..1c80949 --- /dev/null +++ b/task.h @@ -0,0 +1,56 @@ +#ifndef TASK_H +#define TASK_H + +#include +#include "boot_info.h" + +/* Maximum number of concurrent tasks */ +#define TASK_MAX 32 + +/* Stack size per task: 32 KB (8 pages) */ +#define TASK_STACK_PAGES 8 +#define TASK_STACK_SIZE (TASK_STACK_PAGES * 4096) + +/* Maximum task name length */ +#define TASK_NAME_LEN 32 + +/* Task states */ +typedef enum { + TASK_STATE_FREE = 0, /* PCB slot is unused */ + TASK_STATE_READY, /* Runnable, waiting for CPU */ + TASK_STATE_RUNNING, /* Currently executing on CPU */ + TASK_STATE_TERMINATED /* Finished execution */ +} TaskState; + +/* Task entry function signature */ +typedef void (*TaskEntryFn)(void *arg); + +/* ================================================================ + * Process Control Block (PCB) + * ================================================================ */ +typedef struct Task { + UINT32 pid; /* Process ID */ + TaskState state; /* Current state */ + CHAR16 name[TASK_NAME_LEN]; /* Human-readable name */ + UINT64 saved_rsp; /* Saved stack pointer (context switch) */ + UINT64 stack_base; /* Base address of allocated stack */ + UINTN stack_pages; /* Stack size in pages */ + TaskEntryFn entry; /* Entry function pointer */ + void *arg; /* Argument passed to entry */ + UINTN switches; /* Number of times scheduled */ +} Task; + +/* -------- Task API -------- */ + +void task_init(BootInfo *Boot); +Task *task_create(const CHAR16 *name, TaskEntryFn entry, void *arg); +void task_yield(void); +void task_exit(void); +Task *task_current(void); +UINTN task_count(void); +void task_print_list(BootInfo *Boot); + +/* Assembly context switch (defined in context_switch.S) */ +extern void context_switch(UINT64 *old_rsp, UINT64 new_rsp); + +#endif