LDD-Block Drivers

块驱动能够以固定大小的块随机访问设备,例如磁盘。Linux内核将块设备和字符设备视为完全不同的两种设备。
现代操作系统的虚拟内存系统通常会将不需要的设备从内存中换出到次级存储,设备通常是硬盘。从这个角度来讲,块驱动是主存和次级存储之间的通道,可以视为虚拟内存系统的一部分。
块设备的主要设计目标是性能,不像字符设备的性能不会显著影响整个系统的性能。

块(block)是固定大小的一段数据,通常为4KB,不同的文件系统可能有不同的大小。一个扇区(sector)的大小受硬件限制,通常为512Byte。

Registration

和字符驱动一样,块驱动也必须通过注册函数使其设备可用于内核。

Block Driver Registration

大部分块驱动首先将自己注册到内核中,通过函数int register_blkdev(unsigned int major, const char *name);实现。函数的参数是设备将使用的主设备号和相关的名称,出现在/proc/devices文件中。如果major参数为0,函数会分配一个新的主设备号,作为返回值。反注册函数为int unregister_blkdev(unsinged int major, const char *name);
2.6内核中,register_blkdev函数只进行两个操作,分配一个主设备号,在/proc/devices中创建设备项。书中说将来这个函数可能会完全移除,但是我在3.10内核还能找到这个函数。

Disk Registration

register_blkdev只会分配一个主设备号,并不会将设备添加到系统中,要实现设备的注册,需要通过另外一个注册接口。

Block device operations

字符设备通过file_operations结构体向系统提供可进行的操作,块设备对应的数据结构为struct block_device_operations,声明在linux/fs.h中,包含以下操作:

  • int (*open)(struct inode *inode, struct file *filp);
  • int (*release)(struct inode *inode, struct file *filp);
  • int (*ioctl)(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
  • int (*media_changed)(struct gendisk *gd);
    对于可移动设备的驱动有用,判断用户是否更改了驱动器中的介质
  • int (*revalidate_disk)(struct gendisk *gd);
    介质发生变化时调用,驱动程序可以在这个函数内执行使设备就绪的操作
  • struct module *owner;

可以看到,struct block_device_operations结构体中并没有读写函数,在块设备的I/O系统中,读写操作由request函数处理,之后详细说明。

The gendisk structure

struct gendisk声明在linux/genhd.h中,表示一个磁盘设备。事实上,内核还用这个结构体表示一个分区。结构体中的下列程序必须由块驱动初始化:

  • int major;
  • int first_minor;
  • int minors;
    minors指明磁盘设备分区数量,通常为16,允许15个分区
  • char disk_name[32];
    磁盘设备的名称,出现在/proc/partiotions和sysfs中
  • struct block_device_operations *fops;
  • struct request_queue *queue;
    设备的I/O请求队列
  • int flags;
    指明设备的状态,例如可移动设备
  • sector_t capacity;
    驱动器的容量,以512Byte计
  • void *private_data;

gendisk结构体需要特殊的内核操作进行初始化,因此驱动程序无法自己建立,只能通过struct gendisk *alloc_disk(int minors);进行分配,指定设备使用次设备号;通过void del_gendisk(struct gendisk *gd);释放不需要的gendisk结构体。
gendisk结构体包含kobject,也有引用计数,对应的也有get_diskput_disk函数,但是驱动绝不需要调用这些函数。通常情况下,del_gendisk函数会移除gendisk的最后一个引用。
分配一个gendisk结构体并不会将磁盘添加到系统中,还需要调用void add_disk(strcut gendisk *gd);。一旦调用add_disk函数,系统会认为设备已经准备就绪,可能会在函数返回前调用设备的操作函数,内核会读取磁盘的前几个块寻找分区表。

A Note on Sector Sizes

内核将整个磁盘视为一个线性数组,每个元素是512Byte的扇区,但不是所有的硬件都使用同样扇区大小。驱动可以通过blk_queue_hardsect_size设置请求队列的扇区大小,所有的I/O请求都会对齐到设置的扇区大小。

The Block Device Operations

The open and release Methods

可移动设备需要知道设备的用户数,以便在设备长时间没有使用时将其移除,openclose函数负责维护用户计数。
用户空间的一些操作如给磁盘分区、建立文件系统等可以调用open函数,但是块驱动并不能分别。

Supporting Removable Media

block_device_operations结构体中有两个函数和可移动设备有关,media_changed用来判断介质是否发生改变,revalidate在介质发生改变后调用,使驱动程序准备好对新介质进行操作。调用revalidate函数后,内核会尝试重新读取分区表,和设备重新开始。

The ioctl Method

块驱动可以提供ioctl函数来控制设备,但是driver/block/ioctl.c中已经实现了很多命令,ioctl命令在到达驱动程序之前就会被拦截。

Request Processing

request函数是块驱动的核心,是真正执行操作的函数。块设备对系统的性能影响很大,内核的块子系统尽可能的让驱动最大限度利用自己的设备。

Introduction to the request Method

块驱动的request函数定义如下:void request(request_queue_t *queue);,内核需要驱动对设备执行操作时会调用这个函数。request函数在返回之前并不需要完成队列中的所有请求,函数必须开始处理这些请求,保证请求最终会完成。
每个设备都有一个请求队列,因为真正的数据传输可能发生在内核请求很久之后,内核还需要队列来在最适合的时机进行传输。
设备的请求队列在初始化时和request函数关联,dev->queue = blk_init_queue(sbull_request, &dev->lock);。我们还提供了一个自旋锁,内核调用request函数时,会持有这个自旋锁,防止内核添加新的请求到队列中。
如果想在执行request函数时移除这个锁,必须保证不会访问请求队列,或者受自旋锁保护的数据,而且必须在request函数返回前重新获得锁。
最后,request函数的调用和用户空间的进程完全异步,不能假定内核运行在发出请求的进程上下文中,也不能判断当前的请求的I/O缓冲区是在内核还是在用户空间,因此任何访问用户空间的操作都是错误的。

A Simple request Method

内核提供了函数elv_next_request函数来获取队列中第一个未完成的请求,如果队列中没有请求,返回NULL。
队列中可能包含不对磁盘进行操作的非文件系统请求,包括监测操作,或者设置设备工作模式的指令。大部分块驱动不知道如何处理这些请求,只是简单地将其忽略。blk_fs_request可以判断当前的请求是否是文件系统的请求。如果不是,可以将其传递给void end_request(struct request *req, int succeedde);函数结束。
request结构体中包含以下成员:

  • sector_t sector;
    请求在设备的起始扇区号,扇区以512Byte计
  • unsigned long nr_sectors;
    要传输的扇区的数量,扇区以512Byte计
  • char *buffer;
    执行要传输的缓冲区,是内核虚拟地址
  • rq_data_dir(struct request *req);(不属于结构体)
    这个宏能够从请求中提取出传输的方向,0表示从设备读取数据,非0表示向设备写入数据

Request Queues

请求队列保存这未完成的I/O请求,还包含设备能够服务的请求的类型:最大长度,一个请求可以包含多少个分开的段,硬件的扇区大小,对齐要求等等。如果正确配置了请求队列,不会出现设备无法处理的请求。
请求队列还提供了使用多个I/O调度器(I/O scheduler 或者 elevator)的接口,I/O调度器的职责是以最高效的方式将I/O请求传递给驱动。因此,很多调度器会将一批请求按照块的索引进行排序,然后按序发送给驱动。
I/O调度器还负责将相邻的请求合并,一个新请求到达时,调度器会从队列中寻找是否有相邻的请求,如果能够找到这样的请求而且合并后的请求不会太大,就会进行合并。

Queue creation and deletion

一个请求队列是由内核的块I/O子系统建立动态建立的数据结构,函数为request_queue_t *blk_init_queue(request_fn_proc *request, spinlock_t *lock );,参数分别为request函数和控制访问该队列的锁。这个函数需要分配内存,可能会失败,因此需要检查返回值。
要将请求队列返还给系统,调用void blk_cleanup_queue(request_queue_t *);

Queueing functions

内核提供了操作请求队列的函数,但是要调用这些函数必须首先获取队列的自旋锁。

  • struct request *elv_next_request(request_queue_t *queue);
    获取要处理的下一个请求,如果队列为空返回NULL;elv_next_request还会将队列中的请求设为活跃状态,开始执行请求后阻止I/O调度器将其他请求与其合并
  • void blkdev_dequeue_request(struct request *req);
    从队列中删除请求,如果驱动程序同时响应队列中的多个请求,必须通过这种方式将其删除
  • void elv_requeue_request(request_queue_t *queue, struct request *req);
    将已经移除的请求重新添加到队列中

Queue control functions

内核的块层提供了一些控制请求队列的函数:

  • void blk_stop_queue(request_queue_t *queue);
  • void blk_start_queue(request_queue_t *queue);
    如果队列中的请求数已经超过能够处理的上限,可以调用blk_stop_queue告知块层,直到调用blk_start_queue后才会重新开始工作。调用这两个函数时必须持有自旋锁
  • void blk_queue_bounce_limit(request_queue_t *queue, u64 dma_addr);
    告知内核设备可以进行DMA的最大物理地址;如果一个请求超出限制,会采用跳板缓冲区进行操作
  • void blk_queue_max_sectors(request_queue_t *queue, unsigned short max);
  • void blk_queue_max_phys_segments(request_queue_t *queue, unsigned short max);
  • void blk_queue_max_hw_segments(request_queue_t *queue, unsigned short max);
  • void blk_queue_max_segment_size(request_queue_t *queue, unsigned int max);
    设置设备能够处理的请求的大小,sectors函数指明请求中可包含的扇区的最大数,默认为255;phys_segments函数设置驱动可以处理的段数,比如静态分配的scatterlist的长度;hw_segments函数设置设备能够处理的最大的段数(段数通常指系统内存中的不连续区域);segment_size函数告知内核请求中的单个段的大小,默认为65536字节。
  • blk_queue_segment_boundary(request_queue_t *queue, unsigned long mask);
    有些设备不能处理跨过某个边界的请求,通过这个函数设置
  • void blk_queue_dma_alignment(request_queue_t *queue, int mask);
    设置DMA传输的对齐方式,请求的起始地址和请求的长度都需要进行对齐,默认为512字节
  • void blk_queue_hardsect_size(request_queue_t *queue, unsinged short max);
    告知内核设备的扇区大小,内核产生的所有请求都会是设置的大小的整数倍并进行对齐,但是块层和驱动之间的通信仍然是512自己大小的扇区

    The Anatomy of a Request

    每一个request结构体对象代表一个块I/O请求,可能由多个独立的请求合并而来。每个请求通常包含几个段,每个段对应一个内存内的缓冲区。内核会将磁盘上连续的扇区组合起来,但是不会将同一个请求内的读写操作进行组合。
    request结构体包含bio结构体的链表,以及一些方便驱动程序管理的其他信息。bio结构体是块I/O请求的更低层描述。

    The bio structure

    当内核以文件系统的形式,或者虚拟内存子系统,系统调用需要和块I/O设备传输一些块时,通过bio结构体来描述这个操作。然后将bio对象合并到一个已有的或者新建的request结构体。bio结构体包含驱动程序完成请求所需要的所有信息,无需用户进程的再次帮助。
    bio结构体定义在linux/bio.h中,包含以下成员:
  • sector_t bi_sector;
    要传输的第一个扇区,以512Byte计
  • unsigned int bi_size;
    要传输的字节数
  • unsigned long bi_flags;
    标志位,如果最低位是1,表明为写请求
  • unsinged short bio_phys_segments;
  • unsigned short bio_hw_segments;
    该BIO包含的物理段数;DMA映射后硬件可见的段数

bio结构体的核心是一个名为bi_io_vec的数组,元素为bio_vec结构体,定义如下:

struct bio_vec {
    struct page *bv_page;
    unsinged int bv_len;
    unsinged int bv_offset;
};

内核提供了大量的函数来访问bio结构体内的成员变量,包括获取将要传输的下一个page,数据的页内偏移量,当前页面内的内存段数,要传输的数据的内核虚拟地址(不能用于高内存),任何缓冲区的内核虚拟地址(原子操作,不能休眠)。这些操作都是针对当前的缓冲区,即要传输的第一个缓冲区。

Request structure fields

request结构体包含以下成员:

  • sector_t hard_sector;
  • unsigned long hard_nr_sectors;
  • unsigned int hard_cur_sectors;
    用来追踪未传输的扇区的信息:hard_sector保存第一个未传输的扇区,hard_nr_sectors保存所有未传输的扇区数,hard_cur_sectors保存当前bio内未传输的扇区数。这些信息供块子系统使用,驱动不应该使用
  • struct bio *bio;
    该请求包含的bio链表,通过rq_for_each_bio访问
  • char *buffer;
    当前bio要传输的数据的内核虚拟地址
  • unsigned short nr_phys_segments;
    合并连续的地址后当前请求内的物理内存段数
  • struct list_head queuelist;
    用来将请求添加到请求队列中

Barrier requests

块层会在请求到达驱动程序之前将其重新排序,将多个请求发送给驱动器,让硬件判断最优的序列,以便提升I/O性能。但在有些应用中,比如关系型数据库,日志信息必须在操作执行前写入到磁盘中,无限制的重排序可能会带来问题。
为了解决这个问题,块层引入了屏障请求(barrier request)。如果一个请求设置了REQ_HARDBARRER标志,必须在发出后续请求之前写入到驱动器(非易失性存储)。许多驱动器会缓存写请求,以便提升性能,但是会破坏屏障请求的初衷。如果关键数据在断电时还保存在缓存中,这些数据就会丢失。

如果驱动支持屏障请求,首先要告知块层;屏障请求是例外一种请求队列,通过void blk_queue_ordered(request_queue_t *queue, int flag);设置,flag为非零值表明支持屏障请求。int blk_barrier_rq(struct request *req);可以判断请求是否为屏障请求;是为非零值。

Nonretryable requests

块驱动通常会在第一次失败后重试请求,以增强系统可靠性,防止数据丢失。内核有时会将请求标记为不合重试请求,在第一次执行后失败直接退出。
如果驱动程序试图在第一次执行失败后进行重试,需要通过函数int blk_noretry_request(struct request *req);判断请求是否可重试,不可重试返回非零值。

Request Completion Functions

驱动程序在设备完成部分或所有传输后,函数int end_that_request_first(struct request *req, int success, int count);告知块子系统从上次完成后的位置起完成了count个扇区的传输。如果传输成功,success为1,否则为0。扇区的传输完成情况必须按序报告。
end_that_request_first的返回值表明当前请求的所有扇区是否都已传输,0表明所有扇区都已传输,请求已经完成,必须通过函数blkdev_dequeue_request将请求从队列中删除,并且调用函数void end_that_request_last(struct request *req);通知等待请求的进程,并且回收request结构体,必须在持有队列锁的时候才能调用此函数。

Block requests with DMA

高性能的块设备驱动通常利用DMA进行数据传输,可以为每个bio对象创建一个DMA映射然后进行传输。如果设备支持scatter/gather I/O,还可以通过函数int blk_rq_map_sg(request_queue_t *queue, struct request *req, struct scatterlist *list);,用请求中的所有段来构建可供dma_map_sg函数使用的list,返回值为list中的表项数;驱动必须在调用函数前创建scatterlist。
函数还会在创建scatterlist前将相邻的内存段合并,如果不想合并,通过clear_bit(QUEUE_FLAG_CLUSTER, &queue->queue_flags);宏设置。

Doing without a request queue

前文曾经说过,内核会将请求进行重新排序,甚至停止请求队列以便将请求进行组合,以便获得设备比如机械硬盘的性能提升。但是对于随机读写性能很好的设备,这种优化措施就没有必要。对于这些设备,块层提供了无队列的操作模型。要使用无队列的模型,驱动程序需要提供make request函数typedef int (make_request_fn)(request_queue_t *q, struct bio *bio);
虽然请求队列依然存在,但是不会包含任何请求。make_request函数会直接进行传输,也可能将请求重定向到其他设备。
如果直接进行传输,在完成后,需要通过函数void bio_endio(struct bio *bio, unsigned int bytes, int error);告知bio的创建者完成情况。bytes是传输的字节数,error为非零表明有错误产生。
make_request函数总是返回0;如果返回非零值,bio会被再次提交,驱动程序可以修改bio内的一些成员来修改传输。
驱动需要告知块层自己使用了自定义的make_request函数,通过request_queue_t *blk_alloc_queue(int flags);分配请求队列,这个函数不会像blk_init_queue建立队列来保存请求,flags指明内存分配的方式。建立队列和make_request函数后,传递给void blk_queue_make_request(request_queue_t *queue, make_request_fn *func);

Some Other Details

Command Pre-Preparation

块层提供了在elv_next_request返回前检测和预处理请求的介质,方便驱动提前建立真实的驱动器命令、判断能否处理请求、或者其他操作。要使用这种特性,创建一个命令准备函数typedef int (prep_rq_fn)(request_queue_t *queue, struct request *req);
request结构体包含一个名为cmd的成员,大小为BLK_MAX_CDB字节,可以用来保存真正的硬件命令,函数的返回值可以是以下值:

  • BLKPREP_OK
    命令准备过程正常进行,请求可以递交给驱动的request函数
  • BLKPREP_KILL
    请求不能完成,以错误码退出
  • BLKPREP_DEFER
    请求目前不能完成,在队列的第一项,但是不会递交给request函数

elv_next_request函数会在请求返回给驱动程序前调用准备函数,如果函数返回BLKPREP_DEFER,elv_next_request函数会返回NULL给驱动,常用于设备处理的请求已经达到上限的情况。
要使块层调用准备函数,调用void blk_queue_prep_rq(request_queue_t *queue, prep_rq_fn *func);。请求队列默认是没有准备函数的。

Tagged Command Queueing

支持多个请求的硬件通常支持某种标签化的命令队列(tagged command queueing,TCQ)。TCQ给每个命令打上一个整数标签,驱动器完成命令后,可以通过标签告知驱动程序完成的命令是哪个。2.6内核包含TCQ的基本框架,供所有驱动使用。
如果驱动器支持TCQ,需要在初始化时通过函数int blk_queue_init_tags(request_queue_t *queue, int depth, struct blk_queue_tag *tags);告知内核。queue是请求队列,depth是设备能够同时处理的标签的最大数量,tags参数可选,blk_queue_init_tags函数会分配。若要在多个设备间共享同样的标签,可以通过tags参数。
如果设备能够处理的标签数改变,通过int blk_queue_resize_tags(request_queue_t *queue, int new_depth);通知内核,调用函数时需要持有队列锁。
通过int blk_queue_start_tag(request_queue_t *queue, struct request *req);将标签和请求相关联,同样需要持有队列锁。
如果标签可用,函数会将其分配给这个请求,将标签号保存到req->tag,返回0;并将请求从队列中删除,添加到自己的标签追踪结构体中(tag-tracking structure),因此驱动程序不需要手动进行出队操作。如果没有标签可用,blk_queue_start_tag会返回非零值,将请求留在队列中。
一个请求的所有传输完成后,驱动需要调用函数void blk_queue_end_tag(request_queue_t *queue, struct request *req);返还标签,同样需要持有队列锁。应该在end_that_request_first函数返回0后,在end_that_request_last函数前调用这个函数——此时请求已经出队,调用会产生错误。
要根据标签号获取对应的请求,调用函数struct request *blk_queue_find_tag(request_queue_t *queue, int tag);
如果TCQ在执行的过程中发生了错误,导致已经发出的标签无法完成,块层提供了函数可以进行恢复void blk_queue_invalidate_tags(request_queue_t *queue);,将所有未完成的标签返还,将相关的请求重新放入请求队列,调用这个函数时需要持有队列锁。

你可能感兴趣的:(LDD-Block Drivers)