|
Architecturally, there are two categories of messages that a
resource manager will receive:
- connect messages
- I/O messages.
A connect message is issued by the client to perform an
operation based on a pathname (e.g. an io_open message). This may involve
performing operations such as permission checks (does the client have the correct permission to open this
device?) and setting up a context for that request.
An I/O message is one that relies upon this context (created
between the client and the resource manager) to perform subsequent processing of I/O messages (e.g.
io_read).
There are good reasons for this design. It would be
inefficient to pass the full pathname for each and every read() request, for example. The
io_open handler can also perform tasks that we want done only once (e.g.
permission checks), rather than with each I/O message. Also, when the read() has read 4096 bytes from a
disk file, there may be another 20 megabytes still waiting to be read. Therefore, the read() function
would need to have some context information telling it the position within the file it's reading
from.
The resource manager shared library
In a custom embedded system, part of the design effort may be
spent writing a resource manager, because there may not be an off-the-shelf driver available for the custom
hardware component in the system.
Neutrino's resource manager shared library makes this task
relatively simple.
Automatic default message handling
If there are functions that the resource manager doesn't want
to handle for some reason (e.g. a digital-to-analog converter doesn't support a function such as
lseek(), or the software doesn't require it), the shared library will conveniently supply default
actions.
There are two levels of default actions:
- The first level simply returns ENOSYS to the client application, informing it that that particular
function is not supported.
- The second level (i.e. the iofunc_*() shared library) allows a resource manager to automatically
handle various functions.
For more information on default actions, see the section on
"Second level default message handling" in this chapter.
open(), dup(), and close()
Another convenient service that the resource manager shared
library provides is the automatic handling of dup() messages.
Suppose that the client program executed code that
eventually ended up performing:
fd = open ("/dev/device", O_RDONLY);
...
fd2 = dup (fd);
...
fd3 = dup (fd);
...
close (fd3);
...
close (fd2);
...
close (fd);
The client would generate an
io_open message for the first open(), and then two
io_dup messages for the two dup() calls. Then, when the client executed
the close() calls, three io_close messages would be
generated.
Since the dup() functions generate duplicates of the
file descriptors, new context information should not be allocated for each one. When the
io_close messages arrive, because no new context has been allocated for each
dup(), no release of the memory by each io_close message should
occur either! (If it did, the first close would wipe out the context.)
The resource manager shared library provides default handlers
that keep track of the open(), dup(), and close() messages and perform work only for the
last close (i.e. the third io_close message in the example above).
Multiple thread handling
One of the salient features of Neutrino is the ability to use
threads. By using multiple threads, a resource manager can be structured so that several threads are
waiting for messages and then simultaneously handling them.
This thread management is another convenient function
provided by the resource manager shared library. Besides keeping track of both the number of threads created
and the number of threads waiting, the library also takes care of maintaining the optimal number of
threads.
Dispatch functions
Neutrino provides a set of dispatch_* functions
that:
- allow a common blocking point for managers and clients that need to support multiple message types (e.g.
a resource manager could handle its own private message range).
- provide a flexible interface for message types that isn't tied to the resource manager (for clean
handling of private messages and pulse codes)
- decouple the blocking and handler code from threads. Also lets you implement the resource manager event
loop in your main code. This decoupling also makes for easier debugging, because you one can put a breakpoint
between the block function and the handler function.
For more information, see the chapter on Writing a Resource
Manager in Building Embedded Systems.
Combine messages
In order to conserve network bandwidth and to provide support
for atomic operations, Neutrino supports combine messages. A combine message is constructed by the
client's C library and consists of a number of I/O and/or connect messages packaged together into
one.
To support atomic operations, a number of messages must be
combined into one bigger message so that the resource manager receives the entire message all in one
piece.
For example, the function readblock() allows a thread
to atomically perform an lseek() and read() operation. This is done in the client library by
combining the io_lseek and io_read messages
into one. When the resource manager shared library receives the message, it will process both the
io_lseek and io_read messages, effectively
making that readblock() function behave atomically.
Combine messages are also useful for the stat()
function. A stat() call can be implemented in the client's library as an open(), fstat(),
and close(). Instead of generating three separate messages (one for each of the component functions),
the library puts them together into one contiguous combine message. This boosts performance, especially over a
networked connection, and also simplifies the resource manager, which doesn't need a connect function to handle
stat().
The resource manager shared library takes care of the issues
associated with breaking out the individual components of the combine message and passing them to the various
handler functions supplied. Again, this minimizes the effort associated with writing a resource
manager.
Second level default message handling
Because a large number of the messages received by a
resource manager deal with a common set of attributes, Neutrino provides another level of default handling.
This second level, called the iofunc_*() shared library, allows a resource manager to handle functions
like stat(), chmod(), chown(), lseek(), etc. automatically, without the
programmer having to write additional code. As an added benefit, these iofunc_*() default handlers
implement the POSIX semantics for the messages, again offloading work from the programmer.
Three main structures need to be considered:
- context
- attributes structure
- mount structure
 |
A resource manager is responsible for three data structures. |
The first data structure, the context, has already been
discussed (see the section on "Message types"). It holds data used on a per-open basis,
such as the current position into a file (the lseek() offset).
Because a resource manager may be responsible for more than
one device (e.g. devc-ser* may be responsible for
/dev/ser1, /dev/ser2,
/dev/ser3, etc.), the attributes structure holds data on a per-device
basis. The attributes structure contains such items as the user and group ID of the owner of the device, the
last modification time, etc.
For block I/O devices, one more structure is used. This is the
mount structure, which contains data items that are global to the entire mount device.
When a number of client programs have opened various devices
on a particular resource, the data structures may look like this:
 |
Multiple clients opening various devices on a resource manager. |
The iofunc_*() default functions operate on the
assumption that the programmer has used the default definitions for the context block and the attributes
structures. This is a safe assumption for two reasons:
- The default context and attribute structures contain sufficient information for most applications.
- If the default structures don't hold enough information, they can be encapsulated within the structures
that the programmer has defined.
By definition, the default structures must be the first
members of their respective superstructures, allowing clean and simple access to the requisite base members by
the iofunc_*() default functions:
 |
If the default context and attribute structures don't hold enough information, they can be encapsulated within programmer-defined structures. |
The library contains iofunc_*() default handlers for
these client functions:
- chmod()
- chown()
- close()
- devctl()
- fpathconf()
- fseek()
- fstat()
- lock()
- lseek()
- mmap()
- open()
- pathconf()
- stat()
- utime()
|