QEMU+KVM學習筆記

1 QEMU和KVM的關係:
     現在所說的虛擬化,一般都是指在CPU硬件支持基礎之上的虛擬化技術。KVM也同hyper-V、Xen一樣依賴此項技術。沒有CPU硬件虛擬化的支持,KVM是無法工作的。
     準確來說,KVM是Linux的一個模塊。可以用modprobe去加載KVM模塊。加載了模塊後,才能進一步通過其他工具創建虛擬機。但僅有KVM模塊是 遠遠不夠的,因爲用戶無法直接控制內核模塊去作事情:還必須有一個用戶空間的工具纔行。這個用戶空間的工具,開發者選擇了已經成型的開源虛擬化軟件 QEMU。說起來QEMU也是一個虛擬化軟件。它的特點是可虛擬不同的CPU。比如說在x86的CPU上可虛擬一個Power的CPU,並可利用它編譯出 可運行在Power上的程序。KVM使用了QEMU的一部分,並稍加改造,就成了可控制KVM的用戶空間工具了。所以你會看到,官方提供的KVM下載有兩 大部分三個文件,分別是KVM模塊、QEMU工具以及二者的合集。也就是說,你可以只升級KVM模塊,也可以只升級QEMU工具。
 
2 QEMU基本介紹
     Qemu是一個完整的可以單獨運行的軟件,它可以用來模擬機器,非常靈活和可移植。它主要通過一個特殊的'重編譯器'將爲特定處理器編寫二進制代碼轉換爲另一種。(也就是,在PPCmac上面運行MIPS代碼,或者在X86 PC上運行ARM代碼)
 
3 KVM基本介紹
     KVM是一個基於Linux內核的虛擬機,它屬於完全虛擬化範疇,從Linux-2.6.20開始被包含在Linux內核中。KVM基於x86硬件虛擬化技術,它的運行要求Intel VT-x或AMD SVM的支持。
     一般認爲,虛擬機監控的實現模型有兩類:監控模型(Hypervisor)和宿主機模型(Host-based)。由於監控模型需要進行處理器調度,還需要實現各種驅動程序,以支撐運行其上的虛擬機,因此實現難度上一般要大於宿主機模型。KVM的實現採用宿主機模型(Host-based),由於KVM是集成在Linux內核中的,因此可以自然地使用Linux內核提供的內存管理、多處理器支持等功能,易於實現,而且還可以隨着Linux內核的發展而發展。另外,目前KVM的所有I/O虛擬化工作是藉助Qemu完成的,也顯著地降低了實現的工作量。以上可以說是KVM的優勢所在。
 
4 KVM架構
kvm基本結構有2個部分構成:
* kvm 驅動:現在已經是linux kernel的一個模塊了。其主要負責虛擬機的創建,虛擬內存的分配,VCPU寄存器的讀寫以及VCPU的運行。
* Qemu:用於模擬虛擬機的用戶空間組件,提供I/O設備模型,訪問外設的途徑。

 
kvm基本結構如上圖。kvm已經是內核模塊,被看作是一個標準的linux 字符集設備(/dev/kvm)。Qemu通過libkvm應用程序接口,用fd通過ioctl向設備驅動來發送創建,運行虛擬機命令。設備驅動kvm就會來解析命令(kvm_dev_ioctl函數在kvm_main.c文件中),如下圖:

 
 
 
KVM模塊讓Linux主機成爲一個虛擬機監視器(VMM,Virtual Machine Monitor),並且在原有的Linux兩種執行模式基礎上,新增加了客戶模式,客戶模式擁有自己的內核模式和用戶模式。在虛擬機運行時,三種模式的工作各爲:
 
客戶模式:執行非I/O的客戶代碼,虛擬機運行在這個模式下。
用戶模式:代表用戶執行I/O指令,qemu運行在這個模式下。
內核模式:實現客戶模式的切換,處理因爲I/O或者其他指令引起的從客戶模式退出(VM_EXIT)。kvm 模塊工作在這個模式下。
在kvm的模型中,每一個Gust OS都是作爲一個標準的linux進程,都可以使用linux進程管理命令管理。
 
KVM三種類型的文件描述符
     首先是kvm設備本身。kvm內核模塊本身是作爲一個設備驅動程序安裝的,驅動的設備名稱是”/dev/kvm“。要使用kvm,需要先用open打開”/dev/kvm”設備,得到一個kvm設備文件描述符fd,然後利用此fd調用ioctl就可以向設備驅動發送命令了。kvm驅動解析此種請求的函數是kvm_dev_ioctl(kvm_main.c),如KVM_CREATE_VM。
 
     其次是具體的VM。通過KVM_CREATE_VM創建了一個VM後,用戶程序需要發送一些命令給VM,如KVM_CREATE_VCPU。這些命令當然也是要通過ioctl來發送,所以VM也需要對應一個文件描述符纔行。用戶程序中用ioctl發送KVM_CREATE_VM得到的返回值就是新創建VM對應的fd,之後利用此fd發送命令給此VM。kvm驅動解析此種請求的函數是kvm_vm_ioctl。此外,與OS線程類似,每個VM在kvm驅動中會對應一個VM控制塊結構struct kvm,每個對VM的內核操作都基本要訪問這個結構,那麼kvm驅動是如何找到請求這次命令的VM的控制塊的呢?回答這個問題首先要知道,linux內核用一個struct file結構來表示每個打開的文件,其中有一個void *private_data字段,kvm驅動將VM控制塊的地址保存到對應struct file的private_data中。用戶程序發送ioctl時,指定具體的fd,內核根據fd可以找到相應的struct file,傳遞給kvm_vm_ioctl,再通過private_data就可以找到了。
 
     最後是具體的VCPU。原理基本跟VM情況差不多,kvm驅動解析此種請求的函數是kvm_vcpu_ioctl。VCPU控制塊結構爲struct kvm_vcpu。
 
5 KVM工作原理
    KVM的基本工作原理:用戶模式的Qemu利用接口libkvm通過ioctl系統調用進入內核模式。KVM Driver爲虛擬機創建虛擬內存和虛擬CPU後執行VMLAUCH指令進入客戶模式。裝載Guest OS執行。如果Guest OS發生外部中斷或者影子頁表(shadow page)缺頁之類的事件,暫停Guest OS的執行,退出客戶模式進行一些必要的處理。然後重新進入客戶模式,執行客戶代碼。如果發生I/O事件或者信號隊列中有信號到達,就會進入用戶模式處理。KVM採用全虛擬化技術。客戶機不用修改就可以運行。

 
 
二、QEMU簡介
1 QEMU的框架
    QEMU屬於應用層的仿真程序,它支持兩種操作模式:用戶模式仿真和系統模式仿真。用戶模式仿真 允許一個 CPU 構建的進程在另一個 CPU 上執行(執行主機 CPU 指令的動態翻譯並相應地轉換 Linux 系統調用)。系統模式仿真 允許對整個系統進行仿真,包括處理器和配套的外圍設備。每個模擬方式都有其安裝要求。對於系統模擬,安裝客戶操作系統就像安裝一臺單獨的計算機(下載並使用預先配置的磁盤鏡像是一個替代方法)。對於用戶空間模擬,不需要安裝整個系統,但要考慮安裝使用軟件所需的支持庫。也許還需要配置跨編譯器以生成想要測試的二進制文件。
 
    在本文中,我將主要介紹QEMU的系統模式下的工作方式。
 
    QEMU工作在操作系統的用戶態程序,它有一個如一般應用程序的入口點函數——main函數(vl.c源碼文件)。這個main函數也是QEMU的系統模式的入口點函數。在這個main函數中,其主要的工作就是初始化一系列的參數、硬件設備仿真等以及命令行參數轉換,進而進入一個main_loop的循環中。
 
    在QEMU中有很重要的一部分就是TCG(Tiny Code Generator),這就是一個Guest OS程序向Host OS程序轉換的內置翻譯器,主要目的是爲了在Host的硬件上面可以運行Guest的代碼。本文將主要集中在整體流程的介紹以及QEMU與KVM之間的交互,因此這部分將不會花過多精力涉及。
 
。由QEMU的開發者編寫,主要是爲了讓開發者對QEMU有個清晰的認識,但是由於該文章比較古老,因此將根據現有的設計再做調整。
--------------------------------
    Guest OS的運行涉及到Guest OS代碼的執行、timer的處理、IO處以及對monitor命令的響應。
對於這種需要回應從多重資源發來的事件的程序來說,現行有兩種比較流行的架構:
  1. Parallel architecture(平行架構)把那些可以同時執行的工作分成多個進程或是線程。我叫他線程化的架構(threaded architecture)。
  2. Event-driven architecture(事件驅動架構)通過執行一個主循環來發送事件到handler以此對事件做反饋處理。這一方法通常通過使用select(2)或者poll(2)系列的系統調用等待多個文件描述符的方式來實現。
目前,QEMU使用一種混合架構,把事件驅動和線程組合在一起。這種做法之所以有效是因爲只在單個線程上執行的事件循環不能有效利用底層多核心的硬件。再則,有時候使用一個專用線程來減少特定工作的負擔要比把它整合在一個事件驅動的架構中更簡單有效。雖然如此,QEMU的核心還是事件驅動的,大多數代碼都是在這樣一個環境中執行的。
 
QEMU的事件驅動核心
一個事件驅動的架構是以一個派發事件到處理函數的循環爲核心的。
 
QEMU的主事件循環是main_loop_wait()(main-loop.c文件),它主要完成以下工作:
  1. 等待文件描述符變成可讀或可寫。文件描述符是一個關鍵角色,因爲files、sockets、pipes以及其他各種各樣的資源都是文件描述符(file descriptors)。文件描述符的增加方式:qemu_set_fd_handler()。
  2. 處理到期的定時器(timer)。定時器的管理在qemu-timer.c文件中。
  3. 執行bottom-halves(BHs),它和定時器類似會立即過期。BHs用來放置回調函數的重入和溢出。BHs的添加方式:qemu_bh_schedule()。
當一個文件描述符準備好了、一個定時器過期或者是一個BH被調度到時,事件循環就會調用一個回調函數來回應這些事件。回調函數對於它們的環境有兩條規則:
  1. 沒有其他核心同時在執行,所以不需要考慮同步問題。對於核心代碼來說,回調函數是線性和原子執行的。在任意給定的時間裏只有一個線程控制執行核心代碼。
  2. 不應該執行可阻斷系統調用或是長運行計算(long-running computations)。由於事件循環在繼續其他事件時會等待當前回調函數返回,所以如果違反這條規定會導致guest暫停並且使管理器無響應。
第二條規定有時候很難遵守,在QEMU中會有代碼會被阻塞。事實上,qemu_aio_wait()裏面還有嵌套循環,它會等待那些頂層事件循環正在處理的事件的子集。慶幸的是,這些違反規則的部分會在未來重新架構代碼時被移除。新代碼幾乎沒有合理的理由被阻塞,而解決方法之一就是使用專屬的工作線程來卸下(offload)這些長執行或者會被阻塞的代碼。
 
卸下特殊的任務到工作線程
儘管很多I/O操作可以以一種非阻塞的形式執行,但有些系統調用卻沒有非阻塞的替代方式。再者,長運行的計算單純的霸佔着CPU並且很難被分割到回調函數中。在這種情況下專屬的工作線程就可以用來小心的將這些任務移出核心QEMU。
 
posix-aio-compat.c中有一個工作線程的例子,一個異步的文件I/O實現。當核心QEMU放出一個aio請求,這個請求被放到一個隊列總。工作線程從隊列中拿出這個請求,並在核心QEMU中執行它。它們可能會有阻塞的動作,但因爲它們在它們自己的線程中執行所以並不會阻塞剩餘的QEMU執行。這個實現對於必要的同步以及工作線程和核心QEMU的通信有小心的處理。
 
另一個例子是ui/vnc-jobs-async.c中將計算密集型的鏡像解壓縮和解碼移到工作線程中。
 
因爲核心QEMU的主要部分不是線程安全的,所以工作線程不能調用到核心QEMU的代碼。當然簡單的使用類似qemu_malloc()的函數是線程安全的,這些是例外,而不在規則之內。這也引發了工作線程如何將事件傳回核心QEMU的問題。
 
當一個工作線程需要通知核心QEMU時,一個管道或者一個qemu_eventfd()文件描述符將被添加到事件循環中。工作線程可以向文件描述符中寫入,而當文件描述符變成可讀時,事件循環會調用回調函數。另外,必須使用信號來確保事件循環可以在任何環境下執行。這種方式在posix-aio-compat.c中被使用,而且在瞭解guest代碼如何被執行之後變的更有意義。
 
執行guest代碼
目前爲止我們已經大概的看了一下QEMU中的事件循環和它的主要規則。其中執行guest代碼的能力是特別重要的,少了它,QEMU可以響應事件但不會非常有用。
 
這裏有兩種方式用來執行guest代碼:Tiny Code Generator(TCG)和KVM。TCG通過動態二進制轉化(dynamic binary translation)來模擬guest,它也以即時編譯(Just-in-Time compilation)被熟知。而KVM則是利用現有的現代intel和AMD CPU中硬件虛擬化擴展來直接安全的在host CPU上執行guest代碼。在這篇文章中,真正重要的並不是實際的技術,不管是TCG還是KVM都允許我們跳轉到guest代碼中並且執行它。
 
跳入guest代碼中會使我們失去對程序執行的控制而把控制交給guest。而一個正在執行guest代碼的線程不能同時處在事件循環中,因爲guest控制着CPU。一般情況下,花在guest代碼中的時間是有限的。因爲對於被模擬設備的寄存器的讀寫和其他異常導致我們離開guest而把控制交還給QEMU。在極端的情況下一個guest可以花費無限制的時間而不放棄控制權,而這會引起QEMU無響應。
 
爲了解決guest代碼霸佔問題,QEMU線程使用信號來跳出guest。一個UNIX信號從當前的執行流程中抓取控制權並調用一個信號處理函數。這使得QEMU得以採取措施來離開guest代碼並返回它的主循環,因而事件循環纔有機會處理待解決的事件。
 
上述的結果是新事件可能第一時間被發覺如果QEMU當前正在guest代碼中。事實上QEMU大多數時間規避處理事件,但因而產生的額外的延遲也成爲的效能問題。因此,到核心QEMU的I/O結束和從工作線程來的通知使用信號來確保事件循環會被立即處理。
 
你可能會疑惑說到底事件循環和有多核心的SMP guest之間的架構圖會是什麼樣子的。而現在,線程模型和guest代碼都已經提到了,現在我們來討論整體架構。
 
IOTHREAD和NON-IOTHREAD線程架構
傳統的架構是單個QEMU線程來執行guest代碼和事件循環。這個模型就是所謂的non-iothread或者說!CONFIG_IOTHREAD,它是QEMU默認使用./configure && make的設置。QEMU線程執行guest代碼直到一個異常或者信號出現纔回到控制器。然後它在select(2)不被阻塞的情況執行一次事件循環的一次迭代。然後它又回到guest代碼中並重覆上述過程直到QEMU被關閉。
 
如果guest使用,例如-smp 2,以啓動一個多vcpu啓動,也不會有多的QEMU線程被創建。取而代之的是在單個QEMU線程中多重執行兩個vcpu和事件循環。因而non-iothread不能很好利用多核心的host硬件,而使得對SMP guest的模擬性能很差。
 
需要注意的是,雖然只有一個QEMU線程,但可能會有0或多個工作線程。這些線程可能是臨時的也可能是永久的。記住這些工作線程只執行特殊的任務而不執行guest代碼也不處理事件。我之說以要強調這個是因爲當監視host時很容易被工作線程迷惑而把他們當做vcpu線程來中斷。記住,non-iothread只有一個QEMU線程。
 
一種更新的架構是每個vcpu一個QEMU線程外加一個專用的事件循環線程。這個模型被定義爲iothread或者CONFIG_IOTHREAD,它可以通過./configure --enable-io-thread在創建時開啓。每個vcpu線程可以平行的執行guest代碼,以此提供真正的SMP支持。而iothread執行事件循環。核心QEMU代碼不能同時執行的規則通過一個全局互斥來維護,並通過該互斥鎖同步vcpu和iothread間核心QEMU代碼。大多數時候vcpu線程在執行guest代碼而不需要獲取全局互斥鎖。大多數時間iothread被阻塞在select(2)因而也不需要獲取全局互斥鎖。
 
注意,TCG不是線程安全的,所以即使在在iothread模式下,它還是在一個QEMU線程中執行多個vcpu。只有KVM可以真正利用每個vcpu一個線程的優勢。
 
2 QEMU的線程
    HOST將qemu當做一個普通的進程和其他進程統一調度,可以使用資源對qemu進行資源預留隔離(cpuset)和優先級提升(chrt)。qemu進程包含多個線程,分配給GUEST的每個vcpu都對應一個vcpu線程,另外qemu還有一個線程循環執行select專門處理I/O事件。
QEMU的主要線程:
  • 主線程(main_loop),一個
  • vCPU線程,一個或者多個
  • I/O線程(aio),一個或者多個
  • worker thread(VNC/SPICE),一個
qemu裏有個主線程處於無限循環,會做如下操作
  • IO線程裏有個select函數,它阻塞在一個文件描述符(fd)集合上,等待其就緒。fd可以通過qemu_set_fd_handler()
  • 運行到期的定時器,定時器通過qemu_mod_timer添加
  • 運行BH(bottom-halves),BH通過qemu_bh_schedule添加
當文件描述符就緒,定期器到期或者BH被調度,相應的callback會被調用
 
qemu中還有一些worker threads。一些佔用CPU較多的工作會明顯增大主IO線程的IO處理延遲,這些工作可以放在專用的線程裏,例如posix-aio-compat.c中實現了異步文件I/O,當有aio請求產生,該請求被置於隊列,工作線程可以在qemu主線程之外處理這些請求。VNC就是這樣一個例子,它用了一個專門的worker thread(ui/vnc-jobs.c)進行計算密集型的圖像壓縮和編碼工作。
 
3 QEMU的初始化流程
    待續
 
4 QEMU虛擬網卡設備的創建流程
虛擬網卡類型爲virtio-net-pci
virtio網卡設備對應的命令行參數爲 
-device virtio-net-pci,netdev=hostnet0,id=net0,mac=00:16:36:01:c4:86,bus=pci.0,addr=0x3
 
1). 在parse命令行的時候,qemu把所有的-device選項parse後保存到qemu_device_opts中
2). 調用module_call_init(MODULE_INIT_DEVICE); 往系統中添加所有支持的設備類型
   virtio-net-pci的設備類型信息如下(virtio-pci.c):
static PCIDeviceInfo virtio_info[] = {
    {
        .qdev.name  = "virtio-net-pci",
        .qdev.size  = sizeof(VirtIOPCIProxy),
        .init       = virtio_net_init_pci,
        .exit       = virtio_net_exit_pci,
        .romfile    = "pxe-virtio.bin",
        .qdev.props = (Property[]) {
            DEFINE_PROP_BIT("ioeventfd", VirtIOPCIProxy, flags,
                            VIRTIO_PCI_FLAG_USE_IOEVENTFD_BIT, false),
            DEFINE_PROP_UINT32("vectors", VirtIOPCIProxy, nvectors, 3),
            DEFINE_VIRTIO_NET_FEATURES(VirtIOPCIProxy, host_features),
            DEFINE_NIC_PROPERTIES(VirtIOPCIProxy, nic),
            DEFINE_PROP_UINT32("x-txtimer", VirtIOPCIProxy,
                               net.txtimer, TX_TIMER_INTERVAL),
            DEFINE_PROP_INT32("x-txburst", VirtIOPCIProxy,
                              net.txburst, TX_BURST),
            DEFINE_PROP_STRING("tx", VirtIOPCIProxy, net.tx),
            DEFINE_PROP_END_OF_LIST(),
        },
        .qdev.reset = virtio_pci_reset,
    }
   };
 
3). 調用qemu_opts_foreach(&qemu_device_opts, device_init_func, NULL, 1) 創建命令行上指定的設備
4). device_init_func調用qdev_device_add(opts)
5). qdev_device_add函數的流程如下:
   a) 調用qemu_opt_get(opts, "driver")獲取driver選項,這裏應該是virtio-net-pci
   b) 調用qdev_find_info(NULL, driver)來獲取註冊的DeviceInfo,這裏應該是上面virtio_info裏面關於
      virtio-net-pci的結構
   c) 調用qemu_opt_get(opts, "bus")獲取bus路徑,以/分隔各組件。這裏是pci.0
   d) 如果bus路徑不爲空,則調用qbus_find(path)來獲取bus實例(BusState結構)
      qbus_find函數的流程如下:
      d.1) 先找到路徑中的根bus,如果路徑以/開頭,則根bus爲main_system_bus,否則,使用
           qbus_find_recursive(main_system_bus, elem, NULL)來查找。這裏的elem = "pci.0"
      d.2) 如果整個路徑已經完成,則返回當前bus
      d.2) parse出下一個組件,調用qbus_find_dev查找對應的設備
      d.3) parse出下一個組件,調用qbus_find_bus查找屬於上層設備的子bus
      d.4) 返回步驟2
      由於這裏的值是pci.0,因此其實只進行了一次qbus_find_recursive調用
   e) 如果bus路徑爲空,則調用qbus_find_recursive(main_system_bus, NULL, info->bus_info)來獲取bus
      實例。這裏的info是driver("virtio-net-pci")所對應的DeviceInfo,即最上面的結構
      virtio-pci的初始化步驟是virtio_pci_register_devices -> pci_qdev_register_many -> 
      pci_qdev_register,在該函數中,會設置info->bus_info = &pci_bus_info,這樣就把PCIDeviceInfo
      和pci的BusInfo聯繫起來了
      qbus_find_recursive是一個遞歸函數,其流程如下:
      e.1) 如果當前bus的名稱和指定的名稱相同(指定名稱不爲空的情況下),並且當前bus指向的bus info和
           指定的bus info相同(指定bus info不爲空的情況下),則返回當前bus
      e.2) 這裏是一個兩重循環:
           對於當前bus所有附屬的設備(bus->children爲鏈表頭)
               對於當前設備所有的附屬bus(dev->child_bus爲鏈表頭)
                   調用qbus_find_recursive函數
   f) 調用qdev_create_from_info(bus, info)來創建設備,返回的是DeviceState結構。這裏其實返回的是
      一個VirtIOPCIProxy實例,因爲create的時候是根據qdev.size來分配內存大小的。
   g) 如果qemu_opts_id(opts)不爲空,則設置qdev->id
   h) 調用qemu_opt_foreach(opts, set_property, qdev, 1)來設置設備的各種屬性
   i) 調用qdev_init來初始化設備。
   j) qdev_init會調用dev->info->init函數。這裏實際調用的函數是virtio_net_init_pci
 
 
在這裏也大致描述一下bus pci.0是如何生成的
1). 在main函數裏面很前面的地方會調用module_call_init(MODULE_INIT_MACHINE);
2). module_call_init會調用所有已註冊QEMUMachine的init函數。該版本的qemu是註冊了
   pc_machine_rhel610, pc_machine_rhel600, pc_machine_rhel550, pc_machine_rhel544,
   pc_machine_rhel540這幾個 (pc.c)
3). 這些Machine的init函數(pc_init_rhel600, ...)都會調用到pc_init_pci函數
4). pc_init_pci會調用pc_init1,pc_init1在pci_enabled情況下會調用i440fx_init (piix_pci.c)
5). i440fx_init首先會調用qdev_create(NULL, "i440FX-pcihost")創建一個host device
6). 然後調用pci_bus_new在該設備下面創建一個附屬的pci bus。在調用該函數時,傳遞的name爲NULL。
   下面再看看這個bus的名稱怎麼會變成pci.0的
7). pci_bus_new調用pci_bus_new_inplace(bus, parent, name, devfn_min),其中bus指向剛分配的
   內存,parent是前面創建的host device,name爲NULL,devfn_min爲0
8). pci_bus_new_inplace會調用qbus_create_inplace(&bus->qbus, &pci_bus_info, parent, name),
   注意這裏的第二個參數是&pci_bus_info
9). qbus_create_inplace在開始的地方會爲該bus生成一個名稱。因爲傳遞進來的name爲NULL,並且
   parent(那個host device)的id也爲NULL,因此分支會跳到下面的代碼
        
        len = strlen(info->name) + 16;
        buf = qemu_malloc(len);
        len = snprintf(buf, len, "%s.%d", info->name,
                       parent ? parent->num_child_bus : 0);
        for (i = 0; i < len; i++)
            buf[i] = qemu_tolower(buf[i]);
        bus->name = buf;
10). 在該段代碼中,info就是之前pci_bus_new_inplace調用時傳進來的&pci_bus_info,info->name是
    字符串"PCI"。並且,因爲這是在host device上創建的第一個bus,因此parent->num_child_bus = 0,
    最後經過小寫處理之後,該bus的名稱就成爲了"pci.0"
 
    這一段分析所對應的bus/device layout如下
    main-system-bus ---->  i440FX-pcihost ----> pci.0
 
與這段流程類似的有一張流程圖可以更加詳盡的介紹一下流程,但與上文介紹的內容不是一一對應的。
 
5 QEMU網卡的流程
 
 
 
6 QEMU中使用BIOS的流程分析
http://www.ibm.com/developerworks/cn/linux/1410_qiaoly_qemubios/
 
 
三、相關技術-處理器管理和硬件輔助虛擬化技術
    Intel 在2006年發佈了硬件虛擬化技術。其中支持X86體系結構的稱爲Intel VT-x技術。ADM稱爲SVM技術。
VT-x引入了一種新的處理器操作,叫做VMX(Virtual Machine Extension),提供了兩種處理器的工作環境。VMCS結構實現兩種環境之間的切換。VM Entry使虛擬機進去客戶模式,VM Exit使虛擬機退出客戶模式。
1 KVM中Guest OS的調度執行
    VMM調度Guest OS執行時,Qemu通過ioctl系統調用進入內核模式,在KVM Driver中通過get_cpu獲得當前物理CPU的引用。之後將Guest狀態從VMCS中讀出。並裝入物理CPU中。執行VMLAUCH指令使得物理處理器進入非根操作環境,運行客戶代碼。
    當Guest OS執行一些特權指令或者外部事件時,比如I/O訪問,對控制寄存器的操作,MSR的讀寫數據包到達等。都會導致物理CPU發生VMExit,停止運行Guest OS。將Guest OS保存到VMCS中,Host狀態裝入物理處理器中,處理器進入根操作環境,KVM取得控制權,通過讀取VMCS中VM_EXIT_REASON字段得到引起VM Exit的原因。從而調用kvm_exit_handler處理函數。如果由於I/O獲得信號到達,則退出到用戶模式的Qemu處理。處理完畢後,重新進入客戶模式運行虛擬CPU。如果是因爲外部中斷,則在Lib KVM中做一些必要的處理,重新進入客戶模式執行客戶代碼。
2 KVM中內存管理
    KVM使用影子頁表實現客戶物理地址到主機物理地址的轉換。初始爲空,隨着虛擬機頁訪問實效的增加,影子頁表被逐漸建立起來,並隨着客戶機頁表的更新而更新。在KVM中提供了一個哈希列表和哈希函數,以客戶機頁表項中的虛擬頁號和該頁表項所在頁表的級別作爲鍵值,通過該鍵值查詢,如不爲空,則表示該對應的影子頁表項中的物理頁號已經存在並且所指向的影子頁表已經生成。如爲空,則需新生成一張影子頁表,KVM將獲取指向該影子頁表的主機物理頁號填充到相應的影子頁表項的內容中,同時以客戶機頁表虛擬頁號和表所在的級別生成鍵值,在代表該鍵值的哈希桶中填入主機物理頁號,以備查詢。但是一旦Guest OS中出現進程切換,會把整個影子頁表全部刪除重建,而剛被刪掉的頁表可能很快又被客戶機使用,如果只更新相應的影子頁表的表項,舊的影子頁表就可以重用。因此在KVM中採用將影子頁表中對應主機物理頁的客戶虛擬頁寫保護並且維護一張影子頁表的逆向映射表,即從主機物理地址到客戶虛擬地址之間的轉換表,這樣VM對頁表或頁目錄的修改就可以觸發一個缺頁異常,從而被KVM捕獲,對客戶頁表或頁目錄項的修改就可以同樣作用於影子頁表,通過這種方式實現影子頁表與客戶機頁表保持同步。
3 KVM中設備管理
    一個機器只有一套I/O地址和設備。設備的管理和訪問是操作系統中的突出問題、同樣也是虛擬機實現的難題,另外還要提供虛擬設備供各個VM使用。在KVM中通過移植Qemu中的設備模型(Device Model)進行設備的管理和訪問。操作系統中,軟件使用可編程I/O(PIO)和內存映射I/O(MMIO)與硬件交互。而且硬件可以發出中斷請求,由操作系統處理。在有虛擬機的情況下,虛擬機必須要捕獲並且模擬PIO和MMIO的請求,模擬虛擬硬件中斷。
    捕獲PIO:由硬件直接提供。當VM發出PIO指令時,導致VM Exit然後硬件會將VM Exit原因及對應的指令寫入VMCS控制結構中,這樣KVM就會模擬PIO指令。MMIO捕獲:對MMIO頁的訪問導致缺頁異常,被KVM捕獲,通過X86模擬器模擬執行MMIO指令。KVM中的I/O虛擬化都是用戶空間的Qemu實現的。所有PIO和MMIO的訪問都是被轉發到Qemu的。Qemu模擬硬件設備提供給虛擬機使用。KVM通過異步通知機制以及I/O指令的模擬來完成設備訪問,這些通知包括:虛擬中斷請求,信號驅動機制以及VM間的通信。
以虛擬機接收數據包來說明虛擬機和設備的交互。

(1)當數據包到達主機的物理網卡後,調用物理網卡的驅動程序,在其中利用Linux內核中的軟件網橋,實現數據的轉發。
(2)在軟件網撟這一層,會判斷數據包是發往那個設備的,同時調用網橋的發送函數,向對應的端口發送數據包。
(3)若數據包是發往虛擬機的,則要通過tap設備進行轉發,tap設備由兩部分組成,網絡設備和字符設備。網絡設備負責接收和發送數據包,字符設備負責將數據包往內核空間和用戶空間進行轉發。Tap網絡部分收到數據包後,將其設備文件符置位,同時向正在運行VM的進程發出I/O可用信號,引起VM Exit,停止VM運行,進入根操作狀態。KVM根據KVM_EXIT_REASON判斷原因,模擬I/O指令的執行,將中斷注入到VM的中斷向量表中。
(4)返回用戶模式的Qemu中,執行設備模型。返回到kvm_main_loop中,執行kvm_main_loop_wait,之後進入main_loop_wait中,在這個函數裏收集對應設備的設備文件描述符的狀態,此時tap設備文件描述符的狀態同樣被集到fd set。
(5)kvm_main_loop不停地循環,通過select系統調用判斷哪個文件描述符的狀態發生變化,相應的調用對應的處理函數。對予tap來說,就會通過qemu_send_packet將數據發往rtl8139_do_receiver,在這個函數中完成相當於硬件RTL8139網卡的邏輯操作。KVM通過模擬I/O指令操作虛擬RTL8139將數據拷貝到用戶地址空間,放在相應的I/O地址。用戶模式處理完畢後返回內核模式,而後進入客戶模式,VM被再次執行,繼續收發數據包。
 
三、 源碼分析
1 源碼文件結構
源碼文件主要是分爲三部分:kvm核心代碼(平臺無關)、kvm平臺相關代碼以及頭文件
kvm核心代碼目錄:virt/kvm,其中所包含文件:
     * ioapic.h
     * ioapic.c
     * iodev.h
     * kvm_main.c
 
kvm平臺相關源代碼文件。比如針對intel的HVM支持的vmx.c文件,以及針對AMD的HVM支持的svm.c文件。其所在目錄爲:arch/x86/kvm,其中所包含的文件爲:
    * Kconfig
    * Makefile
    * i8259.c
    * irq.c
    * irq.h
    * kvm_svm.h
    * lapic.c
    * lapic.h
* mmu.c
* mmu.h
* paging_tmpl.h
* segment_descriptor.h
* svm.c
* svm.h
* vmx.c
* vmx.h
* x86.c
* x86_emulate.c
 
 
頭文件分爲兩種,根據平臺分爲include/linux和include/asm-x86目錄。
include/linux目錄包含的是通用pc上linux的頭文件,其對應文件爲:
* kvm.h
* kvm_host.h
* kvm_para.h
* kvm_x86_emulate.h
 
include/asm-x86/
* kvm.h
* kvm_host.h
* kvm_para.h
* kvm_x86_emulate.h
 
2 KVM創建和運行虛擬機的流程
    KVM虛擬機創建和運行虛擬機分爲用戶態和核心態兩個部分,用戶態主要提供應用程序接口,爲虛擬機創建虛擬機上下文環境,在libkvm中提供訪問內核字符設備/dev/kvm的接口;內核態爲添加到內核中的字符設備/dev/kvm,模塊加載進內核後即可進行接口用戶空間調用創建虛擬機。在創建虛擬機過程中,kvm字符設備主要爲客戶機創建kvm數據機構,創建該虛擬機的虛擬機文件描述符及其相應的數據結構以及創建虛擬處理器及其相應的數據結構。Kvm創建虛擬機的流程如下圖所示。
 

    首先申明一個kvm_context_t變量用以描述用戶態虛擬機上下文信息,然後調用kvm_init()函數初始化虛擬機上下文信息;函數kvm_create()創建虛擬機實例,該函數通過ioctl系統調用創建虛擬機相關的內核數據結構並且返回虛擬機文件描述符給用戶態kvm_context_t數據結構;創建完內核虛擬機數據結構後,再創建內核pit以及mmio等基本外設模擬設備,然後調用kvm_create_vcpu()函數來創建虛擬處理器,kvm_create_vcpu()函數通過ioctl()系統調用向由vm_fd文件描述符指向的虛擬文件調用創建虛擬處理器,並將虛擬處理器的文件描述符返回給用戶態程序,用以以後的調度使用;創建完虛擬處理器後,由用戶態的QEMU程序申請客戶機用戶空間,用以加載和運行客戶機代碼;爲了使得客戶虛擬機正確執行,必須要在內核中爲客戶機建立正確的內存映射關係,即影子頁表信息。因此,申請客戶機內存地址空間後,調用函數kvm_create_phys_mem()創建客戶機內存映射關係,該函數主要通過ioctl系統調用向vm_fd指向的虛擬文件調用設置內核數據結構中客戶機內存域相關信息,主要建立影子頁表信息;當創建好虛擬處理器和影子頁表後,即可讀取客戶機到指定分配的空間中,然後調度虛擬處理器運行。調度虛擬機的函數爲kvm_run(),該函數通過ioctl系統調用調用由虛擬處理器文件描述符指向的虛擬文件調度處理函數kvm_run()調度虛擬處理器的執行,該系統調用將虛擬處理器vcpu信息加載到物理處理器中,通過vm_entry執行進入客戶機執行。在客戶機正常運行期間kvm_run()函數不返回,只有發生以下兩種情況時,函數返回:1,發生了I/O事件,如客戶機發出讀寫I/O的指令;2,產生了客戶機和內核KVM都無法處理的異常。I/O事件處理完畢後,通過重新調用kvm_run()函數繼續調度客戶機的執行。
 
 
 
 
 
 
 
 
 
 
 
 
內存相關:http://www.linux-kvm.org/page/Memory
 
 
 
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章