📄 (ldd) ch05-字符设备驱动程序的扩展操作.txt
字号:
返回值
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/m
m.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的内存使用”一节中看到内核代
调用也可以。我们已经在第3章“字符设备”的“Scull的内存使用”一节中看到内核代
码可以成功地完成页面失效处理。
象大多数函数一样,verify_area返回一个整数值:0意味着成功,负值代表一个错误,
应该将这个错误返回给调用者。
scull源码在switch之前分析ioctl号码的各个位字段:
(代码)
在调用verify_area之后,再有驱动程序完成真正的数据传送。除了memcpy_tofs和memcp
y_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_acce
ss_length。如果这些编译代码是一个模块,由于这个符号没有开放,模块就不能加载了
ss_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的一样,为了节省空间,没有在
上面的例子中列出。
从调用者的角度看(即从用户空间),传递和接收参数的6种方法如下所示:
从调用者的角度看(即从用户空间),传递和接收参数的6种方法如下所示:
(代码)
如果你需要写一个可以在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包含了这些宏的定义。
非ioctl设备控制
有时通过向设备自身发送写序列能够更好地完成对设备的控制。例如,这一技术使用在
控制台驱动程序中,它称为“escape序列”,用来控制光标移动,改变默认颜色,或是
完成某些配置任务。用这种方法实现设备控制的好处是,用户仅用写数据就可以完成对
设备的控制,无需使用(有时是写)完成设备配置的程序。
例如,程序setterm通过打印escape序列完成对控制台(或其他终端)的配置。这种方法
的优点是可以远程控制设备。由于可以简单地重定向数据流完成配置工作,控制程序可
以运行在另外一台不同的计算机上,而不一定非要在被控设备的计算机上。你已经在终
端上使用了这项技术,但这项技术可以更通用一些。
“通过打印控制”的缺点是,它给设备增加了策略限制;例如,只有你确认控制序列不
“通过打印控制”的缺点是,它给设备增加了策略限制;例如,只有你确认控制序列不
会出现在正常写到设备的数据中时,这项技术才是可用的。对于终端来说,这只能说是
部分正确。尽管文本显示只意味着显示ASCII字符,但有时控制字符也会出现在正在打印
的数据中,因此会影响控制台的配置。例如,当你对二进制文件进行grep时可能会发生
这样的情况;分解出的字符行可能什么都包含,最后经常会造成控制台的字体错误*。
写控制特别适合这样的设备,不传输数据,仅相应命令,如机器人设备。
例如,我所写的驱动程序之一是控制一个在两个轴上的摄像头的移动。在这个驱动程序
中,“设备”是一对旧的步进马达,它既不能读也不能写。向步进马达“发送数据流”
多少没有多大意义。在这种情况下,驱动程序将所写的数据解释为ASCII命令,并将请求
转换为脉冲,实现对步进马达的控制。命令可以是任何象“向左移动14步”,“达到位
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -