赞
踩
在本篇文章中,我们将解释介绍中描述的vhost-net架构,以便从技术角度清楚地解释一切是如何协同工作的。这是一系列博客文章的一部分,介绍了将虚拟化和网络领域融合在一起的virtio-networking的世界。
本文面向架构师和开发人员,他们有兴趣了解在前一篇博客中描述的vhost-net/virtio-net架构的内部工作原理。
我们将首先描述在hypervisor中排列的不同virtio规范标准组件和共享内存区域的方式,以及QEMU如何模拟virtio网络设备以及客户端如何使用开放的virtio规范来实现用于管理和与该设备通信的虚拟化驱动程序。
在向您展示QEMU virtio架构后,我们将分析I/O瓶颈和限制,并使用主机的内核来克服它们,达到在概述文章中介绍的vhost-net架构。
最后但同样重要的是,我们将展示如何将虚拟机连接到主机之外的外部世界,使用Open Virtual Switch(OVS),这是一个开源的虚拟、支持SDN的分布式交换机。
通过阅读本文,您将能够理解vhost-net/virtio-net架构的工作原理,每个组件的目的以及数据包是如何发送和接收的。
在本节中,我们将简要解释一些您需要了解的概念,以便充分理解本文。对于精通这一领域的人来说,这可能看起来很基础,但它将提供一个共同的基础来构建。
让我们从基础知识开始。物理网卡(Network Interface Card,NIC)是连接主机与外部世界的硬件(真实)组件。它可以执行一些卸载工作,例如在网卡中执行校验和计算而不是在CPU中执行、分段卸载(将较大的数据分成小块,例如以太网MTU大小)或大接收卸载(从CPU的角度来看,将许多接收到的数据包数据合并为一个)。
另一方面,我们有tun/tap设备,这些是虚拟的点对点网络设备,用户空间应用程序可以使用它们来交换数据包。如果交换的数据是第2层(以太网帧),则该设备称为tap设备;如果交换的数据是第3层(IP数据包),则称为tun设备
。
当加载tun内核模块时,它会创建一个特殊设备/dev/net/tun。进程可以通过打开它并向其发送特殊的ioctl命令来创建一个tap设备。新的tap设备在/dev文件系统中有一个名称,另一个进程可以打开它,发送和接收以太网帧。
Unix套接字
是一种在同一台计算机上以高效方式进行进程间通信(IPC)的方法。在本文的范围内,通信的服务器将Unix套接字绑定到文件系统中的路径,因此客户端可以使用该路径连接到它。从那一刻起,进程可以交换消息。请注意,Unix套接字还可用于在进程之间交换文件描述符。
eventfd
是执行IPC的一种更轻量级的方式。虽然Unix套接字允许发送和接收任何类型的数据,但eventfd只是生产者可以更改的整数,而消费者可以轮询和读取。这使它们更适合作为等待/通知机制,而不是信息传递。
这两种IPC系统都为通信中的每个进程公开了一个文件描述符。fcntl调用对该文件描述符执行不同的操作,例如使它们变为非阻塞(因此如果没有要读取的内容,读取操作会立即返回)。ioctl调用遵循相同的模式,但实现了特定于设备的操作,例如发送命令。
共享内存是我们在这里将介绍的IPC方法的最后一种。与提供用于通信的通道不同,它使某些进程的内存区域指向相同的内存页,因此一个进程写入的更改会影响其他进程进行的后续读取。
QEMU
是一个托管的虚拟机模拟器,为客户机提供了一组不同的硬件和设备模型。对于主机,qemu看起来像是由标准Linux调度程序调度的常规进程,具有自己的进程内存。在这个过程中,QEMU分配了一个内存区域,客户机将其视为物理内存,并执行虚拟机的CPU指令
。
要在裸金属硬件上执行I/O,例如存储或网络,CPU必须与执行特殊指令并访问特定内存区域的物理设备进行交互,例如设备映射到的内存区域。
当客户端访问这些内存区域时,控制权将返回给QEMU,后者以透明的方式为客户端执行设备仿真。
基于内核的虚拟机(KVM)
是嵌入Linux的开源虚拟化技术。它为虚拟化软件提供了硬件支持,使用内置的CPU虚拟化技术来减少虚拟化开销(缓存、I/O、内存)并提高安全性
。
使用KVM,QEMU可以创建一个具有处理器感知的虚拟CPU(vCPU)的虚拟机,运行本地速度的指令。当KVM达到特殊指令时,比如与设备交互或访问特殊内存区域的指令,vCPU会暂停并通知QEMU暂停的原因,允许超级监视程序对该事件做出反应。
在常规的KVM操作中,超级监视程序打开设备/dev/kvm
,并使用ioctl
调用与其通信,以创建虚拟机、添加CPU、添加内存(由qemu分配,但从虚拟机的角度看是物理内存),发送CPU中断(就像外部设备发送一样),等等。例如,其中一个ioctl运行实际的KVM vCPU,阻塞QEMU,并使vCPU运行,直到找到需要硬件支持的指令为止。在那一刻,ioctl返回(这称为vmexit),并且QEMU知道了退出的原因(例如,冒犯的指令)。
对于特殊内存区域,KVM采用类似的方法,将内存区域标记为只读或根本不映射它们,导致带有KVM_EXIT_MMIO原因的vmexit。
Virtio是一种用于虚拟机数据I/O通信的开放规范,提供了一种简单、高效、标准和可扩展的虚拟设备机制,而不是专为每个环境或每个操作系统设计的机制。它利用了虚拟机客户机和主机可以共享内存进行I/O的特点来实现。
Virtio规范基于两个元素:设备和驱动程序。在典型的实现中,虚拟机监控程序通过多种传输方法将virtio设备暴露给客户机。按设计,它们在虚拟机内部对客户机看起来就像物理设备。
最常见的传输方法是PCI或PCIe总线。但是,设备可以位于预定义的客户机内存地址(MMIO传输)。这些设备可以完全虚拟,没有物理对应物,或者是物理设备,提供兼容的接口。
暴露virtio设备的典型(也是最简单的)方法是通过PCI端口,因为我们可以利用PCI在QEMU和Linux驱动程序中得到成熟和良好支持的事实。真正的PCI硬件使用特定的物理内存地址范围来公开其配置空间(即,驱动程序可以通过访问该内存范围来读取或写入设备的寄存器),和/或特殊的处理器指令。在虚拟机世界中,虚拟机监控程序捕获对该内存范围的访问并执行设备仿真,公开与真实机器相同的内存布局并提供相同的响应。virtio规范还定义了其PCI配置空间的布局,因此实现它是直接的。
当客户机启动并使用PCI/PCIe自动发现机制时,virtio设备会使用PCI供应商ID和PCI设备ID来进行标识。客户机的内核使用这些标识符来确定哪个驱动程序必须处理该设备。特别是,Linux内核已经包含了virtio驱动程序。
virtio驱动程序必须能够分配内存区域,供虚拟机监控程序和设备读取和写入,即通过内存共享。我们将数据通信中使用这些内存区域的部分称为数据平面,将设置它们的过程称为控制平面
。我们将在未来的帖子中提供有关virtio协议实现和内存布局的更多详细信息。
virtio内核驱动程序共享一个通用的传输特定接口(例如:virtio-pci),由实际的传输和设备实现(例如virtio-net或virtio-scsi)使用。
Virtqueues是用于在virtio设备上进行批量数据传输的机制。每个设备可以具有零个或多个virtqueue(链接)。它由一组由客户机分配的缓冲区队列组成,主机通过读取它们或写入它们与之交互。此外,virtio规范还定义了双向通知:
可用缓冲区通知:驱动程序用于通知存在可以由设备处理的缓冲区
已使用缓冲区通知:设备用于通知其已完成处理某些缓冲区。
在PCI情况下,客户机通过写入特定的内存地址来发送可用缓冲区通知,而设备(在这种情况下是QEMU)使用vCPU中断来发送已使用缓冲区通知。
virtio规范还允许动态启用或禁用通知。因此,设备和驱动程序可以批处理缓冲区通知,甚至主动轮询virtqueues中的新缓冲区(繁忙轮询)。这种方法更适用于高流量速率。
总之,virtio驱动程序接口公开了:
设备的特性位(设备和客户机必须进行协商)
状态位
配置空间(包含设备特定信息,如MAC地址)
通知系统(配置更改、可用缓冲区、已使用缓冲区)
零个或多个virtqueue
传输特定的与设备接口
图1:qemu上的virtio-net
virtio网络设备是一张虚拟以太网卡,支持TX/RX的多队列。空缓冲区被放置在N个virtqueues中,用于接收数据包,而发送数据包则被排入另外N个virtqueues以进行传输。另一个virtqueue用于驱动程序和设备在数据平面之外的通信,比如控制高级过滤功能、设置MAC地址或活动队列的数量。与物理NIC一样,virtio设备支持许多卸载功能,并且可以让真实主机的设备执行这些功能。
为了发送数据包,驱动程序向设备发送一个包括元数据信息(如数据包的期望卸载)的缓冲区,然后是要传输的数据包帧。驱动程序还可以将缓冲区拆分成多个gather条目,例如,它可以将元数据头部与数据包帧分开。
这些缓冲区由驱动程序管理并由设备映射。在这种情况下,设备位于hypervisor内部。由于hypervisor(qemu)可以访问所有虚拟机的内存,因此它能够定位缓冲区并读取或写入它们。
以下流程图显示了virtio-net设备配置以及使用virtio-net驱动程序发送数据包的过程,该驱动程序通过PCI与virtio-net设备进行通信。在填充要发送的数据包后,它触发了“可用缓冲区通知”,将控制返回给QEMU,以便通过TAP设备发送数据包。
然后,QEMU通知客户端缓冲区操作(读取或写入)已完成,它通过将数据放置在virtqueue中并发送已使用的通知事件来执行此操作,从而触发了客户端vCPU中的中断。
接收数据包的过程与发送数据包的过程类似。唯一的区别是,在这种情况下,空缓冲区是由客户机预分配的,并且可以使设备可以将传入数据写入这些缓冲区。
图2:Qemu virtio发送缓冲区流程图
前述方法存在一些低效性:
virtio驱动程序发送“可用缓冲区通知”后,vCPU停止运行,控制返回到hypervisor,导致昂贵的上下文切换。
QEMU的额外任务/线程同步机制。
为每个要发送或接收的数据包进行系统调用和数据复制(没有批处理)。
用于发送可用缓冲区通知的ioctl(vCPU中断)。
我们还需要添加另一个系统调用以恢复vCPU的执行,带有所有相关的映射切换等。
为了解决这些限制,设计了vhost协议
。vhost API是一种基于消息的协议,允许hypervisor将数据平面卸载到另一个组件
(处理程序)中,以更高效地执行数据转发。使用此协议,主机将以下配置信息发送给处理程序:
hypervisor的内存布局。这样,处理程序可以在hypervisor的内存空间中定位virtqueues和缓冲区。
一对文件描述符,用于处理程序发送和接收virtio规范中定义的通知。这些文件描述符在处理程序和KVM
之间共享,因此它们可以直接通信,而无需hypervisor的干预。请注意,这些通知仍然可以根据
virtqueue动态禁用。
此过程之后,hypervisor将不再处理数据包(从virtqueues读取或写入数据)。相反,数据平面将完全卸载到处理程序,该处理程序现在可以直接访问virtqueues的内存区域,以及直接向来宾发送和接收通知。
vhost消息可以在任何主机本地传输协议中交换,例如Unix套接字或字符设备,而hypervisor可以充当服务器或客户端(在通信通道的上下文中)。hypervisor是协议的领导者,卸载设备是处理程序,它们都可以发送消息。
为了进一步了解此协议的好处,我们将分析vhost协议的基于内核的实现:vhost-net内核驱动程序。
vhost-net是一个内核驱动程序,实现了vhost协议的处理程序侧,以实现高效的数据平面,即数据包转发。在此实现中,qemu和vhost-net内核驱动程序(处理程序)使用ioctls来交换vhost消息,还使用了一对类似eventfd的文件描述符,称为irqfd
和ioeventfd
,用于与来宾交换通知。
当加载vhost-net内核驱动程序时,它会在/dev/vhost-net上公开一个字符设备。当以vhost-net支持的方式启动qemu时,它会打开该设备,并使用多个ioctl(2)调用来初始化vhost-net实例。这些操作是将hypervisor进程与vhost-net实例关联、准备virtio功能协商并传递来宾物理内存映射给vhost-net驱动程序所必需的。
在初始化过程中,vhost-net内核驱动程序会创建一个名为vhost- p i d 的内核线程,其中 pid的内核线程,其中 pid的内核线程,其中pid是hypervisor进程的进程号。此线程称为“vhost工作线程”。
仍然使用tap设备来与主机通信,但现在工作线程处理I/O事件,即轮询驱动程序通知或tap事件,并转发数据。
Qemu分配一个eventfd并在vhost和KVM中注册它
,以实现通知绕过。vhost-$pid内核线程会对其进行轮询,而当来宾写入特定地址时,KVM会向其写入。这种机制称为ioeventfd
。这样,对特定来宾内存地址的简单读/写操作无需经过昂贵的QEMU进程唤醒,可以直接路由到vhost工作线程。这还具有异步的优势,无需vCPU停止(因此无需立即进行上下文切换)。
另一方面,qemu分配另一个eventfd
,并再次在KVM和vhost中注册它,用于直接注入vCPU中断。这种机制称为irqfd
,它允许主机中的任何进程通过写入来注入vCPU中断到来宾,并具有相同的优势(异步,无需立即进行上下文切换等)。
请注意,对virtio数据包处理后端的此类更改对来宾完全透明,仍然使用标准的virtio接口。
以下块和流程图显示了数据路径从qemu到vhost-net内核驱动程序的卸载:
图3:vhost-net块图
图4:vhost-net发送缓冲区流程图
通向外部世界的通信
虚拟机可以使用tap设备与主机进行通信,但问题仍然存在,即它如何与同一主机上的其他虚拟机或主机外的机器通信(例如:与互联网通信)。
我们可以通过使用内核网络堆栈提供的任何转发或路由机制来实现这一点,比如标准的Linux桥接。然而,更高级的解决方案是使用完全虚拟化的、分布式的、受管理的交换机,例如Open Virtual Switch(OVS)。
正如在概述文章中所述,OVS数据路径在这种情况下作为内核模块运行,ovs-vswitchd
作为用户态控制和管理守护程序,ovsdb-server
作为转发数据库。
如下图所示,OVS数据路径在内核中运行,并在物理网卡和虚拟TAP设备之间转发数据包:
Figure 5: Introduce OVS
我们可以将这个情况扩展到在同一主机环境中运行的多个虚拟机,每个虚拟机都有自己的qemu进程、TAP端口和vhost-net驱动程序,这有助于避免qemu上下文切换。
在本文中,我们展示了virtio-net架构的工作原理,逐步剖析了它,并解释了每个组件的功能。
我们首先解释了默认IO qemu设备的工作原理,通过为客户端提供开放的virtio标准的实现。然后,我们继续研究了客户端如何使用virtio驱动程序与这些设备进行通信,以及如何能够发送和接收数据包、通知主机或接收通知。
接下来,我们评估了将qemu作为数据通路的问题,需要在其上下文中进行切换。然后,我们展示了如何在主机上使用vhost-net内核驱动程序,使用vhost协议从qemu卸载此任务。我们还介绍了在这种新方法中virtio通知的工作原理。
最后但同样重要的是,我们展示了如何将虚拟机连接到超出其所在主机的外部世界。
在下一篇文章中,我们将继续提供一个关于vhost-net/virtio-net架构的实践教程,实验这个解决方案概述和当前技术深度分析博客中提到的不同组件。
如果由于任何原因您跳过了那篇(真的非常有教育意义的!)文章,我们将在下一篇总结文章中介绍一个用于vhost协议的新的用户空间处理程序,使用DPDK。我们将列举它从DPDK和用户空间切换的角度带来的优势,并构建一个使用这些概念的第二个架构
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。