STREAMS Programming Guide

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).