Controlling Hardware with ioctls

by Lisa Corsetti

A few years ago, I had a laptop that I used at work and at home. To simplify my network configuration and not have to change it manually depending on where I was, I decided to use DHCP in both places. It was the standard at work, so I implemented a DHCP server at home. This worked well except when I booted the system without it being plugged in to either network. When I did, the laptop spent a lot of time trying to find a DHCP server without success before continuing the rest of the startup process.

I concluded that an ideal solution to this lag time would be for the system to start with the Ethernet interface down and have it come up if, and only if, the cable was connected to a hub, that is, if I had a link light on the Ethernet interface. The best way to do this appeared to be having a shell script call a program whose return code would indicate whether a link had been established on a particular network interface. So began my quest for a method to determine this link status of my 10/100Base-T interface.

Not having done much low-level Linux programming, it took me a bit of time to discover that most of this type of interaction with device drivers usually is done through the ioctl library call (an abbreviation of I/O control), prototyped in sys/ioctl.h:

int ioctl(int, int, ...)

The first argument is a file descriptor. Because all devices in Linux are accessed like files, the file descriptor used usually is one that has been opened with the device to which you are interfacing as the target. In the case of Ethernet interfaces, however, the fd simply is an open socket. Apparently, no need exists to bind this socket to the interface in question.

The second argument in ioclt.h is an integer that represents an identification number of the specific request to ioctl. The requests inherently must vary from device to device. You can, for example, set the speed of a serial device but not a printer device. Of course, a specific set of commands exists for network interfaces.

Additional arguments are optional and could vary from the ioctl implementation on one device to the implementation on another. As far as I can tell, a third argument always is present, and I have yet to find more than a third. This third argument usually seems to be a pointer to a structure. This allows the passing of an arbitrary amount of data in both directions, the data being defined by the structure to which the pointer refers, simply by passing the pointer.

A basic example of how ioctl works is shown in the following simple program that checks the status of one signal on a serial port:


#include <termios.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>

main()
{
  int fd, status;

   fd = open("/dev/ttyS0", O_RDONLY);
   if (ioctl(fd, TIOCMGET, &status) == -1)
      printf("TIOCMGET failed: %s\n",
             strerror(errno));
   else {
      if (status & TIOCM_DTR)
         puts("TIOCM_DTR is not set");
      else
         puts("TIOCM_DTR is set");
   }
   close(fd);
}

This program opens a tty (serial port) and then calls ioctl with the fd of the serial port, the command TIOCMGET (listed as get the status of modem bits) and a pointer to an integer into which the result is returned.

The ioctl result then is checked to see whether an error was made in processing the request. If there are no problems, we check the values returned by anding them with TIOCM_DTR. This step yields true or false, nonzero or zero, respectively.

Using ioctl for Ethernet drivers is a similar process. The third parameter to ioctl calls for socket ioctl calls (where the fd is a socket handle) often is a pointer to a ifreq (interface request) structure. The type deceleration for ifreq structures can be found in net/if.h.

Unfortunately, documentation for many of the ioctl interfaces is difficult to find, and there are at least three different APIs for accessing network interfaces. I originally wrote this program using the MII (media independent interface) method. While writing this article, with the most recent kernel installed on my machine, however, I discovered I had to add the ETHTOOL method.

After adding ETHTOOL, I then modified the program and wrote each interface method as a subroutine. The modified program tries one method, and if it fails, attempts the other. The third method predates the MII API, and I have not yet run into a machine on which I have needed it, so the code is not included.

The information on using the MII interface was acquired mainly by examining the mii-diag program (ftp.scyld.com/pub/diag/mii-diag.c) written by Donald Becker, which I found on Scyld Computing Corporation's Web site. This site also contains an excellent page (www.scyld.com/diag/mii-status.html) explaining the details of the MII status words that the ioctl functions may return. Here, however, I focus on the ETHTOOL interface because it is the newer method. The program and both interfaces are available from the Linux Journal FTP site at ftp.linuxjournal.com/pub/lj/listings/issue117/6908.tgz.

Information on using the ETHTOOL API also was acquired by scouring various pieces of source code, not the least of which was the source code for the network interface drivers themselves—particularly eepro100.c. Also helpful was an e-mail written by Tim Hockin that I found while Googling.

In writing my program, I set the default interface to eth0 unless a parameter was passed to the program. The interface ID is stored in ifname. Because the ioctl commands I use are specific to network interfaces, using some other device likely will cause a “cannot determine status” to be returned.

Before calling ioctl we need a file handle, so we first must open a socket:


int skfd;
if (( skfd = socket( AF_INET, SOCK_DGRAM, 0 ) ) < 0 )
   {
   printf("socket error\n");
   exit(-1);
   }

In the standard try-to-check-for-all-errors C coding style, I placed this inside an if statement that simply prints an error and terminates the program, returning a -1 if the socket does not open properly. For my purposes, I would rather report errors in determining status as a lack of a link rather than as a presence of one, so a found link is reported as 0 and not found is reported as 1.

The new ETHTOOL API for interfacing to the driver has made determining the status of the link much easier than did the previous method. ioctl was implemented for ETHTOOL interfaces such that there is now only ONE ioctl command, SIOCETHTOOL (which specifies that the call is an ETHTOOL command), and the data block passed then contains the specific subcommand for the ETHTOOL interface.

The standard ioctl data structure (type ifreq) requires two additional items: the name of the interface to which the command should be applied and an address of a structure (type ethtool_value) in which to store the specific command as well as the returned information.

The structures and most other information (including the commands available) are located in the ethtool.h header file. The command that I needed was ETHTOOL_GLINK, documented as “get link status”, which I stored in edata.cmd:

edata.cmd = ETHTOOL_GLINK;

The name of the interface and the address of the edata structure both need to be placed into the ifreq structure:


strncpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name)-1);
ifr.ifr_data = (char *) &edata;

At this point, all that remains is making the ioctl call, checking the return value (to be sure the command type was allowed) and checking the data in the structure pointed to by the returned pointer to see if the link is up or down:


if (ioctl(skfd, SIOCETHTOOL, &ifr) == -1) {
    printf("ETHTOOL_GLINK failed: %s\n", strerror(errno));
    return 2;
}
return (edata.data ? 0 : 1);

In this case, my code returns a 0 for link up, a 1 for link down and a 2 for undetermined or failure. This code allows me to call this function from my rc.local, bring the interface up and either call dhcpcd or pump to get an IP address only if the system is plugged in to a functioning hub/switch. Here is the relevant section of rc.local:


/root/sense_link/sense_link | logger
if /root/sense_link/sense_link > /dev/null; then
  logger "No link sense -- downing eth0"
  /sbin/ifconfig eth0 down
else
  logger "Sensed link - dhcping eth0"
  /sbin/dhcpcd eth0
fi

First, the output of sense_link is sent to the system log. Then, if no link was sensed on eth0, or if it could not be determined, a message is written to the log and eth0 is taken down. If a link was sensed, dhcpcd is executed on eth0.

Once this is implemented, my rc.local file now executes quite quickly when no network cable is plugged in or when a DHCP server is active and found. The only time I still experience significant delays is if I am plugged in to a network where there is no active DHCP server.

I haven't yet tried this code with my 802.11b card to see if it can detect a link on it before attempting to contact a DHCP server, because I usually only have the PCMCIA card plugged in when I am in a location that I know has a server. It would be an interesting experiment and a useful extension, however, for an interested party.

Lisa Corsetti presently is a software architect as well as the president of Anteil, Inc., a consulting firm that focuses on custom Web-based applications for various industries and government. Lisa received a BS in Electrical and Computer Engineering from Drexel University.

Load Disqus comments