|
A key requirement of any realtime operating system
is high-performance character I/O. Character devices can be described as devices to which I/O
consists of a sequence of bytes transferred serially, as opposed to block-oriented devices (e.g. disk
drives).
As in the POSIX and UNIX tradition, these character
devices are located in the OS pathname space under the /dev
directory. For example, a serial port to which a modem or terminal could be connected might appear in
the system as:
/dev/ser1
Typical character devices found on PC hardware
include:
- serial ports
- parallel ports
- text-mode consoles
- pseudo terminals (ptys)
Programs access character devices using the standard
open(), close(), read(), and write() API functions. Additional functions
are available for manipulating other aspects of the character device, such as baud rate, parity, flow
control, etc.
Since it's common to run multiple character devices,
they have been designed as a family of drivers and a shared library called
io-char to maximize code reuse.
 |
In Neutrino 2.0, the character I/O drivers are statically
linked against the io-char shared object. |
 |
The io-char module is implemented as a shared library. |
As shown in this diagram,
io-char is implemented as a shared library (essentially passive
blocks of code resident in memory). The io-char module contains all
the code to support POSIX semantics on the device. It also contains a significant amount of code to
implement character I/O features beyond POSIX but desirable in a realtime system. Since this code is
in the common shared library, all drivers inherit these capabilities.
The driver is the executing process that calls into
the shared library. In operation, the driver process starts first and invokes
io-char. The drivers themselves are just like any other Neutrino
process and can run at different priorities according to the nature of the hardware being controlled
and the client's requesting service.
Once a single character device is running, the
memory cost of adding additional devices is minimal, since only the code to implement the new driver
structure would be new.
Driver/io-char.so communication
The io-char library
manages the flow of data between an application and the device driver. Data flows between
io-char and the driver through a set of shared memory queues for each
character device.
Three queues are used for each device. Each queue is
implemented using a first-in, first-out (FIFO) mechanism.
 |
Device I/O in Neutrino. |
Received data is placed into the raw input queue by
the driver and is consumed by io-char only when application processes
request data. Interrupt handlers within drivers typically call a trusted library routine within
io-char to add data to this queue - this ensures a consistent input
discipline and minimizes the responsibility of the driver (and effort required to create new
drivers).
The io-char module
places output data into the output queue to be consumed by the driver as characters are physically
transmitted to the device. The module calls a trusted routine within the driver process each time new
data is added so it can "kick" the driver into operation (in the event that it was idle). Since
output queues are used, io-char implements write-behind for
all character devices. Only when the output buffers are full will
io-char cause a process to block while writing.
The canonical queue is managed entirely by
io-char and is used while processing input data in edited
mode. The size of this queue determines the maximum edited input line that can be processed for a
particular device.
The sizes of these queues are configurable using
command-line options. Default values are usually more than adequate to handle most hardware
configurations, but you can "tune" these to reduce overall system memory requirements, to accommodate
unusual hardware situations, or to handle unique protocol requirements.
Device drivers simply add received data to the raw
input queue or consume and transmit data from the output queue. The
io-char module decides when (and if) output transmission is to be
suspended, how (and if) received data is echoed, etc.
Device control
Low-level device control is implemented using the
POSIX devctl() call. The POSIX terminal control functions are layered on top of
devctl() as follows:
| Function |
Description |
| tcgetattr() |
Get terminal attributes. |
| tcsetattr() |
Set terminal attributes. |
| tcgetpgrp() |
Get ID of process group leader for a terminal. |
| tcsetpgrp() |
Set ID of process group leader for a terminal. |
| tcsendbreak() |
Send a break condition. |
Neutrino extensions
The Neutrino extensions to the terminal control API
are as follows:
| Function |
Description |
| tcdropline() |
Initiate a disconnect. For a serial device this will pulse the DTR
line. |
| tcinject() |
Inject characters into the canonical buffer. |
The io-char module
acts directly on a common set of devctl() commands supported by most drivers. Applications
send device-specific devctl() commands through io-char to the
drivers.
Input modes
Each device can be in a raw or
edited input mode.
Raw input mode
In raw mode, io-char
performs no editing on received characters. This reduces the processing done on each character to a
minimum and provides the highest performance interface for reading data.
Fullscreen programs and serial communications
programs are examples of applications that use a character device in raw mode.
In raw mode, each character is received into the raw
input buffer by the interrupt handler. When an application requests data from the device, it can
specify under what conditions an input request is to be satisfied. Until the conditions are
satisfied, the interrupt handler won't signal the driver process to run and the driver process won't
return any data to the application. The normal case of a simple read by an application would block
until at least one character was available.
The following diagram shows the full set of
available conditions:
 |
Conditions under which an input request will be satisfied. |
In the case where multiple conditions are specified,
the read will be satisfied when any one of them is satisfied.
MIN
The qualifier MIN is useful when an
application has knowledge of the number of characters it expects to receive.
Any protocol that knows the character count for a
frame of data can use MIN to wait for the entire frame to arrive. This significantly reduces
IPC and process scheduling. MIN is often used in conjunction with TIME or
TIMEOUT. MIN is part of the POSIX standard.
TIME
The qualifier TIME is useful when an
application is receiving streaming data and wishes to be notified when the data stops or pauses. The
pause time is specified in 1/10ths of a second. TIME is part of the POSIX standard.
TIMEOUT
The qualifier TIMEOUT is useful when an
application has knowledge of how long it should wait for data before timing out. The timeout is
specified in 1/10ths of a second.
Any protocol that knows the character count for a
frame of data it expects to receive can use TIMEOUT. This in combination with the baud rate
allows a reasonable guess to be made when data should be available. It acts as a deadman timer to
detect dropped characters. It can also be used in interactive programs with user input to timeout a
read if no response is available within a given time.
TIMEOUT is a Neutrino extension and is not
part of the POSIX standard.
FORWARD
The qualifier FORWARD is useful when a
protocol is delimited by a special framing character. For example, the SLIP and PPP protocols used
for TCP/IP over a serial link start and end their packets with a framing character. When used in
conjunction with TIMEOUT, the FORWARD character can greatly improve the efficiency of a
protocol implementation. The protocol process will receive complete frames, rather than character by
character. In the case of a dropped framing character, TIMEOUT or TIME can be used to
quickly recover.
This greatly minimizes the amount of IPC work for
the OS and results in a much lower processor utilization for a given TCP/IP data rate. It is
interesting to note that neither SLIP nor PPP contain a character count for their frames. Without the
data-forwarding character, an implementation might be forced to read the data one character at a
time.
FORWARD is a Neutrino extension and is not
part of the POSIX standard.
The ability to "push" the processing for application
notification into the service-providing components of the OS reduces the frequency with which
user-level processing must occur. This minimizes the IPC work to be done in the system and frees CPU
cycles for application processing. In addition, if the application implementing the protocol is
executing on a different network node than the communications port, the number of network
transactions is also minimized.
For intelligent, multiport serial cards, the data-
forwarding character recognition can also be implemented within the intelligent serial card itself,
thereby significantly reducing the number of times the card must interrupt the host processor for
interrupt servicing.
Edited input mode
In edited mode,
io-char performs line-editing operations on each received character.
Only when a line is "completely entered" - typically when a carriage return (CR) is received - will
the line of data be made available to application processes. This mode of operation is often referred
to as canonical or sometimes "cooked" mode.
Most non-fullscreen applications run in edited mode,
because this allows the application to deal with the data a line at a time, rather than have to
examine each character received, scanning for an end-of-line character.
In edited mode, each character is received into the
raw input buffer by the interrupt handler. Unlike raw mode where the driver process is scheduled to
run only when some input conditions are met, the interrupt handler will schedule the driver on every
received character.
There are two reasons for this. First, edited input
mode is rarely used for high-performance communication protocols. Second, the work of editing is
significant and not suitable for an interrupt handler.
When the driver process runs, code in
io-char will examine the character and apply it to the canonical
buffer in which it's building a line. When a line is complete and an application requests input, the
line will be transferred from the canonical buffer to the application - the transfer is direct from
the canonical buffer to the application buffer without any intervening copies.
The editing code correctly handles multiple pending
input lines in the canonical buffer and allows partial lines to be read. This can happen, for
example, if an application asked only for 1 character when a 10-character line was available. In this
case, the next read will continue where the last one left off.
The io-char module
provides a rich set of editing capabilities, including full support for moving over the line with
cursor keys and for changing, inserting, or deleting characters. Here are some of the more common
capabilities:
| Character |
Description |
| LEFT |
move the cursor one character to the left |
| RIGHT |
move the cursor one character to the right |
| HOME |
move the cursor to the beginning of the line |
| END |
move the cursor to the end of the line |
| ERASE |
erase the character to the left of the cursor |
| DEL |
erase the character at the current cursor position |
| KILL |
erase the entire input line |
| UP |
erase the current line and recall a previous line |
| DOWN |
erase the current line and recall the next line |
| INS |
toggle between insert mode and typeover mode (every new line starts in insert
mode) |
Line-editing characters vary from terminal to
terminal. The Neutrino console always starts out with a full set of editing keys defined.
If a terminal is connected to Neutrino via a serial
channel, you need to define the editing characters that apply to that particular terminal. To do
this, you can use the stty utility. For example, if you have an ANSI
terminal connected to a serial port (called /dev/ser1), you would use
the following command to extract the appropriate editing keys from the
terminfo database and apply them to
/dev/ser1:
stty term=ansi </dev/ser1
Device subsystem performance
The flow of events within the device subsystem is
engineered to minimize overhead and maximize throughput when a device is in raw mode. To
accomplish this, the following rules are used:
- Interrupt handlers place received data directly into a memory queue. Only when a read operation
is pending, and that read operation can be satisfied, will the interrupt handler schedule
the driver process to run. In all other cases, the interrupt simply returns. Moreover, if
io-char is already running, no scheduling takes place, since the
availability of data will be noticed without further notification.
- When a read operation is satisfied, the driver process replies to the application process
directly from the raw input buffer into the application's receive buffer. The net result is
that the data is copied only once.
These rules - coupled with the extremely small
interrupt and scheduling latencies inherent within the OS - result in a very lean input model that
provides POSIX conformance together with extensions suitable to the realtime requirements of protocol
implementations.
|