前言:本文主要概括了QEMU的代碼結構,特別從代碼翻譯的角度分析了QEMU是如何將客戶機代碼翻譯成TCG代碼和主機代碼並且最終執行的過程。並且在最后描述了QEMU和KVM之間聯系的紐帶。
申明:本文前面部分從qemu detailed study第七章翻譯而來。
1.代碼結構
如我們所知,QEMU是一個模擬器,它能夠動態模擬特定架構的CPU指令,如X86,PPC,ARM等等。QEMU模擬的架構叫目標架構,運行 QEMU的系統架構叫主機架構,QEMU中有一個模塊叫做微型代碼生成器(TCG),它用來將目標代碼翻譯成主機代碼。如下圖所示。
我們也可以將運行在虛擬cpu上的代碼叫做客戶機代碼,QEMU的主要功能就是不斷提取客戶機代碼並且轉化成主機指定架構的代碼。整個翻譯任務分為兩個部分:第一個部分是將做目標代碼(TB)轉化成TCG中間代碼,然后再將中間代碼轉化成主機代碼。
QEMU的代碼結構非常清晰但是內容非常復雜,這里先簡單分析一下總體的結構
1. 開始執行:
主要比較重要的c文件有:/vl.c,/cpus.c, /exec-all.c, /exec.c, /cpu-exec.c.
QEMU的main函數定義在/vl.c中,它也是執行的起點,這個函數的功能主要是建立一個虛擬的硬件環境。它通過參數的解析,將初始化內存,需要的模擬的設備初始化,CPU參數,初始化KVM等等。接着程序就跳轉到其他的執行分支文件如:/cpus.c, /exec-all.c, /exec.c, /cpu-exec.c.
2. 硬件模擬
所有的硬件設備都在/hw/ 目錄下面,所有的設備都有獨自的文件,包括總線,串口,網卡,鼠標等等。它們通過設備模塊串在一起,在vl.c中的machine _init中初始化。這里就不講每種設備是怎么實現的了。
3.目標機器
現在QEMU模擬的CPU架構有:Alpha, ARM, Cris, i386, M68K, PPC, Sparc, Mips, MicroBlaze, S390X and SH4.
我們在QEMU中使用./configure 可以配置運行的架構,這個腳本會自動讀取本機真實機器的CPU架構,並且編譯的時候就編譯對應架構的代碼。對於不同的QEMU做的事情都不同,所以不同架 構下的代碼在不同的目錄下面。/target-arch/目錄就對應了相應架構的代碼,如/target-i386/就對應了x86系列的代碼部分。雖然 不同架構做法不同,但是都是為了實現將對應客戶機CPU架構的TBs轉化成TCG的中間代碼。這個就是TCG的前半部分。
4.主機
這個部分就是使用TCG代碼生成主機的代碼,這部分代碼在/tcg/里面,在這個目錄里面也對應了不同的架構,分別在不同的子目錄里面,如i386就在/tcg/i386中。整個生成主機代碼的過程也可以教TCG的后半部分。
5.文件總結和補充:
/vl.c: 最主要的模擬循環,虛擬機機器環境初始化,和CPU的執行。
/target-arch/translate.c 將客戶機代碼轉化成不同架構的TCG操作碼。
/tcg/tcg.c 主要的TCG代碼。
/tcg/arch/tcg-target.c 將TCG代碼轉化生成主機代碼
/cpu-exec.c 其中的cpu-exec()函數主要尋找下一個TB(翻譯代碼塊),如果沒找到就請求得到下一個TB,並且操作生成的代碼塊。
2. TCG - 動態翻譯
QEMU在 0.9.1版本之前使用DynGen翻譯c代碼.當我們需要的時候TCG會動態的轉變代碼,這個想法的目的是用更多的時間去執行我們生成的代碼。當新的代 碼從TB中生成以后, 將會被保存到一個cache中,因為很多相同的TB會被反復的進行操作,所以這樣類似於內存的cache,能夠提高使用效率。而 cache的刷新使用LRU算法。
編譯器在執行器會從源代碼中產生目標代碼,像GCC這種編譯器,它為了產生像函數調用目標代碼會產生一些特殊的匯編目標代碼,他們能夠讓編譯器需要知道在調用函數。需要什么,以及函數調用以后需要返回什么,這些特殊的匯編代碼產生過程就叫做函數的Prologue和Epilogue,這里就叫前端和后段吧。我在其他文章中也分析過匯編調用函數的過程,至於匯編里面函數調用過程中寄存器是如何變化的,在本文中就不再描述了。
函數的后端會恢復前端的狀態,主要做下面2點:
1. 恢復堆棧的指針,包括棧頂和基地址。
2. 修改cs和ip,程序回到之前的前端記錄點。
TCG就如編譯器一樣可以產生目標代碼,代碼會保存在緩沖區中,當進入前端和后端的時候就會將TCG生成的緩沖代碼插入到目標代碼中。
接下來我們就來看下如何翻譯代碼的:
客戶機代碼
TCG中間代碼
![]()
主機代碼
3. TB鏈
在QEMU中,從代碼cache到靜態代碼再回到代碼cache,這個過程比較耗時,所以在QEMU中涉及了一個TB鏈將所有TB連在一起,可以讓一個TB執行完以后直接跳到下一個TB,而不用每次都返回到靜態代碼部分。具體過程如下圖:
4. QEMU的TCG代碼分析
接下來來看看QEMU代碼中中到底怎么來執行這個TCG的,看看它是如何生成主機代碼的。
main_loop(...){/vl.c} :
函數main_loop 初始化qemu_main_loop_start()然后進入無限循環cpu_exec_all() , 這個是QEMU的一個主要循環,在里面會不斷的判斷一些條件,如虛擬機的關機斷電之類的。
qemu_main_loop_start(...){/cpus.c} :
函數設置系統變量 qemu_system_ready = 1並且重啟所有的線程並且等待一個條件變量。
cpu_exec_all(...){/cpus.c} :
它是cpu循環,QEMU能夠啟動256個cpu核,但是這些核將會分時運行,然后執行qemu_cpu_exec() 。
struct CPUState{/target-xyz/cpu.h} :
它是CPU狀態結構體,關於cpu的各種狀態,不同架構下面還有不同。
cpu_exec(...){/cpu-exec.c}:
這個函數是主要的執行循環,這里第一次翻譯之前說道德TB,TB被初始化為(TranslationBlock *tb) ,然后不停的執行異常處理。其中嵌套了兩個無限循環 find tb_find_fast() 和tcg_qemu_tb_exec().
cantb_find_fast()為客戶機初始化查詢下一個TB,並且生成主機代碼。
tcg_qemu_tb_exec()執行生成的主機代碼
struct TranslationBlock {/exec-all.h}:
結構體TranslationBlock包含下面的成員:PC, CS_BASE, Flags (表明TB), tc_ptr (指向這個TB翻譯代碼的指針), tb_next_offset[2], tb_jmp_offset[2] (接下去的Tb), *jmp_next[2], *jmp_first (之前的TB).
tb_find_fast(...){/cpu-exec.c} :
函數通過調用獲得程序指針計數器,然后傳到一個哈希函數從 tb_jmp_cache[] (一個哈希表)得到TB的所以,所以使用tb_jmp_cache可以找到下一個TB。如果沒有找到下一個TB,則使用tb_find_slow。
tb_find_slow(...){/cpu-exec.c}:
這個是在快速查找失敗以后試圖去訪問物理內存,尋找TB。
tb_gen_code(...){/exec.c}:
開始分配一個新的TB,TB的PC是剛剛從CPUstate里面通過using get_page_addr_code()找到的
phys_pc = get_page_addr_code(env, pc);
tb = tb_alloc(pc);
ph當調用cpu_gen_code() 以后,接着會調用tb_link_page(),它將增加一個新的TB,並且指向它的物理頁表。
cpu_gen_code(...){translate-all.c}:
函數初始化真正的代碼生成,在這個函數里面有下面的函數調用:
gen_intermediate_code(){/target-arch/translate.c}->gen_intermediate_code_internal(){/target-arch/translate.c }->disas_insn(){/target-arch/translate.c}
disas_insn(){/target-arch/translate.c}
函數disas_insn() 真正的實現將客戶機代碼翻譯成TCG代碼,它通過一長串的switch case,將不同的指令做不同的翻譯,最后調用tcg_gen_code。
tcg_gen_code(...){/tcg/tcg.c}:
這個函數將TCG的代碼轉化成主機代碼,這個就不細細說明了,和前面類似。
#define tcg_qemu_tb_exec(...){/tcg/tcg.g}:
通過上面的步驟,當TB生成以后就通過這個函數進行執行.
next_tb = tcg_qemu_tb_exec(tc_ptr) :
extern uint8_t code_gen_prologue[];
#define tcg_qemu_tb_exec(tb_ptr) ((long REGPARM(*)(void *)) code_gen_prologue)(tb_ptr)
通過上面的步驟我們就解析了QEMU是如何將客戶機代碼翻譯成主機代碼的,了解了TCG的工作原理。接下來看看QEMU與KVM是怎么聯系的。
5. QEMU中的IOCTL
在QEMU-KVM中,用戶空間的QEMU是通過IOCTL與內核空間的KVM模塊進行通訊的。
1. 創建KVM
在/vl.c中通過kvm_init()將會創建各種KVM的結構體變量,並且通過IOCTL與已經初始化好的KVM模塊進行通訊,創建虛擬機。然后創建VCPU,等等。
2. KVM_RUN
這個IOCTL是使用最頻繁的,整個KVM運行就不停在執行這個IOCTL,當KVM需要QEMU處理一些指令和IO等等的時候就會退出通過這個IOCTL退回到QEMU進行處理,不然就會一直在KVM中執行。
它的初始化過程:
vl.c中調用machine->init初始化硬件設備接着調用pc_init_pci,然后再調用pc_init1。
接着通過下面的調用初始化KVM的主循環,以及CPU循環。在CPU循環的過程中不斷的執行KVM_RUN與KVM進行交互。
pc_init1->pc_cpus_init->pc_new_cpu->cpu_x86_init->qemu_init_vcpu->kvm_init_vcpu->ap_main_loop->kvm_main_loop_cpu->kvm_cpu_exec->kvm_run
3.KVM_IRQ_LINE
這個IOCTL和KVM_RUN是不同步的,它也是個頻率非常高的調用,它就是一般中斷設備的中斷注入入口。當設備有中斷就通過這個IOCTL最終 調用KVM里面的kvm_set_irq將中斷注入到虛擬的中斷控制器。在kvm中會進一步判斷屬於什么中斷類型,然后在合適的時機寫入vmcs。當然在 KVM_RUN中會不斷的同步虛擬中斷控制器,來獲取需要注入的中斷,這些中斷包括QEMU和KVM本身的,並在重新進入客戶機之前注入中斷。
總結: 通過這篇文章能夠大概的了解QEMU的代碼結構,其中主要包括TCG翻譯代碼的過程以及QEMU和KVM的交互過程。
http://blog.chinaunix.net/uid-26941022-id-3510672.html