一、块设备的I/O操作特点
字符设备与块设备I/O操作的不同在于:
① 块设备只能以块为单位接受输入和返回输出,而字符设备则以字节为单位。大多数设备是字符设备,因为它们不需要缓冲而且不以固定块大小进行操作。
② 块设备对于I/O请求有对应的缓冲区,因此它们可以选择以什么顺序进行响应,字符设备无需缓冲且被直接读写。对于存储设备而言调整读写的顺序作用巨大,因为在读写连续的扇区比分离的扇区更快。
③ 字符设备只能被顺序读写,而块设备可以随机访问。虽然块设备可随机访问,但是对于磁盘这类机械设备而言,顺序地组织块设备的访问可以提高性能。对磁盘1、10、3、2的请求被调整为对1、2、3、10的请求可以提高读写性能。注意,对SD卡、RAMDISK等块设备而言,不存在机械上的原因,进行这样的调整没有必要。
二、Linux块设备驱动结构
block_device_operations结构体
在块设备驱动中,有1个类似于字符设备驱动中file_operations结构体的block_device_operations结构体,它是对块设备操作的集合,定义如代码:block_device_operations结构体
1 struct block_device_operations |
下面对其主要的成员函数进行分析:
• 打开和释放
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。
gendisk结构体
在Linux内核中,使用gendisk(通用磁盘)结构体来表示1个独立的磁盘设备(或分区),这个结构体的定义如代码gendisk结构体
1 struct gendisk |
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字节为单位。
request与bio结构体
1、请求
在Linux块设备驱动中,使用request结构体来表征等待进行的I/O请求,这个结构体的定义如代码request结构体
1 struct request |
request结构体的主要成员包括:
sector_t hard_sector; |
上述3个成员标识还未完成的扇区,hard_sector是第1个尚未传输的扇区,hard_nr_sectors是尚待完成的扇区数,hard_cur_sectors是并且当前I/O操作中待完成的扇区数。这些成员只用于内核块设备层,驱动不应当使用它们。
sector_t sector; |
驱动中会经常与这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;
该值表示相邻的页被合并后,这个请求在物理内存中占据的段的数目。
struct list_head queuelist;
用于链接这个请求到请求队列的链表结构,调用blkdev_dequeue_request()可从队列中移除请求。
使用如下宏可以从request获得数据传送的方向:
rq_data_dir(struct request *req);
0返回值表示从设备中读,非 0返回值表示向设备写。
2、请求队列
一个块请求队列是一个块 I/O 请求的队列,其定义如代码 1 struct request_queue |
请求队列跟踪等候的块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的缺点是比较庞大与复杂,在一些特殊的情况下,特别是在数据吞吐量非常大的数据库系统中它会变得比较缓慢。
Deadline I/O scheduler是针对Anticipatory I/O scheduler的缺点进行改善而来的,表现出的性能几乎与Anticipatory I/O scheduler一样好,但是比Anticipatory小巧。
CFQ I/O scheduler为系统内的所有任务分配相同的带宽,提供一个公平的工作环境,它比较适合桌面环境。事实上在测试中它也有不错的表现,mplayer、xmms等多媒体播放器与它配合的相当好,回放平滑,几乎没有因访问磁盘而出现的跳帧现象。
内核block目录中的noop-iosched.c、as-iosched.c、deadline-iosched.c和cfq-iosched.c文件分别实现了上述调度算法。
可以通过给kernel添加启动参数,选择使用的IO调度算法,如:
kernel elevator=deadline
• 初始化请求队列
request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock); |
该函数的第1个参数是请求处理函数的指针,第2个参数是控制访问队列权限的自旋锁,这个函数会发生内存分配的行为,故它可能会失败,因此一定要检查它的返回值。这个函数一般在块设备驱动的模块加载函数中调用。
• 清除请求队列
void blk_cleanup_queue(request_queue_t * q); |
这个函数完成将请求队列返回给系统的任务,一般在块设备驱动模块卸载函数中调用。
而blk_put_queue()宏则定义为:
#define blk_put_queue(q) blk_cleanup_queue((q))
• 分配“请求队列”
request_queue_t *blk_alloc_queue(int gfp_mask); |
对于FLASH、RAM盘等完全随机访问的非机械设备,并不需要进行复杂的I/O调度,这个时候,应该使用上述函数分配1个“请求队列”,并使用如下函数来绑定“请求队列”和“制造请求”函数。
void blk_queue_make_request(request_queue_t * q, make_request_fn * mfn); |
这种方式分配的“请求队列”实际上不包含任何请求,所以给其加上引号。
• 提取请求
struct request *elv_next_request(request_queue_t *queue); |
上述函数用于返回下一个要处理的请求(由 I/O 调度器决定),如果没有请求则返回NULL。elv_next_request()不会清除请求,它仍然将这个请求保留在队列上,但是标识它为活动的,这个标识将阻止I/O 调度器合并其它的请求到已开始执行的请求。因为elv_next_request()不从队列里清除请求,因此连续调用它2次,2次会返回同一个请求结构体。
• 去除请求
void blkdev_dequeue_request(struct request *req); |
上述函数从队列中去除1个请求。如果驱动中同时从同一个队列中操作了多个请求,它必须以这样的方式将它们从队列中去除。
如果需要将1个已经出列的请求归还到队列中,可以调用:
void elv_requeue_request(request_queue_t *queue, struct request *req); |
另外,块设备层还提供了一套函数,这些函数可被驱动用来控制一个请求队列的操作,主要包括:
• 启停请求队列
void blk_stop_queue(request_queue_t *queue); |
如果块设备到达不能处理等候的命令的状态,应调用blk_stop_queue()来告知块设备层。之后,请求函数将不被调用,除非再次调用blk_start_queue()将设备恢复到可处理请求的状态。
• 参数设置
void blk_queue_max_sectors(request_queue_t *queue, unsigned short max); |
这些函数用于设置描述块设备可处理的请求的参数。blk_queue_max_sectors()描述任一请求可包含的最大扇区数,缺省值为255;blk_queue_max_phys_segments()和 blk_queue_max_hw_segments()都控制1个请求中可包含的最大物理段(系统内存中不相邻的区),blk_queue_max_hw_segments()考虑了系统I/O内存管理单元的重映射,这2个参数缺省都是 128。blk_queue_max_segment_size告知内核请求段的最大字节数,缺省值为65,536。
• 通告内核
void blk_queue_bounce_limit(request_queue_t *queue, u64 dma_addr); |
该函数用于告知内核块设备执行DMA时可使用的最高物理地址dma_addr,如果一个请求包含超出这个限制的内存引用,一个“反弹”缓冲区将被用来给这个操作。这种方式的代价昂贵,因此应尽量避免使用。
可以给dma_addr参数提供任何可能的值或使用预先定义的宏,如BLK_BOUNCE_HIGH(对高端内存页使用反弹缓冲区)、BLK_BOUNCE_ISA(驱动只可在16M的ISA区执行DMA)或者BLK_BOUCE_ANY(驱动可在任何地址执行DMA),缺省值是BLK_BOUNCE_HIGH。
blk_queue_segment_boundary(request_queue_t *queue, unsigned long mask);
如果我们正在驱动编写的设备无法处理跨越一个特殊大小内存边界的请求,应该使用这个函数来告知内核这个边界。例如,如果设备处理跨4MB 边界的请求有困难,应该传递一个0x3fffff 掩码。缺省的掩码是0xffffffff(对应4GB边界)。
void blk_queue_dma_alignment(request_queue_t *queue, int mask);
该函数用于告知内核块设备施加于DMA 传送的内存对齐限制,所有请求都匹配这个对齐,缺省的屏蔽是 0x1ff,它导致所有的请求被对齐到 512字节边界。
void blk_queue_hardsect_size(request_queue_t *queue, unsigned short max);
该函数用于告知内核块设备硬件扇区的大小,所有由内核产生的请求都是这个大小的倍数并且被正确对界。但是,内核块设备层和驱动之间的通信还是以512字节扇区为单位进行。
3、块I/O
通常1个bio对应1个I/O请求,代码清单8.5给出了bio结构体的定义。IO调度算法可将连续的bio合并成1个请求。所以,1个请求可以包含多个bio。bio结构体
1 struct bio |
下面我们对其中的核心成员进行分析:
sector_t bi_sector;
标示这个 bio 要传送的第一个(512字节)扇区。
unsigned int bi_size;
被传送的数据大小,以字节为单位,驱动中可以使用bio_sectors(bio)宏获得以扇区为单位的大小。
unsigned long bi_flags;
一组描述 bio 的标志,如果这是一个写请求,最低有效位被置位,可以使用bio_data_dir(bio)宏来获得读写方向。
unsigned short bio_phys_segments;
unsigned short bio_hw_segments;
分别表示包含在这个 BIO 中要处理的不连续的物理内存段的数目和考虑DMA重映像后的不连续的内存段的数目。
bio的核心是一个称为 bi_io_vec的数组,它由bio_vec结构体组成,bio_vec结构体的定义如代码清单8.6。
代码清单8.6bio_vec结构体
1 struct bio_vec |
我们不应该直接访问bio的bio_vec成员,而应该使用bio_for_each_segment()宏来进行这项工作,可以用这个宏循环遍历整个bio中的每个段,这个宏的定义如代码清单8.7。
bio_for_each_segment宏
1 #define __bio_for_each_segment(bvl, bio, i, start_idx) \ |
图8.2(a)描述了request队列、request与bio数据结构之间的关系,8.2(b)表示了request、bio和bio_vec数据结构之间的关系,8.2(c)表示了bio与bio_vec数据结构之间的关系,因此整个图8.2递归地呈现了request队列、request、bio和bio_vec这4个结构体之间的关系。
(a) request与bio
(b) request、bio和bio_vec
(c)bio与bio_vec
图8.2request队列、request、bio和bio_vec结构体之间的关系
内核还提供了一组函数(宏)用于操作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入口指向的数据缓冲区。它也会屏蔽中断并返回1个原子kmap,因此,在bvec_kunmap_irq()被调用以前,驱动不应该睡眠。
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()获得的内核虚拟地址。
另外,对bio的引用计数通过如下函数完成:
void bio_get(struct bio *bio); //引用bio
void bio_put(struct bio *bio); //释放对bio的引用
三、块设备驱动注册与注销
块设备驱动中的第1个工作通常是注册它们自己到内核,完成这个任务的函数是register_blkdev(),其原型为:int register_blkdev(unsigned int major, const char *name);
major参数是块设备要使用的主设备号,name为设备名,它会在/proc/devices中被显示。 如果major为0,内核会自动分配一个新的主设备号,register_blkdev()函数的返回值就是这个主设备号。如果register_blkdev()返回1个负值,表明发生了一个错误。
与register_blkdev()对应的注销函数是unregister_blkdev(),其原型为:
int unregister_blkdev(unsigned int major, const char *name);
这里,传递给register_blkdev()的参数必须与传递给register_blkdev()的参数匹配,否则这个函数返回-EINVAL。
值得一提的是,在2.6内核中,对 register_blkdev()的调用完全是可选的,register_blkdev()的功能已随时间正在减少,这个调用最多只完全2件事:
① 如果需要,分配一个动态主设备号。
② 在/proc/devices中创建一个入口。
在将来的内核中,register_blkdev()可能会被去掉。但是目前的大部分驱动仍然调用它。代码清单8.8给出了1个块设备驱动注册的模板。
块设备驱动注册模板
1 xxx_major = register_blkdev(xxx_major, "xxx"); |
四、Linux块设备驱动模块加载与卸载
在块设备驱动的模块加载函数中通常需要完成如下工作:① 分配、初始化请求队列,绑定请求队列和请求函数。
② 分配、初始化gendisk,给gendisk的major、fops、queue等成员赋值,最后添加gendisk。
③ 注册块设备驱动。
下面分别给出了使用blk_alloc_queue()分配请求队列并使用blk_queue_make_request()绑定“请求队列”和“制造请求”函数,以及使用blk_init_queue()初始化请求队列并绑定请求队列与请求处理函数2种不同情况下的块设备驱动模块加载函数模板。
块设备驱动模块加载函数模板(使用blk_alloc_queue)
1 static int __init xxx_init(void) |
块设备驱动模块加载函数模板(使用blk_init_queue)
1 static int __init xxx_init(void) |
在块设备驱动的模块卸载函数中通常需要与模块加载函数相反的工作:
① 清除请求队列。
② 删除gendisk和对gendisk的引用。
③ 删除对块设备的引用,注销块设备驱动。
代码清单8.11给出了块设备驱动模块卸载函数的模板。
代码清单8.11 块设备驱动模块卸载函数模板
1 static void __exit xxx_exit(void) |
块设备的打开与释放
块设备驱动的open()和release()函数并非是必须的,1个简单的块设备驱动可以不提供open()和release()函数。
块设备驱动的open()函数和其字符设备驱动中的对等体非常类似,都以相关的inode和file结构体指针作为参数。当一个节点引用一个块设备时,inode->i_bdev->bd_disk包含一个指向关联 gendisk结构体的指针。因此,类似于字符设备驱动,我们也可以将gendisk的private_data赋给file的private_data,private_data同样最好是指向描述该设备的设备结构体xxx_dev的指针,如代码清单8.12。
代码清单8.12 块设备的open()函数中赋值private_data
1 static int xxx_open(struct inode *inode, struct file *filp) |
在一个处理真实的硬件设备的驱动中,open()和release()方法还应当设置驱动和硬件的状态,这些工作可能包括启停磁盘、加锁一个可移出设备和分配DMA缓冲等。
块设备驱动的ioctl函数
与字符设备驱动一样,块设备可以包含一个 ioctl()函数以提供对设备的I/O控制能力。实际上,高层的块设备层代码处理了绝大多数ioctl(),因此,具体的块设备驱动中通常不再需要实现很多ioctl命令。
代码清单8.13给出的ioctl()函数只实现1个命令HDIO_GETGEO,用于获得磁盘的几何信息(geometry,指CHS,即Cylinder、Head、Sector/Track)。
代码清单8.13 块设备驱动的I/O控制函数模板
1 int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, |
块设备驱动I/O请求处理
使用请求队列
块设备驱动请求函数的原型为:
void request(request_queue_t *queue);
这个函数不能由驱动自己调用,只有当内核认为是时候让驱动处理对设备的读写等操作时,它才调用这个函数。
请求函数可以在没有完成请求队列中的所有请求的情况下返回,甚至它1个请求不完成都可以返回。但是,对大部分设备而言,在请求函数中处理完所有请求后再返回通常是值得推荐的方法。代码清单8.14给出了1个简单的request()函数的例子。
代码清单8.14 块设备驱动请求函数例程
1 static void xxx_request(request_queue_t *q) |
上述代码第4行使用elv_next_request()获得队列中第一个未完成的请求,end_request()会将请求从请求队列中剥离。第7行判断请求是否为文件系统请求,如果不是,则直接清除,调用end_request(),传递给end_request()的第2个参数为0意味着处理该请求失败。而第15行传递给end_request()的第2个参数为1意味着该请求处理成功。
end_request()函数非常重要,其源代码如代码清单8.15。
end_request()函数源代码
1 void end_request(struct request *req, int uptodate) |
当设备已经完成1个I/O请求的部分或者全部扇区传输后,它必须通告块设备层,上述代码中的第4行完成这个工作。end_that_request_first()函数的原型为:
int end_that_request_first(struct request *req, int success, int count);
这个函数告知块设备层,块设备驱动已经完成count个扇区的传送。end_that_request_first()的返回值是一个标志,指示是否这个请求中的所有扇区已经被传送。返回值为0表示所有的扇区已经被传送并且这个请求完成,之后,我们必须使用blkdev_dequeue_request()来从队列中清除这个请求。最后,将这个请求传递给end_that_request_last()函数:
void end_that_request_last(struct request *req);
end_that_request_last()通知所有正在等待这个请求完成的对象请求已经完成并回收这个请求结构体。
第6行的add_disk_randomness()函数的作用是使用块 I/O 请求的定时来给系统的随机数池贡献熵,它不影响块设备驱动。但是,仅当磁盘的操作时间是真正随机的时候(大部分机械设备如此),才应该调用它。
给出了1个更复杂的请求函数,它进行了3层遍历:遍历请求队列中的每个请求;遍历请求中的每个bio;遍历bio中的每个段。
请求函数遍历请求、bio和段
1 static void xxx_full_request(request_queue_t *q) |
不使用请求队列
使用请求队列对于一个机械的磁盘设备而言的确有助于提高系统的性能,但是对于许多块设备,如数码相机的存储卡、RAM盘等完全可真正随机访问的设备而言,无法从高级的请求队列逻辑中获益。对于这些设备,块层支持“无队列”的操作模式,为使用这个模式,驱动必须提供一个“制造请求”函数,而不是一个请求函数,“制造请求”函数的原型为:
typedef int (make_request_fn) (request_queue_t *q, struct bio *bio);
上述函数的第1个参数仍然是“请求队列”,但是这个“请求队列”实际不包含任何请求。因此,“制造请求”函数的主要参数是bio结构体,这个bio结构体表示1个或多个要传送的缓冲区。“制造请求”函数或者直接进行传输,或者把请求重定向给其它设备。
在“制造请求”函数中处理bio的方式与8.6.1节中描述的完全一致,但是在处理完成后应该使用bio_endio()函数通知处理结束:
void bio_endio(struct bio *bio, unsigned int bytes, int error);
参数bytes 是已经传送的字节数,它可以比这个bio所代表的字节数少,这意味着“部分完成”,同时bio结构体中的当前缓冲区指针需要更新。当设备进一步处理这个bio后,驱动应该再次调用 bio_endio(),如果不能完成这个请求,应指出一个错误,错误码赋值给error参数。
不管对应的I/O处理成功与否,“制造请求”函数都应该返回0。如果“制造请求”函数返回一个非零值,bio 将被再次提交。
给出了1个“制造请求”函数的例子。
“制造请求”函数例程
1 static int xxx_make_request(request_queue_t *q, struct bio *bio) |
为了使用无队列的I/O请求处理,驱动模块加载函数应遵循代码清单8.9的模板而非8.10的模板,而使用请求队列时,驱动模块加载函数应遵循代码清单8.10的模板。
总结
块设备的I/O操作方式与字符设备存在较大的不同,因而引入了request_queue、request、bio等一系列数据结构。在整个块设备的I/O操作中,贯穿于始终的就是“请求”,字符设备的I/O操作则是直接进行不绕弯,块设备的I/O操作会排队和整合。
驱动的任务是处理请求,对请求的排队和整合由I/O调度算法解决,因此,块设备驱动的核心就是请求处理函数或“制造请求”函数。
尽管在块设备驱动中仍然存在block_device_operations结构体及其成员函数,但其不再包含读写一类的成员函数,而只是包含打开、释放及I/O控制等与具体读写无关的函数。
块设备驱动的结构相当复杂的,但幸运的是,块设备不像字符设备那么包罗万象,它通常就是存储设备,而且驱动的主体已经由Linux内核提供,针对一个特定的硬件系统,驱动工程师所涉及到的工作往往只是编写少量的与硬件直接交互的代码。