文章来源:http://blog.chinaunix.net/u3/94284/showart_1970946.html
概括:
块设备驱动第一个工作通常是注册自己到内核,是通过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(在 <linux/fs.h> 中定义):
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, 定义在 <linux/fs.h>.下面是一个对这个结构中的成员的简短的概览;
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 (定义于 <linux/genhd.h>) 是单独一个磁盘驱动器的内核表示. 事实上, 内核还使用 gendisk 来表示分区, 但是驱动作者不必知道这点. struct gedisk 中有几个成员, 必须被一个块驱动初始化:
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);