QEMU中断设备模拟
一、 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回调函数,对各个域进行初始化。这个过程叫做设备的具现化,使设备状态处于可用。
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中的中断路由过程,整体流程为: