
所以块设备子系统,上承文件系统,下承具体的储存设备子系统,对于下层的诸多设备进行统一的抽象,以向上提供统一的块设备,起作用如下:

就块设备本身来说,可以分为三层,本专题是针对这三层进行学习,了解其基本的工作原理
块设备通常是以数据块大小(如512字节)为单位,能随机访问的设备,典型的块设备是系统中的储存设备,例如:硬盘、闪储、U盘等。
块设备按块进行划分,具体块大小由具体设备决定,通常为扇区(512字节)的整数倍。设备自身定义的访问数据块本书称之为物理数据块,块设备中文件系统还会以若干个物理数据块为单位划分逻辑数据块。块设备驱动程序通过物理块号访问底层设备,对连续块的访问可合并成一次访问,即一次可访问多个连续的物理数据块。
因为对字符设备而言,通常可以直接进行读写操作,数据量较小,可立即获得结果,不需要缓存,而块设备的读写数据量比较大,速度较慢,通常不能立即获得读写结果,读写操作需要缓存和延时。为此,块设备驱动中构建了请求队列用于缓存块设备操作,内核将块设备的读写操作封装成请求,提交到请求队列,由驱动程处理请求(执行数据传输)。
本章首先来学习块设备驱动程序的实现和流程。
块设备驱动程序中主要的数据结有:gendisk表示通用的磁盘,hd_struct结构表示分区,request_queue结构表示块设备的请求队列,block_device_operations结构表示磁盘的底层操作。
物理上的块设备,如硬盘、U盘、光盘等,在块设备驱动程序中统一由gendisk结构实例表示,称之为通用磁盘,本书中统称磁盘。块设备驱动程序的主要工作就是创建gendisk实例,并将其添加到块设备数据库。gendisk结构体定义在include/linux/genhd.h头文件内:

hd_struct结构表示磁盘分区的物理信息,gendisk结构中的part0成员(hd_struct实例)表示整个磁盘的物理信息,如磁盘容量等。hd_struct结构体定义在include/linux/genhd.h

分区表disk_part_tbl结构指针,disk_part_tbl结构体内主要包含指向磁盘分区结构实例的指针数组,结构体定义在include/linux/genhd.h头文件内

block_device_operations结构包含对磁盘底层操作的函数指针,例如:激活设备、发送控制命令、按页读写块设备等。其结构体定义在include/linux/blkdev.h

内核通过设备文件控制磁盘设备,在打开块设备文件时,其文件操作file_operations结构体为def_blk_fops实例,然后调用驱动程序中定义的block_device_operations实例响应的函数,完成对块设备的控制

其主要的框图如下图所示,例如gendisk 只有一个实例,指向 /dev/sda,其内部结构struct disk_part_tbl 结构里是一个 struct hd_struct 的数组,用于表示各个分区。struct block_device_operations fops 指向对于这个块设备的各种操作。struct request_queue queue 是表示在这个块设备上的请求队列。struct hd_struct 是用来表示某个分区的,在上面的例子中,有两个 hd_struct 的实例,分别指向 /dev/sda1、 /dev/sda2。

内核在启动阶段需要为管理块设备驱动程序中的数据结构实例做初始化,主要是创建块设备的kobj_map结构体实例

对于kobj_map_init是创建一个kobject映射域,kobj_map是一个保存了一个255目索引,以主设备号为间隔的哈希表。主要的作用是当kobj_map调用时,会匹配设备,然后把设备主设备号写进哈希表里,然后调用它的base->get,初始化为base_probe。

base_probe获取拥有该设备号的范围,起始也没有做什么

blk_dev_init()函数在/block/blk-core.c文件内实现,主要工作是为块设备驱动中数据结构创建slab缓存

register_blkdev()函数用于向内核注册块设备号,向内核申请块设备主设备号,。内核定义了全局散列表,用于对已使用的块设备主设备号进行管理,其定义在/block/genhd.c文件

全局散列表结构如下图所示,blk_major_name实例根据major%BLKDEV_MAJOR_HASH_SIZE值确定添加到的major_names[]数组项。

genhd_device_init(void)函数内调用register_blkdev(BLOCK_EXT_MAJOR, “blkext”)函数,向内核注册主设备号BLOCK_EXT_MAJOR(259),设备名称为“blkext”。
注册一个块设备驱动,需要以下步骤:
内核提供的alloc_disk(int minors)函数动态创建,参数minors表示从设备号的数量。minors最小值为1,表示磁盘不分区,大于1表示磁盘分区数量为minors-1,从设备号0表示整个磁盘,1表示第一个分区,依此类推。alloc_disk(int minors)函数定义在/block/genhd.c文件内,成功返回gendisk实例指针,否则返回NULL。


主要工作是从通用缓存中分配gendisk实例,并初始化实例,初始化表示整个磁盘的device实例。函数disk_expand_part_tbl(disk, 0)用于创建或扩展分区表结构disk_part_tbl实例,分区表中part[0]指向gendisk实例中内嵌的part0成员(hd_struct实例),表示整个磁盘。

表示gendisk实例的device实例其设备类class结构实例block_class定义在/block/genhd.c文件内,并在genhd_device_init()函数内完成注册。

块设备驱动程序中,在准备好gendisk实例(含请求队列)后,最后也是最重要的一步就是调用add_disk()函数将gendisk实例添加到块设备中。


将调用**rescan_partitions()**函数扫描磁盘分区信息,并(创建)填充至分区hd_struct结构实例(自动创建分区设备文件)。在格式化磁盘,将磁盘格式化成某种分区类型时,磁盘的分区信息将以规定的格式保存在磁盘某一固定位置,称之为分区表(不同分区类型分区表格式位置不同)。
内核在/block/partitions/check.h头文件内定义了parsed_partitions结构,用于暂存分区类型代码中扫描得到的分区信息。

rescan_partitions()函数定义在/block/partition-generic.c文件内,核心代码如下

函数内调用前面介绍的check_partition()函数扫描磁盘分区信息,填充至parsed_partitions结构实例,随后对parsed_partitions实例中parts指向的数组项,从索引值为1的项开始,对每个数组项调用add_partition()函数创建并添加分区hd_struct实例。


函数内创建hd_struct实例,将函数参数提供的分区信息赋予hd_struct实例,表示磁盘的device实例设为分区device实例的父设备。
如果磁盘的名称字符串最后一位为数字,则分区的名称为磁盘名称后加p字符再加分区号,例如:磁盘名称为hd0,则第一个分区名称为hd0p1。如果磁盘名称字符串最后一位不是数字,则分区名称为磁盘名称加分区号,例如:磁盘名称为hd,分区1的名称为hd1,分区名称将做为自动创建的块设备文件名称。
然后,初始化分区device实例,并添加到通用驱动模型中,最后将磁盘相应分区表项指向hd_struct实例,添加分区完成。

块设备要想被内核知道其存在,必须使用内核提供的一系列注册函数进行注册。驱动程序的第一步就是向内核注册自己,提供该功能的函数是
int register_blkdev(unsigned int major, const char *name)
通过注册驱动程序我们获得了主设备号,但是现在还不能对磁盘进行操作。内核对于磁盘的表示是使用的gendisk结构体, gendisk结构中的许多成员必须由驱动程序进行初始化。我们知道了其流程,下面主要来看看可以如何实现,分配一个gendisk结构并不能使磁盘对系统可用。为达到这个目的,必须初始化结构,并调用add_disk。Gendisk中包含了一个指针struct block_ device_ operations * fops ;指向对应的块设备操作函数,我们以我们用的比较多的loop块设备为例来说明整个过程,其驱动主要在drivers/block/loop.c,首先在loop_init中会做以下工作
驱动程序的第一步就是向内核注册自己

请求处理和请求队列


首先,我们会在/proc/devices节点下看到块设备的节点loop,在/dev目录下看到对应的节点信息

此之下主要有三个结构体:对块设备或设备分区的抽象结构体block_device,对磁盘的通用描述gendisk以及磁盘分区描述hd_struct。其中block_device和hd_struct一一互相关联,而gendisk
统一管理众多hd_struct。