Copyright (c) Hyperion Entertainment and contributors.

Introduction to Exec

From AmigaOS Documentation Wiki
Jump to navigation Jump to search

Introduction to Exec

The Multitasking Executive, better known as Exec, is the heart of the Amiga's operating system. All other systems in the Amiga rely on it to control multitasking, to manage the message-based interprocess communications system, and to arbitrate access to system resources. Because just about every software entity on the Amiga (including application programs) needs to use Exec in some way, every Amiga programmer has to have a basic understanding of its fundamentals.

Exec was originally written in 68K assembly code. The migration to PowerPC-based systems necessitated a rewrite of Exec while retaining as much of the original API as possible. The PowerPC reimplementation of Exec is named Exec Second Generation or ExecSG for short. For simplicity, throughout this wiki ExecSG will be referred to as Exec.

Multitasking

A conventional micro-computer spends a lot of its time waiting for things to happen. It has to wait for such things as the user to push buttons on the keyboard or mouse, for data to come in through the serial port, and for data to go out to a disk drive. To make efficient use of the CPU's time, an operating system can have the CPU carry out some other task while it is waiting for such events to occur.

A multitasking operating system reduces the amount of time it wastes, by switching to another program when the current one needs to wait for an event. A multitasking operating system can have several programs, or tasks, running at the same time. Each task runs independently of the others, without having to worry about what the other tasks are doing. From a task's point of view, it's as if each task has a computer all to itself.

The Amiga's multitasking works by switching which task is currently using the CPU. A task can be a user's application program, or it can be a task which controls system resources (like the disk drives or the keyboard). Each task has a priority assigned to it. Exec will let the task with the highest priority use the CPU, but only if the task is ready to run. A task can be in one of three states: ready, sleeping, or running.

A ready task is not currently using the CPU but is waiting to use the processor. Exec keeps a list of the tasks that are ready. Exec sorts this list according to task priority, so Exec can easily find the ready task with the highest priority. When Exec switches the task that currently has control of the CPU, it switches to the task at the top of this list.

A sleeping task is not currently running and is waiting for some event to happen. When that event occurs, Exec will move the sleeping task into the list of ready tasks.

A running task is currently using the CPU. It will remain the current task until one of three things occur:

  • A higher priority task becomes ready, so the OS preempts the current task and switches to the higher priority task.

  • The currently running task needs to wait for an event, so it goes to sleep and Exec switches to the highest priority task in Exec's ready list.

  • The currently running task has had control of the CPU for at least a preset time period called a quantum and there is another task of equal priority ready to run. In this case, Exec will preempt the current task for the ready one with the same priority. This is known as time-slicing. When there is a group of tasks of equal priority on the top of the ready list, Exec will cycle through them, letting each one use the CPU for a quantum (a slice of time).

The terms "task" and "process" are often used interchangeably to represent the generic concept of task. On the Amiga, this terminology can be a little confusing because of the names of the data structures that are associated with Exec tasks. Each task has a structure associated with it called a Task structure (defined in <exec/tasks.h>). Most application tasks use a superset of the Task structure called a Process structure (defined in <dos/dosextens.h>). These terms are confusing to Amiga programmers because there is an important distinction between the Exec task with only a Task structure and an Exec task with a Process structure.

The Process structure builds on the Task structure and contains some extra fields which allow the DOS library to associate an AmigaDOS environment to the task. Some elements of a DOS environment include a current input and output stream and a current working directory. These elements are important to applications that need to do standard input and output using functions like printf().

Exec only pays attention to the Task structure portion of the Process structure, so, as far as Exec is concerned, there is no difference between a task with a Task structure and a task with a Process structure. Exec considers both of them to be tasks.

An application doesn't normally worry about which structure their task uses. Instead, the system that launches the application takes care of it. Both Workbench and the Shell (CLI) attach a Process structure to the application tasks that they launch.

Dynamic Memory Allocation

The Amiga has a soft machine architecture, meaning that all tasks, including those that are part of its operating system, do not use fixed memory addresses. As a result, any program that needs to use a chunk of memory must allocate that memory from the operating system.

There is one function on the Amiga for simple memory allocation: AllocVecTags(). This function accept the a uint32 containing the size of the memory block in bytes followed by a TagItem array for memory attributes. The function returns the address of a memory block aligned to your specifications if successful or NULL if something went wrong.

If an application does not specify any tags when allocating memory, the system looks for MEMF_PRIVATE memory. There are additional memory allocation tags: MEMF_SHARED and MEMF_EXECUTABLE. See the Exec Memory Allocation section for additional information on attributes and tags.

Make Sure You Have Memory.
Always check the result of any memory allocation to be sure the type and amount of memory requested is available. Failure to do so will lead to trying to use an non-valid pointer.

FreeVec() releases memory allocated by AllocVecTags(). It takes only one parameter, a pointer to a memory block allocated by AllocVecTags(). The following example shows how to allocate and deallocate memory.

APTR my_mem = IExec->AllocVecTags(100, TAG_END);
 
if (my_mem != NULL)
{
    /* Your code goes here */
    IExec->FreeVec(my_mem);
}
else  { /* couldn't get memory, exit with an error */ }

Signals

The Amiga uses a mechanism called signals to tell a task that some event occurred. Each task has its own set of 32 signals, 16 of which are set aside for system use. When one task signals a second task, it asks the OS to set a specific bit in the 32-bit long word set aside for the second task's signals.

Signals are what makes it possible for a task to go to sleep. When a task goes to sleep, it asks the OS to wake it up when a specific signal bit gets set. That bit is tied to some event. When that event occurs, that signal bit gets set. This triggers the OS into waking up the sleeping task.

To go to sleep, a task calls a system function called Wait(). This function takes one argument, a bitmask that tells Exec which of the task's signal bits to "listen to". The task will only wake up if it receives one of the signals whose corresponding bit is set in that bitmask. For example, if a task wanted wait for signals 17 and 19, it would call Wait() like this:

mysignals = IExec->Wait(1U << 17 | 1U << 19);

Wait() puts the task to sleep and will not return until some other task sets at least one of these two signals. When the task wakes up, mysignals will contain the bitmask of the signal or signals that woke up the task. It is possible for several signals to occur simultaneously, so any combination of the signals that the task Wait()ed on can occur. It is up to the waking task to use the return value from Wait() to figure out which signal or signals occurred.

More information on signals can be found in Exec Signals.

Interprocess Communications

Another feature of the Amiga OS is its system of message-based interprocess communication. Using this system, a task can send a message to a message port owned by another task. Tasks use this mechanism to do things like trigger events or share data with other tasks, including system tasks. Exec's message system is built on top of Exec's task signaling mechanism. Most Amiga applications programming (especially Intuition programming) relies heavily upon this message-based form of interprocess communication.

When one task sends a message to another task's message port, the OS adds the message to the port's message queue. The message stays in this queue until the task that owns the port is ready to check its port for messages. Typically, a task has put itself to sleep while it is waiting for an event, like a message to arrive at its message port. When the message arrives, the task wakes up to look in its message port. The messages in the message port's queue are arranged in first-in-first-out (FIFO) order so that, when a task receives several messages, it will see the messages in the order they arrived at the port.

A task can use a message to share any kind of data with another task. This is possible because a task does not actually transmit an entire message, it only passes a pointer to a message. When a task creates a message (which can have many Kilobytes of data attached to it) and sends it to another task, the actual message does not move.

Essentially, when task A sends a message to task B, task A lends task B a chunk of its memory, the memory occupied by the message. After task A sends the message, it has relinquished that memory to task B, so it cannot touch the memory occupied by the message. Task B has control of that memory until task B returns the message to task A with the ReplyMsg() function.

Let's look at an example. Many applications use Intuition windows as a source for user input. Without getting into too much detail about Intuition, when an application opens a window, it can set up the window so Intuition will send messages about certain user input events. Intuition sends these messages to a message port created by Intuition for use with this window. When an application successfully opens a window, it receives a pointer to a Window structure, which contains a pointer to this message port (Window.UserPort). For this example, we'll assume the window has been set up so Intuition will send a message only if the user clicks the window's close gadget.

When Intuition opens the window in this example, it creates a message port for the task that opened the Window. Because the most common message passing system uses signals, creating this message port involves using one of the example task's 32 signals. The OS uses this signal to signal the task when it receives a message at this message port. This allows the task to sleep while waiting for a "Close Window" event to arrive. Since this simple example is only waiting for activity at one message port, it can use the WaitPort() function. WaitPort() accepts a pointer to a message port and puts a task to sleep until a message arrives at that port.

This simple example needs two variables, one to hold the address of the window and the other to hold the address of a message.

struct Message *mymsg; /*defined in <exec/ports.h> */
struct Window *mywin;  /* defined in <intuition/intuition.h> */
    ...
 
/* at some point, this application would have successfully opened a */
/* window and stored a pointer to it in mywin.                      */
    ...
 
    /* Here the application goes to sleep until the user clicks the window's close    */
    /* gadget.  This window was set up so that the only time Intuition will send a    */
    /* message to this window's port is if the user clicks the window's close gadget. */
 
    IExec->WaitPort(mywin->UserPort);
    while (mymsg = IExec->GetMsg(mywin->UserPort))
    {
        /* process message now or copy information from message */
        IExec->ReplyMsg(mymsg);
    }
    ...
 
/* Close window, clean up */

The Exec function GetMsg() is used to extract messages from the message port. Since the memory for these messages belongs to Intuition, the example relinquishes each message as it finishes with them by calling ReplyMsg(). Notice that the example keeps on trying to get messages from the message port until mymsg is NULL. This is to make sure there are no messages left in the message port's message queue. It is possible for several messages to pile up in the message queue while the task is asleep, so the example has to make sure it replies to all of them. Note that the window should never be closed within this GetMsg() loop because the while statement is still accessing the window's UserPort.

Note that each task with a Process structure (sometimes referred to as a process) has a special process message port, Process.pr_MsgPort. This message port is only for use by Workbench and the DOS library itself. No application should use this port for its own use!

More detailed information about message ports can be found in Exec Messages and Ports.

Waiting on Message Ports and Signals at the Same Time

Most applications need to wait for a variety of messages and signals from a variety of sources. For example, an application might be waiting for Window events and also timer.device messages. In this case, an application must Wait() on the combined signal bits of all signals it is interested in, including the signals for the message ports where any messages might arrive.

The MsgPort structure, which is defined in <exec/ports.h>, is what Exec uses to keep track of a message port. The UserPort field from the example above points to one of these structures. In this structure is a field called mp_SigBit, which contains the number (not the actual bit mask) of the message port's signal bit. To Wait() on the signal of a message port, Wait() on a bit mask created by shifting 1U to the left mp_SigBit times (1U << msgport->mp_SigBit). The resulting bit mask can be OR'd with the bit masks for any other signals you wish to simultaneously wait on.

Libraries and Devices

One of the design goals for the Amiga OS was to make the system dynamic enough so that the OS could be extended and updated without effecting existing applications. Another design goal was to make it easy for different applications to be able to share common pieces of code. The Amiga accomplished these goals through a system of libraries. An Amiga library consists of a collection of related functions which can be anywhere in system memory (RAM or ROM).

Devices are very similar to libraries, except they usually control some sort of I/O device and contain some extra standard functions. Although this section does not really discuss devices directly, the material here applies to them. For more information on devices, see the Exec Device I/O section of this manual or the Devices.

An application accesses a library's functions through interfaces. Each library exports one or more interfaces. Before a task can use the functions of a particular interface, it must first acquire the library's base pointer by calling the Exec OpenLibrary() function and then obtain an interface pointer by calling GetInterface().

struct Library *OpenLibrary( CONST_STRPTR libName, uint32 libVer );
struct Interface *GetInterface( struct Library *base, CONST_STRPTR ifaceName, uint32 ifaceVer, struct TagItem *tags);

Where libName is a string naming the library and libVer is a version number for the library. The library base pointer is passed on to GetInterface() along with ifaceName which is the name of the interface and ifaceVer which is the version. An optional tag array completes the call.

The libVer number reflects a revision of the system software. The AmigaOS versions chart lists the specific AmigaOS release versions that system libraries versions correspond to.

The OpenLibrary() function looks for a library with a name that matches libName and also with a version at least as high as libVersion. For example, to open version 50 or greater of the Intuition library:

IntuitionBase = IExec->OpenLibrary("intuition.library", 50);

In this example, if version 50 or greater of the Intuition library is not available, OpenLibrary() returns NULL. A version of zero in OpenLibrary() tells the OS to open any version of the library. New code should use a version of 50 or higher unless noted otherwise.

Note
When OpenLibrary() looks for a library, it first looks in memory. If the library is not in memory, OpenLibrary() will look for the library on disk. If libName is a library name with an absolute path (for example, "myapp:mylibs/mylib.library"), OpenLibrary() will follow that absolute path looking for the library. If libName is only a library name ("diskfont.library"), OpenLibrary() will look for the library in the directory that the LIBS: logical assign currently references.

If OpenLibrary() finds the library on disk, it takes care of loading it and initializing it. As part of the initialization process, OpenLibrary() dynamically creates a vector table. There is a vector for each function in the library. The OS needs to create the vector table dynamically because the library functions can be anywhere in memory.

After the library is loaded into memory and initialized, OpenLibrary() can actually "open" the library. It does this by calling the library's Open function vector. Every library has a standard vector set aside for an OPEN function so the library can set up any data or processes that it needs. Normally, a library's OPEN function increments its open count to keep track of how many tasks currently have the library opened.

If any step of OpenLibrary() fails, it returns a NULL value. If OpenLibrary() is successful, it returns the address of the library base. The library base is the address of this library's Library structure (defined in "exec/libraries.h"). The Library structure immediately follows the vector table in memory.

Once the library has been opened the application needs to obtain access to the interfaces with GetInterface() in order to call the functions available. Every task or process is required to call GetInterface() on the instance and should not share the interface pointer.

After an application is finished with a library, it must close it by calling DropInterface() on each interface and finally CloseLibrary():

VOID DropInterface(struct Interface *ifacePtr);
VOID CloseLibrary(struct Library *libPtr);

Where ifacePtr is a pointer to an interface and libPtr is a pointer to the library base returned when the library was opened with OpenLibrary().

For more detailed information about libraries and interfaces see Exec Libraries.

Calling a Library Function

To call a function in an Amiga system library a pointer to an interface is required. For example to call the Intuition function DisplayBeep():

struct IntuitionIFace *IIntuition = IExec->GetInterface(base, "main", 1, NULL);
 
IIntuition->DisplayBeep();

Function parameters in C are pushed on the stack when a program calls a function. The order in which parameters are pushed is defined by the SysV ABI specification.

For more detailed information about calling library functions see Exec Libraries.