352 lines
9.7 KiB
C
352 lines
9.7 KiB
C
#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");
|
||
}
|