本篇文章将带领大家了解以下网络设备虚拟化的演进之路,以及网络设备虚拟化发展的原因。本文是从全虚拟化->virtio->vhost->vfio->vdpa->vduse演进详细分析一下网络虚拟化的干货。
序言
设备虚拟化技术,云计算领域的核心之一。虚拟机里面的形形色色的设备,比如:网卡,磁盘,键盘,鼠标等,都是利用这项技术实现的,本文将从linux网络设备虚拟化的演进角度来看看,这发展的轨迹,网络虚拟化大体上分为三类,全虚拟化,半虚拟化,IO透传。全虚拟化场景下虚拟机操作系统不感知自己是虚拟机,半虚拟化虚拟机能感知自己是虚拟机
PART 01 【全虚拟化】
Trap-and-emulate,
在最早期阶段,设备虚拟化常常和机器模拟器技术,比如:QEMU,绑定在一起。在最开始时候,可以利用QEMU模拟真实的网络设备,模拟设备上的所有寄存器布局,操作。当 QEMU 虚拟机里面设备驱动需要访问该虚拟设备的寄存器时,宿主机截获客户机对I/O设备的访问请求,这条访问指令会被 trap 到 QEMU,由 QEMU 来进行处理,然后通过软件模拟真实的硬件。这种方式对客户机而言非常透明,无需考虑底层硬件的情况,不需要修改操作系统。一切都是这么的完美。
下图为qemu模拟的指令
PART 02 【Virtio】
通过上述这种 trap-and-emulate 的全虚拟化的方式来模拟设备,虽然不需要对真实设备驱动进行变更,但是设备访问过程中,对所有模拟I/O的设备访问都会造成VM-exit,这对虚拟机的使用造成了很大的性能影响。因此,virtio 这类半虚拟化技术应运而生。
相较于I/O全虚拟化的这种方式,Virtio 不再拘泥于依赖已有的设备驱动,而是定义了一套全新的专用于虚拟设备的驱动框架。设备驱动清楚自己是在操作虚拟设备,因此,在真正的I/O路径上规避了大量可能导致vm-exit的 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-OVS 等),因此,Virtio Backend 可以直接访问并获取到 buffer 的地址、长度,进而直接存取这些buffer。当处理完请求之后,Virtio Backend 将数据填充到相应的 buffer,并将对应的 descriptor table index 写入 used ring 中,并通过 eventfd 机制注入中断通知虚机内 virtio 驱动。
我们结合qemu场景来分析,virtio backend后端驱动的最基本要素是虚拟队列机制、消息通知机制、中断机制。虚拟队列机制连接着客户机和宿主机的数据交互。消息通知机制主要用于从客户机到宿主机的消息通知。中断机制主要用于从宿主机到客户机的中断请求和处理。
上图是virtio-net后端模块进行报文处理的系统架构图。其中KVM负责为程序提供虚拟化硬件的内核模块,qemu利用KVM来模拟整个系统的运行环境,tap是内核中的虚拟以太网设备。当客户机发送报文时候,它会利用消息通知机制通知KVM,并推出到用户空间qemu进程,然后由qemu开始对tap设备进行读写。tap->qemu->guest。
从上述virtio技术提出以后,上述模型存在问题如下:
(1)宿主机、客户机、qemu之间的上下文频繁切换带来了多次数据拷贝和cpu特权级的切换,导致了virtio-net性能不如人意。
(2)消息通知机制是当报文到达tap设备时候,内核发出并送到qemu的通知消息,然后qemu利用IOCTL向KVM请求中断,KVM发送中断到客户机,这样的路径带来了不必要的开销。
PART 03 【Vhost-net】
为了对上述报文收发性能瓶颈进行优化,通过卸载virtuo-net模块在报文收发处理上的工作,使得qemu从virtio-net的虚拟队列工作中解放出来,减少上下文切换和数据包拷贝,进而提高报文收发性能,并且宿主机上的vhost-net模块还需要承担报文到达和发送时候的消息通知以及中断的工作,这样 virtio 通信机制从原本的 QEMU 用户态 I/O 线程和虚机驱动(QEMU 用户态 vcpu 线程)通信直接变成了 vhost 内核 I/O 线程和虚机驱动(QEMU 用户态 vcpu 线程)通信。vhost 内核 I/O 线程拿到数据包之后,直接走内核协议栈和网卡驱动进行处理,从而优化掉了 QEMU 到内核态的额外开销。
上图展现了加入linux内核vhost-net模块后virtio-net模块进行报文处理的系统架构图。报文接收仍然包括数据通路和消息通知路径两个方面:
(1)数据通路是从tap设备接收数据报文,通过vhost-net模块把该数据报文,通过vhost-net模块把报文拷贝到虚拟队列中的数据区,从而使得客户机接收报文。
(2)消息通路是当报文从tap设备到达vhost-net时候,通过kvm模块向虚拟机发送中断,通知客户机接收报文。
PART 04【 Vhost-user】
上述vhost-net模块需要在内核态完成报文拷贝和消息处理,这样会给报文处理带来一定的性能损失。因此用户态vhost应运而生。由于 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 服务模型。由于vhost采用内存共享技术,通过共享的虚拟队列完成数据报文的传输和控制,大大降低了vhost和virtio-net之间的数据传输成本。
vhost其实现原理与linux内核态vhost-net类似,它实现用户态API卸载qemu在virtio-net上所承担的虚拟队列功能,同样基于qemu共享内存空间布局、虚拟化队列的访问地址和事件文件描述符给用户态的vhost,使得vhost能进行报文处理以及客户机的通信,由于报文拷贝在用户态,linux内核得到了负担减轻。
PART 05【 VFIO】
随着云计算规模的不断扩大,用户一方面不再满足于 Virtio 这类半虚拟化设备带来的性能体验,另一方面 GPU 这类很难进行 virtio 化的设备应用场景与日俱增。在这种背景下,VFIO 这项技术被提出。
UIO的作用是把一个设备的IO和中断能力暴露给用户态,从而实现在用户态对硬件的直接访问。它的基本实现方法是,当我们probe一个设备的时候,通过uio_register_device()注册为一个字符设备/dev/uioN,用户程序通过对这个设备mmap访问它的IO空间,通过read/select等接口等待中断。
UIO的缺点在于,用户态的虚拟地址无法直接用于做设备的DMA地址(因为在用户态无法知道DMA内存的物理地址),这样限制了UIO的使用范围。基本上UIO现在只能用于做工控卡这种IO量不大,可以直接把内存地址拷贝到IO空间的场景(相当于不做DMA)。我们有人通过UIO设备自己的ioctl来提供求物理地址的机制,从而实现DMA,但这种方案是有风险的,因为你做ioctl求得的物理地址,可能因为swap而被放弃,就算你做gup,但gup只保证物理内存不被释放,不能保证vma还指向这个物理页。
VFIO通过IOMMU的能力来解决这个问题。IOMMU可以为设备直接翻译虚拟地址,这样我们在提供虚拟地址给设备前,把地址映射提供给VFIO,VFIO就可以为这个设备提供页表映射,从而实现用户程序的DMA操作。
背负提供DMA操作这个使命,VFIO要解决一个更大的问题,就是要把设备隔离掉。在Linux的概念中,内核是可信任的,用户程序是不可信任的,如果我们允许用户程序对设备做DMA,那么设备也是不可信任的,我们不能允许设备访问程序的全部地址空间(这会包括内核),所以,每个设备,针对每个应用,必须有独立的页表。这个页表,通过iommu_group承载(iommu_group.domain),和进程的页表相互独立。进程必须主动做DMA映射,才能把对应的地址映射写进去。
如上图所示,通过 VFIO,QEMU 能够直接将自己虚拟出的一个 PCI 设备和一个物理 PCI 设备的数据链路直接打通,当虚拟机里面的设备驱动访问虚拟 PCI 设备的 bar 空间时,通过 EPT 机制,这次 mmio 访问会被重定向到真实物理设备相应的 bar 空间位置,而不再需要 trap 到 QEMU。这样,虚机驱动就相当于可以以接近零消耗的方式直接访问真实物理设备,性能可以达到最佳。同时,VFIO 驱动利用 IOMMU 实现了设备 DMA 和中断的重映射,一方面起到隔离作用,即某个虚机无法操作 VFIO 直通设备向同一宿主机上的其他虚机发起 DMA 和中断,一方面也保证了设备进行 DMA 时通过给定的虚机物理地址能够直接访问到正确的物理内存。
这个逻辑空间其实是有破绽的,IOMMU是基于设备来创建的,一个设备有一个IOMMU(或者如果这个设备和其他设备共享同一个IOMMU硬件,是几个设备才有一个iommu_group),那如果我两个进程要一起使用同一个设备呢?基于现在的架构,你只能通过比如VF(Virtual Function,虚拟设备),在物理上先把一个设备拆成多个,然后还是一个进程使用一个设备。这用于虚拟机还可以,但如果用于其他功能,基本上是没戏了。
再说,VF功能基本都依赖SR-IOV这样的实现,也不是你想用就能用的。
【PART 07 VFIO-mdev】
VFIO 技术在实际应用场景,除了之前提到的不支持热迁移的问题外,还有一个限制就是一个设备只能透传给一个虚机,无法做到资源共享。SR-IOV 技术从某种程度上能够解决这个问题,即将一个物理 PCI 设备从硬件层面进行资源划分,划分成多个 VF,透传给多个虚机进行使用,但是有很多设备可能并不具备 SR-IOV 能力,并且SR-IOV还不支持热迁移,希望提供一个标准的接口来帮助设备驱动实现软件层面的资源切分,并能够利用 VFIO 技术透传给虚机。VFIO-mdev 这个技术框架应运而生。
该技术本质是在内核实现了一个虚拟设备(Mediated device)总线驱动模型,并在 VFIO 内核框架上进行了扩展,增加了对 mdev 这类虚拟设备的支持(mdev bus driver),从原来只支持从标准的硬件 PCI 设备和硬件 platform 设备获取透传信息。
mdev本质上是在VFIO层面实现VF功能。在mdev的模型中,通过mdev_register_device()注册到mdev中的设备称为父设备(parent_dev),但你用的时候不使用父设备,而是通过父设备提供的机制(在sysfs中,后面会详细谈这个)创建一个mdev,这个mdev自带一个iommu_group,这样,你有多个进程要访问这个父设备的功能,每个都可以有独立的设备页表,而且互相不受影响。所以,整个mdev框架包括两个基本概念,一个是pdev(父设备),一个是mdev(注意,我们这里mdev有时指整个vfio-mdev的框架,有时指基于一个pdev的device,请注意区分上下文)。前者提供设备硬件支持,后者支持针对一个独立地址空间的请求。
两者都是device(struct device),前者的总线是真实的物理总线,后者属于虚拟总线mdev,mdev上只有一个驱动vfio_mdev,当你通过pdev创建一个mdev的时候,这个mdev和vfio_mdev驱动匹配,从而给用户态暴露一个普通vfio设备的接口(比如platform_device或者pci_device)的接口。
换句话说,如果一个设备需要给多个进程提供用户态驱动的访问能力,这个设备在probe的时候可以注册到mdev框架中,成为一个mdev框架的pdev。之后,用户程序可以通过sysfs创建这个pdev的mdev。
【PART 08 vDPA】
VFIO定义了一个IOMMU地址翻译模型,它基于平台IOMMU设备,而很多厂商的设备自己集成了地址翻译组件(比如mellox)并不使用平台IOMMU设备,这些厂商的NIC的地址翻译模型并不能很好的适配VFIO的模型。VFIO 和 virtio 这两类技术一直是最主流的设备虚拟化技术。VFIO 能够直接将硬件资源透传给虚机使用,性能最佳,virtio 性能稍逊,但胜在更加灵活。那有没有可能将两者的优势结合起来呢?vDPA 技术框架,就是为了实现这一目标。
vDPA 的全称是 Virtio Data Path Acceleration,它表示一类设备:这类设备的数据面处理是严格遵循 Virtio 协议规范的,即驱动和设备会按照第三节提到的 Virtio 通信流程来进行通信,但控制路径,比如:通信流程里面提到的 ring buffer 和 descriptor table 的内存地址,驱动如何告知设备,设备支持的特性,驱动如何感知,这些都是厂商自定义的。这样做的好处是,可以降低厂商在实现这类设备时的复杂度。
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 技术栈。
【PART 09 VDUSE】
通过上述 vDPA 技术框架,我们基本解决了长期以来设备虚拟化技术在虚机场景下暴露的一些问题,而更重要的是,它将 virtio 技术也带到了容器领域。但这个技术框架还存在一个问题,就是需要硬件设备的支持。回想之前提到的 virtio、vhost、vhost-user,本质上都是软件定义的虚拟设备。那 vDPA 这个技术框架有没有可能也能够使用软件定义的设备呢?VDUSE 这项技术就是用来实现这个目标的,通过 VDUSE,我们可以在一个用户进程实现一个软件定义的 vDPA 设备,并可以通过上述 vDPA 框架接入 virtio 或者 vhost 子系统,供容器或者虚机使用。
结束语
从服务虚拟机到支持容器,从纯软件模拟到硬件直通再到软硬件结合,Linux 设备虚拟化技术这几十年里一直在朝着极致性能、应用灵活等方向不断演进。随着云原生的浪潮,各大硬件厂商的入局,全新的软硬件结合方式不断涌现,我们相信后续也会有更多精彩技术在等待着我们。文章来源:https://www.toymoban.com/news/detail-496320.html
如果觉得本文对您有用欢迎点赞转发加关注!!!文章来源地址https://www.toymoban.com/news/detail-496320.html
到了这里,关于虚拟网络演进之路的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!