10 Implementing Character and Block Device Driver Interfaces
Character and block device drivers can contain the following sections:
-
An open and close device section, which
contains an
open
interface and a
close
interface
-
An
ioctl
section, which
contains an
ioctl
interface
-
An interrupt section, which
contains an interrupt
handler
The following sections explain how to implement or set up each of these
interfaces.
10.1 Implementing the open Interface
A device driver's
open
interface performs the tasks necessary to open a device and prepare it
for I/O operations.
The kernel calls the driver's
open
interface on behalf of applications that make an
open
system call on the device special file that corresponds to the driver.
The code associated with an
open
interface resides in the open and close device section of the device
driver.
You specify the entry point for the driver's
open
interface
in a
dsent
structure.
Section 5.4
describes the
dsent
structure.
Writing Device Drivers: Reference
provides a reference page that gives additional information on the
arguments and tasks associated with
an
open
interface.
To implement an
open
interface you must understand the
dev_t
data type, which is discussed in
Section 8.1.1.
The following list describes some typical tasks that you perform when
implementing
an
open
interface:
-
Set up the
open
interface
-
Perform tasks associated with opening a device
Your
open
interface will probably perform most of these tasks and, possibly, some
additional ones.
The following sections describe each of these tasks, using the
/dev/none
driver as an example.
10.1.1 Setting Up the open Interface
The following code shows you how to set up an
open
interface, using the
/dev/none
driver as an example:
noneopen(dev, flag, format)
dev_t dev; [1]
int flag; [2]
int format; [3]
{
.
.
.
-
Declares an
argument that specifies the major and minor device numbers for
a specific
NONE
device.
The minor device number is used to determine the logical unit number for
the
NONE
device that is to be opened.
[Return to example]
-
Declares an argument to contain flag bits from the
file
/usr/sys/include/sys/file.h.
These flags indicate whether the device is being opened for reading,
writing, or both.
[Return to example]
-
Declares an argument to contain a constant that
identifies whether the device is a character or a block device.
These constants are defined in
/usr/sys/include/sys/mode.h.
[Return to example]
10.1.2 Performing the Tasks Associated with Opening the Device
The
/dev/none
driver's
open
interface (called
noneopen)
shows some of the typical tasks that a device driver's
open
interface can perform.
Specifically, the
/dev/none
driver's
open
interface performs the following tasks: checks to ensure that the
open is unique, marks the device as open, and returns
ESUCCESS
to indicate success.
The following code implements the
noneopen
interface.
The code also shows a call to the
minor
interface.
See
Writing Device Drivers: Advanced Topics
for an example implementation of an
open
interface for disk and tape device drivers.
.
.
.
register int unit = minor(dev); [1]
struct controller *ctlr = noneinfo[unit]; [2]
struct none_softc *sc = &none_softc[unit]; [3]
if(unit >= num_none)
return (ENODEV); [4]
if (sc->sc_openf == DN_OPEN)
return (EBUSY); [5]
if ((ctlr !=0) && (ctlr->alive & ALV_ALIVE))
{
sc->sc_openf = DN_OPEN;
return(ESUCCESS); [6]
}
else return(ENXIO); [7]
}
-
Declares a
unit
variable and initializes it to the device minor number.
Note the use of the
minor
interface to obtain the device minor number.
The
minor
interface takes one argument: the number of the device for which an
associated device minor number will be obtained.
The minor number is encoded in the
dev
argument, which is of type
dev_t.
[Return to example]
-
Declares a pointer to a
controller
structure and calls it
ctlr.
The driver initializes
ctlr
to the
controller
structure associated with this
NONE
device.
The minor device number,
unit,
is used as an index into the array of
controller
structures to determine which
controller
structure is associated with this
NONE
device.
[Return to example]
-
Declares a pointer to a
none_softc
structure and calls it
sc.
The driver initializes
sc
to the address of the
none_softc
structure associated with this
NONE
device.
The minor device number,
unit,
is used as an index into the array of
none_softc
structures to determine which
none_softc
structure is associated with this
NONE
device.
[Return to example]
-
The
NONE
device requires no real work to open; therefore, the code could simply
ignore the call and return
ESUCCESS.
To demonstrate some of the checking that a real driver might perform,
the example provides code that checks to be sure that the device exists.
If the device minor number,
unit,
is greater than or equal to the number of devices configured by the
system, returns the error code
ENODEV,
which indicates there is no such device on the system.
This error code is defined in
/usr/sys/include/sys/errno.h.
Section 7.1.6
shows how the
noneprobe
interface increments the
num_none
variable.
[Return to example]
-
If the
sc_openf
member of the
sc
pointer is equal to
DN_OPEN,
returns the error code
EBUSY,
which indicates that the
NONE
device has already been opened.
This error code is defined in
/usr/sys/include/sys/errno.h.
This example test is used to ensure that this unit of the
driver can be opened only once at a time.
This type of open is referred to as an exclusive access open.
[Return to example]
-
If the
ctlr
pointer is not equal to 0 and the
alive
member of
ctlr
has the
ALV_ALIVE
bit set,
then the device exists.
If this is the case, the
noneopen
interface sets the
sc_openf
member of the
sc
pointer to the open bit
DN_OPEN
and returns
ESUCCESS
to indicate a successful open.
[Return to example]
-
If the device does not exist,
noneopen
returns the error code
ENXIO,
which indicates that the device does not exist.
This error code is defined in
/usr/sys/include/sys/errno.h.
[Return to example]
10.2 Implementing the close Interface
The
open
interface is called every time that any user initiates an
action that invokes the
open
system call.
The
close
interface, however, is called only when the last user initiates
an action that closes the device.
The reason for this difference is to allow the driver to take some
special action when there is no work left to perform.
A device driver's
close
interface performs the tasks necessary to close a device that was
previously opened by the driver's
open
interface.
The kernel calls the driver's
close
interface on behalf of applications that make a
close
system call on the device special file that corresponds to the driver.
This action occurs when the last file descriptor that is open and
associated with this device is closed when the application calls the
close
system call.
The code associated with a
close
interface resides in the open and close device section of the device
driver.
You specify the entry point for the driver's
close
interface
in a
dsent
structure.
Section 5.4
describes the
dsent
structure.
Writing Device Drivers: Reference
provides a reference page that gives additional information on the
arguments and tasks associated with
a
close
interface.
To implement a
close
interface you must understand the
dev_t
data type, which is discussed in
Section 8.1.1.
The following list describes some typical tasks that you perform when
implementing
a
close
interface:
-
Set up the
close
interface
-
Perform tasks associated with closing the device
Your
close
interface will probably perform most of these tasks and, possibly, some
additional ones.
The following sections describe each of these tasks, using the
/dev/none
driver as an example.
10.2.1 Setting Up the close Interface
The following code shows you how to set up a
close
interface, using the
/dev/none
driver as an example:
noneclose(dev, flag, format)
dev_t dev; [1]
int flag; [2]
int format; [3]
{
.
.
.
-
Like the
noneopen
interface, the
noneclose
interface declares an argument that specifies the major and minor
numbers for a specific
NONE
device.
The minor device number is used to determine the logical unit number for
the
NONE
device to be closed.
[Return to example]
-
Like the
noneopen
interface, the
noneclose
interface also declares an argument to contain flag bits from the file
/usr/sys/include/sys/file.h.
Typically, a driver's
close
interface does not make use of this argument.
[Return to example]
-
Although the
format
argument is shown here,
a driver's
close
interface does not typically make use of this argument.
[Return to example]
10.2.2 Performing the Tasks Associated with Closing the Device
The
/dev/none
driver's
close
interface (called
noneclose)
shows some of the typical tasks that a device driver's
close
interface can perform.
Specifically, the
/dev/none
driver's
close
interface
uses the same arguments as
noneopen,
gets the device minor number in the same way, and initializes the
controller
and
none_softc
structures identically.
The purpose of
noneclose
is to turn off the open flag for the specified
NONE
device.
The following code implements the
noneclose
interface.
The code also shows calls to the following interfaces:
write_io_port
and
mb.
See
Writing Device Drivers: Advanced Topics
for an example implementation of a
close
interface for disk and tape device drivers.
.
.
.
register int unit = minor(dev); [1]
struct controller *ctlr = noneinfo[unit]; [2]
struct none_softc *sc = &none_softc[unit]; [3]
register io_handle_t reg =
(io_handle_t) ctlr->addr; [4]
write_io_port(reg + NONE_CSR, 4, 0, 0); [5]
mb(); [6]
sc->sc_openf = DN_CLOSE; [7]
return(ESUCCESS); [8]
}
-
Declares a
unit
variable and initializes it to the device minor number.
Note the use of the
minor
interface to obtain the device minor number.
The
minor
interface takes one argument: the number of the device for which an
associated device minor number will be obtained.
The minor number is encoded in the
dev
argument, which is of type
dev_t.
[Return to example]
-
Declares a pointer to a
controller
structure and calls it
ctlr.
The driver initializes
ctlr
to the
controller
structure associated with this
NONE
device.
The minor device number,
unit,
is used as an index into the array of
controller
structures to determine which
controller
structure is associated with this
NONE
device.
[Return to example]
-
Declares a pointer to a
none_softc
structure and calls it
sc.
The driver initializes
sc
to the address of the
none_softc
structure associated with this
NONE
device.
The minor device number,
unit,
is used as an index into the array of
none_softc
structures to determine which
none_softc
structure is associated with this
NONE
device.
[Return to example]
-
Section 7.1.6
shows that the
/dev/none
device driver stored the I/O handle passed to its
probe
interface in
the
reg
variable.
The
/dev/none
device driver now initializes
reg
to the system virtual address (SVA) for the
NONE
device.
This address is obtained from the
addr
member of the
controller
structure associated with this
NONE
device.
Section 17.2.11
describes the
addr
member.
Because the data types are different, this line performs a type-casting
operation that converts the
addr
member (which is of type
caddr_t)
to be of type
io_handle_t.
[Return to example]
-
The
NONE
device is not a real device and therefore would not initiate interrupts.
However, this line shows how to turn off interrupts by writing the value
zero (0) to the
NONE
device's control/status register.
To accomplish the write operation, the
/dev/none
device driver calls the
write_io_port
interface.
The
write_io_port
interface takes four arguments:
-
The first argument specifies
an I/O handle that you can use to reference a device register or memory
located in bus address space (either I/O space or memory space).
This I/O handle references a device register in the bus address space
where the write operation occurs.
In this call, the
/dev/none
driver specifies the device's I/O address space by adding the system
virtual address (SVA) (stored in the
reg
variable) to the device register offset (represented by the
NONE_CSR
bit).
-
The second argument specifies
the width (in bytes) of the data to be written.
Valid values are 1, 2, 3, 4, and 8.
Not all CPU platforms or bus adapters support all of these values.
In this call, the
/dev/none
driver passes the value 4.
-
The third argument specifies
flags to indicate special processing requests.
Because this argument is not currently used, the
/dev/none
driver passes the value zero (0).
-
The fourth argument specifies
the data to be written to the specified device register in bus address
space.
In this call, the
/dev/none
driver passes the value zero (0).
[Return to example]
-
Calls the
mb
interface to perform a memory barrier on Alpha CPUs.
It is generally recommended that drivers operating on Alpha CPUs
call the
mb
interface immediately after a call to
write_io_port.
See
Section 3.1.6
for a detailed discussion of when to call the
mb
interface.
[Return to example]
-
Turns off the open flag by setting the
sc_openf
member of the
sc
pointer to the close bit
DN_CLOSE.
This action frees up the unit so that subsequent calls to
noneopen
will succeed.
[Return to example]
-
Returns
ESUCCESS
to indicate a successful close of the
NONE
device.
[Return to example]
10.3 Implementing the ioctl Interface
The
ioctl
interface
typically performs all device-related operations other than read or
write operations.
A device driver's
ioctl
interface is called as a result of an
ioctl
system call.
Only those
ioctl
commands that are device specific or that require
action on the part of the device driver result in a call to the driver's
ioctl
interface.
You specify the entry point for the driver's
ioctl
interface
in a
dsent
structure.
Section 5.4
describes the
dsent
structure.
Writing Device Drivers: Reference
provides a reference page that gives additional information on the
arguments and tasks associated with
an
ioctl
interface.
The following example shows a simple implementation of an
ioctl
interface, using the
/dev/none
driver as an example.
The
/dev/none
driver's
ioctl
interface (called
noneioctl)
obtains and clears the count of bytes that was previously written by
nonewrite.
When a user program issues the command to obtain the count, the
/dev/none
driver returns the count through the data pointer passed to the
noneioctl
interface.
When a user program asks to clear the count, the
/dev/none
driver does so.
The code example also shows a call to the
minor
interface.
See
Writing Device Drivers: Advanced Topics
for an example implementation of an
ioctl
interface for disk and tape device drivers.
noneioctl(dev, cmd, data, flag)
dev_t dev; [1]
unsigned int cmd; [2]
caddr_t data; [3]
int flag; [4]
{
int unit = minor(dev); [5]
int *res; [6]
struct none_softc *sc = &none_softc[unit]; [7]
res = (int *) data; [8]
if(cmd == DN_GETCOUNT)
*res = sc->sc_count; [9]
if(cmd == DN_CLRCOUNT)
sc->sc_count = 0; [10]
return (ESUCCESS); [11]
}
-
Declares an
argument that specifies the major and minor device numbers for
a specific
NONE
device.
The minor device number is used to determine the logical unit number for
the
NONE
device on which the
ioctl
operation is to be performed.
[Return to example]
-
Declares an argument to contain the
ioctl
command as specified
in
/usr/sys/include/sys/ioctl.h
or in another include file that you define.
[Return to example]
-
Declares a pointer to the
ioctl
command-specified data that is to
be passed to the device driver or filled in by the device driver.
This argument is a kernel address.
The size of the data cannot exceed the size of a page
(currently 8 kilobytes (KB) on Alpha systems).
At least 128 bytes are guaranteed.
Any size between 128 bytes and the page size may fail if memory cannot
be allocated.
The particular
ioctl
command implicitly determines the action to be
taken.
The
ioctl
system call performs all the necessary copy operations to move data to
and from user space.
[Return to example]
-
Declares an argument that holds the access mode of the device.
The access modes are represented by flag constants defined in
/usr/sys/include/sys/file.h.
[Return to example]
-
Declares a
unit
variable and initializes it to the device minor number.
Note the use of the
minor
interface to obtain the device minor number.
The
minor
interface takes one argument: the number of the device for which an
associated device minor number will be obtained.
The minor number is encoded in the
dev
argument, which is of type
dev_t.
[Return to example]
-
Declares a pointer to a variable called
res
that will store a pointer to the kernel memory allocated by the
ioctl
system call.
This variable also stores the character
count.
The
nonewrite
interface stores
this character count in the
sc_count
member of the
softc
structure associated with this
NONE
device.
Section 8.2.2
discusses the
nonewrite
interface.
[Return to example]
-
Declares a pointer to a
none_softc
structure and calls it
sc.
The driver initializes
sc
to the address of the
none_softc
structure associated with this
NONE
device.
The minor device number,
unit,
is used as an index into the array of
none_softc
structures to determine which
none_softc
structure is associated with this
NONE
device.
[Return to example]
-
Sets the
res
variable to point to the kernel memory allocated by the
ioctl
system call.
The
ioctl
system call copies the data to and from user address space.
Because the data types are different, this line performs a type-casting
operation that converts the
data
argument (which is of type
caddr_t)
to be of type pointer to an
int.
[Return to example]
-
If the
ioctl
command is equal to the
DN_GETCOUNT
macro, sets
res
to the number of bytes in the write request.
This count was previously set by the
nonewrite
interface in the
sc_count
member of the
sc
pointer.
Section 5.1.2
discusses the
DN_GETCOUNT
macro.
Section 8.2.2
discusses the
nonewrite
interface.
[Return to example]
-
If the
ioctl
command is equal to the
DN_CLRCOUNT
macro, sets the count of characters written to the device to the
value zero (0).
This line has the effect of clearing the
sc_count
member of the
softc
structure associated with this
NONE
device.
Section 5.1.2
discusses the
DN_CLRCOUNT
macro.
[Return to example]
-
Returns
ESUCCESS
to indicate successful completion of the
ioctl
operation.
[Return to example]
10.4 Implementing the Interrupt Handler
The interrupt section applies to both character and block device drivers
and it contains a device interrupt handler, which this book refers to as
the driver's interrupt handler.
The interrupt handler
is called as a result of a hardware interrupt.
Digital requires that all drivers call the
handler
interfaces to dynamically register interrupt handlers.
Typically, a device driver registers interrupt handlers in the driver's
probe
interface.
Section 7.1.6
discusses how to register a driver's interrupt handler (using the
/dev/none
driver as an example).
Section 7.1.7
discusses how to register a driver's shared interrupt handler (using the
/dev/xx
driver as an example).
When the kernel interrupt interface that does the initial handling of
interrupts receives an interrupt, it:
-
Saves the state of the CPU (for example, the registers)
-
Sets up the argument list for the call to the driver (that is, the unit
number)
-
Transfers control to the appropriate driver, based on the interrupt
vector index provided by the bus
Upon return from the driver's interrupt handler, the kernel
restores the state of the CPU to allow previously running processes to
run.
The
/dev/none
driver's
interrupt
handler (called
noneintr)
is a stub because there is no physical device to generate an interrupt.
However, most devices can generate interrupts.
For example, a terminal might generate an interrupt when a character is
keyed into it.
There is an interrupt entry point for those drivers that are written for
devices that generate interrupts.
Section 2.1.3.4
discusses a number of important issues relating to interrupts.
Some devices generate more than one type of interrupt.
Thus, drivers controlling these devices can contain more than one
interrupt section.
For the
/dev/none
driver, these issues are not relevant because there are no interrupts,
but they will be important for most drivers.
To give you an idea of some tasks an interrupt handler can perform, the
following example shows the code associated with the
/dev/xx
driver's interrupt handler (called
xxintr).
The
xxintr
interrupt handler is an example of a shared interrupt.
int xx_intr(parameter)
int parameter; [1]
{
.
.
.
register struct xx_softc *sc = xx_softc[unit]; [2]
int intr_status; [3]
.
.
.
intr_status = xx_readio(PNV_INTR_CTRL, sc); [4]
if (!(intr_status & PNV_INTR_OCCURRED))
return(INTR_NOT_SERVICED); [5]
if ((intr_status & PNV_DMA_INTR) { [6]
xx_writeio(PNV_INTR_CTRL,
(intr_status & ~(PNV_CRD_INTR|PNV_ABRT_INTR)), sc);
.
.
.
}
return(INTR_SERVICED); [7]
} /* End of xx_intr */
-
Specifies
any parameter that the driver needs to control operation of the
interrupt handler.
The value that gets passed to the
parameter
argument is the value you stored in the
param
member of the
handler_intr_info
data structure.
In this example, the device driver's interrupt handler does not make
use of this argument.
[Return to example]
-
Specifies a pointer to a
softc
data structure and initializes it to the
softc
structure associated with this
XX
device.
[Return to example]
-
Declares a variable called
intr_status
that stores the bit associated with the interrupt controller register
after the interrupt occurred.
[Return to example]
-
Calls the device driver's
xx_readio
interface, which reads the interrupt control register
PNV_INTR_CTRL
to determine if an interrupt occurred.
(Assume that the
/dev/xx
driver implements the
xx_readio
interface to read the control register.)
[Return to example]
-
If there is no interrupt status bit set in the interrupt control
register, returns the constant
INTR_NOT_SERVICED
to the kernel's interrupt dispatch code.
This constant indicates that the driver's interrupt handler
did not service the shared
interrupt.
[Return to example]
-
If the interrupt control register is set to the
PNV_DMA_INTR
bit, then an interrupt occurred.
Calls the device driver's
xx_writeio
interface to write the bit status to the interrupt control register.
(Assume that the
/dev/xx
driver implements the
xx_writeio
interface to write the bit status.)
[Return to example]
-
Returns
INTR_SERVICED
to the kernel's interrupt dispatch code to indicate that the driver's
interrupt handler serviced the shared interrupt.
[Return to example]