KVM 是 Linux 下基于Intel VT
或AMD-V
等 CPU 硬件虚拟化技术 实现的虚拟化解决方案,全称为 Kernel-based Virtual Machine。KVM 由处于内核态的 KVM 模块kvm.ko
和用户态的 QEMU 两部分构成。内核模块kvm.ko
实现了 CPU 和内存虚拟化 等决定关键性能和核心安全的功能,并向用户空间提供了使用这些功能的接口,QEMU 利用 KVM 模块提供的接口来实现设备模拟、I/O 虚拟化和网络虚拟化等。单个虚拟机是宿主机上的一个普通 QEMU 进程,虚拟机中的 CPU 核(vCPU)是 QEMU 的一个线程,VM 的物理地址空间是 QEMU 的虚拟地址空间。
技术背景
在 虚拟化概览 中介绍了 虚拟化准则,提出 VMM 应当满足的三个条件,并提出了两种类型的 VMM:
- Resource Control,控制程序必须能够管理所有的系统资源。
- Equivalence,在控制程序管理下运行的程序(包括操作系统),除时序和资源可用性之外的行为应该与没有控制程序时的完全一致,且预先编写的特权指令可以自由地执行。
- Efficiency,绝大多数的客户机指令应该由主机硬件直接执行而无需控制程序的参与。
QEMU-KVM
KVM 是必须依赖硬件虚拟化技术辅助(例如 Intel VT-x、AMD-V)的 Hypervisor:
- CPU:有 VMX root 和 non-root 模式的支持,其运行效率是比较高的
- 内存:有 Intel EPT/AMD NPT 的支持,内存虚拟化的效率也比较高
- I/O:KVM 客户机的 I/O 操作需要
VM-Exit
到用户态由 QEMU 进行模拟- 传统的方式是使用纯软件的方式来模拟 I/O 设备,效率并不高
- 现在应用最为广泛的 I/O 虚拟化是 virtio,可以参考 半虚拟化 I/O 框架 Virtio
作为一个Hypervisor
,KVM 本身只关注虚拟机调度和内存管理 这两个方面,I/O 外设 的任务就交给了 Linux 内核 和 QEMU。
KVM 将整个虚拟化应用抽象成三层:
- KVM 包含了一个内核加载模块
kvm.ko
,它只会负责提供vCPU
以及对虚拟内存进行管理和调度 /dev/kvm
是 KVM 内核模块提供给用户空间的一个接口,这个接口被qemu-kvm
调用,通过ioctl
系统调用就可以给用户提供一个工具,用以创建、删除、管理虚拟机- VM 运行期间,QEMU 会通过
kvm.ko
模块提供的系统调用进入内核,由 KVM 负责将虚拟机置于特殊模式运行 - QEMU-KVM 是 KVM 团队通过修改 QEMU 代码而得出的专门用来创建和管理虚拟机的管理工具,为的是 KVM 能更好的和内核打交道
qemu-kvm
就是通过open()
、close()
、ioctl()
等方法去打开、关闭和调用这个接口,从而实现与 KVM 的互动
1 | open("/dev/kvm") |
Guest 特点
Guest
作为一个普通进程运行于宿主机Guest
的 CPU(vCPU)作为进程的线程存在,并受到宿主机内核的调度Guest
继承了宿主机内核的一些属性,例如Huge Pages
Guest
的 磁盘 I/O 和 网络 I/O 会受到宿主机设置的影响Guest
通过宿主机上的虚拟网桥与外部相连
每一个虚拟机Guest
在Host
上都被模拟为一个 QEMU 进程,即emulation
进程。创建虚拟机后,使用virsh
命令即可查看:
1 | > virsh list --all |
可以看到,此虚拟机就是一个普通的 Linux 进程,有自己的PID
,并且有四个线程。线程数量不是固定的,但是至少会有三个(vCPU、I/O、Signal)。其中有两个是vCPU
线程,一个I/O
线程,还有一个Signal
信号处理线程。
1 | > pstree -p 20308 |
KVM 整体架构
CPU虚拟化
在 虚拟化技术概览 中介绍了 CPU 虚拟化的三种技术方案,这里总结如下:
全虚拟化 | 半虚拟化 | 硬件辅助虚拟化 | |
---|---|---|---|
实现 | 动态二进制翻译、优先级压缩 | HyperCall | VMX 模式切换 |
兼容性 | 不修改内核,兼容性好 | 需要定制GuestOS内核,不支持Windows,兼容性差 | 修改内核,兼容性好 |
性能 | 步骤繁琐,性能差 | 性能好 | 较好,接近半虚拟化 |
厂商 | VMware、QEMU | Xen | KVM、VMware ESXi、Xen 3.0 等 |
在 KVM 中,使用的是硬件辅助虚拟化技术。
创建 vCPU
前文有提及IOCTL命令控制虚拟机,这里我们以 kvm_vm_ioctl
作为入口函数分析,看虚拟机如何创建 vCPU。
1 | static long kvm_vm_ioctl(struct file *filp, |
通过接受ioctl的CPU创建指令,执行 kvm_vm_ioctl_create_vcpu
,紧接着,会先后执行 kvm_arch_vcpu_create和kvm_arch_vcpu_setup
1 | static int kvm_vm_ioctl_create_vcpu(struct kvm *kvm, u32 id) |
kvm_arch_vcpu_create
主要负责:
- 为结构体
kvm_vcpu
分配内存空间(这个数据结构表示一个VCPU描述符,所有对VCPU的操作都是对该数据结构相应字段的设置) - 对vmcs结构初始化(Virtual-Machine Control Structure, 该结构记录了cpu切换的上下文环境,帮助cpu在做VM-entry和VM-exit时保存信息)
- 其他寄存器初始化
1 | struct kvm_vcpu { |
kvm_arch_vcpu_setup
主要负责:
- 为vcpu对应的vmcs绑定当前cpu
- 初始化虚拟MMU部件
VM-entry
当接受ioctl入口函数收到的虚拟机运行的指令时,会通过 kvm_arch_vcpu_ioctl_run->__vcpu_run->vcpu_enter_guest的路径进入到vcpu_enter_guest函数。
在一系列操作后会执行kvm_x86_ops->run,这个函数在初始化时,映射成了另外一个函数vmx_vcpu_run,该函数做如下操作:
- 记录进入Guest时间
- 保存host寄存器信息
- 加载Guest寄存器信息
- 通过VMLAUNCH或VMRESUME进入Guest(汇编指令)
VM-exit
在vmx_vcpu_run的函数中会因为接受到中断、异常或主动调用VMCall导致退出,vmx_complete_interrupts函数即通过中断退出。vmx_complete_interrupts函数中会对中断类型做判断,如软中断、硬中断、软件异常、硬件异常等做相应处理。
内存虚拟化
为了实现内存虚拟化,让Guest
使用一个隔离的、从零开始且连续的内存空间,KVM 引入了一层新的地址空间,即客户机物理地址空间(Guest Physical Address,GPA)。这个地址空间并不是真正的物理地址空间,它只是Host
虚拟地址空间在Guest
地址空间的一个映射。对客户机来说,客户机物理地址空间都是从零开始的连续地址空间。但对宿主机来说,客户机的物理地址空间并不一定是连续的,客户机物理地址空间有可能映射在若干个不连续的宿主机地址区间,如下图所示:
为了将客户机物理地址转换成宿主机虚拟地址(Host Virtual Address,HVA),KVM 用一个kvm_memory_slot
数据结构来记录每一个地址区间的映射关系,此数据结构包含了对应此映射区间的起始客户机页帧号(Guest Frame Number,GFN)、映射的内存页数目以及起始宿主机虚拟地址,从而实现 GPA 到 HPA 的转换。
实现内存虚拟化,最主要的是实现客户机虚拟地址GVA
到宿主机物理地址HPA
之间的转换。如果通过之前提到的两步映射的方式,客户机的每次内存访问都需要 KVM 介入,并由软件进行多次地址转换,其效率是非常低的。
因此,为了提高GVA
到HPA
的转换效率,KVM 提供了两种实现方式来进行GVA
到HPA
之间的直接转换:
- 影子页表(Shadow Page Table)
- 基于硬件对虚拟化的支持:EPT
在 虚拟化技术概览 中可以看到对这两种技术的详细介绍。
EPT的入口处理函数是handle_ept_violation,该函数会依次调用kvm_mmu_page_fault->>vcpu->arch.mmu.page_fault(该函数实际是执行tdp_page_fault)。
tdp_page_fault函数主要做几件事情:
- 为核心结构体kvm_mmu_page等申请内存(kvm_mmu_page表示一个EPT中的页表页)
- 如果没有kvm_memory_slot映射pfn,则申请空闲kvm_memory_slot(kvm_memory_slot,客户机可根据页表实现GVA到GPA转换,再通过kvm_memory_slot实现GPA转换成HVA,可根据宿主机的页表实现GPA转HPA,进而完成GVA到HPA转换)
- 做EPT缺页处理,建立gfn和pfn映射关系(gfn (Guest Frame Number)起始客户机页帧号,pfn(Page Frame Number)物理页页帧号)
__direct_map 主要负责检查EPT表完整性,如果有页表项为初始化,则新建页表页并链接到页表项中。
EPT 页表页结构如下:
- EPT的页表结构分为四层(PML4(level-4)、PDPT(level-3)、PD(level-2)、PT(level))
- EPT Pointer (EPTP)指向PML4的首地址
- gpa到hpa:gpa通过四级页表的寻址,得到相应的pfn,然后加上gpa最后12位的offset,得到hpa
- 物理页(physical page)是真正存放数据的页,页表页(MMU page)是存放EPT页表的页
- 每个页表页(MMU page)对应一个数据结构kvm_mmu_page
I/O虚拟化
I/O 虚拟化现在主流三种方案如下,在 虚拟化技术概览 中可以看到对这两种技术的详细介绍。
- 全虚拟化:完全由QEMU软件模拟
- 半虚拟化:借助virtio实现
- PCI pass-through:直接通过PCI直连设备
Guest
虽然作为一个进程存在,但其内核的所有驱动都依然存在。只是硬件设备由 QEMU 模拟。Guest
的所有硬件操作都会由 QEMU 来接管,QEMU 负责与真实的宿主机硬件打交道。
Libvirt
在虚拟化中,libvirt充当着重要的角色,它提供了一个统一、稳定的Hypervisor应用框架,提供了对虚拟化各种对象(Hypervisor、主机、存储池、卷等)的管理,以及对多种不同的 Hypervisor 的支持,包括 Xen 驱动、QEMU 驱动、VMware 驱动等。∂
它的三大组成部分是:
- API(应用程序接口)使得其他人可以开发出基于 libvirt 的虚拟机管理工具,例如
virt-manager
- libvirtd(守护进程)守护进程,接收并处理 API 请求
- virsh(CLI,命令行工具,通常在linux直接敲virsh命令即可方便的操作虚拟机)
如下图所示,libvirt 通过libvird实现核心交互,它既支持本地操作,又支持远程操作(跨虚拟机的操作),它负责与QEMU通信,然后由 QEMU 驱动 KVM,从而实现libvirt 对于虚拟化层的调用。
网络设备相关