一、重要知识点
1.块设备和字符设备的区别
a.字符设备可访问字节大小数据,块设备只能访问固定大小的整块数据(一般为512字节)。
b.块设备支持随机访问,字符设备只能顺序访问。
2.块设备子系统体系架构
如图
从上到下依次为VFS虚拟文件系统、各种类型的磁盘系统、通用块设备层、I/O调度层(优化访问上层的请求(读写请求))、块设备驱动层、块设备硬件层。
我们编写驱动程序要完成的是调用I/O调度层提供的相关接口对块设备硬件层进行读写及相关操作。
3.块设备驱动程序注册
块设备驱动程序使用
int register_blk_dev(unsigned int major, const char*name)向内核注册。如果major为0,则内核为止分配一个主设备号。在内核2.6中,对register_blk_dev的调用时完全可选的,该接口只做了两件事:一是动态分配主设备号,二是在/proc/devices中创建一个入口项。大多数驱动仍会调用,因为这是一个传统。
4.注册磁盘
虽然register_blk能够获得主设备号,当它并不能让系统使用任何磁盘,因此为了管理独立的磁盘,必须使用另外一个单独的注册接口
void add_disk(struct gendist *gd)
下面我们再来看看参数struct gendisk结构
5.磁盘描述结构struct gendisk
内核使用gendisk结构来表示一个独立的磁盘设备
struct gendisk
{
int major; //主设备号
intfirst_minor; //第一个次设备号
intminors; //最大次设备数,如果不能分区则为1
chardisk_name[32]; //设备名称
structhd_struct **part; //磁盘上的分区信息
structblock_device_operations *fops; //块设备操作结构体
structrequest_queue *queue; //请求队列
void*private_data; //私有数据
sector_tcapacity; //扇区数,512字节为1扇区
…………
}
我们再来看块设备的操作结构体struct block_device_operations
6.块设备操作结构体struct block_device_operations
struct struct block_device_operations
{
int (*open)(struct inode *, struct file*);
int(*release)(struct inode*, struct file *);
int (*ioctl)(struct inode*, struct file *, unsigned, unsigned long);
int (*media_changed)(struct gendisk *)
int (*revalidate_disk)(struct gendisk *)
int (*getgeo)(structblock_device *, struct hd_geometry*);
structmodule *owner;
}
int (*open)(structinode *, struct file*);当系统执行mount、创建分区、在分区上创建文件系统,运行文件系统检查程序等时被调用。
int(*release)(struct inode*, struct file *);当系统执行umount等其他关闭设备操作时被调用。
int (*ioctl)(structinode*, struct file *, unsigned, unsigned long);用来提供一些特殊的操作,比如说查询磁盘物理信息等。
int (*media_changed)(structgendisk *)
int (*revalidate_disk)(structgendisk *)
这两个用来支持可移动介质。上层调用media_change以检查介质是否被改变如改变将返回非0值。
在介质改变后,上层将调用revalidate_disk来重新对新的介质进行一些初始化工作。
int (*getgeo)(struct block_device *, structhd_geometry*);用来填充驱动器信息。
在这里我们就发现块设备和字符设备驱动的区别了,该操作结构体中没有读写函数。因为块设备的读写操作是与I/O调度层的I/O请求绑定在一起的,一旦I/O调度层有I/O请求就会调用块设备的读写操作函数。下面开始介绍块设备如何响应I/O请求。
7.I/O请求
当内核以文件系统,虚拟子系统或者调用形式从块设备输入、输出块数据是,它将使用一个bio结构,用来描述这个操作。该结构会被传递给I/O调度层,I/O调度层会把它合并到一个已经存在的request结构中,或者根据需要再创建一个request结构中。为什么要这样做呢?因为内核为了使提高块设备的读写效率,它会将对相邻的扇区进行操作的多个请求(bio)合并成一个request。同样为了提高块设备的读写效率,I/O调度层又将每个request进行一些排序处理组成一个队列(request_que_t),使驱动以某种顺序去读取request_que_t的每一个request,然后进行块设备的实际读写操作。综上,bio是最基本的请求,然后内核会将对相邻扇区访问的bio组成一个request,接着再把request按照某种调度算法排序组成一个队列request_que_t。我们驱动程序要实现的就是提取每一个quest,然后获取其中的信息进行读写操作。
但是有一个问题,并不是所有块设备都像磁盘设备那样扇区之类的结构,比如说flash,ram盘之类的,对这一类的设备进行上述的I/O调度反而会使效率降低,所有内核又提供了实现I/O请求的另外一种方式,就是绕过请求队列,也就是绕过request和request_que_t直接对bio结构进行处理。
下面我们分别来介绍实现I/O请求响应的两种方式。
8.响应I/O请求实现方式一:request队列方式
request数据结构
struct request
{
struct list_head queuelist; //形成request链表的链表结构
sector_t sector; //要操作的首个扇区
unsigned long nr_sectors; //要操作的扇区数
struct bio *bio; //请求的bio链表头
struct bio *biotail; //请求的bio结构体的链表尾
……
}
操作请求队列的函数
初始化请求队列
struct request_queue *blk_init_queue(request_fn_proc*rfn, spinlock_t *lock)
rfn为请求队列的响应函数,这样就将驱动响应函数和I/O请求绑定到了一起。
lock是访问队列权限的自旋锁。
将该函数的返回值赋给gendisk结构的queue成员,这样就I/O调度层就会把组织好的request形成的队列填充到queue里面,然后调用rfn来响应对该块设备的I/O请求。rfn的原型为
typedef void (request_fn_proc) (request_que_t *q),它只有一个参数就是request_que_t队列。
清除请求队列
void blk_cleanup_queue(request_queue_t *q)
当块设备驱动模块卸载时调用此函数。
返回队列中下一个要处理的的请求(request):
struct request *elv_next_request(request_queue_t *queue)
并删除一个请求
void blkdev_dequeue_request(struct request *req)
9.响应I/O请求实现方式二:直接响应bio方式
bio结构的核心是一个名为bi_io_vec数组,它是由下面的结构组成的:
struct bio_vec {
struct page *bv_page;
unsignedint bv_len;
unsignedint bv_offset;
}
它表示了一个映射的物理页的信息。内核使用bio_for_each_segment(bvec,bio, segno)来遍历每个bio_vec结构。bvec是指当前的dio_vec入口, segno是段号。
驱动是程序使用blk_alloc_queue函数分配一个请求队列来告诉块设备子系统,I/O请求响应的是使用bio方式。
request_queue_t *blk_alloc_queue(int flags)
该函数与blk_init_queue的不同之处在于它并未真正实现一个保存的请求队列。flag是一系列标志用来为队列分配内存。通常是GFP_KERNEL。一旦拥有了队列,将它与make_request将响应函数传递给blk_queue_make_request:
void blk_queue_make_request(request_queue_t *queue,mak_request_fn *func);
请求响应函数的原型为
typedef int (make_request_fn) (request *q, struct bio *bio)
可以看出内核传递了一个bio结构给I/O请求响应函数,func可以读取bio的信息进行块设备的读写操作。
二、驱动代码分析
该驱动将一段内存模拟成一个块设备驱动,并使用bio方式实现I/O请求的响应