Skip to the content.

Device Virtualization

设备虚拟化可以分为完全虚拟化和半虚拟化。完全虚拟化就是类似 QEMU 的设备模拟,完全用软件来做,通过 Inter-Virtualization 大致就懂了,这里不再介绍。这篇文章来分析设备透传和 Virtio 虚拟化。

设备透传

SR-IOV(Single Root I/O Virtualization and Sharing),在硬件层面将一个物理设备虚拟出多个设备,每个设备可以透传给一台虚拟机。 SR-IOV 引入了两个新的 function 类型,一个是 PF(Physical Function),一个是 VF(Virtual Function)。一个 SR-IOV 可以支持多个 VF,每个 VF 可以分别透传给 guest,guest 就不用通过 VMM 的模拟设备访问物理设备。每个 VF 都有自己独立的用于数据传输的存储空间、队列、中断以及命令处理单元,即虚拟的物理设备,VMM 通过 PF 管理这些 VF。同时,host 上的软件仍然可以通过 PF 访问物理设备。

虚拟配置空间

guest 访问 VF 的数据不需要经过 VMM,guest 的 I/O 性能提高了,但出于安全考虑,guest 访问 VF 的配置空间时会触发 VM exit 陷入 VMM,这一过程不会影响数据传输效率(但是上下文切换不也降低性能么?注意是配置空间,不是数据)。

在系统启动时,host 的 bios 为 VF 划分了内存地址空间并存储在寄存器 BAR 中,而且 guest 可以直接读取这个信息,但是因为 guest 不能直接访问 host 的物理地址,所以 kvmtool 要将 VF 的 BAR 寄存器中的 HPA 转换为 GPA(初始化的工作),这样 guest 才可以直接访问。之后当 guest 发出对 BAR 对应的内存地址空间的访问时,EPT 或影子页表会将 GPA 翻译为 HPA,PCI 或 Root Complex 将认领这个地址,完成外设读取工作。

DMA 重映射

由于虚拟机在指定设备 DMA 的时候能够随意指定地址,所以需要有一种机制来对设备 DMA 地址访问进行隔离。换句话说,采用设备透传时,如果设备的 DMA 访问没有隔离,该设备就能够访问物理机上所有的地址空间,即 guest 能够访问该设备下其他 guest 和 host 的内存,导致安全隐患。为此,设计了 DMA 重映射机制。

当 VMM 处理透传给 guest 的外设时,VMM 将请求 kernel 为 guest 建立一个页表,这个页表的翻译由 DMA 重映射硬件负责,它限制了外设只能访问这个页面覆盖的内存。当外设访问内存时,内存地址首先到达 DMA 重映射硬件,DMA 重映射硬件根据这个外设的总线号、设备号和功能号确定其对应的页表,查表的出物理内存地址,然后将地址送上总线。

IOMMU

从这幅图看一看出,DMA Remapping 也需要建立类似于 MMU 的页表来完成 DMA 的地址转换。

DMA-Remapping

中断重映射

为避免外设编程发送一些恶意的中断引入了中断虚拟化机制,即在外设和 CPU 之间加了一个硬件中断重映射单元(IOMMU)。当接收到中断时,该单元会对中断请求的来源进行有效性验证,然后以中断号为索引查询中断重映射表,之后代发中断。中断映射表由 VMM 进行设置。

Virtio虚拟化

与完全虚拟化相比,使用 Virtio 协议的驱动和设备模拟的交互不再使用寄存器等传统的 I/O 方式,而是采用了 Virtqueue 的方式来传输数据。这种方式减少了 vm exit 和 vm entry 的次数,提高了设备访问性能。

执行流程

下面以 virtio-blk 为例,简单描述一下 read request 从发出到读到数据的过程。

至此,read request 流程结束。write request 与 read request 的处理流程相同。

virtio

Virtio协议

Virtio 的核心数据结构是 Virtqueue,其是 guest 驱动和 VMM 中模拟设备之间传输数据的载体。一个设备可以有一个 Virtqueue,也可以有多个 Virtqueue。Virtqueue 主要包含 3 个部分:描述符表(vring_desc)、可用描述符区域(vring_avail)和已用描述符表(vring_used)。

/* Virtio ring descriptors: 16 bytes.  These can chain together via "next". */
struct vring_desc {
	/* Address (guest-physical). */
	__virtio64 addr;
	/* Length. */
	__virtio32 len;
	/* The flags as indicated above. */
	__virtio16 flags;
	/* We chain unused descriptors via this, too */
	__virtio16 next;
};

struct vring_avail {
	__virtio16 flags;
	__virtio16 idx;
	__virtio16 ring[];
};

struct vring_used {
	__virtio16 flags;
	__virtio16 idx;
	vring_used_elem_t ring[];
};

每个描述符指向一块内存,该内存保存 guest 写入虚拟设备或虚拟设备写入 guest 的数据。Virtqueue 由 guest 中的驱动负责。

设备直通与 VFIO

PCI设备模拟

这里只介绍 QEMU 是怎样模拟 PCI 设备的,而关于 PCI 设备的基础知识可以看这篇文章。

PCIDevice

老规矩,先看看数据结构。在我看来,虚拟化无非就是定义设备对应的数据结构,初始化它,然后完成对应的操作函数。

QEMU 用 PCIDevice 来完成 PCI 设备的虚拟化。在分析的时候如果觉得 QOM 过于复杂可以忽视它,也不影响对某个模块的模拟,当然最好的方式是理解它,战胜它。

struct PCIDevice {
    DeviceState qdev;
    bool partially_hotplugged;
    bool has_power;

    /* PCI config space */
    uint8_t *config;

    /* Used to enable config checks on load. Note that writable bits are
     * never checked even if set in cmask. */
    uint8_t *cmask; // 用来检测相关功能

    /* Used to implement R/W bytes */
    uint8_t *wmask;

    /* Used to implement RW1C(Write 1 to Clear) bytes */
    uint8_t *w1cmask;

    /* Used to allocate config space for capabilities. */
    uint8_t *used;

    /* the following fields are read only */
    int32_t devfn; // 这个是干嘛的?功能号
    /* Cached device to fetch requester ID from, to avoid the PCI
     * tree walking every time we invoke PCI request (e.g.,
     * MSI). For conventional PCI root complex, this field is
     * meaningless. */
    PCIReqIDCache requester_id_cache;
    char name[64];
    PCIIORegion io_regions[PCI_NUM_REGIONS]; // 这个应该是该设备映射到地址空间的 memory region
    AddressSpace bus_master_as;
    MemoryRegion bus_master_container_region;
    MemoryRegion bus_master_enable_region;

    /* do not access the following fields */
    PCIConfigReadFunc *config_read; // 对应的回调函数
    PCIConfigWriteFunc *config_write;

    /* Legacy PCI VGA regions */
    MemoryRegion *vga_regions[QEMU_PCI_VGA_NUM_REGIONS];
    bool has_vga;

    /* Current IRQ levels.  Used internally by the generic PCI code.  */
    uint8_t irq_state;

    /* Capability bits */
    uint32_t cap_present;

    /* Offset of MSI-X capability in config space */
    uint8_t msix_cap; // MSI(X) 中断方式

    /* MSI-X entries */
    int msix_entries_nr;

    /* Space to store MSIX table & pending bit array */
    uint8_t *msix_table;
    uint8_t *msix_pba;
    /* MemoryRegion container for msix exclusive BAR setup */
    MemoryRegion msix_exclusive_bar;
    /* Memory Regions for MSIX table and pending bit entries. */
    MemoryRegion msix_table_mmio;
    MemoryRegion msix_pba_mmio;
    /* Reference-count for entries actually in use by driver. */
    unsigned *msix_entry_used;
    /* MSIX function mask set or MSIX disabled */
    bool msix_function_masked;
    /* Version id needed for VMState */
    int32_t version_id; // 不是 PCI 配置空间中的域,应该是为了支持虚拟化加的

    /* Offset of MSI capability in config space */
    uint8_t msi_cap;

    /* PCI Express */
    PCIExpressDevice exp;

    /* SHPC */
    SHPCDevice *shpc;

    /* Location of option rom */
    // 设备的 ROM 空间用来干嘛? cache?
    // 有些 PCI 设备具有 ROM 存储空间需要映射到系统内存中
    char *romfile;
    uint32_t romsize;
    bool has_rom;
    MemoryRegion rom;
    uint32_t rom_bar;

    ...
};

关键函数pci_qdev_realize

这个函数完成 PCI 设备的初始化,可以在这个函数中设置断点,就会发现上文中出现的设备会一一调用它进行初始化。下面给出南桥芯片—— piix3 的初始化,从上文的图中可以直到南桥芯片也是挂载在 PCI 根总线上的一个 PCI 设备。

static void pci_qdev_realize(DeviceState *qdev, Error **errp)
{
    PCIDevice *pci_dev = (PCIDevice *)qdev;
    PCIDeviceClass *pc = PCI_DEVICE_GET_CLASS(pci_dev);
    ObjectClass *klass = OBJECT_CLASS(pc);
    Error *local_err = NULL;
    bool is_default_rom;
    uint16_t class_id;

    ...

    // 这个应该是初始化前 64 字节的,在后面分析
    pci_dev = do_pci_register_device(pci_dev,
                                     object_get_typename(OBJECT(qdev)),
                                     pci_dev->devfn, errp);

    // 而这具体的设备的回调函数,应该是用于初始化 64 字节后的数据
    // 后面以南桥芯片的初始化为例分析
    if (pc->realize) {
        pc->realize(pci_dev, &local_err);
    }

	...

    /* rom loading */
    // 有些 PCI 设备有 ROM 存储设备
    is_default_rom = false;
    // QEMU 命令行没有指定 ROM,使用 PCI 设备默认的
    if (pci_dev->romfile == NULL && pc->romfile != NULL) {
        pci_dev->romfile = g_strdup(pc->romfile);
        is_default_rom = true;
    }

    pci_add_option_rom(pci_dev, is_default_rom, &local_err); // 这里会调用 pci_register_bar,继续初始化

    ...

    pci_set_power(pci_dev, true);
}
#0  pci_qdev_realize (qdev=0x555556f67c60, errp=0x7fffffffd550) at ../hw/pci/pci.c:2115
// 这一系列的调用都是 QOM 的实现,可以跳过它
#1  0x0000555555d3d678 in device_set_realized (obj=0x555556f67c60, value=true,
    errp=0x7fffffffd660) at ../hw/core/qdev.c:531
#2  0x0000555555d47703 in property_set_bool (obj=0x555556f67c60, v=0x555556f68870,
    name=0x5555560bc819 "realized", opaque=0x55555681ef50, errp=0x7fffffffd660)
    at ../qom/object.c:2268
#3  0x0000555555d4566e in object_property_set (obj=0x555556f67c60,
    name=0x5555560bc819 "realized", v=0x555556f68870, errp=0x7fffffffd660)
    at ../qom/object.c:1403
#4  0x0000555555d49bd4 in object_property_set_qobject (obj=0x555556f67c60,
    name=0x5555560bc819 "realized", value=0x555556f68780, errp=0x55555677c100 <error_fatal>)
    at ../qom/qom-qobject.c:28
#5  0x0000555555d459e9 in object_property_set_bool (obj=0x555556f67c60,
    name=0x5555560bc819 "realized", value=true, errp=0x55555677c100 <error_fatal>)
    at ../qom/object.c:1472
#6  0x0000555555d3cf0c in qdev_realize (dev=0x555556f67c60, bus=0x555556bc9df0,
    errp=0x55555677c100 <error_fatal>) at ../hw/core/qdev.c:333
#7  0x0000555555d3cf3d in qdev_realize_and_unref (dev=0x555556f67c60, bus=0x555556bc9df0,
    errp=0x55555677c100 <error_fatal>) at ../hw/core/qdev.c:340
#8  0x0000555555a37189 in pci_realize_and_unref (dev=0x555556f67c60, bus=0x555556bc9df0,
    errp=0x55555677c100 <error_fatal>) at ../hw/pci/pci.c:2210
#9  0x0000555555a371d9 in pci_create_simple_multifunction (bus=0x555556bc9df0, devfn=-1,
    multifunction=true, name=0x555555fdf04b "PIIX3") at ../hw/pci/pci.c:2218
// piix3 就是南桥芯片了,负责连接 ISA 等低速设备
#10 0x00005555559b79b8 in piix3_create (pci_bus=0x555556bc9df0, isa_bus=0x7fffffffd880)
    at ../hw/isa/piix3.c:385
#11 0x0000555555b442d1 in pc_init1 (machine=0x555556a2f000,
    host_type=0x555556061704 "i440FX-pcihost", pci_type=0x5555560616fd "i440FX")
    at ../hw/i386/pc_piix.c:209
#12 0x0000555555b44b1d in pc_init_v6_2 (machine=0x555556a2f000) at ../hw/i386/pc_piix.c:425
#13 0x000055555594b889 in machine_run_board_init (machine=0x555556a2f000)
    at ../hw/core/machine.c:1181
#14 0x0000555555c08082 in qemu_init_board () at ../softmmu/vl.c:2652
#15 0x0000555555c082ad in qmp_x_exit_preconfig (errp=0x55555677c100 <error_fatal>)
    at ../softmmu/vl.c:2740
#16 0x0000555555c0a936 in qemu_init (argc=15, argv=0x7fffffffdcb8, envp=0x7fffffffdd38)
    at ../softmmu/vl.c:3775
#17 0x000055555583b6f5 in main (argc=15, argv=0x7fffffffdcb8, envp=0x7fffffffdd38)
    at ../softmmu/main.c:49

当然,还有如下几个重要的设备初始化会调用 pci_qdev_realize,这些设备都是在 pc_init1 中创建的,并挂载在 PCI-HOST 上,后面分析。

#11 0x0000555555a4a68f in i440fx_init (host_type=0x555556061704 "i440FX-pcihost",
    pci_type=0x5555560616fd "i440FX", pi440fx_state=0x7fffffffd9f8,
    address_space_mem=0x5555568220c0, address_space_io=0x555556821fb0, ram_size=8589934592,
    below_4g_mem_size=3221225472, above_4g_mem_size=5368709120,
    pci_address_space=0x555556a5a030, ram_memory=0x5555569a2af0)
    at ../hw/pci-host/i440fx.c:266

#10 0x00005555559b79b8 in piix3_create (pci_bus=0x555556bc9df0, isa_bus=0x7fffffffd9f0)
    at ../hw/isa/piix3.c:385

#11 0x0000555555a369de in pci_vga_init (bus=0x555556bc9df0) at ../hw/pci/pci.c:1998
#12 0x0000555555b602cb in pc_vga_init (isa_bus=0x555556a5b470, pci_bus=0x555556bc9df0)
    at ../hw/i386/pc.c:981

#10 0x0000555555b60975 in pc_nic_init (pcmc=0x55555686d020, isa_bus=0x555556a5b470,
    pci_bus=0x555556bc9df0) at ../hw/i386/pc.c:1143

// 这个应该是硬盘
#10 0x0000555555a37211 in pci_create_simple (bus=0x555556bc9df0, devfn=9,
    name=0x5555560616b1 "piix3-ide") at ../hw/pci/pci.c:2224

// 这个我不知道是哪个设备
#9  0x0000555555912fa4 in piix4_pm_init (bus=0x555556bc9df0, devfn=11, smb_io_base=45312,
    sci_irq=0x555556bc4120, smi_irq=0x555557836ea0, smm_enabled=1, piix4_pm=0x7fffffffda10)
    at ../hw/acpi/piix4.c:549

关键函数do_pci_register_device

该函数完成设备及其对应 PCI 总线上的一些初始化工作。

/* -1 for devfn means auto assign */
static PCIDevice *do_pci_register_device(PCIDevice *pci_dev,
                                         const char *name, int devfn,
                                         Error **errp)
{
    PCIDeviceClass *pc = PCI_DEVICE_GET_CLASS(pci_dev);
    PCIConfigReadFunc *config_read = pc->config_read; // 不同设备对应的回调函数
    PCIConfigWriteFunc *config_write = pc->config_write;
    Error *local_err = NULL;
    DeviceState *dev = DEVICE(pci_dev);
    PCIBus *bus = pci_get_bus(pci_dev);

    ...

    // 如果指定的 devfn 为 -1,表示由总线自己选择插槽(?)
    // 得到插槽号后保存在 PCIDevice->devfn 中,如果在设备命令行中指定了 addr
    // 则 addr 会作为设备的 devfn。
    if (devfn < 0) {
        for(devfn = bus->devfn_min ; devfn < ARRAY_SIZE(bus->devices);
            devfn += PCI_FUNC_MAX) {
            if (pci_bus_devfn_available(bus, devfn) &&
                   !pci_bus_devfn_reserved(bus, devfn)) { // 分配设备号
                goto found;
            }
        }
    found: ;
    }

    ...

    pci_dev->devfn = devfn;
    pci_dev->requester_id_cache = pci_req_id_cache_get(pci_dev);
    pstrcpy(pci_dev->name, sizeof(pci_dev->name), name);

    memory_region_init(&pci_dev->bus_master_container_region, OBJECT(pci_dev),
                       "bus master container", UINT64_MAX);
    address_space_init(&pci_dev->bus_master_as,
                       &pci_dev->bus_master_container_region, pci_dev->name);

    if (phase_check(PHASE_MACHINE_READY)) {
        pci_init_bus_master(pci_dev);
    }
    pci_dev->irq_state = 0;
    // 使用 g_malloc0 为 config, cmask, wmask, w1cmask, used 分配空间
    pci_config_alloc(pci_dev);

    // 这部分就很好理解了
    pci_config_set_vendor_id(pci_dev->config, pc->vendor_id);
    pci_config_set_device_id(pci_dev->config, pc->device_id);
    pci_config_set_revision(pci_dev->config, pc->revision);
    pci_config_set_class(pci_dev->config, pc->class_id);

    if (!pc->is_bridge) {
        if (pc->subsystem_vendor_id || pc->subsystem_id) { // 需要设置这两个域
            pci_set_word(pci_dev->config + PCI_SUBSYSTEM_VENDOR_ID,
                         pc->subsystem_vendor_id); // 通过写对应的位来配置
            pci_set_word(pci_dev->config + PCI_SUBSYSTEM_ID,
                         pc->subsystem_id);
        } else {
            pci_set_default_subsystem_id(pci_dev); // 默认配置
        }
    } else {
        /* subsystem_vendor_id/subsystem_id are only for header type 0 */
        assert(!pc->subsystem_vendor_id);
        assert(!pc->subsystem_id);
    }
    // 具体的配置,应该跟架构相关,这里先不关注
    pci_init_cmask(pci_dev);
    pci_init_wmask(pci_dev);
    pci_init_w1cmask(pci_dev);
    if (pc->is_bridge) {
        pci_init_mask_bridge(pci_dev);
    }
    // 这个是干嘛的
    pci_init_multifunction(bus, pci_dev, &local_err);

    ...

    // 这些都很好理解
    if (!config_read)
        config_read = pci_default_read_config;
    if (!config_write)
        config_write = pci_default_write_config;
    pci_dev->config_read = config_read;
    pci_dev->config_write = config_write;
    bus->devices[devfn] = pci_dev; // 这样 bus 就可以找到对应的设备
    pci_dev->version_id = 2; /* Current pci device vmstate version */
    return pci_dev;
}

关键函数piix3_realize

这里还是拿南桥芯片举例,其对应的回调函数是 piix3_realize

static void piix3_realize(PCIDevice *dev, Error **errp)
{
    PIIX3State *d = PIIX3_PCI_DEVICE(dev);

    if (!isa_bus_new(DEVICE(d), get_system_memory(),
                     pci_address_space_io(dev), errp)) {
        return;
    }

    // 初始化该设备的 MMIO 空间,1 为 1M 空间
    memory_region_init_io(&d->rcr_mem, OBJECT(dev), &rcr_ops, d,
                          "piix3-reset-control", 1);
    memory_region_add_subregion_overlap(pci_address_space_io(dev),
                                        PIIX_RCR_IOPORT, &d->rcr_mem, 1);

    qemu_register_reset(piix3_reset, d);
}

piix3_reset 用硬编码的方式设置 64 字节后的数据。

关键函数pci_register_bar

pci_qdev_realize -> pci_add_option_rom -> pci_register_bar

继续配置 PCI 设备空间。

void pci_register_bar(PCIDevice *pci_dev, int region_num,
                      uint8_t type, MemoryRegion *memory)
{
    PCIIORegion *r;
    uint32_t addr; /* offset in pci config space */
    uint64_t wmask;
    pcibus_t size = memory_region_size(memory);
    uint8_t hdr_type;

    assert(region_num >= 0);
    assert(region_num < PCI_NUM_REGIONS);
    assert(is_power_of_2(size));

    /* A PCI bridge device (with Type 1 header) may only have at most 2 BARs */
    hdr_type =
        pci_dev->config[PCI_HEADER_TYPE] & ~PCI_HEADER_TYPE_MULTI_FUNCTION;
    assert(hdr_type != PCI_HEADER_TYPE_BRIDGE || region_num < 2);

    r = &pci_dev->io_regions[region_num];
    r->addr = PCI_BAR_UNMAPPED;
    r->size = size;
    r->type = type;
    r->memory = memory;
    r->address_space = type & PCI_BASE_ADDRESS_SPACE_IO
                        ? pci_get_bus(pci_dev)->address_space_io
                        : pci_get_bus(pci_dev)->address_space_mem;

    wmask = ~(size - 1);
    if (region_num == PCI_ROM_SLOT) {
        /* ROM enable bit is writable */
        wmask |= PCI_ROM_ADDRESS_ENABLE;
    }

    addr = pci_bar(pci_dev, region_num);
    pci_set_long(pci_dev->config + addr, type);

    if (!(r->type & PCI_BASE_ADDRESS_SPACE_IO) &&
        r->type & PCI_BASE_ADDRESS_MEM_TYPE_64) {
        pci_set_quad(pci_dev->wmask + addr, wmask);
        pci_set_quad(pci_dev->cmask + addr, ~0ULL);
    } else {
        pci_set_long(pci_dev->wmask + addr, wmask & 0xffffffff);
        pci_set_long(pci_dev->cmask + addr, 0xffffffff);
    }
}

关键函数piix3_write_config

接下来我们看看南桥的读写回调函数。

好吧,南桥芯片的读写和我想的不一样,它只有一个写函数,

static void piix3_write_config(PCIDevice *dev,
                               uint32_t address, uint32_t val, int len)
{
    pci_default_write_config(dev, address, val, len);
    if (ranges_overlap(address, len, PIIX_PIRQCA, 4)) {
        PIIX3State *piix3 = PIIX3_PCI_DEVICE(dev);
        int pic_irq;

        pci_bus_fire_intx_routing_notifier(pci_get_bus(&piix3->dev));
        piix3_update_irq_levels(piix3);
        for (pic_irq = 0; pic_irq < PIIX_NUM_PIC_IRQS; pic_irq++) {
            piix3_set_irq_pic(piix3, pic_irq);
        }
    }
}

并且使用了 PCI 设备默认的读写函数,使用 msi 机制进行读写,这个之后再分析。

void pci_default_write_config(PCIDevice *d, uint32_t addr, uint32_t val_in, int l)
{
    int i, was_irq_disabled = pci_irq_disabled(d);
    uint32_t val = val_in;

    assert(addr + l <= pci_config_size(d));

    for (i = 0; i < l; val >>= 8, ++i) {
        uint8_t wmask = d->wmask[addr + i];
        uint8_t w1cmask = d->w1cmask[addr + i];
        assert(!(wmask & w1cmask));
        d->config[addr + i] = (d->config[addr + i] & ~wmask) | (val & wmask);
        d->config[addr + i] &= ~(val & w1cmask); /* W1C: Write 1 to Clear */
    }

    ...

    msi_write_config(d, addr, val_in, l);
    msix_write_config(d, addr, val_in, l);
}

这就是一个普通的 PCI 设备的初始化和访问过程。

PCI设备我们讲到 HOST-PCI 桥完成 CPU 访问的内存地址到 PCI 总线地址的转换,接下来我们分析一下 HOST-PCI 是怎样初始化和访问的。

PCIHostState

北桥的 PCI 部分由结构体 PCIHostState 表示,其中有两个关键的寄存器:CONFGADDRCONFGDATA,系统需要访问 PCI 设备的配置空间都是通过这两个寄存器。将需要访问的 PCI 设备的 dfn 写入 CONFGADDR,操作命令写入 CONFGDATA,PCI-HOST 桥(北桥)就会将命令分发给对应的 PCI 设备。

struct PCIHostState {
    SysBusDevice busdev;

    MemoryRegion conf_mem; // 配置地址寄存器
    MemoryRegion data_mem; // 配置数据寄存器
    MemoryRegion mmcfg;
    uint32_t config_reg;
    bool mig_enabled;
    PCIBus *bus;
    bool bypass_iommu;

    QLIST_ENTRY(PCIHostState) next;
};

关键函数i440fx_init

这个函数主要是初始化 i440fx 本身,然后设置映射信息。

PCIBus *i440fx_init(const char *host_type, const char *pci_type,
                    PCII440FXState **pi440fx_state,
                    MemoryRegion *address_space_mem,
                    MemoryRegion *address_space_io,
                    ram_addr_t ram_size,
                    ram_addr_t below_4g_mem_size,
                    ram_addr_t above_4g_mem_size,
                    MemoryRegion *pci_address_space,
                    MemoryRegion *ram_memory)
{
    DeviceState *dev;
    PCIBus *b;
    PCIDevice *d;
    PCIHostState *s;
    PCII440FXState *f;
    unsigned i;
    I440FXState *i440fx;

    dev = qdev_new(host_type); // 会执行到 i440fx_pcihost_initfn,这些后面都会分析
    s = PCI_HOST_BRIDGE(dev);
    b = pci_root_bus_new(dev, NULL, pci_address_space, // 初始化 PCIBus
                         address_space_io, 0, TYPE_PCI_BUS);
    s->bus = b;
    object_property_add_child(qdev_get_machine(), "i440fx", OBJECT(dev));
    sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal);

    d = pci_create_simple(b, 0, pci_type); // 执行到 pci_qdev_realize,上文分析过
    *pi440fx_state = I440FX_PCI_DEVICE(d);
    f = *pi440fx_state;
    f->system_memory = address_space_mem;
    f->pci_address_space = pci_address_space;
    f->ram_memory = ram_memory;

    i440fx = I440FX_PCI_HOST_BRIDGE(dev);
    range_set_bounds(&i440fx->pci_hole, below_4g_mem_size,
                     IO_APIC_DEFAULT_ADDRESS - 1);

  	... // 建立映射信息

    init_pam(dev, f->ram_memory, f->system_memory, f->pci_address_space,
             &f->pam_regions[0], PAM_BIOS_BASE, PAM_BIOS_SIZE);
    for (i = 0; i < ARRAY_SIZE(f->pam_regions) - 1; ++i) {
        init_pam(dev, f->ram_memory, f->system_memory, f->pci_address_space,
                 &f->pam_regions[i+1], PAM_EXPAN_BASE + i * PAM_EXPAN_SIZE,
                 PAM_EXPAN_SIZE);
    }

    ram_size = ram_size / 8 / 1024 / 1024;
    if (ram_size > 255) {
        ram_size = 255;
    }
    d->config[I440FX_COREBOOT_RAM_SIZE] = ram_size;

    i440fx_update_memory_mappings(f);

    return b;
}

对于 PCI-HOST 的分析应该关注怎样以它为起点构建一个 PCI 树结构,主要的 PCI 设备都是在 pc_init1 中创建和初始化的。

/* PC hardware initialisation */
static void pc_init1(MachineState *machine,
                     const char *host_type, const char *pci_type)
{
    ...

    if (pcmc->pci_enabled) {
        PIIX3State *piix3;

        pci_bus = i440fx_init(host_type, // 1. 创建 PCI-HOST bridge
                              pci_type,
                              &i440fx_state,
                              system_memory, system_io, machine->ram_size,
                              x86ms->below_4g_mem_size,
                              x86ms->above_4g_mem_size,
                              pci_memory, ram_memory);
        pcms->bus = pci_bus;

        piix3 = piix3_create(pci_bus, &isa_bus); // 2. 创建 piix3,并和 i440fx 建立联系
        piix3->pic = x86ms->gsi;
        piix3_devfn = piix3->dev.devfn;
    }

    ...

    pc_vga_init(isa_bus, pcmc->pci_enabled ? pci_bus : NULL); // 3. 创建 vga

    ...

    pc_nic_init(pcmc, isa_bus, pci_bus); // 4. 创建网卡

    if (pcmc->pci_enabled) {
        PCIDevice *dev;

        dev = pci_create_simple(pci_bus, piix3_devfn + 1, // 5. 创建 piix3-ide
                                xen_enabled() ? "piix3-ide-xen" : "piix3-ide");
        pci_ide_create_devs(dev);
        idebus[0] = qdev_get_child_bus(&dev->qdev, "ide.0");
        idebus[1] = qdev_get_child_bus(&dev->qdev, "ide.1");
        pc_cmos_init(pcms, idebus[0], idebus[1], rtc_state);
    }

    ...

    if (pcmc->pci_enabled && x86_machine_is_acpi_enabled(X86_MACHINE(pcms))) {
        DeviceState *piix4_pm;

        smi_irq = qemu_allocate_irq(pc_acpi_smi_interrupt, first_cpu, 0);
        /* TODO: Populate SPD eeprom data.  */
        pcms->smbus = piix4_pm_init(pci_bus, piix3_devfn + 3, 0xb100, // 6. 创建 piix4_pm
                                    x86ms->gsi[9], smi_irq,
                                    x86_machine_is_smm_enabled(x86ms),
                                    &piix4_pm);

        ...
    }

    ...
}

上面我们也看到了,这些设备都会调用 pci_qdev_realize 完成初始化。

我们知道 CPU 是通过读写地址寄存器和数据寄存器来访问 PCI 设备的,而地址寄存器对应的物理地址就是 0xcf8,数据寄存器是 0xcfc,这两个寄存器分别是 PCIHostState -> conf_memPCIHostState -> data_mem,它们在 i440fx_pcihost_initfn 中初始化。

static void i440fx_pcihost_initfn(Object *obj)
{
    PCIHostState *s = PCI_HOST_BRIDGE(obj);

    memory_region_init_io(&s->conf_mem, obj, &pci_host_conf_le_ops, s,
                          "pci-conf-idx", 4); // 这里只是创建一个 memory region 并指定回调函数
    memory_region_init_io(&s->data_mem, obj, &pci_host_data_le_ops, s,
                          "pci-conf-data", 4);
}
static void i440fx_pcihost_realize(DeviceState *dev, Error **errp)
{
    PCIHostState *s = PCI_HOST_BRIDGE(dev);
    SysBusDevice *sbd = SYS_BUS_DEVICE(dev);

    sysbus_add_io(sbd, 0xcf8, &s->conf_mem); // 将 conf_mem 和 0xcf8 关联起来。这种设计!!!需要学习
    sysbus_init_ioports(sbd, 0xcf8, 4);

    // 当然,这里整个实现和 QEMU 的 memory region 强相关,BMBT 可以不这样实现
    sysbus_add_io(sbd, 0xcfc, &s->data_mem);
    sysbus_init_ioports(sbd, 0xcfc, 4);

    /* register i440fx 0xcf8 port as coalesced pio */
    memory_region_set_flush_coalesced(&s->data_mem); // 每次更改内存结构都需要 commit
    memory_region_add_coalescing(&s->conf_mem, 0, 4);
}

这两段地址空间其实就是北桥芯片的一部分,就像 PAM 技术一样,将前 1M 地址空间声明为 rom 空间,这里将这两段地址空间声明为 PCI 寄存器,同时指定了回调函数,

const MemoryRegionOps pci_host_conf_le_ops = {
    .read = pci_host_config_read,
    .write = pci_host_config_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
};

const MemoryRegionOps pci_host_data_le_ops = {
    .read = pci_host_data_read,
    .write = pci_host_data_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
};

PCI设备读写

读写的原理相同,这里只看看写过程。pci_host_config_write 将 guest 需要访问的 PCI 设备地址保存在 PCIHostState -> config_reg 中。

static void pci_host_config_write(void *opaque, hwaddr addr,
                                  uint64_t val, unsigned len)
{
    PCIHostState *s = opaque;

    PCI_DPRINTF("%s addr " TARGET_FMT_plx " len %d val %"PRIx64"\n",
                __func__, addr, len, val);
    if (addr != 0 || len != 4) {
        return;
    }
    s->config_reg = val;
}

配置地址寄存器的访问必须是 4 字节的。通过 intel 手册知道其第 31 位表示是否使能 PCI 设备的配置功能,如果想要读写 PCI 设备的配置空间,需要将该位置为 1,24 ~30 位为保留位,16 ~ 23 位表示 PCI 总线号,11 ~ 15 位表示该总线上的设备号,8 ~ 10 表示对应设备的功能号,2 ~ 7 位表示需要写入对应设备配置空间寄存器的值,0 ~ 1 位为保留位。

static void pci_host_data_write(void *opaque, hwaddr addr,
                                uint64_t val, unsigned len)
{
    PCIHostState *s = opaque;

    if (s->config_reg & (1u << 31)) // 首先判断是否可以读写配置空间
        pci_data_write(s->bus, s->config_reg | (addr & 3), val, len);
}
void pci_data_write(PCIBus *s, uint32_t addr, uint32_t val, unsigned len)
{
    PCIDevice *pci_dev = pci_dev_find_by_addr(s, addr); // 找到需要访问的 PCI 设备
    uint32_t config_addr = addr & (PCI_CONFIG_SPACE_SIZE - 1);

    pci_host_config_write_common(pci_dev, config_addr, PCI_CONFIG_SPACE_SIZE,
                                 val, len); // 读写该设备的配置空间
}
void pci_host_config_write_common(PCIDevice *pci_dev, uint32_t addr,
                                  uint32_t limit, uint32_t val, uint32_t len)
{
    pci_adjust_config_limit(pci_get_bus(pci_dev), &limit);

    ...

    // 不同设备自己的读写回调函数,如 i440fx 就是 i440fx_write_config
    // i440fx 和 piix3 一样,都是使用 pci_default_write_config
    pci_dev->config_write(pci_dev, addr, val, MIN(len, limit - addr));
}

我们看看整个写的过程,

#0  pci_host_config_write (opaque=0x555556bc8d00, addr=0, val=2147483648, len=4)
    at ../hw/pci/pci_host.c:142
#1  0x0000555555bf49bb in memory_region_write_accessor (mr=0x555556bc9020, addr=0,
    value=0x7ffff228f3f8, size=4, shift=0, mask=4294967295, attrs=...)
    at ../softmmu/memory.c:492
#2  0x0000555555bf4c09 in access_with_adjusted_size (addr=0, value=0x7ffff228f3f8, size=4,
    access_size_min=1, access_size_max=4,
    access_fn=0x555555bf48c1 <memory_region_write_accessor>, mr=0x555556bc9020, attrs=...)
    at ../softmmu/memory.c:554
#3  0x0000555555bf7d07 in memory_region_dispatch_write (mr=0x555556bc9020, addr=0,
    data=2147483648, op=MO_32, attrs=...) at ../softmmu/memory.c:1504
#4  0x0000555555beaa5c in flatview_write_continue (fv=0x5555578e0580, addr=3320, attrs=...,
    ptr=0x7ffff7fc7000, len=4, addr1=0, l=4, mr=0x555556bc9020) at ../softmmu/physmem.c:2782
#5  0x0000555555beaba5 in flatview_write (fv=0x5555578e0580, addr=3320, attrs=...,
    buf=0x7ffff7fc7000, len=4) at ../softmmu/physmem.c:2822
#6  0x0000555555beaf1f in address_space_write (as=0x55555675cda0 <address_space_io>,
    addr=3320, attrs=..., buf=0x7ffff7fc7000, len=4) at ../softmmu/physmem.c:2914
#7  0x0000555555beaf90 in address_space_rw (as=0x55555675cda0 <address_space_io>, addr=3320,
    attrs=..., buf=0x7ffff7fc7000, len=4, is_write=true) at ../softmmu/physmem.c:2924
#8  0x0000555555d0b58e in kvm_handle_io (port=3320, attrs=..., data=0x7ffff7fc7000,
    direction=1, size=4, count=1) at ../accel/kvm/kvm-all.c:2642
#9  0x0000555555d0bd3d in kvm_cpu_exec (cpu=0x555556a4e730) at ../accel/kvm/kvm-all.c:2893
#10 0x0000555555d0dc13 in kvm_vcpu_thread_fn (arg=0x555556a4e730)
    at ../accel/kvm/kvm-accel-ops.c:49
#11 0x0000555555ef8857 in qemu_thread_start (args=0x555556a5c920)
    at ../util/qemu-thread-posix.c:556
#12 0x00007ffff6753609 in start_thread () from /lib/x86_64-linux-gnu/libpthread.so.0
#13 0x00007ffff6678163 in clone () from /lib/x86_64-linux-gnu/libc.so.6

OK,但是这里还有一个问题,通过 pci_default_write_config 访问设备时是怎样知道不同设备的配置空间的地址?也就是说 PCI-HOST 怎样将读写请求转发到对应的 PCI 设备?

在每个 PCI 设备初始化的时候会将 devfn 写入 PCI-HOST bridge,然后 guest 或 Seabios 访问设备的配置空间是通过 dfn,这样 PCI-HOST bridge 就可以转发。但这只是配置空间,还有 MMIO 空间也需要配置,这个工作是 Seabios 完成的,可以看这里的分析。

总的来说是 BIOS 会探测 PCI 设备,然后按照将各个 PCI 设备的总线号、设备号、功能号(?)将 BAR 地址写入 i440fx 的配置空间(0xcfc),之后 QEMU 就能根据这些信息建立地址映射。当然 QEMU 工作在用户态,不能直接访问物理设备,这些访问最终会通过系统调用发送给 host 进行处理。

关键函数pci_default_write_config

上面分析到会初始化的几个 PCI 设备都是使用这个作为默认读写函数。

void pci_default_write_config(PCIDevice *d, uint32_t addr, uint32_t val_in, int l)
{
    int i, was_irq_disabled = pci_irq_disabled(d);
    uint32_t val = val_in;

    assert(addr + l <= pci_config_size(d));

    for (i = 0; i < l; val >>= 8, ++i) {
        uint8_t wmask = d->wmask[addr + i];
        uint8_t w1cmask = d->w1cmask[addr + i];
        assert(!(wmask & w1cmask));
        d->config[addr + i] = (d->config[addr + i] & ~wmask) | (val & wmask);
        d->config[addr + i] &= ~(val & w1cmask); /* W1C: Write 1 to Clear */
    }

    // 如果是读写 BAR,需要修改映射信息
    if (ranges_overlap(addr, l, PCI_BASE_ADDRESS_0, 24) ||
        ranges_overlap(addr, l, PCI_ROM_ADDRESS, 4) ||
        ranges_overlap(addr, l, PCI_ROM_ADDRESS1, 4) ||
        range_covers_byte(addr, l, PCI_COMMAND))
        pci_update_mappings(d);

    if (range_covers_byte(addr, l, PCI_COMMAND)) {
        pci_update_irq_disabled(d, was_irq_disabled);
        memory_region_set_enabled(&d->bus_master_enable_region,
                                  (pci_get_word(d->config + PCI_COMMAND)
                                   & PCI_COMMAND_MASTER) && d->has_power);
    }

    msi_write_config(d, addr, val_in, l); // 使用 msi 中断,不用 apic 中断
    msix_write_config(d, addr, val_in, l);
}

然后在 i440fx_init 就会调用 pci_update_mappings 建立对应 memory region。建立起 memory region 之后 guest 访问这些 memory region 就是访问对应的 PCI 设备。

#0  pci_update_mappings (d=0x555556c01e80) at ../hw/pci/pci.c:1375
#1  0x0000555555a38d7c in pci_set_power (d=0x555556c01e80, state=true) at ../hw/pci/pci.c:2868
#2  0x0000555555a370b1 in pci_qdev_realize (qdev=0x555556c01e80, errp=0x7fffffffd610)
    at ../hw/pci/pci.c:2189
/*
 * QOM 相关
 */
#11 0x0000555555a371d9 in pci_create_simple_multifunction (bus=0x555556bc9df0, devfn=0,
    multifunction=false, name=0x5555560616fd "i440FX") at ../hw/pci/pci.c:2218
#12 0x0000555555a37211 in pci_create_simple (bus=0x555556bc9df0, devfn=0,
    name=0x5555560616fd "i440FX") at ../hw/pci/pci.c:2224
#13 0x0000555555a4a68f in i440fx_init (host_type=0x555556061704 "i440FX-pcihost",
    pci_type=0x5555560616fd "i440FX", pi440fx_state=0x7fffffffd9f8,
    address_space_mem=0x5555568220c0, address_space_io=0x555556821fb0, ram_size=8589934592,
    below_4g_mem_size=3221225472, above_4g_mem_size=5368709120,
    pci_address_space=0x555556a5a030, ram_memory=0x5555569a2af0)
    at ../hw/pci-host/i440fx.c:266
#14 0x0000555555b44298 in pc_init1 (machine=0x555556a2f000,
    host_type=0x555556061704 "i440FX-pcihost", pci_type=0x5555560616fd "i440FX")
    at ../hw/i386/pc_piix.c:200

    ...

pci_update_mappings 之后有需要再分析。

PCI设备中断模拟

现在大多使用 MSI(X) 中断,APIC 中断方式就顺带分析一下。MSI(X) 中断方式的分析可以看这里

接下来我们看看 PCI 设备是怎样发起中断的。前面讲到每个 PCI 设备有 4 个中断引脚,单功能设备都用 INTA 触发中断,之后多功能设备会用到 INTB、INTC、INTD。PCI 设备配置空间的 IRQ Pin 域表示使用哪个引脚,这在每个设备初始化的时候会设置。如 e1000,

static void pci_e1000_realize(PCIDevice *pci_dev, Error **errp)
{
    ...

    pci_dev->config_write = e1000_write_config;

    pci_conf = pci_dev->config;

    /* TODO: RST# value should be 0, PCI spec 6.2.4 */
    pci_conf[PCI_CACHE_LINE_SIZE] = 0x10;

    pci_conf[PCI_INTERRUPT_PIN] = 1; /* interrupt pin A */

	...
}

PCI 总线上能够挂载多个设备,而通常中断控制器的中断线是有限的,加上一些中断线已经分配给其他设备,所以通常留给 PCI 设备的中断线只有 4 个或 8 个。将 PCI 设备使用的中断引脚与中断控制器的中断线关联起来,通常叫做 PCI 设备的中断路由。

PCI 设备中断路由涉及 3 个概念:

包括 2 个部分:

触发中断

接下来看看 QEMU 是怎样触发 PCI 中断的。

pc_init1 -> piix3_create

PIIX3State *piix3_create(PCIBus *pci_bus, ISABus **isa_bus)
{
    PIIX3State *piix3;
    PCIDevice *pci_dev;

    if (xen_enabled()) {

        ...

    } else {
        pci_dev = pci_create_simple_multifunction(pci_bus, -1, true,
                                                  TYPE_PIIX3_DEVICE);
        piix3 = PIIX3_PCI_DEVICE(pci_dev);
        pci_bus_irqs(pci_bus, piix3_set_irq, pci_slot_get_pirq,
                     piix3, PIIX_NUM_PIRQS); // PCI 总线的中断路由设置
        pci_bus_set_route_irq_fn(pci_bus, piix3_route_intx_pin_to_irq);
    }
    *isa_bus = ISA_BUS(qdev_get_child_bus(DEVICE(piix3), "isa.0"));

    return piix3;
}

这里有些问题,为什么只有 piix3 会设置中断,i440fx 没有呢?很多设备共用一条中断线,中断控制器怎样识别是哪个 PCI 设备发起中断的呢?看来不只是拉高电平,还有其他的信息。对 PCI 的掌握还不全面,昨天觉得就是这么回事,今天再看,有些地方又模糊了。

static void piix3_set_irq_pic(PIIX3State *piix3, int pic_irq)
{
    qemu_set_irq(piix3->pic[pic_irq], // 最终通过 qemu_set_irq 发起中断
                 !!(piix3->pic_levels &
                    (((1ULL << PIIX_NUM_PIRQS) - 1) <<
                     (pic_irq * PIIX_NUM_PIRQS))));
}

static void piix3_set_irq_level_internal(PIIX3State *piix3, int pirq, int level)
{
    int pic_irq;
    uint64_t mask;

    pic_irq = piix3->dev.config[PIIX_PIRQCA + pirq];
    if (pic_irq >= PIIX_NUM_PIC_IRQS) {
        return;
    }

    mask = 1ULL << ((pic_irq * PIIX_NUM_PIRQS) + pirq);
    piix3->pic_levels &= ~mask;
    piix3->pic_levels |= mask * !!level;
}

static void piix3_set_irq_level(PIIX3State *piix3, int pirq, int level)
{
    int pic_irq;

    pic_irq = piix3->dev.config[PIIX_PIRQCA + pirq];
    if (pic_irq >= PIIX_NUM_PIC_IRQS) {
        return;
    }

    piix3_set_irq_level_internal(piix3, pirq, level);

    piix3_set_irq_pic(piix3, pic_irq);
}

static void piix3_set_irq(void *opaque, int pirq, int level) // 这个是 piix 触发中断
{
    PIIX3State *piix3 = opaque;
    piix3_set_irq_level(piix3, pirq, level);
}
获取LNK

该函数得到设备连接到的 PCI 链接设备(LNK),然后再通过上面的 piix3_set_irq 触发中断。

/*
 * 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_SLOT(pci_dev->devfn) - 1;
    return (pci_intx + slot_addend) & 3;
}

网卡模拟

reference

[1] https://www.cxybb.com/article/gong0791/79578316

[2] QEMU/KVM 源码解析与应用 李强 机械工业出版社