【原创】《Linux设备驱动程序》学习之循序渐进 --- 高级字符驱动程序操作
第六章 --- 高级字符驱动程序操作
ioctl 接口
大部分驱动需要 -- 除了读写设备的能力 -- 通过设备驱动进行各种硬件控制的能力. 大部分设备可进行超出简单的数据传输之外的操作; 用户空间必须常常能够请求, 例如, 设备锁上它的门, 弹出它的介质, 报告错误信息, 改变波特率,或者自我销毁. 这些操作常常通过 ioctl 方法来支持, 它通过相同名子的系统调用来实现.在用户空间, ioctl 系统调用有下面的原型:
int ioctl(int fd, unsigned long cmd, ...);
因此, 原型中的点不表示一个变数目的参数, 而是一个单个可选的参数, 传统上标识为 char *argp. 这些点在那里只是为了阻止在编译时的类型检查. 第 3 个参数的实际特点依赖所发出的特定的控制命令( 第 2 个参数 ). 一些命令不用参数, 一些用一个整数值, 以及一些使用指向其他数据的指针. 使用一个指针是传递任意数据到 ioctl 调用的方法; 设备接着可与用户空间交换任何数量的数据.
ioctl 驱动方法有和用户空间版本不同的原型:
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
根据 Linux 内核惯例来为你的驱动选择 ioctl 号, 你应当首先检查 include/asm/ioctl.h 和 Documentation/ioctl-number.txt. 这个头文件定义你将使用的位段: type(魔数), 序号, 传输方向, 和参数大小. ioctl-number.txt 文件列举了在内核中使用的魔数, 因此你将可选择你自己
的魔数并且避免交叠. 这个文本文件也列举了为什么应当使用惯例的原因.
定义 ioctl 命令号的正确方法使用 4 个位段, 它们有下列的含义. 这个列表中介绍的新符号定义在 <linux/ioctl.h>.
type
魔数. 只是选择一个数(在参考了 ioctl-number.txt之后)并且使用它在整个驱动中. 这个成员是 8 位宽(_IOC_TYPEBITS).
number
序(顺序)号. 它是 8 位(_IOC_NRBITS)宽.
direction
数据传送的方向,如果这个特殊的命令涉及数据传送. 可能的值是 _IOC_NONE(没有数据传输), _IOC_READ, _IOC_WRITE, 和
_IOC_READ|_IOC_WRITE (数据在2个方向被传送). 数据传送是从应用程序的观点来看待的; _IOC_READ 意思是从设备读, 因此设备必须写到用户空间. 注意这个成员是一个位掩码, 因此 _IOC_READ 和 _IOC_WRITE 可使用一个逻辑 AND 操作来抽取.
size
涉及到的用户数据的大小. 这个成员的宽度是依赖体系的, 但是常常是 13 或者 14 位. 你可为你的特定体系在宏 _IOC_SIZEBITS 中找到它的值你使用这个 size 成员不是强制的 - 内核不检查它 -- 但是它是一个好主意. 正确使用这个成员可帮助检测用户空间程序的错误并使你实现向后兼容, 如果你曾需要改变相关数据项的大小. 如果你需要更大的数据结构但是, 你可忽略这个 size 成员. 我们很快见到如何使用这个成员.
头文件 <asm/ioctl.h>, 它包含在 <linux/ioctl.h> 中, 定义宏来帮助建立命令号, 如下: _IO(type,nr)(给没有参数的命令), _IOR(type, nre, datatype)(给从驱动中读数据的), _IOW(type,nr,datatype)(给写数据), 和 _IOWR(type,nr,datatype)(给双向传送). type 和 number 成员作为参数被传递, 并且 size 成员通过应用 sizeof 到 datatype 参数而得到.
下面是一些 ioctl 命令如何在 scull 被定义的. 特别地, 这些命令设置和获得驱动的可配置参数.
/* Use 'k' as magic number */
#define SCULL_IOC_MAGIC 'k'
/* Please use a different 8-bit number in your code */
#define SCULL_IOCRESET _IO(SCULL_IOC_MAGIC, 0)
/*
* S means "Set" through a ptr,
* T means "Tell" directly with the argument value
* G means "Get": reply by setting through a pointer
* Q means "Query": response is on the return value
* X means "eXchange": switch G and S atomically
* H means "sHift": switch T and Q atomically
*/
#define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC, 1, int)
#define SCULL_IOCSQSET _IOW(SCULL_IOC_MAGIC, 2, int)
#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC, 3)
#define SCULL_IOCTQSET _IO(SCULL_IOC_MAGIC, 4)
#define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC, 5, int)
#define SCULL_IOCGQSET _IOR(SCULL_IOC_MAGIC, 6, int)
#define SCULL_IOCQQUANTUM _IO(SCULL_IOC_MAGIC, 7)
#define SCULL_IOCQQSET _IO(SCULL_IOC_MAGIC, 8)
#define SCULL_IOCXQUANTUM _IOWR(SCULL_IOC_MAGIC, 9, int)
#define SCULL_IOCXQSET _IOWR(SCULL_IOC_MAGIC,10, int)
#define SCULL_IOCHQUANTUM _IO(SCULL_IOC_MAGIC, 11)
#define SCULL_IOCHQSET _IO(SCULL_IOC_MAGIC, 12)
#define SCULL_IOC_MAXNR 14
除了少数几个预定义的命令(马上就讨论), ioctl 的 cmd 参数的值当前不被内核使用, 并且在将来也很不可能. 因此, 你可以, 如果你觉得懒, 避免前面展示的复杂的声明并明确声明一组调整数字. 另一方面, 如果你做了, 你不会从使用这些位段中获益, 并且你会遇到困难如果你曾提交你的代码来包含在主线内核中. 头文件<linux/kd.h> 是这个老式方法的例子, 使用 16-位的调整值来定义 ioctl 命令. 那个源代码依靠调整数因为使用那个时候遵循的惯例, 不是由于懒惰. 现在改变它可能导致无理由的不兼容.
预定义命令
尽管 ioctl 系统调用最常用来作用于设备, 内核能识别几个命令. 注意这些命令, 当用到你的设备时, 在你自己的文件操作被调用之前被解码. 因此, 如果你选择相同的号给一个你的 ioctl命令, 你不会看到任何的给那个命令的请求, 并且应用程序获得某些不期望的东西, 因为在 ioctl 号之间的冲突.
预定义命令分为 3 类:
可对任何文件发出的(常规, 设备, FIFO, 或者 socket) 的那些.
只对常规文件发出的那些.
对文件系统类型特殊的那些.
设备驱动编写者只对第一类命令感兴趣, 它们的魔数是 "T".
下列 ioctl 命令是预定义给任何文件, 包括设备特殊的文件:
FIOCLEX
设置 close-on-exec 标志(File IOctl Close on EXec). 设置这个标志使
文件描述符被关闭, 当调用进程执行一个新程序时.
FIONCLEX
清除 close-no-exec 标志(File IOctl Not CLose on EXec). 这个命令恢
复普通文件行为, 复原上面 FIOCLEX 所做的. FIOASYNC 为这个文件设置
或者复位异步通知(如同在本章中"异步通知"一节中讨论的). 注意直到
Linux 2.2.4 版本的内核不正确地使用这个命令来修改 O_SYNC 标志. 因
为两个动作都可通过 fcntl 来完成, 没有人真正使用 FIOASYNC 命令,
它在这里出现只是为了完整性.
FIOQSIZE
这个命令返回一个文件或者目录的大小; 当用作一个设备文件, 但是, 它
返回一个 ENOTTY 错误.
FIONBIO
"File IOctl Non-Blocking I/O"(在"阻塞和非阻塞操作"一节中描述). 这个调用修改在 filp->f_flags 中的 O_NONBLOCK 标志. 给这个系统调用的第 3 个参数用作指示是否这个标志被置位或者清除. (我们将在本章看到这个标志的角色). 注意常用的改变这个标志的方法是使用 fcntl 系统调用, 使用 F_SETFL 命令.
使用 ioctl 参数
在看 scull 驱动的 ioctl 代码之前, 我们需要涉及的另一点是如何使用这个额外的参数. 如果它是一个整数, 就容易: 它可以直接使用. 如果它是一个指针, 但是, 必须小心些.当用一个指针引用用户空间, 我们必须确保用户地址是有效的. 试图存取一个没验证过的用户提供的指针可能导致不正确的行为, 一个内核 oops, 系统崩溃, 或者安全问题.
在第 3 章, 我们看了 copy_from_user 和 copy_to_user 函数, 它们可用来安全地移动数据到和从用户空间. 这些函数也可用在 ioctl 方法中, 但是 ioctl 调用常常包含小数据项, 可通过其他方法更有效地操作. 开始, 地址校验(不传送数据)由函数 access_ok 实现, 它定义在 <asm/uaccess.h>:
int access_ok(int type, const void *addr, unsigned long size);
第一个参数应当是 VERIFY_READ 或者 VERIFY_WRITE, 依据这个要进行的动作是否是读用户空间内存区或者写它. addr 参数持有一个用户空间地址, size 是一个字节量.
在调用 access_ok 之后, 驱动可安全地进行真正的传输. 加上 copy_from_user 和 copy_to_user_ 函数, 程序员可利用一组为被最多使用的数据大小(1, 2, 4, 和 8 字节)而优化过的函数. 这些函数在下面列表中描述, 它们定义在 <asm/uaccess.h>:
put_user(datum, ptr)
__put_user(datum, ptr)
get_user(local, ptr)
__get_user(local, ptr)
兼容性和受限操作
存取一个设备由设备文件上的许可权控制, 并且驱动正常地不涉及到许可权的检查. 但是, 有些情形, 在保证给任何用户对设备的读写许可的地方, 一些控制操作仍然应当被拒绝.内核在许可权管理上排他地使用能力, 并且输出 2 个系统调用 capget 和 capset, 来允许它们被从用户空间管理. 全部能力可在 <linux/capability.h> 中找到.
在进行一个特权操作之前, 一个设备驱动应当检查调用进程有合适的能力; 不这样做可能导致用户进程进行非法的操作, 对系统的稳定和安全有坏的后果. 能力检查是通过 capable 函数来进行的(定义在 <linux/sched.h>):
int capable(int capability);
阻塞 I/O
回顾第 3 章, 我们看到如何实现 read 和 write 方法. 在此, 但是, 我们跳过了一个重要的问题:一个驱动当它无法立刻满足请求应当如何响应? 一个对 read 的调用可能当没有数据时到来, 而以后会期待更多的数据. 或者一个进程可能试图写, 但是你的设备没有准备好接受数据, 因为你的输出缓冲满了. 调用进程往往不关心这种问题; 程序员只希望调用 read 或 write 并且使调用返回, 在必要的工作已完成后. 这样, 在这样的情形中, 你的驱动应当(缺省地)阻塞进程, 使它进入睡眠直到请求可继续.睡眠的介绍
(这里的睡眠即表示休眠)
对于一个进程"睡眠"意味着什么? 当一个进程被置为睡眠, 它被标识为处于一个特殊的状态并且从调度器的运行队列中去除. 直到发生某些事情改变了那个状态, 这个进程将不被在任何 CPU 上调度, 并且, 因此, 将不会运行. 一个睡着的进程已被搁置到系统的一边, 等待以后发生事件.
对于一个 Linux 驱动使一个进程睡眠是一个容易做的事情. 但是, 有几个规则必须记住以安全的方式编码睡眠.
这些规则的第一个是: 当你运行在原子上下文时不能睡眠.
另一件要记住的事情是, 当被唤醒时, 你从不知道你的进程离开 CPU 多长时间或者同时已经发生了什么改变. 你也常常不知道是否另一个进程已经睡眠等待同一个事件; 那个进程可能在你之前醒来并且获取了你在等待的资源. 结果是你不能关于你醒后的系统状态做任何的假设, 并且你必须检查来确保你在等待的条件确实是真的。
一个另外的相关的点, 当然, 是你的进程不能睡眠除非确信其他人, 在某处的, 将唤醒它.
在 Linux 中, 一个等待队列由一个"等待队列头"来管理, 一个 wait_queue_head_t 类型的结构, 定义在<linux/wait.h>中.
简单睡眠
Linux 内核中睡眠的最简单方式是一个宏定义, 称为 wait_event(有几个变体); 它结合了处理睡眠的细节和进程在等待的条件的检查. wait_event 的形式是:wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
在所有上面的形式中, queue 是要用的等待队列头.
过程的另一半, 当然是唤醒. 一些其他的执行线程(一个不同的进程, 或者一个中断处理, 也许)必须为你进行唤醒, 因为你的进程, 当然, 是在睡眠. 基本的唤醒睡眠进程的函数称为 wake_up. 它有几个形式(但是我们现在只看其中 2 个):
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
阻塞和非阻塞操作
有时还有调用进程通知你他不想阻塞, 不管它的 I/O 是否继续. 明确的非阻塞 I/O 由 filp->f_flags 中的 O_NONBLOCK 标志来指示. 这个标志定义于 <linux/fcntl.h>, 被 <linux/fs.h>自动包含. 这个标志得名自"打开-非阻塞", 因为它可在打开时指定(并且起初只能在那里指定).
如果指定 O_NONBLOCK, read 和 write 的行为是不同的. 在这个情况下, 这个调用简单地返回 -EAGAIN(("try it agin")如果一个进程当没有数据可用时调用 read , 或者如果当缓冲中没有空间时它调用 write .
常常地, 打开一个设备或者成功或者失败, 没有必要等待外部的事件. 有时, 打开设备需要一个长的初始化, 这时你可能选择在你的 open 方法中支持 O_NONBLOCK , 如果这个标志被设置,在开始这个设备的初始化之后.立刻返回 -EAGAIN,
只有 read, write, 和 open 文件操作受到非阻塞标志影响.
手动睡眠
程序员如果愿意可以沿用那种方式手动睡眠; <linux/sched.h> 包含了所有需要的定义, 以及围绕例子的内核源码. 具体参见书中157页。
独占等待
为应对实际世界中的惊群问题, 内核开发者增加了一个"独占等待"选项到内核中. 一个互斥等待的行为非常象一个正常的睡眠, 有 2 个重要的不同:
当一个等待队列入口有 WQ_FLAG_EXCLUSEVE 标志置位, 它被添加到等待队列的尾部. 没有这个标志的入口项, 相反, 添加到开始.
当 wake_up 被在一个等待队列上调用, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止.
最后的结果是进行互斥等待的进程被一次唤醒一个, 以顺序的方式, 并且没有引起惊群问题. 但内核仍然每次唤醒所有的非互斥等待者.
poll 和 select
使用非阻塞 I/O 的应用程序常常使用 poll, select, 和 epoll 系统调用. poll, select 和 epoll 本质上有相同的功能: 每个允许一个进程来决定它是否可读或者写一个或多个文件而不阻塞. 这些调用也可阻塞进程直到任何一个给定集合的文件描述符可用来读或写. 因此, 它们常常用在必须使用多输入输出流的应用程序, 而不必粘连在它们任何一个上. 相同的功能常常由多个函数提供, 因为 2 个是由不同的团队在几乎相同时间完成的: select 在 BSD Unix 中引入, 而 poll 是 System V 的解决方案. epoll 调用添加在 2.5.45, 作为使查询函数扩展到几千个文件描述符的方法.支持任何一个这些调用都需要来自设备驱动的支持. 这个支持(对所有 3 个调用)由驱动的 poll 方法调用. 这个方法由下列的原型:
unsigned int (*poll) (struct file *filp, poll_table *wait);
驱动增加一个等待队列到 poll_table 结构通过调用函数 poll_wait:
void poll_wait (struct file *, wait_queue_head_t *, poll_table *);
异步通知
尽管阻塞和非阻塞操作和 select 方法的结合对于查询设备在大部分时间是足够的, 一些情况还不能被我们迄今所见到的技术来有效地解决.
让我们想象一个进程, 在低优先级上执行一个长计算循环, 但是需要尽可能快的处理输入数据. 如果这个进程在响应新的来自某些数据获取外设的报告, 它应当立刻知道当新数据可用时. 这个应用程序可能被编写来调用 poll 有规律地检查数据, 但是, 对许多情况, 有更好的方法. 通过使能异步通知, 这个应用程序可能接受一个信号无论何时数据可用并且不需要让自己去查询.
用户程序必须执行 2 个步骤来使能来自输入文件的异步通知. 首先, 它们指定一个进程作为文件的拥有者. 当一个进程使用 fcntl 系统调用发出 F_SETOWN 命令, 这个拥有者进程的 ID 被保存在 filp->f_owner 给以后使用. 这一步对内核知道通知谁是必要的. 为了真正使能异步通知, 用户程序必须设置 FASYNC 标志在设备中, 通过 F_SETFL fcntl 命令.
在这 2 个调用已被执行后, 输入文件可请求递交一个 SIGIO 信号, 无论何时新数据到达. 信号被发送给存储于 filp->f_owner 中的进程(或者进程组, 如果值为负值).
还有一个问题. 当一个进程收到一个 SIGIO, 它不知道哪个输入文件有新数据提供. 如果多于一个文件被使能异步地通知挂起输入的进程, 应用程序必须仍然靠 poll 或者 select 来找出发生了什么.
llseek 实现
llseek 方法实现了 lseek 和 llseek 系统调用. 我们已经说了如果 llseek 方法从设备的操作中缺失, 内核中的缺省的实现进行移位通过修改 filp->f_pos, 这是文件中的当前读写位置. 请注意对于 lseek 系统调用要正确工作, 读和写方法必须配合, 通过使用和更新它们收到的作为的参数的 offset 项.独享设备
提供存取控制的强力方式是只允许一个设备一次被一个进程打开(单次打开). 这个技术最好是避免因为它限制了用户的灵活性. 一个用户可能想 在一个设备上 运行不同的进程 , 一个读状态信息,而另一个写数据.限制每次只由一个用户访问
单打开设备之外的下一步是使一个用户在多个进程中打开一个设备, 但是一次只允许一个用户打开设备. 这个解决方案使得容易测试设备, 因为用户一次可从几个进程读写, 但是假定这个用户负责维护在多次存取中的数据完整性. 这通过在 open 方法中添加检查来实现; 这样的检查在通常的许可检查后进行, 并且比由拥有者和组许可位所指定的限制存取更加严格.
快速参考
#include <linux/ioctl.h>声明用来定义 ioctl 命令的宏定义. 当前被 <linux/fs.h> 包含.
_IOC_NRBITS
_IOC_TYPEBITS
_IOC_SIZEBITS
_IOC_DIRBITS
ioctl 命令的不同位段所使用的位数. 还有 4 个宏来指定 MASK 和 4 个
指定 SHIFT, 但是它们主要是给内部使用. _IOC_SIZEBIT 是一个要检查的
重要的值, 因为它跨体系改变.
_IOC_NONE
_IOC_READ
_IOC_WRITE
"方向"位段可能的值. "read" 和 "write" 是不同的位并且可相或来指定
read/write. 这些值是基于 0 的.
_IOC(dir,type,nr,size)
_IO(type,nr)
_IOR(type,nr,size)
_IOW(type,nr,size)
_IOWR(type,nr,size)
用来创建 ioclt 命令的宏定义.
_IOC_DIR(nr)
_IOC_TYPE(nr)
_IOC_NR(nr)
_IOC_SIZE(nr)
用来解码一个命令的宏定义. 特别地, _IOC_TYPE(nr) 是 _IOC_READ 和
_IOC_WRITE 的 OR 结合.
#include <asm/uaccess.h>
int access_ok(int type, const void *addr, unsigned long size);
检查一个用户空间的指针是可用的. access_ok 返回一个非零值, 如果应
当允许存取.
VERIFY_READ
VERIFY_WRITE
access_ok 中 type 参数的可能取值. VERIFY_WRITE 是 VERIFY_READ 的
超集.
#include <asm/uaccess.h>
int put_user(datum,ptr);
int get_user(local,ptr);
int __put_user(datum,ptr);
int __get_user(local,ptr);
用来存储或获取一个数据到或从用户空间的宏. 传送的字节数依赖
sizeof(*ptr). 常规的版本调用 access_ok , 而常规版本( __put_user
和 __get_user ) 假定 access_ok 已经被调用了.
#include <linux/capability.h>
定义各种 CAP_ 符号, 描述一个用户空间进程可有的能力.
int capable(int capability);
返回非零值如果进程有给定的能力.
#include <linux/wait.h>
typedef struct { /* ... */ } wait_queue_head_t;
void init_waitqueue_head(wait_queue_head_t *queue);
DECLARE_WAIT_QUEUE_HEAD(queue);
Linux 等待队列的定义类型. 一个 wait_queue_head_t 必须被明确在运
行时使用 init_waitqueue_head 或者编译时使用
DEVLARE_WAIT_QUEUE_HEAD 进行初始化.
void wait_event(wait_queue_head_t q, int condition);
int wait_event_interruptible(wait_queue_head_t q, int condition);
int wait_event_timeout(wait_queue_head_t q, int condition, int time);
int wait_event_interruptible_timeout(wait_queue_head_t q, int condition,int time);
使进程在给定队列上睡眠, 直到给定条件值为真值.
void wake_up(struct wait_queue **q);
void wake_up_interruptible(struct wait_queue **q);
void wake_up_nr(struct wait_queue **q, int nr);
void wake_up_interruptible_nr(struct wait_queue **q, int nr);
void wake_up_all(struct wait_queue **q);
void wake_up_interruptible_all(struct wait_queue **q);
void wake_up_interruptible_sync(struct wait_queue **q);
唤醒在队列 q 上睡眠的进程. _interruptible 的形式只唤醒可中断的进
程. 正常地, 只有一个互斥等待者被唤醒, 但是这个行为可被 _nr 或者
_all 形式所改变. _sync 版本在返回之前不重新调度 CPU.
#include <linux/sched.h>
set_current_state(int state);
设置当前进程的执行状态. TASK_RUNNING 意味着它已经运行, 而睡眠状态
是 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE.
void schedule(void);
选择一个可运行的进程从运行队列中. 被选中的进程可是当前进程或者另
外一个.
typedef struct { /* ... */ } wait_queue_t;
init_waitqueue_entry(wait_queue_t *entry, struct task_struct *task);
wait_queue_t 类型用来放置一个进程到一个等待队列.
void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state);
void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);
void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);
帮忙函数, 可用来编码一个手工睡眠.
void sleep_on(wiat_queue_head_t *queue);
void interruptible_sleep_on(wiat_queue_head_t *queue);
老式的不推荐的函数, 它们无条件地使当前进程睡眠.
#include <linux/poll.h>
void poll_wait(struct file *filp, wait_queue_head_t *q, poll_table *p);
将当前进程放入一个等待队列, 不立刻调度. 它被设计来被设备驱动的
poll 方法使用.
int fasync_helper(struct inode *inode, struct file *filp, int mode, struct fasync_struct **fa);
一个"帮忙者", 来实现 fasync 设备方法. mode 参数是传递给方法的相同
的值, 而 fa 指针指向一个设备特定的 fasync_struct *.
void kill_fasync(struct fasync_struct *fa, int sig, int band);
如果这个驱动支持异步通知, 这个函数可用来发送一个信号到登记在 fa
中的进程.
int nonseekable_open(struct inode *inode, struct file *filp);
loff_t no_llseek(struct file *file, loff_t offset, int whence);
nonseekable_open 应当在任何不支持移位的设备的 open 方法中被调用.
这样的设备应当使用 no_llseek 作为它们的 llseek 方法.