ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
英文原文:https://lwn.net/Articles/658511/。本文在翻譯的基礎上加了一些自己的理解。
?
qemu、virtual box、vmware、xen都是虛擬機,一般用戶接觸到的virtual box和vmware比較多,都是用來ubuntu中跑windows,或者windows中跑ubuntu的。
qemu其實是鼎鼎大名的最基礎的開源模擬器,可以純軟件模擬x86、arm、mips,這一點完虐其它模擬器;也可以使用硬件加速,比如linux下kvm和windows以及mac下的haxm。這些硬件加速又是基于initel VT-x, intel VT-d,以及amd對應的技術,這些技術提供了vCPU,以及硬件的影子頁表(intel EPT),大大減輕了qemu軟件模擬的工作量。
virtual box,qemu-kvm都使用到了qemu,但是僅僅用到了它的設備模擬功能。qemu對于gpu的模擬比較渣,所以基于qemu的android emulator自己實現了opengles 的qemu pipe,使用host電腦上的opengl進行繪圖。
xen在云計算中用的比較多,在這里不做詳細介紹。其它模擬器基本都是運行在普通操作系統之上的一個進程,每一個核是其中的一個線程。
?
本文介紹kvm的使用,在intel平臺下ubuntu12.04中實現一個最簡單的模擬器,計算2+2的結果并通過io端口輸出。
?
內核中kvm api的介紹可以看:Documentation/virtual/kvm/api.txt,其它的一些文檔:Documentation/virtual/kvm/。完整的源碼:https://lwn.net/Articles/658512/。
?
使用kvm的真正的虛擬機,模擬了很多虛擬的設備和固件,還有復雜的初始化狀態(各個設備的初始化,CPU寄存器的初始化等),以及內存的初始化。本文所述的模擬器demo,將使用如下16bit的x86的代碼(為什么是16bit呢,因為x86一上電是實模式,工作于16bit;之后再切換到32bit的保護模式的):
?
mov $0x3f8, %dxadd %bl, %aladd $'0', %alout %al, (%dx)mov $'\n', %alout %al, (%dx)hlt這段代碼充當了guest os,基本上算是一個裸奔的系統了。它實現了2+2,然后再加上'0',把4轉為ascii的'4',并通過端口0x3f8輸出。然后再輸出了'\n',就關機了。
?
?
我們把這段代碼對應的二進制存到數組里面:
?
const uint8_t code[] = {0xba, 0xf8, 0x03, /* mov $0x3f8, %dx */0x00, 0xd8, /* add %bl, %al */0x04, '0', /* add $'0', %al */0xee, /* out %al, (%dx) */0xb0, '\n', /* mov $'\n', %al */0xee, /* out %al, (%dx) */0xf4, /* hlt */}; 怎么得到這些機器碼呢??
?
shuyin.wsy@10-101-175-19:~$ cat simple_os.asmmov $0x3f8, %dxadd %bl, %aladd $'0', %alout %al, (%dx)mov $'\n', %alout %al, (%dx)hlt shuyin.wsy@10-101-175-19:~$ as -o simple_os.o simple_os.asm shuyin.wsy@10-101-175-19:~$ objdump -d simple_os.osimple_os.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 <.text>:0: 66 ba f8 03 mov $0x3f8,%dx4: 00 d8 add %bl,%al6: 04 30 add $0x30,%al8: ee out %al,(%dx)9: b0 0a mov $0xa,%alb: ee out %al,(%dx)c: f4 hlt 可以在這個網頁上查看匯編指令,以及對應的機器碼:http://x86.renejeschke.de/注意開頭多了一個0x66,解釋如下:
?
http://wiki.osdev.org/X86-64_Instruction_Encoding里面的Prefix group 3
?
所以我們需要在simple_os.asm文件的開頭添加.code16,這樣的話就對了,但是objdump顯示的又不對了,需要這樣使用才行:
?
shuyin.wsy@10-101-175-19:~$ objdump -d -Mintel,i8086 simple_os.osimple_os.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 <.text>:0: ba f8 03 mov dx,0x3f83: 00 d8 add al,bl5: 04 30 add al,0x307: ee out dx,al8: b0 0a mov al,0xaa: ee out dx,alb: f4 hlt?
https://sourceware.org/binutils/docs/as/i386_002d16bit.html
http://stackoverflow.com/questions/1737095/how-do-i-disassemble-raw-x86-code
?
我們會把這段代碼,放到虛擬物理內存,也就是GPA(guest physical address)的第二個頁面中(to avoid conflicting with a non-existent real-mode interrupt descriptor table at address 0),防止和實模式的中斷向量表沖突。al和bl初始化為2,cs初始化為0,ip指向第二個頁面的起始位置0x1000。
除此之外,我們還有一個虛擬的串口設備,端口是0x3f8,8bit,用于輸出字符。
?
為了實現一個虛擬機,我們首先需要打開/dev/kvm:
?
kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC);在使用kvm之前,需要使用KVM_GET_API_VERSION ioctl()去檢查下kvm的版本是否正確,看看是否為api12,是才可以繼續運行:
?
?
ret = ioctl(kvm, KVM_GET_API_VERSION, NULL);if (ret == -1)err(1, "KVM_GET_API_VERSION");if (ret != 12)errx(1, "KVM_GET_API_VERSION %d, expected 12", ret);?
檢查完api版本后,可以使用KVM_CHECK_EXTENSION ioctl()去檢查其它extensions是否可用,比如KVM_SET_USER_MEMORY_REGION,用來檢查kvm是否支持硬件影子頁表(http://royluo.org/2016/03/13/kvm-mmu-virtualization/):
?
ret = ioctl(kvm, KVM_CHECK_EXTENSION, KVM_CAP_USER_MEMORY);if (ret == -1)err(1, "KVM_CHECK_EXTENSION");if (!ret)errx(1, "Required extension KVM_CAP_USER_MEM not available");?
?
然后再創建一個虛擬機vm,這個vm和內存,設備,所有的vCPU相關,在host系統中對應一個進程:?
vmfd = ioctl(kvm, KVM_CREATE_VM, (unsigned long)0);虛擬機需要一些虛擬物理內存,用來存放guest os。當guest os進行內存訪問時,如果缺頁,kvm會根據KVM_SET_USER_MEMORY_REGION的設置,去嘗試解決缺頁的問題,如果kvm無法解決,就會退出,退出原因是KVM_EXIT_MMIO,然后由qemu或者其它東西去進行設備的模擬(《android qemu-kvm內存管理和IO映射》)。
?
我們先在host中申請一頁內存,然后把guest os裸奔的代碼拷貝過去:
?
mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); memcpy(mem, code, sizeof(code));然后我們需要把host 虛擬空間的內存和guest os虛擬物理內存的映射關系使用KVM_SET_USER_MEMORY_REGION ioctl()告知kvm:
?
?
struct kvm_userspace_memory_region region = {.slot = 0,.guest_phys_addr = 0x1000,.memory_size = 0x1000,.userspace_addr = (uint64_t)mem,};ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, ®ion);這樣,當guest os訪問到虛擬物理內存的0x1000~0x2000之間的話,kvm會直接訪問到mem所對應的真實的物理內存。
?
?
現在,我們有了一個虛擬機vm,有了一些虛擬物理內存,內存里面有guest os的代碼,那么我們需要給虛擬機添加一個核(vCPU),對應一個線程。當然也可以多核(vCPUs,調用多次KVM_CREATE_VCPU):
?
vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0);每一個vCPU都和一個kvm_run結構體相關,kvm_run用于內核態和用戶態信息的同步,比如從用戶態的虛擬機中獲得內核態的kvm退出的原因,KVM_EXIT_MMIO, KVM_EXIT_IO之類的。先獲得kvm_run結構體的大小,然后分配內存并和vCPU進行綁定:
?
?
mmap_size = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, NULL); run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);vCPU中還有處理器寄存器的狀態,分為兩組,struct kvm_regs和struct kvm_sregs,我們需要設置其中的cs,al,bl,ip等寄存器:
?
?
ioctl(vcpufd, KVM_GET_SREGS, &sregs);sregs.cs.base = 0;sregs.cs.selector = 0;ioctl(vcpufd, KVM_SET_SREGS, &sregs);struct kvm_regs regs = {.rip = 0x1000,.rax = 2,.rbx = 2,.rflags = 0x2,};ioctl(vcpufd, KVM_SET_REGS, ®s);
?
?
好了,東西都準備好了,我們可以開始運行vCPU了:?
while (1) {ioctl(vcpufd, KVM_RUN, NULL);switch (run->exit_reason) {/* Handle exit */}}我們需要根據run->exit_reason來處理kvm的退出狀態,比如guest 關機:
?
?
case KVM_EXIT_HLT:puts("KVM_EXIT_HLT");return 0;初始化失敗:
?
?
case KVM_EXIT_FAIL_ENTRY:errx(1, "KVM_EXIT_FAIL_ENTRY: hardware_entry_failure_reason = 0x%llx",(unsigned long long)run->fail_entry.hardware_entry_failure_reason); case KVM_EXIT_INTERNAL_ERROR:errx(1, "KVM_EXIT_INTERNAL_ERROR: suberror = 0x%x",run->internal.suberror);?
?
以及需要進行設備的模擬器,在這里,只有一個端口為0x3f8的串口設備。模擬設備的效果就是把字符打印出來:?
case KVM_EXIT_IO:if (run->io.direction == KVM_EXIT_IO_OUT &&run->io.size == 1 &&run->io.port == 0x3f8 &&run->io.count == 1)putchar(*(((char *)run) + run->io.data_offset));elseerrx(1, "unhandled KVM_EXIT_IO");break;?
?
測試結果:
tree@tree-OptiPlex-7010:~/Desktop$ gcc -o kvmtest kvmtest.c tree@tree-OptiPlex-7010:~/Desktop$ ./kvmtest 4 KVM_EXIT_HLTqemu-kvm中,qemu的主要任務就是KVM_EXIT_IO, KVM_EXIT_MMIO之后的虛擬設備的模擬,以及KVM_RUN之前設置好相關的設備的東西并進行初始化。
?
總結
以上是生活随笔為你收集整理的ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Android开发坑系列】之事件
- 下一篇: 基本数据类型封装类