armv8虚拟化原理笔记

发布时间 2024-01-13 14:01:21作者: 半山随笔

随便记记,没有章法。

VTTBR_EL2和TTBR1_EL2有啥区别?

VTTBR_EL2是内存虚拟化中stage2页表的基地址存放的寄存器,高16位存放了VMID,用于提高VM TLB性能;

TTBR1_EL2,是指在VHE开启的情况下host OS可以在EL2运行,这时候内核使用的页表基地址就存放在这里;

设备模拟分为软件模拟和直接assign。前者通过stage 2 entry 设置为fault由hypervisor完成模拟,后者由hypervisor将物理设备内存直接映射到VM。

当stage 2 fault产生时hypervisor需要知道触发fault的地址,读or写,属于哪个设备,哪个寄存器,size大小。HPFAR_EL2负责保存触发fault的IPA,ESR_EL2存放了出错的详细信息。问题:怎么知道是哪个设备触发的fault?

有些系统寄存器经常访问,如果每次都trap会影响性能,所以arm提供了相应的virtual寄存器,比如VPIDR_EL2,是MIDR_EL1的virtual版;VMPIDR_EL2是VPIDR_EL1的virtual版。

HFGRTR_EL2, Hypervisor Fine-Grained Read Trap Register,这是一个非常重要的寄存器,负责控制guest在读系统寄存器的时候的行为,是否trap。

ARM DDI 0487中D19.2以H开头的系列寄存器可以控制在guest读写执行某些敏感指令的行为。

enable HCR_EL2.IMO中断会route到EL2。还由FMO,AMObit也有同样的作用。

HCR_EL2的E2H控制vhe的行为,如果打开则由TGE决定当前是VM还是host,1是host,表面host kernel在EL2,也就不需要virtual interrupt了。所以在VM中E2H=1, TGE=0, IMO=FMO=AMO=1,VM=1

中断虚拟化有两种方式,HCR模拟,VI,VF,VSE可以发送虚拟中断;使用vgic;

中断控制器的模拟

GIC的虚拟化只支持cpu interface,对GICD, GICR, GITS的模拟只能依靠MMIO通过data abort来软件模拟,一般是KVM会使用in-kernel的方式模拟。

kvm_arm_gicv3_realize会真正实现gic。通过ioctl KVM_DEV_ARM_VGIC_GRP_ADDR注册gicd gicr的地址到kvm。

kvm_set_irq通过ioctl KVM_IRQ_LINE向VM注入中断。KVM_SIGNAL_MSI可以注入msi中断。KVM_SET_GSI_ROUTING可以设置gsi 路由

创建虚拟机

打开/dev/kvm得到kvm_fd,然后使用ioctl create vm;创建一个vm实例。

创建vcpu

qemu在cpu的realizefn(arm_cpu_realizefn)里面会初始化vcpu。初始化vcpu主要在qemu_init_vcpu里面。最重要的是创建一个vcpu thread,运行kvm_vcpu_thread_fn,在这个函数中,在VMfd上调用ioctl KVM_CREATE_VCPU创建vcpu,kvm会初始化对应vcpu,接着线程进入while循环,

 do {
        if (cpu_can_run(cpu)) {
            r = kvm_cpu_exec(cpu);
            if (r == EXCP_DEBUG) {
                cpu_handle_guest_debug(cpu);
            }
        }
        qemu_wait_io_event(cpu);
    } while (!cpu->unplug || cpu_can_run(cpu));

如果现在还处于stop状态就等着,如果可以运行就执行kvm_cpu_exec。在这个函数中会使用ioctl KVM_RUN真正是虚拟机进入guest运行。

 

qemu在arm64虚拟机上创建内存的调用链:

#0  ram_block_add (new_block=0x555556a64d00, errp=0x7fffffffdd40) at ../softmmu/physmem.c:1975
#1  0x0000555555e3a61b in qemu_ram_alloc_internal
     (size=size@entry=2147483648, max_size=max_size@entry=2147483648, resized=resized@entry=0x0, host=host@entry=0x0, ram_flags=ram_flags@entry=0, mr=mr@entry=0x555556c926e0, errp=0x7fffffffddb0) at ../softmmu/physmem.c:2180
#2  0x0000555555e3d307 in qemu_ram_alloc (size=size@entry=2147483648, ram_flags=ram_flags@entry=0, mr=mr@entry=0x555556c926e0, errp=errp@entry=0x7fffffffddb0) at ../softmmu/physmem.c:2200
#3  0x0000555555e33cad in memory_region_init_ram_flags_nomigrate
    (mr=mr@entry=0x555556c926e0, owner=owner@entry=0x555556c92680, name=name@entry=0x5555569bd720 "mach-virt.ram", size=2147483648, ram_flags=0, errp=errp@entry=0x7fffffffde20) at ../softmmu/memory.c:1563
#4  0x0000555555af6b0e in ram_backend_memory_alloc (errp=0x7fffffffde20, backend=0x555556c92680) at ../backends/hostmem-ram.c:33
#5  ram_backend_memory_alloc (backend=0x555556c92680, errp=0x7fffffffde20) at ../backends/hostmem-ram.c:20
#6  0x0000555555af6c20 in host_memory_backend_memory_complete (uc=<optimized out>, errp=0x7fffffffde70) at ../backends/hostmem.c:336
#7  0x0000555555ed60d7 in user_creatable_complete (uc=0x555556c92680, errp=errp@entry=0x5555569b2670 <error_fatal>) at ../qom/object_interfaces.c:28
#8  0x00005555558f7cb7 in create_default_memdev (errp=0x5555569b2670 <error_fatal>, path=<optimized out>, ms=0x555556bf8400) at ../hw/core/machine.c:1287
#9  machine_run_board_init (machine=0x555556bf8400, mem_path=<optimized out>, errp=0x5555569b2670 <error_fatal>) at ../hw/core/machine.c:1326
#10 0x0000555555aef986 in qemu_init_board () at ../softmmu/vl.c:2493
#11 qmp_x_exit_preconfig (errp=<optimized out>) at ../softmmu/vl.c:2589
#12 0x0000555555af325a in qmp_x_exit_preconfig (errp=<optimized out>) at ../softmmu/vl.c:2584
#13 qemu_init (argc=<optimized out>, argv=<optimized out>, envp=<optimized out>) at ../softmmu/vl.c:3586
#14 0x000055555588f33b in qemu_main (argc=<optimized out>, argv=<optimized out>, envp=<optimized out>) at ../softmmu/main.c:37
#15 0x00007ffff7629d90 in __libc_start_call_main (main=main@entry=0x555555888e30 <main>, argc=argc@entry=20, argv=argv@entry=0x7fffffffe248) at ../sysdeps/nptl/libc_start_call_main.h:58
#16 0x00007ffff7629e40 in __libc_start_main_impl (main=0x555555888e30 <main>, argc=20, argv=0x7fffffffe248, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe238)
    at ../csu/libc-start.c:392
#17 0x000055555588f265 in _start ()

 创建arm64虚拟机的命令行:

/root/qemu/build/qemu-system-aarch64 \
        -m 2048 \
        -cpu cortex-a57 \
        -smp 2 \
        -M virt \
        -nographic \
        -serial mon:stdio \
        -device virtio-scsi-device \
        -drive if=none,file=jammy-server-cloudimg-arm64.img,id=hd0 \
        -device virtio-blk-device,drive=hd0 \
        -bios /root/qemu/pc-bios/edk2-aarch64-code.fd \

 memory分配之后需要通知kvm,还有后续的内存更改通知,这由MemeoryListener来完成。

对于kvm有:

void kvm_memory_listener_register(KVMState *s, KVMMemoryListener *kml,
                                  AddressSpace *as, int as_id, const char *name)
{
   。。。

    kml->listener.region_add = kvm_region_add;
    kml->listener.region_del = kvm_region_del;
    kml->listener.commit = kvm_region_commit;
    kml->listener.log_start = kvm_log_start;
    kml->listener.log_stop = kvm_log_stop;
    kml->listener.priority = MEMORY_LISTENER_PRIORITY_ACCEL;
    kml->listener.name = name;

向kvm注册mmeory region只会添加hva和ipa的联系,并不会让kvm操作stage2的页表,更不会分配内存。此时stage2的页表只有一个pgd,只有在发生stage2的page fault的时候才会route到kvm,kvm这个时候才会去创建s2的页表。

kvm_handle_guest_abort负责解决s2页表缺失的问题。调用链是:

建立页表最重要的是找到ipa和对应的phy_addr。ipa是出错信息中拿到的,物理地址是通过hva拿到的,但是复杂的情况是这个时候可能还没有分配物理内存,所以根本不存在物理地址。hva_to_pfn可以解决这个问题,首先使用快速路径,在内存已经分配的情况下,可以直接拿到这个地址,如果没有分配就进入慢速路径,使用get_user_pages函数分配内存,返回物理地址。如果这样还不够后面还有进一步分操作。拿到物理地址之后就可以使用kvm_pgtable_stage2_map去建立页表了。

 对于mmio就简单很多,凡是没有注册过的ipa,一旦被访问就视为IO区域,一般由userspace处理。

int io_mem_abort(struct kvm_vcpu *vcpu, phys_addr_t fault_ipa)
{
    。。。
    /* Now prepare kvm_run for the potential return to userland. */
    run->mmio.is_write    = is_write;
    run->mmio.phys_addr    = fault_ipa;
    run->mmio.len        = len;
    vcpu->mmio_needed    = 1;

    。。。

    if (is_write)
        memcpy(run->mmio.data, data_buf, len);
    vcpu->stat.mmio_exit_user++;
    run->exit_reason    = KVM_EXIT_MMIO;
    return 0;
}

忽略in-kernel处理的情况,一般mmio会由VMM去处理。

int kvm_cpu_exec(CPUState *cpu)
{
。。。
switch (run->exit_reason) { case KVM_EXIT_IO: /* Called outside BQL */ kvm_handle_io(run->io.port, attrs, (uint8_t *)run + run->io.data_offset, run->io.direction, run->io.size, run->io.count); ret = 0; break; case KVM_EXIT_MMIO: /* Called outside BQL */ address_space_rw(&address_space_memory, run->mmio.phys_addr, attrs, run->mmio.data, run->mmio.len, run->mmio.is_write); ret = 0; break; case KVM_EXIT_IRQ_WINDOW_OPEN: ret = EXCP_INTERRUPT; break; case KVM_EXIT_SHUTDOWN: qemu_system_reset_request(SHUTDOWN_CAUSE_GUEST_RESET); ret = EXCP_INTERRUPT; break; case KVM_EXIT_UNKNOWN: fprintf(stderr, "KVM: unknown exit, hardware reason %" PRIx64 "\n", 。。。

qemu会监控VM trap,如果需要处理就从kvm->run中获取信息,主要包括退出原因,处理完之后再次调用KVM_RUN ioctl进入guest。