目录1. 主控在 PCIe 上长什么样2. 设备级实现VFIO 把主控交给虚拟机2.1 原理2.2 实现流程Linux QEMU/KVM2.3 必须处理的实现细节2.4 和“假直通”的对比不要选错3. 命令级实现NVMe Command Passthrough3.1 数据路径3.2 ioctl 实现经典路径3.3 现代路径io_uring NVMe passthrough3.4 实现时的命令分类3.5 Windows 对应实现4. 用户态直通VFIO SPDK内核旁路5. SR-IOV主控硬件把 Function 切给多台 VM6. 主控固件内部的 Passthrough做芯片/FPGA/固件时6.1 正常路径 vs Passthrough 路径6.2 常见实现形态6.3 固件实现要点自研主控7. SATA/AHCI 直通不要误当成“SSD 主控直通”8. 一条可落地的实现选择9. 最小验证清单SSD 场景里说的Passthrough经常被混成三件完全不同的事。实现路径、要碰的寄存器、以及你最终“绕过”的那一层都不一样。先对齐对象再谈实现。层次直通的对象绕过什么典型使用者设备直通整颗 NVMe PCIe Function主控本身Hypervisor 的块设备模拟虚拟机独占盘、数据库 VM命令直通原始 NVMe SQE/CQE文件系统、通用块层、SCSI 转换nvme-cli、产测、厂商工具、io_uring用户态直通BAR / 门铃 / MSI-X内核 NVMe 驱动SPDK、DPDK 式存储栈主控固件直通物理 NAND / 通道 / 页FTL映射、GC、磨损均衡开通道盘、厂测固件、FPGA 主控NVMe 和 SATA 有一个本质差别后面所有实现都建立在这上面SATA板上有一颗AHCI Host Controller盘里还有一颗SSD 主控中间是 SATA 协议。直通 AHCI 控制器 ≠ 直通盘主控。NVMe盘上那颗主控就是PCIe Endpoint。BAR0 就是 NVMe Controller Registers。对 NVMe 做 PCIe Passthrough直通的就是主控芯片。1. 主控在 PCIe 上长什么样一颗典型 NVMe SSD 对软件暴露的是PCIe Function ├─ BAR0 (至少 16KB)Controller Registers │ 0x00 CAP / VS / INTMS / INTMC / CC / CSTS / NSSR │ 0x18 AQA / ASQ / ACQ Admin 队列 │ 0x1000 SQ0TDBL / CQ0HDBL ... I/O 门铃 ├─ MSI-X Table通常几十到几百个 vector一队列一对 ├─ 可选 BARCMBController Memory Buffer/ PMR └─ Config SpaceVID/DID、FLR、ACS、SR-IOV CapabilityHost 与主控的交互只有四件事MMIO 写门铃Tail/Head Doorbell主控 DMA 读 SQE64B、DMA 写 CQE16B和用户数据PRP/SGLMSI-X 中断或轮询 CompletionsAdmin 命令改控制器状态Enable、Shutdown、Identify、FW Commit…所谓 Passthrough就是让最终使用者直接做这四件事中间层不再把 NVMe 翻译成 virtio/SCSI/通用 bio。2. 设备级实现VFIO 把主控交给虚拟机这是虚拟化里“SSD 主控直通”的标准实现。QEMU/KVM 不模拟 NVMe 控制器只把 BAR、配置空间、中断、DMA 翻译权交给 Guest 里的原生nvme驱动。2.1 原理Guest nvme.sys / nvme.ko │ MMIO 门铃、配置空间 ▼ QEMU vfio-pci只做 trap 配置空间 / 部分 MSI-X │ ▼ VFIO IOMMU │ GPA → HPADMA Remap │ 中断重映射 ▼ 物理 NVMe 主控 ──DMA──► Guest 内存中的 SQ/CQ/数据页IOMMU 是硬前提。没有 IOMMU主控可以按 Guest 给的地址对 Host 物理内存 DMA等于把盘固件变成 DMA 攻击入口。和 GPU 直通相比NVMe 通常更干净多数消费级/企业级 NVMe 独占一个 IOMMU group支持FLRFunction Level Reset二次直通比 GPU 稳得多没有“显示输出/Code 43/vBIOS”问题代价同样是不能热迁移、不能在 Host 和 Guest 间共享这一 Function2.2 实现流程Linux QEMU/KVM① 硬件/固件BIOS 打开 VT-d / AMD-Vi、Above 4G DecodingCPU/主板 IOMMU 正常确认目标盘不是系统盘直通后 Host 立刻失去这块盘② 内核参数intel_iommuon iommupt # AMD: amd_iommuon iommuptiommupt对直通设备走 identity/直通映射优化减少不必要翻译。③ 确认 IOMMU groupshopt -s nullglob for g in /sys/kernel/iommu_groups/*/devices/*; do d${g##*/} printf IOMMU Group %s $(basename $(dirname $(dirname $g))) lspci -nns $d done目标 NVMe 应尽量独占一组。同组设备必须一起直通否则 IOMMU 隔离不成立。④ 从内核 NVMe 驱动抢走主控绑到 vfio-pciBDF0000:03:00.0 VID_DID$(lspci -n -s $BDF | awk {print $3} | tr : ) # 先卸掉块设备会丢失 /dev/nvmeXnY echo $BDF /sys/bus/pci/devices/$BDF/driver/unbind modprobe vfio-pci echo $VID_DID /sys/bus/pci/drivers/vfio-pci/new_id # 更干净的做法 # echo vfio-pci /sys/bus/pci/devices/$BDF/driver_override # echo $BDF /sys/bus/pci/drivers_probe开机自动抢绑定避免 initramfs 里 nvme 驱动先占上# /etc/modprobe.d/vfio.conf softdep nvme pre: vfio-pci options vfio-pci ids8086:0953把8086:0953换成你的lspci -nn结果。⑤ QEMU / libvirtQEMUqemu-system-x86_64 \ -enable-kvm -cpu host \ -device vfio-pci,host0000:03:00.0,idnvme-pt \ ...libvirthostdev modesubsystem typepci managedyes driver namevfio/ source address domain0x0000 bus0x03 slot0x00 function0x0/ /source address typepci domain0x0000 bus0x05 slot0x00 function0x0/ /hostdevProxmoxhostpci0: 0000:03:00.0,pcie1managedyes时 libvirt 会自动 unbind/bind不必手写 sysfs。⑥ Guest装原生 NVMe 驱动Linux 内核自带Windows 用 Microsoftstornvme或厂商驱动lspci应看到真实 VID/DID而不是 Red Hat virtionvme list/Identify Controller能读到真实 MN/SN/FW、Vendor 命令、多 Namespace⑦ 回收关 VM → vfio 释放 → FLR → 可重新bind回nvme。消费级 NVMe 的 FLR 一般可用少数盘 FW 有 reset bug表现为 Host 再挂驱动时 CSTS.RDY 一直不置位只能冷复位 PCIe 或重启。2.3 必须处理的实现细节MSI-X 与队列数主控按CAP.MQES、Identify.SQES/CQES、Number of Queues工作。Guest 驱动会创建几十对 I/O 队列。VFIO 要把全部 MSI-X vector 映射进 Guest。不要在 Host 侧限制 vector 数量。HMBHost Memory Buffer消费级 DRAM-less 主控会向 Host 要一块内存当缓存。直通后 HMB 从Guest 内存分配IOMMU 负责 DMA。保证Guest 内存不要过度 balloon 到 HMB 被回收开启 Huge Pages 可降低 IOMMU IOTLB 压力CMB / PMR若 BAR 里有 Controller Memory BufferGuest 会把它当普通 MMIO/内存用。QEMU vfio-pci 默认映射 BAR不要随手rombar0之类误伤其它 BAR。Namespace 与引导要从直通 NVMe 启动Guest 用 UEFI把该 PCIe 设备放进可启动路径。SeaBIOS 直通 NVMe 经常起不来。不要和 Host mdadm/LVM/ZFS 同时用绑 vfio 之前必须 teardown 所有使用该 ns 的块栈否则有脏设备、残留 mount、multipath 残留。写缓存与掉电直通后 Guest 看到的是真实VWC、Volatile Write Cache。要不要开盘内缓存、要不要 FUA由 Guest 文件系统决定。Host 的nvme set-feature -f 6对已经直通的盘无效。2.4 和“假直通”的对比不要选错方案Guest 看到什么主控命令谁发延迟原生 NVMe 特性vfio-pci 直通真盘真 DIDGuest 驱动接近裸机全部含 Vendor、多 NS、FW 升级QEMU emulatednvme软件模拟的 NVMe 控制器QEMU 软件差子集virtio-blk / virtio-scsi虚拟块设备Host 内核 NVMe较好几乎没有vhost-user-nvme / SPDK vhost半虚拟 NVMe 或 virtio用户态很好看实现NVMe SR-IOV VF真主控切出来的 VFGuest 驱动接近裸机受 PF 策略限制只有第一行和 SR-IOV 是真正的“主控芯片 Passthrough”。virtio-blk 只是把盘当块设备用Guest 发的是 virtioHost 再转成 NVMe主控仍然归 Host 内核。3. 命令级实现NVMe Command Passthrough不把设备抢走而是内核驱动仍然占着主控但允许用户态提交原始 64 字节 SQE。这是产测、刷固件、读 SMART/Log、发 Vendor Unique 的标准做法。Linux 上常被直接叫NVMe Passthrough。3.1 数据路径应用构造 nvme_passthru_cmd → ioctl(/dev/nvme0) 或 io_uring CMD → nvme.ko 填 SQE、映射用户缓冲PRP/SGL → 写门铃 → 主控执行 → CQE.dw0 回到 result状态码回 errno/CQE绕过的是文件系统、I/O scheduler 语义、通用submit_bio、SCSI 翻译。没有绕过内核 NVMe 驱动、中断/轮询、IOMMU如果开了。设备节点/dev/nvme0字符设备Admin 指定 NSID 的 I/O passthrough/dev/nvme0n1块设备一般不要拿它发 Admin3.2 ioctl 实现经典路径#include linux/nvme_ioctl.h #include fcntl.h #include stdint.h #include stdio.h #include string.h #include sys/ioctl.h #include unistd.h /* Identify Controller (Admin opcode 0x06, CNS1) */ int identify_controller(int fd, void *buf_4k) { struct nvme_passthru_cmd cmd { .opcode 0x06, .nsid 0, .addr (uint64_t)(uintptr_t)buf_4k, .data_len 4096, .cdw10 1, /* CNS 1: Identify Controller */ .timeout_ms 0, }; return ioctl(fd, NVME_IOCTL_ADMIN_CMD, cmd); }I/O 命令用NVME_IOCTL_IO_CMD/NVME_IOCTL_IO64_CMD必须填正确nsid。64-bit 版本能带回完整result和更好的状态。对应命令行# Admin passthrough nvme admin-passthru /dev/nvme0 --opcode0x06 --cdw101 --data-len4096 -r -b # I/O passthrough读 LBA 01 个 block nvme io-passthru /dev/nvme0n1 --opcode0x02 --namespace-id1 \ --cdw100 --cdw110 --cdw120 --data-len512 -r3.3 现代路径io_uring NVMe passthroughLinux 5.13 把 NVMe passthrough 接到IORING_OP_URING_CMD避免 ioctl 的系统调用和完成等待模型适合高性能用户态xNVMe、部分数据库、测试框架。要点struct nvme_uring_cmd { __u8 opcode; __u8 flags; __u16 rsvd; __u32 nsid; __u32 cdw2, cdw3; __u64 metadata; __u64 addr; __u32 metadata_len; __u32 data_len; __u32 cdw10, cdw11, cdw12, cdw13, cdw14, cdw15; __u32 timeout_ms; __u32 rsvd2; }; /* sqe-cmd_op NVME_URING_CMD_IO 或 NVME_URING_CMD_IO_VEC */限制实现时必须遵守默认不允许任意 AdminFormat、FW Download、Security、Sanitize 等从 uring 走防止非特权路径毁掉盘数据缓冲要满足 DMA 对齐vec 版本对应 SGL仍然经过内核nvme驱动不是 SPDK 那种用户态门铃3.4 实现时的命令分类不是所有 opcode 都该开放给业务进程。类型例子Passthrough 策略普通 I/ORead/Write/Write Zeroes/DSM(Trim)可开放注意和文件系统缓存一致性Identify / Get Log / Get Feature0x06, 0x02, 0x0A只读相对安全Set Feature0x09改队列数、写缓存、HMB、电源危险Format / Sanitize / FW0x80, 0x84, 0x11, 0x10必须特权 停 I/OVendor Unique0xC0–0xFF 等主控私有产测/加密/原厂工具缓存一致性对/dev/nvme0n1上已挂载的文件系统再 passthrough 写同一 LBA会绕过 page cache造成静默损坏。实现原则产测先umount再nvme字符设备监控只允许 Get Log / Identify用户数据路径要么全走文件系统要么全走 passthroughSPDK/O_DIRECT 独占3.5 Windows 对应实现IOCTL_STORAGE_PROTOCOL_COMMANDStorport NVMe 的 Pass-Through IRP厂商自己的 DeviceIoControlSamsung Magician、Intel MAS 都走这条Admin 能力受 Storport 策略和签名驱动限制比 Linux 字符设备更严。4. 用户态直通VFIO SPDK内核旁路把第 2 节的“把主控给 Guest”改成“把主控给本机一个进程”就是 SPDK 路径。主控芯片仍然是那颗 PCIe Function只是使用者从虚拟机变成用户态轮询驱动。① 解绑 nvme.ko绑定 vfio-pci与虚拟化完全相同 ② 进程打开 /dev/vfio/$GROUPVFIO_GROUP_GET_DEVICE_FD ③ mmap BAR0读 CAP/VS按 NVMe 规范初始化 ④ 在用户态内存建 Admin SQ/CQ写 AQA/ASQ/ACQ ⑤ CC.EN1轮询 CSTS.RDY ⑥ IdentifyCreate I/O CQ/SQ每核一对 ⑦ 业务线程填 SQE → 写 Tail 门铃 → 忙轮询 CQ Phase Tag核心初始化伪代码/* regs 是 mmap 出来的 BAR0 */ void nvme_enable(volatile uint8_t *regs, uint64_t asq, uint64_t acq, uint32_t depth) { uint32_t cc; /* 先 disable */ cc mmio_r32(regs 0x14); cc ~1u; mmio_w32(regs 0x14, cc); while (mmio_r32(regs 0x1C) 1u) /* CSTS.RDY */ ; mmio_w32(regs 0x24, ((depth - 1) 16) | (depth - 1)); /* AQA */ mmio_w64(regs 0x28, asq); mmio_w64(regs 0x30, acq); cc (0 20) | (6 16) | (4 7) | (0 4) | 1; /* IOCQES16B^2? 实际按 CAP 和规范填 CSS/MPS/IOSQES/IOCQES */ mmio_w32(regs 0x14, cc); while ((mmio_r32(regs 0x1C) 1u) 0) ; }实现要点SQ/CQ、数据缓冲必须是DMA 可见的VFIO 容器 IOMMU map或大页 vfio dma map门铃写完需要sfence/wmb否则主控可能读到旧 Tail完成路径用 Phase Tag 轮询不要依赖中断SPDK 默认 pollerAdmin 和 I/O 不要共用锁模型Create Queue 必须串行进程崩溃后主控可能仍在 DMA必须靠 VFIO 的VFIO_DEVICE_RESET/ FLR 回收性能这条路径把中断、块层、bio 拆分、文件系统全拿掉4K 随机可以打满主控队列深度是本地 NVMe 用户态存储的上限实现。5. SR-IOV主控硬件把 Function 切给多台 VM企业级盘部分 PCIe 4.0/5.0 数据中心 SSD在主控里实现 SR-IOVPF (Physical Function) —— Host 管理固件、Namespace 分配、QoS ├── VF0 → VM0 的 vfio-pci ├── VF1 → VM1 └── VF n每个 VF 对 Guest 仍是一颗“NVMe 控制器”自己的 BAR、门铃、队列、MSI-X。主控芯片只有一颗硬件仲裁 DMA 和 NAND 带宽。实现步骤概要# Host 保留 PF不要把 PF 直通出去 echo N /sys/bus/pci/devices/0000:d8:00.0/sriov_numvfs # 每个 VF 绑 vfio-pci再按第 2 节方式交给 VM还要在 PF 上用厂商工具或 NVMe 管理命令把 Namespace 分配给指定 VF有的实现是 private NS有的是 shared NS配置 VF 的队列数、带宽/IOPS QoS不要在 PF 上对同一 NS 跑业务 I/O除非规格明确支持和整卡直通的差别可多租户共享一颗主控Guest 通常不能刷固件、Format 整盘、改全局 Feature热迁移仍然很难VF 内部队列状态在硬件里消费级盘几乎都没有 SR-IOV不要在 消费级 980/SN850 上找这个能力6. 主控固件内部的 Passthrough做芯片/FPGA/固件时如果目标是在 SSD 主控里实现一种 Passthrough 模式厂测、Open-Channel、自研 FPGA 盘那是另一套设计。6.1 正常路径 vs Passthrough 路径Host NVMe 命令 → PCIe/NVMe Protocol Engine解析 SQE拉 PRP/SGL → Command Dispatcher ├─ 正常FTLL2P、GC、WL、ECC、RAID/XOR→ NAND Controller └─ Passthrough按命令里的通道/CE/LUN/Page/Plane 直达 NAND ControllerPassthrough 模式要解决的不是虚拟化而是把 FTL 从数据面拿掉。6.2 常见实现形态① Vendor Unique Admin/IO 命令厂测固件标配自定义 opcode例如CDW10: channel | ce | lun | plane CDW11: block CDW12: page | offset CDW13: nand_cmd (READ_PAGE / PROG_PAGE / ERASE) | options PRP1: 主机缓冲页大小如 16K OOB固件 Dispatcher 看到该 opcode不查 L2P不做 GC/wear直接向 NAND 控制器 IP 下发时序命令把 raw page spare/OOB 搬到 Host 缓冲返回 NAND statusECC fail、program fail 原样上报用途坏块扫描、颗粒特性、原厂校准、FW 量产烧录。量产固件必须用 Feature bit 关掉否则一发命令就能毁掉用户数据。② Open-Channel SSD / LightNVM主控把几何信息暴露给 HostIdentify或特定命令集FTL 在 Host。主控只做按物理地址读写擦ECC有限的坏块表有时带最小读写约束读干扰、program order这是架构级 Passthrough主控变成“智能 NAND 适配器”。③ ZNS / FDP不是完全 bypass FTL而是把映射自由度部分交还 HostZone append、放置提示。实现上仍有 FTL只是 GC 策略被约束。不要把它和 raw NAND passthrough 混为一谈。④ Bridge 芯片上的协议直通USB-NVMe、SAS-NVMe、企业卡上的切换芯片常有NVMe 命令透传把 NVMe SQE 从一侧搬到另一侧对比翻译成 SCSI/BOT功能会丢失Write Zeroes、Fused、Vendor 命令做桥片时Passthrough 的关键是SQ/CQ 门铃转发 PRP/SGL 地址翻译IOMMU 或内部 ATU而不是自己实现 FTL。6.3 固件实现要点自研主控命令入队路径与正常 I/O 隔离Passthrough 命令不要走 FTL 的 LBA 锁否则和用户 I/O 死锁。单独的 admin 上下文或冻结 FTL。DMA 与 NAND 时序时钟域Protocol Engine 写完 Host 缓冲再给 NAND DMAprogram fail 要按页上报不要静默重试厂测需要看见真实失败。安全开关用 OTP/Feature/VU Identify bit 控制。Host 侧Identify能查到 “passthru enabled”。量产关闭。与 NVMe 状态机共存CC.EN、Shutdown、Controller Reset 必须能中止 in-flight NAND passthrough否则 FLR 后通道挂死。不要让 Passthrough 绕过电源保护路径掉电时仍要停 NAND 时钟、完成已发出的 program 或明确 abort否则颗粒损坏。7. SATA/AHCI 直通不要误当成“SSD 主控直通”CPU └─ PCIe AHCI 控制器Intel/AMD/ASM1061... ← VFIO 直通的是这个 └─ SATA 链路 └─ SSD 主控SATA 协议后端Host 看不见 PCIe BAR实现上是Host Controller Passthrough把 AHCI 控制器的 BDF 绑 vfio-pci该控制器下所有 SATA 口全部进入 GuestGuest 跑ahci 厂商 SATA 驱动盘内主控仍然只懂 SATA/ACSHost/Guest 都不能发 NVMe 命令适合一堆 SATA SSD/HDD、光驱、需要 AHCI FIS 级行为的场景。不适合想对 NVMe 主控发 Vendor 命令——那必须直通 NVMe Function 本身。SATA 还有ATA PassthroughSCSI SATSG_IO ATA PASS-THROUGH那是命令级类似第 3 节对象是 ATA 命令而不是 NVMe SQE。8. 一条可落地的实现选择按目标选一条路即可不要混用同一块盘。要让虚拟机看到“真 SSD 主控、真 VID/DID、能刷盘、能上厂商驱动” → VFIO PCIe 直通第 2 节 → 多租户且盘支持 → SR-IOV VF第 5 节 要在 Linux 上读 Log、产测、发 VU 命令但盘仍给内核用 → /dev/nvme0 ioctl 或受限的 io_uring passthrough第 3 节 要本地用户态打满 IOPS、自己实现存储栈 → vfio-pci SPDK/自研用户态驱动第 4 节 要在 FPGA/自研主控里让 Host 直接读写 NAND → Vendor Unique Dispatcher bypass FTL第 6 节同一颗盘的互斥关系内核 nvme.ko 占用 ≠ vfio-pci 占用 ≠ 用户态 SPDK 占用 挂载着的文件系统 ≠ I/O passthrough 乱写同一 NS PF 管理平面 ≠ 把 PF 自己直通进 VM9. 最小验证清单设备直通是否成功# Guest lspci -nn | grep -i non-volatile nvme id-ctrl /dev/nvme0 nvme id-ns /dev/nvme0n1 # 对比 Host 直通前的 SN/MN/FW应完全一致命令直通是否成功nvme smart-log /dev/nvme0 nvme get-log /dev/nvme0 --log-id1 --log-len512用户态直通是否成功# SPDK ./scripts/setup.sh ./build/examples/identify主控固件 passthrough 是否成功Identify 中 VU 位开启读 raw page 的 OOB 与颗粒 datasheet 一致关 FTL 后 program/erase 失败码能原样回 HostController Reset 后通道可再次枚举如果你要的是其中一条的“可照着做”版本说一下具体栈我可以按那个写到配置项/代码级Proxmox/QEMU 直通某条 NVMe含 IOMMU group、开机抢绑定、Guest 启动Linux 下完整的 Admin/IO passthrough C 代码含错误码解析SPDK/vfio 用户态初始化主控自研主控VU passthrough 命令格式和 Dispatcher 状态机