引言
块设备是 Linux 内核存储子系统的核心抽象,负责在文件系统与物理磁盘之间提供统一的数据传输接口。无论是传统的机械硬盘(HDD)、固态硬盘(SSD)还是 NVMe 设备,都通过块设备层进行管理与调度。理解块设备层的架构和 I/O 调度器的工作原理,是优化存储性能、构建高性能 I/O 密集型应用的关键。本文将从块设备核心数据结构出发,深入剖析 multi-queue(blk-mq)架构、I/O 调度算法演进,并通过实战案例展示如何进行 I/O 性能调优。
一、块设备核心架构概览
1.1 核心数据结构
块设备层中最关键的数据结构包括:
- struct block_device:表示一个块设备或分区,包含设备号(bd_dev)、大小(bd_size)、操作表(bd_disk->fops)等字段。
- struct gendisk:通用磁盘描述符,承载请求队列(request_queue)、设备号、操作函数集等信息,每个物理磁盘对应一个 gendisk 实例。
- struct request_queue:I/O 请求队列,是块设备层的核心枢纽。所有块 I/O 请求(bio)在此排队、合并、排序,最终由驱动层处理。
- struct request:表示一个块 I/O 请求的封装,包含目标扇区、方向(读/写)、关联的 bio 链表以及超时计时器。
- struct bio:块 I/O 的基本传输单元,通过 bio_vec 指向物理内存页中的 scatter-gather 缓冲区,支持非连续内存的批量传输。
这些结构之间的关系可以通过下图理解:
用户空间 write()/read()
↓ (系统调用)
VFS 层 (file_operations)
↓ (提交 bio)
文件系统层 (ext4/xfs/...)
↓ (bio 合并/请求生成)
块设备层 (blk-mq / request_queue)
↓ (调度排序)
I/O 调度器 (mq-deadline/bfq/none)
↓ (派发 request)
设备驱动层 (nvme driver / scsi driver / ...)
↓ (DMA 传输)
物理存储设备
1.2 设备注册流程
一个块设备在内核中的注册流程大致如下:
1. alloc_disk() — 分配 gendisk 结构
2. blk_mq_init_queue() — 初始化 multi-queue 请求队列
3. blk_queue_logical_block_size() — 设置逻辑块大小
4. set_capacity() — 设置设备容量
5. add_disk() — 将设备注册到内核,触发 uevent 通知用户空间
alloc_disk() 返回一个部分初始化的 gendisk 实例,驱动需要填充其 major/minor 号、操作表(fops)、请求队列(queue)等字段。blk_mq_init_queue() 会根据硬件队列数量(硬件通常支持多队列以利用多核并发)创建 software staging queue 和 hardware dispatch queue 的映射。
二、blk-mq 多队列架构深度剖析
2.1 单队列瓶颈
在 Linux 4.0 之前,块设备层使用单队列(single-queue)架构。所有 CPU 在提交 I/O 请求时竞争同一个全局锁(request_queue->queue_lock),导致严重的扩展性问题。单队列架构在高并发 NVMe SSD 场景下,I/O 吞吐量在 CPU 数增加时反而下降,形成"锁规模反转"现象。
2.2 blk-mq 架构设计
blk-mq(Multi-Queue Block I/O Queueing Mechanism)引入了三级队列结构,彻底解决了单队列的扩展性问题:
- Software Staging Queue (hctx):每个 CPU 核心或 NUMA 节点拥有独立的软件队列,用于缓存来自文件系统层的 I/O 请求。由于 CPU 本地访问,无需全局锁即可实现高并发提交。
- Hardware Dispatch Queue (hctx->dispatch):硬件上下文对应的派发队列,当设备硬件队列空闲时,调度器将软件队列中的请求移至派发队列。
- Hardware Queue (nvmeq / hw_ctx):硬件端的实际队列,由 NVMe/SCSI 等控制器提供。现代 NVMe SSD 通常支持 64 个或更多的硬件队列,每个队列深度可达 1024。
blk-mq 还引入了 blk_mq_tag_set 机制管理硬件队列标签,通过 tag 编号唯一标识每个正在处理的请求,支持中断驱动的高效完成通知(Completion Queue IRQ → 根据 tag 索引找到 request → 回调 end_io)。
2.3 硬件队列映射策略
blk-mq 支持两种主要的硬件队列映射模式:
- blk_mq_map_queue_type BLK_MQ_MAP_QUEUE:默认模式,将 CPU 按 NUMA 本地性映射到最近的硬件队列,优化本地内存访问延迟。
- blk_mq_map_queue_type BLK_MQ_MAP_HCTX:自定义映射框架,驱动可以通过 blk_mq_ops->map_queue() 实现特定映射策略。
NVMe 驱动中(drivers/nvme/host/pci.c),当一个设备拥有 N 个硬件队列时,blk_mq 会根据 CPU 拓扑结构,将 CPU 0..k-1 映射到 hardware queue 0,CPU k..2k-1 映射到 queue 1,以此类推。这种策略确保了中断亲和性(IRQ affinity),减少跨 NUMA 节点访问。
三、I/O 调度器原理与演进
3.1 传统调度器演进
Linux 内核历史上经历了三代 I/O 调度器:
- CFQ (Completely Fair Queuing, 2.6.18~5.0):为每个进程维护独立的同步 I/O 队列,按时间片轮转分配磁盘带宽。CFQ 在交互式桌面系统上表现良好,但在高吞吐服务器场景下,复杂的排序逻辑成为 CPU 瓶颈。
- Deadline (2.6.10~至今):为每个请求设置过期时间(读 500ms,写 5s),按扇区排序提交请求,避免饥饿问题。Deadline 算法简单高效,适合多线程混合负载场景。
- Noop (2.6.18~至今):最简单的 FIFO 调度器,仅做基本相邻扇区合并,不做排序。适用于 SSD/NVMe 本身具备硬件调度的设备,或上层应用已经良好排序的场景。
3.2 blk-mq 时代的调度器
blk-mq 架构引入了专为多队列优化的调度器实现:
- mq-deadline:Deadline 算法在多队列上的移植版。每个硬件队列维护排序队列(按扇区)和过期队列(按时间),支持批处理派发以降低锁开销。mq-deadline 是许多企业发行版(Ubuntu、RHEL)在 SSD 上的默认调度器。
- BFQ (Budget Fair Queueing):基于 CFQ 理念的改进版,引入"预算"机制控制每个进程的 I/O 配额。BFQ 在混合读写负载下能保证低延迟,尤其适合桌面和交互式应用。BFQ 对多队列支持良好,是较新的 Linux 版本(≥5.0)在慢速存储设备上的默认选择。
- Kyber:由 Facebook 开发的轻量级调度器,为读/写分别设置目标延迟阈值(read_latency_target=2ms, write_latency_target=10ms),动态调整发出请求的数量以逼近目标延迟。Kyber 算法简洁,对延迟敏感型应用(数据库)有良好表现。
- none:blk-mq 模式下的 NOOP,仅做请求合并和硬件队列分发。适用于 NVMe SSD 设备,因为 NVMe 控制器内部已经进行了有效的请求调度。
3.3 调度器参数调优
通过 /sys/block/

发表评论 取消回复