简单说一下自己对x86平台虚拟化的理解,intel有SDM手册,代码都是公开的,难度比较大,理解起来困难,网上有大量优秀博客讲解虚拟化,引用了大量手册和代码,还是很难看懂。个人觉得理解虚拟化不能一上来就看很详细的手册和代码,虚拟化有点绕,先闭上眼睛想想大的道理,掌握了大的道理,再看手册和代码加深理解,否则很容易迷失,对虚拟化的理解只流于表面。
X86体系结构和OS
要理解虚拟化一定要回顾x86体系结构和OS原理,其实这两门课上大学时就学过,当时理解的很肤浅,随着工作的时间越长,理解的越深刻。计算机运转起来飞快,控制的非常精细,表面上给人很多假象,功能越来越复杂,集成度越来越高,不需要掌握很一个细节,但要能简化出一个最简单模型,通过个模型去深入理解背后的原理,思考时要站在不同的角度看问题,CPU的角度,guest的角度,外设的角度,进程的角度,内核的角度,不同角度看问题能让软件逻辑走通。也要不断提出问题,然后试着查资料解答自己的问题,计算机已经复杂到让一个人已经不可能掌握了所有细节,但要形成自己的抽象,各方面逻辑能成环,能自圆其说,只知道一些概念最不可取。
CPU(一堆寄存器)/Cache/Timer
physical memory map, segment and page,MMU and TLB
pci and device,driver and interrupt
kernel space and user space,context and process scheduler
软件和硬件的边界?
对x86和OS回顾后,那么得问自己一个问题,让自己写一个hypervisor怎么实现?guest是不能动的,linux和windows已经编译好的,还是物理机上跑的那些OS,那么是不是得借鉴物理机,哪个厂商什么型号的主板?什么型号的CPU几个核多少G内存?先构造一套假想的硬件,芯片组是什么型号,连接关系什么样的,就想象出下面这张图,这样guest就可以自动识别出这套硬件。
有那些bus,那些device,之间连接关系怎么表示?device的参数(virtio-net queue数量)等怎么表示?bus和device的状态怎么表示?硬件是主板电路连接好的,qemu怎么统一处理?还能支持hotplug?答案就是qdev和QOM(qemu object model)。
静态的东西已经出来了,guest是要运行的,动态怎么办?
CPU分为ring 0和ring 3不同权限,一条条指令执行,有些指令要在ring 3执行,有些要在ring 0执行,指令中包含的地址是虚拟地址。由于guest是事先编译好的,原来在物理机上怎么运行,现在要在虚拟机中一模一样运行,里面该ring 0还是ring 0,该ring 3还是ring 3,该虚拟地址还是原来的虚拟地址,所以得给guest制造假象,但guest的运行最终要落实到真正的硬件CPU和内存上,guest运行于host ring 3,要落实到真正的硬件qemu得做很多工作,那么qemu干什么呢,简单想象guest ring 3指令只要把guest的虚拟地址替换成host的虚拟地址,ring 0指令由于权限太高,得通过host模拟得出个结果然后把这个结果给了guest,这就是软件翻译,太慢了,intel vt-x技术就登场了。
intel vt-x vmx硬件实现了哪些功能?
VMCS(virtual machine controle structure)
non-root(guest)模式和root(host)模式,增加一个ring给guest,让guest指令直接上物理CPU执行,guest指令直接上物理CPU,那么指令中的地址肯定得用MMU硬件单元直接翻译(shadow page和EPT),当然不是guest的任何一条指令都能顺顺当当执行的,尤其是guest操作它以为的真正硬件的指令,因为是假象,其实是不存在的,所以得拦截和模拟,得vmexit然后再vmentry。qemu是host上的进程,ioctl到kvm内核后会vmentry到guest模式,执行guest代码,特权指令trap住vmexit回host模式模拟执行,然后再vmentry到guest模式
CPU虚拟化
一个vcpu在qemu中是一个线程,qemu通过ioctl调用kvm创建vcpu的资源
host调度这些线程,vcpu线程vcpu_run进入kvm内核,然后load_vcpu进入guest模式执行guest的代码
vcpu进入guest模式时从VMCS加载寄存器等,退出guest模式时把寄存器等保存到VMCS上
vcpu_enter_guest->vmx_vcpu_run->__vmx_vcpu_run调用汇编进入guest模式
vmx_handle_exit根据exit_reason来有不同的处理,kvm搞不定就再退回到qemu
vcpu占用了pcpu,host上其它进程要用pcpu,怎么抢占?
vcpu之间cache怎么同步,尤其是一个vcpu在运行,另一个没有运行?
pcpu进入或者退出guest时cache和TLB要不要做处理?
内存虚拟化
x86上物理内存空间大概是这样的。
x86上物理地址空间是怎么组成的那么qemu就要拿自己的进程虚拟空间的内存拼凑出一个空间仍给guest,让guest当作自己的物理地址空间。
在qemu monitor上可以看
虚拟出一块内存给guest用,GVA(guest virtual address)->GPA(guest physical address)->HVA(host virtual address)->HPA(host physical address)
cpu_exec_init_all–>io_mem_init
-->memory_map_init
pc_init1->pc_memory_init ->memory_region_allocate_system_memory->allocate_system_memory_nonnuma->
memory_region_init_ram_shared_nomigrate->qemu_ram_alloc->qemu_ram_alloc_internal->ram_block_add->
phys_mem_alloc(qemu_anon_ram_alloc)->qemu_ram_mmap->mmap
kvm_init->kvm_memory_listener_register->memory_listener_register->listener_add_address_space->kvm_region_add->kvm_set_phys_mem
memory_region_transaction_begin
memory_region_transaction_commit
AddressSpaceDispatch从GPA转到HVA
guest从IO地址空间的一个port读一个数到内存
kvm_mmu_create创建guest MMU的页表,但内容为空
vcpu_enter_guest => kvm_mmu_reload加载guest的页表到VMCS结构的对应字段
EPT_VOLATION和EPT_MISCONFIG
外设和中断
host上driver和硬件外设有明确的接口,硬件外设是个黑盒子。guest中driver是没有变的,那qemu得保持这些接口不变,否则guest里的driver就跑不起来了,接口后面的功能和动作那qemu就随便实现了,最终的结果一样就行了。guest中driver读寄存器那qemu就给一个结果,guest发起DMA操作,qemu就从guest的物理内存搬东西到自己的虚拟空间。
PIC(8259 chip)/APIC(IOAPIC和LAPIC)
INTx/NMI/SMI/SMI-X
qemu和kvm分别模拟中断芯片,混合模拟(on, off, split)
个人认为中断模拟是最难的,host上中断处理流程已经够复杂的了。PIC给CPU发送一个电信号就给CPU一个中断,APIC直接给一个物理地址写,CPU的LAPIC就收中断了,物理机上中断是由硬件发起的,现在只能由hypervisor发起,硬件的中断路由得hypervisor模拟。中断来了OS是怎么取中断号的?怎么操作LAPIC的?中断处理结束了OS怎么操作中断芯片,有哪些指令,这些指令都得trap住然后模拟。
vcpu正在pcpu上运行,ovs把一个数据包放在virtio ring上,怎么打断vcpu运行?中断到底是怎么注入的?kvm运行在一个pcpu A上,vcpu在另一个pcpu B上,难道pcpu A给pcpu B发送一个IPI打断pcpu B?这个IPI和正常的两个pcpu之间的IPI有什么不同没?
vcpu没有运行时来的中断怎么保存的?(pending_event)处理不及时中断会不会丢失?
enter guest时guest是怎么知道有中断在pending?enter guest时不是延着上次执行的地方继续执行吗?为什么要执行中断服务例程呢?难道是硬件搞的?
中断来了要不要exit?怎么减少exit次数?硬件辅助(vAPIC还是APICv)?
总结
虚拟化水很深,大的方面理解了,再看代码深入理解,最后再解决实际碰到的问题,如windows虚拟机中DPC Latency太高怎么解决,嵌套虚拟化的问题。
抛砖引玉,很多问题是不知道答案的,如果你感兴趣,不防想一想找一下答案,找到了答案那么恭喜你,你对虚拟化的理解又上了一个台阶。