第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推出硬件辅助虚拟化之后,虚拟化方案全面向硬件辅助靠拢。而kvm,Lguest这些比较新的方案,则彻底不支持软件的方案,而把硬件辅助当作了设计的根基。
从软件架构上来说,kvm提供了两个内核模块,使用kvm的io_ctl接口可以管理vcpu和内存,为vcpu注入中断和提供时钟信号,而kvm本身没有提供设备的模拟。设备模拟需要应用层软件Qemu来实现。这种架构保证了kvm避免了繁琐的设备模拟和设备驱动部分(内核中80%以上的代码就是驱动部分)。
总结一下kvm软件的架构特点:
q Kvm本身只提供两个内核模块。Kvm实现了vcpu和内存的管理。
q Qemu控制逻辑,负责创建虚拟机,创建vcpu。
2.2 Kvm管理接口
Qemu和kvm关系很深,甚至可以认为双方本来是一个软件,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:代表一个虚拟机。通过vm的io_ctl接口,可以为虚拟机创建vcpu,设置内存区间,创建中断控制芯片,分配中断等等。
q vcpu:代表一个vcpu。通过vcpu的io_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插入的事件。MSR是cpu的模式寄存器,设置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,然后是创建一个文件,这个文件作用是提供对vm的io_ctl控制。
代码清单2-3 V
static struct kvm *kvm_create_vm(void)
{
struct kvm *kvm = kvm_arch_create_vm();
/*设置kvm的mm结构为当前进程的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结构。然后执行初始化工作。
初始化第一步是把kvm的mm结构设置为当前进程的mm。我们知道,mm结构反应了整个进程的内存使用情况,也包括进程使用的页目录信息。
然后是初始化io bus和eventfd。这两者和设备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;
/*调用相关cpu的vcpu_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);
/*调用相关cpu的vcpu_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_create和vcpu_setup来完成vcpu创建。
Intel的vt技术和amd的svm技术所提供的vcpu调用各自不同。我们集中在intel的vt技术,
而省略AMD的SVM。
代码清单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);
/*申请guest的msrs,host的msrs*/
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;
}
/*保存host的MSR值*/
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 store和cr3 load,要把这个新内容写入cpu_based控制里面。(cpu_based控制是vmcs结构的一部分)。
然后是写cr0,cr3寄存器以及cs,ds以及es等段选择寄存器。
之后,要保存host的MSR寄存器的值到前面分配的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函数,这个函数里面有一堆汇编写的语句,用来进入虚拟机以及指定从虚拟机退出的执行地址。最后调用cpu的handle_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这个域。然后在cpu的run函数里面设置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就是传统的中断控制器8259,x86体系最初使用的中断控制器。后来,又推出了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_enabled为1。Tdp就是两维页表的意思,也就是EPT技术。
为陈述方便,给出kvm中下列名字的定义:
q GPA:guest机物理地址
q GVA:guest机虚拟地址
q HVA:host机虚拟地址
q HPA:host机物理地址
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)) /*64位cpu的设置*/
r = paging64_init_context(vcpu);
else if (is_pae(vcpu))/*32位cpu的设置*/
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提供不同的页表层级。64位cpu使用了四级页表。这里指定页表是两级,也就是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指向根页表的物理地址。然后可以看到,vcpu中cr3寄存器的地址要指向这个根页表的物理地址。
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就是建立了GPA到HVA的映射关系。
2.5.3 内存虚拟化过程
这里,有必要描述一下内存虚拟化的过程:
VM要访问GVA 0,那么首先查询VM的页表得到PTE(页表项),通过PTE将GVA 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提供的处理函数处理。对intel的vt技术,就是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;
/*调用mmu的page_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);
..................................
}
这里调用了MMU的page_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);
}
/*通过gfn找pfn*/
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机的页表,如果从GVA到GPA的映射都没建立,那么返回,让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 Vmm对io的处理
当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_write和register_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的复杂性也体现在这里。