Just as with character device drivers, a block device driver is a collection of routines that get called as various operations are performed on the devices controlled by the driver.
This list of functions includes any or all of: open(), release(), ioctl(), interrupt(), init() and request(). The first four of these have the same format and function as their character device driver counterparts.
The first thing to do when you come to write a block device driver is to choose a block major device number for your driver and add it to the file:
/usr/include/linux/major.h
Contained in this file is a list of all the major device numbers used in the standard kernel and a symbolic name for each one. You should add a definition for a symbolic name and a major device number to the list. For example:
#define MY_MAJOR 30
Once this is done you can begin to write your device driver code, which should start with lines like the following:
#include <linux/blkdev.h> #define MAJOR_NR MY_MAJOR #include "bik.h"
Notice the definition of MAJOR_NR to specify the major device number that this device driver will use. This definition needs to appear before the #include "blk.h line, because the symbol will be used in the blk.h file, as you will see later. The definition of MY_MAJOR will automatically be included because <linux/blkdev.h> itself includes the file <linux/major.h>, which you just modified.
You have already seen that physical I/O to block devices is an expensive operation in terms of time, and one which is automatically avoided wherever possible. This is accomplished by using a portion of the system's memory as a buffering mechanism between process reads and writes and the device driver itself. This mechanism is the buffer cache.
What this means is that your block device driver should not contain a read() or a write() function, as a user process which calls either of the read() or write() system calls on a device special file controlled by your device driver will automatically have its request dealt with by the buffer cache mechanism instead.
Only when it is absolutely essential, will the buffer cache need to call your device driver to perform physical I/O. It does this by adding its I/O request to a queue of such requests for your device and then arranging for the request() function in your device driver to be called to deal with the queue of requests.
The request() routine, when called, has the task of reading each of the pending I/O requests in turn from the request queue and arranging to perform the physical read or write operations specified. Each I/O request in the queue is stored in a structure called struct request, which is defined in the file:
/usr/include/linux/blkdev.h
The general layout of a request() function in a device driver without an interrupt service routine is quite straightforward, as shown here:
static void do_my_request(void) { loop: INIT_REQUEST; if (MINOR(CURRENT->dev)>MY_MINOR_MAX) { end_request(O); goto loop; } if (CURRENT->cmd==READ) { end_request(my_read()); goto loop; } if (CURRENT->cmd==WRITE) { end_request(my_write()); goto loop; } end_request(0); goto loop: }
The first thing to note here is that CURRENT is a pointer to the struct request at the head of the request queue. CURRENT is defined in the header file "blk.h".
The request() function begins with the INIT_REQUEST macro (also defined in "bik.h"), which checks to make sure that there is at least one request waiting in the queue. If there are no more requests (i.e. CURRENT==O) the INIT_REQUEST macro causes your request() function to return, with its task complete.
Assuming that there is at least one request in the queue, your request() function should now deal with the request at the head of the queue. When the request has heen dealt with, your request() function should then call end_request().
If the I/O operation was completed successfully, the end_request() function should be called with a parameter value of 1. If the I/O was unsuccessful, a parameter value of zero should be used instead.
The end_request() function logs an error message if necessary, removes the processed request from the queue, arranges to restart any processes that were waiting for this I/O request to complete, and then sets CURRENT to point to the next request if there is one.
After the end_request() call, the request() function loops back to the start to repeat the process with the next request.
In our simple example, the processing of the requests themselves is handled in a very easy manner. Most of the work is based around the contents of the CURRENT request. The request structure layout is as follows:
struct request { int dev; int cmd; int errors; unsigned long sector; unsigned long nr_sectors; unsigned long current_nr_sectors; char *buffer; struct semaphore *sem; struct buffer_head *bh; struct buffer_head *bhtail; struct request *next; }
The main fields in this structure and the tasks that they perform are:
dev | specifies the physical device for this request; |
cmd | command to perform (READ or WRITE); |
sector | sector number to start the read or write; |
nr_sectors | number of sectors to read or write; |
buffer | kernel memory buffer for data read or written. |
In processing a request, first, some kind of simple sanity check is performed to make sure that the CURRENT request specifies a valid physical device. If an error is detected at this point then the end_request() function is called with a zero parameter value.
If the device is valid, the value of CURRENT->cmd is used to call either the my_read() or my_write() function as appropriate. The return values from these two functions must be either 0 (if an error occurs) or 1 (if everything is okay), and these values are then passed straight into end_request() to terminate the CURRENT request with the correct value.
The my_read() and my_write() functions will use the sector, nr_sectors and buffer fields of the CURRENT structure to transfer the data as requested.