1、整体框架
块设备在Linux中是一个完整的子系统。每个块设备驱动程序的核心是它的请求函数。磁盘驱动程序的性能是整个操作系统性能的重要组成部分,因此块设备子系统在编写时非常注意性能,除了从所控制的设备上获得信息外,块设备子系统为驱动程序完成了所有可能的工作,这样可以快速I/O。另一方面,块设备子系统不必关心驱动程序API的大量复杂性。
在Linux中,驱动对块设备的输入或输出(I/O)操作,都会向块设备发出一个请求,用bio 结构体描述。但对于一些磁盘设备而言请求的速度很慢,这时候内核就提供一种队列的机制把这些I/O请求添加到队列中,即请求队列,在驱动中用request_queue结构体描述。在向块设备提交这些请求前内核会先执行请求的合并和排序预操作,以提高访问的效率,然后再由内核中的I/O调度程序子系统来负责提交I/O 请求,调度程序决定队列中的请求的排列顺序以及什么时候派发请求到设备。gendisk数据结构体用来表示一个独立的磁盘设备或分区,用于对底层物理磁盘进行访问。
2、相关概念
2.1 数据单位
扇区(Sector):块设备对数据处理的基本单位,通常1个扇区大小为512字节(对设备而言)。
块(Block):由Linux制定对内核或文件系统等数据处理的基本单位,通常1个块由1个或多个扇区组成(对Linux操作系统而言)。
段(Segment):由若干个相邻的块组成,是Linux内存管理机制中一个内存页的一部分。
2.2 块设备各结构体之间的关系
2.3 bio结构和bio_vec内存数据段结构<linux/bio.h>
当内核以文件系统或系统调用的形式从块I/O设备输入、输出块数据时,通用层(Generic Block Layer)用一个bio结构来表示一个I/O请求,bio的核心是一个数据类型为bio_vec的数组。bio_vec代表了内存中的一个数据段,数据段用页、长度和偏移描述,是I/O请求执行的内存位置。bio结构包含了驱动程序执行请求的全部信息,bio既描述了磁盘的位置,又描述了内存的位置,是上层文件系统与底层物理磁盘之间的纽带。
struct bio {
sector_t bi_sector;//该bio结构所要传输的第一个(512字节)扇区:磁盘的位置
struct bio *bi_next; //请求链表
struct block_device *bi_bdev;//相关的块设备
unsigned long bi_flags//状态和命令标志
unsigned long bi_rw; //读写
unsigned short bi_vcnt;//bio_vesc偏移的个数
unsigned short bi_idx; //bi_io_vec的当前索引
unsigned short bi_phys_segments;//结合后的片段数目
unsigned short bi_hw_segments;//重映射后的片段数目
unsigned int bi_size; //I/O计数
unsigned int bi_hw_front_size;//第一个可合并的段大小;
unsigned int bi_hw_back_size;//最后一个可合并的段大小
unsigned int bi_max_vecs; //bio_vecs数目上限
struct bio_vec *bi_io_vec; //bio_vec链表:内存的位置
bio_end_io_t *bi_end_io;//I/O完成方法
atomic_t bi_cnt; //使用计数
void *bi_private;//拥有者的私有方法
bio_destructor_t *bi_destructor; //销毁方法
};
struct bio_vec {
struct page *bv_page; //数据段所在的页
unsigned short bv_len; //数据段的长度
unsigned short bv_offset; //数据段页内偏移
};
2.4 request请求结构和request_queue请求队列结构<linux/blk_dev.h>
一个request结构代表一个块设备的I/O请求,是一个bio结构的链表,包含1个或多个bio结构,可能通过多个独立请求合并而来。一个请求需要传输的扇区可能分布在整个内存中,如果多个请求对磁盘中相邻扇区进行操作,内核将合并它们。
内核将I/O请求添加到队列中,即请求队列request_quqeue,请求队列是由请求结构实例链接成的双向链表。每个块设备都有一个请求队列,每个请求队列单独执行I/O调度。
内核在调用块设备驱动程序例程处理请求之前,先收集I/O请求并将请求排序 ,然后将连续扇区操作的多个请求进行合并 以提高执行效率,然后再提交I/O 请求。I/O调度程序管理块设备的请求队列,决定队列中的请求的排列顺序以及什么时候派发请求到设备。对I/O请求排序的算法称为电梯算法(elevator algorithm),包括
1、noop(实现简单的FIFO,基本的直接合并与排序),
2、anticipatory(延迟I/O请求,进行临界区的优化排序),
3、Deadline(针对anticipatory缺点进行改善,降低延迟时间),
4、Cfq(均匀分配I/O带宽,公平机制)
请求函数原型:
void request(request_queue_t *queue);
struct request
{
struct list_headqueuelist;
struct list_headdonelist; /*用于挂在已完成请求链表的节点*/
structrequest_queue *q; //指向请求队列
unsigned intcmd_flags; //命令标识
enumrq_cmd_type_bits cmd_type; //命令类型
sector_tsector; //将提交的下一个扇区
sector_thard_sector; //将完成的下一个扇区
unsigned longnr_sectors; //还需传输的扇区数
unsigned longhard_nr_sectors; //将完成的扇区总数
unsigned intcurrent_nr_sectors; //在当前bio中还需要传送的扇区
unsigned inthard_cur_sectors; //在当前段中将完成的扇区数
struct bio*bio; //请求中第一个未完成操作的bio*
struct bio*biotail; //请求链表中末尾的bio*
structhlist_node hash; //融合 hash
};
请求队列是一个动态的数据结构,必须由块设备的I/O子系统创建。
//初始化请求队列
request_queue_t*blk_init_queue(request_fn_proc *request, spinlock_t *lock);
//清除请求队列
void blk_cleanup_queue(request_queue_t *);
//提取请求,返回队列中下一个要处理的请求函数
struct request *elv_next_request(request_queue_t*queue);
//去除请求,将请求从队列中删除
void blkdev_dequeue_request(struct request*req);
//分配请求队列
request_queue_t *blk_alloc_queue(intgfp_mask);
2.5 block_device块设备对象结构<linux/fs.h>
代表一个块设备对象,如整个硬盘或特定分区。如果代表一个分区,则其成员bd_part指向设备的分区结构;如果代表设备,则其成员bd_disk指向设备的通用硬盘结构gendisk。
2.6 gendisk通用硬盘结构<linux/genhd.h>
表示一个独立的磁盘设备或分区,用于对底层物理磁盘进行访问,存储磁盘的信息,包括请求队列、块设备操作函数集等。内核维护一个全局链表gendisk_head,通过add_disk、del_gendisk和get_gendisk维护该链表。
struct gendisk
{
int major; //主设备号
int first_minor; //第一个次设备号
int minors; //次设备号的最大数量
char disk_name[32]; //主设备号驱动程序名字
struct block_device_operations *fops; //块设备操作函数集
struct request_queue *queue; //请求队列
int flags; //描述驱动器状态的标志
sector_t capacity; //通过set_capacity设置的容量
void *private_data;
}
2.7 block_device_operations块设备操作函数集<linux/fs.h>
字符设备使用file_operations结构告诉系统对它们的操作接口,块设备使用类似的结构 struct block_device_operations。
struct block_device_options
{
struct module*owner;//指向拥有该结构的模块的指针
int (*open) (struct inode *inode,struct file *filp);
int (*release) (struct inode *inode, struct file *filp);
int (*ioctl) (struct inode * inode, struct file *filp,unsined int cmd,unsigned longarg);
int (*media_changed) (struct gendisk *gd);//检查是否更换了驱动器内的介质
int(*revalidate_dis) (struct gendisk *gd);
}
块设备没有读写操作,在块设备的I/O子系统中,由request函数处理。
3、块设备结构体的注册和注销
#include <linux/fs.h>
#include<linux/genhd.h>
//申请主设备号
intregister_blkdev(unsigned int major, const char *name);
//分配gendisk
//表示一个独立的磁盘设备或分区,用于对底层物理磁盘进行访问,存储磁盘的信息,包括请求队列、块设备操作函数集等。gendisk结构是一个动态分配的结构,参数minors是该磁盘使用的次设备号的数量,一般就是磁盘分区的数量,此后不能更改minors成员。
struct gendisk*alloc_disk(int minors);
//将gendisk与设备号、设备操作函数关联
gd>major= major;
gd->fops= &blk_ops;
//初始化自旋锁
spin_lock_init(&lock);
//创建并初始化请求队列
gd->queue= blk_init_queue(blk_request, & lock);
//注册gendisk。分配gendisk后,系统还不能使用该磁盘,必须初始化并注册。特别要注意,对add_disk()的调用必须发生在驱动程序的初始化工作完成并能响应磁盘的请求之后。
voidadd_disk(struct gendisk *gd);
//删除队列
blk_cleanup_queue(gd->queue);
//卸载gendisk
void del_gendisk(struct gendisk *gd);
//销毁块设备结构体
put_disk(struct gendisk *gd);
//注销主设备号
int unregister_blkdev(unsigned int major, constchar *name);