在"kvm:the Linux Virtual Machine Monitor"论文中提到,IO虚拟化中有两个重要部分:"Virtualizing Guest-Initiated IO Instructions" and "Host-Initiated Virtual Interrupts"。
Guest-Initiated IO Instructions Virtualization
对于guest发起的IO指令的虚拟是比较直接的:当guest OS在guest mode中执行时,支持vmx的物理CPU实际上是在vmx non-root operation模式中。虚拟机的VMCS中的设置可以控制一旦执行某个IO指令,立刻发生VMExit。函数路径如下所示,VMExit后返回到vcpu_enter_guest,这个函数接着调用vmx_handle_exit对VMExit进行处理。如果此IO指令可以在内核模式就模拟出来(比如内核模拟PIT和PIC),kernel_pio返回真,就不用切换到用户模式,模拟完成后重新调用vcpu_enter_guest;如果不能在内核空间模拟,就一路返回,直到切换到用户模式,在qemu-kvm的kvm_run函数中调用kvm_handle_io进行IO指令模拟,完成后再次用ioctl(KVM_RUN)请求恢复guest运行。
handle guest io instructions code path:
vcpu_enter_guest
| vmexit
kvm_x86_ops->run ---> kvm_x86_ops->handle_exit(vmx_handle_exit)
|
kvm_vmx_exit_handlers[exit_reason](handle_io in vmc.c)
|
kvm_emulate_pio (x86.c)
|
/ \
return 0 kernel_pio(x86.c) --> complete_io
| |
return (userspace) vcpu_enter_guest
| mode switch@ qemu-kvm
kvm_handle_io (kvm_run() kvm_all.c)
|
ioctl(KVM_RUN)
Interrupt Virtualization
中断虚拟化,有两方面:一是如何保证物理中断只有host来处理;二是host如何将一个虚拟中断注入到guest中。
首先看物理中断情况;在没有guest情况下,一旦CPU检测到中断信号,将在下一条指令之前响应中断,根据中断号从host OS IDT中取到对应的中断向量,然后调用interrupt handler。但是,假如guest vcpu正在执行中来了物理中断,此时的物理IDTR指向的是guest OS的IDT。原则上肯定不能是由guest handler去处理物理中断,所以必须通过某种机制来处理,这个机制由两部分配合:
a. 首先是vmx规定,只要此虚拟机vmcs的"VM-Execution control field"中的"External -interrupt exiting"位设置为1,物理中断将导致VMExit(vmcs的配置可以参考“IA32 Intel Architecture Software Developer's Manual Volume 3B System Programming Guide”)。
b. 知道了a后,我就想过,物理中断不是应该立刻由硬件来响应么,那么是响应中断在前,还是VMExit在前呢?如果响应在前,此时IDTR还没有恢复为host的,将导致取guest IDT,所以显然不行,只能是VMExit在前,但是VMExit过程中硬件只能恢复host的非通用寄存器,通用寄存器还得由软件来恢复(vmx_vcpu_run函数代码),物理中断却要在恢复通用寄存器之间了,显然也不行。后来才发现,原来在进入guest执行前,kvm是关中断的,在VMExit完全恢复了host上下文后,才开中断——关中断是vcpu_enter _guest函数中调用了local_irq_disable,开中断是在这个函数从kvm_x86_ops->run返回后(即VMExit后)调用local_irq_enable。现在一切都明白了:guest vcpu执行时,物理中断来了,它可以导致VMExit,但是此时是关中断,所以硬件不会响应中断,中断处于pending,在中断开后,硬件发现pending中断并开始响应,此时已经是在host上下文中,IDTR已经指向host的IDT,因此物理中断实际上是由host handler来处理了。
再来看虚拟中断的情况;KVM用QEMU来提供设备模拟,与物理设备一样,模拟的设备也可以发出中断信号,但是这个中断信号是软件虚拟的,那么这个虚拟中断是怎样注入到虚拟机中去的呢?这个又要分两种情况:一种是此时guest vcpu正在执行,另一种是guest vcpu刚到用户模式执行完IO指令模拟操作,还没有开始VMEntry。
对于后一种情况,可以参考vmx_inject_irq和vmx_inject_nmi(vmx.c),可以发现,kvm是利用vmx支持这种注入操作的特性:vmcs中有"VM-Entry control fields",其中又包含"VM-entry interruption-information field",可以将中断类型和中断号记录到其中,在VMEntry过程中,硬件会自动检测这个信息域,如果有效,就会在真正执行任何guest指令之前,像原来硬件一样响应此中断(包括压IP,CS,FLAGS;从IDT中取interrupt vector等),注意此操作是在恢复了guest上下文之后,所以物理IDTR已经是指向guest OS IDT了,因此这个虚拟中断就由guest handler来处理了。
补充上段:假如在guest运行期间发生了一个物理中断,kvm是如何截获进入将其注入到guest中去的呢?有两个步骤:
1. 在vmx_vcpu_run中,从guest mode返回后会调用complete_interrupt函数。因为硬件会将中断类型和那个中断号写入到VMCS中,所以complete_interrupt的任务就是从VMCS中取出中断信息,然后写入vcpu的控制块中去,使得这个中断变成一种pending event。
2. 在vcpu_enter_guest中,在进入guest mode前会调用inject_pending_event。inject_pending_event发现vcpu控制块中有pending的中断信息,就调用vmx_inject_irq将其写入VMCS中去,就相当于将中断注入到guest中去了。
对于前一种情况,暂时也还不知道KVM具体是如何实现的。不过vmx倒也是支持这种注入操作。只要vmcs的"VM-Execution control field"中的"Interrupt-window exiting"位设置为1,只要guest IF=1,在下一条任意指令前,会发生VMExit,就像普通情况下发生了一次中断一样。也就是说,kvm应该可以通过设置"Interrupt-window exiting"中断guest vcpu的执行,然后像后一种情况一样,把虚拟中断注入到虚拟机中。
在本文中曾经分析过kvm是如何对I/O涉及到的两个方面进行处理的。然而,有个问题仍然没有得到解释,那就是Qemu的设备模拟机制是怎样在kvm中运作的,这里根据目前的理解简单的总结一下。
首先,先不管KVM,单独的Qemu设备模拟一般是要用到物理设备的,就比如PIT的模拟,可以是用物理的HEPT来实现。模拟PIT打开/dev/hpet,然后设定一个频率作为它的模拟时钟源。当周期到时,物理HPET发出物理中断,host OS处理中断时,会发送一个SIGIO信号给Qemu进程,Qemu进程处理这个信号时,就可以模拟一次PIT的时钟周期行为,如发送一个模拟中断给guest。那么,可以想象其他的模拟外设也应当是通过类似的方法来实现的。
在KVM中,有io线程和vcpu线程,假设此时vcpu线程正在运行guest,这时来了一个HPET物理中断,那么:
1. vcpu线程将VM exit到kernel space
2. 在vcpu_enter_guest中开中断,host开始处理中断
3. 在host ISR中将发送一个SIGIO给Qemu进程
4. __vcpu_run中,在vcpu_enter_guest返回后,将判断当前进程有pending的信号(signal_pending(current)),如果有,那么将推出kernel loop,vcpu线程返回到userspace
5. vcpu线程返回到qemu-kvm后,在kvm_main_loop_cpu中调用kvm_main_loop_wait进入等待
6. io线程获得调度,然后通过select可以发现signal fd可以读了,于是调用相应的handler
7. 在SIGIO handler中产生一次模拟的PIT时钟行为
8. 随后io线程又进入select,随后vcpu等待出来,重新进入kernel space,进而重新开始运行
阅读(1135) | 评论(0) | 转发(1) |