Linux块设备软件栈

VFS层(虚拟文件系统层): 由于内核需要跟不同的文件系统打交道,而每一个文件系统所实现的方式和数据结构也不尽相同,所以内核抽象了这一层,专门用来适配各种文件系统,对上提供统一的操作接口,对下层的诸多设备进行同意的抽象。
页缓存层: 负责对于文件的缓存在page cache中
Block Layer(通用块层): 由于绝大多数情况的io操作是跟块设备打交道,所以Linux在此提供了一个类似vfs层的块设备操作抽象层。下层对接各种不同属性的块设备,对上提供统一的Block IO请求标准。
块设备驱动层:
块设备层:这层就是具体的物理设备了,定义了各种真对设备操作方法和规范。
所以块设备子系统,上承文件系统,下承具体的储存设备子系统,对于下层的诸多设备进行统一的抽象,以向上提供统一的块设备,起作用如下:

对上,Block子系统位于文件系统层的下方,通过bdev伪文件系统管理系统中的所有磁盘抽象,使得其他文件系统等访问接口可以找到一个磁盘的抽象
对下,为具体的存储设备提供通用的服务,包括磁盘和分区抽象、IO请求优化、重映射等。
就块设备本身来说,可以分为三层,本专题是针对这三层进行学习,了解其基本的工作原理

通用块层(Block Layer): 位于最上,对存储设备的设备和分区进行文件系统可用的抽象,对上提供统一的Block IO请求标准
IO调度(I/O scheduler): 层位于中间,负责对上层下发的IO的合并优化等工作,提供NOOP,CFQ,DeadLine,Anticipatory 4种IO调度器
块设备驱动层(device driver layer): 将通用块设备作为操作对象的”驱动”,request函数对请求队列中每个bio进行分别处理,根据bio中的信息向磁盘控制器发送命令。处理完成后,调用完成函数end_bio以通知上层完成。
块设备通常是以数据块大小(如512字节)为单位,能随机访问的设备,典型的块设备是系统中的储存设备,例如:硬盘、闪储、U盘等。

块设备按块进行划分,具体块大小由具体设备决定,通常为扇区(512字节)的整数倍。设备自身定义的访问数据块本书称之为物理数据块,块设备中文件系统还会以若干个物理数据块为单位划分逻辑数据块。块设备驱动程序通过物理块号访问底层设备,对连续块的访问可合并成一次访问,即一次可访问多个连续的物理数据块。

因为对字符设备而言,通常可以直接进行读写操作,数据量较小,可立即获得结果,不需要缓存,而块设备的读写数据量比较大,速度较慢,通常不能立即获得读写结果,读写操作需要缓存和延时。为此,块设备驱动中构建了请求队列用于缓存块设备操作,内核将块设备的读写操作封装成请求,提交到请求队列,由驱动程处理请求(执行数据传输)。

本章首先来学习块设备驱动程序的实现和流程。

1 数据结构
块设备驱动程序中主要的数据结有: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。

2 初始化
内核在启动阶段需要为管理块设备驱动程序中的数据结构实例做初始化,主要是创建块设备的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”。

3 驱动程序流程
注册一个块设备驱动,需要以下步骤:

调用register_blkdev(major,name)函数申请(或动态分配)块设备号
调用alloc_disk(minors)函数分配通用磁盘gendisk结构实例,定义block_device_operations结构实例,并赋予gendisk实例
构建请求队列
调用add_disk(disk)函数向块设备数据库注册并激活块设备。
3.1 分配磁盘alloc_disk
内核提供的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()函数内完成注册。

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

根据磁盘分区实例part0(即代表整个磁盘)内的分区编号(通常为0)
gendisk实例中给出的主设备号major以及起始从设备号first_minor,生成part0表示磁盘的块设备号,赋予deivce实例
blk_register_region()函数将gendisk实例添加到块设备
最后调用register_disk(disk)函数向内核注册gendisk实例,主要工作是将表示磁盘的device实例注册到通用驱动模型和sysfs文件系统中,以及扫描磁盘分区信息,创建并填充至分区hd_struct实例


初始化表示整个磁盘的device实例,并调用device_add(ddev)函数添加实例,添加过程中将自动创建磁盘设备文件
调用bdget_disk(disk, 0)函数在bdev伪文件系统中创建表示整个磁盘的block_device实例
如果gendisk实例在添加前设置了磁盘容量,则调用函数blkdev_get()建立表示整个磁盘的block_device实例与gendisk和gendisk.part0实例之间的关联,并扫描磁盘分区信息,创建填充各分区hd_struct实例。
将调用**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实例,添加分区完成。

3 如何编写块设备驱动程序


块设备要想被内核知道其存在,必须使用内核提供的一系列注册函数进行注册。驱动程序的第一步就是向内核注册自己,提供该功能的函数是

int register_blkdev(unsigned int major, const char *name)
1
参数是该设备使用的主设备号及其名字,name通常与设备文件名称相同,但也可以是任意有效的字符串。
如果传递的主设备号是0,内核将分派一个新的主设备号给设备,并将该设备号返回给调用者。
使用该函数,块设备将会显示在/proc/devices。
通过注册驱动程序我们获得了主设备号,但是现在还不能对磁盘进行操作。内核对于磁盘的表示是使用的gendisk结构体, gendisk结构中的许多成员必须由驱动程序进行初始化。我们知道了其流程,下面主要来看看可以如何实现,分配一个gendisk结构并不能使磁盘对系统可用。为达到这个目的,必须初始化结构,并调用add_disk。Gendisk中包含了一个指针struct block_ device_ operations * fops ;指向对应的块设备操作函数,我们以我们用的比较多的loop块设备为例来说明整个过程,其驱动主要在drivers/block/loop.c,首先在loop_init中会做以下工作

驱动程序的第一步就是向内核注册自己


请求处理和请求队列

注册gendisk


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


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

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
                        
原文链接:https://blog.csdn.net/u012489236/article/details/125359060

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值