[TOC]
Linux 源码中的 block
目录:块设备 I/O 的核心中枢
block
目录是 Linux 内核 I/O 栈的心脏。它负责管理所有块设备(如硬盘驱动器 HDD、固态硬盘 SSD、U 盘等)的数据交换。这一层的主要目标是提供一个通用的、高效的框架,将上层文件系统和应用程序的 I/O 请求,转化为底层具体硬件驱动程序可以执行的操作。
一、 历史与背景
这项技术是为了解决什么特定问题而诞生的?
block
层的诞生是为了解决两个核心问题:
- 抽象与解耦:如果没有一个通用的块设备层,每个文件系统(如 ext4, XFS)都需要自己编写直接与各种硬盘驱动(SATA, SCSI, NVMe)对话的代码。这会导致代码冗余、开发复杂且难以维护。
block
层提供了一个标准的接口,使得文件系统无需关心底层硬件的具体细节,反之亦然。 - 性能优化:对存储设备的随机访问通常效率很低,尤其是在机械硬盘上,会导致磁头频繁寻道,浪费大量时间。
block
层通过引入 I/O 调度器 (I/O Scheduler),可以对传入的 I/O 请求进行合并、排序和延迟处理,将随机 I/O 尽可能地转化为顺序 I/O,从而大幅提升吞吐量和系统响应速度。
它的发展经历了哪些重要的里程碑或版本迭代?
- 早期设计 (Single-Queue Layer):在很长一段时间里,Linux 的块设备层是单队列模型。每个块设备只有一个请求队列 (
request_queue
),所有 CPU 核心的 I/O 请求都必须通过一个全局锁来访问这个队列。这个设计在单核、慢速硬盘时代工作得很好。 - 多队列块层 (Multi-Queue Block Layer, blk-mq):随着多核 CPU 和高速 SSD(尤其是 NVMe 设备)的普及,单队列模型的锁竞争问题成为了巨大的性能瓶颈。从 3.13 内核版本开始,Jens Axboe 主导开发了
blk-mq
。它为每个 CPU 核心分配了独立的提交队列(Software Queue),大大减少了锁竞争,使得 I/O 性能可以在多核系统上有效扩展,充分释放了现代硬件的潜力。blk-mq
已成为当前 Linux 内核的默认块层框架。 - IO_uring 的出现:虽然
io_uring
并非block
目录的一部分,但它是 I/O 领域的最新重大革新。它提供了一种极其高效的、真正的异步 I/O 提交和完成机制,通过环形缓冲区与内核共享数据,避免了传统系统调用的开销,进一步提升了 I/O 性能。它与block
层协同工作,代表了 I/O 接口的未来方向。
目前该技术的社区活跃度和主流应用情况如何?
block
层是 Linux 内核最活跃、最核心的子系统之一。由于存储技术(NVMe, ZNS, 计算存储)的飞速发展,block
层的代码始终在不断演进以适应新的硬件和应用场景。所有需要持久化存储的 Linux 系统,从嵌入式设备到大型数据中心服务器,都依赖于 block
层来管理存储 I/O。
二、 核心原理与设计
block
层的核心工作流程可以概括为:bio
-> request
-> 调度 -> 驱动。
核心数据结构
struct bio
(Block I/O vector):这是块设备层最基本的 I/O 单元。它代表一个 I/O 操作,包含目标设备、起始扇区、数据所在的内存页(通过bio_vec
数组描述,支持零拷贝的 “Scatter-Gather I/O”)、操作方向(读/写)等信息。文件系统或虚拟内存子系统将它们的请求打包成bio
结构体,然后提交给block
层。struct request
:这是一个更高级别的 I/O 请求容器。block
层会将一个或多个bio
合并成一个request
。例如,如果两个bio
请求是对磁盘上相邻的扇区进行读操作,它们就可以被合并成一个request
。I/O 调度器操作的对象就是request
。struct request_queue
:每个块设备都拥有一个请求队列。这是block
层的核心,负责接收request
,并调用 I/O 调度器对其进行管理。在blk-mq
框架下,这个结构变得更加复杂,管理着多个硬件和软件队列。
核心工作原理
- 请求生成:文件系统(例如,当你想写入一个文件时)创建一个或多个
bio
结构体来描述这个操作。 - 请求提交:
bio
被提交到block
层。 - 请求排队与调度:
block
层(特别是blk-mq
)将bio
放入对应 CPU 的软件队列中。I/O 调度器(如 BFQ, Kyber, mq-deadline)根据其策略对队列中的request
进行排序和调度,以优化性能(例如,将多个小的写操作合并成一个大的写操作,或者优先处理读请求以降低延迟)。 - 分发给驱动:调度器选出一个
request
后,block
层会将其分发给对应的底层设备驱动程序(例如drivers/block/nvme.c
)。 - 硬件操作:设备驱动程序解析
request
,生成具体的硬件命令(如 NVMe 命令),并通过 DMA 将数据在内存和设备之间传输。 - 完成通知:当硬件完成操作后,会产生一个中断。驱动程序的中断处理函数会被调用,它会通知
block
层该request
已完成。block
层再逐级向上通知,最终唤醒等待该 I/O 的应用程序。
主要优势
- 模块化与抽象化:清晰地分离了逻辑层(文件系统)和物理层(设备驱动)。
- 性能优化:通过 I/O 调度和请求合幷,显著提升存储性能。
- 可扩展性:
blk-mq
架构为现代多核和高速设备提供了出色的性能扩展能力。 - 灵活性:支持插件式的 I/O 调度器,可以根据工作负载和硬件类型选择最合适的调度策略。
关键源码文件
bio.c
:bio
结构体的分配、管理和辅助函数。blk-core.c
: 块设备层的核心逻辑,包括请求的提交和完成处理。blk-mq.c
: 多队列块层 (blk-mq
) 的核心实现。genhd.c
: “General Hard Disk”,负责磁盘和分区的注册、管理(如gendisk
结构体)。*-iosched.c
(e.g.,bfq-iosched.c
,mq-deadline.c
): 各种 I/O 调度器的具体实现。
三、 使用场景与交互
block
层作为中间层,与内核多个重要子系统紧密交互:
上游用户 (Producers):
- 文件系统 (
fs/
): 最主要的用户。当读写文件时,文件系统将操作转换为对文件所在逻辑块的bio
请求。 - 虚拟内存系统 (
mm/
): 当进行页面换入换出(Swapping)时,会通过block
层读写交换分区/文件。 - 裸设备访问: 用户程序通过
/dev/
下的设备节点(如/dev/sda
)直接发起的 I/O 操作(例如dd
命令)。
- 文件系统 (
下游消费者 (Consumers):
- 设备驱动 (
drivers/block/
,drivers/scsi/
,drivers/nvme/
): 这些驱动接收request
,并将其翻译成硬件能懂的命令。 - 设备映射器 (
drivers/md/
): LVM (逻辑卷管理)、Software RAID、dm-crypt (磁盘加密) 等技术通过 Device Mapper 框架工作。它们在block
层之上创建虚拟的块设备,并将对虚拟设备的 I/O 请求,经过处理后,再重新提交给block
层,由block
层转发给底层的物理设备。
- 设备驱动 (
四、 入门实践 (如何观察和交互)
你无法像安装软件一样“使用”block
层,但可以通过多种方式观察和调整它的行为。
查看块设备信息
1 | # 列出所有块设备 |
与 I/O 调度器交互
块设备层的许多参数都通过 sysfs
文件系统暴露在 /sys/block/<device>/queue/
目录下。
1 | # 查看 sda 设备当前可用的 I/O 调度器和正在使用的调度器 |
实时监控 I/O 性能
iostat
: 提供详细的 I/O 统计信息,如tps
(每秒传输次数),kB/s
,await
(平均等待时间) 等。1
iostat -dx 2
iotop
: 类似于top
命令,但用于显示进程的 I/O 使用情况。1
sudo iotop
blktrace
/btrace
: 非常强大的底层块 I/O 跟踪工具,可以详细记录每一个 I/O 请求从生成到完成的整个生命周期。这是内核开发者和性能工程师分析 I/O 问题的利器。
源码浏览建议
- 使用在线的 Linux 源码浏览器,如 Elixir Cross Referencer,可以方便地进行函数跳转和类型定义查看。
- 从一个简单的流程入手,例如
submit_bio
函数,跟踪一个bio
是如何被处理并最终进入request_queue
的。
五、未来趋势
- 对新型硬件的支持:
- Zoned Block Devices: SMR (叠瓦式磁记录) 硬盘和 ZNS (分区命名空间) SSD 要求按特定顺序或在特定区域写入。
block
层正在不断增强,以原生支持这些设备的写入约束。 - 计算存储 (Computational Storage): 允许在存储设备上直接执行计算任务。未来的
block
层可能需要演进出能够卸载特定操作(如压缩、过滤)的接口。
- Zoned Block Devices: SMR (叠瓦式磁记录) 硬盘和 ZNS (分区命名空间) SSD 要求按特定顺序或在特定区域写入。
- 与
io_uring
的深度融合:io_uring
提供了更高性能的 I/O 提交路径,block
层将继续与之深度集成,以减少软件开销,为应用程序提供极致的 I/O 性能。 - 软件定义的简化: 对于某些极高速的设备,传统的 I/O 调度器可能反而会带来开销。因此,
block
层也提供了简化的路径(如none
调度器或直接轮询模式),让上层应用(如数据库)可以自己决定 I/O 策略。
六、总结
Linux 内核的 block
目录是构建高性能、高可靠性存储系统的基石。它不仅是一个驱动和文件系统之间的“中间人”,更是一个高度复杂的、持续优化的性能调优框架。
关键特性总结:
- 抽象层: 统一了对所有块设备的访问接口。
- 性能优化核心: 通过请求合并和 I/O 调度来提升效率。
- 现代架构:
blk-mq
专为多核、高速存储设备设计,具有出色的扩展性。 - 高度可配置: 允许在运行时动态调整调度器和队列参数。
对于想要深入理解 Linux I/O 性能、文件系统实现或存储驱动开发的工程师来说,block
目录是必须深入研究和理解的关键部分。
[TOC]
block/bdev.c 块设备管理(Block Device Management) VFS与块设备层的接口
历史与背景
这项技术是为了解决什么特定问题而诞生的?
这项技术是为了在VFS(虚拟文件系统)的抽象层和块设备层之间建立一个关键的接口和转换层。当用户空间的一个程序尝试打开一个设备文件(如/dev/sda1
)时,VFS看到的是一个inode
和一个file
对象,但它需要一种方式来与底层的、代表物理或逻辑磁盘的块设备实体进行交互。
block/bdev.c
的核心职责就是解决这个问题,具体包括:
- 提供VFS接口:为块设备文件(在
/dev
下的节点)提供标准的file_operations
实现,使得open()
,release()
,ioctl()
等VFS调用能够作用于块设备。 - 管理“活动”设备实例:
genhd.c
中的gendisk
代表一个“已注册的、潜在的”块设备。而bdev.c
则管理struct block_device
对象,这个对象代表一个**“活动的、被打开的”**块设备实例。它负责按需创建、缓存和释放这些实例。 - 实现独占访问(Exclusive Access):这是一个关键的安全和数据一致性特性。
bdev.c
实现了逻辑来防止对一个块设备的危险并发操作。例如,它会阻止在一个已经被挂载了文件系统的设备上运行mkfs
命令,或者阻止同时挂载同一个设备两次(以读写模式)。 - 为裸I/O提供缓存:当用户直接读写块设备文件(如
dd if=/dev/sda ...
)时,这些I/O也需要一个缓存机制。bdev.c
通过为每个block_device
关联一个特殊的内存inode
(bd_inode
),利用这个inode
的页面缓存(Page Cache)来缓存裸设备的块数据。
它的发展经历了哪些重要的里程碑或版本迭代?
bdev.c
是块设备层非常古老和核心的部分,其演进与VFS和设备模型的发展同步。
- 基本功能:早期就确立了通过设备号(
dev_t
)查找和管理block_device
实例的核心模型。 - 引入
bd_inode
:为块设备创建专门的内存inode,使其能够无缝地接入VFS的页面缓存和地址空间(address_space
)管理,这是一个重要的里程碑,统一了文件缓存和裸设备缓存的管理方式。 - 完善独占访问逻辑:随着系统变得越来越复杂,
bdev_get_exclusive()
等函数的逻辑被不断增强,以处理更复杂的嵌套和共享场景,防止数据损坏。 - 与热插拔和设备移除集成:增强了在设备被物理移除时,如何安全地处理仍然被打开的
block_device
实例的逻辑。
目前该技术的社区活跃度和主流应用情况如何?
block/bdev.c
是块层最稳定的部分之一,其核心接口和原理极少改变。
- 绝对核心:任何与块设备文件的交互都必须经过
bdev.c
。每一次mount
,每一次对/dev/sdX
的dd
或fdisk
操作,其内核路径都会经过这个文件。 - 社区状态:代码库非常稳定。社区的活跃度主要体现在修复一些边界条件下的bug,以及在VFS或块层进行其他重构时,同步更新
bdev.c
中的相关代码以保持兼容性。
核心原理与设计
它的核心工作原理是什么?
bdev.c
的核心是管理struct block_device
的生命周期,并将其与VFS的inode
模型进行绑定。
查找与实例化 (
bdev_lookup
,bdget
):当内核需要一个block_device
实例时(例如,在mount
或open
一个设备文件时),它会提供一个设备号dev_t
。bdev.c
会首先在一个全局的哈希表(bdev_hashtable
)中查找是否已存在对应的block_device
实例。- 如果存在,就增加其引用计数并返回。
- 如果不存在,它就会分配一个新的
struct block_device
,并将其与由genhd.c
注册的gendisk
关联起来,然后将其加入哈希表。
特殊的
bd_inode
:每个struct block_device
都有一个关联的inode
,即bdev->bd_inode
。这个inode
非常特殊:- 它不存储在任何物理磁盘上,完全存在于内存中。
- 它的作用是作为VFS和块设备之间的“适配器”。
- 它的
inode->i_mapping
(地址空间)被用来缓存对裸设备的读写。当你dd
一个设备时,数据块实际上被读入了这个bd_inode
的页面缓存中。 - 它的
inode->i_bdev
指针会指回它所属的struct block_device
。
打开操作 (
bdev_open
):当一个设备文件被打开时,VFS会调用bdev.c
中注册的bdev_open
函数。这个函数会:- 获取对应的
block_device
实例。 - 检查并申请访问模式。如果是独占打开(
O_EXCL
),它会调用bdev_get_exclusive()
来“锁定”设备,防止其他独占用户。它还会检查设备是否已被挂载等,以防止冲突。
- 获取对应的
关闭操作 (
bdev_release
):当最后一个用户关闭设备文件时,bdev_release
会被调用。它会递减block_device
的引用计数,并在需要时释放独占锁。
它的主要优势体現在哪些方面?
- 统一接口:使得无论是文件系统、
mkfs
工具还是dd
命令,都可以用标准的open()
/read()
/write()
/close()
模型来与任何块设备交互。 - 安全与一致性:强大的独占访问控制机制是防止文件系统损坏的关键 safeguard。
- 缓存复用:通过
bd_inode
,巧妙地复用了通用的页面缓存机制来为裸设备I/O提供缓存,避免了为裸设备访问再造一套缓存轮子。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 缓存混淆:
bd_inode
缓存裸设备块的行为有时会令用户和管理员感到困惑。例如,在对一个设备进行dd
写操作后,立即dd
读操作可能会非常快,因为它命中了页面缓存,这可能掩盖了真实的磁盘性能。清空页面缓存(drop_caches
)会同时影响文件缓存和裸设备缓存。 - 作为内部组件的复杂性:
gendisk
,block_device
,bd_inode
三者之间的关系非常紧密且微妙,是理解块层的一个难点。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
它是Linux内核中与“打开的”块设备进行交互的唯一且标准的解决方案。
- 挂载文件系统 (
mount
):mount
系统调用在执行时,必须先“打开”目标块设备,获取其block_device
实例,然后将这个实例传递给文件系统的mount
函数,作为文件系统读写数据的句柄。 - 创建文件系统 (
mkfs
):mkfs.ext4 /dev/sda1
这样的命令会以独占模式打开/dev/sda1
,bdev.c
确保此时该设备没有被挂载,然后mkfs
才能安全地在上面写入文件系统元数据。 - 裸设备读写 (
dd
,fdisk
,smartctl
):所有直接操作设备文件的工具,都是通过bdev.c
提供的VFS接口来访问设备的。 - 交换空间的启用 (
swapon
):swapon /dev/sdb2
命令也需要打开块设备,以将其设置为交换分区。
是否有不推荐使用该技术的场景?为什么?
这个提法不适用,因为它是一个必须使用的基础框架。任何需要以文件方式访问块设备的操作,都无法绕过它。
对比分析
请将其 与 其他相似技术 进行详细对比。
struct block_device
(bdev.c
) vs. struct gendisk
(genhd.c
)
这是一个理解块层结构最关键的对比。
特性 | struct block_device (bdev.c ) |
struct gendisk (genhd.c ) |
---|---|---|
核心概念 | 代表一个活动的、被打开的块设备实例或句柄。 | 代表一个已注册的、潜在的块设备及其分区信息。 |
生命周期 | 动态的。当设备被首次打开时按需创建,当没有用户时可以被销毁。 | 相对静态。在驱动加载、设备被发现时创建,在设备移除时销毁。 |
数量关系 | 一个gendisk (如sda )可以对应多个block_device 实例(一个给sda 本身,一个给sda1 ,一个给sda2 …)。 |
一个gendisk 代表一个可分区的“磁盘”实体。 |
主要管理者 | block/bdev.c |
block/genhd.c |
VFS接口 | 直接提供。通过其bd_inode ,使得设备可以被open 。 |
间接关联。它包含了block_device_operations ,但需要通过block_device 才能被VFS使用。 |
好比 | 一张进入图书馆的**“阅览卡”**。 | 图书馆里的**“图书索引卡片”**。 |
bd_inode
(块设备inode) vs. Filesystem Inode (文件系统inode)
特性 | bd_inode (bdev.c ) |
Filesystem Inode (fs/inode.c ) |
---|---|---|
代表对象 | 块设备本身(如/dev/sda1 )。 |
设备上的一个文件或目录(如/mnt/sda1/myfile.txt )。 |
存储位置 | 仅在内存中。 | 存储在磁盘上(文件系统的元数据区),并在内存中有缓存。 |
页面缓存内容 | 缓存的是设备的裸数据块。 | 缓存的是具体文件的内容。 |
交互方式 | 由bdev.c 在内部创建和管理。 |
由具体的文件系统驱动创建和管理。 |
BDEV_I 将通用的 inode 转换为块设备特定的 bdev_inode 结构体
1 | struct bdev_inode { |
I_BDEV 于从 inode 中提取与块设备(block_device)相关的信息
1 | struct block_device *I_BDEV(struct inode *inode) |
bdev_cache_init 用于初始化块设备的缓存
1 | static struct file_system_type bd_type = { |
block/blk-core.c 请求队列管理核心(Request Queue Management Core) I/O提交的中央枢纽
历史与背景
这项技术是为了解决什么特定问题而诞生的?
这项技术是Linux块设备层的心脏,它为了解决如何接收、管理和分派所有I/O请求这一核心问题而诞生。在内核中,文件系统等上层模块只知道要读写文件的某个部分,而底层设备驱动只知道如何操作硬件。block/blk-core.c
就是连接这两者的中央枢纽和翻译官。
它具体解决了以下关键问题:
- 抽象I/O请求队列:需要一个通用的数据结构来代表与一个块设备通信的“通道”,这个结构就是
struct request_queue
。blk-core.c
负责这个核心结构体的创建、配置和生命周期管理。 - 统一I/O提交通道:为内核所有部分(文件系统、MM、裸设备访问)提供一个单一的、标准的入口点来提交I/O请求(
bio
)。这个入口点就是submit_bio()
和其历史前身generic_make_request()
。 - 解耦I/O调度:设备驱动程序不应该关心如何对I/O请求进行排序以优化性能(例如,为了减少HDD的磁头寻道)。
blk-core.c
提供了一个可插拔的I/O调度器框架,允许将不同的调度算法(如BFQ, mq-deadline)附加到请求队列上,而驱动本身对此无感。 - I/O合并与优化 (Plug/Unplug):为了提高HDD的吞吐量,将多个小的、随机的I/O请求在提交给调度器之前先**“暂存”(plugging)起来,然后一次性“释放”(unplugging)**,可以给调度器一个更大的批次来做合并和排序,从而将随机I/O转换为更高效的顺序I/O。
blk-core.c
实现了这个经典的性能优化机制。
它的发展经历了哪些重要的里程碑或版本迭代?
blk-core.c
的发展史就是Linux块设备层的演进史。
- 早期 (基于
request_fn
):最初的块层非常简单,驱动提供一个request_fn
函数,blk-core
将请求放入队列后,就调用这个函数来处理。 - I/O调度器的引入:这是一个重大变革。“电梯算法”(Elevator Scheduler)被引入,
blk-core.c
演变为一个框架,可以在提交请求给驱动之前,先通过调度器对队列中的请求进行排序和合并。 bio
取代buffer_head
:随着bio
结构成为I/O请求的标准载体,blk-core.c
中的核心函数generic_make_request
成为了接收bio
并将其转换为request
结构(调度器的操作单位)的中心。- 向
blk-mq
的过渡:这是最具颠覆性的变革。随着blk-mq
(多队列块层)的引入,blk-core.c
的角色发生了转变。在blk-mq
出现之前,blk-core.c
是唯一的I/O数据路径。在blk-mq
时代,高性能的I/O数据路径被移到了block/blk-mq.c
中。blk-core.c
的职责则更多地转变为控制平面:它仍然负责request_queue
的分配(通过blk_mq_init_queue
等)、通用配置和生命周期管理,但高速的、per-cpu的提交路径则由blk-mq.c
处理。
目前该技术的社区活跃度和主流应用情况如何?
block/blk-core.c
依然是块设备层不可或缺的核心部分,尽管其在数据路径上的角色已被blk-mq
分担。
- 核心控制平面:任何块设备的初始化都离不开
blk-core.c
提供的队列创建和管理API。 - 社区状态:代码库非常成熟。社区的活跃度主要体现在:
- 为
blk-mq
框架提供支持和配置接口。 - 管理I/O调度器的注册和切换。
- 处理一些与传统设备或遗留接口相关的兼容性逻辑。
- 为
核心原理与设计
它的核心工作原理是什么?
blk-core.c
的核心是管理struct request_queue
,并作为I/O提交的初始分派点。
请求队列的创建与配置:
- 当一个块设备驱动(如
sd_mod
)初始化时,它会调用blk_mq_init_queue()
(现代接口)或blk_init_queue()
(传统接口)。这些函数最终会调用到blk-core.c
中的逻辑。 - 这个过程会分配一个
struct request_queue
对象,并对其进行配置,包括:- 关联驱动的请求处理函数。
- 设置队列的各种限制(如最大I/O大小、最大段数)。
- 初始化并附加一个I/O调度器。
- 当一个块设备驱动(如
I/O提交的入口 (
submit_bio
):- 当文件系统等上层模块调用
submit_bio(bio)
时,请求进入块层。 - 在现代
blk-mq
架构下,submit_bio
会调用blk_mq_submit_bio
,这个函数会快速地将bio
放入当前CPU的软件暂存队列中,这是由blk-mq.c
处理的高速路径。 blk-core.c
在这里的角色是提供了submit_bio
这个统一的、稳定的API入口。
- 当文件系统等上层模块调用
I/O调度器的“宿主”:
blk-core.c
管理着系统中所有已注册的I/O调度器。- 它允许通过sysfs(例如
/sys/block/sda/queue/scheduler
)在运行时为一个设备动态地切换I/O调度器。 - 当I/O请求被分派时,
blk-core.c
(或blk-mq.c
)会调用当前附加在队列上的调度器的函数,来决定下一个要处理的请求是哪一个。
I/O Plug/Unplug机制:
- 当一个任务首次发起I/O时,
blk-core.c
的逻辑会检查当前任务是否有一个“插头”(plug)。如果没有,就创建一个。 - 后续的
bio
会被添加到这个plug的链表上,而不是立即被分派。 - 当任务需要睡眠、退出,或者plug上的请求积累到一定数量时,
blk_flush_plug_list()
会被调用,将暂存的所有bio
一次性提交给块层进行处理。
- 当一个任务首次发起I/O时,
它的主要优势体現在哪些方面?
- 中心化管理:为所有块设备提供了一个统一的请求队列管理中心。
- 抽象和模块化:完美地将I/O调度逻辑与设备驱动分离,使得两者可以独立发展和替换。
- 性能优化:经典的plugging机制对于HDD时代的性能提升起到了至关重要的作用。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 历史包袱:由于其悠久的历史,
blk-core.c
中包含了一些为传统单队列模型设计的逻辑(如plugging),这些逻辑对于现代超高速NVMe设备的重要性已经大大降低。 - 作为瓶颈的历史:其传统的单队列提交路径
generic_make_request
是导致块层在多核环境下扩展性差的主要原因,这个问题已经被blk-mq.c
所解决。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
它是Linux内核中管理块设备请求队列的唯一且标准的解决方案。
- 所有块设备驱动:任何块设备驱动程序都必须使用
blk-core.c
提供的API来创建和初始化它的request_queue
。 - 所有I/O提交者:任何需要向块设备提交I/O的上层代码,最终都必须通过
submit_bio()
这个入口进入块层。
是否有不推荐使用该技术的场景?为什么?
它是一个无法绕过的基础框架。任何与块设备的数据交互都离不开它所管理的request_queue
。不适用它的场景就是那些不与块设备交互的场景(如字符设备、网络设备)。
对比分析
请将其 与 其他相似技术 进行详细对比。
blk-core.c
(队列管理) vs. blk-mq.c
(多队列数据路径)
这是理解现代块层最重要的区别。
特性 | blk-core.c (Queue Management Core) |
blk-mq.c (Multi-Queue Data Path) |
---|---|---|
主要职责 | 控制平面:request_queue 的生命周期管理、I/O调度器的注册与附加、通用配置。 |
数据平面:实现高性能的、per-cpu的、无锁的I/O提交路径,管理软件队列到硬件队列的映射。 |
核心函数 | blk_init_queue , blk_cleanup_queue , elevator_switch |
blk_mq_init_queue , blk_mq_submit_bio , blk_mq_run_hw_queue |
性能角色 | 提供基础框架,但其传统路径是性能瓶颈。 | 现代块层性能的引擎,解决了锁争用问题。 |
抽象层次 | 更高层,定义了“什么是请求队列”。 | 更底层,实现了“如何高效地处理请求队列”。 |
blk-core.c
(请求队列) vs. genhd.c
(磁盘表示)
特性 | blk-core.c (请求队列) |
genhd.c (磁盘表示) |
---|---|---|
核心对象 | struct request_queue |
struct gendisk |
核心作用 | 定义了如何与设备通信的通道和规则。 | 定义了设备本身是什么的身份和属性(大小、分区)。 |
交互关系 | 一个gendisk 必须关联一个request_queue 才能进行I/O。gendisk->queue 指针将两者连接。 |
一个gendisk 代表一个逻辑磁盘实体。 |
好比 | 一条高速公路。 | 高速公路上行驶的汽车的登记信息。 |
blk_dev_init: 初始化内核块设备层
此函数是内核块设备子系统的核心初始化例程。它在内核启动过程中被调用一次, 负责建立整个块设备层正常工作所必需的基础设施, 在任何具体的块设备驱动(例如SD卡, eMMC驱动)被注册之前执行。
其工作原理主要包含以下几个方面:
- 编译时健全性检查: 函数首先使用
BUILD_BUG_ON
宏执行一系列静态断言。这些检查在编译时进行, 而非运行时。它们用于确保块层最核心的数据结构(struct request
和struct bio
)中的位域(bitfield)定义是正确的, 即操作码(op-code)和标志位(flags)的数量没有超出为其分配的存储空间。这是一个至关重要的预防性措施, 旨在捕捉可能导致数据损坏或未定义行为的编码错误, 保证了数据结构定义的正确性。 - 创建核心工作队列(
kblockd
): 它会创建一个名为kblockd
的专用内核工作队列。这个工作队列是块设备层异步处理任务的核心, 主要用于”拔下塞子”(unplugging)操作。当I/O请求被合并和延迟时, 是kblockd
负责在合适的时机将这些请求批量提交给下层的设备驱动。它被赋予了高优先级(WQ_HIGHPRI
), 以确保I/O提交的低延迟。同时,WQ_MEM_RECLAIM
标志允许该工作队列在系统内存极度紧张的情况下依然能够运行, 这对于通过写出脏页来回收内存的场景至关重要。 - 创建SLAB内存缓存: 它为
request_queue
结构体创建一个专用的kmem_cache
。request_queue
是代表一个块设备I/O调度队列的核心数据结构。由于request_queue
的创建和销毁可能相对频繁, 使用专门的SLAB缓存可以极大地提升性能, 它通过预分配和管理一组大小相同的内存对象, 避免了通用内存分配器的开销和内存碎片问题。 - 创建DebugFS目录: 它在
debugfs
虚拟文件系统中创建一个名为block
的根目录。这个目录为内核开发者和系统管理员提供了一个接口, 用于在运行时查看块层内部状态、统计数据以及进行调试。
1 | /* |