STREAMS Programming Guide

STREAMS Driver Code Samples

The following discussion describes characteristics of a STREAMS driver:

Printer Driver Example

Example 9–1 is a sample print driver for an interrupt-per-character line printer. The driver is unidirectional—it has no read-side processing. It demonstrates some differences between module and driver programming, including the following:

Most of the STREAMS processing in the driver is independent of the actual printer hardware; in this example, actual interaction with the printer is limited to the lpoutchar function, which prints one character at a time. For purposes of demonstration, the “printer hardware” is actually the system console, accessed through cmn_err(9F). Since there's no actual hardware to generate a genuine hardware interrupt, lpoutchar simulates interrupts using ddi_trigger_softintr(9F). For a real printer, the lpoutchar function is rewritten to send a character to the printer, which should generate a hardware interrupt.

The driver declarations follow. After specifying header files (include <sys/ddi.h> and <sys/sunddi.h> as the last two header files), the driver declares a per-printer structure, struct lp. This structure contains members that enable the driver to keep track of each instance of the driver, such as flags (what the driver is doing), msg (the current STREAMS print message), qptr (pointer to the stream's write queue), dip (the instance's device information handle), iblock_cookie (for registering an interrupt handler), siid (the handle of the soft interrupt), and lp_lock (a mutex to protect the data structure from multithreaded race conditions). The driver next defines the bits for the flags member of struct lp; the driver defines only one flag, BUSY.

Following function prototypes, the driver provides some standard STREAMS declarations: a module_info(9S) structure (minfo), a qinit(9S) structure for the read side (rinit) that is initialized by the driver's open and close entry points, a qinit(9S) structure for the write side (winit) that is initialized by the write put procedure, and a streamtab(9S) that points to rinit and winit. The values in the module name and ID fields in the module_info(9S) structure must be unique in the system. Because the driver is unidirectional, there is no read side put or service procedure. The flow control limits for use on the write side are 50 bytes for the low-watermark and 150 bytes for the high-watermark.

The driver next declares lp_state. This is an anchor on which the various “soft-state” functions provided by the DDK operate. The ddi_soft_state(9F) manual page describes how to maintain multiple instances of a driver.

The driver next declares acb_ops(9S) structure, which is required in all device drivers. In non-STREAMS device drivers, cb_ops(9S) contains vectors to the table-driven entry points. For STREAMS drivers, however, cb_ops(9S) contains mostly nodev entries. The cb_stream field, however, is initialized with a pointer to the driver's streamtab(9S) structure. This indicates to the kernel that this driver is a STREAMS driver.

Next, the driver declares a dev_ops(9S) structure, which points to the various initialization entry points as well as to the cb_ops(9S) structure. Finally, the driver declares a struct moldrv and a struct modlinkage for use by the kernel linker when the driver is dynamically loaded. struct moldrv contains a pointer to mod_driverops (a significant difference between a STREAMS driver and a STREAMS module—a STREAMS module would contain a pointer to mod_strops instead).


Example 9–1 Simple Line Printer Driver

#include <sys/types.h>
#include <sys/param.h>
#include <sys/stream.h>
#include <sys/stropts.h>
#include <sys/signal.h>
#include <sys/errno.h>
#include <sys/cred.h>
#include <sys/stat.h>
#include <sys/modctl.h>
#include <sys/conf.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>

/* This is a private data structure, one per minor device number */

struct lp {
	short flags; /* flags -- see below */
	mblk_t *msg; /* current message being output */
	queue_t *qptr; /* back pointer to write queue */
	dev_info_t *dip; /* devinfo handle */
	ddi_iblock_cookie_t iblock_cookie;
	ddi_softintr_t siid;
	kmutex_t lp_lock; /* sync lock */
};

/* flags bits */

#define BUSY 1 /* dev is running, int is forthcoming */

/*
 * Function prototypes.
 */
static int lpattach(dev_info_t *, ddi_attach_cmd_t);
static int lpdetach(dev_info_t *, ddi_detach_cmd_t);
static int lpgetinfo(dev_info_t *, ddi_info_cmd_t, void *, void **);
static int lpidentify(dev_info_t *);
static uint lpintr(caddr_t lp);
static void lpout(struct lp *lp);
static void lpoutchar(struct lp *lp, char c);
static int lpopen(queue_t*, dev_t*, int, int, cred_t*);
static int lpclose(queue_t*, int, cred_t*);
static int lpwput(queue_t*, mblk_t*);

/* Standard Streams declarations */

static struct module_info minfo = {
	0xaabb,
	"lp",
	0,
	INFPSZ,
	150,
	50
};

static struct qinit rinit = {
	(int (*)()) NULL,
	(int (*)()) NULL,
	lpopen,
	lpclose,
	(int (*)()) NULL,
	&minfo,
	NULL
};

static struct qinit winit = {
	lpwput,
	(int (*)()) NULL,
	(int (*)()) NULL,
	(int (*)()) NULL,
	(int (*)()) NULL,
	&minfo,
	NULL
};

static struct streamtab lpstrinfo = { &rinit, &winit, NULL, NULL };

/*
 * An opaque handle where our lp lives
 */
static void *lp_state;

/* Module Loading/Unloading and Autoconfiguration declarations */

static struct cb_ops lp_cb_ops = {
	nodev, /* cb_open */
	nodev, /* cb_close */
	nodev, /* cb_strategy */
	nodev, /* cb_print */
	nodev, /* cb_dump */
	nodev, /* cb_read */
	nodev, /* cb_write */
	nodev, /* cb_ioctl */
	nodev, /* cb_devmap */
	nodev, /* cb_mmap */
	nodev, /* cb_segmap */
	nochpoll, /* cb_chpoll */
	ddi_prop_op, /* cb_prop_op */
	&lpstrinfo, /* cb_stream */
	D_MP | D_NEW, /* cb_flag */
};

static struct dev_ops lp_ops = {
	DEVO_REV, /* devo_rev */
	0, /* devo_refcnt */
	lpgetinfo, /* devo_getinfo */
	lpidentify, /* devo_identify */
	nulldev, /* devo_probe */
	lpattach, /* devo_attach */
	lpdetach, /* devo_detach */
	nodev, /* devo_reset */
	&lp_cb_ops, /* devo_cb_ops */
	(struct bus_ops *)NULL /* devo_bus_ops */
};

/*
 * Module linkage information for the kernel.
 */
static struct modldrv modldrv = {
	&mod_driverops,
	"Simple Sample Printer Streams Driver", /* Description */
	&lp_ops, /* driver ops */
};

static struct modlinkage modlinkage = {
	MODREV_1, &modldrv, NULL
};

Example 9–2 shows the required driver configuration entry points _init(9E), _fini(9E), and _info(9E). In addition to installing the driver using mod_install(9F), the _init entry point also initializes the per-instance driver structure using ddi_soft_state_init(9F). _fini(9E) performs the complementary calls to mod_remove(9F) and ddi_soft_state_fini(9F) to unload the driver and release the resources used by the soft-state routines.


Example 9–2 Configuration Entry Points

int
_init(void)
{
	int e;

	if ((e = ddi_soft_state_init(&lp_state,
	 sizeof (struct lp), 1)) != 0) {
		return (e);
	}

	if ((e = mod_install(&modlinkage)) != 0) {
		ddi_soft_state_fini(&lp_state);
	}

	return (e);
}

int
_fini(void)
{
	int e;

	if ((e = mod_remove(&modlinkage)) != 0) {
		return (e);
	}
	ddi_soft_state_fini(&lp_state);
	return (e);
}

int
_info(struct modinfo *modinfop)
{
	return (mod_info(&modlinkage, modinfop));
}

Example 9–3 shows the lp driver's implementation of the initialization entry points. In lpidentify, the driver ensures that the name of the device being attached is “lp”.

lpattach first uses ddi_soft_state_zalloc(9F) to allocate a per-instance structure for the printer being attached. Next it creates a node in the device tree for the printer using ddi_create_minor_node(9F); user programs use the node to access the device. lpattach then registers the driver interrupt handler because the sample is driver pseudo-hardware, the driver uses soft interrupts. A driver for a real printer would use ddi_add_intr(9F) instead of ddi_add_softintr(9F). A driver for a real printer would also need to perform any other required hardware initialization in lpattach. Finally, lpattach initializes the per-instance mutex.

In lpdetach, the driver undoes everything it did in lpattach.

lpgetinfo uses the soft-state structures to obtain the required information.


Example 9–3 Initialization Entry Points

static int
lpidentify(dev_info_t *dip)
{
	if (strcmp(ddi_get_name(dip), "lp") == 0) {
		return (DDI_IDENTIFIED);
	} else
		return (DDI_NOT_IDENTIFIED);
}

static int
lpattach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
	int instance;
	struct lp *lpp;

	switch (cmd) {

	case DDI_ATTACH:

		instance = ddi_get_instance(dip);

		if (ddi_soft_state_zalloc(lp_state, instance) != DDI_SUCCESS) {
			cmn_err(CE_CONT, "%s%d: can't allocate state\n",
			 ddi_get_name(dip), instance);
			return (DDI_FAILURE);
		} else
			lpp = ddi_get_soft_state(lp_state, instance);

		if (ddi_create_minor_node(dip, "strlp", S_IFCHR,
		 instance, NULL, 0) == DDI_FAILURE) {
			ddi_remove_minor_node(dip, NULL);
			goto attach_failed;

		}

		lpp->dip = dip;
		ddi_set_driver_private(dip, (caddr_t)lpp);

		/* add (soft) interrupt */


		if (ddi_add_softintr(dip, DDI_SOFTINT_LOW, &lpp->siid,
		 &lpp->iblock_cookie, 0, lpintr, (caddr_t)lpp)
		 != DDI_SUCCESS) {
			ddi_remove_minor_node(dip, NULL);
			goto attach_failed;
		}

		mutex_init(&lpp->lp_lock, "lp lock", MUTEX_DRIVER,
		 (void *)lpp->iblock_cookie);


		ddi_report_dev(dip);
		return (DDI_SUCCESS);

	default:
		return (DDI_FAILURE);
	}

attach_failed:
	/*
	 * Use our own detach routine to toss
	 * away any stuff we allocated above.
	 */
	(void) lpdetach(dip, DDI_DETACH);
	return (DDI_FAILURE);
}

static int
lpdetach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
	int instance;
	struct lp *lpp;

	switch (cmd) {

	case DDI_DETACH:
		/*
		 * Undo what we did in lpattach, freeing resources
		 * and removing things we installed. The system
		 * framework guarantees we are not active with this devinfo
		 * node in any other entry points at this time.
		 */
		ddi_prop_remove_all(dip);
		instance = ddi_get_instance(dip);
		lpp = ddi_get_soft_state(lp_state, instance);
		ddi_remove_minor_node(dip, NULL);
		ddi_remove_softintr(lpp->siid);
		ddi_soft_state_free(lp_state, instance);
		return (DDI_SUCCESS);

	default:
		return (DDI_FAILURE);
	}
}

/*ARGSUSED*/
static int
lpgetinfo(dev_info_t *dip, ddi_info_cmd_t infocmd, void *arg,
          void **result)
{
	struct lp *lpp;
	int error = DDI_FAILURE;

	switch (infocmd) {
	case DDI_INFO_DEVT2DEVINFO:
		if ((lpp = ddi_get_soft_state(lp_state,
		 getminor((dev_t)arg))) != NULL) {
			*result = lpp->dip;
			error = DDI_SUCCESS;
		} else
			*result = NULL;
		break;

	case DDI_INFO_DEVT2INSTANCE:
		*result = (void *)getminor((dev_t)arg);
		error = DDI_SUCCESS;
		break;

	default:
		break;
	}

	return (error);
}

The STREAMS mechanism allows only one stream per minor device. The driver open routine is called whenever a STREAMS device is opened. open matches the correct private data structure with the stream using ddi_get_soft_state(9F). The driver open, lpopen in Example 9–4, has the same interface as the module open.

The stream flag, sflag, must have the value 0, indicating a normal driver open. devp pointers to the major/minor device number for the port. After checking sflag, lpopen uses devp to find the correct soft-state structure.

The next check, if (q->q_ptr)..., determines if the printer is already open. q_ptr is a driver or module private data pointer. It can be used by the driver for any purpose and is initialized to zero by STREAMS before the first open. In this example, the driver sets the value of q_ptr, in both the read and write queue structures, to point to the device's per-instance data structure. If the pointer is non-NULL, it means the printer is already open, so lpopen returns EBUSY to avoid merging printouts from multiple users.

The driver close routine is called by the stream head. Any messages left in the queue are automatically removed by STREAMS. The stream is dismantled and data structures are released.


Example 9–4 Queue Processing Entry Points

/*ARGSUSED*/
static int
lpopen(
	queue_t *q,	/* read queue */
	dev_t *devp,
	int flag,
	int sflag,
	cred_t *credp)

{

	struct lp *lp;

	if (sflag)	/* driver refuses to do module or clone open */
			return (ENXIO);

	if ((lp = ddi_get_soft_state(lp_state, getminor(*devp))) == NULL)
			return (ENXIO);

	/* Check if open already. Can't have multiple opens */

	if (q->q_ptr) {
			return (EBUSY);
	}

	lp->qptr = WR(q);
	q->q_ptr = (char *) lp;
	WR(q)->q_ptr = (char *) lp;
	qprocson(q);
	return (0);
}

/*ARGSUSED*/
static int
lpclose(
	queue_t *q,		/* read queue */
	int flag,
	cred_t *credp)
{

	struct lp *lp;

	qprocsoff(q);
	lp = (struct lp *) q->q_ptr;

	/*
	 * Free message, queue is automatically
	 * flushed by STREAMS
	 */
	mutex_enter(&lp->lp_lock);

	if (lp->msg) {
			freemsg(lp->msg);
			lp->msg = NULL;
	}

	lp->flags = 0;
	mutex_exit(&lp->lp_lock);

	return (0);
}

There are no physical pointers between the read and write queue of a pair. WR(9F) is a queue pointer function. WR(9F) generates the write pointer from the read pointer. RD(9F) and OTHERQ(9F) are additional queue pointer functions. RD(9F) generates the read pointer from the write pointer, and OTHERQ(9F) generates the mate pointer from either.

Driver Flush Handling

The write put procedure in Example 9–5, lpwput, illustrates driver M_FLUSH handling. Note that all drivers are expected to incorporate flush handling.

If FLUSHW is set, the write message queue is flushed, and (in this example) the leading message (lp->msg) is also flushed. lp_lock protects the driver's per-instance data structure.

In most drivers, if FLUSHR is set, the read queue is flushed. However, in this example, no messages are ever placed on the read queue, so flushing it is not necessary. The FLUSHW bit is cleared and the message is sent upstream using qreply(9F). If FLUSHR is not set, the message is discarded.

The stream head always performs the following actions on flush requests received on the read side from downstream. If FLUSHR is set, messages waiting to be sent to user space are flushed. If FLUSHW is set, the stream head clears the FLUSHR bit and sends the M_FLUSH message downstream. In this manner, a single M_FLUSH message sent from the driver can reach all queues in a stream. A module must send two M_FLUSH messages to have the same effect.

lpwput queues M_DATA and M_IOCTL messages and if the device is not busy, starts output by calling lpout. Message types that are not recognized are discarded (in the default case of the switch).


Example 9–5 Driver Flush Handling

static int lpwput(
	queue_t *q,	/* write queue */
	mblk_t *mp)	/* message pointer */
{
	struct lp *lp;

	lp = (struct lp *)q->q_ptr;

	switch (mp->b_datap->db_type) {
	default:
		freemsg(mp);
		break;

	case M_FLUSH: /* Canonical flush handling */
		if (*mp->b_rptr & FLUSHW) {
			flushq(q, FLUSHDATA);
			mutex_enter(&lp->lp_lock); /* lock any access to lp */

			if (lp->msg) {
				freemsg(lp->msg);
				lp->msg = NULL;
			}

			mutex_exit(&lp->lp_lock);

		}

		if (*mp->b_rptr & FLUSHR) {
			*mp->b_rptr &= ~FLUSHW;
			qreply(q, mp);
		} else
			freemsg(mp);

		break;

	case M_IOCTL:
	case M_DATA:
		(void) putq(q, mp);
		mutex_enter(&lp->lp_lock);

		if (!(lp->flags & BUSY))
			lpout(lp);

		mutex_exit(&lp->lp_lock);

	}
	return (0);
}

Print Driver Interrupt

Example 9–6 shows the interrupt handling for the printer driver.

lpintr is the driver-interrupt handler registered by the attach routine.

lpout takes a single character from the queue and sends it to the printer. For convenience, the message currently being output is stored in lp->msg in the per-instance structure. This assumes that the message is called with the mutex held.

lpoutchar sends a single character to the printer (in this case the system console using cmn_err(9F)) and interrupts when complete. Of course, hardware would generate a hard interrupt, so the call to ddi_trigger_softintr(9F) would be unnecessary.


Example 9–6 Driver Interrupt Handling

/* Device interrupt routine */static uint
lpintr(caddr_t lp)	 /* minor device number of lp */
{

	struct lp *lpp = (struct lp *)lp;
	
	mutex_enter(&lpp->lp_lock);

	if (!(lpp->flags & BUSY)) {
			mutex_exit(&lpp->lp_lock);
			return (DDI_INTR_UNCLAIMED);
	}

	lpp->flags &= ~BUSY;
	lpout(lpp);
	mutex_exit(&lpp->lp_lock);

	return (DDI_INTR_CLAIMED);
}

/* Start output to device - used by put procedure and driver */

static void
lpout(
	struct lp *lp)
{

	mblk_t *bp;
	queue_t *q;

	q = lp->qptr;

 loop:
	if ((bp = lp->msg) == NULL) { /*no current message*/
			if ((bp = getq(q)) == NULL) {
				lp->flags &= ~BUSY;	
				return;
			}
			if (bp->b_datap->db_type == M_IOCTL) {
				/* lpdoioctl(lp, bp); */
				goto loop;
			}

			lp->msg = bp; /* new message */

		}

	if (bp->b_rptr >= bp->b_wptr) { /* validate message */

			bp = lp->msg->b_cont;
			lp->msg->b_cont = NULL;
			freeb(lp->msg);
			lp->msg = bp;
			goto loop;
	}

	lpoutchar(lp, *bp->b_rptr++); /*output one character*/
	lp->flags |= BUSY;
}

static void
lpoutchar(
	struct lp *lp,
	char c)
{
	cmn_err(CE_CONT, “%c”, c);
	ddi_trigger_softintr(lp->siid);
}

Driver Flow Control

The same utilities (described in Chapter 10, STREAMS Modules) and mechanisms used for module flow control are used by drivers.

When the message is queued, putq(9F) increments the value of q_count by the size of the message and compares the result to the driver's write high-watermark (q_hiwat) value. If the count reaches q_hiwat, putq(9F) sets the internal FULL indicator for the driver write queue. This causes messages from upstream to be halted (canputnext(9F) returns FALSE) until the write queue count drops below q_lowat. The driver messages waiting to be output through lpout are dequeued by the driver output interrupt routine with getq(9F), which decrements the count. If the resulting count is below q_lowat, getq(9F) back-enables any upstream queue that had been blocked.

For priority band data, qb_count, qb_hiwat, and qb_lowat are used.

STREAMS with flow control can be used on the driver read side to handle temporary upstream blocks.

To some extent, a driver or a module can control when its upstream transmission becomes blocked. Control is available through the M_SETOPTS message (see Appendix A, Message Types) to modify the stream head read-side flow control limits.

Cloning STREAMS Drivers

To eliminate polling, STREAMS drivers can be made clonable. If a STREAMS driver is implemented as a clonable device, a single node in the file system can be opened to access any unused device that the driver controls. This special node guarantees that each user is allocated a separate stream to the driver for each open call. Each stream is associated with an unused minor device, so the total number of streams that may be connected to a particular clonable driver is limited only by the number of minor devices configured for that driver.

In previous examples, each user process connected a stream to a driver by explicitly opening a particular minor device of the driver. Each minor device had its own node in the device tree file system. Often, there is a need for a user process to connect a new stream to a driver regardless of which minor device is used to access the driver. In the past, this forced the user process to poll the various minor device nodes of the driver for an available minor device.

The clone model is useful, for example, in a networking environment where a protocol pseudo-device driver requires each user to open a separate stream over which it establishes communication. (The decision to implement a STREAMS driver as a clonable device is made by the designers of the device driver. Knowledge of the clone driver implementation is not required to use it.)

There are two ways to open as a clone device. The first is to use the STREAMS framework-provided clone device, which arranges to open the device with the CLONEOPEN flag passed in. This method is demonstrated in Example 9–7, which shows the attach and open routines for the pseudo-terminal master ptm(7D) driver. The second way is to have the driver open itself as a clone device, without intervention from the system clone device. This method is demonstrated in the attach and open routines for the log(7D) device in Example 9–8.

The ptm(7D) device, which uses the system-provided clone device, sets up two nodes in the device file system. One has a major number of 23 (ptm's assigned major number) and a minor number of 0. The other node has a major number of 11 (the clone device's assigned major number) and a minor number of 23 (ptm's assigned major number). The driver's attach routine (see Example 9–7) calls to ddi_create_minor_node(9F) twice. First, to set up the “normal” node (major number 23); second, to specify CLONE_DEV as the last parameter, making the system create the node with major 11.


crw-rw-rw-   1 sys       11, 23 Mar  6 02:05 clone:ptmx
crw-------   1 sys       23,  0 Mar  6 02:05 ptm:ptmajor

When the special file /devices/pseudo/clone@0:ptmx is opened, the clone driver code in the kernel (accessed by major 11) passes the CLONEOPEN flag in the sflag parameter to the ptm(7D) open routine. ptm's open routine checks sflag to make sure it is being called by the clone driver. The open routine next attempts to find an unused minor device for the open by searching its table of minor devices. (PT_ENTER_WRITE and PT_EXIT_WRITE are driver-defined macros for entering and exiting the driver's mutex.) If it succeeds (and following other open processing), the open routine constructs a new dev_t with the new minor number, which it passes back to its caller in the devp parameter. (The new minor number is available to the user program that opened the clonable device through an fstat(2) call.)


Example 9–7 Opening a System Clone Device

static int
ptm_attach(dev_info_t *devi, ddi_attach_cmd_t cmd)
{
	  if (cmd != DDI_ATTACH)
			return (DDI_FAILURE);

	  if (ddi_create_minor_node(devi, "ptmajor", S_IFCHR, 0, NULL, 0) 
				== DDI_FAILURE) {
			ddi_remove_minor_node(devi, NULL);
			return (DDI_FAILURE);
	  }
	  if (ddi_create_minor_node(devi, "ptmx", S_IFCHR, 0, NULL, CLONE_DEV) 
				== DDI_FAILURE) {
			ddi_remove_minor_node(devi, NULL);
			return (DDI_FAILURE);
	  }
	  ptm_dip = devi;
	  return (DDI_SUCCESS);
}

static int
ptmopen(
		queue_t	*rqp,		/* pointer to the read side queue */
		dev_t	*devp,		/* pointer to stream tail's dev */
		int		oflag,		/* the user open(2) supplied flags */
		int		sflag,		/* open state flag */
		cred_t	*credp)	/* credentials */
{
		struct pt_ttys	*ptmp;
		mblk_t	*mop;		/* ptr to a setopts message block */
		minor_t	dev;

		if (sflag != CLONEOPEN) {
			return (EINVAL);
		}

		for (dev = 0; dev < pt_cnt; dev++) {
			ptmp = &ptms_tty[dev];
			PT_ENTER_WRITE(ptmp);
			if (ptmp->pt_state & (PTMOPEN | PTSOPEN | PTLOCK)) {
				PT_EXIT_WRITE(ptmp);
			} else
				break;
		}

		if (dev >= pt_cnt) {
			return (ENODEV);
		}

		... <other open processing> ...

		/*
		 * The input, devp, is a major device number, the output is put into
		 * into the same parm as a major,minor pair.
		 */
		*devp = makedevice(getmajor(*devp), dev);
		return (0);
}

The log(7D) driver uses the second method; it clones itself without intervention from the system clone device. The log(7D) driver's attach routine (in Example 9–8) is similar to the one in ptm(7D). It creates two nodes using ddi_create_minor_node(9F), but neither specifies CLONE_DEV as the last parameter. Instead, one of the devices has minor 0, the other minor CLONEMIN. These two devices provide log(7D) two interfaces: the first write-only, the second read-write (see the man page log(7D) for more information). Users open one node or the other. If they open the CONSWMIN (clonable, read-write) node, the open routine checks its table of minor devices for an unused device. If it is successful, it (like the ptm(7D) open routine) returns the new dev_t to its caller in devp.


Example 9–8 Opening the log Driver

static int
log_attach(dev_info_t *devi, ddi_attach_cmd_t cmd)
{
		if (ddi_create_minor_node(devi, "conslog", S_IFCHR, 0, NULL, NULL)
				 == DDI_FAILURE ||
				 ddi_create_minor_node(devi, "log", S_IFCHR, 5, NULL, NULL) 
				 == DDI_FAILURE) {
			ddi_remove_minor_node(devi, NULL);
			return (-1);
		}
		log_dip = devi;
		return (DDI_SUCCESS);
}

static int
logopen(
		queue_t *q,
		dev_t *devp,
		int flag,
		int sflag,
		cred_t *cr)
{
		int i;
		struct log *lp;

		/*
		 * A MODOPEN is invalid and so is a CLONEOPEN.
		 * This is because a clone open comes in as a CLONEMIN device open!!
		 */
		if (sflag)
			return (ENXIO);

		mutex_enter(&log_lock);
		switch (getminor(*devp)) {

		case CONSWMIN:
			if (flag & FREAD) { /* you can only write to this minor */
				mutex_exit(&log_lock);
				return (EINVAL);
			}
			if (q->q_ptr) { /* already open */
				mutex_exit(&log_lock);
				return (0);
			}
			lp = &log_log[CONSWMIN];
			break;

		case CLONEMIN:
			/*
			 * Find an unused minor > CLONEMIN.
			 */
				i = CLONEMIN + 1;
			for (lp = &log_log[i]; i < log_cnt; i++, lp++) {
				if (!(lp->log_state & LOGOPEN))
					break;
			}
			if (i >= log_cnt) {
				mutex_exit(&log_lock);
				return (ENXIO);
			}
			*devp = makedevice(getmajor(*devp), i); /* clone it */
			break;

		default:
			mutex_exit(&log_lock);
			return (ENXIO);
		}

		/*
		 * Finish device initialization.
		 */
		lp->log_state = LOGOPEN;
		lp->log_rdq = q;
		q->q_ptr = (void *)lp;
		WR(q)->q_ptr = (void *)lp;
		mutex_exit(&log_lock);
		qprocson(q);
		return (0);
}

Loop-Around Driver

The loop-around driver is a pseudo-driver that loops data from one open stream to another open stream. The associated files are almost like a full-duplex pipe to user processes. The streams are not physically linked. The driver is a simple multiplexer that passes messages from one stream's write queue to the other stream's read queue.

To create a connection, a process opens two streams, obtains the minor device number associated with one of the returned file descriptors, and sends the device number in an ioctl(2) to the other stream. For each open, the driver open places the passed queue pointer in a driver interconnection table, indexed by the device number. When the driver later receives an M_IOCTL message, it uses the device number to locate the other stream's interconnection table entry, and stores the appropriate queue pointers in both of the streams' interconnection table entries.

Subsequently, when messages other than M_IOCTL or M_FLUSH are received by the driver on either stream's write side, the messages are switched to the read queue following the driver on the other stream's read side. The resultant logical connection is shown in Figure 9–2. Flow control between the two streams must be handled explicitly, since STREAMS do not automatically propagate flow control information between two streams that are not physically connected.

Figure 9–2 Loop-Around Streams

Diagram shows how a loop-around driver loops data from one open stream to another open stream.

Example 9–9 shows the loop-around driver code. The loop structure contains the interconnection information for a pair of streams. loop_loop is indexed by the minor device number. When a stream is opened to the driver, the driver places the address of the corresponding loop_loop element in the q_ptr (private data structure pointer) of the read-side and write-side queues. Since STREAMS clears q_ptr when the queue is allocated, a NULL value of q_ptr indicates an initial open. loop_loop verifies that this stream is connected to another open stream.

The code presented here for the loop-around driver represents a single-threaded, uniprocessor implementation. Chapter 12, Multithreaded STREAMS presents multiprocessor and multithreading issues such as locking to prevent race conditions and data corruption.

Example 9–9 contains the declarations for the driver.


Example 9–9 Declarations for the Loop-Around Driver

/* Loop-around driver */

#include <sys/types.h>
#include <sys/param.h>
#include <sys/stream.h>
#include <sys/stropts.h>
#include <sys/signal.h>
#include <sys/errno.h>
#include <sys/cred.h>
#include <sys/stat.h>
#include <sys/modctl.h>
#include <sys/conf.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>

static int loop_identify(dev_info_t *);
static int loop_attach(dev_info_t *, ddi_attach_cmd_t);
static int loop_detach(dev_info_t *, ddi_detach_cmd_t);
static int loop_devinfo(dev_info_t *, ddi_info_cmd_t, void *, void **);
static int loopopen (queue_t*, dev_t*, int, int, cred_t*);
static int loopclose (queue_t*, int, cred_t*);
static int loopwput (queue_t*, mblk_t*);
static int loopwsrv (queue_t*);
static int looprsrv (queue_t*);

static dev_info_t *loop_dip;	/* private devinfo pointer */

static struct module_info minfo = {
		0xee12,
		“loop”,
		0,
		INFPSZ,
		512,
		128
};

static struct qinit rinit = {
		(int (*)()) NULL,
		looprsrv,
		loopopen,
		loopclose,
		(int (*)()) NULL,
		&minfo,
		NULL
};

static struct qinit winit = {
		loopwput,
		loopwsrv,
		(int (*)()) NULL,
		(int (*)()) NULL,
		(int (*)()) NULL,
		&minfo,
		NULL
};

static struct streamtab loopinfo= {
		&rinit,
		&winit,
		NULL,
		NULL
};

struct 	loop {
		queue_t *qptr;			/* back pointer to write queue */
		queue_t *oqptr;		/* pointer to connected read queue */
};

#define LOOP_CONF_FLAG (D_NEW | D_MP)

static struct cb_ops cb_loop_ops = {
   nulldev,               /* cb_open */
   nulldev,               /* cb_close */
   nodev,                 /* cb_strategy */
   nodev,                 /* cb_print */
   nodev,                 /* cb_dump */
   nodev,                 /* cb_read */
   nodev,                 /* cb_write */
   nodev,                 /* cb_ioctl */
   nodev,                 /* cb_devmap */
   nodev,                 /* cb_mmap */
   nodev,                 /* cb_segmap */
   nochpoll,              /* cb_chpoll */
   ddi_prop_op,           /* cb_prop_op */
   ( &loopinfo),          /* cb_stream */
   (int)(LOOP_CONF_FLAG)  /* cb_flag */
};

static struct dev_ops loop_ops = {
   DEVO_REV,                /* devo_rev */
   0,                       /* devo_refcnt */
   (loop_devinfo),          /* devo_getinfo */
   (loop_identify),         /* devo_identify */
   (nulldev),               /* devo_probe */
   (loop_attach),           /* devo_attach */
   (loop_detach),           /* devo_detach */
   (nodev),                 /* devo_reset */
   &(cb_loop_ops),          /* devo_cb_ops */
   (struct bus_ops *)NULL,  /* devo_bus_ops */
   (int (*)()) NULL         /* devo_power */
};

#define LOOP_SET ((`l'<<8)|1) /* in a .h file */
#define NLOOP	64
static struct loop loop_loop[NLOOP];
static int loop_cnt = NLOOP;

/*
 * Module linkage information for the kernel.
 */
extern struct mod_ops mod_strmodops;

static struct modldrv modldrv = {
		&mod_driverops, "STREAMS loop driver", &loop_ops
};

static struct modlinkage modlinkage = {
		MODREV_1, &modldrv, NULL
};

_init()
{
		return (mod_install(&modlinkage));
}

_info(modinfop)
		struct modinfo *modinfop;
{
		return (mod_info(&modlinkage, modinfop));
}

_fini(void)
{
		return (mod_remove(&modlinkage));
}

Example 9–10 contains the initialization routines.


Example 9–10 Initialization Routines for the Loop-around Driver

static int
loop_identify(dev_info_t *devi)
{
	  if (strcmp(ddi_get_name(devi), "loop") == 0)
			return (DDI_IDENTIFIED);
	  else
			return (DDI_NOT_IDENTIFIED);
}

static int
loop_attach(dev_info_t *devi, ddi_attach_cmd_t cmd)
{
	  if (cmd != DDI_ATTACH)
			return (DDI_FAILURE);

	  if (ddi_create_minor_node(devi, "loopmajor", S_IFCHR, 0, NULL, 0) 
				== DDI_FAILURE) {
			ddi_remove_minor_node(devi, NULL);
			return (DDI_FAILURE);
	  }
	  if (ddi_create_minor_node(devi, "loopx", S_IFCHR, 0, NULL, CLONE_DEV)
				 == DDI_FAILURE) {
			ddi_remove_minor_node(devi, NULL);
			return (DDI_FAILURE);
	  }

	  loop_dip = devi;

	  return (DDI_SUCCESS);
}

static int
loop_detach(dev_info_t *devi, ddi_detach_cmd_t cmd)
{
	  if (cmd != DDI_DETACH)
			return (DDI_FAILURE);

	  ddi_remove_minor_node(devi, NULL);
	  return (DDI_SUCCESS);
}

/*ARGSUSED*/
static int
loop_devinfo(
	  dev_info_t *dip, 
	  ddi_info_cmd_t infocmd, 
	  void *arg, 
	  void **result)
{
	  int error;

	  switch (infocmd) {
	  case DDI_INFO_DEVT2DEVINFO:
			if (loop_dip == NULL) {
				error = DDI_FAILURE;
			} else {
				*result = (void *) loop_dip;
				error = DDI_SUCCESS;
			}
			break;
	  case DDI_INFO_DEVT2INSTANCE:
			*result = (void *)0;
			error = DDI_SUCCESS;
			break;
	  default:
			error = DDI_FAILURE;
	  }
	  return (error);

}

The open procedure (in Example 9–11) includes canonical clone processing that enables a single file system node to yield a new minor device/vnode each time the driver is opened. In loopopen, sflag can be CLONEOPEN, indicating that the driver picks an unused minor device. In this case, the driver scans its private loop_loop data structure to find an unused minor device number. If sflag is not set to CLONEOPEN, the passed-in minor device specified by getminor(*devp) is used.


Example 9–11 Opening the Loop-Around Driver

/*ARGSUSED*/
static int loopopen(
		queue_t *q,
		dev_t *devp,
		int flag,
		int sflag,
		cred_t *credp)
{
		struct loop *loop;
		minor_t newminor;

		if (q->q_ptr) /* already open */
			return(0);

		/*
		 * If CLONEOPEN, pick a minor device number to use.
		 * Otherwise, check the minor device range.
		 */

		if (sflag == CLONEOPEN) {
			for (newminor = 0; newminor < loop_cnt; newminor++ ) {
				if (loop_loop[newminor].qptr == NULL)
					break;
			}
		} else
			newminor = getminor(*devp);

		if (newminor >= loop_cnt)
			return(ENXIO);

		/*
		 * construct new device number and reset devp
		 * getmajor gets the major number
		 */

		*devp = makedevice(getmajor(*devp), newminor);
		loop = &loop_loop[newminor];
		WR(q)->q_ptr = (char *) loop;
		q->q_ptr = (char *) loop;
		loop->qptr = WR(q);
		loop->oqptr = NULL;

		qprocson(q);

		return(0);
}

Because the messages are switched to the read queue following the other stream's read side, the driver needs a put procedure only on its write side. loopwput (in Example 9–12) shows another use of an ioctl(2). The driver supports the ioc_cmd value LOOP_SET in the iocblk(9S) of the M_IOCTL message. LOOP_SET makes the driver connect the current open stream to the stream indicated in the message. The second block of the M_IOCTL message holds an integer that specifies the minor device number of the stream to which to connect.

The LOOP_SET ioctl(2) processing involves several checks:

If these checks pass, the read queue pointers for the two streams are stored in the respective oqptr fields. This cross-connects the two streams indirectly, through loop_loop.

The put procedure incorporates canonical flush handling.

loopwput queues all other messages (for example, M_DATA or M_PROTO) for processing by its service procedure. A check is made that the stream is connected. If not, M_ERROR is sent to the stream head. Certain message types can be sent upstream by drivers and modules to the stream head where they are translated into actions detectable by user processes. These messages may also modify the state of the stream head:

M_ERROR

Causes the stream head to lock up. Message transmission between stream and user processes is terminated. All subsequent system calls except close(2) and poll(2) fail. Also causes M_FLUSH, clearing all message queues, to be sent downstream by the stream head.

M_HANGUP

Terminates input from a user process to the stream. All subsequent system calls that would send messages downstream fail. Once the stream head read message queue is empty, EOF is returned on reads. This can also result in SIGHUP being sent to the process group's session leader.

M_SIG/M_PCSIG

Causes a specified signal to be sent to the process group associated with the stream.

putnextctl(9F) and putnextctl1(9F) allocate a nondata (that is, not M_DATA, M_DELAY, M_PROTO, or M_PCPROTO) type message, place one byte in the message (for putnextctl1(9F)), and call the put(9E) procedure of the specified queue.


Example 9–12 Use of ioctl to Copy Data From User Space to Kernel Space

static int loopwput(queue_t *q, mblk_t *mp)
{
		struct loop *loop;
		int to;

		loop = (struct loop *)q->q_ptr;

		switch (mp->b_datap->db_type) {
			case M_IOCTL: {
				struct iocblk	*iocp;
				int		error=0;

				iocp = (struct iocblk *)mp->b_rptr;

				switch (iocp->ioc_cmd) {

	 				case LOOP_SET: {
						/*
						 * if this is a transparent ioctl return an error; 
						 * the complete solution is to convert the message 
						 * into an M_COPYIN message so that the data is
						 * ultimately copied from user space 
						 * to kernel space.
						 */

						if (iocp->ioc_count == TRANSPARENT) {
							error = EINVAL;
							goto iocnak;
						}

						/* fetch other minor device number */

						to = *(int *)mp->b_cont->b_rptr;

						/*
						 * Sanity check. ioc_count contains the amount
						 * of user supplied data which must equal the
						 * size of an int.
						 */

						if (iocp->ioc_count != sizeof(int)) {
							error = EINVAL;
							goto iocnak;
						}

						/* Is the minor device number in range? */

						if (to >= loop_cnt || to < 0) {
							error = ENXIO;
							goto iocnak;
						}

						/* Is the other device open? */

						if (!loop_loop[to].qptr) {
							error = ENXIO;
							goto iocnak;
						}

						/* Check if either dev is currently connected */

						if (loop->oqptr || loop_loop[to].oqptr) {
							error = EBUSY;
							goto iocnak;
						}

						/* Cross connect the streams through 
						 * the loopstruct 
						 */

						loop->oqptr = RD(loop_loop[to].qptr);
						loop_loop[to].oqptr = RD(q);

						/*
						 * Return successful ioctl. Set ioc_count
						 * to zero, since no data is returned.
						 */

						mp->b_datap->db_type = M_IOCACK;
						iocp->ioc_count = 0;
						qreply(q, mp);
						break;
					}

					default:
						error = EINVAL;
						iocnak:

						/*
						 * Bad ioctl. Setting ioc_error causes 
						 * the ioctl call to return that particular errno.
						 * By default, ioctl returns EINVAL on failure.
						 */

						mp->b_datap->db_type = M_IOCNAK;
						iocp->ioc_error = error;
						qreply(q, mp);
						break;
					}

					break;
				}

				case M_FLUSH: {

					if (*mp->b_rptr & FLUSHW) {
						flushq(q, FLUSHALL);	/* write */
						if (loop->oqptr)
							flushq(loop->oqptr, FLUSHALL);
						/* read on other side equals write on this side */
					}

					if (*mp->b_rptr & FLUSHR) {
						flushq(RD(q), FLUSHALL);
						if (loop->oqptr != NULL)
							flushq(WR(loop->oqptr), FLUSHALL);
					}

					switch(*mp->b_rptr) {

					case FLUSHW:
						*mp->b_rptr = FLUSHR;
						break;

					case FLUSHR:
						*mp->b_rptr = FLUSHW;
						break;

					}

					if (loop->oqptr != NULL)
						(void) putnext(loop->oqptr, mp);
					break;
				}

				default: /* If this Stream isn't connected, send 
							 * M_ERROR upstream.
							 */
					if (loop->oqptr == NULL) {
						freemsg(mp);
						(void) putnextctl1(RD(q), M_ERROR, ENXIO);
						break;
					}
					(void) putq(q, mp);

				}

		return (0);
}

Service procedures are required in this example on both the write side and read side for flow control (see Example 9–13). The write service procedure, loopwsrv, takes on the canonical form. The queue being written to is not downstream, but upstream (found through oqptr) on the other stream.

In this case, there is no read-side put procedure so the read service procedure, looprsrv, is not scheduled by an associated put procedure, as has been done previously. looprsrv is scheduled only by being back-enabled when its upstream flow control blockage is released. The purpose of the procedure is to re-enable the writer (loopwsrv) by using oqptr to find the related queue. loopwsrv cannot be directly back-enabled by STREAMS because there is no direct queue linkage between the two streams. Note that no message is queued to the read service procedure. Messages are kept on the write side so that flow control can propagate up to the stream head. qenable(9F) schedules the write-side service procedure of the other stream.


Example 9–13 Loop-Around Driver Flow Control

staticintloopwsrv (queue_t*q)
{
		mblk_t *mp;
		structloop *loop;
		loop = (structloop*) q->q_ptr;

		while ((mp = getq (q)) != NULL){
		  /* Check if we can put the message up 
		   * the other Stream read queue
		   */
		  if (mp->b_datap->db_type <= QPCTL && !canputnext (loop->oqptr)) {
				(void) putbq (q,mp);				/*read-side is blocked*/
				break;
		  }

		  /* send message to queue following other Stream read queue */

		  (void) putnext (loop->oqptr, mp);
		}
		return (0);
}

staticintlooprsrv (queue_t*q)
{
		/* Enter only when "backenabled" by flow control */
		structloop *loop;
		loop = (structloop*) q->q_ptr;
		if (loop->oqptr == NULL)
			return (0);

		/*manually enable write service procedure*/
		qenable (WR (loop->oqptr));
		return (0);
}

loopclose breaks the connection between the streams, as shown in Example 9–14. loopclose sends an M_HANGUP message up the connected stream to the stream head.


Example 9–14 Breaking Stream Connections for Loop-Around Device

/*ARGSUSED*/
static int loopclose (
		queue_t *q,
		int flag,
		cred_t *credp)
{
		struct loop *loop;

		loop = (struct loop *)q->q_ptr;
		loop->qptr = NULL;

		/*
		 * If we are connected to another stream, break the linkage, and 
		 * send a hangup message. The hangup message causes the stream head 
		 * to reject writes, allow the queued data to be read completely,
		 * and then return EOF on subsequent reads.
		 */

		if (loop->oqptr) {
			(void) putnextctl(loop->oqptr, M_HANGUP);
			((struct loop *)loop->oqptr->q_ptr)->oqptr = NULL;
			loop->oqptr = NULL;
		}

		qprocsoff(q);
		return (0);
}

An application using this driver would first open the clone device node created in the attach routine (/devices/pseudo/clone@0:loopx) two times to obtain two streams. The application can determine the minor numbers of the devices by using fstat(2). Next, it joins the two streams by using the STREAMS I_STR ioctl(2) (see streamio(7I)) to pass the LOOP_SET ioctl(2) with one of the stream's minor numbers as an argument to the other stream. Once this is completed, the data sent to one stream using write(2) or putmsg(2) can be retrieved from the other stream with read(2) or getmsg(2). The application also can interpose STREAMS modules between the stream heads and the driver using the I_PUSH ioctl(2).