展示自己、证明自己
分类: 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的时候,直接跳过这个指令。