avcado wrote: ↑Sun Sep 21, 2025 9:02 am
I had been thinking a lot about what architecture I wanted my kernel to run on. The idea was to "rejuvenate" some old hardware I own, and had the idea to write a 32-bit kernel. I may rewrite the kernel to support 64-bit, but for right now, I'm happy with 32-bit mode.
If you designed your kernel to be portable and have decent architecture abstractions, you wouldn't need to rewrite it outright. There is a difference between supporting an architecture and shackling yourself to it.
avcado wrote: ↑Sun Sep 21, 2025 9:02 am
I had been thinking about writing a software task switcher (doing something like a red-robin), but I'm not entirely sure on how you save contexts.
From my understanding:
- The list of tasks is a linked list.
- On every n ticks of some timer interrupt, save the current task's context, load the next task's context (if task->next is null, use task->head) and return from the interrupt.
Separate the timing of the task switch from the mechanism. A lot of tasks will block waiting for some event before any time quota is up.
I'd put the logic for task switching itself into a function called "schedule()", which is a function returning nothing and taking no arguments. The function finds the next runnable task and switches to it, so for example:
Code: Select all
void schedule(void) {
struct task *old = this_cpu()->current;
struct task *new = find_next_runnable_task();
if (new != old)
arch_task_switch(old, new);
}
And then on i386, the task switch is something like
Code: Select all
void arch_task_switch(struct task *old, struct task *new) {
if (old->flags & TIF_FPU)
arch_save_fpu(old->fpusave_area);
/* if new task is a kernel task, we only need to load its CR3 if old is condemned */
if (!(new->flags & TIF_KERNEL) || (old->flags & TIF_CONDEMNED))
arch_load_cr3(new->cr3);
arch_ll_task_switch(&old->stack_bottom, new->stack_bottom);
/* when we get here, the old task has been resumed */
/* our cr3 has been loaded by whoever resumed us already. */
this_cpu()->tss.esp0 = old->stack_top;
if (old->flags & TIF_FPU)
arch_load_fpu(old->fpusave_area);
this_cpu()->current = old;
}
And the low level task switch:
Code: Select all
arch_ll_task_switch:
movl 4(%esp), %eax
pushl %ebp
pushl %ebx
pushl %esi
pushl %edi
movl %esp, (%eax)
movl 24(%esp), %esp
popl %edi
popl %esi
popl %ebx
popl %ebp
retl
So it just saves the nonvolatile registers to stack and loads the new stack. I wouldn't use the names setjmp() and longjmp() for this, because the compiler may have opinions on what those names are supposed to mean.
Now, how do you make a new task? Maybe something like this? Have this ready in assembler:
Code: Select all
start_task:
andl $-16, %esp
push $0
push %ebx
pushl %esi
pushl %edi
call start_task_c
And in C:
Code: Select all
_Noreturn void start_task_c(int (*func)(void *), void *arg, struct task *task) {
current = task;
task_exit(func(arg))
}
extern const char start_task[];
struct task *create_task(int (*func)(void *), void *, uintptr_t cr3) {
char *mem = allocate_kernel_stack(); /* allocates one or two pages */
if (!mem) return 0;
struct task *rv = (void *)(((uintptr_t)mem + kernel_stack_size() - sizeof (struct task)) & -alignof(struct task));
memset(rv, 0, sizeof (struct task));
uint32_t *stack = (void *)((uintptr_t)rv & -16);
rv->stack_top = (uintptr_t)stack;
rv->cr3 = cr3;
*--stack = (uintptr_t)start_task;
*--stack = 0; //ebp
*--stack = (uintptr_t)rv; //ebx
*--stack = (uintptr_t)arg; //esi
*--stack = (uintptr_t)func //edi
rv->stack_bottom = (uintptr_t)stack;
return rv;
}
I'm letting the main thread function return a value, but so far have no idea if I'd even use the return value. Exiting a thread is then just setting TIF_CONDEMNED and calling schedule() in an infinite loop. schedule() will never schedule in a condemned task. And somewhere you need code to collect all the condemned tasks and free them. Or maybe just put them in a pile for reuse later.
Now, you keep getting back to the timer interrupt. You can call schedule() from the timer interrupt handler, but be aware that that function blocks, so you can only do so after acknowledging the interrupt to the interrupt controller and possibly the timer. But with the above design, you can also call schedule() from a syscall handler, for example, while waiting for data.
The above code is entirely untested. I am merely trying to convey an idea. It doesn't contain the actual scheduling code, because that is your design problem to solve. It doesn't contain code to actually allocate memory for the FPU data, and to save it. I trust you to read up on that. There is an unpleasant amount of code doubling between start_task_c() and the second half of arch_task_switch(), but for now, you have a starting point.