字符设备驱动程序的扩展操作ioctl

字符设备驱动程序的扩展操作ioctl

        由于使用了一连串的“.”的缘故,该原型在Unix系统调用列表之中非常突出,这些点代表可变数目参数。但是在实际系统中,系统调用实际上不会有可变数目个参数。因为用户程序只能通过用户空间和内核空间的硬件“门”才能访问内核,系统调用必须有精确定义的参数个数。因此,ioctl的第3个参数事实上只是一个可选参数,这里用点只是为了在编译时防止编译器进行类型检查。第3个参数的具体情况与要完成的控制命令(第2个参数)有关。某些命令不需要参数,某些需要一个整数做参数,而某些则需要一个指针做参数。使用指针通常是可以用来向ioctl传递任意数目数据;设备可以从用户空间接收任意大小的数据。

inode和filp指针是根据应用程序传递的文件描述符fd计算而得的,与read和write的用法一致。参数cmd不经修改地传递给驱动程序,可选的arg参数无论是指针还是整数值,它都以unsigned long的形式传递给驱动程序。如果调用程序没有传递第3个参数,驱动程序所接收的arg没有任何意义。

由于附加参数的类型检查被关闭了,如果非法参数传递给ioctl,编译器无法向你报警,程序员在运行前是无法注意这个错误的。这是我所见到的ioctl语义方面的唯一一个问题。

如你所想,大多数ioctl实现都包括一个switch语句来根据cmd参数选择正确的操作。不同的命令对应不同的数值,为了简化代码我们通常会使用符号名代替数值。这些符号名都是在预处理中赋值的。不同的驱动程序通常会在它们的头文件中声明这些符号;scull就在scull.h中声明了这些符号。

选择ioctl命令

在编写ioctl代码之前,你需要选择对应不同命令的命令号。遗憾的是,简单地从1开始选择号码是不能奏效的。

为了防止对错误的设备使用正确的命令,命令号应该在系统范围内是唯一的。这种失配并不是不很容易发生,程序可能发现自己正在对象FIFO和kmouse这类非串口输入流修改波特率。如果每一个ioctl命令都是唯一的,应用程序就会获得一个EINVAL错误,而不是无意间成功地完成了操作。

为了达到唯一性的目的,每一个命令号都应该由多个位字段组成。Linux的第一版使用了一个16位整数:高8位是与设备相关的“幻”数,低8位是一个序列号码,在设备内是唯一的。这是因为,用Linus的话说,他有点“无头绪”,后来才接收了一个更好的位字段分割方案。遗憾的是,很少有驱动程序使用新的约定,这就挫伤了程序员使用新约定的热情。在我的源码中,为了发掘这种约定都提供了那些功能,同时防止被其他开发人员当成异教徒而禁止,我使用了新的定义命令的方法。

为了给我的驱动程序选择ioctl号,你应该首先看看include/asm/ioctl.h和Documentation/ioctl-number.txt这两个文件。头文件定义了位字段:类型(幻数),基数,传送方向,参数的尺寸等等。ioctl-number.txt文件中罗列了在内核中使用的幻数。这个文件的新版本(2.0以及后继内核)也给出了为什么应该使用这个约定的原因。

很不幸,在1.2.x中发行的头文件没有给出切分ioctl位字段宏的全集。如果你需要象我的scull一样使用这种新方法,同时还要保持向后兼容性,你使用scull/sysdep.h中的若干代码行,我在那里给出了解决问题的文档的代码。

现在已经不赞成使用的选择ioctl号码的旧方法非常简单:选择一个8位幻数,比如“k”(十六进制为0x6b),然后加上一个基数。

如果应用程序和驱动程序都使用了相同的号码,你只要在驱动程序里实现switch语句就可以了。但是,这种在传统Unix中有基础的定义ioctl号码的方法,不应该再在新约定中使用。这里我介绍就方法只是想给你看看一个ioctl号码大致是个什么样子的。

新的定义号码的方法使用了4个位字段,它们有如下意义。下面我所介绍的新符号都定义在<linux/ioctl.h>中。

类型

幻数。选择一个号码,并在整个驱动程序中使用这个号码。这个字段有8位宽(_IOC_TYPEBITS)。

号码

基(序列)数。它也是8位宽(_IOC_NRBITS)。

方向

如果该命令有数据传输,它定义数据传输的方向。可以使用的值有,_IOC_NONE(没有数据传输),_IOC_READ,_IOC_WRITE和_IOC_READ | _IOC_WRITE(双向传输数据)。数据传输是从应用程序的角度看的;IOC_READ意味着从设备中读数据,驱动程序必须向用户空间写数据。注意,该字段是一个位屏蔽码,因此可以用逻辑AND操作从中分解出_IOC_READ和_IOC_WRITE。

尺寸

所涉及的数据大小。这个字段的宽度与体系结构有关,当前的范围从8位到14位不等。你可以在宏_IOC_SIZEBITS中找到某种体系结构的具体数值。不过,如果你想要你的驱动程序可移植,你只能认为最大尺寸可达255个字节。系统并不强制你使用这个字段。如果你需要更大尺度的数据传输,你可以忽略这个字段。下面我们将介绍如何使用这个字段。

包含在<linux/ioctl.h>之中的头文件<asm/ioctl.h>定义了可以用来构造命令号码的宏:_IO(type,nr),_IOR(type,nr,size),_IOW(type,nr,size)和IOWR(type,nr,size)。每一个宏都对应一种可能的数据传输方向,其他字段通过参数传递。头文件还定义了解码宏:_IOC_DIR(nr),_IOC_TYPE(nr),_IOC_NR(nr)和_IOC_SIZE(nr)。我不打算详细介绍这些宏,头文件里的定义已经足够清楚了,本节稍后会给出样例。

这里是scull中如果定义ioctl命令的。特别地,这些命令设置并获取驱动程序的配置参数。在标准的宏定义中,要传送的数据项的尺寸有数据项自身的实例代表,而不是sizeof(item),这是因为sizeof是宏扩展后的一部分。

最后一条命令,HARDRESET,用来将模块使用计数器复位为0,这样就可以在计数器发生错误时就可以卸载模块了。实际的源码定义了从IOCHQSET到HARDRESET间的所有命令,但这里没有列出。

我选择用两种方法实现整数参数传递――通过指针和显式数值,尽管根据已有的约定,ioctl应该使用指针完成数据交换。同样,这两种方法还用于返回整数:通过指针和设置返回值。如果返回值是正的,这就可以工作;对与任何一个系统调用的返回值,正值是受保护的(如我们在read和write所见到的),而负值则被认为是一个错误值,用其设置用户空间中的errno变量。

“交换”和“移位”操作并不专用于scull设备。我实现“交换”操作是为了给出“方向”字段的所有可能值,而“移位”操作则将“告知”和“查询”操作组合在一起。某些时候是需要原子性*测试兼设置这类操作的――特别是当应用程序需要加锁和解锁时。

显式的命令基数没有什么特殊意义。它只是用来区分命令的。事实上,由于ioctl号码的“方向”为会有所不同,你甚至可以在读命令和写命令中使用同一个基数。我选择除了在声明中使用基数外,其他地方都不使用它,这样我就不必为符号值赋值了。这也是为什么显式的号码出现在上面的定义中。我只是向你介绍一种使用命令号码的方法,你可以自由地采用不同的方法使用它。

当前,参数cmd的值内核并没有使用,而且以后也不可能使用。因此,如果你想偷懒,你可以省去上面那些复杂的声明,而直接显式地使用一组16位数值。但另一方面,如果你这样做了,你就无法从使用位字段中受益了。头文件<linux/kd.h>就是这种旧风格方法的例子,但是它们并不是因为偷懒才这样做的。修改这个文件需要重新编译许多应用程序。

返回值

ioctl的实现通常就是根据命令号码的一个switch语句。但是,当命令号码不能匹配任何一个合法操作时,default选择使用是什么?这个问题是很有争议性的。大多数内核函数返回-EINVAL(“非法参数”),这是由于命令参数确实不是一个合法的参数,这样做是合适的。然而,POSIX标准上说,如果调用了一个不合适的ioctl命令,应该返回-ENOTTY。对应的消息是“不是终端”――这不是用户所期望的。你不得不决定是严格依从标准还是一般常识。我们将本章的后面介绍为什么依从POSIX标准需要返回ENOTTY。

预定义命令

尽管ioctl系统调用大部分都用于操作设备,但还有一些命令是由内核识别的。注意,这些命令是在你自己的文件操作前调用的,所以如果你选择了和它们相同的命令号码,你将无法接收到那个命令的请求,而且由于ioctl命令的不唯一性,应用程序会请求一些未可知的请求。

预定义命令分为3组:用于任何文件(普通,设备,FIFO和套接字文件)的,仅用于普通文件的以及和文件系统相关的;最后一组命令只能在宿主机文件系统上执行(间chattr命令)。设备驱动程序编写者仅对第1组感兴趣就可以了,它们的幻数是“T”。分析其他组的工作将留做读者的练习;ext2_ioctl是其中最有意思的函数(尽管比你想象的要容易得多),它实现了只追加标志和不可变标志。

下列ioctl命令对任何文件都是预定义的:

FIOCLEX

设置exec时关闭标志(File IOctl Close on EXec)。

FIONCLEX

清除exec时关闭标志。

FIOASYNC

设置或复位文件的同步写。Linux中没有实现同步写;但这个调用存在,这样请求同步写的应用程序就可以编译和运行了。如果你不知道同步写是怎么回事,你也不用费神去了解它了:你不需要它。

FIONBIO

“File IOctl Nonblocking I/O(文件ioctl非阻塞型I/O)”(稍后在“阻塞型和非阻塞型操作”一节中介绍)。该调用修改filp->f_flags中的O_NONBLOCK标志。传递给系统调用的第3个参数用来表明该标志是设置还是清除。我们将在本章后面谈到它的作用。注意,fcntl系统调用使用F_SETFL命令也可以修改这个标志。

列表中的最后一项引入了一个新系统调用fcntl,它看起来和ioctl很象。事实上,fcntl调用与ioctl非常相似,它也有一个命令参数和额外(可选的)一个参数。它和ioctl分开主要是由于历史原因:当Unix开发人员面对“控制”I/O操作的问题时,他们决定文件和设备应该是不同的。那时,唯一的设备是终端,这也就解释了为什么-ENOTTY是标准的非法ioctl命令的返回值。这个问题是是否保持向后兼容性的老问题。

使用ioctl参数

我们需要讲解的最后一点是,在分析scull驱动程序的ioctl代码前,首先弄明白如何使用那个额外的参数。如果它是一个整数就非常简单了:可以直接使用它。如果它是一个指针,就必须注意一些问题了。

当用一个指针引用用户空间时,我们首先要确保它指向了合法的用户空间,并且对应页面当前恰在映射中。如果内核代码试图访问范围之外的地址,处理器就会发出一个异常。内核代码中的异常将由上至2.0.x的内核转换为oops消息。设备驱动程序应该通过验证将要访问的用户地址空间的合法性来防止这种失效的发生,如果地址是非法的应该返回一个错误码。

Linux 2.1中引入新功能之一就是内核代码的异常处理。遗憾的是,正确的实现需要驱动程序-内核接口的较大改动。本章给出的非法只适用于旧内核,从1.2.13到2.0.x。新接口将在第17章“近期发展”的“处理内核空间失效”一节中介绍,那里给出的例子通过某些预处理宏将使支持的内核扩展到2.1.43。

内核1.x.y和2.0.x的地址验证是通过函数verify_area实现的,它的原型定义在<linux/mm.h>中。

第一个参数应该是VERIFY_READ或VERIFY_WRITE,这取决于你要在内存区上完成读还是写操作。ptr参数是一个用户空间地址,extent是一个字节计数。例如,如果ioctl需要从用户空间读一个整数,extent就是sizeof(int)。如果在指定的地址上进行读和写操作,使用VERIFY_WRITE,它是VERIFY_READ的超集。

验证读只检查地址是否是合法的:除此之外,验证写要好检查只读和copy-on-write页面。copy-on-write页面一个共享可写页面,它还没有被任何共享进程写过;当你验证写时,verify_area完成“复制兼完成可写配置”操作。很有意思的是,这里无需检查页面是否“在”内存中,这是由于合法页面将由失效函数正确地进行处理,甚至从内核代码中调用也可以。我们已经在第3章“字符设备”的“Scull的内存使用”一节中看到内核代码可以成功地完成页面失效处理。

象大多数函数一样,verify_area返回一个整数值:0意味着成功,负值代表一个错误,应该将这个错误返回给调用者。scull源码在switch之前分析ioctl号码的各个位字段。

在调用verify_area之后,再有驱动程序完成真正的数据传送。除了memcpy_tofs和memcpy_fromfs函数外,程序员还可以使用两个专为常用数据尺寸(1,2和4个字节,在以及64位平台上的8个字节)优化的函数。这些函数定义在<asm/segment.h>中。

put_user(datum, ptr)

实际上它是一个最终调用__put_user的宏;编译时将其扩展为一条机器指令。驱动程序应该尽可能使用put_user,而不是memcpy_tofs。由于在宏表达式中不进行类型检查,你可以传递给put_user任何类型的数据指针,不过它应该是一个用户空间地址。数据传输的尺寸依赖于ptr参数的类型,这是在编译时通过特殊的gcc伪函数实现的,这里没有介绍的必要。结果,如果ptr是一个字符指针,就传递1个字节,依此类推分别有2,4和8个字节。如果被指引的数据不是所支持的尺寸,被编译的代码就会调用函数bad_user_access_length。如果这些编译代码是一个模块,由于这个符号没有开放,模块就不能加载了。

get_user(ptr)

这个宏用来从用户空间获取一个数据。除了数据传输的方向不同外,它与put_user是一样的。

当insmod不能解析符号时,bad_user_access_length的又臭又长的名字可以当作一个很有意义的错误信息。这样,开发人员就可以在向大众分布模块前加载和测试模块,他会很快找到并修改错误。相反,如果使用了不正确尺寸的put_user和get_user直接编译到了内核中,bad_user_access_length就会导致系统panic。尽管对于尺寸错误的数据传输来说,oops比其系统panic要友好得多,但还是选择了较为激进的方法来尽力杜绝这种错误。

scull的ioctl实现只传送设备的可配置参数。

还有6项是操作scull_qset的。这些操作scull_quantum的一样。

从调用者的角度看(即从用户空间)。

如果你需要写一个可以在Linux 1.2里运行的模块,get_user和put_user会是非常棘手的函数,因为它们直到内核1.3才引入到系统中。在切换到类型依赖宏之前,程序员使用一些称为get_user_byte等等的函数。旧的宏只在内核1.3中定义了,在2.0内核中,只有你事先使用了#define WE_REALLY_WANT_TO_USE_A_BROKEN_INTERFACE时才能使用旧的宏。不过为了可移植性,为旧内核定义put_user是一种更好的解决方法,于是为了驱动程序可以在旧内核中良好运行,scull/sydep.h包含了这些宏的定义。

在关于字符设备驱动程序,我们构建了一个完整的设备驱动程序,从中用户可以读也可以写。但实际一个驱动程序通常会提供比同步read和write更多的功能。

通过补充设备读写操作的功能之一就是控制硬件,最常用的通过设备驱动程序完成控制动作的方法就是实现ioctl方法。另一种方法是检查写到设备中的数据流,使用特殊序列做为控制命令。尽管有时也使用后者,但应该尽量避免这样使用。不过稍后我们还是会在本章的“非ioctl设备控制”一节中介绍这项技术。

正如所猜想的,ioctl系统调用为驱动程序执行“命令”提供了一个设备相关的入口点。与read和其他方法不同,ioctl是设备相关的,它允许应用程序访问被驱动硬件的特殊功能――配置设备以及进入或退出操作模式。这些“控制操作”通常无法通过read/write文件操作完成。例如,你向串口写的所有数据都通过串口发送出去了,你无法通过写设备改变波特率。这就是ioctl所要做的:控制I/O通道。

实际设备(与scull不同)的另一个重要功能是,读或写的数据需要同其他硬件交互,需要某些同步机制。阻塞型I/O和异步触发的概念将满足这些需求,本章将通过一个改写的scull设备介绍这些内容。驱动程序利用不同进程间的交互产生异步事件。与最初的scull相同,你无需特殊硬件来测试驱动程序是否可以工作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阳光向日葵之沈阳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值