LDD3读书笔记-----高级字符驱动操作

来源:互联网 发布:黑数据库 编辑:程序博客网 时间:2024/05/09 11:48

1. ioctl 接口

在用户空间, ioctl 系统调用有下面的原型:

int ioctl(int fd, unsigned long cmd, ...); 

ioctl 驱动方法有和用户空间版本不同的原型:

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数. cmd 参数从用户那里不改变地传下来, 并且可选的参数 arg 参数以一个 unsigned long 的形式传递, 不管它是否由用户给定为一个整数或一个指针. 如果调用程序不传递第 3 个参数, 被驱动操作收到的 arg 值是无定义的. 因为类型检查在这个额外参数上被关闭, 编译器不能警告你如果一个无效的参数被传递给 ioctl, 并且任何关联的错误将难以查找.

选择 ioctl 命令

根据 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 参数而得到.

这个头文件还定义宏, 可被用在你的驱动中来解码这个号: _IOC_DIR(nr), _IOC_TYPE(nr), _IOC_NR(nr), 和 _IOC_SIZE(nr). 我们不进入任何这些宏的细节, 因为头文件是清楚的, 并且在本节稍后有例子代码展示.

返回值:返回 -EINVAL, 对于响应一个无效的 ioctl 命令.

预定义的命令

预定义命令分为 3 类:

  • 可对任何文件发出的(常规, 设备, FIFO, 或者 socket) 的那些.

  • 只对常规文件发出的那些.

  • 对文件系统类型特殊的那些.

最后一类的命令由宿主文件系统的实现来执行(这是 chattr 命令如何工作的). 设备驱动编写者只对第一类命令感兴趣, 它们的魔数是 "T". 查看其他类的工作留给读者作为练习; ext2_ioctl 是最有趣的函数(并且比预期的要容易理解), 因为它实现 append-only 标志和 immutable 标志.

下列 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 命令.

列表中的最后一项介绍了一个新的系统调用, fcntl, 它看来象 ioctl. 事实上, fcntl 调用非常类似 ioctl, 它也是获得一个命令参数和一个额外的(可选地)参数. 它保持和 ioctl 独立主要是因为历史原因: 当 Unix 开发者面对控制 I/O 操作的问题时, 他们决定文件和设备是不同的. 那时, 有 ioctl 实现的唯一设备是 ttys, 它解释了为什么 -ENOTTY 是标准的对不正确 ioctl 命令的回答. 事情已经改变, 但是 fcntl 保留为一个独立的系统调用.

2.阻塞 I/O

睡眠的介绍

对于一个进程"睡眠"意味着什么? 当一个进程被置为睡眠, 它被标识为处于一个特殊的状态并且从调度器的运行队列中去除. 直到发生某些事情改变了那个状态, 这个进程将不被在任何 CPU 上调度, 并且, 因此, 将不会运行. 一个睡着的进程已被搁置到系统的一边, 等待以后发生事件.

睡眠规则

第一个是: 当你运行在原子上下文时不能睡眠。

持有一个自旋锁, seqlock, 或者 RCU 锁时不能睡眠.

已关闭中断你也不能睡眠

在 Linux 中, 一个等待队列由一个"等待队列头"来管理, 一个 wait_queue_head_t 类型的结构, 定义在<linux/wait.h>中. 一个等待队列头可被定义和初始化, 使用:

DECLARE_WAIT_QUEUE_HEAD(name); 

或者动态地, 如下:

wait_queue_head_t my_queue;init_waitqueue_head(&my_queue);

 

简单睡眠

当一个进程睡眠, 它这样做以期望某些条件在以后会成真. 如我们之前注意到的, 任何睡眠的进程必须在它再次醒来时检查来确保它在等待的条件真正为真. 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 是要用的等待队列头. 注意它是"通过值"传递的. 条件是一个被这个宏在睡眠前后所求值的任意的布尔表达式; 直到条件求值为真值, 进程继续睡眠. 注意条件可能被任意次地求值, 因此它不应当有任何边界效应.

如果你使用 wait_event, 你的进程被置为不可中断地睡眠, 如同我们之前已经提到的, 它常常不是你所要的. 首选的选择是 wait_event_interruptible, 它可能被信号中断. 这个版本返回一个你应当检查的整数值; 一个非零值意味着你的睡眠被某些信号打断, 并且你的驱动可能应当返回 -ERESTARTSYS. 最后的版本(wait_event_timeout 和 wait_event_interruptible_timeout)等待一段有限的时间; 在这个时间期间(以嘀哒数表达的)超时后, 这个宏返回一个 0 值而不管条件是如何求值的.

另一半, 当然, 是唤醒. 一些其他的执行线程(一个不同的进程, 或者一个中断处理, 也许)必须为你进行唤醒, 因为你的进程, 当然, 是在睡眠. 基本的唤醒睡眠进程的函数称为 wake_up. 它有几个形式(但是我们现在只看其中 2 个):
void wake_up(wait_queue_head_t *queue);void wake_up_interruptible(wait_queue_head_t *queue);

wake_up 唤醒所有的在给定队列上等待的进程(尽管这个情形比那个要复杂一些, 如同我们之后将见到的). 其他的形式(wake_up_interruptible)限制它自己到处理一个可中断的睡眠. 通常, 这 2 个是不用区分的(如果你使用可中断的睡眠); 实际上, 惯例是使用 wake_up 如果你在使用 wait_event , wake_up_interruptible 如果你在使用 wait_event_interruptible.

阻塞和非阻塞操作

明确的非阻塞 I/O 由 filp->f_flags 中的 O_NONBLOCK 标志来指示. 这个标志定义于 <linux/fcntl.h>, 被 <linux/fs.h>自动包含. 这个标志得名自"打开-非阻塞", 因为它可在打开时指定(并且起初只能在那里指定). 如果你浏览源码, 你会发现一些对一个 O_NDELAY 标志的引用; 这是一个替代 O_NONBLOCK 的名子, 为兼容 System V 代码而被接受的. 这个标志缺省地被清除, 因为一个等待数据的进程的正常行为仅仅是睡眠. 在一个阻塞操作的情况下, 这是缺省地, 下列的行为应当实现来符合标准语法:

  • 如果一个进程调用 read 但是没有数据可用(尚未), 这个进程必须阻塞. 这个进程在有数据达到时被立刻唤醒, 并且那个数据被返回给调用者, 即便小于在给方法的 count 参数中请求的数量.

  • 如果一个进程调用 write 并且在缓冲中没有空间, 这个进程必须阻塞, 并且它必须在一个与用作 read 的不同的等待队列中. 当一些数据被写入硬件设备, 并且在输出缓冲中的空间变空闲, 这个进程被唤醒并且写调用成功, 尽管数据可能只被部分写入如果在缓冲只没有空间给被请求的 count 字节.

这 2 句都假定有输入和输出缓冲; 实际上, 几乎每个设备驱动都有. 要求有输入缓冲是为了避免丢失到达的数据, 当无人在读时. 相反, 数据在写时不能丢失, 因为如果系统调用不能接收数据字节, 它们保留在用户空间缓冲. 即便如此, 输出缓冲几乎一直有用, 对于从硬件挤出更多的性能.

3.非阻塞 I/O

使用非阻塞 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, select, 或者 epoll 系统调用, 涉及一个和驱动相关的文件描述符. 这个设备方法负责这 2 步:

  • 1. 在一个或多个可指示查询状态变化的等待队列上调用 poll_wait. 如果没有文件描述符可用作 I/O, 内核使这个进程在等待队列上等待所有的传递给系统调用的文件描述符.

  • 2. 返回一个位掩码, 描述可能不必阻塞就立刻进行的操作.

这 2 个操作常常是直接的, 并且趋向与各个驱动看起来类似. 但是, 它们依赖只能由驱动提供的信息, 因此, 必须由每个驱动单独实现.

这 2 个操作常常是直接的, 并且趋向与各个驱动看起来类似. 但是, 它们依赖只能由驱动提供的信息, 因此, 必须由每个驱动单独实现.

poll_table 结构, 给 poll 方法的第 2 个参数, 在内核中用来实现 poll, select, 和 epoll 调用; 它在 <linux/poll.h>中声明, 这个文件必须被驱动源码包含. 驱动编写者不必要知道所有它内容并且必须作为一个不透明的对象使用它; 它被传递给驱动方法以便驱动可用每个能唤醒进程的等待队列来加载它, 并且可改变 poll 操作状态. 驱动增加一个等待队列到 poll_table 结构通过调用函数 poll_wait:

 void poll_wait (struct file *, wait_queue_head_t *, poll_table *); 

poll 方法的第 2 个任务是返回位掩码, 它描述哪个操作可马上被实现; 这也是直接的. 例如, 如果设备有数据可用, 一个读可能不必睡眠而完成; poll 方法应当指示这个时间状态. 几个标志(通过 <linux/poll.h> 定义)用来指示可能的操作:

POLLIN

如果设备可被不阻塞地读, 这个位必须设置.

POLLRDNORM

这个位必须设置, 如果"正常"数据可用来读. 一个可读的设备返回( POLLIN|POLLRDNORM ).

POLLRDBAND

这个位指示带外数据可用来从设备中读取. 当前只用在 Linux 内核的一个地方( DECnet 代码 )并且通常对设备驱动不可用.

POLLPRI

高优先级数据(带外)可不阻塞地读取. 这个位使 select 报告在文件上遇到一个异常情况, 因为 selct 报告带外数据作为一个异常情况.

POLLHUP

当读这个设备的进程见到文件尾, 驱动必须设置 POLLUP(hang-up). 一个调用 select 的进程被告知设备是可读的, 如同 selcet 功能所规定的.

POLLERR

一个错误情况已在设备上发生. 当调用 poll, 设备被报告位可读可写, 因为读写都返回一个错误码而不阻塞.

POLLOUT

这个位在返回值中设置, 如果设备可被写入而不阻塞.

POLLWRNORM

这个位和 POLLOUT 有相同的含义, 并且有时它确实是相同的数. 一个可写的设备返回( POLLOUT|POLLWRNORM).

POLLWRBAND

如同 POLLRDBAND , 这个位意思是带有零优先级的数据可写入设备. 只有 poll 的数据报实现使用这个位, 因为一个数据报看传送带外数据.

应当重复一下 POLLRDBAND 和 POLLWRBAND 仅仅对关联到 socket 的文件描述符有意义: 通常设备驱动不使用这些标志.

与 read 和 write 的交互

从设备中读数据

  • 如果在输入缓冲中有数据, read 调用应当立刻返回, 没有可注意到的延迟, 即便数据少于应用程序要求的, 并且驱动确保其他的数据会很快到达. 你可一直返回小于你被请求的数据, 如果因为任何理由而方便这样(我们在 scull 中这样做), 如果你至少返回一个字节. 在这个情况下, poll 应当返回 POLLIN|POLLRDNORM.

  • 如果在输入缓冲中没有数据, 缺省地 read 必须阻塞直到有一个字节. 如果 O_NONBLOCK 被置位, 另一方面, read 立刻返回 -EAGIN (尽管一些老版本 SYSTEM V 返回 0 在这个情况时). 在这些情况中, poll 必须报告这个设备是不可读的直到至少一个字节到达. 一旦在缓冲中有数据, 我们就回到前面的情况.

  • 如果我们处于文件尾, read 应当立刻返回一个 0, 不管是否阻塞. 这种情况 poll 应该报告 POLLHUP.

写入设备

  • 如果在输出缓冲中有空间, write 应当不延迟返回. 它可接受小于这个调用所请求的数据, 但是它必须至少接受一个字节. 在这个情况下, poll 报告这个设备是可写的, 通过返回 POLLOUT|POLLWRNORM.

  • 如果输出缓冲是满的, 缺省地 write 阻塞直到一些空间被释放. 如果 O_NOBLOCK 被设置, write 立刻返回一个 -EAGAIN(老式的 System V Unices 返回 0). 在这些情况下, poll 应当报告文件是不可写的. 另一方面, 如果设备不能接受任何多余数据, write 返回 -ENOSPC("设备上没有空间"), 不管是否设置了 O_NONBLOCK.

  • 在返回之前不要调用 wait 来传送数据, 即便当 O_NONBLOCK 被清除. 这是因为许多应用程序选择来找出一个 write 是否会阻塞. 如果设备报告可写, 调用必须不阻塞. 如果使用设备的程序想保证它加入到输出缓冲中的数据被真正传送, 驱动必须提供一个 fsync 方法. 例如, 一个可移除的设备应当有一个 fsnyc 入口.

尽管有一套通用的规则, 还应当认识到每个设备是唯一的并且有时这些规则必须稍微弯曲一下. 例如, 面向记录的设备(例如磁带设备)无法执行部分写.

刷新挂起的输出

我们已经见到 write 方法如何自己不能解决全部的输出需要. fsync 函数, 由同名的系统调用而调用, 填补了这个空缺. 这个方法原型是:

 int (*fsync) (struct file *file, struct dentry *dentry, int datasync); 

如果一些应用程序需要被确保数据被发送到设备, fsync 方法必须被实现为不管 O_NONBLOCK 是否被设置. 对 fsync 的调用应当只在设备被完全刷新时返回(即, 输出缓冲为空), 即便这需要一些时间. datasync 参数用来区分 fsync 和 fdatasync 系统调用; 这样, 它只对文件系统代码有用, 驱动可以忽略它.

fsync 方法没有不寻常的特性. 这个调用不是时间关键的, 因此每个设备驱动可根据作者的口味实现它. 大部分的时间, 字符驱动只有一个 NULL 指针在它们的 fops 中. 阻塞设备, 另一方面, 常常实现这个方法使用通用的 block_fsync, 它接着会刷新设备的所有的块.

poll_table 结构只是对一个函数的封装, 这个函数建立了实际的数据结构. 那个数据结构, 对于 poll和 select, 是一个内存页的链表, 其中包含 poll_table_entry 结构. 每个 poll_table_entry 持有被传递给 poll_wait 的 struct file 和 wait_queue_head_t 指针, 以及一个关联的等待队列入口. 对 poll_wait 的调用有时还添加这个进程到给定的等待队列. 整个的结构必须由内核维护以至于这个进程可被从所有的队列中去除, 在 poll 或者 select 返回之前.

  异步通知

尽管阻塞和非阻塞操作和 select 方法的结合对于查询设备在大部分时间是足够的, 一些情况还不能被我们迄今所见到的技术来有效地解决.