📄 (ldd) ch05-字符设备驱动程序的扩展操作.txt
字号:
(LDD) Ch05-字符设备驱动程序的扩展操作
第5章 字符设备驱动程序的扩展操作
在关于字符设备驱动程序的那一章中,我们构建了一个完整的设备驱动程序,从中用户
可以读也可以写。但实际一个驱动程序通常会提供比同步read和write更多的功能。现在
如果出了什么毛病,我已经配备了调试工具,我们可以大胆的实验并实现新操作。
通过补充设备读写操作的功能之一就是控制硬件,最常用的通过设备驱动程序完成控制
动作的方法就是实现ioctl方法。另一种方法是检查写到设备中的数据流,使用特殊序列
做为控制命令。尽管有时也使用后者,但应该尽量避免这样使用。不过稍后我们还是会
在本章的“非ioctl设备控制”一节中介绍这项技术。
正如我在前一章中所猜想的,ioctl系统调用为驱动程序执行“命令”提供了一个设备相
关的入口点。与read和其他方法不同,ioctl是设备相关的,它允许应用程序访问被驱动
硬件的特殊功能――配置设备以及进入或退出操作模式。这些“控制操作”通常无法通
硬件的特殊功能――配置设备以及进入或退出操作模式。这些“控制操作”通常无法通
过read/write文件操作完成。例如,你向串口写的所有数据都通过串口发送出去了,你
无法通过写设备改变波特率。这就是ioctl所要做的:控制I/O通道。
实际设备(与scull不同)的另一个重要功能是,读或写的数据需要同其他硬件交互,需
要某些同步机制。阻塞型I/O和异步触发的概念将满足这些需求,本章将通过一个改写的
scull设备介绍这些内容。驱动程序利用不同进程间的交互产生异步事件。与最初的scul
l相同,你无需特殊硬件来测试驱动程序是否可以工作。直到第8章“硬件管理”我才会
真正去与硬件打交道。
ioctl
在用户空间内调用ioctl函数的原型大致如下:
(代码)
由于使用了一连串的“.”的缘故,该原型在Unix系统调用列表之中非常突出,这些点代
表可变数目参数。但是在实际系统中,系统调用实际上不会有可变数目个参数。因为用
户程序只能通过第2章“编写和运行模块”的“用户空间和内核空间”一节中介绍的硬件
“门”才能访问内核,系统调用必须有精确定义的参数个数。因此,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和Documentati
on/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_TYP
EBITS)。
号码
基(序列)数。它也是8位宽(_IOC_NRBITS)。
方向
方向
如果该命令有数据传输,它定义数据传输的方向。可以使用的值有,_IOC_NONE(没有数
据传输),_IOC_READ,_IOC_WRITE和_IOC_READ | _IOC_WRITE(双向传输数据)。数据
传输是从应用程序的角度看的;IOC_READ意味着从设备中读数据,驱动程序必须向用户
空间写数据。注意,该字段是一个位屏蔽码,因此可以用逻辑AND操作从中分解出_IOC_R
EAD和_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命令的。特别地,这些命令设置并获取驱动程序的配置参
数。在标准的宏定义中,要传送的数据项的尺寸有数据项自身的实例代表,而不是sizeo
数。在标准的宏定义中,要传送的数据项的尺寸有数据项自身的实例代表,而不是sizeo
f(item),这是因为sizeof是宏扩展后的一部分。
(代码)
最后一条命令,HARDRESET,用来将模块使用计数器复位为0,这样就可以在计数器发生
错误时就可以卸载模块了。实际的源码定义了从IOCHQSET到HARDRESET间的所有命令,但
这里没有列出。
我选择用两种方法实现整数参数传 莰D―通过指针和显式数值,尽管根据已有的约定,i
octl应该使用指针完成数据交换。同样,这两种方法还用于返回整数:通过指针和设置
返回值。如果返回值是正的,这就可以工作;对与任何一个系统调用的返回值,正值是
受保护的(如我们在read和write所见到的),而负值则被认为是一个错误值,用其设置
用户空间中的errno变量。
“交换”和“移位”操作并不专用于scull设备。我实现“交换”操作是为了给出“方向
”字段的所有可能值,而“移位”操作则将“告知”和“查询”操作组合在一起。某些
时候是需要原子性*测试兼设置这类操作的――特别是当应用程序需要加锁和解锁时。
显式的命令基数没有什么特殊意义。它只是用来区分命令的。事实上,由于ioctl号码的
“方向”为会有所不同,你甚至可以在读命令和写命令中使用同一个基数。我选择除了
在声明中使用基数外,其他地方都不使用它,这样我就不必为符号值赋值了。这也是为
什么显式的号码出现在上面的定义中。我只是向你介绍一种使用命令号码的方法,你可
什么显式的号码出现在上面的定义中。我只是向你介绍一种使用命令号码的方法,你可
以自由地采用不同的方法使用它。
当前,参数cmd的值内核并没有使用,而且以后也不可能使用。因此,如果你想偷懒,你
可以省去上面那些复杂的声明,而直接显式地使用一组16位数值。但另一方面,如果你
这样做了,你就无法从使用位字段中受益了。头文件<linux/kd.h>就是这种旧风格方法
的例子,但是它们并不是因为偷懒才这样做的。修改这个文件需要重新编译许多应用程
序。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -