[TOC]

Linux 源码中的 block 目录:块设备 I/O 的核心中枢

block 目录是 Linux 内核 I/O 栈的心脏。它负责管理所有块设备(如硬盘驱动器 HDD、固态硬盘 SSD、U 盘等)的数据交换。这一层的主要目标是提供一个通用的、高效的框架,将上层文件系统和应用程序的 I/O 请求,转化为底层具体硬件驱动程序可以执行的操作。

一、 历史与背景

这项技术是为了解决什么特定问题而诞生的?

block 层的诞生是为了解决两个核心问题:

  1. 抽象与解耦:如果没有一个通用的块设备层,每个文件系统(如 ext4, XFS)都需要自己编写直接与各种硬盘驱动(SATA, SCSI, NVMe)对话的代码。这会导致代码冗余、开发复杂且难以维护。block 层提供了一个标准的接口,使得文件系统无需关心底层硬件的具体细节,反之亦然。
  2. 性能优化:对存储设备的随机访问通常效率很低,尤其是在机械硬盘上,会导致磁头频繁寻道,浪费大量时间。block 层通过引入 I/O 调度器 (I/O Scheduler),可以对传入的 I/O 请求进行合并、排序和延迟处理,将随机 I/O 尽可能地转化为顺序 I/O,从而大幅提升吞吐量和系统响应速度。

它的发展经历了哪些重要的里程碑或版本迭代?

  1. 早期设计 (Single-Queue Layer):在很长一段时间里,Linux 的块设备层是单队列模型。每个块设备只有一个请求队列 (request_queue),所有 CPU 核心的 I/O 请求都必须通过一个全局锁来访问这个队列。这个设计在单核、慢速硬盘时代工作得很好。
  2. 多队列块层 (Multi-Queue Block Layer, blk-mq):随着多核 CPU 和高速 SSD(尤其是 NVMe 设备)的普及,单队列模型的锁竞争问题成为了巨大的性能瓶颈。从 3.13 内核版本开始,Jens Axboe 主导开发了 blk-mq。它为每个 CPU 核心分配了独立的提交队列(Software Queue),大大减少了锁竞争,使得 I/O 性能可以在多核系统上有效扩展,充分释放了现代硬件的潜力。blk-mq 已成为当前 Linux 内核的默认块层框架。
  3. 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 框架下,这个结构变得更加复杂,管理着多个硬件和软件队列。

核心工作原理

  1. 请求生成:文件系统(例如,当你想写入一个文件时)创建一个或多个 bio 结构体来描述这个操作。
  2. 请求提交bio 被提交到 block 层。
  3. 请求排队与调度block 层(特别是 blk-mq)将 bio 放入对应 CPU 的软件队列中。I/O 调度器(如 BFQ, Kyber, mq-deadline)根据其策略对队列中的 request 进行排序和调度,以优化性能(例如,将多个小的写操作合并成一个大的写操作,或者优先处理读请求以降低延迟)。
  4. 分发给驱动:调度器选出一个 request 后,block 层会将其分发给对应的底层设备驱动程序(例如 drivers/block/nvme.c)。
  5. 硬件操作:设备驱动程序解析 request,生成具体的硬件命令(如 NVMe 命令),并通过 DMA 将数据在内存和设备之间传输。
  6. 完成通知:当硬件完成操作后,会产生一个中断。驱动程序的中断处理函数会被调用,它会通知 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
2
3
4
5
# 列出所有块设备
lsblk

# 查看内核识别的分区信息
cat /proc/partitions

与 I/O 调度器交互

块设备层的许多参数都通过 sysfs 文件系统暴露在 /sys/block/<device>/queue/ 目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看 sda 设备当前可用的 I/O 调度器和正在使用的调度器
cat /sys/block/sda/queue/scheduler
# 输出示例: [mq-deadline] kyber bfq none

# 动态切换 I/O 调度器为 bfq
# 注意:这需要 root 权限
echo bfq | sudo tee /sys/block/sda/queue/scheduler

# 查看和调整队列深度(可排队的请求数)
cat /sys/block/sda/queue/nr_requests

# 调整预读大小(单位 KB)
cat /sys/block/sda/queue/read_ahead_kb

实时监控 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 层可能需要演进出能够卸载特定操作(如压缩、过滤)的接口。
  • 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关联一个特殊的内存inodebd_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/sdXddfdisk操作,其内核路径都会经过这个文件。
  • 社区状态:代码库非常稳定。社区的活跃度主要体现在修复一些边界条件下的bug,以及在VFS或块层进行其他重构时,同步更新bdev.c中的相关代码以保持兼容性。

核心原理与设计

它的核心工作原理是什么?

bdev.c的核心是管理struct block_device的生命周期,并将其与VFS的inode模型进行绑定

  1. 查找与实例化 (bdev_lookup, bdget):当内核需要一个block_device实例时(例如,在mountopen一个设备文件时),它会提供一个设备号dev_tbdev.c会首先在一个全局的哈希表(bdev_hashtable)中查找是否已存在对应的block_device实例。

    • 如果存在,就增加其引用计数并返回。
    • 如果不存在,它就会分配一个新的struct block_device,并将其与由genhd.c注册的gendisk关联起来,然后将其加入哈希表。
  2. 特殊的bd_inode:每个struct block_device都有一个关联的inode,即bdev->bd_inode。这个inode非常特殊:

    • 不存储在任何物理磁盘上,完全存在于内存中。
    • 它的作用是作为VFS和块设备之间的“适配器”。
    • 它的inode->i_mapping(地址空间)被用来缓存对裸设备的读写。当你dd一个设备时,数据块实际上被读入了这个bd_inode的页面缓存中。
    • 它的inode->i_bdev指针会指回它所属的struct block_device
  3. 打开操作 (bdev_open):当一个设备文件被打开时,VFS会调用bdev.c中注册的bdev_open函数。这个函数会:

    • 获取对应的block_device实例。
    • 检查并申请访问模式。如果是独占打开(O_EXCL),它会调用bdev_get_exclusive()来“锁定”设备,防止其他独占用户。它还会检查设备是否已被挂载等,以防止冲突。
  4. 关闭操作 (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/sda1bdev.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
2
3
4
5
6
7
8
9
struct bdev_inode {
struct block_device bdev;
struct inode vfs_inode;
};

static inline struct bdev_inode *BDEV_I(struct inode *inode)
{
return container_of(inode, struct bdev_inode, vfs_inode);
}

I_BDEV 于从 inode 中提取与块设备(block_device)相关的信息

1
2
3
4
5
struct block_device *I_BDEV(struct inode *inode)
{
return &BDEV_I(inode)->bdev;
}
EXPORT_SYMBOL(I_BDEV);

bdev_cache_init 用于初始化块设备的缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static struct file_system_type bd_type = {
.name = "bdev",
.init_fs_context = bd_init_fs_context,
.kill_sb = kill_anon_super,
};

void __init bdev_cache_init(void)
{
int err;
/* SLAB_HWCACHE_ALIGN:确保缓存对象与硬件缓存对齐,提高访问效率。
SLAB_RECLAIM_ACCOUNT 和 SLAB_ACCOUNT:用于内存回收和内存使用统计。
SLAB_PANIC:如果缓存创建失败,触发内核 panic */
bdev_cachep = kmem_cache_create("bdev_cache", sizeof(struct bdev_inode),
0, (SLAB_HWCACHE_ALIGN|SLAB_RECLAIM_ACCOUNT|
SLAB_ACCOUNT|SLAB_PANIC),
init_once);
err = register_filesystem(&bd_type);
if (err)
panic("Cannot register bdev pseudo-fs");
blockdev_mnt = kern_mount(&bd_type);
if (IS_ERR(blockdev_mnt))
panic("Cannot create bdev pseudo-fs");
blockdev_superblock = blockdev_mnt->mnt_sb; /* For writeback */
}

block/blk-core.c 请求队列管理核心(Request Queue Management Core) I/O提交的中央枢纽

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术是Linux块设备层的心脏,它为了解决如何接收、管理和分派所有I/O请求这一核心问题而诞生。在内核中,文件系统等上层模块只知道要读写文件的某个部分,而底层设备驱动只知道如何操作硬件。block/blk-core.c就是连接这两者的中央枢纽和翻译官

它具体解决了以下关键问题:

  • 抽象I/O请求队列:需要一个通用的数据结构来代表与一个块设备通信的“通道”,这个结构就是struct request_queueblk-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块设备层的演进史。

  1. 早期 (基于request_fn):最初的块层非常简单,驱动提供一个request_fn函数,blk-core将请求放入队列后,就调用这个函数来处理。
  2. I/O调度器的引入:这是一个重大变革。“电梯算法”(Elevator Scheduler)被引入,blk-core.c演变为一个框架,可以在提交请求给驱动之前,先通过调度器对队列中的请求进行排序和合并。
  3. bio取代buffer_head:随着bio结构成为I/O请求的标准载体,blk-core.c中的核心函数generic_make_request成为了接收bio并将其转换为request结构(调度器的操作单位)的中心。
  4. 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提交的初始分派点

  1. 请求队列的创建与配置

    • 当一个块设备驱动(如sd_mod)初始化时,它会调用blk_mq_init_queue()(现代接口)或blk_init_queue()(传统接口)。这些函数最终会调用到blk-core.c中的逻辑。
    • 这个过程会分配一个struct request_queue对象,并对其进行配置,包括:
      • 关联驱动的请求处理函数。
      • 设置队列的各种限制(如最大I/O大小、最大段数)。
      • 初始化并附加一个I/O调度器。
  2. 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入口。
  3. I/O调度器的“宿主”

    • blk-core.c管理着系统中所有已注册的I/O调度器。
    • 它允许通过sysfs(例如/sys/block/sda/queue/scheduler)在运行时为一个设备动态地切换I/O调度器。
    • 当I/O请求被分派时,blk-core.c(或blk-mq.c)会调用当前附加在队列上的调度器的函数,来决定下一个要处理的请求是哪一个。
  4. I/O Plug/Unplug机制

    • 当一个任务首次发起I/O时,blk-core.c的逻辑会检查当前任务是否有一个“插头”(plug)。如果没有,就创建一个。
    • 后续的bio会被添加到这个plug的链表上,而不是立即被分派。
    • 当任务需要睡眠、退出,或者plug上的请求积累到一定数量时,blk_flush_plug_list()会被调用,将暂存的所有bio一次性提交给块层进行处理。

它的主要优势体現在哪些方面?

  • 中心化管理:为所有块设备提供了一个统一的请求队列管理中心。
  • 抽象和模块化:完美地将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驱动)被注册之前执行。

其工作原理主要包含以下几个方面:

  1. 编译时健全性检查: 函数首先使用BUILD_BUG_ON宏执行一系列静态断言。这些检查在编译时进行, 而非运行时。它们用于确保块层最核心的数据结构(struct requeststruct bio)中的位域(bitfield)定义是正确的, 即操作码(op-code)和标志位(flags)的数量没有超出为其分配的存储空间。这是一个至关重要的预防性措施, 旨在捕捉可能导致数据损坏或未定义行为的编码错误, 保证了数据结构定义的正确性。
  2. 创建核心工作队列(kblockd): 它会创建一个名为kblockd的专用内核工作队列。这个工作队列是块设备层异步处理任务的核心, 主要用于”拔下塞子”(unplugging)操作。当I/O请求被合并和延迟时, 是kblockd负责在合适的时机将这些请求批量提交给下层的设备驱动。它被赋予了高优先级(WQ_HIGHPRI), 以确保I/O提交的低延迟。同时, WQ_MEM_RECLAIM标志允许该工作队列在系统内存极度紧张的情况下依然能够运行, 这对于通过写出脏页来回收内存的场景至关重要。
  3. 创建SLAB内存缓存: 它为request_queue结构体创建一个专用的kmem_cacherequest_queue是代表一个块设备I/O调度队列的核心数据结构。由于request_queue的创建和销毁可能相对频繁, 使用专门的SLAB缓存可以极大地提升性能, 它通过预分配和管理一组大小相同的内存对象, 避免了通用内存分配器的开销和内存碎片问题。
  4. 创建DebugFS目录: 它在debugfs虚拟文件系统中创建一个名为block的根目录。这个目录为内核开发者和系统管理员提供了一个接口, 用于在运行时查看块层内部状态、统计数据以及进行调试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/*
* blk_dev_init: 块设备初始化函数.
* __init 宏指示编译器将此函数放入特殊的 ".init.text" 段.
* 在内核启动过程结束后, 这个段所占用的内存会被释放.
* @return: 成功时返回 0.
*/
int __init blk_dev_init(void)
{
/*
* BUILD_BUG_ON 是一个静态断言, 如果其条件在编译时为真, 编译将失败.
* 这条断言检查请求操作码的最后一个枚举值, 是否超出了在位域中为操作码分配的位数.
* 这确保了所有的请求操作码都可以被正确地存储.
*/
BUILD_BUG_ON((__force u32)REQ_OP_LAST >= (1 << REQ_OP_BITS));
/*
* 这条断言检查操作码位数和标志位数之和, 是否超出了 struct request 结构体中 cmd_flags 成员的总位数.
* sizeof_field(struct request, cmd_flags) 获取 cmd_flags 字段的大小(字节), 乘以8得到位数.
* 这确保了操作码和标志位能够完全放入 cmd_flags 字段中.
*/
BUILD_BUG_ON(REQ_OP_BITS + REQ_FLAG_BITS > 8 *
sizeof_field(struct request, cmd_flags));
/*
* 这条断言与上一条类似, 但检查的是 struct bio 结构体中的 bi_opf 字段.
* 这确保了操作码和标志位也能完全放入 bio 的 bi_opf 字段中. bio 是块I/O的基本单元.
*/
BUILD_BUG_ON(REQ_OP_BITS + REQ_FLAG_BITS > 8 *
sizeof_field(struct bio, bi_opf));

/*
* 创建一个名为 "kblockd" 的工作队列.
* WQ_MEM_RECLAIM: 允许此工作队列在内存回收期间运行, 对I/O至关重要.
* WQ_HIGHPRI: 赋予此工作队列高优先级, 因为它用于unplugging操作, 直接影响I/O延迟和吞吐量.
* 0: 表示并发级别, 0意味着内核会根据CPU核心数自动设置. 在单核系统上, 这意味着一次只有一个work item运行.
*/
kblockd_workqueue = alloc_workqueue("kblockd",
WQ_MEM_RECLAIM | WQ_HIGHPRI, 0);
/*
* 检查工作队列是否创建成功.
*/
if (!kblockd_workqueue)
/*
* 如果失败, 调用 panic(). 这是一个不可恢复的致命错误,
* 因为没有 kblockd, 整个块设备层将无法正常工作.
*/
panic("Failed to create kblockd\n");

/*
* 创建一个 SLAB 缓存, 用于快速分配和释放 request_queue 结构体.
* KMEM_CACHE 是一个宏, 用于更方便地调用 kmem_cache_create.
* SLAB_PANIC: 标志位, 如果这个缓存的创建失败, 内核将 panic.
* 这同样表明了 request_queue 对块层的核心重要性.
*/
blk_requestq_cachep = KMEM_CACHE(request_queue, SLAB_PANIC);

/*
* 在 debugfs 虚拟文件系统的根目录下创建一个名为 "block" 的目录.
* 如果 debugfs 没有被挂载或内核不支持, 此函数会返回 NULL, 但不会导致错误.
* 这个目录将作为所有块设备相关调试信息的根节点.
*/
blk_debugfs_root = debugfs_create_dir("block", NULL);

/*
* 初始化成功, 返回 0.
*/
return 0;
}