《qemu kvm源码解析与应用》学习笔记
本文最后更新于:2023年9月21日 下午
David Wheeler有一句名言:“计算机科学中的任何问题 都可以通过增加一个中间层来解决。”
- 数字逻辑电路-》汇编-》C语言。从机器码、汇编语言到C语言,再 到高级语言,其本质就是一个不断虚拟的过程,将底层复杂的接口转变成了上层容易使用的接口。
- 硬盘由柱面、磁道、扇区构成。应用程序能够通过文件管理的接口方便地创建、读取、写入文件
- TCP/IP协议栈模型。经过网络层、传输层的抽象之后,应用程序不需要直接跟网络数据包的收发细节打交道
- 每一个进程都是对计算机的抽象,进程都认为自己独占整个计算机系统的资源,有着独立的CPU和内存
虚拟机(VirtualMachine,VM)
- 最简单的虚拟机是进程。进程可以看作是一组资源的集合,有自己独立的进程地址空间以及独立的CPU和寄存器。一个进程在执行指令、访问内存的时候并不会影响其他进程。
- 模拟器是另一种形式的虚拟机。它可以使为一种硬件指令集(Instruction Set Architecture,ISA)编译的程序运行在另一种硬件指令集上。模拟器可以通过解释来实现,即对程序的源ISA指令一条一条进行分析,然后执行相应的ISA指令上的操作。模拟器也可以通过二进制翻译实现,即首先将程序中所有的源ISA指令翻译成目标ISA上具有同样功能的指令,然后在目标ISA指令机器上执行。
管理全局物理资源的软 件叫作虚拟机监控器(Virtual Machine Monitor,VMM),VMM之于虚拟机 就如同操作系统之于进程,VMM利用时分复用或者空分复用的办法将硬件资 源在各个虚拟机之间进行分配
系统虚拟化模拟指的是QEMU能够模拟一个完整的系统虚拟机,该虚拟机有自己的虚拟 CPU、芯片组、虚拟内存以及各种虚拟外部设备,能够为虚拟机中运行的操作系统和应用软件呈现出与物理计算机完全一致的硬件视图。
早期的QEMU都是软件模拟的,很明显其在性能上是不能满足要求的。所以早期的云计算平台通常使用 Xen作为其底层虚拟化平台。前面提到过,Xen早期是在x86架构上直接完成 的虚拟化,这需要修改虚拟机内部的操作系统,也使得Xen的整个VMM非常复 杂,缺陷比较多。
4.1 CPU虚拟化介绍
4.1.1 CPU虚拟化简介
在物理机中,操作系统和应用程序都是直接运行在硬件上
- 操作系统的代码运行在ring0
- 应用程序的代码运行在ring3
- 应用程序需要执行一些敏感操作、访问一些系统资源时,需要执行特殊的指令陷入到操作系统内核,由内核进行一些安全检查,代替应用程序访问这些资源
指令
- 能够在ring3执行的指令叫作非特权指令
- 只能够在ring0执行的指令叫作特权指令
- 如果指令会影响到整个系统,叫作敏感指令,如果只影响自身所在的进程,就叫作非敏感指令
- 典型的敏感指令包括读写时钟、读写中断寄存器等
存在一些属于敏感指令但不是特权指令的指令,也就是说用户程序能够运行一些可以改变/获取全局资源的指令,这造成了x86虚拟化难的问题
一些虚拟化方式
- Bochs与QEMU(不含KVM)类的纯软件模拟严格来讲并不算是虚拟化软件,应该叫作模拟器,因为它们都是一条一条指令地解析,然后执行的
- VMWare早期的方案,虚拟化用户态的程序直接在CPU上执行,但是一些特权指令会通过动态的二进制翻译去执行
- Xen方案,该方案修改了虚拟机操作系统内核的代码,使虚拟机内核运行在ring1,并且对虚拟机中操作系统内核的敏感指令进行替换进而使其陷入到ring0的Xen内核
- 各个方案都有缺点,如纯软件模拟的性能非常差,Xen方案又只能支持有源码的操作系统,VMware的Workstation综合来看相对不错。
4.1.2 VMX架构简介
CPU虚拟化的VT-x技术增加VMX架构来实现CPU的硬件虚拟化
- 虚拟机监控器(VMM)以及虚拟机(VM)
- VMM对整个系统的CPU和硬件有完全的控制权,它抽象出虚拟的CPU给各个VM,并且能够将VM的CPU直接调度到物理CPU上运行。VMM需要对各个VM进行管理,包括创建、配置、删除VM实例、为其分配资源、确保各个VM之间的隔离与独立,还需要处理VM对资源的访问、确保公平,所有这些都需要VMM运行的权限高于VM
- VM本身不会意识到其处在虚拟化环境中。每一个VM都相互独立,有自己独立的CPU、内核、中断和设备等,这些资源都是VMM提供的
- VMM执行的模式叫作VMX root operation模式,VM执行的模式叫作VMX non-root operation模式,每种模式都有自己的ring0和ring3结构。这两种模式之间的转换叫作VMX转换。从VMX root转换到VMX non-root叫作VM Entry,而从VMX non-root转换到VMXroot则叫作VM Exit
- 在VMX non-root模式中,执行一些特殊的指令(如之前所说的影响系统全局的指令)或者发生一些特殊的事件都会导致VMExit,使VM退出到VMM
- VMX operation让虚拟机的操作系统不需要修改就能够运行
要让CPU进入VMX operation,首先需要执行一个VMXON的指令,因为VMM本身也需要记录一些数据,所以在执行VMXON之前,需要先分配一个VMXON的区域,并进行初始化,VMM可以通过执行VMXOFF指令退出VMX operation。
每一个VM都会有一个对应的虚拟机控制结构(Virtual Machine Control Structure,VMCS)区域与之对应,用来保存该VM的相关信息。在进行VMLAUNCH之前需要提前对VMCS进行分配并初始化
VMM可以通过VM Entry使一个VM进入到运行状态,首次进入VM是通过执行VMLAUNCH指令发起的
4.1.3 VMCS介绍
每个虚拟机的VCPU都有一个对应的VMCS,VMCS区域的大小为4KB,VMM通过它的64位地址来对该区域进行访问。VMCS之于VCPU的作用类似于进程描述符之于进程的作用。传统上操作系统的进程会共享物理CPU资源,操作系统负责在多个进程之间分配CPU,每个进程都有进程描述符来保存进程的信息,并且在进程切换时保存硬件上下文,使得进程能够在下次被调度的时候正常运行。同样,VCPU之间会共享物理CPU,VMM负责在多个VCPU之间分配物理CPU,每个VCPU都有自己的描述符,当VMM在切换VCPU运行时需要保存此刻的VCPU状态,从而在下次的VCPU调度中使得VCPU能够从被中断的那个点开始正常运行。
操作VMCS的指令包括VMCLEAR、VMPTRLD、VMREAD和VMWRITE
VMCS的格式
- 前8个字节是固定的,比如标识不同vmcs版本信息
- 其余为VMCS数据区。这个区域的格式是由实现决定的,VMM通过VMREAD和MWRITE指令在这里读写(书上有对应的信息区域图示)
- Guest-state区域,有各个寄存器的状态以及一些处理器的状态。进行VM Entry时,虚拟机处理器的状态信息从这个区域加载,进行VM Exit时,虚拟机的当前状态信息写入到这个区域。
- Host-state区域。当发生VM Exit的时候,需要切换到VMM的上下文运行,此时处理器的状态信息从这个区域加载
- VM Exit控制区域。这个区域用来指定虚拟机在发生VM Exit时的行为,如一些寄存器的保存。
- VM Exit信息区域。这个区域包含了最近产生的VM Exit信息,典型的信息包括退出的原因以及相应的数据,如指令执行的退出会记录指令的长度等。
- VM Entry控制区域。这个区域用来指定虚拟机在发生VM Entry时的行为,如一些寄存器的加载,还有一些虚拟机的事件注入
- VM-execution控制区域。这个区域用来控制处理器在进入VM Entry之后的处理器行为,这个区域很庞大,包含了多种控制,如哪些时间会引起VM Exit,一个异常位图指示哪些异常会发生VM Exit,APIC的虚拟化控制等
4.2 KVM模块初始化介绍
4.2.1 KVM源码组织
KVM
- 一个用于 Linux 内核的虚拟化解决方案,允许在同一硬件平台上运行多个虚拟机,充分地重用了内核的诸多功能
- 以色列初创公司Qumranet在CPU推出硬件虚拟化之后开发的一个基于内核的虚拟机监控器
代码结构
- kvm主体代码位于内核树virt/kvm目录下面,表示所有CPU架构的公共代码,这也是内核kvm.ko对应的源码
- x86的架构相关的代码在arch/x86/kvm下,同一个架构可能会有多种不同的实现,vmx.c(对应intel VM-X方案),svm.c(对应AMD-V方案),这也是intel-kvm.ko和amd-kvm.ko的来源
1 |
|
- arch: 包含特定硬件架构的代码,如x86、ARM、MIPS等。不同的硬件架构在这个目录下有各自的子目录,包含了与架构相关的代码和配置文件。
- block: 包含块设备层的代码,处理块设备(硬盘等)的I/O操作和管理。
- certs: 包含用于签名Linux内核代码的证书文件,用于确保代码的可信度和完整性。
- crypto: 包含加密相关的代码,用于提供Linux内核中的加密和密码学功能。
- Documentation: 包含Linux内核的文档,提供开发者和用户的详细说明、使用指南和文档。
- drivers: 包含各种设备驱动的代码,涵盖了多种硬件设备,如网络适配器、显卡、声卡等。可以发现驱动文件夹占了源码中绝大多数的空间
- firmware: 包含一些硬件设备的固件文件,这些文件在启动时或设备初始化时可能会用到。
- fs: 包含文件系统相关的代码,包括各种文件系统的实现,如EXT4、FAT、NTFS等。
- include: 包含用于编译Linux内核的头文件,这些头文件定义了内核中使用的结构体、宏等。
- init: 包含系统初始化和引导过程相关的代码,包括内核初始化和启动。
- ipc: 包含进程间通信(IPC)相关的代码,用于实现进程之间的通信和同步。
- kernel: 包含核心内核代码,包括调度器、中断处理、系统调用等。
- lib: 包含一些通用的库函数和工具函数,用于帮助内核开发。
- mm: 包含内存管理相关的代码,用于管理系统的物理内存和虚拟内存。
- net: 包含网络协议栈的代码,用于处理网络数据包的收发和处理。
- samples: 包含示例代码,用于演示某些功能的用法和实现。
- scripts: 包含一些用于构建和配置内核的脚本文件。
- security: 包含与系统安全性相关的代码,如访问控制、权限管理等。
- sound: 包含声音子系统的代码,用于处理声卡和音频相关的功能。
- tools: 包含一些辅助工具和调试工具,用于内核开发和调试。
- usr: 包含用户空间工具和应用程序的代码,这些工具可以在用户空间使用。
- virt: 包含虚拟化相关的代码,用于支持虚拟化技术。
.gitignore
、.mailmap
、COPYING
、CREDITS
、Kconfig
、MAINTAINERS
、Makefile
、README
等,这些文件包含了一些关于版权、许可证、贡献者、配置和文档等信息
比较重要的目录
目录 | 关注点和重要性 |
---|---|
arch | 硬件架构相关的代码,不同的硬件架构有各自的子目录。硬件架构和底层硬件的研究、实现和优化 |
kernel | 核心内核代码,调度器、中断处理、系统调用等。操作系统内核功能和设计的研究和实现 |
drivers | 各种设备驱动的代码,硬件设备的交互、优化和性能。设备驱动和硬件交互的研究和实现 |
mm | 内存管理相关的代码,物理内存、虚拟内存的管理和优化。内存管理、内存优化和分配策略的研究 |
net | 网络协议栈的代码,网络数据包的收发、协议实现和网络性能优化。网络协议、性能和优化的研究 |
security | 系统安全性相关的代码,访问控制、权限管理和安全策略。操作系统安全性和安全策略的研究和实现 |
Documentation | Linux内核的文档,开发者和用户指南,理解和使用内核的重要资源。文档对于内核的理解和使用非常重要。 |
KVM的不同虚拟化实现(Intel和AMD)都会向KVM模块注册一个kvm_x86_ops结构体。KVM中的一些函数可能首先会调用kvm_arch_xxx函数,xxx是架构类型,比如x86,则会调用kvm_x86_ops结构中的相关回调函数。kvm_x86_ops是一个结构体,包含了一组函数指针,用于处理在 x86 架构下的虚拟化操作。这些函数指针实际上是回调函数,它们实现了特定的虚拟化功能。
- 回调函数: 在
kvm_x86_ops
结构体中的每个函数指针都指向一个回调函数,这些函数是实际执行虚拟化操作的地方。 kvm_arch_xxx
函数: 这些是架构特定的函数,其名称中的xxx
表示具体的架构类型,比如x86
。这些函数在 KVM 模块的实现中可能首先被调用,然后再根据具体的架构类型调用相应的kvm_x86_ops
中的函数。这样的架构分离使得 KVM 能够支持多种不同的硬件架构。kvm_x86_ops
结构体: 这是一个包含一组函数指针的结构体,用于在 x86 架构下处理虚拟化操作。每个函数指针指向一个特定的函数,实现了特定的虚拟化功能。这些函数定义了 KVM 在 x86 架构下的行为和处理方式,可以根据需要进行扩展和定制。
kvm.ko和kvm-intel.ko是两个内核模块,它们协同工作以实现KVM虚拟化
- kvm.ko:这是KVM的通用代码生成的内核模块。它本身在初始化阶段并不执行实际虚拟化任务,只是将通用的KVM代码加载到内存中,为KVM的其他组件提供支持。它导出了KVM的接口,这些接口将会暴露给用户空间,供用户程序调用。
- kvm-intel.ko:这是针对Intel CPU架构的特定代码生成的内核模块。它负责实际的虚拟化功能,包括创建、管理和运行虚拟机。kvm-intel.ko会在KVM初始化完成后负责启动和关闭虚拟化,以及处理与Intel CPU架构相关的虚拟化任务。
- 当用户程序调用KVM接口来执行虚拟化任务时,kvm.ko中的通用代码将会调用kvm-intel.ko中的架构相关代码来处理实际的虚拟化操作。这包括在虚拟机中执行指令、管理虚拟设备等任务。
4.2.2 KVM模块初始化
KVM模块的初始化主要包括初始化CPU与架构无关的数据以及设置与架构相关的虚拟化支持
VMM只有在CPU处于保护模式并且开启分页时才能进入VMX模式
开启VMX模式需要做的事情
CPU支持检测: 通过CPUID指令的ECX寄存器的第5位来检测CPU是否支持VMX。
VMX能力检测: 通过读取相关的MSR寄存器,如IA32_VMX_BASIC、IA32_VMX_PINBASED_CTLS和IA32_VMX_PROCBASED_CTLS,来检测CPU所支持的VMX能力。
分配VMXON区域: 分配一段4KB对齐的内存作为VMXON区域,大小通过读取IA32_VMX_BASIC MSR寄存器确定。
VMXON区域初始化: 通过MSR寄存器初始化VMXON区域,包括设置版本标识等信息。
CR0和CR4寄存器设置: 确保当前CPU运行模式的CR0和CR4寄存器满足进入VMX的条件,需要满足的设置通过IA32_VMX_CR0_FIXED0和IA32_VMX_CR0_FIXED1、IA32_VMX_CR4_FIXED0和IA32_VMX_CR4_FIXED1寄存器报告。
开启VMX模式: 设置CR4寄存器中的CR4.VMXE位为1,开启VMX模式。
IA32_FEATURE_CONTROL寄存器设置: 确保IA32_FEATURE_CONTROL寄存器的锁定位(0位)为1,通常由BIOS进行设置。
执行VMXON指令: 使用VMXON区域的物理地址作为操作数,调用VMXON指令以开启VMX模式。
VMX模式的启动和关闭: 一旦进入VMX模式,可以在VMX root模式(CPL=0)下执行VMXOFF指令来关闭VMX模式。
在VMX(Virtual Machine Extensions)模式下,VMX root操作模式是指处理器运行在特权级别0(CPL = 0)下的状态,即内核态。VMXOFF指令用于关闭VMX模式,将处理器从VMX操作模式切换回到非虚拟化的操作模式。为了确认VMX模式是否已经成功关闭,可以通过检查RFLAGS寄存器的CF(Carry Flag)和ZF(Zero Flag)位的值。
具体来说,在执行VMXOFF指令后,如果RFLAGS寄存器的CF和ZF都为0,这表示VMX模式已经成功关闭。这种情况下的解释如下:
- CF = 0:表示VMXOFF指令成功执行,没有发生错误。
- ZF = 0:表示VMX模式已经成功退出,处理器不再处于虚拟化模式。
intel-kvm.ko的模块注册函数是vmx_init,其主要任务为调用架构无关函数kvm_init进行KVM模块的初始化
vmx_init调用了kvm_init
1 |
|
这段代码初始化KVM虚拟化,提供了VMX虚拟化实现的回调函数和相关参数,为后续的虚拟化操作做好了准备。在KVM初始化后,可以使用其他API来创建虚拟机、分配VCPU、设置寄存器等,以实现虚拟化功能
kvm_init
: 这是一个用于初始化KVM的函数。它会执行一系列操作来启动KVM虚拟化,并返回一个整数结果以指示是否成功初始化。&vmx_x86_ops
: 这个参数是一个指向包含Intel VT-x虚拟化实现回调函数的结构体的指针(是kvm_x86_ops类型)。这个结构体包含了一系列回调函数,用于处理不同的虚拟化操作,如硬件检测、虚拟机创建、寄存器设置等。这些函数是特定于硬件的,用于实现VMX(Virtual Machine Extensions)虚拟化技术的支持。sizeof(struct vcpu_vmx)
: 这是第二个参数,表示VMX实现的VCPU(Virtual CPU)结构体的大小。VCPU结构体包含了与虚拟CPU相关的信息和状态。__alignof__(struct vcpu_vmx)
: 这是对VCPU结构体的对齐要求。这个参数通常用于确保结构体按照正确的字节对齐方式分配内存,以优化访问和操作。THIS_MODULE
: 这是一个表示当前模块的标识符,用于标记KVM模块的所属。
1 |
|
函数调用分析
函数名 | 功能描述 |
---|---|
kvm_arch_init | 初始化架构相关的代码。 |
kvm_irqfd_init | 初始化irqfd相关的数据,主要是创建一个线程。 |
kvm_arch_hardware_setup | 创建启动KVM时所需的数据结构,初始化硬件特性。 |
kvm_arch_check_processor_compat | 检测所有CPU的特性是否一致。 |
注册通知对象 | - kvm_cpu_notifier:用于CPU的热插拔时通知。 - kvm_reboot_notifier:系统重启时通知。 |
缓存VCPU结构体 | 将创建VCPU所需的cache赋值给kvm_vcpu_cache,用于快速分配VCPU空间。 |
设置file_operations的owner | 将3个file_operations的owner设置为当前模块,分别对应不同的设备。 |
调用misc_register创建设备 | 创建名为kvm_dev的misc设备,将其file_operations设置为kvm_chardev_ops。 |
设置kvm_preempt_ops | 设置kvm_preempt_ops的sched_in和sched_out,用于虚拟机VCPU所在线程被抢占或调度时的处理。 |
kvm_arch_init作为通用代码一般都会调用kvm_arch_xxx这种与架构相关的代码,所以这个函数定义在arch/x86/kvm目录的x86.c中
1 |
|
- 首先,函数接受一个参数
opaque
,其中存储了KVM实现的结构体(例如,vmx_x86_ops
),这些结构体包含了实现的回调函数等信息。 - 代码开始执行检查,确保只有一个KVM实现能够加载到内核。如果
kvm_x86_ops
已经存在,表示已加载其他模块,则打印错误并返回。 - 接着,通过调用
ops->cpu_has_kvm_support()
函数检测CPU是否支持VMX模式(对应开启条件1)。如果不支持,则打印错误并返回。 - 然后,通过调用
ops->disabled_by_bios()
函数检测是否被BIOS关闭(对应开启条件7)。如果被关闭,则打印错误并返回。 - 如果通过上述检查,首先分配一个percpu变量
shared_msrs
,用于存储共享的MSR寄存器数据。 - 调用
kvm_mmu_module_init()
函数初始化内存虚拟化工作。 - 调用
kvm_set_mmio_spte_mask()
函数设置MMIO内存的标识符。 - 将传递进来的KVM实现的结构体赋值给全局变量
kvm_x86_ops
。 - 执行一些初始化操作,如设置页表项的掩码、初始化计时器等。
- 注册性能回调函数,以便在性能分析中获取客户机信息。
- 如果CPU支持XSAVE指令集,获取主机的XCR0值。
- 最后,执行LAPIC(Local Advanced Programmable Interrupt Controller)的初始化,注册一个pvclock_gtod_notifier通知。
kvm_arch_hardware_setup,同样,其主要调用了实现相关的vmx_x86_ops的hardware_setup成员
1 |
|
- 该函数首先调用了架构特定的
hardware_setup
函数,进行硬件初始化操作,以确保虚拟化的相关硬件支持得以正确配置。 - 如果支持 TSC 控制,代码会对 TSC 进行相关配置,计算并设置合适的 TSC 缩放比例,并将其存储在相关变量中。
- 最后,函数进行 MSR 列表的初始化,这些 MSR 用于处理特定的模型相关寄存器。
1 |
|
vmx_check_processor_compat
是一个初始化函数,用于检查当前CPU的虚拟化兼容性。*(int *)rtn = 0;
初始化返回值为0,表示兼容性检查通过。setup_vmcs_config(&vmcs_conf)
调用setup_vmcs_config
函数,获取当前CPU的 VMCS 配置信息,存储在vmcs_conf
结构中。- 如果获取配置失败,则设置返回值为
-EIO
,表示I/O错误。 memcmp(&vmcs_config, &vmcs_conf, sizeof(struct vmcs_config)) != 0
对比当前CPU的 VMCS 配置与预期的配置是否一致。- 如果配置不一致,输出错误信息,并设置返回值为
-EIO
,表示兼容性检查失败。 - 在hardware_setup函数中调用setup_vmcs_config这一步骤的目的是确保在不同的物理CPU上,使用相同的虚拟机配置(
vmcs_config
),以确保 VCPIU 在不同物理 CPU 上调度时的一致性和稳定性。因为不同的硬件特性可能会导致不同的虚拟化行为,而通过比较并保持一致的 VMCS 配置,可以尽量减少在不同硬件间出现问题的可能性。
1 |
|
kvm_init的最后一个重要工作是创建一个misc设备“/dev/kvm”
可以看到,该设备只支持ioctl系统调用,当然,open和close这些系统调用会被misc设备框架处理。kvm_dev_ioctl代码如下
1 |
|
kvm_dev_ioctl
函数是用于处理/dev/kvm
设备的 ioctl 请求的主要函数。- 根据不同的 ioctl 请求,函数执行相应的操作。
- 对于通用接口,比如
KVM_GET_API_VERSION
、KVM_CREATE_VM
和KVM_CHECK_EXTENSION
,执行相应的处理,并返回相应的结果。 - 对于架构相关的接口,调用
kvm_arch_dev_ioctl
函数进行处理,该函数会根据架构不同,进一步处理 ioctl 请求。 - 对于其他一些 ioctl 请求,如
KVM_GET_VCPU_MMAP_SIZE
,计算并返回与虚拟机运行有关的内存大小。 - 如果是不支持的操作,返回错误码 -EOPNOTSUPP。
- 处理了 KVM 设备的 ioctl 请求,根据不同的请求类型,执行相应的操作
总结一下kvm模块初始化到底做了什么:
- 硬件检查: 初始化过程首先会进行硬件检查,确保主机的硬件支持虚拟化扩展,如 Intel VT-x 或 AMD-V。这些硬件扩展允许虚拟机在更加隔离的环境中运行,提高性能和安全性。
- 分配结构缓存: KVM 初始化过程分配了一些常用的数据结构的缓存,这些结构将用于管理虚拟机和虚拟 CPU 的状态。
- 创建设备节点: 通过创建
/dev/kvm
设备节点,用户空间程序可以通过这个设备与 KVM 内核模块进行通信,发起虚拟化请求和操作。 - 获取 VMCS 配置: 在初始化过程中,KVM 模块会获取 VMCS(Virtual Machine Control Structure)的配置信息,这些信息用于初始化 VMCS 结构。VMCS 是一个关键的数据结构,用于控制虚拟机运行的各个方面。
- 设置全局变量: 根据主机 CPU 的特性和支持,KVM 模块会设置一些全局变量,以适应不同的硬件环境。例如,根据 CPU 是否支持 EPT(Extended Page Tables)等特性,设置相应的全局标志。
- 为每个物理 CPU 分配 VMCS: KVM 初始化过程会为每个物理 CPU 分配一个 VMCS 结构,并将这些结构保存在 percpu 变量中。这为虚拟机在不同的物理 CPU 上切换和调度提供了支持。
- 进入 VMX 模式: 在初始化过程中,并没有将 CPU 设置为 VMX 模式。VMX 模式是虚拟机扩展的一种硬件虚拟化模式,需要通过设置 CR4 寄存器的 VMXE 位并分配 VMXON 区域来开启。然而,在创建第一个虚拟机之前,这些步骤不会执行。这是一种惰性策略,只有在实际需要创建虚拟机时才会启用 VMX 模式,以避免不必要的开销。
综上所述,KVM 模块的初始化过程主要包括硬件检查、资源分配、设备节点创建、全局变量设置、VMCS 配置等步骤。该过程确保了在虚拟化环境下能够准备好必要的数据结构和配置,以便在创建和管理虚拟机时进行有效的虚拟化操作。真正的 VMX 模式开启是在创建第一个虚拟机时才会执行,以避免不必要的性能损耗。
4.3 虚拟机的创建
要创建一个KVM虚拟机,需要用户侧的QEMU发起请求,下面从QEMU和KVM两个方面来考察KVM虚拟机创建过程
4.3.1 QEMU侧虚拟机的创建
当在QEMU命令行加入–enable-kvm时,解析会进入下面的case分支,给machine optslist这个参数项加了一个accel=kvm参数
1 |
|
该case处理命令行选项QEMU_OPTION_enable_kvm
,这个选项是在命令行中加入了--enable-kvm
时触发的
在这个分支中,首先声明了一个olist
变量,它指向一个QemuOptsList
对象。这是一个用于存储QEMU选项的列表,这里的olist
用于查找名为"machine"
的选项。
qemu_find_opts
函数: 这个函数用于查找与给定选项名匹配的QemuOptsList
对象。在这里,它用于查找名为"machine"
的选项对应的QemuOptsList
对象。
qemu_opts_parse_noisily
函数目的是在给定的QemuOptsList
列表中创建一个新的QemuOpts
对象,并从提供的参数字符串中解析选项值
参数说明:
list
:指向一个QemuOptsList
对象,表示要在其中创建新的QemuOpts
对象。params
:一个包含参数字符串的C风格字符数组,其中包含要解析的选项及其值。permit_abbrev
:一个布尔值,如果为true
,则允许第一个key=value
在参数字符串中省略key=
,并且会被视为list->implied_opt_name
的键值对。
一个QEMU命令行实例:
1 |
|
- 使用了
--enable-kvm
参数来启用KVM虚拟化加速 - 使用了
-machine accel=kvm
选项来指定虚拟机的加速方式为KVM -m 2G
用于分配2GB内存给虚拟机-cpu host
用于使用宿主机的CPU特性-hda ubuntu.img
指定虚拟机的硬盘镜像
之后main函数会在4360行调用configure_accelerator(current_machine)
,该函数会从machine的参数列表中取出accel的值,找出所属的类型,然后调用accel_init_machine。
1 |
|
accel_init_machine这段代码实现了加速器的初始化过程,特别是针对 KVM 加速器,它会创建一个新的 KVMState
对象,将其赋值给虚拟机的 accelerator
成员,并调用加速器的初始化回调函数。如果初始化失败,则会清除相应的状态。这样,在虚拟机运行时,KVM 加速器将能够根据加速器状态进行加速操作
- 首先,
accel_init_machine
函数被调用,用于初始化特定的加速器(AccelClass)。 - 在此函数中,首先获取了加速器的类对象(
ObjectClass
)和类名(cname
)。 - 接下来,通过调用
object_new(cname)
创建了一个新的AccelState
对象,该对象是特定加速器的状态数据结构,对于 KVM 加速器来说,实际上是一个KVMState
对象。 - 然后,将新创建的
AccelState
(即KVMState
)对象赋值给虚拟机的accelerator
成员,即ms->accelerator = accel;
。 - 设置
acc->allowed
为true
,表示允许使用此加速器。 - 调用加速器的
init_machine
回调函数,对于 KVM 加速器来说,这是kvm_init
函数。- 如果
init_machine
函数返回值小于 0,表示初始化失败,于是清空虚拟机的accelerator
成员,并将acc->allowed
设置为false
,同时释放之前创建的AccelState
对象(object_unref(OBJECT(accel));
)。
- 如果
- 最后,返回
init_machine
的返回值。
1 |
|
在 QEMU 中,使用
KVMState
结构体来表示与 KVM 相关的数据结构
kvm_init函数通过 KVM 在硬件层面提供加速功能,以提高虚拟机的性能和效率。它会检查 KVM 是否支持所需的功能,然后创建虚拟机并进行一些初始化工作,确保虚拟机能够正确运行在 KVM 加速器上
- 打开 KVM 设备:首先,函数尝试打开 “/dev/kvm” 设备,通过系统调用
qemu_open
。如果打开失败,会输出错误信息并返回错误码。 - 检查 KVM 版本:使用
KVM_GET_API_VERSION
ioctl 查询 KVM API 版本,并与代码中定义的KVM_API_VERSION
进行比较,以确保 KVM 版本支持。如果版本检查失败,会输出相应的错误信息。 - 创建虚拟机(VM):通过
KVM_CREATE_VM
ioctl 在 KVM 层面创建一个虚拟机,并将文件描述符保存到KVMState
结构体的成员变量中。这个虚拟机将用于承载客户机的运行。 - 检查扩展支持:使用不同的
kvm_check_extension
函数检查 KVM 是否支持各种特性和扩展,如内存插槽数量、虚拟 CPU 事件、协同 MMIO、IRQ 路由等。如果不支持某些扩展,会输出相应的错误信息。 - 初始化架构相关内容:调用
kvm_arch_init
函数,完成一些与特定架构相关的初始化工作,如对于 x86 架构,需要设置支持 vm86 模式的一些 IOCTL。 - 创建 IRQCHIP:如果允许使用 IRQCHIP,调用
kvm_irqchip_create
函数创建 IRQ 控制器。 - 注册监听器:注册 MEMORY 和 IO 监听器,用于处理内存和 I/O 的事件,例如内存区域的读写。
- 设置中断处理函数:将中断处理函数指向
kvm_handle_interrupt
,该函数用于处理中断事件。 - 其他功能设置:设置一些其他的参数和功能,如多个 IOEVENTFD 支持等。
1 |
|
4.3.2 KVM侧虚拟机的创建
kvm_init最重要的作用是调用“/dev/kvm”设备的ioclt(KVM_CREATE_VM)接口在KVM模块中创建一台虚拟机。本质上一个QEMU进程就是一台虚拟机。KVM中用结构体kvm表示虚拟机
4.2节中提到,KVM在初始化的时候会注册“/dev/kvm”设备,该设备在内核对应的ioctl函数为kvm_dev_ioctl,该函数实现了所有KVM层面的ioctl,对于KVM_CREATE_VM,其处理函数是kvm_dev_ioctl_create_vm,代码如下
1 |
|
这段代码实现了在 KVM 模块中创建虚拟机的流程。它首先调用 kvm_create_vm
函数创建虚拟机实例,然后根据支持合并 MMIO 的情况进行初始化,最后通过 anon_inode_getfd
创建一个匿名文件描述符,将虚拟机实例与文件描述符关联,以便后续通过该文件描述符进行虚拟机操作
kvm_create_vm是创建虚拟机的核心函数,代码如下
1 |
|
这段代码主要完成了以下操作:
- 分配并初始化
kvm
结构体,表示一个虚拟机。 - 初始化虚拟机的各种锁、计数等成员。
- 调用架构相关的初始化函数,如
kvm_arch_init_vm
。 - 开启硬件虚拟化功能。
- 初始化内存槽等数据结构。
- 初始化 MMU 通知机制。
- 将虚拟机添加到全局虚拟机链表中。
- 返回创建的虚拟机实例。
尤其需要注意的是:
- kvm_create_vm接着会初始化KVM的相关成员,如这里的mmu_lock成员表示操作虚拟机MMU数据的锁,由于虚拟机的内存其实也就是QEMU进程的虚拟内存,所以这里需要引用到当前QEMU进程的mm_struct。对应代码
atomic_inc(¤t->mm->mm_count);
- KVM有个类型为kvm_arch的arch成员,用于存放与架构相关的数据,kvm_arch_init_vm用来初始化这些数据。对应
r = kvm_arch_init_vm(kvm, type);//初始化类型为kvm_arch的arch成员
- 接下来kvm_create_vm调用hardware_enable_all来最终开启VMX模式,hardware_enable_all会在创建第一个虚拟机的时候对每个CPU调用hardware_enable_nolock,后者会调用kvm_arch_hardware_enable函数。
- r = hardware_enable_all();
- on_each_cpu(hardware_enable_nolock, NULL, 1);
- hardware_enable_nolock调用kvm_arch_hardware_enable函数
- kvm_arch_hardware_enable主要调用Intel VMX实现的hardware_enable回调函数,该函数的主要作用就是设置CR4的VMXE位并且调用VMXON指令开启VMX
4.4 QEMU CPU的创建
4.4.1 CPU模型定义
QEMU(Quick Emulator)的CPU模型继承结构
- QEMU是一个能够模拟多种硬件架构的工具,它可以在宿主机上模拟运行不同架构的CPU
- 为了支持不同的CPU模型和架构,需要建立一种继承结构来表示这些CPU对象
- TYPE_X86_CPU,它表示x86架构下的通用CPU功能。在这个类型的基础上,根据具体的物理CPU或虚拟CPU模型,创建了不同的CPU类型,例如”pentium”、”Haswell”等。这些具体的CPU类型会在基本的TYPE_X86_CPU类型的基础上添加特定的功能和属性,以模拟各种不同的CPU行为
- 存在一个名为”qemu-x86_64-cpu”的虚拟CPU模型,它是默认情况下在x86_64架构下使用的CPU模型
QEMU支持的x86 CPU都定义在一个builtin_x86_defs数组中,该数组的类型为X86CPUDefinition,定义如下
1 |
|
const char *name;
:这是一个指向表示CPU名称的字符串的指针。它用于标识特定的CPU模型,例如 “pentium”、”Haswell” 等。uint32_t level;
:这个字段表示CPUID指令支持的最大功能号。CPUID是一个指令,用于查询处理器的特性和功能。uint32_t xlevel;
:这个字段表示CPUID扩展功能支持的最大功能号。xlevel对应于CPUID中的扩展功能。char vendor[CPUID_VENDOR_SZ + 1];
:这是一个包含CPU制造商信息的字符数组。它是一个12字节的ASCII字符串,以NULL结尾,用于标识CPU的制造商。int family;
:这个字段表示CPU家族(family)的标识。家族在x86体系结构中是一个较大的分类,用于标识CPU在某个系列中的归属。int model;
:这个字段表示CPU型号(model)。型号用于更细致地标识CPU在特定家族中的具体型号。int stepping;
:这个字段表示CPU的步进信息,用于进一步区分同一型号中的不同版本。FeatureWordArray features;
:这是一个记录CPU特性的数组。它可能是一个位数组,每个位对应一个特定的CPU特性,用于描述CPU的功能和支持的扩展。char model_id[48];
:这个字段包含了CPU的全名或模型标识符。它是一个字符串,用于提供更详细的CPU型号信息。
builtin_x86_defs是一个非常大的数组,每一项表示一种模拟的CPU模型。QEMU既模拟了一些实际的CPU,比如pentium和SandyBridge,也模拟了一些虚拟机的CPU类型,比如qemu64和kvm64等。
1 |
|
x86_cpu_register_types 在 QEMU 中注册 x86 CPU 类型和模型
1 |
|
x86_cpu_type_info
和host_x86_cpu_type_info
:这些变量是用于定义 x86 CPU 类型的 TypeInfo 结构。x86_cpu_type_info
是用于 QEMU 内部的虚拟 CPU 类型,而host_x86_cpu_type_info
是用于与宿主机上的 CPU 特性保持一致的宿主 CPU 类型。x86_register_cpudef_type
:这是一个函数,用于根据给定的X86CPUDefinition
结构构建一个TypeInfo
结构,并通过type_register
函数将这个类型注册到 QEMU 类型系统中。它为每个不同的 CPU 模型创建一个类型,并提供必要的信息,例如 CPU 名称、特性、家族、型号等。x86_cpu_register_types
:这个函数是用于注册 x86 CPU 类型和模型的主要函数。它首先注册了虚拟的 x86 CPU 类型x86_cpu_type_info
,然后通过遍历builtin_x86_defs
数组中的 CPU 模型定义,为每个模型调用x86_register_cpudef_type
函数,将其注册为 QEMU 类型。在这个过程中,也根据需要注册了宿主机的 CPU 类型host_x86_cpu_type_info
。
1 |
|
一个X86CPU表示一个x86虚拟CPU
- CPUState包含所有CPU类型都会有的数据,如CPU的核数、线程数以及对应的线程,由于历史原因,也会有诸如TCG和KVM等模拟器相关的数据,如kvm_fd、kvm_state、kvm_run等KVM相关的数据。CPUState中有一个env_ptr指针,指向的是其CPU架构的CPU状态信息。
- env_ptr: 这是 CPUState 中的一个指针,指向特定 CPU 架构的 CPU 状态信息。它的作用是为了方便地从通用的 CPUState 结构中获取特定架构的具体 CPU 数据,使得不同架构的 CPU 可以在同一个通用结构中共存。例如,对于 x86 架构的 CPU,env_ptr 指向的就是 CPUX86State 结构。
- X86CPUState(env): 这是 X86 架构特定的 CPU 数据结构。在 X86CPU 结构中,会包含一个指向 X86CPUState 的指针(env),这个指针指向了包含了重要的 x86 架构 CPU 数据的结构。CPUX86State 中包含了通用寄存器、EIP、EFLAGS、段寄存器等寄存器的值,还包含了 KVM 相关的异常、中断信息以及 CPUID 的信息。在模拟过程中,这些数据用于模拟 x86 架构 CPU 的状态和行为。
4.4.2 CPU对象的初始化
CPU类型的初始化基本都是设置一系列的回调函数,但类型TYPE_X86_CPU的初始化函数x86_cpu_common_class_init较为特殊
1 |
|
x86_cpu_common_class_init
函数:这是用于初始化 x86 CPU 类的共享属性和方法的函数。它通过参数oc
(ObjectClass 结构指针)和data
来进行初始化。在这个函数中,它首先获取了 x86 CPU 类和父类(CPU 类和设备类)的指针。X86_CPU_CLASS(oc)
:这是一个宏,用于将oc
转换为X86CPUClass
类型的指针,以便可以访问 x86 CPU 类的属性和方法。CPU_CLASS(oc)
:这是一个宏,用于将oc
转换为CPUClass
类型的指针,以便可以访问通用 CPU 类的属性和方法。DEVICE_CLASS(oc)
:这是一个宏,用于将oc
转换为DeviceClass
类型的指针,以便可以访问设备类的属性和方法。xcc->parent_realize = dc->realize;
和xcc->parent_unrealize = dc->unrealize;
:- 这两行代码将设备类的
realize
和unrealize
函数赋值给 x86 CPU 类中的parent_realize
和parent_unrealize
成员。 - 这是为了在 x86 CPU 类的具现化和销毁过程中,能够在执行特定的
x86_cpu_realizefn
和x86_cpu_unrealizefn
函数之前,执行通用的设备类的具现化和销毁函数。
- 这两行代码将设备类的
dc->realize = x86_cpu_realizefn;
和dc->unrealize = x86_cpu_unrealizefn;
:- 这两行代码将设备类的
realize
和unrealize
X86CPU
子对象的x86_cpu_realizefn
和x86_cpu_unrealizefn
函数。 - 这是为了确保在具现化和销毁过程中执行 x86 CPU 类的特定函数。
- 这两行代码将设备类的
这一块关于CPU类型在初始化时设置的具现函数,后面再仔细看看
接下来详细分析CPU对象的创建过程
按照继承关系,首先调用TYPE_CPU的对象初始化函数cpu_common_initfn。这个函数是在所有CPU类型(比如x86、ARM等)共享的TYPE_CPU的初始化函数。它会在每个CPU对象创建时被调用,无论具体是什么类型的CPU。这个函数通常设置一些通用的初始值,用于在CPU对象创建时进行初始化。由于不同CPU类型可能有不同的特性和需求,这个函数通常只涉及通用的初始设置。
x86_cpu_initfn函数: 这个函数是在具体的x86 CPU类型(TYPE_X86_CPU)的初始化函数。每次创建x86 CPU对象时都会调用这个函数,它的主要作用是为特定的x86架构CPU对象创建各种属性和数据。