块设备层

本页目录

一次应用写入可能拆成多个设备命令,也可能先停留在缓存中。沿块层要分别追踪数据范围、排队位置和完成承诺⁠,不能把应用 IOPS 与设备命令数直接等同。

查阅目标阅读位置
bio、单位与拆分合并第 1~2 节
多队列、组批与调度第 3~5 节
队列深度、持久化与观测第 6~9 节

1. 一次 write 不等于一次设备命令

缓冲写通常先修改页缓存;后续回写才产生块 I/O。缓存命中的读取可能根本不访问设备。直接 I/O 绕过部分缓存路径,但对齐、文件系统和设备限制仍可能使一次调用分解成多个请求。

文件操作、bio、request 与硬件命令不一一对应 上层数据范围 文件系统 / 直接块访问 bio 描述 设备区间 + 内存片段 request 按队列与设备约束组织 完成返回上层 仍需区分完成与持久化 blk-mq 调度 / 派发 合并、排队、tag 与背压 驱动与控制器 DMA、命令和完成状态 一个 bio 可拆分,多个 bio 可合并;完成顺序也不必等于提交顺序。

bio 描述一次块数据操作及其内存范围⁠,不是磁盘事务的原子单元。它可被克隆、拆分、重新映射;多个兼容 bio 又可合入同一 request。device-mapper、RAID 等堆叠层还可能产生面向不同下层设备的 I/O。

现代内核中,当前位置和剩余长度主要由 bio.bi_iter 的 bi_sector、bi_size 等描述,bi_status 是 blk_status_t。不能把旧版本平铺成员的伪结构当作当前可编译 API。

三种“段”

  • bio_vec 描述内存范围,包含起始 page、偏移和长度;现代 multipage bvec 可以跨页,不一定一条恰好对应一个 page。
  • bio iterator 描述当前正在处理的部分,不能只根据 bi_vcnt 推导剩余 I/O。
  • 驱动最终使用的 DMA scatter-gather 段又经过映射和合并,不必与原 bvec 数量相同。

映射规则参阅 IOMMU 与 DMA API。

2. 扇区单位与拆分合并

同一个设备字节位置,放到不同层使用不同单位:

表达层次1 MiB 起点的表示
字节偏移1048576
Linux 块层 512 字节 sector2048
4096 字节逻辑块的 NVMe LBA256

块层 sector 固定按 512 字节计数,不代表设备逻辑块也必须是 512 字节。

两个连续 4 KiB bio,设备区间分别为 [2048,2056) 与 [2056,2064) 个 sector;若操作、标志、队列限制等兼容,可能合成一个 8 KiB request。内存物理地址是否相邻不是唯一条件,因为 request 可以用 SG 表达离散内存。

反过来,一个 256 KiB bio 在只允许 128 KiB 传输、或段数/边界更严格的路径中可能拆成多个请求。限制不仅是总字节数,还包括最大段数、单段长度、边界、对齐和不同操作类型的特殊约束。

因此,应用 IOPS、bio 数量、request 数量和设备命令数量不是同一个指标。合并率提高可能减少命令开销,但不能据此认为业务请求变少。

3. blk-mq:多入口并行,并非完全无锁

blk_mq_ctx 提供面向 CPU 的软件提交上下文;blk_mq_hw_ctx 管理面向驱动派发队列的状态。CPU 按队列映射选择 hctx,读、写、轮询等队列类型也可拥有不同映射。

hctx 是软件结构,不是控制器直接读取的那块 SQ 内存。驱动把 request 转换成设备命令并放入自己的队列。软件队列、硬件队列、CPU 数和中断向量数都不保证相等,也不能把 ctx 说成通用的 per-cgroup 队列。

多队列分散共享热点,但队列列表、调度器、tag 分配和驱动仍可需要锁或原子操作。一个调度器还可能维护 request_queue 范围的共享状态,例如 mq-deadline 的调度状态;不能统一写成“所有调度器彼此独立地运行在每个 hctx 上”。见 blk-mq 文档。

Tag 与背压

Tag 用于标识和管理在途 request,使完成路径能定位请求。存在 scheduler tag、driver tag 及共享 tag 集等不同配置;不能认为每个 request 从创建到完成都只占同一个固定硬件 tag。

资源不足时,请求可能等待、延后派发、重排队,或按 NOWAIT 等约定返回不能立即执行;这与设备已接收命令后返回介质错误不同。驱动接受请求后,还必须按协议最终完成它,不能把“暂时忙”与“已拥有请求但稍后完成”混用。

4. Plug:短时间组批,不是事务

plug 关联当前任务,暂存适合组批的 request,增加合并机会并减少逐个派发开销。基本调用形式如下;这是已有内核提交路径中的局部结构,不是用户态程序:

struct blk_plug plug;
blk_start_plug(&plug);
/* 在这里提交已经正确建立、寿命受管理的 bio。 */
blk_finish_plug(&plug);

参数是 struct blk_plug *,不是 current。plug 也不保证所有 I/O 一直扣留到 finish:阻塞调度、容量等条件可以促使提前下发。它既不保证整批原子完成,也不保证整批按提交顺序完成。

组批增加吞吐的同时可能增加等待,因此不能无限扩大批次。读写依赖、flush 和文件系统事务顺序仍由对应机制保证。

5. 调度器解决的是策略取舍

调度器核心思路需要核实的代价
none不额外引入 elevator 排序策略仍有块层/驱动排队,不代表无任何合并或等待
mq-deadline地址排序与到期队列配合,平衡读写等待expire 是调度目标,不是硬件完成的硬实时上限
kyber根据延迟反馈限制不同类别在途请求token 控制并发,不是简单固定速率令牌桶
BFQ按实体预算与权重提供带宽、公平性及响应性公平/交互优化、队列等待与吞吐存在取舍

不要按“NVMe 必须 none,桌面必须 BFQ”替代测量。存储介质、设备内部并发、混合读写、cgroup、后台回写和 CPU 成本会改变结论。

read_expire / write_expire 不保证请求从应用到完成的最大时延;writes_starved 控制读写选择策略,也不是给每个业务设置 SLA。调度器参数单位和可用项应读取实际 sysfs。参阅 mq-deadline、Kyber、BFQ。

6. 队列深度、吞吐与延迟的定量关系

在稳定系统、同一测量边界下,用 Little 定律 L = λ × W:

输入数值
完成率 λ100000 IOPS
平均响应时间 W200 μs = 0.0002 s
平均在途数 L100000 × 0.0002 = 20

先对齐统计边界,再解释这个均值。

这个 20 是观测均值,不是必需把某个 sysfs 参数设为 20。队列有波动,软件和硬件还各有额度。增加并发可能在设备未饱和时提高吞吐;饱和后再增加深度,往往主要增加排队和尾延迟。

同样,100000 次每次 4096 字节的数据操作约为 409.6 MB/s,即 390.625 MiB/s。IOPS 必须结合 I/O 大小、读写比例、同步语义和延迟一起解释。

下面用一个固定服务能力的闭环队列把公式画出来。先增加并发资源,再增加请求深度,比较吞吐平台出现后,多出来的请求究竟停在哪里。

这幅图采用理想闭环模型:Q 个请求持续在途,K 个并行服务位,每项服务时间 d;λ 是每秒完成数,W 是同一边界内的平均时间。

正在呈现知识画面
吞吐进入平台后,多出的深度去了哪里?

未饱和时增加并发可提高完成率;服务槽满后,多出的工作主要增加排队。Little 定律必须在同一统计边界内使用平均量。

7. 完成、顺序与持久化

块层完成通知说明该操作在当前设备栈契约下结束,不能单凭普通 write completion 断言数据已进入断电后仍保留的介质。易失写缓存会改变这一边界。

标志持久化约束
REQ_PREFLUSH在后续数据阶段前处理此前缓存写入的持久化
REQ_FUA本次写入达到相应持久化条件后才报告完成

设备原生支持与块层模拟会产生不同命令序列。两者都不直接保证任意范围写入原子性。见写缓存控制。

应用的 fsync() 由文件系统协调数据和必要元数据,再下推合适的块层操作。文件创建/改名涉及的目录持久化还要考虑目录同步,不能只根据一个 bio 标志解释整个文件事务。

8. 只读查看真实队列参数

from pathlib import Path

fields = ('logical_block_size', 'physical_block_size', 'max_sectors_kb',
          'max_hw_sectors_kb', 'nr_requests', 'scheduler', 'write_cache')
for dev in sorted(Path('/sys/block').glob('*')):
    if dev.name.startswith(('loop', 'ram')):
        continue
    print(dev.name)
    for field in fields:
        try:
            print(' ', field, (dev / 'queue' / field).read_text().strip())
        except OSError:
            pass
    print(' ', 'visible hctx directories:', len(list((dev / 'mq').glob('[0-9]*'))))

scheduler 中方括号表示当前选中项;缺少 mq 目录可能来自堆叠设备等情况。nr_requests 不能通用解释为物理设备每队列最大命令数,max_sectors_kb 也没有适用于所有设备的固定默认值。

9. 从统计回到证据

iostat -x 的 await 包含相应设备统计边界内的等待和服务,不是应用请求端到端时延。%util 接近 100% 对并行设备不等于“吞吐已经达到绝对上限”,更不能当成 CPU 利用率。

blktrace/块层 tracepoint 可以观察排队、派发和完成,但拆分、合并、重试、部分完成和事件丢失会使简单按地址配对失效。先确定身份与层次,再计算时间差。统计字段见块设备统计,追踪方法见追踪与调试。