基本逻辑
Linux内核KVM的分析可以参考这里,不过这个分析是基于RISCV的。Linux的KVM基本框架可以
参考基于RISCV的这个文档,我们这里着重分析ARM KVM的实现。
ARM64 KVM的初始化入口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| kvm_arm_init /* 初始化各个异常向量表 */ +-> kvm_init_vector_slots /* 注意,这里初始化的是各个子系统的类,而不是实例。从下面的实例会看的更清楚 */ +-> init_subsystems | /* arch/arm64/kvm/vgic/vgic-init.c */ | +-> kvm_vgic_hyp_init | | +-> vgic_v3_probe | | +-> kvm_register_vgic_device | | /* 按device type把对应的device回调注册入系统 */ | | +-> kvm_register_device_ops(&kvm_arm_vgic_v3_ops, KVM_DEV_TYPE_ARM_VGIC_V3) | | +-> kvm_vgic_register_its_device | | | | /* 注册各个timer的中断处理函数 */ | +-> kvm_timer_hyp_init | | /* | | * 拿到drivers/clocksource/arm_arch_timer.c里定义的arch_timer_kvm_info, | | * 读取count的方法在timecounter,也在KVM里得到,注意这里读的是CNTVCT。 | | */ | | +-> info = arch_timer_get_kvm_info() | | timecounter = &info->timecounter | | | | /* 把info->virtual_irq给到host_vtimer_irq,应该是27号中断 */ | | +-> kvm_irq_init | | | | /* | | * 注册host vtimer irq。vcnt溢出时触发这个中断,这个中断会报道kvm里。 | | * 中断处理函数向vCPU注入对应的vtimer中断。在kvm_timer_should_fire里 | | * 判断是否需要注入,在kvm_timer_update_irq里注入vtimer中断。 | | * | | * todo: 怎么配置这个中断在kvm处理? 如何判断注入哪个vCPU? | | */ | | +-> request_percpu_irq(host_vtimer_irq, kvm_arch_timer_handler, "kvm guest vtimer", ...) | | | /* 创建/dev/kvm文件,后续的VM/vCPU等资源都从这个文件的ioctl创建出来 */ +-> kvm_init +-> ioctl: KVM_CREATE_VM/KVM_CHECK_EXTENSION...
|
实例的初始化接口是各个ioctl接口。
基本数据结构
整个虚拟机的数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| kvm +-> kvm_arch arch // arm64 kvm结构 +-> arch_timer_vm_data timer_data // timer公共 +-> voffset +-> ppi[]
+-> vgic_dist vgic // GIC,父类是vgic_io_device +-> vgic_dist_base // 表示这个vm上的GICD的base +-> its_vm its_vm // 表示这个vm上的ITS +-> its_vpe **vpes // 表示这个vm上的各个vPE +-> vpe_id +-> irq_domain *domain +-> id_regs[] // 系统寄存器 +-> ctr_el0
+-> kvm_s2_mmu mmu // 内存
|
虚拟机上vCPU的数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| kvm_vcpu +-> kvm_vcpu_arch arch // arm64 vcpu结构,具体vCPU实例 +-> arch_timer_cpu timer_cpu // arm64 vcpu timer结构 | +-> arch_timer_context timers[] // 一个vcpu上多种类型timer | +-> arch_timer_offset offset | +-> vm_offset指针 | +-> hrtimer +-> vgic_cpu vgic_cpu // GIC和core相关的部件 +-> vgic_v3_cpu_if vgic_v3 +-> its_vpe its_vpe // its_vpe的指针会保存到如上vpes数组 +-> irq_domain *sgi_domain +-> vgic_io_device rd_iodev +-> vgic_redist_region *rdreg /* vCPU SGI和PPI的vgic_irq,vLPI的vgic_irq保存在vgic_dist->lpi_xa */ +-> private_irqs[] +-> kvm_pmu // PMU kvm_pmu_events events kvm_pmc pmc idx perf_event *perf_event irq_num ...
|
KVM中被模拟device的需要有个总线模型把它们的逻辑组织起来,相关的数据结构是kvm_io_device、
kvm_io_bus以及kvm_io_range。kvm_io_device描述一个KVM里的设备,kvm_io_bus是kvm_io_device的
bus,kvm_io_bus被划分位多个range,每个range都有对应的kvm_io_range。
KVM里的device用面向对象的方式组织起来,比如,ARM64里KVM的vgic的父类即为kvm_io_device,
每层数据结构中类的继承关系如下:
1 2 3 4 5 6 7 8 9
| kvm_io_device - kvm_io_device_ops *op = kvm_io_gic_ops ^ | vgic_io_device - vgic_io_device *regions = its_registers ^ ^ | \ vgic_its vgic_disc
|
基于如上的数据结构,vgic_register_its/dist/redist_iodev分别把ITS/GICD/GICR注册到
系统中。具体的调用点在后面创建ITS的流程里。
这些KVM中的device的模拟在KVM里进行,当guest内核访问这些device触发guest退出到KVM时,
在KVM就可以直接模拟对应device的逻辑。如上的这些数据结构支持KVM可以找到对应device
的处理逻辑。
创建VM
open如上的/dev/kvm得到一个fd,对fd做ioctl KVM_CREATE_VM,返回一个匿名fd表示被创
建的vm:
1 2 3 4 5 6 7 8 9 10 11
| /* virt/kvm/kvm_main.c */ kvm_dev_ioctl_create_vm +-> kvm_create_vm /* 具体构架相关 */ +-> kvm_arch_init_vm +-> kvm_init_stage2_mmu +-> kvm_vgic_early_init /* 更新kvm->arch.timer_data.ppi[] */ +-> kvm_timer_init_vm +-> kvm_arm_init_hypercalls ... todo ...
|
对这个vm fd的各种ioctl用来创建VM的各个部件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| /* virt/kvm/kvm_main.c */ kvm_vm_ioctl +-> KVM_CREATE_VCPU +-> KVM_SET_USER_MEMORY_REGION +-> KVM_GET_DIRTY_LOG +-> KVM_CLEAR_DIRTY_LOG ... +-> KVM_IRQFD +-> KVM_SET_GSI_ROUTING
/* 创建kvm上的各个设备, GICv3设备就是通过这个接口创建起来的 */ +-> KVM_CREATE_DEVICE
+-> KVM_CREATE_GUEST_MEMFD ...
|
使用vm ioctl KVM_CREATE_VCPU 创建vCPU的流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| kvm_vm_ioctl的KVM_CREATE_VCPU +-> kvm_vm_ioctl_create_vcpu +-> kvm_arch_vcpu_create /* vtimer */ +-> kvm_timer_vcpu_init | /* | * 把如上vm_offset指针指向内容更新为当前CNTVCT的值,这还没有更新到 | * 寄存器里。 | */ | +-> timer_set_offset(vcpu_vtimer(vcpu), kvm_phys_timer_read()) | /* todo: ... */ | +-> hrtimer_init | /* todo: ... */ | +-> timer->bg_timer.function = kvm_bg_timer_expire | /* | * 简单初始化,相当于放了一个桩,后面通过vcpu的KVM_ARM_VCPU_PMU_V3_CTRL | * 以及相关的PMU这一级的attr进行初始化和属性配置: | * kvm_arm_vcpu_arch_set_attr(KVM_ARM_VCPU_PMU_V3_CTRL/attr) | */ +-> kvm_pmu_vcpu_init | | /* 注册当前vCPU的GICR */ +-> kvm_vgic_vcpu_init(vcpu) +-> vgic_register_redist_iodev
|
使用vCPU的对应的fd, 通过ioctl操作vCPU, 操作有KVM_RUN/KVM_GET_REGS/KVM_SET_REGS…
其中default会走到架构自定义的vCPU ioclt里,ARM64会在其中做vcpu_init。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| /* virt/kvm/kvm_main.c */ kvm_vcpu_ioctl /* 拉起vCPU的入口,下面拉出来独立分析 */ +-> KVM_RUN +-> kvm_arch_vcpu_run_pid_change <--- 语意是第一次运行? | +-> kvm_vgic_map_resources | | | | +-> vgic_v3_map_resources | | +-> vgic_v4_configure_vsgis <--- | | +-> vgic_v4_enable_vsgis | | | | /* 这里才注册GICD */ | | +-> vgic_register_dist_iodev | | /* 更新kvm->buses[bus_idx] */ | | +-> kvm_io_bus_register_dev | +-> kvm_timer_enable | /* todo: ... */ | +-> kvm_vgic_map_phys_irq +-> kvm_arch_vcpu_ioctl_run <--- 拉起vCPU !! +-> default +-> kvm_arch_vcpu_ioctl
KVM_ARM_VCPU_INIT: +-> kvm_arch_vcpu_ioctl_vcpu_init +-> kvm_vcpu_set_target /* 在这里面初始化各种系统寄存器、pmu、timer、sve */ +-> kvm_reset_vcpu /* * 其中调用每个系统寄存器的reset回调,把返回值保存到 * kvm->arch->id_regs[]。 */ +-> kvm_reset_sys_regs /* todo: ... */ +-> kvm_timer_vcpu_reset KVM_SET_ONE_REG KVM_GET_ONE_REG ... KVM_SET_DEVICE_ATTR // 各种core相关设备的初始化和属性配置 +-> KVM_ARM_VCPU_PMU_V3_CTRL +-> IRQ/INIT/FILTER/SET_PMU +-> KVM_ARM_VCPU_TIMER_CTRL +-> KVM_ARM_VCPU_PVTIME_CTRL KVM_GET_DEVICE_ATTR ...
|
CPU虚拟化
拉起vCPU的入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| /* arch/arm64/kvm/arm.c */ kvm_arch_vcpu_ioctl_run /* * 控制vCPU运行的核心逻辑,注意这里如果vCPU退到KVM里可以处理,KVM处理完后会马 * 上再次进入vCPU执行。 */ while (ret > 0) { /* 拉起vCPU的核心逻辑,单独分析 */ +-> kvm_arm_vcpu_enter_exit ... /* VM退出到KVM的入口,arch/arm64/kvm/handle_exit.c */ +-> handle_exit /* trap到KVM的逻辑入口 */ +-> handle_trap_exceptions /* * 根据trap时报的EC类型选择不同的处理函数,整个处理函数的列表定义在: * arm_exit_handlers数组。 * * 其中,对于系统寄存器触发的trap在kvm_handle_sys_reg中处理。对mmio * 的处理流程是: * kvm_handle_guest_abort->io_mem_abort->kvm_io_bus_read/write。 */ +-> kvm_get_exit_handler +-> exit_handler }
|
vCPU从kvm_arm_vcpu_enter_exit进入,遇到host的异常或中断跳到对应的处理程序中处理,
然后又从kvm_arm_vcpu_enter_exit里回到KVM里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| kvm_arm_vcpu_enter_exit +-> __kvm_vcpu_run // 只看vhe的场景 +-> local_daif_mask +-> __kvm_vcpu_run_vhe +-> __activate_traps | +-> ___activate_traps(vcpu, __compute_hcr(vcpu)) | /* 进vCPU之前把异常向量换成kvm_hyp_vector */ | +-> write_sysreg(__this_cpu_read(kvm_hyp_vector), vbar_el1) | | /* 准备好guest的上下文后,eret切到EL1 guest执行 */ +-> __guest_enter // arch/arm64/kvm/hyp/entry.S | | /* | * 换上host的异常向量表: entry.S里的vectors。vectors里的各个异常向量 | * 的宏展开里有字符串拼接的跳转函数名字,这些函数定义在entry-common.c: | * | * el1t_64_sync_handler/el1t_64_irq_handler - 触发异常和taken都在EL1 | * el1h_64_sync_handler/el1h_64_irq_handler / | * el0t_64_sync_handler/el0t_64_irq_handler - 从EL0切到EL1出异常中断 | * | * 注意,如果host在EL2,如上EL1实际上是EL2。 | */ +-> __deactivate_traps
+->local_daif_restore
|
cpu_set_hyp_vector里从一些异常向量里选择一个更新kvm_hyp_vector。
1 2 3 4 5
| init_subsystems -> cpu_hyp_init -> cpu_hyp_reinit -> cpu_hyp_init_features -> cpu_set_hyp_vector
|
先看一个向量__kvm_hyp_vector。
/* arch/arm64/kvm/kvm/hyp/hyp-entry.S */
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| SYM_CODE_START(__kvm_hyp_vector) invalid_vect el2t_sync_invalid // Synchronous EL2t invalid_vect el2t_irq_invalid // IRQ EL2t invalid_vect el2t_fiq_invalid // FIQ EL2t invalid_vect el2t_error_invalid // Error EL2t valid_vect el2_sync // Synchronous EL2h <--- A invalid_vect el2h_irq_invalid // IRQ EL2h invalid_vect el2h_fiq_invalid // FIQ EL2h valid_vect el2_error // Error EL2h valid_vect el1_sync // Synchronous 64-bit EL1 <--- B valid_vect el1_irq // IRQ 64-bit EL1 valid_vect el1_fiq // FIQ 64-bit EL1 valid_vect el1_error // Error 64-bit EL1 valid_vect el1_sync // Synchronous 32-bit EL1 valid_vect el1_irq // IRQ 32-bit EL1 valid_vect el1_fiq // FIQ 32-bit EL1 valid_vect el1_error // Error 32-bit EL1 SYM_CODE_END(__kvm_hyp_vector)
|
如上异常向量表在进入虚拟机之前被配置上,所以只有在虚机里有异常需要host处理,或者
host本身有异常或中断需要处理时,才会进入到这个异常向量表。这时相当于低级别特权级
进入高级别特权级处理异常和中断,就是B这组异常向量。而A组异常向量只有el2_sync和
el2_error两个有效,似乎是EL2遇到非法指令才会走到这里。
具体看下B这组异常向量里的各个向量。EL1异常需要EL2处理时,跳到el2_sync,随后
el2_sync->el1_trap->__guest_exit,在__guest_exit里恢复host的上下文,然后ret返回到
__guest_enter的下一条指令。整体逻辑上看,虚机从__guest_enter进入,同样从__guest_enter
退出来。el1_irq/el1_fiq/el1_error,把异常原因保存到x0,同样跳到__guest_exit。
内存虚拟化
todo: …
中断虚拟化
ARM KVM里模拟了GIC中的各个部件,当guest访问GIC相关的系统寄存器和MMIO寄存器时,会
trap到KVM完成模拟。这里要整理的逻辑有:1. KVM里中断控制器的数据结构和初始化逻辑,
2. GIC相关的系统寄存器和MMIO寄存器的访问逻辑,3. 每种中断运行时的基本逻辑。本文
只整理前两点,第三点需要另外文档中展开描述。
GICD/GICR的数据结构的位置在上文已经提到过。ARM KVM下ITS的数据结构是vgic_its,它
的保存位置在,kvm->devices(kvm_device的list)->kvm_device->private,可见kvm里用一
个devices链表保存所有kvm_device,而vgic_its就是一种kvm_device。如下ITS对应的create
函数创建vgic_its并把其保存到private里。
如上已经提到过GICv3和ITS的注册点: kvm_arm_init->init_subsystems->kvm_vgic_hyp_init…
触发中断控制器实例创建的是后续的ioctl。GICv3/ITS设备创建是通过vm ioctl的
KVM_CREATE_DEVICE接口:
1 2 3 4 5 6 7 8 9 10 11 12 13
| /* virt/kvm/kvm_main */ kvm_ioctl_create_device /* * 在kvm_device_ops_table中找见之前注册的设备,调用对应的create。注意,在 * kvm_arm_init里已经注册过KVM_DEV_TYPE_ARM_VGIC_V3和KVM_DEV_TYPE_ARM_VGIC_ITS。 * * GICv3的create是vgic_create, arch/arm64/kvm/vgic/vgic-init.c。 * ITS的create是vgic_its_create,arch/arm64/kvm/vgic/vgic-its.c,注意,ITS * 的create里有GICv4的create。 */ +-> op->create /* 为设备创建匿名文件,后面set_attr/get_attr等操作都是通过这个文件的ioctl */ +-> anon_inode_getfd
|
虽然有create函数,但是GICv3的实例创建确是set_attr中的KVM_DEV_ARM_VGIC_CTRL_INIT
里做的。
1 2 3 4 5 6 7 8
| vgic_v3_set_attr +-> vgic_set_common_attr +-> vgic_init +-> kvm_vgic_dist_init +-> vgic_v4_init +-> kvm_vgic_vcpu_init // 循环操作每个vcpu +-> kvm_vgic_setup_default_irq_routing ...
|
如上的逻辑还没有把vgic_its/vgic_disc/vgic_cpu(GICR)加入kvm_io_device/bus/range描
述的数据结构中。vgic_its的注册由set_attr中的接口完成,基地址从用户态配置下来,这
里根据基地址就可以确认KVM bus/range中的位置。
1 2 3
| vgic_its_set_attr // KVM_DEV_ARM_VGIC_GRP_ADDR +-> vgic_register_its_iodev +-> kvm_io_bus_register_dev
|
如上vgic_dist的注册在KVM_RUN的流程里,vgic_cpu的注册在KVM_CREATE_VCPU的流程里。
这里的逻辑有点乱:(
可以看到模拟GIC既可以在KVM中直接访问,也可以从用户态通过ioctl接口访问。从KVM中访
问的逻辑在如上CPU虚拟化章节有提到。
从ioctl访问使用的接口是:kvm_vcpu_ioctl + KVM_SET_DEVICE_ATTR +
KVM_DEV_ARM_VGIC_GRP_ITS/DIST/REDIST_REGS,KVM_DEV_ARM_VGIC_GRP_CPU_SYSREGS
基于如上的数据结构,我们可以分析特定中断类型的业务逻辑,可以大概分类为:1. vSPI的
基本逻辑,2. vLPI以及vLPI直通的基本逻辑,3. vSGI以及vSGI直通的基本逻辑,4. vtimer
的基本逻辑。