mrjbom wrote:Explain how this code works.
It doesn't. Not as given, anyway. It is merely illustrative. The first of your quoted lines sets the in-memory version of CR3 to the last argument of the function. For a reason I have not figured out yet, many tutorials represent physical addresses as pointers (they aren't, since you cannot dereference them directly). That's why it is given in that form. It actually should just be called CR3 and have type uintptr_t. The reason you have to switch CR3 on task switch is that all user space processes have a different address space in the user space half. Optimization: If you are switching to a kernel task, you don't need to load CR3. Saves a TLB flush.
The second line... yeah, that won't work that way. The way I do it is: I allocate two contiguous pages, put the task struct right at the end of the allocation, and use everything before it as stack. I never deallocate this structure again; if the task finishes, I mark it as Condemned, and it can be resurrected by the task allocator.
mrjbom wrote:I'm also wondering how I can automatically switch between threads, maybe use timers or something like that.
I tried to keep the structure of my task switcher simple, so here is how that works: Whenever something returns to user space (interrupt or syscall), I test the task flags of the current tasks for "signal pending" and "task timeout". If either is set, I reenter the kernel in a function that handles these states. The handler for "signal pending" is not relevant here. For "task timeout", it calls schedule() once and returns. schedule() will remove the "task timeout" flag, then pick the next task to run and switch there. Since the then-current task is still runnable, eventually execution will resume where it left off. Now the only thing left to do is install a timer interrupt handler that sets the "task timeout" flag.
mrjbom wrote:The article says that this is not quite "correct" multitasking, what should I use for my tasks?
Well, lots of details were left out. And those details make the task switcher more complicated. You have to handle debug registers in it, and FPU, and SSE/AVX, etc. Or at the very least set the TS bit in CR0. There are optimizations like I alluded to above: Kernel tasks can use any address space and don't need CR3 reloaded. And the detail of how to pick the next task is entirely left out.
The code in that article is meant as illustration of the principles involved, not as cut-n-paste solution.