虛擬機器進入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系統虛擬化:原理與實現》。