基于 Linux Kernel 源码深度分析(kernel 6.x / master 分支) 关键路径:
drivers/virtio/|drivers/net/virtio_net.c|drivers/block/virtio_blk.cdrivers/vhost/|arch/x86/include/asm/paravirt_types.h
- virtio 规范概述
- split virtqueue 深度解析
- packed virtqueue 深度解析
- virtio-net 驱动深度分析
- virtio-blk 驱动分析
- vhost 内核后端
- paravirt_ops 半虚拟化接口
- virtio-mem 与 virtio-balloon
- 性能优化与调优
- 总结与对比
虚拟化技术经历了三个关键演化阶段:
全虚拟化(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 支持
+--------------------------------------------------+
| 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 / ... |
+--------------------------------------------------+
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.c,drivers/virtio/virtio_pci_legacy.c
用于嵌入式环境(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
...
专为 IBM Z 系列大型机设计,使用 Channel Command Word 机制,相关驱动:drivers/s390/virtio/virtio_ccw.c。
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) |
virtio 规范定义了设备初始化必须遵循的状态机,对应 Status 寄存器的位:
RESET (0x00)
│
▼
ACKNOWLEDGE (0x01) ── Guest OS 发现设备
│
▼
DRIVER (0x02) ────── 驱动知道如何驱动该设备
│
▼
FEATURES_OK (0x08) ─ 特性协商完成
│
▼
DRIVER_OK (0x04) ─── 驱动就绪,设备可工作
│
▼ (发生错误时)
FAILED (0x80) ─────── 不可恢复错误,需要重置
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;
}内核在 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,
};单个 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);
}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 读到不完整的描述符。
更新 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。
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 等)
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
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 合并] ← 顺序写,硬件预取友好
单个 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;
};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 → 已完成
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 个描述符处触发"
split ring packed ring
──────────────────────────────────────────────────────
内存区域数 3(分离) 1(合并)
缓存行效率 较差(3处写入) 较好(顺序写)
回绕检测 独立 idx 比较 wrap counter
通知精度 EVENT_IDX 或 flags 三种模式 + DESC
间接描述符 支持 支持
IN_ORDER 支持 支持 支持
硬件 VIRTIO 无要求 适合 NIC 硬件卸载
规范版本 virtio 0.9+ virtio 1.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 │ │
│ └────────────┘ │
└─────────────────────────────────────────────────────────────+
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;
};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,
};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 队列
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)
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)
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)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)
};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)
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)
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;
}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;
}
}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
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/块设备)
↑ 零额外拷贝
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 超时(微秒)
};每个 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;
};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 直接访问,避免额外拷贝。
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)
▼
物理网卡
vhost 通过 irq_bypass_producer(vhost_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。
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
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; // 全局实例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;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),而是操作内存中的影子变量,大幅减少特权指令开销。
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;直接调用 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 跳过此检查。
在虚拟化环境中,自旋锁(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;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(共享内存)
精度 纳秒级 纳秒级
Linux 虚拟机动态内存管理有两条技术路线:
virtio-balloon(传统方案):
Host 告知 Guest "膨胀/放气 N 页"
Guest 主动分配/释放这些页
缺点:需要 Guest 协作;页面选择不优;可能引起 OOM
virtio-mem(现代方案,v5.8+):
Host 以块(block)为粒度管理 Guest 内存
Guest 直接看到内存范围的增减
支持 NUMA 感知,与 memory hotplug 框架集成
不需要 Guest 分配页面
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 // 大页分配失败次数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 物理页
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 重新分配(可能触发缺页)
频繁的中断注入和 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 处理而非直接中断,减少上下文切换
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)
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)
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 完成吞吐
传输层 发现方式 通知机制 典型场景
─────────────────────────────────────────────────────────────
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)
─────────────────────────────────────────────────────────────
特性 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)
调试复杂度 低 较高
后端 位置 延迟 吞吐 适用场景
────────────────────────────────────────────────────────────
QEMU 用户态 用户态 高 低 开发/调试
vhost-net 内核态 中 高 生产 KVM
vhost-blk 内核态 低 高 高性能存储
vhost-user(DPDK) 用户态 极低 极高 NFV/电信
SR-IOV VF(直通) N/A 最低 最高 高性能计算
+──────────────────────────────────────────────────────────────+
│ 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) │
+──────────────────────────────────────────────────────────────+
| 文件 | 内容 |
|---|---|
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 分析生成