| Page: | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|
This chapter provides an overview of the STREAMS subsystem, and is intended to highlight the principal differences between STREAMS and standard UNIX System V block and character device drivers. This includes:
The STREAMS subsystem was added to the UNIX operating system to respond to the shortcomings of the character I/O mechanism. It overcame these drawbacks by providing the building blocks for implementing robust, modular data connections for a wide variety of hardware configurations.
The STREAMS subsystem is made up of the following three components:
In this chapter, STREAMS always refers to this system, which makes it possible to build and use an individual Stream. As with other types of devices, a user process communicates with a STREAMS device through system calls. However, opening and communicating with a STREAMS device differs in several ways:
A user process may build a customized stream using special ioctl(2) commands to push modules onto a basic stream (consisting only of the stream head and a driver). Modules added to a stream may perform, for example, canonical processing or they may implement a communication protocol. By breaking out functionality into modules, the driver itself can be kept simple and flexible, and modules can be mixed and matched, as needed.
Modules can also be reused by different streams, decreasing the size of code included in the kernel.
An essential concept in STREAMS programming is the message. All transferred data, control information, queue flushing, errors, and signals are transformed into messages in a Stream. By imposing this uniformity on all information flowing in a Stream, STREAMS can use a standard set of kernel functions and structures for moving and processing messages. To distinguish the different types of information typically passed between devices and processes, STREAMS classifies messages according to two main criteria: message contents type and message priority.
Messages are either data or control type messages. Some examples of control messages are M_IOCTL (generated in response to ioctl(2) system calls), M_SIG (sent upstream to post a signal to a process), and M_DELAY (to request a real-time delay).
Three message types are classified as data messages: M_DATA (which contain only data) and M_PROTO, and M_PCPROTO (which contain some control information in addition to data). The STREAMS function datamsg(D3) is used to test a message to see if it is a data message. Several other functions (flushq(D3), putctl(D3), putctl1(D3)) depend on this distinction.
| Home |
|---|
Messages are further classified as ordinary (also called normal) or priority (also called high priority). Normally, messages are passed from module to module by calling the put routine of a module with a pointer to the message as an argument. The module places ordinary messages on its own message queue where it remains until scheduled for processing. Ordinary messages are defined as those subject to the STREAMS flow control mechanisms, and are processed in the order in which they were placed on the message queues.
Some messages, for example error and nak (negative acknowledgment) messages, must move through the system quickly and so are designated priority messages. These messages are always placed at the head of the queue of messages waiting to be processed. When the queue's service routine is called, priority messages are processed before all ordinary messages.
To ensure uniformity in the passing of messages in a Stream, all messages share a common structure. A message consists of at least one instance of each of the following three constructs:
For most operations, a message is treated as a unit and is referenced by a pointer to its mblk_t structure. See Chapter 5, STREAMS Messages, for more information.
Three STREAMS structures must be declared for a driver to be correctly installed on a UNIX system. First, a module_info structure must be declared and populated with information about the queue to be created. Normally, there will be one instance of the structure for both the read and write sides of the driver but, if they have identical requirements, they may share a module_info structure, as shown here.
static struct module_info spminfo= {0, "sp", 0, INFPSZ,
5120, 1024};
The six members of the module_info structure are: the identification number, the name, the maximum and minimum packet sizes, and the high and low water marks.
In the previous example, no identification number is assigned 0, the name is sp, and no effective minimum and maximum packet sizes are specified (the minimum is 0 and the maximum is set to an infinitely large constant (INFPSZ)). The high and low water marks (5120 and 1024, respectively) are for flow control. The specified numbers are compared against a weighted byte count of all messages held on a message queue.
The second required structure is the qinit(D4) structure. Again, one qinit structure is required for each queue of a driver. When a stream is opened, the system allocates the required queue structures for the driver, and loads the driver entry point addresses from the qinit structures, which must contain non-NULL entries for all routines to be included in the driver, as shown:
static struct qinit rinit={sprput, NULL,spopen,spclose, NULL
&spminfo, NULL};
static struct qinit winit={NULL, NULL,NULL,
NULL, NULL, &spminfo, NULL};
The seven members of the qinit structure are: the put routine, the service routine, the open routine, the close routine, the admin routine (reserved for future expansion), and the addresses of the module_info and the module_stat structures.
The third required structure, streamtab, is pointed to by the cdevsw table, and contains addresses of the read and write qinit structures.
struct streamtab spinfo={&rinit,&winit, NULL, NULL};
For multiplexing drivers, a set of upper and lower qinit structures are required, and therefore the streamtab structure contains four entries, which are shown as follows:
struct streamtab spinfo= {&urinit, &uwinit, &lrinit, &lwinit};
| Home |
|---|
This section outlines each of the entry points that may be included in a STREAMS module or driver. The inclusion of a particular routine depends on the functionality required. For example, the CLONE driver (described in Section 3.4.1.1) has only an open routine. The emphasis of this section is on how drivers, not modules, use these routines. Much of the information applies to modules as well.
When a STREAMS device is opened (with the open(2) system call), the subsystem uses the cdevsw structure to identify the device type, and creates a stream consisting of the stream head and the driver. The driver's open routine is then executed. Though similar to a non-STREAMS driver open, the STREAMS routine has a different syntax. The dev and flag arguments are the same as in the standard driver open, although some of the flag values do not apply to STREAMS devices. The other two arguments are:
The driver must return 0 if it is successful, or an appropriate errno value if it is not.
The only entry points that can communicate with the user level in a stream are the open and close routines. Only these two routines can sleep (at a priority less than PZERO), but must explicitly return to the driver routine if a signal is received. They may also access the user structure.
The open and close routines must be specified in the read queue of the driver.
The open routine may be used to initialize a private driver data structure, pointed to by the q_ptr member of the queue structure. See open(D2) for more information about opening STREAMS drivers.
When the value of sflag has been set to CLONEOPEN, the CLONE driver is invoked. The CLONE driver has been provided to select a minor device number (that is, an unused stream). Without the CLONE driver, user processes would have to make an ioctl(2) call to search through a driver's minor devices for an unused one. To eliminate this requirement, STREAMS allows a driver to be implemented as a cloneable device. The CLONE driver removes the need to search for an unused stream.
Networking applications may sometimes require a separate stream for each communication channel. Because the user process needs a minor device number but is not concerned about the particular number, the CLONE driver is used to make the selection.
The CLONE driver consists of only an open routine. The minor portion of the device number passed to the CLONE driver is actually the major number of the cloneable device. The CLONE driver looks for the cloneable device in the cdevsw. See clone(7).
The driver open routine must first test the sflag to see if it has been set to a value of CLONEOPEN. If it has, the driver searches for the first unused minor device number, up to devcnt (the maximum number of streams this device may support). An example is shown next.
case CLONEOPEN:
for (dev=0 ; dev < devcnt; dev++);
The code shown then searches through the table until it finds the first open minor device, and returns that value.
| Home |
|---|
The put and service routines represent the two basic ways a STREAMS module or driver processes messages. The put routine bypasses the flow control mechanism altogether, providing the fastest possible throughput. srv (service) routines are subject to flow control by the scheduler. A driver that must wait for output to complete before sending another message should use the scheduling mechanism.
Whether a driver has a put routine, a srv routine, or both depends on the processing the driver must perform. For a module, which has at least a driver downstream and the stream head upstream, put and srv procedures may be appropriate for both queues of the module. However, a driver does not normally need to have a write service routine, because it is not passing messages to another queue.
The flexibility of STREAMS makes it almost impossible to establish rigid rules about which routines a driver should include. The next two sections show some of the typical processing done by the put and srv routines.
| Home |
|---|
The pseudo-code example shown next is a generic write queue put routine for a driver and illustrates the basic structure used by many drivers.
1 xxwput(q, mp)
2 queue_t *q;
3 mblk_t *mp;
4 {
5 switch (message type) {
6
7 case M_FLUSH:
8 flush specified queues
9 send flush message upstream
10 free message block
11
12 case M_IOCTL:
13 if recognizable command type
14 handle
15 else
16 send M_IOCNAK message upstream
17
18 case M_DATA:
19 output data to device
20
21 default:
22 send error message upstream
23 )
The main task of this routine is to detect the incoming message's type, and then use a switch statement to process each type. The next five subsections illustrate how different message types are typically handled.
Each line of the pseudo-code will be expanded into C language statements illustrating how the functionality is implemented.
The message type of a STREAMS message is stored in the db_type field of the data block (dblk_t) structure of the message. If mp is a pointer to the message block, the type can be referenced with the following statement:
switch (mp->b_datap->db_type)
This line corresponds to line 5 in Figure 3-1.
Drivers must flush message queues when either the FLUSHR (flush the read queue) or FLUSHW (flush the write queue) bits have been set.
1 if (*mp->b_rptr & FLUSHW) flushq(q, FLUSHDATA);
2 if (*mp->b_rptr & FLUSHR) {
3 flushq(RD(q), FLUSHDATA);
4 *mp->rptr &= ~FLUSHW;
5 qreply(q, mp);
6 return;
7 }
8 freemsg(mp);
In line 1, the first byte of the message (mp->b_rptr) is tested to see if FLUSHW is set. If it is, the flushq(D3) function is called to remove messages from queue q. The FLUSHDATA flag removes only data messages; FLUSHALL would also remove control messages.
If the FLUSHR bit is also set (line 2), messages destined for the user process are flushed. The RD(D3) macro (line 3) is used to access the mate queue of q. The FLUSHW bit is then cleared (line 4) and the message is sent upstream (line 5). In line 8 the freemsg(D3) function deallocates the memory used by the message and data blocks.
| Home |
|---|
The stream head interprets I_STR type ioctl(2) commands and constructs M_IOCTL messages from them. Processing depends on the driver and type of message. If the message type is not recognized, the driver should send a negative acknowledgment message back upstream, as shown next.
mp->b_datap->db_type = M_IOCNAK;
qreply(q, mp);
return;
Using the same message buffer as the incoming message, the driver changes the incoming message into a message of type M_IOCNAK and sends the negative acknowledgment back upstream via qreply().
M_DATA messages may represent the normal type of data for output to the device. Processing may occur in line or, more likely, in a subordinate routine that is called to handle the actual output.
Data messages also may be enqueued for processing by the srv (service) routine with the putq(D3) function, as shown next.
putq(q, mp);
The arguments to the function are the pointer to the queue to be enabled (q) and a pointer to the message to be enqueued (mp.)
The default case is included to catch all unrecognized message types received by the driver.
mp->b_datap->db_type = M_ERROR;
mp->b_rptr = mp->b_datap->db_base;
*mp->b_rptr = EPROTO;
mp->b_wptr = mp->b_rptr+1;
qreply(q, mp);
return;
In the same way as the negative acknowledgment was sent upstream, an error message (M_ERROR) is sent to the user process.
| Home |
|---|
The service routine is called by the STREAMS scheduler to process messages linked to the queue. The scheduler calls service routines for all active queues in FIFO order.
Service routines are typically included in modules rather than in drivers, and a driver's downstream (write) queue generally does not need one. On the upstream side, some drivers may simply discard data if unable to pass it to the next queue. If this approach is inappropriate, the driver's read queue may include a service routine, as shown in this pseudo-code example.
1 xxrsrv(q)
2 queue_t *q;
3 {
4 while (more messages on queue)
5 retrieve next message
6 if (next queue is full)
7 put back on queue
8 else
9 process message
10 send to next queue
11 }
The rest of this section shows how a driver typically handles read-side messages. As was done in the put routine example, C language fragments corresponding to the pseudo-code will be presented.
The getq(D3) function attempts to retrieve the next message on the queue.
while (mp = getq(q))
The upstream queue must be tested with the canput(D3) function to see if the message may be passed to the next put procedure.
if (!canput(q->q_next))
The putbq(D3) function places the message back on the queue, and awaits a successful canput call. All priority messages are placed ahead of ordinary messages.
putbq(q, mp)
The message pointed at by mp is placed at the beginning of the message queue pointed at by q.
| Home |
|---|
The putnext(D3) macro passes the messages to the put procedure of the next queue upstream, but only after canput has succeeded.
putnext(q, mp)
mp is a pointer to the message to be sent, and q is a pointer to the sending (not the next, or receiving) queue.
Like the open routine, the close routine is specified in the read queue of the driver. The argument to the close routine is a pointer to the queue.
Typically, the close routine of a STREAMS driver performs the following functions:
| Home |
|---|
| Contents | Previous Chapter | Next Chapter | Index | Glossary |