2024年2月4日发(作者:)
浅析Xen虚拟I/O设备的原理与实现
余珂 杨晓伟
摘要
Xen是业界领先的开放源码虚拟机项目。对于I/O设备的虚拟,它主要采用的方法是:设计一套简单通用并且高效的协议,实现虚拟机中I/O设备与本地物理I/O设备的高效交互,最终获得很高的性能。VBD (virtual block device)和VNIF (virtual network interface)是采用这种方法实现的高性能虚拟磁盘I/O和网络I/O设备,性能接近物理设备。本文主要介绍了Xen虚拟I/O设备的原理与实现,结构如下:首先介绍Xen虚拟机的纯虚拟机I/O模型,然后是VBD/VNIF为代表的泛虚拟化I/O模型的原理和架构,最后分别详细介绍了VBD和VNIF的实现。
1. Xen虚拟机I/O模型概述
在上一期的《剖析Xen虚拟机架构》中,我们介绍了Xen硬件虚拟化技术和泛虚拟化技术以及基于该技术的Xen硬件虚拟机体系结构。在这一期中,我们再详细介绍一下该体系结构中的I/O子系统。Xen虚拟机可以分为完全虚拟化和泛虚拟化,与此对应,I/O子系统则可以分为泛虚拟化(Xen虚拟设备)、完全虚拟化(仿真设备)和本征系统(直接分配设备)。
1.1. 完全虚拟化I/O模型概述
首先我们简要回顾一下Intel® VT的架构。它提供了2个运行环境:根(Root)环境和非根(Non-root)环境。根环境专门用于运行虚拟机监控程序(Hypervisor),而非根环境作为一个受限环境用来运行多个虚拟机。运行环境之间可以相互转化,从根环境到非根环境叫VMEntry;从非根环境到根环境叫VMExit。每个虚拟机对应一个CPU维护的VMCS数据结构,其中记录根环境配置、非根环境配置、VMExit执行控制、VMExit原因等信息。
在这个架构下,I/O设备可以如何虚拟化呢?从系统软件的角度来看,I/O设备其实就是特定资源的集合:I/O端口,内存映射I/O,中断等。当操作系统按照硬件手册规定的方式操作资源,如读写I/O端口,接收中断,I/O设备就会按照规定完成的操作(读写磁盘,收发网络包等)。所以进行I/O设备虚拟化,一个很自然的想法就是:捕获操作系统对这些网络设备资源的操作,然后根据硬件手册来模拟规定的操作。
图1给出了Xen的架构,上一期对这个架构有详细描述,我们不妨以此为例,了解I/O设备虚拟化的流程。为了尽量把Hypervisor做的精简、可靠,Hypervisor中并没有外设驱动,对外设硬件的I/O请求是由Hypervisor、Domain0和DM配合共同完成的。一个I/O请求的处理流程如下:
(1)Domain N内核执行In/Out指令,触发VMExit,处理器调用Hypervisor设置的VMExit的处理函数。
(2)Hypervisor将I/O指令的具体信息写入Domain N与DM共享的一页I/O共享页中,并通过事件通道(Event channel)通知Domain0。接着Hypervisor阻塞该虚拟机,并且调用调度算法。
(3)Hypervisor恢复Domain0的状态,并把执行控制交给Domain0。
(4)Domain0中首先被执行的是注册的回调函数hypervisor_callback,它再调用
evtchn_do_upcall。
(5)evtchn_do_upcall里收集有哪些虚拟机有多少I/O请求。
(6)执行控制从Domain0的内核态返回用户程序态。DM本来通过一条select系统调用等待I/O请求,此时得到调度后的DM一旦等到请求到来就返回。
(7)通过读取I/O共享页,DM识别是对哪类外设访问,调用对应虚拟外设初始化时注册的回调函数。
(8)根据不同请求,虚拟外设的回调函数或者只是把虚拟外设的状态写回I/O共享页中,或者发生一次真正的数据拷贝。最后,DM仍然通过事件通道通知Hypervisor处理完毕。
(9)Hypervisor得到通知后,解除对应的请求I/O的Domain N的阻塞。未来某一时刻,该Domain就可以再次被调度到,继续运行了。
Domain0(根环境)
应用程序(特权级3)
CP
DM
Domain n(非根环境)
应用程序(特权级3)
7
6
修改的Linux(特权级1)
5
evtchn
4
3
事件通道
未修改的内核
(特权级0)
8
1
2
I/O共享页
虚拟机
监控器
VMExit
物理硬件(处理器,内存,I/O设备)
图1 Xen I/O处理流程
这种完全虚拟化的方法,优点和缺点一样明显。优点是,因为是在平台层次上虚拟化,因此通用性很好,适用于所有的操作系统,只要操作系统有该虚拟设备的驱动程序,虚拟设备就能工作。缺点是,这种方法引入了复杂的I/O操作路径,不仅需要在两个虚拟机之间切换,还需要多次的处理器运行模式的切换。我们知道上下文切换的代价是很大的,所以这种方法难以实现高性能的设备。对于磁盘设备、网络设备等I/O访问非常频繁的设备来说,这个缺点尤其明显。
1.2. 泛虚拟化I/O模型概述
既然完全虚拟化的方法有性能上的瓶颈,那么,有什么办法能够进一步提高虚拟设备的性能呢?从上面的分析可以看出,提高性能的关键在于减少上下文切换的次数。完全虚拟化
的方法,只能根据设备硬件手册所规定的借口来实现,并且操作系统的驱动程序也是根据硬件手册写成,因此很难减少资源捕获的次数,也就是上下文切换的次数。因此一个很自然的思路就是,重新定义并简化虚拟设备的接口,在一次上下文切换中做尽量多的操作,从而实现高效的I/O设备。这就是泛虚拟化I/O模型。
Domain0(Device Domain)
用户态
内核
设备驱动 后端驱动
控制面板
Domain N
用户态
内核
前端驱动
事件通道,授权表,环缓冲区
虚拟机监控器
物理硬件(处理器,内存,I/O设备)
图2 VBD/VNIF 原理架构
图2给出了泛虚拟化I/O模型的原理架构。VBD/VNIF是泛虚拟化I/O模型在Xen里面的实现,分别对应磁盘设备和网络设备。VBD/VNIF由前端(frontend)和后端(backend)组成。前端是运行在普通Domain内核态的驱动程序,负责创建虚拟设备,并且转发虚拟设备的I/O请求。后端是运行在Domain0中的内核态的驱动程序,负责接收前端转发的I/O请求,并且通过真正的设备驱动来访问物理设备,完成I/O请求。Hypervisor通过提供事件通道、授权表和环缓冲区来帮助前端与后端进行通讯。
VBD/VNIF非常简单高效,唯一的缺点是相对缺乏通用性,因为VBD/VNIF是操作系统相关的,需要为每种操作系统实现前端。Linux 2.4、Linux 2.6,Windows下面都有不同的前端实现。后端只需要运行在Domain0中,因此只需要一种实现,目前是Linux 2.6的内核驱动。
下面我们来具体介绍VBD/VNIF的实现。VBD比较简单,VNIF则相对复杂一些。
2. VBD的实现
2.1. 底层支持
刚才提到,前端-后端的通信需要底层提供一些特别的支持。Xen采用了事件通道(event
channel)、环缓冲区(ring buffer)、授权表(grant table)和xenstore机制,很好的解决了这个问题。
“事件通道”是类似于中断的一种机制,用于通知虚拟机对事件进行处理。事件通道在上期文章已经有过具体描述,此处不再赘述。在VBD/VNIF中,当有请求等待处理,或者请求已经完成需要查收时,前端或者后端使用事件通道来通知对方。
“环缓冲区”顾名思义,就是环状的缓冲区,里面放置请求和应答。如图3所示,环缓冲区采用生产者消费者模式,甲方首先放置请求,乙方获取并处理完请求后,将处理结果再次放置回环缓冲区中,甲方最后获取处理结果。甲和乙一次可以放置和处理多个请求,只要
环状缓冲区还有空间。
Request consumer
请求队列
Request producer
未提交的应答队列
空闲队列
Response produce
已提交的应答队列
Response consumer
图3:环状缓冲区
“授权表”是一种页面访问授权机制。通常情况下,Domain A和Domain B只能访问属于自己的页面。但是,Domain A可以通过授权表操作,授权Domain B可以读/写Domain A的指定页面;在经过DomainA的授权后,Domain B 可以通过授权表的操作,真正对指定页面进行读写。授权表主要用于前端与后端的大块数据传输。VBD主要使用授权表来进行数据的读写;VNIF还使用授权表来实现数据页的交换,实现网络包的零拷贝。
“Xenstore”本质上是一个小的树状数据库,类似于windows中的注册表,主要用于存放各个虚拟机的配置数据。数据库存放在Domain0,由一个守护进程负责对它的所有操作(读、写、事件触发等等)。VBD/VNIF就使用xenstore来保存和共享配置数据。VBD/VNIF的前端和后端还大量使用xenstore的事件触发功能来进行异步通信。
2.2. VBD初始化过程
VBD的模型由普通DomainN中的前端驱动和Domain0中的后端驱动组成。DomainN发起请求,设备虚拟机完成真正的设备访问。典型情况下,每个虚拟硬盘对应于设备虚拟机里面的一个映象文件或者分区,其初始化过程如下:
(1)CP在创建DomainN的时候根据配置,在Xenstore数据库中为每个虚拟磁盘添加后端结点,以下就是一个实例:
vbd = ""
1 = ""
768 = ""
domain = " ExampleHVMDomain"
frontend = "/local/domain/1/device/vbd/768"
dev = "hda"
state = "1"
params = "/mnt/sda5/"
mode = "w"
frontend-id = "1"
type = "file”
该配置指定把模拟成DomainN虚拟机ExampleHVMDomain的磁盘hda(主设备号为3,次设备号为0)。
(2)后端驱动是挂载在虚拟总线Xenbus上的,通过Linux 2.6的设备模型,我们可以
在/sys/bus/xen-backend/drivers下看到它。启动时它就注册了Xenstore数据库提供的VBD的检查点(Watch)。一旦有新的VBD设备加入,它就能立即发现,并通过device_register()加入系统的Xenbus中,我们可以在/sys/bus/xen-backend/devices/下看到它。
(3)总线驱动会通过match()函数为每个注册到自己上面的设备查找合适的驱动,一个VBD设备最终会对应到blkback后端驱动。
(4)VBD设备加入系统时会产生热插拔事件,这将触发后端系统中的/etc/hotplug/脚本得到调用,由它负责打开一个loop设备,绑定映象文件,并把结果写入Xenstore数据库。
(5)后端驱动的检测点发现该变化后,通过loop驱动打开该loop设备,以后后端对磁盘的访问都将由该驱动完成的。填写完一系列磁盘参数后,通知前端驱动处理。
(6)前端驱动从Xenstore里面读出参数,就可以创建虚拟磁盘了。使用alloc_disk()创建一个通用磁盘数据结构,填入参数。这里block_ops系列回调函数基本没有什么特殊之处,只是一些日常例程,open()的时候增加使用计数;release()时减少计数,释放资源;getgeo()返回虚拟磁盘的扇区/磁道/柱面信息。需要特殊说明的是request()函数,对于虚拟磁盘这样的块设备,由它发起真正磁盘访问。
2.3. VBD的磁盘访问
显然,后端驱动是没法直接访问设备虚拟机里面的文件内容的,需要借助前端驱动配合来完成。前端和后端驱动使用事件通道实现控制信息的传递,使用授权表完成内存页共享。磁盘访问分为读和写两类,我们以读磁盘为例,说明VBD的处理流程。
(1) 当处理request()时,前端驱动首先利用授权表把在VMX内存空间中分配的,用于存放读取结果的页面授予前端驱动所在设备虚拟机,使其对该页面的写权限。
(2) 前端驱动把该磁盘请求(request)放入一个和后端共享的环形缓冲区中,并通过事件通道通知后端驱动。
(3) 后端驱动被唤醒后,读取共享缓冲区中的请求,把对应授权表中授权的页面映射到自己的内存空间里,生成一个真正的磁盘请求。
(4) 磁盘请求完成时,结果已经在映射页面里面了。后端驱动释放该映射,在共享缓冲区中放入一个应答(response),同时通过事件通道唤醒前端驱动。
(5) 此时,前端驱动中已经有了读取结果,做完一些日常清理工作,一次读磁盘请求就完成了。
3. VNIF的实现
下面详细介绍VNIF的前端与后端,以及发包和收包的实现。Xen VNIF是在Linux下面实现的,所以这里也以Linux 2.6为例。当然,在其它操作系统中实现的原理也是一样的,只需根据具体操作系统的网络子系统做修改,比如在Windows下面就需要使用NDIS接口来实现。
3.1. VNIF的前端
VNIF的前端是运行在虚拟机Linux内核中的驱动程序。VNIF前端主要实现三个功能:创建虚拟网络设备、收包和发包。
创建虚拟网络设备
在虚拟机的配置文件(/etc/xen/xmexample) 中,有一项VNIF设置,可以设置虚拟网络设备,
最简单的设置如下
vif = [ '' ]
表示为虚拟机创建一个网络设备,所有参数采用缺省配置。当然,如果有多个vif设置,就表示为虚拟机创建多个网络设备。
当控制面板解析到这个设置之后,会在Xenstore中写入相应的表项:
vif = ""
0 = ""
backend-id = "0"
mac = "00:16:3e:32:cf:99"
handle = "0"
state = "0"
backend = "/local/domain/0/backend/vif/4/0"
这些项目包含了前端创建虚拟网络设备所需的参数,如vif编号为0,对应的后端编号为0,mac地址是随机产生的“00:16:3e:32:cf:99”。
当前端创建vif时,会遍历xenstore所有的vif表项,为每个vif项创建虚拟网络设备。以上面的设置为例,前端会创建一个vif0。创建每个vif可进一步细分为如下步骤:
第一步,创建一个以太网设备。感谢Linux丰富而灵活的内核接口,这个工作借助于Linux的内核接口struct net_device *alloc_etherdev(int sizeof_priv)很容易就能完成。该接口的功能是创建一个以太网设备,该接口封装了分配内存和初始化net_deivce结构的大部分细节。
第二步,初始化vnif相关的结构,包括授权表表项初始化,环缓冲区的初始化,事件通道的初始化等,为vnif收发包做准备。此外也需要将vnif的发包、收包函数赋值到设备的函数表上去。
第三步,一切已经就绪,就可以将虚拟设备挂到内核当中去了。调用Linux内核接口int
register_netdev(struct net_device *dev)将新创建的虚拟设备注册到内核中,此时用”ifconfig”命令就可以看到eth0了。至此,虚拟网络设备的创建完毕。
发包
我们知道Linux中发包的流程是,上层的协议(TCP, IP等)将数据封装成sk_buff格式,传给底层的网络设备(如以太网),然后内核调用网络设备的发包函数完成发包过程。对于VNIF前端的发包函数来说,它的功能也符合这个流程,那就是:接收上层传下来的sk_buff, 将包发送出去。只是由于前端是虚拟设备,因此不是真正将包发送到物理设备上,而是需要将包进一步转发到后端,由后端真正完成包的转发。
下面我们来了解VNIF是如何使用自定义的接口实现发包的(network_start_xmit):
1. 发包函数收到sk_buff之后,首先申请授权表的表项,然后调用授权表操作赋予后端读sk_buff的数据页的权限。
2. 发包函数申请环缓冲区的表项,在里面放置发包请求,发包请求中包含了上一步申请的
授权表表项的索引。后端将使用授权表表项的索引来找到sk_buff的数据。
3. 发包函数发事件通道通知,告知后端有发包请求需要处理。
4. 后端会异步的处理该请求,具体步骤后面部分详细描述。
5. 发包函数调用network_tx_buf_gc来回收步骤1、2所分配的环缓冲区和授权表,这里是回收已经经过后端处理过的请求。对于未处理的请求,将会等到下一次调用再回收。
收包 (netif_poll)
收包函数和发包函数类似,也是通过事件通道、环缓冲区和授权表来和后端进行交互,完成收包功能。和发包函数略有不同的是,收包采用的不是中断方式,而是轮询方式。这是因为Linux的开发者发现,对于网络设备而言,由于网络包到达很频繁,轮询其实比中断更加高效。因此Linux内核中为网络设备中提供了轮询接口,VNIF采用了这个接口。
收包函数主要的工作是
1. 首先在环缓冲区中放置一定量空的数据页, 通过授权表允许后端读写或者页交换。
2. 当后端发现包到达时,会将包的数据放置到环缓冲区中的空页中,这里可以采用采用读写复制或者页交换的方式来传输包数据。
3. 收包函数轮询时,如果在环缓冲区中发现已经收到的包,就将数据封装成sk_buff传递到上层协议(IP),同时继续分配空的数据页到环缓冲区中。
3.2. VNIF的后端(VNIF backend)
VNIF后端作为内核模块在Domain0中运行,起着前端和Domain0物理网卡之间桥梁的作用。
和前端相对应,VNIF后端也主要实现了虚拟设备的创建,收包/发包这些功能。如前所述,前端的功能最终是通过后端才真正得以实现的,那么后端是怎么实现的呢?下面我们来分析
创建虚拟设备
Dom 0
虚拟网桥
Dom 1
xenbr0
peth0
vif1.0
eth0
后端 前端
图: 后端的虚拟设备
读者也许会问,既然前端已经有虚拟设备了,为什么后端还要创建虚拟设备呢?从图中就可以看的很清楚了。Domain0的后端需要为前端的多个网络接口服务,并且把包根据MAC地址进行分发。熟悉网桥的读者肯定会脱口而出,这不就是网桥的功能吗。不错,由于Linux对网桥(bridge)具有良好的支持,我们可以非常容易的创建一个软件网桥,然后把虚拟设
备逐一挂上去,实现包的转发。同时,前端创建的虚拟设备不在Dom0中,没法挂到网桥上,所以,后端需要为每个前端的虚拟设备在Dom0中创建一个虚拟设备,作为代表挂到网桥上去。图中vif1.0就是后端创建的设备,对应的是Dom1中的eth0。当然,如果给Dom1配置了多个网络设备,后端相应的也会创建多个设备。
具体的创建过程并不复杂,首先是后端调用alloc_netdev来创建并初始化虚拟网络设备,然后调用register_netdevice向内核注册该设备。最后Control Panel会在用户态使用brctl addif命令将虚拟设备vifx.y加入到网桥当中去。
发包/收包
后端的发包和收包与前端的发包收包一一对应,流程如下所示:
后端得到前端发来的事件通道通知:环缓冲区有包要发
后端从网桥那边接收到数据包
后端从环缓冲区中得到包请求
后端从环缓冲区中取出前端预先放置的收包请求
后端通过授权表获得包数据的读权限,并将包数据拷贝到dom0内存中
后端根据收包请求,通过授权表获得数据页的控制权
拷贝还是页交换
后端将拷贝过来的数据包向网桥发,网桥负责转发到物理设备上
拷贝数据包
通过事件通道通知前端有数据包
数据包页交换
后端发包流程
后端收包流程
这里解释一下收包流程中的数据包拷贝和页交换:所谓拷贝很容易理解,就是将收到的数据包拷贝到前端所提供的数据页上,这样前端就收到数据了。所谓页交换,简单来说,是将后端与前端的页进行交换。举例来说,假设PAGE A是Dom0的位于物理地址P0的页,
PAGE B是Dom1的位于物理地址P1的页,那么经过页交换之后,PAGEA就变成Dom1位于物理地址P1的页,而PAGEB则变成Dom0位于物理地址P0的页。由此可以看出,页交换实际上实现了包的零拷贝,是一种高效的实现。
那么,既然页交换高效,为什么还要提供页拷贝方式呢?这是因为,页交换未必所有时候都高效,它依赖于Hypervisor内存管理的实现。事实上,当使用影子内存(shadow memory)方式的时候,页交换的效率并不如页拷贝方式。因此,对于使用影子内存的虚拟机来说,页拷贝方式是一个更好的选择。当然,随着影子内存代码的改进,页交换方式最终还是能够比页拷贝更高效的,到时候页拷贝方式也许就真的不需要了。
4. 总结
本文介绍了Xen的I/O设备模型,包括完全虚拟化和泛虚拟化的I/O模型。两种模型各
有优缺点:完全虚拟化的方法通用性很好,但是性能上存在瓶颈;泛虚拟化的方法通用性稍差,但是性能很好。本文着重介绍了泛虚拟化方法在Xen中的实现:VBD/VNIF,包括了Xen底层支持和以及VBD/VNIF的流程。相信读者通过阅读本文,能够对Xen的I/O设备模型有一个初步的了解。
本文发布于:2024-02-04 10:02:02,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170701212253210.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |