同一 namespace 可以有多条路径,一次命令完成也可能仍依赖易失缓存。理解 NVMe,要把存储对象、队列协议、数据地址和恢复状态分别定位。
| 查阅目标 | 阅读位置 |
| controller、namespace 与 SQ/CQ | 第 1~2 节 |
| LBA、PRP 与恢复 | 第 3~4 节 |
| 多路径、DM 与缓存模式 | 第 5~8 节 |
1. Controller、namespace 与 subsystem
controller 提供主机访问接口及管理能力;namespace 提供逻辑块地址空间;subsystem 可以包含多个 controller 并提供共享 namespace。namespace 不是必然对应一块独立 NAND 芯片,也不能简单等同于分区。
一个 namespace 可以被不同路径访问,是否能识别为同一存储对象,要结合 subsystem、NSID 和 UUID/NGUID/EUI 等标识及共享属性,而不是只比较容量或 /dev 名字。路径消失后设备编号可能变化,持久定位应使用正确的稳定标识。
Identify 返回能力和格式等信息,实际 I/O 队列资源还需要协商与创建。控制器支持上限、驱动实际使用数量和工作负载当前并发是三种不同值。
2. SQ/CQ:两个生产消费协议
PCIe 路径中,主机把命令写入 submission queue,按协议通知控制器新的 tail;控制器执行后将 completion queue entry 写回,主机处理并推进 CQ head。
一个 SQ 关联一个 CQ,协议允许多个 SQ 共享一个 CQ;Linux 驱动常用成对组织方式,但不是协议强制一一对应。MSI-X 向量也不必与每个队列一一绑定,轮询队列可能不用中断。队列数量由硬件、驱动、CPU 拓扑和资源共同决定,不保证每 CPU 独占且完全无锁。
CQ 的 phase tag 帮助区分环形槽位在当前轮次是否已经被控制器写成有效完成。主机发现有效状态后,还要按 DMA/内存顺序规则读取其余字段。命令可以乱序完成,通过 SQ 标识与 command identifier 等定位请求,不能只假设最早提交的先结束。
让完成队列填满并消费一圈,再回到槽 0。图中故意保留消费过的 CID 字节;决定它是否有效的是当前 head 与期待 phase,而不是槽位看起来是否非空。
正在呈现知识画面
槽位里仍有旧字节,为什么不算新完成?主机在当前 head 比较 CQE phase 与期待 phase,匹配才是这一轮可消费的完成。回绕改变期待值,因此旧字节不会被误认为新一轮完成。
{"id":"nvme-completion-phase","title":"槽位里仍有旧字节,为什么不算新完成?","summary":"主机在当前 head 比较 CQE phase 与期待 phase,匹配才是这一轮可消费的完成。回绕改变期待值,因此旧字节不会被误认为新一轮完成。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"produce\" data-i18n=\"produce\"></button><button id=\"consume\" data-i18n=\"consume\"></button></div><div class=\"track-row\"><span data-i18n=\"host\"></span><div id=\"host\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"device\"></span><div id=\"device\" class=\"track\"></div></div><div class=\"cq\"><div class=\"slot\" id=\"slot-0\"><header>slot 0</header><strong id=\"cid-0\"></strong><span id=\"phase-0\"></span><small id=\"pointer-0\"></small></div><div class=\"slot\" id=\"slot-1\"><header>slot 1</header><strong id=\"cid-1\"></strong><span id=\"phase-1\"></span><small id=\"pointer-1\"></small></div><div class=\"slot\" id=\"slot-2\"><header>slot 2</header><strong id=\"cid-2\"></strong><span id=\"phase-2\"></span><small id=\"pointer-2\"></small></div><div class=\"slot\" id=\"slot-3\"><header>slot 3</header><strong id=\"cid-3\"></strong><span id=\"phase-3\"></span><small id=\"pointer-3\"></small></div></div><div class=\"metrics\"><div><span data-i18n=\"pending\"></span><strong id=\"pending\"></strong></div><div><span data-i18n=\"consumed\"></span><strong id=\"consumed\"></strong></div></div><div class=\"card\"><header data-i18n=\"match\"></header><div id=\"match\" class=\"readout\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></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.cq{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:20px 0}.slot{border:1px dashed var(--rule);border-radius:8px;padding:12px;min-width:0;font:12px var(--viz-font-ui)}.slot:nth-child(3){order:3}.slot:nth-child(4){order:2}.slot header{color:var(--muted)}.slot strong{display:block;font-size:20px;margin:10px 0;overflow-wrap:anywhere}.slot span,.slot small{display:block;min-height:2em}.slot small{font-size:11px;margin-top:9px}.slot.pending{border-style:solid;background:var(--accent-soft);border-color:var(--accent)}.slot.head{box-shadow:inset 0 0 0 2px var(--second)}.readout{min-height:7em}","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)));}let slots=Array.from({length:4},()=>({phase:0,cid:null,pending:false})),head=0,tail=0,hp=1,tp=1,count=0,serial=0,consumed=0;function draw(){cells('host',[{text:head+' / phase '+hp,cls:'gold'}]);cells('device',[{text:tail+' / phase '+tp,cls:'on'}]);for(let i=0;i<4;i++){$('#slot-'+i).classList.toggle('pending',slots[i].pending);$('#slot-'+i).classList.toggle('head',i===head);set('cid-'+i,slots[i].cid===null?'—':'#'+slots[i].cid);set('phase-'+i,'phase '+slots[i].phase);set('pointer-'+i,(i===head?'HEAD ':'')+(i===tail?'TAIL':''));}const match=slots[head].phase===hp;set('match',slots[head].phase+' '+(match?'=':'≠')+' '+hp+'\\n'+t(match?'valid':'stale'));set('pending',count);set('consumed',consumed);$('#produce').disabled=count===4;$('#consume').disabled=!match;set('explanation',t('note'));}$('#produce').onclick=()=>{if(count===4)return;slots[tail]={phase:tp,cid:++serial,pending:true};tail=(tail+1)%4;if(tail===0)tp^=1;count++;draw();};$('#consume').onclick=()=>{if(slots[head].phase!==hp)return;slots[head].pending=false;head=(head+1)%4;if(head===0)hp^=1;count--;consumed++;draw();};$('#reset').onclick=()=>{slots=Array.from({length:4},()=>({phase:0,cid:null,pending:false}));head=tail=count=serial=consumed=0;hp=tp=1;draw();};draw();","audio":false,"strings":{"consume":"主机消费 head","consumed":"已消费总数","device":"控制器 tail / 写入 phase","heading":"槽位里仍有旧字节,为什么不算新完成?","host":"主机 head / 期待 phase","intro":"让控制器产生完成,或让主机消费 head 槽。填满并消费一圈,观察双方 phase 怎样翻转,而旧槽内容不会自动清零。","match":"当前 head 的检查","note":"最容易误判的是绕回槽 0 后:CID 仍在那里,phase 却属于上一轮。主机只检查当前 head,不把其他槽的偶然 phase 匹配当成可乱序消费。","pending":"未消费完成数","produce":"控制器写入一个完成","reset":"重置","scope":"四槽 CQ,初始所有槽 phase=0,双方当前 phase=1。只模拟 CQ,有效完成按序写入,CID 仅作身份。生产者不覆盖未消费槽,主机更新 head 视为已通知控制器。忽略 DMA 顺序屏障、错误状态、SQ 关联和实际 doorbell 时序。","scopeLabel":"模型范围与约定","stale":"不匹配,等待新完成","valid":"匹配,可消费"}}
为什么 doorbell 不能写成固定“一次 MMIO”
命令发布前要先保证相关写入可被设备按协议看到。主机可批量发布多个 SQ entry 再通知,也会更新 CQ head;doorbell buffer、轮询与中断合并等机制还会改变访问频率。
所以 NVMe 可以降低控制开销,但不存在跨平台统一“每 I/O 恰好一次 MMIO、比 AHCI 固定快五倍”的推论。完成延迟还受闪存、缓存、队列等待、CPU、NUMA 和电源状态影响。
3. LBA、NLB 与 PRP 的具体例子
以 4096 字节逻辑块为例,从 LBA 256 读取 8192 字节:
| 字段或计算 | 结果 |
| 起点字节偏移 | 256 × 4096 = 1 MiB |
| 逻辑块数量 | 8192 / 4096 = 2 |
| 命令 NLB | 零基计数,填写 1 |
先确定 namespace 格式,才能正确解释命令单位。
这与 Linux 块层的 512 字节 sector 单位不同,驱动必须转换。真实 struct nvme_command 包含不同命令格式的联合体,字段并不是所有命令通用的同一串平铺成员;多字节字段按协议小端编码。
PRP 描述的是控制器可访问的数据地址
控制器页大小 4096 字节,起始偏移 1024,总长度 8192,需要三张页:
| 数据段 | 可放字节 |
| 第一页剩余部分 | 4096 − 1024 = 3072 |
| 第二页 | 4096 |
| 第三页剩余数据 | 1024 |
因此第一页之后还需要两页,接下来才能判断 PRP2 的含义。
- PRP1 指向第一段起点,允许相应页内偏移。
- 若剩余数据只需一页,PRP2 可直接指向该数据页。
- 本例剩余需要两页,因此 PRP2 指向 PRP list,由列表给出后续页地址。
PRP list 不是“PRP2 永远是第二个数据指针”,列表跨页时又有链接规则。地址通过 DMA API 建立,不能把名字里的 Physical 理解成可直接填 CPU 虚拟地址或未经转换的物理地址。
SGL 使用带类型和长度等信息的描述符,支持能力依命令集、controller 与 transport 不同。NVMe/RDMA、NVMe/TCP 的数据传输也不能原样套用 PCIe 主机内存 PRP 机制。阅读入口:NVM Express 规范及内核 include/linux/nvme.h。
4. Linux 提交与恢复路径
drivers/nvme/host/core.c 负责共享协议和 controller/namespace 生命周期,pci.c 处理 PCIe 传输,tcp.c、rdma.c、fc.c 等处理各自传输,fabrics.c 提供 fabrics 共用部分。
PCIe 初始化通常先使设备和管理队列可用,再获取能力、协商资源、创建 I/O 队列并扫描 namespace。探测和恢复中有异步 work,不能把某个版本函数名列表视为永远同步执行的固定顺序。
提交 request 时,驱动设置命令、映射数据、处理资源失败,再把命令发布给 controller。队列环绕不能未经证明就用 index & (depth - 1):这种位运算只对二次幂深度成立。
超时说明主机尚未确认完成,不证明设备没有执行写入。驱动会根据状态实施 abort、reset、重连或失败处置,并处理旧请求与迟到完成;上层不能据“重试成功”推断旧副作用完全没发生。完成仍要区分普通写缓存、Flush 与 FUA 的持久化语义。
5. 原生 multipath 与 ANA
Linux 原生 NVMe multipath 在满足标识和配置条件时,把多个 controller 的 namespace 路径汇聚成一个逻辑访问对象。ANA 描述路径的访问状态,例如 optimized 与 non-optimized;主机据此及 I/O policy 选择路径。
NUMA、round-robin、queue-depth 等策略在支持它们的版本中各有用途,不能把默认策略写死为 round-robin;实际状态应读取运行系统。路径选择也不是 I/O elevator 的另一名字:前者选访问路径,后者安排队列中的请求。
多路径提高路径冗余,不自动提供数据副本、备份或跨独立磁盘复制。故障恢复期间请求可能排队或失败,其时延受控制器和连接恢复策略影响。见 NVMe multipath。
NVMe/TCP、RDMA、FC 将 NVMe 访问扩展到相应传输;网络拥塞、认证/加密配置、重连和 target 后端都会改变性能与可靠性,不应给 RDMA 写一个脱离条件的固定微秒延迟。
6. SCSI 与 device-mapper 在哪里
SCSI 上层驱动包括磁盘 sd、光驱 sr、磁带 st;中层处理通用命令管理和错误恢复;低层驱动连接 HBA 或具体传输。SATA 经 libata 等路径也可能在 Linux 中呈现为 SCSI 磁盘,所以 /dev/sdX 不能单凭名字判断物理总线。
device-mapper 按 target 将逻辑块地址映射到下层设备,可组合多层:
| Target/机制 | 作用 | 不提供的保证 |
| linear | 拼接或偏移映射 | 不自动增加冗余 |
| crypt | 块加密 | 不单独解析 LUKS 格式,也不默认认证所有篡改 |
| thin / thin-pool | 按需分配与共享块快照 | 逻辑容量不等于已经保留物理空间 |
| cache | 快设备缓存慢设备的块 | dirty 数据仍依赖缓存设备存活 |
| writecache | 特定持久介质上的写缓存 | 不是通用读写缓存的改名 |
| multipath | 将多条路径汇聚 | 不等于 RAID 副本 |
| raid | 在 DM 中使用相应 RAID 机制 | 不能据此声称 MD RAID 已停止开发 |
LVM 是用户态卷管理系统,可配置多种 DM target,不仅是“dm-linear 命令包装”。MD、DM RAID 与 LVM 的接口和管理关系需要分清。
Thin pool 为何要同时监控数据和元数据
thin volume 先声明逻辑容量,实际写入才分配数据块;快照共享块,后续写入可能触发分配和映射更新。数据池满与元数据池满是不同故障,可能进入排队、只读或错误路径,具体行为由配置和状态决定。
快照仍依赖原 pool 的数据和元数据,不等于独立备份。文件删除也不保证 pool 立即回收空间,还要看 discard、快照引用和目标支持。见 thin provisioning。
7. 缓存模式改变故障时数据在哪里
| 缓存模式 | 确认时需要关注的数据位置 |
| writeback | 最新版本可能只在缓存设备,后端仍旧 |
| writethrough | 按模式完成后端写入后确认 |
| writearound | 写入绕过缓存,失效规则依实现 |
模式改变故障时的恢复依据,不能只比较正常运行时的速度。
不是“SSD 就安全、HDD 就危险”,而是要确定 dirty 数据、映射元数据及 flush 约定的持久性。具有断电保护的缓存设备能缓解某些断电风险,但设备本身损坏仍可能丢失尚未回写的数据。
dm-cache、dm-writecache 和 bcache 是不同实现,管理接口与迁移/拆除流程不通用。bcache 也不是 bcachefs 的缩写。参见 dm-cache、bcache。
8. 只读观察与定位
lsblk -o NAME,TYPE,SIZE,LOG-SEC,PHY-SEC,PKNAME,MOUNTPOINTS
安装 nvme-cli 后,可用 nvme list、nvme list-subsys 查看对象和路径,nvme id-ctrl / id-ns 查看指定对象能力。SMART 的 percentage_used 是估计耐久消耗,不是“剩余寿命倒计时”;错误计数需要结合变化趋势和具体错误解释。
从应用看到 I/O 错误时,依次确认逻辑卷、DM/RAID、namespace/path、controller 和下层设备。不要把同一块数据在多个堆叠层的统计重复累加成物理吞吐。块层测量与完成语义见块设备层,密钥层次见Crypto API 与存储加密。