Linux内核分析之虚拟化-02

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 实现。