關於Linux虛擬化技術KVM的科普 科普二(KVM虛擬機代碼揭祕)

代碼分析文章《KVM虛擬機代碼揭祕——QEMU代碼結構分析》、《KVM虛擬機代碼揭祕——中斷虛擬化》、《KVM虛擬機代碼揭祕——設備IO虛擬化》、《KVM虛擬機代碼揭祕——QEMU的PCI總線與設備(上)》、《KVM虛擬機代碼揭祕——QEMU的PCI總線與設備(下)》。先從大的方面分析代碼結構,而後分中斷、IO、PCI總線與設備詳細介紹。前端

KVM虛擬機代碼揭祕——QEMU代碼結構分析

關於TCG的解釋:TCG(Tiny Code Generator),QEMU的官方解釋在http://wiki.qemu-project.org/Documentation/TCGlinux

TCG的做用就是將Target的指令經過TCG前端轉換成TCG ops,進而經過TCG後端轉換成Host上運行的指令。ios

須要將QEMU移植到一個新CPU上運行,須要關注TCG後端。須要基於QEMU模擬一個新CPU,須要關注TCG前端。算法

qemu-img

在根目錄生成,參照Makefile可知有以下文件組成:後端

qemu-img$(EXESUF): qemu-img.o $(block-obj-y) $(crypto-obj-y) $(io-obj-y) $(qom-obj-y) $(COMMON_LDADDS)
qemu-nbd$(EXESUF): qemu-nbd.o $(block-obj-y) $(crypto-obj-y) $(io-obj-y) $(qom-obj-y) $(COMMON_LDADDS)
qemu-io$(EXESUF): qemu-io.o $(block-obj-y) $(crypto-obj-y) $(io-obj-y) $(qom-obj-y) $(COMMON_LDADDS)api

qemu-system-x86_64

因爲target比較多,編譯也費時。能夠指定便以特定的target:架構

./configure --target-list=x86_64-softmmu

qemu-system-x86_64的入口定義在vl.c的main中:async

main
    ->main_loop
        ->main_loop_wait
            ->os_host_main_loop_wait

1.代碼結構

QEMU的main函數定義在vl.c中,是執行程序的起點,主要功能是創建一個虛擬的硬件環境。函數

.
├── audio
├── backends
├── block
├── bsd-user
├── chardev
├── configure
├── contrib
├── crypto  加密解密算法等。
├── docs
├── dtc
├── fpu
├── fsdev
├── hw  全部硬件設備,包括總線、串口、網卡、鼠標等等。經過設備模塊串在一塊兒。
├── include
├── io
├── linux-headers
├── linux-user
├── Makefile
├── migration
├── nbd
├── net
├── pc-bios
├── pixman
├── po
├── qapi
├── qga
├── qobject
├── qom
├── README
├── replay
├── roms
├── scripts
├── stubs
├── target  不一樣架構的對應目錄,將客戶CPU架構的TBs轉化成TCG中間代碼,這個就是TCG前半部分。
├── tcg  這部分是使用TCG代碼生成主機的代碼,不一樣架構對應不一樣子目錄。整個生成主機代碼的過程即TCG後半部分。
├── tests
├── trace
├── trace-events
├── trace-events-all
├── ui
├── util
├── VERSION
├── vl.c  main函數,程序執行起點。最主要的模擬循環,虛擬機環境初始化和CPU的執行。
├── x86_64-softmmu  ./configure配置生成的目錄

oop

2.TCG

QEMU是一個模擬器,它可以動態模擬特定架構CPU指令,QEMU模擬的架構叫目標架構;運行QEMU的系統架構叫主機架構。

QEMU中有一個模塊叫微型代碼生成器,將目標代碼翻譯成主機代碼。

運行在虛擬CPU上的代碼叫作客戶機代碼,QEMU主要功能就是不斷提取客戶機代碼而且轉化成主機代碼。

整個翻譯分紅兩部分:將目標代碼(TB)轉化成TCG中間代碼,而後再將中間代碼轉化成主機代碼。

 

當新的代碼從TB(Translation Block)中生成之後,將會保存到一個cache中,由於不少相同的TB會被反覆的進行操做,因此這樣相似於內存的cache,可以提升使用效率。而cache的刷新使用LRU算法。

tb_gen_code
    ->gen_intermediate_code
    ->tcg_gen_code

由tb_gen_code調用,將客戶機代碼轉換成主機代碼。gen_intermediate_code以前是客戶及代碼,tcg_gen_code以後是主機代碼,二者之間是TCG中間代碼。

    

3.QEMU中的TCG代碼分析

x86_cpu_realizefn  架構相關初始化函數
    ->qemu_init_vcpu 
        ->qemu_tcg_init_vcpu
            ->qemu_tcg_cpu_thread_fn  vcpu線程函數
                ->tcg_cpu_exec
                    ->cpu_exec  這個函數是主要的執行循環,這裏第一次翻譯TB,而後不停的執行異常處理。
                        ->tb_find  首先在Hash表中查找,若是找不到則調用tb_gen_code建立一個TB。
                            ->tb_gen_code  分配一個新的TB。
                                ->gen_intermediate_code
                                ->tcg_gen_code  將TCG代碼轉換成主機代碼。
                        ->cpu_loop_exec_tb
                            ->cpu_tb_exec  執行TB主機代碼
                                ->tcg_qemu_tb_exec

KVM虛擬機代碼揭祕——中斷虛擬化

KVM中斷虛擬化主要依賴於VT-x技術,VT-x主要提供了兩種中斷事件機制,分別是中斷退出和中斷注入。

中斷退出:指虛擬機發生中斷時,主動式的客戶機發生VM-Exit,這樣可以在主機中實現對客戶機中斷的注入。

中斷注入:是指將中斷寫入VMCS對應的中斷信息位,來實現中斷的注入,當中斷完成後經過讀取中斷的返回信息來分析中斷是否正確。

中斷注入的標誌性函數kvm_set_irq,是中斷注入的最開始。

第一個參數s,傳遞設置IRQ須要的vmfd句柄,以及IRQ的ioctl類型。

第2、三參數,是IRQ中斷號,以及觸發類型。

int kvm_set_irq(KVMState *s, int irq, int level)
{
    struct kvm_irq_level event;
    int ret;

    assert(kvm_async_interrupts_enabled());

    event.level = level;
    event.irq = irq;
    ret = kvm_vm_ioctl(s, s->irq_set_ioctl, &event);  將irq_set_ioctl和具體IRQ信息寫入vmfd。
    if (ret < 0) {
        perror("kvm_set_irq");
        abort();
    }

    return (s->irq_set_ioctl == KVM_IRQ_LINE) ? 1 : event.status;
}

上面的ioctl對應內核中的kvm_vm_ioctl,內核首先case到KVM_IRQ_LINE。而後解析

static long kvm_vm_ioctl(struct file *filp,
               unsigned int ioctl, unsigned long arg)
{

#ifdef __KVM_HAVE_IRQ_LINE
    case KVM_IRQ_LINE_STATUS:
    case KVM_IRQ_LINE: {
        struct kvm_irq_level irq_event;

        r = -EFAULT;
        if (copy_from_user(&irq_event, argp, sizeof(irq_event)))
            goto out;

        r = kvm_vm_ioctl_irq_line(kvm, &irq_event,
                    ioctl == KVM_IRQ_LINE_STATUS);
        if (r)
            goto out;

        r = -EFAULT;
        if (ioctl == KVM_IRQ_LINE_STATUS) {
            if (copy_to_user(argp, &irq_event, sizeof(irq_event)))
                goto out;
        }

        r = 0;
        break;
    }
#endif


}

KVM中斷路由(何爲?)

kvm_arch_vm_ioctl
    ->KVM_CREATE_IRQCHIP(kvm_setup_default_irq_routing)
        ->kvm_set_irq_routing
            ->setup_routing_entry
                ->kvm_set_routing_entry
                    ->KVM_IRQCHIP_PIC_MASTER(kvm_set_pic_irq)
                    ->KVM_IRQCHIP_PIC_SLAVE(kvm_set_pic_irq)
                    ->KVM_IRQCHIP_IOAPIC(kvm_set_ioapic_irq)
                    ->KVM_IRQ_ROUTING_MSI(kvm_set_msi)
                    ->KVM_IRQ_ROUTING_HV_SINT(kvm_hv_set_sint)

從上能夠看出針對不一樣類型的ROUTING方式和IRQCHIP,跳轉到對應的中斷注入函數。IRQCHIP類型的中斷路由有PIC和IOAPIC;還有MSI和SINT類型。

PIC全稱 Programmable Interrupt Controller,一般是指Intel 8259A雙片級聯構成的最多支持15個interrupts的中斷控制系統

APIC全稱Advanced Programmable Interrupt Controller,APIC是爲了多核平臺而設計的。它由兩個部分組成IOAPIC和LAPIC,其中IOAPIC一般位於南橋中用於處理橋上的設備所產生的各類中斷,LAPIC則是每一個CPU都會有一個。IOAPIC經過APICBUS(如今都是經過FSB/QPI)將中斷信息分 派給每顆CPU的LAPIC,CPU上的LAPIC可以智能的決定是否接受系統總線上傳遞過來的中斷信息,並且它還能夠處理Local端中斷的 pending、nesting、masking,以及IOAPIC於Local CPU的交互處理。

 

設置好虛擬中斷控制器以後,在KVM_RUN退出之後,就開始遍歷虛擬中斷控制器,若是發現中斷,就將中斷寫入中斷信息位.

vcpu_run
    ->vcpu_enter_guest
        ->inject_pending_event

inject_pending_event在進入Guest以前被調用。

KVM虛擬機代碼揭祕——設備IO虛擬化

虛擬設備的IO地址註冊

KVM虛擬機設備模擬實在QEMU中實現的,而KVM實現的實質上只是IO的攔截。真正的虛擬設備IO地址註冊實在QEMU代碼裏面實現的。

QEMU中,初始化硬件設備的時候須要註冊IO空間,有兩種方法:

  1. PIO(Port IO) 端口IO
  2. MIO(Memory IO) 內存映射IO

PS:發覺這裏面介紹的代碼和最新的4.x已經很大差別,因此略過。

KVM虛擬機代碼揭祕——QEMU的PCI總線與設備(上)

QEMU的PCI總線

QEMU在初始化硬件的時候,最開始的函數就是pc_init1。在這個函數裏面會相繼的初始化CPU、中斷控制器、ISA總線,而後就要判斷是否須要支持PCI。若是支持則調用i440fx_init初始化PCI總線。

static void pc_init1(MachineState *machine,
                     const char *host_type, const char *pci_type)
{

if (pcmc->pci_enabled) {
    pci_bus = i440fx_init(host_type,
                          pci_type,
                          &i440fx_state, &piix3_devfn, &isa_bus, pcms->gsi,
                          system_memory, system_io, machine->ram_size,
                          pcms->below_4g_mem_size,
                          pcms->above_4g_mem_size,
                          pci_memory, ram_memory);
    pcms->bus = pci_bus;
} else {
    pci_bus = NULL;
    i440fx_state = NULL;
    isa_bus = isa_bus_new(NULL, get_system_memory(), system_io,
                          &error_abort);
    no_hpet = 1;
}

}

i440fx_init函數主要參數就是以前初始化好的ISA總線以及中斷控制器,返回值就是PCI總線,以後咱們就能夠將設備通通掛載在這個上面。

QEMU的PCI-PCI橋

在QEMU中,全部的設備包括總線,橋,通常設備都對應一個設備結構,經過register函數將全部的設備連接起來,就像Linux的模塊同樣,在QEMU啓動的時候會初始化全部的QEMU設備,而對於PCI設備來講,QEMU在初始化之後還會進行一次RESET,將全部的PCI bar上的地址清空,而後進行統一分配。

QEMU(x86)裏面的PCI的默認PCI設都是掛載主總線上的,貌似沒有看到PCI-PCI橋,而橋的做用通常也就是鏈接兩個總線,而後進行終端和IO的映射。

QEMU的PCI設備

通常的PCI設備其實和橋很像,甚至更簡單,關鍵區分橋和通常設備的地方就是class屬性和bar地址。

struct PCIDevice表示了PCI設備的信息。

pci_register_bat主要給bar分配IO地址。

void pci_register_bar(PCIDevice *pci_dev, int region_num,
                      uint8_t type, MemoryRegion *memory)
{
    PCIIORegion *r;
    uint32_t addr; /* offset in pci config space */
    uint64_t wmask;
    pcibus_t size = memory_region_size(memory);

    assert(region_num >= 0);
    assert(region_num < PCI_NUM_REGIONS);
    if (size & (size-1)) {
        fprintf(stderr, "ERROR: PCI region size must be pow2 "
                    "type=0x%x, size=0x%"FMT_PCIBUS"\n", type, size);
        exit(1);
    }

    r = &pci_dev->io_regions[region_num];
    r->addr = PCI_BAR_UNMAPPED;
    r->size = size;
    r->type = type;
    r->memory = memory;
    r->address_space = type & PCI_BASE_ADDRESS_SPACE_IO
                        ? pci_dev->bus->address_space_io
                        : pci_dev->bus->address_space_mem;

    wmask = ~(size - 1);
    if (region_num == PCI_ROM_SLOT) {
        /* ROM enable bit is writable */
        wmask |= PCI_ROM_ADDRESS_ENABLE;
    }

    addr = pci_bar(pci_dev, region_num);
    pci_set_long(pci_dev->config + addr, type);

    if (!(r->type & PCI_BASE_ADDRESS_SPACE_IO) &&
        r->type & PCI_BASE_ADDRESS_MEM_TYPE_64) {
        pci_set_quad(pci_dev->wmask + addr, wmask);
        pci_set_quad(pci_dev->cmask + addr, ~0ULL);
    } else {
        pci_set_long(pci_dev->wmask + addr, wmask & 0xffffffff);
        pci_set_long(pci_dev->cmask + addr, 0xffffffff);
    }
}

KVM虛擬機代碼揭祕——QEMU的PCI總線與設備(下)

代碼對不上,略過。

相關文章
相關標籤/搜索