VxWorks Programming Guide: Tasks, IPC, I/O, Semaphores
VxWorks provides a broad set of C APIs for real-time task management, synchronization, inter-task communication, device I/O, networking, timers, signals, and interrupt handling.
Most VxWorks applications are built by combining a relatively small set of core libraries, including taskLib, msgQLib, semLib, ioLib, wdLib, eventLib, and the socket APIs.
This guide provides a practical reference for the most commonly used VxWorks programming interfaces and highlights how the major primitives differ in real-time applications.
๐ Documentation and Common Headers #
VxWorks Programming Guide #
The VxWorks installation typically includes the programming documentation under:
\docs\vxworks\guide\index.html
The exact installation path depends on the VxWorks and development-environment version.
Commonly Used Headers #
A typical VxWorks application may use headers such as:
#include "taskLib.h"
#include "msgQLib.h"
#include "semLib.h"
#include "ioLib.h"
#include "wdLib.h"
#include "logLib.h"
#include "socket.h"
Additional functionality requires the corresponding subsystem headers, such as:
#include "selectLib.h"
#include "eventLib.h"
#include "sigLib.h"
#include "errnoLib.h"
๐พ VxWorks I/O System #
VxWorks uses a unified I/O abstraction for many device types. Serial ports, files, pipes, pseudo-devices, and other I/O resources can commonly be accessed through file descriptors.
The general workflow is:
Create or register device
|
v
open()
|
v
read() / write() / ioctl()
|
v
close()
Core I/O Functions #
| Function | Purpose |
|---|---|
creat() |
Create a file or device instance |
open() |
Open a file or device and obtain a descriptor |
read() |
Read data from a descriptor |
write() |
Write data to a descriptor |
ioctl() |
Perform device-specific control operations |
close() |
Close a descriptor |
remove() |
Remove a file or device entry |
A typical descriptor-based operation looks like:
int fd;
fd = open("/some/device", O_RDWR, 0);
if (fd != ERROR)
{
/* read(), write(), or ioctl() */
close(fd);
}
The actual pathname and supported operations depend on the installed VxWorks device driver.
Pseudo-Memory Devices with memDrv
#
The memory device driver provides a pseudo-file interface backed by RAM. It can be useful when software needs file-like I/O semantics without a physical storage device.
Relevant APIs include:
memDrv()
memDevCreate()
memDevCreateDir()
memDevDelete()
Example:
void memDevExample(void)
{
unsigned char buffer[1024];
int fd;
int data = 123;
memDrv();
memDevCreate("/mem/mem1", (char *)buffer, sizeof(buffer));
fd = open("/mem/mem1", O_RDWR, 0644);
if (fd != ERROR)
{
write(fd, (char *)&data, sizeof(data));
/* Perform additional operations. */
close(fd);
}
memDevDelete("/mem/mem1");
}
The lifetime of the backing memory must be managed carefully. In this example, buffer is automatic storage, so a real application should ensure that the memory remains valid for the entire lifetime of the pseudo-device.
๐ I/O Multiplexing with select()
#
The select() API allows a task to wait for activity across multiple file descriptors.
It is particularly useful when a task needs to monitor several I/O channels without dedicating a separate task to each descriptor.
The general prototype is:
int select(
int width,
fd_set *pReadFds,
fd_set *pWriteFds,
fd_set *pExceptFds,
struct timeval *pTimeOut
);
Descriptor Set Macros #
The primary descriptor-set operations are:
FD_SET(fd, &fdset);
FD_CLR(fd, &fdset);
FD_ZERO(&fdset);
FD_ISSET(fd, &fdset);
Their purposes are:
| Macro | Purpose |
|---|---|
FD_SET() |
Add a descriptor to a set |
FD_CLR() |
Remove a descriptor from a set |
FD_ZERO() |
Initialize or clear a descriptor set |
FD_ISSET() |
Test whether a descriptor was reported as ready |
Example:
void selectExample(void)
{
fd_set readFds;
int fds[4];
int width = 0;
int i;
fds[0] = open("/tyCo/0", O_RDWR, 0);
fds[1] = open("/tyCo/1", O_RDWR, 0);
fds[2] = open("/pipe/p1", O_RDWR, 0);
fds[3] = open("/mem/m1", O_RDWR, 0);
for (i = 0; i < 4; i++)
{
if (fds[i] != ERROR && fds[i] > width)
width = fds[i];
}
width++;
FD_ZERO(&readFds);
for (i = 0; i < 4; i++)
{
if (fds[i] != ERROR)
FD_SET(fds[i], &readFds);
}
if (select(width, &readFds, NULL, NULL, NULL) == ERROR)
{
for (i = 0; i < 4; i++)
{
if (fds[i] != ERROR)
close(fds[i]);
}
return;
}
for (i = 0; i < 4; i++)
{
if (fds[i] != ERROR && FD_ISSET(fds[i], &readFds))
{
/* Process incoming data on fds[i]. */
}
}
}
When using select(), the width argument is conventionally the highest descriptor value plus one.
๐งต Task Management with taskLib
#
VxWorks tasks are the fundamental execution units used to implement concurrent application behavior.
Common task-management functions include:
| Function | Description |
|---|---|
taskSpawn() |
Create and activate a task |
taskInit() |
Initialize a task using caller-provided resources |
taskActivate() |
Activate an initialized task |
exit() |
Terminate the calling task |
taskDelete() |
Delete a task |
taskDeleteForce() |
Force task deletion |
taskSuspend() |
Suspend a task |
taskResume() |
Resume a suspended task |
taskRestart() |
Restart a task |
taskPrioritySet() |
Change task priority |
taskPriorityGet() |
Retrieve task priority |
taskLock() |
Disable normal task preemption |
taskUnlock() |
Re-enable task scheduling |
taskSafe() |
Protect a task from deletion |
taskUnsafe() |
Remove deletion protection |
taskDelay() |
Delay execution for a number of clock ticks |
taskIdSelf() |
Obtain the calling task’s ID |
taskIdVerify() |
Verify a task ID |
taskTcb() |
Access task-control information |
taskOptionsSet() / taskOptionsGet() |
Configure or query task options |
taskRegsGet() / taskRegsSet() |
Get or set task register context |
taskName() / taskNameToId() |
Query or resolve task names |
taskIsReady() / taskIsSuspended() |
Query task state |
taskIdListGet() |
Retrieve active task IDs |
VxWorks task priorities normally use a numeric range in which lower values represent higher priority.
๐จ Message Queues #
Message queues provide asynchronous communication between tasks.
They are appropriate when one task needs to send discrete messages to another task without directly sharing synchronization state.
A queue can be created with:
MSG_Q_ID msgQID;
void initMsgQ(void)
{
msgQID = msgQCreate(8, 1, MSG_Q_FIFO);
if (msgQID == NULL)
{
printf("Message queue creation failed!\n");
}
}
A sender can enqueue a message:
void taskSend(void)
{
if (msgQSend(msgQID, "A", 1, NO_WAIT, MSG_PRI_NORMAL) != OK)
{
printf("Message send failed!\n");
}
}
A receiver can block until a message arrives:
void taskReceive(void)
{
unsigned char ch;
if (msgQReceive(msgQID, (char *)&ch, 1, WAIT_FOREVER) > 0)
{
printf("Received from msgq: %c\n", ch);
}
}
The queue depth, message size, priority behavior, and timeout should be selected according to the application’s real-time requirements.
๐ฐ Pipes and File-Descriptor-Based IPC #
VxWorks pipes expose inter-task communication through the standard I/O interface.
One important advantage is that pipes can integrate with select(), allowing a task to monitor pipe activity together with other file descriptors.
A pipe can be created with:
pipeDevCreate("/pipe/mypipe", 8, 1);
A task can write to it through a normal descriptor:
void taskSendPipe(void)
{
int pd;
pd = open("/pipe/mypipe", O_WRONLY, 0644);
if (pd == ERROR)
return;
write(pd, "a", 1);
close(pd);
}
Another task can receive the data:
void taskReceivePipe(void)
{
int pd;
unsigned char ch;
pd = open("/pipe/mypipe", O_RDONLY, 0644);
if (pd == ERROR)
return;
if (read(pd, (char *)&ch, 1) > 0)
{
printf("Received from pipe: %c\n", ch);
}
close(pd);
}
If multiple producers share a resource around the pipe operation, a mutex may be used to protect the critical section.
๐ Semaphore Types #
VxWorks provides several semaphore types for different synchronization patterns.
The principal creation APIs are:
SEM_ID semBCreate(int queueOptions, SEM_B_STATE initialState);
SEM_ID semMCreate(int options);
SEM_ID semCCreate(int queueOptions, int initialCount);
Binary Semaphores #
Binary semaphores represent a two-state synchronization object:
SEM_FULL
SEM_EMPTY
They are commonly used for task synchronization and signaling.
Example:
SEM_ID semBID;
void initSemB(void)
{
semBID = semBCreate(SEM_Q_FIFO, SEM_EMPTY);
}
void taskSyncGive(void)
{
semGive(semBID);
}
void taskSyncWait(void)
{
semTake(semBID, WAIT_FOREVER);
}
A binary semaphore is not the same as a mutex. It is primarily a signaling/synchronization primitive rather than an ownership-based resource lock.
Mutex Semaphores #
Mutex semaphores are intended for mutual exclusion and resource ownership.
SEM_ID semMID;
void initMutex(void)
{
semMID = semMCreate(SEM_Q_FIFO);
}
Depending on the VxWorks version and configured options, mutexes can provide facilities such as:
- Priority inheritance.
- Recursive locking.
- Deletion safety.
- Ownership tracking.
Mutex semaphores should not be used from interrupt service routines.
Counting Semaphores #
Counting semaphores maintain an integer count and are useful when representing multiple identical resources.
SEM_ID semCID;
void initCountingSem(void)
{
semCID = semCCreate(SEM_Q_FIFO, 4);
}
Each successful semTake() consumes one count, while semGive() increments the count.
Semaphore Comparison #
| Semaphore | Primary Use | Key Characteristic |
|---|---|---|
semBCreate() |
Task synchronization / signaling | Binary full/empty state |
semMCreate() |
Mutual exclusion | Ownership and mutex-specific protection |
semCCreate() |
Resource counting | Integer resource count |
The correct primitive depends on whether the application needs signaling, exclusive ownership, or resource counting.
๐ก VxWorks Events #
The VxWorks event mechanism allows one task to send an event to another task identified by its task ID.
A sender can issue an event with:
void taskSendEvent(TASK_ID targetTaskId)
{
if (eventSend(targetTaskId, 0x00000001) != OK)
{
printf("Event send failed!\n");
}
}
A receiving task can wait for events:
void taskReceiveEvent(void)
{
UINT32 ev;
if (eventReceive(
0x00ffffff,
EVENTS_WAIT_ANY,
WAIT_FOREVER,
&ev) == OK)
{
if (ev & 0x00000001)
{
printf("Event 0x1 received!\n");
}
}
}
Events are useful when the notification itself is small and the receiving task does not need an associated message payload.
โฑ๏ธ Watchdog Timers #
The wdLib API provides software watchdog timers.
A watchdog can be created with:
WDOG_ID wdID;
void initWatchdog(void)
{
wdID = wdCreate();
if (wdID == NULL)
{
printf("Watchdog create failed!\n");
}
}
The watchdog can then be started using system clock ticks:
void startWatchdog(void)
{
if (wdStart(
wdID,
sysClkRateGet() * 5,
(FUNCPTR)proc_wd,
0) != OK)
{
printf("Watchdog start failed!\n");
}
}
The callback should be designed with the watchdog execution context in mind:
int proc_wd(int param)
{
logMsg(
"Watchdog timer expired!\n",
0, 0, 0, 0, 0, 0);
return 0;
}
A watchdog callback should generally perform minimal, deterministic work and avoid operations that can block unexpectedly.
๐ Network Programming with BSD Sockets #
VxWorks provides BSD-style socket APIs for TCP/IP networking.
Common functions include:
| Function | Purpose |
|---|---|
socket() |
Create a socket endpoint |
bind() |
Assign a local address and port |
listen() |
Put a socket into passive listening mode |
accept() |
Accept an incoming connection |
connect() |
Establish an active connection |
connectWithTimeout() |
Establish a connection with a timeout |
send() / sendto() / sendmsg() |
Transmit data |
recv() / recvfrom() / recvmsg() |
Receive data |
setsockopt() |
Configure socket options |
getsockopt() |
Query socket options |
getsockname() |
Obtain the local socket address |
getpeername() |
Obtain the remote socket address |
shutdown() |
Shut down part or all of a socket connection |
A typical TCP server follows this sequence:
socket()
|
v
bind()
|
v
listen()
|
v
accept()
|
v
send() / recv()
|
v
shutdown() / close()
A TCP client typically uses:
socket()
|
v
connect()
|
v
send() / recv()
|
v
shutdown() / close()
Because VxWorks is an RTOS, socket operations should be designed with explicit timeout and blocking behavior in mind when deterministic task execution is required.
๐จ Error Handling with errnoLib
#
VxWorks maintains an error value that can be queried with:
errnoGet()
and explicitly modified with:
errnoSet()
For example:
#define MEMORY_LEAK 0x20005
int currentErr;
currentErr = errnoGet();
errnoSet(MEMORY_LEAK);
When diagnosing a failing VxWorks API call, check the return value first and then inspect errno where the relevant API documents it as meaningful.
For example:
if (someApiCall() == ERROR)
{
printf("Error = 0x%x\n", errnoGet());
}
Application-specific error values should be selected so that they do not conflict with the error-number conventions used by the particular VxWorks release.
๐ฃ Signal Handling #
Signals provide asynchronous notification between tasks or processes.
A signal handler can be registered with:
void proc_sig(int param)
{
logMsg(
"Signal %d received\n",
param, 0, 0, 0, 0, 0);
}
void task1(void)
{
signal(30, proc_sig);
}
Another task can send the signal:
void task2(int targetTaskId)
{
kill(targetTaskId, 30);
}
Signal handlers operate under restrictions associated with asynchronous execution. Complex work is generally better deferred to a normal task context rather than performed directly inside a signal handler.
โก Interrupt Handling #
VxWorks provides architecture-specific interrupt interfaces for connecting application or driver interrupt handlers to hardware interrupt vectors.
On x86 systems, hardware IRQ numbers and processor interrupt vectors are not necessarily identical. A common legacy mapping is:
IRQ 0 -> vector 0x20
IRQ 1 -> vector 0x21
...
IRQ 15 -> vector 0x2F
For example, IRQ 9 corresponds to:
0x20 + 9 = 0x29
A simplified connection might look like:
void Int9Handler(int param)
{
/* Handle interrupt source. */
}
void initInterrupt(void)
{
intConnect(
INUM_TO_IVEC(9 + 0x20),
Int9Handler,
0);
sysIntEnablePIC(9);
}
The exact interrupt controller, vector mapping, and enable/acknowledge sequence are architecture- and BSP-dependent.
Interrupt handlers should execute quickly and avoid blocking operations. Longer processing should normally be deferred to a task or other appropriate execution context.
๐๏ธ Understanding taskSpawn()
#
taskSpawn() provides a convenient way to create and activate a VxWorks task.
A representative prototype is:
int taskSpawn(
char *name,
int priority,
int options,
int stackSize,
FUNCPTR mainFunc,
int arg1,
int arg2,
int arg3,
int arg4,
int arg5,
int arg6,
int arg7,
int arg8,
int arg9,
int arg10
);
The important parameters are:
| Parameter | Meaning |
|---|---|
name |
Task name |
priority |
Task priority |
options |
Task execution options |
stackSize |
Stack allocation size |
mainFunc |
Task entry function |
arg1โarg10 |
Arguments passed to the entry function |
A task can be created with:
taskSpawn(
"tWorker",
100,
0,
8192,
(FUNCPTR)workerTask,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
The exact function prototype and argument conventions should match the VxWorks release being used.
taskSpawn() Versus taskInit()
#
The primary difference is resource ownership and activation.
| API | Model |
|---|---|
taskSpawn() |
Dynamically creates the task and its required resources, then activates it |
taskInit() |
Initializes a task using caller-supplied resources; activation is performed separately |
Conceptually:
taskSpawn()
|
+-- Allocate/initialize TCB
+-- Allocate/initialize stack
+-- Initialize task context
+-- Activate task
Whereas:
taskInit()
|
+-- Caller provides TCB/stack resources
+-- Initialize task
|
v
taskActivate()
Static task initialization can be useful when resource ownership, memory placement, or startup behavior needs tighter control.
๐งฉ Choosing the Right IPC Primitive #
VxWorks offers multiple mechanisms for communication and synchronization. They should not be treated as interchangeable.
| Requirement | Appropriate Primitive |
|---|---|
| Transfer a message payload | Message queue |
| Stream/file-descriptor-oriented communication | Pipe |
| Signal a task that an operation occurred | Binary semaphore |
| Protect a shared resource | Mutex semaphore |
| Track multiple identical resources | Counting semaphore |
| Deliver a small event bitmask | VxWorks events |
| Monitor multiple I/O descriptors | select() |
| Communicate over a network | BSD sockets |
| Notify asynchronously | Signal |
| React to hardware | Interrupt handler |
A common architecture might therefore look like:
Hardware ISR
|
v
Binary semaphore / event
|
v
Worker task
|
+---- Message queue ----> Processing task
|
+---- Pipe -------------> I/O task
|
+---- Socket ------------> Network peer
This separation helps keep interrupt handlers short while moving potentially blocking or computationally expensive work into normal task context.
๐ Practical API Reference #
The following mapping provides a compact reference for common VxWorks development tasks.
| Category | Primary API / Header | Typical Use |
|---|---|---|
| Task management | taskLib.h |
Create, suspend, resume, delete, and configure tasks |
| Message queues | msgQLib.h |
Task-to-task message passing |
| Semaphores | semLib.h |
Synchronization and resource protection |
| Device I/O | ioLib.h |
File and device operations |
| I/O multiplexing | selectLib.h |
Monitor multiple descriptors |
| Events | eventLib.h |
Task-directed event notification |
| Watchdogs | wdLib.h |
Timed callbacks |
| Logging | logLib.h |
Diagnostic logging |
| Networking | socket.h / socket APIs |
TCP/IP communication |
| Signals | sigLib.h |
Asynchronous task/process notification |
| Error handling | errnoLib.h |
Retrieve and set error values |
| Interrupts | iv.h and BSP APIs |
Connect and enable hardware interrupts |
๐ง Key Takeaways #
Effective VxWorks programming depends less on using individual APIs in isolation and more on selecting the correct execution and synchronization model.
The most important distinctions are:
- Use tasks for independent execution contexts.
- Use message queues when data must be transferred between tasks.
- Use pipes when communication should integrate with the VxWorks I/O subsystem.
- Use binary semaphores for synchronization and notification.
- Use mutex semaphores for protecting shared resources.
- Use counting semaphores for finite resource pools.
- Use events and signals for lightweight notifications.
- Use watchdogs for timed callbacks.
- Use sockets for network communication.
- Use interrupt handlers for short, hardware-driven response paths.
- Use
select()when a task must monitor multiple file descriptors. - Use
errnoGet()to diagnose APIs that report failures through the VxWorks error mechanism.
For real-time systems, correctness also depends on execution context. Code that is acceptable in a normal task may be inappropriate inside an ISR, watchdog callback, or signal handler. Blocking behavior, priority interactions, stack usage, interrupt latency, and resource ownership should therefore be considered whenever these APIs are combined into a production VxWorks application.