Copyright (c) Hyperion Entertainment and contributors.
Difference between revisions of "Exec Tasks"
Steven Solie (talk | contribs) |
Steven Solie (talk | contribs) |
||
Line 184: | Line 184: | ||
=== Task Priority === |
=== Task Priority === |
||
− | A |
+ | A task's priority indicates its importance relative to other tasks. Higher-priority tasks receive the processor before lower-priority tasks do. Task priority is stored as a signed number ranging from -128 to +127. Higher priorities are represented by more positive values; zero is considered the neutral priority. Normally, system tasks execute somewhere in the range of +20 to -20, and most application tasks execute at priority 0. |
− | It is not wise to needlessly raise a |
+ | It is not wise to needlessly raise a task's priority. Sometimes it may be necessary to carefully select a priority so that the task can properly interact with various system tasks. The SetTaskPri() Exec function is provided for this purpose. |
== Task Termination == |
== Task Termination == |
Revision as of 23:12, 4 April 2012
Contents
Exec Tasks
One of the most powerful features of the Amiga operating system is its ability to run and manage multiple independent program tasks, providing each task with processor time based on their priority and activity. These tasks include system device drivers, background utilities, and user interface environments, as well as normal application programs. This multitasking capability is provided by the Exec library's management of task creation, termination, scheduling, event signals, traps, exceptions, and mutual exclusion.
This chapter deals with Exec on a lower level than most applications programmers need and assumes you are already familiar with the Exec basics discussed in the "Introduction to Exec" chapter of this manual.
Task Structure
Exec maintains task context and state information in a task-control data structure. Like most Exec structures, Task structures are dynamically linked onto various task queues through the use of an embedded Exec list Node structure (see the Lists chapter). Any task can find its own task structure by calling FindTask(NULL). The C-language form of this structure is defined in the <exec/tasks.h> include file:
struct Task { struct Node tc_Node; UBYTE tc_Flags; UBYTE tc_State; BYTE tc_IDNestCnt; /* intr disabled nesting */ BYTE tc_TDNestCnt; /* task disabled nesting */ ULONG tc_SigAlloc; /* sigs allocated */ ULONG tc_SigWait; /* sigs we are waiting for */ ULONG tc_SigRecvd; /* sigs we have received */ ULONG tc_SigExcept; /* sigs we will take excepts for */ UWORD tc_TrapAlloc; /* traps allocated */ UWORD tc_TrapAble; /* traps enabled */ APTR tc_ExceptData; /* points to except data */ APTR tc_ExceptCode; /* points to except code */ APTR tc_TrapData; /* points to trap code */ APTR tc_TrapCode; /* points to trap data */ APTR tc_SPReg; /* stack pointer */ APTR tc_SPLower; /* stack lower bound */ APTR tc_SPUpper; /* stack upper bound + 2*/ VOID (*tc_Switch)(); /* task losing CPU */ VOID (*tc_Launch)(); /* task getting CPU */ struct List tc_MemEntry; /* allocated memory */ APTR tc_UserData; /* per task data */ };
A similar assembly code structure is available in the <exec/tasks.i> include file.
Most of these fields are not relevant for simple tasks; they are used by Exec for state and administrative purposes. A few fields, however, are provided for the advanced programs that support higher level environments (as in the case of processes) or require precise control (as in devices). The following sections explain these fields in more detail.
Task Creation
To create a new task you must allocate a task structure, initialize its various fields, and then link it into Exec with a call to AddTask().
The task structure may be allocated by calling the AllocMem() function with the MEMF_CLEAR and MEMF_PUBLIC allocation attributes. These attributes indicate that the data structure is to be pre-initialized to zero and that the structure is shared.
The Task fields that require initialization depend on how you intend to use the task. For the simplest of tasks, only a few fields must be initialized:
The task list node structure. This includes the task’s priority, its type, and its name (refer to the “Exec Lists and Queues” chapter).
The lower memory bound of the task’s stack.
The upper memory bound of the task’s stack.
The initial stack pointer. Because task stacks grow downward in memory, this field is usually set to the same value as tc_SPUpper.
Zeroing all other unused fields will cause Exec to supply the appropriate system default values. Allocating the structure with the MEMF_CLEAR attribute is an easy way to be sure that this happens.
Once the structure has been initialized, it must be linked to Exec. This is done with a call to AddTask() in which the following parameters are specified:
AddTask(struct Task *task, APTR initialPC, APTR finalPC )
The task argument is a pointer to your initialized Task structure. Set initialPC to the entry point of your task code. This is the address of the first instruction the new task will execute.
Set finalPC to the address of the finalization code for your task. This is a code section that will receive control if the initialPC routine ever performs a return (RTS). This exists to prevent your task from being launched into random memory upon an accidental return. The finalPC routine should usually perform various program-related clean-up duties and should then remove the task. If a zero is supplied for this parameter, Exec will use its default finalization code (which simply calls the RemTask() function).
Under Release 2, AddTask() returns the address of the newly added task or NULL for failure. Under 1.3 and older versions of the OS, no values are returned.
Task Creation With amiga.lib
A simpler method of creating a task is provided by the amiga.lib Exec support function CreateTask(), which can be accessed if your code is linked with amiga.lib.
CreateTask(char *name, LONG priority, APTR initialPC, ULONG stacksize)
A task created with CreateTask() may be removed with the amiga.lib DeleteTask() function, or it may simply return when it is finished. CreateTask() adds a MemList to the tc_MemEntry of the task it creates, describing all memory it has allocated for the task, including the task stack and the Task structure itself. This memory will be deallocated by Exec when the task is either explicitly removed (RemTask() or DeleteTask()) or when it exits to Exec’s default task removal code (RemTask()).
Note that a bug in the CreateTask() code caused a failed memory allocation to go unnoticed in V33 and early versions of Release 2 amiga.lib.
If your development language is not linkable with amiga.lib, it may provide an equivalent built-in function, or you can create your own based on the createtask.c code in the Amiga ROM Kernel Reference Manual: Includes and Autodocs.
Depending on the priority of a new task and the priorities of other tasks in the system, the newly added task may begin execution immediately.
boxSharing Library PointersAlthough in most cases it is possible for a parent task to pass a library base to a child task so the child can use that library, for some libraries, this is not possible. For this reason, the only library base sharable between tasks is Exec’s library base.
Here is an example of simple task creation. In this example there is no coordination or communication between the main process and the simple task it has created. A more complex example might use named ports and messages to coordinate the activities and shutdown of two tasks. Because our task is very simple and never calls any system functions which could cause it to be signalled or awakened, we can safely remove the task at any time.
boxKeep This In Mind.Because the simple task’s code is a function in our program, we must stop the subtask before exiting.
;/* simpletask.c - Execute me to compile me with SAS C 5.10 LC -b1 -cfistq -v -y -j73 simpletask.c Blink FROM LIB:c.o,simpletask.o TO simpletask LIBRARY LIB:LC.lib,LIB:Amiga.lib quit simpletask.c - Uses the amiga.lib function CreateTask() to create a simple subtask. See the Includes and Autodocs manual for CreateTask() source code */ #include <exec/types.h> #include <exec/memory.h> #include <exec/tasks.h> #include <libraries/dos.h> #include <clib/exec_protos.h> #include <clib/alib_protos.h> #include <stdlib.h> #include <stdio.h> #ifdef LATTICE int CXBRK(void) { return(0); } /* Disable Lattice CTRL/C handling */ int chkabort(void) {return(0);} #endif #define STACK_SIZE 1000L /* Task name, pointers for allocated task struct and stack */ struct Task *task = NULL; char *simpletaskname = "SimpleTask"; ULONG sharedvar; /* our function prototypes */ void simpletask(void); void cleanexit(UBYTE *,LONG); void main(int argc,char **argv) { sharedvar = 0L; task = CreateTask(simpletaskname,0,simpletask,STACK_SIZE); if(!task) cleanexit("Can't create task",RETURN_FAIL); printf("This program initialized a variable to zero, then started a\n"); printf("separate task which is incrementing that variable right now,\n"); printf("while this program waits for you to press RETURN.\n"); printf("Press RETURN now: "); getchar(); printf("The shared variable now equals %ld\n",sharedvar); /* We can simply remove the task we added because our simpletask does not make */ /* any system calls which could cause it to be awakened or signalled later. */ Forbid(); DeleteTask(task); Permit(); cleanexit("",RETURN_OK); } void simpletask() { while(sharedvar < 0x8000000) sharedvar++; /* Wait forever because main() is going to RemTask() us */ Wait(0L); } void cleanexit(UBYTE *s, LONG e) { if(*s) printf("%s\n",s); exit(e); }
Task Stack
Every task requires a stack. All task stacks are user mode stacks (in the language of the 68000) and are addressed through the A7 CPU register. All normal code execution occurs on this task stack. Special modes of execution (processor traps and system interrupts for example) execute on a single supervisor mode stack and do not directly affect task stacks.
Task stacks are normally used to store local variables, subroutine return addresses, and saved register values. Additionally, when a task loses the processor, all of its current registers are preserved on this stack (with the exception of the stack pointer itself, which must be saved in the task structure).
The amount of stack used by a task can vary widely. The theoretical minimum stack size is 72 bytes, which is the number required to save 17 CPU registers and a single return address. Of course, a stack of this size would not give you adequate space to perform any subroutine calls (because the return address occupies stack space). On the other hand, a stack size of 1K would suffice to call most system functions but would not allow much in the way of local variable storage. Processes that call DOS library functions need an additional 1500 bytes of stack.
Because stack-bounds checking is not provided as a service of Exec, it is important to provide enough space for your task stack. Stack overflows are always difficult to debug and may result not only in the erratic failure of your task but also in the mysterious malfunction of other Amiga subsystems. Some compilers provide a stack-checking option.
boxYou Can’t Always Check The Stack.Such stack-checking options generally cannot be used if part of your code will be running on the system stack (interrupts, 680x0 exceptions, handlers, servers), or on a different task’s stack (libraries, devices, created tasks).
When choosing your stack size, do not cut it too close. Remember that any recursive routines in your code may use varying amounts of stack, and that future versions of system routines may use additional stack variables. By dynamically allocating buffers and arrays, most application programs can be designed to function comfortably within the default process stack size of 4000 bytes.
Task Priority
A task's priority indicates its importance relative to other tasks. Higher-priority tasks receive the processor before lower-priority tasks do. Task priority is stored as a signed number ranging from -128 to +127. Higher priorities are represented by more positive values; zero is considered the neutral priority. Normally, system tasks execute somewhere in the range of +20 to -20, and most application tasks execute at priority 0.
It is not wise to needlessly raise a task's priority. Sometimes it may be necessary to carefully select a priority so that the task can properly interact with various system tasks. The SetTaskPri() Exec function is provided for this purpose.
Task Termination
Task termination may occur as the result of a number of situations:
A program returning from its initialPC routine and dropping into its finalPC routine or the system default finalizer.
A task trap that is too serious for a recovery action. This includes traps like processor bus error, odd address access errors, etc.
A trap that is not handled by the task. For example, the task might be terminated if your code happened to encounter a processor TRAP instruction and you did not provide a trap handling routine.
An explicit call to Exec RemTask() or amiga.lib DeleteTask().
Task termination involves the deallocation of system resources and the removal of the task structure from Exec. The most important part of task termination is the deallocation of system resources. A task must return all memory that it allocated for its private use, it must terminate any outstanding I/O commands, and it must close access to any system libraries or devices that it has opened.
It is wise to adopt a strategy for task clean-up responsibility. You should decide whether resource allocation and deallocation is the duty of the creator task or the newly created task. Often it is easier and safer for the creator to handle the resource allocation and deallocation on behalf of its offspring. In such cases, before removing the child task, you must make sure it is in a safe state such as Wait(0L) and not still using a resources or waiting for an event or signal that might still occur.
boxNOTE:Certain resources, such as signals and created ports, must be allocated and deallocated by the same task that will wait on them. Also note that if your subtask code is part of your loaded program, you must not allow your program to exit before its subtasks have cleaned up their allocations, and have been either deleted or placed in a safe state such as Wait(0L).
Task Exclusion
From time to time the advanced system program may find it necessary to access global system data structures. Because these structures are shared by the system and by other tasks that execute asynchronously to your task, a task must prevent other tasks from using these structures while it is reading from or writing to them. This can be accomplished by preventing the operating system from switching tasks by forbidding or disabling. A section of code that requires the use of either of these mechanisms to lock out access by others is termed a critical section. Use of these methods is discouraged. For arbitrating access to data between your tasks, semaphores are a superior solution. (See the “Exec Semaphores” chapter)
Forbidding Task Switching
Forbidding is used when a task is accessing shared structures that might also be accessed at the same time from another task. It effectively eliminates the possibility of simultaneous access by imposing nonpreemptive task scheduling. This has the net effect of disabling multitasking for as long as your task remains in its running state.
While forbidden, your task will continue running until it performs a call to Wait() or exits from the forbidden state. Interrupts will occur normally, but no new tasks will be dispatched, regardless of their priorities.
When a task running in the forbidden state calls the Wait() function, directly or indirectly, it implies a temporary exit from its forbidden state. Since almost all stdio, device I/O, and file I/O functions must Wait() for I/O completion, performing such calls will cause your task to Wait(), temporarily breaking the forbid. While the task is waiting, the system will perform normally. When the task receives one of the signals it is waiting for, it will again reenter the forbidden state.
To become forbidden, a task calls the Forbid() function. To escape, the Permit() function is used. The use of these functions may be nested with the expected affects; you will not exit the forbidden mode until you call the outermost Permit().
As an example, the Exec task list should only be accessed when in a Forbid() state. Accessing the list without forbidding could lead to incorrect results or it could crash the entire system. To access the task list also requires the program to disable interrupts which is discussed in the next section.
Disabling Tasks
Disabling is similar to forbidding, but it also prevents interrupts from occurring during a critical section. Disabling is required when a task accesses structures that are shared by interrupt code. It eliminates the possibility of an interrupt accessing shared structures by preventing interrupts from occurring. Use of disabling is strongly discouraged.
To disable interrupts you can call the Disable() function. To enable interrupts again, use the Enable() function. Although assembler DISABLE and ENABLE macros are provided, assembler programmers should use the system functions rather than the macros for upwards compatibility, ease of debugging, and smaller code size.
Like forbidden sections, disabled sections can be nested. To restore normal interrupt processing, an Enable() call must be made for every Disable(). Also like forbidden sections, any direct or indirect call to the Wait() function will enable interrupts until the task regains the processor.
boxWARNING:It is important to realize that there is a danger in using disabled sections. Because the software on the Amiga depends heavily on its interrupts occurring in nearly real time, you cannot disable for more than a very brief instant. Disabling interrupts for more than 250 <math>\mu</math>seconds can interfere with the normal operation of vital system functions, especially serial I/O.
boxWARNING:Masking interrupts by changing the 68000 processor interrupt priority levels with the MOVE SR instruction can also be dangerous and is very strongly discouraged. The disable- and enable-related functions control interrupts through the 4703 custom chip and not through the 68000 priority level. In addition, the processor priority level can be altered only from supervisor mode (which means this process is much less efficient).
It is never necessary to both Disable() and Forbid(). Because disabling prevents interrupts, it also prevents preemptive task scheduling. When disable is used within an interrupt, it will have the effect of locking out all higher level interrupts (lower level interrupts are automatically disabled by the CPU). Many Exec lists can only be accessed while disabled. Suppose you want to print the names of all system tasks. You would need to access both the TaskReady and TaskWait lists from within a single disabled section. In addition, you must avoid calling system functions that would break a disable by an indirect call to Wait() (printf() for example). In this example, the names are gathered into a list while task switching is disabled. Then task switching is enabled and the names are printed.
;/* tasklist.c - Execute me to compile me with SAS C 5.10 LC -b1 -cfistq -v -y -j73 tasklist.c Blink FROM LIB:c.o,tasklist.o TO tasklist LIBRARY LIB:LC.lib,LIB:Amiga.lib quit tasklist.c - Snapshots and prints the ExecBase task list */ #include <exec/types.h> #include <exec/lists.h> #include <exec/nodes.h> #include <exec/memory.h> #include <exec/execbase.h> #include <clib/alib_protos.h> #include <clib/exec_protos.h> #include <stdio.h> #include <string.h> #ifdef LATTICE int CXBRK(void) { return(0); } /* disable SAS/C CTRL-C handing */ int chkabort(void) {return(0); } #endif static UBYTE *VersTag = "$VER: tasklist 37.2 (31.3.92)"; extern struct ExecBase *SysBase; /* Use extended structure to hold task information */ struct TaskNode { struct Node tn_Node; ULONG tn_TaskAddress; ULONG tn_SigAlloc; ULONG tn_SigWait; UBYTE tn_Name[32]; }; void main(int argc, char **argv) { struct List *ourtasklist; struct List *exectasklist; struct Task *task; struct TaskNode *node, *tnode, *rnode = NULL; struct Node *execnode; /* Allocate memory for our list */ if (ourtasklist = AllocMem(sizeof(struct List), MEMF_CLEAR)) { /* Initialize list structure (ala NewList()) */ ourtasklist->lh_Head = (struct Node *)&ourtasklist->lh_Tail; ourtasklist->lh_Tail = 0; ourtasklist->lh_TailPred = (struct Node *)&ourtasklist->lh_Head; /* Make sure tasks won't switch lists or go away */ Disable(); /* Snapshot task WAIT list */ exectasklist = &(SysBase->TaskWait); for (execnode = exectasklist->lh_Head; execnode->ln_Succ; execnode = execnode->ln_Succ) { if (tnode = AllocMem(sizeof(struct TaskNode), MEMF_CLEAR)) { /* Save task information we want to print */ strncpy(tnode->tn_Name, execnode->ln_Name, 32); tnode->tn_Node.ln_Pri = execnode->ln_Pri; tnode->tn_TaskAddress = (ULONG)execnode; tnode->tn_SigAlloc = ((struct Task *)execnode)->tc_SigAlloc; tnode->tn_SigWait = ((struct Task*)execnode)->tc_SigWait; AddTail(ourtasklist, (struct Node *)tnode); } else break; } /* Snapshot task READY list */ exectasklist = &(SysBase->TaskReady); for (execnode = exectasklist->lh_Head; execnode->ln_Succ; execnode = execnode->ln_Succ) { if (tnode = AllocMem(sizeof(struct TaskNode), MEMF_CLEAR)) { /* Save task information we want to print */ strncpy(tnode->tn_Name, execnode->ln_Name, 32); tnode->tn_Node.ln_Pri = execnode->ln_Pri; tnode->tn_TaskAddress = (ULONG)execnode; tnode->tn_SigAlloc = ((struct Task *)execnode)->tc_SigAlloc; tnode->tn_SigWait = ((struct Task*)execnode)->tc_SigWait; AddTail(ourtasklist, (struct Node *)tnode); if(!rnode) rnode = tnode; /* first READY task */ } else break; } /* Re-enable interrupts and taskswitching */ Enable(); /* Print now (printing above would have defeated a Forbid or Disable) */ printf("Pri Address SigAlloc SigWait Taskname\n"); node = (struct TaskNode *)(ourtasklist->lh_Head); printf("\nWAITING:\n"); while (tnode = (struct TaskNode *)node->tn_Node.ln_Succ) { if(tnode == rnode) printf("\nREADY:\n"); /* we set rnode above */ printf("%02d 0x%08lx 0x%08lx 0x%08lx %s\n", node->tn_Node.ln_Pri, node->tn_TaskAddress, node->tn_SigAlloc, node->tn_SigWait, node->tn_Name); /* Free the memory, no need to remove the node, referenced once only */ FreeMem(node,sizeof(struct TaskNode)); node = tnode; } FreeMem(ourtasklist, sizeof(struct List)); /* Say who we are */ printf("\nTHIS TASK:\n"); task = FindTask(NULL); printf("%02d 0x%08lx 0x%08lx 0x%08lx %s\n", task->tc_Node.ln_Pri, task, task->tc_SigAlloc, task->tc_SigWait, task->tc_Node.ln_Name); } }
Task Semaphores
Semaphores can be used for the purposes of mutual exclusion. With this method of locking, all tasks agree on a locking convention before accessing shared data structures. Tasks that do not require access are not affected and will run normally, so this type of exclusion is considered preferable to forbidding and disabling. This form of exclusion is explained in more detail in the “Exec Semaphores” chapter.
Task Exceptions
Exec can provide a task with its own task-local “interrupt” called an exception. When some exceptional event occurs, an Exec exception occurs which stops a particular task from executing its normal code and forces it to execute a special, task-specific exception handling routine.
If you are familiar with the 680x0, you may be used to using the term “exceptions” in a different way. The 680x0 has its own form of exception that has nothing to do with an Exec exception. These are discussed in more detail in the “Task Traps” section of this chapter. Do not confuse Exec exceptions with 680x0 exceptions.
To set up an exception routine for a task requires setting values in the task’s control structure (the Task structure). The tc_ExceptCode field should point to the task’s exception handling routine. If this field is zero, Exec will ignore all exceptions. The tc_ExceptData field should point to any data the exception routine needs.
Exec exceptions work using signals. When a specific signal or signals occur, Exec will stop a task and execute its exception routine. Use the Exec function SetExcept() to tell Exec which of the task’s signals should trigger the exception.
When an exception occurs, Exec stops executing the tasks normal code and jumps immediately into the exception routine, no matter what the task was doing. The exception routine operates in the same context the task’s normal code; it operates in the CPU’s user mode and uses the task’s stack.
Before entering the exception routine, Exec pushes the normal task code’s context onto the stack. This includes the PC, SR, D0-D7, and A0-A6 registers. Exec then puts certain parameters in the processor registers for the exception routine to use. D0 contains a signal mask indicating which signal bit or bits caused the exception. Exec disables these signals when the task enters its exception routine. If more than one signal bit is set (i.e. if two signals occurred simultaneously), it is up to the exception routine to decide in what order to process the two different signals. A1 points to the related exception data (from tc_ExceptData), and A6 contains the Exec library base. You can think of an exception as a subtask outside of your normal task. Because task exception code executes in user mode, however, the task stack must be large enough to supply the extra space consumed during an exception.
While processing a given exception, Exec prevents that exception from occurring recursively. At exit from your exception-processing code, you should make sure D0 contains the signal mask the exception routine received in D0 because Exec looks here to see which signals it should reactivate. When the task executes the RTS instruction at the end of the exception routine, the system restores the previous contents of all of the task registers and resumes the task at the point where it was interrupted by the exception signal.
boxExceptions Are Tricky.Exceptions are difficult to use safely. An exception can interrupt a task that is executing a critical section of code within a system function, or one that has locked a system resource such as the disk or blitter (note that even simple text output uses the blitter.) This possibility makes it dangerous to use most system functions within an exception unless you are sure that your interrupted task was performing only local, non-critical operations.
Task Traps
Task traps are synchronous exceptions to the normal flow of program control. They are always generated as a direct result of an operation performed by your program’s code. Whether they are accidental or purposely generated, they will result in your program being forced into a special condition in which it must immediately handle the trap. Address error, privilege violation, zero divide, and trap instructions all result in task traps. They may be generated directly by the 68000 processor (Motorola calls them “exceptions”) or simulated by software.
A task that incurs a trap has no choice but to respond immediately. The task must have a module of code to handle the trap. Your task may be aborted if a trap occurs and no means of handling it has been provided. Default trap handling code (tc_TrapCode) is provided by the OS. You may instead choose to do your own processing of traps. The tc_TrapCode field is the address of the handler that you have designed to process the trap. The tc_TrapData field is the address of the data area for use by the trap handler.
The system’s default trap handling code generally displays a Software Error Requester or Alert containing an exception number and the program counter or task address. Processor exceptions generally have numbers in the range hex 00 to 2F. The 68000 processor exceptions of particular interest are as follows.
[h] Traps (68000 Exception Vector Numbers)
<tbody> </tbody>2 | Bus error | access of nonexistent memory |
3 | Address error | long/word access of odd address (68000) |
4 | Illegal instruction | illegal opcode (other than Axxx or Fxxx) |
5 | Zero divide | processor division by zero |
6 | CHK instruction | register bounds error trap by CHK |
7 | TRAPV instruction | overflow error trap by TRAPV |
8 | Privilege violation | user execution of supervisor opcode |
9 | Trace | status register TRACE bit trap |
10 | Line 1010 emulator | execution of opcode beginning with $A |
11 | Line 1111 emulator | execution of opcode beginning with $F |
32-47 | Trap instructions | TRAP N instruction where N = 0 to 15 |
A system alert for a processor exception may set the high bit of the longword exception number to indicate an unrecoverable error (for example $8000 0005 for an unrecoverable processor exception #5). System alerts with more complex numbers are generally Amiga-specific software failures. These are built from the definitions in the <exec/alerts.h> include file.
The actual stack frames generated for these traps are processor-dependent. The 68010, 68020, and 68030 processors will generate a different type of stack frame than the 68000. If you plan on having your program handle its own traps, you should not make assumptions about the format of the supervisor stack frame. Check the flags in the AttnFlags field of the ExecBase structure for the type of processor in use and process the stack frame accordingly.
Trap Handlers
For compatibility with the 68000, Exec performs trap handling in supervisor mode. This means that all task switching is disabled during trap handling.
At entry to the task’s trap handler, the system stack contains a processor-dependent trap frame as defined in the 68000/10/20/30 manuals. A longword exception number is added to this frame. That is, when a handler gains control, the top of stack contains the exception number and the trap frame immediately follows.
To return from trap processing, remove the exception number from the stack (note that this is the supervisor stack, not the user stack) and then perform a return from exception (RTE).
Because trap processing takes place in supervisor mode, with task dispatching disabled, it is strongly urged that you keep trap processing as short as possible or switch back to user mode from within your trap handler. If a trap handler already exists when you add your own trap handler, it is smart to propagate any traps that you do not handle down to the previous handler. This can be done by saving the previous address from tc_TrapCode and having your handler pass control to that address if the trap which occurred is not one you wish to handle.
The following example installs a simple trap handler which intercepts processor divide-by-zero traps, and passes on all other traps to the previous default trap code. The example has two code modules which are linked together. The trap handler code is in assembler. The C module installs the handler, demonstrates its effectiveness, then restores the previous tc_TrapCode.
;/* trap_c.c - Execute me to compile me with SAS C 5.10 LC -b0 -cfistq -v -y -j73 trap_c.c Blink FROM LIB:c.o,trap_c.o,trap_a.o TO trap LIBRARY LIB:LC.lib,LIB:Amiga.lib quit trap_c.c - C module of sample integer divide-by-zero trap */ #include <exec/types.h> #include <exec/tasks.h> #include <clib/exec_protos.h> #include <stdlib.h> #include <stdio.h> #ifdef LATTICE int CXBRK(void) { return(0); } /* Disable Lattice CTRL/C handling */ int chkabort(void) {return(0); } #endif extern ULONG trapa(); /* assembler trap code in trap_a.asm */ APTR oldTrapCode; ULONG countdiv0; void main(int argc, char **argv) { struct Task *thistask; ULONG k,j; thistask = FindTask(NULL); /* Save our task's current trap code pointer */ oldTrapCode = thistask->tc_TrapCode; /* Point task to our assembler trap handler code. Ours will just count */ /* divide-by-zero traps, and pass other traps on to the normal TrapCode */ thistask->tc_TrapCode = (APTR)trapa; countdiv0 = 0L; for(k=0; k<4; k++) /* Let's divide by zero a few times */ { printf("dividing %ld by zero... ",k); j = k/0L; printf("did it\n"); } printf("\nDivide by zero happened %ld times\n",countdiv0); thistask->tc_TrapCode = oldTrapCode; /* Restore old trap code */ }
* trap_a.asm - Example trap handling code (leaves D0 intact). Entered * in supervisor mode with the following on the supervisor stack: * 0(sp).l = trap# * 4(sp) Processor dependent exception frame INCLUDE "exec/types.i" INCLUDE "libraries/dos.i" XDEF _trapa XREF _countdiv0 XREF _oldTrapCode CODE _trapa: ; our trap handler entry CMPI.L #5,(SP) ; is this a divide by zero ? BNE.S notdiv0 ; no ADD.L #1,_countdiv0 ; yes, increment our div0 count endtrap: ADDQ #4,SP ; remove exception number from SSP RTE ; return from exception notdiv0: TST.L _oldTrapCode ; is there another trap handler ? BEQ.S endtrap ; no, so we'll exit MOVE.L _oldTrapCode,-(SP) ; yes, go on to old TrapCode RTS ; jumps to old TrapCode END
Trap Instructions
The TRAP instructions in the 68000 generate traps 32-47. Because many independent pieces of system code may desire to use these traps, the AllocTrap() and FreeTrap() functions are provided. These work in a fashion similar to that used by AllocSignal() and FreeSignal(), mentioned above.
Allocating a trap is simply a bookkeeping job within a task. It does not affect how the system calls the trap handler; it helps coordinate who owns what traps. Exec does nothing to determine whether or not a task is prepared to handle a particular trap. It simply calls your code. It is up to your program to handle the trap.
To allocate any trap, you can use the following code:
if (-1 == (trap = AllocTrap(-1))) printf("all trap instructions are in use\n");
Or you can select a specific trap using this code:
if (-1 == (trap = AllocTrap(3))) printf("trap #3 is in use\n");
To free a trap, you use the FreeTrap() function passing it the trap number to be freed.
Processor and Cache Control
Exec provides a number of to control the processor mode and, if available, the caches. All these functions work independently of the specific M68000 family processor type. This enables you to write code which correctly controls the state of both the MC68000 and the MC68040. Along with processor mode and cache control, functions are provided to obtain information about the condition code register (CCR) and status register (SR). No functions are provided to control a paged memory management unit (PMMU) or floating point unit (FPU).
[h] Processor and Cache Control Functions
<thead> </thead> <tbody> </tbody>Function | Description |
---|---|
GetCC() | Get processor condition codes. |
SetSR() | Get/set processor status register. |
SuperState() | Set supervisor mode with user stack. |
Supervisor() | Execute a short supervisor mode function. |
UserState() | Return to user mode with user stack. |
CacheClearE() | Flush CPU instruction and/or data caches (V37). |
CacheClearU() | Flush CPU instruction and data caches (V37). |
CacheControl() | Global cache control (V37). |
CachePostDMA() | Perform actions prior to hardware DMA (V37). |
CachePreDMA() | Perform actions after hardware DMA (V37). |
Supervisor Mode
While in supervisor mode, you have complete access to all data and registers, including those used for task scheduling and exceptions, and can execute privileged instructions. In application programs, normally only task trap code is directly executed in supervisor mode, to be compatible with the MC68000. For normal applications, it should never be necessary to switch to supervisor mode itself, only indirectly through Exec function calls. Remember that task switching is disabled while in supervisor mode. If it is absolutely needed to execute code in supervisor mode, keep it as brief as possible.
Supervisor mode can only be entered when a 680x0 exception occurs (an interrupt or trap). The Supervisor() function allows you to trap an exception to a specified assembly function. In this function your have full access to all registers. No registers are saved when your function is invoked. You are responsible for restoring the system to a sane state when you are done. You must return to user mode with an RTE instruction. You must not return to user mode by executing a privileged instruction which clears the supervisor bit in the status register. Refer to a manual on the M68000 family of CPUs for information about supervisor mode and available privileged instructions per processor type.
The MC68000 has two stacks, the user stack (USP) and supervisor stack (SSP). As of the MC68020 there are two supervisor stacks, the interrupt stack pointer (ISP) and the master stack pointer (MSP). The SuperState() function allows you to enter supervisor mode with the USP used as SSP. The function returns the SSP, which will be the MSP, if an MC68020 or greater is used. Returning to user mode is done with the UserState() function. This function takes the SSP as argument, which must be saved when SuperState() is called. Because of possible problems with stack size, Supervisor() is to be preferred over SuperState().
Status Register
The processor status register bits can be set or read with the SetSR() function. This function operates in supervisor mode, thus both the upper and lower byte of the SR can be read or set. Be very sure you know what you are doing when you use this function to set bits in the SR and above all never try to use this function to enter supervisor mode. Refer to the M68000 Programmers Reference Manual by Motorola Inc. for information about the definition of individual SR bits per processor type.
Condition Code Register
On the MC68000 a copy of the processor condition codes can be obtained with the MOVE SR,<ea> instruction. On MC68010 processors and up however, the instruction MOVE CCR,<ea> must be used. Using the specific MC68000 instruction on later processors will cause a 680x0 exception since it is a privileged instruction on those processors. The GetCC() function provides a processor independent way of obtaining a copy of the condition codes. For all processors there are 5 bits which can indicate the result of an integer or a system control instruction:
'
<tbody> </tbody>X - extend | N - negative | Z - zero | V - overflow | C - carry |
The X bit is used for multiprecision calculations. If used, it is copy of the carry bit. The other bits state the result of a processor operation.
Cache Functions
As of the MC68020 all processors have an instruction cache, 256 bytes on the MC68020 and MC68030 and 4 KBytes on a MC68040. The MC68030 and MC68040 have data caches as well, 256 bytes and 4 KBytes respectively. All the processors load instructions ahead of the program counter (PC), albeit it that the MC68000 and MC68010 only prefetch one and two words respectively. This means the CPU loads instructions ahead of the current program counter. For this reason self-modifying code is strongly discouraged. If your code modifies or decrypts itself just ahead of the program counter, the pre-fetched instructions may not match the modified instructions. If self-modifying code must be used, flushing the cache is the safest way to prevent this.
DMA Cache Functions
The CachePreDMA() and CachePostDMA() functions allow you to flush the data cache before and after Direct Memory Access. Typically only DMA device drivers benefit from this. These functions take the processor type, possible MMU and cache mode into account. When no cache is available they end up doing nothing. These functions can be replaced with ones suitable for different cache hardware. Refer to the ROM Kernel Reference Manual: Includes and Autodocs for implementation specifics.
Since DMA device drivers read and write directly to memory, they are effected by the CopyBack feature of the MC68040 (explained below). Using DMA with CopyBack mode requires a cache flush. If a DMA device needs to read RAM via DMA, it must make sure that the data in the caches has been written to memory first, by calling CachePreDMA(). In case of a write to memory, the DMA device should first clear the caches with CachePreDMA(), write the data and flush the caches again with CachePostDMA().
The 68040 and CPU Caches
The 68040 is a much more powerful CPU than its predecessors. It has 4K of cache memory for instructions and another 4K cache for data. The reason for these two separate caches is so that the CPU core can access data and CPU instructions at the same time.
Although the 68040 provides greater performance it also brings with it greater compatibility problems. Just the fact that the caches are so much larger than Motorola’s 68030 CPU can cause problems. However, this is not its biggest obstacle.
The 68040 data cache has a mode that can make the system run much faster in most cases. It is called CopyBack mode. When a program writes data to memory in this mode, the data goes into the cache but not into the physical RAM. That means that if a program or a piece of hardware were to read that RAM without going through the data cache on the 68040, it will read old data. CopyBack mode effects two areas of the Amiga: DMA devices and the CPU’s instruction reading.
CopyBack mode effects DMA devices because they read and write data directly to memory. Using DMA with CopyBack mode requires a cache flush. If a DMA device needs to read RAM via DMA, it must first make sure that data in the caches has been written to memory. It can do this by calling the Exec function CachePreDMA(). If a DMA device is about to write to memory, it should call CachePreDMA() before the write, do the DMA write, and then call CachePostDMA(), which makes sure that the CPU uses the data just written to memory.
An added advantage of using the CachePreDMA() and CachePostDMA() functions is that they give the OS the chance to tell the DMA device that the physical addresses and memory sizes are not the same. This will make it possible in the future to add features such as virtual memory. See the Autodocs for more information on these calls.
The other major compatibility problem with the 68040’s CopyBack mode is with fetching CPU instructions. CPU instructions have to be loaded into memory so the CPU can copy them into its instruction cache. Normally, instructions that will be executed are written to memory by the CPU (i.e., loading a program from disk). In CopyBack mode, anything the CPU writes to memory, including CPU instructions, doesn’t actually go into memory, it goes into the data cache. If instructions are not flushed out of the data cache to RAM, the 68040 will not be able to find them when it tries to copy them into the instruction cache for execution. It will instead find and attempt to execute whatever garbage data happened to be left at that location in RAM.
To remedy this, any program that writes instructions to memory must flush the data cache after writing. The V37 Exec function CacheClearU() takes care of this. Release 2 of the Amiga OS correctly flushes the caches as needed after it does the LoadSeg() of a program (LoadSeg() loads Amiga executable programs into memory from disk). Applications need to do the same if they write code to memory. It can do that by calling CacheClearU() before the call to CreateProc(). In C that would be:
extern struct ExecBase *SysBase; . . . /* If we are in 2.0, call CacheClearU() before CreateProc() */ if (SysBase->LibNode.lib_Version >= 37) CacheClearU(); /* Now do the CreateProc() call... */ proc=CreateProc(... /* whatever your call is like */ ...); . . .
For those of you programming in assembly language:
******************************************************************************* * Check to see if we are running in V37 ROM or better. If so, we want to call * CacheClearU() to make sure we are safe on future hardware such as the 68040. * This section of code assumes that a6 points at ExecBase. a0/a1/d0/d1 are * trashed in CacheClearU() * cmpi.w #37,LIB_VERSION(a6) ; Check if exec is >= V37 bcs.s TooOld ; If less than V37, too old... jsr _LVOCacheClearU(a6) ; Clear the cache... TooOld: ; Exit gracefully. *******************************************************************************
Note that CreateProc() is not the only routine where CopyBack mode could be a problem. Any program code copied into memory for execution that is not done via LoadSeg() will need to call CacheClearU(). Many input device handlers have been known to allocate and copy the handler code into memory and then exit back to the system. These programs also need to have this call in them. The above code will work under older versions of the OS, and will do the correct operations in Release 2 (and beyond).
Function Reference
The following chart gives a brief description of the Exec functions that control tasks. See the Amiga ROM Kernel Reference Manual: Includes and Autodocs for details about each call.
[h] Exec Task, Processor and Cache Control Functions
<thead> </thead> <tbody> </tbody>Exec Task Function | Description |
---|---|
AddTask() | Add a task to the system. |
AllocTrap() | Allocate a processor trap vector. |
Disable() | Disable interrupt processing. |
Enable() | Enable interrupt processing. |
FindTask() | Find a specific task. |
Forbid() | Forbid task rescheduling. |
FreeTrap() | Release a process trap. |
Permit() | Permit task rescheduling. |
SetTaskPri() | Set the priority of a task. |
RemTask() | Remove a task from the system. |
CacheClearE() | Flush CPU instruction and/or data caches (V37). |
CacheClearU() | Flush CPU instruction and data caches (V37). |
CacheControl() | Global cache control (V37). |
CachePostDMA() | Perform actions prior to hardware DMA (V37). |
CachePreDMA() | Perform actions after hardware DMA (V37). |
GetCC() | Get processor condition codes. |
SetSR() | Get/set processor status register. |
SuperState() | Set supervisor mode with user stack. |
Supervisor() | Execute a short supervisor mode function. |
UserState() | Return to user mode with user stack. |
CreateTask() | amiga.lib function to setup and add a new task. |
DeleteTask() | amiga.lib function to delete a task created with CreateTask(). |