|
|
||||||||||
|
|||||||||||
Understanding How Tasks Work - Part 1This is the first article in a series that discusses in technical detail how tasks (threads) work with respect to task switching, instance data, inter-task communication, scheduling, and priority. For a general overview of threads, see a previous article.Task SwitchingIn a strict sense, multi-tasking cannot occur on a single CPU, and therefore, multi-tasking must be simulated by sharing CPU time between different tasks. Time can be shared between tasks through cooperation, time-slicing, preemption or any combination of the above. All of these time-sharing techniques have one thing in common: they require the ability to switch from one task to another (context switching).Each Task Needs its Own StackBefore explaining how task switching is accomplished it must be understood that tasks cannot share a common stack like subroutines (although the cyclical executive approach does accomplish a form of tasking with a single stack). To illustrate, consider a multi-tasking system that erroneously uses a single stack. Consider taskA which is currently nested 2 subroutine calls deep (subX was called which called subY). taskA is now switched out to run taskB, which calls subZ While in subZ control is switched back to taskA, which was in subY. When subY attempts to return to subX, an error will occur, as it will not return to subX because subZ's return information was put onto the stack by taskB.The solution is for each task to have its own stack. So, in the above example, when control is switched from taskB back to taskA, the stack would also be switched back to taskA's stack, and subY will return to subX as it should. How Task Switching is AccomplishedBefore switching from taskA to taskB, all information necessary to resume taskA must be saved. The information (the task's context) that must be saved is:
It may seem that the simplest way to save this information is to create a structure for each task, commonly referred to as the task control block, or tcb. Before switching out taskA, all of its context could be saved in its tcb. Then, the context from taskB could be restored from taskB's tcb, which will restore taskB's, CPU flags, registers, and its stack by changing the stack pointer. Note however that the instruction pointer register (IP) cannot be written to so the last step of the task switch would be to jump to the value of IP saved in taskB's tcb. This approach, however, has shortcomings, which will become apparent below. The preferred approach to task switching is to save only the task's stack pointer in the tcb, and save everything else on the task's private stack. One might try switching from taskA to taskB as follows.
However, this approach is still flawed. The problem is that interrupts are not disabled, which means that an interrupt may occur whose isr might attempt a task switch itself which will result in a task switch occurring in the middle of an on going task switch. This can cause problems because the active task may be different when control is returned to the interrupted task switch routine, which means that the tcb of the active task has changed and problems will arise. Even if interrupts were disabled before starting the task switch an interrupt can still occur before the task switch is finished because when the flags register for taskB is restored, interrupts may become enabled, since the interrupt enable bit is one of the bits in the flag register. The solution is to perform a task switch via software interrupt, that is, instead of calling a standard subroutine to perform the task switch, invoke the task switch routine by executing a software interrupt instruction. The problems mentioned above are solved because (1) the software interrupt instruction (INT) saves the current flags on the stack and disables interrupts before calling the isr (taskSwitch in our case), (2) the IRET instruction, which is used to return from the isr, returns to the interrupted code and restores the flags in one indivisible instruction. The task switching is best explained by presenting the code which performs a task switch using the software interrupt technique. The following task switch routine uses 8086 assembly language. On entry, SP points to taskA's stack; on exit SP points to taskB's stack. taskSwitch: pushall ; Macro to save all registers onto current stack ; that is, save all of taskA's registers on ; the current stack which is taskA's stack. mov ax,sp ; Get taskA's SP into ax. mov TaskASP,ax ; Save taskA's SP. call getNextSp ; Get SP of highest priority waiting ; task (taskB) and return it in ax. mov sp, ax ; SP is now taskB's SP. popall ; Macro to restore all registers, that is, ; restore all of taskB's registers from ; taskB's stack. iret ; Return to the point where taskB was ; suspended and restore taskB's flags.The above is for illustration and is not complete. For example, the stack segment register should also be saved and restored if different tasks use different segments. For complete details, please send us email. The routine taskSwitch cannot be called directly like a subroutine, but rather indirectly through the interrupt vector table using a software interrupt. The 8086 software interrupt instruction (INT) is used as follows. INT interruptNumberThe interrupt number is a number between 0 and 255, which when multiplied by 4 is an index into the interrupt vector table. The INT instruction jumps to the address it finds in the interrupt table at offset interruptNumber * 4. So, before the INT instruction can be used, an unused interrupt vector table entry must be loaded with the address of the routine taskSwitch. Assuming that interrupt vector number 0xfa is unused, and that the address of taskSwitch in codeSegment:Offset notation is 0x00:0xf020, the following code sets up the interrupt vector table so that taskSwitch can be called by invoking INT 0xfa. MOV DI, (0FAh * 4) ; Load vector table offset into DI. MOV [DI], 0F020h ; Load taskSwitch's offset into ; first word of vector table. MOV [DI+2}, 0 ; Load taskSwitch's CS into ; second word of vector table.Now taskSwitch can be invoked as follows. INT 0FAh Deciding When to Switch TasksGenerally, in fully preemptive systems a task switch can occur through cooperation, time-slicing, preemption or voluntary suspension. Consider the following example.
void taskA(void)
{
while (TRUE) {
readData();
processData();
suspend();
}
}
In the example above, the call to suspend simply invokes the isr taskSwitch via software interrupt. taskSwitch then resumes the highest priority waiting task. suspend is implemented as shown below.
#define suspend() asm {INT 0FAh}
Note that in the above code, taskA will not run again until it is scheduled again to run. Therefore, as shown above, the task will run once, suspend itself, and never run again, assuming that no other software reschedules taskA to run.
Preferable for the taskA example is a yield instead of suspend. yield is implemented as follows. #define yield() rescheduleMe(); suspend()The function rescheduleMe adds the active task (taskA in this case) back into the queue of ready to run waiting tasks (ready queue), then suspends itself. The implementation of rescheduleMe is left for a future article. Next month we will discuss starting tasks, scheduling, and priority. Go to Part 2
We welcome comments. Let us know what subjects you would like written up.
Send comments to Mike@TicsRealtime.com
Copyright © 2000, Tics Realtime
| |||||||||||