热门关键字:  ubuntu  分区  linux系统进程  Fedora  函数
当前位置 :| 主页>Linux教程>内核研究>

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

来源: 作者: 时间:2007-08-27 Tag: 点击:
作者:松哥
email
jccz_zys@tom.com
MSN: jccz_zys@163.net
QQ: 15210449
                                                  (转载请注明出处)
从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框架源代码分析
根据前面的图示,我们从入口函数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内核支持不同层次、类型的套接字,如ipv4ipv6套接字以及sock_raw原始套接字,对于这些套接字的处理有一定的相似性,又有其不同的地方。所以引入第二次跳转的目的也即在此,以支持对不同的协议类型的套接字进行不同控制,详情见下面小节的介绍。
最新评论共有 4 位网友发表了评论
发表评论
评论内容:不能超过250字,需审核,请自觉遵守互联网相关政策法规。
用户名: 密码:
匿名?
注册