Character I/O

Introduction

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:

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 library called io-char to maximize code reuse.


io-char


The io-char module is implemented as a library.

As shown in this diagram, io-char is implemented as a library. 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 library, all drivers inherit these capabilities.

The driver is the executing process that calls into the library. In operation, the driver starts first and invokes io-char. The drivers themselves are just like any other QNX 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 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 memory queues associated with each character device.

Three queues are used for each device. Each queue is implemented using a first-in, first-out (FIFO) mechanism.


Device control


Device I/O in QNX 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. (For details on raw versus edited or canonical input, see the section "Input modes" later in this chapter.)

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 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 devctl() call. The POSIX terminal control functions are layered on top of devctl() as follows:

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.
tcflow()
Suspend or restart data transmission/reception.

QNX extensions

The QNX extensions to the terminal control API are as follows:

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 to run, and the driver 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:


io-char conditions


Conditions for satisfying an input request.

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 QNX 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 PPP protocol used for TCP/IP over a serial link starts and ends its 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's interesting to note that PPP doesn't contain a character count for its frames. Without the data-forwarding character, an implementation might be forced to read the data one character at a time.

FORWARD is a QNX 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 nonfullscreen 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 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 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:

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 console always starts out with a full set of editing keys defined.

If a terminal is connected 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:

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.

Console devices

System consoles (with VGA-compatible graphics chips in text mode) are managed by the devc-con or devc-tcon driver. The video display card/screen and the system keyboard are collectively referred to as the physical console.

The devc-con permits multiple sessions to be run concurrently on a physical console by means of virtual consoles. The devc-con console driver process typically manages more than one set of I/O queues to io-char, which are made available to user processes as a set of character devices with names like /dev/con1, /dev/con2, etc. From the application's point of view, there "really are" multiple consoles available to be used.

Of course, there's only one physical console (screen and keyboard), so only one of these virtual consoles is actually displayed at any one time. The keyboard is "attached" to whichever virtual console is currently visible.

The devc-tcon manager, a "tiny" implementation of devc-con, is intended for memory-constrained systems and supports only a single console.

Terminal emulation

Both console drivers emulate an ANSI terminal; devc-tcon implements a subset of devc-con's extended ANSI emulation.

Serial devices

Serial communication channels are managed by the devc-ser* family of driver processes. These drivers can manage more than one physical channel and provide character devices with names such as /dev/ser1, /dev/ser2, etc.

When devc-ser* is started, command-line arguments can specify which -- and how many -- serial ports are installed. On a PC-compatible system, this will typically be the two standard serial ports often referred to as com1 and com2. The devc-ser* driver directly supports most nonintelligent multiport serial cards.

QNX Neutrino has several serial drivers (e.g. devc-ser8250, devc-serppc800, etc.). For details, see the (devc-ser* entries in Utilities Reference).


Note: Many drivers have a "tiny" version (devc-t*) intended for memory-constrained systems. Note that the tiny versions support raw mode only.

The devc-ser* drivers support hardware flow control (except under edited mode) provided that the hardware supports it. Loss of carrier on a modem can be programmed to deliver a SIGHUP signal to an application process (as defined by POSIX).

Parallel devices

Parallel printer ports are managed by the devc-par driver. When devc-par is started, command-line arguments can specify which parallel port is installed.

The devc-par driver is an output-only driver, so it has no raw input or canonical input queues. The size of the output buffer can be configured with a command-line argument. If configured to a large size, this creates the effect of a software print buffer.

Pseudo terminal devices (ptys)

Pseudo terminals are managed by the devc-pty driver. Command-line arguments to devc-pty specify the number of pseudo terminals to create.

A pseudo terminal (pty) is a pair of character devices: a master device and a slave device. The slave device provides an interface identical to that of a tty device as defined by POSIX. However, while other tty devices represent hardware devices, the slave device instead has another process manipulating it through the master half of the pseudo terminal. That is, anything written on the master device is given to the slave device as input; anything written on the slave device is presented as input to the master device. As a result, pseudo-ttys can be used to connect processes that would otherwise expect to be communicating with a character device.


Pseudo-ttys connecting processes


Pseudo-ttys.

Ptys are routinely used to create pseudo-terminal interfaces for programs like pterm, a terminal emulator that runs under the Photon microGUI and telnet, which uses TCP/IP to provide a terminal session to a remote system.