Overview

This chapter includes:

What is a resource manager?

In general terms, a resource manager is a process that registers a name in the filesystem name space. Other processes use that path to communicate with the resource manager.

To give QNX Neutrino a great degree of flexibility, to minimize the runtime memory requirements of the final system, and to cope with the wide variety of devices that may be found in a custom embedded system, the OS allows user-written processes to act as resource managers that can be started and stopped dynamically.

Resource managers are typically responsible for presenting an interface to various types of devices. This may involve managing actual hardware devices (like serial ports, parallel ports, network cards, and disk drives) or virtual devices (like /dev/null, a network filesystem, and pseudo-ttys).

In other operating systems, this functionality is traditionally associated with device drivers. But unlike device drivers, resource managers don't require any special arrangements with the kernel. In fact, a resource manager runs as a process that's separate from the kernel and looks just like any other user-level program.

The kernel (procnto) is itself a resource manager; it handles /dev/null, /proc, and several other resources in the same way any other process handles them.

A resource manager accepts messages from other programs and, optionally, communicates with hardware. It registers a pathname prefix in the pathname space (e.g. /dev/ser1), and other processes can open that name using the standard C library open() function, and then read() from, and write() to, the resulting file descriptor. When this happens, the resource manager receives an open request, followed by read and write requests.

A resource manager isn't restricted to handling just open(), read(), and write() calls — it can support any functions that are based on a file descriptor or file pointer, as well as other forms of IPC.

Adding resource managers in Neutrino won't affect any other part of the OS — the drivers are developed and debugged like any other application. And since the resource managers are in their own protected address space, a bug in a device driver won't cause the entire OS to shut down.

If you've written device drivers in most UNIX variants, you're used to being restricted in what you can do within a device driver; but since a device driver in Neutrino is just a regular process, you aren't restricted in what you can do (except for the restrictions that exist inside an ISR).


Note: In order to register a prefix in the pathname space, a resource manager must be run as root.

For example, a serial port may be managed by a resource manager called devc-ser8250, although the actual resource may be called /dev/ser1 in the pathname space. When a process requests serial port services, it does so by opening a serial port (in this case /dev/ser1).

fd = open("/dev/ser1", O_RDWR);
for (packet = 0; packet < npackets; packet++)
{
    write(fd, packets[packet], PACKET_SIZE);
}
close(fd);

Because resource managers execute as processes, their use isn't restricted to device drivers — any server can be written as a resource manager. For example, a server that's given DVD files to display in a GUI interface wouldn't be classified as a driver, yet it could be written as a resource manager. It can register the name /dev/dvd and as a result, clients can do the following:

fd = open("/dev/dvd", O_WRONLY);
while (data = get_dvd_data(handle, &nbytes))
{
    bytes_written = write(fd, data, nbytes);
    if (bytes_written != nbytes)
    {
        perror ("Error writing the DVD data");
    }
}
close(fd);

Why write a resource manager?

Here are a few reasons why you'd want to write a resource manager:

The types of resource managers

There are two types of resource managers:

The type you use depends on what you want the resource manager to do, as well as on the amount of work you want to do yourself in order to present a proper POSIX filesystem to the client.

Device resource managers

Device resource managers create only single-file entries in the filesystem, each of which is registered with the process manager. Each name usually represents a single device. These resource managers typically rely on the resource-manager library to do most of the work in presenting a POSIX device to the user.

For example, a serial port driver registers names such as /dev/ser1 and /dev/ser2. When the user does ls -l /dev, the library does the necessary handling to respond to the resulting _IO_STAT messages with the proper information. The person who writes the serial port driver is able to concentrate instead on the details of managing the serial port hardware.

Filesystem resource managers

Filesystem resource managers register a mountpoint with the process manager. A mountpoint is the portion of the path that's registered with the process manager. The remaining parts of the path are managed by the filesystem resource manager. For example, when a filesystem resource manager attaches a mountpoint at /mount, and the path /mount/home/thomasf is examined:

/mount/
Identifies the mountpoint that's managed by the process manager.
home/thomasf
Identifies the remaining part that's to be managed by the filesystem resource manager.

Examples of using filesystem resource managers are:

Communication via native IPC

Once a resource manager has established its pathname prefix, it will receive messages whenever any client program tries to do an open(), read(), write(), etc. on that pathname. For example, after devc-ser* has taken over the pathname /dev/ser1, and a client program executes:

fd = open ("/dev/ser1", O_RDONLY);

the client's C library will construct an _IO_CONNECT message, which it then sends to the devc-ser* resource manager via IPC.

Some time later, when the client program executes:

read (fd, buf, BUFSIZ);

the client's C library constructs an _IO_READ message, which is then sent to the resource manager.

A key point is that all communications between the client program and the resource manager are done through native IPC messaging. This allows for a number of unique features:


Note: All QNX Neutrino device drivers and filesystems are implemented as resource managers. This means that everything that a “native” QNX Neutrino device driver or filesystem can do, a user-written resource manager can do as well.

Consider FTP filesystems, for instance. Here a resource manager would take over a portion of the pathname space (e.g. /ftp) and allow users to cd into FTP sites to get files. For example, cd /ftp/rtfm.mit.edu/pub would connect to the FTP site rtfm.mit.edu and change directory to /pub. After that point, the user could open, edit, or copy files.

Application-specific filesystems would be another example of a user-written resource manager. Given an application that makes extensive use of disk-based files, a custom tailored filesystem can be written that works with that application and delivers superior performance.

The possibilities for custom resource managers are limited only by the application developer's imagination.

Examples of resource managers

Before getting into the workings of a resource manager, let's consider some actual and possible uses:

Transparent Distributed Processing (Qnet) statistics

For instance, Transparent Distributed Processing (Qnet) — part of the io-pkt core networking stack — contains resource-manager code that registers the name /proc/qnetstats. If you open this name and read from it, the resource manager code responds with a body of text that describes the statistics for Qnet.

The cat utility takes the name of a file and opens the file, reads from it, and displays whatever it reads to standard output (typically the screen). As a result, you can type:

cat /proc/qnetstats
      

The Qnet resource manager code responds with text such as:

kif net_server                :         0,3         
kif waiting                   :         1,2         
kif net_client                :         0,1         
kif buffer                    :         0,1         
kif outbound_msgs             :         0,1         
kif vtid                      :         0,1         
kif server_msgs               :         0,1         
kif nd_down                   :        42
kif nd_up                     :       132
kif nd_changed                :         3
kif send_acks                 :         0
kif client_kercalls           :        14
kif server_msgs               :    202898
kif server_unblock            :         0
qos tx_begin_errors           :         0
qos tx_done_errors            :         0
qos tx_throttled              :         0
qos tx_failed                 :         8
qos pkts_rxd_noL4             :         0
qos tx_conn_created           :        43
qos tx_conn_deleted           :        41
qos rx_conn_created           :        35
qos rx_conn_deleted           :        33
qos rx_seq_order              :         0
      

Robot arm

You could also use command-line utilities for a robot-arm driver. The driver could register the name, /dev/robot/arm/angle, and any writes to this device are interpreted as the angle to set the robot arm to. To test the driver from the command line, you'd type:

echo 87 >/dev/robot/arm/angle
      

The echo utility opens /dev/robot/arm/angle and writes the string (“87”) to it. The driver handles the write by setting the robot arm to 87 degrees. Note that this was accomplished without writing a special tester program.

Another example would be names such as /dev/robot/registers/r1, r2, ... Reading from these names returns the contents of the corresponding registers; writing to these names set the corresponding registers to the given values.

Even if all of your other IPC is done via some non-POSIX API, it's still worth having one thread written as a resource manager for responding to reads and writes for doing things as shown above.

GPS devices

In general, a GPS device sends a stream of data every second. The stream is composed of information organized in command groups. Here's an example of the output from a GPS:

$GPGSA,A,3,17,16,22,31,03,18,25,,,,,,1.6,1.0,1.2*39 
$GPGGA,185030.30,4532.8959,N,07344.2298,W,1,07,1.0,23.8,M,-32.0,M,,*69 
$GPGLL,4532.8959,N,07344.2298,W,185030.30,A*12 
$GPRMC,185030.00,A,4532.8959,N,07344.2298,W,0.9,116.9,160198,,*27 
$GPVTG,116.9,T,,,0.9,N,1.7,K*2D 
$GPZDA,185030.30,16,01,1998,,*65 
$GPGSV,2,1,08,03,55,142,50,22,51,059,51,18,48,284,53,31,23,187,52*78

Each line corresponds to a data set. Here's the C structure of some of the data sets:

typedef struct GPSRMC_s { 
   double UTC; 
   int Status; 
   Degree_t Latitude; 
   NORTHSOUTH Northing; 
   Degree_t Longitude; 
   EASTWEST Easting; 
   float Speed; 
   float Heading; 
} GPSRMC_t; 

typedef struct GPSVTG_s { 
   float Heading; 
   float SpeedInKnots; 
   float SpeedInKMH; 
} GPSVTG_t; 

typedef struct GPSUTM_s { 
   UTM_t X; 
   UTM_t Y; 
} GPSUTM_t;

You could provide one API per GPS format command: gps_get_rmc(), gps_get_vtg(), get_get_utm(), and so on. Internally, each function would send a message with a different command, and the reply was the data last received by the GPS.

The first obstacle was that read() and write() are half-duplex operations; you can't use them to send a command and get data back. You could do this:

GPSUTM_t utm; 
Int cmd = GPS_GET_GSA;

fd = open( "/dev/gps1", O_RDWR ); 
write( fd, &cmd, sizeof( cmd) ); 
read( fd, &data, sizeof(data) ); 
close(fd);

but this code looks unnatural. Nobody would expect read() and write() to be used in that way. You could use devctl() to send a command and request specific data, but if you implement the driver as a resource manager, you can have a different path for every command set. The driver would create the following pathnames:

and a program wanting to get GSA information would do this:

gps_gsa_t gsa;
int fd;

fd = open ( "/dev/gps1/gsa.bin", O_RDONLY ); 
read( fd, &gsa, sizeof( gsa ) ); 
close ( fd);

The benefit of having both the .txt and .bin extensions is that data returned by the read() would be in ASCII format if you use the .txt file. If you use the .bin file instead, the data is returned in a C structure for easier use. But why support *.txt if most programs would prefer to use the binary representation? The reason is simple. From the shell you could type:

# cat /dev/gps1/rmc.txt

and you'd see:

# GPRMC,185030.00,A,4532.8959,N,07344.2298,W,0.9,116.9,160198,,*27

You now have access to the GPS data from the shell. But you can do even more:

Contrary to what you might think, it's quite simple to support these features from within the resource manager.

Database example

This particular design asked for a program to centralize file access. Instead of having each program handle the specific location of the data files (on the hard disk or in FLASH or RAM), the developers decided that one program would handle it. Furthermore, file updates done by one program required that all programs using that file be notified. So instead of having each program notify each other, only one program would take care of the notification. That program was cleverly named “database”.

The API in the original design included db_read(), db_write(), db_update_notification(), etc., but a resource manager is an even better fit. The API was changed to the familiar open(), read(), write(), select(), ionotify() and so on.

Client programs were seeing only one path, /dbase/filename, but the files found in /dbase/ weren't physically there; they could be scattered all over the place.

The database resource manager program looked at the filename during open() and decided where the file needed to go or to read from. This, of course, depended on the specific field in the filename. For example, if the file had a .tmp extension, it would go to the RAM disk.

The real beauty of this design is that the designers of the client program could test their application without having the database program running. The open() would be handled directly by the filesystem.

To support notification of a file change, a client would use ionotify() or select() on the file descriptor of the files. Unfortunately, that feature isn't supported natively by the filesystem, so the database program needs to be running in order for you to test the operation.

I2C (Inter-Integrated Circuit) driver

This example is a driver for an I2C bus controller of the PowerPC MPC860. The I2C bus is simply a 2-wire bus, and the hardware is extremely cheap to implement. The bus supports up to 127 devices on the bus, and each device can handle 256 commands. When devices want to read or write information to or from another device, they must first be set up as a master to own the bus. Then the device sends out the device address and command register number. The slave then acts upon the command received.

You think a resource manager wouldn't apply in this case. All read() and write() operations require a device address and command register. You could use devctl(), but a resource manager again provides a cleaner interface.

With a resource manager, each device would live under its own directory, and each register would have a filename:

/dev/i2c1//

An important thing to note is that each filename (127 devices * 256 registers = 32512 filenames) doesn't really need to exist. Each device would be created live, as it's required. Therefore, each open() is actually an O_CREAT.

To prevent any problems caused by a high number of possibly existing files, you could make an ls command of the /dev/i2c1 directory return nothing. Alternatively, you could make ls list the filenames that have been opened at least once. At this point, it's important to clarify that the existence of the filenames is totally handled by the resource manager; the OS itself isn't used in that process. So it isn't a problem if filenames respond to open() requests, but not to ls.

One element is left to solve: the I2C bus has a concept of a baud rate. There are already C functions to set up baud rates, and you can make it work via the stty command from the shell.

People using the driver don't have to worry about libraries or include files because there are none. The resource manager, at no cost, allows each command register to be accessed via the shell, or for that matter, though SAMBA from a Linux or Windows machine. Access via the shell makes debugging so easy — there's no need to write custom test tools, and it's unbelievably flexible, not to mention the support for separate permissions for each and every command register of every device.

One more thing: this code could be clearer:

fd = open ( "/dev/i2c1/34/45" );
read( fd, &variable, sizeof( variable ) );
close(fd);

It would be much better to have this instead:

fd = open ( "/dev/i2c1/flash/page_number" );
read( fd, &page_number, sizeof( page_number ) );
close (fd );

Of course, you could use #define directives to show meaningful information, but an even better solution is to create a configuration file that the driver could read to create an alias. The configuration file looks like this:

[4=temperature_sensor] 
10=max 
11=min 
12=temperature 
13=alarm 
[5=flash] 
211=page_number

The field inside the square brackets defines the device address and name. The data that follows specifies each register of that device. The main advantages to this approach are:

These predefined devices would always show via the ls command.

When not to use a resource manager

There are times when a resource manager isn't required.

The most obvious case would be if a program doesn't need to receive messages. But still, if your program is event-driven, you might want to look at using the dispatch library to handle internal events. In the future, if your program ever needs to receive messages, you can easily turn it into a resource manager.

Another case might be if your program interfaces only with its children. The parent has access to all the children's information required to interface with them.

Nevertheless, you can turn almost everything into a resource manager. If your resource manager's client uses only the POSIX API, there's less documentation to write, and the code is very portable. Of course, in many cases, providing an API on top of the POSIX API is often very desirable. You could hide the details of devctl() or custom messages. The internals of the API could then be changed if you have to port to a different OS.

If you must transfer high volumes of data at a very high rate, a resource manager can be a problem. Since resource managers basically interface via IPC kernel primitives, they're slower than a simple memcpy(), but nothing prevents you from using a mix of shared memory, POSIX API, and custom messages, all to be hidden in your own API. It wouldn't be too difficult to have the API detect whether or not the resource manager is local and to use shared memory when local and IPC when remote — a way to get the best of both worlds.