一、块设备简介
块设备驱动是存储设备驱动,块设备驱动相比字符设备驱动的主要区别如下:
①、块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
②、块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后再一次性将缓冲区中的数据写入块设备中。
二、块设备驱动框架1、注册注销块设备
int register_blkdev(unsigned int major, const char *name) void unregister_blkdev(unsigned int major, const char *name)
major: 要注销的块设备主设备号。 name: 要注销的块设备名字。
2、申请和删除磁盘设备
struct gendisk *alloc_disk(int minors)
minors: 次设备号数量, 也就是 gendisk 对应的分区数量。
void del_gendisk(struct gendisk *gp)
gp: 要删除的 gendisk。
3、将 gendisk 添加到内核
void add_disk(struct gendisk *disk)
disk: 要添加到内核的 gendisk。
4、设置 gendisk 容量
void set_capacity(struct gendisk *disk, sector_t size)
disk: 要设置容量的 gendisk。 size: 磁盘容量大小,注意这里是扇区数量。块设备中最小的可寻址单元是扇区,一个扇区一般是 512 字节,有些设备的物理扇区可能不是 512 字节。不管物理扇区是多少,内核和块设备驱动之间的扇区都是 512 字节。所以 set_capacity 函数设置的大小就是块设备实际容量除以512 字节得到的扇区数量。比如一个 2MB 的磁盘,其扇区数量就是(210241024)/512=4096。
5、调整 gendisk 引用计数
内核会通过 get_disk 和 put_disk 这两个函数来调整 gendisk 的引用计数,根据名字就可以知道, get_disk 是增加 gendisk 的引用计数, put_disk 是减少 gendisk 的引用计数。
truct kobject *get_disk(struct gendisk *disk)void put_disk(struct gendisk *disk)
6、块设备操作集
struct block_device_operations {int (*open) (struct block_device *, fmode_t);void (*release) (struct gendisk *, fmode_t);int (*rw_page)(struct block_device *, sector_t, struct page *,int rw);int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);long (*direct_access)(struct block_device *, sector_t,void **, unsigned long *pfn, long size);unsigned int (*check_events) (struct gendisk *disk,unsigned int clearing); /* ->media_changed() is DEPRECATED, use ->check_events() instead */int (*media_changed) (struct gendisk *);void (*unlock_native_capacity) (struct gendisk *);int (*revalidate_disk) (struct gendisk *);int (*getgeo)(struct block_device *, struct hd_geometry *); /* this callback is with swap_lock and sometimes page table lockheld */void (*swap_slot_free_notify) (struct block_device *,unsigned long);struct module *owner; };
块设备数据读写过程:
(1)、请求队列 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 都要有一个请求处理函数,请求处理函数request_fn_proc
void (request_fn_proc) (struct request_queue *q)
lock: 自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来。,请求队列会使用这个自旋锁。
②、删除请求队列
当卸载块设备驱动的时候我们还需要删除掉前面申请到的 request_queue,删除请求队列使用函数 blk_cleanup_queue
void blk_cleanup_queue(struct request_queue *q)
q: 需要删除的请求队列。
③、分配请求队列并绑定制造请求函数
blk_init_queue 函数完成了请求队列的申请以及请求处理函数的绑定,这个一般用于像机械硬盘这样的存储设备,需要 I/O 调度器来优化数据读写过程。但是对于 EMMC、 SD 卡这样的非机械设备,可以进行完全随机访问,所以就不需要复杂的 I/O 调度器了。对于非机械设备我们可以先申请 request_queue,然后将申请到的 request_queue 与“制造请求”函数绑定在一起。
struct request_queue *blk_alloc_queue(gfp_t gfp_mask)
gfp_mask: 内存分配掩码
2、 请求 request
请求队列(request_queue)里面包含的就是一系列的请求(request), request 是一个结构体, 需要从 request_queue 中取出一个一个的 request,然后再从每个 request 里面取出 bio,最后根据 bio 的描述讲数据写入到块设备,或者从块设备中读取数据。
①、 获取请求
request *blk_peek_request(struct request_queue *q)
q: 指定 request_queue。 返回值: request_queue 中下一个要处理的请求(request),如果没有要处理的请求就返回NULL。
②、开启请求
void blk_start_request(struct request *req)
③、一步到位处理请求
blk_fetch_request 函数来一次性完成请求的获取和开启
struct request *blk_fetch_request(struct request_queue *q){struct request *rq;rq = blk_peek_request(q);if (rq)blk_start_request(rq);return rq;}
3、 bio 结构
每个 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 调度器来完成的。