In Chapter 1 we constructed a driver that could read from and write to a device. In addition to reading and writing, most drivers need to perform other I/O operations, such as reporting error information, ejecting removable media, or activating self-destruct sequences. This chapter details how to make drivers do those things.
We’ll start by describing the ioctl interface, also known as the input/output control interface. This interface is commonly used for device communication and control. Then we’ll describe the sysctl interface, also known as the system control interface. This interface is used to dynamically change or examine the kernel’s parameters, which includes device drivers.
The ioctl interface is the catchall of I/O operations (Stevens, 1992). Any operation that cannot be expressed using d_read
or d_write
(that is, any operation that’s not a data transfer) is supported by d_ioctl
.[3] For example, the CD-ROM driver’s d_ioctl
function performs 29 distinct operations, such as ejecting the CD, starting audio playback, stopping audio playback, muting the audio, and so on.
The function prototype for d_ioctl
is defined in the <sys/conf.h>
header as follows:
typedef int d_ioctl_t(struct cdev *dev, u_long cmd, caddr_t data, int fflag, struct thread *td);
Here, cmd
is an ioctl command passed from user space. ioctl commands are driver-defined numeric constants that identify the different I/O operations that a d_ioctl
function can perform. Generally, you’d use the cmd
argument in a switch
statement to set up a code block for each I/O operation. Any arguments required for an I/O operation are passed through data
.
Here is an example d_ioctl
function:
Just concentrate on the structure of this code and ignore what it does.
static int echo_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag, struct thread *td) { int error = 0; switch (cmd) { case ECHO_CLEAR_BUFFER: memset(echo_message->buffer, ' ', echo_message->buffer_size); echo_message->length = 0; uprintf("Buffer cleared. "); break; case ECHO_SET_BUFFER_SIZE: error = echo_set_buffer_size(*(int *)data); if (error == 0) uprintf("Buffer resized. "); break; default: error = ENOTTY; break; } return (error); }
Notice how the cmd
argument is the expression for the switch
statement. The constants ECHO_CLEAR_BUFFER
and ECHO_SET_BUFFER_SIZE
are (obviously) the ioctl commands. All ioctl commands are defined using one of four macros. I’ll discuss these macros in the following section.
Additionally, notice how the data
argument is cast—as an integer pointer—before it is dereferenced. This is because data
is fundamentally a “pointer to void
.”
Pointers to void
can hold any pointer type, so they must be cast before they’re dereferenced. In fact, you can’t directly dereference a pointer to void
.
Finally, according to the POSIX standard, when an inappropriate ioctl command is received, the error code ENOTTY
should be returned (Corbet et al., 2005). Hence, the default
block sets error
to ENOTTY
.
At one point in time, only TTY drivers had an ioctl function, which is why ENOTTY
means “error: inappropriate ioctl for device” (Corbet et al., 2005).
Now that you’ve examined the structure of a d_ioctl
function, I’ll explain how to define an ioctl command.
3.147.140.206