块设备驱动程序(Linux设备驱动程序)

一个块设备驱动程序主要通过传输固定大小的随机数据来访问设备。
Linux内核视块设备为字符设备相异的基本设备类型。
块驱动程序有自己完成特定任务的接口。

高效的块设备驱动程序在性能上是严格要求的。
块驱动程序是在核心内存与其他存储介质之间的管道,因此可以被认为是虚拟内存子系统的组成部分。

许多字符设备可以在远低于其最快速率下工作。
Linux块设备驱动程序接口使得块设备可以发挥其最大的功效。

一个数据块指的是固定大小的数据,其大小的值由内核确定。
数据块的大小通常是4096个字节。
与数据块对应的是扇区,它是由底层硬件决定大小的一个块。
内核所处理的设备扇区大小是512字节。
无论何时内核为用户提供了一个扇区编号,该扇区的大小就是512字节。



注册块设备驱动程序
向内核注册自己,执行该任务的函数是register_blkdev:
int register_blkdev(unsigned int major, const char *name);
int unregister_blkdev(unsigned int major, const char *name);
函数register_blkdev所执行的功能随时间的推移而越来越少,在这里该接口所做的事情是:其一,如果需要的话分配一个动态的主设备号;其二,在/proc/devices中创建一个入口项。

虽然register_blkdev能获得主设备号,但是并不能让系统使用任何磁盘。
为了管理独立的磁盘,必须使用另外一个单独的注册接口。


字符设备使用file_operations结构告诉系统对它们的操作接口。
块设备使用类似的数据结构,在<linux/fs.h>中声明了结构block_device_operations。

int (*media_changed)(struct gendisk *gd);
内核调用该函数以检查用户是否更换了驱动器内的介质,如果用户更换了,返回一非零值。
该函数只是用于那些支持可移动介质(并且为驱动程序设置了“介质更换”标志位)的驱动器。

int (*revalidata_disk)(struct gendisk *gd);
当介质被更换时,调用它做出响应。


没有函数负责读和写数据。
在块设备的I/O子系统中,这些操作是由request函数处理的。


内核使用gendisk结构(在<linux/genhd.h>中声明)表示一个独立的磁盘。
内核还是用gendisk结构表示分区,如果驱动器是可被分区的,用户将要为每个可能的分区分配一个次设备号。


内核为使用gendisk结构提供了一些函数。
gendisk结构是一个动态分配的结构,它需要一些内核的特殊处理来进行初始化。驱动程序不能自己动态分配该结构,而是必须调用:
struct gendisk *alloc_disk(int minors);

void del_gendisk(struct gendisk *gd);

void add_disk(struct gendisk *gd);
一旦调用了add_disk,磁盘设备将被“激活”,并随时会调用它提供的方法。

内核可能会读取前面几个块的数据以获得分区表,因此在驱动程序完全被初始化并且能够响应对磁盘的请求前,不要调用add_disk。


请求处理
每个块设备驱动程序的核心是它的请求函数。


void request(request_queue_t *queue);
当内核需要驱动程序处理读取、写入以及其他对设备的操作时,就会调用该函数。
每个设备都有一个请求队列。

request函数是在一个原子上下文中运行的。

request机构代表了一个块设备的I/O执行请求。

一个块请求队列可以包含那些实际并不向磁盘读出写入数据的请求。
这些请求包括生产商信息、底层诊断操作,或者是与特殊设备模式相关的指令,比如对可记录介质的写模式的设定。


block_fs_request调用告诉用户该请求是一个文件系统请求——移动块数据的请求。
如果不是文件系统请求,则将其传递给end_request:
void end_request(struct request *req, int succeeded);

一个块设备请求队列可以这样描述:包含块设备I/O请求的序列。

请求队列实现了插件接口,使得多个I/O调度器的使用成为可能。
一个I/O调度器的作用是以性能最大化为目的,为驱动程序提供I/O请求。

一个请求队列就是一个动态的数据结构,该结构必须由块设备的I/O子系统创建。
创建和初始化请求队列的函数是:
rquest_queue_t *blk_init_queue(request_fn_proc *request, spinlock_t *lock);
把请求队列返回给系统(通常在模块卸载时):
void blk_cleanup_queue(request_queue_t *);

内核提供了函数elv_next_request用来获得队列中第一个未完成的请求;当没有请求需要处理时,该函数返回NULL。
elv_next_request并不从队列中删除请求。如果不加以干涉而两次调用该函数,则两次都返回相同的request结构。
只有当请求完成后,它们才离开队列。
elv_next_request为其做了活动标记,该标记保证了当开始执行该请求时,I/O调度器不再将该请求与其他请求合并。

将请求从队列中实际删除,使用:
void blkdev_dequeue_request(struct request *req);
如果驱动程序同时处理同一队列中的多个请求,则驱动程序必须按照此方式将它们拿出队列。

将拿出队列的请求再返回给队列:
void elv_requeue_request(request_queue_t *queue, struct request *req);


队列控制函数


块设备I/O在Linux内核中如何表示
一个request结构作为一个bio结构的链表实现。
bio结构是在底层对部分块设备I/O请求的描述。


当内核以文件系统、虚拟内存子系统或者系统调用的形式决定从块I/O设备输入、输出块数据时,它将再结合一个bio结构,用来描述这个操作。该结构被传递给I/O代码,代码把它合并到一个已经存在的request结构中,或者再创建一个新的request结构。
bio结构包含了驱动程序执行请求的全部信息,而不必与初始化这个请求的用户空间的进程相关联。

如果一个请求被设置了REQ_HARDBARRER标志,那么在其他后续请求被初始化前,它必须被写入驱动器。


不可重试请求


请求完成函数
当设备完成在一个I/O请求的部分或者全部的扇区时,它必须调用下面的函数通知块设备子系统:
int end_that_request_first(struct request *req, int success, int count);


end_that_request_first的返回值表明该请求中的所有扇区是否被传输。


end_that_request_last通知任何等待已经完成请求的对象。

你可能感兴趣的:(块设备驱动程序(Linux设备驱动程序))