Skip to content

Latest commit

 

History

History
2262 lines (1781 loc) · 82.9 KB

File metadata and controls

2262 lines (1781 loc) · 82.9 KB

Linux virtio 与半虚拟化深度解析

基于 Linux Kernel 源码深度分析(kernel 6.x / master 分支) 关键路径:drivers/virtio/ | drivers/net/virtio_net.c | drivers/block/virtio_blk.c drivers/vhost/ | arch/x86/include/asm/paravirt_types.h


目录

  1. virtio 规范概述
  2. split virtqueue 深度解析
  3. packed virtqueue 深度解析
  4. virtio-net 驱动深度分析
  5. virtio-blk 驱动分析
  6. vhost 内核后端
  7. paravirt_ops 半虚拟化接口
  8. virtio-mem 与 virtio-balloon
  9. 性能优化与调优
  10. 总结与对比

1. virtio 规范概述

1.1 半虚拟化的历史背景

虚拟化技术经历了三个关键演化阶段:

全虚拟化(Full Virtualization):Hypervisor 对 Guest 完整模拟真实硬件,Guest OS 无需任何修改。以 QEMU/TCG 模式为代表,每条特权指令都需要 trap-and-emulate,I/O 路径尤其低效——每次磁盘写入都要经过模拟的 IDE/AHCI 控制器,引发大量 VM-exit。

硬件辅助虚拟化(Hardware-Assisted Virtualization):Intel VT-x / AMD-V 提供硬件陷阱机制,显著降低上下文切换开销,但 I/O 仍是瓶颈。

半虚拟化(Paravirtualization,PV):Guest OS 知晓自身运行于虚拟环境,通过标准化接口(hypercall / 共享内存)直接与 Hypervisor 通信,绕过低效模拟路径。Xen 是最早的成功实践,但其接口与特定 Hypervisor 绑定。

virtio 由 Rusty Russell(IBM)于 2007 年提出,核心思想是:定义一套与 Hypervisor 无关的标准 I/O 接口,让 Guest 驱动和 Host 后端可以独立演化。

virtio 版本演化:

v0.9.x (2007-2011) ──── split ring,legacy 接口,大端序支持
    │
v1.0  (2016, OASIS)──── 现代接口(modern),字节序固定小端,BAR4
    │                    废弃 legacy 字段,device/driver status 重定义
    │
v1.1  (2019) ──────────── packed ring 引入,减少缓存行竞争
    │                    VIRTIO_F_RING_PACKED,单描述符环
    │
v1.2  (2022) ──────────── Admin virtqueue,共享内存(SHMEM)
                         群组管理,P2P DMA 支持

1.2 virtio 规范三层架构

+--------------------------------------------------+
|              virtio 规范层次结构                  |
+--------------------------------------------------+
|  Transport Layer(传输层)                        |
|  ┌──────────┐ ┌──────────┐ ┌──────────────────┐ |
|  │ virtio-  │ │ virtio-  │ │   virtio-CCW     │ |
|  │   PCI    │ │  MMIO    │ │  (IBM S/390)     │ |
|  └──────────┘ └──────────┘ └──────────────────┘ |
+--------------------------------------------------+
|  Core Layer(核心层)                             |
|  virtqueue / vring / feature negotiation         |
+--------------------------------------------------+
|  Device Layer(设备层)                           |
|  net / blk / scsi / gpu / mem / balloon / ...   |
+--------------------------------------------------+

PCI 传输层

virtio-PCI 设备通过标准 PCI 枚举被发现。现代设备(virtio 1.0+)使用 PCI Capability 结构暴露 5 个 BAR 区域:

PCI Config Space:
  Vendor ID: 0x1AF4 (Red Hat / virtio)
  Device ID: 0x1000 (net), 0x1001 (blk), 0x1050 (gpu), ...

Capabilities:
  VIRTIO_PCI_CAP_COMMON_CFG  (BAR 0) - 公共配置:设备状态、队列选择
  VIRTIO_PCI_CAP_NOTIFY_CFG  (BAR 2) - 通知区域(kick)
  VIRTIO_PCI_CAP_ISR_CFG     (BAR 3) - 中断状态寄存器
  VIRTIO_PCI_CAP_DEVICE_CFG  (BAR 4) - 设备特定配置
  VIRTIO_PCI_CAP_PCI_CFG     (-)     - 访问上述配置的 PCI 方式

相关驱动:drivers/virtio/virtio_pci_modern.cdrivers/virtio/virtio_pci_legacy.c

MMIO 传输层

用于嵌入式环境(ARM 等),设备通过设备树(DTS)或 ACPI 枚举,寄存器直接映射到内存地址。相关驱动:drivers/virtio/virtio_mmio.c

MMIO 设备的 Magic 值为 0x74726976(ASCII "virt"),驱动通过读取此值识别设备:

MMIO Register Map:
  0x000: MagicValue    = 0x74726976
  0x004: Version       = 2 (modern) 或 1 (legacy)
  0x008: DeviceID
  0x00C: VendorID
  0x010: DeviceFeatures
  0x020: DriverFeatures
  0x030: QueueSel
  0x038: QueueNumMax
  ...

CCW 传输层(IBM S/390)

专为 IBM Z 系列大型机设计,使用 Channel Command Word 机制,相关驱动:drivers/s390/virtio/virtio_ccw.c

1.3 Feature 协商机制

Feature 协商是 virtio 向前/向后兼容的核心机制。设备和驱动各自声明支持的特性位(feature bits),双方取交集作为最终协商结果。

Feature 协商流程:

  Device                          Driver
    │                               │
    │── DeviceFeatures (位图) ──────>│
    │                               │  读取设备支持的特性
    │<── DriverFeatures(子集)──────│
    │                               │  驱动写入自己支持的特性
    │   FEATURES_OK 状态位           │
    │<── Status |= FEATURES_OK ─────│
    │                               │  设备验证特性集合
    │── 重读 FEATURES_OK ───────────>│
    │                               │  确认设备接受

关键 Feature bits(include/uapi/linux/virtio_config.h):

Feature Bit 含义
VIRTIO_F_NOTIFY_ON_EMPTY 24 队列空时通知
VIRTIO_F_ANY_LAYOUT 27 任意 sg 分割
VIRTIO_RING_F_INDIRECT_DESC 28 间接描述符表
VIRTIO_RING_F_EVENT_IDX 29 事件索引(精细中断控制)
VIRTIO_F_VERSION_1 32 现代接口(必需)
VIRTIO_F_ACCESS_PLATFORM 33 需要 IOMMU 隔离
VIRTIO_F_RING_PACKED 34 packed ring 格式
VIRTIO_F_IN_ORDER 35 按序完成
VIRTIO_F_ORDER_PLATFORM 36 平台排序保证
VIRTIO_F_SR_IOV 37 SR-IOV 支持

设备特定 feature bits(以 virtio-net 为例):

Feature Bit 含义
VIRTIO_NET_F_CSUM Host 端校验和卸载
VIRTIO_NET_F_GUEST_CSUM Guest 端校验和卸载
VIRTIO_NET_F_CTRL_VQ 控制虚拟队列
VIRTIO_NET_F_MQ 多队列支持
VIRTIO_NET_F_RSC_EXT 接收端 RSC
VIRTIO_NET_F_HASH_REPORT RSS hash 上报
VIRTIO_NET_F_GUEST_USO4 UDP SO 卸载 (IPv4)

1.4 设备初始化状态机

virtio 规范定义了设备初始化必须遵循的状态机,对应 Status 寄存器的位:

RESET (0x00)
    │
    ▼
ACKNOWLEDGE (0x01) ── Guest OS 发现设备
    │
    ▼
DRIVER (0x02) ────── 驱动知道如何驱动该设备
    │
    ▼
FEATURES_OK (0x08) ─ 特性协商完成
    │
    ▼
DRIVER_OK (0x04) ─── 驱动就绪,设备可工作
    │
    ▼ (发生错误时)
FAILED (0x80) ─────── 不可恢复错误,需要重置

2. split virtqueue 深度解析

2.1 三环数据结构

split virtqueue(分裂环)是 virtio 最经典的队列格式,由三个并列数组组成,定义在 include/uapi/linux/virtio_ring.h

// include/uapi/linux/virtio_ring.h:104
struct vring_desc {
    __virtio64 addr;    // 缓冲区物理地址(Guest-physical)
    __virtio32 len;     // 缓冲区长度
    __virtio16 flags;   // 描述符标志
    __virtio16 next;    // 链中下一个描述符的索引
};

// include/uapi/linux/virtio_ring.h:111
struct vring_avail {
    __virtio16 flags;   // VRING_AVAIL_F_NO_INTERRUPT
    __virtio16 idx;     // 下一个可用槽位索引(单调递增)
    __virtio16 ring[];  // 描述符链头部索引的环形数组
};

// include/uapi/linux/virtio_ring.h:128
struct vring_used {
    __virtio16 flags;      // VRING_USED_F_NO_NOTIFY
    __virtio16 idx;        // 下一个已用槽位索引(单调递增)
    vring_used_elem_t ring[]; // 已完成描述符链信息
};

描述符标志位(include/uapi/linux/virtio_ring.h:38):

#define VRING_DESC_F_NEXT     1   // 通过 next 字段继续链
#define VRING_DESC_F_WRITE    2   // 写缓冲区(否则为只读)
#define VRING_DESC_F_INDIRECT 4   // 间接描述符表

三环在内存中的物理布局(连续内存块,include/uapi/linux/virtio_ring.h:167):

内存地址低 ────────────────────────────────────── 内存地址高
┌──────────────────┬──────────────────────────────────┬───────┐
│  desc[0..N-1]    │  avail_flags│avail_idx│ring[0..N]│ pad   │
│  (16B × N)       │  (2B)      │ (2B)    │(2B × N+1)│(align)│
├──────────────────┴──────────────────────────────────┼───────┤
│  used_flags│used_idx│ring[0..N-1]│avail_event        │       │
│  (2B)      │(2B)    │(8B × N)   │(2B)               │       │
└──────────────────────────────────────────────────────┘

对齐要求(include/uapi/linux/virtio_ring.h:89):
  VRING_AVAIL_ALIGN_SIZE = 2
  VRING_USED_ALIGN_SIZE  = 4
  VRING_DESC_ALIGN_SIZE  = 16

总大小计算(include/uapi/linux/virtio_ring.h:206):

static inline unsigned vring_size(unsigned int num, unsigned long align)
{
    return ((sizeof(struct vring_desc) * num
             + sizeof(__virtio16) * (3 + num)
             + align - 1) & ~(align - 1))
           + sizeof(__virtio16) * 3
           + sizeof(struct vring_used_elem) * num;
}

2.2 内核内部结构

内核在 UAPI 结构之上维护了内部管理结构(drivers/virtio/virtio_ring.c:106):

// drivers/virtio/virtio_ring.c:106
struct vring_virtqueue_split {
    struct vring vring;               // 实际内存布局(三个环)

    u16 avail_flags_shadow;           // avail->flags 的影子寄存器
    u16 avail_idx_shadow;             // avail->idx 的影子(防重复写)

    struct vring_desc_state_split *desc_state;  // 每描述符状态(callback data)
    struct vring_desc_extra *desc_extra;         // 额外元数据(DMA addr/len/flags)

    dma_addr_t queue_dma_addr;        // 队列 DMA 地址
    size_t queue_size_in_bytes;

    u32 vring_align;
    bool may_reduce_num;
};

// drivers/virtio/virtio_ring.c:77
struct vring_desc_state_split {
    void *data;                        // 回调数据(由驱动提交的 token)
    struct vring_desc *indir_desc;     // 间接描述符表指针
    u32 total_in_len;                  // 跳过缓冲区的入方向总长
};

// drivers/virtio/virtio_ring.c:99
struct vring_desc_extra {
    dma_addr_t addr;    // 描述符 DMA 地址
    u32 len;            // 描述符长度
    u16 flags;          // 描述符标志
    u16 next;           // 空闲链表中的下一个
};

顶层 vring_virtqueue 结构(drivers/virtio/virtio_ring.c:192):

// drivers/virtio/virtio_ring.c:192
struct vring_virtqueue {
    struct virtqueue vq;          // 公开的 virtqueue 接口

    bool use_map_api;             // 是否使用 DMA API
    bool weak_barriers;           // 是否允许弱内存屏障
    bool broken;                  // 队列已损坏
    bool indirect;                // 支持间接描述符
    bool event;                   // 支持 EVENT_IDX

    enum vq_layout layout;        // VQ_LAYOUT_SPLIT / PACKED / *_IN_ORDER

    unsigned int free_head;       // 空闲描述符链头(split 模式)

    unsigned int num_added;       // 上次 kick 后新增的缓冲区数
    u16 last_used_idx;            // 最后处理的 used 索引

    bool event_triggered;         // EVENT_IDX 触发标志

    union {
        struct vring_virtqueue_split split;
        struct vring_virtqueue_packed packed;
    };

    bool (*notify)(struct virtqueue *vq);  // kick 实现(写 notify 寄存器)
    bool we_own_ring;
    union virtio_map map;
};

VQ layout 枚举(drivers/virtio/virtio_ring.c:70):

// drivers/virtio/virtio_ring.c:70
enum vq_layout {
    VQ_LAYOUT_SPLIT          = 0,
    VQ_LAYOUT_PACKED,
    VQ_LAYOUT_SPLIT_IN_ORDER,
    VQ_LAYOUT_PACKED_IN_ORDER,
};

2.3 描述符链(scatter-gather)

单个 I/O 请求通常由多个物理不连续的内存片段组成,virtio 用描述符链(descriptor chain)表达这种 scatter-gather 结构:

描述符链示例(一个 virtio-blk 写请求):

desc[0]: addr=<out_hdr>,  len=16,  flags=NEXT          (请求头,只读)
    │
    └──> desc[1]: addr=<data_sg1>, len=4096, flags=NEXT (数据段1,只读)
              │
              └──> desc[2]: addr=<data_sg2>, len=2048, flags=NEXT (数据段2,只读)
                        │
                        └──> desc[3]: addr=<status>, len=1, flags=WRITE (状态,可写)
                                 flags=0 (链结束)

avail->ring[avail_idx % N] = 0  (链的头描述符索引)
avail->idx++                     (公开新条目)
kick()                           (通知 Host)

间接描述符(VRING_DESC_F_INDIRECT):当一个请求需要很多 sg 段时,直接占用环中多个描述符槽位会降低并发度。间接描述符允许用单个描述符指向一张"间接表"(普通内存页中的 vring_desc 数组),从而只占用环中 1 个槽位:

直接模式(消耗 4 个环槽位):
  ring: [head0] ──> desc[0] ──> desc[1] ──> desc[2] ──> desc[3]

间接模式(只消耗 1 个环槽位):
  ring: [head0] ──> desc[0] {flags=INDIRECT, addr=indirect_table}
                      │
                      └──> indirect_table[] 在普通内存页中:
                           [entry0] [entry1] [entry2] [entry3]

内核判断是否使用间接描述符(drivers/virtio/virtio_ring.c:297):

// drivers/virtio/virtio_ring.c:297
static bool virtqueue_use_indirect(const struct vring_virtqueue *vq,
                                   unsigned int total_sg)
{
    return (vq->indirect && total_sg > 1 && vq->vq.num_free);
}

2.4 virtqueue_add_sgs() 提交流程

virtqueue_add_sgs() 是 Driver 侧将 scatter-gather 列表提交到 split virtqueue 的核心函数,最终调用 virtqueue_add_split()drivers/virtio/virtio_ring.c:597):

virtqueue_add_sgs()
    │
    ├── virtqueue_add()           ← 公开 API 入口
    │       │
    │       └── vq->ops->add()   ← 通过 virtqueue_ops 分派
    │               │
    │               └── virtqueue_add_split()  ← split ring 实现

virtqueue_add_split() 的完整流程(drivers/virtio/virtio_ring.c:597):

Step 1: 检查队列状态
  if (vq->broken) return -EIO;

Step 2: 确定是否使用间接描述符
  head = vq->free_head;
  if (virtqueue_use_indirect(vq, total_sg))
      desc = alloc_indirect_split(vq, total_sg, gfp);

Step 3: 检查空闲描述符数量
  if (vq->vq.num_free < descs_used) {
      if (out_sgs) vq->notify();  // 紧急通知 Host 消费
      return -ENOSPC;
  }

Step 4: 填充描述符链(out_sgs,DMA_TO_DEVICE)
  for (n = 0; n < out_sgs; n++) {
      for (sg in sgs[n]) {
          vring_map_one_sg(vq, sg, DMA_TO_DEVICE, &addr, &len);
          i = virtqueue_add_desc_split(vq, desc, extra, i, addr,
                                       len, flags | VRING_DESC_F_NEXT);
      }
  }

Step 5: 填充描述符链(in_sgs,DMA_FROM_DEVICE)
  for (; n < out_sgs + in_sgs; n++) {
      for (sg in sgs[n]) {
          flags = VRING_DESC_F_WRITE;
          vring_map_one_sg(vq, sg, DMA_FROM_DEVICE, ...);
          i = virtqueue_add_desc_split(..., flags);
      }
  }

Step 6: 若使用间接,映射间接表
  addr = vring_map_single(vq, desc, size, DMA_TO_DEVICE);
  virtqueue_add_desc_split(vq, vq->split.vring.desc, ...,
                           head, addr, size, VRING_DESC_F_INDIRECT);

Step 7: 更新管理状态
  vq->vq.num_free -= descs_used;
  vq->split.desc_state[head].data = data;  // 保存回调 token

Step 8: 写入 avail ring(但不立即更新 idx)
  avail = vq->split.avail_idx_shadow & (vring.num - 1);
  vring.avail->ring[avail] = head;          // 写描述符链头部

Step 9: 内存屏障 + 更新 avail->idx(公开给 Host)
  virtio_wmb(vq->weak_barriers);           // 写屏障
  vq->split.avail_idx_shadow++;
  vring.avail->idx = avail_idx_shadow;    // Host 现在可见
  vq->num_added++;

关键的内存屏障语义:virtio_wmb() 确保描述符内容和 avail ring 条目在 avail->idx 更新之前对 Host 可见,防止 Host 读到不完整的描述符。

2.5 kick 机制

更新 avail->idx 后,Driver 不一定立即 kick Host,而是由 virtqueue_notify() / virtqueue_kick() 决定是否发送通知(写 PCI notify 寄存器触发 VM-exit 或 eventfd)。

virtqueue_kick_prepare_split() 检查是否真的需要 kick(drivers/virtio/virtio_ring.c:794):

// drivers/virtio/virtio_ring.c:794
static bool virtqueue_kick_prepare_split(struct vring_virtqueue *vq)
{
    u16 new, old;
    bool needs_kick;

    virtio_mb(vq->weak_barriers);  // 全内存屏障

    old = vq->split.avail_idx_shadow - vq->num_added;
    new = vq->split.avail_idx_shadow;
    vq->num_added = 0;

    if (vq->event) {
        // EVENT_IDX 模式:精确通知
        needs_kick = vring_need_event(
            virtio16_to_cpu(vdev, vring_avail_event(&vq->split.vring)),
            new, old);
    } else {
        // 标志位模式:查询 used->flags
        needs_kick = !(vq->split.vring.used->flags &
                       VRING_USED_F_NO_NOTIFY);
    }
    return needs_kick;
}

vring_need_event() 的精确语义(include/uapi/linux/virtio_ring.h:219):

// include/uapi/linux/virtio_ring.h:219
static inline int vring_need_event(__u16 event_idx,
                                   __u16 new_idx, __u16 old)
{
    return (__u16)(new_idx - event_idx - 1) < (__u16)(new_idx - old);
}

这个看似奇怪的无符号减法是为了正确处理 16 位索引的回绕(wrap-around)。只有当 new_idx 越过了 event_idx 时才返回 true,避免每个缓冲区都触发 kick。

2.6 virtqueue_get_buf() 消费完成

Host 处理完请求后,将结果写入 used ring,并(可选地)通过中断通知 Guest。Guest 通过 virtqueue_get_buf() 消费完成条目。

核心实现 virtqueue_get_buf_ctx_split()drivers/virtio/virtio_ring.c:917):

Step 1: 检查是否有新的已完成条目
  if (!more_used_split(vq)):
      return NULL;

  // more_used_split 检查:
  // (u16)last_used_idx != vring.used->idx

Step 2: 读屏障 + 读取 used ring 条目
  virtio_rmb(vq->weak_barriers);   // 确保 Host 写操作对 Guest 可见

  last_used = vq->last_used_idx & (vring.num - 1);
  i   = vring.used->ring[last_used].id;   // 描述符链头部索引
  len = vring.used->ring[last_used].len;  // Host 写入的总字节数

Step 3: 校验
  if (i >= vring.num) BAD_RING(...);
  if (!desc_state[i].data) BAD_RING(...);

Step 4: 回收描述符
  ret = desc_state[i].data;        // 取出 Driver 的 token
  detach_buf_split(vq, i, ctx);    // 归还描述符到空闲链表
  vq->last_used_idx++;

Step 5: 更新 used_event_idx(触发下次 Host 中断的门槛)
  if (!(avail_flags_shadow & VRING_AVAIL_F_NO_INTERRUPT))
      virtio_store_mb(...,
          &vring_used_event(&vq->split.vring),
          vq->last_used_idx);

return ret;  // 返回 Driver 提交时的 token(如 skb、blk_req 等)

2.7 split ring 整体时序图

Driver (Guest)                          Host (QEMU/vhost)
    │                                        │
    │── 1. 填充 desc[i] ────────────────────>│ (共享内存,Host 不感知)
    │                                        │
    │── 2. avail->ring[idx] = head ─────────>│ (共享内存)
    │                                        │
    │── 3. wmb() ───────────────────────────>│ (确保 desc 在 avail 之前可见)
    │                                        │
    │── 4. avail->idx++ ────────────────────>│ (Host 现在可见新条目)
    │                                        │
    │── 5. kick (write notify reg) ─── VM-exit ──>│
    │                                        │
    │                               │── 6. 读 avail->idx
    │                               │── 7. 读 avail->ring[]
    │                               │── 8. 读 desc 链
    │                               │── 9. 执行 I/O
    │                               │── 10. 写 used->ring[]
    │                               │── 11. rmb()
    │                               │── 12. used->idx++
    │                               │── 13. 注入中断 ──┐
    │                                        │          │
    │<── 14. 中断处理 ─────────────────────────────────┘
    │
    │── 15. virtqueue_get_buf()
    │── 16. 归还 desc,回调 Driver

3. packed virtqueue 深度解析

3.1 设计动机

split ring 有一个明显的缓存效率问题:三个独立的环结构(desc/avail/used)分散在不同内存区域,高并发下频繁读写导致大量缓存行失效(cache line bouncing)。

packed ring(VIRTIO_F_RING_PACKED,virtio 1.1)将三个环合并为单个环,每个描述符同时承载"可用""已完成"两种状态,通过标志位中的 wrap counter 位区分:

split ring(三环分离,缓存不友好):
  [desc ring]  ...  [avail ring]  ...  [used ring]
     4KB    gap       4KB        gap     4KB

packed ring(单环,缓存友好):
  [desc/avail/used 合并] ← 顺序写,硬件预取友好

3.2 packed ring 数据结构

单个 packed 描述符(include/uapi/linux/virtio_ring.h:236):

// include/uapi/linux/virtio_ring.h:236
struct vring_packed_desc {
    __le64 addr;    // 缓冲区地址(或间接表地址)
    __le32 len;     // 缓冲区长度
    __le16 id;      // 描述符链 ID(由 Driver 分配)
    __le16 flags;   // 状态标志(包含 AVAIL/USED/NEXT/WRITE/INDIRECT)
};

关键标志位(include/uapi/linux/virtio_ring.h:48):

// include/uapi/linux/virtio_ring.h:48
#define VRING_PACKED_DESC_F_AVAIL  7   // 表示"Driver 可用"位(bit 7)
#define VRING_PACKED_DESC_F_USED  15   // 表示"Device 已用"位(bit 15)

事件通知结构(include/uapi/linux/virtio_ring.h:229):

// include/uapi/linux/virtio_ring.h:229
struct vring_packed_desc_event {
    __le16 off_wrap;  // 描述符偏移[14:0] | wrap counter[15]
    __le16 flags;     // ENABLE/DISABLE/DESC 通知模式
};

事件标志(include/uapi/linux/virtio_ring.h:61):

// include/uapi/linux/virtio_ring.h:61
#define VRING_PACKED_EVENT_FLAG_ENABLE   0x0  // 启用事件(所有完成均通知)
#define VRING_PACKED_EVENT_FLAG_DISABLE  0x1  // 禁用事件
#define VRING_PACKED_EVENT_FLAG_DESC     0x2  // 仅在特定描述符处通知

内核内部 packed ring 管理结构(drivers/virtio/virtio_ring.c:135):

// drivers/virtio/virtio_ring.c:135
struct vring_virtqueue_packed {
    struct {
        unsigned int num;
        struct vring_packed_desc *desc;           // 单环描述符数组
        struct vring_packed_desc_event *driver;   // Driver 事件抑制区
        struct vring_packed_desc_event *device;   // Device 事件抑制区
    } vring;

    bool avail_wrap_counter;    // Driver 侧 wrap counter
    u16 avail_used_flags;       // 当前 AVAIL/USED 标志组合
    u16 next_avail_idx;         // 下一个可用描述符位置
    u16 event_flags_shadow;     // driver->flags 的影子值

    struct vring_desc_state_packed *desc_state;
    struct vring_desc_extra *desc_extra;

    dma_addr_t ring_dma_addr;
    dma_addr_t driver_event_dma_addr;
    dma_addr_t device_event_dma_addr;
    size_t ring_size_in_bytes;
    size_t event_size_in_bytes;
};

3.3 wrap counter 机制

packed ring 的核心创新在于 wrap counter:通过一个 1-bit 计数器区分描述符的新旧版本,避免对分离 avail/used 环的需求。

wrap counter 语义:

初始状态:avail_wrap_counter = 1,used_wrap_counter = 1

Driver 写入描述符时:
  flags |= (avail_wrap_counter << VRING_PACKED_DESC_F_AVAIL);
  flags |= (avail_wrap_counter << VRING_PACKED_DESC_F_USED);
  // AVAIL=1, USED=1 表示"Driver 声明可用"

当 next_avail_idx 环绕到 0 时:
  avail_wrap_counter ^= 1;  // flip
  // AVAIL=0, USED=0 表示下一轮的"Driver 可用"

Device 判断描述符是否可用:
  bool avail = (flags >> F_AVAIL) & 1;
  bool used  = (flags >> F_USED) & 1;
  desc_is_avail = (avail == avail_wrap_counter) &&
                  (used  == avail_wrap_counter);
  // 等效于:两位相同且等于当前 wrap counter

Device 完成后标记:
  flags &= ~(1 << F_AVAIL);
  flags |= (used_wrap_counter << F_AVAIL);
  flags |= (used_wrap_counter << F_USED);
  // 两位现在等于 used_wrap_counter(与 avail_wrap_counter 相反)
wrap counter 时序:

初始:avail_wc=1, used_wc=1, N=4

Slot: [0]     [1]     [2]     [3]     [0']    [1']
      A=1    A=1    A=1    A=1     A=0    A=0
      U=1    U=1    U=1    U=1     U=0    U=0
      ↑ Driver 写入            ↑ 环绕,wc flip

Device 完成 slot[0] 后:
      A=1    ...
      U=1
      → used_wc 仍为 1,Guest 读到 A==U==used_wc → 已完成

当 used_wc 环绕 slot[0'] 后:
      A=0
      U=0
      → Guest 读到 A==U==0 == used_wc=0 → 已完成

3.4 packed ring 的通知抑制

packed ring 使用独立的 vring_packed_desc_event 结构控制通知:

Driver 事件抑制区(告诉 Device 何时通知 Driver):
  driver->flags = VRING_PACKED_EVENT_FLAG_ENABLE   // 每次完成都通知
  driver->flags = VRING_PACKED_EVENT_FLAG_DISABLE  // 不通知(polling 模式)
  driver->flags = VRING_PACKED_EVENT_FLAG_DESC     // 仅在 off_wrap 描述符完成时通知

Device 事件抑制区(告诉 Driver 何时 kick Device):
  device->flags = VRING_PACKED_EVENT_FLAG_ENABLE   // Driver 每次添加都 kick
  device->flags = VRING_PACKED_EVENT_FLAG_DISABLE  // Driver 不需要 kick
  device->flags = VRING_PACKED_EVENT_FLAG_DESC     // 仅在特定描述符时 kick
off_wrap 字段编码(include/uapi/linux/virtio_ring.h:75):
  bit[14:0] = 描述符环内偏移(off)
  bit[15]   = wrap counter(wc)

  // 表示"在第 wc 轮的第 off 个描述符处触发"

3.5 性能对比:split vs packed

                split ring              packed ring
──────────────────────────────────────────────────────
内存区域数      3(分离)               1(合并)
缓存行效率      较差(3处写入)         较好(顺序写)
回绕检测        独立 idx 比较           wrap counter
通知精度        EVENT_IDX 或 flags      三种模式 + DESC
间接描述符      支持                    支持
IN_ORDER 支持   支持                    支持
硬件 VIRTIO     无要求                  适合 NIC 硬件卸载
规范版本        virtio 0.9+            virtio 1.1+

4. virtio-net 驱动深度分析

4.1 驱动架构概览

virtio-net 驱动架构:

+─────────────────────────────────────────────────────────────+
│  net_device (virtnet_info)                                  │
│  ┌────────────┐  ┌────────────┐  ┌────────────────────────┐│
│  │send_queue[] │  │receive_    │  │  control_queue (cvq)   ││
│  │  vq: SQ0   │  │queue[]     │  │  MAC 过滤/多播          ││
│  │  vq: SQ1   │  │  vq: RQ0  │  │  offload 控制           ││
│  │  ...       │  │  vq: RQ1  │  │  RSS 配置               ││
│  │  napi_tx   │  │  ...      │  └────────────────────────┘│
│  └────────────┘  │  napi_rx  │                             │
│                  │  xdp_prog │                             │
│                  └────────────┘                             │
└─────────────────────────────────────────────────────────────+

4.2 virtnet_info 核心结构体

virtnet_info 是驱动的顶层管理结构(drivers/net/virtio_net.c:392):

// drivers/net/virtio_net.c:392
struct virtnet_info {
    struct virtio_device *vdev;   // virtio 设备实例
    struct virtqueue *cvq;        // 控制队列
    struct net_device *dev;       // Linux 网络设备

    struct send_queue    *sq;     // TX 队列数组(multiqueue)
    struct receive_queue *rq;     // RX 队列数组(multiqueue)

    unsigned int status;          // 设备状态

    u16 max_queue_pairs;          // 设备支持的最大队列对数
    u16 curr_queue_pairs;         // 当前活跃队列对数
    u16 xdp_queue_pairs;          // XDP 使用的队列对数
    bool xdp_enabled;

    bool big_packets;             // 支持大包(>MTU)
    unsigned int big_packets_num_skbfrags;
    bool mergeable_rx_bufs;       // 合并 RX 缓冲区(VIRTIO_NET_F_MRG_RXBUF)

    bool has_rss;                 // RSS 支持
    bool has_rss_hash_report;
    u8 rss_key_size;
    u16 rss_indir_table_size;
    u32 rss_hash_types_supported;

    bool has_cvq;                 // 有控制队列
    struct mutex cvq_lock;

    bool any_header_sg;           // 头部可分散在多个 sg
    u8 hdr_len;                   // virtio_net 头部大小

    bool tx_tnl;                  // TX UDP 隧道支持
    bool rx_tnl;                  // RX UDP 隧道支持
    bool rx_tnl_csum;

    struct work_struct config_work;
    struct work_struct rx_mode_work;
    bool rx_mode_work_enabled;

    bool affinity_hint_set;
    bool rx_dim_enabled;           // 动态中断调制

    struct virtnet_interrupt_coalesce intr_coal_tx;
    struct virtnet_interrupt_coalesce intr_coal_rx;

    unsigned long guest_offloads;           // 当前启用的 offload
    unsigned long guest_offloads_capable;   // 支持的 offload

    struct failover *failover;     // SR-IOV failover
    u64 device_stats_cap;
    // ... RSS 配置(flexible array)
};

send_queue 结构(drivers/net/virtio_net.c:302):

// drivers/net/virtio_net.c:302
struct send_queue {
    struct virtqueue *vq;             // 对应的 TX virtqueue

    struct scatterlist sg[MAX_SKB_FRAGS + 2];  // sg 数组(header + frags + 1)

    char name[16];                    // "output.N"

    struct virtnet_sq_stats stats;    // TX 统计:packets/bytes/kicks/timeouts
    struct virtnet_interrupt_coalesce intr_coal;

    struct napi_struct napi;          // TX NAPI(napi_tx 模式)
    bool reset;                       // 队列是否处于 reset 状态
    struct xsk_buff_pool *xsk_pool;   // AF_XDP zero-copy 支持
    dma_addr_t xsk_hdr_dma_addr;
};

receive_queue 结构(drivers/net/virtio_net.c:327):

// drivers/net/virtio_net.c:327
struct receive_queue {
    struct virtqueue *vq;                  // 对应的 RX virtqueue
    struct napi_struct napi;               // RX NAPI

    struct bpf_prog __rcu *xdp_prog;      // 绑定的 XDP 程序

    struct virtnet_rq_stats stats;         // RX 统计

    u16 calls;                             // 中断计数
    bool dim_enabled;                      // 动态中断调制开关
    struct mutex dim_lock;
    struct dim dim;                        // DIM 状态机

    u32 packets_in_napi;

    struct virtnet_interrupt_coalesce intr_coal;

    struct page *pages;                    // 页面缓存链表
    struct ewma_pkt_len mrg_avg_pkt_len;   // EWMA 平均包长
    struct page_frag alloc_frag;           // 页片分配器

    struct scatterlist sg[MAX_SKB_FRAGS + 2];
    unsigned int min_buf_len;             // 最小单缓冲区大小

    char name[16];                        // "input.N"

    struct xdp_rxq_info xdp_rxq;         // XDP RX 队列信息
    struct virtnet_rq_dma *last_dma;      // 最后一个 DMA 描述

    struct xsk_buff_pool *xsk_pool;       // AF_XDP 缓冲区池
    struct xdp_rxq_info xsk_rxq_info;
    struct xdp_buff **xsk_buffs;
};

4.3 多队列(multiqueue)设计

virtio-net 多队列拓扑(4 队列对 + 1 控制队列):

virtqueue 分配:
  vq[0]  = RQ[0]  (input.0)    \
  vq[1]  = SQ[0]  (output.0)   } 队列对 0  ── CPU 0
  vq[2]  = RQ[1]  (input.1)    \
  vq[3]  = SQ[1]  (output.1)   } 队列对 1  ── CPU 1
  vq[4]  = RQ[2]  (input.2)    \
  vq[5]  = SQ[2]  (output.2)   } 队列对 2  ── CPU 2
  vq[6]  = RQ[3]  (input.3)    \
  vq[7]  = SQ[3]  (output.3)   } 队列对 3  ── CPU 3
  vq[8]  = CVQ    (control)    ── 控制命令

IRQ 亲和:每个队列对绑定到对应 CPU,实现 RSS(Receive Side Scaling)

通过控制队列发送 VIRTIO_NET_CTRL_MQ_VQ_PAIRS_SET 命令设置活跃队列对数,通过 VIRTIO_NET_CTRL_HASH_FUNC 配置 RSS 哈希函数和缩放表。

Guest offload 数组(drivers/net/virtio_net.c:74):

// drivers/net/virtio_net.c:74
static const unsigned long guest_offloads[] = {
    VIRTIO_NET_F_GUEST_TSO4,    // TCP SegOffload (IPv4) RX
    VIRTIO_NET_F_GUEST_TSO6,    // TCP SegOffload (IPv6) RX
    VIRTIO_NET_F_GUEST_ECN,     // ECN 支持
    VIRTIO_NET_F_GUEST_UFO,     // UDP Fragmentation Offload RX
    VIRTIO_NET_F_GUEST_CSUM,    // RX 校验和卸载
    VIRTIO_NET_F_GUEST_USO4,    // UDP Segmentation Offload (IPv4)
    VIRTIO_NET_F_GUEST_USO6,    // UDP Segmentation Offload (IPv6)
    VIRTIO_NET_F_GUEST_HDRLEN,
    VIRTIO_NET_F_GUEST_UDP_TUNNEL_GSO_MAPPED,
    VIRTIO_NET_F_GUEST_UDP_TUNNEL_GSO_CSUM_MAPPED,
};

4.4 TX 路径完整分析

TX 路径调用链:

net_device->ndo_start_xmit()
    │
    └── start_xmit(skb, dev)
            │
            ├── 获取 SQ = vi->sq[queue_index]
            ├── virtnet_xmit_skb(sq, skb)
            │       │
            │       ├── 构建 virtio_net_hdr(GSO/csum 信息)
            │       ├── sg_init_table(sq->sg, ...)
            │       ├── sg_set_buf(&sq->sg[0], hdr, hdr_len)
            │       ├── skb_to_sgvec(skb, sq->sg + 1, ...)
            │       └── virtqueue_add_outbuf(sq->vq, sq->sg, num_sg,
            │                               skb, GFP_ATOMIC)
            │
            ├── skb_orphan(skb)            // 释放 socket 引用
            ├── nf_reset_ct(skb)           // 清理 netfilter 状态
            │
            └── virtqueue_kick(sq->vq)    // 通知 Host(写 notify 寄存器)

TX 完成(NAPI TX 或中断):

virtnet_napi_tx_poll() / virtnet_done_cb()
    │
    └── free_old_xmit(sq, ...)
            │
            ├── while (skb = virtqueue_get_buf(sq->vq, &len)):
            │       │
            │       ├── dev_consume_skb_any(skb)   // 释放 skb
            │       └── sq->stats.packets++
            │
            └── netif_tx_wake_queue(sq->napi.dev)  // 恢复 TX 队列

4.5 RX 路径完整分析

RX 路径(NAPI 驱动):

中断触发:
  virtnet_interrupt() ──> napi_schedule(&rq->napi)

NAPI poll:
  virtnet_poll(napi, budget)
      │
      ├── virtnet_receive(rq, budget, &received)
      │       │
      │       ├── while (received < budget):
      │       │       │
      │       │       ├── buf = virtqueue_get_buf(rq->vq, &len)
      │       │       │     (获取已完成的 RX 缓冲区)
      │       │       │
      │       │       ├── if (xdp_prog):
      │       │       │       virtnet_xdp_handler(xdp_prog, xdp, ...)
      │       │       │       → XDP_PASS:  传递给协议栈
      │       │       │       → XDP_DROP:  丢弃
      │       │       │       → XDP_TX:    在同一设备发送(hairpin)
      │       │       │       → XDP_REDIRECT: 重定向到其他设备/队列
      │       │       │
      │       │       └── receive_buf(vi, rq, buf, len, ...)
      │       │               │
      │       │               ├── 解析 virtio_net_hdr
      │       │               ├── 构建 skb(copy 或 page flip)
      │       │               ├── skb->ip_summed = CHECKSUM_UNNECESSARY
      │       │               ├── skb_shinfo(skb)->gso_size = hdr->gso_size
      │       │               └── napi_gro_receive(&rq->napi, skb)
      │       │
      │       └── virtnet_add_bufs_to_rq(rq, received)
      │               │
      │               └── 补充空 RX 描述符(防止 RQ 耗尽)
      │
      └── if (received < budget): napi_complete_done(napi, received)

4.6 GSO/checksum offload

virtio-net 头部(include/uapi/linux/virtio_net.h)携带 offload 信息:

struct virtio_net_hdr_v1 {
    u8  flags;          // VIRTIO_NET_HDR_F_NEEDS_CSUM | F_DATA_VALID
    u8  gso_type;       // VIRTIO_NET_HDR_GSO_TCPV4/6 | GSO_UDP | NONE
    u16 hdr_len;        // 传输层头部长度
    u16 gso_size;       // MSS(最大段大小)
    u16 csum_start;     // 校验和计算起始偏移
    u16 csum_offset;    // 校验和字段在 csum_start 的偏移
    u16 num_buffers;    // MRG_RXBUF 时合并的缓冲区数(RX 用)
};

TX offload 流程:

if (skb->ip_summed == CHECKSUM_PARTIAL):
    hdr->flags       = VIRTIO_NET_HDR_F_NEEDS_CSUM
    hdr->csum_start  = skb_checksum_start_offset(skb)
    hdr->csum_offset = skb->csum_offset

if (skb_is_gso(skb)):
    if (skb_shinfo(skb)->gso_type & SKB_GSO_TCPV4):
        hdr->gso_type = VIRTIO_NET_HDR_GSO_TCPV4
    hdr->hdr_len  = skb_headlen(skb)
    hdr->gso_size = skb_shinfo(skb)->gso_size

RX offload 反向流程(Host 已完成分段/校验):

if (hdr->flags & VIRTIO_NET_HDR_F_DATA_VALID):
    skb->ip_summed = CHECKSUM_UNNECESSARY

if (hdr->gso_type != VIRTIO_NET_HDR_GSO_NONE):
    skb_shinfo(skb)->gso_size = hdr->gso_size
    skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(skb->len, gso_size)
    // 传递给 GRO(Generic Receive Offload)
    napi_gro_receive(napi, skb)

4.7 XDP 支持

XDP(eXpress Data Path)在 virtio-net 中实现了近原生性能的包处理(drivers/net/virtio_net.c):

XDP 在 virtio-net 中的集成点:

1. virtnet_xdp_set():加载/卸载 XDP 程序
   ├── 调整 RX headroom(XDP_PACKET_HEADROOM)
   ├── 切换为 page 模式(禁用 mergeable_rx_bufs)
   └── 重配 RX 队列

2. virtnet_xdp_handler():在 NAPI poll 中对每个 RX 包调用
   ├── xdp_buff 构建(利用 headroom 避免 memmove)
   ├── bpf_prog_run_xdp(prog, xdp)
   └── 根据返回值处置:
       XDP_PASS    → 继续正常 RX 栈
       XDP_DROP    → page_pool_recycle_direct()
       XDP_TX      → 直接从同一 sq 发送(零拷贝)
       XDP_REDIRECT → xdp_do_redirect()(转发到其他 CPU/设备)
       XDP_ABORTED → 统计 xdp_drops++

3. AF_XDP(XSK)零拷贝:
   sq->xsk_pool / rq->xsk_pool
   → 共享内存 UMEM 直接由用户态程序访问

XDP TX 路径(VIRTIO_XDP_TX 标志,drivers/net/virtio_net.c:62):

// drivers/net/virtio_net.c:62
#define VIRTIO_XDP_TX    BIT(0)
#define VIRTIO_XDP_REDIR BIT(1)

5. virtio-blk 驱动分析

5.1 核心数据结构

virtio_blk 顶层结构(drivers/block/virtio_blk.c:55):

// drivers/block/virtio_blk.c:55
struct virtio_blk {
    struct mutex vdev_mutex;          // 保护 vdev 指针
    struct virtio_device *vdev;

    struct gendisk *disk;             // Linux 磁盘设备
    struct blk_mq_tag_set tag_set;   // blk-mq 标签集
    struct work_struct config_work;   // 配置空间更新任务

    int index;                        // 设备索引(vda/vdb/...)
    int num_vqs;                      // 虚拟队列数量
    int io_queues[HCTX_MAX_TYPES];   // 各类型 hw_ctx 的队列数
    struct virtio_blk_vq *vqs;       // 虚拟队列数组

    unsigned int zone_sectors;        // Zoned 设备扇区数
};

每个 virtqueue 的包装(drivers/block/virtio_blk.c:49):

// drivers/block/virtio_blk.c:49
struct virtio_blk_vq {
    struct virtqueue *vq;
    spinlock_t lock;           // 保护同一队列的并发访问
    char name[VQ_NAME_LEN];   // "req.N"
} ____cacheline_aligned_in_smp;

每个请求的上下文(drivers/block/virtio_blk.c:88):

// drivers/block/virtio_blk.c:88
struct virtblk_req {
    struct virtio_blk_outhdr out_hdr;  // 请求头(type/ioprio/sector)
    union {
        u8 status;                     // 完成状态(VIRTIO_BLK_S_OK/IOERR)
        struct {
            __virtio64 sector;         // Zone Append 返回的扇区号
            u8 status;
        } zone_append;
    } in_hdr;
    size_t in_hdr_len;

    struct sg_table sg_table;          // 数据 sg 表
    struct scatterlist sg[];           // 内联 sg 数组(VIRTIO_BLK_INLINE_SG_CNT=2)
};

5.2 blk-mq 集成

virtio-blk 采用 blk-mq(多队列块层)架构,每个 virtqueue 对应一个 blk_mq_hw_ctx

blk-mq 与 virtio-blk 对应关系:

  Application
      │ submit_bio()
      ▼
  Block Layer (blk-mq)
      │ 分配 request,选择 hw_ctx
      ▼
  blk_mq_ops.queue_rq()
      │ = virtblk_queue_rq()
      ▼
  struct virtio_blk_vq *vq = get_virtio_blk_vq(hctx)
      │
      ├── virtblk_setup_cmd(vdev, req, vbr)  // 构建请求头
      ├── virtblk_map_data(hctx, req, vbr)   // 映射数据 sg
      └── virtblk_add_req(vq->vq, vbr)       // 提交到 virtqueue
              │
              └── virtqueue_add_sgs(vq, sgs, num_out, num_in, vbr, GFP_ATOMIC)

5.3 请求格式与 sg 布局

virtblk_add_req() 构建的 sg 数组(drivers/block/virtio_blk.c:139):

// drivers/block/virtio_blk.c:139
static int virtblk_add_req(struct virtqueue *vq, struct virtblk_req *vbr)
{
    struct scatterlist out_hdr, in_hdr, *sgs[3];
    unsigned int num_out = 0, num_in = 0;

    sg_init_one(&out_hdr, &vbr->out_hdr, sizeof(vbr->out_hdr));
    sgs[num_out++] = &out_hdr;          // 只读:请求头

    if (vbr->sg_table.nents) {
        if (type == VIRTIO_BLK_T_OUT)
            sgs[num_out++] = vbr->sg_table.sgl;   // 只读:写数据
        else
            sgs[num_out + num_in++] = vbr->sg_table.sgl; // 可写:读数据
    }

    sg_init_one(&in_hdr, &vbr->in_hdr.status, vbr->in_hdr_len);
    sgs[num_out + num_in++] = &in_hdr;  // 可写:状态

    return virtqueue_add_sgs(vq, sgs, num_out, num_in, vbr, GFP_ATOMIC);
}

READ 请求的描述符链:

desc[0]: out_hdr {type=T_IN, sector=X, ioprio=0}  (只读,16B)
    │
    └──> desc[1]: data_buffer[0..511]              (可写,512B×N)
              │
              └──> desc[2]: in_hdr.status          (可写,1B)

WRITE 请求的描述符链:

desc[0]: out_hdr {type=T_OUT, sector=X}            (只读,16B)
    │
    └──> desc[1]: data_buffer[0..511]              (只读,512B×N)
              │
              └──> desc[2]: in_hdr.status          (可写,1B)

5.4 请求类型完整映射

virtblk_setup_cmd() 处理所有请求类型(drivers/block/virtio_blk.c:238):

// drivers/block/virtio_blk.c:253
switch (req_op(req)) {
case REQ_OP_READ:         type = VIRTIO_BLK_T_IN;           // 0
case REQ_OP_WRITE:        type = VIRTIO_BLK_T_OUT;          // 1
case REQ_OP_FLUSH:        type = VIRTIO_BLK_T_FLUSH;        // 4
case REQ_OP_DISCARD:      type = VIRTIO_BLK_T_DISCARD;      // 11
case REQ_OP_WRITE_ZEROES: type = VIRTIO_BLK_T_WRITE_ZEROES; // 13
case REQ_OP_SECURE_ERASE: type = VIRTIO_BLK_T_SECURE_ERASE; // 14
case REQ_OP_ZONE_OPEN:    type = VIRTIO_BLK_T_ZONE_OPEN;    // Zoned
case REQ_OP_ZONE_CLOSE:   type = VIRTIO_BLK_T_ZONE_CLOSE;
case REQ_OP_ZONE_FINISH:  type = VIRTIO_BLK_T_ZONE_FINISH;
case REQ_OP_ZONE_APPEND:  type = VIRTIO_BLK_T_ZONE_APPEND;
case REQ_OP_ZONE_RESET:   type = VIRTIO_BLK_T_ZONE_RESET;
case REQ_OP_ZONE_RESET_ALL: type = VIRTIO_BLK_T_ZONE_RESET_ALL;
}

5.5 完成路径

Host 完成 I/O → 注入中断 → virtblk_done() 中断处理

virtblk_done(vq_irq, vbq):
    │
    └── virtblk_vq_done(vq):
            │
            ├── spin_lock_irqsave(&vq->lock)
            ├── while (vbr = virtqueue_get_buf(vq->vq, &len)):
            │       │
            │       ├── req = blk_mq_rq_from_pdu(vbr)
            │       ├── virtblk_result(vbr->in_hdr.status) → blk_status_t
            │       └── blk_mq_complete_request(req, status)
            │
            └── spin_unlock_irqrestore(&vq->lock)

状态码转换(drivers/block/virtio_blk.c:113):

// drivers/block/virtio_blk.c:113
static inline blk_status_t virtblk_result(u8 status)
{
    switch (status) {
    case VIRTIO_BLK_S_OK:    return BLK_STS_OK;
    case VIRTIO_BLK_S_UNSUPP: return BLK_STS_NOTSUPP;
    case VIRTIO_BLK_S_ZONE_OPEN_RESOURCE:  return BLK_STS_ZONE_OPEN_RESOURCE;
    case VIRTIO_BLK_S_ZONE_ACTIVE_RESOURCE: return BLK_STS_ZONE_ACTIVE_RESOURCE;
    default:                  return BLK_STS_IOERR;
    }
}

5.6 多队列与 poll 队列

virtio-blk 支持三种类型的 hw_ctx:

HCTX_TYPE_DEFAULT  → 普通 I/O 队列(中断驱动)
HCTX_TYPE_READ     → 读 I/O 队列(可与 DEFAULT 共享)
HCTX_TYPE_POLL     → polling 队列(无中断,CPU 主动轮询)

poll_queues 模块参数:控制 POLL 类型队列数
  # 仅对 NVMe-oF 等低延迟场景有意义

配置示例:
  /sys/block/vda/queue/io_poll = 1  # 启用 polling

6. vhost 内核后端

6.1 vhost 整体架构

vhost 是 Linux 内核中 virtio 设备的后端实现,允许在宿主机内核态直接处理 virtqueue 操作,绕过 QEMU 用户态,显著降低 VM-exit 次数。

不使用 vhost(所有 I/O 过 QEMU 用户态):
  Guest virtio Driver
      │ VM-exit (kick)
      ▼
  KVM (内核)
      │ upcall
      ▼
  QEMU (用户态)
      │ I/O 模拟
      ▼
  宿主机内核(socket/块设备)

使用 vhost(内核直接处理):
  Guest virtio Driver
      │ VM-exit (kick) → eventfd
      ▼
  KVM → eventfd → vhost worker(内核态)
      │ 直接读写 virtqueue(共享内存)
      ▼
  宿主机内核(TAP/socket/块设备)
      ↑ 零额外拷贝

6.2 vhost_dev 与 vhost_virtqueue

vhost_dev 是 vhost 设备的顶层结构(drivers/vhost/vhost.h:178):

// drivers/vhost/vhost.h:178
struct vhost_dev {
    struct mm_struct *mm;           // Guest 的内存映射(用于访问共享内存)
    struct mutex mutex;             // 设备级别锁
    struct vhost_virtqueue **vqs;  // 虚拟队列指针数组
    int nvqs;                       // 队列数量

    struct eventfd_ctx *log_ctx;   // 脏页日志(迁移用)
    struct vhost_iotlb *umem;      // 用户内存映射表
    struct vhost_iotlb *iotlb;     // IOMMU TLB
    spinlock_t iotlb_lock;

    struct list_head read_list;    // 异步消息读取队列
    struct list_head pending_list;

    wait_queue_head_t wait;

    int iov_limit;                 // IOV 数量上限
    int weight;                    // 每次 poll 的包数权重
    int byte_weight;               // 每次 poll 的字节数权重

    struct xarray worker_xa;       // worker 哈希表(xarray)
    bool use_worker;               // 是否使用独立 worker 线程
    bool fork_owner;               // worker 继承 owner 的 cgroup/调度策略

    int (*msg_handler)(struct vhost_dev *dev, u32 asid,
                       struct vhost_iotlb_msg *msg);
};

vhost_virtqueue 是 vhost 对单个 virtqueue 的封装(drivers/vhost/vhost.h:94):

// drivers/vhost/vhost.h:94
struct vhost_virtqueue {
    struct vhost_dev *dev;
    struct vhost_worker __rcu *worker;  // 处理该队列的 worker

    struct mutex mutex;
    unsigned int num;           // 队列深度(ring 大小)

    // 用户空间的三个环(Guest 物理地址空间中的指针)
    vring_desc_t  __user *desc;
    vring_avail_t __user *avail;
    vring_used_t  __user *used;

    const struct vhost_iotlb_map *meta_iotlb[VHOST_NUM_ADDRS];

    struct file *kick;          // kick eventfd 文件
    struct vhost_vring_call call_ctx;  // 中断注入(irq_bypass)
    struct eventfd_ctx *error_ctx;
    struct eventfd_ctx *log_ctx;

    struct vhost_poll poll;     // epoll 监听 kick eventfd

    vhost_work_fn_t handle_kick;  // kick 处理函数指针

    u16 last_avail_idx;         // 上次读取的 avail->idx(wrap=高位)
    u16 next_avail_head;        // IN_ORDER 模式下下一个头部
    u16 avail_idx;              // 缓存的 avail->idx
    u16 last_used_idx;          // 上次写入的 used->idx

    u16 used_flags;             // VRING_USED_F_NO_NOTIFY
    u16 signalled_used;
    bool signalled_used_valid;

    bool log_used;              // 是否记录 used ring 写入(迁移用)
    u64 log_addr;

    struct iovec iov[UIO_MAXIOV];         // 工作 iov 数组
    struct iovec iotlb_iov[64];
    struct iovec *indirect;
    struct vring_used_elem *heads;

    struct vhost_iotlb *umem;   // 用户内存映射(继承自 vhost_dev)
    struct vhost_iotlb *iotlb;
    void *private_data;          // 设备特定数据(如 socket fd)

    VIRTIO_DECLARE_FEATURES(acked_features);   // 已协商的 features
    u64 acked_backend_features;

    void __user *log_base;
    struct vhost_log *log;
    struct iovec log_iov[64];

    bool is_le;                 // 字节序(小端/大端)
    u32 busyloop_timeout;       // busy loop 超时(微秒)
};

6.3 vhost worker 线程

每个 vhost 设备有一个或多个 worker 线程(kthread 或 vhost_task),负责轮询 virtqueue 并处理请求(drivers/vhost/vhost.h:40):

// drivers/vhost/vhost.h:40
struct vhost_worker {
    struct task_struct *kthread_task; // 若不使用 fork_owner
    struct vhost_task  *vtsk;         // 若使用 fork_owner(继承 cgroup 等)
    struct vhost_dev   *dev;

    struct mutex mutex;            // 保护与 device-wide flush 的互斥
    struct llist_head work_list;   // 待处理工作的无锁链表
    u64 kcov_handle;               // 内核覆盖率追踪句柄

    u32 id;
    int attachment_cnt;            // 附加的 vq 数量
    bool killed;                   // worker 是否已被终止

    const struct vhost_worker_ops *ops;  // create/stop/wakeup 操作表
};

vhost_work 是提交到 worker 的工作单元(drivers/vhost/vhost.h:24):

// drivers/vhost/vhost.h:24
struct vhost_work {
    struct llist_node   node;    // 无锁链表节点
    vhost_work_fn_t     fn;      // 工作函数(如 handle_tx_net)
    unsigned long       flags;   // VHOST_WORK_QUEUED
};

Worker 处理流程(简化):

vhost_worker_thread() 或 vhost_task_fn():
    │
    └── loop:
            │
            ├── 等待 work_list 非空(sleep / wakeup by eventfd)
            │
            ├── llist_for_each_entry_safe(work, work_list):
            │       work->fn(work)        // 执行工作
            │
            └── 清空 work_list,等待下一轮

vhost_poll 机制(监听 kick eventfd)(drivers/vhost/vhost.h:56):

// drivers/vhost/vhost.h:56
struct vhost_poll {
    poll_table          table;   // 内核 poll 机制
    wait_queue_head_t  *wqh;    // kick eventfd 的等待队列
    wait_queue_entry_t  wait;   // 注册到 wqh 的等待项
    struct vhost_work   work;   // 当 kick 触发时提交的工作
    __poll_t            mask;   // EPOLLIN
    struct vhost_dev   *dev;
    struct vhost_virtqueue *vq;
};

6.4 vhost-net 与 TAP 零拷贝

vhost-net 数据路径(TX:Guest → Host Network):

Guest virtio-net Driver
    │ 1. 写 avail ring,kick (eventfd)
    ▼
KVM eventfd 通知 → 唤醒 vhost worker
    │
    ▼
vhost_net_handle_tx_net() [内核态]
    │
    ├── vhost_get_vq_desc()        // 读 avail ring,获取描述符链
    │       │ (通过 copy_from_user 访问 Guest 内存)
    │       └── 构建 iov[] 数组(指向 Guest 物理页)
    │
    ├── sock->ops->sendmsg()       // 发送到 TAP socket
    │       │ 使用 vhost 的 iov[],直接从 Guest 物理页发送
    │       └── 零额外拷贝(iov 直接映射 Guest 内存)
    │
    └── vhost_add_used_and_signal()  // 写 used ring,注入中断

TAP → 宿主机内核网络栈 → 物理网卡(或 bridge/OVS)
vhost-net 数据路径(RX:Host Network → Guest):

外部网络包到达 → TAP socket readable
    │
    ├── vhost_poll 检测到 EPOLLIN → 唤醒 worker
    │
    ▼
vhost_net_handle_rx_net() [内核态]
    │
    ├── vhost_get_vq_desc()        // 获取 Guest 预分配的 RX 描述符
    │       └── 构建指向 Guest 内存的 iov[]
    │
    ├── sock->ops->recvmsg()       // 从 TAP 读取数据
    │       └── 直接写入 Guest 内存(iov 指向 Guest 物理页)
    │
    └── vhost_add_used_and_signal()  // 通知 Guest 数据就绪

内存访问方式:vhost 通过 use_mm(dev->mm) 切换到 Guest 的地址空间,然后使用 get_user_pages() 获取 Guest 物理页,构建 iov 直接访问,避免额外拷贝。

6.5 vhost-user:DPDK 用户态后端

vhost-user 将 vhost 协议暴露到用户态,允许 DPDK 等高性能用户态程序实现 virtio 后端:

vhost vs vhost-user 对比:

                 vhost(内核态)        vhost-user(用户态)
─────────────────────────────────────────────────────────
实现位置         内核 drivers/vhost/    用户态(DPDK librte_vhost)
通信协议         ioctl()                Unix Domain Socket + cmsg
内存访问         use_mm + get_user_pages mmap(共享 IOVA 区域)
性能             高(无用户态切换)      极高(DPDK PMD + huge pages)
灵活性           内核版本绑定            独立演化
典型用途         KVM + bridge/TAP       DPDK OVS,SR-IOV,SmartNIC

vhost-user 协议消息(通过 Unix socket):

VHOST_USER_GET_FEATURES         // 获取后端支持的 features
VHOST_USER_SET_FEATURES         // 设置协商后的 features
VHOST_USER_SET_MEM_TABLE        // 传递 Guest 内存映射表(fd + offset)
VHOST_USER_SET_VRING_NUM        // 设置队列深度
VHOST_USER_SET_VRING_ADDR       // 设置三环的物理地址
VHOST_USER_SET_VRING_KICK       // 传递 kick eventfd
VHOST_USER_SET_VRING_CALL       // 传递 call eventfd(中断注入)
VHOST_USER_SET_VRING_ENABLE     // 启用队列

DPDK vhost-user 数据路径(零拷贝):

Guest virtio Driver
    │ kick (eventfd)
    ▼
DPDK vhost-user library
    │ rte_vhost_dequeue_burst()
    │   → 直接读写 QEMU 映射的 Guest hugepage
    │   → 构建 rte_mbuf 指向 Guest 内存(零拷贝)
    ▼
DPDK PMD(物理网卡)
    │ rte_eth_tx_burst()
    │   → DMA 直接从 Guest 内存到网卡(IOMMU + SR-IOV)
    ▼
物理网卡

6.6 irq_bypass:KVM 中断直通

vhost 通过 irq_bypass_producervhost_vring_call.producer)与 KVM 的 irq_bypass_consumer 配对,实现 posted interrupt 机制:

标准中断路径(无 posted interrupt):
  vhost worker → eventfd_signal() → KVM → 注入虚拟中断 → VM-entry

Posted interrupt 路径(Intel VT-x APIC Virtualization):
  vhost worker → 写 Posted Interrupt Descriptor → 硬件自动注入 → 无 VM-exit

这是 vhost 性能的关键优化,避免了中断注入引起的额外 VM-exit。


7. paravirt_ops 半虚拟化接口

7.1 概念与设计

paravirt_ops(简称 pv_ops)是 x86 架构内核中一套可替换的间接函数表,允许 Guest OS 将特权操作(TLB 刷新、中断控制、MSR 访问等)替换为针对特定 Hypervisor 优化的实现,而无需维护单独的内核分支。

pv_ops 工作原理:

裸机(NATIVE)内核:
  flush_tlb_user() ──> native_flush_tlb_user()  // INVLPG 指令

KVM Guest 内核:
  flush_tlb_user() ──> kvm_flush_tlb_user()     // 可选:远程 TLB shootdown 优化
  save_fl()         ──> pvops_save_fl()          // 读取影子 EFLAGS.IF

Xen PV Guest 内核:
  flush_tlb_user() ──> xen_flush_tlb_user()     // hypercall
  write_cr3()      ──> xen_write_cr3()          // hypercall(Xen 管理页表)

关键头文件:arch/x86/include/asm/paravirt_types.h

7.2 pv_ops 全局结构

paravirt_patch_template 是所有 pv_ops 的容器(arch/x86/include/asm/paravirt_types.h:190):

// arch/x86/include/asm/paravirt_types.h:190
struct paravirt_patch_template {
    struct pv_cpu_ops cpu;   // CPU 特权指令(MSR/CR 寄存器/段描述符)
    struct pv_irq_ops irq;   // 中断控制(IF 标志/HALT)
    struct pv_mmu_ops mmu;   // 内存管理(TLB/页表操作)
} __no_randomize_layout;

extern struct paravirt_patch_template pv_ops;  // 全局实例

7.3 pv_cpu_ops:CPU 操作钩子

pv_cpu_ops 覆盖所有特权 CPU 操作(arch/x86/include/asm/paravirt_types.h:31):

// arch/x86/include/asm/paravirt_types.h:31
struct pv_cpu_ops {
    void (*io_delay)(void);     // I/O 端口延迟(in/out 后的等待)

    // CONFIG_PARAVIRT_XXL:
    unsigned long (*get_debugreg)(int regno);
    void (*set_debugreg)(int regno, unsigned long value);

    unsigned long (*read_cr0)(void);
    void (*write_cr0)(unsigned long);
    void (*write_cr4)(unsigned long);

    // 段描述符操作
    void (*load_tr_desc)(void);
    void (*load_gdt)(const struct desc_ptr *);
    void (*load_idt)(const struct desc_ptr *);
    void (*set_ldt)(const void *desc, unsigned entries);
    unsigned long (*store_tr)(void);
    void (*load_tls)(struct thread_struct *t, unsigned int cpu);
    void (*load_gs_index)(unsigned int idx);
    void (*write_ldt_entry)(struct desc_struct *ldt, int entrynum, ...);
    void (*write_gdt_entry)(struct desc_struct *, int entrynum, ...);
    void (*write_idt_entry)(gate_desc *, int entrynum, ...);
    void (*alloc_ldt)(struct desc_struct *ldt, unsigned entries);
    void (*free_ldt)(struct desc_struct *ldt, unsigned entries);

    void (*load_sp0)(unsigned long sp0);

    // cpuid 模拟(禁用特定 capability bits)
    void (*cpuid)(unsigned int *eax, unsigned int *ebx,
                  unsigned int *ecx, unsigned int *edx);

    // MSR 操作
    u64 (*read_msr)(u32 msr);
    void (*write_msr)(u32 msr, u64 val);
    int (*read_msr_safe)(u32 msr, u64 *val);
    int (*write_msr_safe)(u32 msr, u64 val);

    u64 (*read_pmc)(int counter);

    void (*start_context_switch)(struct task_struct *prev);
    void (*end_context_switch)(struct task_struct *next);
} __no_randomize_layout;

7.4 pv_irq_ops:中断控制钩子

pv_irq_ops 覆盖中断相关操作(arch/x86/include/asm/paravirt_types.h:90):

// arch/x86/include/asm/paravirt_types.h:90
struct pv_irq_ops {
    // CONFIG_PARAVIRT_XXL:
    struct paravirt_callee_save save_fl;      // 读取中断标志(EFLAGS.IF)
    struct paravirt_callee_save irq_disable;  // CLI(关中断)
    struct paravirt_callee_save irq_enable;   // STI(开中断)

    void (*safe_halt)(void);   // HLT(可安全中断的 halt)
    void (*halt)(void);        // HLT(不可中断的 halt)
} __no_randomize_layout;

paravirt_callee_save 是一个包装类型,保存函数指针的同时声明被调用者保存寄存器约定,减少不必要的寄存器保存/恢复开销:

// arch/x86/include/asm/paravirt-base.h
struct paravirt_callee_save {
    void *func;   // 实际函数指针
};

在 KVM Guest 中,irq_disable/irq_enable 替换为**影子 IF(shadow IF flag)**操作:KVM Guest 不真正执行 CLI/STI(会触发 VM-exit),而是操作内存中的影子变量,大幅减少特权指令开销。

7.5 pv_mmu_ops:内存管理钩子

pv_mmu_ops 是最复杂的操作集,覆盖 TLB 和页表操作(arch/x86/include/asm/paravirt_types.h:107):

// arch/x86/include/asm/paravirt_types.h:107
struct pv_mmu_ops {
    // TLB 操作
    void (*flush_tlb_user)(void);
    void (*flush_tlb_kernel)(void);
    void (*flush_tlb_one_user)(unsigned long addr);
    void (*flush_tlb_multi)(const struct cpumask *cpus,
                            const struct flush_tlb_info *info);

    // mm_struct 生命周期钩子
    void (*exit_mmap)(struct mm_struct *mm);
    void (*notify_page_enc_status_changed)(unsigned long pfn,
                                           int npages, bool enc);

    // CONFIG_PARAVIRT_XXL:
    struct paravirt_callee_save read_cr2;
    void (*write_cr2)(unsigned long);

    unsigned long (*read_cr3)(void);
    void (*write_cr3)(unsigned long);

    void (*enter_mmap)(struct mm_struct *mm);

    // 页表分配/释放
    int  (*pgd_alloc)(struct mm_struct *mm);
    void (*pgd_free)(struct mm_struct *mm, pgd_t *pgd);

    // 各级页表页的分配/释放
    void (*alloc_pte)(struct mm_struct *mm, unsigned long pfn);
    void (*alloc_pmd)(struct mm_struct *mm, unsigned long pfn);
    void (*alloc_pud)(struct mm_struct *mm, unsigned long pfn);
    void (*alloc_p4d)(struct mm_struct *mm, unsigned long pfn);
    void (*release_pte)(unsigned long pfn);
    void (*release_pmd)(unsigned long pfn);
    void (*release_pud)(unsigned long pfn);
    void (*release_p4d)(unsigned long pfn);

    // 页表项修改
    void (*set_pte)(pte_t *ptep, pte_t pteval);
    void (*set_pmd)(pmd_t *pmdp, pmd_t pmdval);

    pte_t (*ptep_modify_prot_start)(...);
    void  (*ptep_modify_prot_commit)(...);

    // 页表值读写(callee-save 优化)
    struct paravirt_callee_save pte_val;
    struct paravirt_callee_save make_pte;
    struct paravirt_callee_save pgd_val;
    struct paravirt_callee_save make_pgd;

    // PUD/PMD/P4D 操作
    void (*set_pud)(pud_t *pudp, pud_t pudval);
    struct paravirt_callee_save pmd_val;
    struct paravirt_callee_save make_pmd;
    struct paravirt_callee_save pud_val;
    struct paravirt_callee_save make_pud;
    void (*set_p4d)(p4d_t *p4dp, p4d_t p4dval);
    struct paravirt_callee_save p4d_val;
    struct paravirt_callee_save make_p4d;

    void (*set_pgd)(pgd_t *pgdp, pgd_t pgdval);

    struct pv_lazy_ops lazy_mode;     // 延迟批处理模式

    void (*set_fixmap)(unsigned idx, phys_addr_t phys, pgprot_t flags);
} __no_randomize_layout;

7.6 pv_ops 调用机制与性能优化

直接调用 pv_ops 结构中的函数指针会引入间接跳转,影响分支预测。内核使用运行时 alternative patching 机制优化这一开销(arch/x86/include/asm/paravirt_types.h:348):

// arch/x86/include/asm/paravirt_types.h:348
#define ____PVOP_CALL(ret, array, op, call_clbr, extra_clbr, ...)   \
    ({                                                                \
        PVOP_CALL_ARGS;                                               \
        asm volatile(ALTERNATIVE(PARAVIRT_CALL,                      \
                         ALT_CALL_INSTR, ALT_CALL_ALWAYS)           \
                     : call_clbr, ASM_CALL_CONSTRAINT               \
                     : paravirt_ptr(array, op),                      \
                       ##__VA_ARGS__                                  \
                     : "memory", "cc" extra_clbr);                   \
        ret;                                                          \
    })

ALTERNATIVE 宏的工作原理:

编译时:生成 PARAVIRT_CALL(间接调用)作为初始代码
启动时:alternatives_apply() 扫描所有 alternative 位点
       对于裸机启动:用直接 CALL 替换间接调用
       对于 PV Guest:保留间接调用或替换为特定指令序列

结果:
  裸机:直接调用,无间接跳转开销
  PV Guest:间接调用到 pv_ops 表,或内联汇编序列

PARAVIRT_CALL 宏(arch/x86/include/asm/paravirt_types.h:211):

// arch/x86/include/asm/paravirt_types.h:211
#define PARAVIRT_CALL                       \
    ANNOTATE_RETPOLINE_SAFE "\n\t"          \
    "call *%[paravirt_opptr]"

注意 ANNOTATE_RETPOLINE_SAFE:由于这是已知目标的间接调用(编译时可确定),不需要 retpoline 防护,该注解告诉 objtool 跳过此检查。

7.7 PV spinlock(队列自旋锁 PV)

在虚拟化环境中,自旋锁(spinlock)存在严重问题:当持锁的 vCPU 被抢占(de-scheduled)时,其他等待的 vCPU 会无效自旋,浪费 CPU 资源(锁保持者抢占问题,Lock Holder Preemption)。

PV spinlock 通过以下方式解决(arch/x86/kernel/kvm.c):

PV spinlock 机制:

1. 尝试获取锁时:
   如果 CPU 处于 spinning 超过阈值(如 2000 轮),
   执行 KVM hypercall KVM_HC_KICK_CPU 并 HALT(让出 CPU)

2. 释放锁时:
   如果有等待者(通过 per-CPU 等待标志检测),
   执行 KVM hypercall 唤醒等待 vCPU

   kvm_kick_lock_holder()
       │
       └── kvm_hypercall1(KVM_HC_KICK_CPU, cpu)
               │ VM-exit → KVM → schedule vCPU

结果:持锁者被抢占时,等待者 HALT 而非 spin,减少 CPU 浪费

相关 pv_ops hook(arch/x86/include/asm/paravirt.h):

// 注册 PV spinlock 操作
pv_ops.lock.queued_spin_lock_slowpath = __pv_queued_spin_lock_slowpath;
pv_ops.lock.queued_spin_unlock = __pv_queued_spin_unlock;

7.8 KVM 半虚拟化时钟(kvmclock)

kvmclock 是 KVM 与 Guest 共享的半虚拟化时钟,解决虚拟机时钟漂移问题(arch/x86/kernel/kvmclock.c):

kvmclock 工作原理:

Host KVM 维护:
  struct kvm_clock_data {
      u64 clock;          // 单调时间(纳秒)
      u32 flags;
      ...
  };

共享内存页:
  struct pvclock_vcpu_time_info {
      u32 version;        // 版本号(奇数表示 Host 正在写入)
      u64 tsc_timestamp;  // 对应 TSC 时刻
      u64 system_time;    // 对应系统时间(纳秒)
      u32 tsc_to_system_mul;  // TSC 到时间的乘数
      s8  tsc_shift;      // TSC 移位值
      u8  flags;
  };

Guest 读取时间(无 VM-exit):
  1. 读取 version(奇数则重试)
  2. 读取 TSC(rdtsc)
  3. 计算:time = system_time + ((tsc - tsc_timestamp) * mul >> shift)
  4. 再次读取 version 验证一致性(seqlock 语义)

kvmclock 注册为 pv_ops 时钟源:

// arch/x86/kernel/kvmclock.c
pv_ops.time.get_wallclock  = kvm_get_wallclock;
pv_ops.time.set_wallclock  = kvm_set_wallclock;
pv_ops.time.get_tsc_khz    = kvm_get_tsc_khz;
pv_ops.time.get_cpu_khz    = kvm_get_cpu_khz;

// 注册高精度时钟源
clocksource_register_hz(&kvm_clock, NSEC_PER_SEC);

kvmclock 优势:

特性                  TSC 直接读取       kvmclock
───────────────────────────────────────────────────
VM 迁移后正确         否(TSC 跳变)     是(重新校准)
VM 挂起/恢复正确      否                 是
多 vCPU 一致性        否(各 CPU TSC 不同) 是
VM-exit 数量          0                  0(共享内存)
精度                  纳秒级             纳秒级

8. virtio-mem 与 virtio-balloon

8.1 内存热插拔的两种 virtio 方案

Linux 虚拟机动态内存管理有两条技术路线:

virtio-balloon(传统方案):
  Host 告知 Guest "膨胀/放气 N 页"
  Guest 主动分配/释放这些页
  缺点:需要 Guest 协作;页面选择不优;可能引起 OOM

virtio-mem(现代方案,v5.8+):
  Host 以块(block)为粒度管理 Guest 内存
  Guest 直接看到内存范围的增减
  支持 NUMA 感知,与 memory hotplug 框架集成
  不需要 Guest 分配页面

8.2 virtio-balloon 机制

virtio-balloon 使用两个 virtqueue:inflate 队列(Guest 向 Host 交还页面)和 deflate 队列(Host 归还页面给 Guest)。

Balloon Inflate(Host 需要内存,要求 Guest 压缩):

Host 设置 num_pages 配置字段(VIRTIO_BALLOON_CONFIG)
    │
    ▼
virtio_balloon_changed() 配置变更中断
    │
    ▼
balloon_fill() [工作队列]
    │
    ├── 循环分配 Guest 物理页(alloc_page)
    ├── 将页面 PFN 写入 inflate virtqueue
    └── virtqueue_add_outbuf() + virtqueue_kick()

Host vhost/QEMU 后端:
    ├── 接收 PFN 列表
    └── 从 Guest 内存中"借用"这些页(Host 可分配给其他用途)
Balloon Deflate(Host 归还内存给 Guest):

Host 减少 num_pages 配置字段
    │
    ▼
balloon_remove_pages() [工作队列]
    │
    ├── 通过 deflate virtqueue 通知 Host 释放页面
    └── __free_page(page) 归还到 Guest 页面分配器

统计信息 virtqueue(VIRTIO_BALLOON_F_STATS_VQ)允许 Host 查询 Guest 内存压力指标:

// include/uapi/linux/virtio_balloon.h
VIRTIO_BALLOON_S_SWAP_IN    // 换入页数
VIRTIO_BALLOON_S_SWAP_OUT   // 换出页数
VIRTIO_BALLOON_S_MAJFLT     // 主缺页错误数
VIRTIO_BALLOON_S_MINFLT     // 次缺页错误数
VIRTIO_BALLOON_S_MEMFREE    // 空闲内存(kB)
VIRTIO_BALLOON_S_MEMTOT     // 总内存(kB)
VIRTIO_BALLOON_S_AVAIL      // 可用内存(kB)
VIRTIO_BALLOON_S_CACHES     // 磁盘缓存(kB)
VIRTIO_BALLOON_S_HTLB_PGALLOC  // 大页分配次数
VIRTIO_BALLOON_S_HTLB_PGFAIL   // 大页分配失败次数

8.3 virtio-mem 机制

virtio-mem 是更现代的内存热插拔方案,驱动在 drivers/virtio/virtio_mem.c

virtio-mem 内存块模型:

Host 暴露一个大的内存区域(plugged_size),划分为 blocks:
  ┌─────────────────────────────────────────────────────┐
  │  virtio-mem 设备内存区域(如 8GB)                  │
  │  ┌──────┬──────┬──────┬──────┬──────┬──────┐       │
  │  │block0│block1│block2│block3│block4│block5│  ...  │
  │  │(已插)│(已插)│(未插)│(未插)│(已插)│(未插)│       │
  │  └──────┴──────┴──────┴──────┴──────┴──────┘       │
  │  每个 block = VIRTIO_MEM_DEFAULT_BLOCK_SIZE (2MB)   │
  └─────────────────────────────────────────────────────┘

virtio-mem 请求类型(通过唯一一个 virtqueue 发送):

// include/uapi/linux/virtio_mem.h
VIRTIO_MEM_REQ_PLUG           // 请求 Host 插入指定 block
VIRTIO_MEM_REQ_UNPLUG         // 请求 Host 拔出指定 block
VIRTIO_MEM_REQ_UNPLUG_ALL     // 拔出所有已插 block
VIRTIO_MEM_REQ_STATE          // 查询 block 状态(已插/未插)

与 Linux memory hotplug 框架集成:

virtio-mem 插入流程:

VIRTIO_MEM_REQ_PLUG (block 3)
    │
    ▼
Host 确认 VIRTIO_MEM_RESP_ACK
    │
    ▼
virtio_mem_add_memory()
    │
    └── add_memory_driver_managed()   // Linux memory hotplug
            │
            ├── arch_add_memory()     // 建立物理内存映射
            ├── online_pages()        // 将页面添加到 Buddy
            └── NUMA 亲和性设置
virtio-mem 拔出流程(比插入复杂):

1. 判断 block 是否可拔出(block 内所有页面可移动?)
   virtio_mem_offline_and_remove_memory()

2. memory_offline_and_remove()
   │
   ├── 扫描 block 内所有页面
   ├── 如有不可移动页 → 失败,放弃拔出
   ├── migrate_pages()  // 将可移动页迁移到其他地方
   └── remove_memory()  // 从 Buddy 移除,取消物理映射

3. VIRTIO_MEM_REQ_UNPLUG (block 3)
   → Host 释放对应 Host 物理页

8.4 Free Page Reporting

virtio-balloon 的 VIRTIO_BALLOON_F_FREE_PAGE_HINT feature 允许 Guest 主动上报其空闲页面,Host 可利用这些信息进行内存去重(KSM)或压缩(memory compression),无需实际 balloon 操作。

Free Page Reporting 流程:

Guest: 扫描 Buddy Allocator 中的空闲页
    │
    ├── 将空闲页 PFN 批量写入 free_page_vq
    ├── Host 接收后 MADV_DONTNEED 对应 Host 物理页
    │   (归还给 Host OS,不影响 Guest 可见内存大小)
    └── 当 Guest 再次分配这些页时,Host 重新分配(可能触发缺页)

9. 性能优化与调优

9.1 中断合并(Interrupt Coalescing)

频繁的中断注入和 VM-exit 是 virtio 性能的主要瓶颈。内核提供多种中断合并机制:

1. VIRTIO_RING_F_EVENT_IDX(精确通知门槛):
   Driver 设置 used_event_idx = last_used_idx + N
   Host 仅当完成第 N 个之后才中断,减少中断频率

2. NAPI 动态中断调制(DIM):
   RX DIM(virtio-net receive_queue.dim_enabled):
   根据 IRQ 速率和包率动态调整 coalesce 参数
   struct dim + struct virtnet_interrupt_coalesce

3. busyloop_timeout(vhost):
   vq->busyloop_timeout(微秒)
   在没有 kick 时,vhost worker 轮询这段时间再睡眠
   适合低延迟场景(牺牲 CPU 换延迟)

4. napi_tx(virtio-net TX NAPI):
   module_param napi_tx = true
   TX 完成通过 NAPI 处理而非直接中断,减少上下文切换

9.2 大页(hugepage)与内存对齐

split ring 大小公式(N=256 队列深度):
  desc ring:   16B × 256  = 4096B  (1 页)
  avail ring:  (2+2+2×256) = 516B  (< 1 页)
  used ring:   (2+2+8×256) = 2052B (< 1 页)
  总计:~6.5KB(对齐后约 8KB,2 个物理页)

packed ring(同样 N=256):
  单环:16B × 256 = 4096B (1 页)
  driver/device event: 4B × 2 = 8B
  总计:~4.1KB(更小,缓存效率更高)

建议:将 virtqueue 内存分配在大页(2MB HugePage)
以避免 TLB 压力(尤其对 vhost-user + DPDK)

9.3 IOMMU 与 DMA API

virtio DMA 路径决策(drivers/virtio/virtio_ring.c:333):

vring_use_map_api():
  │
  ├── !virtio_has_dma_quirk(vdev)  → 使用 DMA API(安全)
  │   (VIRTIO_F_ACCESS_PLATFORM 要求 IOMMU 隔离)
  │
  ├── xen_domain()                 → 使用 DMA API(Xen IOMMU)
  │
  └── 其他                         → 绕过 DMA API(直接物理地址)
      (历史遗留:KVM + QEMU 不需要 IOMMU,物理地址即总线地址)

VIRTIO_F_ACCESS_PLATFORM:
  现代安全部署(如 VFIO 直通)必须启用此 feature
  要求所有 DMA 通过 IOMMU,防止恶意设备 DMA 攻击(DMA remapping)

9.4 IN_ORDER 优化

VIRTIO_F_IN_ORDER feature:设备保证按提交顺序完成请求。

IN_ORDER 优化效果:

不使用 IN_ORDER:
  desc_state[] 需要完整的 per-slot 回调数据
  detach_buf_split() 需要遍历整个描述符链

使用 IN_ORDER(VQ_LAYOUT_SPLIT_IN_ORDER):
  free_head 直接顺序推进(不需要链式空闲链表)
  batch_last 机制:一次 get_buf() 可批量完成多个连续条目
  desc_extra[head].next 不再需要维护
  → 减少 cache miss,提高 I/O 完成吞吐

10. 总结与对比

10.1 virtio 传输层对比

传输层       发现方式        通知机制           典型场景
─────────────────────────────────────────────────────────────
virtio-PCI   PCI 枚举       MMIO Write         服务器虚拟化
             Vendor=0x1AF4  (PCIe MSI/MSI-X)   (KVM/VMware/HyperV)
─────────────────────────────────────────────────────────────
virtio-MMIO  DTS/ACPI       MMIO Write         嵌入式/ARM
             Magic=virt     (GIC 共享中断)     (QEMU ARM virt)
─────────────────────────────────────────────────────────────
virtio-CCW   Channel I/O    CCW interrupt      IBM Z/S390
             Subchannel Set                    (z/VM, KVM on Z)
─────────────────────────────────────────────────────────────

10.2 split ring vs packed ring 完整对比

特性                split ring              packed ring
─────────────────────────────────────────────────────────────
规范版本             virtio 0.9+            virtio 1.1+
内核支持             主线(长期)            v5.0+
内存布局             3 个分离环             1 个合并环
缓存效率             一般(3 处写操作)      优(顺序写)
状态编码             avail/used 分离索引    wrap counter 位
通知抑制             flags + EVENT_IDX      3 种模式 + DESC 精确
间接描述符           支持                   支持
IN_ORDER             支持                   支持
IOMMU 兼容           完整                   完整
硬件 offload 适合性  一般                   更适合(单环 DMA)
调试复杂度           低                     较高

10.3 后端实现对比

后端                  位置      延迟      吞吐      适用场景
────────────────────────────────────────────────────────────
QEMU 用户态           用户态    高        低        开发/调试
vhost-net             内核态    中        高        生产 KVM
vhost-blk             内核态    低        高        高性能存储
vhost-user(DPDK)    用户态    极低      极高      NFV/电信
SR-IOV VF(直通)     N/A       最低      最高      高性能计算

10.4 半虚拟化技术栈全景

+──────────────────────────────────────────────────────────────+
│  Guest Kernel(x86 PV)                                      │
│                                                              │
│  virtio Driver  ◄────────────────  pv_ops 表                │
│  (net/blk/mem)  │                  │                         │
│       │         │ virtqueue        │ CPU/IRQ/MMU hooks       │
│       ▼         │ (共享内存)       │ kvmclock                │
│  vring_virtqueue│                  │ PV spinlock             │
│       │         │                  ▼                         │
│       └─────────┼──> kick (eventfd/MMIO write / hypercall)  │
+─────────────────┼────────────────────────────────────────────+
                  │ VM-exit / hypercall
+─────────────────┼────────────────────────────────────────────+
│  Host Kernel    │                                            │
│                 │                                            │
│  KVM ◄──────────┘ 处理 VM-exit                              │
│   │                                                          │
│   ├── vhost-net worker ─────────────────> TAP/socket        │
│   ├── vhost-blk worker ─────────────────> block device      │
│   └── vhost-user (通过 fd) ──────────────> DPDK process     │
│                                                              │
│  irq_bypass_producer ◄────────────────────────────────────  │
│  (Posted Interrupt)                                          │
+──────────────────────────────────────────────────────────────+

10.5 关键源文件索引

文件 内容
include/uapi/linux/virtio_ring.h vring_desc/avail/used/packed 结构,标志位定义
include/uapi/linux/virtio_config.h VIRTIO_F_* feature bits
include/uapi/linux/virtio_net.h virtio_net_hdr,网络 feature bits
include/uapi/linux/virtio_blk.h virtio_blk_outhdr,块设备请求类型
include/uapi/linux/virtio_balloon.h balloon 统计类型
include/uapi/linux/virtio_mem.h virtio-mem 请求/响应结构
drivers/virtio/virtio_ring.c split/packed ring 完整实现
drivers/virtio/virtio_pci_modern.c PCI 传输层(modern)
drivers/virtio/virtio_mmio.c MMIO 传输层
drivers/net/virtio_net.c virtio-net 驱动(网络)
drivers/block/virtio_blk.c virtio-blk 驱动(块设备)
drivers/virtio/virtio_mem.c virtio-mem 内存热插拔
drivers/virtio/virtio_balloon.c virtio-balloon
drivers/vhost/vhost.c vhost 核心(worker/poll)
drivers/vhost/vhost.h vhost_dev/virtqueue 结构
drivers/vhost/net.c vhost-net(TAP 零拷贝)
drivers/vhost/blk.c vhost-blk
arch/x86/include/asm/paravirt_types.h pv_ops 结构定义,PVOP 宏
arch/x86/kernel/paravirt.c pv_ops 初始化
arch/x86/kernel/kvmclock.c KVM 半虚拟化时钟
arch/x86/kernel/kvm.c KVM Guest 特定初始化(PV spinlock 等)

由 Claude Code 分析生成