QEMU中断设备模拟

一、 qemu侧irqchip的实现

Qemu在main函数之前,已经创建了TYPE_I8259、ioapic、TYPE_APIC三个类型,用于创建这三个设备,实现在qemu侧的irqchip。
如果irqchip在hypervisor中实现,则需要创建三个新的设备,相比前面提到的三个设备要简单很多,主要是用来实现中断从qemu到hypervisor的分发过程。Irqchip实现在hypervisor的好处是,guest的所有中断都在驱动中进行处理,减少了hypervisor到qemu的陷出过程。(本文后续hypervisor以HAXM为例分析)

二、中断数据结构

  1. GSIState:记录了中断状态,对应PIC和IOAPIC的各个管脚,ISA_NUM_IRQS为16,IOAPIC_NUM_PINS,对应PIC和IOAPIC的管脚数。
    image
  2. qemu_irq是一个指向IRQState结构的指针,可以表示一个中断引脚,handler表示执行的函数,n表示管脚号:
    image

三、设备创建

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:
image
该函数会动态分配一个指向ModuleEntry类型的节点,将i8259、ioapic、apic类型的init函数赋给ModuleEntry的init的函数,并插入到ModuleTypeList类型的链表里面:
image
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:
image
该函数创建了两个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设备

  1. 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);
...
}

四、中断路由初始化

  1. Pc_init1中分配了一个GSIState的结构gsi_state,然后调用了qemu_allocate_irqs,通过qemu_extend_irqs分配了一组qemu_irq:
    image
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是整个虚拟机中断路由的起点,分配完之后的数据结构如下图所示:
image
最终pcms->gsi赋值给了piix3的pic指针。

五. 设备中断初始化

中断的初始化主要在pc_piix.c文件的pc_init1函数中。在南桥芯片piix4的模拟中,会建立ISA设备和PCI设备与中断控制器的对应关系。

  1. 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#向中断控制器提交中断请求,为均衡每个链接设备的负载,通常会进行交错连接。示例如下:
    image
    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;
}
  1. ISA设备中断
    pc_init1中调用了isa_bus_irqs,将isa_bus的irqs回调函数设置为了pcms->gsi,

六、 中断注入流程

  1. 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中的中断路由过程,整体流程为:
image

七. PIC设备创建调用流程图

image

八. 中断相关结构体对应关系

image

posted @ 2022-05-12 17:36  Edver  阅读(1930)  评论(1编辑  收藏  举报