第十三章 Linux块设备驱动
本章导读
块设备提供块设备提供设备的存取,设备的存取,可以随机的以固定大小的块传输数据,例如我们最为常见的磁盘设备,当然块设备和字符设备有较大差别,块设备有自己的驱动接口。简单来说,内核决定一个块是固定的4096字节,当然该值可以随着依赖文件系统的变化而改变。块设备驱动采用register_blkdev向系统进行注册,unregister_blkdev取消注册,块设备采用操作结构 struct block_device_operations,此例子中我实现一个面向块的、基于内存的设备驱动(下面成为sbull_drv)。 在内核中,用struct gendisk表示单个磁盘驱动器。 struct gedisk 中的一些成员, 块设备驱动必须初始化它们: major,first_minor,minors(描述被磁盘使用的设备号,一个驱动器必须使用最少一个次编号),disk_name,block_device_operations,request_queue (IO请求管理结构),private_data,gendisk(需要内核操作初始化,驱动无法自行初始化该结构,而是调用)。
a, register_blkdev注册块设备
b,初始化 struct sbull_dev,确定设备内存大小,分配底层内存
c,初始化自旋锁,blk_init_queue分配请求队列,实现请求队列和请求函数关联
d,alloc_disk分配设备的gendisk,安装初始化对应的gendisk结构,f,编写block_device_operations成员对应的功能接口函数实现,add_disk调用该磁盘
块设备驱动高性能的地方关键在于其请求队列(简单讲即块IO请求的队列),请求队列对等候的IO请求进行跟踪,也可以对请求队列进行配置成你希望处理的请求类型。请求队列实现插入接口,使用多I/O调度器,I/O调度器以最大性能的方式向驱动提交请求,累计足够多的的请求并将它们块索引以顺序排列提交给驱动,I/O调度器还负责合并邻近的请求。一个请求结构被实现为一个 bio 结构的链表,结合一些维护信息来使驱动可以跟踪它的位置。
块设备是与字符设备并列的概念,这两类设备在Linux中驱动的结构有较大差异,总体而言,块设备驱动比字符设备驱动要复杂得多,在I/O操作上表现出极大的不同,缓冲、I/O调度、请求队列等都是与块设备驱动相关的概念。本章将向您展示Linux块设备驱动的编程方法。
13.1节分析块设备I/O操作的特点,对比字符设备与块设备在I/O操作上的差异。
13.2节从整体上描述Linux块设备驱动的结构,分析主要的数据结构、函数及其关系。
13.3~13.5节分别阐述块设备驱动模块加载与卸载、打开与释放和ioctl()函数。
13.6节非常重要,它讲述了块设备I/O操作所依赖的请求队列的概念及用法。
13.2节与13.3~13.6节是整体与部分的关系,13.2~13.6节与13.7节是迭代递进关系。
13.7节在13.1~13.6节讲解内容的基础上,总结Linux下块设备的读写流程。而13.7节则给出了块设备驱动的具体实例,即RAMDISK的驱动。
13.1块设备的I/O操作特点
字符设备与块设备I/O操作的不同在于:
① 块设备只能以块为单位接受输入和返回输出,而字符设备则以字节为单位。大多数设备是字符设备,因为它们不需要缓冲而且不以固定块大小进行操作。
② 块设备对于I/O请求有对应的缓冲区,因此它们可以选择以什么顺序进行响应,字符设备无需缓冲且被直接读写。对于存储设备而言调整读写的顺序作用巨大,因为在读写连续的扇区比分离的扇区更快。
③ 字符设备只能被顺序读写,而块设备可以随机访问。虽然块设备可随机访问,但是对于磁盘这类机械设备而言,顺序地组织块设备的访问可以提高性能。如图13.1,对磁盘1、10、3、2的请求被调整为对1、2、3、10的请求可以提高读写性能。注意,对SD卡、RAMDISK等块设备而言,不存在机械上的原因,进行这样的调整没有必要。
图13.1 调整块设备I/O操作的顺序
13.2 Linux块设备驱动结构
13.2.1 block_device_operations结构体
在块设备驱动中,有1个类似于字符设备驱动中file_operations结构体的block_device_operations结构体,它是对块设备操作的集合,定义如代码清单13.1。
代码清单13.1 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; //模块拥有者
13 };
下面对其主要的成员函数进行分析:
• 打开和释放
int (*open)(struct inode *inode, struct file *filp);
int (*release)(struct inode *inode, struct file *filp);
与字符设备驱动类似,当设备被打开和关闭时将调用它们。
• IO控制
int (*ioctl)(struct inode *inode, struct file *filp, unsigned int cmd,
unsigned long arg);
上述函数是ioctl() 系统调用的实现,块设备包含大量的标准请求,这些标准请求由Linux块设备层处理,因此大部分块设备驱动的ioctl()函数相当短。
• 介质改变
int (*media_changed) (struct gendisk *gd);
被内核调用来检查是否驱动器中的介质已经改变,如果是,则返回一个非零值,否则返回0。这个函数仅适用于支持可移动介质的驱动器(非可移动设备的驱动不需要实现这个方法),通常需要在驱动中增加1个表示介质状态是否改变的标志变量。
• 使介质有效
int (*revalidate_disk) (struct gendisk *gd);
revalidate_disk()函数被调用来响应一个介质改变,它给驱动一个机会来进行必要的工作以使新介质准备好。
• 获得驱动器信息
int (*getgeo)(struct block_device *, struct hd_geometry *);
该函数根据驱动器的几何信息填充一个hd_geometry结构体,hd_geometry结构体包含磁头、扇区、柱面等信息。
• 模块指针
struct module *owner;
一个指向拥有这个结构体的模块的指针,它通常被初始化为THIS_MODULE。
13.2.2 gendisk结构体
在Linux内核中,使用gendisk(通用磁盘)结构体来表示1个独立的磁盘设备(或分区),这个结构体的定义如代码清单13.2。
代码清单13.2 gendisk结构体
1 struct gendisk
2 {
3 int major; /* 主设备号 */
4 int first_minor; /*第1个次设备号*/
5 int minors; /* 最大的次设备数,如果不能分区,则为1*/
6 char disk_name[32]; /* 设备名称 */
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 };
major、first_minor和minors共同表征了磁盘的主、次设备号,同一个磁盘的各个分区共享1个主设备号,而次设备号则不同。fops为block_device_operations,即上节描述的块设备操作集合。queue是内核用来管理这个设备的 I/O请求队列的指针。capacity表明设备的容量,以512个字节为单位。private_data可用于指向磁盘的任何私有数据,用法与字符设备驱动file结构体的private_data类似。
Linux内核提供了一组函数来操作gendisk,主要包括:
• 分配gendisk
gendisk结构体是一个动态分配的结构体,它需要特别的内核操作来初始化,驱动不能自己分配这个结构体,而应该使用下列函数来分配gendisk:
struct gendisk *alloc_disk(int minors);
minors 参数是这个磁盘使用的次设备号的数量,一般也就是磁盘分区的数量,此后minors不能被修改。
• 增加gendisk
gendisk结构体被分配之后,系统还不能使用这个磁盘,需要调用如下函数来注册这个磁盘设备:
void add_disk(struct gendisk *gd);
特别要注意的是对add_disk()的调用必须发生在驱动程序的初始化工作完成并能响应磁盘的请求之后。
• 释放gendisk
当不再需要一个磁盘时,应当使用如下函数释放gendisk:
void del_gendisk(struct gendisk *gd);
• gendisk引用计数
gendisk中包含1个kobject成员,因此,它是一个可被引用计数的结构体。通过get_disk()和put_disk()函数可用来操作引用计数,这个工作一般不需要驱动亲自做。通常对 del_gendisk()的调用会去掉gendisk的最终引用计数,但是这一点并不是一定的。因此,在del_gendisk()被调用后,这个结构体可能继续存在。
• 设置gendisk容量
void set_capacity(struct gendisk *disk, sector_t size);
块设备中最小的可寻址单元是扇区,扇区大小一般是2的整数倍,最常见的大小是512字节。扇区的大小是设备的物理属性,扇区是所有块设备的基本单元,块设备无法对比它还小的单元进行寻址和操作,不过许多块设备能够一次就传输多个扇区。虽然大多数块设备的扇区大小都是512字节,不过其它大小的扇区也很常见,比如,很多CD-ROM盘的扇区都是2K大小。
不管物理设备的真实扇区大小是多少,内核与块设备驱动交互的扇区都以512字节为单位。因此,set_capacity()函数也以512字节为单位。
13.2.3 request与bio结构体
1、请求
在Linux块设备驱动中,使用request结构体来表征等待进行的I/O请求,这个结构体的定义如代码清单13.3。
代码清单13.3 request结构体
1 struct request
2 {
3 struct list_head queuelist; /*链表结构*/
4 unsigned long flags; /* REQ_ */
5
6 sector_t sector; /* 要传送的下1个扇区 */
7 unsigned long nr_sectors; /*要传送的扇区数目*/
8 /*当前要传送的扇区数目*/
9 unsigned int current_nr_sectors;
10
11 sector_t hard_sector; /*要完成的下1个扇区*/
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 };
request结构体的主要成员包括:
sector_t hard_sector;
unsigned long hard_nr_sectors;
unsigned int hard_cur_sectors;
上述3个成员标识还未完成的扇区,hard_sector是第1个尚未传输的扇区,hard_nr_sectors是尚待完成的扇区数,hard_cur_sectors是并且当前I/O操作中待完成的扇区数。这些成员只用于内核块设备层,驱动不应当使用它们。
sector_t sector;
unsigned long nr_sectors;
unsigned int current_nr_sectors;
驱动中会经常与这3个成员打交道,这3个成员在内核和驱动交互中发挥着重大作用。它们以512字节大小为1个扇区,如果硬件的扇区大小不是512字节,则需要进行相应的调整。例如,如果硬件的扇区大小是2048字节,则在进行硬件操作之前,需要用4来除起始扇区号。
hard_sector、hard_nr_sectors、hard_cur_sectors与sector、nr_sectors、current_nr_sectors之间可认为是“副本”关系。
struct bio *bio;
bio是这个请求中包含的bio结构体的链表,驱动中不宜直接存取这个成员,而应该使用后文将介绍的rq_for_each_bio()。
char *buffer;
指向缓冲区的指针,数据应当被传送到或者来自这个缓冲区,这个指针是一个内核虚拟地址,可被驱动直接引用。
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 *req,
struct scatterlist *sglist);
该函数与dma_map_sg()类似,它返回scatterlist列表入口的数量。
struct list_head queuelist;
用于链接这个请求到请求队列的链表结构,调用blkdev_dequeue_request()可从队列中移除请求。
使用如下宏可以从request获得数据传送的方向:
rq_data_dir(struct request *req);
0返回值表示从设备中读,非 0返回值表示向设备写。
2、请求队列
一个块请求队列是一个块 I/O 请求的队列,其定义如代码清单13.4。
代码清单13.4 request队列结构体
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 };
请求队列跟踪等候的块I/O请求,它存储用于描述这个设备能够支持的请求的类型信息、它们的最大大小、多少不同的段可进入一个请求、硬件扇区大小、对齐要求等参数,其结果是:如果请求队列被配置正确了,它不会交给该设备一个不能处理的请求。
请求队列还实现一个插入接口,这个接口允许使用多个I/O调度器,I/O调度器(也称电梯)的工作是以最优性能的方式向驱动提交I/O请求。大部分I/O 调度器累积批量的 I/O 请求,并将它们排列为递增(或递减)的块索引顺序后提交给驱动。进行这些工作的原因在于,对于磁头而言,当给定顺序排列的请求时,可以使得磁盘顺序地从一头到另一头工作,非常像一个满载的电梯,在一个方向移动直到所有它的“请求”已被满足。
另外,I/O调度器还负责合并邻近的请求,当一个新 I/O 请求被提交给调度器后,它会在队列里搜寻包含邻近扇区的请求;如果找到一个,并且如果结果的请求不是太大,调度器将合并这2个请求。
对磁盘等块设备进行I/O操作顺序的调度类似于电梯的原理,先服务完上楼的乘客,再服务下楼的乘客效率会更高,而“上蹿下跳”,顺序响应用户的请求则会导致电梯无序地忙乱。
Linux 2.6包含4个I/O调度器,它们分别是No-op I/O scheduler、Anticipatory I/O scheduler、Deadline I/O scheduler与CFQ I/O scheduler。
Noop I/O scheduler是一个简化的调度程序,它只作最基本的合并与排序。
Anticipatory I/O scheduler是当前内核中默认的I/O调度器,它拥有非常好的性能,在2.5中它就相当引人注意。在与2.4内核进行的对比测试中,在2.4中多项以分钟为单位完成的任务,它则是以秒为单位来完成的,正因为如此它成为目前2.6中默认的I/O调度器。Anticipatory I/O scheduler的缺点是比较庞大