#include<unistd.h>
int ioctl( int fd, int request, .../* void *arg */ ); 返回0——成功, -1——出错
第一个参数 fd 指示某个文件描述符(当然也包括 套接口描述符)
第二个参数 request 指示要ioctl执行的操作
第三个参数 总是某种指针,具体的指向类型依赖于 request 参数
我们可以把和网络相关的请求(request)划分为6 类:
套接口操作
文件操作
接口操作
ARP 高速缓存操作
路由表操作
流系统
下表列出了网络相关ioctl 请求的request 参数以及arg 地址必须指向的数据类型:
类别 request 说明 (第三个参数)数据类型
SIOCATMARK 是否位于带外标记 int
套接口 SIOCSPGRP 设置套接口的进程ID或组ID int
SIOCGPGRP 获取套接口的进程ID或组ID int
文件 FIONBIO 设置/清除非阻塞IO标识 int
FIOASYNC 设置/清除信号驱动异步IO标识 int
FIONREAD 获取接收缓冲区中的字节数 int
……
SIOCGIFCONF 获取所有接口的清单 struct ifconf
接口 SIOCSIFADDR 设置接口的ip地址 struct ifreq
SIOCGIFADDR 获取接口地址 struct ifreq
SIOCSIFFLAGS 设置接口标识 struct ifreq
SIOCGIFFLAGS 获取接口标识 struct ifreq
SIOCSIFDSTADDR 设置点到点地址 struct ifreq
SIOCGIFDSTADDR 获取点到点地址 struct ifreq
SICSIFBRDADDR 设置广播地址 struct ifreq
SICGIFBRDADDR 获取广播地址 struct ifreq
……
SIOCSARP 创建/修改ARP表项 struct arpreq
ARP SIOCGARP 获取ARP表项 struct arpreq
SIOCDARP 删除ARP表项 struct arpreq
路由 ……
(有很多request请求没有列出,而且不同的系统所提供的request也有所不同,比如linux就不提供SIOCGSIZIFCONF请求)
上面的表中,用ioctl执行接口操作的请求时,要用到结构体 struct ifconf 与 struct ifreq
struct ifconf{
int ifc_len; // 缓冲区ifcu_buf的大小
union{
caddr_t ifcu_buf; // 其实就是char *类型。
struct ifreq *ifcu_req; // 为ifconf分配空间时我们用ifcu_buf指针;当要取得或设置该缓冲区中的ifreq类型时,则用这个指针
}ifc_ifcu;
};
#define ifc_buf ifc_ifcu.ifcu_buf //buffer address
#define ifc_req ifc_ifcu.ifcu_req //array of structures returned
struct ifreq
{
char ifrn_name[IFNAMSIZ]; /* if name, e.g. "eth0" */
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]; /* Just fits the size */
char ifru_newname[IFNAMSIZ];
void * ifru_data;
struct if_settings ifru_settings;
} ifr_ifru;
};
#define ifr_name ifr_ifrn.ifrn_name /* interface name */
#define ifr_hwaddr ifr_ifru.ifru_hwaddr /* MAC address */
#define ifr_addr ifr_ifru.ifru_addr /* address */
#define ifr_dstaddr ifr_ifru.ifru_dstaddr /* other end of p-p lnk */
#define ifr_broadaddr ifr_ifru.ifru_broadaddr /* broadcast address */
#define ifr_netmask ifr_ifru.ifru_netmask /* interface net mask */
#define ifr_flags ifr_ifru.ifru_flags /* flags */
#define ifr_metric ifr_ifru.ifru_ivalue /* metric */
#define ifr_mtu ifr_ifru.ifru_mtu /* mtu */
#define ifr_map ifr_ifru.ifru_map /* device map */
#define ifr_slave ifr_ifru.ifru_slave /* slave device */
#define ifr_data ifr_ifru.ifru_data /* for use by interface */
#define ifr_ifindex ifr_ifru.ifru_ivalue /* interface index */
#define ifr_bandwidth ifr_ifru.ifru_ivalue /* link bandwidth */
#define ifr_qlen ifr_ifru.ifru_ivalue /* Queue length */
#define ifr_newname ifr_ifru.ifru_newname /* New name */
#define ifr_settings ifr_ifru.ifru_settings /* Device/proto settings*/
ifconf 结构包含了 ifreq 结构指针, 在使用ifconf之前,我们得先为其ifcu_buf指针(或者说ifcu_req指针)分配缓冲区
其他不知道该怎么说。
贴代码吧。 一个用ioctl实现的类似ifconfig命令的小程序。
#include <stdio.h>//printf()
#include <unistd.h>//ioctl()
#include <sys/ioctl.h>//ioctl
#include <sys/socket.h>//socket()
#include <net/if.h>//struct ifconf{} & struct ifreq{}
#include <string.h>//strcpy()
#include <arpa/inet.h>//inet_ntoa()
#include <stdlib.h>//malloc() & free()
int print_if_addr(int fd, char *interface_name);//打印接口的ip地址
int print_if_mac(int fd, char *interface_name);//打印接口的mac地址
int print_if_broadaddr(int fd, char *interface_name);//打印接口的广播地址
int print_if_mask(int fd, char *interface_name);//打印接口的掩码
int print_if_mtu(int fd, char *interface_name);//打印接口的mtu
int print_all_interface();//打印所有接口的基本信息
int print_if_addr6(char *interface_name);//打印接口的ipv6地址
int print_interface_info(char *interface_name);//打印接口的以上所有信息
int set_if_up(char *interface_name);//启动接口
int set_if_down(char *interface_name);//关闭接口
int set_if_ip(char *interface_name, char *ip_str);//设置接口的ip地址
void usage();//打印该程序的使用手册
int main(int argc, char **argv)
{
int sockfd;
printf("\n**********funway:用ioctl函数来实现ifconfig命令的效果**********\n");
switch(argc)
{
case 1:
print_all_interface();
break;
case 2:
print_interface_info(argv[1]);
break;
case 3:
if(strcmp(argv[2], "up") == 0)
set_if_up(argv[1]);
else if(strcmp(argv[2], "down") == 0)
set_if_down(argv[1]);
else
set_if_ip(argv[1], argv[2]);
break;
default:
usage();
break;
}
return 0;
}
void usage()
{
printf("usage: ./myifconfig [interface [down|up|ip]]\n");
}
int print_if_addr(int fd, char *if_name)
{
struct sockaddr_in *ip;
struct ifreq ifr;
strcpy(ifr.ifr_name, if_name);
if(ioctl(fd, SIOCGIFADDR, &ifr) < 0)
{
perror("ioctl SIOCGIFADDR error");
return -1;
}
ip = (struct sockaddr_in *)&ifr.ifr_addr;//获得ipv4地址
printf("IP: %s\n", inet_ntoa(ip->sin_addr));//将ipv4地址转换为主机字节序的字符串并输出
return 0;
}
int print_if_broadaddr(int fd, char *if_name)
{
struct sockaddr_in *ip;
struct ifreq ifr;
strcpy(ifr.ifr_name, if_name);
if(ioctl(fd, SIOCGIFBRDADDR, &ifr) < 0)
{
perror("ioctl SIOCGIFBRDADDR error");
return -1;
}
ip = (struct sockaddr_in *)&ifr.ifr_broadaddr;//获得广播地址
printf("Broadcast: %s\n", inet_ntoa(ip->sin_addr));
return 0;
}
int print_if_mask(int fd, char *if_name)
{
struct sockaddr_in *ip;
struct ifreq ifr;
strcpy(ifr.ifr_name, if_name);
if(ioctl(fd, SIOCGIFNETMASK, &ifr) < 0)
{
perror("ioctl SIOCGIFNETMASK error");
return -1;
}
ip = (struct sockaddr_in *)&ifr.ifr_ifru.ifru_netmask;//获得子网掩码。注意!我们仍放在struct aockaddr_in结构中返回
printf("Mask: %s\n", inet_ntoa(ip->sin_addr));
return 0;
}
int print_if_mac(int fd, char *if_name)
{
unsigned char *p;//注意! 这里要用unsigned char,而不是char!因为char要对[1xxx xxxx]这样的数进行补码运算的。
//但我们等下要打印的mac地址是不需要符号的数值
struct ifreq ifr;
strcpy(ifr.ifr_name, if_name);
if(ioctl(fd, SIOCGIFHWADDR, &ifr) < 0)
{
perror("ioctl SIOCGIFHWADDR error");
return -1;
}
p = (char *)&ifr.ifr_ifru.ifru_hwaddr.sa_data[0];//获得接口的MAC地址,用字符串指针返回
printf("MAC: %02x:%02x:%02x:%02x:%02x:%02x\n", *p, *(p+1), *(p+2), *(p+3), *(p+4), *(p+5));
//printf(" MAC:%02x:%02x:%02x:%02x:%02x:%02x\n", *p++, *p++, *p++, *p++, *p++, *p++);
//这么写会导致输出为倒序。这并不是p指针有什么问题,不信你可以用
// for(;;)
//printf(p++);
//来试验就是正确的,我猜倒序的原因是编译器的优化问题吧
return 0;
}
int print_if_mtu(int fd, char *if_name)
{
unsigned int mtu;
struct ifreq ifr;
strcpy(ifr.ifr_name, if_name);
if(ioctl(fd, SIOCGIFMTU, &ifr) < 0)
{
perror("ioctl SIOCGIFMTU error");
return -1;
}
mtu = ifr.ifr_ifru.ifru_mtu;//获得子网掩码。注意!我们仍放在struct aockaddr_in结构中返回
printf("MTU: %d\n", mtu);
return 0;
}
int print_if_addr6(char *if_name)
{
unsigned int mtu;
struct ifreq ifr;
int sockfd;
if((sockfd = socket(AF_INET6, SOCK_DGRAM, 0)) < 0)
{
perror("Socket error");
return -1;
}// 创建用来检查网络接口的套接字
/*strcpy(ifr.ifr_name, if_name);
if(ioctl(fd, SIOCGIFMTU, &ifr) < 0)
{
perror("ioctl SIOCGIFMTU error");
return -1;
}
mtu = ifr.ifr_ifru.ifru_mtu;//获得子网掩码。注意!我们仍放在struct aockaddr_in结构中返回
printf("ipv6: %d\n", mtu);
*/
//未写完,不知道怎么获得ipv6地址。。。
return 0;
}
int print_all_interface()
{
struct ifconf ifc;
struct ifreq *ifr_p;
int sockfd, len, old_len = 0, i;
char *buf;
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("Socket error");
return -1;
}// 创建用来检查网络接口的套接字
len = 10 * sizeof(struct ifreq);
for( ; ; )
{
if((buf = malloc(len)) == NULL)
{
perror("malloc error");
return -1;
}
ifc.ifc_len = len;
ifc.ifc_buf = buf;
if(ioctl(sockfd, SIOCGIFCONF, &ifc) < 0)
{
perror("ioctl SIOCGIFCONF error");
return -1;
}
if(ifc.ifc_len == old_len)
break;
old_len = ifc.ifc_len;
len += 10 * sizeof(struct ifreq);
free(buf);
}
printf("we have %d interfaces\n", ifc.ifc_len / sizeof(struct ifreq));
for(i = 0; i < ifc.ifc_len / sizeof(struct ifreq); i++)
{
ifr_p = &ifc.ifc_req[i];
printf("\ninterface [%s]:\n", ifr_p->ifr_name);
print_if_addr(sockfd, ifr_p->ifr_name);
print_if_broadaddr(sockfd, ifr_p->ifr_name);
print_if_mask(sockfd, ifr_p->ifr_name);
print_if_mac(sockfd, ifr_p->ifr_name);
print_if_mtu(sockfd, ifr_p->ifr_name);
}
close(sockfd);
return 0;
}
int print_interface_info(char *if_name)
{
int sockfd;
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("Socket error");
return -1;
}// 创建用来检查网络接口的套接字
printf("%s:\n", if_name);
print_if_addr(sockfd, if_name);
print_if_broadaddr(sockfd, if_name);
print_if_mask(sockfd, if_name);
print_if_mac(sockfd, if_name);
print_if_mtu(sockfd, if_name);
close(sockfd);
return 0;
}
int set_if_up(char *if_name)//启动接口
{
struct ifreq ifr;
int sockfd;
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("Socket error");
return -1;
}// 创建用来检查网络接口的套接字
strcpy(ifr.ifr_name, if_name);
if(ioctl(sockfd, SIOCGIFFLAGS, &ifr) < 0)
{
perror("ioctl SIOCGIFFLAGS error");
return -1;
}
ifr.ifr_flags |= IFF_UP;
if(ioctl(sockfd, SIOCSIFFLAGS, &ifr) < 0)
{
perror("ioctl SIOCSIFFLAGS error");
return -1;
}
return 0;
}
int set_if_down(char *if_name)//关闭接口
{
struct ifreq ifr;
int sockfd;
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("Socket error");
return -1;
}// 创建用来检查网络接口的套接字
strcpy(ifr.ifr_name, if_name);
if(ioctl(sockfd, SIOCGIFFLAGS, &ifr) < 0)
{
perror("ioctl SIOCGIFFLAGS error");
return -1;
}
ifr.ifr_flags &= ~IFF_UP;//将IIF_UP取反后与原来的标志进行 与运算。
if(ioctl(sockfd, SIOCSIFFLAGS, &ifr) < 0)
{
perror("ioctl SIOCSIFFLAGS error");
return -1;
}
return 0;
}
int set_if_ip(char *if_name, char *ip_str)//设置接口的ip地址
{
struct ifreq ifr;
struct sockaddr_in ip_addr;
int sockfd;
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("Socket error");
return -1;
}// 创建用来检查网络接口的套接字
ip_addr.sin_family = AF_INET;
if(inet_pton(AF_INET, ip_str, &ip_addr.sin_addr) < 1)
{
perror("error ipv4 addr:");
return -1;
}
strcpy(ifr.ifr_name, if_name);
memcpy(&ifr.ifr_addr, &ip_addr, sizeof(struct sockaddr_in));
if(ioctl(sockfd, SIOCSIFADDR, &ifr) < 0)
{
perror("ioctl SIOCSIFADDR error");
return -1;
}
return 0;
}
ioctl在驱动中:
设备驱动程序的一个基本功能就是管理和控制设备,同时为用户应用程序提供管理和控制设备的接口。我们前面的“Hello World”驱动程序已经可以提供读写功能了,在这里我们将扩展我们的驱动以支持设备控制接口,在Linux中这个接口是通过ioctl函数来实现的。
设备控制接口(ioctl 函数)
回想一下我们在
字符设备驱动
中介绍的struct file_operations 结构,这里我们将介绍一个新的方法:
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); |
这是驱动程序设备控制接口函数(ioctl函数)的内核原型定义,struct inode * 和struct file* 描述了操作的文件,unsigned int 描述了ioctl命令号,这是一个重要的参数,我们稍后会对它做详细介绍。最后一个参数是unsigned long数据类型,描述了ioctl命令可能带有的参数,它可能是一个整数或指针数据。
ioctl命令号是这个函数中最重要的参数,它描述的ioctl要处理的命令。Linux中使用一个32位的数据来编码ioctl命令,它包含四个部分:dir:type:nr:size。
代表数据传输的方向,占2位,可以是_IOC_NONE(无数据传输,0U),_IOC_WRITE(向设备写数据,1U)或_IOC_READ(从设备读数据,2U)或他们的逻辑或组合,当然只有_IOC_WRITE和_IOC_READ的逻辑或才有意义。
描述了ioctl命令的类型,8位。每种设备或系统都可以指定自己的一个类型号,ioctl用这个类型来表示ioctl命令所属的设备或驱动。一般用ASCII码字符来表示,如 'a'。
ioctl命令序号,一般8位。对于一个指定的设备驱动,可以对它的ioctl命令做一个顺序编码,一般从零开始,这个编码就是ioctl命令的序号。
ioctl命令的参数大小,一般14位。ioctl命令号的这个数据成员不是强制使用的,你可以不使用它,但是我们建议你指定这个数据成员,通过它我们可以检查用户空间数据的大小以避免错误的数据操作,也可以实现兼容旧版本的ioctl命令。
我们可以自己来直接指定一个ioctl命令号,它可能仅仅是一个整数集,但Linux中的ioctl命令号都是有特定含义的,因此通常我们不推荐这么做。其实Linux内核已经提供了相应的宏来自动生成ioctl命令号:
_IO(type,nr) _IOR(type,nr,size) _IOW(type,nr,size) _IOWR(type,nr,size) |
宏_IO用于无数据传输,宏_IOR用于从设备读数据,宏 _IOW用于向设备写数据,宏_IOWR用于同时有读写数据的IOCTL命令。相对的,Linux内核也提供了相应的宏来从ioctl命令号种解码相应的域值:
_IOC_DIR(nr) _IOC_TYPE(nr) _IOC_NR(nr) _IOC_SIZE(nr) |
这些宏都定义在<asm/ioctl.h>头文件中(一般在<asm-generic.h>头文件中)。一般在使用中,先指定各个IOCTL命令的顺序编号(一般从0开始),然后根据使用的环境用这些宏来自动生成IOCTL命令号,在后面的例子中你可以了解实际的使用场景。
ioctl函数的返回值是一个整数类型的值,如果命令执行成功,ioctl返回零,如果出现错误,ioctl函数应该返回一个负值。这个负值会作为errno值反馈给调用此ioctl的用户空间程序。关于返回值的具体含义,请参考<linux/errno.h>和<asm/errno.h>头文件。
这里有必要说明一下ioctl命令的参数,因为它很容易犯错误。如果ioctl命令参数仅仅是一个整数,那么事情很简单了,我们可以在ioctl函数中直接使用它。但如果它是一个指针数据,那么使用上就要小心了。首先要说明这个参数是有用户空间的程序传递过来的,因此这个指针指向的地址是用户空间地址,在Linux中,用户空间地址是一个虚拟地址,在内核空间是无法直接使用它的。为了解决在内核空间使用用户空间地址的数据,Linux内核提供了以下函数,它们用于在内核空间访问用户空间的数据,定义在<asm/uaccess.h>头文件中:
unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n); unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n); |
copy_from_user和copy_to_user一般用于复杂的或大数据交换,对于简单的数据类型,如int或char,内核提供了简单的宏来实现这个功能:
#define get_user(x,ptr) #define put_user(x,ptr) |
其中,x是内核空间的简单数据类型地址,ptr是用户空间地址指针。
我们需要牢记:在内核中是无法直接访问用户空间地址数据的。因此凡是从用户空间传递过来的指针数据,务必使用内核提供的函数来访问它们。
这里有必要再一次强调的是,在内核模块或驱动程序的编写中,我们强烈建议你使用内核提供的接口来生成并操作ioctl命令号,这样可以对命令号赋予特定的含义,使我们的程序更加的健壮;另一方面也可以提高程序的可移植性。
举例
好了,是时候举个例子了。我们将扩展我们的helloworld驱动添加ioctl函数。
首先,我们添加一个头文件来定义ioctl接口需要用到的数据(hello.h):
#ifndef _HELLO_H #define _HELLO_H #include <asm/ioctl.h> #define MAXBUF 20 typedef struct _buf_data{ int size; char data [MAXBUF]; }buf_data; #define HELLO_IOCTL_NR_BASE 0 #define HELLO_IOCTL_NR_SET_DATA (HELLO_IOCTL_NR_BASE + 1) #define HELLO_IOCTL_NR_MAX (HELLO_IOCTL_NR_GET_BUFF + 1) #define HELLO_IOCTL_SET_DATA _IOR('h', HELLO_IOCTL_NR_SET_DATA, buf_data*) #endif |
然后为我们的驱动程序添加ioctl接口hello_ioctl,并实现这个函数:
static int hello_ioctl (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg) { int cmd_nr; int err; buf_data buff; err = 0; cmd_nr = _IOC_NR (cmd); switch (cmd_nr){ case HELLO_IOCTL_NR_SET_DATA: if (copy_from_user(&buff, (unsigned char *)arg, sizeof(buf_data))) { err = -ENOMEM; goto error; } memset(hello_buf, 0, sizeof(hello_buf)); memcpy(hello_buf, buff.data, buff.size); break; default: printk("hello_ioctl: Unknown ioctl command (%d)\n", cmd); break; } error: return err; } static struct file_operations hello_fops = { .read = hello_read, .write = hello_write, .open = hello_open, .ioctl = hello_ioctl, .release = hello_release, }; |
后记
到这里我们已经向您展示了Linux内核驱动程序的设备控制接口(ioctl接口),详细的介绍了它的使用,并给出了一个实际的例子,尽管它很简单,但已经足够了。到这里你可以写出一个标准的Linux驱动程序了。不过这里还有个问题,那就是我们不得不从/proc/devices文件里读取设备号然后手动创建设备节点。我们是否可以让系统自动的创建这个设备节点文件呢?当然可以。不过在那之前,我们必须深入了解Linux的设备驱动模型。后面的章节我们就详细的介绍Linux的设备驱动模型及Hotplug机制。