[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 | /* |








