<块设备驱动>
块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。
块设备驱动相比字符设备驱动的主要区别如下:
①块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。
字符设备是以字节为单位进行数据传输的,不需要缓冲。
②块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,
等到条件成熟以后在一次性将缓冲区中的数据写入块设备中。
块设备结构不同其 I/O 算法也会不同,比如对于 EMMC、SD 卡、NAND Flash 这类没有任何机械设备的存储设备
就可以任意读写任何的扇区(块设备物理存储单元)。
对于机械硬盘这样带有磁头的设备,读取不同的盘面或者磁道里面的数据,磁头都需要进行移动,因此对于机械硬盘而言,
将那些杂乱的访问按照一定的顺序进行排列可以有效提高磁盘性能。linux 里面针对不同的存储设备实现了不同的 I/O 调度算法。
1. 块设备驱动框架
1). block_device 结构体:
linux 内核使用 block_device 结构体表示块设备,定义在include/linux/fs.h 文件中。
struct block_device {
dev_t bd_dev;
int bd_openers;
......
......
struct gendisk *bd_disk;
......
......
};
对于 block_device 结构体,需要重点关注 bd_disk 成员变量,此成员变量为 gendisk 结构体指针类型。
内核使用 block_device 来表示一个具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话 bd_disk 就指向通用磁盘结构 gendisk。
2). 注册/注销块设备:
和字符设备驱动一样,我们需要向内核注册新的块设备、申请设备号,块设备注册函数为register_blkdev:
int register_blkdev(unsigned int major, const char *name)
参数 major 在 1~255 之间的话表示自定义主设备号,major 为 0 的话表示由系统自动分配主设备号(1~255)。
注销块设备函数为unregister_blkdev:
void unregister_blkdev(unsigned int major, const char *name)
3). gendisk 结构体:
linux 内核使用 gendisk 结构体来描述一个磁盘设备,定义在 include/linux/genhd.h中。
struct gendisk {
int major; //磁盘设备的主设备号
int first_minor; //磁盘的第一个次设备号。
int minors; //磁盘的次设备号数量,也就是磁盘的分区数量
char disk_name[DISK_NAME_LEN]; //磁盘名
......
struct disk_part_tbl __rcu *part_tbl; //磁盘对应的分区表
......
const struct block_device_operations *fops; //块设备操作集
......
struct request_queue *queue; //磁盘对应的请求队列,针对该磁盘设备的请求都放到此队列中,
//驱动程序需要处理此队列中的所有请求
......
......
};
4). 编写块设备驱动的时候需要分配并初始化一个 gendisk,linux 内核提供了一组 gendisk 操作函数:
a. 申请 gendisk
struct gendisk *alloc_disk(int minors)
b. 删除 gendisk
void del_gendisk(struct gendisk *gp)
c. 将 gendisk 添加到内核(将申请到的gendisk 添加到内核后系统才能使用)
void add_disk(struct gendisk *disk)
d. 设置 gendisk 容量(参数size是扇区数量,其大小等于块设备实际物理容量除以512 字节)
void set_capacity(struct gendisk *disk, sector_t size)
e. 调整 gendisk 引用计数
truct kobject *get_disk(struct gendisk *disk) //增加 gendisk 的引用计数
void put_disk(struct gendisk *disk) //减少 gendisk 的引用计数
5). block_device_operations 结构体:
和字符设备的 file _operations 一样,块设备也有操作集,为结构体 block_device_operations,此结构体定义在 include/linux/blkdev.h。
2. 块设备 I/O 请求过程
1). 请求队列 request_queue
内核将对块设备的读写都发送到请求队列 request_queue 中,request_queue 中是大量的request(请求结构体),
而 request 又包含了 bio,bio 保存了读写相关数据。每个磁盘(gendisk)都分配了一个 request_queue。
①初始化请求队列
首先需要申请并初始化一个 request_queue,然后在初始化 gendisk 的时候将这个request_queue 地址赋值给 gendisk 的 queue 成员变量。
使用 blk_init_queue 函数来完成request_queue 的申请与初始化:
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
其中,rfn为请求处理函数指针,每个 request_queue 对应一个请求处理函数,
lock为自旋锁指针。这两个参数都需要自定义实现。
②删除请求队列
当卸载块设备驱动时,需要删除掉申请到的 request_queue。删除请求队列使用函数 blk_cleanup_queue:
void blk_cleanup_queue(struct request_queue *q)
③分配请求队列并绑定制造请求函数
blk_init_queue 函数完成了请求队列的申请以及请求处理函数的绑定,这个一般用于像机械硬盘的存储设备,需要 I/O 调度器来优化数据读写过程。
对于 EMMC、SD 卡这样的非机械设备,不需要复杂的 I/O 调度器。
对于非机械设备可以先申请 request_queue,然后将申请到的 request_queue 与“制造请求”函数绑定在一起。
非机械设备的 request_queue 申请函数 blk_alloc_queue:
struct request_queue *blk_alloc_queue(gfp_t gfp_mask) //gfp_mask一般为 GFP_KERNEL
需要为 blk_alloc_queue 函数申请到的请求队列绑定一个“制造请求”函数,用到函数 blk_queue_make_request:
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn) //mfn需要自定义实现
2). 请求 request
需要从 request_queue 中取出一个一个的 request,然后再从每个 request 里面取出 bio,
最后根据 bio 的描述将数据写入到块设备,或者从块设备中读取数据。
①获取请求
使用blk_peek_request函数从request_queue中依次获取每个request:
request *blk_peek_request(struct request_queue *q)
②开启请求
使用 blk_start_request函数开始处理获取到的request:
void blk_start_request(struct request *req)
③一步到位处理请求
可以使用 blk_fetch_request 函数来一次性完成请求的获取和开启:
struct request *blk_fetch_request(struct request_queue *q)
④其他和请求有关的函数
blk_end_request() //请求中指定字节数据被处理完成。
blk_end_request_all() //请求中所有数据全部处理完成。
blk_end_request_cur() //当前请求中的 chunk。
blk_end_request_err() //处理完请求,直到下一个错误产生。
__blk_end_request() //和 blk_end_request 函数一样,但是需要持有队列锁。
__blk_end_request_all() //和 blk_end_request_all 函数一样,但是需要持有队列锁。
__blk_end_request_cur() //和 blk_end_request_cur 函数一样,但是需要持有队列锁。
__blk_end_request_err() //和 blk_end_request_err 函数一样,但是需要持有队列锁。
3). bio 结构
上层应用程序对于块设备的读写会被构造成一个或多个 bio 结构。
上层会将 bio 提交给 I/O 调度器,I/O 调度器会将这些 bio 构造成 request 结构,而一个物理存储设备对应一个 request_queue。
新产生的 bio 可能被合并到 request_queue 里现有的 request 中,也可能产生新的 request,
然后插入到 request_queue 中合适的位置,这一切都是由 I/O 调度器来完成的。
bio 是个结构体,定义在 include/linux/blk_types.h 中。
struct bio {
struct bio *bi_next; /* 请求队列的下一个 bio */
struct block_device *bi_bdev; /* 指向块设备 */
unsigned long bi_flags; /* bio 状态等信息 */
unsigned long bi_rw; /* I/O 操作,读或写 */
struct bvec_iter bi_iter; /* I/O 操作,读或写 */
......
......
struct bio_vec *bi_io_vec; /* bio_vec 列表 */
......
};
其中,bvec_iter 结构体描述物理存储设备地址信息,比如要操作的扇区地址等。
struct bvec_iter {
sector_t bi_sector; /* I/O 请求的设备起始扇区(512 字节) */
unsigned int bi_size; /* 剩余的 I/O 数量 */
unsigned int bi_idx; /* blv_vec 中当前索引 */
unsigned int bi_bvec_done; /* 当前 bvec 中已经处理完成的字节数 */
};
bio_vec 结构体描述了 RAM 数据信息,比如页地址、页偏移以及长度。
struct bio_vec {
struct page *bv_page; /* 页 */
unsigned int bv_len; /* 长度 */
unsigned int bv_offset; /* 偏移 */
};
①遍历请求中的 bio
遍历请求中的 bio 使用函数__rq_for_each_bio,这是一个宏:
#define __rq_for_each_bio(_bio, rq) if ((rq->bio)) for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
②遍历 bio 中的所有段(操作数据)
用到bio_for_each_segment 函数,此函数也是一个宏:
#define bio_for_each_segment(bvl, bio, iter) __bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
③通知 bio 处理结束
如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,
在 bio 处理完成以后要通过内核 bio 处理完成,使用 bio_endio 函数:
bvoid bio_endio(struct bio *bio, int error) //error:bio 处理成功就直接填 0,失败的话就填个负值,比如-EIO。