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:

Checking for Invalid buf Requests

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.

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.