|
1.
Introduction to VxWorks Programming
Some of the rudiments of VxWorks programming are presented here.
These do not appear in the same order presented in Ref
1, which should be read by any serious VxWorks programmer.
Rather they reenforce some of the material that can be found there,
but appear in a top-down order, with basic concepts described first,
followed by details.
1.1 Tasks
In VxWorks, the unit of execution is the task, corresponding
to a Unix/Linux process. Tasks may be spawned (i.e.,
created), deleted, resumed, suspended, and preempted (i.e., interrupted)
by other tasks, or may be delayed by the task itself. A task
has its own set of context registers including stack. The term
thread, while not unknown in VxWorks jargon, does not exist
in a formal sense as in other operating systems. A thread, when
the term is used, may be thought of as a sub-sequence of connected
program steps inside a task, such as the steps the VxWorks kernel
performs in spawning a task, or the sequence of instructions in an
else-clause following an if-statement. Tasks can communicate
with each other in a manner similar to Interprocess Communications
in Unix and Linux.
Tasks are in one of four states, diagrammed in Figure 1-1, adapted
from Ref. 1.

Figure 1-1 Task State Diagram
A newly spawned task enters the state diagram through the suspended
state.
Tasks may be scheduled for execution by assigning them priorities,
ranging from 0 (higest priority) to 255. Once started, that
is, having entered the ready state in Figure 1, a task may
execute to completion, or it may be assigned a fixed time slice in
round-robin scheduling. A task blocks (enters the pended
state) while another task takes control. A task may be prempted
because it has consumed its time slice, or because another task with
higher priority exists. Task priorities may be changed during execution.
A task may avoid being preempted by locking the scheduler while it
has control. (This does not prevent interrupts from occurring.)
A task may also enter the delayed state, for example while
waiting a fixed time between reading samples within a task before
processing them as a group, during which time another task may take
control. The delay is controlled by an external timer which
runs independently of processing (combined with a tick counter maintained
by the kernel) that awakens the delayed task and avoids having the
task tie up resources with an index counter which would prevent another
task from executing.
The suspended state is used to halt a task for debugging without
loss of its context registers.
Several system tasks exist concurrently with user defined tasks.
These are the root task, named tUsrRoot; the logging task tLogTask;
exception task tExcTask; and the network task tNetTask.
Intertask communication (corresponding to Unix/Linux Interprocess
Communication) can occur through semaphores that provide interlocking
and synchronization of tasks, and messaging that allow tasks to communicate
events or data with each other. Although semaphores and messaging
are implemented with different kernel mechanisms, it is customary
to treat them together.
Semphores can be categorized as ordinary binary semaphores
and a special class of binary semaphores called mutual exclusion
semaphores. Binary semaphores are used for task synchronization.
As implemented in VxWorks, a binary semaphore has two values: full
and empty. When full, it is available for a task. When
empty, it is unavailable. A pending task proceeds by taking
an available semaphore, which makes the semaphore empty or unavailable..
When the semaphore is no longer needed (because the task is about
to return to the pending state), it gives the semaphore which
makes it full or available for another task. A mutual
exclusion semaphore (also called a mutex) allow one task to
have exclusive use of a resource while it is needed.
The difference between an ordinary binary semaphore and a mutex semaphore
is in the way the semaphore is initialized. For an ordinary
binary semaphore, a task attempting to synchronize to an external
event creates an empty semaphore. When it reaches the point
to be synchronized, it attempts to take a semaphore. If unavailable,
it waits at this point. A second task which controls the synchronization
event gives the semaphore when it is no longer needed. The first
task receives notification that the semaphore is available and proceeds.
For a mutex semaphore, a task wishing to block other tasks from a
resouce first creates a full semaphore, and then takes the semaphore,
making it unavailable to other tasks. When it is no longer needed,
it the task gives the semaphore, making the resource available.
A mutual exclusion semaphore must have matching "takes"
and "gives".
These ideas can be illustrated with the following code segments.
Example of synchronization through binary semaphore
#include "vxWorks.h"
#include "semLib.h"
#define T_PRIORITY 50
SEM_ID syncExampleSem; // named
semaphore object
void initialize (void)
{
// set up FIFO queue with emtpy
binary semaphore
syncSem = semBCreate (SEM_Q_FIFO, SEM_EMPTY);
// create task1
taskSpawn ("task1", T_PRIORITY, 0, 10000,
task1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
// create task2
taskSpawn ("task2", T_PRIORITY, 0, 10000,
task2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
void task1 (void)
{
// stay here until semaphore becomes available
semTake (syncExampleSem, WAIT_FOREVER);
// do something
}
void task2 (void)
{
// do something
// now let task1 execute
semGive (synExampleSem);
} Example of resource
lockout through mutual exclusion semaphore
#include "vxWorks.h"
#include "semLib.h"
#define T_HI_PRIORITY 20
#define T_LO_PRIORITY 200
SEM_ID semMutex; // named semaphore
object
char alphabet[27]; // memory resource to have
exclusive access
void initialize (void)
{
//create binary semaphore
which is initially full
semMutex = semBCreate (SEM_Q_PRIORITY,
SEM_FULL);
// spawn high priority task
taskSpawn ("hi_priority_task", T_HI_PRIORITY, 10000, tHiPriorityTask,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
// spawn low priority task
taskSpawn ("lo_priority_task", T_LO_PRIORITY, 10000, tLoPriorityTask,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
void
tHiPriorityTask (void)
{
int i;
// enter critical region - any other tasks wanting access to alphabet[]
should
// wait for available semaphore
semTake (semMutex, WAIT_FOREVER);
// write alphabet to global array
for (i= 0; i < 26; i++)
alphabet[i] = 'A' + i;
alphabet[i] = '\0';
// leave critical region
semGive (semMutex);
}
void
tLoPriorityTask (void)
{
// enter critical region
semTake (semMutex, WAIT_FOREVER);
// array members guaranteed stable while being read by this task
printf ("alphabet= %s ", alphabet);
// leave critical region
semGive (semMutex);
}
A potential
problem can occur in the second example. Suppose a third task
with medium priority, say 50, between the high and low priority tasks
is also spawned, but doesn't require access to the mutually excluded
region. Assume that the high priority task is called repetitively
from somewhere else, requiring it to enter and leave the critical
region each time it's executed. This medium priority task will
preempt the low priority task, because of the difference in priorities.
If the third task is overly long, then the low priority task, which
reads the alphabet sequence is delayed in releasing its access to
the critical area, holding up execution of the high priority task,
as illustrated schematically in Figure 1-2. The medium priority
task has effectively assumed higher priority than the high priority
task. This is an example of priority inversion.

Figure 1-2 Priority Inversion
Problem
The solution is to promote
the low priority task temporarily to the same priority as the highest
priority task which prevents the low priority task from being preempted
by the medium priority task. The low priority task creates a
"inversion-safe" mutex semaphore which permits it to assume
the temporarily higher priority. Once it reaches this priority,
it remains here until all mutex semaphores owned by the task have
been released.
POSIX semaphores, not discussed here, may also be
used in VxWorks programs.
1.2 Messaging
Closely related to the idea of semaphores is
the concept of the message which passes data between tasks.
Messaging could be used to accomplish task interlocking as well, but setting
up a message consumes more time than initializing a semaphore. Messaging
is useful for passing variables between asynchronous tasks. VxWorks
supplies seven functions for messaging: msgQCreate ( ), msgQDelete ( ),
msgQSend ( ), msgQReceive ( ), msgQNumMsgs ( ), msgQShow ( ), msgQInfoGet
( ). A message may be placed ahead of previous
messages by sending it with the attribute MSG_PRI_URGENT. As for
semaphores, POSIX messages may also be used in VxWorks programs, and in
fact, offer some capabilities that VxWorks messages don't possess.
The following fragment illustrates a common use for a message.
We will illustrate using VxWorks messges. The procedure is similar
for POSIX messages. Task 1 has written data to a file whose name
is not known to other tasks, and wishes to inform Task 2 that the data
is available in the specified file.
#define MAX_MSGS 10
#define MAX_MSG_LEN 50
task1 (void)
{
char strFileName
[MAX_MSG_LEN];
strcpy (strFileName, "./filename1");
// write to file with name strFileName
and fd (file descriptor) handle file1
...
fclose (file1);
// create message
queue
if ( (exampleMsgId = msgQCreate (MAX_MSGS,
MAX_MSG_LEN, MSQ_PRI_NORMAL) ) == NULL)
return ERROR;
if (msgQSend (exampleMsgId, strFileName,
sizeof (strFileName) + 1, WAIT_FOREVER) == ERROR)
return ERROR;
}
task2
(void)
{
char msgBuf [MAX_MSG_LEN];
// fetch message from queue
if (msqQReceive (exampleMsgId, msgBuf, MAX_MSG_LEN,
WAIT_FOREVER) == ERROR)
return ERROR;
// open file for reading
...
}
There are two things to note. The second
task may query the message exampleMsgId before it has been initialized.
But since the object is known to exist (it is a global variable),
this does not cause an error. Also note that the sender has closed
the file before informing the receiver of its name. This approach
requires sending the file's fully qualified path name as a possibly long
string, which consumes time at the sending and receiving ends. The
programmer could send the file's handle as message data, but this
would require that the writing task keep the file open for reading by
another task and leads to a new set of interlock problems which are best
avoided.
1.3 Input/Output
In VxWorks, an input/output data stream in treated
as a file regardless of the I/O device. File I/O can be organized
by block or by character. Character devices include display terminals
and external hardware devices such as A/D converters or real-time clocks.
Block devices include local or network disk drives. File I/O can
be real, as in the previous examples, or virtual as in the case of pipes
and network sockets. Pipes enable tasks to communicate with each
other. The pipe driver is called pipeDrv. If the device
stream extends across a netowork, it becomes a socket. The stream
socket is similar in concept to the Unix or Windows TCP/IP socket.
(For TCP/IP, VxWorks uses Berkeley Software Distribution version. 4.x
Unix socket functions.)
A file is handled by its file descriptor fd
corresonding to a FILE structure in POSIX. For each kind of file,
there is a different kind of driver which permits the following I/O operations,
summarized in the following.
1.3.1 Basic I/O Operations
| creat (const char *name, int flag
) |
Create a file. |
| open (const char *name, int
flags, int mode) |
Open (and possibly create) a file. |
| close ( int fd) |
Close a file. |
| remove ( const char *name) |
Remove a file. |
| read ( int fd, char *buffer, size_t maxbytes) |
Read an existing file. |
| write (int fd, char *buffer, size_t nbytes ) |
Write to an existing file. |
| ioctl (int fd, int function, int
arg ) |
Perform control operations on a file. |
BASIC I/O OPERATIONS
Note: mode specifies the file's permissions.
The string name denotes what kind of device the file is.
When a file is created or opened, the I/O system searches through a list
of device names, and physical file directories for at least a partial
beginning match of the names and a file descriptor is returned if a match
is established. If no match is found, a default device can be specified,
or if no default is specified (the more common case), then the I/O system
reports an error. Thereafter, the basic I/O operations specified
previously are mapped to the specific file's I/O routines. For instance,
read ( ) and write ( ) result in low-level calls to xxread( ) and
xxwrite ( ), which are defined in the device's driver in the case
of physical devices or in the local file system routines in the case of
disk files.
1.3.2 Devices and Files
Two kinds of databases are maintained for I/O operations:
the Driver Table and the File Descriptor (FD) Table. The Driver
Table has entries for character devices, and the FD Table has entries
for block devices.
Each record in the Driver Table corresponds to a device
descriptor. A device descriptor describes a specific device,
for example a particular Ethernet port. The Driver Table is built
at boot by adding the device descriptors, which form a linked list.
The information in a device descriptor contains (a) the device name string,
e.g., "/tyE2/0"; (b) the corresponding record number in the Driver Table,
an integer assigned at the time the device is added (see below); (c) the
names of device-specific routines which implement the seven basic I/O
operations above. These will have the same names as the operation
prefixed by the device name, for example, ether3creat ( ) and ether3write
( ). These low level functions are implented in a device driver
file for the particular device. It is important to note that a different
driver exists for each physical device, which may use similar functions
for another driver (of an identical device). If Ether 1 and Ether
2 are the names of two devices, then after installation, there will be
an implentation of ether1creat ( ), and so forth, and ether2creat ( ),
et al.
The installation of a character device at boot is a
two-step process. A driver for the device is installed with iosDrvInstall
( ) which returns a record number
in the Driver Table where the driver was installed. The device is
added to the linked list of device descriptors (which contain the information
in the
specific Driver Table record) with a call to iosDevAdd ( ). Both
operations must be completed before the device is installed.
The following table summarizes the type of I/O
device (or file) and the library where its driver(s) is(are) defined.
If the type of device is present, then the device's library should be
included in the VxWorks build.
| FILE/DEVICE |
LIBRARY |
| Local File Systems |
|
| MSDOS (16-bit FAT), dosFs |
dosFsLib |
| MSDOS (32-bit FAT), dosFs32 |
dosFs32Lib |
| RT-11 |
rt11FsLib |
| Raw File System, rawFs |
rawFsLib |
| Tape File System, tapeFs |
tapeFsLib |
| CD-ROM, cdromFs |
cdromFsLib |
| Virtual Devices |
|
| pipes |
pipeDrv |
| memory I/O (non-file) |
memDrv |
| RAM files |
ramDrv |
| Network File System (NFS) |
nfsDrv |
| Physical Devices |
|
| Terminal driver |
ttyDrv |
| Pseudo-terminal driver |
ptyDrv |
| SCSI |
scsiLib sysLib.c |
|