© / Posted in Linux / September 17, 2009

概述


从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的控制传递框架,我们接下来的内容将根据此图向大家做详细的解释:

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.5struct 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和ipxnetlinkunix等等地址族所对应的文件提供了各自的协议规程操作函数指针以支持不同的ioctl处理函数,大家有兴趣可以参考内核相关源码.

可见,通过二次跳转表,内核可以支持不同协议规程做不同的操作,包括控制处理。本文把重点放在ipv4的ioctl控制函数,引导大家深入到其处理源码.

 

 

4.6inet_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

本文仅有一篇评论 ↓↓

    1. 不错,受教了

    添加新评论 ↑↑