Multitasking Howto

Multitasking Howto

This little HowTo is intended to show, how to set up a multitasking environment for a custom OS. If you have questions: don't hesitate to drop me a mail.

For the beginning: you will of course need several Knowledge like how to handle linked Lists or BinaryTrees, which I won't cover in this text. I will show you the basics of a stack-based multitasking subsystem.

Multitasking on a single processor machine: to switch between a bunch of processes in a quick manner: each of them posesses for either a certain time or until it gives it up - the CPU.

First, lets define the process structure: it covers the essential informations about a process and is the MAIN element an operating system uses to handle processes.

You see in this segment five very important fields: esp,ss,kstack,ustack,cr3 - these, especially esp, are accessed via the low-level asm-routines. esp holds the actual position of the KERNEL-ESP of the actual process, which has been interrupted by any isr or system call. The asm stub stuffs the esp-adress in this field.

We also need a TSS: in this system wide available structure, the processor finds the Kernel stack of the interrupted process in the esp0-field. Upon each task switch this field has to be updated to the according kernel-stack adress of the NEXT process. This may even be the LAST process. The stack switching method doesn't care about it. Here is a definition of a tss.

You take the current process' structure, put its adress into eax, and access/update the corresponding fields: esp is the actual esp - it is on the structs first position, so no offset is needed. For the other fields you have to add the offset to the adress in eax.

You also have to update the esp0 field in the system tss.

This done, you tell the cpu, where the kernelstack for the process it has to run next is located.

Upon interrupt, it takes this adress as its esp and every other popping/pushing is done on this stack. After having pushed all the relevant hardware states on the kernel-stack of the process, you have to save the position of esp after the last popped register into the structures esp-field.

Upon leaving the isr, it takes the new ESP value from the current-process-structure, replaces the value in the esp0 field of the tss with the adress of the top of the new kstack, and then pops off all the registers and returns to the process. If it is a user-process, the two last items popped off upon iret are user-stack-segment and user-stack.

It is really not that difficult to get started, this stack based task switching. But of course, you have to fill in the kstack of a new process with the proper values for it starts off with exactly what is located on the kstack to be popped off, when the process is first scheduled for execution.

Why do you need to fill in the esp0 field of the tss at every task switch: It is mainly because a user process which runs at dpl3 (User Level), can enter kernel space (=dpl0 - system level) by issuing an interrupt or accessing a call gate. I 'd rather prefer the software interrupt approach. The thing looks like this:

You put the Values onto the kstack in the order in which they would be pushed onto the it by an isr. the last position of stacksetup, you stuff into the esp field. This is a really straight forward thing.The Entry point of your process is the adress of the process' main function. This is the adress to which eip is set upon iret to this process after it's ben scheduled for it's first execution.

Now, I'll show you a little example of scheduling to round it off a little. Basically, this scheduler just moves the processes around in the queue in round robin manner. Also I'll show a little isr.

//global pointer to current task:
prozess_t *p;
//the isr:
void timer_handler(void){
if(task_to_bill->prozess_timetorun>0){
task_to_bill->prozess_timetorun--;
if(task_to_bill->prozess_timetorunprozess_timetorunprozess_timetorun=10;//refill timeslice.
//remove process from the head of the queue.
proz=remove_first_element_from_queue(&roundrobin_prozesse,0);
//put the element to the rear of the queue.
add_element_to_queue_rear(&roundrobin_prozesse,proz);
}
//pick the process at the head of any queue. (f. ex. round robin queue)
//and put it in p.
choose_prozess(irq);
}

So, it fits together: At each timer interrupt, the current process p's state is saved. then, the isr decrements the process timeslice by one. If there isn't any timeslice left, the process is put to the end of the queue by the scheduler and the next one is to be started: it is then located in p from where the isr-stub takes the relevant values and restores the process' state - and starts/restarts it.

You have to keep in mind at any time, that the operating system is event driven. Interrupts are events, as well as system calls. the operating system reacts to them and carries out the requested operations or methods necessary to satisfy devices which have triggered an event.You can also perform task switches upon receipt of an event (irq/software int/exception).