手机版
你好,游客 登录 注册 搜索
背景:
阅读新闻

Linux内核IOCTL网络控制框架实现实例分析

[日期:2007-12-10] 来源:Linux公社  作者:Linux [字体: ]
四、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控制函数,引导大家深入到其处理源码.

linux
本文评论   查看全部评论 (0)
表情: 表情 姓名: 字数

       

评论声明
  • 尊重网上道德,遵守中华人民共和国的各项有关法律法规
  • 承担一切因您的行为而直接或间接导致的民事或刑事法律责任
  • 本站管理人员有权保留或删除其管辖留言中的任意内容
  • 本站有权在网站内转载或引用您的评论
  • 参与本评论即表明您已经阅读并接受上述条款