相关参考
KVM概述
KVM特性:
- KVM基于Intel VT-x或者AMD-V等硬件技术的全虚拟化。
- KVM是基于Linux内核基础,加了虚拟化的部分管理的模块。
- KVM由KVM-kernel驱动部分+QEMU-KVM应用部分组成。
内核KVM源码目录及组成
先看下Intel X86的虚拟化部分Makefile:
arch/x86/kvm/Makefile
ccflags-y += -Iarch/x86/kvm
KVM := ../../../virt/kvm
kvm-y += $(KVM)/kvm_main.o $(KVM)/coalesced_mmio.o
kvm-y += x86.o emulate.o i8259.o irq.o lapic.o \
i8254.o ioapic.o irq_comm.o cpuid.o pmu.o mtrr.o \
hyperv.o debugfs.o mmu/mmu.o mmu/page_track.o
kvm-intel-y += vmx/vmx.o vmx/vmenter.o vmx/pmu_intel.o vmx/vmcs12.o vmx/evmcs.o vmx/nested.o
obj-$(CONFIG_KVM) += kvm.o
obj-$(CONFIG_KVM_INTEL) += kvm-intel.o
可以看出,Kvm主要由kvm和kvm-intel组成:
- kvm.o是kvm的核心模块
- IOMMU、中断控制、设备管理、kvm arch等部分代码
- kvm并没有完全实现一整个PC系统虚拟化,而仅仅将部分重要的CPU虚拟化、I/O虚拟化和内存虚拟化部分针对硬件辅助的能力进行有效地抽象和对接,其他一些模块需要借助于Qemu
- 也就是说,kvm基本只实现硬件辅助虚拟化相关部分,而不支持的用Qemu来模拟实现
- kvm-intel.o是intel平台架构虚拟化模块,平台相关
KVM初始化流程
使用环境:ubuntu 18.04 Desktop,参考 /boot/config-5.4.0-52-generic 配置模块初始化:
整理流程代码概述
// linux/x86/linux-5.7.14/arch/x86/kvm/vmx/vmx.c
static struct kvm_x86_init_ops vmx_init_ops __initdata = {
.cpu_has_kvm_support = cpu_has_kvm_support,
.disabled_by_bios = vmx_disabled_by_bios,
.check_processor_compatibility = vmx_check_processor_compat,
.hardware_setup = hardware_setup,
.runtime_ops = &vmx_x86_ops,
};
// linux/x86/linux-5.7.14/arch/x86/kvm/vmx/vmx.c
vmx_init() // 初始化入口
├─ kvm_init(KVM_GET_API_VERSION) // 初始化KVM框架
| ├─ kvm_arch_init() // 架构相关初始化
| | ├─ cpu_has_kvm_support() // CPU是否支持kvm
| | ├─ disabled_by_bios() // bios是否禁用vt
| | ├─ boot_cpu_has() // CPU是否支持一些特性
| | ├─ kmem_cache_create("x86_fpu") // x86_fpu kmem_cache
| | ├─ alloc_percpu() // kvm_shared_msrs
| | ├─ kvm_mmu_module_init() // mmu模块初始化
| | ├─ kvm_mmu_set_mask_ptes() // shadow pte mask设置
| | ├─ kvm_timer_init() // 时钟初始化
| | ├─ kvm_lapic_init() // lapic初始化
| ├─ kvm_irqfd_init() //
| ├─ kvm_arch_hardware_setup() //
| | ├─ kvm_x86_ops->hardware_setup() // CPU是否支持kvm
| | | ├─ rdmsrl_safe() // 读msr
| | | ├─ store_idt() // 保存idt
| | | ├─ setup_vmcs_config() // 建立vmcs_config和vmx_capability
| | | ├─ boot_cpu_has() // CPU特性支持
| | | ├─ cpu_has_vmx_vpid() // cpu是否支持vpid
| | | ├─ cpu_has_vmx_invvpid() // cpu是否支持invvpid
| | | ├─ cpu_has_vmx_ept() // cpu是否支持ept
| | | ├─ kvm_configure_mmu() // mmu相关硬件判断和全局变量
| | | ├─ cpu_has_vmx_XXX() // cpu是否有XXX
| | | ├─ vmx_enable_tdp() // ept支持时开启tdp
| | | ├─ kvm_disable_tdp() // 关闭tdp
| | | ├─ kvm_set_posted_intr_wakeup_handler() // posted intr wakeup handler
| | | └─ alloc_kvm_area() // 给每个cpu分配一个struct vmcs
| | └─ kvm_init_msr_list() // 将msr保存到全局变量msrs_to_save[]数组
| ├─ smp_call_function_single() // 对每个online cpu进行兼容性检查
| ├─ cpuhp_setup_state_nocalls() // 注册cpu状态变化的回调函数
| ├─ register_reboot_notifier() // 注册reboot时候的通知函数
| ├─ kvm_cache_create_usercopy() // 创建vcpu 的 kmem cache, 对象大小是sizeof(struct vcpu_vmx)
| ├─ kvm_async_pf_init() // 异步
| ├─ misc_register(&kvm_dev) // 注册字符设备文件/dev/kvm
| ├─ register_syscore_ops() // 注册系统核心函数, 这里是suspend和resume
| ├─ kvm_init_debug() // 初始化debugfs
| └─ kvm_vfio_ops_init() // vfio的操作初始化
├─ vmx_setup_l1d_flush() //
└─ vmx_check_vmcs12_offsets()
初始化流程-我们先大概浏览下框架**
vmx_init // vmx/vmx.c
// IS_ENABLED(CONFIG_HYPERV) 这里不考虑,HYPER-V是windows的虚拟化产品
kvm_init // virt/kvm_main.c
kvm_arch_init // kvm/x86/x86.c 硬件架构相关初始化,判断硬件支持能力
kvm_irqfd_init // 创建kvm-irqfd-cleanup 工作队列
zalloc_cpumask_var
kvm_arch_hardware_setup
for_each_online_cpu(cpu)
smp_call_function_single(cpu, check_processor_compat, &c, 1);
cpuhp_setup_state_nocalls
register_reboot_notifier
kvm_async_pf_init
misc_register(&kvm_dev); // qemu对用户层接口:/dev/kvm接口注册-使用misc混杂设备
register_syscore_ops(&kvm_syscore_ops);
kvm_init_debug(); // debugfs init创建
kvm_vfio_ops_init();
.....
初始化-kvm_arch_init
kvm_arch_init() // 这里操作接口传入 vmx_init_ops
/******* 第一部分:KVM的硬件环境检测 ********/
kvm_x86_ops.hardware_enable // 这里用来判断是否使能过-没使能,否则重复加载错误
// cpu_has_vmx 汇编读取CPU-ID,判断CPU硬件是否支持虚拟化
ops->cpu_has_kvm_support()
// vmx_disabled_by_bios 读取BIOS是否禁止了 vmx
ops->disabled_by_bios()
// kvm明确规定guest是需要FPU浮点支持和FXSAVE/FXRSTOR指令支持
!boot_cpu_has(X86_FEATURE_FPU) || !boot_cpu_has(X86_FEATURE_FXSR)
/******** 第二部分:KVM资源分配 **********/
// __alignof__(struct fpu) 判断结构体内部是多少字节对齐
x86_fpu_cache = kmem_cache_create // 创建SLAB高速缓存 struct fpu
x86_emulator_cache = kvm_alloc_emulator_cache // emulator-仿真,看名称是创建仿真高速缓存
shared_msrs = alloc_percpu(struct kvm_shared_msrs); // 给每个CPU都有一个msrs变量
/******** 第三部分:MMU相关 **********/
kvm_mmu_module_init
__set_nx_huge_pages(get_nx_auto_mode()); // 不研究
kvm_mmu_reset_all_pte_masks(); //
kvm_set_mmio_spte_mask();
kvm_mmu_set_mask_ptes
/******** 第四部分:混杂项 **********/
kvm_timer_init
初始化-kvm_arch_hardware_setup
kvm_arch_hardware_setup
dmsrl_safe(MSR_EFER, &host_efer);
ops->hardware_setup(); // X86的 hardware_setup 函数
memcpy(&kvm_x86_ops, ops->runtime_ops, sizeof(kvm_x86_ops)); // 这里有个run time ops
/ *
*当在CPU检测代码中检测到MKTME或SME时,boot_cpu_data.x86_phys_bits会减少,
但是处理器会将这些减少的位视为“ keyID”,因此它们不是保留位。
因此,KVM需要查看CPUID报告的物理地址位。
* /
if (likely(boot_cpu_data.extended_cpuid_level >= 0x80000008))
return cpuid_eax(0x80000008) & 0xff;
/ *
*非常奇怪有VMX或SVM但没有MAXPHYADDR; 可能是具有自定义CPUID的VM。 继续执行发现的所有内核,因为这些功能不可虚拟化(SME / SEV还需要大于0x80000008的CPUID)。
* /
return boot_cpu_data.x86_phys_bits;
/ *
QEMU-KVM创建虚拟机
kvm_dev_ioctl
kvm_dev_ioctl_create_vm(arg); // case KVM_CREATE_VM:
kvm_create_vm(type);
kvm_coalesced_mmio_init(kvm);
get_unused_fd_flags(O_CLOEXEC);
anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR);
kvm_create_vm_debugfs(kvm, r)
kvm_uevent_notify_change(KVM_EVENT_CREATE_VM, kvm);
fd_install(r, file);
qemu 源码分析
在《qemu调试系统》中描述了如何创建一个X86的虚拟机,这里简要贴下如何编译qemu和使用qemu:
cd qemu
./configure --prefix=/opt/x86_64/qemu-x86-bin --target-list=x86_64-softmmu --gdb=/usr/bin/gdb \
--enable-linux-aio --enable-debug --enable-debug-info
sudo make && sudo make install
sudo echo "PATH=\$PATH:/usr/local/qemu_x86/bin" >> ~/.bashrc
source ~/.bashrc
which qemu-system-x86_64
/usr/local/qemu_x86/bin/qemu-system-x86_64 \
-smp 2 \
-cpu host \
-enable-kvm \
-m 512M \
-kernel linux/arch/x86/boot/bzImage \
-hda ./x86_64.img \
-hdb ./Freeze.img \
-nographic \
-append "root=/dev/sda rw rootfstype=ext4 console=ttyS0 init=linuxrc loglevel=8"
驱动初始化函数
QEMU初始化代码并不是在main入口函数中执行,而是有一部分驱动初始化模块:
#define module_init(function, type) \
static void __attribute__((constructor)) do_qemu_init_ ## function(void) \
{ \
register_module_init(function, type); \
}
#define block_init(function) module_init(function, MODULE_INIT_BLOCK)
#define opts_init(function) module_init(function, MODULE_INIT_OPTS)
#define type_init(function) module_init(function, MODULE_INIT_QOM)
#define trace_init(function) module_init(function, MODULE_INIT_TRACE)
#define xen_backend_init(function) module_init(function, \
MODULE_INIT_XEN_BACKEND)
#define libqos_init(function) module_init(function, MODULE_INIT_LIBQOS)
#define fuzz_target_init(function) module_init(function, \
MODULE_INIT_FUZZ_TARGET)
#define migration_init(function) module_init(function, MODULE_INIT_MIGRATION)
可以看到,attribute((constructor))用法解析 中讲述了: constructor 修饰的属性,在main函数之前执行。
会将所有相同的驱动模块加入到同一个类型的链表中。
所以软件中 module_call_init(MODULE_INIT_XXX) 会遍历当前节点的所有设备进行初始化。
入口函数总览
qemu_init初始化函数
module_call_init
在 驱动初始化函数 部分描述了,系统上电后,会将所有的设备注册到对应类型的链表中,这个接口是调用了当前接口类型下所有设备的init函数 进行初始化。
qemu_add_opts和qemu_add_drive_opts
static QemuOptsList *vm_config_groups[48];
static QemuOptsList *drive_config_groups[5];
添加 操作到对应的配置或驱动组 模块
argv_parse
这个函数最终会解析:qemu-options.def 中的结构体信息进行描述,这里在编译时会打包到
static const QEMUOption qemu_options[]中;
const QEMUOption *popt; popt = lookup_opt(argc, argv, &optarg, &optind);
popt = qemu_options; // 这个函数会从这个全局变量里边查找*****
typedef struct QEMUOption {
const char *name;
int flags;
int index;
uint32_t arch_mask;
} QEMUOption;
static const QEMUOption qemu_options[] = {
{ "h", 0, QEMU_OPTION_h, QEMU_ARCH_ALL },
#define QEMU_OPTIONS_GENERATE_OPTIONS
#include "qemu-options-wrapper.h"
{ NULL },
};
#define DEF(option, opt_arg, opt_enum, opt_help, arch_mask) \
{ option, opt_arg, opt_enum, arch_mask },
#define DEFHEADING(text)
#define ARCHHEADING(text, arch_mask)
#include "qemu-options.def"