Asynchronous Data Transfers (Block Drivers)
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. An
interrupt also takes place if an error occurs. The basic steps for performing
asynchronous data transfers are:
Check for invalid buf(9S) requests.
Enqueue the request.
Start the first transfer.
Handle the interrupting device.
Checking for Invalid buf 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 (Block Drivers) for more details.
Enqueuing the Request
Unlike synchronous data transfers, a driver does not wait for an asynchronous request
to complete. Instead, the driver adds the request to a queue. The head
of the queue can be the current transfer. The head of the
queue can also be a separate field in the state structure for holding
the active request, as in Example 16-5.
If the queue is initially empty, then the hardware is not busy
and strategy(9E) starts the transfer before returning. Otherwise, if a transfer completes with
a non-empty queue, the interrupt routine begins a new transfer. Example 16-5 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 specifies
which type of list management, such as insertion policies, is used to 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.
The following example provides multiple threads with access to the driver shared data,
such as the transfer list. You must identify the shared data and must
protect the data with a mutex. See Chapter 3, Multithreading for more details about
mutex locks.
Example 16-5 Enqueuing Data Transfer Requests for Block Drivers
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) can 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);
}
Starting the First Transfer
Device drivers that implement queuing usually have a start() routine. start() 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, whether busy or free.
Note - start() must be written to be called from any context. start() can be
called by both the strategy routine in kernel context and the interrupt routine
in interrupt context.
start() is called by strategy(9E) every time strategy() 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 the handler returns from
a claimed interrupt so that a nonempty queue can be serviced. If the
queue is empty, start() returns immediately.
Because start() is a private driver routine, start() can take any arguments
and can return any type. The following code sample is written to be
used as a DMA callback, although that portion is not shown. Accordingly, the
example 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 16-6 Starting the First Data Request for a Block Driver
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,
* mark the device busy with pm_busy_components(9F),
* and then ensure that the device
* is powered up by calling pm_raise_power(9F).
*
* 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);
}
Handling 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 16-7 Block Driver Routine for Asynchronous Interrupts
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), 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);
}