(笔记)Linux下的ioctl()函数详解
我这⾥说的ioctl函数是指驱动程序⾥的,因为我不知道还有没有别的场合⽤到了它,所以就规定了我们讨论的范围。写这篇⽂章是因为我前⼀阵⼦被ioctl给搞混了,这⼏天才弄明⽩它,于是在这⾥清理⼀下头脑。
⼀、什么是ioctl
ioctl是设备驱动程序中对设备的I/O通道进⾏管理的函数。所谓对I/O通道进⾏管理,就是对设备的⼀些特性进⾏控制,例如串⼝的传输波特率、马达的转速等等。它的调⽤个数如下:int ioctl(int fd, ind cmd, …);
其中fd是⽤户程序打开设备时使⽤open函数返回的⽂件标⽰符,cmd是⽤户程序对设备的控制命令,⾄于后⾯的省略号,那是⼀些补充参数,⼀般最多⼀个,这个参数的有⽆和cmd的意义相关。
ioctl函数是⽂件结构中的⼀个属性分量,就是说如果你的驱动程序提供了对ioctl的⽀持,⽤户就可以在⽤户程序中使⽤ioctl函数来控制设备的I/O通道。
下表列出了⽹络相关ioctl 请求的request 参数以及arg 地址必须指向的数据类型:
类别Request说明数据类型
套接⼝SIOCATMARK
SIOCSPGRP
SIOCGPGRP
是否位于带外标记
设置套接⼝的进程ID 或进程组ID
获取套接⼝的进程ID 或进程组ID
int
int
int
⽂件FIONBIN
FIOASYNC
FIONREAD
FIOSETOWN
FIOGETOWN
设置/ 清除⾮阻塞I/O 标志
设置/ 清除信号驱动异步I/O 标志
获取接收缓存区中的字节数
设置⽂件的进程ID 或进程组ID
获取⽂件的进程ID 或进程组ID
int
int
int
int
int
接⼝SIOCGIFCONF
SIOCSIFADDR
SIOCGIFADDR
SIOCSIFFLAGS
SIOCGIFFLAGS
SIOCSIFDSTADDR
SIOCGIFDSTADDR
SIOCGIFBRDADDR
SIOCSIFBRDADDR
SIOCGIFNETMASK
SIOCSIFNETMASK
SIOCGIFMETRIC
SIOCSIFMETRIC
SIOCGIFMTU
SIOCxxx
获取所有接⼝的清单
设置接⼝地址
获取接⼝地址
设置接⼝标志
获取接⼝标志
设置点到点地址
获取点到点地址
获取⼴播地址
设置⼴播地址
获取⼦⽹掩码
设置⼦⽹掩码
获取接⼝的测度
设置接⼝的测度
获取接⼝MTU
(还有很多取决于系统的实现)
struct ifconf
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
ARP SIOCSARP
SIOCGARP
SIOCDARP
创建/ 修改ARP 表项
获取ARP 表项
删除ARP 表项
struct arpreq
struct arpreq
struct arpreq
路由SIOCADDRT
SIOCDELRT
增加路径
删除路径
struct rtentry
struct rtentry
流I_xxx
⼆、 ioctl的必要性
如果不⽤ioctl的话,也可以实现对设备I/O通道的控制,但那是蛮拧了。例如,我们可以在驱动程序中实现write的时候检查⼀下是否有特殊约定的数据流通过,如果有的话,那么后⾯就跟着控制命令(⼀般在socket编程中常常这样做)。但是如果这样做的话,会导致代码分⼯不明,程序结构混乱,程序员⾃⼰也会头昏眼花的。所以,我们就使⽤ioctl来实现控制的功能。要记住,⽤户程序所作的只是通过命令码(
cmd)告诉驱动程序它想做什么,⾄于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情。
三、 ioctl如何实现
这是⼀个很⿇烦的问题,我是能省则省。要说清楚它,没有四五千字是不⾏的,所以我这⾥是不可能把它说得⾮常清楚了,不过如果读者对⽤户程序是怎么和驱动程序联系起来感兴趣的话,可以看我前⼀阵⼦写的《write的奥秘》。读者只要把write换成ioctl,就知道⽤户程序的ioctl是怎么和驱动程序中的ioctl实现联系在⼀起的了。我这⾥说⼀个⼤概思路,因为我觉得《Linux设备驱动程序》这本书已经说的⾮常清楚了,但是得花⼀些时间来看。
在驱动程序中实现的ioctl函数体内,实际上是有⼀个switch{case}结构,每⼀个case对应⼀个命令码,做出⼀些相应的操作。怎么实现这些操作,这是每⼀个程序员⾃⼰的事情。因为设备都是特定的,这⾥也没法说。关键在于怎样组织命令码,因为在ioctl中命令码是唯⼀联系⽤户程序命令和驱动程序⽀持的途径。命令码的组织是有⼀些讲究的,因为我们⼀定要做到命令和设备是⼀⼀对应的,这样才不会将正确的命令发给错误的设备,或者是把错误的命令发给正确的设备,或者是把错误的命令发给错误的设备。这些错误都会导致不可预料的事情发⽣,⽽当程序员发现了这些奇怪的事情的时候,再来调试程序查错误,那将是⾮常困难的事情。所以在Linux核⼼中是这样定义⼀个命令码的:
____________________________________
| 设备类型 | 序列号 | ⽅向 |数据尺⼨|
|----------|--------|-------|-----------|
| 8 bit    | 8 bit  |2 bit |8~14 bit|
|----------|--------|-------|-----------|
这样⼀来,⼀个命令就变成了⼀个整数形式的命令码;但是命令码⾮常的不直观,所以Linux Kernel中提供了⼀些宏。这些宏可根据便于理解的字符串⽣成命令码,或者是从命令码得到⼀些⽤户可以理解的字符串以标明这个命令对应的设备类型、设备序列号、数据传送⽅向和数据传输尺⼨。
这些宏我就不在这⾥解释了,具体的形式请读者察看Linux核⼼源代码中的宏,⽂件⾥给这些宏做了完整的定义。这⾥我只多说⼀个地⽅,那就是"幻数"。"幻数"是⼀个字母,数据长度也是8,⽤⼀个特定的字母来标明设备类型,这和⽤⼀个数字是⼀样的,只是更加利于记忆和理解。就是这样,再没有更复杂的了。更多的说了也没⽤,读者还是看⼀看源代码吧,推荐各位阅读《Linux 设备驱动程序》所带源代码中的short⼀例,因为它⽐较短⼩,功能⽐较简单,可以看明⽩ioctl的功能和细节。
四、 cmd参数如何得出
这⾥确实要说⼀说,cmd参数在⽤户程序端由⼀些宏根据设备类型、序列号、传送⽅向、数据尺⼨等⽣成,这个整数通过系统调⽤传递到内核中的驱动程序,再由驱动程序使⽤解码宏从这个整数中得到设备的类型、序列号、传送⽅向、数据尺⼨等信息,然后通过switch{case}结构进⾏相应的操作。要透彻理解,只能是通过阅读源代码,我这篇⽂章实际上只是⼀个引⼦。cmd参数的组织还是⽐较复杂的,我认为要搞熟它还是得花不少时间的,但是这是值得的,因为驱动程序中最难的是对中断的理解。
五、⼩结
ioctl其实没有什么很难的东西需要理解,关键是理解cmd命令码是怎么在⽤户程序⾥⽣成并在驱动程序⾥解析的,程序员最主要的⼯作量在switch{case}结构中,因为对设备的I/O控制都是通过这⼀部分的代码实现的。
******************************************************************************************************************************************
⼀般的说,⽤户空间的IOCTL系统调⽤如下所⽰:ioctl(int fd, int command, (char *) argstruct);  因为这个调⽤拥有与⽹络相关的代码,所以⽂件描述符号fd就是socket()系统调⽤所返回的,⽽command参数可以是/usr/include /linux/sockios.h 头⽂件中的任何⼀个。这些命令根据它可以解决的问题所涉及的⽅⾯⽽被分为多种类型。⽐如:
改变路由表(SIOCADDRT, SIOCDELRT) 
读取或更新ARP/RARP缓存(SIOCDARP, SIOCSRARP)
⼀般的和⽹络有关的函数(SIOCGIFNAME, SIOCSIFADDR等等)
Goodies⽬录中包含了很多展⽰ioctl⽤法的⽰例程序,看这些程序的时候,注意根据ioctl的命令类型来使⽤具体的调⽤参数结构,⽐如:和路由表相关的IOCTL⽤RTENTRY结
构,rtentry结构是被定义在/usr/include/linux/route.h⽂件中的,和 ARP相关的ioctl调⽤到的arpreq结构被定义在/usr/include/linux/if_arp.h⽂件之中。⽹络接⼝相关的ioctl命令最具有代表性的特征的是都以S或G开头,其实就是设置或得到数据,getifinfo.c程序⽤这些命令去读取IP地址信息,硬件地址信息,⼴播地址信息和与⽹络接⼝相关的标志。对于这些ioctl,第三个参数是⼀个IFREQ结构体,这个结构体被定义在/usr/include/linux/if.h 头⽂件中。在⼀些情况下,新的ioctl命令可能会被使⽤ (除了在那个头⽂件中被定义的除外),⽐如 WAVELAN⽆线⽹卡保持着⽆线信号强度的信息,这些信息可能会对⽤户程序有⽤。⽤户程序是怎么访问到这些信息的呢?我们的第⼀反应就是在 sockios.h头⽂件中定义⼀个新的命令,⽐如SIOCGIFWVLNSS,但不幸的是,这个命令在其他的⽹络接⼝上是没有任何意义的。另外试图在其他接⼝上⽤这个命令⽽并⾮是在⽆线⽹⼝上⽤会出现违规访问,我们需要的是定义新特性接⼝命令的机理。幸运的是,LINUX操作系统为此⽬的⽽内置了钩⼦,如果你再看⼀下sockios.h这个头⽂件,你会注意到每⼀个设备都有⼀个预定义的SIOCDEVPRIVATE命令,实现它的任务就全权交给了写这个设
备驱动的程序员了。根据常规约定,⼀个⽤户程序调⽤⼀个特定的ioctl命令如下:ioctl(sockid, SIOCDEVPRIVATE, (char *) &ifr); 这⾥ifr是⼀个ifreq结构体变量,它⽤⼀个和这个设备联系的接⼝名称来填充ifr的ifr NAME域,⽐如前述的⽆线⽹卡接⼝名称为eth1。
不失⼀般性,⼀个⽤户程序将同样要与内核交换命令参数和操作结果,⽽这些已经通过了对域ifr.ifr_data的填充⽽做到了。⽐如这个⽹卡的信号强度信息被返回到这个域当中。LINUX 源代码已经包含了两个特殊设备:de4x5和ewrk3,他们定义和实现了特殊的ioctl命令。这些设备的源代码在以下的⽂件中:de4x5.h, de4x5.c, ewrk3.h, ewrk3.c。两个设备都为在⽤户空间和驱动间交换数据定义了他们⾃⼰的私有结构,在ioctl之前,⽤户程序需填充需要的数据并且将 ifr.ifr_data指向这个结构体。
在进⼊代码前,让我们跟踪⼀下处理ioctl系统调⽤的若⼲步骤。所有接⼝类型的ioctl请求都导致dev_ioctl()被调⽤,这个ioctl仅仅是个包装,⼤部分的真实的操作留给了dev_ifsioc(),这个 dev_ioctl()要做的唯⼀⼀件事情就是检查调⽤过程是否拥有合适的许可去核发这个命令,然后dev_ifsioc()⾸先要做的事情之⼀就是得到和名字域ifr.ifr_name中所对应的设备结构,这在⼀个很⼤的switch语块的代码后实现。
SIOCDEVPRIVATE命令和SIOCDEVPRIVATE+15的命令参数全部交给了默认操作,这些都是switch的分⽀语句。这⾥发⽣的是,内核检查是否⼀个设备特殊的ioctl的回调已经在设备结构中被设置,这个回调是保持在设备结构中的⼀个函数指针。如果回调已经被设置了,内核就会调⽤它。
所以,为了实现⼀个特殊的ioctl,需要做的就是写⼀个特殊ioctl的回调,然后让device结构中的do_ioctl域指向它。对于EWK3设备,这个函数叫做ewrk3_ioctl(),对应的设备结构在ewrk3_init()中被初始化,ewrk3_ioctl()的代码清晰的展⽰了ifr.ifr_data的作⽤,是为了在⽤户程序和驱动之间交换信息。注意,内存的这个区域有双⽅向交换数据的作⽤,例如,在ewrk3驱动代码中ifr.ifr_data最初的2个字节被⽤做向驱动传递预想要的动作。同样第五个字节指向的缓冲区⽤于交换其他的信息。
当你浏览ewrk3_ioctl()代码的时候,记住在⼀个应⽤中⽤户空间的指令是⽆法访问内核空间的,由于这个原因,内核给驱动编写⼈员提供了2个特殊的步骤。他们是memcpy_tofs()和memcpy_fromfs()。内核⾥的做法是⽤memcpy_tofs() 拷贝内核数据到⽤户空间,类似的memcpy_fromfs()也是这样的,只是他拷贝⽤户数据到内核空间。这些程序步骤是由于调⽤verify_area()⽽被执⾏的,⽬的是确认数据访问不会违法。同样需要记住printk()的⽤法是打印调试信息,这个函数和printf()很相像,但是它不能处理浮点数据。printf()函数在内核中是不能被使⽤的。由printk()产⽣的输出被转储到了⼀个⽬录./usr/adm /messages。
******************************************************************************************************************************************
linux系统ioctl使⽤⽰例
These were writed and collected by kf701,
you can use and modify them but NO WARRANTY.
Contact with me :
程序1:检测接⼝的inet_addr, netmask, broad_addr
程序2:检查接⼝的物理连接是否正常
程序3:测试物理连接
程序4:调节⾳量
***************************程序1****************************************
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>函数printf作用
#include <sys/ioctl.h>
#include <net/if.h>
static void usage()
{
printf("usage : ipconfig interface \n");
exit(0);
}
int main(int argc,char **argv)
{
struct sockaddr_in *addr;
struct ifreq ifr;
char *name,*address;
int sockfd;
if(argc != 2)  usage();
else  name = argv[1];
sockfd = socket(AF_INET,SOCK_DGRAM,0);
strncpy(ifr.ifr_name,name,IFNAMSIZ-1);
if(ioctl(sockfd,SIOCGIFADDR,&ifr) == -1)
perror("ioctl error"),exit(1);
addr = (struct sockaddr_in *)&(ifr.ifr_addr);
address = inet_ntoa(addr->sin_addr);
printf("inet addr: %s ",address);
if(ioctl(sockfd,SIOCGIFBRDADDR,&ifr) == -1)
perror("ioctl error"),exit(1);
addr = (struct sockaddr_in *)&ifr.ifr_broadaddr;
address = inet_ntoa(addr->sin_addr);
printf("broad addr: %s ",address);
if(ioctl(sockfd,SIOCGIFNETMASK,&ifr) == -1)
perror("ioctl error"),exit(1);
addr = (struct sockaddr_in *)&ifr.ifr_addr;
address = inet_ntoa(addr->sin_addr);
printf("inet mask: %s ",address);
printf("\n");
exit(0);
}
******************************** 程序2***************************************************** #include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <stdlib.h>
#include <unistd.h>
typedef unsigned short u16;
typedef unsigned int u32;
typedef unsigned char u8;
#include <linux/ethtool.h>
#include <linux/sockios.h>
int detect_mii(int skfd, char *ifname)
{
struct ifreq ifr;
u16 *data, mii_val;
unsigned phy_id;
/* Get the vitals from the interface. */
strncpy(ifr.ifr_name, ifname, IFNAMSIZ);
if (ioctl(skfd, SIOCGMIIPHY, &ifr) < 0)
{
fprintf(stderr, "SIOCGMIIPHY on %s failed: %s\n", ifname, strerror(errno));
(void) close(skfd);
return 2;
}
data = (u16 *)(&ifr.ifr_data);
phy_id = data[0];
data[1] = 1;
if (ioctl(skfd, SIOCGMIIREG, &ifr) < 0)
{
fprintf(stderr, "SIOCGMIIREG on %s failed: %s\n", ifr.ifr_name, strerror(errno));        return 2;
}
mii_val = data[3];
return(((mii_val & 0x0016) == 0x0004) ? 0 : 1);
}
int detect_ethtool(int skfd, char *ifname)
{
struct ifreq ifr;
struct ethtool_value edata;
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name)-1);
ifr.ifr_data = (char *) &edata;
if (ioctl(skfd, SIOCETHTOOL, &ifr) == -1)
{
printf("ETHTOOL_GLINK failed: %s\n", strerror(errno));
return 2;
}
return (edata.data ? 0 : 1);
}
int main(int argc, char **argv)
{
int skfd = -1;
char *ifname;
int retval;
if( argv[1] )  ifname = argv[1];
else  ifname = "eth0";
/* Open a socket. */
if (( skfd = socket( AF_INET, SOCK_DGRAM, 0 ) ) < 0 )
{
printf("socket error\n");
exit(-1);
}
retval = detect_ethtool(skfd, ifname);
if (retval == 2)
retval = detect_mii(skfd, ifname);
close(skfd);
if (retval == 2)
printf("Could not determine status\n");
if (retval == 1)
printf("Link down\n");
if (retval == 0)
printf("Link up\n");
return retval;
}