Friday, February 7, 2020

kvm_all.c - kvm_init, kvm_init_vcpu

继续接着前面的东西来,了解QEMU如何在KVM虚拟化环境中创建和初始化虚拟机和vCPU。

1. kvm_init
kvm_init函数用于打开KVM设备文件,如下所示,它还填充KVMStatefdvmfd

static int kvm_init(MachineState *ms)
{
……
    KVMState *s;
……
    s = KVM_STATE(ms->accelerator);
……
    s->vmfd = -1;
    s->fd = qemu_open("/dev/kvm", O_RDWR);
    if (s->fd == -1) {
        fprintf(stderr, "Could not access KVM kernel module: %mn");
        ret = -errno;
        goto err;
    }
……
    do {
        ret = kvm_ioctl(s, KVM_CREATE_VM, type);
    } while (ret == -EINTR);

    if (ret < 0) {
        fprintf(stderr, "ioctl(KVM_CREATE_VM) failed: %d %sn", -ret,
                strerror(-ret));
        goto err;
    }

    s->vmfd = ret;
……
    ret = kvm_arch_init(ms, s);
    if (ret < 0) {
        goto err;
    }
……
    return ret;
}

如上所示,带有KVM_CREATE_VM参数的ioctl将返回vmfd。一旦QEMU有了fdvmfd,就必须再填充一个文件描述符,即kvm_fd或vcpu fd。
2. kvm_init_vcpu

int kvm_init_vcpu(CPUState *cpu)
{

……
    KVMState *s = kvm_state;
    int ret;
……
    ret = kvm_get_vcpu(s, kvm_arch_vcpu_id(cpu));
    if (ret < 0) {
        goto err;
    }

    cpu->kvm_fd = ret;
    cpu->kvm_state = s;
 ……
    ret = kvm_arch_init_vcpu(cpu);
err:
    return ret;
}

一些内存页在qemu-kvm进程和KVM内核模块之间共享。你可以在kvm_init_vcpu()函数中看到这样的映射。也就是说,每个vCPU的两个主机内存页为QEMU用户空间进程和KVM内核模块之间的通信提供了通道:kvm_runpio_data
还要理解,在执行这些返回前面fd的ioctl期间,Linux内核会分配一个文件结构和相关的匿名节点。

vCPU是QEMU-KVM创建的线程,要运行来自guest代码,这些vCPU线程需要使用KVM_RUN作为参数执行ioctl,即第一篇文章(http://www.qemu.world/?x=entry:entry200206-201418)的:

run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0);

kvm_all.c - ioctls (kvm_ioctl, kvm_vm_ioctl, kvm_vcpu_ioctl, kvm_device_ioctl)

kvm_all.c是QEMU与KVM通信相关的一个非常重要的文件,因此它必然也有许多和通信相关的内容。KVM是一个内核模块,普通权限的QEMU若要和KVM通信,则使用ioctl是最方便的。

在kvm_all.c中,封装了多个ioctl相关的函数:kvm_ioctl, kvm_vm_ioctl, kvm_vcpu_ioctl, kvm_device_ioctl,这些ioctl最终会反映到系统的KVM,VM和vCPU三个级别上。 这些ioctl与KVM分类的ioctl基本一一对应。

要通过ioctl访问KVM内核模块,QEMU-KVM必须打开/dev/kvm,并将结果文件描述符存储在KVMState->fd中。

这四个函数基本内容一模一样,只是通过不同的文件描述符来发送ioctl。接下来来依次介绍它们。

1. kvm_ioctl
调用这个发送的ioctl,需要由KVMState->fd去发送,其中KVMState->fd是通过打开/dev/kvm获得的文件描述符。
定义如下:

int kvm_ioctl(KVMState *s, int type, ...) {
……
    ret = ioctl(s->fd, type, arg);
……
}

2. kvm_vm_ioctl
调用这个函数发送的ioctl主要经由KVMState->vmfd来执行。

定义如下:

int kvm_vm_ioctl(KVMState *s, int type, ...) {
……
    ret = ioctl(s->vmfd, type, arg);
……
}

3. kvm_vcpu_ioctl
与前面类似的,这个ioctl经由cpu->kvm_fd来执行。 CPUState等的定义可参考上一篇(http://www.qemu.world/?x=entry:entry200207-131048)文章。

定义如下:

int kvm_vcpu_ioctl(CPUState *cpu, int type, ...){
……
    ret = ioctl(cpu->kvm_fd, type, arg);
……
}

4. kvm_device_ioctl
这个稍稍有点不一样,它是个更通用一点的wrapper,先看定义:

int kvm_device_ioctl(int fd, int type, ...)
{
……
    ret = ioctl(fd, type, arg);
……
}

fd是从第一个参数传来的,基本就是个工具函数,使用例子例如:

int kvm_device_check_attr(int dev_fd, uint32_t group, uint64_t attr)
{
……
    return kvm_device_ioctl(dev_fd, KVM_HAS_DEVICE_ATTR, &attribute) ? 0 : 1;
}

kvm_all.c - struct KVMState, CPUState, CPUX86State

KVMState结构包含QEMU中VM表示的重要文件描述符。例如fd, vmfd

struct KVMState
{
    AccelState parent_obj;

    int nr_slots;
    int fd;
    int vmfd;
    int coalesced_mmio;
    int coalesced_pio;
    struct kvm_coalesced_mmio_ring *coalesced_mmio_ring;
    bool coalesced_flush_in_progress;
……
};

QEMU维护一个CPUState结构,它位于include/hw/core/cpu.h,用于定义一个CPU内核/线程的状态。CPUState的结构含义如下:

cpu_index:CPU索引,这里面能获取到很多信息。
cluster_index:标识此CPU所在的群集。对于未定义集群的板卡或未分配给集群的“松散” CPU,将为UNASSIGNED_CLUSTER_INDEX;否则,它将与CPU对象的TYPE_CPU_CLUSTERQOM父级的cluster-id属性相同。
nr_cores:此CPU软件包中的内核数。
nr_threads:此CPU中的线程数。
running:如果CPU当前正在运行(无锁),则为true。
has_waiter:如果CPU当前正在等待cpu_exec_end,true。在cpu_list_lock下有效。
created:指示是否已成功创建CPU线程。
interrupt_request:表示一个未决的中断请求。
halted:如果CPU处于挂起状态,则为非零。
stop:表示待处理的停止请求。
stopped:表示CPU已被人为停止。
unplug:指示挂起的CPU拔出请求。
crash_occurred:表示操作系统报告了该CPU的紧急崩溃事件
singlestep_enabled:单步执行的标志。
icount_extra:直到下一个定时器事件的指令。
can_do_io:如果内存映射的IO安全,则为非零。确定性执行要求仅对TB的最后一条指令执行IO,以便中断立即生效。
cpu_ases:指向CPUAddressSpaces数组的指针(定义该CPU具有的AddressSpaces)
num_ases:@cpu_ases中的CPUAddressSpaces数
as:指向第一个地址空间的指针,以方便仅具有单个地址空间的目标
env_ptr:指向特定于子类的CPUArchState字段的指针。
icount_decr_ptr:指向子类中IcountDecr字段的指针。
gdb_regs:其他GDB寄存器。
gdb_num_regs:GDB可访问的寄存器总数。
gdb_num_g_regs:GDB’g'数据包中的寄存器数。
next_cpu:下一个CPU共享TB缓存。
opaque:用户数据。
mem_io_pc:访问内存的主机程序计数器。
kvm_fd:KVM的vCPU文件描述符。
work_mutex:锁定以防止多次访问queued_work_xxx。
queued_work_first:第一个异步工作挂起。
trace_dstate_delayed:延迟了对trace_dstate的更改(包括对trace_dstate的所有更改)。
trace_dstate:此vCPU的事件的动态跟踪状态(位掩码)。
plugin_mask:插件事件位图。仅通过异步工作进行修改。
ignore_memory_transaction_failures:具有相同名称的MachineState标志的缓存副本:允许开发板禁止调用CPU do_transaction_failed挂钩函数。

对于例如X86模式的CPU来说,QEMU会维护一个CPUX86State结构列表,每个虚拟CPU对应一个结构。它位于target/i386/cpu.h,通用寄存器(以及RSP和RIP)的内容是CPUX86State的一部分。由于定义太长,我删除了绝大多数内容。

typedef struct CPUX86State {
    /* standard registers */
    target_ulong regs[CPU_NB_REGS];
    target_ulong eip;
    target_ulong eflags; 
   ……
    target_ulong cr[5]; /* NOTE: cr1 is unused */
    int32_t a20_mask;
……
    ZMMReg xmm_regs[CPU_NB_REGS == 8 ? 8 : 32];
    ZMMReg xmm_t0;
    MMXReg mmx_t0;
……
    /* For KVM */
    uint32_t mp_state;
    int32_t exception_nr;
    int32_t interrupt_injected;
    uint8_t soft_interrupt;
    uint8_t exception_pending;
    uint8_t exception_injected;
……
} CPUX86State;

Thursday, February 6, 2020

kvm_all.c - kvm_cpu_exec, kvm_handle_io

简介
kvm_all.c位于/accel/kvm/kvm-all.c,用于QEMU - KVM的通信,与其相关的许多重要的函数,例如kvm_handle_iokvm_cpu_exec都位于此。

kvm_cpu_exec,kvm状态下的大消息循环,对应tcg模式下的tcg_cpu_exec。这个函数处理大量的vcpu产生的事件。

该函数在调用cpu_exec_start启动vcpu后,(1)调用kvm_vcpu_ioctl发送ioctl KVM_RUN给kvm,此时启动vCPU。该ioctl会进入对vCPU运行的等待状态。如果kvm发生VMEXIT,即从KVM返回到qemu-kvm用户空间时,则该函数读取run->exit_reason,获取退出的原因,并调用对应的处理函数。

KVM是英特尔和AMD等供应商提供的硬件扩展的推动者,它们的虚拟化扩展包括SVM和VMX。KVM使用这些扩展在主机CPU上直接执行客户代码。但是,如果存在事件(例如,作为由QEMU模拟的客户内核代码访问硬件设备寄存器的操作的一部分),则KVM必须退出回QEMU并传递控制。然后QEMU可以模拟操作的结果。

有不同的退出原因,如以下代码所示:

对应如下:

KVM_EXIT_IO - kvm_handle_io
KVM_EXIT_MMIO - address_space_rw
KVM_EXIT_IRQ_WINDOW_OPEN - return EXCP_INTERRUPT
KVM_EXIT_SHUTDOWN - qemu_system_reset_request
KVM_EXIT_UNKNOWN - 异常退出
KVM_EXIT_INTERNAL_ERROR - kvm_handle_internal_error
KVM_EXIT_SYSTEM_EVENT - 根据事件类型,分别有
    KVM_SYSTEM_EVENT_SHUTDOWN = qemu_system_shutdown_request
    KVM_SYSTEM_EVENT_RESET = qemu_system_reset_request
    KVM_SYSTEM_EVENT_CRASH = qemu_system_guest_panicked
    其他 = kvm_arch_handle_exit
其他 - kvm_arch_handle_exit

处理完后,如果没有产生错误,则继续循环,回到(1)处等待kvm。

如果产生错误,则调用cpu_exec_end,停止cpu执行。

——————————–

kvm_handle_io则相对简单,它其实是对address_space_rw的封装。它会将传入的,要写入对应设备的数据依次写入。通常,这与直接写往ioport并没有什么区别。