NVMe Write Zeroes及周边命令:语义、命令帧与实测坑

发布时间:2026/10/27 9:58:30
NVMe Write Zeroes及周边命令:语义、命令帧与实测坑 盘一台机器上的盘最容易被忽略的往往不是读带宽、写带宽这些跑分指标而是“这块区间我想让它变成干净的零”这类管理动作。几年前给一批测试机做批量镜像还原脚本里老老实实地用dd往整盘灌/dev/zero一块 1.6 TB 的企业级盘要跑接近四十分钟跑完还得再校验一遍。后来有个做固件的朋友看了一眼说你这纯属用 PCIe 带宽喂 NAND一条 Write Zeroes 就完事了。当时将信将疑地试了一下同样的区域命令下去几秒钟就返回hexdump抽查读回全是零。那次之后我才认真去读 NVMe 的 NVM Command Set才发现 Write Zeroes 不是一个孤立的命令它周围站着一圈语义相近、用途各异的“亲戚”同门的 Deallocate、Compare、Write Uncorrectable跨协议的 SCSI WRITE SAME、UNMAP还有 ATA 那边的 TRIM。这些命令长得像、名字像语义差别却不小用错了轻则性能打折重则把数据擦得莫名其妙。这篇就把 Write Zeroes 这一条命令连同它周围这一圈关系户从命令帧到主机软件栈从协议位到实测坑一次性盘清楚。做存储测试、虚拟化、文件系统、固件验证的同行可以直接拿去对照刚接触 NVMe 的朋友也能顺着这条线把整个命令集的脉络摸一遍。1. 主机灌零的时代结束了Write Zeroes在链路上省了什么1.1 一次镜像还原暴露出来的带宽浪费先说清楚问题的原始形态。假设你要把一块盘的某个区域清成零最朴素的做法是在主机侧准备一块全零的内存 buffer然后反复下发写命令把零从内存搬到盘上。这条路在协议层面完全正确但成本极高主机要消耗 CPU 去准备 bufferPCIe 链路要承载这些零字节的传输SSD 的 DMA 引擎要接收、要解包、要写进 SRAM最后 FTL 还要为这些零分配物理页、发起 NAND 编程。整个过程里真正有价值的只有最后一步的“结果”——那些 LBA 读出来是零前面所有的搬运都是纯开销。一块盘做整盘清零按 1.6 TB 算就是上千 GB 的数据从主机流向设备。就算按每通道 6 GB/s 的理论带宽算光传输就得几分钟算上命令开销、队列深度受限、写放大实际耗时往往是几十分钟级别。更要命的是这些零数据会在 NAND 上留下真实的物理页SSD 的可用空间被吃掉一块后面真正写业务数据的时候反而要先做垃圾回收。所以“写零”这件事主机做得越多设备越难受。Write Zeroes 命令的设计动机就是把这件事从主机侧挪到设备侧。主机只告诉设备“从 LBA X 开始连续 N 个逻辑块请你保证它们读出来是零”至于设备内部怎么实现主机不管。这个语义上的微小转移带来的是整条数据路径的成本重构PCIe 上不再有零字节流动DMA buffer 不需要准备命令队列里只多了一个 64 字节的条目。协议栈往下走SSD 的固件拿到这条命令之后理论上有好几条路可以走走哪条决定了这条命令到底能省多少。1.2 命令语义让介质自己“造零”严格来说Write Zeroes 在 NVMe 里的语义定义是控制器应当使指定 LBA 范围在后续的读命令中返回全零除非该命名空间另有声明。注意这里的措辞是“返回全零”它约束的是读回值而不是物理介质的状态。这个区别非常关键它留出了后面要讲的 DEAC 语义漂移的空间也解释了为什么不同 SSD 上同一条命令的性能能差出一个数量级。从设备的实现角度看要让一个 LBA 读出来是零至少有三种做法。第一种最笨也最直接真的在 NAND 上编程一份零数据。这种做法在语义上最稳妥读回值绝对确定但性能上几乎和主机灌零没有本质区别只是省了 PCIe 传输那一小段NAND 编程的耗时一分不少写放大也照样存在。第二种是映射到一个只读的“零页”FTL 里预先准备一个装满零的物理页把所有被 Write Zeroes 覆盖的 LBA 都指向这一页读命令走映射表直接返回这一页的内容。这种做法省掉了绝大部分 NAND 编程代价是映射表会被撑大而且这个零页必须被保护起来不能被 GC 回收。第三种是先把这些 LBA 标记为 deallocated然后在读路径上根据 DLFEAT 字段声明的规则返回零。这一条路最省资源但它的确定性和“真正写零”之间就有了缝隙。提示如果你的业务依赖“写零之后读回一定是零”这个强保证不要只看控制器是否支持 Write Zeroes还要看它是否支持 DEAC 位以及 DLFEAT 字段里声明的读回值是什么。有些型号在不支持 DEAC 的情况下反而会老老实实走前两种实现确定性和性能之间要做取舍。从协议演进的角度看Write Zeroes 很早就进入了 NVM Command Set但“设备可以把写零顺手优化成 deallocate”这个开关是后来才补上的。这个补丁让 Write Zeroes 从一个偏保守的语义保证命令变成了一个既能保证读回值、又能借用 deallocate 性能的命令但同时也把语义的复杂度往上抬了一档。1.3 什么样的硬件才真正把这条命令吃进去一条命令“支持”和“用好”是两回事。判断一个控制器是不是真的把 Write Zeroes 吃进去我一般看三件事ONCS 里的支持位、NSFEAT 里的精简置备标志、以及 DLFEAT 里的读回值声明。ONCS 是 Identify Controller 返回的 Optional NVM Command Support 字段第 3 位对应的就是 Write Zeroes。这一位为 0命令下去直接返回 Invalid Opcode主机侧看到的错误码是 0x0001块层会回退到实际的写零路径你根本感知不到异常只是性能上没有任何收益。NSFEAT 是命名空间级别的特性位第 0 位如果置位说明这个命名空间支持精简置备也就是说设备允许物理上不分配空间而逻辑上仍然存在。这一位和 Write Zeroes 的第三种实现强相关如果它不置位设备就不太可能把写零映射成 deallocate。DLFEAT 在 Identify Namespace 的偏移 33 字节低三位是 DLRV定义了 deallocate 之后的 LBA 读回来是什么三个 0 表示全零001 表示全一010 及以上表示未定义。这一位的取值直接决定了你怎么理解一条 Write Zeroes 之后读到的数据。我实际碰到过一个比较典型的坑某型盘在 ONCS 里置了 Write Zeroes 位DLFEAT 声明 DLRV 为 0看起来一切正常但真跑起来发现一条覆盖 128 MiB 的 Write Zeroes 命令耗时接近主机灌零的 70%。用厂商工具抓了内部 trace 才看明白固件的实现路径是“把零数据在 SRAM 里拼出来再编程 NAND”完全没有走映射表。这种实现协议上挑不出毛病但对你来说这条命令就是白支持。所以判断一条命令值不值得用光看能力位不够还得做实测——同样的区域Write Zeroes 和主机灌零各跑一遍看耗时是不是差出一个数量级以上差不到这个量级就说明设备内部还是老老实实在写 NAND。2. CDW10到CDW12一条命令帧里埋着的三个语义开关2.1 SLBA与NLB的边界计算Write Zeroes 的命令帧结构很紧凑属于典型的 NVMe I/O 命令布局。前两个 qword 是保留和元数据指针接着两个 qword 是 PRP1 和 PRP2这两项在 Write Zeroes 里通常用不上因为命令本身不携带数据载荷但协议上仍然保留字段位置以保持命令格式对齐。真正的参数从 CDW10 开始。CDW10 和 CDW11 拼成一个 64 位的 SLBA也就是起始逻辑块地址的低 32 位和高 32 位。CDW12 的低 16 位是 NLB也就是 Number of Logical Blocks注意它是零基的填 0 表示 1 个逻辑块填 2047 表示 2048 个逻辑块。这一点是很多人第一次写测试脚本时容易弄错的地方nvme write-zeroes的--block-count参数同样是零基的传 2047 才是 2048 个块。如果按“块数”的直觉填 2048实际会覆盖 2049 个块最后多擦一个块如果那个块上正好有文件系统的元数据问题就出来了。NLB 是 16 位理论最大 65536 个逻辑块按 4 KiB 块大小算就是 256 MiB 单条命令的覆盖范围。这个上限在某些大块清零场景下会显得不够比如做整盘擦除几 TB 的空间要拆成上万条命令。好在 NVMe 的命令队列深度通常很深主机侧可以并发下发实际吞吐不受单条命令上限的严重制约。但手工写脚本的时候要注意一次覆盖超过 65536 块就必须拆分否则 NLB 会溢出回绕实际清掉的范围和你预期的完全不同这种 bug 在校验阶段才会暴露排查起来很费劲。还有一点容易被忽略SLBA NLB 不能越过命名空间的 NSZE 边界。如果越界控制器返回 LBA Out of Range 错误状态码是 0x0080。有些脚本为了省事直接按整盘大小算 NLB 不拆分在某些容量不是 2 的整数次幂的盘上就会踩到这个边界最后一条命令报错前面的都成功了看起来像是“清了 99%”。2.2 DEAC位省下NAND编程的那一下CDW12 的 bit 25 是 DEAC 位全称 Deallocate。这一位置 1 的时候语义发生了一次微妙但重要的变化控制器被允许把这段 LBA 当作 deallocate 处理也就是放弃“必须真的写零”这个强约束转而采用“标记为空闲”的做法。从主机的角度看命令还是同一条参数还是同一组只是多置了一个位但设备内部的执行路径可能完全换了一条。DEAC 置位带来的最大好处是省掉 NAND 编程。设备不需要为这些 LBA 分配物理页只需要在映射表里把它们标记成无效或者指向一个共享的零页。对于 SSD 来说这几乎等于零成本操作命令的完成时间可以从毫秒级降到微秒级。在做整盘清零或者大批量稀疏文件初始化的时候这个差异非常可观。代价是读回值的确定性下降。DEAC 置位之后这些 LBA 读回来是不是零取决于 DLFEAT 的 DLRV 声明。如果 DLRV 是 0读回全零和真正的写零没有区别如果 DLRV 是 1读回全一如果是 2 及以上读回值未定义可能是零、可能是旧数据、可能是随机值。这就意味着你在用 DEAC 之前必须先读一次 DLFEAT确认你的业务逻辑能接受对应的读回值。虚拟化场景里如果把这个区域再映射给虚拟机虚拟机看到的可能是全一某些文件系统会把它当成坏块标记问题就很隐蔽了。注意DEAC 位在较早的协议版本里可能不存在。如果你的目标设备固件比较老置了这一位可能会被当成保留位忽略命令照常执行但走的是普通写零路径性能上没有收益也不会报错。这种“静默忽略”是最难排查的一类问题建议在测试阶段先用一条小命令验证耗时确认 DEAC 真的生效了再上批量。再补充一个实操细节某些控制器在 DEAC 置位且 NLB 很大时会做内部分段处理分段的粒度可能是 128 KiB 或者 1 MiB。分段本身不影响语义但会让命令的实际耗时呈现非线性比如 NLB 从 255 涨到 256 时耗时突然翻倍。做性能对比测试的时候要把这个因素考虑进去不要拿一条 4 KiB 的命令和一条 128 MiB 的命令直接比吞吐。2.3 DLFEAT、NSFEAT与ONCS能力位要连着看三个能力位字段分布在两个数据结构里第一次接触很容易看混。ONCS 在 Identify Controller 里是控制器级别的能力说明“这个控制器支持不支持这条命令”。NSFEAT 和 DLFEAT 在 Identify Namespace 里是命名空间级别的特性说明“这个命名空间上这条命令的语义是什么”。同一个控制器下挂多个命名空间ONCS 是共享的NSFEAT 和 DLFEAT 可以各不相同这是做多命名空间测试时必须注意的地方。具体到字段位ONCS 第 3 位是 Write Zeroes 支持位第 1 位是 Write Uncorrectable第 0 位是 Compare第 2 位是 Dataset Management。这四位经常是一起出现的做命令集验证的时候可以一次性全部检查。NSFEAT 第 0 位是 Thin Provisioning第 3 位是 Deallocated or Unwritten Logical Block Error Support前者决定设备是否可能把写零优化成 deallocate后者决定读一个未写过的 LBA 会不会直接报错而不是返回零。DLFEAT 在偏移 33 字节低三位 DLRV 定义 deallocate 后的读回值第 3 位定义读未写块是否报错。这个第 3 位和 NSFEAT 的第 3 位是联动的两个都置位读未写块才会真正返回错误状态。如果你的业务逻辑依赖“读到零”来判断区域是否为空而设备配成了“读未写块报错”那么一次 Write Zeroes 之后去读得到的是错误而不是零校验脚本会误判成写零失败。我用一个小脚本一次性把这三个字段都读出来做对照比手工一条条 grep 效率高得多#!/bin/bash DEV${1:-/dev/nvme0} NS${2:-${DEV}n1} echo Controller level (ONCS) nvme id-ctrl $DEV | grep -E ^oncs|^oacs|^vwc echo Namespace level (NSFEAT / DLFEAT) nvme id-ns $NS | grep -E ^nsfeat|^dlfeat|^nsze|^ncap|^nuse echo LBA format nvme id-ns $NS | grep -E ^lbaf把这三组字段放在一起看基本就能对这台设备上 Write Zeroes 的行为做出判断ONCS 第 3 位为 1 是前提NSFEAT 第 0 位为 1 说明有优化的可能DLFEAT 的 DLRV 值决定了 DEAC 之后读回什么。三个都指向“优化路径可用”的配置才值得在上面做大规模清零。3. 同门近亲Deallocate、Compare、Write Uncorrectable各自守着哪块地盘3.1 Deallocate与Write Zeroes的语义鸿沟Dataset Management 这条命令在 NVM Command Set 里的定位是“批量管理数据集的属性”它最常见的用法是其中的 Deallocate 属性。很多人把 Deallocate 和 Write Zeroes 当成同一件事的两种叫法这是最容易出问题的一种误解。两者的共同点只是“都会让原来的数据不可恢复”除此之外几乎处处不同。Deallocate 的语义是“告诉设备这些 LBA 上的数据我不再需要了你可以回收物理资源”。它约束的是设备的资源管理行为不约束读回值。设备回收之后这些 LBA 读回来是什么完全由 DLFEAT 的 DLRV 决定可能是零、可能是全一、可能是未定义。Write Zeroes 则明确约束读回值必须是零。换句话说Write Zeroes 是一个“结果导向”的命令Deallocate 是一个“资源导向”的命令。前者保证你能读到什么后者只保证设备能省下什么。从性能上看Deallocate 几乎总是比 Write Zeroes 快因为它不需要为读回值做任何保证。从安全性上看Deallocate 也不比 Write Zeroes 差两者都不会真的把数据从 NAND 上擦掉物理数据仍然可能残留在介质上真正的安全擦除要靠 Sanitize 命令。所以如果你的目的是“让这些位置读出来是零”用 Deallocate 是有风险的如果目的是“把这些物理资源还给设备”用 Write Zeroes 反而浪费。两者在主机软件栈里对应的是两条不同的路径。blkdiscard不带-z走的是 DISCARD也就是 Deallocate带-z走的是 ZEROOUT也就是 Write Zeroes。文件系统的 punch hole 走 Deallocatezero range 走 Write Zeroes。分清楚这两个语义是后面所有操作选择的前提。命令参数上Deallocate 比 Write Zeroes 复杂一些。它通过 CDW10 的低 8 位指定范围数量减一最多 256 个范围剩下的字段通过 PRP 指向一块内存里面是一组 16 字节的范围描述符每个描述符包含 4 字节的上下文属性、4 字节的逻辑块数量和 8 字节的起始 LBA。这个设计允许一条命令回收最多 256 段不连续的区间在文件系统大规模释放块的场景下很有用。Write Zeroes 只支持一段连续区间超出就要拆成多条命令。3.2 Compare写零前的“确认键”Compare 命令在 ONCS 的第 0 位功能是把主机提供的数据和盘上对应 LBA 的数据做逐字节比较全部一致就返回成功有任何不一致就返回 Compare Failure状态码 0x85。这条命令本身不改变任何数据是一个只读操作但它在写零相关的流程里扮演了一个很有意思的角色。典型用法是在做“条件写零”这类场景先把预期内容读出来比较确认这块区域的状态和预期一致再下 Write Zeroes。比如做镜像还原的时候某些机器的旧数据是敏感的需要确认这块区域已经被前一次写零覆盖过这时候 Compare 就能派上用场。它不是必需的但它是验证语义的一个廉价手段比全量读回来在主机侧比对要省带宽——虽然 Compare 的实际数据流仍然是主机到设备但它省掉了主机侧的比对逻辑和随后的读写往返。Compare 的参数结构和 Write 命令几乎一样CDW10 和 CDW11 拼成 SLBACDW12 的低 16 位是 NLB只是没有数据写入方向数据是从主机流向设备做比较用。这里有一个容易忽略的点Compare 失败的时候控制器的返回是命令级别的失败而不是读错误。也就是说如果某个 LBA 读出来就是介质错误比如真的坏了Compare 返回的是 Unrecovered Read Error状态码 0x0281和普通的比较不匹配是两回事。区分这两种失败在自动化脚本里很重要前者说明介质有问题后者说明数据不符合预期。Compare 的另一个用途是在调试 Write Zeroes 的时候做交叉验证。下完 Write Zeroes 之后用 Compare 传一份全零的数据去比比得过说明读回确实是零比不过说明设备的实现和你的预期有出入。这比写脚本去读一整段再在内存里判断要省事也更贴近协议层的视角。3.3 Write Uncorrectable造坏块的正当用途Write Uncorrectable 在 ONCS 第 1 位功能是让控制器把指定 LBA 范围标记成不可纠正错误状态之后读这些 LBA 会返回 Unrecovered Read Error。从名字上看这像是一条“破坏性”命令实际上它的正当用途全是测试和验证。在存储测试里Write Uncorrectable 是构造坏块场景的标准手段。你要验证文件系统的坏块处理逻辑、验证 RAID 或纠删码的重建路径、验证上层应用对读错误的重试策略总不能真的等一块盘自然老化出坏块。用 Write Uncorrectable 定点标记几个 LBA读的时候就会触发错误路径整个过程可控、可重复、可恢复——恢复的方式是 Write Zeroes 或者普通的 Write 覆盖这几个 LBA因为写入会重新建立映射把不可纠正状态清掉。这一点和 Write Zeroes 形成了一个有趣的闭环Write Zeroes 可以修复 Write Uncorrectable 造成的错误状态。所以在做测试序列设计的时候可以拿 Write Uncorrectable 造错误验证完再拿 Write Zeroes 清掉不需要重新格式化整个命名空间。这个组合我在做文件系统容错验证的时候用过很多次比反复 mkfs 省时间得多。参数上 Write Uncorrectable 和 Write Zeroes 几乎一样SLBA 加 NLB同样受 65536 块上限约束。区别是它没有 DEAC 位这种优化开关语义非常直接就是“把这些位置标记成坏”。还有一点某些控制器对 Write Uncorrectable 的覆盖范围有限制比如只允许标记一定数量以内的 LBA超过就返回错误。这个上限在 Identify Controller 里不一定有对应的字段声明只能实测。4. 跨协议的远房亲戚WRITE SAME、UNMAP与ATA TRIM的映射关系4.1 SCSI WRITE SAME的NDOB位与UNMAP位如果从 SCSI 那边过来看到 Write Zeroes 的第一反应是“这不就是 WRITE SAME 吗”。这个直觉大体正确但 WRITE SAME 的语义比 Write Zeroes 更宽。SCSI 的 WRITE SAME(10) 和 WRITE SAME(16) 命令的原始语义是把主机提供的一个逻辑块的内容重复写入到指定的 LBA 范围。它允许主机传任意一个块的数据设备负责复制填充所以它不只是能写零理论上能写任何重复模式。WRITE SAME 的 CDB 里有两个关键位。一个是 NDOBNo Data-Out Buffer置位的时候主机不传数据设备自己决定写什么这时候写的内容由设备的实现决定规范建议是全零但不是强制。另一个是 UNMAP 位置位的时候命令的语义直接退化成 unmap也就是 deallocate完全不再写任何数据。这两个位的组合决定了 WRITE SAME 到底走哪条路径。从 NVMe Write Zeroes 的视角看它对应的是 SCSI WRITE SAME 在 NDOB 置位且 UNMAP 不置位时的场景也就是“不传数据、设备自己写零”。至于 WRITE SAME 的 UNMAP 路径对应的是 NVMe 的 Deallocate。这个映射关系在做协议转换层或者兼容性测试的时候特别重要一个 SCSI 主机过来的 WRITE SAME 命令你得先解析 UNMAP 位和 NDOB 位再决定翻译成 NVMe 的 Write Zeroes 还是 DSM Deallocate翻译错了语义就变了。我见过一次典型的翻译错误某中间层把 SCSI 的 WRITE SAME 无脑翻译成 NVMe 的 Write Zeroes结果主机发的是 UNMAP 置位的 WRITE SAME本意是“把空间还给设备”翻译之后变成了“保证读回零”设备走了映射零页的路径物理空间一点没省下来性能还不如直接 deallocate。这种问题在协议转换产品里不算罕见做兼容性验证的时候要专门构造带 UNMAP 位的 WRITE SAME 来测。SSI 侧还有一个 UNMAP 命令本身opcode 是 0x42通过参数列表一次提交多个范围和 NVMe 的 DSM Deallocate 在功能上是对齐的。两者的范围数量上限不同SCSI UNMAP 的参数列表理论上可以很长NVMe 的 DSM 单条命令最多 256 个范围。做跨协议性能对比的时候这个粒度差异会体现在命令数量上。4.2 ATA世界为什么没有真正对等的命令ATA 这边对应的命令是 TRIM从 SATA 协议的角度看TRIM 的语义非常接近 NVMe 的 Deallocate告诉设备这些 LBA 不再需要设备可以回收。它没有 NVMe Write Zeroes 那种“保证读回零”的语义也没有 SCSI WRITE SAME 那种“重复填充一个块”的能力。所以在 ATA 世界里如果你想“写零”只能老老实实从主机灌零TRIM 只是告诉设备“这些位置的数据作废了”读回来是什么完全取决于设备实现。这个差异的根源在于 ATA 协议的设计年代和定位。TRIM 是在 SSD 普及初期为了缓解写放大和性能衰减引入的核心诉求是“让操作系统告诉设备哪些块真的没用了”并不涉及读回值的保证。NVMe 是后设计的协议直接吸收了 TRIM 的经验并在此基础上把“写零”这个高频需求单独拿出来做成了一条命令顺便还加了 DEAC 位让设备有优化空间。所以 NVMe 的 Write Zeroes 在概念上是 SCSI WRITE SAME 和 ATA TRIM 的一个折中产物既有 SCSI 那边的读回值保证又借了 ATA 那边的资源回收思路。在 SATA 上做类似 Write Zeroes 的动作比较接近的手段是hdparm --trim-sector-ranges或者blkdiscard但这些都是 discard 语义不保证读回零。如果业务确实需要读回零就只能走主机灌零没有别的选择。这也是 NVMe 盘在虚拟化和云场景里比 SATA 盘更受欢迎的原因之一批量初始化、稀疏文件、快照克隆这些操作NVMe 有原生命令支持SATA 只能靠主机硬扛。4.3 一张跨协议语义对照表把几个协议的对应命令放在一张表里对照很多模糊的地方会一下子清楚。下面这张表是我自己整理出来贴在工位上的做跨协议测试的时候随时查协议命令语义约束读回值保证资源回收NVMeWrite Zeroes (DEAC0)指定 LBA 读回为零强保证视实现而定NVMeWrite Zeroes (DEAC1)允许按 deallocate 处理由 DLFEAT 决定强保证NVMeDSM Deallocate标记 LBA 不再需要无保证强保证NVMeWrite Uncorrectable标记 LBA 不可纠正读回报错无NVMeSanitize物理层面清除数据不适用强保证SCSIWRITE SAME (NDOB1, UNMAP0)设备写零通常保证视实现而定SCSIWRITE SAME (UNMAP1)等价于 unmap无保证强保证SCSIUNMAP标记 LBA 不再需要无保证强保证ATATRIM标记 LBA 不再需要无保证强保证这张表里最值得琢磨的是 Sanitize 那一行。很多人把 Write Zeroes 当成“安全擦除”来用这是一个相当危险的误解。Write Zeroes 只是在逻辑层面让读命令返回零物理介质上的旧数据可能仍然残留通过某些手段比如芯片级读取仍然有可能恢复。真正的安全擦除只有 Sanitize 或者 Format NVM 带 Crypto Erase 才能做到。如果业务场景涉及敏感数据销毁Write Zeroes 只能作为流程中的一步最终必须用 Sanitize 收尾。5. 从REQ_OP_WRITE_ZEROES到nvme-cli命令在主机的两层入口5.1 块层与文件系统的触发链路Linux 块层里有一个专门的 bio 操作类型叫REQ_OP_WRITE_ZEROES它对应的就是 NVMe 的 Write Zeroes。这个操作类型的引入就是为了让上层能够表达“我要这块区域变成零”的意图而不必亲自准备零数据。块层的通用函数blkdev_issue_zeroout()是这条链路的入口它会先尝试用REQ_OP_WRITE_ZEROES下发如果设备不支持或者返回错误再回退到实际的写零页路径。这个回退逻辑值得仔细说一下因为它是很多“为什么 Write Zeroes 没生效”问题的根源。blkdev_issue_zeroout()的行为受 flags 影响最主要的是BLKDEV_ZERO_NOUNMAP。如果不加这个 flag函数在设备不支持 Write Zeroes 的时候可能会尝试用 discard 代替语义就从“保证读回零”退化成了“只回收资源”。加了BLKDEV_ZERO_NOUNMAP就禁止这个退化设备不支持就直接走实际的写零页。所以在写内核模块或者自己的工具时如果业务语义要求读回必须是零一定要把这个 flag 带上。文件系统这边ext4 和 xfs 都提供了fallocate的FALLOC_FL_ZERO_RANGE标志对应的就是“把这段范围清零”。内核实现里会调用blkdev_issue_zeroout()最终落到设备上就是 Write Zeroes。另一个相关的是FALLOC_FL_PUNCH_HOLE对应的语义是“打洞”走的是 discard 路径落到设备上是 Deallocate。这两个标志在应用层面看起来都是“释放空间”实际行为差异很大选错了会导致文件读出来是零还是随机值。我做过一个对比测试在一个支持 Write Zeroes 的命名空间上对一个 1 GiB 的文件分别用fallocate -zzero range和fallocate -ppunch hole处理然后立刻dd读回来。-z之后读回全是零-p之后读回是一堆看起来随机的数据实际上是设备上残留的旧数据因为 punch hole 走的是 Deallocate没有读回值保证。这个差异在备份、克隆、快照这些场景里影响很大如果克隆工具用的是 punch hole克隆出来的文件可能有部分区域是脏的。5.2 blkdiscard、fallocate与BLKZEROOUT的实操差异在用户态往块设备上写零有三条常见路径分别是blkdiscard -z、fallocate的文件范围清零以及ioctl的BLKZEROOUT。这三条路径最终都通向blkdev_issue_zeroout()但在参数和适用范围上有区别。blkdiscard -z /dev/nvme0n1是最直接的一条作用于整块块设备。不带-z的时候它走 discard也就是 Deallocate带-z的时候走 zeroout也就是 Write Zeroes。这个命令的语义是“把整个设备或者指定区间清零”用起来最省事但也最危险一旦作用在挂载了文件系统的设备上数据会直接没。我在生产环境里只在确认设备未挂载的情况下用或者加--force参数明确表达这个意图。fallocate -z -l 1G filename作用于文件底层调FALLOC_FL_ZERO_RANGE适合在文件系统层面做清零。它的好处是只影响文件数据范围不动元数据也不影响文件系统的其他部分。缺点是依赖文件系统支持ext4、xfs、btrfs 这几个主流文件系统都支持但一些老的或者特殊的文件系统可能不支持fallocate会回退到实际的写零。回退的时候用户态是感知不到的只有看strace或者内核 trace 才能发现。BLKZEROOUT是 ioctl作用于块设备的指定区间参数是起始字节和长度内部会转成 LBA 和 NLB。它比blkdiscard更细粒度可以只清一段。写自己的工具的时候用这个 ioctl 比较合适比手工组 NVMe 命令要安全得多。这个 ioctl 在老版本内核里可能不存在做跨版本兼容的时候要检查返回值。5.3 nvme-cli手工发命令的完整示例nvme-cli 提供的write-zeroes子命令是验证这条命令最直接的工具绕过了块层直接构造 NVMe 命令帧。做设备兼容性测试或者排查块层问题的时候我一般先用它确认设备本身的行为再往上查块层的翻译。# 第一条查能力和 LBA 格式确认支持 Write Zeroes 且块大小为 4 KiB nvme id-ctrl /dev/nvme0 | grep -E ^oncs|^vwc nvme id-ns /dev/nvme0n1 | grep -E ^nsfeat|^dlfeat|^lbaf # 第二条从 LBA 0 开始清零 2048 个块-c 是零基所以传 2047 nvme write-zeroes /dev/nvme0n1 --start-block0 --block-count2047 # 第三条带 DEAC 位允许设备走 deallocate 优化路径 # 先确认 nvme-cli 版本支持 --deac 参数老版本可能是 --deac1 的形式 nvme write-zeroes /dev/nvme0n1 --start-block0 --block-count2047 --deac # 第四条用 DSM Deallocate 做对照验证两种路径的耗时差异 nvme dsm /dev/nvme0n1 --slbs0 --blocks2048 --ad # 第五条用 Compare 验证读回值确实是零 # 先准备一个 4 KiB 的全零文件 dd if/dev/zero of/tmp/zero.bin bs4096 count1 nvme compare /dev/nvme0n1 --start-block0 --block-count0 \ --data-size4096 --data/tmp/zero.bin这里有几个参数细节值得说明。--block-count和--blocks都是零基的传 2047 表示 2048 个块这个和 NVMe 协议里的 NLB 定义一致nvme-cli 没有做额外的偏移。--start-block是 SLBA直接对应协议字段。--data-size是 Compare 时主机传输的数据长度必须是块大小的整数倍。--deac参数在比较新的 nvme-cli 版本里才有如果手上的版本较老可能需要升级到 1.16 以上。再补一个实测技巧做性能对比的时候每组至少跑 5 次去掉第一次。第一次跑的时候设备的映射表和缓存状态都是冷的耗时会明显偏高用来和后面的数据平均会失真。另外在做大批量清零之前先把测试区域用普通 Write 写满把映射表建立起来再对比 Write Zeroes 和灌零的耗时这样测出来的才是稳态差异否则前面那条“区域本来就是空的”会让 Write Zeroes 显得格外快。6. 踩坑实录命令返回成功、读回却不是零的几种情况6.1 控制器把Write Zeroes悄悄翻译成普通写这是最常见也最难受的一类坑。命令返回状态是成功主机侧看不到任何异常但实际耗时和主机灌零差不多说明设备内部根本没有走优化路径。前面提过的那块 128 MiB 写零耗时接近灌零 70% 的盘就是这种情况固件把零数据在 SRAM 里拼出来再编程 NAND协议上完全合规因为规范只要求“读回是零”没规定必须用哪种实现。排查这类问题第一步是把 Write Zeroes 的耗时和主机灌零的耗时并排测。如果两者差异小于 2 倍基本可以断定设备没有走优化路径。第二步是看设备的 SMART 信息注意Data Units Written这个计数如果一条 Write Zeroes 之后这个计数明显增长说明设备真的往 NAND 上写了数据如果基本不动说明走的是 deallocate 或者映射零页。这个观察方法比抓 trace 简单得多nvme-cli 的smart-log子命令就能读。我碰到过一个更隐蔽的变种设备的Data Units Written不增长看起来像是走了优化路径但读回值校验的时候发现中间有几段不是零。查下去发现是设备把 Write Zeroes 翻译成了一连串的 Deallocate而 DLFEAT 的 DLRV 声明是 0理论上应该读回零但中间有几段 LBA 之前被 Write 过映射表里的项没有被正确覆盖读回来是旧数据。这是一个固件 bug触发条件是“先写后零再读”做测试序列的时候把顺序打乱就能复现。这种 bug 只能靠长时间跑随机序列来抓单条命令的测试覆盖不到。6.2 元数据区与混合命名空间的连带效应NVMe 的命名空间可以带元数据每个逻辑块除了 4 KiB 的用户数据外还可以有 8 字节或 64 字节的元数据由 LBA Format 里的 MS 字段声明。Write Zeroes 命令默认只作用于用户数据区元数据区怎么处理不同实现的行为不一致。有些设备会把元数据也清零有些保留原值有些返回未定义。如果你的业务依赖元数据区的内容写零之后元数据变成了什么必须实测确认不能假设。这个坑在做文件系统校验的时候会冒出来文件系统把校验和存在元数据区写零把用户数据清了但元数据没清读回的时候校验和与全零的用户数据对不上文件系统报出校验错误。表面上看起来像是“写零把文件系统写坏了”实际是元数据的处理不符合预期。规避的办法是看 Identify Namespace 的 LBA Format 表确认 MS 字段如果非零就要在写零之后单独处理元数据或者干脆用 Format NVM 重建命名空间。混合命名空间是另一个容易踩的地方。同一个控制器下挂多个命名空间各自的 LBA Format 可能不同块大小可能是 512 字节也可能是 4 KiB。Write Zeroes 的 NLB 是按当前命名空间的逻辑块算的如果你在脚本里按 4 KiB 计算 NLB实际下发到一个 512 字节块的命名空间上覆盖范围会大出 8 倍把后面的区域一起清掉。做多命名空间测试的时候一定要在每条命令之前重新读一次 ID-NS 确认块大小不要沿用上一个命名空间的假设。还有一种情况是命名空间被格式化过之后LBA Format 变了。比如原来用 4 KiB 块Format NVM 之后切到 512 字节块之前存下来的 LBA 和 NLB 就全部失效了。自动化脚本里如果硬编码了这些值格式化之后会算错范围。我的做法是每次操作前都重新解析 ID-NS把 NSZE、LBAF、LBADS 这几个值读出来现算多花几毫秒省掉一整类 bug。6.3 持久化与掉电语义的边界Write Zeroes 完成之后这些零是不是立刻持久化了答案取决于命名空间有没有易失写缓存VWC。如果 VWC 置位命令完成只表示数据进了设备的缓存还没有落到 NAND 上。这时候掉电重新上电之后读那些 LBA读到的可能是旧数据写零的效果丢了。要保证持久化命令完成之后需要跟一条 Flush等 Flush 返回才算落地。这个语义和普通 Write 是一样的但写零场景下更容易被忽略因为大家潜意识里觉得“擦除”是个很重的操作应该立刻生效。实际上对于走映射零页或者 deallocate 路径的实现写零操作在缓存里就是一个标记位掉电后缓存丢了标记也就丢了映射表恢复到掉电前的状态读回来还是旧数据。做掉电测试的时候如果把 Write Zeroes 当成“原子擦除”来设计测试用例结果会不符合预期。注意VWC 置位的设备上做批量写零一定要在批次结束之后跟一次 Flush。如果设备支持

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询