本文发自 http://www.binss.me/blog/qemu-note-of-Q35-machine/,转载请注明出处。

QEMU支持的架构非常少,在Q35出现之前,就只有诞生于1996年的i440FX + PIIX一个架构在苦苦支撑。一方面是Intel不断推出新的芯片组,搞出了PCIe、AHCI等等新东西。i440FX已经无法满足需求,为此在 KVM Forum 2012 上Jason Baron带来了PPT:A New Chipset For Qemu - Intel's Q35。Q35是Intel在2007年6月推出的芯片组,最吸引人的就是其支持PCI-e。

根据Intel Q35文档,Q35的拓扑结构如图所示:

可见其北桥为MCH,南桥为ICH9。CPU 通过 前端总线(FSB) 连接到 北桥(MCH),北桥为内存、PCIE等提供接入,同时连接到南桥(ICH9)。南桥为 USB / PCIE / SATA 等提供接入。

Q35 拓扑结构

那么在QEMU中实现的Q35拓扑结构是否真的如上图所示呢?我们在 QEMU 中通过 info qtree 查询,简化后的结构为:

(qemu) info qtree
bus: main-system-bus
  dev: hpet, id ""
    gpio-in "" 2
    gpio-out "" 1
    gpio-out "sysbus-irq" 32
  dev: ioapic, id ""
    gpio-in "" 24
    version = 32 (0x20)
  dev: q35-pcihost, id ""
    bus: pcie.0
      type PCIE
      dev: e1000, id ""
      dev: VGA, id ""
      dev: ICH9 SMB, id ""
      dev: ich9-ahci, id ""
        bus: ide.5
          type IDE
        bus: ide.4
          type IDE
        bus: ide.3
          type IDE
        bus: ide.2
          type IDE
          dev: ide-cd, id ""
        bus: ide.1
          type IDE
        bus: ide.0
          type IDE
          dev: ide-hd, id ""
            drive = "ide0-hd0"
      dev: ICH9-LPC, id ""
        gpio-out "gsi" 24
        bus: isa.0
          type ISA
          dev: i8257, id ""
          dev: i8257, id ""
          dev: port92, id ""
            gpio-out "a20" 1
          dev: vmmouse, id ""
          dev: vmport, id ""
          dev: i8042, id ""
            gpio-out "a20" 1
          dev: isa-parallel, id ""
          dev: isa-serial, id ""
          dev: isa-pcspk, id ""
            iobase = 97 (0x61)
          dev: isa-pit, id ""
            gpio-in "" 1
            gpio-out "" 1
          dev: mc146818rtc, id ""
            gpio-out "" 1
          dev: isa-i8259, id ""
            gpio-in "" 8
            gpio-out "" 1
            master = false
          dev: isa-i8259, id ""
            gpio-in "" 8
            gpio-out "" 1
            master = true
      dev: mch, id ""
  dev: fw_cfg_io, id ""
  dev: kvmclock, id ""
  dev: kvmvapic, id ""

注意dev和bus是交替出现的,更加简化的设备图如下:

    bus             dev          bus     dev         bus       dev
main-system-bus - ioapic
                - q35-pcihost - pcie.0 - mch
                                       - ICH9-LPC - isa.0 - isa-i8259
                                       - ICH9 SMB
                                       - ich9-ahci
                                       - VGA
                                       - e1000
                                       - ...

由于对硬件和架构的不熟悉,这里反反复复研究了一天才有点眉目。我的理解如下:

main-system-bus 就是系统总线,ioapic直接连到系统总线上,符合我们对IOAPIC的认知,在q35架构图CPU为 Core 和 Pentium Pentium E2000 系列,而根据文档,Intel自从Pentium4/Xeon后就取消了APIC bus,换用系统总线。

但让我纠结的是,mch为什么会连到 pcie.0 上?按照定义,mch是Intel对北桥的称呼。但mch的全称其实是memory controller hub,在这里它指的就真的是内存控制器这个hub。

既然 mch 不是北桥,那么 q35-pcihost 自然就是了,一方面host bridge也是北桥的称呼,另一方面它上连系统总线,下连pcie总线。再看其定义:

typedef struct Q35PCIHost {
    /*< private >*/
    PCIExpressHost parent_obj;
    /*< public >*/

    MCHPCIState mch;
} Q35PCIHost;

typedef struct MCHPCIState {
    /*< private >*/
    PCIDevice parent_obj;
    /*< public >*/

    MemoryRegion *ram_memory;
    MemoryRegion *pci_address_space;
    MemoryRegion *system_memory;
    MemoryRegion *address_space_io;
    PAMMemoryRegion pam_regions[13];
    MemoryRegion smram_region, open_high_smram;
    MemoryRegion smram, low_smram, high_smram;
    MemoryRegion tseg_blackhole, tseg_window;
    Range pci_hole;
    uint64_t below_4g_mem_size;
    uint64_t above_4g_mem_size;
    uint64_t pci_hole64_size;
    uint32_t short_root_bus;
} MCHPCIState;

和更加验证了我的想法。然后内存控制器又是用pci统一编号的,因此接在pcie.0上还算合理。

同理 ICH9-LPC 是 PCI/ISA bridge,也就是我们通常所说的南桥。然后上面接了条ISA总线,用来连接传统的(Legacy)ISA设备,比如PIC。

结合虚拟机内部的lshw输出以帮助理解:

binss@ubuntu:~$ lshw -businfo
WARNING: you should run this program as super-user.
Bus info          Device      Class      Description
====================================================
                              system     Computer
                              bus        Motherboard
                              memory     1999MiB System memory
cpu@0                         processor  Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20G
cpu@1                         processor  Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20G
cpu@2                         processor  Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20G
cpu@3                         processor  Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20G
pci@0000:00:00.0              bridge     82G33/G31/P35/P31 Express DRAM Controll
pci@0000:00:01.0              display    VGA compatible controller
pci@0000:00:02.0  enp0s2      network    82540EM Gigabit Ethernet Controller
pci@0000:00:1f.0              bridge     82801IB (ICH9) LPC Interface Controller
pci@0000:00:1f.2              storage    82801IR/IO/IH (ICH9R/DO/DH) 6 port SATA
pci@0000:00:1f.3              bus        82801I (ICH9 Family) SMBus Controller
                  scsi2       storage
scsi@2:0.0.0      /dev/cdrom  disk       DVD reader

设备关系

个人认为有两种关系,一种是 挂载关系 ,通过设置类对象成员的形式来定义设备之间的关系。另一种是 组合树+关联关系 ,通过设置设备的属性来定义QOM之间的关系。

挂载关系

设备会挂载到bus上。创建设备对象的调用链如下:

qdev_create => qdev_try_create => 如果传入参数bus为NULL,则 bus = sysbus_get_default() => 如果全局变量 main_system_bus 不存在,调用 main_system_bus_create 创建并返回
                               => qdev_set_parent_bus => 设置 dev->parent_bus

因此一个device挂载在哪个bus上,是由其 DeviceState 的 parent_bus 成员指定的。 在这里,main_system_bus 即 main-system-bus ,创建时不传入bus参数的设备都将挂载在该bus上,比如 ioapic 和 q35-pcihost

同理 ICH9-LPC 等设备在 qdev_create 时会传入bus参数,其指向 pcie.0 ,因此表示被挂载在 pcie.0 这个 PCIBus 上

除此之外,对于 PCI 设备 PCIDevice ,虽然其"父类" DeviceState 的 parent_bus 已经指定挂载在哪个bus上,但其还自己维护了一个 bus 成员,指向 PCIBus 。该成员在 do_pci_register_device 时设置。

以上定义了下游device和bus的关系,而上游device和bus的关系由 device 的 bus 成员来指向。比如

q35-pcihost 的 PCIExpressHost-PCIHostState 有 PCIBus 类型的 bus 成员,其在 q35_host_realize 时设置为 pcie.0

ICH9-LPC 的 ICH9LPCState 有 ISABus 类型的 isa_bus 成员, 其在 ich9_lpc_realize 时设置为 isa.0 (名字是在qbus_realize中拼凑出来的)

组合树关系

我们知道,无论是 bus 和 device ,本质上都是 QOM 对象。这些对象以路径的形式联系在一起,构成一棵组合树。

将 info qom-tree 精简(去除irq和memory-region)后如下:

/machine (pc-q35-2.8-machine)
  /unattached (container)
    /device[2] (host-x86_64-cpu)
      /lapic (kvm-apic)
    /device[6] (ICH9-LPC)
      /isa.0 (ISA)
    /device[15] (i8042)
    /device[19] (i8257)
    /device[3] (host-x86_64-cpu)
      /lapic (kvm-apic)
    /device[7] (isa-i8259)
    /device[20] (i8257)
    /device[24] (ICH9 SMB)
      /i2c (i2c-bus)
    /device[8] (isa-i8259)
    /device[17] (vmmouse)
    /device[21] (ich9-ahci)
      /ide.4 (IDE)
      /ide.5 (IDE)
      /ide.0 (IDE)
      /ide.1 (IDE)
      /ide.2 (IDE)
      /ide.3 (IDE)
    /sysbus (System)
    /device[33] (VGA)
  /q35 (q35-pcihost)
    /pcie.0 (PCIE)
    /ioapic (ioapic)
    /mch (mch)
  /peripheral-anon (container)
  /peripheral (container)

这样的关系是通过child属性来维系的。

main_system_bus_create 创建了 main-system-bus ,同时

object_property_add_child(container_get(qdev_get_machine(), "/unattached"), "sysbus", OBJECT(main_system_bus), NULL);

而 qdev_get_machine 定义如下:

Object *qdev_get_machine(void)
{
    static Object *dev;

    if (dev == NULL) {
        dev = container_get(object_get_root(), "/machine");
    }

    return dev;
}

Object *object_get_root(void)
{
    static Object *root;

    if (!root) {
        root = object_new("container");
    }

    return root;
}

Object *container_get(Object *root, const char *path)
{
    Object *obj, *child;
    gchar **parts;
    int i;

    // 切分路径
    parts = g_strsplit(path, "/", 0);
    assert(parts != NULL && parts[0] != NULL && !parts[0][0]);
    obj = root;

    // 逐级访问,如果发现某一级不存在,则创建一个container补上
    for (i = 1; parts[i] != NULL; i++, obj = child) {
        child = object_resolve_path_component(obj, parts[i]);
        if (!child) {
            child = object_new("container");
            object_property_add_child(obj, parts[i], child, NULL);
        }
    }

    g_strfreev(parts);

    return obj;
}

首先 qdev_get_machine 调用了 container_get(object_get_root(), "/machine") ,而 object_get_root 负责返回根级对象。如果未创建则会创建一个再返回。我们可以看到它是一个container,根据其定义,可以发现其继承了Object,但没有定义新的成员,相当于alias:

static const TypeInfo container_info = {
    .name          = "container",
    .instance_size = sizeof(Object),
    .parent        = TYPE_OBJECT,
};

然后 container_get 负责从传入的第一个参数(root)出发,返回相对路径为第二个参数(path)的对象。它将相对路径按"/"进行切分,对每一级调用 object_resolve_path_component

Object *object_resolve_path_component(Object *parent, const gchar *part)
{
    ObjectProperty *prop = object_property_find(parent, part, NULL);
    if (prop == NULL) {
        return NULL;
    }

    if (prop->resolve) {
        return prop->resolve(parent, prop->opaque, part);
    } else {
        return NULL;
    }
}

本质上就是查找名为 路径名 的属性,然后将其属性值返回。该属性值就是当前级对象在路径上的的下一级对象,然后再从下一级对象继续调用 object_resolve_path_component 找下下级对象。

如此迭代,直到相对路径切分出来的所有级别都被访问到,然后将最后一级对象,也就是目标对象返回。

如果某一级对象不存在,则创建一个container,将其作为当前级的child属性,然后继续下一级。

因此 main-system-bus 的路径为 /machine/unattached/sysbus ,符合 qom-tree。我们可以通过 object_resolve_path 根据路径找到设备对象:

(gdb) p object_resolve_path("/machine/unattached/sysbus", 0)
$92 = (Object *) 0x555556769d20
(gdb) p object_resolve_path("/machine/unattached/sysbus", 0)->class->type->name
$93 = 0x555556683760 "System"

同理,经过:

object_property_add_child(qdev_get_machine(), "q35", OBJECT(q35_host), NULL);
object_property_add_child(object_resolve_path(parent_name, NULL), "ioapic", OBJECT(dev), NULL);

于是 ioapic 的路径为 /machine/q35/ioapic ,回想上文提到 ioapic 和 q35-pcihost 一起挂在 main-system-bus 上,属于平级关系,显然挂载关系和组合树关系有不一致之处。

对于bus来说,在 qbus_realize => object_property_add_child(OBJECT(bus->parent), bus->name, OBJECT(bus), NULL) 中被添加为上一级对象的child属性,比如 pcie.0 被作为 q35-pcihost 的pcie.0属性,于是路径为 /machine/q35/pcie.0

对于memory region来说,在 memory_region_init => object_property_add_child 中被添加为上一级对象的child属性。

关联关系

在组合树关系中,我们可以看到,设备之间通过child属性,构成了一棵设备树,以machine为根,分门别类地向下展开。通过路径,我们能够从根设备出发,一级一级地向下迭代,找到目标设备。

但组合树还不能完全表达设备之间的关系,在child关系中,父节点掌控了子节点的生命周期。如果我们不需要这么强的关系,而仅仅是需要表示一种设备和另一种设备有关联,应该如何表示呢?

为此QOM定义了backlink关系。通过设置设备的link属性,表示一个设备引用了另外一个设备,在设置后,一个设备可以通过它的link属性访问到另一个设备,这也模拟了在硬件上两个设备之间直接连起来的场景。

一种最典型的backlink就是 bus 和插在它上面的child device。child device会有指向bus的link属性,比如:

pc_q35_init => qdev_create(NULL, TYPE_Q35_HOST_DEVICE) => qdev_try_create => object_new 会调用其父类的实例函数,即 device_initfn ,则:

static void device_initfn(Object *obj)
{
    ...
    object_property_add_link(OBJECT(dev), "parent_bus", TYPE_BUS,
                             (Object **)&dev->parent_bus, NULL, 0,
                             &error_abort);
}

将 main-system-bus 设置为 q35-pcihost 的link属性,名为 parent_bus 。注意这里的 dev->parent_bus 还是空的,因为 parent_bus 还没设置,直到:

pc_q35_init => qdev_create(NULL, TYPE_Q35_HOST_DEVICE) => qdev_try_create => qdev_set_parent_bus 的 dev->parent_bus = bus; 才会填上。因此属性值存的不是对象成员的值,而是对象成员的指针。当然由于 parent_bus 成员本来就是指针,于是这里是指针的指针(Object **)。

相反,bus会有指向child device的link属性:

qdev_set_parent_bus 中, dev->parent_bus = bus; 的下一行就是 bus_add_child :

static void bus_add_child(BusState *bus, DeviceState *child)
{
    char name[32];
    BusChild *kid = g_malloc0(sizeof(*kid));

    kid->index = bus->max_index++;
    kid->child = child;
    object_ref(OBJECT(kid->child));

    QTAILQ_INSERT_HEAD(&bus->children, kid, sibling);

    /* This transfers ownership of kid->child to the property.  */
    snprintf(name, sizeof(name), "child[%d]", kid->index);
    object_property_add_link(OBJECT(bus), name,
                             object_get_typename(OBJECT(child)),
                             (Object **)&kid->child,
                             NULL, /* read-only property */
                             0, /* return ownership on prop deletion */
                             NULL);
}

它将 q35-pcihost device设置为 main-system-bus 的link属性,名为 child[3] (注:前三个是kvmvapic、kvmclock和fw_cfg_io),值为 bus->children中的对应child的地址(指针的指针)。

于是 bus 和 device 之间的关系就通过两个 link 属性关联起来了。

/machine (pc-q35-2.8-machine)
  /unattached (container)
    /sysbus (System) <------------------
                      -------          |
                            | child[3] | parent_bus
                      <------          |
  /q35 (q35-pcihost) -------------------
    /pcie.0 (PCIE)
    /ioapic (ioapic)
    /mch (mch)

如果说设备之间的关系是一棵树,那么多了backlink后设备之间的关系就成为了一幅有向图。

于是我们可以通过路径 /machine/unattached/sysbus/child[3] 访问 q35-pcihost :

(gdb) p object_resolve_path("/machine/unattached/sysbus/child[3]", 0)
$95 = (Object *) 0x55555694ec00
(gdb) p object_resolve_path("/machine/unattached/sysbus/child[3]", 0)->class->type->name
$96 = 0x555556690ae0 "q35-pcihost"

而路径是可以回绕的,于是有这样的玩法:

(gdb) p object_resolve_path("/machine/unattached/sysbus", 0)
$97 = (Object *) 0x555556769d20
(gdb) p object_resolve_path("/machine/unattached/sysbus/child[3]/parent_bus", 0)
$98 = (Object *) 0x555556769d20
(gdb) p object_resolve_path("/machine/unattached/sysbus/child[3]/parent_bus/child[3]/parent_bus", 0)
$99 = (Object *) 0x555556769d20
(gdb) p object_resolve_path("/machine/unattached/sysbus/child[3]/parent_bus/child[3]/parent_bus/child[3]/parent_bus", 0)
$100 = (Object *) 0x555556769d20
...

总结

本文大致介绍了QEMU中"最新"q35架构的组成,同时对设备之间的关系也进行了分析。

接下来将研究QEMU中设备的模拟,包括中断控制器PIC、IOAPIC,以及中断是如何在设备之间传递和路由的。