Multitasking


One of the most important jobs of the kernel is managing the tasks that run on a 3DO system. Because Portfolio supports preemptive multitasking, the kernel must provide conventions for deciding which tasks get CPU time, and for saving a task's state when execution switches from one task to another.

Privileged and Non-Privileged Tasks

The kernel makes an important distinction between tasks running on a 3DO system, and divides them into two categories: privileged tasks and non-privileged tasks. Privileged tasks have special rights that non-privileged tasks do not have. Tasks that you create are always non-privileged. Only special tasks created by a 3DO system can be privileged.

Multitasking

The kernel supports preemptive context switching for multitasking. It normally devotes one time quantum (normally 15 milliseconds) of CPU time to a task and then switches execution to another task, where it devotes another time quantum before switching to yet another task. To make the switch without destroying the current state of the executing task, the kernel saves the context of the current task in the task's TCB (task control block, a data structure that contains the parameters of each task), and reads the context of the next task from its control block before executing that task. (A task's context includes states such as its allocated address spaces and its register set.)

At any point during a time quantum, the kernel can preempt the current task, and immediately switch execution to a more important task if necessary. To determine how and when to switch from one task to another, the kernel reads task states and priorities.

Task States

A task can be in one of three states:

The kernel executes tasks in the ready queue only, switching from task to task as required. It does not execute tasks in the waiting queue, so waiting tasks don't require any CPU cycles-a courtesy to the other tasks running on the system.

To determine how the ready-to-run tasks are executed, the kernel considers each task's priority.

Task Priorities

Each task has a priority that is associated with it. The priority is a value from 10 to 199: 10 is the lowest priority, 199 is the highest priority. This priority can be changed at any time to give the task a higher or lower priority than others in the ready queue-or to give the task an equal priority with other tasks.

Priority determines the order in which tasks in the ready queue are executed. The kernel executes only the highest-priority task (or tasks) in the ready queue and doesn't devote any CPU time to lower-priority tasks. If several tasks all share the same highest priority, the kernel rotates among those tasks, devoting one time quantum to each. Lower-priority tasks in the ready queue don't receive any CPU time at all until there are no higher-priority tasks in the ready queue: the higher-priority tasks either finish execution and exit the system, move to the wait queue, or change to a lower priority.

Whenever a new task with a higher priority than the one running enters the ready queue, or whenever an existing task is given a higher priority than the one running, the kernel preempts the running task. It immediately switches execution to the higher-priority task, even if the switch occurs in the middle of a time quantum. The higher-priority task then starts at the beginning of its own time quantum.

Note that round-robin scheduling takes place only when tasks of equal priority have the highest priority in the ready queue. If only one task has the highest priority, only that task runs, and all others languish until it finishes or is kicked out of the CPU limelight by another task with a higher priority. Note also that if a task executes a Yield() call, it forgoes the rest of its quantum, and yields the CPU immediately to other tasks of equal priority.

Waiting Tasks

To get into the wait queue, a task must execute a wait function call (such as WaitSignal(), WaitIO(), or WaitPort()) to define what it's waiting for. It then becomes a waiting task and receives no CPU time. When its wait conditions are satisfied, a task moves to the ready queue, where it can compete for CPU time.

The wait queue is an important feature for keeping running tasks working at top speed without, wasting CPU cycles on tasks waiting for external events. For the wait queue to work, each task must not use a loop that constantly checks for an event. Repeatedly checking for an event, known as "busy-waiting," is greatly scorned in the Portfolio world-it eats up unnecessary CPU cycles and makes your task unpopular with other developers and users around the world.

Task Termination

As each task runs, it accumulates its own memory and set of resources. The kernel keeps track of the memory and resources allocated to each task and when that task quits or dies, the kernel automatically closes those resources and returns the memory to the free memory pool. This means that the task doesn't have to close resources and free memory on its own before it quits, a convenient feature. Good programming practice, however, is to close and release resources and free memory within a task whenever they are no longer in use. If a thread exits, it must free its memory so that the parent task can allocate it for its own use.

Parent Tasks, Child Tasks, and Threads

Any task can launch another task. The launched task then becomes the child of the launching task and the child task is a resource of the parent task. This means that when the parent task quits, all of its children quit too.

To sever the parent/child relationship between two tasks so that the child task doesn't quit with the parent task, the parent can use the SetItemOwner() function call to transfer ownership of the child task to the child task itself. When the parent task quits, the child task continues to run.

A parent task spawns child tasks to take care of real-time processing and other operations. A child task has one big disadvantage, it doesn't share memory with the parent. Because it must allocate its own memory in pages, it can waste memory if its memory requirements are small. And because parent and child don't share memory, they can't share values stored in shared data structures. To overcome these disadvantages, a parent task can spawn a thread.

A thread is a child task that shares the parent task's memory. The owner of the thread can transfer ownership of a thread to any thread in the parent's task family except to the thread itself. A task family is a task and all of its threads.