块设备驱动开发
块设备介绍
块设备是针对存储设备的,比如 SD 卡、 EMMC、 NAND Flash、 Nor Flash、 SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动,块设备驱动相比字符设备驱动的主要区别如下:
①、块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
②、块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后再一次性将缓冲区中的数据写入块设备中。
linux 内 核 使 用 block_device 表 示 块 设 备 , block_device 为 一 个 结 构 体 , 定 义 在include/linux/fs.h 文件中,结构体内容如下:
1 | 1 struct block_device { |
注册块设备和注销块设备和字符设备驱动一样,我们需要向内核注册新的块设备、申请设备号,块设备注册函数为register_blkdev,如果不使用某个块设备了,那么就需要注销掉,函数为unregister_blkdev,
1 | int register_blkdev(unsigned int major, const char *name)/*如果参数 major 在 1~255 之间的话表示自定义主设备号,那么返回 0 表示注册成功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那么返回值就是系统分配的主设备号(1~255),如果返回负值那就表示注册失败。*/ |
linux 内核使用 gendisk 来描述一个磁盘设备,这是一个结构体,定义在 include/linux/genhd.h中,内容如下:
1 | 1 struct gendisk { |
关于编写块的设备驱动的时候需要分配并初始化一个 gendisk,使用的 API 函数。
1、 申请 gendisk
使用 gendisk 之前要先申请, allo_disk 函数用于申请一个 gendisk,函数原型如下:
1 | struct gendisk *alloc_disk(int minors) |
2、删除 gendisk
如果要删除 gendisk 的话可以使用函数 del_gendisk,函数原型如下:
1 | void del_gendisk(struct gendisk *gp) |
3、将 gendisk 添加到内核
使用 alloc_disk 申请到 gendisk 以后系统还不能使用,必须使用 add_disk 函数将申请到的gendisk 添加到内核中, add_disk 函数原型如下:
1 | void add_disk(struct gendisk *disk) |
4、设置 gendisk 容量
每一个磁盘都有容量,所以在初始化 gendisk 的时候也需要设置其容量,使用函数set_capacity,函数原型如下:
1 | void set_capacity(struct gendisk *disk, sector_t size) |
块设备中最小的可寻址单元为扇区,扇区大小一般是2的整数倍,最常见的大小是512字节,扇区大小是设备的物理属性,扇区是所有块设备的基本单元,块设备无法对比它还小的单元进行寻址和操作,不过许多块设备能够一次传输多个扇区。
5、调整 gendisk 引用计数
内核会通过 get_disk 和 put_disk 这两个函数来调整 gendisk 的引用计数, get_disk 是增加 gendisk 的引用计数, put_disk 是减少 gendisk 的引用计数,这两个函数原型如下所示:
1 | struct kobject *get_disk(struct gendisk *disk) |
块设备也有操作集,为结构体 block_device_operations,此结构体定义在 include/linux/blkdev.h 中,结构体内容如下:
1 | 1 struct block_device_operations { |
块设备 I/O 请求过程
在编写块设备驱动的时候,每个磁盘(gendisk)都要分配一个 request_queue ,内核将对块设备的读写都发送到请求队列 request_queue 中, request_queue 中是大量的request(请求结构体),而 request 又包含了 bio, bio 保存了读写相关数据。
通常一个bio对应一个I/O请求。调度算法可以将连续的bio合并成一个请求,所以一个请求包含多个bio。

请求队列API函数解析
1 | request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock) |
请求API函数解析
1 | request *blk_peek_request(struct request_queue *q) |

每个 request 里面会有多个 bio, bio 保存着最终要读写的数据、地址等信息。上层应用程序对于块设备的读写会被构造成一个或多个 bio 结构, bio 结构描述了要读写的起始扇区、要读写的扇区数量、是读取还是写入、页偏移、数据长度等等信息。上层会将 bio 提交给 I/O 调度器,I/O 调度器会将这些 bio 构造成 request 结构,而一个物理存储设备对应一个 request_queue,request_queue 里面顺序存放着一系列的 request。新产生的 bio 可能被合并到 request_queue 里现有的 request 中,也可能产生新的 request,然后插入到 request_queue 中合适的位置,这一切都是由 I/O 调度器来完成的。

1 | //bio 是个结构体,定义在 include/linux/blk_types.h 中 |
重点来看一下第 6 行和第 30 行,第 6 行为 bvec_iter 结构体类型的成员变量,第 30 行为bio_vec 结构体指针类型的成员变量。
bvec_iter 结构体描述了要操作的设备扇区等信息。
1 | struct bvec_iter { |

①、遍历请求中的 bio
请求中包含有大量的 bio,因此就涉及到遍历请求中所有 bio 并进行处理。遍历请求中的 bio 使用函数______rq_for_each_bio,这是一个宏,内容如下:
1 |
_bio 就是遍历出来的每个 bio, rq 是要进行遍历操作的请求, _bio 参数为 bio 结构体指针类型, rq 参数为 request 结构体指针类型。
②、遍历 bio 中的所有段
bio 包含了最终要操作的数据,因此还需要遍历 bio 中的所有段,这里要用到bio_for_each_segment 函数,此函数也是一个宏,内容如下:
1 |
第一个 bvl 参数就是遍历出来的每个 bio_vec,第二个 bio 参数就是要遍历的 bio,类型为bio 结构体指针,第三个 iter 参数保存要遍历的 bio 中 bi_iter 成员变量。
③、通知 bio 处理结束
如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,在 bio 处理完成以后要通过内核 bio 处理完成,使用 bio_endio 函数,函数原型如下:
1 | bvoid bio_endio(struct bio *bio, int error) |
bio: 要结束的 bio。
error: 如果 bio 处理成功的话就直接填 0,如果失败的话就填个负值,比如-EIO。
返回值: 无
小结
block_device: 描述一个分区或整个磁盘对内核的一个块设备实例
gendisk: 描述一个通用硬盘(generic hard disk)对象。
hd_struct: 描述分区应有的分区信息
bio: 描述块数据传送时怎样完成填充或读取块给driver
request: 描述向内核请求一个列表准备做队列处理。
request_queue: 描述内核申请request资源建立请求链表并填写BIO形成队列。

在Linux中,驱动对块设备的输入或输出(I/O)操作,都会向块设备发出一个请求,在驱动中用>request结构体描述。但对于一些磁盘设备而言请求的速度很慢,这时候内核就提供一种队列的机制把这些I/O请求添加到队列中(即:请求队列),在驱动中用request_queue结构体描述。在向块设备提交这些请求前内核会先执行请求的合并和排序预操作,以提高访问的效率,然后再由内核中的I/O调度程序子系统来负责提交 I/O 请求, 调度程序将磁盘资源分配给系统中所有挂起的块 I/O 请求,其工作是管理块设备的请求队列,决定队列中的请求的排列顺序以及什么时候派发请求到设备。
由通用块层(Generic Block Layer)负责维持一个I/O请求在上层文件系统与底层物理磁盘之间的关系。在通用块层中,通常用一个bio结构体来对应一个I/O请求。
Linux提供了一个gendisk数据结构体,用来表示一个独立的磁盘设备或分区,用于对底层物理磁盘进行访问。在gendisk中有一个类似字符设备中file_operations的硬件操作结构指针,是block_device_operations结构体。
当多个请求提交给块设备时,执行效率依赖于请求的顺序。如果所有的请求是同一个方向(如:写数据),执行效率是最大的。内核在调用块设备驱动程序例程处理请求之前,先收集I/O请求并将请求排序,然后,将连续扇区操作的多个请求进行合并以提高执行效率(内核算法会自己做,不用你管),对I/O请求排序的算法称为电梯算法(elevator algorithm)。电梯算法在I/O调度层完成。
代码框架


