为了技术,我不会停下学习的脚步,我相信我还能走二十年。
分类:
2012-07-01 21:12:07
原文地址:块设备驱动(一) 作者:embededgood
概括:
块设备驱动第一个工作通常是注册自己到内核,是通过register_blkdev完成的,虽然register_blkdev可用来获得一个主设备号,但是它不使磁盘驱动器对系统可用,有一个分开的注册接口你必须使用来管理单独的驱动器,使用这一接口用到block_device_operations和gendisk结构体,在linux内核中,使用gendisk(通用磁盘)结构体来表示1个独立的磁盘设备(或分区)。
块设备驱动的核心是请求处理函数和“制造请求”函数,所谓请求处理函数或“制造请求”函数就是我们所说的IO请求处理,请求处理函数的介绍引入了request结构体,请求函数的原型是void request(request_queue_t *queue);而不使用请求处理或者“制造请求”函数,它的的主要参数是bio结构体;
定义:
系统中能够随机(不需要按顺序)访问固定大小数据片(chunks)的设备被称为块设备。
这些数据片就称为块,最常见的是硬盘,还有软盘驱动器,CD-ROM驱动器和闪存等,注意,它们都是以安装文件系统的方式使用的——这也是块设备一般的访问方式。
与字符设备的区别:
1、 字符设备按照字符流的方式被有序访问,如串口和键盘就都属于字符设备,如果一个硬件设备是以字符流的方式访问的话,那就应该将它归于字符设备,反过来,如果一个设备是随机(无序的)访问的,那么它就属于块设备。
2、 根本区别是它们能否可以被随机访问,也就是说,能否在访问设备时随意的从一个位置跳转到另一个位置。
3、 块设备只能以块为单位接受输入和返回输出,而字符设备以字节为单位。
4、 块设备对于IO请求有对应的缓冲区,因此它们可以选择以什么顺序进行响应,字符设备无须缓冲且被直接读写。
5、 字符设备只能被顺序读写,而块设备可以随机访问。
解剖一个块设备
块设备中最小的可寻址单元是扇区,扇区大小一般是2的整数倍,而最常见的大小是512个字节,扇区的大小是设备的物理属性,扇区是所有块设备的基本单元——块设备无法对比它还小的单元进行寻址和操作,不过许多块设备能够一次就传输多个扇区。
虽然各种软件的用途不同,但是它们都会用到自己的最小逻辑可寻址单元——块,块是文件系统一种抽象——只能基于块来访问文件系统,虽然物理磁盘寻址是按扇区级进行的,但是内核执行的所有磁盘操作都是按照块进行的,由于扇区是设备的最小可寻址单元,所以块不能比扇区还小,只能数倍于扇区大小。
扇区对内核的重要性在于所有设备的IO 操作都必须基于扇区来进行,反过来,块是内核使用的较高层概念,它是比扇区高一层的抽象。
一、块设备的注册与注销
块驱动, 象字符驱动, 必须使用一套注册接口来使内核可使用它们的设备. 概念是类似的, 但是块设备注册的细节是都不同的.
大部分块驱动采取的第一步是注册它们自己到内核. 这个任务的函数是 register_blkdev(在
int register_blkdev(unsigned int major, const char *name);
参数是你的设备要使用的主编号和关联的名子(内核将显示它在 /proc/devices). 如果 major 传递为0, 内核分配一个新的主编号并且返回它给调用者. 如常, 自 register_blkdev 的一个负的返回值指示已发生了一个错误.
取消注册的对应函数是:
int unregister_blkdev(unsigned int major, const char *name);
这里, 参数必须匹配传递给 register_blkdev 的那些, 否则这个函数返回 -EINVAL 并且什么都不注销.
在2.6内核, 对 register_blkdev 的调用完全是可选的. 由 register_blkdev 所进行的功能已随时间正在减少; 这个调用唯一的任务是
(1) 如果需要, 分配一个动态主设备号
(2) 在 /proc/devices 创建一个入口.
在将来的内核, register_blkdev 可能被一起去掉. 同时, 但是, 大部分驱动仍然调用它; 它是惯例.
磁盘注册
虽然 register_blkdev 可用来获得一个主编号, 它不使任何磁盘驱动器对系统可用. 有一个分开的注册接口你必须使用来管理单独的驱动器. 使用这个接口要求熟悉一对新结构, 这就是我们的起点.
块设备操作
字符设备通过 file_ 操作结构使它们的操作对系统可用. 一个类似的结构用在块设备上; 它是 struct block_device_operations, 定义在
int (*open)(struct inode *inode, struct file *filp);
int (*release)(struct inode *inode, struct file *filp);
就像它们的字符驱动对等体一样工作的函数; 无论何时设备被打开和关闭都调用它们. 一个字符驱动可能通过启动设备或者锁住门(为可移出的介质)来响应一个 open 调用. 如果你将介质锁入设备, 你当然应当在 release 方法中解锁.当然一个简单的块设备驱动可以不提供open和release函数。
int (*ioctl)(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
实现 ioctl 系统调用的方法. 但是, 高层的块设备层代码处理了绝大多数ioctl(),因此大部分的块驱动 ioctl 方法相当短,也就是说具体的块设备驱动中不需要实现很多的ioctl命令。
int (*media_changed) (struct gendisk *gd);
被内核调用来检查是否用户已经改变了驱动器中的介质的方法, 如果是这样返回一个非零值. 显然, 这个方法仅适用于支持可移出的介质的驱动器(并且最好给驱动一个"介质被改变"标志); 在其他情况下可被忽略.
struct gendisk 参数是内核任何表示单个磁盘; 我们将在下一节查看这个结构.
int (*revalidate_disk) (struct gendisk *gd);
revalidate_disk 方法被调用来响应一个介质改变; 它给驱动一个机会来进行需要的任何工作使新介质准备好使用. 这个函数返回一个 int 值, 但是值被内核忽略.
struct module *owner;
一个指向拥有这个结构的模块的指针; 它应当常常被初始化为 THIS_MODULE.
专心的读者可能已注意到这个列表一个有趣的省略: 没有实际读或写数据的函数. 在块 I/O 子系统, 这些操作由请求函数处理, 它们应当有它们自己的一节并且在本章后面讨论. 在我们谈论服务请求之前, 我们必须完成对磁盘注册的讨论.
gendisk 结构
struct gendisk (定义于
int major; 主设备号
int first_minor; 第一个次设备号
int minors; 最大的次设备数,如果不能分区,则为1
描述被磁盘使用的设备号的成员. 同一磁盘的各个分区共享一个注设备号,而次设备号则不同,至少, 一个驱动器必须使用最少一个次编号. 如果你的驱动会是可分区的, 但是(并且大部分应当是), 你要分配一个次编号给每个可能的分区. 次编号的一个普通的值是 16, 它允许"全磁盘"设备盒 15 个分区. 一些磁盘驱动使用 64 个次编号给每个设备.
char disk_name[32];
应当被设置为磁盘驱动器名子的成员. 它出现在 /proc/partitions 和 sysfs.
struct block_device_operations *fops;
来自前一节的块设备操作集合.
struct request_queue *queue;
被内核用来管理这个设备的 I/O 请求的结构; 我们在"请求处理"一节中检查它.
int flags;
一套标志(很少使用), 描述驱动器的状态. 如果你的设备有可移出的介质, 你应当设置 GENHD_FL_REMOVABLE. CD-ROM 驱动器可设置 GENHD_FL_CD. 如果, 由于某些原因, 你不需要分区信息出现在 /proc/partitions, 设置 GENHD_FL_SUPPRESS_PARTITIONS_INFO.
sector_t capacity;
这个驱动器的容量, 以512-字节扇区来计. sector_t 类型可以是 64 位宽. 驱动不应当直接设置这个成员; 相反, 传递扇区数目给 set_capacity.
void *private_data;
块驱动可使用这个成员作为一个指向它们自己内部数据的指针.
内核提供了一小部分函数来使用 gendisk 结构. 我们在这里介绍它们, 接着看 sbull 如何使用它们来使系统可使用它的磁盘驱动器.
struct gendisk 是一个动态分配的结构, 它需要特别的内核操作来初始化; 驱动不能自己分配这个结构. 相反, 你必须调用:
struct gendisk *alloc_disk(int minors);
minors 参数应当是这个磁盘使用的次编号数目; 注意你不能在之后改变 minors 成员并且期望事情可以正确工作. 当不再需要一个磁盘时, 它应当被释放, 使用:
void del_gendisk(struct gendisk *gd);
一个 gendisk 是一个被引用计数的结构(它含有一个 kobject). 有 get_disk 和 put_disk 函数用来操作引用计数, 但是驱动应当从不需要做这个. 正常地, 对 del_gendisk 的调用去掉了最一个 gendisk 的最终的引用, 但是不保证这样. 因此, 这个结构可能继续存在(并且你的方法可能被调用)在调用 del_gendisk 之后. 但是, 如果你删除这个结构当没有用户时(即, 在最后的释放之后, 或者在你的模块清理函数), 你可确信你不会再收到它的信息.
分配一个 gendisk 结构不能使系统可使用这个磁盘. 要做到这点, 你必须初始化这个结构并且调用 add_disk:
void add_disk(struct gendisk *gd);
这里记住一件重要的事情:一旦你调用add_disk, 这个磁盘是"活的"并且它的方法可被在任何时间被调用. 实际上, 这样的第一个调用将可能发生, 即便在 add_disk 返回之前; 内核将读前几个字节以试图找到一个分区表. 因此你不应当调用 add_disk 直到你的驱动被完全初始化并且准备好响应对那个磁盘的请求.
二、块设备驱动的IO请求处理
每个块驱动的核心是它的请求函数. 这个函数是真正做工作的地方 --或者至少开始的地方; 剩下的都是开销。
对请求方法的介绍
块驱动的请求方法有下面的原型:
void request(request_queue_t *queue);
这个函数不能由驱动自己调用,这个函数被调用, 无论何时内核认为你的驱动是时候处理对设备的读, 写, 或者其他操作. 请求函数在返回之前实际不需要完成所有的在队列中的请求; 实际上, 它可能不完成它们任何一个, 对大部分真实设备. 它必须, 但是, 驱动这些请求并且确保它们最终被驱动全部处理。
每个设备有一个请求队列. 这是因为实际的从和到磁盘的传输可能在远离内核请求它们时发生, 并且因为内核需要这个灵活性来调度每个传送, 在最好的时刻(将影响磁盘上邻近扇区的请求集合到一起, 例如). 并且这个请求函数, 你可能记得, 和一个请求队列相关, 当这个队列被创建时. 让我们回顾 sbull 如何创建它的队列:
dev->queue = blk_init_queue(sbull_request, &dev->lock);
这样, 当这个队列被创建时, 请求函数和它关联到一起. 我们还提供了一个自旋锁作为队列创建过程的一部分. 无论何时我们的请求函数被调用, 内核持有这个锁. 结果, 请求函数在原子上下文中运行; 它必须遵循所有的 5 章讨论过的原子代码的通用规则.
在你的请求函数持有锁时, 队列锁还阻止内核去排队任何对你的设备的其他请求. 在一些条件下, 你可能考虑在请求函数运行时丢弃这个锁. 如果你这样做, 但是, 你必须保证不存取请求队列, 或者任何其他的被这个锁保护的数据结构, 在这个锁不被持有时. 你必须重新请求这个锁, 在请求函数返回之前.
最后, 请求函数的启动(常常地)与任何用户空间进程之间是完全异步的. 你不能假设内核运行在发起当前请求的进程上下文. 你不知道由这个请求提供的 I/O 缓冲是否在内核或者用户空间. 因此任何类型的明确存取用户空间的操作都是错误的并且将肯定引起麻烦. 如你将见到的, 你的驱动需要知道的关于请求的所有事情, 都包含在通过请求队列传递给你的结构中。
下面几节更深入地研究块层如何完成它的工作, 已经这些工作导致的数据结构.
1、请求队列
最简单的说, 一个块请求队列就是: 一个块 I/O 请求的队列. 如果你往下查看, 一个请求队列是一令人吃惊得复杂的数据结构. 幸运的是, 驱动不必担心大部分的复杂性.
请求队列跟踪等候的块I/O请求. 但是它们也在这些请求的创建中扮演重要角色. 请求队列存储参数, 来描述这个设备能够支持什么类型的请求: 它们的最大大小, 多少不同的段可进入一个请求, 硬件扇区大小, 对齐要求, 等等. 如果你的请求队列被正确配置了, 它应当从不交给你一个你的设备不能处理的请求.
请求队列还实现一个插入接口, 这个接口允许使用多 I/O 调度器(或者电梯). 一个 I/O 调度器的工作是提交 I/O 请求给你的驱动, 以最大化性能的方式. 为此, 大部分 I/O 调度器累积批量的 I/O 请求, 排列它们为递增(或递减)的块索引顺序, 并且以那个顺序提交请求给驱动. 磁头, 当给定一列排序的请求时, 从磁盘的一头到另一头工作, 非常象一个满载的电梯, 在一个方向移动直到所有它的"请求"(等待出去的人)已被满足. 2.6 内核包含一个"底线调度器", 它努力确保每个请求在预设的最大时间内被满足, 以及一个"预测调度器", 它实际上短暂停止设备, 在一个预想中的读请求之后, 这样另一个邻近的读将几乎是马上到达. 到本书为止, 缺省的调度器是预测调度器, 它看来有最好的交互的系统性能.
I/O 调度器还负责合并邻近的请求. 当一个新 I/O 请求被提交给调度器, 它在队列里搜寻包含邻近扇区的请求; 如果找到一个, 并且如果结果的请求不是太大, 这 2 个请求被合并.
请求队列有一个 struct request_queue 或者 request_queue_t 类型. 这个类型, 和许多操作它的函数, 定义在
2、队列的创建和删除
如同我们在我们的例子代码中见到的, 一个请求队列是一个动态的数据结构, 它必须被块 I/O 子系统创建. 这个创建和初始化一个队列的函数是:
request_queue_t *blk_init_queue(request_fn_proc *request, spinlock_t *lock);
当然, 参数是, 这个队列的请求函数和一个控制对队列存取的自旋锁. 这个函数分配内存(实际上, 不少内存)并且可能失败因为这个; 你应当一直检查返回值, 在试图使用这个队列之前.
作为初始化一个请求队列的一部分, 你可设置成员 queuedata(它是一个 void * 指针 )为任何你喜欢的值. 这个成员是请求队列的对于我们在其他结构中见到的 private_data 的对等体.
为返回一个请求队列给系统(在模块卸载时间, 通常), 调用 blk_cleanup_queue:
void blk_cleanup_queue(request_queue_t *);
这个调用后, 你的驱动从给定的队列中不再看到请求,并且不应当再次引用它.