Home

Tasking


To actually achieve anything the OS runs a number of tasks. Some of these are user programs, others are central to the OS itself; for example, hardware drivers, such as those for the keyboard or the display, run as separate tasks. One of the prime functions of an OS is to manage these separate tasks; each must be activated at regular intervals to give the impression that they are all running all of the time. In 32-bit (Protected) mode the x86 processors actually provide hardware support for task switching. In practice most operating systems do not make use of this hardware support; this is so that their code is portable to other processors. In any case, in 64-bit mode the processor does not provide this hardware support, so it all has to be done in software.

Information about tasks is stored in the structure Task as defined in the file include/kstructs.h (an assembly code version of this structure is also defined in include/kstructs.inc). This structure consists of a pointer to the next task structure (so that we can create a linked list of structures, a flag to determine whether the task is waiting for something else to happen (e.g. an interrupt), fields for the processor registers, some fields to record the location of some items, and a pid (Processor Identifier Descriptor) so that we can tell which process is which. The task structure is always accessed via register r15; this makes the assembler code easier. You must ensure that the address of the appropriate task structure is in r15 before accessing the fields of the structure. Each time a task switch occurs (i.e. the task is activated or deactivated) various of these fields - in particular the processor registers - need to be read or updated.

Task switches can be called by a program when it wants to wait for some event. Otherwise they will occur at regular intervals, triggered by the timer interrupt, thus ensuring that all processes get a fair share of processor time. (This is an extremely simplistic method of scheduling processes; one of the major feature of a real OS is to manage the processor time that processes get equably. There are many algorithms to accomplish this.)

Task switches are accomplished by the routine TaskSwitch in tasking.s, which switches to the next runnable task. Tasks are stored on one of three queues; there is a queue of runnable tasks, a queue of blocked tasks, and a queue of dead tasks. (There is also, for convenience, a list of all tasks, but we can ignore this for the moment.) The first task on the runnable queue, which should normally be the current running task, is moved to the end of the queue and then the new head of the queue is selected as the task to switch to. If there is no task on the runnable queue, a special low-priority task is selected to run. (This task processes the dead task queue to ensure that all resources used by a finished task are released back to the system. If there are no dead tasks the processor will be halted until the next interrupt.) Routines to manipulate the task queues are defined in tasklist.c.

Now that we have determined which task to switch to, both routines save the contents of the registers to the task structure and retrieve those of the new task. (Note - I'm only concerned with the general purpose registers at this stage. In future I might look at saving the floating-point, and other, registers.) As one of the registers saved and restored is the stack pointer, the final ret instruction returns not to the code that called the task switch but to the code that caused a task switch from the task being switched to. (If that doesn't seem clear, think about it. We don't want to return to the code that called the task switch - and as the memory map has been changed by restoring cr3 it would not even make sense to do so - but to the point that the new task was at before it last relinquished the processor.)

There are a couple of things about saving/restoring registers that needs to be explained further. First, one has to be a little careful about r15. When the routine is called r15 points to the task to switch to, which is what we want to save, but we change it to point to the current task (so that we can access its task structure). Hence the push %r15 instruction before currentTask is stored in r15; this pushed value is later popped to rax and then stored to the task structure. Second, these two routines are designed to be called as subroutines from within an interrupt routine (int $20). If we just saved the stack pointer we would just be saving the address of the calling interrupt routine; what we really want to save is stack pointer when the interrupt routine called the subroutine. This is why there is a pop %rcx before saving the stack pointer and a push %rcx after restoring it.