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 incremented. This means that a semaphore that is given twice can be taken twice without blocking. Table 2-13 shows an example time sequence of tasks taking and giving a counting semaphore that was initialized to a count of 3.

Table 2-13:   Counting Semaphore Example


Semaphore Call
Count after Call
Resulting Behavior

semCCreate( )
3
Semaphore initialized with an initial count of 3.
semTake( )
2
Semaphore taken.
semTake( )
1
Semaphore taken.
semTake( )
0
Semaphore taken.
semTake( )
0
Task blocks waiting for semaphore to be available.
semGive( )
0
Task waiting is given semaphore.
semGive( )
1
No task waiting for semaphore; count incremented.

Counting semaphores are useful for guarding multiple copies of resources. For example, the use of five tape drives might be coordinated using a counting semaphore with an initial count of 5, or a ring buffer with 256 entries might be implemented using a counting semaphore with an initial count of 256. The initial count is specified as an argument to the semCCreate( ) routine.

Special Semaphore Options

The uniform Wind semaphore interface includes two special options. These options are not available for the POSIX-compatible semaphores described in 3.6 POSIX Semaphores.

Timeouts

As an alternative to blocking until a semaphore becomes available, semaphore take operations can be restricted to a specified period of time. If the semaphore is not taken within that period, the take operation fails.

This behavior is controlled by a parameter to semTake( ) that specifies the amount of time in ticks that the task is willing to wait in the pended state. If the task succeeds in taking the semaphore within the allotted time, semTake( ) returns OK. The errno set when a semTake( ) returns ERROR due to timing out before successfully taking the semaphore depends upon the timeout value passed.

A semTake( ) with NO_WAIT (0), which means do not wait at all, sets errno to S_objLib_OBJ_UNAVAILABLE. A semTake( ) with a positive timeout value returns S_objLib_OBJ_TIMEOUT. A timeout value of WAIT_FOREVER (-1) means wait indefinitely.

Queues

Wind semaphores include the ability to select the queuing mechanism employed for tasks blocked on a semaphore. They can be queued based on either of two criteria: first-in first-out (FIFO) order, or priority order; see Figure 2-13.

Figure 2-13:   Task Queue Types

Priority ordering better preserves the intended priority structure of the system at the expense of some overhead in semTake( ) in sorting the tasks by priority. A FIFO queue requires no priority sorting overhead and leads to constant-time performance. The selection of queue type is specified during semaphore creation with semBCreate( ), semMCreate( ), or semCCreate( ). Semaphores using the priority inheritance option (SEM_INVERSION_SAFE) must select priority-order queuing.

Semaphores and VxWorks Events

This section describes using VxWorks events with semaphores. You can also use VxWorks events with other VxWorks objects. For more information, see 2.4 VxWorks Events.

Using Events

A semaphore can send events to a task, if it is requested to do so by the task. To request that a semaphore send events, a task must register with the semaphore using semEvStart( ). From that point on, every time the semaphore is released with semGive( ), and as long as no other tasks are pending on it, the semaphore sends events to the registered task. To request that the semaphore stop sending events, the registered task calls semEvStop( ).

Only one task can be registered with a semaphore at any given time. The events a semaphore sends to a task can be retrieved by the task using routines in eventLib. Details on when semaphores send events are documented in the reference entry for semEvStart( ).

In some applications, the creator of a semaphore may wish to know when a semaphore failed to send events. Such a scenario can occur if a task registers with a semaphore, and is subsequently deleted before having time to unregister. In this situation, a given operation could cause the semaphore to attempt to send events to the deleted task. Such an attempt would obviously fail. If the semaphore is created with the SEM_EVENTSEND_ERROR_NOTIFY option, the given operation returns an error. Otherwise, VxWorks handles the error quietly.

Using eventReceive( ), a task may pend on events meant to be sent by a semaphore. If the semaphore is deleted, the task pending on events is returned to the ready state, just like the tasks that may be pending on the semaphore itself.

Existing VxWorks API

The VxWorks events implementation does not propose to keep track of all the resources a task is currently registered with. Therefore, a resource can attempt to send events to a task that no longer exists. For example, a task may be deleted or may self-destruct while still registered with a resource to receive events. This error is detected only when the resource becomes free, and is reported by having semGive( ) return ERROR. However, in this case the error does not mean the semaphore was not given or that the message was not properly delivered. It simply means the resource could not send events to the registered task. This is a different behavior from the one presently in place under VxWorks, however it is the same behavior that exists for pSOS message queues and semaphores.

Performance Impact

When a task is pending for the semaphore, there is no performance impact on semGive( ). However, if this is not the case (for example, if the semaphore is free), the call to semGive( ) takes longer to complete since events may have to be sent to a task. Furthermore, the call may unpend a task waiting for events, which means the caller may be preempted, even if no task is waiting for the semaphore.

The semDestroy( ) routine performance is impacted in cases where a task is waiting for events from the semaphore, since the task has to be awakened. Also note that, in this case, events need not be sent.   

2.3.4   Message Queues

Modern real-time applications are constructed as a set of independent but cooperating tasks. While semaphores provide a high-speed mechanism for the synchronization and interlocking of tasks, often a higher-level mechanism is necessary to allow cooperating tasks to communicate with each other. In VxWorks, the primary intertask communication mechanism within a single CPU is message queues. (The VxWorks distributed message queue component provides for sharing message queues between processors across any transport media; see 10. Distributed Message Queues).

Message queues allow a variable number of messages, each of variable length, to be queued. Tasks and ISRs can send messages to a message queue, and tasks can receive messages from a message queue.

Figure 2-14:   Full Duplex Communication Using Message Queues

Multiple tasks can send to and receive from the same message queue. Full-duplex communication between two tasks generally requires two message queues, one for each direction; see Figure 2-14.

There are two message-queue subroutine libraries in VxWorks. The first of these, msgQLib, provides Wind message queues, designed expressly for VxWorks; the second, mqPxLib, is compatible with the POSIX standard (1003.1b) for real-time extensions. See 3.5.1 Comparison of POSIX and Wind Scheduling for a discussion of the differences between the two message-queue designs.

Wind Message Queues

Wind message queues are created, used, and deleted with the routines shown in Table 2-14. This library provides messages that are queued in FIFO order, with a single exception: there are two priority levels, and messages marked as high priority are attached to the head of the queue.

Table 2-14:   Wind Message Queue Control


Call
Description

msgQCreate( )
Allocates and initializes a message queue.
msgQDelete( )
Terminates and frees a message queue.
msgQSend( )
Sends a message to a message queue.
msgQReceive( )
Receives a message from a message queue.

A message queue is created with msgQCreate( ). Its parameters specify the maximum number of messages that can be queued in the message queue and the maximum length in bytes of each message. Enough buffer space is allocated for the specified number and length of messages.

A task or ISR sends a message to a message queue with msgQSend( ). If no tasks are waiting for messages on that queue, the message is added to the queue's buffer of messages. If any tasks are already waiting for a message from that message queue, the message is immediately delivered to the first waiting task.

A task receives a message from a message queue with msgQReceive( ). If messages are already available in the message queue's buffer, the first message is immediately dequeued and returned to the caller. If no messages are available, then the calling task blocks and is added to a queue of tasks waiting for messages. This queue of waiting tasks can be ordered either by task priority or FIFO, as specified in an option parameter when the queue is created.

Timeouts

Both msgQSend( ) and msgQReceive( ) take timeout parameters. When sending a message, the timeout specifies how many ticks to wait for buffer space to become available, if no space is available to queue the message. When receiving a message, the timeout specifies how many ticks to wait for a message to become available, if no message is immediately available. As with semaphores, the value of the timeout parameter can have the special values of NO_WAIT (0), meaning always return immediately, or WAIT_FOREVER (-1), meaning never time out the routine.

Urgent Messages

The msgQSend( ) function allows specification of the priority of the message as either normal (MSG_PRI_NORMAL) or urgent (MSG_PRI_URGENT). Normal priority messages are added to the tail of the list of queued messages, while urgent priority messages are added to the head of the list.

Example 2-3:   Wind Message Queues

/* In this example, task t1 creates the message queue and sends a message 
 * to task t2. Task t2 receives the message from the queue and simply 
 * displays the message. 
 */ 
 
/* includes */ 
#include "vxWorks.h" 
#include "msgQLib.h" 
 
/* defines */ 
#define MAX_MSGS (10) 
#define MAX_MSG_LEN (100) 
 
MSG_Q_ID myMsgQId; 
 
task2 (void) 
    { 
    char msgBuf[MAX_MSG_LEN]; 
 
    /* get message from queue; if necessary wait until msg is available */ 
    if (msgQReceive(myMsgQId, msgBuf, MAX_MSG_LEN, WAIT_FOREVER) == ERROR) 
        return (ERROR); 
 
    /* display message */ 
    printf ("Message from task 1:\n%s\n", msgBuf); 
    } 
 
#define MESSAGE "Greetings from Task 1" 
task1 (void) 
    { 
    /* create message queue */ 
    if ((myMsgQId = msgQCreate (MAX_MSGS, MAX_MSG_LEN, MSG_Q_PRIORITY))  
        == NULL) 
        return (ERROR); 
 
    /* send a normal priority message, blocking if queue is full */ 
    if (msgQSend (myMsgQId, MESSAGE, sizeof (MESSAGE), WAIT_FOREVER, 
                  MSG_PRI_NORMAL) == ERROR) 
        return (ERROR); 
    }

Displaying Message Queue Attributes

The VxWorks show( ) command produces a display of the key message queue attributes, for either kind of message queue. For example, if myMsgQId is a Wind message queue, the output is sent to the standard output device, and looks like the following:

-> show myMsgQId 
Message Queue Id  : 0x3adaf0  
Task Queuing      : FIFO  
Message Byte Len  : 4  
Messages Max      : 30  
Messages Queued   : 14 
Receivers Blocked : 0  
Send timeouts     : 0  
Receive timeouts    : 0 

Servers and Clients with Message Queues

Real-time systems are often structured using a client-server model of tasks. In this model, server tasks accept requests from client tasks to perform some service, and usually return a reply. The requests and replies are usually made in the form of intertask messages. In VxWorks, message queues or pipes (see 2.3.5 Pipes) are a natural way to implement this.

For example, client-server communications might be implemented as shown in Figure 2-15. Each server task creates a message queue to receive request messages from clients. Each client task creates a message queue to receive reply messages from servers. Each request message includes a field containing the msgQId of the client's reply message queue. A server task's "main loop" consists of reading request messages from its request message queue, performing the request, and sending a reply to the client's reply message queue.

The same architecture can be achieved with pipes instead of message queues, or by other means that are tailored to the needs of the particular application.

Figure 2-15:   Client-Server Communications Using Message Queues

Message Queues and VxWorks Events

This section describes using VxWorks events with message queues. You can also use VxWorks events with other VxWorks objects. For more information, see 2.4 VxWorks Events.

Using Events

A message queue can send events to a task, if it is requested to do so by the task. To request that a message queue send events, a task must register with the message queue using msgQEvStart( ). From that point on, every time the message queue receives a message and there are no tasks pending on it, the message queue sends events to the registered task. To request that the message queue stop sending events, the registered task calls msgQEvStop( ).

Only one task can be registered with a message queue at any given time. The events a message queue sends to a task can be retrieved by the task using routines in eventLib. Details on when message queues send events are documented in the reference entry for msgQEvStart( ).

In some applications, the creator of a message queue may wish to know when a message queue failed to send events. Such a scenario can occur if a task registers with a message queue, and is subsequently deleted before having time to unregister. In this situation, a send operation could cause the message queue to attempt to send events to the deleted task. Such an attempt would obviously fail. If the message queue is created with the SG_Q_EVENTSEND_ERROR_NOTIFY option, the send operation returns an error. Otherwise, VxWorks handles the error quietly.

Using eventReceive( ), a task may pend on events meant to be sent by a message queue. If the message queue is deleted, the task pending on events is returned to the ready state, just like the tasks that may be pending on the message queue itself.

Existing VxWorks API

The VxWorks events implementation does not propose to keep track of all the resources a task is currently registered with. Therefore, a resource can attempt to send events to a task that no longer exists. For example, a task may be deleted or may self-destruct while still registered with a resource to receive events. This error is detected only when the resource becomes free, and is reported by having msgQSend( ) return ERROR. However, in this case the error does not mean the semaphore was not given or that the message was not properly delivered. It simply means the resource could not send events to the registered task. This is a different behavior than the one presently in place under VxWorks, however it is the same behavior that exists for pSOS message queues.

Performance Impact

There is no performance impact on msgQSend( ) when a task is pending for the message queue. However, when this is not the case, the call to msgQSend( ) takes longer to complete, since events may have to be sent to a task. Furthermore, the call may unpend a task waiting for events, which means the caller may be preempted, even if no task is waiting for the message.

The msgQDestroy( ) routine performance is impacted in cases where a task is waiting for events from the message queue, since the task has to be awakened. Also note that, in this case, events need not be sent.

2.3.5   Pipes

Pipes provide an alternative interface to the message queue facility that goes through the VxWorks I/O system. Pipes are virtual I/O devices managed by the driver pipeDrv. The routine pipeDevCreate( ) creates a pipe device and the underlying message queue associated with that pipe. The call specifies the name of the created pipe, the maximum number of messages that can be queued to it, and the maximum length of each message:

status = pipeDevCreate ("/pipe/name", max_msgs, max_length);

The created pipe is a normally named I/O device. Tasks can use the standard I/O routines to open, read, and write pipes, and invoke ioctl routines. As they do with other I/O devices, tasks block when they read from an empty pipe until data is available, and block when they write to a full pipe until there is space available. Like message queues, ISRs can write to a pipe, but cannot read from a pipe.

As I/O devices, pipes provide one important feature that message queues cannot--the ability to be used with select( ). This routine allows a task to wait for data to be available on any of a set of I/O devices. The select( ) routine also works with other asynchronous I/O devices including network sockets and serial devices. Thus, by using select( ), a task can wait for data on a combination of several pipes, sockets, and serial devices; see 4.3.8 Pending on Multiple File Descriptors: The Select Facility.

Pipes allow you to implement a client-server model of intertask communications; see Servers and Clients with Message Queues.

2.3.6   Network Intertask Communication

Sockets

In VxWorks, the basis of intertask communications across the network is sockets. A socket is an endpoint for communications between tasks; data is sent from one socket to another. When you create a socket, you specify the Internet communications protocol that is to transmit the data. VxWorks supports the Internet protocols TCP and UDP. VxWorks socket facilities are source compatible with BSD 4.4 UNIX.

TCP provides reliable, guaranteed, two-way transmission of data with stream sockets. In a stream-socket communication, two sockets are "connected," allowing a reliable byte-stream to flow between them in each direction as in a circuit. For this reason, TCP is often referred to as a virtual circuit protocol.

UDP provides a simpler but less robust form of communication. In UDP communications, data is sent between sockets in separate, unconnected, individually addressed packets called datagrams. A process creates a datagram socket and binds it to a particular port. There is no notion of a UDP "connection." Any UDP socket, on any host in the network, can send messages to any other UDP socket by specifying its Internet address and port number.

One of the biggest advantages of socket communications is that it is "homogeneous." Socket communications among processes are exactly the same regardless of the location of the processes in the network, or the operating system under which they are running. Processes can communicate within a single CPU, across a backplane, across an Ethernet, or across any connected combination of networks. Socket communications can occur between VxWorks tasks and host system processes in any combination. In all cases, the communications look identical to the application, except, of course, for their speed.

For more information, see VxWorks Network Programmer's Guide: Networking APIs and the reference entry for sockLib.

Remote Procedure Calls (RPC)

Remote Procedure Calls (RPC) is a facility that allows a process on one machine to call a procedure that is executed by another process on either the same machine or a remote machine. Internally, RPC uses sockets as the underlying communication mechanism. Thus with RPC, VxWorks tasks and host system processes can invoke routines that execute on other VxWorks or host machines, in any combination.

As discussed in the previous sections on message queues and pipes, many real-time systems are structured with a client-server model of tasks. In this model, client tasks request services of server tasks, and then wait for their reply. RPC formalizes this model and provides a standard protocol for passing requests and returning replies. Also, RPC includes tools to help generate the client interface routines and the server skeleton.

For more information about RPC, see VxWorks Network Programmer's Guide: RPC, Remote Procedure Calls.

2.3.7   Signals

VxWorks supports a software signal facility. Signals asynchronously alter the control flow of a task. Any task or ISR can raise a signal for a particular task. The task being signaled immediately suspends its current thread of execution and executes the task-specified signal handler routine the next time it is scheduled to run. The signal handler executes in the receiving task's context and makes use of that task's stack. The signal handler is invoked even if the task is blocked.

Signals are more appropriate for error and exception handling than as a general-purpose intertask communication mechanism. In general, signal handlers should be treated like ISRs; no routine should be called from a signal handler that might cause the handler to block. Because signals are asynchronous, it is difficult to predict which resources might be unavailable when a particular signal is raised. To be perfectly safe, call only those routines that can safely be called from an ISR (see Table 2-21). Deviate from this practice only when you are sure your signal handler cannot create a deadlock situation.

The wind kernel supports two types of signal interface: UNIX BSD-style signals and POSIX-compatible signals. The POSIX-compatible signal interface, in turn, includes both the fundamental signaling interface specified in the POSIX standard 1003.1, and the queued-signals extension from POSIX 1003.1b. For more information, see 3.9 POSIX Queued Signals. For the sake of simplicity, we recommend that you use only one interface type in a given application, rather than mixing routines from different interfaces.

For more information about signals, see the reference entry for sigLib.


*      
NOTE: The VxWorks implementation of sigLib does not impose any special restrictions on operations on SIGKILL, SIGCONT, and SIGSTOP signals such as those imposed by UNIX. For example, the UNIX implementation of signal( ) cannot be called on SIGKILL and SIGSTOP.

Basic Signal Routines

By default, VxWorks uses the basic signal facility component INCLUDE_SIGNALS. This component automatically initializes signals with sigInit( ). Table 2-15 shows the basic signal routines.

Table 2-15:   Basic Signal Calls (BSD and POSIX 1003.1b)


POSIX 1003.1b
Compatible
Call
UNIX BSD
Compatible
Call
Description

signal( )
signal( )
Specifies the handler associated with a signal.
kill( )
kill( )
Sends a signal to a task.
raise( )
N/A
Sends a signal to yourself.
sigaction( )
sigvec( )
Examines or sets the signal handler for a signal.
sigsuspend( )
pause( )
Suspends a task until a signal is delivered.
sigpending( )
N/A
Retrieves a set of pending signals blocked from delivery.
sigemptyset( )
sigfillset( )

sigaddset( )
sigdelset( )
sigismember( )
sigsetmask( )
Manipulates a signal mask.
sigprocmask( )
sigsetmask( )
Sets the mask of blocked signals.
sigprocmask( )
sigblock( )
Adds to a set of blocked signals.

The colorful name kill( )harks back to the origin of these interfaces in UNIX BSD. Although the interfaces vary, the functionality of BSD-style signals and basic POSIX signals is similar.

In many ways, signals are analogous to hardware interrupts. The basic signal facility provides a set of 31 distinct signals. A signal handler binds to a particular signal with sigvec( ) or sigaction( ) in much the same way that an ISR is connected to an interrupt vector with intConnect( ). A signal can be asserted by calling kill( ). This is analogous to the occurrence of an interrupt. The routines sigsetmask( ) and sigblock( ) or sigprocmask( ) let signals be selectively inhibited.

Certain signals are associated with hardware exceptions. For example, bus errors, illegal instructions, and floating-point exceptions raise specific signals.

Signal Configuration

The basic signal facility is included in VxWorks by default with INCLUDE_SIGNALS component.



2.4    VxWorks Events

VxWorks events introduce functionality similar to pSOS events into VxWorks 5.5. VxWorks events are included in the standard VxWorks facilities and are used to port pSOS events functionality to VxWorks. This section provides a brief summary of VxWorks events; then it describes pSOS events and VxWorks events in detail, comparing them and their APIs.


*      
NOTE: This section uses the term events to describe pSOS and VxWorks events. These references are not to be confused with WindView events.

VxWorks events are a means of communication between tasks and interrupt routines (ISRs), between tasks and other tasks, or between tasks and VxWorks objects. In the context of VxWorks events, these objects are referred to as resources, and they include semaphores and message queues. Only tasks can receive events; whereas tasks, ISRs, or resources can send them.

In order for a task to receive events from a resource, the task must register with the resource. In order for the resource to send events, the resource must be free. The communication between tasks and resources is peer-to-peer, meaning that only the registered task can receive events from the resource. In this respect, events are like signals, in that they are directed at one task. A task, however, can wait on events from multiple resources; thus, it can be waiting for a semaphore to become free and for a message to arrive in a message queue.

Events are synchronous in nature (unlike signals), meaning that a receiving task must block or pend while waiting for the events to occur. When the desired events are received, the pending task continues its execution, as it would after a call to msgQReceive( ) or semTake( ), for example. Thus, unlike signals, events do not require a handler.

Tasks can also wait on events that are not linked to resources. These are events that are sent from another task or from an ISR. A task does not register to receive these events; the sending task or ISR simply has to know of the task's interest in receiving the events. As an example, this scenario is similar to having an ISR give a binary semaphore, knowing there is a task interested in obtaining that semaphore.

The meaning of each event differs for each task. For example, when an event, eventX, is received, it can be interpreted differently by each task that receives it. Also, once an event is received by a task, the event is ignored if it is sent again to the same task. Consequently, it is not possible to track the number of times each event has been sent to a task.


*      
WARNING: Because events are not, and cannot be, reserved, two independent applications can attempt to use the same events on the same task. As a precaution, middleware applications using VxWorks events should always publish a list of the events they are using.

2.4.1   pSOS Events

This section describes the functionality of pSOS events. This functionality provides the basis of VxWorks events, but does not fully describe their behavior. For details, see VxWorks Enhancements to pSOS Events.

Sending and Receiving Events

In the pSOS operating system, events can be sent from a resource to a task, from an ISR to a task, or directly between two tasks. Tasks, ISRs, and resources all use the same ev_send( ) API to send events.

For a task to receive events from a resource, the task must register with that resource and request it to send a specific set of events when it becomes free. The resource is either a semaphore or a message queue. When the resource becomes free, it sends the set of events to the registered task. This task may, or may not be, waiting for the events.

As mentioned above, a task can also receive events from another task. For example, if two tasks agree to send events between them, taskA could send taskB a specific events set when it (taskA) finishes executing, to let taskB know that this has occurred. As with events sent from a resource, the receiving task may, or may not be, waiting for the events.

Waiting for Events

A task can wait for multiple events from one or more sources. Each source can send multiple events, and a task can also wait to receive only one event, or all events. For example, a task may be waiting for events 1 to 10, where 1 to 4 come from a semaphore, 5 to 6 come from a message queue, and 7 to 10 come from another task.

A task can also specify a timeout when waiting for events.

Registering for Events

Only one task can register itself to receive events from a resource. If another task subsequently registers with the same resource, the previously registered task is automatically unregistered, without its knowledge. This behavior differs in VxWorks events. For details, see VxWorks Enhancements to pSOS Events.

When a task registers with a resource, events are not sent immediately, even if the resource is free at the time of registration. The events are sent the next time the resource becomes free. For example, a semaphore will send events the next time it is given, as long as no task is waiting for it. (Being given does not always mean that a resource is free; see Freeing Resources). This behavior is configurable for VxWorks events. For details, see VxWorks Enhancements to pSOS Events.

Freeing Resources

When a resource sends events to a task to indicate that it is free, it does not mean that resource is reserved. Therefore, a task waiting for events from a resource will unpend when the resource becomes free, however the resource may be taken in the meantime. There are no guarantees that the resource will still be available, if the task subsequently attempts to take ownership of it.

As mentioned above, a resource only sends events when it becomes free. This is not synonymous with being released. To clarify, if a semaphore is given, it is actually being released. However, it is not considered free if another task is waiting for it at the time it is released. Therefore, in cases where two or more tasks are constantly exchanging ownership of a resource, that resource never becomes free; thus, it may never send events.

pSOS Events API

The pSOS events API routines are listed in Table 2-16:   

Table 2-16:   pSOS Events API


Routine
Meaning

ev_send( )
Sends events to a task.
ev_receive( )
Waits for events.
sm_notify( )
Registers a task to be notified of semaphore availability.
q_notify( )
Registers a task to be notified of message arrival on a message queue.
q_vnotify( )
Registers a task to be notified of message arrival on a variable-length message queue.

  

2.4.2   VxWorks Events

The implementation of VxWorks events is based on the way pSOS events work. This section first clarifies some of the crucial terms used to discuss VxWorks events. Then it describes VxWorks events in more detail, comparing their functionality to that of pSOS events.

Free Resource Definition

A key concept in understanding events sent by resources, is that resources send events when they become free. Thus, it is crucial to define what it means for a resource to be free for VxWorks events.

Mutex Semaphore

Binary Semaphore

Counting Semaphore

Message Queue

VxWorks Enhancements to pSOS Events

When VxWorks events were implemented, some enhancements were made to the basic pSOS functionality. This section describes those enhancements and configuration options, and compares the resulting behavior of VxWorks events with pSOS events.

  • SIngle Task Resource Registration.  

As mentioned in 2.4.1 pSOS Events, under pSOS, when a task registers with a resource to send pSOS events, it can inadvertently deregister another task that had previously registered with the resource. This prevents the first task from receiving events from the resource with which it registered. Consequently, the task that first registered with the resource could stay in a pend state indefinitely.

In order to solve this problem, VxWorks events provide an option whereby the second task is not allowed to register with the resource, if another task is already registered with it. If a second task tries to register with the resource, an error is returned. In VxWorks, you can configure the registration mechanism to use either the VxWorks or the pSOS behavior.

  • Option for Immediate Send.  

As mentioned in Registering for Events, when a pSOS task registers with a resource, the resource does not send events to the task immediately, even if it is free at the time of registration. For VxWorks events, the default behavior is the same. However, VxWorks events provide an option that allows a task, at the time of registration, to request that the resource send the events immediately, if the resource is free at the time of registration.

  • Option for Automatic Unregister.  

There are situations in which a task may want to receive events from a resource only once, and then unregister. The pSOS implementation requires a task to explicitly unregister after having received events from the resource. The VxWorks implementation provides an option whereby a registering task can tell the resource to only send events once, and automatically unregister the task when this occurs.

  • Automatic Unpend upon Resource Deletion.  

When a resource (a semaphore or message queue) is deleted, the semDelete( ) and msgQDelete( ) implementation unpends any task. This prevents the task from pending indefinitely, while waiting for events from the resource being deleted. The pending task then resumes execution, and receives an ERROR return value from the eventReceive( ) call that caused the task to pend. See also, Existing VxWorks API and Existing VxWorks API.

Task Events Register

Each task has its own events field or container, referred to as the task events register. The task events register is a per task 32-bit field used to store the events that a task receives from resources, ISRs, and other tasks.

You do not access the task events register directly. Tasks, ISRs, and resources fill the events register of a particular task by sending events to that task. A task can also send itself events, thereby filling its own events register. Events 25 to 32 (VXEV25 or 0x01000000 to VXEV32 or 0x80000000) are reserved for system use only, and are not available to VxWorks users. Table 2-17 describes the routines that affect the contents of the events register.

Table 2-17:   Event Register Routines


Routine
Effects

eventReceive( )
Clears or leaves the contents of the event register intact, depending on the options selected.
eventClear( )
Clears the contents of the event register
eventSend( )
Copies events into the event register.
semGive( )
Copies events into the event register, if a task is registered with the semaphore.
msgQSend( )
Copies events into the event register, if a task is registered with the message queue.

VxWorks Events API

For details on the API for VxWorks events, see the reference entries for eventLib, semEvLib, and msgQEvLib.

Show Routines

For the purpose of debugging systems that make use of events, the taskShow, semShow, and msgQShow libraries display event information.

The taskShow library displays the following information:

  • the contents of the event register
  • the desired events
  • the options specified when eventReceive( ) was called

The semShow( ) and msgQShow( ) libraries display the following information:

  • the task registered to receive events
  • the events the resource is meant to send to that task
  • the options passed to semEvStart( ) or msgQEvStart( )

2.4.3   API Comparison

The VxWorks events API has made modifications to the pSOS events API for the purpose of better describing the action of the routines. The pSOS API uses a family of notify routines for registering and unregistering from a resource. However, these names do not correctly reflect the action of resources, which is to either send, or not send, events. Thus, the VxWorks API more precisely describes the request for a resource to start sending events and to stop sending events to a task. This implementation uses the semEvStart( ) and msgQEvStart( ) routines to tell resources to start sending events, and the semEvStop( ) and msgQEvStop( ) routines to tell a resource to stop sending events.

Table 2-18 compares the similarities and differences between the VxWorks and pSOS events API:

Table 2-18:   Comparison of Events


VxWorks Routine
pSOS Routine
Comments

eventSend
ev_send
Direct port
eventReceive
ev_receive
Direct port
eventClear
New functionality in VxWorks.
semEvStart
sm_notify
semEvStart is equivalent to calling sm_notify with a nonzero events argument.
semEvStop
sm_notify
semEvStop is equivalent to calling sm_notify with an events argument equal to 0.
msgQEvStart
q_vnotify
msgQEvStart is equivalent to calling q_notify with a nonzero events argument.
msgQEvStop
q_vnotify
msgQEvStop is equivalent to calling q_notify with an events argument equal to 0.
q_notify
VxWorks does not have a fixed-length message queue mechanism.



2.5    Watchdog Timers

VxWorks includes a watchdog-timer mechanism that allows any C function to be connected to a specified time delay. Watchdog timers are maintained as part of the system clock ISR. For information about POSIX timers, see 3.2 POSIX Clocks and Timers.

Functions invoked by watchdog timers execute as interrupt service code at the interrupt level of the system clock. However, if the kernel is unable to execute the function immediately for any reason (such as a previous interrupt or kernel state), the function is placed on the tExcTask work queue. Functions on the tExcTask work queue execute at the priority level of the tExcTask (usually 0).

Restrictions on ISRs apply to routines connected to watchdog timers. The functions in Table 2-19 are provided by the wdLib library.

Table 2-19:   Watchdog Timer Calls


Call
Description

wdCreate( )
Allocates and initializes a watchdog timer.
wdDelete( )
Terminates and deallocates a watchdog timer.
wdStart( )
Starts a watchdog timer.
wdCancel( )
Cancels a currently counting watchdog timer.

A watchdog timer is first created by calling wdCreate( ). Then the timer can be started by calling wdStart( ), which takes as arguments the number of ticks to delay, the C function to call, and an argument to be passed to that function. After the specified number of ticks have elapsed, the function is called with the specified argument. The watchdog timer can be canceled any time before the delay has elapsed by calling wdCancel( ).

Example 2-4:   Watchdog Timers

/* Creates a watchdog timer and sets it to go off in 3 seconds.*/ 
 
/* includes */ 
#include "vxWorks.h" 
#include "logLib.h" 
#include "wdLib.h" 
 
/* defines */ 
#define  SECONDS (3) 
 
WDOG_ID myWatchDogId; 
task (void) 
    { 
    /* Create watchdog */ 
    if ((myWatchDogId = wdCreate( )) == NULL) 
        return (ERROR); 
 
    /* Set timer to go off in SECONDS - printing a message to stdout */ 
    if (wdStart (myWatchDogId, sysClkRateGet( ) * SECONDS, logMsg,  
                 "Watchdog timer just expired\n") == ERROR) 
        return (ERROR); 
    /* ... */ 
    }


2.6    Interrupt Service Code: ISRs

Hardware interrupt handling is of key significance in real-time systems, because it is usually through interrupts that the system is informed of external events. For the fastest possible response to interrupts, VxWorks runs interrupt service routines (ISRs) in a special context outside of any task's context. Thus, interrupt handling involves no task context switch. Table 2-20 lists the interrupt routines provided in intLib and intArchLib.

For boards with an MMU, the optional product VxVMI provides write protection for the interrupt vector table; see 12. Virtual Memory Interface.

Table 2-20:   Interrupt Routines


Call
Description

intConnect( )
Connects a C routine to an interrupt vector.
intContext( )
Returns TRUE if called from interrupt level.
intCount( )
Gets the current interrupt nesting depth.
intLevelSet( )
Sets the processor interrupt mask level.
intLock( )
Disables interrupts.
intUnlock( )
Re-enables interrupts.
intVecBaseSet( )
Sets the vector base address.
intVecBaseGet( )
Gets the vector base address.
intVecSet( )
Sets an exception vector.
intVecGet( )
Gets an exception vector.

2.6.1   Connecting Routines to Interrupts

You can use system hardware interrupts other than those used by VxWorks. VxWorks provides the routine intConnect( ), which allows C functions to be connected to any interrupt. The arguments to this routine are the byte offset of the interrupt vector to connect to, the address of the C function to be connected, and an argument to pass to the function. When an interrupt occurs with a vector established in this way, the connected C function is called at interrupt level with the specified argument. When the interrupt handling is finished, the connected function returns. A routine connected to an interrupt in this way is called an interrupt service routine (ISR).

Interrupts cannot actually vector directly to C functions. Instead, intConnect( ) builds a small amount of code that saves the necessary registers, sets up a stack entry (either on a special interrupt stack, or on the current task's stack) with the argument to be passed, and calls the connected function. On return from the function it restores the registers and stack, and exits the interrupt; see Figure 2-16.

Figure 2-16:   Routine Built by intConnect( )

For target boards with VME backplanes, the BSP provides two standard routines for controlling VME bus interrupts, sysIntEnable( ) and sysIntDisable( ).

2.6.2   Interrupt Stack

All ISRs use the same interrupt stack. This stack is allocated and initialized by the system at start-up according to specified configuration parameters. It must be large enough to handle the worst possible combination of nested interrupts.

Some architectures, however, do not permit using a separate interrupt stack. On such architectures, ISRs use the stack of the interrupted task. If you have such an architecture, you must create tasks with enough stack space to handle the worst possible combination of nested interrupts and the worst possible combination of ordinary nested calls. See the reference entry for your BSP to determine whether your architecture supports a separate interrupt stack.

Use the checkStack( ) facility during development to see how close your tasks and ISRs have come to exhausting the available stack space.

2.6.3   Writing and Debugging ISRs

There are some restrictions on the routines you can call from an ISR. For example, you cannot use routines like printf( ), malloc( ), and semTake( ) in your ISR. You can, however, use semGive( ), logMsg( ), msgQSend( ), and bcopy( ). For more information, see 2.6.4 Special Limitations of ISRs.

2.6.4   Special Limitations of ISRs

Many VxWorks facilities are available to ISRs, but there are some important limitations. These limitations stem from the fact that an ISR does not run in a regular task context and has no task control block, so all ISRs share a single stack.

Table 2-21:   Routines that Can Be Called by Interrupt Service Routines


Library
Routines

bLib
All routines
errnoLib
errnoGet( ), errnoSet( )
fppArchLib
fppSave( ), fppRestore( )
intLib
intContext( ), intCount( ), intVecSet( ), intVecGet( )
intArchLib
intLock( ), intUnlock( )
logLib
logMsg( )
lstLib
All routines except lstFree( )
mathALib
All routines, if fppSave( )/fppRestore( ) are used
msgQLib
msgQSend( )
pipeDrv
write( )
rngLib
All routines except rngCreate( ) and rngDelete( )
selectLib
selWakeup( ), selWakeupAll( )
semLib
semGive( ) except mutual-exclusion semaphores, semFlush( )
sigLib
kill( )
taskLib
taskSuspend( ), taskResume( ), taskPrioritySet( ), taskPriorityGet( ), taskIdVerify( ), taskIdDefault( ), taskIsReady( ), taskIsSuspended( ), taskTcb( )
tickLib
tickAnnounce( ), tickSet( ), tickGet( )
tyLib
tyIRd( ), tyITx( )
vxLib
vxTas( ), vxMemProbe( )
wdLib
wdStart( ), wdCancel( )

For this reason, the basic restriction on ISRs is that they must not invoke routines that might cause the caller to block. For example, they must not try to take a semaphore, because if the semaphore is unavailable, the kernel tries to switch the caller to the pended state. However, ISRs can give semaphores, releasing any tasks waiting on them.

Because the memory facilities malloc( ) and free( ) take a semaphore, they cannot be called by ISRs, and neither can routines that make calls to malloc( ) and free( ). For example, ISRs cannot call any creation or deletion routines.

ISRs also must not perform I/O through VxWorks drivers. Although there are no inherent restrictions in the I/O system, most device drivers require a task context because they might block the caller to wait for the device. An important exception is the VxWorks pipe driver, which is designed to permit writes by ISRs.

VxWorks supplies a logging facility, in which a logging task prints text messages to the system console. This mechanism was specifically designed for ISR use, and is the most common way to print messages from ISRs. For more information, see the reference entry for logLib.

An ISR also must not call routines that use a floating-point coprocessor. In VxWorks, the interrupt driver code created by intConnect( ) does not save and restore floating-point registers; thus, ISRs must not include floating-point instructions. If an ISR requires floating-point instructions, it must explicitly save and restore the registers of the floating-point coprocessor using routines in fppArchLib.

All VxWorks utility libraries, such as the linked-list and ring-buffer libraries, can be used by ISRs. As discussed earlier (2.2.6 Task Error Status: errno), the global variable errno is saved and restored as a part of the interrupt enter and exit code generated by the intConnect( ) facility. Thus, errno can be referenced and modified by ISRs as in any other code. Table 2-21 lists routines that can be called from ISRs.

2.6.5   Exceptions at Interrupt Level

When a task causes a hardware exception such as an illegal instruction or bus error, the task is suspended and the rest of the system continues uninterrupted. However, when an ISR causes such an exception, there is no safe recourse for the system to handle the exception. The ISR has no context that can be suspended. Instead, VxWorks stores the description of the exception in a special location in low memory and executes a system restart.

The VxWorks boot programs test for the presence of the exception description in low memory and if it is detected, display it on the system console. The e command in the boot ROMs re-displays the exception description; see Tornado User's Guide: Setup and Startup.

One example of such an exception is the following message:

workQPanic: Kernel work queue overflow.

This exception usually occurs when kernel calls are made from interrupt level at a very high rate. It generally indicates a problem with clearing the interrupt signal or a similar driver problem.

2.6.6   Reserving High Interrupt Levels

The VxWorks interrupt support described earlier in this section is acceptable for most applications. However, on occasion, low-level control is required for events such as critical motion control or system failure response. In such cases it is desirable to reserve the highest interrupt levels to ensure zero-latency response to these events. To achieve zero-latency response, VxWorks provides the routine intLockLevelSet( ), which sets the system-wide interrupt-lockout level to the specified level. If you do not specify a level, the default is the highest level supported by the processor architecture. For information about architecture-specific implementations of intLockLevelSet( ), see the appropriate VxWorks architecture supplement.


*      
CAUTION: Some hardware prevents masking certain interrupt levels; check the hardware manufacturer's documentation.

2.6.7   Additional Restrictions for ISRs at High Interrupt Levels

ISRs connected to interrupt levels that are not locked out (either an interrupt level higher than that set by intLockLevelSet( ), or an interrupt level defined in hardware as non-maskable) have special restrictions:

  • The ISR can be connected only with intVecSet( ).

  • The ISR cannot use any VxWorks operating system facilities that depend on interrupt locks for correct operation. The effective result is that the ISR cannot safely make any call to any VxWorks function, except reboot.

For more information, see the VxWorks architecture supplement document for the architecture in question.


*      
WARNING: The use of NMI with any VxWorks functionality, other than reboot, is not recommended. Routines marked as "interrupt safe" do not imply they are NMI safe and, in fact, are usually the very ones that NMI routines must not call (because they typically use intLock( ) to achieve the interrupt safe condition).

2.6.8   Interrupt-to-Task Communication

While it is important that VxWorks support direct connection of ISRs that run at interrupt level, interrupt events usually propagate to task-level code. Many VxWorks facilities are not available to interrupt-level code, including I/O to any device other than pipes. The following techniques can be used to communicate from ISRs to task-level code:

  • Shared Memory and Ring Buffers.  

ISRs can share variables, buffers, and ring buffers with task-level code.

  • Semaphores.  

ISRs can give semaphores (except for mutual-exclusion semaphores and VxMP shared semaphores) that tasks can take and wait for.

  • Message Queues.  

ISRs can send messages to message queues for tasks to receive (except for shared message queues using VxMP). If the queue is full, the message is discarded.

  • Pipes.  

ISRs can write messages to pipes that tasks can read. Tasks and ISRs can write to the same pipes. However, if the pipe is full, the message written is discarded because the ISR cannot block. ISRs must not invoke any I/O routine on pipes other than write( ).

  • Signals.  

ISRs can "signal" tasks, causing asynchronous scheduling of their signal handlers.


1:  For example, a network interface, an HDLC, and so on.