[快速上手Linux设备驱动]之块设备驱动流程详解一walfred已经在[快速上手Linux设备驱动]之我看字符设备驱动一 文中详细讲解了linux下字符设备驱动,并紧接着用四篇文章描述了Linux的设备模型,分别是总线、设备、驱动以及是类子系统。为什么要在现在才开始 讲解块设备驱动呢,这里面是有原因,当初walfred自己学习时,是先看的块设备驱动然后才是linux设备模型,导致理解上面有了点偏差,现在我先将 linux设备模型抛出之后,再叙述下linux下的块设备驱动。 [快速上手Linux设备驱动]之块设备驱动流程分两篇文章讲解,分别是: 1、[快速上手Linux设备驱动]之块设备驱动流程详解一 即是本文,主要讲解块设备去字符设备的区别以及块设备驱动中牵涉到的几个重要的结构体。 2、[快速上手Linux设备驱动]之块设备驱动流程详解二 讲述了一个块设备驱动的模板 1块设备与字符设备的区别 1.1从字面上理解,块设备和字符设备最大的区别在于读写数据的基本单元不同。块设备读写数据的基本单元为块,例如磁盘通常为一个sector(扇区),而字符设备的基本单元为字节。所以Linux中块设备驱动往往为磁盘设备的驱动,但是由于磁盘设备的 IO性能与CPU相比很差,因此,块设备的数据流往往会引入文件系统的Cache机制。 1.2从实现角度来看,Linux为块设备和字符设备提供了两套机制。字符设备实现的比较简 单,内核例程和用户态API一一对应,用户层的Read函数直接对应了内核中的Read例程,这种映射关系由字符设备的file_operations维 护。块设备接口相对于字符设备复杂,read、write API没有直接到块设备层,而是直接到文件系统层,然后再由文件系统层发起读写请求。 2相关结构体 2.1 block_device_operations 与字符设备驱动程序一样,块设备驱动程序也包含一个在<linux/fs.h>中定义的block_device_operations结构,其定义如下所示。 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); long (*unlocked_ioctl) (struct file *, unsigned, unsigned long); long (*compat_ioctl) (struct file *, unsigned, unsigned long); int (*direct_access) (struct block_device *, sector_t, unsigned long *); int (*media_changed) (struct gendisk *); int (*revalidate_disk) (struct gendisk *); int (*getgeo)(struct block_device *, struct hd_geometry *); struct module *owner; }; 从该结构的定义中,可以看出块设备并不提供read()、write()等函数接口。对块设备的读写请求都是以异步方式发送到设备相关的request 队列之中。 关于block_device_operations,walfred曾在[快速上手Linux设备驱动]之一切皆是文件思想一文中有详细叙述。 2.2 gendisk 一个块设备物理实体由一个gendisk结构体来表示(在</linux/genhd.h>中定义),每个gendisk可以支持多个分区。 每个gendisk中包含了本物理实体的全部信息以及操作函数接口。整个块设备的注册过程是围绕gendisk来展开的。在驱动程序中需要初始化的gendisk的一些成员如下所示。 struct gendisk { int major; /* 主设备号 */ int first_minor; /* 第一个次设备号 */ int minors; /* 次设备号个数,一个块设备至少需要使用一个次设备号,而且块设 备的每个分区都需要一个次设备号,因此这个成员等于1,则表明该块 设备是不可被分区的,否则可以包含minors – 1 个分区。*/ char disk_name[32]; /* 块设备名称,在/proc/partions中显示 */ struct hd_struct **part; /* 分区表 */ struct block_device_operations *fops; /* 块设备操作接口,与字符设备的 file_operations结构对应*/ struct request_queue *queue; /* I/O请求队列 */ void *private_data; /* 指向驱动程序私有数据 */ sector_t capacity; /* 块设备可包含的扇区数 */ …… /* 其他省略 */ }; 2.3 request_queue 和 request request和request_queue结构体:Linux块设备驱动中,使用request结构体来表征等待进行的IO请求;并用request_queue来表征一个块IO请求队列.两个结构体的定义如下: request结构体 struct request{ struct list_head queuelist; unsigned long flags; sector_t sector;/*要传输的下一个扇区*/ unsigned long nr_sectors;/*要传送的扇区数目*/ unsigned int current_nr_sector;/*当前要传送的扇区*/ sector_t hard_sector;/*要完成的下一个扇区*/ unsigned long hard_nr_sectors;/*要被完成的扇区数目*/ unsigned int hard_cur_sectors;/*当前要被完成的扇区数目*/ struct bio* bio;/*请求的bio结构体的链表*/ struct bio* biotail;/*请求的bio结构体的链表尾*/
/*请求在屋里内存中占据的不连续的段的数目*/ unsigned short nr_phys_segments; unsigned short nr_hw_segments; int tag; char* buffer;/*传送的缓冲区,内核的虚拟地址*/ int ref_count;/*引用计数*/ ... }; 说明: request结构体的主要成员包括: sector_t hard_sector;/*要完成的下一个扇区*/ unsigned long hard_nr_sectors;/*要被完成的扇区数目*/ unsigned int hard_cur_sectors;/*当前要被完成的扇区数目*/ /* * 上述三个成员依次是第一个尚未传输的扇区,尚待完成的扇区数,当前IO操作中待完成的扇区数 * 但驱动中一般不会用到他们.而是下面的一组成员. */ sector_t sector;/*要传输的下一个扇区*/ unsigned long nr_sectors;/*要传送的扇区数目*/ unsigned int current_nr_sector;/*当前要传送的扇区*/ /* * 这三个成员,以字节为单位.如果硬件的扇区大小不是512字节.如字节,则在开始对硬件进行操作之 * 前,应先用4来除起始扇区号.前三个成员,与后三个成员的关系可以理解为"副本". */
关于unsigned short nr_phys_segments:该成员表示相邻的页被合并后,这个请求在物理内存中的段的数目.如果该设备支持SG(分散/聚合,scatter/gather),可根据该字段申请sizeof(scatterlist*) nr_phys_segments的内存,并使用下面的函数进行DMA映射: int blk_rq_map_sg(request_queue_t* q, struct request* rq, struct scatterlist *sg); 该函数与dma_map_sg()类似,返回scatterlist列表入口的数量. 关于struct list_head queuelist:该成员用于链接这个请求到请求队列的链表结构,函数blkdev_ dequeue_request()可用于从队列中移除请求.宏rq_data_dir(struct request* req)可获得数据传送方向.返回0表示从设备读取,否则表示写向设备. 2.4 request_queue请求队列 struct request_queue{ ... /*自旋锁,保护队列结构体*/ spinlock_t __queue_lock; spinlock_t* queue_lock; struct kobject kobj;/*队列kobject*/ /*队列设置*/ unsigned long nr_requests;/*最大的请求数量*/ unsigned int nr_congestion_on; unsigned int nr_congestion_off; unsigned int nr_batching; unsigned short max_sectors;/*最大扇区数*/ unsigned short max_hw_sectors; unsigned short max_phys_sectors;/*最大的段数*/ unsigned short max_hw_segments; unsigned short hardsect_size;/*硬件扇区尺寸*/ unsigned int max_segment_size;/*最大的段尺寸*/ unsigned long seg_boundary_mask;/*段边界掩码*/ unsigned int dma_alignment;/*DMA传送内存对齐限制*/ struct blk_queue_tag* queue_tags; atomic_t refcnt;/*引用计数*/ unsigned int in_flight; unsigned int sg_timeout; unsigned int sg_reserved_size; int node; struct list_head drain_list; struct request* flush_rq; unsigned char ordered; }; 说明:请求队列跟踪等候的块IO请求,它存储用于描述这个设备能够支持的请求的类型信息,他们的最大大小,多少不同的段可以进入一个请求,硬件扇区大小,对齐要求等参数.其结果是:如果请求队列被配置正确了,它不会交给该设备一个不能处理的请求. 请求队列还要实现一个插入接口,这个接口允许使用多个IO调度器,IO调度器以最优性能的方式向驱动提交IO请求.大部分IO调度器是积累批量的IO请求,并将其排列为递增/递减的块索引顺序后,提交给驱动.另外,IO调度器还负责合并邻近的请求,当一个新的IO请求被提交给调度器后,它会在队列里搜寻包含邻近的扇区的请求.如果找到一个,并且请求合理,调度器会将这两个请求合并. 2.5块I/O 通常一个bio对应一个IO请求.IO调度算法可将连续的bio合并成一个请求.所以一个请求包含多个bio. struct bio{ sector_t bi_sector;/*要传送的第一个扇区*/ struct bio* bi_next;/*下一个bio*/ struct block_device* bi_bdev; unsigned long bi_flags; /*如果是一个写请求,最低有效位被置位,可使用bio_data_dir(bio)宏来获取读写方向*/ unsigned long bi_rw;/*地位表示R/W方向,高位表示优先级*/ unsigned short bi_vcnt;/*bio_vec数量*/ unsigned short bi_idx; /*当前bvl_vec索引*/ unsigned short bi_phys_segments;/*不相邻的物理段的数目*/ unsigned short bi_hw_segments;/*物理合并和DMA remap合并后不相邻的物理扇区*/ unsigned int bi_size; /*被传送的数据大小(byte),用bio_sector(bio)获取扇区为单位的大小*/ /*为了明了最大的hw尺寸,考虑bio中第一个和最后一个虚拟的可合并的段的尺寸*/ unsigned int bi_hw_front_size; unsigned int bi_hw_back_size; unsigned int bi_max_vecs;/*能持有的最大bvl_vecs数*/ struct bio_vec* bio_io_vec;/*实际的vec列表*/ bio_end_io_t* bio_end_io; atomic_t bi_cnt; void* bi_private; bio_destructor_t* bi_destructor; }; //结构体包含三个成员 struct bio_vec{ struct page* bv_page;//页指针 unsigned int bv_len;//传送的字节数 unsigned int bv_offset;//偏移位置 };
/*一般不直接访问bio的bio_vec成员,而使用bio_for_each_segment()宏进行操作. *该宏循环遍历整个bio中的每个段. */ #define __bio_for_each_segment(bvl, bio, i, start_idx)\ for( bvl = bio_iovec_idx((bio),(start_idx)),i = (start_idx);\ i <(bio)->bi_vcnt;\ bvl++, i++\ ) #define bio_for_each_segment(bvl, bio, i)\ __bio_for_each_segment(bvl, bio, i, (bio)->bi_idx) 在内核中,提供了一组函数(宏)用于操作bio: int bio_data_dir(struct bio* bio); 该函数用于获得数据传送方向. struct page* bio_page(struct bio* bio); 该函数用于获得目前的页指针. int bio_offset(struct bio* bio); 该函数返回操作对应的当前页的页内偏移,通常块IO操作本身就是页对齐的. int bio_cur_sectors(struct bio* bio); 该函数返回当前bio_vec要传输的扇区数. char* bio_data(struct bio* bio); 该函数返回数据缓冲区的内核虚拟地址. char* bvec_kmap_irq(struct bio_vec* bvec, unsigned long* offset); 该函数也返回一个内核虚拟地址此地址可用于存取被给定的bio_vec入口指向的数据缓冲区.同时会屏蔽中断并返回一个原子kmap,因此,在此函数调用之前,驱动不应该是睡眠状态. void bvec_kunmap_irq(char* buffer, unsigned long flags); 该函数撤销函数bvec_kmap_irq()创建的内存映射. char* bio_kmap_irq(struct bio* bio, unsigned long* flags); 该函数是对bvec_kmap_irq函数的封装,它返回给定的比偶的当前bio_vec入口的映射. char* __bio_kmap_atomic(struct bio* bio, int i, enum km_type type); 该函数是通过kmap_atomic()获得返回给定bio的第i个缓冲区的虚拟地址. void __bio_kunmap_atomic(char* addr, enum km_type type); 该函数返还由函数__bio_kmap_atomic()获得的内核虚拟地址给系统. void bio_get(struct bio* bio); void bio_put(struct bio* bio); 上面两个函数分别完成对bio的引用和引用释放. 下图可以体现出bio/request/request_queue/bio_vec四个结构体之间的关系.
1
|
|