在上一篇中我们通过Grant Table让DomU和Dom0能够通过内存共享进行通信,众所周知在Xen里domU的I/O都是交由dom0来完成的。而I/O driver也有两种实现方式: paravirtualized和qemu-emulated。本篇主要讨论Xen上PV Driver所使用的数据结构: I/O Device Ring。
- Grant Table
- I/O Ring Structure(本篇)
- Event Channel Implementation
- Event Channel Usage
- XenStore Usage
- Write a PV Driver
- Connect to XenBus
Origin of Virtualization
最早提出虚拟化的是Gerald J. Popek和Robert P. Goldberg在1974年发表的一篇Formal requirements for virtualizable third generation architectures中提出来的,在其中作者提到了几个对虚拟化的几个要求:
- Efficiency
- Innocuous instructions should execute directly on hardware
- Resource control
- Executed programs may not affect the system resources
- Equivalence
- The behavior of a program executing under the VMM should be the same as if the program were executed directly on the hardware (except possibly for timing and resource availability)
最后一点要求Guest是不能知道自己运行在虚拟化下的。然而这样一个要求使得当时不得不对许多情况进行二进制的模拟以致虚拟机的性能一直非常糟糕。其中I/O的模拟就是由Qemu完成的,通过Qemu来模拟一个设备,当Guest有请求的时候请求会被转发给Qemu来模拟执行。
Paravirtualization
Xen的理念是对domU进行适当的修改,让domU意识到自己是在虚拟化环境下,这样可以大大简化hypervisor的设计和实现。 而I/O部分Xen就使用了PV driver, PV driver的详细部分我会在之后的文章中去讨论。 这里需要了解的是PV driver分为前端和后端,domU里跑的是frontend, dom0里跑的是backend, 这时I/O的操作实际上通过frontend直接交由dom0在硬件上直接执行, 免去了qemu模拟带来的性能开销。
既然有front和back, 那么它们之间就需要通信, 两端通信所使用的是一个生产者消费者模型的ring, 由一方填入数据,另一方处理数据。
Ring
Ring的好处在于异步通信,domU的front可以把数据丢到ring里立刻执行接下来的任务,不用等dom0的back进行回应。而ring的建立和数据传递则要以来Grant Table机制。由于front的request和back的response是相同频率(每次backend都消费一个request并填回一个response), 所以一个ring刚好服务一对driver。
response的开头永远紧接着request的结尾,因此只要比较request start和response end就能知道ring是不是已经用完了,用完的话就扩充整个buffer。
1 | /* To make a new ring datatype, you need to have two message structures, |
在ring.h
中定义了DEFINE_RING_TYPES
宏,使用它当我们传入request和response的类型时,它就会据此类型为我们建立一个shared ring和其front end与back end。两者共同操纵shared ring, 在各自的内部又会记录consume的数目。front需要调用SHARED_RING_INIT
来初始化真正共享的页, 它会初始化start, end指针的起始位置。而后front与back分别使用FRONT_RING_INIT
与BACK_RING_INIT
初始化各自操作ring时使用的数据结构。
mytag_sring
中记录了req_prod
, rsp_prod
, req_event
, rsp_event
4个变量,
其中req/rsp_prod
记录的是最新的info的地址,
分别由front和back来更新。
而struct front/back_ring
中则记录了req/rsp_prod_pvt
和rsp/req_cons
,
每次push新的req/rsp时,都先更新pvt(pivot)变量,调用宏后宏会更新req/rsp_prod
。
在consume时就更新自己本地的rsp/req_cons
变量;在更新完后进行check,
FINAL_CHECK
宏会检查有没有pending的req/rsp并更新sring
中的rsp/req_event
。
因此只要判断(pvt - event) < (pvt - prod)
就能知道是不是应该通知远端ring中还存在未处理的内容。
Demo: Ring Buffer
Ring本身就是依靠Grant Table进行共享的(共享一个ring buffer), 而本身通过ring我们可以异步的传数据。在I/O传输中ring中传的就是grant refernece. front将一个个gref放入ring作为request, back就不断地处理这些request并返回response。以此来完成大量数据的传递。因此我们这次对之前的内存共享进行修改,在Grant Table基础上建立ring并用ring来进行通信。
目前的demo没有调用FINAL_CHECK
。
DomU
首先定义类型:
1 | /* Ring request & respond, used by DEFINE_RING_TYPES macro */ |
按照说明初始化, 并发送一个request给dom0
1 | /* Step 2: Put shared ring on this page to be shared */ |
dom0
后端的代码和前端类似,取出一个request并填回一个response。注意,这里我没有使用FINAL_CHECK
,正常使用中是要加上的。
1 | rc = back_end.ring.req_cons; |
处理的时候拷贝到本地再处理。
Summary
可以发现,其实I/O ring只不过就是一些操作共享内存的数据结构和宏定义,而对于Xen来说它只提供了Grant Table这一内存共享机制,在上面进行什么样的操作Xen并不关心。在I/O driver中就是利用ring来传递真正存放内容的内存页的grant refernece的。如果要做batch的话把FINAL_CHECK
中event的更新改为+N (N为每次batch的数目), 前端在push request的时候也要按照N个N个来push。
运行示例
运行的时候忘记了,先退出了domU的kernel module。 所以这里刚好也看一下错误退出的错误提示…
1 | alice@domU: $ sudo insmod alice_domU.ko; dmesg |