2

Basic OS



2.1    Introduction

Modern real-time systems are based on the complementary concepts of multitasking and intertask communications. A multitasking environment allows a real-time application to be constructed as a set of independent tasks, each with its own thread of execution and set of system resources. The intertask communication facilities allow these tasks to synchronize and communicate in order to coordinate their activity. In VxWorks, the intertask communication facilities range from fast semaphores to message queues and from pipes to network-transparent sockets.

Another key facility in real-time systems is hardware interrupt handling, because interrupts are the usual mechanism to inform a system of external events. To get the fastest possible response to interrupts, interrupt service routines (ISRs) in VxWorks run in a special context of their own, outside any task's context.

This chapter discusses the tasking facilities, intertask communication, and the interrupt handling facilities that are at the heart of the VxWorks run-time environment. You can also use POSIX real-time extensions with VxWorks. For more information, see 3. POSIX Standard Interfaces.



2.2    VxWorks Tasks

It is often essential to organize applications into independent, though cooperating, programs. Each of these programs, while executing, is called a task. In VxWorks, tasks have immediate, shared access to most system resources, while also maintaining enough separate context to maintain individual threads of control.


*      
NOTE: The POSIX standard includes the concept of a thread, which is similar to a task, but with some additional features. For details, see 3.4 POSIX Threads.

2.2.1   Multitasking

Multitasking provides the fundamental mechanism for an application to control and react to multiple, discrete real-world events. The VxWorks real-time kernel, wind, provides the basic multitasking environment. Multitasking creates the appearance of many threads of execution running concurrently when, in fact, the kernel interleaves their execution on the basis of a scheduling algorithm. Each task has its own context, which is the CPU environment and system resources that the task sees each time it is scheduled to run by the kernel. On a context switch, a task's context is saved in the task control block (TCB).

A task's context includes:

  • a thread of execution; that is, the task's program counter
  • the CPU registers and (optionally) floating-point registers
  • a stack for dynamic variables and function calls
  • I/O assignments for standard input, output, and error
  • a delay timer
  • a time-slice timer
  • kernel control structures
  • signal handlers
  • debugging and performance monitoring values

In VxWorks, one important resource that is not part of a task's context is memory address space: all code executes in a single common address space. Giving each task its own memory space requires virtual-to-physical memory mapping, which is available only with the optional product VxVMI; for more information, see 12. Virtual Memory Interface.

2.2.2   Task State Transition

The kernel maintains the current state of each task in the system. A task changes from one state to another as a result of kernel function calls made by the application. When created, tasks enter the suspended state. Activation is necessary for a created task to enter the ready state. The activation phase is extremely fast, enabling applications to pre-create tasks and activate them in a timely manner. An alternative is the spawning primitive, which allows a task to be created and activated with a single function. Tasks can be deleted from any state.

Table 2-1:   Task State Symbols


State Symbol
Description

READY
The state of a task that is not waiting for any resource other than the CPU.
PEND
The state of a task that is blocked due to the unavailability of some resource.
DELAY
The state of a task that is asleep for some duration.
SUSPEND
The state of a task that is unavailable for execution. This state is used primarily for debugging. Suspension does not inhibit state transition, only task execution. Thus, pended-suspended tasks can still unblock and delayed-suspended tasks can still awaken.
DELAY + S
The state of a task that is both delayed and suspended.
PEND + S
The state of a task that is both pended and suspended.
PEND + T
The state of a task that is pended with a timeout value.
PEND + S + T
The state of a task that is both pended with a timeout value and suspended.
state + I
The state of task specified by state, plus an inherited priority.

Table 2-1 describes the state symbols that you see when working with Tornado development tools. Figure 2-1 shows the corresponding state diagram of the wind kernel states.

Figure 2-1:   Task State Transitions

2.2.3   Wind Task Scheduling

Multitasking requires a scheduling algorithm to allocate the CPU to ready tasks. The default algorithm in wind is priority-based preemptive scheduling. You can also select to use round-robin scheduling for your applications. Both algorithms rely on the task's priority. The wind kernel has 256 priority levels, numbered 0 through 255. Priority 0 is the highest and priority 255 is the lowest. Tasks are assigned a priority when created. You can also change a task's priority level while it is executing by calling taskPrioritySet( ). The ability to change task priorities dynamically allows applications to track precedence changes in the real world.

The routines that control task scheduling are listed in Table 2-2. .

Table 2-2:   Task Scheduler Control Routines


Call
Description

kernelTimeSlice( )
Controls round-robin scheduling.
taskPrioritySet( )
Changes the priority of a task.
taskLock( )
Disables task rescheduling.
taskUnlock( )
Enables task rescheduling.

POSIX also provides a scheduling interface. For more information, see 3.5 POSIX Scheduling Interface.

Preemptive Priority Scheduling

A preemptive priority-based scheduler preempts the CPU when a task has a higher priority than the current task running. Thus, the kernel ensures that the CPU is always allocated to the highest priority task that is ready to run. This means that if a task- with a higher priority than that of the current task- becomes ready to run, the kernel immediately saves the current task's context, and switches to the context of the higher priority task. For example, in Figure 2-2, task t1 is preempted by higher-priority task t2, which in turn is preempted by t3. When t3 completes, t2 continues executing. When t2 completes execution, t1 continues executing.

Figure 2-2:   Priority Preemption

The disadvantage of this scheduling algorithm is that, when multiple tasks of equal priority must share the processor, if a single task is never blocked, it can usurp the processor. Thus, other equal-priority tasks are never given a chance to run. Round-robin scheduling solves this problem.

Round-Robin Scheduling

A round-robin scheduling algorithm attempts to share the CPU fairly among all ready tasks of the same priority. Round-robin scheduling uses time slicing to achieve fair allocation of the CPU to all tasks with the same priority. Each task, in a group of tasks with the same priority, executes for a defined interval or time slice.

Round-robin scheduling is enabled by calling kernelTimeSlice( ), which takes a parameter for a time slice, or interval. This interval is the amount of time each task is allowed to run before relinquishing the processor to another equal-priority task. Thus, the tasks rotate, each executing for an equal interval of time. No task gets a second slice of time before all other tasks in the priority group have been allowed to run.

In most systems, it is not necessary to enable round-robin scheduling, the exception being when multiple copies of the same code are to be run, such as in a user interface task.

If round-robin scheduling is enabled, and preemption is enabled for the executing task, the system tick handler increments the task's time-slice count. When the specified time-slice interval is completed, the system tick handler clears the counter and the task is placed at the tail of the list of tasks at its priority level. New tasks joining a given priority group are placed at the tail of the group with their run-time counter initialized to zero.

Enabling round-robin scheduling does not affect the performance of task context switches, nor is additional memory allocated.

If a task blocks or is preempted by a higher priority task during its interval, its time-slice count is saved and then restored when the task becomes eligible for execution. In the case of preemption, the task will resume execution once the higher priority task completes, assuming that no other task of a higher priority is ready to run. In the case where the task blocks, it is placed at the tail of the list of tasks at its priority level. If preemption is disabled during round-robin scheduling, the time-slice count of the executing task is not incremented.

Time-slice counts are accrued by the task that is executing when a system tick occurs, regardless of whether or not the task has executed for the entire tick interval. Due to preemption by higher priority tasks or ISRs stealing CPU time from the task, it is possible for a task to effectively execute for either more or less total CPU time than its allotted time slice.

Figure 2-3 shows round-robin scheduling for three tasks of the same priority: t1, t2, and t3. Task t2 is preempted by a higher priority task t4 but resumes at the count where it left off when t4 is finished.

Figure 2-3:   Round-Robin Scheduling

Preemption Locks

The wind scheduler can be explicitly disabled and enabled on a per-task basis with the routines taskLock( ) and taskUnlock( ). When a task disables the scheduler by calling taskLock( ), no priority-based preemption can take place while that task is running.

However, if the task explicitly blocks or suspends, the scheduler selects the next highest-priority eligible task to execute. When the preemption-locked task unblocks and begins running again, preemption is again disabled.

Note that preemption locks prevent task context switching, but do not lock out interrupt handling.

Preemption locks can be used to achieve mutual exclusion; however, keep the duration of preemption locking to a minimum. For more information, see 2.3.2 Mutual Exclusion.

A Comparison of taskLock( ) and intLock( )

When using taskLock( ), consider that it will not achieve mutual exclusion. Generally, if interrupted by hardware, the system will eventually return to your task. However, if you block, you lose task lockout. Thus, before you return from the routine, taskUnlock( ) should be called.

When a task is accessing a variable or data structure that is also accessed by an ISR, you can use intLock( ) to achieve mutual exclusion. Using intLock( ) makes the operation "atomic" in a single processor environment. It is best if the operation is kept minimal, meaning a few lines of code and no function calls. If the call is too long, it can directly impact interrupt latency and cause the system to become far less deterministic.

Driver Support Task Priority

All application tasks should be priority 100 - 250. However, driver "support" tasks (tasks associated with an ISR) can be in the range of 51-99. These tasks are crucial; for example, if a support task fails while copying data from a chip, the device loses that data.1 The system netTask( ) is at priority 50, so user tasks should not be assigned priorities below that task; if they are, the network connection could die and prevent debugging capabilities with Tornado.

2.2.4   Task Control

The following sections give an overview of the basic VxWorks task routines, which are found in the VxWorks library taskLib. These routines provide the means for task creation and control, as well as for retrieving information about tasks. See the VxWorks API Reference entry for taskLib for further information.

For interactive use, you can control VxWorks tasks from the host or target shell; see the Tornado User's Guide: Shell and 6. Target Tools in this manual.

Task Creation and Activation

The routines listed in Table 2-3 are used to create tasks.

The arguments to taskSpawn( ) are the new task's name (an ASCII string), the task's priority, an "options" word, the stack size, the main routine address, and 10 arguments to be passed to the main routine as startup parameters:

id = taskSpawn ( namepriorityoptionsstacksizemainarg1arg10 );

The taskSpawn( ) routine creates the new task context, which includes allocating the stack and setting up the task environment to call the main routine (an ordinary subroutine) with the specified arguments. The new task begins execution at the entry to the specified routine.

Table 2-3:   Task Creation Routines


Call
Description

taskSpawn( )
Spawns (creates and activates) a new task.
taskInit( )
Initializes a new task.
taskActivate( )
Activates an initialized task.

The taskSpawn( ) routine embodies the lower-level steps of allocation, initialization, and activation. The initialization and activation functions are provided by the routines taskInit( ) and taskActivate( ); however, we recommend you use these routines only when you need greater control over allocation or activation.

Task Stack

It is hard to know exactly how much stack space to allocate, without reverse-engineering the system configuration. To help avoid a stack overflow, and task stack corruption, you can take the following approach. When initially allocating the stack, make it much larger than anticipated; for example, from 20KB to up to 100KB, depending upon the type of application. Then, periodically monitor the stack with checkStack( ), and if it is safe to make them smaller, modify the size.

Task Names and IDs

When a task is spawned, you can specify an ASCII string of any length to be the task name. VxWorks returns a task ID, which is a 4-byte handle to the task's data structures. Most VxWorks task routines take a task ID as the argument specifying a task. VxWorks uses a convention that a task ID of 0 (zero) always implies the calling task.

VxWorks does not require that task names be unique, but it is recommended that unique names be used in order to avoid confusing the user. Furthermore, to use the Tornado development tools to their best advantage, task names should not conflict with globally visible routine or variable names. To avoid name conflicts, VxWorks uses a convention of prefixing all task names started from the target with the character t and task names started from the host with the character u.

You may not want to name some or all of your application's tasks. If a NULL pointer is supplied for the name argument of taskSpawn( ), then VxWorks assigns a unique name. The name is of the form tN, where N is a decimal integer that is incremented by one for each unnamed task that is spawned.

The taskLib routines listed in Table 2-4 manage task IDs and names.

Table 2-4:   Task Name and ID Routines


Call
Description

taskName( )
Gets the task name associated with a task ID.
taskNameToId( )
Looks up the task ID associated with a task name.
taskIdSelf( )
Gets the calling task's ID.
taskIdVerify( )
Verifies the existence of a specified task.

Task Options

When a task is spawned, you can pass in one or more option parameters, which are listed in Table 2-5. The result is determined by performing a logical OR operation on the specified options.

Table 2-5:   Task Options


Name
Hex Value
Description

VX_FP_TASK
0x0008
Executes with the floating-point coprocessor.
VX_NO_STACK_FILL
0x0100
Does not fill the stack with 0xee.
VX_PRIVATE_ENV
0x0080
Executes a task with a private environment.
VX_UNBREAKABLE
0x0002
Disables breakpoints for the task.
VX_DSP_TASK
0x0200
1 = DSP coprocessor support.
VX_ALTIVEC_TASK
0x0400
1 = ALTIVEC coprocessor support.

You must include the VX_FP_TASK option when creating a task that:

  • Performs floating-point operations.

  • Calls any function that returns a floating-point value.

  • Calls any function that takes a floating-point value as an argument.

For example:

tid = taskSpawn ("tMyTask", 90, VX_FP_TASK, 20000, myFunc, 2387, 0, 0, 
                0, 0, 0, 0, 0, 0, 0);

Some routines perform floating-point operations internally. The VxWorks documentation for each of these routines clearly states the need to use the VX_FP_TASK option.

After a task is spawned, you can examine or alter task options by using the routines listed in Table 2-6. Currently, only the VX_UNBREAKABLE option can be altered.

Table 2-6:   Task Option Routines


Call
Description

taskOptionsGet( )
Examines task options.
taskOptionsSet( )
Sets task options.

Task Information

The routines listed in Table 2-7 get information about a task by taking a snapshot of a task's context when the routine is called. Because the task state is dynamic, the information may not be current unless the task is known to be dormant (that is, suspended).

Table 2-7:   Task Information Routines


Call
Description

taskIdListGet( )
Fills an array with the IDs of all active tasks.
taskInfoGet( )
Gets information about a task.
taskPriorityGet( )
Examines the priority of a task.
taskRegsGet( )
Examines a task's registers (cannot be used with the current task).
taskRegsSet( )
Sets a task's registers (cannot be used with the current task).
taskIsSuspended( )
Checks whether a task is suspended.
taskIsReady( )
Checks whether a task is ready to run.
taskTcb( )
Gets a pointer to a task's control block.

Task Deletion and Deletion Safety

Tasks can be dynamically deleted from the system. VxWorks includes the routines listed in Table 2-8 to delete tasks and to protect tasks from unexpected deletion.

Table 2-8:   Task-Deletion Routines


Call
Description

exit( )
Terminates the calling task and frees memory
(task stacks and task control blocks only).1
taskDelete( )
Terminates a specified task and frees memory
(task stacks and task control blocks only).*
taskSafe( )
Protects the calling task from deletion.
taskUnsafe( )
Undoes a taskSafe( ) (makes the calling task available for deletion).

1:  Memory that is allocated by the task during its execution is not freed when the task is terminated.


*      
WARNING: Make sure that tasks are not deleted at inappropriate times. Before an application deletes a task, the task should release all shared resources that it holds.

Tasks implicitly call exit( ) if the entry routine specified during task creation returns. A task can kill another task or itself by calling taskDelete( ).

When a task is deleted, no other task is notified of this deletion. The routines taskSafe( ) and taskUnsafe( ) address problems that stem from unexpected deletion of tasks. The routine taskSafe( ) protects a task from deletion by other tasks. This protection is often needed when a task executes in a critical region or engages a critical resource.


*      
NOTE: You can use VxWorks events to send an event when a task finishes executing. For more information, see 2.4 VxWorks Events.

For example, a task might take a semaphore for exclusive access to some data structure. While executing inside the critical region, the task might be deleted by another task. Because the task is unable to complete the critical region, the data structure might be left in a corrupt or inconsistent state. Furthermore, because the semaphore can never be released by the task, the critical resource is now unavailable for use by any other task and is essentially frozen.

Using taskSafe( ) to protect the task that took the semaphore prevents such an outcome. Any task that tries to delete a task protected with taskSafe( ) is blocked. When finished with its critical resource, the protected task can make itself available for deletion by calling taskUnsafe( ), which readies any deleting task. To support nested deletion-safe regions, a count is kept of the number of times taskSafe( ) and taskUnsafe( ) are called. Deletion is allowed only when the count is zero, that is, there are as many "unsafes" as "safes." Only the calling task is protected. A task cannot make another task safe or unsafe from deletion.

The following code fragment shows how to use taskSafe( ) and taskUnsafe( ) to protect a critical region of code:

    taskSafe (); 
    semTake (semId, WAIT_FOREVER);  /* Block until semaphore available */ 
    . 
    .   /* critical region code */ 
    . 
    semGive (semId);                /* Release semaphore */ 
    taskUnsafe (); 

Deletion safety is often coupled closely with mutual exclusion, as in this example. For convenience and efficiency, a special kind of semaphore, the mutual-exclusion semaphore, offers an option for deletion safety. For more information, see Mutual-Exclusion Semaphores.

Task Control

The routines listed in Table 2-9 provide direct control over a task's execution.

Table 2-9:   Task Control Routines


Call
Description

taskSuspend( )
Suspends a task.
taskResume( )
Resumes a task.
taskRestart( )
Restarts a task.
taskDelay( )
Delays a task; delay units are ticks, resolution in ticks.
nanosleep( )
Delays a task; delay units are nanoseconds, resolution in ticks.

VxWorks debugging facilities require routines for suspending and resuming a task. They are used to freeze a task's state for examination.

Tasks may require restarting during execution in response to some catastrophic error. The restart mechanism, taskRestart( ), recreates a task with the original creation arguments.

Delay operations provide a simple mechanism for a task to sleep for a fixed duration. Task delays are often used for polling applications. For example, to delay a task for half a second without making assumptions about the clock rate, call:

taskDelay (sysClkRateGet ( ) / 2);

The routine sysClkRateGet( ) returns the speed of the system clock in ticks per second. Instead of taskDelay( ), you can use the POSIX routine nanosleep( ) to specify a delay directly in time units. Only the units are different; the resolution of both delay routines is the same, and depends on the system clock. For details, see 3.2 POSIX Clocks and Timers.

As a side effect, taskDelay( ) moves the calling task to the end of the ready queue for tasks of the same priority. In particular, you can yield the CPU to any other tasks of the same priority by "delaying" for zero clock ticks:

taskDelay (NO_WAIT);     /* allow other tasks of same priority to run */ 

A "delay" of zero duration is only possible with taskDelay( ); nanosleep( ) considers it an error.


*      
NOTE: ANSI and POSIX APIs are similar.

System clock resolution is typically 60Hz (60 times per second). This is a relatively long time for one clock tick, and would be even at 100Hz or 120Hz. Thus, since periodic delaying is effectively polling, you may want to consider using event-driven techniques as an alternative.

2.2.5   Tasking Extensions

To allow additional task-related facilities to be added to the system, VxWorks provides hook routines that allow additional routines to be invoked whenever a task is created, a task context switch occurs, or a task is deleted. There are spare fields in the task control block (TCB) available for application extension of a task's context

These hook routines are listed in Table 2-10; for more information, see the reference entry for taskHookLib. .

Table 2-10:   Task Create, Switch, and Delete Hooks


Call
Description

taskCreateHookAdd( )
Adds a routine to be called at every task create.
taskCreateHookDelete( )
Deletes a previously added task create routine.
taskSwitchHookAdd( )
Adds a routine to be called at every task switch.
taskSwitchHookDelete( )
Deletes a previously added task switch routine.
taskDeleteHookAdd( )
Adds a routine to be called at every task delete.
taskDeleteHookDelete( )
Deletes a previously added task delete routine.

When using hook routines, be aware of the following restrictions:

  • Task switch hook routines must not assume any VM context is current other than the kernel context (as with ISRs).

  • Task switch and swap hooks must not rely on knowledge of the current task or invoke any function that relies on this information; for example, taskIdSelf( ).

  • A switch or swap hook must not rely on the taskIdVerify(pOldTcb) mechanism to determine if the delete hook, if any, has already executed for the self-destructing task case. Instead, some other state information needs to be changed; for example, using a NULL pointer in the delete hook to be detected by the switch hook.

The taskCreateAction hook routines execute in the context of the creator task, and any new objects are owned by the creator task's home protection domain, or the creator task itself. It may, therefore, be necessary to assign the ownership of new objects to the task that is created in order to prevent undesirable object reclamation in the event that the creator task terminates.

User-installed switch hooks are called within the kernel context and therefore do not have access to all VxWorks facilities. Table 2-11 summarizes the routines that can be called from a task switch hook; in general, any routine that does not involve the kernel can be called.

Table 2-11:   Routines that Can Be Called by Task Switch Hooks


Library
Routines

bLib
All routines
fppArchLib
fppSave( ), fppRestore( )
intLib
intContext( ), intCount( ), intVecSet( ), intVecGet( ), intLock( ), intUnlock( )
lstLib
All routines except lstFree( )
mathALib
All are callable if fppSave( )/fppRestore( ) are used
rngLib
All routines except rngCreate( ) and roundlet( )
taskLib
taskIdVerify( ), taskIdDefault( ), taskIsReady( ), taskIsSuspended( ), taskTcb( )
vxLib
vxTas( )


*      
NOTE: For information about POSIX extensions, see 3. POSIX Standard Interfaces.

2.2.6   Task Error Status: errno

By convention, C library functions set a single global integer variable errno to an appropriate error number whenever the function encounters an error. This convention is specified as part of the ANSI C standard.

Layered Definitions of errno

In VxWorks, errno is simultaneously defined in two different ways. There is, as in ANSI C, an underlying global variable called errno, which you can display by name using Tornado development tools; see the Tornado User's Guide. However, errno is also defined as a macro in errno.h; this is the definition visible to all of VxWorks except for one function. The macro is defined as a call to a function __errno( )that returns the address of the global variable, errno (as you might guess, this is the single function that does not itself use the macro definition for errno). This subterfuge yields a useful feature: because __errno( )is a function, you can place breakpoints on it while debugging, to determine where a particular error occurs.

Nevertheless, because the result of the macro errno is the address of the global variable errno, C programs can set the value of errno in the standard way:

errno = someErrorNumber;

As with any other errno implementation, take care not to have a local variable of the same name.

A Separate errno Value for Each Task

In VxWorks, the underlying global errno is a single predefined global variable that can be referenced directly by application code that is linked with VxWorks (either statically on the host or dynamically at load time). However, for errno to be useful in the multitasking environment of VxWorks, each task must see its own version of errno. Therefore errno is saved and restored by the kernel as part of each task's context every time a context switch occurs. Similarly, interrupt service routines (ISRs) see their own versions of errno.

This is accomplished by saving and restoring errno on the interrupt stack as part of the interrupt enter and exit code provided automatically by the kernel (see 2.6.1 Connecting Routines to Interrupts). Thus, regardless of the VxWorks context, an error code can be stored or consulted with direct manipulation of the global variable errno.

Error Return Convention

Almost all VxWorks functions follow a convention that indicates simple success or failure of their operation by the actual return value of the function. Many functions return only the status values OK (0) or ERROR (-1). Some functions that normally return a nonnegative number (for example, open( ) returns a file descriptor) also return ERROR to indicate an error. Functions that return a pointer usually return NULL (0) to indicate an error. In most cases, a function returning such an error indication also sets errno to the specific error code.

The global variable errno is never cleared by VxWorks routines. Thus, its value always indicates the last error status set. When a VxWorks subroutine gets an error indication from a call to another routine, it usually returns its own error indication without modifying errno. Thus, the value of errno that is set in the lower-level routine remains available as the indication of error type.

For example, the VxWorks routine intConnect( ), which connects a user routine to a hardware interrupt, allocates memory by calling malloc( ) and builds the interrupt driver in this allocated memory. If malloc( ) fails because insufficient memory remains in the pool, it sets errno to a code indicating an insufficient-memory error was encountered in the memory allocation library, memLib. The malloc( ) routine then returns NULL to indicate the failure. The intConnect( ) routine, receiving the NULL from malloc( ), then returns its own error indication of ERROR. However, it does not alter errno leaving it at the "insufficient memory" code set by malloc( ). For example:

if ((pNew = malloc (CHUNK_SIZE)) == NULL) 
    return (ERROR);

It is recommended that you use this mechanism in your own subroutines, setting and examining errno as a debugging technique. A string constant associated with errno can be displayed using printErrno( ) if the errno value has a corresponding string entered in the error-status symbol table, statSymTbl. See the reference entry errnoLib for details on error-status values and building statSymTbl.

Assignment of Error Status Values

VxWorks errno values encode the module that issues the error, in the most significant two bytes, and uses the least significant two bytes for individual error numbers. All VxWorks module numbers are in the range 1-500; errno values with a "module" number of zero are used for source compatibility.

All other errno values (that is, positive values greater than or equal to 501<<16, and all negative values) are available for application use.

See the reference entry on errnoLib for more information about defining and decoding errno values with this convention.

2.2.7   Task Exception Handling

Errors in program code or data can cause hardware exception conditions such as illegal instructions, bus or address errors, divide by zero, and so forth. The VxWorks exception handling package takes care of all such exceptions. The default exception handler suspends the task that caused the exception, and saves the state of the task at the point of the exception. The kernel and other tasks continue uninterrupted. A description of the exception is transmitted to the Tornado development tools, which can be used to examine the suspended task; see the Tornado User's Guide: Shell for details.

Tasks can also attach their own handlers for certain hardware exceptions through the signal facility. If a task has supplied a signal handler for an exception, the default exception handling described above is not performed. A user-defined signal handler is useful for recovering from catastrophic events. Typically, setjmp( ) is called to define the point in the program where control will be restored, and longjmp( ) is called in the signal handler to restore that context. Note that longjmp( ) restores the state of the task's signal mask.

Signals are also used for signaling software exceptions as well as hardware exceptions. They are described in more detail in 2.3.7 Signals and in the reference entry for sigLib.

2.2.8   Shared Code and Reentrancy

In VxWorks, it is common for a single copy of a subroutine or subroutine library to be invoked by many different tasks. For example, many tasks may call printf( ), but there is only a single copy of the subroutine in the system. A single copy of code executed by multiple tasks is called shared code. VxWorks dynamic linking facilities make this especially easy. Shared code makes a system more efficient and easier to maintain; see Figure 2-4.

Figure 2-4:   Shared Code

Shared code must be reentrant. A subroutine is reentrant if a single copy of the routine can be called from several task contexts simultaneously without conflict. Such conflict typically occurs when a subroutine modifies global or static variables, because there is only a single copy of the data and code. A routine's references to such variables can overlap and interfere in invocations from different task contexts.

Most routines in VxWorks are reentrant. However, you should assume that any routine someName( ) is not reentrant if there is a corresponding routine named someName_r( ) -- the latter is provided as a reentrant version of the routine. For example, because ldiv( ) has a corresponding routine ldiv_r( ), you can assume that ldiv( ) is not reentrant.

VxWorks I/O and driver routines are reentrant, but require careful application design. For buffered I/O, we recommend using file-pointer buffers on a per-task basis. At the driver level, it is possible to load buffers with streams from different tasks, due to the global file descriptor table in VxWorks.

This may or may not be desirable, depending on the nature of the application. For example, a packet driver can mix streams from different tasks because the packet header identifies the destination of each packet.

The majority of VxWorks routines use the following reentrancy techniques:

  • dynamic stack variables

  • global and static variables guarded by semaphores

  • task variables

We recommend applying these same techniques when writing application code that can be called from several task contexts simultaneously.


*      
NOTE: In some cases reentrant code is not preferable. A critical section should use a binary semaphore to guard it, or use intLock( ) or intUnlock( ) if called from by an ISR.


*      
NOTE: Init( ) functions should be callable multiple times, even if logically they should only be called once. As a rule, functions should avoid static variables that keep state information. Init( ) functions are one exception, where using a static variable that returns the success or failure of the original Init( ) is appropriate.

Dynamic Stack Variables

Many subroutines are pure code, having no data of their own except dynamic stack variables. They work exclusively on data provided by the caller as parameters. The linked-list library, lstLib, is a good example of this. Its routines operate on lists and nodes provided by the caller in each subroutine call.

Subroutines of this kind are inherently reentrant. Multiple tasks can use such routines simultaneously, without interfering with each other, because each task does indeed have its own stack. See Figure 2-5.

Figure 2-5:   Stack Variables and Shared Code

Guarded Global and Static Variables

Some libraries encapsulate access to common data. This kind of library requires some caution because the routines are not inherently reentrant. Multiple tasks simultaneously invoking the routines in the library might interfere with access to common variables. Such libraries must be made explicitly reentrant by providing a mutual-exclusion mechanism to prohibit tasks from simultaneously executing critical sections of code. The usual mutual-exclusion mechanism is the mutex semaphore facility provided by semMLib and described in Mutual-Exclusion Semaphores.

Task Variables

Some routines that can be called by multiple tasks simultaneously may require global or static variables with a distinct value for each calling task. For example, several tasks may reference a private buffer of memory and yet refer to it with the same global variable.

To accommodate this, VxWorks provides a facility called task variables that allows 4-byte variables to be added to a task's context, so that the value of such a variable is switched every time a task switch occurs to or from its owner task. Typically, several tasks declare the same variable (4-byte memory location) as a task variable. Each of those tasks can then treat that single memory location as its own private variable; see Figure 2-6. This facility is provided by the routines taskVarAdd( ), taskVarDelete( ), taskVarSet( ), and taskVarGet( ), which are described in the reference entry for taskVarLib.

Figure 2-6:   Task Variables and Context Switches

Use this mechanism sparingly. Each task variable adds a few microseconds to the context switching time for its task, because the value of the variable must be saved and restored as part of the task's context. Consider collecting all of a module's task variables into a single dynamically allocated structure, and then making all accesses to that structure indirectly through a single pointer. This pointer can then be the task variable for all tasks using that module.

Multiple Tasks with the Same Main Routine

With VxWorks, it is possible to spawn several tasks with the same main routine. Each spawn creates a new task with its own stack and context. Each spawn can also pass the main routine different parameters to the new task. In this case, the same rules of reentrancy described in Task Variables apply to the entire task.

This is useful when the same function needs to be performed concurrently with different sets of parameters. For example, a routine that monitors a particular kind of equipment might be spawned several times to monitor several different pieces of that equipment. The arguments to the main routine could indicate which particular piece of equipment the task is to monitor.

In Figure 2-7, multiple joints of the mechanical arm use the same code. The tasks manipulating the joints invoke joint( ). The joint number (jointNum) is used to indicate which joint on the arm to manipulate.

Figure 2-7:   Multiple Tasks Utilizing Same Code

     

2.2.9   VxWorks System Tasks

Depending on its configuration, VxWorks may include a variety of system tasks. These are described below.

Root Task: tUsrRoot

The root task is the first task executed by the kernel. The entry point of the root task is usrRoot( )in installDir/target/config/all/usrConfig.c and initializes most VxWorks facilities. It spawns such tasks as the logging task, the exception task, the network task, and the tRlogind daemon. Normally, the root task terminates and is deleted after all initialization has occurred.

Logging Task: tLogTask

The log task, tLogTask, is used by VxWorks modules to log system messages without having to perform I/O in the current task context. For more information, see 4.5.3 Message Logging and the reference entry for logLib.

Exception Task: tExcTask

The exception task, tExcTask, supports the VxWorks exception handling package by performing functions that cannot occur at interrupt level. It is also used for actions that cannot be performed in the current task's context, such as task suicide. It must have the highest priority in the system. Do not suspend, delete, or change the priority of this task. For more information, see the reference entry for excLib.

Network Task: tNetTask

The tNetTask daemon handles the task-level functions required by the VxWorks network. Configure VxWorks with the INCLUDE_NET_LIB component to spawn the tNetTask task.

Target Agent Task: tWdbTask

The target agent task, tWdbTask, is created if the target agent is set to run in task mode. It services requests from the Tornado target server; for information about this server, see the Tornado User's Guide: Overview. Configure VxWorks with the INCLUDE_WDB component to include the target agent.

Tasks for Optional Components

The following VxWorks system tasks are created if their associated configuration constants are defined; for more information, see the Tornado User's Guide: Configuration and Build.

tShell

tRlogind

tTelnetd

tPortmapd



2.3    Intertask Communications

The complement to the multitasking routines described in 2.2 VxWorks Tasks is the intertask communication facilities. These facilities permit independent tasks to coordinate their actions.

VxWorks supplies a rich set of intertask communication mechanisms, including:

  • Shared memory, for simple sharing of data.
  • Semaphores, for basic mutual exclusion and synchronization.
  • Mutexes and condition variables for mutual exclusion and synchronization using POSIX interfaces.
  • Message queues and pipes, for intertask message passing within a CPU.
  • Sockets and remote procedure calls, for network-transparent intertask communication.
  • Signals, for exception handling.

The optional products VxMP and VxFusion provide for intertask communication between multiple CPUs. See 11. Shared-Memory Objects and 10. Distributed Message Queues.

2.3.1   Shared Data Structures

The most obvious way for tasks to communicate is by accessing shared data structures. Because all tasks in VxWorks exist in a single linear address space, sharing data structures between tasks is trivial; see Figure 2-8. Global variables, linear buffers, ring buffers, linked lists, and pointers can be referenced directly by code running in different contexts.

Figure 2-8:   Shared Data Structures

2.3.2   Mutual Exclusion

While a shared address space simplifies exchange of data, interlocking access to memory is crucial to avoid contention. Many methods exist for obtaining exclusive access to resources, and vary only in the scope of the exclusion. Such methods include disabling interrupts, disabling preemption, and resource locking with semaphores.

For information about POSIX mutexes, see 3.7 POSIX Mutexes and Condition Variables.

Interrupt Locks and Latency

The most powerful method available for mutual exclusion is the disabling of interrupts. Such a lock guarantees exclusive access to the CPU:

funcA () 
    { 
    int lock = intLock(); 
    . 
    .   /* critical region of code that cannot be interrupted */ 
    . 
    intUnlock (lock); 
    }

While this solves problems involving mutual exclusion with ISRs, it is inappropriate as a general-purpose mutual-exclusion method for most real-time systems, because it prevents the system from responding to external events for the duration of these locks. Interrupt latency is unacceptable whenever an immediate response to an external event is required. However, interrupt locking can sometimes be necessary where mutual exclusion involves ISRs. In any situation, keep the duration of interrupt lockouts short.


*      
WARNING: Do not call VxWorks system routines with interrupts locked. Violating this rule may re-enable interrupts unpredictably.

Preemptive Locks and Latency

Disabling preemption offers a somewhat less restrictive form of mutual exclusion. While no other task is allowed to preempt the current executing task, ISRs are able to execute:

funcA () 
    { 
    taskLock (); 
    . 
    .  /* critical region of code that cannot be interrupted */ 
    . 
    taskUnlock (); 
    }

However, this method can lead to unacceptable real-time response. Tasks of higher priority are unable to execute until the locking task leaves the critical region, even though the higher-priority task is not itself involved with the critical region. While this kind of mutual exclusion is simple, if you use it, make sure to keep the duration short. A better mechanism is provided by semaphores, discussed in 2.3.3 Semaphores.


*      
WARNING: The critical region code should not block. If it does, preemption could be re-enabled.

2.3.3   Semaphores

VxWorks semaphores are highly optimized and provide the fastest intertask communication mechanism in VxWorks. Semaphores are the primary means for addressing the requirements of both mutual exclusion and task synchronization, as described below:

  • For mutual exclusion semaphores interlock access to shared resources. They provide mutual exclusion with finer granularity than either interrupt disabling or preemptive locks, discussed in 2.3.2 Mutual Exclusion.

  • For synchronization semaphores coordinate a task's execution with external events.

There are three types of Wind semaphores, optimized to address different classes of problems:

binary
The fastest, most general-purpose semaphore. Optimized for synchronization or mutual exclusion.

mutual exclusion
A special binary semaphore optimized for problems inherent in mutual exclusion: priority inheritance, deletion safety, and recursion.

counting
Like the binary semaphore, but keeps track of the number of times a semaphore is given. Optimized for guarding multiple instances of a resource.

VxWorks provides not only the Wind semaphores, designed expressly for VxWorks, but also POSIX semaphores, designed for portability. An alternate semaphore library provides the POSIX-compatible semaphore interface; see 3.6 POSIX Semaphores.

The semaphores described here are for use on a single CPU. The optional product VxMP provides semaphores that can be used across processors; see 11. Shared-Memory Objects.

Semaphore Control

Instead of defining a full set of semaphore control routines for each type of semaphore, the Wind semaphores provide a single uniform interface for semaphore control. Only the creation routines are specific to the semaphore type. Table 2-12 lists the semaphore control routines.

Table 2-12:   Semaphore Control Routines


Call
Description

semBCreate( )
Allocates and initializes a binary semaphore.
semMCreate( )
Allocates and initializes a mutual-exclusion semaphore.
semCCreate( )
Allocates and initializes a counting semaphore.
semDelete( )
Terminates and frees a semaphore.
semTake( )
Takes a semaphore.
semGive( )
Gives a semaphore.
semFlush( )
Unblocks all tasks that are waiting for a semaphore.

The semBCreate( ), semMCreate( ), and semCCreate( ) routines return a semaphore ID that serves as a handle on the semaphore during subsequent use by the other semaphore-control routines. When a semaphore is created, the queue type is specified. Tasks pending on a semaphore can be queued in priority order (SEM_Q_PRIORITY) or in first-in first-out order (SEM_Q_FIFO).


*      
WARNING: The semDelete( ) call terminates a semaphore and deallocates all associated memory. Take care when deleting semaphores, particularly those used for mutual exclusion, to avoid deleting a semaphore that another task still requires. Do not delete a semaphore unless the same task first succeeds in taking it.

Binary Semaphores

The general-purpose binary semaphore is capable of addressing the requirements of both forms of task coordination: mutual exclusion and synchronization. The binary semaphore has the least overhead associated with it, making it particularly applicable to high-performance requirements. The mutual-exclusion semaphore described in Mutual-Exclusion Semaphores is also a binary semaphore, but it has been optimized to address problems inherent to mutual exclusion. Alternatively, the binary semaphore can be used for mutual exclusion if the advanced features of the mutual-exclusion semaphore are deemed unnecessary.

A binary semaphore can be viewed as a flag that is available (full) or unavailable (empty). When a task takes a binary semaphore, with semTake( ), the outcome depends on whether the semaphore is available (full) or unavailable (empty) at the time of the call; see Figure 2-9. If the semaphore is available (full), the semaphore becomes unavailable (empty) and the task continues executing immediately. If the semaphore is unavailable (empty), the task is put on a queue of blocked tasks and enters a state of pending on the availability of the semaphore.

Figure 2-9:   Taking a Semaphore

When a task gives a binary semaphore, using semGive( ), the outcome also depends on whether the semaphore is available (full) or unavailable (empty) at the time of the call; see Figure 2-10. If the semaphore is already available (full), giving the semaphore has no effect at all. If the semaphore is unavailable (empty) and no task is waiting to take it, then the semaphore becomes available (full). If the semaphore is unavailable (empty) and one or more tasks are pending on its availability, then the first task in the queue of blocked tasks is unblocked, and the semaphore is left unavailable (empty).

Figure 2-10:   Giving a Semaphore

Mutual Exclusion

Binary semaphores interlock access to a shared resource efficiently. Unlike disabling interrupts or preemptive locks, binary semaphores limit the scope of the mutual exclusion to only the associated resource. In this technique, a semaphore is created to guard the resource. Initially the semaphore is available (full).

/* includes */ 
#include "vxWorks.h" 
#include "semLib.h" 
 
SEM_ID semMutex; 
 
/* Create a binary semaphore that is initially full. Tasks * 
 * blocked on semaphore wait in priority order.            */ 
 
semMutex = semBCreate (SEM_Q_PRIORITY, SEM_FULL);

When a task wants to access the resource, it must first take that semaphore. As long as the task keeps the semaphore, all other tasks seeking access to the resource are blocked from execution. When the task is finished with the resource, it gives back the semaphore, allowing another task to use the resource.

Thus, all accesses to a resource requiring mutual exclusion are bracketed with semTake( ) and semGive( ) pairs:

semTake (semMutex, WAIT_FOREVER); 
. 
.  /* critical region, only accessible by a single task at a time */ 
. 
semGive (semMutex);
Synchronization

When used for task synchronization, a semaphore can represent a condition or event that a task is waiting for. Initially, the semaphore is unavailable (empty). A task or ISR signals the occurrence of the event by giving the semaphore (see 2.6 Interrupt Service Code: ISRs for a complete discussion of ISRs). Another task waits for the semaphore by calling semTake( ). The waiting task blocks until the event occurs and the semaphore is given.

Note the difference in sequence between semaphores used for mutual exclusion and those used for synchronization. For mutual exclusion, the semaphore is initially full, and each task first takes, then gives back the semaphore. For synchronization, the semaphore is initially empty, and one task waits to take the semaphore given by another task.

In Example 2-1, the init( ) routine creates the binary semaphore, attaches an ISR to an event, and spawns a task to process the event. The routine task1( ) runs until it calls semTake( ). It remains blocked at that point until an event causes the ISR to call semGive( ). When the ISR completes, task1( ) executes to process the event. There is an advantage of handling event processing within the context of a dedicated task: less processing takes place at interrupt level, thereby reducing interrupt latency. This model of event processing is recommended for real-time applications.

Example 2-1:   Using Semaphores for Task Synchronization

/* This example shows the use of semaphores for task synchronization. */
/* includes */ 
#include "vxWorks.h" 
#include "semLib.h" 
#include "arch/arch/ivarch.h" /* replace arch with architecture type */
SEM_ID syncSem;             /* ID of sync semaphore */
init ( 
    int someIntNum 
    ) 
    { 
    /* connect interrupt service routine */ 
    intConnect (INUM_TO_IVEC (someIntNum), eventInterruptSvcRout, 0); 
 
    /* create semaphore */ 
    syncSem = semBCreate (SEM_Q_FIFO, SEM_EMPTY); 
 
    /* spawn task used for synchronization. */ 
    taskSpawn ("sample", 100, 0, 20000, task1, 0,0,0,0,0,0,0,0,0,0); 
    } 
 
task1 (void) 
    { 
    ...  
    semTake (syncSem, WAIT_FOREVER); /* wait for event to occur */ 
    printf ("task 1 got the semaphore\n"); 
    ...   /* process event */ 
    } 
 
eventInterruptSvcRout (void) 
    { 
    ...  
    semGive (syncSem);       /* let task 1 process event */ 
    ...  
    }

Broadcast synchronization allows all processes that are blocked on the same semaphore to be unblocked atomically. Correct application behavior often requires a set of tasks to process an event before any task of the set has the opportunity to process further events. The routine semFlush( ) addresses this class of synchronization problem by unblocking all tasks pended on a semaphore.

Mutual-Exclusion Semaphores

The mutual-exclusion semaphore is a specialized binary semaphore designed to address issues inherent in mutual exclusion, including priority inversion, deletion safety, and recursive access to resources.

The fundamental behavior of the mutual-exclusion semaphore is identical to the binary semaphore, with the following exceptions:

  • It can be used only for mutual exclusion.
  • It can be given only by the task that took it.
  • It cannot be given from an ISR.
  • The semFlush( ) operation is illegal.
Priority Inversion

Figure 2-11 illustrates a situation called priority inversion.

Figure 2-11:   Priority Inversion

Priority inversion arises when a higher-priority task is forced to wait an indefinite period of time for a lower-priority task to complete. Consider the scenario in Figure 2-11: t1, t2, and t3 are tasks of high, medium, and low priority, respectively. t3 has acquired some resource by taking its associated binary guard semaphore. When t1 preempts t3 and contends for the resource by taking the same semaphore, it becomes blocked. If we could be assured that t1 would be blocked no longer than the time it normally takes t3 to finish with the resource, there would be no problem because the resource cannot be preempted. However, the low-priority task is vulnerable to preemption by medium-priority tasks (like t2), which could inhibit t3 from relinquishing the resource. This condition could persist, blocking t1 for an indefinite period of time.

The mutual-exclusion semaphore has the option SEM_INVERSION_SAFE, which enables a priority-inheritance algorithm. The priority-inheritance protocol assures that a task that holds a resource executes at the priority of the highest-priority task blocked on that resource. Once the task priority has been elevated, it remains at the higher level until all mutual-exclusion semaphores that the task holds are released; then the task returns to its normal, or standard, priority. Hence, the "inheriting" task is protected from preemption by any intermediate-priority tasks. This option must be used in conjunction with a priority queue (SEM_Q_PRIORITY).

Figure 2-12:   Priority Inheritance

In Figure 2-12, priority inheritance solves the problem of priority inversion by elevating the priority of t3 to the priority of t1 during the time t1 is blocked on the semaphore. This protects t3, and indirectly t1, from preemption by t2.

The following example creates a mutual-exclusion semaphore that uses the priority inheritance algorithm:

semId = semMCreate (SEM_Q_PRIORITY | SEM_INVERSION_SAFE);
Deletion Safety

Another problem of mutual exclusion involves task deletion. Within a critical region guarded by semaphores, it is often desirable to protect the executing task from unexpected deletion. Deleting a task executing in a critical region can be catastrophic. The resource might be left in a corrupted state and the semaphore guarding the resource left unavailable, effectively preventing all access to the resource.

The primitives taskSafe( ) and taskUnsafe( ) provide one solution to task deletion. However, the mutual-exclusion semaphore offers the option SEM_DELETE_SAFE, which enables an implicit taskSafe( ) with each semTake( ), and a taskUnsafe( ) with each semGive( ). In this way, a task can be protected from deletion while it has the semaphore. This option is more efficient than the primitives taskSafe( ) and taskUnsafe( ), as the resulting code requires fewer entrances to the kernel.

semId = semMCreate (SEM_Q_FIFO | SEM_DELETE_SAFE);
Recursive Resource Access

Mutual-exclusion semaphores can be taken recursively. This means that the semaphore can be taken more than once by the task that holds it before finally being released. Recursion is useful for a set of routines that must call each other but that also require mutually exclusive access to a resource. This is possible because the system keeps track of which task currently holds the mutual-exclusion semaphore.

Before being released, a mutual-exclusion semaphore taken recursively must be given the same number of times it is taken. This is tracked by a count that increments with each semTake( ) and decrements with each semGive( ).

Example 2-2:   Recursive Use of a Mutual-Exclusion Semaphore

/* Function A requires access to a resource which it acquires by taking 
 * mySem;  
 * Function A may also need to call function B, which also requires mySem: 
 */ 
 
/* includes */ 
#include "vxWorks.h" 
#include "semLib.h" 
SEM_ID mySem; 
 
/* Create a mutual-exclusion semaphore. */ 
init () 
    { 
    mySem = semMCreate (SEM_Q_PRIORITY); 
    } 
funcA () 
    { 
    semTake (mySem, WAIT_FOREVER); 
    printf ("funcA: Got mutual-exclusion semaphore\n"); 
    ...  
    funcB (); 
    ... 
    semGive (mySem); 
    printf ("funcA: Released mutual-exclusion semaphore\n"); 
    } 
funcB () 
    { 
    semTake (mySem, WAIT_FOREVER); 
    printf ("funcB: Got mutual-exclusion semaphore\n"); 
    ...  
    semGive (mySem); 
    printf ("funcB: Releases mutual-exclusion semaphore\n"); 
    }

Counting Semaphores

Counting semaphores are another means to implement task synchronization and mutual exclusion. The counting semaphore works like the binary semaphore except that it keeps track of the number of times a semaphore is given. Every time a semaphore is given, the count is incremented; every time a semaphore is taken, the count is decremented. When the count reaches zero, a task that tries to take the semaphore is blocked. As with the binary semaphore, if a semaphore is given and a task is blocked, it becomes unblocked. However, unlike the binary semaphore, if a semaphore is given and no tasks are blocked, then the count is inc