Linux内核分析之虚拟化-02
This language version is unavailable; showing the other language.
51.1 virtio 规范与 virtqueue
virtio 是"为虚拟化而生"的设备标准:客户机与宿主约定一套共享内存的环形队列协议(virtqueue),客户驱动填描述符、宿主后端消费,通知走最低成本的路径(共享内存标志或单次 MMIO)。它消灭了"模拟真实硬件"(模拟一个 e1000 意味着每包都走真实网卡的寄存器协议)的低效,是半虚拟化 I/O 的事实标准。本节解剖 virtqueue 的描述符环、通知机制与传输层(PCI/MMIO/VDPA)。
51.1.1 virtqueue:拆分环
split virtqueue 的三段结构 (共享内存, 客户与宿主同视):
描述符表 desc[idx] (每项 16B):
addr/len/flags(NEXT/WRITE/INDIRECT)/next
→ "一块缓冲的地址与长度" (可链成链: 多段请求)
可用环 avail (客户→宿主的方向):
idx + 环形数组[desc 索引] + avail_event
→ 客户说"这些描述符链给你了"
已用环 used (宿主→客户的方向):
idx + 环形数组[desc 索引, 写入长度] + used_event
→ 宿主说"这些链我处理完了"
一次块读请求的走线:
客户 virtio_blk 驱动:
组描述符链: [blk 头(读)]→[数据缓冲(写)]→[状态(写)]
填 avail → 通知宿主 (virtio_pci 的 MMIO notify,
一次写! 或 event_idx 省略通知)
宿主 (QEMU/vhost):
消费 desc 链 → 完成 IO → 填 used → 中断客户
客户收中断 → used 环取回 → 完成回调
空间成本: 每队列 ~几 KB 共享内存; 通知成本:
单次 MMIO 写 (~几百 ns) — 对照 49.3 节
"模拟真网卡寄存器协议"的数十次 MMIO
// include/linux/virtio.h:34(内核侧抽象, 节选要点)
struct virtqueue {
struct list_head list;
void (*callback)(struct virtqueue *vq); /* 宿主处理后客户侧回调 */
const char *name;
struct virtio_device *vdev;
unsigned int index;
...
bool event; /* event_idx 通知抑制 */
...
};
// :168
struct virtio_device {
int index;
bool registered;
struct device dev; /* 39 章模型设备! */
const struct virtio_device_id *id;
const struct config_ops *config;
struct vringh *vrh;
...
};
virtio_device 内嵌 struct device(39.1.1 节 kobject 基类)——virtio 设备是标准设备模型公民:probe/match/sysfs/电源管理全套可用。VIRTIO_F_VERSION_1(virtio_config.h:67)是 1.0+ 规范的协商位——队列布局与通知语义的版本协商经由配置空间的 feature bits 完成。
51.1.2 通知抑制与 packed 环
通知的开销消除:
event_idx (VIRTIO_RING_F_EVENT_IDX):
双方各自记录"我处理到第几个", 只在越过
对方关心的门槛时才通知 — 批处理下的通知
次数 ÷N (45 章 TCP 窗口式思想)
packed virtqueue (VIRTIO_F_RING_PACKED):
描述符与可用/已用合并为单环 (缓存友好,
无需三段内存), wrap 标志判空满
ordering 与中断聚合: used_event门槛控制
中断频率 (44.3 节 coalescing 思想)
51.1.3 传输层:PCI/MMIO/VDPA
三种传输 (drivers/virtio/ 的文件地图):
virtio_pci_modern.c PCIe 上的 virtio (现代 caps 布局,
common isr/config/notify 各自 BAR 区)
virtio_pci_legacy.c 旧式单 BAR 协议 (兼容)
virtio_mmio.c 嵌入式 MMIO 总线 (无 PCI 的 SoC)
virtio_vdpa.c vdpa 总线 (vhost 数据面下沉, 51.3)
设备发现: PCI vendor=0x1af4 (Red Hat, 标准)
device id 0x1000+ (legacy) / 0x1040+ (modern)
→ 39.4 节标准 probe 流程 → virtio_blk/net/... 驱动
客户内 "lspci 看得到 virtio 网卡/磁盘" —
因为它就是真的 PCI 设备 (只是行为是约定的环)
virtio 的消费者清单:virtio_blk(磁盘)、virtio_net(网卡)、virtio_console、virtio_balloon(内存气球:宿主挤占客户内存,12 章内存超卖的关键件)、virtio_mem/virtio_input/virtio_rtc——每个都是标准 Linux 驱动,位于 drivers/ 对应目录(41 章块/44 章网的驱动框架复用)。
小结
virtio 以"共享内存拆分环(desc/avail/used)+ 一次 MMIO 通知 + event_idx 抑制"定义客户-宿主的 I/O 协议,VIRTIO_F_VERSION_1 与 feature bits 完成版本协商,packed 环进一步压缓存足迹;传输层覆盖 PCI/MMIO/VDPA,设备发现走标准 PCI 厂商号与 39 章设备模型。它把"模拟真实硬件"的低效替换为"约定的共享内存协议"——半虚拟化的本质收益。下一节看设备侧实现。
51.2 virtio 设备实现 —— net、blk、console
virtio 的三个旗舰设备展示了同一队列协议的三种用法:virtio_net 用两个队列对收发网络包并支持多队列 RSS;virtio_blk 用单队列的描述符链表达块请求(41.2 节 request 的 virtio 形态);virtio_console 用最小队列提供串口。本节解剖三个设备驱动如何把通用框架接到 Linux 的网络/块/TTY 子系统。
51.2.1 virtio_net:队列对与多队列
virtio_net 的队列布局:
recv 队列对 (RX): 客户预投空缓冲 → 宿主填数据 → 客户收
send 队列对 (TX): 客户填包 → 宿主消费发出 → 完成回报
控制队列 (ctrl vq): MAC 表/杂项命令 (少走)
多队列 (MQ): 每CPU一对收发队列 + RSS 哈希分派
(44.2 节多队列网卡思想的 virtio 版)
收包路径 (与 44.3 节 NAPI 对接):
预投: fill 描述符链 [头(1B)][包缓冲页]
宿主写入 → used 环 → 客户中断 → NAPI poll
→ virtqueue_get_buf → skb 组装 → netif_receive_skb
(page_pool/xdp 变体: 直接给 XDP 程序处理, 44.3.5)
offload 协商 (44.2.3 节 features 的 virtio 版):
VIRTIO_NET_F_GUEST_TSO4/6, F_CSUM — 宿主替客户
做分段/校验 (客户省 CPU), feature bits 决定
51.2.2 virtio_blk:块请求的描述符链
一次读请求的描述符链 (41.2 节 queue_rq 的翻译产物):
[desc0] virtio_blk_outhdr {type=IN, sector, ...} (读方向: 设备写)
[desc1] 数据缓冲 (4KB, WRITE 标志=设备写)
[desc2] u8 status (完成状态)
三段链一次 avail 通知 → 宿主 QEMU/vhost 转成
宿主侧真实 IO (qemu: aio 到镜像文件; vhost: 直接
提交到底层块设备) → used 环回报 status
与 41 章块层的对接:
virtio_blk 驱动注册 blk-mq 队列 (tag_set,
41.1.1 节) — queue_rq = "把 request 翻成描述符链"
discard/write-zeroes 等命令也是专属链类型
virtio_scsi 是需要完整 SCSI 栈(LUN/多 target)时的替代——多一个 control/event 队列与 SCSI 命令封装;virtio_blk 则是精简高性能路径(云盘的默认形态)。
51.2.3 virtio_console/balloon:控制面设备
非数据面设备的两种代表:
virtio_console (串口):
最小实现: 一对队列 + TTY 驱动对接
→ 容器/VM 的 console 输出 (无真实串口硬件)
virtio_balloon (内存气球, 12 章超卖的执行器):
宿主"充气" → 客户驱动 alloc_pages 占住内存并
把页表报给宿主 → 宿主拿走这些物理页
放气 → 客户收回 (客户可归还的内存池)
free page reporting 变体: 客户主动报"这些页
我不用" → 宿主回收 → 客户再要时零页返回
(与 18.5 节热插拔/ 12 章 memory.max 组成
宿主内存弹性的完整工具箱)
| 设备 | 队列 | 对接子系统 | 关键特性位 |
|---|---|---|---|
| virtio_net | RX/TX×N + ctrl | netdev/NAPI(44 章) | GUEST_TSO/CTRL_MQ/RSS |
| virtio_blk | 1+N | blk-mq(41 章) | SEG_MAX/SCSI/DISCARD |
| virtio_scsi | cmd+ctrl+event | SCSI 栈 | 多 LUN |
| virtio_console | port 对 | TTY | 多端口 |
| virtio_balloon | inflate/deflate/stat | mm 弹性 | FREE_PAGE_REPORTING |
实现位置:设备驱动在 drivers/block/virtio_blk.c、drivers/net/virtio_net.c——它们是标准 Linux 驱动(41/44 章框架),virtio 核心只提供 virtqueue 抽象层——"半虚拟化设备也是标准驱动"的架构验证。
小结
三个旗舰设备演示同一队列协议的三种接法:virtio_net 的多队列对接 NAPI/RSS、virtio_blk 的描述符链翻译 blk-mq 请求、virtio_console/balloon 的控制面用法(balloon 是 12 章内存超卖的执行器)。所有 virtio 驱动都是标准 Linux 驱动(复用 41/44 章框架),virtio 核心只是 virtqueue 适配层——设备的"虚拟性"被完整封装在传输层之下。下一节看宿主侧的数据面下沉——vhost。
51.3 Vhost 与 Vhost-VDPA
vhost 把 virtqueue 的宿主侧消费端从 QEMU 用户态下沉到内核线程:数据面(描述符消费、IO 提交)不再穿越"客户→KVM→QEMU→宿主设备"的两次上下文切换,而是内核 vhost worker 直接处理——延迟减半、吞吐翻倍。Vhost-VDPA 更进一步把数据面下沉到硬件(DPU/智能网卡)。本节解剖 vhost 的内核协议与数据面下沉谱系。
51.3.1 vhost:内核态的后端
// drivers/vhost/vhost.h:94(节选要点)
struct vhost_virtqueue {
struct vhost_dev *dev;
...
struct vhost_vring_virtqueue virtqueue; /* 环的内核态视图 */
...
struct vhost_poll poll; /* 客户 kick 的监听 */
...
};
// include/uapi/linux/vhost.h
#define VHOST_SET_MEM_TABLE _IOW(VHOST_VIRTIO, 0x03, struct vhost_memory) /* :38 */
#define VHOST_SET_VRING_KICK _IOW(VHOST_VIRTIO, 0x20, struct vhost_vring_file) /* :109 */
vhost 的建立协议 (QEMU 经 /dev/vhost-net ioctl):
VHOST_SET_MEM_TABLE ← 告诉内核"客户内存的 GPA→宿主 va 映射"
(QEMU 的 memory slot 镜像 — 49.1 节 slot 的 vhost 版)
VHOST_SET_VRING_KICK ← eventfd (32.1 节!) 接收客户通知
VHOST_SET_VRING_CALL ← eventfd 回传完成 (中断注入通道)
VHOST_SET_FEATURES ← 协商 feature bits (51.1 节)
数据面 (KVM 加速下):
客户写 avail → EPT 缺页? → KVM 发现该 GPA 是
virtio notify 区 → 无需 QEMU: vhost 的 eventfd
被唤醒 (KVM 内核态直连!)
→ vhost worker 线程消费 desc → 直接发宿主网络
(tap/物理口) → 完成 → CALL eventfd → KVM 注入
客户中断
对比 QEMU 后端: 客户退出 → QEMU 线程唤醒 → 处理 →
ioctl 注中断 — 两次上下文切换被 eventfd 直连消除
eventfd 是 vhost 的神经(32.1 节的招牌应用):KICK/ CALL 两个 eventfd 把"客户通知"与"完成注入"都变成内核可等可写的文件——QEMU 退化为控制面(配置/热迁移/错误处理),数据面全程内核。
51.3.2 VDPA:数据面下沉到硬件
VDPA (virtio data path acceleration) 的谱系:
QEMU 用户态后端 → vhost 内核后端 → vDPA 硬件后端
(全软件, 两切换) (数据面内核) (数据面在 DPU/网卡)
│
虚拟化谱系对照: virtio 协议被硬件原生实现
全模拟(慢) → 半虚拟化 virtio → vhost → vDPA → SR-IOV 直通
(兼容递减, 性能递增 — SR-IOV 最快但失去
热迁移/快照等虚拟化能力; vDPA 是折中:
硬件速度 + virtio 语义 + 可迁移)
vDPA 内核框架 (drivers/virtio/virtio_vdpa.c +
drivers/vdpa/): 把"硬件 virtio 兼容设备"包装成
标准 virtio_device (51.1.2 节) — 客户/宿主驱动
无感知; 管理面 (配置/生命周期) 走 vhost 系 ioctl
51.3.3 选型与观测
| 后端 | 数据面 | 通知路径 | 热迁移 | 典型场景 |
|---|---|---|---|---|
| QEMU 用户态 | QEMU 线程 | 退出→QEMU→ioctl 注入 | 完整 | 兼容优先 |
| vhost-net | 内核 worker | KVM↔eventfd 直连 | 完整 | 通用默认 |
| vhost-user (DPDK/OVS) | 独立用户态进程 | 共享内存+eventfd | 支持 | NFV/用户态网络栈 |
| vDPA | DPU/智能网卡硬件 | 硬件 | 受限 | 100G+ 高性能 |
| SR-IOV VF | 硬件直通 | 硬件中断 | 差(需特殊处理) | 极致性能 |
观测:
/sys/kernel/debug/vhost/ 队列深度/轮询统计 (37.3)
qemu 监控 info network 后端形态
perf: vhost_worker 线程 CPU 占比 — 数据面位置的证据
小结
vhost 把 virtqueue 的宿主消费端下沉内核:VHOST_SET_MEM_TABLE 镜像客户内存映射、KICK/CALL 两个 eventfd(32.1 节)把通知与中断注入变成内核直连——QEMU 退化为控制面;vDPA 把数据面再下沉到硬件(DPU 原生 virtio),与 SR-IOV 构成"兼容-性能"谱系的另一端。虚拟化 I/O 的演进史(全模拟→virtio→vhost→vDPA→直通)就是上下文切换与数据拷贝的消除史。第九部分(虚拟化)三章至此完成;下一部分进入安全模块——LSM 框架与 SELinux/AppArmor 两大 MAC 实现。