一、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向下索引的过程。文章来源:https://www.toymoban.com/news/detail-764867.html
而pci_host_config_write/read_common接口,在前256Bconfig空间的模拟中,也同样是会调用这个接口,所以后续的处理都是一样的。只是入口处路径不一致。文章来源地址https://www.toymoban.com/news/detail-764867.html
到了这里,关于QEMU pcie config空间访问机制的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!