虚拟化技术virtio 笔记

DPDK学习: KNI 原理解析 - 知乎 (zhihu.com)

虚拟化技术 — VirtIO 虚拟设备接口标准 - 知乎 (zhihu.com)

VirtIO 由 Rusty Russell 开发,最初是为了支持自己开发的 lguest Hypervisor,其设计目标是在虚拟化环境下提供与物理设备相近的 I/O 功能和性能,并且避免在虚拟机中安装额外的驱动程序。基于这一目标,后来通过开源的方式将 VirtIO 延伸为一种虚拟化设备接口标准,并广泛的支持 KVM、QEMU、Xen 和 VMware 等虚拟化解决方案。

为了追求更高的性能和更低的开销表现,VirtIO 采用了 Para-virtualizaiton(半虚拟化/准虚拟化)技术路线,本质区别于性能低下的 Full-virtualizaiton(e.g. QEMU I/O Emulation)。这也意味着使用 VirtIO 的 GuestOS 需要经过一定的代码修改(安装非原生的 Device Driver),这也是为什么需要将 VirtIO 定位于 “虚拟化设备接口标准“ 的原因,其整体的集成架构需要由 Front-end(GuestOS Kernel Device Driver)和 Back-end(VMM Provider)共同遵守统一的传输协议。

在应用 VirtIO 的场景中,除了 GuestOS 需要安装额外的 VirtIO Front-end Device Driver 这一点不足之外(通常在制作 QCOW2 镜像时会安装好),相对的带来了下列诸多好处:

  1. 高性能:VirtIO 省去了 Full-virtualizaiton 模式下的 Traps(操作捕获)环节,GuestOS 通过 VirtIO Interfaces 可以直接与 Hypervisor 中的 Device Emulation 进行交互。
  2. 低开销:VirtIO 优化了 CPU 在内核态和用户态之间频繁切换,以及 CPU 在 VM Exit 和 VM Entry 之间频繁陷入陷出所带来的性能开销。
  3. 标准化:VirtIO 实现了统一的虚拟设备接口标准,可以应用在多种虚拟化解决方案中。

VirtIO 虚拟设备接口标准

为了抑制 Para-virtualizaiton 所具有的跨平台兼容性缺点,VirtIO 明智的走上了开源之路,通过构建标准而统一的生态来争取最小化的兼容性排斥问题,反而让其逆转成为了一种优势。

在 VirtIO 提出的虚拟化设备接口标准中,包括多个子系统,每个子系统都定义了一组虚拟设备类型和协议。例如:

  • VirtIO-Block(块设备):提供虚拟磁盘设备的接口。
  • VirtIO-Net(网络设备):提供虚拟网络设备的接口。
  • VirtIO-Serial(串口设备):提供虚拟串口设备的接口。
  • VirtIO-Memory(内存设备):提供虚拟内存设备的接口。
  • VirtIO-Input(输入设备):提供虚拟输入设备的接口。

此外还有 VirtIO-SCSI、VirtIO-NVMe、VirtIO-GPU、VirtIO-FS、VirtIO-VSock 等等虚拟设备类型和协议。

VirtIO 的前后端分层架构标准

VirtIO 虚拟设备接口标准的分层软件架构如下图所示:

  • Front-end:是一组通用的,安装在 GuestOS Kernel 中的 VirtIO Device Driver,通过 Hyper-call 的方式对 Back-end 进行调用。
  • Back-end:是一组 Hypervisor 专用的,运行在 VMM 中的设备模拟程序,提供了 Hyper-call 调用接口。
  • Transport:是一组标准的传输层接口,基于环形队列的方式来批量处理 I/O 请求

VirtIO 的数控路径分离架构标准

VirtIO 还定义了 “控制路径” 和 “数据路径” 的分离架构,两者的侧重各有不同,控制平面追求尽可能的灵活以兼容不用的设备和厂商,而数据平面则追求由更高的转发效率以快速的交换数据包。

  • 控制路径:控制建立或删除 Front-end 和 Back-end 之间的数据路径,提供可管理的灵活性。
  • 数据路径:在 Front-end 和 Back-end 之间进行数据传输,追求性能。

对应的,VirtIO 标准也可以分为两个部分:

  • VirtIO Spec:定义了如何在 Front-end 和 Back-end 之间构建控制路径和数据路径的标准,例如:数据路径规定采用环形队列缓冲区布局。
  • Vhost Protocol:定义了如何降数据路径的高性能实现标准,例如:基于 Kernel、DPDK、SmartNIC Offloading 的实现方式。

以 QEMU 为例,其根据 VirtIO Spec 实现了控制路径,而数据路径则可以 Bypass QEMU,使用 vhost-net、vhost-user 等实现。

VirtIO 的传输层标准

VirtIO 的传输层标准称之为 Virtqueues。以 QEMU VirtIO-Net 为例,对于一个虚拟网卡设备而言,采用了非常类似于物理网卡设备(NIC Rx/Tx Queues 和 Kernel Rx/Tx Rings)的设计思路。

Front-end 和 Back-end 之间存在 2 个 Virtqueues,对应到 NIC 的 Rx/Tx Queues。Virtqueues 的底层利用了 IPC 共享内存技术,在 HostOS 和 GuestOS 之间建立直接的传输通道,绕开了 VMM 的低效处理。

  1. Receive Queue:是一块 Buffer 内存空间,存放具体的由 Back-end 发送到 Front-end 数据。
  2. Send Queue:是一块 Buffer 内存空间,存放具体的由 Front-end 发送到 Back-end 数据。

同时,每个 Virtqueue 都具有 2 个 Vrings,对应到 Kernel 的 Rx/Tx Rings:

  1. Available Ring:存放 Front-end 向 Back-end 发送的 Queue 中可用的 Buffer 内存地址。
  2. Used Ring:存放 Back-end 向 Front-end 发送的 Queue 中已使用的 Buffer 内存地址。

当然,Front-end 和 Back-end 之间还需要一种双向通知机制,对应到 NIC 和 CPU 之间的硬中断信号:

  1. Available Buffer Notification:Front-end 使用此信号通知 Back-end 存在待处理的缓冲区。
  2. Used Buffer Notification:Back-end 使用此信号标志已完成处理某些缓冲区。

具体的,当 GuestOS 发送数据时,Front-end 将数据填充到 Send Queue 内存空间,然后将地址记录到 Available Ring,并在适当的时候向 Back-end 发送 Available Buffer Notification;QEMU 接收到通知后,从 Available Ring 中获取到数据的数据,完成数据接收,然后记录 New Buffer Head Index 到 Used Ring,并在适当的时候向 Front-end 发送一个 Used Buffer Notification。Front-end 接收到通知后释放对应的 Send Queue 内存空间,Available Ring 指针指向下一位。

当 GuestOS 接收数据时,Front-end 首先分配好 NULL 的 Receive Queue 内存空间,然后将地址记录到 Available Ring,并在适当的时候向 Back-end 发送 Available Buffer Notification;QEMU 接收到通知后,从 Available Ring 中获取到数据填充入口地址,完成数据填充,然后记录 New Buffer Head Index 到 Used Ring,并在适当的时候向 Front-end 发送一个 Used Buffer Notification。Front-end 接收到通知后从 Receive Queue 中读取数据,Available Ring 指针指向下一位。

值得注意的是,在 PCI Passthrough 场景中,可以使用 virtio-pci(更多的是使用 VFIO)。虽然此时依旧会沿用了 Available/Used buffer notification 这一控制面的双向通知机制,但实际的数据面,则是由 DMA 和专用队列来完成的,具有更高的性能。这体现了数控分离带来的好处。

VirtIO 标准在 Linux 中的实现

在连接了 VirtIO 虚拟设备接口标准中,前后端分层(横向)、数控路径分离(纵向)架构之后,再回头看 VirtIO 在 Linux 中的具体实现方式。如下图所示:

  1. 驱动层(Front-end):安装到 GuestOS Kernel 中的各种 I/O 设备的驱动程序。例如:virtio-net、virtio-blk。
  2. 传输层:定义了控制面和数据面的传输标准,例如:vhost-net(Kernel)、vhost-user(DPDK)、virtio-pci(PCI、PCIe)。
    1. 控制平面通信层(virtio):用于 Front-end 与 Back-end 之间进行控制信令的交换和协商(数据结构、Notify 等通信机制),如上述 Vings,控制数据平面的建立/关闭。
    2. 数据平面通信层(Transport):用于 Front-end 与 Back-end 之间进行数据交换,如上述 Virtqueues。
  3. 设备层(Back-end):运行在后端 Hypervisor 上的设备模拟程序,或是一个真实的硬件设备。

kvm

kvm是linux内核中的一个模块,这个模块的主要功能是开启vmx功能(基于硬件的完全虚拟化),使Linux主机成为一个hypervison。kvm模块有两个能力,一个是上面说的开启vmx功能,一个是提供用户态的使用的接口。

KVM/QEMU/qemu-kvm/libvirt 概念全解_51CTO博客_QEMU-KVM

qemu

qemu本身并不依赖kvm,qemu是一个纯软件实现的虚拟化软件,可以单独运行,但效率低。

qemu + kvm

硬件技术的发展,促进了虚拟化软件的发展,单纯的使用纯软件模拟虚拟化,无法充分利用硬件的虚拟化能力,社区通过修改qemu的代码,通过qemu调用kvm提供的硬件能力(比如cpu虚拟化能力,内存虚拟化能力),从而显著的提高虚拟化效率。

虚拟化技术 — VirtIO 虚拟设备接口标准 - 知乎 (zhihu.com)

  • VirtIO
  • VirtIO 虚拟设备接口标准
    • VirtIO 的前后端分层架构标准
    • VirtIO 的数控路径分离架构标准
    • VirtIO 的传输层标准
  • VirtIO 标准在 Linux 中的实现
  • QEMU与KVM架构介绍 - 知乎 (zhihu.com)

  • QEMU与KVM架构介绍

KVM/QEMU/qemu-kvm/libvirt 概念全解_51CTO博客_QEMU-KVM

Linux云计算底层技术之一文读懂 Qemu 模拟器 - 知乎 (zhihu.com)

下面通过一个虚拟机启动过程看看 Qemu 是如何与 KVM 交互的。

// 第一步,获取到 KVM 句柄
kvmfd = open("/dev/kvm", O_RDWR);
// 第二步,创建虚拟机,获取到虚拟机句柄。
vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0);
// 第三步,为虚拟机映射内存,还有其他的 PCI,信号处理的初始化。
ioctl(kvmfd, KVM_SET_USER_MEMORY_REGION, &mem);
// 第四步,将虚拟机镜像映射到内存,相当于物理机的 boot 过程,把镜像映射到内存。
// 第五步,创建 vCPU,并为 vCPU 分配内存空间。
ioctl(kvmfd, KVM_CREATE_VCPU, vcpuid);
vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
// 第五步,创建 vCPU 个数的线程并运行虚拟机。
ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0);
// 第六步,线程进入循环,并捕获虚拟机退出原因,做相应的处理。
for (;;) {
ioctl(KVM_RUN)
switch (exit_reason) {
case KVM_EXIT_IO:  /* ... */
case KVM_EXIT_HLT: /* ... */
}
}
// 这里的退出并不一定是虚拟机关机,
// 虚拟机如果遇到 I/O 操作,访问硬件设备,缺页中断等都会退出执行,
// 退出执行可以理解为将 CPU 执行上下文返回到 Qemu。

Qemu 源码结构

Qemu 软件虚拟化实现的思路是采用二进制指令翻译技术,主要是提取 guest 代码,然后将其翻译成 TCG 中间代码,最后再将中间代码翻译成 host 指定架构的代码,如 x86 体系就翻译成其支持的代码形式,ARM 架构同理。

所以,从宏观上看,源码结构主要包含以下几个部分:

  • /vl.c:最主要的模拟循环,虚拟机环境初始化,和 CPU 的执行。
  • /target-arch/translate.c:将 guest 代码翻译成不同架构的 TCG 操作码。
  • /tcg/tcg.c:主要的 TCG 代码。
  • /tcg/arch/tcg-target.c:将 TCG 代码转化生成主机代码。
  • /cpu-exec.c:主要寻找下一个二进制翻译代码块,如果没有找到就请求得到下一个代码块,并且操作生成的代码块

virtio简介(一)—— 框架分析

在传统的设备模拟中,虚拟机内部设备驱动完全不知道自己处在虚拟化环境中,所以I/O操作会完整的走 虚拟机内核栈->QEMU->宿主机内核栈,产生很多VM Exit和VM Entry,导致性能很差。Virtio方案旨在提高I/O性能。在改方案中虚拟机能够感知到自己处于虚拟化环境中,并且会加载相应的virtio总线驱动和virtio设备驱动,执行自己定义的 协议进行数据传输,减少VM Exit和VM Entry操作。

VirtIO由 Rusty Russell 开发,对准虚拟化 hypervisor 中的一组通用模拟设备IO的抽象。Virtio是一种前后端架构,包括前端驱动(Guest内部)、后端设备(QEMU设备)、传输协议(vring)。框架如下图所示:
    前端驱动:
      虚拟机内部的 virtio模拟设备对应的驱动。作用为 接收用户态的请求,然后按照传输协议对请求进行封装,再写I/O操作,发送通知到QEMU后端设备。
    后端设备:
      在QEMU中创建,用来接收前端驱动发送的I/O请求,然后按照传输协议进行解析,在对物理设备进行操作,之后通过终端机制通知前端设备。
    传输协议:
      使用virtio队列(virtio queue,virtqueue)完成。设备有若干个队列,每个队列处理不同的数据传输(如virtio-balloon包含ivq、dvq、svq三个)。
      virtqueue通过vring实现。Vring是虚拟机和QEMU之间共享的一段环形缓冲区,QEMU和前端设备都可以从vring中读取数据和放入数据。

从代码上看,virtio的代码主要分两个部分:QEMU和内核驱动程序。Virtio设备的模拟就是通过QEMU完成的,QEMU代码在虚拟机启动之前,创建虚拟设备。虚拟机启动后检测到设备,调用内核的virtio设备驱动程序来加载这个virtio设备。

    对于KVM虚拟机,都是通过QEMU这个用户空间程序创建的,每个KVM虚拟机都是一个QEMU进程,虚拟机的virtio设备是QEMU进程模拟的,虚拟机的内存也是从QEMU进程的地址空间内分配的。

    VRING是由虚拟机virtio设备驱动创建的用于数据传输的共享内存,QEMU进程通过这块共享内存获取前端设备递交的IO请求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值