IMPLEMENTATION MODULE TaskControl; (****************************************************************) (* *) (* The dispatcher of the operating system, and *) (* related procedures and data structures. *) (* *) (* This version supports priority inheritance. *) (* *) (* This is the portable part of the PMOS task control *) (* software. The non-portable part is in module *) (* InnerKernel. *) (* *) (* Programmer: P. Moylan *) (* Last edited: 16 March 1995 *) (* Status: Working *) (* *) (* Description: *) (* See individual procedures. This is the module which *) (* does all task switching, also task creation. *) (* The kernel of the operating system consists of this *) (* module, module InnerKernel, and module Semaphores. The *) (* module called Timer is also intimately linked to the *) (* kernel; it is technically not part of the kernel, but *) (* does most of its work by calls to kernel procedures. *) (* *) (* The present kernel implementation allows a task to *) (* be in one of the following (mutually exclusive) states: *) (* 1. Active *) (* - running *) (* - ready *) (* - blocked waiting for a Lock *) (* 2. Inactive *) (* - sleeping *) (* - waiting for a semaphore Signal *) (* - waiting for a Signal with a timeout *) (* - dead *) (* In most of these cases, the state is defined by the *) (* kernel list on which the task is placed. (All active *) (* tasks are on the same "Ready" structure, but we have *) (* enough other information to distinguish among the three *) (* active states.) The exception is where a task is *) (* performing a timed semaphore wait, since there the *) (* task is simultaneously on two lists. When such a task *) (* is woken up, it is run immediately in order to make *) (* indivisible the operation of removing the task from *) (* the two lists. If this causes a higher-priority task *) (* to be preempted, a second task switch reinstates this *) (* higher-priority task before we leave the kernel. *) (* (Procedure RecheckPriorities looks after this detail.) *) (* *) (* The priority of a task is normally a constant; but the *) (* priority can be increased in the case of an active *) (* task holding a Lock, where a higher-priority task is *) (* waiting to obtain that Lock. In such cases the task's *) (* dynamic priority is equal to the priority of the *) (* highest-priority task which it is blocking. As it *) (* turns out, there is no need to record the dynamic *) (* priorities for each active task; it is sufficient to *) (* maintain a global variable CurrentPriority, which holds *) (* the dynamic priority of the currently running task. *) (* *) (* Interrupt tasks follow their own set of rules and do *) (* not fit into the above classification. An interrupt *) (* task should never obtain a Lock, perform a semaphore *) (* Wait (although a Signal is permitted), or Sleep. The *) (* only possible states for an interrupt task are running *) (* (responding to an interrupt) or idle (waiting for an *) (* interrupt). When an interrupt task is running, the *) (* variables CurrentTask and CurrentPriority continue to *) (* refer to the interrupted task. *) (* *) (****************************************************************) FROM SYSTEM IMPORT (* proc *) ADR; FROM ConfigurationOptions IMPORT (* const*) TimeSlicingEnabled, MaxTaskNumber, StackSize; FROM InnerKernel IMPORT (* type *) TaskSelector, FloatSaveSelector, (* proc *) EnterKernel, LeaveKernel, NPXsave, NPXrestore, InitMainTask, MakeFloatSaveSelector, TaskInit, Transfer, IOTransfer, StartInterruptTask, DisconnectFromInterrupt; (* Procedure Transfer is the low-level primitive for the *) (* stack switch operation, which is the most crucial part of *) (* a task switch. Procedures NPXsave and NPXrestore look after *) (* saving and restoring the state of the NPX (Numeric Processor *) (* Extension, i.e. the floating point hardware) coprocessor. *) (* TaskInit introduces a new task into the system. *) FROM Storage IMPORT (* proc *) ALLOCATE, DEALLOCATE; FROM TerminationControl IMPORT (* proc *) SetTerminationProcedure, Crash; (************************************************************************) TYPE TaskNumber = [0..MaxTaskNumber]; InterruptType = CARDINAL; Lock = POINTER TO LockRecord; TaskPointer = POINTER TO TaskDescriptor; Task = TaskPointer; (* The kernel maintains a number of queues, which for the most part *) (* are FIFO queues. (Very occasionally, we need to remove a task *) (* from the middle of a queue.) Mainly because of the limitations *) (* of the Modula-2 opaque export, we choose to represent a queue by *) (* a single "head" pointer, and to implement a doubly linked list *) (* structure with the aid of "previous" and "next" pointers in the *) (* task descriptors. *) TaskQueue = TaskPointer; (* Each task in the system has a task descriptor. *) (* The fields in a task descriptor are: *) (* previous, next: pointers to preceding and following task *) (* descriptors on the queue (i.e. whatever system queue *) (* this task descriptor happens to be on). *) (* name: a character string describing the task. Used only for *) (* diagnostic purposes. *) (* priority: task priority. This is the task's base priority. *) (* Because of priority inheritance, the task may at times *) (* be running at a higher priority, but we don't need to *) (* record this in the task descriptor; it turns out to be *) (* sufficient to keep a single global variable (see *) (* CurrentPriority below) to hold the dynamic priority of *) (* the currently running task. *) (* WaitingFor: the Lock, if any, on which this task is blocked. *) (* FirstLock: the first in the list of Locks held by this task. *) (* selector: a selector for the task state of this task. In the *) (* current implementation, most of the state information *) (* (register contents, etc.) is saved on the task's stack *) (* when a task switch occurs, and the selector field is *) (* used to save the stack pointer. *) (* FPselector: used for floating point save/restore. *) (* StackBase: the low address of the memory segment set aside for *) (* the stack. This is needed only when we kill a task, and *) (* wish to reclaim the memory used by its stack. *) (* TimeLeft: for a running task, this records how much of its *) (* current time slice the task still has left. For a *) (* sleeping task, it shows how long to keep the task *) (* asleep (but not in terms of absolute time - see *) (* procedure Delay). *) (* InterruptHandler: true iff this task is an interrupt task. *) (* FloatingPointUser: true iff this task is a user (or potential *) (* user) of the numeric coprocessor. *) (* dead: true iff this is an unused descriptor or a descriptor *) (* for a task which has terminated. *) (* sleeping: true iff this is a task which is on the sleep queue. *) (* InterruptNo: interrupt number, used only for interrupt tasks. *) (* nextsleeper: pointer to next task descriptor on the sleep *) (* queue. Unused except when this task is a sleeping task *) (* or one which is timing out. *) TaskDescriptor = RECORD previous, next : Task; name : NameString; priority : PriorityLevel; WaitingFor, FirstLock : Lock; selector : TaskSelector; FPselector : FloatSaveSelector; StackBase : ADDRESS; TimeLeft : INTEGER; InterruptHandler, FloatingPointUser, dead, sleeping : BOOLEAN; InterruptNo : InterruptType; nextsleeper : Task; END; (* A LockRecord holds all the data for a lock. Note that we do *) (* not maintain the traditional blocked list; in the more standard *) (* implementations of a binary semaphore this would be needed to *) (* know which task to unblock when the lock is released, but the *) (* present algorithm leaves everything unblocked, relying on a *) (* re-check at the time of a task switch. *) (* The "next" and "previous" fields link together all locks held *) (* by a single task. This list would not be needed if we assumed *) (* perfectly disciplined users of the locks; but we need to be *) (* able to deal with things like tasks which exit while still *) (* holding locks. *) LockRecord = RECORD Locked: BOOLEAN; Holder: Task; next, previous: Lock; END (*RECORD*); (************************************************************************) VAR (* TaskTable is an array of task descriptors. After procedure *) (* InitialiseTaskControl returns, there will be two tasks in the *) (* system: the initialisation task, and the null task. The *) (* remaining task descriptors will be on UnusedDescriptors, *) (* available for the creation of new tasks. A task descriptor is *) (* allocated by procedure CreateTask, and is returned to *) (* UnusedDescriptors by procedure TaskExit. *) TaskTable: ARRAY TaskNumber OF TaskDescriptor; (* CurrentTask is the current non-interrupt task. (Note: when an *) (* interrupt task is running, CurrentTask continues to refer to the *) (* interrupted task.) LastTask is the task which was running just *) (* before CurrentTask got control. Most of the time LastTask is *) (* irrelevant to us; but it is needed when dealing with some *) (* complications which arise when waking a sleeping task. *) LastTask, CurrentTask: Task; (* CurrentPriority is the _dynamic_ priority of the currently *) (* running task. If no other task is blocked on a Lock held by the *) (* current task, then CurrentPriority is the current task's base *) (* priority. Otherwise, CurrentPriority is equal to the maximum *) (* base priority of all tasks which are blocked (directly or *) (* indirectly) by the current task as a result of their waiting to *) (* obtain a Lock. *) CurrentPriority: PriorityLevel; (* Because of the way we do priority inheritance, our treatment *) (* of the "ready list" is somewhat unconventional. This list holds *) (* not only tasks which are ready, but also *) (* - the currently running task, and *) (* - all tasks waiting to obtain a Lock. *) (* (Do not confuse obtaining a Lock with waiting on a general *) (* semaphore. Tasks doing a semaphore Wait are not on the ready *) (* list.) When we choose a task on the ready list and discover *) (* that it is blocked, we switch to its blocker instead, with the *) (* blocker inheriting the priority of the blocked task. *) (* Note: priority level 0 is special - there is normally only one *) (* task, namely the null task, at priority 0. *) ReadyList: ARRAY PriorityLevel OF TaskQueue; (* InterruptTaskActive = TRUE iff an interrupt task is running. *) InterruptTaskActive: BOOLEAN; (* UnusedDescriptors is a queue of all task descriptors which are *) (* unused, i.e. which are available for use as descriptors of newly *) (* created tasks. *) UnusedDescriptors: TaskQueue; (* The timequota array holds the time-slice quota, in units of *) (* clock ticks, for each priority level. Quotas are based on a *) (* task's base priority, not on its inherited priority. *) timequota: ARRAY PriorityLevel OF CARDINAL; (************************************************************************) (* KERNEL QUEUE OPERATIONS *) (************************************************************************) PROCEDURE CreateQueue (VAR (*OUT*) KQ: TaskQueue); (* Creates an initially empty kernel queue. *) BEGIN KQ := NIL; END CreateQueue; (*************************************************************************) PROCEDURE AddToQueue (VAR (*INOUT*) Q: TaskQueue; T: Task); (* Adds task T to the tail of queue Q. *) VAR tail: TaskPointer; BEGIN IF Q = NIL THEN Q := T; WITH T^ DO previous := T; next := T; END (*WITH*); ELSE WITH Q^ DO tail := previous; previous := T; END (*WITH*); tail^.next := T; WITH T^ DO previous := tail; next := Q; END (*WITH*); END (*IF*); END AddToQueue; (************************************************************************) PROCEDURE TakeFromQueue (VAR (*INOUT*) Q: TaskQueue): Task; (* Removes the first entry from queue Q and returns it. *) (* Assumption: the caller has already verified that the queue is *) (* nonempty. *) VAR result: Task; tail: TaskPointer; BEGIN result := Q; tail := Q^.previous; IF tail = Q THEN Q := NIL; ELSE Q := Q^.next; Q^.previous := tail; tail^.next := Q; END (*IF*); WITH result^ DO previous := NIL; next := NIL; END (*WITH*); RETURN result; END TakeFromQueue; (************************************************************************) PROCEDURE LeaveQueue (VAR (*INOUT*) KQ: TaskQueue); (* Removes the current task from queue KQ. The caller is *) (* responsible for knowing that the current task really is on KQ. *) BEGIN WITH CurrentTask^ DO IF next = CurrentTask THEN KQ := NIL; ELSE previous^.next := next; next^.previous := previous; IF KQ = CurrentTask THEN KQ := next; END (*IF*); END (*IF*); previous := NIL; next := NIL; END (*WITH*); END LeaveQueue; (************************************************************************) (* THE NULL TASK *) (************************************************************************) PROCEDURE NullTask; (* The only function of the null task is to soak up processor time *) (* when no other task is able to run. *) BEGIN NotUsingFloatingPoint; LOOP (* Do nothing *) END (*LOOP*); END NullTask; (************************************************************************) (* DISPATCHER *) (************************************************************************) PROCEDURE TaskSwitch (T: Task); (* Performs a task switch from the current task to task T. It is *) (* assumed that the current task has already been moved to the *) (* appropriate kernel queue. *) (* This is, in principle, a system-dependent procedure, whose *) (* detailed implementation depends on the processor architecture *) (* and on compiler conventions. To keep this module portable, a *) (* separate procedure InnerKernel.Transfer is called to handle the *) (* system-dependent part. *) (* Special case: if an interrupt task is running, the actual task *) (* switch is deferred until the interrupt task has finished *) (* processing the current interrupt and has reached its next call *) (* to WaitForInterrupt. Procedure Transfer also looks after this *) (* detail. *) BEGIN IF T^.dead THEN Crash ("Task switch to nonexistent task"); END (*IF*); LastTask := CurrentTask; IF T <> LastTask THEN CurrentTask := T; (* Save and restore the floating point state if necessary. *) (* Notice that if an interrupt task is running (i.e. if *) (* InterruptTaskActive = TRUE), the saved state is that of *) (* the interrupted task, not the interrupt task. This is *) (* the appropriate action since there is a system-wide *) (* assumption that interrupt handlers may not perform *) (* floating point operations. *) IF LastTask^.FloatingPointUser THEN NPXsave (LastTask^.FPselector); END (*IF*); IF T^.FloatingPointUser THEN NPXrestore (T^.FPselector); END (*IF*); Transfer (LastTask^.selector, T^.selector); (* By the time we get back to here, one or more other *) (* tasks have been running; and we resume at this point *) (* by virtue of another task having executed Transfer. *) END (*IF*); END TaskSwitch; (************************************************************************) PROCEDURE RunNextTask; (* Performs a task switch to the first task on the ready list *) (* whose dynamic priority is CurrentPriority, if that task is not *) (* blocked. If it is blocked, we switch to its blocker instead. *) VAR T: Task; L: Lock; BEGIN T := ReadyList[CurrentPriority]; LOOP L := T^.WaitingFor; IF (L = NIL) OR NOT L^.Locked THEN EXIT(*LOOP*) END(*IF*); T := L^.Holder; END (*LOOP*); IF TimeSlicingEnabled THEN T^.TimeLeft := timequota[T^.priority]; END (*IF*); TaskSwitch (T); END RunNextTask; (************************************************************************) PROCEDURE SelectAnotherTask; (* Performs a task switch from the current task to the next ready *) (* task. It is assumed that the current task has already been *) (* removed from the active list. Notice that there is always at *) (* least one ready task, namely the null task. *) BEGIN WHILE ReadyList[CurrentPriority] = NIL DO DEC (CurrentPriority); END (*WHILE*); RunNextTask; END SelectAnotherTask; (************************************************************************) PROCEDURE QueueAndSwitchTasks (VAR (*INOUT*) KQ: TaskQueue); (* Puts the current task on the tail of list KQ, and gives control *) (* to the highest-priority ready task. *) BEGIN IF CurrentTask^.FirstLock <> NIL THEN Crash ("Task became inactive while holding a lock."); END (*IF*); LeaveQueue (ReadyList[CurrentTask^.priority]); AddToQueue (KQ, CurrentTask); SelectAnotherTask; END QueueAndSwitchTasks; (************************************************************************) PROCEDURE MarkAsReady (VAR (*INOUT*) FromQ: TaskQueue); (* Removes the first task from queue FromQ and makes it ready. If *) (* the task has a higher priority than the currently running task, *) (* then we perform an immediate task switch. Otherwise, the new *) (* task descriptor is placed on the ready list. *) (* Special case: if the incoming task is a sleeping task, it must *) (* be a timing-out task which has been reactivated before its *) (* timeout period has expired. We run such a task immediately, *) (* but don't yet put it on the list of active tasks. The task runs *) (* just far enough to clean up its kernel data structures, after *) (* which procedure RecheckPriorities decides whether to switch back *) (* to the preempted task or to legitimise the task switch. *) VAR thispriority: PriorityLevel; T: Task; BEGIN T := TakeFromQueue (FromQ); IF T^.sleeping THEN TaskSwitch (T); ELSE thispriority := T^.priority; AddToQueue (ReadyList[thispriority], T); IF thispriority > CurrentPriority THEN CurrentPriority := thispriority; IF TimeSlicingEnabled THEN T^.TimeLeft := timequota[thispriority]; END (*IF*); TaskSwitch (T); END (*IF*); END (*IF*); END MarkAsReady; (************************************************************************) PROCEDURE TimeSliceCheck; (* Called from the clock interrupt routine. We check whether the *) (* running task has used its time quota, and if so switch to the *) (* next ready task. Remark: it will sometimes happen that the *) (* current task has used up its time quota, but that all ready *) (* tasks have a lower priority than the current task. In that *) (* case, the end result of the "task switch" will be that the *) (* current task gets its time quota refreshed and continues running.*) (* This procedure does nothing is time-slicing is disabled. *) VAR savedPSW: CARDINAL; BEGIN IF TimeSlicingEnabled THEN savedPSW := EnterKernel(); WITH CurrentTask^ DO DEC (TimeLeft); IF TimeLeft <= 0 THEN ReadyList[priority] := ReadyList[priority]^.next; RunNextTask; END (*IF*); END (*WITH*); LeaveKernel (savedPSW); END (*IF*); END TimeSliceCheck; (************************************************************************) (* TASK TERMINATION *) (************************************************************************) PROCEDURE TaskExit; (* Removes the currently running task from the system, and performs *) (* a task switch to the next ready task. *) (* There is normally no need for a task to call this procedure, *) (* because it is automatically called when the task code "falls out *) (* the bottom" by executing its final procedure return. The stack *) (* is set up, at the time a task is created, in such a way that *) (* TaskExit will be entered at that time. *) VAR savedPSW: CARDINAL; BEGIN savedPSW := EnterKernel(); WITH CurrentTask^ DO IF FirstLock <> NIL THEN ReleaseAllLocks; END (*IF*); (* The following section is temporarily commented out, *) (* because what is needed is a little more complex than *) (* what is shown here. This is just a crude first *) (* approximation to remind me to look at this issue later. *) (* If this code is run by an interrupt task then *) (* CurrentTask doesn't even identify its descriptor. *) (* The best solution might turn out to be to convert the *) (* interrupt task to a non-interrupt task (i.e. disconnect *) (* it from the interrupt mechanism) and then put it on the *) (* ready list with an arrangement to call the "normal" exit *) (* procedure when next it runs. *) (* IF InterruptHandler THEN DisconnectFromInterrupt (selector); InterruptHandler := FALSE; END (*IF*); *) dead := TRUE; LeaveQueue (ReadyList[priority]); END (*WITH*); AddToQueue (UnusedDescriptors, CurrentTask); (* Note that the current task is now not on any of the active *) (* kernel queues, and it never will be again. Therefore, there *) (* will never be a return from the SelectAnotherTask call below.*) (* This, plus the fact that we are still inside the kernel and *) (* cannot be interrupted, justifies our deallocation of the *) (* stack space even though the stack is about to be used to *) (* push the return address for the SelectAnotherTask call. *) (* (But this approach should perhaps be reviewed, since it does *) (* rely on assumptions about how DEALLOCATE works.) *) IF CurrentTask^.StackBase = NIL THEN Crash ("TaskExit called from main program"); ELSE DEALLOCATE (CurrentTask^.StackBase, StackSize); SelectAnotherTask; END (*IF*); (* We never reach this point. *) END TaskExit; (************************************************************************) (* TASK INITIALISATION *) (************************************************************************) PROCEDURE UsingFloatingPoint; (* Tells the kernel that this task is one which uses the floating *) (* point processor. *) BEGIN IF InterruptTaskActive THEN Crash ("Interrupt task may not use floating point"); END (*IF*); CurrentTask^.FloatingPointUser := TRUE; END UsingFloatingPoint; (************************************************************************) PROCEDURE NotUsingFloatingPoint; (* Tells the kernel that this task is one which does not use the *) (* floating point processor. Calling this (optional) procedure *) (* speeds up task switching slightly, but it does put the onus on *) (* the caller to be certain that no floating point operations will *) (* be executed. *) BEGIN CurrentTask^.FloatingPointUser := FALSE; END NotUsingFloatingPoint; (************************************************************************) PROCEDURE MakeTask (tptr: Task; StartAddress: PROC; taskpriority: PriorityLevel; AllowInterrupts: BOOLEAN; taskname: NameString); (* Initialises the stack for a new task, and fills in the fields of *) (* its task descriptor tptr^. The remaining parameters are the *) (* start address of the task code; the priority of the new task; *) (* a flag which is TRUE iff the task is to be started with *) (* interrupts enabled; and a textual name for diagnostic purposes. *) (* Remark: An important function of this procedure is to set up *) (* initial conditions for the task so that it looks as if it *) (* entered the ready list in the same way as, for example, a *) (* blocked task which has just become unblocked. Procedure *) (* InnerKernel.TaskInit looks after most of that detail. *) BEGIN WITH tptr^ DO (* Create the task stack, and give it the necessary *) (* initial contents. *) ALLOCATE (StackBase, StackSize); selector := TaskInit (StackBase, StackSize, AllowInterrupts, TaskExit, StartAddress); FPselector := MakeFloatSaveSelector (selector); (* Set up the remaining task descriptor values. Some of *) (* the assignments below are redundant, but I believe that *) (* filling in most of the fields is desirable in that it *) (* makes this module more readable and guards against *) (* strange bugs being introduced in future revisions. *) name := taskname; priority := taskpriority; WaitingFor := NIL; FirstLock := NIL; IF TimeSlicingEnabled THEN TimeLeft := timequota[priority]; ELSE TimeLeft := 0; END (*IF*); InterruptHandler := FALSE; dead := FALSE; sleeping := FALSE; nextsleeper := NIL; FloatingPointUser := TRUE; InterruptNo := 0; (* Save the creator's floating point state in the save area *) (* of the new task, to ensure that the new task starts with *) (* a meaningful floating point state. Unfortunately the *) (* NPXsave seems to reset the floating point hardware *) (* to an undesired default, so we must restore the state *) (* again to get back where we were. *) NPXsave (FPselector); NPXrestore (FPselector); END (*WITH*); END MakeTask; (************************************************************************) PROCEDURE CreateTask (StartAddress: PROC; taskpriority: PriorityLevel; taskname: NameString); (* Must be called to introduce a task to the system. The first *) (* parameter, which should be the name of a procedure containing *) (* the task code, gives the starting address. The second parameter *) (* is the task's base priority. If this task has a higher priority *) (* than its creator, it will run immediately. Otherwise, it *) (* becomes ready. *) (* The effective priority of a task can be higher than its base *) (* priority, as the result of priority inheritance. This happens *) (* when the task holds a lock on which a higher-priority task is *) (* waiting. *) VAR tptr: Task; savedPSW: CARDINAL; BEGIN savedPSW := EnterKernel(); tptr := UnusedDescriptors; IF tptr = NIL THEN Crash ("Too many tasks created"); END (*IF*); MakeTask (tptr, StartAddress, taskpriority, TRUE, taskname); tptr^.FloatingPointUser := TRUE; MarkAsReady (UnusedDescriptors); LeaveKernel (savedPSW); END CreateTask; (************************************************************************) (* PROCEDURES TO DEAL WITH INTERRUPT TASKS *) (************************************************************************) PROCEDURE CreateInterruptTask (InterruptNumber: CARDINAL; StartAddress: PROC; taskname: NameString); (* Introduces an interrupt task to the system. The first parameter *) (* is the hardware-defined interrupt number, and the second is the *) (* address of the procedure whose code is the interrupt handler. *) (* An interrupt task differs from an ordinary task in that, when it *) (* is not running, it is idle rather than ready, and the dispatcher *) (* does not consider it to be among the tasks eligible to run. *) (* (It is run by a task switch which is made directly by the *) (* assembly language code which fields the interrupt). When the *) (* interrupt task has responded to the interrupt, it must call *) (* procedure WaitForInterrupt to put itself back in the idle state. *) (* On the next interrupt, it will continue from just after the call *) (* to WaitForInterrupt. Normally, therefore, the interrupt task *) (* will be written as an infinite loop. If for any reason the *) (* interrupt task exits by falling out of the bottom of its code, *) (* it will be destroyed in the same way as a normal task which *) (* terminates. That could be fatal, unless steps have been taken *) (* to reset the interrupt vector. *) VAR ITptr: Task; savedPSW: CARDINAL; BEGIN savedPSW := EnterKernel(); IF UnusedDescriptors = NIL THEN Crash ("Too many tasks created"); END (*IF*); ITptr := TakeFromQueue (UnusedDescriptors); MakeTask (ITptr, StartAddress, MAX(PriorityLevel), FALSE, taskname); WITH ITptr^ DO InterruptHandler := TRUE; FloatingPointUser := FALSE; InterruptNo := InterruptNumber; END (*WITH*); (* Now start the interrupt task and connect it to the desired *) (* interrupt. This operation includes executing the task's *) (* initialisation code, i.e. everything up to its first call to *) (* procedure WaitForInterrupt. *) InterruptTaskActive := TRUE; StartInterruptTask (ITptr^.selector, InterruptNumber); (* When we get back to here, the interrupt task has run to its *) (* first call to WaitForInterrupt, and InterruptTaskActive is *) (* FALSE again. *) LeaveKernel (savedPSW); END CreateInterruptTask; (************************************************************************) PROCEDURE KillInterruptTask (InterruptNumber: CARDINAL); (* Removes an interrupt task from the system. *) VAR k: TaskNumber; found: BOOLEAN; BEGIN k := MaxTaskNumber; REPEAT WITH TaskTable[k] DO found := InterruptHandler AND NOT dead AND (InterruptNo = InterruptNumber); END (*WITH*); DEC (k); UNTIL found OR (k = 0); IF found THEN INC (k); WITH TaskTable[k] DO DisconnectFromInterrupt (selector); InterruptHandler := FALSE; dead := TRUE; DEALLOCATE (StackBase, StackSize); END (*WITH*); AddToQueue (UnusedDescriptors, ADR(TaskTable[k])); END (*IF*); END KillInterruptTask; (************************************************************************) PROCEDURE WaitForInterrupt; (* Called by an interrupt task, to make itself dormant until the *) (* next interrupt comes along. It is not necessary to specify *) (* the interrupt number, since this was fixed at the time the *) (* interrupt task was created. *) VAR savedPSW: CARDINAL; BEGIN savedPSW := EnterKernel(); IF NOT InterruptTaskActive THEN Crash ("Normal task has called WaitForInterrupt"); END (*IF*); (* Procedure IOTransfer performs a task switch from the *) (* interrupt task to the interrupted task, whose state was *) (* saved at the time the interrupt occurred. *) InterruptTaskActive := FALSE; IOTransfer; (* We return to this point after the next interrupt. *) (* Return to the interrupt task. *) InterruptTaskActive := TRUE; LeaveKernel (savedPSW); END WaitForInterrupt; (************************************************************************) (* CREATING AND DESTROYING LOCKS *) (************************************************************************) PROCEDURE CreateLock (VAR (*OUT*) L: Lock); (* Creates a new lock. *) VAR savedPSW: CARDINAL; BEGIN savedPSW := EnterKernel(); NEW (L); WITH L^ DO Locked := FALSE; Holder := NIL; next := NIL; previous := NIL; END (*WITH*); LeaveKernel (savedPSW); END CreateLock; (************************************************************************) PROCEDURE DestroyLock (VAR (*INOUT*) L: Lock); (* Disposes of a lock. *) VAR savedPSW: CARDINAL; BEGIN savedPSW := EnterKernel(); (* It is legitimate for a task to obtain L before destroying *) (* it, in which case we must remove L from the list of locks *) (* held by this task. However we leave the Locked field set in *) (* case some task still has a dangling pointer to this lock. *) WITH L^ DO IF Holder <> NIL THEN IF previous = NIL THEN Holder^.FirstLock := next ELSE previous^.next := next; END (*IF*); IF next <> NIL THEN next^.previous := previous; END (*IF*); END (*IF*); Locked := TRUE; END (*WITH*); DISPOSE (L); LeaveKernel (savedPSW); END DestroyLock; (************************************************************************) (* OBTAINING A LOCK *) (************************************************************************) PROCEDURE Obtain (L: Lock); (* Obtains lock L, waiting if necessary. *) VAR Blocker: Task; M: Lock; savedPSW: CARDINAL; BEGIN savedPSW := EnterKernel(); IF L^.Locked THEN (* Work out the blocker of the current task. Note: in an *) (* earlier version of this module I was able to avoid the *) (* loop below by keeping a BlockedBy field in each task *) (* descriptor; but it turned out to be more trouble than it *) (* was worth to update the BlockedBy information when a *) (* lock was released. It's less expensive to incur the *) (* penalty here and keep the Release code simple. In any *) (* case the penalty is not severe - it would take a really *) (* baroque example to slow us down here. *) M := L; REPEAT Blocker := M^.Holder; M := Blocker^.WaitingFor; UNTIL (M = NIL) OR NOT M^.Locked; IF Blocker = CurrentTask THEN Crash ("Deadlock detected"); END (*IF*); CurrentTask^.WaitingFor := L; IF TimeSlicingEnabled THEN Blocker^.TimeLeft := timequota[Blocker^.priority]; END (*IF*); TaskSwitch (Blocker); (* When execution resumes at this point (via *) (* RunNextTask), L^.Locked will be FALSE. *) CurrentTask^.WaitingFor := NIL; END (*IF*); WITH L^ DO Locked := TRUE; Holder := CurrentTask; (* Add L to the list of locks held by CurrentTask. *) previous := NIL; next := CurrentTask^.FirstLock; IF next <> NIL THEN next^.previous := L; END (*IF*); CurrentTask^.FirstLock := L; END (*WITH*); LeaveKernel (savedPSW); END Obtain; (************************************************************************) (* RELEASING A LOCK *) (************************************************************************) PROCEDURE Release (L: Lock); (* Releases lock L. This implicitly changes the "blocked by" *) (* status of all tasks blocked by the current task, but it turns *) (* out to be faster to get procedure RunNextTask to recheck the *) (* blocking status than to keep track of "blocked by" information. *) (* All we need to know here is whether we've inherited a priority *) (* greater than our base priority. If so, we must have been *) (* blocking another task which should run now - we can trust *) (* RunNextTask to work out the identity of that task. *) VAR savedPSW: CARDINAL; BEGIN savedPSW := EnterKernel(); WITH L^ DO (* Remove L from the list of locks held by CurrentTask. *) IF previous = NIL THEN CurrentTask^.FirstLock := next ELSE previous^.next := next; END (*IF*); IF next <> NIL THEN next^.previous := previous; END (*IF*); (* Mark the lock as free. *) Locked := FALSE; Holder := NIL; END (*WITH*); (* If we have inherited an augmented priority, a task of higher *) (* priority might just have become unblocked. *) IF CurrentTask^.priority < CurrentPriority THEN RunNextTask; END (*IF*); LeaveKernel (savedPSW); END Release; (************************************************************************) PROCEDURE ReleaseAllLocks; (* Releases all locks held by the current task. Application-level *) (* tasks normally won't need to call this procedure; it is *) (* provided to support the system shutdown function and for things *) (* like "emergency abort" operations. *) VAR savedPSW: CARDINAL; BEGIN savedPSW := EnterKernel(); WHILE CurrentTask^.FirstLock <> NIL DO Release (CurrentTask^.FirstLock); END (*WHILE*); LeaveKernel (savedPSW); END ReleaseAllLocks; (************************************************************************) (* PROCEDURES TO DEAL WITH SLEEPING TASKS *) (************************************************************************) VAR SleepQueueHead: Task; (* SleepQueueHead points to a priority queue of descriptors for *) (* tasks which are sleeping. The first element of the list has the *) (* time-to-go in its TimeLeft field. For subsequent elements, *) (* TimeLeft records the difference between its time-to-go and that *) (* of the preceding list element. There are some situations in *) (* which the delay specification cannot be met exactly, so it is *) (* possible for the TimeLeft field to go negative. *) (************************************************************************) (* SLEEP QUEUE INITIALISATION *) (************************************************************************) PROCEDURE InitialiseSleepQueue; (* Creates an initially empty queue of sleeping tasks. *) BEGIN SleepQueueHead := NIL; END InitialiseSleepQueue; (************************************************************************) (* 'PUT-ME-TO-SLEEP' PROCEDURES *) (************************************************************************) PROCEDURE InsertCurrentTaskInSleepQueue (sleeptime: INTEGER); (* Inserts the current task into the sleep queue. This queue is a *) (* linear list. The time to sleep is stored in incremental form: *) (* for every task on the list except the first, the TimeLeft field *) (* in the task descriptor holds the difference between the time to *) (* go for this task and that of the task ahead of it in the list. *) (* This means that the clock interrupt routine need only decrement *) (* the TimeLeft field of the first sleeping task. *) VAR previous, following: Task; BEGIN CurrentTask^.sleeping := TRUE; (* Find the correct place in the sleep queue. *) following := SleepQueueHead; previous := NIL; LOOP IF following = NIL THEN EXIT(*LOOP*) END (*IF*); IF sleeptime < following^.TimeLeft THEN EXIT(*LOOP*) END (*IF*); previous := following; following := following^.nextsleeper; DEC (sleeptime, previous^.TimeLeft); END (*LOOP*); (* Insert the task descriptor for this task between *) (* previous^ and following^. *) IF previous = NIL THEN SleepQueueHead := CurrentTask; ELSE previous^.nextsleeper := CurrentTask; END (*IF*); CurrentTask^.nextsleeper := following; (* Fix the TimeLeft fields for the current and following tasks. *) CurrentTask^.TimeLeft := sleeptime; IF following <> NIL THEN DEC (following^.TimeLeft, sleeptime); END (*IF*); END InsertCurrentTaskInSleepQueue; (************************************************************************) PROCEDURE RemoveCurrentFromSleepQueue; (* Removes the current task from the sleep queue. *) VAR previous, current: Task; BEGIN previous := NIL; current := SleepQueueHead; WHILE current <> CurrentTask DO previous := current; current := current^.nextsleeper; END (*WHILE*); IF current^.nextsleeper <> NIL THEN INC (current^.nextsleeper^.TimeLeft, current^.TimeLeft); END (*IF*); IF previous = NIL THEN SleepQueueHead := current^.nextsleeper; ELSE previous^.nextsleeper := current^.nextsleeper; END (*IF*); CurrentTask^.sleeping := FALSE; END RemoveCurrentFromSleepQueue; (************************************************************************) PROCEDURE RecheckPriorities; (* This procedure is called, just before leaving the kernel, by a *) (* sleeping task which has been woken up. The task is running but *) (* has not yet been put on the list of active tasks. If this task *) (* has a high enough priority, we put it on the active list and let *) (* it continue to run. Otherwise, we mark it as ready but switch *) (* back to the task which it preempted. *) VAR thispriority: PriorityLevel; BEGIN thispriority := CurrentTask^.priority; IF thispriority > CurrentPriority THEN AddToQueue (ReadyList[thispriority], CurrentTask); CurrentPriority := thispriority; IF TimeSlicingEnabled THEN CurrentTask^.TimeLeft := timequota[thispriority]; ELSE CurrentTask^.TimeLeft := 0; END (*IF*); ELSE AddToQueue (ReadyList[thispriority], CurrentTask); TaskSwitch (LastTask); END (*IF*); END RecheckPriorities; (************************************************************************) PROCEDURE Delay (sleeptime: INTEGER); (* Puts the calling task to sleep for approximately the specified *) (* number of clock ticks. The time delay is necessarily *) (* approximate, because (a) the clock might tick over when we are *) (* in the middle of this procedure; (b) after the expiration of the *) (* delay period, the sleeping task is made ready, but this does not *) (* guarantee that it will run immediately. *) VAR savedPSW: CARDINAL; BEGIN savedPSW := EnterKernel(); IF CurrentTask^.FirstLock <> NIL THEN Crash ("Task sleeping while holding a lock"); END (*IF*); LeaveQueue (ReadyList[CurrentTask^.priority]); InsertCurrentTaskInSleepQueue (sleeptime); SelectAnotherTask; (* We resume at this point after waking up. *) RecheckPriorities; LeaveKernel (savedPSW); END Delay; (************************************************************************) (* CLOCK TICK HANDLER FOR SLEEPING TASKS *) (************************************************************************) PROCEDURE CheckSleepers; (* Called from the clock interrupt task. We check whether a *) (* sleeping task needs to be woken up, and wake it up if so. *) (* Note: this procedure typically needs no explicit critical *) (* section protection. It is called from the clock interrupt *) (* handler, therefore runs with interrupts disabled. We prefer, *) (* however, to stick to the rule that all kernel procedures are *) (* protected via EnterKernel and LeaveKernel, since it makes the *) (* kernel easier to check for correctness. This convention also *) (* helps portability. It means, for example, that this procedure *) (* will still work in the multiprocessor case, or in the case where *) (* the clock interrupt routine does not disable all interrupts. *) VAR first, second: Task; savedPSW: CARDINAL; BEGIN savedPSW := EnterKernel(); first := SleepQueueHead; IF first <> NIL THEN (* update sleeping time of head of sleep queue *) DEC (first^.TimeLeft); (* Is it time to wake up first sleeping task? *) IF first^.TimeLeft <= 0 THEN (* Yes. Correct sleeping time of second sleeper. *) second := first^.nextsleeper; SleepQueueHead := second; IF second <> NIL THEN INC (second^.TimeLeft, first^.TimeLeft); END (* IF second <> NIL *); (* Wake up the first sleeping task. Because of *) (* complications when this task is still on a semaphore *) (* blocked list, we must run the task immediately *) (* rather than simply make it ready. The task is not *) (* yet put on the list of active tasks, because it *) (* might be preempting a task of higher priority. *) (* Before the newly awakened task leaves the kernel, it *) (* will call procedure RecheckPriorities to clean up *) (* such details. *) first^.sleeping := FALSE; TaskSwitch (first); END (* IF sleeping time has expired *) END (* IF sleep queue not empty *); LeaveKernel (savedPSW); END CheckSleepers; (************************************************************************) (* TASK QUEUEING WITH TIMEOUT *) (************************************************************************) PROCEDURE QueueWithTimeout (VAR (*INOUT*) KQ: TaskQueue; TimeLimit: INTEGER): BOOLEAN; (* Like procedure QueueAndSwitchTasks, this procedure puts the *) (* current task on the tail of list KQ, and gives control to the *) (* highest-priority ready task. The difference is that we allow *) (* this task to remain on KQ for at most TimeLimit timer ticks. *) (* If the task is removed from KQ before the time limit expires, *) (* we return a result of FALSE. If the time limit expires first, *) (* we remove the queued task from KQ anyway, and make it runnable, *) (* and return a result of TRUE when it does run. *) (* Note: this procedure may be called only from inside the kernel. *) (* While being timed out, a task is simultaneously on two kernel *) (* queues: the specified KQ, and the Sleep queue. It will be *) (* removed from one of these - which one depends, of course, on *) (* whether the time limit expires - by some other kernel action, *) (* and at that stage we must remove it from the other queue before *) (* it leaves the kernel. If we did not guarantee indivisibility *) (* of the operation of removing the task from the two queues, we *) (* would run the risk of putting the same task onto the ready *) (* structure twice. *) VAR TimedOut: BOOLEAN; BEGIN InsertCurrentTaskInSleepQueue (TimeLimit); QueueAndSwitchTasks (KQ); (* When execution of this task resumes at this point, we have *) (* been activated either by being removed from KQ or by a timer *) (* event (but never both simultaneously). That is, we have *) (* been removed from one queue, and must now remove ourself *) (* from the other. *) TimedOut := NOT CurrentTask^.sleeping; IF TimedOut THEN LeaveQueue (KQ); ELSE RemoveCurrentFromSleepQueue; END (*IF*); (* Additional complication: the kernel procedures have so far *) (* given the newly awakened task preferential treatment, to *) (* allow it to remove itself from both queues before leaving *) (* the kernel. If this has caused a higher-priority task to be *) (* preempted, we must give the processor back. *) RecheckPriorities; RETURN TimedOut; END QueueWithTimeout; (************************************************************************) (* PMOS SHUTDOWN *) (************************************************************************) PROCEDURE ShutDown; (* Kills all remaining interrupt tasks, and partially inhibits *) (* multitasking by giving the current task - which is running the *) (* program termination code - the maximum possible priority. This *) (* still allows the possibility of a task switch caused by *) (* something like a semaphore Wait; but it does ensure that the *) (* mere unblocking of another task, for example by releasing a *) (* Lock, will not cause a task switch. Notice too that time *) (* slicing is irrelevant beyond this point, because the timer *) (* interrupt task will be disabled. *) VAR k: TaskNumber; savedPSW: CARDINAL; BEGIN savedPSW := EnterKernel(); (* Remove any interrupt handlers which might still be active. *) (* The reason for doing this backwards is that this is more *) (* likely to work in the case of multiple handlers for the *) (* same interrupt number. At some time I should look for a *) (* more robust solution for this case. *) FOR k := MaxTaskNumber TO 0 BY -1 DO WITH TaskTable[k] DO IF InterruptHandler AND NOT dead THEN DisconnectFromInterrupt (selector); InterruptHandler := FALSE; dead := TRUE; DEALLOCATE (StackBase, StackSize); AddToQueue (UnusedDescriptors, ADR(TaskTable[k])); END (*IF*); END (*WITH*); END (*FOR*); (* Increase the priority of the current task. *) LeaveQueue (ReadyList[CurrentTask^.priority]); CurrentPriority := MAX(PriorityLevel); CurrentTask^.priority := CurrentPriority; AddToQueue (ReadyList[CurrentPriority], CurrentTask); LeaveKernel (savedPSW); END ShutDown; (************************************************************************) (* MODULE INITIALISATION *) (************************************************************************) PROCEDURE InitialiseTaskControl; (* Must be called before any tasks are created, to initialise the *) (* task control data structures. *) VAR p: PriorityLevel; j: TaskNumber; k: InterruptType; quota: CARDINAL; BEGIN InterruptTaskActive := FALSE; (* Initialise the timequota array. *) IF TimeSlicingEnabled THEN quota := 1; FOR p := MaxPriority TO 1 BY -1 DO timequota[p] := quota; quota := 2*quota; END (*FOR*); timequota[0] := 32767; END (*IF*); (* Collect all the unused task descriptors into a linear list. *) CreateQueue (UnusedDescriptors); FOR j := 0 TO MaxTaskNumber DO TaskTable[j].dead := TRUE; TaskTable[j].sleeping := FALSE; AddToQueue (UnusedDescriptors, ADR (TaskTable[j]) ); END (*FOR*); (* The active list and the sleep queue are initially empty. *) FOR p := 0 TO MaxPriority DO CreateQueue (ReadyList[p]); END (*FOR*); InitialiseSleepQueue; (* Create a descriptor for the current task, i.e. for the task *) (* which called this procedure. We give it a low priority *) (* because this task is often running the background jobs. We *) (* also set its StackBase to NIL because the stack for this *) (* task already exists, i.e. it's not created by this module. *) LastTask := NIL; CurrentTask := TakeFromQueue (UnusedDescriptors); CurrentPriority := 1; WITH CurrentTask^ DO name := "Main program"; priority := CurrentPriority; WaitingFor := NIL; FirstLock := NIL; FPselector := MakeFloatSaveSelector (InitMainTask()); StackBase := NIL; InterruptHandler := FALSE; dead := FALSE; FloatingPointUser := TRUE; sleeping := FALSE; InterruptNo := 0; END (*WITH*); AddToQueue (ReadyList[CurrentPriority], CurrentTask); (* Create the null task. It will not run immediately, because *) (* the current (initialisation) task has higher priority. *) CreateTask (NullTask, 0, "Null task"); (* We have a working operating system. It is now safe to *) (* enable clock interrupts, and to call CreateTask to introduce *) (* more tasks to the system. Device interrupts can be enabled *) (* when the device drivers are installed. *) END InitialiseTaskControl; (************************************************************************) (* INITIALISATION *) (************************************************************************) BEGIN InitialiseTaskControl; SetTerminationProcedure (ShutDown); END TaskControl.