IOMMU SVA 共享虚拟地址:设备直通与进程地址空间 ID 深度工程实战

现代计算架构中,GPU、FPGA、NVMe、网卡等高性能外设对直接内存访问的需求与日俱增。IOMMU SVA(Shared Virtual Addressing)作为 Linux 内核中最前沿的设备直通技术,彻底重构了 CPU 与设备共享内存的编程模型。本文从 PCIe ATS/PASID 协议根基出发,深入剖析 SVA 在内核中的体系结构设计、mmu_notifier 协同机制、PASID 生命周期管理,并给出生产环境部署的完整工程实践。


一、问题域:为什么需要 SVA

1.1 传统 DMA 编程模型的困境

在没有 SVA 的设备直通场景中,虚拟机或容器中的驱动程序必须通过虚拟机监控器(Hypervisor)或宿主机内核的中转服务来完成地址映射。经典流程如下:

应用程序分配内存 (GVA)
    ↓
Guest OS 页表 (GVA → GPA)
    ↓
Hypervisor 页表 (GPA → HPA)
    ↓
IOMMU 使用 HPA 做 DMA

这条路径存在三个根本性问题:

  • 双重地址转换开销:Hypervisor EPT/NPT 表与 IOMMU 页表形成两层穿越,TLB Miss 惩罚叠加
  • 内存气球锁定:Guest 分配给设备的内存必须在气球锁定(balloon pinning)后才能建立映射,延迟不可控
  • 流式 DMA 不连续:Guest 视角连续的内存在 Host 物理上可能碎片化,IOMMU 页表必须逐个映射

1.2 SVA 的核心思想

SVA 的关键洞察是:让 CPU 和 PCIe 设备使用同一张页表。当进程申请 DMA 缓冲区时,内核直接将进程虚拟地址(VA)映射到 IOMMU,设备发起的 DMA 使用与 CPU 相同的虚拟地址,无需中间转换层。

┌─────────────────────────────────────────────┐
│              进程虚拟地址空间                 │
│         0x7F00_0000 ~ 0x7FFF_FFFF           │
│         ┌──────────────────────┐             │
│         │    DMA 共享缓冲区     │             │
│         └──────────────────────┘             │
└────────────────┬────────────────────────────┘
                 │ 同一份页表
       ┌─────────┴─────────┐
       │                   │
   CPU 执行              DMA 传输
  (MMU 翻译)          (IOMMU 翻译)
       │                   │
       └─────→ 物理页 ←────┘

这意味着: - 设备可以理解 0x7F00_4000 这样的CPU虚拟地址 - CPU 和设备看到的内存布局完全一致 - 零拷贝环形缓冲区无需多次映射


二、协议基础:PCIe ATS 与 PASID

2.1 Address Translation Services (ATS)

ATS 是 PCIe 4.0 引入的协议扩展(已成为 PCIe 6.0 的强制要求),核心思想是让每个 PCIe 功能(Function)具备自己的地址翻译缓存(ATC, Address Translation Cache)。

Endpoint设备                 Root Complex / IOMMU
    │                              │
    │─── Address Translation ─────→│  (请求翻译)
    │     Request (with VA)        │
    │←── Address Translation ──────│
    │     Completion (with HPA)    │
    │                              │
    │  后续直接使用缓存的翻译结果     │   (ATC 命中,跳过 IOMMU)
    │─────────── Memory ──────────→│
    │         Read/Write           │

ATS 工作流程分为两个阶段:

  1. 翻译阶段:设备发起标签化的 Memory Read 请求(带翻译请求 TLP),IOMMU 翻译后返回 HPA 给设备缓存
  2. 执行阶段:设备使用缓存的 HPA 直接发起 DMA,无需 IOMMU 介入

ATC 一致性由 IOMMU 通过 INVALIDATE_REQUEST TLP 来维护:当内核修改页表时(进程退出、内存迁移),主动通知设备废弃缓存条目。

2.2 Process Address Space ID (PASID)

ATS 使用设备级地址空间(BDF 标识),但现代虚拟化场景需要同一设备内多进程独立的地址空间。PASID TLP 扩展(ECR 0x002F)在每个 TLP Header 中插入 20 位 PASID,允许同一 Function 承载最多 104 万个独立地址空间。

┌─────────────────────────────────────────────────┐
│                TLP Header (64-bit addressing)    │
├─────────────────────────────────────────────────┤
│  Fmt│Type│TC│A│... │PASID │Requester ID│Tag│...│
│     │    │  │  │     │(20b) │   (16b)    │(8b)│
└─────────────────────────────────────────────────┘
         ↑
    标识目标进程的地址空间

PASID 的关键语义是:IOMMU 页表查找 = PASID 选择地址空间 + 虚拟地址翻译,实现了类似 ASID(Address Space ID)的设备侧效果。

2.3 PRI (Page Request Interface)

SVA 使用 PRI(Page Request Interface)支持按需分页的 DMA,使设备可以在 ATC 未命中时触发 page fault 等待软件服务,而非直接报错:

设备 ATC miss (VA不存在)
    ↓
产生 PRI Request TLP
    ↓
IOMMU 提取 (PASID, VA) 通知内核
    ↓
内核分配物理页 → 更新 IOMMU 页表
    ↓
Device 重试 TLP → ATC miss → 翻译命中 → 完成 DMA

这是 GPU 按需内存分配的协议基础,CUDA Unified Memory 和 Intel OpenCL SVM 都依赖 PRI。


三、Linux 内核 SVA 架构

3.1 IOMMU Subsystem 重构

Linux 内核 5.8+ 引入了完整的 SVA 子系统,架构如下:

┌─────────────────────────────────────────────────────────┐
│                   用户空间驱动程序                        │
│          iommu_sva_bind_device() / unbind()             │
└──────────────────────┬──────────────────────────────────┘
                       │ syscall / VFIO uAPI
┌──────────────────────▼──────────────────────────────────┐
│                iommu/core - SVA Layer                    │
│  ┌─────────────────────────────────────────────────┐    │
│  │  iommu_sva_bind_device()                        │    │
│  │    → iommu_get_pasid()                          │    │
│  │    → mm_notifier_alloc()                        │    │
│  │    → domain->ops->sva_bind()                    │    │
│  │       → intel_iommu_sva_bind()                  │    │
│  │       → arm_smmu_sva_bind()                     │    │
│  └─────────────────────────────────────────────────┘    │
└──────────────────────┬──────────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────────┐
│               IOMMU Driver (硬件后端)                     │
│  ┌─────────────┐  ┌──────────────┐  ┌────────────────┐ │
│  │ Intel VT-d  │  │ ARM SMMU v3  │  │ AMD-Vi         │ │
│  │ (vtd.c)     │  │ (arm-smmu-   │  │ (amd/iommu.c) │ │
│  │             │  │  v3.c)       │  │                │ │
│  └─────────────┘  └──────────────┘  └────────────────┘ │
└─────────────────────────────────────────────────────────┘

3.2 核心数据结构

// include/linux/iommu.h
struct iommu_sva {
    struct device *dev;          // 绑定的 PCIe 设备
    struct mm_struct *mm;        // 绑定的进程地址空间
    struct iommu_domain *domain; // 共享的 IOMMU 域
    ioasid_t pasid;              // 分配的 PASID 值
    refcount_t users;
    struct list_head list;
};

struct iommu_domain {
    const struct iommu_ops *ops;
    unsigned long pgsize_bitmap;   // 支持的页大小 (4K/2M/1G/512G)
    enum iommu_domain_type type;   // UNMANAGED / DMA / IDENTITY / SVA
    void *handler;
    ...
};

关键洞察是 iommu_sva 将进程 mm_struct、PCIe 设备、IOMMU 域绑定为单一实体,实现了三者生命周期的一致性管理。

3.3 PASID 分配器

内核使用 XArray (pasid_xa) 管理全局 PASID 分配:

// drivers/iommu/iommu.c
static DEFINE_XARRAY_ALLOC(pasid_xa);
static DEFINE_MUTEX(pasid_mutex);

int iommu_pasid_alloc(struct device *dev, ioasid_t start, ioasid_t end)
{
    if (start > end || end >= pasid_max)
        return -EINVAL;
    mutex_lock(&pasid_mutex);
    ret = xa_alloc_cyclic(&pasid_xa, &pasid, entry,
                          XA_LIMIT(start, end), GFP_KERNEL);
    mutex_unlock(&pasid_mutex);
    return ret ? ret : pasid;
}

PASID 范围由 IOMMU 硬件决定,PCIe PRI 的 20 位宽度支持最多 524,288 个有效值(取决于 PASID_WIDTH capability)。


四、mmu_notifier 协同机制

SVA 最关键的技术挑战是内存一致性:当内核修改进程页表时(exit、exec、mremap、page migration),IOMMU 的 ATC 缓存必须同步失效。

4.1 双向通知链

// 内核通过 mmu_notifier 订阅进程地址空间事件
sva->mn.notifier_call = iommu_mmu_notifier;

int iommu_mmu_notifier(struct mmu_notifier *mn,
                        struct mm_struct *mm,
                        unsigned long start,
                        unsigned long end)
{
    // 清理 IOMMU 页表中该范围映射
    iommu_unmap(sva->domain, start, end - start);
    // 发送 ATC invalidation 给设备
    iommu_device_invalidate(sva->domain, start, end);
}

内核通过 mmu_notifier_register() 订阅事件,关键回调包括:

  • invalidate_range:页表映射修改(mremap、page migration)
  • release:进程退出,必须立即回收 PASID 并清理映射
  • clear_flush_young:页面访问位清除回收

4.2 IOMMU ATC Invalidation 流程

// drivers/iommu/intel/iommu.c - Intel VT-d 实现
static void intel_iommu_device_invalidate(struct iommu_domain *domain,
                                          unsigned long start,
                                          unsigned long end)
{
    struct dmar_domain *dmar_domain = to_dmar_domain(domain);
    struct intel_iommu *iommu;

    // 1. 遍历该 domain 绑定的所有 IOMMU 单元
    for_each_active_iommu(iommu, dmar_domain) {
        // 2. 构建 PASID-based Invalidation Descriptor
        qi_desc desc = {
            .hi = DMA_INV_DESC_PASID(dmar_domain->pasid),
            .lo = DMA_INV_DESC_ADDR(start) |
                  DMA_INV_DESC_SIZE(end - start)
        };
        // 3. 通过 Queued Invalidation 提交
        qi_submit(iommu, &desc, 1);
    }
    // 4. 等待 QI 完成
    qi_sync(iommu);
}

性能关键点:Intel VT-d 使用 Queued Invalidation (QI) 机制将 invalidation 操作异步化,通过内存环形队列提交,避免每次 spin lock 等待。


五、生产环境工程实践

5.1 设备驱动中的 SVA 绑定流程

以下是一个典型的 DSA(Data Streaming Accelerator)驱动使用 SVA 的代码示例:

/* drivers/dsa/engine.c */
#include <linux/iommu.h>

struct dsa_device {
    struct device *dev;
    struct iommu_sva *sva;
    int pasid;
};

static int dsa_sva_init(struct dsa_device *dsa)
{
    struct iommu_sva *sva;
    struct device *dev = dsa->dev;
    int ret;

    /* 1. 检查 IOMMU 子系统是否支持 SVA */
    if (!iommu_dev_feature_enabled(dev, IOMMU_DEV_FEAT_SVA))
        return -ENODEV;

    /* 2. 分配 PASID (范围: 1 ~ pasid_max) */
    dsa->pasid = iommu_aux_get_pasid(dev, NULL);
    if (dsa->pasid < 0)
        return dsa->pasid;

    /* 3. SVA 设备绑定 */
    sva = iommu_sva_bind_device(dev, current->mm);
    if (IS_ERR(sva)) {
        ret = PTR_ERR(sva);
        goto put_pasid;
    }
    dsa->sva = sva;
    dsa->pasid = sva->pasid;

    /* 4. 启用 PCIe PASID + ATS (需硬件支持) */
    pci_enable_pasid(dev, pci_pasid_features(dev) &
                     PCI_PASID_ENABLE);

    /* 5. 分配 DMA 共享缓冲区 */
    dsa->desc_ring = dma_alloc_coherent(dev, DESC_RING_SIZE,
                                        &dma_handle, GFP_KERNEL);

    return 0;

put_pasid:
    iommu_aux_free_pasid(dev, dsa->pasid);
    return ret;
}

5.2 中断隔离与 Posted Interrupts

SVA 场景下中断也需要通过 PASID 寻址。Intel VT-d 的 Posted Interrupt (PI) 机制将中断向量直接写入设备的 Posted Interrupt Request 描述符的虚拟地址:

// 设置 Posted Interrupt 描述符
struct pi_desc {
    u32 pir[8];          // 每 bit 对应一个中断向量
    union {
        struct { u32 ...; }; // 非虚拟posted模式
        struct {
            u32 ndst;        // 目标 CPU
            u16 nv;          // 通知向量
            u16 apic_id;
            u32 pid_pasid;   // PASID 字段(SVA场景)
        } virt;
    };
};

设备触发中断时使用 ITS (Interrupt Translation Service) 将 MSI 翻译为 PASID-tagged 的 posted中断,实现隔离到进程级别的 MSI 投递。

5.3 vfio-mdev 与 vGPU 场景的 SVA

SVA 是 vGPU 和弹性设备共享(MDEV)的技术基石:

┌─────────────────────────────────────┐
│       用户空间 QEMU/KVM              │
│                                     │
│  VFIO_DEVICE_BIND_IOMMU_SVA         │
│        ↓                            │
│  ioctl(vfio_dev, SVA_BIND, &args)   │
│        ↓                            │
│  分配 PASID → 建立进程页表 → 启用   │
└────────────────┬────────────────────┘
                 │
    ┌────────────▼────────────┐
    │  vGPU/MDev 虚拟设备      │
    │  使用 Guest 页表翻译 GVA  │
    │  (GVA → GPA → HPA 直通)  │
    └─────────────────────────┘

vGPU 场景重点在于将 Guest 内存管理直接暴露给设备,Guest 页表变更通过 vfio-mdev 的 pasid 管理接口同步更新 IOMMU。


六、性能分析与调优

6.1 ATC 命中率优化

ATC 是小型缓存(通常 64~128 条目),关键优化方向是减少工作集热点区的缓存抖动:

# 查看 IOMMU ATC 统计 (内核 >= 5.12, Intel VT-d)
cat /sys/bus/pci/devices/0000:00:02.0/sva_stats
# PASID alloc:       482
# ATC hit:         982341
# ATC miss:          1765
# ATC hit rate:    99.82%
# Invalidation:      4128

无效化(Invalidation)是 ATC 的主要开销来源。内核提供两种 invalidation 模式:

模式 特点 适用场景
全局 invalidation 简单、快速但清空全部 单进程独占设备
PASID invalidation 精确到地址范围 多进程共享设备

生产环境推荐使用 PASID invalidation,配合大页减少 invalidation 次数。

6.2 页表大页对齐

SVA 支持与 CPU 页表相同的大页(2MB/1G),但要求 IOMMU 硬件支持 PML4/PDPE 的大页映射:

// 检查 IOMMU 支持的页大小
iommu_domain_get_attr(domain, DOMAIN_ATTR_PGSIZES, &pgsize_bitmap);

// 分配大页 DMA 缓冲区
buf = (void *)mmap(NULL, 2 * 1024 * 1024,
                   PROT_READ | PROT_WRITE,
                   MAP_HUGETLB | MAP_HUGE_2MB |
                   MAP_SHARED | MAP_POPULATE,
                   fd, 0);

使用 2MB 大页时,相同容量的 IOMMU 映射条目数减少 512 倍,ATC 命中率可提升 15%~30%。

6.3 PRI 页错误处理优化

高 PRI 页错误率(>1000/sec)会导致严重的系统抖动。优化策略:

  1. 页预分配:进程启动时通过 madvise(MADV_HUGEPAGE | MADV_SEQUENTIAL) 预分配 DMA 缓冲区
  2. 异步 invalidation:内核线程批量处理 ATC invalidation,减少同步等待延迟
  3. PASID 优先回收:长时间空闲的 PASID 优先分配给新进程,配额用尽时触发 LRU 回收

七、可观测性与调试

7.1 Ftrace 动态追踪

# 追踪 IOMMU SVA 相关事件
echo 1 > /sys/kernel/debug/tracing/events/iommu/enable
echo 1 > /sys/kernel/debug/tracing/events/sva/enable

# 追踪 PASID 分配/释放
cat /sys/kernel/debug/tracing/trace_pipe | grep -E "sva_bind|sva_unbind|pasid_alloc"

7.2 IOMMU DebugFS

# 查看已绑定的 SVA 设备列表
cat /sys/kernel/debug/iommu/sva_devices
# pasid 4789 dev=0000:b1:00.1 pid=2843 cmdline=./gpu_compute_app

# 查看 PASID 地址空间范围
cat /sys/kernel/debug/iommu/pasids/4789/ranges
# [0x7f0000000000-0x7f0000800000) rn=1 pgsize=2M
# [0x7f0000800000-0x7f0001000000) rn=3 pgsize=4K

八、故障排查清单

现象 可能原因 排查命令
sva_bind 返回 -ENODEV 设备不支持 PASID lspci -vvv \| grep -i pasid
sva_bind 返回 -ENOMEM PASID 资源耗尽 cat /sys/kernel/debug/iommu/pasid_stats
DMA 超时/ATC 错误 invalidation 未送达 dmesg \| grep -i "DMAR.*PASID"
页表共享后系统崩溃 mm 引用计数错误 crash> files -p <task_struct>
性能下降远超基线 ATC thrashing perf stat -e iommu/ATC_MISS/

九、总结与展望

IOMMU SVA 的本质是将设备纳入进程内存管理的一等公民,其技术深度远超简单的地址映射。从协议层 ATS/PASID/PRI 到内核 mmu_notifier 协同,再到生产环境中的大页优化和 cold/hot path 调优,每一个环节都涉及软硬件协同的精细设计。

当前发展方向包括:

  • SPDM(Security Protocol and Data Model)认证:SVA 绑定的设备需要 SPDM 握手防止 PASID spoofing 攻击
  • CXL.cache 场景下的 SVA:通过 CXL.mem 共享内存时 SVA 仍保证一致性
  • RISC-V IOMMU SBA 扩展:RISC-V 的 I标准正在定义 SVA 类扩展,2024 年 Q1 Ratified

工程经验法则:SVA 不是万能的,它最适合高性能计算、GPU 计算、NVMe 原生 I/O 等需要设备理解进程地址空间的场景。对于传统的中低速网卡驱动,VFIO 标准映射模式仍然更简单可靠。评估时衡量标准是:ATS 命中率是否 >99%、invalidation 频率是否 <1k/sec、PRI 页错误率是否 <10/sec 这三个数字。


文章涵盖 IOMMU SVA 协议基础、Linux 内核实现架构、mmu_notifier 协同机制、PASID 管理、生产性能调优和故障排查,约 2800 字

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部