本文讲述一个网络数据包从到达物理网卡,一直到中断注入给VM的整个过程。
为了讲述清晰,假设宿主物理机有两个物理CPU,分别为CPU0和CPU1。假设GuestOS运行在CPU1上,物理网卡接到数据包后把中断请求发送到CPU0.
1.网络数据包Package到达物理网卡NIC, NIC收到数据包后,向CPU0发送中断请求,通知CPU0有网络数据包到达。
2.CPU0收到中断请求后,调用中断处理函数,处理这个中断请求。但是,这个数据包可能是发给VM的,所以这里Host不会直接调用Host的中断处理函数,而是会调用QEMU中实现的Bridge(Brigde就是一个网桥)。Bridge会把请求再转发给QEMU模拟的TAP设备。
3.TAP设备会进行判断这个请求是发给谁的,如果是发给Host的,则直接调用Host的中断处理函数;如果是发给VM的,那么就会TAP就会调用一系列函数,比如select,知道调用到kvm_vm_ioctl(, KVM_IRQ_LINE_STATUS,)。到此为止,QEMU模拟结束,开始进入KVM执行。
4.kvm_set_irq() (irqchip.c)函数说明
-
int kvm_set_irq(struct kvm *kvm, int irq_source_id, u32 irq, int level,bool line_status){
-
...
-
if (irq < irq_rt->nr_rt_entries)
-
/*提取中断路由表中对应的中断路由实体,map[irq]是一个对应中断的路由实体表头结点,这里遍历它能够得到所有对应的路由实体。*/
-
hlist_for_each_entry(e, &irq_rt->map[irq], link)
-
while(i--) {
-
int r;
-
/*触发对应路由实体的触发函数,这个函数在之前的安装中断路由的时候已经注册,注册函数是 <span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px; text-align: -webkit-left; ">setup_routing_entry() </span>*/
-
r = irq_set[i].set(&irq_set[i], kvm, irq_source_id, level);
-
if (r < 0)
-
continue;
-
ret = r + ((ret < 0) ? 0 : ret);
-
}
-
return ret;
-
}
由 setup_routing_entry() (在irqchip.c中)代码,可知上面ira_set[i].set(..) 调用的就是kvm_set_ioapic_irq().
具体注册路径为:setup_routing_entry() -->kvm_set_routing_entry()-->e->set = kvm_set_ioapic_irq()
5. ioapic_service()说明
-
static int ioapic_service(struct kvm_ioapic *ioapic, unsigned int idx,
-
bool line_status)
-
{
-
union kvm_ioapic_redirect_entry *pent;
-
int injected = -1;
-
/*读取“中断重定向表”*/
-
pent = &ioapic->redirtbl[idx];
-
/*检查中断是否被屏蔽,如果被屏蔽,则不触发中断*/
-
if (!pent->fields.mask) {
-
/*发送到LAPIC*/
-
injected = ioapic_deliver(ioapic, idx, line_status);
-
if (injected && pent->fields.trig_mode == IOAPIC_LEVEL_TRIG)
-
pent->fields.remote_irr = 1;
-
}
-
return injected;
-
}
6.ioapic_deliver()
这个函数主要功能:读取中断重定向表,设置中断请求的dest_id, vector,dest_mode等请求信息,然后再调用kvm_irq_delivery_to_apic()把中断请求发送给LAPIC
7.kvm_irq_delivery_to_apic()函数
这是一个非常关键的函数! 是一个枢纽函数,无论IOAPIC发给LAPIC中断请求(外部I/O中断),还是LAPIC发个LAPIC中断请求(IPI中断),最后都是调用的这个函数!
这个函数的功能有: 根据传入的参数irq,获取目标LAPIC编号dest_id,再根据dest_id找到其对应的vcpu,最后调用kvm_apic_set_irq() 设置目标LAPIC的中断请求寄存器。
8.__apic_accept_irq()
函数功能:向lapic添加一个Pending IRQ
首先根据delivery_mode选择添加的方式,基本都会调用下面两个函数:
kvm_make_request(..,vcpu): 这个函数就是向目标vcpu添加一个reqest请求。当目标vcpu再次enter_guest时,check到这个中断请求,进行中断注入,vcpu重新运行,捕获这个中断。
kvm_vcpu_kick(vcpu): 这个函数的功能就是,判断vcpu是否正在物理cpu上运行,如果是,则让vcpu退出,以便进行中断注入。
----------------------------------------
到此为止,kvm模拟的IOAPIC, LAPIC就结束了。下面就是vcpu_enter_guest()进行中断注入的过程了
----------------------------------------
9.vcpu_enter_guest()
当vcpu再次被调度进行vm_entry时,就会执行这个vcpu_enter_guest()函数。
在vcpu run之前,会调用kvm_check_reqest(KVM_REQ_EVENT,vcpu)检查是否有中断请求需要注入。
在上面8中,调用过kvm_make_request(),所以这里if (kvm_check_request(KVM_REQ_EVENT, vcpu) || req_int_win)条件判断成立,执行下面的inject_pending_event(vcpu)
10.inject_pending_event()
这个函数的功能就是,把中断请求写入到目标vcpu的中断请求寄存器。最后就是写VMCS中断的irq寄存器。
调用为kvm_x86_ops->set_irq(vcpu),这正调用的就是vmx_inject_irq()
在static struct kvm_x86_ops vmx_x86_ops ={ .. .set_irq = vmx_inject_irq }.
vmx_inject_irq() 调用vmx_write32() 写VMCS 的IRQ寄存器。
11. vmx_vcpu_run()
vcpu_enter_guest()最后调用 vmx_vcpu_run(), vcpu开始执行。
在执行前,会读取VMCS中的寄存器信息,由于前面写了IRQ,所以vcpu运行就会知道有中断请求到来,再调用GuestOS的中断处理函数处理中断请求。
----------------------
到此,从一个网络包到达网卡,到qemu 和kvm模拟中断,到中断注入给vcpu,再到vcpu运行发现中断,调用中断处理函数,完整过程大概如此。
----------------------
处理器间IPI中断
IPI中断处理过程要简单的多。
LAPIC中有两个重要的寄存器:
LAPIC_ICR: 存放中断向量
LAPIC_ICR2: 存放中断请求目标
如果一个vcpu0要个vcpu1发IPI中断,vcpu0只要调用apic_reg_write()写LAPIC的这两个寄存器就可以了。
再调apic_sent_ipi()-->kvm_irq_deliver_to_apic()
中断注入
中断注入实际是向客户机CPU注入一个事件,这个事件包括异常和外部中断和NMI。异常我们一般看作为同步,中断被认为异步。
硬件具体实现就中断注入实际就是设置VMCS中字段VM-Entry interruption-infomation字段。中断注入实际在VM运行前完成的,具体代码如下:
static int vcpu_enter_guest(struct kvm_vcpu *vcpu) {
inject_pending_event(vcpu);
}
vcpu_enter_guest函数运行虚拟机,运行虚拟机代码已省掉。中断注入实际在VM运行前,接下来看看具体如何注入。
static void inject_pending_event(struct kvm_vcpu *vcpu)
{
if (vcpu->arch.nmi_injected) {
kvm_x86_ops->set_nmi(vcpu);
return;
}
if (vcpu->arch.interrupt.pending) {
kvm_x86_ops->set_irq(vcpu);
return;
}
/* try to inject new event if pending */
if (vcpu->arch.nmi_pending) {
if (kvm_x86_ops->nmi_allowed(vcpu)) {
vcpu->arch.nmi_pending = false;
vcpu->arch.nmi_injected = true;
kvm_x86_ops->set_nmi(vcpu);
}
} else if (kvm_cpu_has_interrupt(vcpu)) {
if (kvm_x86_ops->interrupt_allowed(vcpu)) {
kvm_queue_interrupt(vcpu, kvm_cpu_get_interrupt(vcpu),
false);
kvm_x86_ops->set_irq(vcpu);
}
}
}
首先用户态实现中断控制器,不可屏蔽中断和其他中断注入过程。用户态中断采集在qemu代码中实现
判断是否有等待注入中断,存在话立即注入
接下来内核态模拟的中断控制器,中断注入过程,不可屏蔽中断和其他中断注入过程。
判断KVM内核态是否有不可屏蔽中断,有并且客户机cpu允许中断话,注入中断到客户机cpu中。
判断KVM内核态是否有中断,有中断并且客户机cpu允许中断话,获取优先级高中断进行排队,注入中断到客户机cpu中。
另外一个情况,如果有中断但是客户机不允许中断,只能等待下一下中断注入。如果下一次有更高级别中断发生,该中断还是不能注入而选择更高级别中断注入。