Writing Device Drivers

Asynchronous Data Transfers

This section presents a method for performing asynchronous I/O transfers. The driver queues the I/O requests and then returns control to the caller. Again, the assumption is that the hardware is a simple disk device that allows one transfer at a time. The device interrupts when a data transfer has completed or when an error occurs.

  1. Check for invalid buf(9S) requests.

    As in the synchronous case, the device driver should check the buf(9S) structure passed to strategy(9E) for validity. See "Synchronous Data Transfers" for more details.

  2. Enqueue the request.

    Unlike synchronous data transfers, a driver does not wait for an asynchronous request to complete. Instead, it adds the request to a queue. The head of the queue can be the current transfer, or a separate field in the state structure can be used to hold the active request (as in this example). If the queue was initially empty, then the hardware is not busy, and strategy(9E) starts the transfer before returning. Otherwise, whenever a transfer completes and the queue is non-empty, the interrupt routine begins a new transfer. This example actually places the decision of whether to start a new transfer into a separate routine for convenience.

    The driver can use the av_forw and the av_back members of the buf(9S) structure to manage a list of transfer requests. A single pointer can be used to manage a singly linked list, or both pointers can be used together to build a doubly-linked list. The device hardware specification will specify which type of list management (such as insertion policies) will optimize the performance of the device. The transfer list is a per-device list, so the head and tail of the list are stored in the state structure.

    Example 10-6 is designed to allow multiple threads access to the driver shared data, so it is extremely important to identify any such data (such as the transfer list) and protect it with a mutex. See Chapter 4, Multithreading, for more details about mutex locks.


    Example 10-6 Asynchronous Block Driver strategy(9E) Routine

    static int
    xxstrategy(struct buf *bp)
    {
    	struct xxstate *xsp;
    	minor_t instance;
    	instance = getminor(bp->b_edev);
    	xsp = ddi_get_soft_state(statep, instance);
    	...
    	validate transfer request
    	...
    	Add the request to the end of the queue. Depending on the device, a sorting algorithm, such as disksort(9F)
    may be used if it improves the performance of the device.
    	mutex_enter(&xsp->mu);
    	bp->av_forw = NULL;
    	if (xsp->list_head) {
    	   	/* Non-empty transfer list */
    	   	xsp->list_tail->av_forw = bp;
    	   	xsp->list_tail = bp;
    	} else {
    	   	/* Empty Transfer list */
    	   	xsp->list_head = bp;
    	   	xsp->list_tail = bp;
    	}
    	mutex_exit(&xsp->mu);
    	/* Start the transfer if possible */
    	(void) xxstart((caddr_t)xsp);
    	return (0);
    }

  3. Start the first transfer.

    Device drivers that implement queuing usually have a start()( ) routine. start()( ) is so called because it is this routine that dequeues the next request and starts the data transfer to or from the device. In this example, start()( ) processes all requests, regardless of the state of the device (busy or free).


    Note -

    start() must be written so that it can be called from any context, since it can be called by both the strategy routine (in kernel context) and the interrupt routine (in interrupt context).


    start()( ) is called by strategy(9F ) every time it queues a request so that an idle device can be started. If the device is busy, start()( ) returns immediately.

    start()( ) is also called by the interrupt handler before it returns from a claimed interrupt so that a nonempty queue can be serviced. If the queue is empty, start()( ) returns immediately.

    Since start()( ) is a private driver routine, it can take any arguments and return any type. Example 10-7 is written as if it will also be used as a DMA callback (although that portion is not shown), so it must take a caddr_t as an argument and return an int. See "Handling Resource Allocation Failures" for more information about DMA callback routines.


    Example 10-7 Block Driver start()() Routine

    static int
    xxstart(caddr_t arg)
    {
    	struct xxstate *xsp = (struct xxstate *)arg;
    	struct buf *bp;
    
    	mutex_enter(&xsp->mu);
    	/*
    	 * If there is nothing more to do, or the device is
    	 * busy, return.
    	 */
    	if (xsp->list_head == NULL || xsp->busy) {
    	   	mutex_exit(&xsp->mu);
    	   	return (0);
    	}
    	xsp->busy = 1;
    	/* Get the first buffer off the transfer list */
    	bp = xsp->list_head;
    	/* Update the head and tail pointer */
    	xsp->list_head = xsp->list_head->av_forw;
    	if (xsp->list_head == NULL)
    	   	xsp->list_tail = NULL;
    	bp->av_forw = NULL;
    	mutex_exit(&xsp->mu);
    	
    	if the device has power manageable components (see Chapter 8, Power Management),
    mark the device busy with pm_busy_components, and then ensure that the device
    	 is powered up by calling ddi_dev_is_needed.
    	Set up DMA resources with ddi_dma_alloc_handle(9F) and  
    ddi_dma_buf_bind_handle(9F).
    	xsp->bp = bp;
    	ddi_put32(xsp->data_access_handle, &xsp->regp->dma_addr,
    			cookie.dmac_address);
    	ddi_put32(xsp->data_access_handle, &xsp->regp->dma_size,
    			 (uint32_t)cookie.dmac_size);
    	ddi_put8(xsp->data_access_handle, &xsp->regp->csr,
    			 ENABLE_INTERRUPTS | START_TRANSFER);
    	return (0);
    }

  4. Handle the interrupting device.

    The interrupt routine is similar to the asynchronous version, with the addition of the call to start()() and the removal of the call to cv_signal(9F).


    Example 10-8 Asynchronous Block Driver Interrupt Routine

    static u_int
    xxintr(caddr_t arg)
    {
    	struct xxstate *xsp = (struct xxstate *)arg;
    	struct buf *bp;
    	uint8_t status;
    	mutex_enter(&xsp->mu);
    	status = ddi_get8(xsp->data_access_handle, &xsp->regp->csr);
    	if (!(status & INTERRUPTING)) {
    	    	mutex_exit(&xsp->mu);
    	    	return (DDI_INTR_UNCLAIMED);
    	}
    	/* Get the buf responsible for this interrupt */
    	bp = xsp->bp;
    	xsp->bp = NULL;
    	/*
    	 * This example is for a simple device which either
    	 * succeeds or fails the data transfer, indicated in the
    	 * command/status register.
    	 */
    	if (status & DEVICE_ERROR) {
    	    	/* failure */
    	    	bp->b_resid = bp->b_bcount;
    	    	bioerror(bp, EIO);
    	} else {
    	    	/* success */
    	    	bp->b_resid = 0;
    	}
    	ddi_put8(xsp->data_access_handle, &xsp->regp->csr,
    	    	CLEAR_INTERRUPT);
    	/* The transfer has finished, successfully or not */
    	biodone(bp);
    	if the device has power manageable components that were marked busy in strategy(9F)	
    (9E), mark them idle now with pm_idle_component(9F)
    	release any resources used in the transfer, such as DMA resources
    ddi_dma_unbind_handle(9F) and 
    ddi_dma_free_handle(9F)
    	/* Let the next I/O thread have access to the device */
    	xsp->busy = 0;
    	mutex_exit(&xsp->mu);
    	(void) xxstart((caddr_t)xsp);
    	return (DDI_INTR_CLAIMED);
    }