跳转至

第4篇:设备直通 — VFIO、IOMMU 与中断重映射

源码:virt/kvm/vfio.c (387行) | drivers/vfio/vfio_main.c (1856行) | vmx/posted_intr.c (319行) | svm/avic.c (1321行)

系列目录:KVM 内核源码深度解析


一、设备直通总体架构

设备直通让虚拟机直接访问物理 PCIe 设备,需要三大组件协同:

    Guest VM
   ┌─────────┐
   │ Driver  │ → DMA (GPA) ──┐     MSI/MSI-X ──┐
   └─────────┘               │                 │
                              ▼                 ▼
   ┌──────────┐  ┌──────────────┐  ┌────────────────┐
   │  VFIO    │  │  IOMMU DMA   │  │ 中断重映射       │
   │ 设备驱动  │  │ GPA → HPA    │  │ MSI → vCPU     │
   └────┬─────┘  └──────┬───────┘  └───────┬────────┘
        └───────────────┼──────────────────┘
              ┌──────────────────┐
              │  Physical Device │
              └──────────────────┘

二、VFIO 核心架构

2.1 VFIO 与 KVM 的模块化绑定

VFIO 使用 symbol_get 机制避免与 KVM 的编译时强依赖,允许两者作为独立模块加载:

// vfio_main.c:445-475
void vfio_device_get_kvm_safe(struct vfio_device *device, struct kvm *kvm)
{
    void (*pfn)(struct kvm *kvm);
    bool (*fn)(struct kvm *kvm);
    bool ret;

    lockdep_assert_held(&device->dev_set->lock);
    if (!kvm)
        return;

    pfn = symbol_get(kvm_put_kvm);          // 动态获取释放函数
    fn = symbol_get(kvm_get_kvm_safe);      // 动态获取获取函数
    if (WARN_ON(!pfn)) return;
    ret = fn(kvm);
    symbol_put(kvm_get_kvm_safe);
    if (!ret) { symbol_put(kvm_put_kvm); return; }

    device->put_kvm = pfn;
    device->kvm = kvm;
}
// vfio_main.c:477-493
void vfio_device_put_kvm(struct vfio_device *device)
{
    lockdep_assert_held(&device->dev_set->lock);
    if (!device->kvm) return;
    device->put_kvm(device->kvm);
    device->put_kvm = NULL;
    symbol_put(kvm_put_kvm);
    device->kvm = NULL;
}

2.2 文件级别 KVM 绑定

在 cdev 路径中,KVM 先绑定到文件,设备 open 时再传播到 vfio_device::kvm

// vfio_main.c:1516-1528
static void vfio_device_file_set_kvm(struct file *file, struct kvm *kvm)
{
    struct vfio_device_file *df = file->private_data;
    spin_lock(&df->kvm_ref_lock);
    df->kvm = kvm;
    spin_unlock(&df->kvm_ref_lock);
}

公共入口同时处理 group 和 cdev 两种路径:

// vfio_main.c:1538-1549
void vfio_file_set_kvm(struct file *file, struct kvm *kvm)
{
    struct vfio_group *group;
    group = vfio_group_from_file(file);
    if (group) vfio_group_set_kvm(group, kvm);
    if (vfio_device_from_file(file))
        vfio_device_file_set_kvm(file, kvm);
}
EXPORT_SYMBOL_GPL(vfio_file_set_kvm);

三、KVM-VFIO 桥接层

virt/kvm/vfio.c (387行) 在 KVM 框架内注册一个伪设备类型。

3.1 核心数据结构

// virt/kvm/vfio.c:24-36
struct kvm_vfio_file {
    struct list_head node;
    struct file *file;
};

struct kvm_vfio {
    struct list_head file_list;
    struct mutex lock;
    bool noncoherent;
};

3.2 Device Ops 与创建

// virt/kvm/vfio.c:347-377
static const struct kvm_device_ops kvm_vfio_ops = {
    .name = "kvm-vfio",
    .create = kvm_vfio_create,
    .release = kvm_vfio_release,
    .set_attr = kvm_vfio_set_attr,
    .has_attr = kvm_vfio_has_attr,
};

static int kvm_vfio_create(struct kvm_device *dev, u32 type)
{
    lockdep_assert_held(&dev->kvm->lock);
    /* Only one VFIO "device" per VM */
    list_for_each_entry(tmp, &dev->kvm->devices, vm_node)
        if (tmp->ops == &kvm_vfio_ops)
            return -EBUSY;
    kv = kzalloc_obj(*kv, GFP_KERNEL_ACCOUNT);
    INIT_LIST_HEAD(&kv->file_list);
    mutex_init(&kv->lock);
    dev->private = kv;
    return 0;
}

3.3 文件管理与缓存一致性

kvm_vfio_file_add (行143-186) 将 VFIO 文件加入 VM 的文件列表,调用 kvm_vfio_file_set_kvm 通过 symbol_get 跨模块绑定:

// virt/kvm/vfio.c:38-49
static void kvm_vfio_file_set_kvm(struct file *file, struct kvm *kvm)
{
    void (*fn)(struct file *file, struct kvm *kvm);
    fn = symbol_get(vfio_file_set_kvm);
    if (!fn) return;
    fn(file, kvm);
    symbol_put(vfio_file_set_kvm);
}

kvm_vfio_update_coherency (行120-141) 遍历所有设备检查是否需要 noncoherent DMA,按需调用 kvm_arch_register_noncoherent_dma

3.4 IOCTL 接口

用户态通过 KVM_SET_DEVICE_ATTR 操作:

// virt/kvm/vfio.c:266-290
static int kvm_vfio_set_file(struct kvm_device *dev, long attr,
                             void __user *arg)
{
    switch (attr) {
    case KVM_DEV_VFIO_FILE_ADD:
        return kvm_vfio_file_add(dev, fd);
    case KVM_DEV_VFIO_FILE_DEL:
        return kvm_vfio_file_del(dev, fd);
    }
    return -ENXIO;
}

设备释放时 (kvm_vfio_release, 行324-343) 遍历 file_list 逐一解除 KVM 绑定、释放引用。


四、IOMMU DMA 重映射

4.1 两层翻译路径

  Device DMA Request
  ┌──────────────┐
  │  IOVA        │  ← 设备"看到"的地址(由 VFIO IOMMU 域管理)
  └──────┬───────┘
         │ IOMMU 页表(iommufd/type1 维护)
  ┌──────────────┐
  │  GPA         │  ← Guest 物理地址
  └──────┬───────┘
         │ EPT/NPT 页表(KVM MMU 维护)
  ┌──────────────┐
  │  HPA         │  ← 最终物理地址
  └──────────────┘

4.2 现代路径:iommufd

VFIO 现推荐使用 iommufd 而非 legacy type1 IOMMU 接口。iommufd 支持: - IOAS (I/O Address Space) 多设备共享 - 硬件嵌套分页(guest-managed IOMMU 页表) - 统一 hardware-agnostic 接口

VFIO 设备打开时自动绑定 iommufd,通过 iommufd_access_create 建立访问通道,随后调用设备驱动的 attach_ioas 建立映射。

4.3 内存变更同步

KVM memslot 变更时,mmu_notifier 链通知 IOMMU 层同步映射。关键路径: - KVM MMU notifier → VFIO IOMMU notifier - 新内存自动建立 IOVA→HPA 映射 - Balloon inflate / memory hot-unplug 时解除映射


五、中断重映射:Intel Posted Interrupts

5.1 核心理念

Posted Interrupts 允许设备中断通过硬件直接投递到 vCPU 的 Posted Interrupt Descriptor (PID),无需 VM-exit。

PID 是 64 字节对齐的内存结构,地址写入 VMCS 的 posted-interrupt descriptor address 字段:

  Offset  Size  Field
  ──────  ────  ────────────────────────────────
  0       32    PIR  (Posted Interrupt Requests) — 256-bit 位图
  32      2     ON   (Outstanding Notification)   — bit 256
  34      2     SN   (Suppress Notification)      — bit 257
  36      2     Reserved
  38      2     NV   (Notification Vector 0-15)
  40      4     NDST (Notification Destination)

5.2 PI Load 流程

// posted_intr.c:57-145
void vmx_vcpu_pi_load(struct kvm_vcpu *vcpu, int cpu)
{
    struct pi_desc *pi_desc = vcpu_to_pi_desc(vcpu);
    struct pi_desc old, new;
    unsigned int dest;

    if (!enable_apicv || !lapic_in_kernel(vcpu))
        return;

    // 快速路径:vCPU 未迁移且未从 wakeup 恢复
    if (pi_desc->nv != POSTED_INTR_WAKEUP_VECTOR && vcpu->cpu == cpu) {
        if (pi_test_and_clear_sn(pi_desc))
            goto after_clear_sn;
        return;
    }

    // 慢速路径:处理迁移、从 wakeup list 移除
    if (pi_desc->nv == POSTED_INTR_WAKEUP_VECTOR) {
        raw_spin_lock(spinlock);
        list_del(&vt->pi_wakeup_list);
        raw_spin_unlock(spinlock);
    }

    dest = cpu_physical_id(cpu);
    // 原子更新 PID:设置 NDST、清除 SN、恢复 NV
    do {
        new.control = old.control;
        new.ndst = dest;
        __pi_clear_sn(&new);
        new.nv = POSTED_INTR_VECTOR;
    } while (pi_try_set_control(pi_desc, &old.control, new.control));

after_clear_sn:
    smp_mb__after_atomic();
    if (!pi_is_pir_empty(pi_desc))
        pi_set_on(pi_desc);
}

5.3 Wakeup Handler

当 vCPU 阻塞 (HLT) 时,KVM 将其加入 per-CPU wakeup 列表,PID 的 NV 改为 POSTED_INTR_WAKEUP_VECTORpi_enable_wakeup_handler, 行162-199)。设备中断到来时触发 IPI 唤醒 vCPU。


六、中断重映射:AMD AVIC

6.1 GATag 编码

AVIC 使用 32 位 GATag (Guest Address Tag) 编码 VM ID 和 vCPU index:

// avic.c:47-66
#define AVIC_VCPU_IDX_MASK    AVIC_PHYSICAL_MAX_INDEX_MASK
#define AVIC_VM_ID_SHIFT      HWEIGHT32(AVIC_PHYSICAL_MAX_INDEX_MASK)
#define AVIC_VM_ID_MASK       (GENMASK(31, AVIC_VM_ID_SHIFT) >> AVIC_VM_ID_SHIFT)

#define AVIC_GATAG_TO_VMID(x)     ((x >> AVIC_VM_ID_SHIFT) & AVIC_VM_ID_MASK)
#define AVIC_GATAG_TO_VCPUIDX(x)  (x & AVIC_VCPU_IDX_MASK)

#define __AVIC_GATAG(vm_id, vcpu_idx) \
    ((((vm_id) & AVIC_VM_ID_MASK) << AVIC_VM_ID_SHIFT) | \
     ((vcpu_idx) & AVIC_VCPU_IDX_MASK))

6.2 硬件表

AVIC 依赖两块硬件表:

表名 描述
Physical APIC ID Table 每个物理 APIC ID → (VMCB addr, GATag)
Logical APIC ID Table 逻辑 APIC ID → 物理 APIC ID,8 cluster × 16 APIC

6.3 x2AVIC

Zen 4+ 支持 x2AVIC (x2APIC 模式的硬件虚拟化)。KVM 控制哪些 MSR 被截获:

// avic.c:122-170
static const u32 x2avic_passthrough_msrs[] = {
    X2APIC_MSR(APIC_ID), X2APIC_MSR(APIC_LVR),
    X2APIC_MSR(APIC_TASKPRI), X2APIC_MSR(APIC_EOI),
    X2APIC_MSR(APIC_ISR), X2APIC_MSR(APIC_TMR),
    X2APIC_MSR(APIC_IRR), X2APIC_MSR(APIC_ICR),
    /* 注意!LVTT 始终截获,TSC-deadline 不被硬件虚拟化 */
    X2APIC_MSR(APIC_LVTTHMR),
    // ...
};

6.4 GALog 与 VM Hash Table

AVIC 无法直接投递时,硬件写入 Guest Activity Log (GALog)。KVM 维护 VM ID → kvm_svm 的哈希表来查找目标 VM:

// avic.c:114-118
#define SVM_VM_DATA_HASH_BITS  8
static DEFINE_HASHTABLE(svm_vm_data_hash, SVM_VM_DATA_HASH_BITS);
static u32 next_vm_id = 0;

6.5 IPI 虚拟化

AMD IPIv 允许 vCPU 间 IPI 通过硬件直接投递(enable_ipiv 参数,avic.c:104)。


七、Posted Interrupts vs AVIC

Intel Posted Interrupts:
  Device MSI → VT-d IRTE → PID.PIR → vCPU
    • vCPU 运行中:硬件写 PIR,零 VM-Exit
    • vCPU 阻塞:NV=WAKEUP → IPI → KVM 处理
    • 数据结构:64B PID + VMCS 字段

AMD AVIC:
  Device MSI → AMD IOMMU DevTable → Physical APIC ID Table → vCPU
    • 查找物理 APIC ID Table → VMCB + GATag
    • 成功:直接投递到 guest vAPIC,零 VM-Exit
    • 失败:写入 GALog → KVM 事后处理
    • 数据结构:Physical/Logical APIC ID 表 + VMCB

八、设备分配完整流程

QEMU/KVM Userspace
  ├─ 1. open("/dev/vfio/devices/vfioX")  → VFIO cdev
  │     └─ vfio_df_device_first_open() → iommufd 绑定
  ├─ 2. ioctl(VFIO_DEVICE_ATTACH_IOMMUFD_PT)
  │     └─ 建立 IOMMU 映射 (遍历 KVM memslots)
  ├─ 3. KVM_CREATE_DEVICE(KVM_DEV_TYPE_VFIO)
  │     └─ kvm_vfio_create()  (virt/kvm/vfio.c:355)
  ├─ 4. KVM_SET_DEVICE_ATTR(KVM_DEV_VFIO_FILE_ADD, fd)
  │     └─ kvm_vfio_file_add()  (virt/kvm/vfio.c:143)
  │        ├─ fget(fd) → 验证 VFIO 文件
  │        ├─ kvm_vfio_file_set_kvm() → 跨模块绑定 KVM
  │        └─ kvm_vfio_update_coherency()
  ├─ 5. 配置 IRQ bypass
  │     └─ kvm_irqfd_assign() → irq_bypass_register_producer()
  │        └─ 桥接 VFIO IRQ → KVM posted interrupt
  └─ 6. 运行时
        ├─ KVM mmu_notifier → IOMMU 映射同步
        └─ Memory hotplug → 增量映射

九、完整架构图

╔═══════════════════════ Guest VM ═══════════════════════╗
║  Guest Driver → DMA (GPA)          MSI/MSI-X → vCPU   ║
╚══════════════╤══════════════════════════╤═══════════════╝
               │                          │
  ╔════════════╪══════ Hypervisor ════════╪══════════════╗
  ║            ▼                          ▼              ║
  ║  ┌──────────────┐           ┌──────────────────┐    ║
  ║  │ VFIO-KVM桥   │           │ 中断重映射         │    ║
  ║  │ virt/kvm/    │           │  posted_intr.c    │    ║
  ║  │ vfio.c:379   │           │  avic.c           │    ║
  ║  └──────┬───────┘           └────────┬─────────┘    ║
  ║         ▼                            ▼              ║
  ║  ┌──────────────┐           ┌──────────────────┐    ║
  ║  │ VFIO Core    │           │ IOMMU Driver      │    ║
  ║  │ vfio_main.c  │◄─────────>│ VT-d / AMD IOMMU │    ║
  ║  │ • device ops │  iommufd  │ • DMA 重映射      │    ║
  ║  │ • KVM引用    │           │ • 中断重映射      │    ║
  ║  └──────────────┘           └────────┬─────────┘    ║
  ╚══════════════════════════════════════╪═══════════════╝
                                ┌────────┴────────┐
                                │  Physical Device │
                                │  (NVMe/GPU/NIC)  │
                                └─────────────────┘

中断路径 (Posted Interrupt):
  Device MSI → VT-d IRTE → PID.PIR
    ├─ vCPU running → 硬件直接投递 (0 VM-Exit)
    └─ vCPU blocked → WAKEUP_VECTOR IPI → KVM 唤醒

DMA 路径:
  Device DMA (IOVA) → IOMMU 页表 → GPA
    ├─ 已映射  → 直接翻译为 HPA
    └─ 未映射  → IOMMU fault → mmu_notifier → 动态建立

十、关键文件索引

文件 行数 描述
virt/kvm/vfio.c 387 KVM-VFIO 桥接层:伪设备注册、文件管理、缓存一致性
drivers/vfio/vfio_main.c 1856 VFIO 核心:设备注册/注销、KVM 引用管理
drivers/vfio/device_cdev.c ~ VFIO cdev 路径:设备 open/close、iommufd 绑定
arch/x86/kvm/vmx/posted_intr.c 319 Intel Posted Interrupts:PID 管理、wakeup handler
arch/x86/kvm/svm/avic.c 1321 AMD AVIC:GATag 编码、APIC 表、x2AVIC、GALog

下一篇文章

第5篇: 半虚拟化 — pvclock、kvmclock、steal time 与 Hyper-V Enlightenment


💬 评论