Understanding the VxWorks Kernel: I/O System Explained

In order to achieve platform independence for application layer programs, the operating system provides a set of standard interface functions to the application layer. These interface functions remain consistent across all platforms, but as platforms change, the underlying drivers or near driver parts of the operating system middleware may be adjusted. This allows user programs to be independent of specific hardware platforms, increasing the efficiency of application layer development and avoiding redundant coding. General Purpose Operating Systems (GPOS) like Unix and Linux separate this set of standard interface functions provided to the application layer from the operating system, existing specifically in the form of standard libraries, enhancing the platform independence of applications and completely shielding the differences between platforms through the operating system.

Similar to Unix and Linux, VxWorks also provides a set of standard file operation interface functions to the application layer, which are actually similar to those provided by GPOS. We refer to this as the standard I/O library, which is provided by the ioLib.c file in VxWorks. The ioLib.c file provides the following standard interface functions: creat, open, unlink, remove, close, rename, read, write, ioctl, lseek, readv, writev, etc. One significant difference between the VxWorks operating system and GPOS is that VxWorks does not distinguish between user mode and kernel mode; user layer programs can directly call kernel functions without the need for mechanisms like trap instructions and without restrictions on usage permissions. Therefore, the interfaces provided to the application layer by VxWorks do not need to be accessed through peripheral libraries, but are provided directly in the form of kernel files. User programs can directly use the functions defined in the ioLib.c file, which have the same names as those in the GPOS standard library, simulating the standard library in VxWorks.

The I/O system of VxWorks provides application programs with simple, unified, device-independent interfaces:

  • Terminal-oriented character devices or communication line devices;

  • Random access block devices, such as hard disks;

  • Virtual devices, such as pipes and sockets;

  • Data or analog I/O devices used for monitoring and control;

  • Network devices for accessing remote devices.

In the VxWorks system, application programs access I/O devices through files, where a file typically represents the following two types of devices:

  • Unstructured raw devices, such as serial communication channels and pipe devices used for inter-task communication;

  • Structured logical files on randomly accessible devices that contain a file system;

For example, the following files:

/usr/myfile
/pipe/mypipe
/tyCo/0

The first file myfile is on a hard disk named /usr; the second is on a named pipe, which usually starts with /pipe; the third file maps to a serial channel. The I/O system can handle these devices in the form of files. In VxWorks, despite the vastly different physical properties of these devices, they are all referred to as files, borrowing this design philosophy from Unix.

Devices in the VxWorks system are handled by program modules known as drivers. Using the I/O system does not require knowledge of the specific implementation mechanisms of these drivers and devices, but the VxWorks I/O system grants the drivers of specific devices great flexibility. Although all I/O systems are indexed by files, there are two different implementation methods: basic I/O implementation and buffered I/O implementation. The difference between these two implementation methods lies in whether data is buffered and the manner in which system calls are implemented, as shown in Figure 1.

Understanding the VxWorks Kernel: I/O System Explained

Figure 1 Overview of the VxWorks I/O System

The filename is represented by a string, and an unstructured file is represented by a device name. For example, for file system devices, the device name is closely followed by the file name, such as /tyCo/0 for a specific serial I/O channel, and DEV1:/file1 represents a file file1 on device DEV1:.

When a filename is specified in an I/O call, the I/O system searches for the device based on this filename, and then the I/O routine locates this device; if the I/O system cannot find the device specified by the filename, the I/O routine will be directed to the default device. We can specify this default device to be any device in the system, or it may not contain any device. If there is no task device, then when the I/O system cannot find the device specified by the filename, it will return an error. VxWorks provides the interface ioDefPathGet() to obtain the current default device, and the interface ioDefPathSet() to set the current default device.

Non-blocking devices are named when added to the I/O system, which is usually completed during system initialization. Block devices are named when their specified file system is initialized. The VxWorks I/O system imposes no restrictions on the way devices are named. Unless searching for matching devices and files, the I/O system does not interpret device and file names.

However, adopting a traditional convention for naming devices and files is still very useful: most device names start with a slash “/”, except for non-NFS network devices and VxWorks DOS devices.

By convention, NFS-based network devices are named with a leading slash “/” for their mount points, such as /usr.

Non-NFS network devices are typically named with a remote machine name followed by a colon, such as host:, while the remaining names are the filenames in the remote system directory.

File systems using dosFs typically use a combination of uppercase letters and numbers, followed by a colon: for naming, such as DEV:

7.1 VxWorks I/O Framework

The VxWorks I/O system differs from the I/O systems of Unix or Linux in that the work responding to user requests is distributed between devices unrelated to the I/O system and the device drivers themselves.

In GPOS, device drivers provide certain lower-level I/O routines to read or write character sequences from devices located by strings. High-level communication protocol routines based on character devices are usually implemented by parts of the I/O system that are independent of the devices. Before the driver routines gain control, user requests heavily rely on the services of the I/O system.

Although this solution makes it easier to implement drivers and ensures that their behavior is as consistent as possible, it has the drawback that when the driver developer implements protocols that do not exist in the current I/O system, they face significant difficulties. In real-time systems, if the throughput of certain devices is crucial, we may need to bypass standard protocols, or such devices may not fit standard models at all.

In the VxWorks system, user requests are processed as little as possible before control is transferred to the device driver. The role of the VxWorks I/O system resembles that of a switchboard, responsible for routing user requests to the appropriate driver routines. Each driver can handle raw user requests for the device that suits it best. Additionally, driver developers can also utilize high-level library routines to implement standard protocols based on character devices or block devices. Therefore, the VxWorks I/O system has two advantages: on one hand, standard driver programs can be written for the vast majority of devices with minimal driver-related code; on the other hand, driver developers can autonomously handle user requests in non-standard ways where appropriate.

Devices generally fall into two categories (excluding network devices for the moment): block devices or character devices. Block devices are used to store file systems, where data is stored in blocks, and block devices are accessed randomly, with hard disks or floppy disks belonging to block devices; devices that do not fall into the block device category are referred to as character devices, which include serial devices or graphical input devices.

The VxWorks I/O system consists of three elements: drivers, devices, and files. Next, we will take character devices as an example, much of the analysis also applies to block devices. Of course, block devices must interact with the VxWorks file system, so their organizational structure is slightly different from that of character devices.

7.2 VxWorks I/O Basic Interfaces

The basic I/O interfaces are the lowest-level interfaces in the VxWorks system. The basic I/O interfaces in VxWorks are source-level compatible with the I/O interface primitives in the standard C library and are supported by the VxWorks ioLib library. There are seven basic interfaces, as shown in Table 1.

Table 1 Basic I/O Interfaces

Understanding the VxWorks Kernel: I/O System Explained

In basic I/O, files are referenced by a file descriptor fd (file descriptor), which is an integer returned by open() or creat() in the above table. Other basic I/O interfaces use this fd as a parameter to specify the file being operated upon.

The file descriptor fd is global to the system. For example, if task A calls write() on fd7 and task B calls write() on fd7, both refer to the same file.

When a file is opened, fd will be allocated and returned for user use, and when the file is closed, fd will also be released. In the VxWorks system, the number of fds is limited. To avoid exceeding the limits of the VxWorks system, it is best to close fds when they are no longer in use. The number of fds is set during VxWorks system initialization.

In the VxWorks system, the following three descriptors are reserved by the system and have special significance:

  1. 0: Standard input;

  2. 1: Standard output;

  3. 2: Standard error output;

The basic I/O routines open() and creat() in VxWorks typically do not return the above three descriptors. Standard descriptors allow tasks and modules to be independent of their actual I/O allocations. If a module sends output information to the standard output descriptor (fd=1), then the output can be redirected to any file or device without modifying that module.

VxWorks allows two levels of redirection. First, there is a global allocation of the three standard file descriptors; secondly, individual tasks can reallocate the three standard descriptors, redirecting them to devices allocated only to those tasks.

Global Redirection: At VxWorks system initialization, the standard descriptors are redirected to the system terminal. When tasks are created, they are not allocated private file descriptors fd and can only use the global standard descriptors. VxWorks provides ioGlobalStdSet() to redirect global descriptors, for example, the following example redirects the global standard output descriptor (fd=1) to the file descriptor fileFd pointing to an open file:

ioGlobalStdSet(1, fileFd);

If a task does not have private file descriptors, it can use the system global standard descriptors, for example, task tRlogind calls ioGlobalStdSet() to redirect I/O output to the rlogin session task.

Task-level Redirection: The standard descriptors for a specific task can be redirected using the interface ioTaskStdSet(). The first parameter of this routine is the task ID to be redirected (ID=0 means itself), the second parameter is the standard descriptor to be redirected, and the third parameter is the file descriptor to redirect to. For example, in the following example, task A redirects standard output to the fileFd descriptor:

ioTaskStdSet(0,1, fileFd);

7.3 VxWorks Driver Hierarchy

As we mentioned earlier, the VxWorks I/O framework is provided by the ioLib.c file, but the functions provided by the ioLib.c file are merely the top-level interfaces and do not fulfill specific user requests; instead, they pass requests further to other kernel modules, with the module beneath ioLib.c being iosLib.c. We refer to the ioLib.c file as the upper-level interface subsystem and the iosLib.c file as the I/O subsystem. Note the distinction between the two. The upper-level interface subsystem is directly visible to the user layer, while the I/O subsystem is generally invisible (of course, users can also directly call functions defined in iosLib.c, but this generally requires more encapsulation and violates the service hierarchy provided by the kernel). It exists as an intermediate layer between the upper-level interface subsystem and the lower-level driver system. Figure 2 shows the driver hierarchy of the VxWorks system.

Understanding the VxWorks Kernel: I/O System Explained

Figure 2 VxWorks Kernel Driver Hierarchy

From Figure 2, it can be seen that the I/O subsystem plays a crucial role in the entire driver hierarchy, managing various types of device drivers below it. In other words, various types of devices (including network devices) must be registered with the I/O subsystem to be accessible by the kernel. Therefore, at this level of the I/O subsystem, the kernel maintains three critical arrays to manage the drivers belonging to devices, the devices themselves, and the current system file handles.

It is worth noting that the VxWorks file system actually exists as an intermediate layer within the kernel driver hierarchy, registering with the I/O subsystem while placing the underlying block device drivers under its management to improve data access efficiency. Among these file systems, dosFs and rawFs are the two most commonly used file system types, both of which have been supported since early versions of VxWorks.

7.4 Composition of the VxWorks I/O Driver Framework

Since the I/O subsystem plays a management role in the entire driver hierarchy, it maintains three tables related to system devices and drivers: the device table, the driver table, and the file handle table. Next, we will introduce these three table structures and glimpse the overall picture of the VxWorks I/O subsystem through these three tables.

Taking a character device as an example, suppose the driver for this character device has been registered with the I/O subsystem and a file node for this character device has been created. We will introduce the calling process of user layer requests being passed down to the lower-level driver using the example of a user opening a device operation.

Before using a device, a user must first open that device. By calling the open() function with the file node of the character device as the pathname, the open function transfers the request to iosOpen(). The I/O subsystem maintains all the drivers and devices currently in the system. It queries the device based on the device node name passed when calling the open() function. Upon finding the device, it learns from the relevant field values in the device structure the index number of the driver corresponding to that device. The I/O subsystem retrieves the driver function set corresponding to that device from the system driver list based on the driver program number and calls the lower-level driver open() response function, which registers interrupts, enables the hardware operation, and completes the user layer’s request to open the device.

The open() function returns an integer, which we refer to as the file descriptor. In addition to the system driver and system device tables, the third table maintained by the I/O subsystem is the current system file descriptor table. Each entry in this table is a data structure, where the index of the entry in the table itself is the file descriptor, while the content of the entry indicates the device and driver corresponding to that file descriptor. Subsequent operations on the device, such as reading, writing, controlling, or closing, will be based on the file descriptor. The file descriptor can directly address the driver program of the device being operated upon.

7.4.1 System Driver Table

The system driver table maintained by the I/O subsystem contains all the drivers currently registered with the I/O subsystem. These drivers can be the driver layer that directly drives the hardware, such as general character drivers, or intermediate layer drivers, such as file system intermediate layers, TTY intermediate layers, USB I/O intermediate layers, etc. For intermediate layer drivers, the lower-level hardware drivers will be managed by these intermediate layers themselves and will no longer be managed through the I/O subsystem. For example, the lower-level serial port driver will be managed through the TTY intermediate layer rather than through the I/O subsystem.

The underlying implementation of the system driver table is an array, and the number of array elements is specified during the initialization of the I/O subsystem in the VxWorks kernel. The iosInit function is used to initialize the I/O subsystem, and the prototype of the iosInit function is as follows:

STATUS iosInit
(
int max_drivers,            /* maximum number of drivers allowed */
int max_files,              /* max number of files allowed open at once */
char *nullDevName           /* name of the null device (bit bucket) */
)

Where:

Parameter 1 (max_drivers): specifies the number of elements in the system driver table, i.e., the maximum number of drivers supported by the system.

Parameter 2 (max_files): specifies the maximum number of files that can be opened simultaneously by the system. This parameter essentially specifies the number of elements in the system file descriptor table.

Parameter 3 (nullDevName): specifies the device node name of the null device, generally “/null”.

The system driver table is represented in the kernel by drvTable, which is declared as follows:

DRV_ENTRY * drvTable; /* driver entry point table */

In the iosInit function, the drvTable is initialized based on the maximum number of drivers passed in, as shown in the following code example:

STATUS iosInit
(
int max_drivers,            /* maximum number of drivers allowed */
int max_files,              /* max number of files allowed open at once */
char *nullDevName           /* name of the null device (bit bucket) */
)
{
int i;
int size;
maxDrivers      = max_drivers;
maxFiles  = max_files;
.......略...........
/* allocate driver table and make all entries null */

size = maxDrivers * sizeof (DRV_ENTRY);
drvTable = (DRV_ENTRY *) malloc ((unsigned) size);

if (drvTable == NULL)
 return (ERROR);
bzero ((char *) drvTable, size);
for (i = 0; i < maxDrivers; i++)
 drvTable [i].de_inuse = FALSE;
......略............
return (OK);
}

Each entry in the system driver table is a structure of type DRV_ENTRY, which is defined in h/private/iosLibP.h as follows:

typedef struct           /* DRV_ENTRY - entries in driver jump table */
{
FUNCPTR de_create;
FUNCPTR de_delete;
FUNCPTR de_open;
FUNCPTR de_close;
FUNCPTR de_read;
FUNCPTR de_write;
FUNCPTR de_ioctl;
BOOL        de_inuse;
} DRV_ENTRY;

It can be seen that DRV_ENTRY is essentially a structure of function pointers, where each member points to a function that accomplishes a specific function, corresponding to the interface provided to the user layer. The member de_inuse indicates whether an entry is free.

The iosInit() function creates the system driver table. From the above code example, this table is essentially an array pointed to by drvTable, and the size of the array is determined by the first parameter passed to the iosInit function. The entry with index 0 in drvTable is reserved by the Wind kernel specifically for the null device driver number, so the allocation of driver numbers actually starts from 1.

The I/O subsystem provides iosDrvInstall() for driver registration, and the prototype of the iosDrvInstall() function is as follows:

int iosDrvInstall
(
FUNCPTR pCreate,    /* pointer to driver create function */
FUNCPTR pDelete,    /* pointer to driver delete function */
FUNCPTR pOpen,      /* pointer to driver open function */
FUNCPTR pClose,     /* pointer to driver close function */
FUNCPTR pRead,      /* pointer to driver read function */
FUNCPTR pWrite,     /* pointer to driver write function */
FUNCPTR pIoctl      /* pointer to driver ioctl function */
)

A device driver, during its initialization process, not only configures the hardware device registers but also registers the driver and device with the I/O subsystem, thus making the device visible to users. It can be seen that the parameters of the iosDrvInstall() function are a series of function addresses that correspond to the standard interface functions provided to the user layer. A driver does not need to implement all of the above functions; for functions that do not need to be implemented, a NULL pointer can be passed. The basic implementation of iosDrvInstall() traverses the drvTable array, queries for a free entry, initializes the members of the entry with the function addresses passed in, and sets de_inuse to TRUE, finally returning the index of that entry in the array as the driver number. The device initialization function will use this driver number to call iosDevAdd() to add the device to the I/O subsystem. After this, users can use the device node name set during the iosDevAdd function call to open the device, and after opening, perform read, write, or control operations to complete the specific functions requested by the user.

Users can enter iosDrvShow on the command line to display all the drivers currently stored in the system driver table. Table 2 provides a simple illustration of the system driver table.

Table 2 System Driver Table

Understanding the VxWorks Kernel: I/O System Explained

A non-blocking device driver implements the basic I/O functions: creat(), delete(), open(), close(), read(), write(), and ioctl(). Typically, this driver contains routines to implement these seven basic I/O functions, but if some of these routines are not meaningful for the device corresponding to this driver, the corresponding implementation is NULL.

Drivers can choose to allow multiple tasks to wait on multiple file descriptors to be activated, which is implemented in the driver’s ioctl() routine. We will analyze this when we look at the select() routine.

Block device drivers interact with the file system rather than directly with the VxWorks I/O system. The file system, in turn, performs the vast majority of I/O functions, while the drivers only provide routines to read and write blocks, reset the driver, perform I/O control, and check the device status.

When users call basic I/O functions, the I/O system routes these requests to the appropriate routine of the specified driver, and the driver routine executes in the context of the calling task, appearing as if the application program is directly calling these routines. However, drivers can freely use functions that are normally available to tasks, including I/O or other devices, meaning that most drivers must include certain mechanisms to provide mutual exclusion access to code critical sections, with the notification mechanism being the semaphore mechanism from the semLib library.

In addition to implementing the seven basic I/O functions, the drivers of the VxWorks I/O system also include the following three routines:

  1. 1. An initialization routine, typically named xxDrv(), which installs the driver in the I/O system, connects interrupts to the device serviced by the driver, and performs some necessary hardware initialization operations;

  2. 2. A routine to add the device serviced by the driver to the VxWorks I/O system, typically named xxDevCreate();

  3. 3. An interrupt-level routine for connecting interrupts generated by the device serviced by the driver.

The driver table and driver installation: The function of the I/O system is to route user requests to the appropriate routine of the suitable driver, which completes the user request. The I/O system accomplishes this by maintaining a table of entry addresses for each driver’s various routines. By calling the internal routine iosDrvIntall() of the I/O system, drivers can be dynamically installed; the parameters of iosDrvIntall() are the entry addresses of the seven basic I/O routines of the new driver to be added. iosDrvIntall() fills the entry addresses of these seven functions into a free slot in the driver table and returns the index of that slot. This index is referred to as the driver number, which will be referenced by the devices serviced by that driver.

If the entry routine addresses of the seven basic I/O functions of the driver are set to NULL, it means that this driver does not need to handle these functions. For example, for non-file-system drivers, close() and delete() do not need to do anything.

The VxWorks file system (such as dosFsLib) includes its own routines in the driver table, which are created when the file system library is initialized. The initialization process for non-blocking device drivers is shown in Figure 3.

Understanding the VxWorks Kernel: I/O System Explained

Figure 3 Initialization Process of Non-blocking Device Drivers

Figure 3 shows the actions taken by the example driver and the I/O system when the initialization routine xxDrv() runs.

The driver calls iosDrvInstall() to specify the addresses of the seven basic I/O functions, and then the I/O system performs the following operations:

  1. 1. Locate the next available free slot in the driver table; in this example, it is the free slot slot2;

  2. 2. Fill the addresses of the seven basic I/O functions into the free slot slot2 in the driver table;

  3. 3. Return the index of the newly installed driver in the free slot slot2 as the driver number.

7.4.2 System Device Table

Some drivers can service multiple instances of a specified device. For instance, a single driver for serial communication devices can handle multiple segments with only different parameters, such as the device address.

In the VxWorks I/O system, devices are defined by a data structure known as a device header (DEV_HDR), which contains the device name string and the driver number servicing this device. In the VxWorks system, all device headers reside in memory and are linked into a device linked list. The device header is the starting part of a large data structure determined by the specific driver, known as the device descriptor. In addition to the device header, the device descriptor contains members related to the specific device, such as device address, buffer, semaphore, etc.

The VxWorks kernel represents each device using the DEV_HDR data structure, which is defined as follows:

typedef struct                   /* DEV_HDR - device header for all device structures */
{
DL_NODE         node;                 /* device linked list node */
short         drvNum;            /* driver number for this device */
char *       name;                /* device name */
} DEV_HDR;

This structure includes a linking pointer (used to link this structure into a queue), the driver index number, and the device node name. The kernel provides this structure relatively simply, storing only some key system information about the device. The lower-level drivers have a custom data structure representing the devices they drive, which includes the base address of the device registers, interrupt number, possible data buffers, pointers to save kernel callback functions, and some flags. The most critical point is that the DEV_HDR kernel structure must be the first member variable of this custom data structure, as this user-defined structure will eventually need to be added to the system device queue. Thus, it must be possible to convert between the user-defined structure and the DEV_HDR structure by making the DEV_HDR structure the first member variable of the user-defined structure. The following code is a simple example of a user-defined device structure.

typedef struct xxDev
{
DEV_HDR devHdr; // Kernel-provided structure, must be the first member variable of the custom structure.
UINT32 regBase;   // Base address of the device registers.
UINT32 buffPtr;     // Base address of the data buffer.
BOOL isOpen;        // Device opened flag.
UINT8 intLvl;         // Device interrupt number.
FUNCPTR putData; // Kernel callback function pointer, this pointer points to the function that provides data to the kernel.
FUNCPTR getData; // Kernel callback function pointer, this pointer points to the function that retrieves data from the kernel.
… // Other device parameters.
}

To allow users to operate on devices, the driver must register the device with the I/O subsystem, a process also known as creating a device node.

The iosDevAdd() function provided by the I/O subsystem is used to register a device called by the driver. The prototype of this function is as follows:

STATUS iosDevAdd
(
DEV_HDR *pDevHdr, /* pointer to device's structure */
char *name,       /* name of device */
int drvnum        /* no. of servicing driver, */
 /* returned by iosDrvInstall() */
)

The parameters passed to iosDevAdd are:

  1. Parameter 1 (pDevHdr): is a DEV_HDR structure type. Generally, we pass the user-defined structure as the first parameter, which is also the reason why the DEV_HDR structure type member variable must be the first member of the user-defined structure.

  2. Parameter 2 (name): indicates the device node name, which will be used as the path when the user program calls to open the device.

  3. Parameter 3 (drvnum): is the index number of the driver corresponding to the device. This driver number is the return value of the iosDrvInstall() function. In the device initialization function, we first call iosDrvInstall() to register the driver, and then use the driver number returned by the iosDrvInstall() function to call iosDevAdd to add the device to the system. After these two steps are completed, the device can be used by user programs.

osDevAdd() adds a device to the system device list maintained by the I/O subsystem. This list is a queue, and the members of the queue are linked together by pointers, which is accomplished by the node member variable in the DEV_HDR structure. The system device list is pointed to by the kernel variable iosDvList. Figure 4 shows a simple illustration of the system device list.

Understanding the VxWorks Kernel: I/O System Explained

Figure 4 System Device List Illustration

The first device in the system device list is added by the kernel itself, which is a null device. All data written to the null device will be discarded directly, making this mechanism effective for suppressing some outputs. The null device is an internal device of the kernel, and driver number 0 is specifically reserved for the null device. The null device has its own DEV_HDR structure, which does not have other parameters, so Figure 3 only shows a DEV_HDR structure for the null device, while other devices typically require additional parameters defined outside the DEV_HDR structure.

Figure 4 also shows two serial devices that exist in the system. These two serial ports use the same driver. In fact, the driver index number displayed here is the driver number of the TTY driver, not the actual lower-level serial driver number. The lower-level serial driver is managed through the TTY driver, so the operations on different serial ports are separated only at the TTY driver layer. All serial drivers first need to be processed through the same TTY driver layer before requests are forwarded to the specific lower-level serial driver. Users can enter iosDevShow or devs on the command line to display all devices in the system.

When a user calls the open() function to open a device file, the I/O subsystem will match the device node name in the system device list using the provided file pathname. The matching method is the best match, meaning that the device with the closest name will be returned. For instance, if the input file path is “/pipe/xyz” and there are two devices in the system device table: “/pipe/xy” and “/pipe/xyz”, then the “/pipe/xyz” device will be returned, regardless of its position in the list. Of course, if the provided file pathname is shorter, the first device in the system device table will be returned. For example, if the provided file pathname is “/pipe/x”, then among the devices in the system device table “/pipe/xy” and “/pipe/xyz”, the one that appears first in the device table will be returned. For cases where the pathname is longer than the device name, this is more common in operations on block devices. Generally, when we create a file system on a block device, we create a device node for the block device, and all operations on the block device are under this root node. At this point, the block device node becomes the criterion for determining which block device (if there are multiple block devices in the system) a file or directory being operated on belongs to.

As previously mentioned, non-blocking devices are dynamically added to the I/O system using the internal routine iosDevAdd(). The parameters of iosDevAdd() include the address of the device descriptor of this new device, the device name, and the driver number servicing that device. The device descriptor is specified by the driver and contains the necessary information related to the device, but the device header must be the first member of the device descriptor. The driver does not need to add the device header part of the device descriptor, only filling in the information related to the specific device in the device descriptor. The iosDevAdd() routine is responsible for filling in the device name, device number in the device header, and adding the device to the system device linked list.

To add a block device to the I/O system, it is necessary to call the device initialization routines for the block device’s file system (such as dosFsDevCreate() or rawFsDevInit()), which automatically call the iosDevAdd() routine.

The iosDevFind() routine is used to locate the device structure (via a pointer to the DEV_HDR structure, which is the first member of the device descriptor structure) and verify whether the device name exists in the device table. Below is an example of using iosDevFind():

char *  pTail;                                               /* Pointer to the tail of the device name devName */
char devName[6] = "DEV1:";                   /* Device name */
DOS_VOLUME_DESC *  pDosVolDesc;   /* The first device is the device header DEV_HDR */
...
pDosVolDesc = iosDevFind(devName, (char**)&pTail);
if (NULL == pDosVolDesc)
{
/* ERROR: The device name does not exist, and there is no default device */
}
else
{
/* pDosVolDesc is a valid pointer to the device header DEV_HDR */
/* pTail points to the start of the device name devName */
/* Check the device name through pTail to determine whether the device name is a default device or a specified device */
}

Figure 5 illustrates the process of a driver’s device creation routine xxDevCreate() adding a device to the I/O system using iosDevAdd().

Understanding the VxWorks Kernel: I/O System Explained

Figure 5 Illustration of Adding a Device to the I/O System

7.4.3 File Descriptors

The third table maintained by the I/O subsystem is the system file descriptor table, which stores all the file descriptors currently opened within the system. The underlying implementation of the file descriptor table is also an array, just as the driver number is used for the driver table, the index of the entries in the file descriptor table is used as the file descriptor ID, which is the return value of the open function. For file descriptors, it is important to note: although standard input, standard output, and standard error output use file descriptors 0, 1, and 2, they may occupy only one entry in the system file descriptor table, meaning they all use the same entry. The VxWorks kernel manages the contents of the standard file descriptors separately from the contents of the system file descriptor table. In fact, the contents of the system file descriptor table are more oriented towards hardware devices, meaning that each time an open function is called, a new valid entry is added to the system file descriptor table until the array is full, at which point the open function call will return failure. The index offset in the table is returned to the user as the file descriptor, with all subsequent operations using this file handle.

Users can enter iosFdShow on the command line to display all the valid entries currently in the system file descriptor table. Figure 6 provides a simple illustration of the system file descriptor table.

Understanding the VxWorks Kernel: I/O System Explained

Figure 6 Illustration of the System File Descriptor Table

Note: The determination of each file descriptor is determined by the timing of when the corresponding device file is opened. Generally speaking, the serial port used as standard input/output is the earliest device opened, while other devices are opened by user programs only when needed. However, this cannot be generalized.

Figure 6 also shows that the standard file descriptors 0, 1, and 2 point to the indices of the system file descriptor table. At this point, the “/tyCo/0” device is also being used as standard input/output. The ioStdFd array has exactly three elements, where the 0 index element represents standard input, the 1 index element represents standard output, and the 2 index element represents standard error output. The index of the ioStdFd array itself represents the file descriptor, while the content of the elements represents the file descriptor used when performing actual operations on devices. The usrRoot function sets all three elements of ioStdFd to 3, meaning that the first serial device corresponds to the file descriptor “3”, thus using the first serial device as the standard input/output device.

7.4.4 Relationship Between the Three Tables

Next, we will use an open() call as an example to illustrate how the three tables maintained by the I/O subsystem collaborate to complete user requests. As shown in Figures 7 and 8, when a user calls the open function to open the file “/xx0”, the VxWorks kernel I/O subsystem will perform the following series of responses.

  • The VxWorks I/O subsystem uses the file pathname to match the system device table and queries for a matching device. Here, a matching device is found in the device list.

  • The VxWorks I/O subsystem reserves a free item in the system file descriptor table to create a file descriptor. If the subsequent call is successful, this free item will be returned to the user as the index value (offset by 3), which will be used as the file descriptor.

  • The VxWorks I/O subsystem retrieves the driver number from the matching item in the device list and then uses this driver number as an index to retrieve the lower-level device driver response function corresponding to the user layer open() call from the system driver table, calling the x_open() function. The first parameter of x_open() is set to the corresponding hardware device structure, the second parameter is the remaining part after removing the device name itself (which is NULL here), and the third and fourth parameters are the permission and mode parameters passed by the user. x_open() will complete the hardware device configuration, enable operation, register interrupts, etc., preparing for the user’s subsequent read and write device operations. x_open() also initializes the first parameter (the device structure), which will be used by the lower-level driver in subsequent operations.

Understanding the VxWorks Kernel: I/O System Explained

Figure 7 User Request I/O Service Process (Part 1)

  • The lower-level driver returns the device structure, indicating that the device has been successfully opened, or returns NULL or ERROR to indicate that the call has failed.

  • The I/O subsystem initializes the reserved free item in the file descriptor table, filling in the driver number and device structure.

  • Finally, the open function call returns a file descriptor, which is the index value of the previously reserved free item (now initialized and in use) in the table (offset by 3). Here, it is the first entry in the file descriptor table, i.e., fd=0+3=3.

Understanding the VxWorks Kernel: I/O System Explained

Figure 8 User Request I/O Service Process (Part 2)

Conclusion:

In this chapter, we have provided a detailed introduction to the kernel driver hierarchy under VxWorks, focusing on the I/O subsystem. The VxWorks kernel uses three tables to manage all drivers, devices, and opened files in the system. These three tables are central to the VxWorks kernel driver hierarchy. The VxWorks I/O subsystem plays a crucial role in the driver hierarchy, as all drivers in the system are directly or indirectly managed by the I/O subsystem, and all user requests must be routed through the I/O subsystem. For more complex devices (such as disk devices, Flash devices, USB devices), the VxWorks kernel specifically provides intermediate layers for driver management to simplify the complexity of the underlying hardware drivers. At this point, we conclude our introduction to the VxWorks I/O driver framework. The VxWorks Programmer’s Guide contains detailed descriptions of the VxWorks I/O driver framework, and many interpretations and example codes in this chapter are derived from the VxWorks Programmer’s Guide.

Leave a Comment

×