设为首页 收藏本站
查看: 1021|回复: 0

[经验分享] 虚拟化原理之kvm

[复制链接]
累计签到:1 天
连续签到:1 天
发表于 2015-10-10 11:53:23 | 显示全部楼层 |阅读模式
2KVM 虚拟化
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(&quot;mov $.Lkvm_vmx_return, %0&quot; : &quot;=r&quot;(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&#20540;*/
  for (i = 0; i < NR_VMX_MSR; &#43;&#43;i) {
  u32 index = vmx_msr_index;
  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];
  &#43;&#43;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寄存器的&#20540;到前面分配的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;
  &#43;&#43;vcpu->stat.request_irq_exits;
  }
                   /*是否有阻塞的signal*/
  if (signal_pending(current)) {
  r = -EINTR;
  kvm_run->exit_reason = KVM_EXIT_INTR;
  &#43;&#43;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(&quot;handle_io\n&quot;);
              kvm_handle_io(run->io.port,
                            (uint8_t *)run &#43; run->io.data_offset,
                            run->io.direction,
                            run->io.size,
                            run->io.count);
              ret = 0;
              break;
          case KVM_EXIT_MMIO:
              DPRINTF(&quot;handle_mmio\n&quot;);
              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(&quot;irq_window_open\n&quot;);
              ret = EXCP_INTERRUPT;
              break;
          case KVM_EXIT_SHUTDOWN:
              DPRINTF(&quot;shutdown\n&quot;);
              qemu_system_reset_request();
              ret = EXCP_INTERRUPT;
              break;
          case KVM_EXIT_UNKNOWN:
              fprintf(stderr, &quot;KVM: unknown exit, hardware reason %&quot; PRIx64 &quot;\n&quot;,
                      (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(&quot;kvm_arch_handle_exit\n&quot;);
              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 &#43; ((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保持老的数&#20540;不变*/
  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 &#43; 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寄存器的&#20540;*/
  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(&quot;%s: guest page fault\n&quot;, __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(&quot;gfn %lx is mmio\n&quot;, 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)
  {
  &#43;&#43;vcpu->stat.io_exits;
  exit_qualification = vmcs_readl(EXIT_QUALIFICATION);
         ...................................
  size = (exit_qualification & 7) &#43; 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;
         /*要赋&#20540;退出的种种参数*/
  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处理赋&#20540;各种参数,然后看内核能否处理这个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 &#43; length; i &#43;= size) {
          ioport_read_table[bsize] = func;
          if (ioport_opaque != NULL && ioport_opaque != opaque)
              hw_error(&quot;register_ioport_read: invalid opaque for address 0x%x&quot;,
                       i);
          ioport_opaque = 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&#43;&#43;) {
          if (direction == KVM_EXIT_IO_IN) {
              switch (size) {
              case 1:
                  stb_p(ptr, cpu_inb(port));
                  break;
                   }
          ptr &#43;= 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的复杂性也体现在这里。


         版权声明:本文为博主原创文章,未经博主允许不得转载。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-125054-1-1.html 上篇帖子: Xen 和 KVM 的性能对比 下篇帖子: 虚拟化原理之kvm
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表