首頁>技術>

虛擬機器進入Guest模式後,並不會永遠處於Guest模式。從Host的角度來說,VM就是Host的一個程序,一個Host上的多個VM與Host共享系統的資源。因此,當訪問系統資源時,就需要退出到Host模式,由Host作為統一的管理者代為完成資源訪問。

比如當虛擬機器進行I/O訪問時,首先需要陷入Host,VMM中的虛擬磁碟收到I/O請求後,如果虛擬機器磁碟映象儲存在本地檔案,那麼就代為讀寫本地檔案,如果是儲存在遠端叢集,那麼就透過網路傳送到遠端儲存叢集。再比如訪問裝置I/O記憶體對映的地址空間,當訪問這些地址時,將觸發頁面異常,但是這些地址對應的不是記憶體,而是模擬裝置的I/O空間,因此需要KVM介入,呼叫相應的模擬裝置處理I/O。通常虛擬機器並不會呈現Host的CPU資訊,而是呈現一個指定的CPU型號,在這種情況下,顯然cpuid指令也不能在Guest模式執行,需要KVM介入對cpuid指令進行模擬。

當然,除了Guest主動觸發的陷入,還有一些陷入是被動觸發的,比如外部時鐘中斷、外設的中斷等。對於外部中斷,一般都不是來自Guest的訴求,而只是需要Guest將CPU資源讓給Host。

01 訪問外設

前文中提到,虛擬化的3個條件之一是資源控制,即由VMM控制和協調宿主機資源給各個虛擬機器,而不能由虛擬機器控制宿主機的資源。以虛擬機器的不同處理器之間傳送核間中斷為例,核間中斷是由一個CPU透過其對應的LAPIC傳送中斷訊號到目標CPU對應的LAPIC,如果不加限制地任由Guest 訪問CPU的物理LAPIC晶片,那麼這個中斷訊號就可能被髮送到其他物理CPU了。而對於虛擬化而言,不同的CPU只是不同的執行緒,核間中斷本質上是在同一個程序的不同執行緒之間傳送中斷訊號。當Guest的一個CPU(執行緒)傳送核間中斷時,應該陷入VMM中,由虛擬的LAPIC找到目標CPU(執行緒),向目標CPU(執行緒)注入中斷。

1. MMIO

MMIO是PCI規範的一部分,I/O裝置被對映到記憶體地址空間而不是I/O空間。從處理器的角度來看,I/O對映到記憶體地址空間後,訪問外設與訪問記憶體一樣,簡化了程式設計。以MMIO方式訪問外設時不使用專用的訪問外設的指令(out、outs、in、ins),是一種隱式的I/O訪問,但是因為這些對映的地址空間是留給外設的,因此CPU將產生頁面異常,從而觸發虛擬機器退出,陷入VMM中。以LAPIC為例,其使用一個4KB大小的裝置記憶體儲存各個暫存器的值,核心將這個4KB大小的頁面對映到地址空間中:

linux-1.3.31/arch/i386/kernel/smp.cvoid smp_boot_cpus(void){    …    apic_reg = vremap(0xFEE00000,4096);    …}linux-1.3.31/include/asm-i386/i82489.h#define     APIC_ICR    0x300linux-1.3.31/include/asm-i386/smp.hextern __inline void apic_write(unsigned long reg, unsigned long v){    *((unsigned long *)(apic_reg+reg))=v;}

程式碼中地址0xFEE00000是32位x86架構為LAPIC的4KB的裝置記憶體分配的匯流排地址,對映到地址空間中的邏輯地址為apic_reg。LAPIC各個暫存器都儲存在這個4KB裝置記憶體中,各個暫存器可以使用相對於4KB記憶體的偏移定址。比如,icr暫存器的低32位的偏移為0x300,因此icr暫存器的邏輯地址為apic_reg + 0x300,此時訪問icr暫存器就像訪問普通記憶體一樣了,寫icr暫存器的程式碼如下所示:

linux-1.3.31/arch/i386/kernel/smp.cvoid smp_boot_cpus(void){    …            apic_write(APIC_ICR, cfg);   /* Kick the second  */    …}

當Guest執行這條指令時,由於這是為LAPIC保留的地址空間,因此將觸發Guest發生頁面異常,進入KVM模組:

commit 97222cc8316328965851ed28d23f6b64b4c912d2KVM: Emulate local APIC in kernellinux.git/drivers/kvm/vmx.cstatic int handle_exception(struct kvm_vcpu *vcpu, …){    …    if (is_page_fault(intr_info)) {        …        r = kvm_mmu_page_fault(vcpu, cr2, error_code);        …        if (!r) {            …            return 1;        }        er = emulate_instruction(vcpu, kvm_run, cr2, error_code);        …    }    …}

顯然對於這種頁面異常,缺頁異常處理函式是沒法處理的,因為這個地址範圍根本就不是留給記憶體的,所以,最後邏輯就到了函式emulate_instruction。後面我們會看到,為了提高效率和簡化實現,Intel VMX增加了一種原因為apic access的虛擬機器退出,我們會在“中斷虛擬化”一章中討論。可以毫不誇張地說,MMIO的模擬是KVM指令模擬中較為複雜的,程式碼非常晦澀難懂。要理解MMIO的模擬,需要對x86指令有所瞭解。我們首先來看一下x86指令的格式,如圖1所示。

圖1 x86指令格式

首先是指令字首(instruction prefixes),典型的比如lock字首,其對應常用的原子操作。當指令前面添加了lock字首,後面的操作將鎖記憶體匯流排,排他地進行該次記憶體讀寫,高效能程式設計領域經常使用原子操作。此外,還有常用於mov系列指令之前的rep字首等。

每一個指令都包含操作碼(opcode),opcode就是這個指令的索引,佔用1~3位元組。opcode是指令編碼中最重要的部分,所有的指令都必須有opcode,而其他的5個域都是可選的。

與操作碼不同,運算元並不都是嵌在指令中的。操作碼指定了暫存器以及嵌入在指令中的立即數,至於是在哪個暫存器、在記憶體的哪個位置、使用哪個暫存器索引記憶體位置,則由ModR/M和SIB透過編碼查表的方式確定。

displacement表示偏移,immediate表示立即數。

我們以下面的程式碼片段為例,看一下編譯器將MMIO訪問翻譯的彙編指令:

// test.cchar *icr_reg;void write() {    *((unsigned long *)icr_reg) = 123;

我們將上述程式碼片段編譯為彙編指令:

gcc -S test.c

核心彙編指令如下:

// test.s    movq    icr_reg(%rip), %rax    movq    $123, (%rax)

可見,這段MMIO訪問被編譯器翻譯為mov指令,源運算元是立即數,目的運算元icr_reg(%rip)相當於icr暫存器對映到記憶體地址空間中的記憶體地址。因為這個地址是一段特殊的地址,所以當Guest訪問這個地址,即上述第2行程式碼時,將產生頁面異常,觸發虛擬機器退出,進入KVM模組。

commit 97222cc8316328965851ed28d23f6b64b4c912d2KVM: Emulate local APIC in kernellinux.git/drivers/kvm/x86_emulate.c int x86_emulate_memop(struct x86_emulate_ctxt *ctxt, …) {     unsigned d;     u8 b, sib, twobyte = 0, rex_prefix = 0;     …     for (i = 0; i < 8; i++) {         switch (b = insn_fetch(u8, 1, _eip)) {     …     d = opcode_table[b];     …     if (d & ModRM) {         modrm = insn_fetch(u8, 1, _eip);         modrm_mod |= (modrm & 0xc0) >> 6;         …     }     …     switch (d & SrcMask) {     …     case SrcImm:         src.type = OP_IMM;         src.ptr = (unsigned long *)_eip;         src.bytes = (d & ByteOp) ? 1 : op_bytes;         …         switch (src.bytes) {         case 1:             src.val = insn_fetch(s8, 1, _eip);             break;         …     }     …     switch (d & DstMask) {     …     case DstMem:         dst.type = OP_MEM;         dst.ptr = (unsigned long *)cr2;         dst.bytes = (d & ByteOp) ? 1 : op_bytes;     …     }     …     switch (b) {     …     case 0x88 ... 0x8b: /* mov */     case 0xc6 ... 0xc7: /* mov (sole member of Grp11) */         dst.val = src.val;         break;     …     } writeback:     if (!no_wb) {         switch (dst.type) {         …         case OP_MEM:             …                 rc = ops->write_emulated((unsigned long)dst.ptr,                              &dst.val, dst.bytes,                              ctxt->vcpu);     …     ctxt->vcpu->rip = _eip;     … }

函式x86_emulate_memop首先解析程式碼的字首,即程式碼第6~8行。在處理完指令字首後,變數b透過函式insn_fetch讀入的是操作碼(opcode),然後需要根據操作碼判斷指令運算元的定址方式,該方式記錄在一個數組opcode_table中,以操作碼為索引就可以讀出定址方式,見第9行程式碼。如果使用了ModR/M和SIB定址運算元,則解碼ModR/M和SIB部分見第11~15行程式碼。

第17~29行程式碼解析源運算元,對於以MMIO方式寫APIC的暫存器來說,源運算元是立即數,所以進入第19行程式碼所在的分支。因為立即數直接嵌在指令編碼裡,所以根據立即數佔據的位元組數,呼叫insn_fetch從指令編碼中讀取立即數,見第25~27行程式碼。為了減少程式碼的篇幅,這裡只列出了立即數為1位元組的情況。

第31~38行程式碼解析目的運算元,對於以MMIO方式寫APIC的暫存器來說,其目的運算元是記憶體,所以進入第33行程式碼所在的分支。本質上,這條指令是因為向目的運算元指定的地址寫入時引發頁面異常,而引起異常的地址記錄在cr2暫存器中,所以目的運算元的地址就是cr2暫存器中的地址,見第35行程式碼。

確定好了源運算元和目的運算元後,接下來就要模擬操作碼所對應的操作了,即第40~47行程式碼。對於以MMIO方式寫APIC的暫存器來說,其操作是mov,所以進入第42、43行程式碼所在分支。這裡模擬了mov指令的邏輯,將源運算元的值寫入目的運算元指定的地址,見第44行程式碼。

指令模擬完成後,需要更新指令指標,跳過已經模擬完的指令,否則會形成死迴圈,見第60行程式碼。

對於一個裝置而言,僅僅簡單地把源運算元賦值給目的運算元指向的地址還不夠,因為寫暫存器的操作可能會伴隨一些副作用,需要裝置做些額外的操作。比如,對於APIC而言,寫icr暫存器可能需要LAPIC向另外一個處理器發出IPI中斷,因此還需要呼叫裝置的相應處理函式,這就是第56~58行程式碼的目的,函式指標write_emulated指向的函式為emulator_write_emulated:

commit c5ec153402b6d276fe20029da1059ba42a4b55e5KVM: enable in-kernel APIC INIT/SIPI handlinglinux.git/drivers/kvm/kvm_main.cint emulator_write_emulated(unsigned long addr, const void *val,…){    …    return emulator_write_emulated_onepage(addr, val, …);}static int emulator_write_emulated_onepage(unsigned long addr,…){    …    mmio_dev = vcpu_find_mmio_dev(vcpu, gpa);    if (mmio_dev) {        kvm_iodevice_write(mmio_dev, gpa, bytes, val);        return X86EMUL_CONTINUE;    }    …}

函式emulator_write_emulated_onepage根據目的運算元的地址找到MMIO裝置,然後kvm_iodevice_write呼叫具體MMIO裝置的處理函式。對於LAPIC模擬裝置,這個函式是apic_mmio_write。如果Guest核心寫的是icr暫存器,可以清楚地看到伴隨著這個“寫icr暫存器”的動作,LAPIC還有另一個副作用,即向其他CPU傳送IPI:

commit c5ec153402b6d276fe20029da1059ba42a4b55e5KVM: enable in-kernel APIC INIT/SIPI handlinglinux.git/drivers/kvm/lapic.cstatic void apic_mmio_write(struct kvm_io_device *this, …){    …    case APIC_ICR:        …        apic_send_ipi(apic);    …}

鑑於LAPIC的暫存器的訪問非常頻繁,所以Intel從硬體層面做了很多支援,比如為訪問LAPIC的暫存器增加了專門退出的原因,這樣就不必首先進入缺頁異常函式來嘗試處理,當缺頁異常函式無法處理後再進入指令模擬函式,而是直接進入LAPIC的處理函式:

commit f78e0e2ee498e8f847500b565792c7d7634dcf54KVM: VMX: Enable memory mapped TPR shadow (FlexPriority)linux.git/drivers/kvm/vmx.cstatic int (*kvm_vmx_exit_handlers[])(…) = {    …    [EXIT_REASON_APIC_ACCESS]             = handle_apic_access,};static int handle_apic_access(struct kvm_vcpu *vcpu, …){    …    er = emulate_instruction(vcpu, kvm_run, 0, 0, 0);    …}

2. PIO

PIO使用專用的I/O指令(out、outs、in、ins)訪問外設,當Guest透過這些專門的I/O

指令訪問外設時,處於Guest模式的CPU將主動發生陷入,進入VMM。Intel PIO指令支援兩種模式,一種是普通的I/O,另一種是string I/O。普通的I/O指令一次傳遞1個值,對應於x86架構的指令out、in?;string I/O指令一次傳遞多個值,對應於x86架構的指令outs、ins。因此,對於普通的I/O,只需要記錄下val,而對於string I/O,則需要記錄下I/O值所在的地址。

我們以向塊裝置寫資料為例,對於普通的I/O,其使用的是out指令,格式如表1所示。

表1 out指令格式

我們可以看到,無論哪種格式,out指令的源運算元都是暫存器al、ax、eax系列。因此,當陷入KVM模組時,KVM模組可以從Guest的rax暫存器的值中取出Guest準備寫給外設的值,KVM將這個值儲存到結構體kvm_run中。對於string型別的I/O,需要記錄的是資料所在的記憶體地址,這個地址在陷入KVM前,CPU會將其記錄在VMCS的欄位GUEST_LINEAR_ADDRESS中,KVM將這個值從VMCS中讀出來,儲存到結構體kvm_run中:

commit 6aa8b732ca01c3d7a54e93f4d701b8aabbe60fb7[PATCH] kvm: userspace interfacelinux.git/drivers/kvm/vmx.cstatic int handle_io(struct kvm_vcpu *vcpu, …){    …    if (kvm_run->io.string) {    …        kvm_run->io.address = vmcs_readl(GUEST_LINEAR_ADDRESS);    } else        kvm_run->io.value = vcpu->regs[VCPU_REGS_RAX]; /* rax */    return 0;}

然後,程式的執行流程流轉到I/O模擬裝置,模擬裝置將從結構體kvm_run中取出I/O相關的值,儲存到本地檔案映象或透過網路發給儲存叢集。I/O模擬的更多細節我們將在“裝置虛擬化”一章討論。

02 特殊指令

有一些指令從機制上可以直接在Guest模式下本地執行,但是其在虛擬化上下文的語義與非虛擬化下完全不同。比如cpuid指令,在虛擬化上下文執行這條指令時,其本質上並不是獲取物理CPU的特性,而是獲取VCPU的特性;再比如hlt指令,在虛擬化上下文執行這條指令時,其本質上並不是停止物理CPU的執行,而是停止VCPU的執行。所以,這種指令需要陷入KVM進行模擬,而不能在Guest模式下本地執行。在這一節,我們以這兩個指令為例,討論這兩個指令的模擬。

1. cpuid指令模擬

cpuid指令會返回CPU的特性資訊,如果直接在Guest模式下執行,獲取的將是宿主機物理CPU的各種特性,但是實際上,透過一個執行緒模擬的CPU的特性與物理CPU可能會有很大差別。比如,因為KVM在指令、裝置層面透過軟體方式進行了模擬,所以這個模擬的CPU可能要比物理CPU支援更多的特性。再比如,對於虛擬機器而言,其可能在不同宿主機、不同叢集之間遷移,因此也需要從虛擬化層面給出一個一致的CPU特性,所以cpuid指令需要陷入VMM特殊處理。

Intel手冊中對cpuid指令的描述如表2所示。

表2 cpuid指令

cpuid指令使用eax暫存器作為輸入引數,有些情況也需要使用ecx暫存器作為輸入引數。比如,當eax為0時,在執行完cpuid指令後,eax中包含的是支援最大的功能(function)號,ebx、ecx、edx中是CPU製造商的ID;當eax值為2時,執行cpuid指令後,將在暫存器eax、ebx、ecx、edx中返回包括TLB、Cache、Prefetch的資訊;再比如,當eax值為7,ecx值為0時,將在暫存器eax、ebx、ecx、edx中返回處理器擴充套件特性。

起初,KVM的使用者空間透過cpuid指令獲取Host的CPU特徵,加上使用者空間的配置,定義好VCPU支援的CPU特性,傳遞給KVM核心模組。KVM模組在核心中定義了接收來自使用者空間定義的CPU特性的結構體:

commit 06465c5a3aa9948a7b00af49cd22ed8f235cdb0fKVM: Handle cpuid in the kernel instead of punting to userspacelinux.git/include/linux/kvm.hstruct kvm_cpuid_entry {    __u32 function;    __u32 eax;    __u32 ebx;    __u32 ecx;    __u32 edx;    __u32 padding;};

使用者空間按照如下結構體kvm_cpuid的格式組織好CPU特性後,透過如下KVM模組提供的介面傳遞給KVM核心模組:

commit 06465c5a3aa9948a7b00af49cd22ed8f235cdb0fKVM: Handle cpuid in the kernel instead of punting to userspacelinux.git/include/linux/kvm.h/* for KVM_SET_CPUID */struct kvm_cpuid {    __u32 nent;    __u32 padding;    struct kvm_cpuid_entry entries[0];};linux.git/drivers/kvm/kvm_main.cstatic long kvm_vcpu_ioctl(struct file *filp,               unsigned int ioctl, unsigned long arg){    …    case KVM_SET_CPUID: {        struct kvm_cpuid __user *cpuid_arg = argp;        struct kvm_cpuid cpuid;        …        if (copy_from_user(&cpuid, cpuid_arg, sizeof cpuid))            goto out;        r = kvm_vcpu_ioctl_set_cpuid(vcpu, &cpuid, cpuid_arg->entries);    …}static int kvm_vcpu_ioctl_set_cpuid(struct kvm_vcpu *vcpu,                    struct kvm_cpuid *cpuid,                    struct kvm_cpuid_entry __user *entries){    …    if (copy_from_user(&vcpu->cpuid_entries, entries,               cpuid->nent * sizeof(struct kvm_cpuid_entry)))    …}

KVM核心模組將使用者空間組織的結構體kvm_cpuid複製到核心的結構體kvm_cpuid_entry 例項中。首次讀取時並不確定entry的數量,所以第1次讀取結構體kvm_cpuid,其中的欄位nent包含了entry的數量,類似讀訊息頭。獲取了entry的數量後,再讀結構體中包含的entry。所以從使用者空間到核心空間的複製執行了兩次。

事實上,除了硬體支援的CPU特性外,KVM核心模組還提供了一些軟體方式模擬的特性,所以使用者空間僅從硬體CPU讀取特性是不夠的。為此,KVM後來實現了2.0版本的cpuid指令的模擬,即cpuid2,在這個版本中,KVM核心模組為使用者空間提供了介面,使用者空間可以透過這個介面獲取KVM可以支援的CPU特性,其中包括硬體CPU本身支援的特性,也包括KVM核心模組透過軟體方式模擬的特性,使用者空間基於這個資訊構造VCPU的特徵。具體內容我們就不展開介紹了。

在Guest執行cpuid指令發生VM exit時,KVM會根據eax中的功能號以及ecx中的子功能號,從kvm_cpuid_entry例項中索引到相應的entry,使用entry中的eax、ebx、ecx、edx覆蓋結構體vcpu中的陣列regs中相應的欄位。當再次切入Guest時,KVM會將它們載入到物理CPU的通用暫存器,這樣在進入Guest後,Guest就可以從這幾個暫存器讀取CPU相關資訊和特性。相關程式碼如下:

commit 06465c5a3aa9948a7b00af49cd22ed8f235cdb0fKVM: Handle cpuid in the kernel instead of punting to userspacevoid kvm_emulate_cpuid(struct kvm_vcpu *vcpu){    int i;    u32 function;    struct kvm_cpuid_entry *e, *best;    …    function = vcpu->regs[VCPU_REGS_RAX];    …    for (i = 0; i < vcpu->cpuid_nent; ++i) {        e = &vcpu->cpuid_entries[i];        if (e->function == function) {            best = e;            break;        }        …    }    if (best) {        vcpu->regs[VCPU_REGS_RAX] = best->eax;        vcpu->regs[VCPU_REGS_RBX] = best->ebx;        vcpu->regs[VCPU_REGS_RCX] = best->ecx;        vcpu->regs[VCPU_REGS_RDX] = best->edx;    }    …    kvm_arch_ops->skip_emulated_instruction(vcpu);}

最後,我們以一段使用者空間處理cpuid的過程為例結束本節。假設我們虛擬機器所在的叢集由小部分支援AVX2的和大部分不支援AVX2的機器混合組成,為了可以在不同型別的Host之間遷移虛擬機器,我們計劃CPU的特徵不支援AVX2指令。我們首先從KVM核心模組獲取其可以支援的CPU特徵,然後清除AVX2指令的支援,程式碼大致如下:

struct kvm_cpuid2 *kvm_cpuid;kvm_cpuid = (struct kvm_cpuid2 *)malloc(sizeof(*kvm_cpuid) +      CPUID_ENTRIES * sizeof(*kvm_cpuid->entries));kvm_cpuid->nent = CPUID_ENTRIES;ioctl(vcpu_fd, KVM_GET_SUPPORTED_CPUID, kvm_cpuid);for (i = 0; i < kvm_cpuid->nent; i++) {  struct kvm_cpuid_entry2 *entry = &kvm_cpuid->entries[i];  if (entry->function == 7) {    /* Clear AVX2 */    entry->ebx &= ~(1 << 6);    break;  };}ioctl(vcpu_fd, KVM_SET_CPUID2, kvm_cpuid);

2. hlt指令模擬

當處理器執行hlt指令後,將處於停機狀態(Halt)。對於開啟了超執行緒的處理器,hlt指令是停止的邏輯核。之後如果收到NMI、SMI中斷,或者reset訊號等,則恢復執行。但是,對於虛擬機器而言,如果任憑Guest的某個核本地執行hlt,將導致物理CPU停止執行,然而我們需要停止的只是Host中用於模擬CPU的執行緒。因此,Guest執行hlt指令時需要陷入KVM中,由KVM掛起VCPU對應的執行緒,而不是停止物理CPU:

commit b6958ce44a11a9e9425d2b67a653b1ca2a27796fKVM: Emulate hlt in the kernellinux.git/drivers/kvm/vmx.cstatic int handle_halt(struct kvm_vcpu *vcpu, …){    skip_emulated_instruction(vcpu);    return kvm_emulate_halt(vcpu);}linux.git/drivers/kvm/kvm_main.cint kvm_emulate_halt(struct kvm_vcpu *vcpu){    …        kvm_vcpu_kernel_halt(vcpu);    …}static void kvm_vcpu_kernel_halt(struct kvm_vcpu *vcpu){    …    while(!(irqchip_in_kernel(vcpu->kvm) &&          kvm_cpu_has_interrupt(vcpu))          && !vcpu->irq_summary          && !signal_pending(current)) {        set_current_state(TASK_INTERRUPTIBLE);        …        schedule();        …    }    …    set_current_state(TASK_RUNNING);}

VCPU對應的執行緒將自己設定為可被中斷的狀態(TASK_INTERRUPTIBLE),然後主動呼叫核心的排程函式schedule()將自己掛起,讓物理處理器執行其他就緒任務。當掛起的VCPU執行緒被其他任務喚醒後,將從schedule()後面的一條語句繼續執行。當準備進入下一次迴圈時,因為有中斷需要處理,則跳出迴圈,將自己設定為就緒狀態,接下來VCPU執行緒則再次進入Guest模式。

03 訪問具有副作用的暫存器

Guest在訪問CPU的很多暫存器時,除了讀寫暫存器的內容外,一些訪問會產生副作用。對於這些具有副作用的訪問,CPU也需要從Guest陷入VMM,由VMM進行模擬,也就是完成副作用。

典型的比如前面提到的核間中斷,對於LAPIC而言,寫中斷控制暫存器可能需要LAPIC向另外一個處理器傳送核間中斷,傳送核間中斷就是寫中斷控制暫存器這個操作的副作用。因此,當Guest訪問LAPIC的中斷控制暫存器時,CPU需要陷入KVM中,由KVM呼叫虛擬LAPIC晶片提供的函式向目標CPU傳送核間中斷。

再比如地址翻譯,每當Guest內切換程序,Guest的核心將設定cr3暫存器指向即將執行的程序的頁表。而當使用影子頁表機制完成虛擬機器地址(GVA)到宿主機物理地址(HPA)的對映時,我們期望物理CPU的cr3暫存器指向KVM為Guest中即將投入執行的程序準備的影子頁表,因此當Guest切換程序時,CPU需要從Guest陷入KVM中,讓KVM將cr3暫存器設定為指向影子頁表。因此,當使用影子頁表機制時,KVM需要設定VMCS中的Processor-Based VM-Execution Controls的第15位CR3-load exiting,當設定了CR3-load exiting後,每當Guest訪問物理CPU的cr3暫存器時,都將觸發物理CPU陷入KVM,KVM呼叫函式handle_cr設定cr3暫存器指向影子頁表,如下程式碼所示。

commit 6aa8b732ca01c3d7a54e93f4d701b8aabbe60fb7[PATCH] kvm: userspace interfacelinux.git/drivers/kvm/vmx.cstatic int handle_cr(struct kvm_vcpu *vcpu, …){    u64 exit_qualification;    int cr;    int reg;    exit_qualification = vmcs_read64(EXIT_QUALIFICATION);    cr = exit_qualification & 15;    reg = (exit_qualification >> 8) & 15;    switch ((exit_qualification >> 4) & 3) {    case 0: /* mov to cr */        switch (cr) {        …        case 3:            vcpu_load_rsp_rip(vcpu);            set_cr3(vcpu, vcpu->regs[reg]);            skip_emulated_instruction(vcpu);            return 1;    …}

關於作者:王柏生,資深技術專家,先後就職於中科院軟體所、紅旗Linux和百度,現任百度主任架構師。在作業系統、虛擬化技術、分散式系統、雲計算、自動駕駛等相關領域耕耘多年,有著豐富的實踐經驗。著有暢銷書《深度探索Linux作業系統》(2013年出版)。

謝廣軍,計算機專業博士,畢業於南開大學計算機系。資深技術專家,有多年的IT行業工作經驗。現擔任百度智慧雲副總經理,負責雲計算相關產品的研發。多年來一直從事作業系統、虛擬化技術、分散式系統、大資料、雲計算等相關領域的研發工作,實踐經驗豐富。

*本文經出版社授權釋出,更多關於虛擬化技術的內容推薦閱讀《深度探索Linux系統虛擬化:原理與實現》。

14
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 沒有周全的災備方案,怎能積極防範勒索者病毒