[置顶] KVM虚拟机代码揭秘——设备IO虚拟化

前言:本文针对intel VT-X技术,结合QEMU和KVM代码以及自己写的实例详细分析了一个虚拟设备的IO虚拟化过程。虽然现在KVM虚拟化性能非常好,发展也非常迅速,但是资料相对比较少,理论知识不是很成熟,其中理解上可能会有些偏差,希望大家指出并与本人联系和讨论. 联系方式: QQ:150197475  QQ群:33273092

转载的请加上此部分,以便技术的交流和本人进一步的研究。谢谢合作!

 

 

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拦截流程如下图所示:

[置顶] KVM虚拟机代码揭秘——设备IO虚拟化_第1张图片

图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的拦截和处理。

 

 

 

你可能感兴趣的:(虚拟机,IO,struct,null,虚拟化)