一、 qemu侧irqchip的实现
Qemu在main函数之前,已经创建了TYPE_I8259、ioapic、TYPE_APIC三个类型,用于创建这三个设备,实现在qemu侧的irqchip。 如果irqchip在hypervisor中实现,则需要创建三个新的设备,相比前面提到的三个设备要简单很多,主要是用来实现中断从qemu到hypervisor的分发过程。Irqchip实现在hypervisor的好处是,guest的所有中断都在驱动中进行处理,减少了hypervisor到qemu的陷出过程。(本文后续hypervisor以HAXM为例分析)
二、中断数据结构
-
GSIState:记录了中断状态,对应PIC和IOAPIC的各个管脚,ISA_NUM_IRQS为16,IOAPIC_NUM_PINS,对应PIC和IOAPIC的管脚数。
-
qemu_irq是一个指向IRQState结构的指针,可以表示一个中断引脚,handler表示执行的函数,n表示管脚号:
三、设备创建
Qemu中对应HAXM新创建了三个设备,包括i8259、hax-ioapic、hax-apic,其中hax-apic在中断分发中没有起到什么作用,只有一些辅助的功能。
1.
设备创建的流程
1)
type_init进行类型的初始化
设备文件的最后一行会调用type_init,对设备的TypeInfo类型进行处理。type_init宏对应module_init宏,其对应的函数添加了
attribute
((constructor))属性,因此会在main函数执行之前调用register_module_init:
该函数会动态分配一个指向ModuleEntry类型的节点,将i8259、ioapic、apic类型的init函数赋给ModuleEntry的init的函数,并插入到ModuleTypeList类型的链表里面: 2)
module_call_init
main函数会调用module_call_init函数,对之前插入到链表中的所有TypeInfo调用init回调函数,对应的为hax_pic_register_types、hax_apic_register_types、hax_ioapic_register_types,这些回调函数将完成设备TypeInfo类型到TypeImpl类型的转换,并以name为键值插入到hash表中完成类型的注册,至此完成了QOM中的类型注册过程。 TypeInfo类型和TypeImpl类型大部分成员相同,最大的不同是TypeImpl中加入了parent_type和class成员。 3)
qdev_create
目前qemu(2.12)创建的是i440FX+piix3的主板,在主板模拟时,会调用qdev_create函数根据之前得到的TypeImpl结构创建pic和ioapic设备。Qdev_create只是初始化了设备的状态,允许设备属性的设置,对设备的真正初始化还需要调用设备的realize函数。 qdev_create调用了qdev_try_create,进一步调用object_new,object_new会根据之前类型中注册的name,找到对应的TypeImpl结构,调用object_new_eith_type。 object_new_eith_type函数调用type_initialize。type_initialize首先设置了一些field,并为clazz成员分配了一个ObjectClass,然后初始化了所有的父类和祖类类型,包括实际类型和抽象类型,最后依次调用了父类和祖类的class_base_init函数与自己的class_init函数。 之后object_new_with_type首先分配了对象的实际空间,然后调用object_initialize_with_type函数,根据TypeImpl结构对该对象进行了初始化。至此完成了类对象的构造部分,现在设备已经创建好,但是里面的数据还没有填充,因此设备仍然处于不可用的状态。 4)
qdev_init_nofail
正式创建设备,qdev_init_nofail会调用object_property_set_bool函数,将设备的realized属性设置为true,并执行了设备对应的realize回调函数,对各个域进行初始化。这个过程叫做设备的具现化,使设备状态处于可用。
(免费订阅,永久学习)学习地址:
LinuxC/C++服务器开发/架构师 面试题、学习资料、教学视频和学习路线图(资料包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis、MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等),或点击这里加
qun
免费领取,关注我持续更新哦! !
2. i8259设备
1)i8259设备即pic设备,对应的TypeInfo为hax_i8259_info,在调用class_init回调函数时,会将realize函数设置为hax_pic_realize。Hax_pic_realize函数设置了PIC设备对应的设备和elcr寄存器的memory region,然后调用了parent也就是pic_common的realize函数pic_common_realize。 2)在pc_piix.c中,会对i8259进行具现化,调用的函数为hax_i8259_init:
该函数创建了两个i8259芯片,传入的参数中,true代表pic的master芯片,false代表pic的slave芯片。I8259中调用了qdev_create创建了pic的设备,设置了一些设备属性之后,包括设备的io基址、记录电平的elcr寄存器、芯片的主备属性,然后调用了qdev_init_nofail进行了设备的具现化:
ISADevice *i8259_init_chip(const char *name, ISABus *bus, bool master) { DeviceState *dev; ISADevice *isadev; isadev = isa_create(bus, name); dev = DEVICE(isadev); qdev_prop_set_uint32(dev, "iobase", master ? 0x20 : 0xa0); qdev_prop_set_uint32(dev, "elcr_addr", master ? 0x4d0 : 0x4d1); qdev_prop_set_uint8(dev, "elcr_mask", master ? 0xf8 : 0xde); qdev_prop_set_bit(dev, "master", master); qdev_init_nofail(dev); return isadev; }
3.ioapic设备
1)ioapic设备对应的类型为hax_ioapic_info,与pic设备类似,设置了realize、get、put、reset等函数。hax_ioapic_realize函数中,设置了hax-ioapic设备对应的memory region,调用了qdev_init_gpio_in函数,为设备创建了gpio的一个list,每个gpio都分配了回调函数hax_ioapic_set_irq。 2)pc_piix.c中,如果PCI设备使能,会调用ioapic_init_gsi对ioapic进行创建,ioapic_init_gsi函数会调用qdev_create创建”hax-ioapic”设备,然后调用qdev_init_nofail进行具现化。具现化会调用realize函数来创建gpio,之后会将创建的gpio赋值给gsi_state。
4. lapic设备
-
apic设备对应设别类型apic_info,设备名称“apic”。每个cpu对应一个lapic,因此lapic的创建在cpu创建的流程中: x86_cpu_realizefn -> x86_cpu_apic_create
static void x86_cpu_apic_create(X86CPU *cpu, Error **errp) { APICCommonState *apic; ObjectClass *apic_class = OBJECT_CLASS(apic_get_class()); cpu->apic_state = DEVICE(object_new(object_class_get_name(apic_class))); object_property_add_child(OBJECT(cpu), "lapic", OBJECT(cpu->apic_state), &error_abort); object_unref(OBJECT(cpu->apic_state)); qdev_prop_set_uint32(cpu->apic_state, "id", cpu->apic_id); /* TODO: convert to link<> */ apic = APIC_COMMON(cpu->apic_state); apic->cpu = cpu; apic->apicbase = APIC_DEFAULT_ADDRESS | MSR_IA32_APICBASE_ENABLE; }
目前为止已经准备好了lapic设备的类接口,然后同样在cpu创建过程中实现了设备的实例化: x86_cpu_realizefn -> x86_cpu_apic_realize
static void x86_cpu_apic_realize(X86CPU *cpu, Error **errp) { APICCommonState *apic; static bool apic_mmio_map_once; if (cpu->apic_state == NULL) { return; } object_property_set_bool(OBJECT(cpu->apic_state), true, "realized", errp); ... }
四、中断路由初始化
-
Pc_init1中分配了一个GSIState的结构gsi_state,然后调用了qemu_allocate_irqs,通过qemu_extend_irqs分配了一组qemu_irq:
qemu_irq *qemu_extend_irqs(qemu_irq *old, int n_old, qemu_irq_handler handler, void *opaque, int n) { qemu_irq *s; int i; if (!old) { n_old = 0; } s = old ? g_renew(qemu_irq, old, n + n_old) : g_new(qemu_irq, n); for (i = n_old; i < n + n_old; i++) { s[i] = qemu_allocate_irq(handler, opaque, i); } return s; }
可以看到,分配的这一组qemu_irq最终赋值给了pcms->gsi,共有24个,对应IOAPIC和PIC的gsi,qemu_allocate_irq函数中qemu_irq的handler被设置为hax_pc_gsi_handler,opaque被设置为了gsi_state。之前提到,GSIState对应了ioapic和pic设备的管脚,在ioapic和pic设备具现化之后,会将对应的管脚赋值给gsi_state。 pcms->gsi是整个虚拟机中断路由的起点,分配完之后的数据结构如下图所示:
最终pcms->gsi赋值给了piix3的pic指针。
五. 设备中断初始化
中断的初始化主要在pc_piix.c文件的pc_init1函数中。在南桥芯片piix4的模拟中,会建立ISA设备和PCI设备与中断控制器的对应关系。
-
PCI设备中断 a) 设备的模拟 b) PCI设备到中断控制器的路由 i. PCI总线上能挂很多个设备,而通常中断控制器的中断线是有限的,加上一些中断线已经分配给了主板上的设备,所以通常留给PCI设备的中断线个数只有4个或者8个,QEMU在i440fx主板上模拟给PCI设备的只有4个中断线。 ii. PCI设备的4条中断线对应了4个PCI链接设备,分别为LNKA、LNKB、LNKC、LNKD,分别连接中断控制器的4个引脚。PCI设备可以通过四根中断引脚INTA~D#向中断控制器提交中断请求,为均衡每个链接设备的负载,通常会进行交错连接。示例如下:
iii. 系统软件需要使用中断路由表存放PCI链接设备与中断控制器的连接关系,通常是由BIOS等系统软件建立的。而PCI设备的INTx与链接设备的连接关系模拟,则是在build_prt函数中进行,通常的映射关系为: (slot + pin) & 3 –> LINK[D|A|B|C]。 c) pc_init1中会调用i440fx_init函数来初始化pci_bus,并通过调用pci_bus_irqs设置了PCI总线的IRQ路由。
PCIDevice *pci_dev = pci_create_simple_multifunction(b, -1, true, "PIIX3"); piix3 = PIIX3_PCI_DEVICE(pci_dev); pci_bus_irqs(b, piix3_set_irq, pci_slot_get_pirq, piix3, PIIX_NUM_PIRQS); pci_bus_set_route_irq_fn(b, piix3_route_intx_pin_to_irq);
PIIX_NUM_PIRQS表示的是PCI连接设备的数量,PCI连接到中断控制器的配置是BIOS或者内核通过PIIX3的PIRQ[A-D]4个引脚配置的。 pci_slot_get_pirq得到设备连接到的PCI连接设备,假设设备用的引脚为x,设备的功能号为y,则连接设备为(x+y)&3。
/* return the global irq number corresponding to a given device irq pin. We could also use the bus number to have a more precise mapping. */ static int pci_slot_get_pirq(PCIDevice *pci_dev, int pci_intx) { int slot_addend; slot_addend = (pci_dev->devfn >> 3) - 1; return (pci_intx + slot_addend) & 3; }
-
ISA设备中断 pc_init1中调用了isa_bus_irqs,将isa_bus的irqs回调函数设置为了pcms->gsi,
六、 中断注入流程
-
PCI设备在需要注入中断时,会调用pci_set_irq。pci_set_irq会先调用pci_intx函数来得到设备使用的INTX引脚,然后调用pci_irq_handler函数:
/* 0 <= irq_num <= 3. level must be 0 or 1 */ static void pci_irq_handler(void *opaque, int irq_num, int level) { PCIDevice *pci_dev = opaque; int change; change = level - pci_irq_state(pci_dev, irq_num); if (!change) return; pci_set_irq_state(pci_dev, irq_num, level); pci_update_irq_status(pci_dev); if (pci_irq_disabled(pci_dev)) return; pci_change_irq_level(pci_dev, irq_num, change); }
pci_irq_handler首先判断了设备当前irq中断线的状态是否改变,然后调用pci_set_irq_state根据level设置了irq的状态,pci_update_irq_status将设备配置空间的中断状态位进行更新,最后调用了pci_change_irq_level:
{ PCIBus *bus; for (;;) { bus = pci_get_bus(pci_dev); irq_num = bus->map_irq(pci_dev, irq_num); if (bus->set_irq) break; pci_dev = bus->parent_dev; } bus->irq_count[irq_num] += change; bus->set_irq(bus->irq_opaque, irq_num, bus->irq_count[irq_num] != 0); }
pci_change_irq_level函数首先会获取当前设备对应的PCI总线,然后调用了map_irq回调函数,这个函数就是之前在pci_bus_irqs中设置的pci_slot_get_priq,根据之前说的规则,取出对应的链接设备号;set_irq回调就是另一个参数piix3_set_irq函数,该函数主要调用了piix3_set_irq_level:
static void piix3_set_irq_level(PIIX3State *piix3, int pirq, int level) { int pic_irq; pic_irq = piix3->dev.config[PIIX_PIRQC + pirq]; if (pic_irq >= PIIX_NUM_PIC_IRQS) { return; } piix3_set_irq_level_internal(piix3, pirq, level); piix3_set_irq_pic(piix3, pic_irq); }
Piix3_set_irq_level函数从piix3设备的config[PIIX_PIRQC+pirq]配置空间中取出中断线,也就是从链接设备到对应中断线的过程,然后在piix3_set_irq_pic中调用了qemu_set_irq来触发中断:
/* PIIX3 PCI to ISA bridge */ static void piix3_set_irq_pic(PIIX3State *piix3, int pic_irq) { qemu_set_irq(piix3->pic[pic_irq], !!(piix3->pic_levels & (((1ULL << PIIX_NUM_PIRQS) - 1) << (pic_irq * PIIX_NUM_PIRQS)))); }
之前提到过,piix3->pic对应的就是pcms->gsi,pic_irq对应了中断线,作为gsi的index。 \2. Qemu_set_irq简单地调用了irq对应的handler,pci设备对应的handler就是hax_pc_gsi_handler:
void hax_pc_gsi_handler(void *opaque, int n, int level) { GSIState *s = opaque; if (n < ISA_NUM_IRQS) { /* Kernel will forward to both PIC and IOAPIC */ qemu_set_irq(s->i8259_irq[n], level); } else { qemu_set_irq(s->ioapic_irq[n], level); } }
hax_pc_gsi_handler会根据中断号来选择发送PIC的中断还是IOAPIC的中断。PIC的回调函数为hax_pic_set_irq,IOAPIC的回调函数为hax_ioapic_set_irq。实际上HAXM收到中断后并不清楚是要发给PIC还是IOAPIC,在PIC的分发过程中会判断中断掩码,确认guest使用的是否是PIC;IOAPIC现在还没有进行判断,guest收到后会根据当前使用的中断控制器来决定是否忽略来自APIC的中断。 \3. Hax_pic_set_irq和hax_ioapic_set_irq都会调用ioctl(HAX_VM_IOCTL_IRQ_LINE_STATUS)将中断传递给HAXM,进入HAXM中的中断路由过程,整体流程为:
七. PIC设备创建调用流程图
八. 中断相关结构体对应关系
原文链接:
https://www.cnblogs.com