关于驱动程序中的Ioctl---Coly分析- -
来源:ChinaITLab 收集整理 2004-10-12 12:17:00
一、 什么是ioctl ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的调用个数如下:
int ioctl(int fd, ind cmd, …);其中fd就是用户程序打开设备时使用open函数返回的文件标示符,cmd就是用户程序对设备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,有或没有是和cmd的意义相关的。
ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数控制设备的I/O通道。
二、 ioctl的必要性 如果不用ioctl的话,也可以实现对设备I/O通道的控制,但那就是蛮拧了。例如,我们可以在驱动程序中实现write的时候检查一下是否有特殊约定的数据流通过,如果有的话,那么后面就跟着控制命令(一般在socket编程中常常这样做)。但是如果这样做的话,会导致代码分工不明,程序结构混乱,程序员自己也会头昏眼花的。
所以,我们就使用ioctl来实现控制的功能。要记住,用户程序所作的只是通过命令码告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情。
三、 ioctl如何实现 这是一个很麻烦的问题,我是能省则省。要说清楚它,没有四五千字是不行的,所以我这里是不可能把它说得非常清楚了,不过如果有读者对用户程序怎么和驱动程序联系起来感兴趣的话,可以看我前一阵子写的《write的奥秘》。读者只要把write换成ioctl,就知道用户程序的ioctl是怎么和驱动程序中的ioctl实现联系在一起的了。
我这里说一个大概思路,因为我觉得《Linux设备驱动程序》这本书已经说的非常清楚了,但是得化一些时间来看。
在驱动程序中实现的ioctl函数体内,实际上是有一个switch{case}结构,每一个case对应一个命令码,做出一些相应的操作。怎么实现这些操作,这是每一个程序员自己的事情,因为设备都是特定的,这里也没法说。关键在于怎么样组织命令码,因为在ioctl中命令码是唯一联系用户程序命令和驱动程序支持的途径。
命令码的组织是有一些讲究的,因为我们一定要做到命令和设备是一一对应的,这样才不会将正确的命令发给错误的设备,或者是把错误的命令发给正确的设备,或者是把错误的命令发给错误的设备。这些错误都会导致不可预料的事情发生,而当程序员发现了这些奇怪的事情的时候,再来调试程序查找错误,那将是非常困难的事情。所以在Linux核心中是这样定义一个命令码的: ____________________________________ | 设备类型 | 序列号 | 方向 |数据尺寸| |----------|--------|------|--------| | 8 bit | 8 bit |2 bit |8~14 bit| |----------|--------|------|--------|
这样一来,一个命令就变成了一个整数形式的命令码。但是命令码非常的不直观,所以Linux Kernel中提供了一些宏,这些宏可根据便于理解的字符串生成命令码,或者是从命令码得到一些用户可以理解的字符串以标明这个命令对应的设备类型、设备序列号、数据传送方向和数据传输尺寸。
这些宏我就不在这里解释了,具体的形式请读者察看Linux核心源代码中的和,文件里给除了这些宏完整的定义。这里我只多说一个地方,那就是"幻数"。
幻数是一个字母,数据长度也是8,所以就用一个特定的字母来标明设备类型,这和用一个数字是一样的,只是更加利于记忆和理解。就是这样,再没有更复杂的了。
更多的说了也没有,读者还是看一看源代码吧,推荐各位阅读《Linux 设备驱动程序》所带源代码中的short一例,因为它比较短小,功能比较简单,可以看明白ioctl的功能和细节。
四、 cmd参数如何得出 这里确实要说一说,cmd参数在用户程序端由一些宏根据设备类型、序列号、传送方向、数据尺寸等生成,这个整数通过系统调用传递到内核中的驱动程序,再由驱动程序使用解码宏从这个整数中得到设备的类型、序列号、传送方向、数据尺寸等信息,然后通过switch{case}结构进行相应的操作。
要透彻理解,只能是通过阅读源代码,我这篇文章实际上只是一个引子。Cmd参数的组织还是比较复杂的,我认为要搞熟它还是得花不少时间的,但是这是值得的,驱动程序中最难的是对中断的理解。
五、 小结 ioctl其实没有什么很难的东西需要理解,关键是理解cmd命令码是怎么在用户程序里生成并在驱动程序里解析的,程序员最主要的工作量在switch{case}结构中,因为对设备的I/O控制都是通过这一部分的代码实现的。 //----------------------------------------------------------------------------------------------------- 它可以进行所有的io控制,但是具体的意义和io设备有关。
ioctl(Port TCSETA,&TermConfig) 设置波特率.数据位,奇偶位等. example: TermConfig.c_cflag |= B9600; TermConfig.c_cflag = CREAD | CLOCAL; TermConfig.c_cflag |= CS7; TermConfig.c_cflag |= CSTOPB; TermConfig.c_cflag |= PARENB | PARODD; TermConfig.c_cflag |= ( IXON | IXOFF ); TermConfig.c_iflag = 0; TermConfig.c_oflag = 0; TermConfig.c_lflag = 0; TermConfig.c_line = 0; TermConfig.c_cc[VMIN] = 0; TermConfig.c_cc[VTIME] = 10; if( ioctl( PortID, TCSETA, &TermConfig ) < 0 ) { perror( "InitSerialPort() ioctl failed./n" ); close( PortID ); return -1; } //--------------------------------------------------------------------------------------------------------- 基于套接字描述符的ioctl( )系统调用详解 <!--[if !supportLists]-->(一) ioctl( )系统调用的实现 ioctl( )系统调用提供了一个通用命令接口,原型为 int ioctl(int fd, unsigned long com, caddr_t data) 。其中fd是一个设备描述符或者是一个网络连接(即套接字描述sockfd)。本文针对的是套接字描述符. 首先用户进程通过socket(AF_xxx, SOCK_xxx, flag)函数创建一个TCP或者UDP套接字sockfd, 然后再调用ioctl( )函数发送io命令.而从src/sys/kern/sockio.c中我们发现基于socket的ioctl( )函数实际上是调用了bsd_ioctl(struct CYG_FILE_TAG *fp, CYG_ADDRWORD cmd, CYG_ADDRWORD data): // Table entrys NSTAB_ENTRY( bsd_nste, 0, "bsd_tcpip", "", 0, bsd_init, bsd_socket); struct cyg_sock_ops bsd_sockops = { bsd_bind, bsd_connect, bsd_accept, bsd_listen, bsd_getname, bsd_shutdown, bsd_getsockopt, bsd_setsockopt, bsd_sendmsg, bsd_recvmsg }; cyg_fileops bsd_sock_fileops = { bsd_read, bsd_write, bsd_lseek, bsd_ioctl, bsd_select, bsd_fsync, bsd_close, bsd_fstat, bsd_getinfo, bsd_setinfo }; 下面是bsd_ioctl( )函数的具体实现: static int bsd_ioctl(struct CYG_FILE_TAG *fp, CYG_ADDRWORD cmd, CYG_ADDRWORD data) { struct socket *so = (struct socket *)fp->f_data; void *p = 0; switch (cmd) { case FIONBIO: if (*(int *)data) so->so_state |= SS_NBIO; else so->so_state &= ~SS_NBIO; return (0); case FIOASYNC: if (*(int *)data) { so->so_state |= SS_ASYNC; so->so_rcv.sb_flags |= SB_ASYNC; so->so_snd.sb_flags |= SB_ASYNC; } else { so->so_state &= ~SS_ASYNC; so->so_rcv.sb_flags &= ~SB_ASYNC; so->so_snd.sb_flags &= ~SB_ASYNC; } return (0); case FIONREAD: *(int *)data = so->so_rcv.sb_cc; return (0); case SIOCATMARK: *(int *)data = (so->so_state&SS_RCVATMARK) != 0; return (0); } /* * Interface/routing/protocol specific ioctls: * interface and routing ioctls should have a * different entry since a socket's unnecessary */ if (IOCGROUP(cmd) == 'i') return (ifioctl(so, (u_long)cmd, (caddr_t)data, p)); if (IOCGROUP(cmd) == 'r') return (rtioctl((u_long)cmd, (caddr_t)data, p)); return ((*so->so_proto->pr_usrreqs->pru_control)(so, cmd, (caddr_t)data, 0, 0)); } 从以上标注过的语句里我们发现:1)如果cmd属于'i'组(与接口相关的io控制命令),则调用ifioctl( )函数;2)如果cmd属于'r'组(与路由相关的io控制命令),则调用rtioctl( )函数;3)如果cmd属于's'组(属于无接口的ioctl,与进程相关的文件描述符的命令,如SIOCSPGRP,SIOCGPGRP,FIONBIO,FIOASYNC等),则调用(*so->so_proto->pr_usrreqs->pru_control)(so, cmd, (caddr_t)data, 0, 0)函数完成相关操作/*该函数放在后面说明*/.
(二)ioctl命令的层次关系 与ip地址无关的io命令(针对ifnet{}, ifaddr{}结构) 与ip地址结构相关的io命令(针对in_ifaddr{}结构) |_________________________________| | 涉及到底层硬件设备(如以太网接口,slip接口等)的io命令 由此,我们可以把ioctl命令分为一下4类: 1.与ip地址无关,针对ifnet{}, ifaddr{}结构,且不需要底层硬件设备支持的操作(即不涉及对改底层硬件设备的查询或设置)。 SIOCCGIFCONF, SIOCGIFFLAGS, SIOCGIFMETRIC, SIOCGIFMTU, SIOCGIFPHYS SIOCSIFMETRIC, SIOCSIFMTU, SIOCSIFPHYS 注:针对这类命令,直接在ifioctl( )函数中进行处理. 2.与ip地址相关,针对in_ifaddr{}结构,且不需要底层硬件设备支持的操作(不涉及对底层硬件设备的查询或设置)。 SIOCGIFADDR, SIOCGIFBRDADDR, SIOCGIFNETMASK, SIOCGIFDSTADDR, SIOCSIFBRDADDR, SIOCSIFNETMASK, 注:针对这类命令,ifioctl( )调用了(*so->so_proto->pr_usrreqs->pru_control)(so, cmd, (caddr_t)data, ifp, p),这类命令是属于有接口的ioctl,将所有相关的数据传给与请求指定的接口相关联的协议的用户请求函数的控制函数,通过src/sys/netinet/tcp_usrreq.c和src/sys/netinet/udp_usrreq.c里面的代码,我们发现,对于TCP和UDP插口,该函数实际上调用了in_control( ): struct pr_usrreqs tcp_usrreqs = { tcp_usr_abort, tcp_usr_accept, tcp_usr_attach, tcp_usr_bind, tcp_usr_connect, pru_connect2_notsupp, in_control, tcp_usr_detach, tcp_usr_disconnect, tcp_usr_listen, in_setpeeraddr, tcp_usr_rcvd, tcp_usr_rcvoob, tcp_usr_send, pru_sense_null, tcp_usr_shutdown, in_setsockaddr, sosend, soreceive, sopoll }; struct pr_usrreqs udp_usrreqs = { udp_abort, pru_accept_notsupp, udp_attach, udp_bind, udp_connect, pru_connect2_notsupp, in_control, udp_detach, udp_disconnect, pru_listen_notsupp, in_setpeeraddr, pru_rcvd_notsupp, pru_rcvoob_notsupp, udp_send, pru_sense_null, udp_shutdown, in_setsockaddr, sosend, soreceive, sopoll }; 第2类的ioctl命令由于并不涉及到对底层硬件设备的配置或查询,故对它们的处理直接放在in_control( )中完成. 3.与ip地址无关,针对ifnet{}, ifaddr{}结构,且需要底层硬件设备支持的操作(需要修改底层硬件设备的配置)。 SIOCGIFFLAGS, SIOCADDMULTI, SIOCDELMULTI 注:对于这类ioctl命令,由于涉及到了底层硬件设备,ifioctl( )在完成对通用接口的配置后,通过调用ifp->if_ioctl( )函数对底层硬件设备进行配置.对于以太网接口,ifp->if_ioctl = eth_drv_ioctl/*由于ecos针对所有的以太网络设备做了一层抽象,我们称之为通用io层,所以此处调用的是eth_drv_ioctl()(注:由eth_drv_ioctl()去调用 at91sam9260_eth_ioctl() ), 而不是直接去调用at91sam9260_eth_ioctl()*/;对于slip接口,ifp->if_ioctl = slioctl等等. 4.与ip地址相关,针对in_ifaddr{}结构,且需要底层硬件设备的支持的操作(需要修改底层硬件设备的配置) SIOCSIFADDR, SIOCAIFADDR , SIOCDIFADDR SIOCSIFDSTADDR 注:针对这类的ioctl命令,当in_control()完成了对结构in_ifaddr的更新后,同时要调用ifp->if_ioctl( )函数对底层硬件设备进行配置. 5.直接针对底层硬件设备的操作(查询,设置等) SIOCGIFHWADDR, SIOCGIFSTATS SIOCSIFHWADDR, SIOCGIFSTATSUD 注:由于此类命令只涉及对底层硬件设备的配置,不改变该接口通用地址结构的成员,故ifioctl( )直接调用了ifp->if_ioctl()函数进行处理.
(三)几个ioctl命令的处理 1.SIOCCGIFCONF //查询所有接口当前的配置信息 ifioctl()并不在对该命令进行直接处理,而是通过调用函数ifconf(int cmd,caddr_t data)来获取当前所有接口的配置信息. 2.SIOCSIFADDR 我们观察到对SIOCSIFADDR的具体处理并没有放在in_control()函数中,而是通过调用in_ifinit( )函数来实现.函数in_ifinit(struct ifnet * ifp, struct in_ifaddr *ia, struct sockaddr_in *sin, int scurb)完成了一下几个操作: (a)将用户进程通过ifreq结构传递下来的ip地址(由指针sin指向)复制到指定的in_ifaddr{}接口结构(由struct in_ifaddr * ia指向)中,并调用ifp->if_ioctl()通知硬件,其中eth_drv_ioctl()又调用了if_ethersubr.c中的ether_ioctl()函数来完成ifp->if_init()和arp_ifinit()的操作; (b)如果是以太网接口,则必须为通用地址结构选择链路层路由函数.如果最后一个参数scrub==1,则调用in_ifscrub()找到并废除任何基于老地址的路由; (c)设置该接口的ia_netmask,ia_subnetmask及ia_sockmask字段; (d)复制ifp所指向的接口度量值到通用地址结构,并根据ifp所指向的接口的类型,设置ia所指向的通用结构中的接口标志ia_flag. (e)调用rtinit()函数为该接口安装路由 (f)如果ifp指向的接口有多播能力,调用in_addrmulti()函数将该接口加入多播组。 3.SIOCSIFADDR, SIOCSIFNETMASK, SIOCSIFDSTADDR 这三个命令均须经由in_control()函数.并且要求所调用的插口是由一个超级用户进程创建的(通过检查sockfd->so_state中的SS_PRIV标志).
//------------------------------------------------------------------------------------------------------------------- IOCTL Talking To Device Files 与设备文件对话 (writes and IOCTLs) 设备文件是用来代表相对应的硬件设备。绝大多数的硬件设备是用来进行输出和输入操作的, 所以在内核中肯定有内核从进程中获得发送到设备的输出的机制。这是通过打开一个设备文件然后 向其中进行写操作来实现的,如同对普通文件的写操作。在下面的的例子中,这是通过 device_write实现的。 但这并不总是够用。设想你有一个通过串口连接的调制解调器(即使你使用的是内置调制解调器, 对于CPU来说同样也是通过连接在串口上来实现工作的)。通常我们通过打开一个设备文件向调制解调器 发送信息(将要通过通信线路传输的指令或数据)或读取信息(从通信线路中返回的响应指令或数据)。 但是,我们如何设置同串口对话的速率,也就是向串口传输数据的速率这个问题仍然没有解决。 解决之道是在Unix系统中的函数ioctl(Input Output ConTroL的简写)。 每个设备可以有自己的ioctl命令,通过读取ioctl's 可以从进程中向内核发送信息,或写ioctl's向进程返回信息 [1],或者两者都是,或都不是。函数ioctl 调用时需要三个参数:合适的设备文件的文件描述符,ioctl号,和一个可以被一个任务使用来 传递任何东西的long类型的参数[2] ioctl号是反映主设备号,ioctl的种类,对应的命令和参数类型的数字。它通常是通过在头文件中宏调用 (_IO, _IOR, _IOW 或_IOWR,取决于其种类)来建立的。该头文件应该被使用 ioctl的用户程序包含(这样它们就可以生成正确的ioctl's) 和内核驱动模块包含(这样模块才能理解它)。在下面的例子中,头文件为chardev.h,源程序为ioctl.c。 即使你只想在自己的模块中使用ioctls,你最好还是接收正式的 ioctl标准,这样当你意外的使用别人的ioctls, 或别人使用你的时,你会知道有错误发生。详情参见内核代码目录树下的文件 Documentation/ioctl-number.txt. Example 7-1. chardev.c |