storage R&D guy.
全部博文(1000)
分类: LINUX
2015-04-16 15:13:24
原文地址:kvm-qemu 设备IO虚拟化 作者:cywcdwxjf
1. 虚拟设备的IO地址注册
如我们所知,KVM虚拟机的设备模拟是在QEMU中实现的,而KVM实现的实质上只是IO的拦截。换句话说,真正的虚拟设备IO地址注册是在QEMU代码里面实现的。
在QEMU中,在初始化我们的硬件设备的时候需要注册我们的IO空间,在这里有下面两种IO注册方法:
(1) PIO(port IO) 端口IO
(2) MIO(memory may IO)内存映射IO
为了说明原理,本文只讨论PIO相关的实现,MMIO类似。注册IO对对于一般的ISA设备我们可以直接调用下面的函数进行IO地址的注册,使用起来非常的简单。
int register_ioport_read(pio_addr_t start, int length, int size,IOPortReadFunc *func, void *opaque);
int register_ioport_write(pio_addr_t start, int length, int size, IOPortWriteFunc *func, void *opaque);
对于PCI设备来说,IO地址注册就要多一步,因为要进行PCI bar地址与IO的映射,所以必须先调用下面函数来给bar注册PCI地址。
void pci_register_bar(PCIDevice *pci_dev, int region_num,
pcibus_t size, uint8_t type,
PCIMapIORegionFunc *map_func);
关键参数说明:第一个是PCI设备指针,第三个是我们需要注册IO地址的空间长度,最后一个是我们要进行IO操作映射的初始化函数指针。
static void map_func(PCIDevice *pci_dev,int region_num, pcibus_t addr,pcibus_t size,int type);
关键参数说明:第一个依然是PCI设备指针,第三个是PCI地址映射的PIO起始地址,这个起始地址是在我们注册PCI地址的时候,PCI总线通过 计算比较PIO地址空间得到的一个PIO地址起始空间,所以这里不能够随便的改变,因为PCI地址空间需要和PIO空间进行映射。所以在我们注册设备 PIO空间的时候必须将这个地址作为注册IO空间的起始地址。这个函数实在更新bar映射的时候被调用的,实际上它的作用就是给PCI设备安装IO读写函 数,能够操作IO,如果在KVM里面实现IO拦截,这里的函数似乎就失去意义了。
举个例子进一步说明:
1.注册PIC地址。空间0x800,映射函数xche_ioport_map。
pci_register_bar(&s->dev,1,0x800,PCI_BASE_ADDRESS_SPACE_IO,xche_ioport_map);
2.实现映射函数,PCI bar地址初始化以后会将映射IO的起始地址作为addr参数传到映射函数,然后通过之前的register函数注册IO地址空间,在这个操作以后,一旦 这些位的IO发生读写,虚拟机就会产生VM-exit,进而我们的ioread和iowrite就能够被调用。
static void xche_ioport_map(PCIDevice *pci_dev,int region_num,pcibus_t addr,pcibus_t size,int type)
{
CXState *s = DO_UPCAST(CXState,dev,pci_dev);
register_ioport_write(addr,0x800,1,xche_ioport_writeb,s);
register_ioport_read(addr,0x800,1,xche_ioport_readb,s);
}
这样,我们虚拟的IO空间就成功的注册了。
2. KVM IO地址的拦截
我们之前已经知道,QEMU运行在用户空间,KVM运行在内核空间,客户机运行在KVM内部,QEMU通过IOCTL与KVM进行交互,从这里可以 看出,KVM直接与客户机进行交互。所以客户机的IO操作,KVM先得到,可以进行拦截,这个也是我们能实现拦截的前提条件。下面通过一个我自己实现的实 例来说明怎么在KVM里进行IO拦截。
(1)通过IOCTL,可以在QEMU中调用KVM的初始化函数,初始化KVM设备
QEMU:
kvm_vm_ioctl(kvm_state, KVM_CREATE_XCHE);
KVM:
case KVM_CREATE_XCHE:
kvm->arch.vxche = kvm_create_xche(kvm,0x1000);
(2)注册KVM设备。主要就是进行内存的分配和IO总线的注册。
static const struct kvm_io_device_ops xche_dev_ops = {
.read = xche_ioport_read,
.write = xche_ioport_write,
};
/* Caller must hold slots_lock */
struct kvm_xche *kvm_create_xche(struct kvm *kvm, gpa_t base_addr, gpa_t length)
{
struct kvm_xche *xche;
int ret;
xche = kzalloc(sizeof(struct kvm_xche), GFP_KERNEL);
if (!xche)
return NULL;
/*获取中断资源id,在KVM中注册的设备这个ID都是唯一的,对应着QEMU和KVM里面的设备*/
xche->irq_source_id = kvm_request_irq_source_id(kvm);
if (xche->irq_source_id < 0) {
kfree(xche);
return NULL;
}
xche->kvm = kvm;
kvm_iodevice_init(&xche->dev, &xche_dev_ops);
/*将设备注册到KVM里面的PIO总线*/
ret = kvm_io_bus_register_dev(kvm, KVM_PIO_BUS, xche->dev);
return xche;
}
通过上面的步骤我们就成功的注册了KVM设备,并且将我们的IO读写函数挂到了KVM的PIO总线,这样,当虚拟机退出的时候,分析需要处理IO, 就会遍历所有挂在PIO总线上的设备,分别调用它们的读写函数,这样就实现了IO操作的触发,而在虚拟机退出以后还会判断此段IO是否挂载设备,如果设备 不存在就会退回QEMU处理,否则直接在KVM内部处理,这样就实现了IO的拦截。
KVM拦截流程如下图所示:
图1 KVMIO拦截
3.KVM IO读写处理
前面完成了KVM对IO设备的添加和对IO操作的拦截,现在当我们成功拦截到IO以后应该如何操作呢?
IO操作会主动的调用我们之前设置的读写函数xche_ioport_read和xche_ioport_write。那我们需要做的就是实现这两个函数,在这里本文只简单描述实现这两个函数的框架,具体实现和具体设备相关。
下面用一个read函数来进行说明:
这个读函数,第一个是设备指针,第二个是PIO发生读写的地址,第三个是地址数据指针,我们通过改变这个指针就能实现客户机读读数据的功能。
static int xche_ioport_read(struct kvm_io_device *this, gpa_t addr, int len, void *data)
{
struct kvm_xche *xche = dev_to_xche(this);
struct kvm *kvm = xche->kvm;
u32 val = *(u32 *) data;
int pos,ret;
/*判断是否是这个设备的IO事件,实现IO地址过滤*/
if (!xche_in_range(addr))
return -EOPNOTSUPP;
val &= 0x00ff;
/*通过掩码进一步提取地址*/
pos = addr&0x1F;
/*根据不同的地址执行不同的操作*/
switch (pos){
case:
break;
...
...
}
if (len > sizeof(ret))
len = sizeof(ret);
/*将数据拷贝到读取的数据地址/
memcpy(data, (char *)&ret, len);
return 0;
}
在这个函数中,因为所有的IO退出都会触发每一个挂在在IO总线上面的设备读写函数,所以在这里 要进行一个IO地址过滤,只处理本设备映射的地址。这样通过这个函数我们就实现了IO读的虚拟化,模拟了硬件的各种IO操作,主要的模拟也就在 switch中实现,因为设备不同操作也不同,所以就不举例说明了。同样写函数的实现也类似,只是少一个操作不需要向IO地址写入数据。
总结:通过本文的描述就能够在KVM中实现添加一个自己想要虚拟的设备,这需要再QEMU挂载真是模拟的设备, 并且在KVM中进行拦截,然而KVM中的拦截是个可选过程,同样在QEMU中也能实现。不过在KVM中实现,可能让虚拟机不用再退回到用户空间,提高一定 的效率。当然不是所有的设备都适合在kVM中进行IO的拦截和处理。