展示自己、证明自己
分类: C/C++
2014-07-07 10:50:36
Kvm利用了qemu的外设(irqchip在kmod中有实现,为了提高中断处理的效率),所以,在kmod中,如果guest发生了io事件,我们需要返回到qemu中来处理。具体的处理流程如下:
Guest执行io指令 -> 发生vmexit-> 返回qemu -> 处理io
下面,我们将具体的结合代码来分析(在使用vmx的情况下)。
首先,我们从发生vmexit之后开始:
vmx.c中的handle_io :
static int handle_io(struct kvm_vcpu *vcpu) { unsigned long exit_qualification; int size, in, string; unsigned port; exit_qualification = vmcs_readl(EXIT_QUALIFICATION); //获取exit qualification string = (exit_qualification & 16) != 0; //判断是否为string io (ins, outs) in = (exit_qualification & 8) != 0; //判断io方向,是in 还是out ++vcpu->stat.io_exits; if (string || in) //如果是输入类的指令,或者是string io,就进入emulator处理 return emulate_instruction(vcpu, 0) == EMULATE_DONE; port = exit_qualification >> 16; //得到端口号 size = (exit_qualification & 7) + 1; //大小 skip_emulated_instruction(vcpu); //跳过这个指令 return kvm_fast_pio_out(vcpu, size, port); //进行out操作 }
这里我们可以看到,根据io的方向,以及是否为string io,处理方式是不同的。下面我们先看非string类型的 out操作是如何处理的:
X86.c:
int kvm_fast_pio_out(struct kvm_vcpu *vcpu, int size, unsigned short port) { unsigned long val = kvm_register_read(vcpu, VCPU_REGS_RAX); //获得rax的值 int ret = emulator_pio_out_emulated(&vcpu->arch.emulate_ctxt, size, port, &val, 1); //进入emulator /* do not return to emulator after return from userspace */ vcpu->arch.pio.count = 0; //因为非string类型的out操作一步就能完成,所以,从qemu进入kmod的时候不用再进入emulator了 return ret; } static int emulator_pio_out_emulated(struct x86_emulate_ctxt *ctxt, int size, unsigned short port, const void *val, unsigned int count) { struct kvm_vcpu *vcpu = emul_to_vcpu(ctxt); //根据emulator的上下文获取vcpu memcpy(vcpu->arch.pio_data, val, size * count); //将io的数据复制到kmod和qemu共享的buffer中 return emulator_pio_in_out(vcpu, size, port, (void *)val, count, false); } static int emulator_pio_in_out(struct kvm_vcpu *vcpu, int size, unsigned short port, void *val, unsigned int count, bool in) { trace_kvm_pio(!in, port, size, count); vcpu->arch.pio.port = port; vcpu->arch.pio.in = in; vcpu->arch.pio.count = count; vcpu->arch.pio.size = size; if (!kernel_pio(vcpu, vcpu->arch.pio_data)) { //如果在kmod中能完成io的话,就完成处理,不需要再返回qemu了 vcpu->arch.pio.count = 0; return 1; } //将相应的数据保存到kvm_run中。 vcpu->run->exit_reason = KVM_EXIT_IO; vcpu->run->io.direction = in ? KVM_EXIT_IO_IN : KVM_EXIT_IO_OUT; vcpu->run->io.size = size; vcpu->run->io.data_offset = KVM_PIO_PAGE_OFFSET * PAGE_SIZE; vcpu->run->io.count = count; vcpu->run->io.port = port; //返回qemu中进行处理。 return 0; }
返回qemu之后,在qemu的 kvm_all.c中:
int kvm_cpu_exec(CPUArchState *env) { struct kvm_run *run = env->kvm_run; int ret, run_ret; … … switch (run->exit_reason) { //根据kvm_run中存放的退出原因来选择处理方式 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); //这里可以看到,qemu会根据kvm_run中存放的数据来进行处理了 ret = 0; break; … … } static void kvm_handle_io(uint16_t port, void *data, int direction, int size, uint32_t count) { //在这个函数中,我们就可以看到,最终 是调用了cpu_inb、cpu_outb这些函数和具体的设备进行交互。 int i; uint8_t *ptr = data; for (i = 0; i < count; i++) { if (direction == KVM_EXIT_IO_IN) { switch (size) { case 1: stb_p(ptr, cpu_inb(port)); break; case 2: stw_p(ptr, cpu_inw(port)); break; case 4: stl_p(ptr, cpu_inl(port)); break; } } else { switch (size) { case 1: cpu_outb(port, ldub_p(ptr)); break; case 2: cpu_outw(port, lduw_p(ptr)); break; case 4: cpu_outl(port, ldl_p(ptr)); break; } } ptr += size; } }
这样,kvm就完成了一次对guest执行out指令的虚拟。
在处理string io和in的时候,会有所不同。从上述流程可以看到,如果虚拟单个out指令,在kmod中可以直接把out的数据返回给qemu,qemu完成out操 作,但是,如果是in指令,qemu只能把得到的数据写到kvm_run中,kmod必须在下一次vmentry的时候,将qemu得到的数据放到相应的 位置,所以,在handle_io中,如果是in或者string指令,没有调用skip_emulated_instruction,这样,在qemu 完成in或者一次out之后,还会在同样的地方发生vmexit,这样再由emulator完成相应的处理,针对string类型的指 令,emulator会进行解码等操作,确认io的次数和源操作数、目的操作数等。
下面,我们再来看一下string io,和in指令是如何虚拟的:
调用 emulate_instruction时,针对x86架构,该函数调用了x86_emulate_instruction:
Asm-x86/kvm_host.h
static inline int emulate_instruction(struct kvm_vcpu *vcpu, int emulation_type) { return x86_emulate_instruction(vcpu, 0, emulation_type, NULL, 0); }
X86.c:
int x86_emulate_instruction(struct kvm_vcpu *vcpu, unsigned long cr2, int emulation_type, void *insn, int insn_len) { int r; … … //这个函数做了对指令的decode等等过程,在这里省略,在过程中会调用到em_in和em_out … … if (ctxt->have_exception) { inject_emulated_exception(vcpu); r = EMULATE_DONE; } else if (vcpu->arch.pio.count) { if (!vcpu->arch.pio.in) //如果是out,不用再下次进入kmod时直接进入emulator vcpu->arch.pio.count = 0; else { writeback = false; //如果是in,则需要在下次qemu进入kmod的时候,完成io,实际上就是将qemu得到的数据写到正确的位置 vcpu->arch.complete_userspace_io = complete_emulated_pio; } r = EMULATE_USER_EXIT; … … }
下面的逻辑是进入kmod时,如果要完成in指令,进行的流程
int kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu, struct kvm_run *kvm_run) { … … //在这里我们可以看到,如果需要完成qemu的io,在这里调用刚才注册的complete_emulated_pio。 if (unlikely(vcpu->arch.complete_userspace_io)) { int (*cui)(struct kvm_vcpu *) = vcpu->arch.complete_userspace_io; vcpu->arch.complete_userspace_io = NULL; r = cui(vcpu); if (r <= 0) goto out; } else WARN_ON(vcpu->arch.pio.count || vcpu->mmio_needed); r = __vcpu_run(vcpu); … … } static int complete_emulated_pio(struct kvm_vcpu *vcpu) { BUG_ON(!vcpu->arch.pio.count); return complete_emulated_io(vcpu); } static inline int complete_emulated_io(struct kvm_vcpu *vcpu) { int r; vcpu->srcu_idx = srcu_read_lock(&vcpu->kvm->srcu); r = emulate_instruction(vcpu, EMULTYPE_NO_DECODE);//再次进入emulator,不过这次不用decode了,直接进行相应处理。 srcu_read_unlock(&vcpu->kvm->srcu, vcpu->srcu_idx); if (r != EMULATE_DONE) return 0; return 1; }
上述过程讲述了in指令在进入kmod时,将数据写到正确位置的流程,下面,我们将讲述刚才x86_emulate_instruction时,走到em_in和em_out之后的流程:
em_out:
Emulate.c
static int em_out(struct x86_emulate_ctxt *ctxt) { ctxt->ops->pio_out_emulated(ctxt, ctxt->src.bytes, ctxt->dst.val, &ctxt->src.val, 1);//实际上调用了emulator_pio_out_emulated,将相应的信息写到kvm_run,但是这里并没有返回 /* Disable writeback. */ ctxt->dst.type = OP_NONE; return X86EMUL_CONTINUE; //注意这里返回的是continue,返回emulator之后,会再进行处理,如果是string操作,会再次回到本函数,emulator_pio_out_emulated也就会一点点把数据写到buffer里,最后再返回qemu做out处理,这里分两个逻辑,具体可以在emulator代码中看到 } int x86_emulate_insn(struct x86_emulate_ctxt *ctxt) { … … if (ctxt->rep_prefix && (ctxt->d & String)) {//如果是串操作 unsigned int count; struct read_cache *r = &ctxt->io_read; if ((ctxt->d & SrcMask) == SrcSI) count = ctxt->src.count; else count = ctxt->dst.count; register_address_increment(ctxt, reg_rmw(ctxt, VCPU_REGS_RCX), -count); //rcx减少相应的count if (!string_insn_completed(ctxt)) {//如果串操作没完成 /* * Re-enter guest when pio read ahead buffer is empty * or, if it is not used, after each 1024 iteration. */ //每1024次或者cache满了就goto done,rip不变,下次vmentry之后还会发生同样的vmexit,走同样的流程,只不过rcx、rdi变了 if ((r->end != 0 || reg_read(ctxt, VCPU_REGS_RCX) & 0x3ff) && (r->end == 0 || r->end != r->pos)) { /* * Reset read cache. Usually happens before * decode, but since instruction is restarted * we have to do it here. */ ctxt->mem_read.end = 0; writeback_registers(ctxt); return EMULATION_RESTART;//这里会再去调用em_out,具体流程在emulator的代码中,在以后的连载中会再解析emulator的代码,这里只针对串操作来说明 } goto done; /* skip rip writeback *///如果串操作完成或者cache满,就返回qemu处理一次。 } } … … }
下面是em_in:
emulate.c:
static int em_in(struct x86_emulate_ctxt *ctxt) { if (!pio_in_emulated(ctxt, ctxt->dst.bytes, ctxt->src.val, &ctxt->dst.val)) return X86EMUL_IO_NEEDED; //如果是第一次进入本函数,就会返回qemu return X86EMUL_CONTINUE; //是第二次进入本函数,数据已经写入正确位置 } static int pio_in_emulated(struct x86_emulate_ctxt *ctxt, unsigned int size, unsigned short port, void *dest) { struct read_cache *rc = &ctxt->io_read; if (rc->pos == rc->end) { /* refill pio read ahead */ unsigned int in_page, n; unsigned int count = ctxt->rep_prefix ? address_mask(ctxt, reg_read(ctxt, VCPU_REGS_RCX)) : 1; //这里可以看到,如果有rep前缀,即发生了string的io,这里count即为rcx的值。 in_page = (ctxt->eflags & EFLG_DF) ? offset_in_page(reg_read(ctxt, VCPU_REGS_RDI)) : PAGE_SIZE - offset_in_page(reg_read(ctxt, VCPU_REGS_RDI)); n = min(min(in_page, (unsigned int)sizeof(rc->data)) / size, count); if (n == 0) n = 1; rc->pos = rc->end = 0; if (!ctxt->ops->pio_in_emulated(ctxt, size, port, rc->data, n)) return 0;//这里实际上走到了emulator_pio_in_emulated rc->end = n * size; } if (ctxt->rep_prefix && !(ctxt->eflags & EFLG_DF)) { ctxt->dst.data = rc->data + rc->pos; ctxt->dst.type = OP_MEM_STR; ctxt->dst.count = (rc->end - rc->pos) / size; rc->pos = rc->end; } else { memcpy(dest, rc->data + rc->pos, size); rc->pos += size; } return 1; } X86.c static int emulator_pio_in_emulated(struct x86_emulate_ctxt *ctxt, int size, unsigned short port, void *val, unsigned int count) { struct kvm_vcpu *vcpu = emul_to_vcpu(ctxt); int ret; if (vcpu->arch.pio.count) //count不为0表示需要把数据放到相应位置,而且这时意味着这时第二次进入这里,数据已经就位,可以写到正确的地方了。 goto data_avail; ret = emulator_pio_in_out(vcpu, size, port, val, count, true); //如果是第一次进入本函数 if (ret) { data_avail: memcpy(val, vcpu->arch.pio_data, size * count);//这里才把数据写到了正确的位置 vcpu->arch.pio.count = 0; return 1; } return 0; }
总结起来,kvm对于io的处理逻辑如下:
1、 对于string类型的in,会进入emulator,然后把count等值设置好,然后返回qemu处理,qemu将数据写到buffer之后,下一次 进入kmod的时候,在__vcpu_run之前,调用complete_userspace_io会再次进入emulator,这次进入 emulator不用解码,会把相应数据写到正确的位置,如此循环直到串操作完成。
2、 对于string类型的out,会进入emulator,不断的把需要输出的数据写入buffer,待buffer满或者rcx到0之后,会返回qemu直接输出数据。如此循环直到串操作完成。
3、 对于非string的out操作,会直接返回qemu进行一次out操作,下次vmentry的时候,直接跳过这个指令。