本文讲述一个网络数据包从到达物理网卡,一直到中断注入给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() -->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()