kvm-qemu 裝置IO虛擬化
- 虛擬裝置的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空間就成功的註冊了。
- 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的攔截和處理。