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:
struct termio
{
unsigned short int c_iflag;
unsigned short int c_oflag;
unsigned short int c_cflag;
unsigned short int c_lflag;
unsigned char c_line;
unsigned char c_cc[NCC];
};
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:
#define B50 0000001
#define B75 0000002
#define B110 0000003
#define B134 0000004
#define B150 0000005
#define B200 0000006
#define B300 0000007
#define B600 0000010
#define B1200 0000011
#define B1800 0000012
#define B2400 0000013
#define B4800 0000014
#define B9600 0000015
#define B19200 0000016
#define B38400 0000017
Once you’ve got the code in place, use this command to compile it:
$ gcc -o get_line_speed get_line_speed.c
Now run it:
$ ./get_line_speed /dev/pts/0
[./get_line_speed] /dev/pts/0 is 38400 baud
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:
$ ./set_line_speed /dev/pts/0 9600
Confirm that the line speed has changed:
$ ./get_line_speed /dev/pts/0
[./get_line_speed] /dev/pts/0 is 9600 baud
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:
struct ifconf
{
int ifc_len;
union
{
char *ifcu_buf;
struct ifreq *ifcu_req;
} ifc_ifcu;
};
The SIOCGIFCONF function returns a number of records of type ifreq (one for each interface) in a buffer pointed to by conf.ifc_buf. Then ioctl is called with a pointer to conf:
struct ifreq
{
union
{
char ifrn_name[IFNAMSIZ];
} ifr_ifrn;
union {
struct sockaddr ifru_addr;
struct sockaddr ifru_dstaddr;
struct sockaddr ifru_broadaddr;
struct sockaddr ifru_netmask;
struct sockaddr ifru_hwaddr;
short ifru_flags;
int ifru_ivalue;
int ifru_mtu;
struct ifmap ifru_map;
char ifru_slave[IFNAMSIZ];
char ifru_newname[IFNAMSIZ];
void * ifru_data;
void * ifru_data;
struct if_settings ifru_settings;
} ifr_ifru;
};
The SIOCGIFCONF function returns a number of records of type ifreq (one for each interface) in a buffer pointed to by conf.ifc_buf. Then, ioctl is called with a pointer to conf:
struct ifconf conf;
char if_data[1024];
conf.ifc_len = sizeof(if_data);
conf.ifc_buf = if_data;
ioctl(sd, SIOCGIFCONF, &conf);
The code listing for show_ints.c is in Listing 3. Run it, having compiled it first, with:
$ ./show_ints
[./show_ints] lo eth0 eth1
It shows that my machine has a loop back interface and two” Ethernet” interfaces, where eth0 is an Ethernet device and eth1 is a wireless device.
This next example shows how to set the IP address and subnet mask on an interface. The macros for these two functions (defined in linux/sockios.h) are:
#define SIOCSIFADDR 0x8916
#define SIOCSIFNETMASK 0x891c
Listing 4 shows the code for set_ipaddr.c. As root, set the IP address and network mask:
$ sudo ./set_ipaddr eth0 10.0.0.1 255.255.240.0>
Then confirm that the IP address and subnet mask have been set:
$ ifconfig eth0 | grep inet
inet addr:10.0.0.1 Bcast:10.0.15.255 Mask:255.255.240.0
Wireless Interfaces
Next, let’s focus on functions specific to the wireless card. The first example shows how to get the ESSID of the associated network. The function for getting the ESSID is defined in linux/wireless.h:
#define SIOCSIWESSID 0x8B1A
The important data structures for wireless ioctl calls are iwreq and iwreq_data (linux/wireless.h). The definition of the iwreq struct is:
struct iwreq
{
union
{
char ifrn_name[IFNAMSIZ];
} ifr_ifrn;
union iwreq_data u;
};
The iwreq_data struct is shown below. For brevity, some of the fields have been omitted:
union iwreq_data
{
char name[IFNAMSIZ];
struct iw_point essid; /* Extended network name */
:
:
struct iw_param rts; /* RTS threshold threshold */
struct iw_param frag; /* Fragmentation threshold */
:
:
struct iw_param param; /* Other small parameters */
struct iw_point data; /* Other large parameters */
};
Certain fields in the data structures are assigned prior to the ioctl call. The ESSID is written to the memory location referenced by w.u.essid.pointer, thus:
struct iwreq w;
char essid[IW_ESSID_MAX_SIZE];
:
strncpy(w.ifr_ifrn.ifrn_name, ifname, IFNAMSIZ);
w.u.essid.pointer = (caddr_t *) essid;
w.u.data.length = IW_ESSID_MAX_SIZE;
w.u.data.flags = 0;
:
ioctl(sd, SIOCGIWESSID, &w);
The code for get_essid.c is in Listing 5 (again, found online at http://www.linux-mag.com/code/08.03.ioctl.tar). Running this command on a wireless device current associated with a network, gives:
$ ./get_essid eth1
[./get_essid] essid for eth1: mywifi
You can confirm this result with the command:
$ iwconfig eth1 | grep ESSID
eth1 IEEE 802.11g ESSID:"mywifi"
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:
#define SIOCSIWRTS 0x8B22 /* set RTS/CTS threshold (bytes) */
#define SIOCGIWRTS 0x8B23 /* get RTS/CTS threshold (bytes) */
#define SIOCSIWFRAG 0x8B24 /* set fragmentation thr (bytes) */
#define SIOCGIWFRAG 0x8B25 /* get fragmentation thr (bytes) */
The code for get_rts_frag.c and set_rts_frag.c are shown in Listings 6 and 7 respectively. Once both have been compiled, run get_rts_frag on the wireless device:
$ ./get_rts_frag eth1
[./get_rts_frag] rts threshold: 2347 (disabled)
[./get_rts_frag] fragmentation threshold: 2346 (disabled)
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:
w.u.rts.disabled = 0;
w.u.rts.value = atoi(rts_value);
ioctl(sd, SIOCSIWRTS, &w); w.u.frag.disabled = 0;
w.u.frag.value = atoi(frag_value);
ioctl(sd, SIOCSIWFRAG, &w);
Run set_rts_frag as root:
$ sudo ./set_rts_frag eth1 2000 2000
Now use get_rts_frag to confirm that the thresholds have been set:
$ ./get_rts_frag eth1
[./get_rts_frag] rts threshold: 2000 (enabled)
[./get_rts_frag] fragmentation threshold: 2000 (enabled)
Double check with the iwconfig command:
$ iwconfig eth1 | grep RTS
Retry limit:15 RTS thr:2000 B Fragment thr:2000 B
Summary
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.
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.
nice i hope that we can see more in depth articles like this, Ph. Alan ;) please do it frequently :) .
Hi Alan,
That was a wonderful and simple explaination, Keep it UP.
Please Do Post Such Articles frequently.
There one problem with one of the links provided in the article,http://www.linux-mag.com/code/08.03.ioctl.tar.
Thank You
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
Just to be clear… the show_ints code can only see interfaces which are in an ‘up’ state.
If you were truly doing interface discovery (i.e. for an installer or some such) and no interfaces are in a configured state, what would the code look like to identify the interfaces?
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.
Ok. I didn’t know whether ioctl could be used in some other way to obtain the ‘down’ interfaces as well. That was more my reason for asking. I wasn’t (intentionally) trying to be annoyingly pedantic.
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.