虚拟化原理之kvm

来源:互联网 发布:pdb数据库蛋白质结构 编辑:程序博客网 时间:2024/05/17 09:33

2章 KVM 虚拟化

2.1   kvm技术基础

KVM(kernel-based virtual machine)的名字,基于kernel的虚拟机,已经很准确的说出了kvm的设计思路:也就是依赖linux内核,完全利用linux内核来实现cpu的调度,内存管理的功能。而另一个开源虚拟机xen,则自己开发了一套底层操作系统功能。从vcpu调度到内存管理一应俱全。虽然xen这个系统也是基于linux的,但是发展路线不同,和目前linux内核相比,已经面目全非了。这就是kvm受到开源组织的欢迎,而xen一直被排斥的根源。

虽然说早期的kvm是全虚拟化,而xen是半虚拟化,但发展到今天,xen支持全虚拟化,而kvm也早就有了半虚拟化的patch。技术上可以互相渗透,而软件架构一旦确定了,反而难改。不能因为xen是半虚拟化,就认为linux内核排斥半虚拟化的方案。实际上,另一个进了内核的开源虚拟机Lguest,它就是一个半虚拟化的方案。当然,现在linux内核本身都推出了半虚拟化架构,做半虚拟化也没以前那么繁琐了。

另一个趋势是基于硬件的虚拟化成为主流。早期x86虚拟化的低性能让人印象深刻,所以在intel推出硬件辅助虚拟化之后,虚拟化方案全面向硬件辅助靠拢。而kvmLguest这些比较新的方案,则彻底不支持软件的方案,而把硬件辅助当作了设计的根基。

从软件架构上来说,kvm提供了两个内核模块,使用kvmio_ctl接口可以管理vcpu和内存,为vcpu注入中断和提供时钟信号,而kvm本身没有提供设备的模拟。设备模拟需要应用层软件Qemu来实现。这种架构保证了kvm避免了繁琐的设备模拟和设备驱动部分(内核中80%以上的代码就是驱动部分)。

总结一下kvm软件的架构特点:

q Kvm本身只提供两个内核模块。Kvm实现了vcpu和内存的管理。

q Qemu控制逻辑,负责创建虚拟机,创建vcpu

2.2 Kvm管理接口

Qemukvm关系很深,甚至可以认为双方本来是一个软件,Qemu是应用层的控制部分,而kvm是内核执行部分。软件复用能达到如此天衣无缝的地步,是一件很神奇的事情,也说明kvm设计时候的思路之巧。

所以分析kvm,必须首先从Qemu的代码分析入手。为了避免繁琐,引入太多知识点,而混杂不清。所以把Qemu的代码做简化处理。

代码清单2-1  Qemu启动代码

    s->fd = qemu_open("/dev/kvm", O_RDWR);

    ret = kvm_ioctl(s, KVM_GET_API_VERSION, 0);

    

    s->vmfd = kvm_ioctl(s, KVM_CREATE_VM, 0);

   ...............................

    ret = kvm_vm_ioctl(s, KVM_CREATE_VCPU, env->cpu_index);

   .............................

    env->kvm_fd = ret;

    run_ret = kvm_vcpu_ioctl(env, KVM_RUN, 0);

可以看到,kvm提供了一个设备/dev/kvm,对kvm的控制要通过这个设备提供的io_ctl接口实现。这是linux内核提供服务的最通用方式,不再赘述。

kvm提供了三种概念,分别通过不同的io_ctl接口来控制。

q kvm:代表kvm模块本身,用来管理kvm版本信息,创建一个vm

q vm:代表一个虚拟机。通过vmio_ctl接口,可以为虚拟机创建vcpu,设置内存区间,创建中断控制芯片,分配中断等等。

q vcpu:代表一个vcpu。通过vcpuio_ctl接口,可以启动或者暂停vcpu,设置vcpu的寄存器,为vcpu注入中断等等。

Qemu的使用方式,首先是打开/dev/kvm设备,通过KVM_CREATE_VM创建一个虚拟机对象,然后通过KVM_CREATE_VCPU为虚拟机创建vcpu对象,最后通过KVM_RUN设置vcpu运行起来。因为是简化的代码,中断芯片的模拟,内存的模拟,寄存器的设置等等都已经省略了。

2.3 VT技术和vmcs结构

前文讲到kvm是基于硬件辅助虚拟化来实现的。这个硬件辅助的虚拟化技术,在不同的cpu架构中有不同的实现。在x86的平台下,intel实现了VT技术,而另一家x86芯片厂家AMD也推出了自己的虚拟化技术AMD-V。反映到代码上,intel技术的代码都在/arch/x86/kvm目录里面的vmx.c文件,而AMD的实现代码在相同目录的svm.c文件中。

回顾一下虚拟化技术的实现,经典的虚拟化使用了陷入-模拟的模式,而硬件辅助虚拟化引入了根模式(root operation)和非根模式(none-root operation),每种模式都有ring0-3的四级特权级别。所以,在硬件辅助虚拟化中,陷入的概念实际上被VM-EXIT操作取代了,它代表从非根模式退出到根模式,而从根模式切换到非根模式是VM-Entry操作。

2.3.1 需要具备的硬件知识

做系统软件的必须和硬件打交道,这就必须深入cpu架构和设备的架构。但是intel的架构浩大繁杂,说明文档多达上千页,深入了解着实有难度,另外一种趋势是软硬件的分离已经进行了多年,而系统软件的作者多半是软件人员,而非硬件人员。作为软件人员,了解必备的硬件知识是需要的,也是理解代码和架构的基础。同时,在操作系统软件的理解中,分清软件部分的工作和硬件部分的工作是必备条件,这也是操作系统软件中最让人困惑的部分。

对于虚拟化的vt技术而言,它的软件部分基本体现在vmcs结构中(virtual machine control block)。主要通过vmcs结构来控制vcpu的运转。

q Vmcs是个不超过4K的内存块。

q Vmcs通过下列的指令控制,vmclear:清空vmcs结构,vmread:读取vmcs数据,vmwrite:数据写入vmcs

q 通过VMPTR指针指向vmcs结构,该指针包含vmcs的物理地址。

Vmcs包含的信息可以分为六个部分。

q Guest state area:虚拟机状态域,保存非根模式的vcpu运行状态。当VM-Exit发生,vcpu的运行状态要写入这个区域,当VM-Entry发生时,cpu会把这个区域保存的信息加载到自身,从而进入非根模式。这个过程是硬件自动完成的。保存是自动的,加载也是自动的,软件只需要修改这个区域的信息就可以控制cpu的运转。

q Host state area:宿主机状态域,保存根模式下cpu的运行状态。只在vm-exit时需要将状态

q VM-Execution control filelds:包括page fault控制,I/O位图地址,CR3目标控制,异常位图,pin-based运行控制(异步事件),processor-based运行控制(同步事件)。这个域可以设置那些指令触发VM-Exit。触发VM-Exit的指令分为无条件指令和有条件指令,这里设置的是有条件指令。

q VM-entry contorl filelds:包括vm-entry控制,vm-entry MSR控制,VM-Entry插入的事件。MSRcpu的模式寄存器,设置cpu的工作环境和标识cpu的工作状态。

q VM-exit control filelds:包括VM-Exit控制,VM-Exit MSR控制。

q VM退出信息:这个域保存VM-Exit退出时的信息,并且描述原因。

有了vmcs结构后,对虚拟机的控制就是读写vmcs结构。后面对vcpu设置中断,检查状态实际上都是在读写vmcs结构。在vmx.h文件给出了intel定义的vmcs结构的内容。

2.4  cpu虚拟化

2.4.1 Vcpu数据结构

struct kvm_vcpu {

struct kvm *kvm;

#ifdef CONFIG_PREEMPT_NOTIFIERS

struct preempt_notifier preempt_notifier;

#endif

int vcpu_id;

struct mutex mutex;

int   cpu;

struct kvm_run *run;

unsigned long requests;

unsigned long guest_debug;

int fpu_active;

int guest_fpu_loaded;

wait_queue_head_t wq;

int sigset_active;

sigset_t sigset;

struct kvm_vcpu_stat stat;

#ifdef CONFIG_HAS_IOMEM

int mmio_needed;

int mmio_read_completed;

int mmio_is_write;

int mmio_size;

unsigned char mmio_data[8];

gpa_t mmio_phys_addr;

#endif

struct kvm_vcpu_arch arch;

};

这个结构定义了vcpu的通用结构,其中重点是kvm_vcpu_arch,这个是和具体cpu型号有关的信息。

struct kvm_vcpu_arch {

u64 host_tsc;

/*

 * rip and regs accesses must go through

 * kvm_{register,rip}_{read,write} functions.

 */

unsigned long regs[NR_VCPU_REGS];

u32 regs_avail;

u32 regs_dirty;

unsigned long cr0;

unsigned long cr2;

unsigned long cr3;

unsigned long cr4;

unsigned long cr8;

u32 hflags;

u64 pdptrs[4]; /* pae */

u64 shadow_efer;

u64 apic_base;

struct kvm_lapic *apic;    /* kernel irqchip context */

int32_t apic_arb_prio;

int mp_state;

int sipi_vector;

u64 ia32_misc_enable_msr;

bool tpr_access_reporting;

struct kvm_mmu mmu;

/* only needed in kvm_pv_mmu_op() path, but it's hot so

 * put it here to avoid allocation */

struct kvm_pv_mmu_op_buffer mmu_op_buffer;

struct kvm_mmu_memory_cache mmu_pte_chain_cache;

struct kvm_mmu_memory_cache mmu_rmap_desc_cache;

struct kvm_mmu_memory_cache mmu_page_cache;

struct kvm_mmu_memory_cache mmu_page_header_cache;

gfn_t last_pt_write_gfn;

int   last_pt_write_count;

u64  *last_pte_updated;

gfn_t last_pte_gfn;

struct {

gfn_t gfn; /* presumed gfn during guest pte update */

pfn_t pfn; /* pfn corresponding to that gfn */

unsigned long mmu_seq;

} update_pte;

struct i387_fxsave_struct host_fx_image;

struct i387_fxsave_struct guest_fx_image;

gva_t mmio_fault_cr2;

struct kvm_pio_request pio;

void *pio_data;

u8 event_exit_inst_len;

struct kvm_queued_exception {

bool pending;

bool has_error_code;

u8 nr;

u32 error_code;

} exception;

struct kvm_queued_interrupt {

bool pending;

bool soft;

u8 nr;

} interrupt;

int halt_request; /* real mode on Intel only */

int cpuid_nent;

struct kvm_cpuid_entry2 cpuid_entries[KVM_MAX_CPUID_ENTRIES];

/* emulate context */

struct x86_emulate_ctxt emulate_ctxt;

gpa_t time;

struct pvclock_vcpu_time_info hv_clock;

unsigned int hv_clock_tsc_khz;

unsigned int time_offset;

struct page *time_page;

bool singlestep; /* guest is single stepped by KVM */

bool nmi_pending;

bool nmi_injected;

struct mtrr_state_type mtrr_state;

u32 pat;

int switch_db_regs;

unsigned long db[KVM_NR_DB_REGS];

unsigned long dr6;

unsigned long dr7;

unsigned long eff_db[KVM_NR_DB_REGS];

u64 mcg_cap;

u64 mcg_status;

u64 mcg_ctl;

u64 *mce_banks;

};

q 有寄存器信息,cr0,cr2,cr3等。

q 有内存mmu的信息,

q 有中断控制芯片的信息kvm_lapic

q 有io请求信息kvm_pio_request

q 有vcpu的中断信息interrupt

2.4.2 vcpu创建

首先是Qemu创建VM,从代码分析一下:

代码清单2-2  V

static int kvm_dev_ioctl_create_vm(void)

{

int fd;

struct kvm *kvm;

kvm = kvm_create_vm();

if (IS_ERR(kvm))

return PTR_ERR(kvm);

       /*生成kvm-vm控制文件*/

fd = anon_inode_getfd("kvm-vm", &kvm_vm_fops, kvm, 0);

if (fd < 0)

kvm_put_kvm(kvm);

return fd;

}

调用了函数kvm_create_vm,然后是创建一个文件,这个文件作用是提供对vmio_ctl控制。

代码清单2-3  V

static struct kvm *kvm_create_vm(void)

{

struct kvm *kvm = kvm_arch_create_vm();

       /*设置kvmmm结构为当前进程的mm,然后引用计数加一*/

kvm->mm = current->mm;

atomic_inc(&kvm->mm->mm_count);

spin_lock_init(&kvm->mmu_lock);

spin_lock_init(&kvm->requests_lock);

kvm_io_bus_init(&kvm->pio_bus);

kvm_eventfd_init(kvm);

mutex_init(&kvm->lock);

mutex_init(&kvm->irq_lock);

kvm_io_bus_init(&kvm->mmio_bus);

init_rwsem(&kvm->slots_lock);

atomic_set(&kvm->users_count, 1);

spin_lock(&kvm_lock);

       /*kvm链表加入总链表*/

list_add(&kvm->vm_list, &vm_list);

spin_unlock(&kvm_lock);

return kvm;

}

可以看到,这个函数首先是申请一个kvm结构。然后执行初始化工作。

初始化第一步是把kvmmm结构设置为当前进程的mm。我们知道,mm结构反应了整个进程的内存使用情况,也包括进程使用的页目录信息。

然后是初始化io buseventfd。这两者和设备io有关。

最后把kvm加入到一个全局链表头。通过这个链表头,可以遍历所有的vm虚拟机。

创建VM之后,就是创建VCPU

代码清单2-4  V

static int kvm_vm_ioctl_create_vcpu(struct kvm *kvm, u32 id)

{

int r;

struct kvm_vcpu *vcpu, *v;

       /*调用相关cpuvcpu_create*/

vcpu = kvm_arch_vcpu_create(kvm, id);

if (IS_ERR(vcpu))

return PTR_ERR(vcpu);

preempt_notifier_init(&vcpu->preempt_notifier, &kvm_preempt_ops);

       /*调用相关cpuvcpu_setup*/

r = kvm_arch_vcpu_setup(vcpu);

if (r)

return r;

       /*判断是否达到最大cpu个数*/

mutex_lock(&kvm->lock);

if (atomic_read(&kvm->online_vcpus) == KVM_MAX_VCPUS) {

r = -EINVAL;

goto vcpu_destroy;

}

       /*判断该vcpu是否已经存在*/

kvm_for_each_vcpu(r, v, kvm)

if (v->vcpu_id == id) {

r = -EEXIST;

goto vcpu_destroy;

}

       /*生成kvm-vcpu控制文件*/

/* Now it's all set up, let userspace reach it */

kvm_get_kvm(kvm);

r = create_vcpu_fd(vcpu);

if (r < 0) {

kvm_put_kvm(kvm);

goto vcpu_destroy;

}

kvm->vcpus[atomic_read(&kvm->online_vcpus)] = vcpu;

smp_wmb();

atomic_inc(&kvm->online_vcpus);

mutex_unlock(&kvm->lock);

return r;

vcpu_destroy:

mutex_unlock(&kvm->lock);

kvm_arch_vcpu_destroy(vcpu);

return r;

}

从代码可见,分别调用相关cpu提供的vcpu_createvcpu_setup来完成vcpu创建。

Intelvt技术和amdsvm技术所提供的vcpu调用各自不同。我们集中在intelvt技术,

而省略AMDSVM

代码清单2-5  vmx_create_vcpu

static struct kvm_vcpu *vmx_create_vcpu(struct kvm *kvm, unsigned int id)

{

int err;

       /*申请一个vmx结构*/

struct vcpu_vmx *vmx = kmem_cache_zalloc(kvm_vcpu_cache, GFP_KERNEL);

int cpu;

       .......................................

err = kvm_vcpu_init(&vmx->vcpu, kvm, id);

      /*申请guestmsrs,hostmsrs*/ 

vmx->guest_msrs = kmalloc(PAGE_SIZE, GFP_KERNEL);

vmx->host_msrs = kmalloc(PAGE_SIZE, GFP_KERNEL);

       /*申请一个vmcs结构*/

vmx->vmcs = alloc_vmcs();

vmcs_clear(vmx->vmcs);

cpu = get_cpu();

vmx_vcpu_load(&vmx->vcpu, cpu);

       /*设置vcpu为实模式,设置各种寄存器*/

err = vmx_vcpu_setup(vmx);

vmx_vcpu_put(&vmx->vcpu);

put_cpu();

if (vm_need_virtualize_apic_accesses(kvm))

if (alloc_apic_access_page(kvm) != 0)

goto free_vmcs;

return &vmx->vcpu;

}

首先申请一个vcpu_vmx结构,然后初始化vcpu_vmx包含的mmu,仿真断芯片等等成员。

MSR寄存器是cpu模式寄存器,所以要分别为guest host申请页面,这个页面要保存MSR寄存器的信息。然后申请一个vmcs结构。然后调用vmx_vcpu_setup设置vcpu工作在实模式。

代码清单2-6  vmx_vcpu_setup

static int vmx_vcpu_setup(struct vcpu_vmx *vmx)

{u32 host_sysenter_cs, msr_low, msr_high;

u32 junk;

u64 host_pat, tsc_this, tsc_base;

unsigned long a;

struct descriptor_table dt;

int i;

unsigned long kvm_vmx_return;

u32 exec_control;

     

/* Control */

vmcs_write32(PIN_BASED_VM_EXEC_CONTROL,

vmcs_config.pin_based_exec_ctrl);

exec_control = vmcs_config.cpu_based_exec_ctrl;

/*如果不支持EPT,有条件退出指令要增加*/

if (!enable_ept)

exec_control |= CPU_BASED_CR3_STORE_EXITING |

CPU_BASED_CR3_LOAD_EXITING  |

CPU_BASED_INVLPG_EXITING;

vmcs_write32(CPU_BASED_VM_EXEC_CONTROL, exec_control);

if (cpu_has_secondary_exec_ctrls()) {

exec_control = vmcs_config.cpu_based_2nd_exec_ctrl;

if (!vm_need_virtualize_apic_accesses(vmx->vcpu.kvm))

exec_control &=

~SECONDARY_EXEC_VIRTUALIZE_APIC_ACCESSES;

if (vmx->vpid == 0)

exec_control &= ~SECONDARY_EXEC_ENABLE_VPID;

if (!enable_ept)

exec_control &= ~SECONDARY_EXEC_ENABLE_EPT;

if (!enable_unrestricted_guest)

exec_control &= ~SECONDARY_EXEC_UNRESTRICTED_GUEST;

vmcs_write32(SECONDARY_VM_EXEC_CONTROL, exec_control);

}

vmcs_write32(PAGE_FAULT_ERROR_CODE_MASK, !!bypass_guest_pf);

vmcs_write32(PAGE_FAULT_ERROR_CODE_MATCH, !!bypass_guest_pf);

vmcs_write32(CR3_TARGET_COUNT, 0);           /* 22.2.1 */

vmcs_writel(HOST_CR0, read_cr0());  /* 22.2.3 */

vmcs_writel(HOST_CR4, read_cr4());  /* 22.2.3, 22.2.5 */

vmcs_writel(HOST_CR3, read_cr3());  /* 22.2.3  FIXME: shadow tables */

vmcs_write16(HOST_CS_SELECTOR, __KERNEL_CS);  /* 22.2.4 */

vmcs_write16(HOST_DS_SELECTOR, __KERNEL_DS);  /* 22.2.4 */

vmcs_write16(HOST_ES_SELECTOR, __KERNEL_DS);  /* 22.2.4 */

vmcs_write16(HOST_FS_SELECTOR, kvm_read_fs());    /* 22.2.4 */

vmcs_write16(HOST_GS_SELECTOR, kvm_read_gs());    /* 22.2.4 */

vmcs_write16(HOST_SS_SELECTOR, __KERNEL_DS);  /* 22.2.4 */

vmcs_writel(HOST_FS_BASE, 0); /* 22.2.4 */

vmcs_writel(HOST_GS_BASE, 0); /* 22.2.4 */

vmcs_write16(HOST_TR_SELECTOR, GDT_ENTRY_TSS*8);  /* 22.2.4 */

kvm_get_idt(&dt);

vmcs_writel(HOST_IDTR_BASE, dt.base);   /* 22.2.4 */

asm("mov $.Lkvm_vmx_return, %0" : "=r"(kvm_vmx_return));

vmcs_writel(HOST_RIP, kvm_vmx_return); /* 22.2.5 */

vmcs_write32(VM_EXIT_MSR_STORE_COUNT, 0);

vmcs_write32(VM_EXIT_MSR_LOAD_COUNT, 0);

vmcs_write32(VM_ENTRY_MSR_LOAD_COUNT, 0);

rdmsr(MSR_IA32_SYSENTER_CS, host_sysenter_cs, junk);

vmcs_write32(HOST_IA32_SYSENTER_CS, host_sysenter_cs);

rdmsrl(MSR_IA32_SYSENTER_ESP, a);

vmcs_writel(HOST_IA32_SYSENTER_ESP, a);   /* 22.2.3 */

rdmsrl(MSR_IA32_SYSENTER_EIP, a);

vmcs_writel(HOST_IA32_SYSENTER_EIP, a);   /* 22.2.3 */

if (vmcs_config.vmexit_ctrl & VM_EXIT_LOAD_IA32_PAT) {

rdmsr(MSR_IA32_CR_PAT, msr_low, msr_high);

host_pat = msr_low | ((u64) msr_high << 32);

vmcs_write64(HOST_IA32_PAT, host_pat);

}

if (vmcs_config.vmentry_ctrl & VM_ENTRY_LOAD_IA32_PAT) {

rdmsr(MSR_IA32_CR_PAT, msr_low, msr_high);

host_pat = msr_low | ((u64) msr_high << 32);

/* Write the default value follow host pat */

vmcs_write64(GUEST_IA32_PAT, host_pat);

/* Keep arch.pat sync with GUEST_IA32_PAT */

vmx->vcpu.arch.pat = host_pat;

}

       /*保存hostMSR*/

for (i = 0; i < NR_VMX_MSR; ++i) {

u32 index = vmx_msr_index[i];

u32 data_low, data_high;

u64 data;

int j = vmx->nmsrs;

if (rdmsr_safe(index, &data_low, &data_high) < 0)

continue;

if (wrmsr_safe(index, data_low, data_high) < 0)

continue;

data = data_low | ((u64)data_high << 32);

vmx->host_msrs[j].index = index;

vmx->host_msrs[j].reserved = 0;

vmx->host_msrs[j].data = data;

vmx->guest_msrs[j] = vmx->host_msrs[j];

++vmx->nmsrs;

}

vmcs_write32(VM_EXIT_CONTROLS, vmcs_config.vmexit_ctrl);

/* 22.2.1, 20.8.1 */

vmcs_write32(VM_ENTRY_CONTROLS, vmcs_config.vmentry_ctrl);

vmcs_writel(CR0_GUEST_HOST_MASK, ~0UL);

vmcs_writel(CR4_GUEST_HOST_MASK, KVM_GUEST_CR4_MASK);

tsc_base = vmx->vcpu.kvm->arch.vm_init_tsc;

rdtscll(tsc_this);

if (tsc_this < vmx->vcpu.kvm->arch.vm_init_tsc)

tsc_base = tsc_this;

guest_write_tsc(0, tsc_base);

return 0;

}

这个函数要写一堆的寄存器和控制信息,信息很多。所以只重点分析其中的几个地方:

cpu不支持EPT扩展技术时候,有条件退出vm的指令要增加。这些指令是cr3 storecr3 load,要把这个新内容写入cpu_based控制里面。(cpu_based控制是vmcs结构的一部分)。

然后是写cr0,cr3寄存器以及csds以及es等段选择寄存器。

之后,要保存hostMSR寄存器的值到前面分配的guest_msrs页面。

2.4.3 Vcpu运行

推动vcpu运行,让虚拟机开始运行,主要在__vcpu_run函数执行。

代码清单2-7  V

static int __vcpu_run(struct kvm_vcpu *vcpu, struct kvm_run *kvm_run)

{

int r;

       ..................................

down_read(&vcpu->kvm->slots_lock);

vapic_enter(vcpu);

r = 1;

while (r > 0) {

                 /*vcpu进入guest模式*/ 

if (vcpu->arch.mp_state == KVM_MP_STATE_RUNNABLE)

r = vcpu_enter_guest(vcpu, kvm_run);

else {

up_read(&vcpu->kvm->slots_lock);

kvm_vcpu_block(vcpu);

down_read(&vcpu->kvm->slots_lock);

if (test_and_clear_bit(KVM_REQ_UNHALT, &vcpu->requests))

{

switch(vcpu->arch.mp_state) {

case KVM_MP_STATE_HALTED:

vcpu->arch.mp_state =

KVM_MP_STATE_RUNNABLE;

case KVM_MP_STATE_RUNNABLE:

break;

case KVM_MP_STATE_SIPI_RECEIVED:

default:

r = -EINTR;

break;

}

}

}

                ..............................

                clear_bit(KVM_REQ_PENDING_TIMER, &vcpu->requests);

                /*检查是否有阻塞的时钟timer*/

if (kvm_cpu_has_pending_timer(vcpu))

kvm_inject_pending_timer_irqs(vcpu);

                 /*检查是否有用户空间的中断注入*/ 

if (dm_request_for_irq_injection(vcpu, kvm_run)) {

r = -EINTR;

kvm_run->exit_reason = KVM_EXIT_INTR;

++vcpu->stat.request_irq_exits;

}

                 /*是否有阻塞的signal*/

if (signal_pending(current)) {

r = -EINTR;

kvm_run->exit_reason = KVM_EXIT_INTR;

++vcpu->stat.signal_exits;

}

                 /*执行一个调度*/

if (need_resched()) {

up_read(&vcpu->kvm->slots_lock);

kvm_resched(vcpu);

down_read(&vcpu->kvm->slots_lock);

}

}

up_read(&vcpu->kvm->slots_lock);

post_kvm_run_save(vcpu, kvm_run);

vapic_exit(vcpu);

return r;

}

这里理解的关键是vcpu_enter_guest进入了Guest,然后一直是vcpu在运行,当退出这个函数的时候,虚拟机已经执行了VM-Exit指令,也就是说,已经退出了虚拟机,进入根模式了。

退出之后,要检查退出的原因。如果有时钟中断发生,则插入一个时钟中断,如果是用户空间的中断发生,则退出原因要填写为KVM_EXIT_INTR

注意一点的是,对于导致退出的事件,vcpu_enter_guest函数里面已经处理了一部分,处理的是虚拟机本身运行导致退出的事件。比如虚拟机内部写磁盘导致退出,就在vcpu_enter_guest里面处理(只是写了退出的原因,并没有真正处理)。Kvm是如何知道退出的原因的?这个就是vmcs结构的作用了,vmcs结构里面有VM-Exit的信息。

退出VM之后,如果内核没有完成处理,那么要退出内核到QEMU进程。然后是QEMU进程要处理。后面io处理一节可以看到QEMU的处理过程。

代码清单2-8  vcpu_enter_guest

static int vcpu_enter_guest(struct kvm_vcpu *vcpu, struct kvm_run *kvm_run)

{

int r;

bool req_int_win = !irqchip_in_kernel(vcpu->kvm) &&

kvm_run->request_interrupt_window;

       /*装载mmu*/

r = kvm_mmu_reload(vcpu);

kvm_x86_ops->prepare_guest_switch(vcpu);

kvm_load_guest_fpu(vcpu);

       /*注入阻塞的事件,中断,异常和nmi*/

inject_pending_event(vcpu, kvm_run);

if (kvm_lapic_enabled(vcpu)) {

update_cr8_intercept(vcpu);

kvm_lapic_sync_to_vapic(vcpu);

}

       /*计算进入guest的时间*/

kvm_guest_enter();

kvm_x86_ops->run(vcpu, kvm_run);

/*

 * We must have an instruction between local_irq_enable() and

 * kvm_guest_exit(), so the timer interrupt isn't delayed by

 * the interrupt shadow.  The stat.exits increment will do nicely.

 * But we need to prevent reordering, hence this barrier():

 */

       /*计算退出的时间*/

kvm_guest_exit();

       ................................/*退出之前,设置各种参数*/

r = kvm_x86_ops->handle_exit(kvm_run, vcpu);

out:

return r;

}

首先要装载mmu,然后注入事件,像中断,异常什么的。然后调用cpu架构相关的run函数,这个函数里面有一堆汇编写的语句,用来进入虚拟机以及指定从虚拟机退出的执行地址。最后调用cpuhandle_exit,用来从vmcs读取退出的信息。

将注入中断的函数简化一下。

代码清单2-9  V

static void vmx_inject_irq(struct kvm_vcpu *vcpu)

{

int irq = vcpu->arch.interrupt.nr;

..........................

intr = irq | INTR_INFO_VALID_MASK;

       ...............................

vmcs_write32(VM_ENTRY_INTR_INFO_FIELD, intr);

}

可以看到,实际上注入中断就是写vmcs里面的VM_ENTRY_INTR_INFO_FIELD这个域。然后在cpurun函数里面设置cpu进入非根模式,vcpu会自动检查vmcs结构,然后注入中断,这是硬件自动完成的工作。而处理中断,就是Guest os内核所完成的工作了。

2.4.4 调度

kvm只是个内核模块,虚拟机实际上是运行在QEMU的进程上下文中。所以vcpu的调度实际上直接使用了linux自身的调度机制。也就是linux自身的进程调度机制。

QEMU可以设置每个vcpu都运作在一个线程中。

代码清单2-10  qemu_kvm_start_vcpu

static void qemu_kvm_start_vcpu(CPUState *env)

{

    env->thread = qemu_mallocz(sizeof(QemuThread));

    env->halt_cond = qemu_mallocz(sizeof(QemuCond));

    qemu_cond_init(env->halt_cond);

    qemu_thread_create(env->thread, qemu_kvm_cpu_thread_fn, env);

    .................................................

}

Qemu的代码,看到Qemu启动了一个kvm_cpu_thread线程。这个线程是循环调用

kvm_cpu_exec函数。

代码清单2-11  kvm_cpu_exec

int kvm_cpu_exec(CPUState *env)

{

    struct kvm_run *run = env->kvm_run;

    int ret, run_ret;

    do {

        ...............................

        run_ret = kvm_vcpu_ioctl(env, KVM_RUN, 0);

       ......................................

       /*处理退出的事件*/

        switch (run->exit_reason) {

        case KVM_EXIT_IO:

            DPRINTF("handle_io\n");

            kvm_handle_io(run->io.port,

                          (uint8_t *)run + run->io.data_offset,

                          run->io.direction,

                          run->io.size,

                          run->io.count);

            ret = 0;

            break;

        case KVM_EXIT_MMIO:

            DPRINTF("handle_mmio\n");

            cpu_physical_memory_rw(run->mmio.phys_addr,

                                   run->mmio.data,

                                   run->mmio.len,

                                   run->mmio.is_write);

            ret = 0;

            break;

        case KVM_EXIT_IRQ_WINDOW_OPEN:

            DPRINTF("irq_window_open\n");

            ret = EXCP_INTERRUPT;

            break;

        case KVM_EXIT_SHUTDOWN:

            DPRINTF("shutdown\n");

            qemu_system_reset_request();

            ret = EXCP_INTERRUPT;

            break;

        case KVM_EXIT_UNKNOWN:

            fprintf(stderr, "KVM: unknown exit, hardware reason %" PRIx64 "\n",

                    (uint64_t)run->hw.hardware_exit_reason);

            ret = -1;

            break;

        case KVM_EXIT_INTERNAL_ERROR:

            ret = kvm_handle_internal_error(env, run);

            break;

        default:

            DPRINTF("kvm_arch_handle_exit\n");

            ret = kvm_arch_handle_exit(env, run);

            break;

        }

    } while (ret == 0);

   ..............................

    env->exit_request = 0;

    cpu_single_env = NULL;

    return ret;

}

这个函数就是调用了前面分析过的KVM_RUN。回顾一下前面的分析,KVM_RUN就进入了虚拟机,如果从虚拟化退出到这里,那么Qemu要处理退出的事件。这些事件,可能是因为io引起的KVM_EXIT_IO,也可能是内部错误引起的KVM_EXIT_INTERNAL_ERROR。如果事件没有被完善处理,那么要停止虚拟机。

2.4.5 中断

如何向vcpu注入中断?是通过向VMCS表写入中断数据来实现

在真实的物理环境,中断是由中断控制芯片来触发的,虚拟化的kvm环境就必须通过软件模拟一个中断控制芯片,这个是通过KVM_CREATE_IRQCHIP来实现的。

然后,如果Qemu想注入一个中断,就通过KVM_IRQ_LINE实现。这个所谓中断控制芯片只是在内存中存在的结构,kvm通过软件方式模拟了中断的机制。

KVM_CREATE_IRQCHIP实际上调用了kvm_create_pic这个函数。

代码清单2-12  kvm_create_pic

struct kvm_pic *kvm_create_pic(struct kvm *kvm)

{

struct kvm_pic *s;

int ret;

s = kzalloc(sizeof(struct kvm_pic), GFP_KERNEL);

if (!s)

return NULL;

spin_lock_init(&s->lock);

s->kvm = kvm;

s->pics[0].elcr_mask = 0xf8;

s->pics[1].elcr_mask = 0xde;

s->irq_request = pic_irq_request;

s->irq_request_opaque = kvm;

s->pics[0].pics_state = s;

s->pics[1].pics_state = s;

/*

 * Initialize PIO device

 */

kvm_iodevice_init(&s->dev, &picdev_ops);

ret = kvm_io_bus_register_dev(kvm, &kvm->pio_bus, &s->dev);

if (ret < 0) {

kfree(s);

return NULL;

}

return s;

}

可以看到,这个函数很简单,其实就是申请了一个kvm_pic的结构。然后指定irq_request指针为pic_irq_request

KVM_IRQ_LINE实际上调用的是kvm_set_irq,分析一下它是如何注入中断的。

代码清单2-13  kvm_set_irq

int kvm_set_irq(struct kvm *kvm, int irq_source_id, int irq, int level)

{

struct kvm_kernel_irq_routing_entry *e;

unsigned long *irq_state, sig_level;

int ret = -1;

...................................................

/* Not possible to detect if the guest uses the PIC or the

 * IOAPIC.  So set the bit in both. The guest will ignore

 * writes to the unused one.

 */

list_for_each_entry(e, &kvm->irq_routing, link)

if (e->gsi == irq) {

int r = e->set(e, kvm, sig_level);

if (r < 0)

continue;

ret = r + ((ret < 0) ? 0 : ret);

}

return ret;

}

从英文解释可以看到,因为不可能判断Guest使用的是PIC还是APIC,所以为每一个中断路由都设置中断。

这里解释一下,PIC就是传统的中断控制器8259x86体系最初使用的中断控制器。后来,又推出了APIC,也就是高级中断控制器。APIC为多核架构做了更多设计。

这里的这个set函数,其实就是kvm_pic_set_irq

代码清单2-14  V

int kvm_pic_set_irq(void *opaque, int irq, int level)

{     struct kvm_pic *s = opaque;

      ............................

if (irq >= 0 && irq < PIC_NUM_PINS) {

ret = pic_set_irq1(&s->pics[irq >> 3], irq & 7, level);

pic_update_irq(s);

}

  ............................................

}

可以看到,前面申请的kvm_pic结构作为参数被引入。然后设置irq到这个结构的pic成员。

代码清单2-15  pic_update_irq

static void pic_update_irq(struct kvm_pic *s)

{

int irq2, irq;

irq2 = pic_get_irq(&s->pics[1]);

if (irq2 >= 0) {

/*

 * if irq request by slave pic, signal master PIC

 */

pic_set_irq1(&s->pics[0], 2, 1);

pic_set_irq1(&s->pics[0], 2, 0);

}

irq = pic_get_irq(&s->pics[0]);

if (irq >= 0)

s->irq_request(s->irq_request_opaque, 1);

else

s->irq_request(s->irq_request_opaque, 0);

}

此时调用irq_request,就是初始化中断芯片时候绑定的函数pic_irq_request

代码清单2-16  pic_irq_request

static void pic_irq_request(void *opaque, int level)

{

struct kvm *kvm = opaque;

struct kvm_vcpu *vcpu = kvm->bsp_vcpu;

struct kvm_pic *s = pic_irqchip(kvm);

int irq = pic_get_irq(&s->pics[0]);

       /*设置中断*/ 

s->output = level;

       if (vcpu && level && (s->pics[0].isr_ack & (1 << irq))) {

s->pics[0].isr_ack &= ~(1 << irq);

kvm_vcpu_kick(vcpu);

}

}

这个函数很简单,就是设置中断控制芯片的output,然后调用kvm_vcpu_kick

kvm_vcpu_kick这个地方很容易混淆。

VM-exit退出后,就接上了前文分析过的部分。Vcpu再次进入虚拟机的时候,通过inject_pengding_event检查中断。这里面就查出来通过KVM_IRQ_LINE注入的中断,然后后面就是写vmcs结构了,已经分析过了。

2.5  vcpu的内存虚拟化

在kmv初始化的时候,要检查是否支持vt里面的EPT扩展技术。如果支持,enable_ept这个变量置为1,然后设置tdp_enabled1Tdp就是两维页表的意思,也就是EPT技术。

为陈述方便,给出kvm中下列名字的定义:

q GPAguest机物理地址

q GVAguest机虚拟地址

q HVAhost机虚拟地址

q HPAhost机物理地址

2.5.1 虚拟机页表初始化

vcpu初始化的时候,要调用init_kvm_mmu来设置不同的内存虚拟化方式。 

代码清单2-17  init_kvm_mmu

static int init_kvm_mmu(struct kvm_vcpu *vcpu)

{

vcpu->arch.update_pte.pfn = bad_pfn;

if (tdp_enabled)

return init_kvm_tdp_mmu(vcpu);

else

return init_kvm_softmmu(vcpu);

}

设置两种方式,一种是支持EPT的方式,一种是soft mmu,也就是影子页表的方式。

代码清单2-18  V

static int init_kvm_softmmu(struct kvm_vcpu *vcpu)

{

int r;

       /*无分页模式的设置*/ 

if (!is_paging(vcpu))

r = nonpaging_init_context(vcpu);

else if (is_long_mode(vcpu)) /*64cpu的设置*/

r = paging64_init_context(vcpu);

else if (is_pae(vcpu))/*32cpu的设置*/

r = paging32E_init_context(vcpu);

else

r = paging32_init_context(vcpu);

vcpu->arch.mmu.base_role.glevels = vcpu->arch.mmu.root_level;

return r;

}

这个函数为多种模式的cpu设置了不同的虚拟化处理函数。选择32位非PAE模式的cpu进行分析。

代码清单2-19  V

static int paging32_init_context(struct kvm_vcpu *vcpu)

{

struct kvm_mmu *context = &vcpu->arch.mmu;

reset_rsvds_bits_mask(vcpu, PT32_ROOT_LEVEL);

context->new_cr3 = paging_new_cr3;

context->page_fault = paging32_page_fault;

context->gva_to_gpa = paging32_gva_to_gpa;

context->free = paging_free;

context->prefetch_page = paging32_prefetch_page;

context->sync_page = paging32_sync_page;

context->invlpg = paging32_invlpg;

context->root_level = PT32_ROOT_LEVEL;

context->shadow_root_level = PT32E_ROOT_LEVEL;

       /*页表根地址设为无效*/

context->root_hpa = INVALID_PAGE;

return 0;

}

这个函数要设置一堆函数指针。其中paging32_page_fault等函数直接找是找不到的。这是内核代码经常用的一个技巧(好像别的代码很少见到这种用法)。真正定义在paging_tmpl.h这个文件。通过FNAME这个宏根据不同的cpu平台定义了各自的函数。比如paging32_page_fault实际上就是FNAME(page_fault)这个函数。

我们知道,linux为不同的cpu提供不同的页表层级。64cpu使用了四级页表。这里指定页表是两级,也就是PT32_ROOT_LEVEL,同时设定页表根地址为无效。此时页表尚未分配。

何时去分配vcpu的页表哪?是在vcpu_enter_guest的开始位置,通过调用kvm_mmu_reload实现。

代码清单2-20  kvm_mmu_reload

static inline int kvm_mmu_reload(struct kvm_vcpu *vcpu)

{      /*页表根地址不是无效的,则退出,不用分配。*/

if (likely(vcpu->arch.mmu.root_hpa != INVALID_PAGE))

return 0;

return kvm_mmu_load(vcpu);

}

首先检查页表根地址是否无效,如果无效,则调用kvm_mmu_load

代码清单2-21  V

int kvm_mmu_load(struct kvm_vcpu *vcpu)

{

int r;

r = mmu_alloc_roots(vcpu);

       /*同步页表*/

mmu_sync_roots(vcpu);

/* set_cr3() should ensure TLB has been flushed */

kvm_x86_ops->set_cr3(vcpu, vcpu->arch.mmu.root_hpa);

       ....................

}

mmu_alloc_roots这个函数要申请内存,作为根页表使用,同时root_hpa指向根页表的物理地址。然后可以看到,vcpucr3寄存器的地址要指向这个根页表的物理地址。

2.5.2 虚拟机物理地址

我们已经分析过,kvm的虚拟机实际上运行在Qemu的进程上下文中。于是,虚拟机的物理内存实际上是Qemu进程的虚拟地址。Kvm要把虚拟机的物理内存分成几个slot。这是因为,对计算机系统来说,物理地址是不连续的,除了bios和显存要编入内存地址,设备的内存也可能映射到内存了,所以内存实际上是分为一段段的。

Qemu通过KVM_SET_USER_MEMORY_REGION来为虚拟机设置内存。

代码清单2-22  kvm_set_memory_region

int __kvm_set_memory_region(struct kvm *kvm,

    struct kvm_userspace_memory_region *mem,

    int user_alloc)

{

int r;

gfn_t base_gfn;

unsigned long npages;

unsigned long i;

struct kvm_memory_slot *memslot;

struct kvm_memory_slot old, new;

r = -EINVAL;

      /*找到现在的memslot*/

memslot = &kvm->memslots[mem->slot];

base_gfn = mem->guest_phys_addr >> PAGE_SHIFT;

npages = mem->memory_size >> PAGE_SHIFT;

new = old = *memslot;

       /*new是新的slots,old保持老的数值不变*/

new.base_gfn = base_gfn;

new.npages = npages;

new.flags = mem->flags;

new.user_alloc = user_alloc;

       /*用户已经分配了内存,slot的用户空间地址就等于用户分配的地址*/

if (user_alloc)

new.userspace_addr = mem->userspace_addr;

spin_lock(&kvm->mmu_lock);

if (mem->slot >= kvm->nmemslots)

kvm->nmemslots = mem->slot + 1;

*memslot = new;

spin_unlock(&kvm->mmu_lock);

kvm_free_physmem_slot(&old, npages ? &new : NULL);

return 0;

}

这个函数大幅简化了。看代码时候,要注意对内存地址页的检查和内存overlap的检查部分。经过简化之后,代码很清晰了。就是创建一个新的memslot,代替原来的memslot。一个内存slot,最重要部分是指定了vm的物理地址,同时指定了Qemu分配的用户地址,前面一个地址是GPA,后面一个地址是HVA。可见,一个memslot就是建立了GPAHVA的映射关系。

2.5.3 内存虚拟化过程

这里,有必要描述一下内存虚拟化的过程:

VM要访问GVA 0,那么首先查询VM的页表得到PTE(页表项),通过PTEGVA 0映射到物理地址GPA 0.

GPA 0此时不存在,发生页缺失。

KVM接管。

memslot,可以知道GPA对应的其实是HVA x,然后从HVA x,可以查找得到HPA y,然后将HPA y这个映射写入到PTE

VM再次存取GVA 0,这是从页表项已经可以查到HPA y了,内存可正常访问。

首先,从page_fault处理开始。从前文的分析,知道VM里面的异常产生VM-Exit,然后由各自cpu提供的处理函数处理。对intelvt技术,就是handle_exception这个函数。

代码清单2-23  V

static int handle_exception(struct kvm_vcpu *vcpu, struct kvm_run *kvm_run)

{

       /*vmcs,获得VM-exit的信息*/

intr_info = vmcs_read32(VM_EXIT_INTR_INFO);

       /*发现是page_fault引起*/

if (is_page_fault(intr_info)) {

/* EPT won't cause page fault directly */

                 /*如果支持EPT,不会因为page_fault退出,所以是bug*/

if (enable_ept)

BUG();

                 /*cr2寄存器的值*/ 

cr2 = vmcs_readl(EXIT_QUALIFICATION);

trace_kvm_page_fault(cr2, error_code);

if (kvm_event_needs_reinjection(vcpu))

kvm_mmu_unprotect_page_virt(vcpu, cr2);

return kvm_mmu_page_fault(vcpu, cr2, error_code);

}

return 0;

}

从这个函数,可以看到对vmcs的使用。通过读vmcs的域,可以获得退出vm的原因。如果是page_fault引起,则调用kvm_mmu_page_fault去处理。

代码清单2-24  kvm_mmu_page_fault

int kvm_mmu_page_fault(struct kvm_vcpu *vcpu, gva_t cr2, u32 error_code)

{

int r;

enum emulation_result er;

       /*调用mmupage_fault*/

r = vcpu->arch.mmu.page_fault(vcpu, cr2, error_code);

if (r < 0)

goto out;

if (!r) {

r = 1;

goto out;

}

       /*模拟指令*/

er = emulate_instruction(vcpu, vcpu->run, cr2, error_code, 0);

      ..................................

}

这里调用了MMUpage_fault处理函数。这个函数就是前面初始化时候设置的paging32_page_fault。也就是通过FNAME宏展开的FNAME(page_fault)

代码清单2-25  page_fault

static int FNAME(page_fault)(struct kvm_vcpu *vcpu, gva_t addr,

       u32 error_code)

{

/*guest页表,物理地址是否存在 */

r = FNAME(walk_addr)(&walker, vcpu, addr, write_fault, user_fault,

     fetch_fault);

/*页还没映射,交Guest OS处理 */

if (!r) {

pgprintk("%s: guest page fault\n", __func__);

inject_page_fault(vcpu, addr, walker.error_code);

vcpu->arch.last_pt_write_count = 0; /* reset fork detector */

return 0;

}

if (walker.level >= PT_DIRECTORY_LEVEL) {

level = min(walker.level, mapping_level(vcpu, walker.gfn));

walker.gfn = walker.gfn & ~(KVM_PAGES_PER_HPAGE(level) - 1);

}

       /*通过gfnpfn*/

pfn = gfn_to_pfn(vcpu->kvm, walker.gfn);

/* mmio ,如果是mmio,是io访问,不是内存,返回*/

if (is_error_pfn(pfn)) {

pgprintk("gfn %lx is mmio\n", walker.gfn);

kvm_release_pfn_clean(pfn);

return 1;

}

       /*写入HVA到页表*/

sptep = FNAME(fetch)(vcpu, addr, &walker, user_fault, write_fault,

     level, &write_pt, pfn);

 .............................

}

对照前面的分析,比较容易理解这个函数了。首先是查guest机的页表,如果从GVAGPA的映射都没建立,那么返回,让Guest OS做这个工作。

然后,如果映射已经建立,GPA存在,那么从Guest的页面号,查找Host的页面号。如何执行这个查找?从memslot可以知道user space首地址,就可以把物理地址GPA转为HVA,通过HVA就可以查到HPA,然后找到所在页的页号。

最后,写HVA到页表里面。页表在那里?回顾一下前面kvm_mmu_load的过程,页表是host申请的。通过页表搜索,就可以找到要写入的页表项。

2.6   IO虚拟化

IO虚拟化有两种方案,一种是半虚拟化方案,一种是全虚拟化方案。全虚拟化方案不需要该Guest的代码,那么Guest里面的io操作最终都变成io指令。在前面的分析中,其实已经涉及了io虚拟化的流程。在VM-exit的时候,前文分析过page fault导致的退出。那么io指令,同样会导致VM-exit退出,然后kvm会把io交给Qemu进程处理。

而半虚拟化方案,基本都是把io变成了消息处理,从guest机器发消息出来,然后由host机器处理。此时,在guest机器的驱动都被接管,已经不能被称为驱动(因为已经不再处理io指令,不和具体设备打交道),称为消息代理更合适。

2.6.1 Vmmio的处理

当guest因为执行io执行退出后,由handle_io函数处理。

代码清单2-26  V

static int handle_io(struct kvm_vcpu *vcpu, struct kvm_run *kvm_run)

{

++vcpu->stat.io_exits;

exit_qualification = vmcs_readl(EXIT_QUALIFICATION);

       ...................................

size = (exit_qualification & 7) + 1;

in = (exit_qualification & 8) != 0;

port = exit_qualification >> 16;

      .................................................

 return kvm_emulate_pio(vcpu, kvm_run, in, size, port);

}

要从vmcs读退出的信息,然后调用kvm_emulate_pio处理。

代码清单2-27  V

int kvm_emulate_pio(struct kvm_vcpu *vcpu, struct kvm_run *run, int in,

  int size, unsigned port)

{

       unsigned long val;

       /*要赋值退出的种种参数*/

vcpu->run->exit_reason = KVM_EXIT_IO;

vcpu->run->io.direction = in ? KVM_EXIT_IO_IN : KVM_EXIT_IO_OUT;

vcpu->run->io.size = vcpu->arch.pio.size = size;

vcpu->run->io.data_offset = KVM_PIO_PAGE_OFFSET * PAGE_SIZE;

vcpu->run->io.count = vcpu->arch.pio.count = vcpu->arch.pio.cur_count = 1;

vcpu->run->io.port = vcpu->arch.pio.port = port;

vcpu->arch.pio.in = in;

vcpu->arch.pio.string = 0;

vcpu->arch.pio.down = 0;

vcpu->arch.pio.rep = 0;

       .................................

       /*内核能不能处理?*/ 

if (!kernel_pio(vcpu, vcpu->arch.pio_data)) {

complete_pio(vcpu);

return 1;

}

return 0;

}

这里要为io处理赋值各种参数,然后看内核能否处理这个io,如果内核能处理,就不用Qemu进程处理,否则退出内核态,返回用户态。从前文的分析中,我们知道返回是到Qemu的线程上下文中。实际上就是kvm_handle_io这个函数里面。

2.6.2 虚拟化io流程

用户态的Qemu如何处理io指令?首先,每种设备都需要注册自己的io指令处理函数到Qemu

这是通过register_ioport_writeregister_ioport_read是实现的。

代码清单2-28  register_ioport_read

int register_ioport_read(pio_addr_t start, int length, int size,

                         IOPortReadFunc *func, void *opaque)

{

    int i, bsize;

    /*把处理函数写入ioport_read_table这个全局数据*/    

    for(i = start; i < start + length; i += size) {

        ioport_read_table[bsize][i] = func;

        if (ioport_opaque[i] != NULL && ioport_opaque[i] != opaque)

            hw_error("register_ioport_read: invalid opaque for address 0x%x",

                     i);

        ioport_opaque[i] = opaque;

    }

    return 0;

}

通过这个函数,实际上把io指令处理函数登记到一个全局的数组。每种支持的设备都登记在这个数组中。

再分析kvm_handle_io的流程。

代码清单2-29  V

static void kvm_handle_io(uint16_t port, void *data, int direction, int size,

                          uint32_t count)

{

    .............................

    for (i = 0; i < count; i++) {

        if (direction == KVM_EXIT_IO_IN) {

            switch (size) {

            case 1:

                stb_p(ptr, cpu_inb(port));

                break;

                 } 

        ptr += size;

    }

}

对于退出原因是KVM_EXIT_IO_IN的情况,调用cpu_inb处理。Cpu_inb是个封装函数,它的作用就是调用ioport_read.

代码清单2-30  ioport_read

static uint32_t ioport_read(int index, uint32_t address)

{

    static IOPortReadFunc * const default_func[3] = {

        default_ioport_readb,

        default_ioport_readw,

        default_ioport_readl

    };

    /*从全局数组读入处理函数*/

    IOPortReadFunc *func = ioport_read_table[index][address];

    if (!func)

        func = default_func[index];

    return func(ioport_opaque[address], address);

}

这里代码很清晰,就是从登记io指令函数的数组中读出处理函数,然后调用每种设备所登记的指令处理函数处理,完成io

各种设备都有自己的处理函数,所以Qemu需要支持各种不同的设备,Qemu的复杂性也体现在这里。

原创粉丝点击