QEMU pcie config空间访问机制

一、PCIE config空间

pci设备的config空间只有256字节,X86架构下是通过两个IO端口访问的,0xCF8/0xCFC端口,分别用于选通地址和传输数据。当前大部分设备都是pcie设备,config空间扩展到了4KB,而对于[256-4096)的扩展config空间,X86是通过memory映射的方式访问,并非IO端口的形式。也就是X86会把pcie的config空间映射到一片memory空间,访问这片空间的时候RC就会发出config tlp报文。

这是真实的硬件设计,而对于QEMU+KVM的虚机场景,显然是要基于硬件实现和虚拟化的需求设计虚机访问config空间的完整流程。

文章对于qemu的虚机memory管理机制不单独介绍。

二、config空间(前256B)的模拟

这一部分的模拟,首先从hw/pci-host/q35.c查看。q35是一种常用的X86机型,当前qemu里的X86机型都是使用q35来模拟。

qemu-system-x86_64 -machine q35,accel=kvm -cpu host -smp sockets=2,cores=2,threads=1 -m 4096

具体的q35的machine模拟初始化接口,里面代码很多,我们只关注pci host主桥的初始化。在pc_q35_init接口里直接调用qdev_new()接口(非命令行显式创建)实例化一个host主桥,设备类型为TYPE_Q35_HOST_DEVICE,这个设备的驱动定义在hw/pci-host/q35.c里。

/* PC hardware initialisation */
static void pc_q35_init(MachineState *machine)
{
    /* create pci host bus */
    q35_host = Q35_HOST_DEVICE(qdev_new(TYPE_Q35_HOST_DEVICE));
}

在TYPE_Q35_HOST_DEVICE的realize接口中,对0xCF8/0xCFC两个端口进行了注册,注册到conf_mem和data_mem两个MemoryRegion里。

static void q35_host_realize(DeviceState *dev, Error **errp)
{
    sysbus_add_io(sbd, MCH_HOST_BRIDGE_CONFIG_ADDR, &pci->conf_mem);
    sysbus_init_ioports(sbd, MCH_HOST_BRIDGE_CONFIG_ADDR, 4);

    sysbus_add_io(sbd, MCH_HOST_BRIDGE_CONFIG_DATA, &pci->data_mem);
    sysbus_init_ioports(sbd, MCH_HOST_BRIDGE_CONFIG_DATA, 4);
}

在TYPE_Q35_HOST_DEVICE的instance_init接口中,添加了0xCF8/0xCFC端口对应的MemoryRegion的处理函数。


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,
};

    memory_region_init_io(&phb->conf_mem, obj, &pci_host_conf_le_ops, phb,
                          "pci-conf-idx", 4);
    memory_region_init_io(&phb->data_mem, obj, &pci_host_data_le_ops, phb,
                          "pci-conf-data", 4);

而pci_host_conf/data_le_ops的定义是在hw/pci/pci-host.c里,对于0xCF8端口的处理,因为是地址选通,所以只需要在write的时候记录、read的时候返回记录的值即可。

对于0xCFC端口的处理逻辑,则是实际的数据访问,访问地址就存储在0xCF8端口write操作记录的地址里。重点贴一下pci_dev_find_by_addr的代码,这个接口体现了啥呢,0xCF8端口地址的组织形式,包含了bdf号+config空间offset。所以可以根据这个地址获取到bdf号,然后根据bdf号去找到PCIDevice结构。

找到PCIDevice结构后,pci_host_config_write_common接口可以获取到config空间的内容。这里就不再赘述了。pci_default_write_config和pci_default_read_config接口是PCI_DEV层默认的config read和write接口。当然具体的设备也可以根据实际的需求设置自己的read/write接口。

/*
 * PCI address
 * bit 16 - 24: bus number
 * bit  8 - 15: devfun number
 * bit  0 -  7: offset in configuration space of a given pci device
 */

/* the helper function to get a PCIDevice* for a given pci address */
static inline PCIDevice *pci_dev_find_by_addr(PCIBus *bus, uint32_t addr)
{
    uint8_t bus_num = addr >> 16;
    uint8_t devfn = addr >> 8;

    return pci_find_device(bus, bus_num, devfn);
}

void pci_data_write(PCIBus *s, uint32_t addr, uint32_t val, unsigned len)
{
    uint8_t bus_num = 0xff;
    uint8_t devfn = 0xff;
    uint32_t bdf = 0;
    uint8_t tlp_type = 0xff;

    PCIDevice *pci_dev = pci_dev_find_by_addr(s, addr);
    uint32_t config_addr = addr & (PCI_CONFIG_SPACE_SIZE - 1);
    if (!pci_dev) {
        return;
    }

   pci_host_config_write_common(pci_dev, config_addr, PCI_CONFIG_SPACE_SIZE,
                                 val, len);
}

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

根据bdf号查到PCIDevice的路径也顺便记录一下。其实大概可以看出QEMU里PCIBus、PCIDevice的基于BDF号的树状组织结构。每个PCIBus都会有child链表,遍历的时候都是向下遍历,从PCIHostState里指向的root bus向下遍历,直到找到对应bus number的bus。找到准确的PCIBus之后,每个PCIBus里包含256个PCIDevice的指针,进而根据devfn索引到每一个PCIDevice。

PCIDevice *pci_find_device(PCIBus *bus, int bus_num, uint8_t devfn)
{
    bus = pci_find_bus_nr(bus, bus_num);

    if (!bus)
        return NULL;

    return bus->devices[devfn];
}

static PCIBus *pci_find_bus_nr(PCIBus *bus, int bus_num)
{
    PCIBus *sec;

    if (!bus) {
        return NULL;
    }

    if (pci_bus_num(bus) == bus_num) {
        return bus;
    }

    /* Consider all bus numbers in range for the host pci bridge. */
    if (!pci_bus_is_root(bus) &&
        !pci_secondary_bus_in_range(bus->parent_dev, bus_num)) {
        return NULL;
    }

    /* try child bus */
    for (; bus; bus = sec) {
        QLIST_FOREACH(sec, &bus->child, sibling) {
            if (pci_bus_num(sec) == bus_num) {  //PCI_SECONDARY_BUS匹配,说明设备就是挂在当前的bus下,直接返回当前的bus
                return sec;
            }
            /* PXB buses assumed to be children of bus 0 */
            if (pci_bus_is_root(sec)) {
                if (pci_root_bus_in_range(sec, bus_num)) {
                    break;
                }
            } else {
                if (pci_secondary_bus_in_range(sec->parent_dev, bus_num)) {  //secondary in range,说明在下一级child bus里,break退出内层循环QLIST_FOREACH
                    break;
                }
            }
        }
    }

    return NULL;
}

三、扩展config空间的模拟

上一节是针对前256字节使用IO端口0xCF8/CFC端口访问的config空间访问。本节是针对采用memory映射机制访问的扩展config空间访问实现。这一部分的入口是在hw/pci/pcie-host.c里了。


static const MemoryRegionOps pcie_mmcfg_ops = {
    .read = pcie_mmcfg_data_read,
    .write = pcie_mmcfg_data_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
};

static void pcie_host_init(Object *obj)
{
    PCIExpressHost *e = PCIE_HOST_BRIDGE(obj);

    e->base_addr = PCIE_BASE_ADDR_UNMAPPED;
    memory_region_init_io(&e->mmio, OBJECT(e), &pcie_mmcfg_ops, e, "pcie-mmcfg-mmio",
                          PCIE_MMCFG_SIZE_MAX);
}

而pcie_mmcfg_data_read/write接口里的处理形式也跟I/O端口访问的接口处理流程相似。都是先查找到PCIDevice,然后访问其config空间。

IO端口访问形式的config端口里会带有设备的BDF号。神奇的是,memory映射地址也可以直接索引到设备的BDF号,使用gdb跟踪qemu梳理了一下。看起来是X86架构会自动将一片连续的地址空间大小为0x10000000(4K*2^16个设备)分配给config空间的memory地址映射使用,每个设备分配的config空间地址是按照BDF号平铺的。根据地址在地址空间内的的offset,再除以4KB就得到了BDF号。

实际看代码,这种索引机制还给BUS号多分配了1个bit=9bit。具体还得再看看原因。

/*
 * PCI express ECAM (Enhanced Configuration Address Mapping) format.
 * AKA mmcfg address
 * bit 20 - 28: bus number
 * bit 15 - 19: device number
 * bit 12 - 14: function number
 * bit  0 - 11: offset in configuration space of a given device
 */
#define PCIE_MMCFG_SIZE_MAX             (1ULL << 29)
#define PCIE_MMCFG_SIZE_MIN             (1ULL << 20)
#define PCIE_MMCFG_BUS_BIT              20
#define PCIE_MMCFG_BUS_MASK             0x1ff
#define PCIE_MMCFG_DEVFN_BIT            12
#define PCIE_MMCFG_DEVFN_MASK           0xff
#define PCIE_MMCFG_CONFOFFSET_MASK      0xfff
#define PCIE_MMCFG_BUS(addr)            (((addr) >> PCIE_MMCFG_BUS_BIT) & 
                                         PCIE_MMCFG_BUS_MASK)
#define PCIE_MMCFG_DEVFN(addr)          (((addr) >> PCIE_MMCFG_DEVFN_BIT) & 
                                         PCIE_MMCFG_DEVFN_MASK)

/* a helper function to get a PCIDevice for a given mmconfig address */
static inline PCIDevice *pcie_dev_find_by_mmcfg_addr(PCIBus *s,
                                                     uint32_t mmcfg_addr)
{
    return pci_find_device(s, PCIE_MMCFG_BUS(mmcfg_addr),
                           PCIE_MMCFG_DEVFN(mmcfg_addr));
}

static uint64_t pcie_mmcfg_data_read(void *opaque,
                                     hwaddr mmcfg_addr,
                                     unsigned len)
{
    PCIExpressHost *e = opaque;
    PCIBus *s = e->pci.bus;
    PCIDevice *pci_dev = pcie_dev_find_by_mmcfg_addr(s, mmcfg_addr);
    uint32_t addr;
    uint32_t limit;

    if (!pci_dev) {
        return ~0x0;
    }
    limit = pci_config_size(pci_dev);

    return pci_host_config_read_common(pci_dev, addr, limit, len);
}

至于从BDF号索引到PCIDevice的过程,在前256Bconfig空间的模拟中已经介绍过了,是一个从root bus向下索引的过程。

而pci_host_config_write/read_common接口,在前256Bconfig空间的模拟中,也同样是会调用这个接口,所以后续的处理都是一样的。只是入口处路径不一致。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>