块设备的IO操作特点:
字符设备与块设备I/O 操作的不同:
1)块设备只能以块为单位接收输入和返回输出,而字符设备则以字节为单位。大多数设备是字符设备,因为他们不需要缓冲而且不以固定块大小进行操作。
2)块设备对于I/O请求有对应的缓冲区,因此它们可以选择以什么顺序进行响应,字符设备无须缓冲区且被直接读写。对于存储设备而言,调整读写的顺序作用很大,因为在读写连续的扇区的存储速度比分离的扇区更快。
3)字符设备只能被顺序读写,而块设备可以随机访问。
查看块设备可随机访问,但是对于磁盘这类机械设备而言,顺序的组织块设备的访问可以提高性能,在Linux中我们通常通过磁盘文件系统EXT4,Ubuifs等访问磁盘,但是磁盘也有一种原始磁盘的访问方式,如直接访问/dev/sdb1等。所有分EXT4,UBIFS,原始块设备有都工作于VFS之下,而EXT4,UBIFS,原始块设备之下又包含块I/O调度层以进行排序和合并。I/O调度层的基本目的是将请求按照它们对应在块设备的扇区号进行排列,以减少磁头的移动,提高效率。
Linux块设备驱动结构:
block_device_operations结构体是对块设备操作的集合,打开和释放,I/O控制,介质改变,使介质有效,获得驱动器的信息,模块指针。主要介绍一下介质改变
int (*media_change)(struct gendisk*gd) 被内核调用以检查驱动器中的介质是否已经改变,如果是,则返回一个非0值,否则返回0。这个函数仅仅适用于支持可移动的介质的驱动器,通常需要在驱动中增加一个表示介质状态是否改变的标志变量,非可移动设备的驱动不需要实现这个方法:unsigned int (*check_events)(struct gendisk *disk,unsigned int clearing ); media_change()这个回调函数目前已经过时了,已被check_events()代替。
int (*getgo)(struct block_device*,struct hd_geometry*);该函数根据驱动器的几何信息填充一个hd_geomtry结构体,hd_geometry结构体包含磁头,扇区,柱面等信息,其定义于include/linux/hdreg.h头文件中。
gendisk结构体 表示一个独立的磁盘设备,内核中提供了一组函数来操作这个结构体gendisk。分配gendisk,增加gendisk,释放gendisk,gendisk 引用计数。
bio,request和request_queue 通常一个bio对应上层传递给块层的I/O请求。每个bio结构体实例及其包含的bvec_iter,bio_vec结构体实例描述了该I/O请求的开始扇区,数据方向,数据放入的页。与bio对应的数据每次存放的内存不一定是连续的,Bio_dev结构体用来描述与这个Bio请求对应的所有的内存,它可能不总是在一个页面里面,因此需要一个向量,定义如 struct bio_vec{
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
}
I/O 调度算法可将连续的bio合并成一个请求。
每个块设备或者块设备的分区都对应有自身的request_queue,从I/O调度器合并和排序出来的请求会被分发到设备级的request_queue.
(1) 初始化请求队列
request_queue_t *blk_init_queue(request_fn_proc *rfn,spinlock_t *lock); 该函数的第一个参数是请求处理函数的指针,第二个参数是控制访问队列权限的子选锁,这个函数会发生内存分配的行为,它可能会失败,因此一定要检查它的返回值。
(2)清除请求队列
void blk_cleanup_queue(request_queue_t *q ); 这个函数完成将请求队列返回给系统的任务,一般在块设备驱动卸载过程中调用
(3)分配请求队列
request_queue_t *blk_alloc_queue(int gfp_mask);
对于RAMDISK这种完全随机访问的非机械设备,并不需要进行复杂的I/O调,这个时候,可以直接“展开”I/O调度器,使用如下函数来绑定请求队列和“制造请求”函数(make_request_fn ).
void blk_queue_make_request(request_queue_t *q,make_request_fn *mfn);
blk__alloc_queue()和blk_queue_make_request()结合起来使用的逻辑一般是:
xxx_queue = blk_alloc_queue(GFP_KERNEL);
blk_queue_make_request(xxx_queue,xxx_make_request);
(4)提取请求
struct request*blk_peek_request(struct request_queue *q);用于返回下一个要处理的请求,如果没有请求则返回NULL。它不会清楚请求,而是仍然将这个请求保留在队列上。原先的老的函数elv_next_request()已经不存在。
(5)启动请求
void blk_start_request(struct request*req);
(6)遍历bio和片段
(7)报告完成
void __blk_end_request_all(struct request *rq,int error);
void blk_end_request_all(struct request *rq,int error);
上述俩个函数用于报告请求是否完成,error为0表示成功,小于0表示失败。