设备虚拟化技术,一直是云计算领域最重要的基础技术之一。我们在虚拟机里面看到的形形色色的设备,比如:网卡,磁盘,键盘,鼠标等,都离不开这项技术的帮助。这篇文章,我们将从技术演进的角度来谈一谈 Linux 现有的以及即将到来的设备虚拟化技术。
Trap-and-emulate
在最早期阶段,设备虚拟化常常和机器模拟器技术,比如:QEMU,绑定在一起。我们可以通过 QEMU 模拟真实设备的所有寄存器布局和操作流程,当 QEMU 虚拟机里面设备驱动需要访问该虚拟设备的寄存器时,这条访问指令会被 trap 到 QEMU,由 QEMU 来进行处理。这样,虚拟机里面的设备驱动在操作该虚拟设备时就像在访问真实的硬件设备一样,设备驱动也不需要任何变更。
Virtio
通过上述这种 trap-and-emulate 的方式来模拟设备,虽然不需要对真实设备驱动进行变更,但是设备访问过程中,频繁的陷入&陷出带来了严重的性能问题,因此,virtio 这类半虚拟化技术应运而生,并于 2008 年合入 Linux 内核主线。
相较于 trap-and-emulate 这种方式,Virtio 不再拘泥于依赖已有的设备驱动,而是定义了一套全新的专用于虚拟设备的驱动框架。设备驱动清楚自己是在操作虚拟设备,因此,在真正的 I/O 路径上规避了大量可能导致陷入&陷出的 mmio/pio 操作,从而提高了性能。虚机里面的 Virtio 驱动和 QEMU 模拟的 Virtio 设备的数据交互,本质上是一套基于共享内存 + 环形队列的通信机制。核心数据结构(split virtqueue)包括:两个 ringbuffer (avail ring, used ring) 和一个 descriptor table。工作机制类似 DMA,虚机内 virtio 驱动首先会将一个请求在内存中需要传输的散列 buffer 的地址、长度组成一个个描述符写入到 descriptor table 中,然后将这些描述符对应的 descriptor table 上的 index 写入到 avail ring 中,并通过 eventfd 机制通知到宿主机上的 virtio backend。由于这些 ringbuffer、descriptor table 以及散列 buffer 都在共享内存中(虚机本质上是一个用户态进程,因此虚机内存是由用户态申请和分配,并可以 share 给其他进程,如:SPDK,DPDK 等),因此,Virtio Backend 可以直接访问并获取到散列 buffer 的地址、长度,进而直接存取这些散列 buffer。当处理完请求之后,Virtio Backend 将数据填充到相应的 buffer,并将对应的 descriptor table index 写入 used ring 中,并通过 eventfd 机制注入中断通知虚机内 virtio 驱动。
Vhost
Virtio 技术提出之后,Virtio 设备通常是在 QEMU 里面来进行模拟,数据收发都需要经过 QEMU,再到虚机内部。但逐渐地,开发者们发现,当模拟网卡时,在 QEMU 里面进行数据收发,最终都需要通过系统调用的方式,陷入内核来操作网卡硬件进行实际的数据收发。那么,QEMU 到内核的这次上下文切换以及带来的额外拷贝开销,有没有办法优化呢?
Linux 内核社区最终在 2010 年合入了 vhost 这个技术来进行优化,通过将 virtio 的数据面 offload 到一个内核线程来进行处理,这样 virtio 通信机制从原本的 QEMU 用户态 I/O 线程和虚机驱动(QEMU 用户态 vcpu 线程)通信变成了 vhost 内核 I/O 线程和虚机驱动(QEMU 用户态 vcpu 线程)通信。vhost 内核 I/O 线程拿到数据包之后,直接走内核协议栈和网卡驱动进行处理,从而优化掉了 QEMU 到内核态的额外开销。
VFIO
随着云计算规模的不断扩大,用户一方面不再满足于 Virtio 这类半虚拟化设备带来的性能体验,另一方面 GPU 这类很难进行 virtio 化的设备应用场景与日俱增。在这种背景下,VFIO 这项技术被提出并在 2012 年合入 Linux 内核主线。VFIO 全称是 Virtual Function I/O,它实际是一个用户态设备驱动框架,相较于更早的 uio 这个用户态设备驱动框架,VFIO 能够有效利用硬件 IOMMU 机制进行安全隔离,从而能够广泛地应用在云计算这类有多租户需求的场景中。
如上图所示,通过 VFIO,QEMU 能够直接将自己虚拟出的一个 PCI 设备和一个物理 PCI 设备的数据链路直接打通,当虚拟机里面的设备驱动访问虚拟 PCI 设备的 bar 空间时,通过 EPT 机制,这次 mmio 访问会被重定向到真实物理设备相应的 bar 空间位置,而不再需要 trap 到 QEMU。这样,虚机驱动就相当于可以以接近零消耗的方式直接访问真实物理设备,性能可以达到最佳。同时,VFIO 驱动利用 IOMMU 实现了设备 DMA 和中断的重映射,一方面起到隔离作用,即某个虚机无法操作 VFIO 直通设备向同一宿主机上的其他虚机发起 DMA 和中断,一方面也保证了设备进行 DMA 时通过给定的虚机物理地址能够直接访问到正确的物理内存。
Vhost-user
虽然,VFIO 能够给虚机带来接近物理机的 I/O 性能体验,但该技术仍存在一个缺陷,即不支持热迁移,带 VFIO 设备的虚机将没法像传统带 virtio 设备的虚机那样进行热迁移。这就使得开发者们,又开始探寻新的既能满足性能需求又具备运维灵活性的设备虚拟化技术。2014 年 QEMU 社区合入的 vhost-user 技术就是其中之一,由于 QEMU 和 vhost 的线程模型对 I/O 性能的优化并不友好,而且由每个虚机单独分出线程来处理 I/O 这种方式从系统全局角度来看可能也并不是最优的,因此,vhost-user 提出了一种新的方式,即将 virtio 设备的数据面 offload 到另一个专用进程来处理。这样,由于是专用进程,线程模型不再受传统 QEMU 和 vhost 线程模型制约,可以任意优化,同时,还可以以 1:M 的方式同时处理多个虚机的 I/O 请求,而且相较于 vhost 这种内核线程方式,用户进程在运维方面更加具备灵活性,vhost-user 框架在提出之初,就受到广泛关注,并引申出了 SPDK 和 OVS-DPDK 这类以 polling + 用户态驱动为核心的新的虚机 I/O 服务模型。
VFIO-mdev
VFIO 技术在实际应用场景,除了之前提到的不支持热迁移的问题外,还有一个限制就是一个设备只能透传给一个虚机,无法做到资源共享。SR-IOV 技术从某种程度上能够解决这个问题,即将一个物理 PCI 设备从硬件层面进行资源划分,划分成多个 VF,透传给多个虚机进行使用,但是有很多设备可能并不具备 SR-IOV 能力,因此,Linux 内核社区在 2016 年合入了 VFIO-mdev 这个技术框架,希望提供一个标准的接口来帮助设备驱动实现软件层面的资源切分并能够利用 VFIO 技术透传给虚机。
该技术本质上是在内核实现了一个虚拟设备(Mediated device)总线驱动模型,并在 VFIO 内核框架上进行了扩展,增加了对 mdev 这类虚拟设备的支持(mdev bus driver),从原来只支持从标准的硬件 PCI 设备和硬件 platform 设备获取透传信息,比如:PCI bar 空间,变成了既支持直接从硬件设备获取又可以从 mdev 设备驱动定义的虚拟设备接口来获取。这样,比如,当需要将一个 PCI 设备的 bar 空间作为资源切分的话,通过实现合适的 mdev 设备驱动,就可以将 bar 空间以 4KB(页面大小)为粒度,分别透传给不同虚机使用。
vDPA
VFIO 和 virtio 这两类技术一直是最主流的设备虚拟化技术。VFIO 能够直接将硬件资源透传给虚机使用,性能最佳,virtio 性能稍逊,但胜在更加灵活。那有没有可能将两者的优势结合起来呢?2020 年被合入 Linux 内核主线的 vDPA 技术框架,就是为了实现这一目标。
vDPA 的全称是 Virtio Data Path Acceleration,它表示一类设备:这类设备的数据面处理是严格遵循 Virtio 协议规范的,即驱动和设备会按照第三节提到的 Virtio 通信流程来进行通信,但控制路径,比如:通信流程里面提到的 ring buffer 和 descriptor table 的内存地址,驱动如何告知设备,设备支持的特性,驱动如何感知,这些都是厂商自定义的,不一定会遵循 Virtio 协议。这样做的好处是,可以降低厂商在实现这类设备时的复杂度。
Linux 内核为了将这类设备应用起来,就提出了 vDPA 这样一个技术框架。这个技术框架本质上和 VFIO-mdev 类似,也实现了一个虚拟设备(vDPA device)总线驱动模型,和 VFIO-mdev 不同的是,通过 vDPA 框架虚拟出来的设备,既可以给虚机使用,又可以直接从宿主机(比如:容器)进行访问。这一切都归功于,vDPA 设备的数据路径是遵循 Virtio 协议规范的,因此,可以直接被宿主机上的 virtio 驱动直接访问。同时,该技术框架对 vhost 内核子系统进行了扩展,赋予了类似 VFIO 技术框架的功能,允许将 vDPA 设备用来进行数据通信的硬件资源(ring buffer, descriptor table,doorbell 寄存器等)透传给虚机使用,这样,虚拟机的 virtio 驱动进行数据通信时,也是直接访问硬件资源,而不再需要通过 vhost、vhost-user 等方式进行处理了。更重要的一点是,由于虚机驱动是原本的 virtio 驱动,因此,当需要支持热迁移时,QEMU 可以灵活切换会软件模拟的方式,来保证热迁移的顺利进行。这样,vDPA 这个设备虚拟化技术框架既保证了最佳性能又保留了 virtio 设备的灵活性,而且还统一了虚机和容器的 I/O 技术栈。
VDUSE
通过上述 vDPA 技术框架,我们基本解决了长期以来设备虚拟化技术在虚机场景下暴露的一些问题,而更重要的是,它将 virtio 技术也带到了容器领域。但这个技术框架还存在一个问题,就是需要硬件设备的支持。回想之前提到的 virtio、vhost、vhost-user,本质上都是软件定义的虚拟设备。那 vDPA 这个技术框架有没有可能也能够使用软件定义的设备呢?VDUSE 这项技术就是用来实现这个目标的,通过 VDUSE,我们可以在一个用户进程实现一个软件定义的 vDPA 设备,并可以通过上述 vDPA 框架接入 virtio 或者 vhost 子系统,供容器或者虚机使用。
该项技术是由我们自主研发并在去年 10 月向 Linux 内核社区正式开源的。现阶段我们的方案已经被合入 Linux 内核主线,将在 Linux 5.15 版本与大家见面。同时,我们也会在 9 月 15 日举办的 KVM Forum 这个虚拟化领域的高端技术论坛会议上进行线上分享。
结束语
从服务虚拟机到支持容器,从纯软件模拟到硬件直通再到软硬件结合,Linux 设备虚拟化技术这几十年里一直在朝着极致性能、应用灵活等方向不断演进。随着云原生的浪潮,各大硬件厂商的入局,全新的软硬件结合方式不断涌现,我们相信后续也会有更多精彩技术在等待着我们。
参考链接
- https://www.ozlabs.org/~rusty/virtio-spec/virtio-paper.pdf
- https://www.redhat.com/en/blog/introduction-virtio-networking-and-vhost-net
- https://www.kernel.org/doc/html/latest/driver-api/vfio.html
- https://gitlab.com/qemu-project/qemu/-/blob/master/docs/interop/vhost-user.rst
- https://www.kernel.org/doc/html/latest/driver-api/vfio-mediated-device.html
- https://www.redhat.com/en/blog/introduction-vdpa-kernel-framework
- https://www.kernel.org/doc/html/latest/userspace-api/vduse.html
- https://events.linuxfoundation.org/kvm-forum/program/schedule/