三种虚拟化方式
1.
对于虚拟机内核来讲,只要将标志位设为虚拟机状态,我们就可以直接在 CPU 上执行大部分的指令,不需要虚拟化软件在中间转述,除非遇到特别敏感的指令,才需要将标志位设为物理机内核态运行,这样大大提高了效率。
所以,安装虚拟机的时候,我们务必要将物理 CPU 的这个标志位打开。想知道是否打开,对于 Intel,你可以查看 grep “vmx” /proc/cpuinfo;对于 AMD,你可以查看 grep “svm” /proc/cpuinfo
这叫作硬件辅助虚拟化(Hardware-Assisted Virtualization)。
2.
另外就是访问网络或者硬盘的时候,为了取得更高的性能,也需要让虚拟机内核加载特殊的驱动,也是让虚拟机内核从代码层面就重新定位自己的身份,不能像访问物理机一样访问网络或者硬盘,而是用一种特殊的方式。
我知道我不是物理机内核,我知道我是虚拟机,我没那么高的权限,我很可能和很多虚拟机共享物理资源,所以我要学会排队,我写硬盘其实写的是一个物理机上的文件,那我的写文件的缓存方式是不是可以变一下。我发送网络包,根本就不是发给真正的网络设备,而是给虚拟的设备,我可不可以直接在内存里面拷贝给它,等等等等。
一旦我知道我不是物理机内核,痛定思痛,只好重新认识自己,反而能找出很多方式来优化我的资源访问。
这叫作半虚拟化(Paravirtualization)。
3.
完全虚拟化
我们先来看第一种方式,完全虚拟化(Full virtualization)。其实说白了,这是一种“骗人”的方式。虚拟化软件会模拟假
虚拟化的本质是用 qemu 的软件模拟硬件,但是模拟方式比较慢,需要加速;
虚拟化主要模拟 CPU、内存、网络、存储,分别有不同的加速办法;
CPU 和内存主要使用硬件辅助虚拟化进行加速,需要配备特殊的硬件才能工作;
网络和存储主要使用特殊的半虚拟化驱动加速,需要加载特殊的驱动程序
虚拟化cpu
内存虚拟化
我们这里来总结一下,虚拟机的内存管理也是需要用户态的 qemu 和内核态的 KVM 共同完成。为了加速内存映射,需要借助硬件的 EPT 技术。
在用户态 qemu 中,有一个结构 AddressSpace address_space_memory 来表示虚拟机的系统内存,这个内存可能包含多个内存区域 struct MemoryRegion,组成树形结构,指向由 mmap 分配的虚拟内存。
在 AddressSpace 结构中,有一个 struct KVMMemoryListener,当有新的内存区域添加的时候,会被通知调用 kvm_region_add 来通知内核。
在用户态 qemu 中,对于虚拟机有一个结构 struct KVMState 表示这个虚拟机,这个结构会指向一个数组的 struct KVMSlot 表示这个虚拟机的多个内存条,KVMSlot 中有一个 void *ram 指针指向 mmap 分配的那块虚拟内存。
kvm_region_add 是通过 ioctl 来通知内核 KVM 的,会给内核 KVM 发送一个 KVM_SET_USER_MEMORY_REGION 消息,表示用户态 qemu 添加了一个内存区域,内核 KVM 也应该添加一个相应的内存区域。
和用户态 qemu 对应的内核 KVM,对于虚拟机有一个结构 struct kvm 表示这个虚拟机,这个结构会指向一个数组的 struct kvm_memory_slot 表示这个虚拟机的多个内存条,kvm_memory_slot 中有起始页号,页面数目,表示这个虚拟机的物理内存空间。
虚拟机的物理内存空间里面的页面当然不是一开始就映射到物理页面的,只有当虚拟机的内存被访问的时候,也即 mmap 分配的虚拟内存空间被访问的时候,先查看 EPT 页表,是否已经映射过,如果已经映射过,则经过四级页表映射,就能访问到物理页面。
如果没有映射过,则虚拟机会通过 VM-Exit 指令回到宿主机模式,通过 handle_ept_violation 补充页表映射。先是通过 handle_mm_fault 为虚拟机的物理内存空间分配真正的物理页面,然后通过 __direct_map 添加 EPT 页表映射
存储虚拟化
在虚拟机里面,应用层调用 write 系统调用写入文件。
write 系统调用进入虚拟机里面的内核,经过 VFS,通用块设备层,I/O 调度层,到达块设备驱动。
虚拟机里面的块设备驱动是 virtio_blk,它和通用的块设备驱动一样,有一个 request queue,另外有一个函数 make_request_fn 会被设置为 blk_mq_make_request,这个函数用于将请求放入队列。
虚拟机里面的块设备驱动是 virtio_blk 会注册一个中断处理函数 vp_interrupt。当 qemu 写入完成之后,它会通知虚拟机里面的块设备驱动。
blk_mq_make_request 最终调用 virtqueue_add,将请求添加到传输队列 virtqueue 中,然后调用 virtqueue_notify 通知 qemu。
在 qemu 中,本来虚拟机正处于 KVM_RUN 的状态,也即处于客户机状态。
qemu 收到通知后,通过 VM exit 指令退出客户机状态,进入宿主机状态,根据退出原因,得知有 I/O 需要处理。
qemu 调用 virtio_blk_handle_output,最终调用 virtio_blk_handle_vq。
virtio_blk_handle_vq 里面有一个循环,在循环中,virtio_blk_get_request 函数从传输队列中拿出请求,然后调用 virtio_blk_handle_request 处理请求。
virtio_blk_handle_request 会调用 blk_aio_pwritev,通过 BlockBackend 驱动写入 qcow2 文件。
写入完毕之后,virtio_blk_req_complete 会调用 virtio_notify 通知虚拟机里面的驱动。数据写入完成,刚才注册的中断处理函数 vp_interrupt 会收到这个通知。
网络虚拟化