从ioctl 这个名称上看,它是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等, 但实际上ioctl所处理的对象并不限制是真正的I/O设备,还可以是其它任何一个内核设备.ioctl以系统调用的形式提供了一条用户与内核交互的便捷途径。当前一些宽带计费网关、防火墙系统均利用Ioctl与内核良好的通信互动特点支持用户对基于内核模块的软件系统的控制.本文针对i386平台下的 ioctl内核网络源代码控制框架进行剖析解释,在文章最后列举一个实例,通过编程实践展示如何通过ioctl控制函数实现自定义的功能的控制,使读者可以对ioctl实现原理有一个全面的认识,本文只对ioctl实现流程框架做一定的叙述,并不会深入到具体的控制函数。为了更好的阅读本文,要求读者对 Linux 下的网络编程有一定的了解。
本文约定:
1、以下内容如果没有特殊说明,均参照linux内核2.4.0版本
2、“->”箭头符表示函数调用关系,如sys_socket->sock_map_fd表示sys_socket函数调用的sock_map_fd函数。
3、第五节的实践是在RedHat9上实现,基于2.4.20内核,但本文所述在2.4内核下都适用。
二、用户空间ioctl控制函数调用形式
通过man 2 ioctl命令查看ioctl函数的调用形式类似如下:
#include <sys/ioctl.h>
int ioctl(int d, int request, ...);
其中d就是用户程序打开设备时使用open函数返回的文件描述符,request就是用户程序对设备的控制命令,至于后面的省略号,则是一些补充参数,一般最多一个,有或没有是和request的意义相关的,详情请参考man 2 ioctl_list以了解更多。ioctl函数是文件结构中的一个属性分量,就是说如果驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数控制设备的I/O通道或其它一些自己想要控制且设备支持的功能。
三、内核主要函数调用框架
内核实现ioctl()函数的是sys_ioctl(),在内核中主要调用框架图如下,它清晰地给我们展示ioctl的控制传递框架,我们接下来的内容将根据此图向大家做详细的解释:
根据前面的图示,我们从入口函数sys_ioctl开始分析:
4.1、入口函数:sys_ioctl
以下源码在fs/ioctl.c中,其中删除了部分与网络控制关系不大的代码:
asmlinkage long sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg)
{
…//根据fd获取文件结构(struct file)
lock_kernel();
switch (cmd) {
case FIOCLEX://对文件设置专用标志,通知内核自动关闭打开的文件
…
case FIONCLEX://与FIOCLEX标志相反,清除专用标志
…
case FIONBIO://将文件操作设置成阻塞/非阻塞
…
case FIOASYNC:// 将文件操作设置成同步/异步IO
… //以上省略的代码是关于具体的磁盘文件系统的控制处理,
//关于socket的阻塞或非阻塞等设置很简单,有兴趣的读者直接阅读源码吧
default: //文件其它部分的处理被放在了default部分
error = -ENOTTY;
if (S_ISREG(filp->f_dentry->d_inode->i_mode)) //普通文件
error = file_ioctl(filp, cmd, arg); //
else if (filp->f_op && filp->f_op->ioctl) //socket控制在此处理
error = filp->f_op->ioctl(filp->f_dentry->d_inode, filp, cmd, arg);
}
unlock_kernel();
fput(filp);
out:
return error;
}
注意上面蓝色字体部分,即为调用网络部分的代码入口。大家注意在default情况下,有个S_ISREG宏对文件类型作判断,其定义在include/linux/stat.h中:
#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) //符号连接文件
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) //普通文件
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) //目录文件
#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) //字符设备文件
#define S_ISBLK(m) (((m) & S_IFMT) == S_IFBLK) //块设备文件
#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO) //管道文件
#define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK) //socket套接字文件
因为linux内核把socket套接字当作文件来处理,内核在创建socket套接字时,为套接字分配文件id以及生成与id对应的文件节点,节点的 i_mode域是代表文件类型的位域标志字段,所以内核定义了上述宏来简化判断操作。由于套接字文件不属于普通文件之列,所以程序直接执行蓝色字体部分。
4.2、入口函数跳转
我们来看一下filp->f_op->ioctl函数指针指向了什么函数,可以参考net/socket.c文件中的sys_socket->sock_map_fd函数中的一行代码(蓝色部分代码):
static int sock_map_fd(struct socket *sock)
{
…
sock->file = file;
file->f_op = sock->inode->i_fop = &socket_file_ops;
file->f_mode = 3;
file->f_flags = O_RDWR;
file->f_pos = 0;
…
}
内核在用户创建socket套接字时就将此套接字的文件操作函数指针初始化了。从上面的代码我们可以看到,filp->f_op以及文件对应的 socket节点的i_fop指针都被赋值为指向socket_file_ops结构,所以我们来看看内核是如何实现这个控制过程的转移的。还是在内核的 net/socket.c文件中,定义了socket_file_ops结构如下:
static struct file_operations socket_file_ops = {
llseek: sock_lseek,
read: sock_read,
write: sock_write,
poll: sock_poll,
ioctl: sock_ioctl,
mmap: sock_mmap,
open: sock_no_open, /* special open code to disallow open via /proc */
release: sock_close,
fasync: sock_fasync,
readv: sock_readv,
writev: sock_writev
};
从上面的代码来看,这个结构定义了socket描述字的文件操作函数,如对描述字调用read函数读数据时最终将访问sock_read函数,对描述字调用 write函数读数据时最终将访问sock_write函数,等等。而对ioctl的访问最终将转化为调用sock_ioctl函数,看到此处我们明白了,filp->f_op->ioctl(filp->f_dentry->d_inode, filp, cmd, arg)调用实质上转化为对sock_ioctl函数的调用。
4.3、sock_ioctl函数
sock_ioctl函数依然在net/socket.c文件中,列出如下:
int sock_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
struct socket *sock;
int err;
unlock_kernel();
sock = socki_lookup(inode);
err = sock->ops->ioctl(sock, cmd, arg);
lock_kernel();
return err;
}
此处函数引入inode参数实质是通过节点找到套接字对应的socket结构,通过socket的struct proto_ops类型的字段ops执行具体的控制操作(即sock->ops->ioctl(sock, cmd, arg)),函数socki_lookup也在文件net/socket.c中,列出如下:
extern __inline__ struct socket *socki_lookup(struct inode *inode)
{
return &inode->u.socket_i;
}
写到这大家可能要问为什么不直接在filp->f_op->ioctl函数指针指向的函数里面执行ioctl控制操作而要做两次跳转呢?其实这与linux良好的设计规范和业务支持的实际情况都有关系,第一次跳转是转入套接字单独处理,因为内核中网络部分是非常重要的,可以与文件系统相提并论,将网络部分独立出来处理在设计思路上更清晰;另外,linux内核支持不同层次、类型的套接字,如ipv4、ipv6套接字以及sock_raw原始套接字,对于这些套接字的处理有一定的相似性,又有其不同的地方。所以引入第二次跳转的目的也即在此,以支持对不同的协议类型的套接字进行不同控制,详情见下面小节的介绍。
4.4、二次跳转
闲话少说,步入正题。接下来我们看看sock->ops->ioctl函数指针调用了什么函数,首先看看 sock变量的结构类型struct socket,大家要多注意这个结构,在后面我们也列出了相关结构相互引用图中涉及到的这个结构的几个字段,以加深大家的印象.结构的源代码在include/linux/Net.h文件中:
struct socket
{
socket_state state;
unsigned long flags;
struct proto_ops *ops;
struct inode *inode;
struct fasync_struct *fasync_list; /* Asynchronous wake up list */
struct file *file; /* File back pointer for gc */
struct sock *sk;
wait_queue_head_t wait;
…
};
套接字就是通过结构中ops指针来执行具体的ioctl控制函数的。struct proto_ops定义在同样的头文件中:
struct proto_ops {
int family;
int (*release) (struct socket *sock);
int (*bind) (struct socket *sock, struct sockaddr *umyaddr, int sockaddr_len);
int (*connect) (struct socket *sock, struct sockaddr *uservaddr, int sockaddr_len, int flags);
int (*socketpair) (struct socket *sock1, struct socket *sock2);
int (*accept) (struct socket *sock, struct socket *newsock, int flags);
int (*getname) (struct socket *sock, struct sockaddr *uaddr, int *usockaddr_len, int peer);
unsigned int (*poll) (struct file *file, struct socket *sock, struct poll_table_struct *wait);
int (*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg);
int (*listen) (struct socket *sock, int len);
int (*shutdown) (struct socket *sock, int flags);
int (*setsockopt) (struct socket *sock, int level, int optname, char *optval, int optlen);
int (*getsockopt) (struct socket *sock, int level, int optname, char *optval, int *optlen);
int (*sendmsg) (struct socket *sock, struct msghdr *m, int total_len, struct scm_cookie *scm);
int (*recvmsg) (struct socket *sock, struct msghdr *m, int total_len, int flags, struct scm_cookie *scm);
int (*mmap) (struct file *file, struct socket *sock, struct vm_area_struct * vma);
};
补充一下基础知识,一个套接字接口在逻辑上有三个要素:网域,类型和规程(协议).
网域:表明套接字接口用于哪一中网络或这说哪一族网络规程.就是我们通常说的地址族(family),常见的有AF_UNIX/AF_INET/AF_X25/AF_IPX等待.
类型:表明通讯中所遵循的模式,主要有两种模式:”有连接”和”无连接”,对应到以太网就是SOCK_STREAM和SOCK_DGRAM两种.
规程:具体的网络协议.通常,网域和类型基本就能够确定使用的规程了.
这里的proto_ops结构就是通过不同的实例来支持具体的网域的不同类型、规程所使用的通信函数,每个网域都有多种类型、多种规程,所以也有多个 proto_ops实例,给这个实例赋值具体规程的处理函数,如ipv4的有连接和无连接实例所指定的控制函数都是inet_ioctl(如果处理不同也可以指向不同的控制函数),这样可以使具体的控制操作转向具体的处理,细节实现我们下一小节介绍.
构造内核时,内核会初始化网络地址族,即初始化net_families[NRPORO]全局量, 这是一个静态指针数组。每个网域地址族的初始化函数都由其中一个元素来表征,例如,“INET”和它的初始程序地址分别是PF_INET(等同于 AF_INET)和inet_create。当套接口启动时被初始化时,要调用每一网域初始化程序,为具体的类型指定处理函数,内核初始化网域地址族后net_families[NRPORO]变量的相关字段取值状态示意图如下:
对IPV4地址族来说,这个初始化函数就是inet_create,其代码在net/ipv4/af_inet.c中:
static int inet_create(struct socket *sock, int protocol)
{
…
switch (sock->type) {
case SOCK_STREAM:
if (protocol && protocol != IPPROTO_TCP) //类型与规程检测
goto free_and_noproto;
protocol = IPPROTO_TCP;
prot = &tcp_prot;
sock->ops = &inet_stream_ops; //此处指定函数跳转表
break;
case SOCK_SEQPACKET:
goto free_and_badtype;
case SOCK_DGRAM:
if (protocol && protocol != IPPROTO_UDP)
goto free_and_noproto;
protocol = IPPROTO_UDP;
sk->no_check = UDP_CSUM_DEFAULT;
prot=&udp_prot;
sock->ops = &inet_dgram_ops; //此处指定函数跳转表
break;
case SOCK_RAW:
if (!capable(CAP_NET_RAW)) //检验是否有创建原始套接字的权限
…
sock->ops = &inet_dgram_ops;//
if (protocol == IPPROTO_RAW)
sk->protinfo.af_inet.hdrincl = 1;
break;
default:
goto free_and_badtype;
}
…
}
从上面的代码可以看出:已注册的网域的类型所对应的操作被存在socket结构的ops 指针中,它就是指向具体的proto_ops数据结构实例,如inet_stream_ops、inet_dgram_ops等。proto_ops结构由地址族类型和一系列指向与特定地址族对应的socket操作函数的指针组成。ops 字段通过地址族标识符来索引,接下来我们看看proto_ops结构。
4.5、struct proto_ops结构实例
前面说过,具体的ioctl执行过程时通过两次跳转而来,其中第二次就是针对各个不同层次、类型的套接字。我们来看看内核中所定义的各个具体的proto_ops结构实例以分析不同的控制执行流程. 内核中为每个规程定义了一个proto_ops结构实例,常见的如下:
1、在net/ipv4/Af_inet.c文件中:
struct proto_ops inet_stream_ops = {
…
poll: tcp_poll,
ioctl: inet_ioctl,
listen: inet_listen,
…
};
struct proto_ops inet_dgram_ops = {
…
poll: datagram_poll,
ioctl: inet_ioctl,
listen: sock_no_listen,
…
};
可见这两个实例有相当多的处理函数都是一样的,并且最终调用相同的控制函数inet_ioctl.
2、在net/ipv6/Af_inet6.c文件中提供了inet6_stream_ops和inet6_dgram_ops,其地址族及ioctl处理函数分别为PF_INET6和inet6_ioctl:
struct proto_ops inet6_stream_ops = {
family: PF_INET6,
…
ioctl: inet6_ioctl, /* must change */
…
};
struct proto_ops inet6_dgram_ops = {
family: PF_INET6,
…
ioctl: inet6_ioctl, /* must change */
…
};
3、在net/packet/Af_ packet 6.c文件中提供了packet_ops_spkt和packet_ops,其地址族及ioctl处理函数分别为PF_PACKET和packet_ioctl:
struct proto_ops packet_ops = {
family: PF_PACKET,
…
ioctl: packet_ioctl,
…
};
还有x25和ipx、netlink、unix域等等地址族所对应的文件提供了各自的协议规程操作函数指针以支持不同的ioctl处理函数,大家有兴趣可以参考内核相关源码.
可见,通过二次跳转表,内核可以支持不同协议规程做不同的操作,包括控制处理。本文把重点放在ipv4的ioctl控制函数,引导大家深入到其处理源码.
4.6、inet_ioctl函数
由于inet_ioctl函数内容分支很多,但功能、处理不难理解,所以我把一些不常见的内容都省去,挑简单重要的说,完全在于抛砖引玉:
static int inet_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg)
{
…
switch(cmd)
{
case FIOSETOWN://设置属主
case SIOCSPGRP://设置进程组
err = get_user(pid, (int *) arg);
if (err)
return err;
if (current->pid != pid && current->pgrp != -pid &&
!capable(CAP_NET_ADMIN))
return -EPERM;
sk->proc = pid;
return(0);
case FIOGETOWN://获取属主
case SIOCGPGRP://获取进程组
return put_user(sk->proc, (int *)arg);
case SIOCGSTAMP://
if(sk->stamp.tv_sec==0)
return -ENOENT;
err = copy_to_user((void *)arg,&sk->stamp,sizeof(struct timeval));
if (err)
err = -EFAULT;
return err;
case SIOCADDRT://增加路由
case SIOCDELRT://删除路由
case SIOCRTMSG:
return(ip_rt_ioctl(cmd,(void *) arg));//IP路由配置
case SIOCDARP://删除arp项
case SIOCGARP://获取arp项
case SIOCSARP://创建/修改arp项
return(arp_ioctl(cmd,(void *) arg));//arp配置
case SIOCGIFADDR://获取接口地址
case SIOCSIFADDR://设置接口地址
case SIOCGIFBRDADDR://获取广播地址
case SIOCSIFBRDADDR://设置广播地址
case SIOCGIFNETMASK://获取网络掩码
case SIOCSIFNETMASK://设置网络掩码
case SIOCGIFDSTADDR://获取p2p地址
case SIOCSIFDSTADDR://设置p2p地址
case SIOCSIFPFLAGS: //
case SIOCGIFPFLAGS:
case SIOCSIFFLAGS://设置接口标志
return(devinet_ioctl(cmd,(void *) arg));//网络接口相关配置,linux内核自带的ifconfig
//的很多处理都是通过这里实现的
case SIOCGIFBR:
case SIOCSIFBR://网桥设置,稍后的实例就是介绍如何截获网桥控制钩子
#if defined(CONFIG_BRIDGE) || defined(CONFIG_BRIDGE_MODULE) //如果内核支持网桥功能
#ifdef CONFIG_KMOD//若支持内核模块动态加载
if (br_ioctl_hook == NULL)//网桥钩子为空则动态请求模块
request_module("bridge");//加载网桥模块
#endif
if (br_ioctl_hook != NULL)
return br_ioctl_hook(arg);//通过钩子函数处理命令参数
#endif
case SIOCGIFDIVERT://
case SIOCSIFDIVERT:
#ifdef CONFIG_NET_DIVERT
return(divert_ioctl(cmd, (struct divert_cf *) arg));
#else
return -ENOPKG;
#endif /* CONFIG_NET_DIVERT */
return -ENOPKG;
case SIOCADDDLCI://
case SIOCDELDLCI:// 数据链路连接标识控制
#ifdef CONFIG_DLCI
lock_kernel();
err = dlci_ioctl(cmd, (void *) arg);//控制函数
unlock_kernel();
return err;
#endif
#ifdef CONFIG_DLCI_MODULE
#ifdef CONFIG_KMOD
if (dlci_ioctl_hook == NULL)//如果钩子函数为空,则加载模块
request_module("dlci");
#endif
if (dlci_ioctl_hook) {//钩子函数指针不空
lock_kernel();
err = (*dlci_ioctl_hook)(cmd, (void *) arg);//调用钩子函数
unlock_kernel();
return err;
}
#endif
return -ENOPKG;
default:
…
return err;
}
/*NOTREACHED*/
return(0);
}
从上面的函数代码来看, ?套接字有关的控制请求主要有如下几类:
1、文件操作
2、套接字操作
3、路由选项操作
4、接口操作
5、ARP高速缓存操作
6、网桥控制
7、数据链路连接标识控制
结合代码中的注释,读者不难理解具体的控制分支。具体的控制处理就转到具体的函数里面去处理了,例如关于内核自带的命令工具ifconfig对ip地址的配置处理,基本都在devinet_ioctl函数中;关于arp命令的处理都在arp_ioctl中处理;关于路由配置都在ip_rt_ioctl中处理。其中参数arg是用户空间传来的自定义的数据,可以是结构,可以是联合或其它一些更复杂的类型,由具体的业务模块来解释处理。在随后的实践中,我们就是通过arg的不同解释来做不同的处理。
4.7、网络主要结构相关字段相互引用图
通过上面的分析,大家应该大致明白了linux内核网络ioctl控制框架的实现了。下面是在内核网络组件初始化后,ipv4相关的结构字段之间相互引用图,供大家阅读是参考:
结合前面主要函数调用关系图与源码分析,读者可以很清晰的顺着上图所示的箭头,从ioctl入口函数开始,方便地找到具体的处理模块.其中,文件操作对象 socket_file_ops调用sock_ioctl()时,通过inode节点的socket_i字段最终找到inet_ioctl()函数.
五、调用实践
此处介绍通过自己编写控制程序,在用户空间调用ioctl函数控制内核显示一行信息的例子供大家参考:
1.编写运行于用户空间的控制程序
(1)一般先定义自己的结构参数类型,如下:
typedef struct stMyIoctlArg {
unsigned int cmd;//其实就是第一个参数,当作自己的命令参数
unsigned int arg1; //用于提供给具体的命令参数
unsigned int arg2;
…//如果有更多参数直接加在后面
} IOCTL_ARG,P IOCTL_ARG;
(2)然后在main中赋值并调用ioctl函数:
#define FIOCSMYSHOW 0x1234
int main( int argc, char **argv )
{
int fd;
IOCTL_ARG arg; //需要组织传递到内核的参数
arg.cmd= FIOCSMYSHOW //自定义命令
…//其它参数赋值
int fd = socket( AF_INET, SOCK_STREAM, 0 );//创建控制socket
if ( fd < 0 )
{
perror( "socket failed" );
return 0;
}
if ( ioctl( fd, SIOCSIFBR, &arg) < 0 ) //通过网桥请求参数来控制内核作相关操作
{
perror( "ioctl( SIOCSIFBR ) failed" );
close( fd );
return 0;
}
…
close(fd);
…
}
例子源代码:
2.内核功能支持
2.1、修改内核相关代码:
(1)在内核include/linux/sockios.h的尾部加入前面定义的公共的结构与常量:
typedef struct stMyIoctlArg {
unsigned int cmd;//其实就是第一个参数,当作自己的命令参数
unsigned int arg1;
unsigned int arg2;
…//如果有更多参数直接加在后面
} IOCTL_ARG,P IOCTL_ARG;
#define FIOCSMYSHOW 0x1234
(2)在inet_ioctl函数网桥处理分支处增加如下蓝色字体内容:
IOCTL_ARG myarg;//在inet_ioctl函数开始时加入此变量定义
…
#if defined(CONFIG_BRIDGE) || defined(CONFIG_BRIDGE_MODULE)
if ( copy_from_user( & myarg, (void *) arg, sizeof(IOCTL_ARG ) ) ) //拷贝用户空间参数
return -EFAULT;
switch (myarg.cmd ){
case FIOCSMYSHOW ://解析自己的命令
printk(KERN_INFO “get ioctl hook.\n”); //可以增加对arg1/arg2等参数的解析处理
return 0; //直接返回
break;
…
default:
break;
}
#ifdef CONFIG_KMOD
if (br_ioctl_hook == NULL)
request_module("bridge");
#endif
if (br_ioctl_hook != NULL)
return br_ioctl_hook(arg);
#endif
内核修改文件: 。注意在修改内核代码后,用README中的命令编译一下修改的文件,没有错误才编译内核,避免走弯路重新编译。
2.2、编译内核
具体编译过程请参照网络上的文章,我所用到的重要的命令有:
make mrproper
make oldconfig
make xconfig //在network options中选择802.1 ethernet bridge选项支持网桥功能
make dep
make bzImage
make modules
make modules_install
depmod -a
cp System.map /boot/System.map-2.4.20-8custom
cp arch/i386/boot/bzImage /boot/vmlinuz-2.4.20-8custom
new-kernel-pkg --install --mkinitrd --depmod 2.4.20-8custom
3.运行控制程序
内核编译前运行显示:
内核编译后运行显示:
4.查看结果
可以通过dmesg | grep hook命令查看结果,显示:
这正是我们在内核中要打印的字符,说明我们的控制命令已经通知给内核了。
六、结束语
ioctl系统调用是最常用的用户与内核空间交互的手段之一,linux系统自带的相当多的命令工具尤其是网络控制工具都是采用ioctl控制框架实现了用户和内核通信的桥梁,在当前一些基于内核模块技术的软件系统中也有重要的用途,如某些宽带计费网关、防火墙软件、网络交换机等。了解ioctl控制框架,无疑会提高我们对linux内核通信机制的认识,也可以指导我们的实践工作。