引言

Linux 内核的块设备层(Block Layer)是存储子系统的核心枢纽,它位于文件系统/设备驱动之间,负责将上层 I/O 请求高效、有序地提交到存储设备。理解块设备层与 I/O 调度机制,对于存储性能调优、数据库系统优化、以及高并发场景下的 I/O 瓶颈分析至关重要。本文将深入剖析 Linux 块层的完整架构,从 bio 结构到 I/O 调度器,从多队列 blk-mq 到 io_uring 新范式。

1. 块设备层核心数据结构

1.1 bio 结构体

bio(Block I/O)是块层的基本请求单元,代表一次 I/O 操作。它包含了目标设备(bdev)、数据缓冲区链表(bi_io_vec[])、操作类型(读/写/刷盘)、以及完成回调。

bio 支持 段(segment) 链式组织:一个 bio 可以不连续地分布在多个内存页中,通过 bi_next 形成链表。这种设计允许上层(如文件系统)直接提交跨越多个非连续页的 I/O,无需合并。

1.2 request 与 request_queue

request 是块层将多个 bio 合并或排序后提交给驱动的更大请求单元。request_queue 是块设备的请求队列,每个块设备(或 gendisk)拥有一个。核心字段包括:

  • queue_limits:设备硬件限制(最大段数、段大小、DMA 边界等)
  • make_request_fn:I/O 进入队列的入口函数
  • queue_depth:并发出深度的限制

1.3 gendisk 与 hd_struct

gendisk 代表一个完整的块设备(如 /dev/sda),其成员 part_tbl 维护分区信息。hd_struct 描述单个分区的元数据(起始扇区、大小、I/O 统计)。

2. 传统 I/O 调度器演进

2.1 Linus 电梯(Noop)

最简单的调度器,仅做 FIFO 合并(相邻扇区的 bio 直接合并)。适用于有硬件 FTL 的设备(如 SSD、NVMe),因为设备自身已做重排序优化。

2.2 CFQ(Completely Fair Queuing)

CFQ 为每个进程分配独立的时间片来提交 I/O,目标是公平分配 I/O 带宽。其核心机制包括:

  • per-process red-black tree:按扇区位置排序,实现电梯式寻道优化
  • 时间片轮转:每个进程在规定时间内可提交一定数量的请求
  • 同步/异步分离:同步请求(读、 metadata)放入同步队列享受更高优先级,异步(写回)归入异步队列

CFQ 在机械硬盘时代是主流选择,但在高并发 SSD 上因锁争用严重而表现不佳。

2.3 Deadline 调度器

Deadline 通过引入 过期时间 机制,解决了 CFQ 的饥饿问题:

  • 每个 I/O 请求都附带一个过期时间(读 500ms、写 5000ms)
  • 维护四个队列:读排序、写排序、读批量、写批量
  • 优先处理即将过期的请求,防止写操作饿死读操作
  • 排序队列用红黑树按扇区位置组织,批量队列用 FIFO

Deadline 特别适合读多写少的负载,如数据库和 Web 服务器,因为读请求的过期时间更短,会被优先服务。

2.4 BFQ(Budget Fair Queueing)

BFQ 是一种更精确的公平调度器,通过 budget 机制为每个进程分配 I/O 预算:

  • 每个进程根据权重获得 budget(代表可发送的扇区数)
  • budget 用完后切换到下一进程,时间粒度更细
  • 支持低延迟模式:交互式进程(键盘输入等)的请求可获得极低延迟
  • 自动识别空闲进程并给予额外时间片

BFQ 在桌面和实时场景表现优秀,但在高性能 NVMe 上可能开销稍大。

3. 多队列 blk-mq 架构

3.1 传统单队列瓶颈

Linux 早期版使用单一请求队列(single-queue),所有 CPU 通过同一个队列提交请求,存在严重的锁争用问题。在高速 NVMe 设备上,单队列架构容易成为瓶颈。

3.2 blk-mq 双层队列设计

blk-mq(Multi-Queue Block Layer)采用 per-CPU 或 per-NUMA 节点 的硬件提交队列(hw_queue) 可选的软件映射队列(map_queue):

  • 软件分发队列(submission queue):per-CPU 或 per-Node,每个 CPU 有独立队列,消除全局锁
  • 硬件队列(hardware dispatch queue):映射到硬件 MSI-X 中断亲和性,同一队列的中断绑定到对应 CPU
  • tag mapping:软件队列与硬件队列之间通过映射矩阵(map)灵活配置

这种设计让 I/O 提交和完成中断都做到 CPU-local,极大提升了多核 NVMe 的并发吞吐。

3.3 I/O 调度器在 blk-mq 中的位置

blk-mq 推进后,出现了 mq-deadline、bfq-mq、kyber 等一批专为多队列设计的调度器。Kyber 调度器特别值得关注,它采用 Eytzinger 布局的数组堆 管理延迟目标:

  • 设定读/写的延迟目标(如读 2ms、写 10ms)
  • 动态调整 next_request 阈值控制批处理窗口
  • 逻辑简单,算法复杂度低,特别适合低延迟设备

4. io_uring — 异步 I/O 的新范式

4.1 传统 AIO 的局限

Linux 原生 AIO(libaio)仅限 O_DIRECT 文件 I/O,不支持套接字、不支持 buffered I/O,且每次提交/完成需要两次完整系统调用。

4.2 io_uring 环形队列设计

io_uring 通过 共享内存环形队列 实现了用户态与内核零系统调用的 I/O 交互:

  • SQ(Submission Queue):用户态写入 SQE(提交队列条目),写入 SQ tail 指针(当 head ≠ tail 时触发 kernel submit)
  • CQ(Completion Queue):内核写入 CQE(完成队列条目),用户态直接轮询无需系统调用
  • IORING_SETUP_SQPOLL 模式:内核线程主动轮询 SQ,用户态连 io_uring_enter() 都无需调用

4.3 高级特性

  • Fixed buffers:预注册缓冲区,避免每次 I/O 的 get_user_pages 开销
  • Fixed files:预注册文件描述符,避免每请求 fget/fput
  • Linked SQE:请求链式顺序提交;SQE 的 IOSQE_IO_LINK 标志确保前一个成功才执行下一个
  • Kernel 侧 poll(IORING_SETUP_IOPOLL):在块设备上做纯轮询模式,零中断开销

5. 生产环境性能调优实践

5.1 调度器选择

  • NVMe SSD / 高性能存储:none(设备自带 FTL 已做优化)
  • SAS/SATA SSD:mq-deadline(平衡延迟与吞吐)
  • 机械硬盘数据库:mq-deadline(读优先、防饥饿)
  • 桌面交互式场景:bfq(低延迟保障)

5.2 队列参数调优

  • nr_requests:增大队列深度可提升吞吐,但会增加延迟
  • read_ahead_kb:顺序读预读窗口,数据库全表扫描可设大值(如 4096KB)
  • nomerges:设为 2 关闭所有合并,适合低延迟 SSD 场景

5.3 io_uring 工程实践

  • 高并发场景启用 SQPOLL IOPOLL 模式
  • 预分配固定缓冲区(IORING_REGISTER_BUFFERS)减少 pin/unpin 开销
  • 批量提交 SQEs 后单次 kick,减少 ring 竞争
  • 监控 CQ overflow(IORING_SQ_CQOverflow),overflow 时需及时收割完成事件

6. I/O 路径全流程总结

一个块设备 I/O 请求的典型路径:

  1. 用户态调用 write()/read/writev() → VFS 层
  2. VFS → 文件系统层(ext4/xfs),通过 address_space_operations 提交
  3. 文件系统将 I/O 封装成 bio,调用 submit_bio() 进入块层
  4. I/O 调度器对 bio 合并/排序/插入队列
  5. blk-mq 硬件队列分发到驱动(nvme_queue_rq)
  6. 驱动将请求转换为 PRP/SG 列表,写入 SQ 尾铃寄存器(Doorbell)
  7. 设备 DMA 完成后写 CQ 中断,软中断驱动 IRQ 处理
  8. 块层回调 bio 的 bi_end_io 通知文件系统
  9. 7. 未来展望

    • io_uring 持续演进:network zero-copy、ring fd 注册扩展、provider-based buffers
    • ZNS(Zoned Namespace)支持:面向新型分区存储设备,通过 blk-mq 的 zone awareness 直接管理写约束
    • 存储 IO 融合:CXL 内存池化将模糊块与字符设备的界限
    • eBPF 在块层的应用:blk_iolatency BPF 程序实现单个设备的延迟告警和节流

    总结

    Linux 块设备层是一个高度工程化的子系统,从 bio 段管理到多队列调度,每一层都针对特定场景做了精细优化。对于存储工程师而言,理解 blk-mq 的映射关系和 io_uring 的环形队列机制,是解锁现代 NVMe 极致性能的关键。选择合适的 I/O 调度器、调整队列深度、以及考虑 io_uring 替代方案,都是生产环境调优的常用手段。随着存储介质持续演进,Linux 块层也在持续迭代,未来 CXL 与 io_uring 的深度融合值得期待。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部