2.3   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.

2.3.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 apparently independent program is called a task. 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:

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 7. Virtual Memory Interface.

2.3.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 the 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.

The wind kernel states are shown in the state transition diagram in Figure 2-1, and a summary of the corresponding state symbols you will see when working with Tornado development tools is shown in Table 2-1.

Table 2-1:  Task State Transitions


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. 

             

2.3.3   Wind Task Scheduling

Multitasking requires a scheduling algorithm to allocate the CPU to ready tasks. Priority-based preemptive scheduling is the default algorithm in wind, but you can select round-robin scheduling for your applications as well. The routines listed in Table 2-2 control task scheduling.

Table 2-2:  Task Scheduler Control Routines


Call
Description

kernelTimeSlice( )  
Control round-robin scheduling. 
taskPrioritySet( )  
Change the priority of a task. 
taskLock( )  
Disable task rescheduling. 
taskUnlock( )  
Enable task rescheduling. 

Preemptive Priority Scheduling

With a preemptive priority-based scheduler, each task has a priority and the kernel ensures that the CPU is allocated to the highest priority task that is ready to run. This scheduling method is preemptive in that if a task that has higher priority than 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. 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.

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; however, while executing, a task can change its priority using taskPrioritySet( ). The ability to change task priorities dynamically allows applications to track precedence changes in the real world.

Round-Robin Scheduling

Preemptive priority scheduling can be augmented with round-robin scheduling. A round-robin scheduling algorithm attempts to share the CPU fairly among all ready tasks of the same priority. Without round-robin scheduling, when multiple tasks of equal priority must share the processor, a single task can usurp the processor by never blocking, thus never giving other equal-priority tasks a chance to run.

Round-robin scheduling achieves fair allocation of the CPU to tasks of the same priority by an approach known as time slicing. Each task of a group of tasks executes for a defined interval, or time slice; then another task executes for an equal interval, in rotation. The allocation is fair in that no task of a priority group gets a second slice of time before the other tasks of a group are given a slice.

Round-robin scheduling can be enabled with the routine 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.

More precisely, a run-time counter is kept for each task and incremented on every clock tick. When the specified time-slice interval is completed, the counter is cleared and the task is placed at the tail of the queue of tasks at its priority. New tasks joining a priority group are placed at the tail of the group with a run-time counter initialized to zero.

If a task is preempted by a higher priority task during its interval, its run-time count is saved and then restored when the task is again eligible for execution. 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.

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.4.2 Mutual Exclusion.

2.3.4   Tasking Control

The following sections give an overview of the basic VxWorks tasking routines, which are found in the VxWorks library taskLib. These routines provide the means for task creation, control, and information. See the reference entry for taskLib for further discussion. For interactive use, you can control VxWorks tasks from the host-resident shell; see the Tornado User's Guide: Shell.

Task Creation and Activation

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

Table 2-3:  Task Creation Routines


Call
Description

taskSpawn( )  
Spawn (create and activate) a new task. 
taskInit( )  
Initialize a new task. 
taskActivate( )  
Activate an initialized task. 

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

id = taskSpawn ( name, priority, options, stacksize, main, arg1, arg10 );

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.

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 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.

A task name should not conflict with any existing task name. 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 letter t and task names started from the host with the letter 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 increases by one for each unnamed task that is spawned.


*

NOTE: In the shell, task names are resolved to their corresponding task IDs to simplify interaction with existing tasks; see the Tornado User's Guide: Shell.

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

Table 2-4:  Task Name and ID Routines


Call
Description

taskName( )  
Get the task name associated with a task ID. 
taskNameToId( )  
Look up the task ID associated with a task name. 
taskIdSelf( )  
Get the calling task's ID. 
taskIdVerify( )  
Verify the existence of a specified task. 

Task Options

When a task is spawned, an option parameter is specified by performing a logical OR operation on the desired options, listed in the following table. Note that VX_FP_TASK must be specified if the task performs any floating-point operations.

Table 2-5:  Task Options


Name
Hex Value
Description

VX_FP_TASK  
0x8 
Execute with the floating-point coprocessor. 
VX_NO_STACK_FILL  
0x100 
Do not fill stack with 0xee. 
VX_PRIVATE_ENV  
0x80 
Execute task with a private environment. 
VX_UNBREAKABLE  
0x2 
Disable breakpoints for the task. 

To create a task that includes floating-point operations, use:

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

Task options can also be examined and altered after a task is spawned by means of 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( )  
Examine task options. 
taskOptionsSet( )  
Set 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 called. The state of a task is dynamic, and 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( )  
Fill an array with the IDs of all active tasks. 
taskInfoGet( )  
Get information about a task. 
taskPriorityGet( )  
Examine the priority of a task. 
taskRegsGet( )  
Examine a task's registers. 
taskRegsSet( )  
Set a task's registers. 
taskIsSuspended( )  
Check if a task is suspended. 
taskIsReady( )  
Check if a task is ready to run. 
taskTcb( )  
Get a pointer to 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 protect tasks from unexpected deletion.


*

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

Tasks implicitly call exit( ) if the entry routine specified during task creation returns. Alternatively, a task can explicitly call exit( ) at any point to kill itself. A task can kill another task by calling taskDelete( ).

Table 2-8:  Task-Deletion Routines


Call
Description

exit( )  
Terminate the calling task and free memory (task stacks and task control blocks only).1  
taskDelete( )  
Terminate a specified task and free memory (task stacks and task control blocks only).* 
taskSafe( )  
Protect the calling task from deletion. 
taskUnsafe( )  
Undo a taskSafe( ) (make 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.

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.

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." Protection operates only on the calling task. 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 
    . 
    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( )  
Suspend a task. 
taskResume( )  
Resume a task. 
taskRestart( )  
Restart a task. 
taskDelay( )  
Delay a task; delay units are ticks. 
nanosleep( ) 
Delay a task; delay units are nanoseconds. 

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. The Tornado shell also uses this mechanism to restart itself in response to a task-abort request; for information, see the Tornado User's Guide: Shell.

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 2.7 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.

2.3.5   Tasking Extensions

To allow additional task-related facilities to be added to the system without modifying the kernel, wind provides task create, switch, and delete hooks, which 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( ) 
Add a routine to be called at every task create. 
taskCreateHookDelete( ) 
Delete a previously added task create routine. 
taskSwitchHookAdd( ) 
Add a routine to be called at every task switch. 
taskSwitchHookDelete( ) 
Delete a previously added task switch routine. 
taskDeleteHookAdd( ) 
Add a routine to be called at every task delete. 
taskDeleteHookDelete( ) 
Delete a previously added task delete routine. 

User-installed switch hooks are called within the kernel context. Thus, switch hooks 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( ) 

2.3.6   POSIX Scheduling Interface

The POSIX 1003.1b scheduling routines, provided by schedPxLib, are shown in Table 2-12. These routines let you use a portable interface to get and set task priority, get the scheduling policy, get the maximum and minimum priority for tasks, and if round-robin scheduling is in effect, get the length of a time slice. To understand how to use the routines in this alternative interface, be aware of the minor differences between the POSIX and Wind methods of scheduling.

Differences Between POSIX and Wind Scheduling

POSIX and Wind scheduling routines differ in the following ways:

Tasks and processes are alike in that they can be scheduled independently.

The POSIX scheduling routines are included when INCLUDE_POSIX_SCHED is selected for inclusion in the project facility VxWorks view; see Tornado User's Guide: Projects for information on configuring VxWorks.

Table 2-12:  POSIX Scheduling Calls


Call
Description

sched_setparam( ) 
Set a task's priority. 
sched_getparam( ) 
Get the scheduling parameters for a specified task. 
sched_setscheduler( ) 
Set scheduling policy and parameters for a task. 
sched_yield( ) 
Relinquish the CPU. 
sched_getscheduler( ) 
Get the current scheduling policy. 
sched_get_priority_max( ) 
Get the maximum priority. 
sched_get_priority_min( ) 
Get the minimum priority. 
sched_rr_get_interval( ) 
If round-robin scheduling, get the time slice length. 

Getting and Setting POSIX Task Priorities

The routines sched_setparam( ) and sched_getparam( ) set and get a task's priority, respectively. Both routines take a task ID and a sched_param structure (defined in installDir/target/h/sched.h). A task ID of 0 sets or gets the priority for the calling task. The sched_priority member of the sched_param structure specifies the new task priority when sched_setparam( ) is called. The routine sched_getparam( ) fills in the sched_priority with the specified task's current priority.

Example 2-1:  Getting and Setting POSIX Task Priorities

/* This example sets the calling task's priority to 150, then verifies 
 * that priority. To run from the shell, spawn as a task: 
 *   -> sp priorityTest 
 */
/* includes */ #include "vxWorks.h" #include "sched.h"
/* defines */ #define PX_NEW_PRIORITY 150
STATUS priorityTest (void) { struct sched_param myParam; /* initialize param structure to desired priority */
myParam.sched_priority = PX_NEW_PRIORITY; if (sched_setparam (0, &myParam) == ERROR) { printf ("error setting priority\n"); return (ERROR); }
/* demonstrate getting a task priority as a sanity check; ensure it * is the same value that we just set. */
if (sched_getparam (0, &myParam) == ERROR) { printf ("error getting priority\n"); return (ERROR); }
if (myParam.sched_priority != PX_NEW_PRIORITY) { printf ("error - priorities do not match\n"); return (ERROR); } else printf ("task priority = %d\n", myParam.sched_priority);
return (OK); }

The routine sched_setscheduler( ) is designed to set both scheduling policy and priority for a single POSIX process (which corresponds in most other cases to a single Wind task). In the VxWorks kernel, sched_setscheduler( )controls only task priority, because the kernel does not allow tasks to have scheduling policies that differ from one another. If its policy specification matches the current system-wide scheduling policy, sched_setscheduler( ) sets only the priority, thus acting like sched_setparam( ). If its policy specification does not match the current one, sched_setscheduler( ) returns an error.

The only way to change the scheduling policy is to change it for all tasks; there is no POSIX routine for this purpose. To set a system-wide scheduling policy, use the Wind function kernelTimeSlice( ) described in Round-Robin Scheduling.

Getting and Displaying the Current Scheduling Policy

The POSIX routine sched_getscheduler( ) returns the current scheduling policy. There are two valid scheduling policies in VxWorks: preemptive priority scheduling (in POSIX terms, SCHED_FIFO) and round-robin scheduling by priority (SCHED_RR).

Example 2-2:  Getting POSIX Scheduling Policy

/* This example gets the scheduling policy and displays it. */ 
 
/* includes */
#include "vxWorks.h" #include "sched.h"
STATUS schedulerTest (void) { int policy;
if ((policy = sched_getscheduler (0)) == ERROR) { printf ("getting scheduler failed\n"); return (ERROR); }
/* sched_getscheduler returns either SCHED_FIFO or SCHED_RR */
if (policy == SCHED_FIFO) printf ("current scheduling policy is FIFO\n"); else printf ("current scheduling policy is round robin\n");
return (OK); }

Getting Scheduling Parameters: Priority Limits and Time Slice

The routines sched_get_priority_max( ) and sched_get_priority_min( ) return the maximum and minimum possible POSIX priority values, respectively.

If round-robin scheduling is enabled, you can use sched_rr_get_interval( ) to determine the length of the current time-slice interval. This routine( )takes as an argument a pointer to a timespec structure (defined in time.h), and writes the number of seconds and nanoseconds per time slice to the appropriate elements of that structure.

Example 2-3:  Getting the POSIX Round-Robin Time Slice

/* The following example checks that round-robin scheduling is enabled, 
 * gets the length of the time slice, and then displays the time slice. 
 */
/* includes */
#include "vxWorks.h" #include "sched.h"
STATUS rrgetintervalTest (void) { struct timespec slice;
/* turn on round robin */
kernelTimeSlice (30);
if (sched_rr_get_interval (0, &slice) == ERROR) { printf ("get-interval test failed\n"); return (ERROR); }
printf ("time slice is %l seconds and %l nanoseconds\n", slice.tv_sec, slice.tv_nsec); return (OK); }

2.3.7   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.5.1 Connecting Application Code 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);

We recommend 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 an error, in the most significant two bytes, and use 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.3.8   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. Signals are also used for signaling software exceptions as well as hardware exceptions. They are described in more detail in 2.4.7 Signals and in the reference entry for sigLib.

2.3.9   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 particularly easy. Shared code also makes the system more efficient and easier to maintain; see Figure 2-4.

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, all routines which have a corresponding name_r( ) routine should be assumed non-reentrant. 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.

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.

Guarded Global and Static Variables

Some libraries encapsulate access to common data. One example is the memory allocation library, memLib, which manages pools of memory to be used by many tasks. This library declares and uses its own static data variables to keep track of pool allocation.

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 semaphore facility provided by semLib and described in 2.4.3 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.

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.

     

2.3.10   VxWorks System Tasks

VxWorks includes several system tasks, described below.

The Root Task: tUsrRoot

The root task, tUsrRoot, 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. You are free to add any necessary initialization to the root task. For more information, see 8.5 Configuring VxWorks.

The 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 3.5.3 Message Logging and the reference entry for logLib.

The Exception Task: tExcTask

The exception task, tExcTask, supports the VxWorks exception handling package by performing functions that cannot occur at interrupt level. 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.

The Network Task: tNetTask

The tNetTask daemon handles the task-level functions required by the VxWorks network.

The Target Agent Task: tWdbTask

The target agent task, tWdbTask, is created if the target agent is set to run in task mode; see 8.6.1 Scaling Down VxWorks. It services requests from the Tornado target server; for information on this server, see the Tornado User's Guide: Overview.

Tasks for Optional Components

The following VxWorks system tasks are created if their associated configuration constants are defined; for more information, see 8.5 Configuring VxWorks.

tShell
If you have included the target shell in the VxWorks configuration, it is spawned as this task. Any routine or task that is invoked from the target shell, rather than spawned, runs in the tShell context. For more information, see 9. Target Shell.

tRlogind
If you have included the target shell and the rlogin facility in the VxWorks configuration, this daemon allows remote users to log in to VxWorks. It accepts a remote login request from another VxWorks or host system and spawns tRlogInTask and tRlogOutTask. These tasks exist as long as the remote user is logged on. During the remote session, the shell's (and any other task's) input and output are redirected to the remote user. A tty-like interface is provided to the remote user through the use of the VxWorks pseudo-terminal driver, ptyDrv. For more information, see 3.7.1 Serial I/O Devices (Terminal and Pseudo-Terminal Devices) and the reference entry for ptyDrv.

tTelnetd
If you have included the target shell and the telnet facility in the VxWorks configuration, this daemon allows remote users to log in to VxWorks with telnet. It accepts a remote login request from another VxWorks or host system and spawns the input task tTelnetInTask and output task tTelnetOutTask. These tasks exist as long as the remote user is logged on. During the remote session, the shell's (and any other task's) input and output are redirected to the remote user. A tty-like interface is provided to the remote user through the use of the VxWorks pseudo-terminal driver, ptyDrv. See 3.7.1 Serial I/O Devices (Terminal and Pseudo-Terminal Devices) and the reference entry for ptyDrv for further explanation.

tPortmapd
If you have included the RPC facility in the VxWorks configuration, this daemon is an RPC server that acts as a central registrar for RPC servers running on the same machine. RPC clients query the tPortmapd daemon to find out how to contact the various servers.