Controlling Input/Output of Device Drivers

In this article I show how to control input/output (I/O) of device drivers. Device I/O control is performed by calling driver functions in the kernel. These functions available vary from device to device. As a result, some investigation of the system header files are required in order understand how to call them and interpret the results they return.

Kernel functions are accessed via system calls, however, instead of introducing new systems calls for each I/O function, ioctl is provided as a generic system call for all device I/O control. Network interface configuration commands, for example, make extensive use of the ioctl system call. Try running strace ifconfig eth0 (for example) and you will see a number of calls to ioctl. This article focuses on network devices (serial port, Ethernet cards and Wireless cards) and gives examples of how to monitor and configure them at the system call level.

The ioctl System Call

A device driver consists of a set of kernel space functions that can be accessed from user space via system calls. These functions are implemented to resemble the file system interface. The file operations struct, defined in include/linux/fs.h under the kernel source tree, gives a list of the functions. A driver not may implement all these functions (unwanted functions are defined as NULL).

However, it is likely that the driver will require a number of extra functions for input/output control. Furthermore, these functions tend to be specific to the particular device. For example, a wireless device needs a function to set the access point MAC address, but such a function is unnecessary on an Ethernet card. Rather than introducing new system calls into the kernel; a single system call, ioctl, is used to call device specific functions. The synopsis for ioctl (from the man page) is:

int ioctl(int fildes, int request, /*arg*/...);

The first argument (filedes) is the open file descriptor of the device. For network devices, such as Ethernet and wireless cards, a socket descriptor is used The second argument (request specifies the requested function (as an integer value). ioctl functions are defined as macros in the header files under /usr/include. Traditionally, in Unix, the choice of ioctl function numbers were somewhat ad hoc. However, if drivers shared the same function numbers, an ioctl call to an incorrect device could result in some undesirable outcome.

For this reason Linux adopts a numbering convention in order to ensure function numbers are” globally unique”. A call to the wrong driver returns a EINVAL error rather than some random result. The third argument is an untyped pointer to some memory location in user space which is used to exchange data between driver and application. The structure of this memory location will depend upon the function being called. The most useful source of information on driver function data structure are the header files in /usr/include.

The Terminal

For the first example, I’ll use ioctl to get the line speed of a tty. This can be done from the command line using the stty command. The sequence of commands below tells me the current line speed of my (pseudo) tty is set to 38400 baud (albeit, this is pretty meaningless for a pseudo tty):

$ tty
/dev/pts/0
$ stty speed </dev/pts/0
38400

The state information for a terminal device (even a pseudo one) is held in variable of type termio. The definition of termio is found in bits/ioctl-types.h:

For this example, we are only interested in the c_cflag field, the four least significant bits of which store the line speed of the terminal. The TCGETA function is used to get terminal state information. The macro is defined in the kernel source at asm-i386/ioctls.h:

#define TCGETA 0x5405

We define desc as a termio structure which is then passed as pointer in ioctl call:

struct termio desc;
ioctl(fd,TCGETA,&desc);

The value of the line speed is given by:

line_speed = desc.c_cflag & (unsigned int) 15

The value of line_speed should correspond to a value of one of the macros defined in bits/termios.h:

In this next example we write a program to set the line speed. The ioctl function for this is TCSETA, and is defined in asm-i386/ioctls.h:

#define TCSETA 0x5406

To set the line speed of the terminal device, first we have to issue a TCGETA, in order to save the current state of terminal. The reason for doing this is that the state of the other terminal characteristics must be preserved when we change the line speed. Having read the terminal characteristics (into desc), we mask out the line speed bits:

desc.c_cflag &= 0x02DCCBAUD;

The baud rate is set (in this case to 9600 baud) and then TCSETA is called:

desc.c_cflag |= B9600;
ioctl(fd,TCSETA, &desc);

The full code listing for setting the line speed setting is in Listing 2. Compile set_line_speed.c, then set the line speed (where the baud rate of the line is passed as a command-line argument) with:

Double check using stty speed>/dev/pts/0. Note that it’s recommended that you use the cfgetispeed, cfgetospeed, cfsetispeed, and cfsetospeed macros for setting line speeds, rather than calling ioctl directly.

Network Interfaces

ioctl calls can be issued to network interfaces using an open socket descriptor (instead of a file descriptor). The line of code shown here will suffice:

sd = socket(AF_INET, SOCK_DGRAM, 0)

This example shows how to get a list of all the network interfaces for your machine. This is done by issuing the SIOCGIFCONF function, which is defined in bits/ioctl.h (and in linux/sockios.h for some reason):

#define SIOCGIFCONF 0x8912

The SIOCGICONF function requires to important data structures, ifconf and ifreq (defined in linux/if.h). The ifconf struct is defined as:

If you run this command on an Ethernet device (such as eth0 in my case) you will get a” Operation not supported” error. These last two examples get and set the RTS and Fragmentation thresholds. The corresponding macros are defined as:

It shows the RTS and Fragmentation thresholds are 2347 (w.u.rts.value) and 2346 (w.u.frag.value) bytes respectively. Also note that RTS and Fragmentation are disabled (w.u.rts.disabled and w.u.frag.disabled set to 1). We assign the thresholds (from values passed on the command line) prior to calling ioctl. Note that we also have to enabled RTS and Fragmentation:

Functions for I/O control of device drivers are specific to the device. Rather than developing new system calls in the kernel for each driver, ioctl is used calling driver specific functions. Data structures passed between kernel and user space are also device dependent so it is important to examine the header files in /usr/include for clues on how to call ioctl functions correctly.

The ioctl functions can be issued on devices using file or socket descriptors. I’ve provided number of examples I/O control on communication devices have been presented in this article, and I hope that you can extrapolate those into something useful for your own projects.

Comments on "Controlling Input/Output of Device Drivers"

djeepp

I guess a prerequisite to reading this article would be a basic understanding of coding in c of which I have none of. This stuff is way over my head.

Thanks for the feedback Rakesh. The article appeared in Linux Magazine a couple of months ago, so they are responsible for publishing the article on-line. To save space the editor took the code listings out and put them on the web. I don’t know why the URL doesn’t work, but I’ve let them know – hopefully it will be fixed soon. However, as an alternative, I’ve put the tar file here: http://agholt.googlepages.com/ioctl.tar.gz

Yes you are correct, I probably should have made that clear in the article. I’m just really trying to demonstrate ioctls calls rather write a fully fledged “interface discovery” utility. If I were, I’d probably just get them out of /proc/net/dev. But thanks for the clarification.

Cirnath: I didn’t think you were trying to be pedantic – it’s a good point. Browsing through the header files SIOCGIFCONF is the only function I see that returns a list of interfaces and, as you correctly point out, it doesn’t return down interfaces – if it doesn’t have an IP address. A down interface with an IP address does show up (how weird is that?) I guess the behaviour of SIOCGIFCONF is down to the whim of the kernel developers.