一次应用写入可能拆成多个设备命令,也可能先停留在缓存中。沿块层要分别追踪数据范围、排队位置和完成承诺,不能把应用 IOPS 与设备命令数直接等同。
| 查阅目标 | 阅读位置 |
| bio、单位与拆分合并 | 第 1~2 节 |
| 多队列、组批与调度 | 第 3~5 节 |
| 队列深度、持久化与观测 | 第 6~9 节 |
1. 一次 write 不等于一次设备命令
缓冲写通常先修改页缓存;后续回写才产生块 I/O。缓存命中的读取可能根本不访问设备。直接 I/O 绕过部分缓存路径,但对齐、文件系统和设备限制仍可能使一次调用分解成多个请求。
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 字节 sector | 2048 |
| 4096 字节逻辑块的 NVMe LBA | 256 |
块层 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);
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 |
| 平均响应时间 W | 200 μs = 0.0002 s |
| 平均在途数 L | 100000 × 0.0002 = 20 |
先对齐统计边界,再解释这个均值。
这个 20 是观测均值,不是必需把某个 sysfs 参数设为 20。队列有波动,软件和硬件还各有额度。增加并发可能在设备未饱和时提高吞吐;饱和后再增加深度,往往主要增加排队和尾延迟。
同样,100000 次每次 4096 字节的数据操作约为 409.6 MB/s,即 390.625 MiB/s。IOPS 必须结合 I/O 大小、读写比例、同步语义和延迟一起解释。
下面用一个固定服务能力的闭环队列把公式画出来。先增加并发资源,再增加请求深度,比较吞吐平台出现后,多出来的请求究竟停在哪里。
λ=dmin(Q,K),W=λQ
这幅图采用理想闭环模型:Q 个请求持续在途,K 个并行服务位,每项服务时间 d;λ 是每秒完成数,W 是同一边界内的平均时间。
正在呈现知识画面
吞吐进入平台后,多出的深度去了哪里?未饱和时增加并发可提高完成率;服务槽满后,多出的工作主要增加排队。Little 定律必须在同一统计边界内使用平均量。
{"id":"io-depth-throughput-latency","title":"吞吐进入平台后,多出的深度去了哪里?","summary":"未饱和时增加并发可提高完成率;服务槽满后,多出的工作主要增加排队。Little 定律必须在同一统计边界内使用平均量。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"q\" data-i18n=\"q\"></label><output for=\"q\" id=\"q-value\"></output><input id=\"q\" type=\"range\" min=\"1\" max=\"16\" value=\"4\" step=\"1\"></div><div class=\"control\"><label for=\"k\" data-i18n=\"k\"></label><output for=\"k\" id=\"k-value\"></output><input id=\"k\" type=\"range\" min=\"1\" max=\"8\" value=\"4\" step=\"1\"></div><div class=\"control\"><label for=\"d\" data-i18n=\"d\"></label><output for=\"d\" id=\"d-value\"></output><input id=\"d\" type=\"range\" min=\"20\" max=\"1000\" value=\"100\" step=\"20\"></div><div class=\"track-row\"><span data-i18n=\"service\"></span><div id=\"service\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"queued\"></span><div id=\"queued\" class=\"track\"></div></div><p class=\"caption\" data-i18n=\"rate\"></p><div class=\"curve\"><svg viewBox=\"0 0 320 150\" aria-hidden=\"true\"><path class=\"axes\" d=\"M10 5V135H310\"/><polyline id=\"curve-rate\"/><line id=\"mark-rate\" y1=\"5\" y2=\"135\"/><circle id=\"dot-rate\" r=\"4\"/></svg><span id=\"max-rate\"></span></div><div class=\"axis\"><span>Q=1</span><span>8</span><span>16</span></div><p class=\"caption\" data-i18n=\"latency\"></p><div class=\"curve\"><svg viewBox=\"0 0 320 150\" aria-hidden=\"true\"><path class=\"axes\" d=\"M10 5V135H310\"/><polyline id=\"curve-latency\"/><line id=\"mark-latency\" y1=\"5\" y2=\"135\"/><circle id=\"dot-latency\" r=\"4\"/></svg><span id=\"max-latency\"></span></div><div class=\"axis\"><span>Q=1</span><span>8</span><span>16</span></div><div class=\"metrics\"><div><span data-i18n=\"rate\"></span><strong id=\"rate\"></strong></div><div><span data-i18n=\"latency\"></span><strong id=\"latency\"></strong></div></div><div class=\"formula\" id=\"arithmetic\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.curve{position:relative}.curve svg{display:block;width:100%;height:auto;max-height:220px;overflow:visible}.curve .axes{fill:none;stroke:var(--rule)}.curve polyline{fill:none;stroke:var(--accent);stroke-width:3}.curve line{stroke:var(--second);stroke-width:1;stroke-dasharray:4 3}.curve circle{fill:var(--second)}.curve>span{position:absolute;left:14px;top:0;font:11px var(--viz-font-ui);background:var(--paper);padding:2px}.formula{min-height:6em}#queued{display:grid;grid-template-columns:repeat(8,minmax(0,1fr))}#queued .cell,#service .cell{min-height:20px;height:20px;padding:0;font-size:9px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}function draw(){const q=+$('#q').value,k=+$('#k').value,d=+$('#d').value,rate=Math.min(q,k)*1e6/d,latency=q/rate*1e6;for(const[id,v]of [['q',q],['k',k],['d',d]])set(id+'-value',v);cells('service',Array.from({length:8},(_,i)=>({text:i<k?'●':'—',cls:i<Math.min(q,k)?'on':'empty'})));cells('queued',Array.from({length:16},(_,i)=>({text:i<Math.max(0,q-k)?'●':'',cls:i<Math.max(0,q-k)?'gold':'empty'})));for(const[name,fn,max]of [['rate',n=>Math.min(n,k)*1e6/d,k*1e6/d],['latency',n=>n*d/Math.min(n,k),16*d/k]]){const points=Array.from({length:16},(_,i)=>[10+i*20,135-fn(i+1)/max*120]);$('#curve-'+name).setAttribute('points',points.map(p=>p.join(',')).join(' '));$('#mark-'+name).setAttribute('x1',points[q-1][0]);$('#mark-'+name).setAttribute('x2',points[q-1][0]);$('#dot-'+name).setAttribute('cx',points[q-1][0]);$('#dot-'+name).setAttribute('cy',points[q-1][1]);set('max-'+name,viz.number(max,0)+(name==='rate'?' IOPS':' μs'));}set('rate',viz.number(rate,0));set('latency',viz.number(latency,1));set('arithmetic','λ = min('+q+', '+k+') / '+d+' μs\\nQ = λ × W = '+q);set('explanation',t('note'));}for(const id of ['q','k','d'])$('#'+id).oninput=draw;draw();","audio":false,"strings":{"d":"每项服务时间 D / μs","heading":"吞吐进入平台后,多出的深度去了哪里?","intro":"改变队列深度 Q、并行服务槽 K 和固定服务时间 D。两条曲线共享 Q 轴,竖线标出当前点。","k":"并行服务槽 K","latency":"平均响应 W / μs","note":"看同一 Q 竖线上的两个结果。把 Q 推过 K,完成率保持平台,平均响应时间则继续增加;它们并不是两个互不相关的性能数字。","q":"在途深度 Q","queued":"等待服务的请求","rate":"完成率 λ / IOPS","reset":"重置","scope":"固定 Q 的封闭循环、K 个理想并行服务槽、每项固定 D,无其他瓶颈或抖动。λ=min(Q,K)/D,W=Q/λ。K 是教学模型容量,不对应真实 NVMe 的固定内部并行数;均值曲线不预测尾延迟。","scopeLabel":"模型范围与约定","service":"服务槽:蓝色正在服务,空白不可用"}}
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 可以观察排队、派发和完成,但拆分、合并、重试、部分完成和事件丢失会使简单按地址配对失效。先确定身份与层次,再计算时间差。统计字段见块设备统计,追踪方法见追踪与调试。