块设备驱动程序
<1>.块设备和字符设备的区别
1、读取数据的单元不同,块设备读写数据的基本单元是块,字符设备的基本单元是字节。
2、块设备可以随机访问,字符设备只能顺序访问。
块设备的访问:
当多个请求提交给块设备时,执行效率依赖于请求的顺序。如果所有的请求是同一个方向(如:写数据),执行效率是最大的。内核在调用块设备驱动程序例程处理请求之前,先收集I/O请求并将请求排
序,然后,将连续扇区操作的多个请求进行合并以提高执行效率,对I/O请求排序的算法称为电梯算法(elevator algorithm)。
电梯算法在I/O调度层完成。内核提供了不同类型的电梯算法,电梯算法有
noop(实现简单的FIFO,基本的直接合并与排序),
anticipatory(延迟I/O请求,进行临界区的优化排序),
Deadline(针对anticipatory缺点进行改善,降低延迟时间),
Cfq(均匀分配I/O带宽,公平机制)
电梯调度算法:我们先来说一说电梯是怎样工作的。当电梯上的时候,如果在电梯所在层的上层和下层都有请求的话,电梯会先处理上层的请求,同样当电梯下的时候如果上层下层都有请求的话,电梯会先处理下层的请求。那么我们这里电梯调度算法也是采用如此的策略,比如当读数据的时候,如果有读请求也有写请求,那么要先处理读请求。在写数据的时候,如果有读请求也有写请求,那么先处理写请求。
块设备驱动相关数据结构:
block_device: 旨在描述一个分区或整个磁盘对内核的一个块设备实例
gendisk: 旨在描述一个通用硬盘(generic hard disk)对象。
hd_struct: 旨在描述分区应有的分区信息
bio: 旨在描述块数据传送时怎样完成填充或读取块给driver
request: 旨在描述向内核请求一个列表准备做队列处理。
request_queue: 旨在描述内核申请request资源建立请求链表并填写BIO形成队列。
<2>.通用硬盘结构 gendisk
结构体gendisk代表了一个通用硬盘(generic hard disk)对象,它存储了一个硬盘的信息,包括请求队列、分区链表和块设备操作函数集等。块设备驱动程序分配结构gendisk实例,装载分区表,分配请求队列并填充结构的其他域。
支持分区的块驱动程序必须包含 <linux/genhd.h> 头文件,并声明一个结构gendisk,内核还维护该结构实例的一个全局链表gendisk_head,通过函数add_gendisk、del_gendisk和get_gendisk维护该链表。
struct gendisk {
int major; //主设备号
int first_minor; //次设备号
int minors; //次设备号的最大数量,没有分区的设备,此值为1
char disk_name[DISK_NAME_LEN]; //驱动名
struct block_device_operations *fops; //块设备操作函数集
struct request_queue *queue; //请求队列
………………………………………………
int node_id;
};
分配gendisk: struct gendisk *alloc_disk(int minors);
增加gendisk: void add_disk(struct gendisk *gd);
释放gendisk: void del_gendisk(struct gendisk *gd);
gendisk引用计数: 通过get_disk()和put_disk()函数可用来操作引用计数
设置gendisk容量: void set_capacity(struct gendisk *disk, sector_t size);
<3>.设备操作 block_device_operations
struct block_device_operations {
int (*open)(struct block_device *,fmode_t );
int (*release)(struct gendisk * ,fmode_t);
int (*ioctl)(struct block_device *,fmode_t ,unsigned ,unsigned long);
………………………………………………………………………………
}
块设备注册与注销:
int register_blkdev(unsigned int major, const char *name);
int unregister_blkdev(unsigned int major, const char *name);
<4>.请求结构 request
linux内核中,使用struct request来表示等待处理的块设备IO请求。
struct request
{
struct list_head queuelist; //链表结构
sector_t sector; //要操作的首个扇区
unsigned long nr_sectors; //要操作的扇区个数
struct bio *bio; //请求的bio结构体的链表
struct bio *biotail; //请求的bio结构体的链表尾
…………………………………………………………
}
<5>.请求队列结构 request_queue
每个块设备都有一个请求队列,每个请求队列单独执行I/O调度,请求队列是由请求结构实例链接成的双向链表,链表以及整个队列的信息用结构request_queue描述,称为请求队列对象结构或请求队列结构。
struct request_queue
{
struct list_head queue_head; /*待处理的请求队列的链表头*/
struct request *last_merge; /*指向队列中上次合并的请求*/
elevator_t *elevator; /*指向电梯算法对象*/
........................................
}
队列操作函数:
struct request_queue *blk_init_queue(request_fn_proc *rfn,spinlock_t *lock); //初始化请求队列,一般在模块加载函数中被调用。
void blk_cleanup_queue(request_queue *q); //清除请求队列,完成将请求队列返回给系统任务,一般在模块卸载函数中调用。
struct request *elv_next_request(request_queue_t *queue); //返回下一个要处理的请求,如果没有请求则返回NULL。elv_next_request()不会清除请求,仍然把这个请求放到队列上,因此连续调用它两次,会返回同一个请求结构体。
struct request *blk_fetch_request(struct request_queue *q); //提取请求
struct request *blk_put_request(struct request *req); //去除请求
void elv_requeue_request(request_queue_t *queue, struct request *req); //将1个已经出列的请求归还到队列中
void blkdev_dequeue_request(struct request *req); //从队列中删除一个请求结构体。
<6>.bio结构
通常1个bio对应1个I/O请求,IO调度算法可将连续的bio合并成1个请求。所以,1个请求可以包含多个bio。
struct bio
{
sector_t bi_sector; //第一个要访问的扇区
unsigned int bi_size; //以字节为单位要传输的数据大小
struct bio_vec *bi_io_vec; //实际的vector列表
...............................
}
内核还提供了一组函数(宏)用于操作bio:
int bio_data_dir(struct bio *bio); //这个函数可用于获得数据传输的方向是READ还是WRITE。
struct page *bio_page(struct bio *bio) ; //这个函数可用于获得目前的页指针。
int bio_offset(struct bio *bio) ; //这个函数返回操作对应的当前页内的偏移,通常块I/O操作本身就是页对齐的。
int bio_cur_sectors(struct bio *bio) ; //这个函数返回当前bio_vec要传输的扇区数。
char *bio_data(struct bio *bio) ; //这个函数返回数据缓冲区的内核虚拟地址。
<7>.内存数据段结构 bio_vec
struct bio_vec
{
struct page *bv_page; //页指针
unsigned int bv_len; //要传输数据的长度
unsigned int bv_offset; //偏移量
}
使用bio_for_each_segment()宏来访问bio的bio_vec成员,可以用这个宏循环遍历整个bio中的每个段.
<8>.Request 与 bio关系
<9>.请求处理的另一种实现方法
分配请求队列:request_queue_t *blk_alloc_queue(int fgp_mask);
对于FLASH、RAM盘等完全随机访问的非机械设备,并不需要进行复杂的I/O调度,这个时候,应该使用上述函数分配1个“请求队列”,并使用如下函数来绑定“请求队列”和“制造请求”函数。
void blk_queue_make_request(request_queue_t *q,make_request_fn *fn); //绑定请求队列和制造请求函数
void blk_queue_logical_block_size(struct request_queue *q, unsigned short size); //该函数用于告知内核块设备硬件扇区的大小,所有由内核产生的请求都是这个大小的倍数并且被正确对界。但是,内核块设备层和驱动之间的通信还是以512字节扇区为单位进行。
<10>.块设备驱动测试
#insmod simple-blk.ko
#ls /dev/simp_blkdev
#mkfs.ext3 /dev/simp_blkdev
#mkdir -p /mnt/blk
#mount
#cp /etc/init.d/* /mnt/blk
#ls /mnt/blk
#umount /mnt/blk
#ls /mnt/blk
<11>.块设备驱动总结:
在块设备驱动的模块加载函数中通常需要完成如下工作:
① 分配、初始化请求队列,绑定请求队列和请求函数。
② 分配、初始化gendisk,给gendisk的major、fops、queue等成员赋值,最后添加gendisk。
③ 注册块设备驱动。
在块设备驱动的模块卸载函数中通常需要与模块加载函数相反的工作:
① del_gendisk(xxx_disks);//清除请求队列。
② blk_cleanup_queue(xxx_queue[i]); //清除请求队列
③ unregister_blkdev(xxx_major, "xxx"); //删除对块设备的引用,注销块设备驱动。
参考链接:http://bbs.chinaunix.net/thread-2017377-1-1.html
http://blog.csdn.net/yangdelong/article/details/5499797
参考示例代码链接:http://hi.baidu.com/fighter0425/item/593be8d5b17c8af0795daa87