linux内核的块设备驱动框架详解
Posted 正在起飞的蜗牛
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux内核的块设备驱动框架详解相关的知识,希望对你有一定的参考价值。
1、块设备和字符设备的差异
(1)块设备只能以块为单位接受输入和返回输出,而字符设备则以字节为单位。大多数设备是字符设备,因为它们不需要缓冲而且不以固定块大小进行操作;
(2)块设备对于 I/O 请求有对应的缓冲区,因此它们可以选择以什么顺序进行响应,字符设备无须缓冲且被直接读写;
(3)字符设备只能被顺序读写,而块设备可以随机访问;
2、块设备驱动的特点
(1)块和字符是两种不同的访问设备的策略,通一个设备可以同时支持块和字符两种访问策略,但是效率会有差别;
(2)设备本身的物理特性决定了采用块和字符两种访问策略效率会有差别,块设备驱动最适合存储设备,因为存储设备本身就是以块为单位进行读写操作;
(3)虽然块设备可随机访问,但是对于磁盘这类机械设备而言,顺序地组织块设备的访问可以提高性能;而对 SD 卡、 RamDisk 等块设备而言,不存在机械上的原因,进行这样的调整没有必要
3、块设备相关的重要概念
(1)扇区(Sector),概念来自于早期磁盘,在硬盘、DVD中还有用,在Nand/SD中已经没意义了,扇区是块设备本身的特性,大小一般为512的整数倍,因为历史原因很多时候都向前兼容定义为512.
(2)块(block),概念来自于文件系统,是内核对文件系统数据处理的基本单位,大小为若干个扇区,常见有512B、1KB、4KB等
(3)段(Section),概念来自于内核,是内核的内存管理中一个页或者部分页,由若干个连续为块组成,称为段页式访问。
(4)页(Page),概念来自于内核,是内核内存映射管理的基本单位。linux内核的页式内存映射名称来源于此。
总结:块设备驱动对存储硬件以扇区为单位进行交换数据,对上层文件系统以块为单位交换数据;
4、磁原理存储设备和电原理存储设备的区别
(1)磁原理存储设备是早于电原理存储设备出现,磁原理存储设备包括机械硬盘、CD、磁带等,电原理存储设备主要是flash存储,包括NandFlash、iNand、SD卡、eMMC卡、NorFlash等;
(2)上面提到的扇区、磁道等概念都是磁原理存储设备才有的,但是现在的内核中,电原理存储设备也会使用这些概念,主要是为了兼容性和统一性;
(3)磁原理存储设备由于物理限制,连续读相邻扇区的速度比读不连续扇区的速度要快,主要原因是读不连续的扇区需要移动磁头,而移动磁头是机械工作比较耗时;而电原理存储设备不存在这个问题,无论读哪块扇区速度都是一样的;
(4)基于第三点讲到的原因,磁原理存储设备会调用内核的IO调用系统,去优化读写扇区的顺序,而电原理存储设备不需要IO调度系统,在后面会具体介绍;
(5)使用请求队列对于一个机械的磁盘设备而言的确有助于提高系统的性能,但是对于电原理存储设备,无法从高级的请求队列逻辑中获益;(请求队列实际就是对于IO调度系统)
5、块设备的访问方式
(1)操作字符设备是通过操作设备节点进行,把设备节点当做一个文件进行操作,调用open、close、ioctl等函数接口;
(2)块设备驱动也会生成设备节点,但是并不像字符设备一样调用open、close、ioctl等函数接口去操作设备节点,而是通过mount挂载的方式,以文件系统的方式去访问块设备;
(3)块设备被mount挂载前先要格式化,也就是把块设备按照某种文件系统格式进行初始化,初始化之后挂载到VFS的某个目录下,就可以像操作普通文件一样去操作块设备里的数据;
6、块设备驱动框图
(1)在mount成功后,应用程序通过I/O系统调用来访问块设备的文件,和应用程序交互的是VFS虚拟文件系统,但是块设备可能是EXT3、EXT4等文件系统,中间会做从VSF文件系统到块设备文件系统的转换;
(2)块设备层:
<1>块设备层有缓冲区,如果要读取的块在缓冲区能找到就可以直接返回,不必再去访问块设备;
<2>如果要访问的块不在缓冲区中,则将要访问的块数据添加到对应块设备的请求队列之中;
(3)I/O调度层:
<1>I/O调度层负责管理请求队列,决定请求的处理顺序,也会根据情况合并请求,提高访问效率;
<2>I/O调度层主要是针对机械式存储设备,因为机械式存储设备访问不相邻的扇区需要移动磁头,这个动作是比较耗时的;而电原理存储设备不存在这个问题,所以电原理存储设备可以不需要I/O调度层;
<3>I/O调度层处理请求是回调设备驱动层注册的请求处理函数;
(4)设备驱动层:
<1>设备驱动层是和具体设备相关的,不同的存储设备对应不同的设备驱动程序;
<2>虽然设备驱动程序不同,但是整体框架都是按照内核块设备驱动框架提供的接口实现的;
7、I/O调度层
(1)I/O调度层是优化对块设备的访问顺序,提供访问效率,对块设备的请求进行排序、合并等操作;
(2)I/O调度层采用电梯算法去优化请求的访问顺序,因为对块设备的请求很类似于电梯的情况,可以把读块设备的请求看做电梯上行,把写块设备的请求看做电梯下行,最终效果就是要让电梯运行最少距离把所有需要的楼层都访问一遍;
(3)内核实现了四种电梯算法,具体参见上图;
8、块设备相关的结构体
8.1、struct gendisk
1 struct gendisk
2
3 int major; /* 主设备号 */
4 int first_minor; /*第 1 个次设备号*/
5 int minors; /* 最大的次设备数,如果不能分区,则为 1*/
6 char disk_name[32]; /* 设备名称,将来显示在/proc/partitions、sysfs中 */
7 struct hd_struct **part; /* 磁盘上的分区信息 */
8 struct block_device_operations *fops; /*块设备操作结构体*/
9 struct request_queue *queue; /*请求队列*/
10 void *private_data; /*私有数据*/
11 sector_t capacity; /*扇区数, 512 字节为 1 个扇区*/
12
13 int flags;
14 char devfs_name[64];
15 int number;
16 struct device *driverfs_dev;
17 struct kobject kobj;
18
19 struct timer_rand_state *random;
20 int policy;
21
22 atomic_t sync_io; /* RAID */
23 unsigned long stamp;
24 int in_flight;
25 #ifdef CONFIG_SMP
26 struct disk_stats *dkstats;
27 #else
28 struct disk_stats dkstats;
29 #endif
30 ;
(1)struct gendisk结构体在内核中用于表示块设备或者分区,如果是同一块设备的不同分区,则各个分区共享同一主设备号,次设备号不同;
(2)向内核注册块设备就是构建一个struct gendisk结构体,然后调用注册函数进行注册;
8.2、struct block_device_operations
1 struct block_device_operations
2
3 int(*open)(struct inode *, struct file*); //打开
4 int(*release)(struct inode *, struct file*); //释放
5 int(*ioctl)(struct inode *, struct file *, unsigned, unsigned long); //ioctl函数一般比较简单,因为不少请求都被块设备层处理
6 long(*unlocked_ioctl)(struct file *, unsigned, unsigned long);
7 long(*compat_ioctl)(struct file *, unsigned, unsigned long);
8 int(*direct_access)(struct block_device *, sector_t, unsigned long*);
9 int(*media_changed)(struct gendisk*); //被内核调用来检查是否驱动器中的介质已经改变
10 int(*revalidate_disk)(struct gendisk*); //被调用来响应一个介质改变,它给驱动一个机会来进行必要的工作以使新介质准备好
11 int(*getgeo)(struct block_device *, struct hd_geometry*);//获得驱动器信息
12 struct module *owner; //模块拥有者,通常被初始化为 THIS_MODULE
13 ;
(1)struct block_device_operations结构体类似于字符设备的struct file_operations结构体,都是定义的一些操作设备的方法;
(2)但是块设备的block_device_operations结构体中没有read、write方法,块设备的读写通过请求队列的方式实现;
8.3、struct request_queue
1 struct request_queue
2
3 ...
4 /* 保护队列结构体的自旋锁 */
5 spinlock_t _ _queue_lock;
6 spinlock_t *queue_lock;
7
8 /* 队列 kobject */
9 struct kobject kobj;
10
11 /* 队列设置 */
12 unsigned long nr_requests; /* 最大的请求数量 */
13 unsigned int nr_congestion_on;
14 unsigned int nr_congestion_off;
15 unsigned int nr_batching;
16
17 unsigned short max_sectors; /* 最大的扇区数 */
18 unsigned short max_hw_sectors;
19 unsigned short max_phys_segments; /* 最大的段数 */
20 unsigned short max_hw_segments;
21 unsigned short hardsect_size; /* 硬件扇区尺寸 */
22 unsigned int max_segment_size; /* 最大的段尺寸 */
23
24 unsigned long seg_boundary_mask; /* 段边界掩码 */
25 unsigned int dma_alignment; /* DMA 传送的内存对齐限制 */
26
27 struct blk_queue_tag *queue_tags;
28
29 atomic_t refcnt; /* 引用计数 */
30
31 unsigned int in_flight;
32
33 unsigned int sg_timeout;
34 unsigned int sg_reserved_size;
35 int node;
36
37 struct list_head drain_list;
38
39 struct request *flush_rq;
40 unsigned char ordered;
41 ;
(1)在块设备驱动框架中,将应用对块设备的操作用请求来表达,然后用一个请求队列来管理这些请求;
(2)请求队列会保存用于描述这个设备能够支持的请求的类型信息、它们的最大大小、多少不同的段可进入一个请求、硬件扇区大小、对齐要求等参数,需要保证不会向设备提交一个不能处理的请求;
(3)请求队列和IO调度器紧密相关,I/O调度器会对请求队列中的请求进行排序或者合并,使得以最优的性能向设备驱动提交I/O请求;
8.4、struct request
1 struct request
2
3 struct list_head queuelist; /*链表结构*/
4 unsigned long flags; /* REQ_ */
5
6 sector_t sector; /* 要传送输的下一个扇区 */
7 unsigned long nr_sectors; /*要传送的扇区数目*/
8
9 unsigned int current_nr_sectors; /*当前要传送的扇区数目*/
10
11 sector_t hard_sector; /*要完成的下一个扇区*/
12 unsigned long hard_nr_sectors; /*要被完成的扇区数目*/
13
14 unsigned int hard_cur_sectors; /*当前要被完成的扇区数目*/
15
16 struct bio *bio; /*请求的 bio 结构体的链表*/
17 struct bio *biotail; /*请求的 bio 结构体的链表尾*/
18
19 void *elevator_private;
20
21 unsigned short ioprio;
22
23 int rq_status;
24 struct gendisk *rq_disk;
25 int errors;
26 unsigned long start_time;
27
28 /*请求在物理内存中占据的不连续的段的数目, scatter/gather 列表的尺寸*/
29 unsigned short nr_phys_segments;
30
31 /*与 nr_phys_segments 相同,但考虑了系统 I/O MMU 的 remap */
32 unsigned short nr_hw_segments;
33
34 int tag;
35 char *buffer; /*传送的缓冲,内核虚拟地址*/
36
37 int ref_count; /* 引用计数 */
38 ...
39 ;
在 Linux 块设备驱动中,使用request结构体来表征等待进行的 I/O 请求;
8.5、struct bio
8.5.1、结构体定义
1 struct bio
2
3 sector_t bi_sector; /* 要传输的第一个扇区 */
4 struct bio *bi_next; /* 下一个 bio */
5 struct block_device *bi_bdev;
6 unsigned long bi_flags; /* 状态、命令等 */
7 unsigned long bi_rw; /* 低位表示 READ/WRITE,高位表示优先级*/
8 struct bvec_iter bi_iter; /* 迭代器,标明数据要操作的块设备的位置 */
9 unsigned short bi_vcnt; /* bio_vec 数量 */
10 unsigned short bi_idx; /* 当前 bvl_vec 索引 */
11
12 /*不相邻的物理段的数目*/
13 unsigned short bi_phys_segments;
14
15 /*物理合并和 DMA remap 合并后不相邻的物理段的数目*/
16 unsigned short bi_hw_segments;
17
18 unsigned int bi_size; /* 以字节为单位所需传输的数据大小 */
19
20 /* 为了明了最大的 hw 尺寸,我们考虑这个 bio 中第一个和最后一个
21 虚拟的可合并的段的尺寸 */
22 unsigned int bi_hw_front_size;
23 unsigned int bi_hw_back_size;
24
25 unsigned int bi_max_vecs; /* 我们能持有的最大 bvl_vecs 数 */
26
27 struct bio_vec *bi_io_vec; /* 实际的 vec 列表 */
28
29 bio_end_io_t *bi_end_io;
30 atomic_t bi_cnt;
31
32 void *bi_private;
33
34 bio_destructor_t *bi_destructor; /* destructor */
35
36 /*
37 * We can inline a number of vecs at the end of the bio, to avoid
38 * double allocations for a small number of bio_vecs. This member
39 * MUST obviously be kept at the very end of the bio.
40 */
41 struct bio_vec bi_inline_vecs[0];
42 ;
(1)一个bio对应一个 I/O 请求,I/O调度算法可将连续的bio合并成一个请求,所以一个struct request结构体可以包含多个bio;
(2)bio的核心成员是bio_vec,不过我们不应该直接访问bio的bio_vec成员,而应该使用bio_for_each_segment()宏来进行这项工作,可以
用这个宏循环遍历整个bio中的每个段;
(3)bi_iter是迭代器,标明此次数据要写到块设备的哪个位置;
(4)bio_vec结构体指明数据再内存中的位置,bi_iter标明数据在块设备中的位置;
(5)bi_inline_vecs:这是一个数组成员个数为零的数组,是不占用内存的,相当于一个标号,标明bio结构体的结束地址,具体用法参考博客:《在结构体最后定义一个成员数为0的数组的意义以及工作当中的使用》;
8.5.2、bi_inline_vecs和bi_io_vec
(1)在bio结构体中有bi_inline_vecs和bi_io_vec两个变量来描述bio_vec,在申请 struct bio结构体内存时会多申请4个bio_vec结构体的内存跟在bio结构体的后面,这里的作用是用牺牲内存的方式来换取效率。
(2)当bio中所包含的bio_vec个数小于4个时,就直接保存到bio结构体的尾部,bi_io_vec变量也指向bi_inline_vecs处,两个变量指向同一位置;
(3)当bio中所包含的bio_vec个数大于4个时,需要重新申请内存去保存bio_vec,然后把申请内存的指针赋值给bi_io_vec,此时之前申请的bio尾部的4个bio_vec结构体的内存就浪费了,但是申请内存是比较耗时的;
8.6、struct bio_vec
1 struct bio_vec
2
3 struct page *bv_page; /* 页指针 */
4 unsigned int bv_len; /* 传输的字节数 */
5 unsigned int bv_offset; /* 偏移位置 */
6 ;
bio_vec表明数据在内存中的位置;
9、结构体之间的关联
9.1、request_queue、request、bio三者联系
(1)把应用程序操作块设备的动作用request来表达,每个块设备都有一个request_queue队列来管理对块设备的request;
(2)由于I/O调度器的优化,可能合并请求,所以一个request可能包含多个bio,也就是一个请求包含多个I/O操作;
9.2、bio、bio_vec数组、bvec_iter之间的联系
10、gendisk结构体相关操作函数
函数原型 | 作用 |
---|---|
struct gendisk *alloc_disk(int minors) | 分配gendisk结构体,minors参数是次设备号的数量加一,如果不分区就填1 |
void add_disk(struct gendisk *gd) | 向内核添加gendisk,在添加之前必须保证gendisk结构体已经初始化完成 |
void del_gendisk(struct gendisk *gd) | 释放gendisk |
get_disk()和 put_disk() | 增加/减少gendisk的引用计数,这个驱动一般不会调用,内核提供的块设备框架会处理 |
void set_capacity(struct gendisk *disk, sector_t size) | 设置gendisk的容量,其中size是以扇区(一般是512字节)为单位 |
11、请求队列相关操作函数
函数原型 | 作用 |
---|---|
request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock) | rfn是驱动向内核注册的请求处理函数;lock是驱动申请的自旋锁,用于管理请求队列的访问 |
void blk_cleanup_queue(request_queue_t * q); | 清除请求队列,一般在块设备驱动的卸载函数中调用 |
request_queue_t *blk_alloc_queue(int gfp_mask); | 分配请求队列 |
void blk_queue_make_request(request_queue_t * q, make_request_fn * mfn); | 绑定请求队列和"制造请求函数" |
struct request *elv_next_request(request_queue_t *queue) | 从请求队列中获取请求 |
void blkdev_dequeue_request(struct request *req) | 从请求队列中去除一个请求 |
void elv_requeue_request(request_queue_t *queue, struct request *req) | 将一个已经出列的请求归还到请求队列中 |
void blk_stop_queue(request_queue_t *queue) | 停止请求队列,告诉块设备层块设备处于不能处理请求的状态 |
void blk_start_queue(request_queue_t *queue); | 开启请求队列 |
void blk_queue_max_sectors(request_queue_t *queue, unsigned short max) | 设置请求可包含的最大扇区数,默认是255 |
void blk_queue_max_phys_segments(request_queue_t *queue, unsigned short max); | 设置一个请求中可包含的最大物理段(系统内存中不相邻的区),不设置默认128 |
void blk_queue_max_hw_segments(request_queue_t *queue, unsigned short max); | 设置一个请求中可包含的最大物理段(系统内存中不相邻的区),考虑了系统 I/O 内存管理单元的重映射,不设置默认128 |
void blk_queue_max_segment_size(request_queue_t *queue, unsigned int max); | 告知内核请求段的最大字节数,缺省值为65536 |
void blk_queue_bounce_limit(request_queue_t *queue, u64 dma_addr) | 告知内核块设备执行 DMA 时可使用的最高物理地址 dma_addr |
blk_queue_segment_boundary(request_queue_t *queue, unsigned long mask) | 告知内核块设备无法处理跨越一个特殊大小内存边界 |
void blk_queue_dma_alignment(request_queue_t *queue, int mask) | 告知内核块设备施加于 DMA 传送的内存对齐限制 |
void blk_queue_hardsect_size(request_queue_t *queue, unsigned short max) | 告知内核块设备硬件扇区的大小 |
12、bio相关的操作函数
12.1、bio_for_each_segment()宏
1 #define _ _bio_for_each_segment(bvl, bio, i, start_idx) \\
2 for (bvl = bio_iovec_idx((bio), (start_idx)), i = (start_idx); \\
3 i < (bio)->bi_vcnt; \\
4 bvl++, i++)
5
6 #define bio_for_each_segment(bvl, bio, i) \\
7 _ _bio_for_each_segment(bvl, bio, i, (bio)->bi_idx
循环遍历整个 bio 中的每个段
12.2、__rq_for_each_bio()宏
#define __rq_for_each_bio(_bio, rq)\\
if ((rq->bio))\\
for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
//_bio 是遍历出来的每个 bio,为 bio 结构体指针类
//rq 是要进行遍历操作的请求,为 request 结构体指针类型
12.3、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) | 返回数据缓冲区的内核虚拟地址 |
char *bvec_kmap_irq(struct bio_vec *bvec, unsigned long *flags) | 返回一个内核虚拟地址,这个地址可用于存取被给定的 bio_vec 入口指向的数据缓冲区 |
void bvec_kunmap_irq(char *buffer, unsigned long *flags); | 是 bvec_kmap_irq()函数的“反函数”,它撤销 bvec_kmap_irq()创建的映射 |
char *bio_kmap_irq(struct bio *bio, unsigned long *flags) | 对 bvec_kmap_irq()的包装,它返回给定的 bio 的当前 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) | 引用 bio |
void bio_put(struct bio *bio) | 释放对 bio 的引用 |
void bio_endio(struct bio *bio, int error) | 通知 bio 处理结束:如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,在 bio 处理完成以后要通知内核 bio 处理完成 |
13、块设备的注册和卸载
int register_blkdev(unsigned int major, const char *name);
int unregister_blkdev(unsigned int major, const char *name);
(1)major:在注册时填0则内核自动分配一个未使用的主设备号,如果major是1-255范围的一个数,则表示向内核申请该主设备号,如果已经被占用则返回失败;
(2)name:设备名,注册成功后在"/proc/devices"中可以查看;
14、块设备驱动的注册过程
(1)分配、初始化请求队列,绑定请求队列和请求函数;
(2)分配、初始化 gendisk,给 gendisk 的 major、 fops、 queue 等成员赋值,最后添加 gendisk;
(3)注册块设备驱动;
(4)内核的块设备驱动框架会调用块设备驱动绑定的请求处理函数,去处理应用下发的对块设备的I/O请求;
15、块设备驱动程序示例代码
(1)这里用内存来模拟块设备,申请一块内存当做块设备存储介质;
(2)分别用请求队列和不用请求队列来编写块设备驱动示例程序,其中用请求队列是对应磁原理存储设备,不用请求队列对应电原理存储设备;
(3)使用请求队列:就是要调用内核的I/O调度器,每次给块设备驱动处理的请求由I/O调度器决定;
(4)不使用请求队列:就是不调用内核的I/O调度器,每次给块设备驱动处理的请求中可以包含多个bio,一次性处理多个I/O请求
15.1、使用请求队列的ramdisk代码
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/errno.h>
#include <linux/interrupt.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/timer.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/ioport.h>
#include <linux/init.h>
#include <linux/wait.h>
#include <linux/blkdev.h>
#include <linux/blkpg.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <asm/dma.h>
#define RAMBLOCK_SIZE (1024*1024) // 1MB,2048扇区
static struct gendisk *my_ramblock_disk; // 磁盘设备的结构体
static struct request_queue *my_ramblock_queue; // 等待队列
static DEFINE_SPINLOCK(my_ramblock_lock);
static int major;
static unsigned char *my_ramblock_buf; // 虚拟块设备的内存指针
static void do_my_ramblock_request(struct request_queue *q)
struct request *req;
static int r_cnt = 0; //实验用,打印出驱动读与写的调度次数
static int w_cnt = 0;
//从请求队列中取出一个请求
req = blk_fetch_request(q);
while (NULL != req)
//要传输的数据所在位置,blk_rq_pos返回要传送输的下一个扇区
unsigned long start = blk_rq_pos(req) *512;
//获取要操作的数据的长度
unsigned long len = blk_rq_cur_bytes(req);
//判断此次I/O操作是读还是写
if(rq_data_dir(req) == READ)
// 读请求
memcpy(req->buffer, my_ramblock_buf + start, len); //读操作,
printk("do_my_ramblock-request read %d times\\n", r_cnt++);
else
// 写请求
memcpy( my_ramblock_buf+start, req->buffer, len); //写操作
printk("do_my_ramblock request write %d times\\n", w_cnt++);
//判断是否是最后一个请求,如果不是则获取下一个请求
if(!__blk_end_request_cur(req, 0树莓派基于Linux内核驱动开发详解