PCI入门
文章目录
1. PCI基础
基于PCI总线的处理器系统结构图如下:
1.1 Host主桥
PCI总线树主要由Host主桥(存在与SouthBridge)、PCI总线、PCI设备构成,Host主桥主要功能包括:
- 隔离处理器系统的存储器域和PCI总线域,每个Host主桥都对应一个PCI总线域
- 管理PCI总线域
- 完成处理起与PCI设备间的数据交换
1.2 PCI总线
PCI总线由Host主桥或PCI桥管理 ,用来连接各类设备
1.3 PCI设备
- 桥设备,管理下游PCI总线,转发上游总线事务
- PCI Agnet设备,如PCI网卡、显卡等
1.4 PCI设备配置
由于PCI/PCIe设备分为Bridge和Agent两种,所以配置空间也有两种类型。
1.4.1 PCI设备配置空间
(1)Device ID和Vendor ID寄存器
这两个寄存器只读,Vendor ID代表PCI设备的生产厂商,而Device ID代表这个厂商所生产的具体设备。如Intel公司的82571EB芯片网卡,其中Vendor ID为0x8086,Device为0x105E。
(2)Revision ID和Class Code寄存器
这两个寄存器只读。其中Revision ID寄存器记载PCI设备的版本号。该寄存器可以被认为是Device ID的寄存器的扩展。Class Code寄存器记载PCI设备的分类,该寄存器由三个字段组成,分别是Base Class Code、Sub Class Code和Interface。其中Base Class Code讲PCI设备分类为显卡、网卡、PCI桥等设备;Sub Class Code对这些设备进一步细分。Interface定义编程接口。除此之外硬件逻辑设计也需要使用寄存器识别不同的设备。当Base Class Code寄存器为0x06,Sub Class Code寄存器为0x04时,表示当前PCI设备为一个标准的PCI桥。
(3)Header Type寄存器
该寄存器只读,由8位组成。
- 第7位为1表示当前PCI设备是多功能设备,为0表示为单功能设备
- 第0~6位表示当前配置空间的类型,为0表示该设备使用PCI Agent设备的配置空间,普通PCI设备都是用这种配置头;为1表示使用PCI桥的配置空间,PCI桥使用这种配置头。系统软件需要使用该寄存器区分不同类型的PCI配置空间。
(4)Cache Line Size寄存器
该寄存器记录处理器使用的Cache行长度。在PCI总线中和cache相关的总线事务,如存储器写无效等需要使用这个寄存器。该寄存器由系统软件设置,硬件逻辑使用。
(5)Expansion ROM base address寄存器
有些PCI设备在处理器还没有运行操作系统前,就需要完成基本的初始化。为了实现这个"预先执行"功能,PCI设备需要提供一段ROM程序,而处理器在初始化过程中将运行这段ROM程序,初始化这些PCI设备。Expansion ROM base address寄存器记载这段ROM程序的基地址。
(6)Capabilities Pointer寄存器
在PCI设备中,该寄存器是可选的,但是在PCIe设备中必须支持这个寄存器,Capabilities Pointer寄存器存放Capabilitise寄存器组的基地址,利用Capabilities寄存器组存放一些与PCI设备相关的扩展配置信息。
(7)Base Address Register 0~5寄存器
该组寄存器简称为BAR寄存器,BAR寄存器保存PCI设备使用的地址空间的基地址,该基地址保存的是该设备在PCI总线域中的地址。在PCI设备复位之后,该寄存器存放PCI设备需要使用的基址空间大小,这段空间是I/O空间还是存储器空间。系统软件可以使用该寄存器,获取PCI设备使用的BAR空间的长度,其方法是向BAR寄存器写入0xFFFFFFFF,之后在读取该寄存器。
1.4.2 PCI桥配置配置空间
PCI桥使用的许多配置空间与PCI Agent的寄存器是类似的,如Device ID、Vendor ID、Status、Command等,这里主要介绍PCI桥中与PCI Agent的配置空间不相同的寄存器。在PCI桥中只有两组BAR寄存器,即Base Address Register 0~1寄存器。这两组寄存器含义与PCI Agent设备一致。但是在PCI桥中,这两组寄存器是可选的。大多数中不存在私有寄存器,操作系统也不需要为PCI桥提供专门的驱动程序,这类桥也被称为透明桥。在PCI桥的配置空间中,有许多寄存器是PCI桥索特有的,包括存储器、I/O地址空间和总线号,用于管理其下连接的PCI总线树使用的各种资源。
(1) Subordinate Bus Number、Secondary Bus Number和Primary Bus Number寄存器
PCI桥可以管理其下的PCI总线子树。其中Subordinate Bus Number寄存器存放当前PCI子树中,编号最大的PCI总线号。而Secondary Bus Number存放当前PCI桥Secondary Bus使用的总线号,这个PCI总线号也是该PCI桥管理的PCI子树中编号最小的PCI总线号。因此一个PCI桥能够管理的PCI总线号在Secondary Bus Number~bordinate Bus Number之间。这两个寄存器的值由系统软件遍历PCI总线树时设置。Primary Bus Number寄存器存放该PCI桥上游的PCI总线号,该寄存器可读写。Subordinate Bus Number、Secondary Bus Number和Primary Bus Number寄存器在初始化时必须为0,系统软件将根据这几个寄存器是否为0,判断PCI桥是否被配置过。
(2) I/O Limit和I/O Base寄存器
在PCI桥管理的PCI子树中包含许多PCI设备,而这些设备可能会使用I/O地址空间。PCI桥使用这两个寄存器,存放PCI字树中所有设备使用的I/O地址空间集合的基地址和大小。
(3)Memory Limit和Memory Base寄存器
在PCI桥管理的PCI子树中包含许多PCI设备,而这些设备可能会使用存储址空间。这两个寄存器存放子树中所有这些设备使用的存储地址空间集合的基地址和大小。
(4)Prefetchable Memory Limit和Prefetchable Memory Base寄存器
在PCI桥管理的PCI子树中包含许多PCI设备,如果这些PCI设备支持预读,则需要从PCI桥的可预读空间中获取地址空间。PCI桥的这两个寄存器存放这些PCI设备使用的可预取存储器空间的基地址和大小。
1.4.3 PCI总线树初始化
在PCI总线树中,有多少个PCI桥(包括Host主桥),就含有多少条总线。系统软件在遍历当前PCI总线树时,需要对这些PCI总线进行编号,即初始化PCI桥的Primary、Secondary和Subordinate Bus Number寄存器。下图是利用DFS(Depth First Search)算法枚举后,总线号的分配情况及桥上总线相关寄存器的初始化情况。
1.4.4 x86平台PCI设备配置
x86处理器定义了两个I/O端口寄存器,分别为CONFIG_ADDRESS和CONFIG_DATA寄存器,地址分别为0xCF8和0xCFC。利用这两个I/O端口访问PCI设备的配置空间。其中CONFIG_ADDRESS寄存器存放PCI设备的ID号,CONFIG_DATA寄存器存放配置的读写数据。
CONFIG_ADDRESS寄存器结构如下图:
- Enable位,该位为1时,对CONFIG_DATA寄存器进行读写时将引发PCI总线的配置周期
- Bus Number字段,第23~16位,PCI设备的总线号
- Device Number字段,第15~11位,PCI设备的设备号
- Function Number字段,第10~8位,PCI设备的功能号
- Register Number字段,第7~2位,记录PCI设备的寄存器号
当CONFIG_ADDRESS寄存器的Enable位为1时,对CONFIG_DATA寄存器进行的I/O读写访问会由Host主桥转换为PCI配置读写总线事务,然后发送到PCI总线上,根据CONFIG_ADDRESS中的总线号、设备号、功能号、寄存器号完成对特定设备的指定寄存器读写。
PCIe扩展了配置空间大小,最大支持到4K。其中:
- 0 - 3Fh 是基本配置空间,PCI和PCIe都支持
- PCI Express Capability Structure ,PCI可选支持,PCIe支持
- PCI Express Extended Capability Structure,PCI不支持,PCIe支持
利用IO的访问方式只能访问256Byte空间,所以为了访问4K Byte,支持通过mmio的方式访问配置空间,但是为了兼容性,保留了I/O访问方式。
1.5 PCI设备读写
对PCI设备的BAR(Base Address Register)配置寄存器初始化后,就可以通过BAR空间地址对PCI设备进行读写操作,下图解释了处理器对PCI设备空间进行读写的过程。
1.6 INTx和MSI
在PCI总线中,所有需要提交中断请求的设备,必须能够通过INTx引脚提交中断请求,而MSI机制是一个可选机制。而在PCIe总线中,PCIe设备必须支持MSI或者MSI-X中断请求机制,而可以不支持INTx中断消息。在PCIe总线中,MSI和MSI-X中断机制使用存储器写请求TLP向处理器提交中断请求,下文为简便起见将传递MSI/MSI-X中断消息的存储器写报文简称为MSI/MSI-X报文。
与INTx相比,MSI中断主要优势如下:
- INTx利用额外的引脚传输中断信号,与存储器读写操作不同步,当处理器接收到中断信号时,存储器中的数据可能不是最新,通过软件额外执行读操作解决这个问题。MSI中断通过写事务产生中断,不会有这个问题
- IINTx中断允许每个device拥有4个中断,MSI允许每个device有1,2,4,8,16或者是32个中断。
MSI中断实现:
2. PCI和PCIe区别
先分别看下基于pci总线和pcie总线的拓扑图:
PCIe与PCI两者在电器特性上差别很大,这里不做主要阐述。两类总线拓扑结构上的主要变化就是PCIe支持端到端的连接,无法像PCI一样,在一条总线上挂接多个设备或桥。PCIe在软件上是兼容PCI总线的,对于这两种总线的拓扑结构是有一定的转换关系的:
- 对于PCIe的RC等价于PCI中的Host Bridge
- device都是相同的
- 对于Bridge和switch的转换关系如下图
3.Linux下PCI枚举实现
3.1 PCI树的枚举与总线分配
在linux进行枚举前,实际上bios已经做过一次枚举,并且完成了对一些桥和设备的配置。对于pci的枚举主要包含bus号的分配和resource的分配,并且在分配的过程中需要考虑对pci设备的热插拔支持。如果编译时,配置了CONFIG_PCI_DIRECT宏,则在系统启动时会再次遍历,遍历的作用有两方面:1)验证bios操作是否正确;2)对于在bios中未能正确配置的设备重新配置。下面详细介绍linux下相关实现。
pci_subsys_init
+
|
+--->pci_legacy_init /* 创建root bus,完成pci总线扫描,建立bus、deviece数据结构关联 */
| +
| +------------->pcibios_scan_root
| +
| +----------->x86_pci_root_bus_resources (1)
| |
| |
| +----------->pci_scan_root_bus
| | +
| | +----->pci_scan_root_bus_msi (2)
| |
| +----------->pci_bus_add_devices
|
|
+--->pcibios_init /* 为桥和设备分配resource资源 */
+
+-------->pcibios_resource_survey
(1)x86_pci_root_bus_resources
添加resource资源,主要包含ioport_resource和iomem_resource两种,数据结构如下:
/*
* x86 I/O 空间
*/
struct resource ioport_resource = {
.name = "PCI IO",
.start = 0,
.end = IO_SPACE_LIMIT,
.flags = IORESOURCE_IO,
};
EXPORT_SYMBOL(ioport_resource);
/*
* x86 mem 空间
*/
struct resource iomem_resource = {
.name = "PCI mem",
.start = 0,
.end = -1,
.flags = IORESOURCE_MEM,
};
EXPORT_SYMBOL(iomem_resource);
(2)pci_scan_root_bus_msi
这个函数做的事情比较多,主要流程如下:
pci_scan_root_bus_msi
+
+--------------->pci_create_root_bus
| /*创建root_bus,并关联到上面提到的ioport_resource和iomem_resource */
|
|
+--------------->pci_bus_insert_busn_res
| /* 为root_bus 分配总线资源 */
|
|
+--------------->pci_scan_child_bus
| /* 递归遍历总线下所有总线、设备、桥,并建立数据结构关联 */
|
|
+--------------->pci_bus_update_busn_res_end
/* 修正之前分配的总线资源,有时用不到*/
其中,pci_scan_child_bus实现了pci总线的递归遍历,主要代码如下(driver/pci/probe.c):
unsigned int pci_scan_child_bus(struct pci_bus *bus)
{
unsigned int devfn, pass, max = bus->busn_res.start;
struct pci_dev *dev;
dev_dbg(&bus->dev, "scanning bus\n");
/* Go find them, Rover! */
/*
* 由于当前并不知道当前总线上有哪些设备,所以只能通过最暴力的方法,
* 遍历所有可能的设备号和功能号,devfn将设备号和功能号合并一起了,
* 之所以每次 += 8是因为在pci_scan_slot中会完成对同一个设备号不同
* 功能号的处理
*/
for (devfn = 0; devfn < 0x100; devfn += 8)
pci_scan_slot(bus, devfn);
/* Reserve buses for SR-IOV capability. */
max += pci_iov_bus_range(bus);
/*
* After performing arch-dependent fixup of the bus, look behind
* all PCI-to-PCI bridges on this bus.
*/
if (!bus->is_added) {
dev_dbg(&bus->dev, "fixups for bus\n");
/*
* 这里会对桥的PCI_BRIDGE_RESOURCES的连续三个资源进行配置
* 包括I/O、mem、预取mem
*/
pcibios_fixup_bus(bus);
bus->is_added = 1;
}
/*
* 这里进行两次遍历,由于有些桥bios没有配置,第一遍先获取最大的bus号,
* 然后第二遍基于最大bus号,再为没有配置的桥分配bus
* 这里注意:由于pci_scan_bridge会递归调用pci_scan_child_bus,所以
* 每个桥都会调用两遍pci_scan_bridge,也就是说第二次的max值都是基于本桥
* 自己的,而不是全局的,只有这样才保证bus号连续
*/
for (pass = 0; pass < 2; pass++)
list_for_each_entry(dev, &bus->devices, bus_list) {
if (pci_is_bridge(dev))
max = pci_scan_bridge(bus, dev, max, pass);
}
/*
* Make sure a hotplug bridge has at least the minimum requested
* number of buses.
*/
if (bus->self && bus->self->is_hotplug_bridge && pci_hotplug_bus_size) {
if (max - bus->busn_res.start < pci_hotplug_bus_size - 1)
max = bus->busn_res.start + pci_hotplug_bus_size - 1;
}
/*
* We've scanned the bus and so we know all about what's on
* the other side of any bridges that may be on this bus plus
* any devices.
*
* Return how far we've got finding sub-buses.
*/
dev_dbg(&bus->dev, "bus scan returning with max=%02x\n", max);
return max;
}
该函数最主要的功能就是由root_bus为起始bus,递归遍历整个pci树下的所有设备和总线,并创建数据结构关联,为了支持热pci桥和pci设备的插拔,可以通过pci_hotplug_bus_size预留bus号,该数值可配。数据结构整体框图如下:
注:
- 每个pci_bus数据结构都有resource资源数组,指向引申出该bus的桥上资源,这里root_bus和其它bus不同,root_bus对应的是host_bridge,此时会用resources结构来分配资源,即ioport_resource和iomem_resource
- 有些数据结构没有画出来,比如children、parent等,这些都比较简单
- linux在对pci总线树进行遍历时,无论时bus的分配、或者是mmio或者I/O的分配,都可以理解为是一种资源的管理。这里都利用了右图的那种数据结构,也是按照树的层次实现的,实现也比较简单,对于bus的分配,由busn_res成员管理。对于mmio或者I/O的分配,由resouce和resources成员实现,其中root_bus只使用resources成员
- 对于每个桥,会执行两遍pci_scan_bridge,bus总线号是按照dfs算法分配,对于每个桥,要知道当前桥下多少条bus,然后对于当前桥下如果有没配置的桥,则在之前遍历的基础上做加法。同时,预留了一部分总线号用于热插拔的实现,对于热插拔的具体实现,后面会有讲到,同时会利用分配好的总线号配置桥的Subordinate Bus Number、Secondary Bus Number和Primary Bus Number寄存器
3.2 PCI空间分配
上面讲述了对于整个PCI树的枚举,并且分配好了bus号。还有个重点是对于mmio和I/O空间的分配,这里主要存在两种情况,一种是对桥的分配,一种是对设备空间的分配。对于一些已经由bios完成的配置(且配置正确),这里可以直接使用,也可以重新分配。由pcibios_resource_survey函数完成,先看下代码流程(arch/x86/pci/common.c):
void __init pcibios_resource_survey(void)
{
struct pci_bus *bus;
DBG("PCI: Allocating resources\n");
/*
* dfs为桥分配空间
* 因为桥上的两个bar空间一般都不使用,这里的分配空间主要是分配
* I/O Limit和I/O Base寄存器
* Memory Limit和Memory Base寄存器
* Prefetchable Memory Limit和Prefetchable Memory Base寄存器
*
* 对于分配过程中,如果产生冲突问题或者start为0情况,后续还有额外处理
* 对于start为0的原因有可能是为了重新分配资源
* 对于分配冲突,有可能是bios在配置过程中分配的区间过大
*/
list_for_each_entry(bus, &pci_root_buses, node)
pcibios_allocate_bus_resources(bus);
/*
* dfs为设备分配空间
*/
list_for_each_entry(bus, &pci_root_buses, node)
pcibios_allocate_resources(bus, 0);
list_for_each_entry(bus, &pci_root_buses, node)
pcibios_allocate_resources(bus, 1);
e820_reserve_resources_late();
/*
* Insert the IO APIC resources after PCI initialization has
* occurred to handle IO APICS that are mapped in on a BAR in
* PCI space, but before trying to assign unassigned pci res.
*/
ioapic_insert_resources();
}
上面的空间分配过程中,是基于bios遍历的结果,并验证了配置的正确性。对于配置中产生冲突的设备或bios没有配置的设备,会利用pcibios_assign_resources函数分配资源,代码流程如下:
pcibios_assign_resources
+
|
+--------->pci_assign_unassigned_resources
+
|
+-->pci_assign_unassigned_root_bus_resources
+
|
+------>__pci_bus_size_bridges
| +
| |
| +------>pbus_size_io
| |
| +------>pbus_size_mem
|
|
+------>__pci_bus_assign_resources
其中,实现分配功能主要是__pci_bus_size_bridges
和__pci_bus_assign_resources
。__pci_bus_size_bridges
也会递归调用自身,并且利用pbus_size_io函数及pbus_size_mem函数统计当前桥下需要的mem空间和I/O空间数量,在统计的过程中,会考虑对热插拔情况的支持,即会预留出一些空间用于支持热插拔,具体的预留空间大小由pci_hotplug_io_size和pci_hotplug_mem_size决定(pci_setup函数设置)。统计好资源后,会将所有统计信息存放到链表中,并且热插拔预留空间和设备本身需要的空间是分离存储的,原因后面会讲到。
需要分配的资源信息已经统计好了,接下来由__pci_bus_assign_resources
函数进行真正的资源分配。函数主要流程如下:
__pci_bus_assign_resources
+
|
+->pbus_assign_resources_sorted
+
+------->__dev_sort_resources /*按照所需对齐大小,由大到小排序*/
|
|
|
+------->__assign_resources_sorted /* 分配资源 */
这里真正进行资源分配的函数是__assign_resources_sorted,具体代码如下(driver/pci/setup-bus.c):
static void __assign_resources_sorted(struct list_head *head,
struct list_head *realloc_head,
struct list_head *fail_head)
{
/*
* Should not assign requested resources at first.
* they could be adjacent, so later reassign can not reallocate
* them one by one in parent resource window.
* Try to assign requested + add_size at beginning
* if could do that, could get out early.
* if could not do that, we still try to assign requested at first,
* then try to reassign add_size for some resources.
*
* Separate three resource type checking if we need to release
* assigned resource after requested + add_size try.
* 1. if there is io port assign fail, will release assigned
* io port.
* 2. if there is pref mmio assign fail, release assigned
* pref mmio.
* if assigned pref mmio's parent is non-pref mmio and there
* is non-pref mmio assign fail, will release that assigned
* pref mmio.
* 3. if there is non-pref mmio assign fail or pref mmio
* assigned fail, will release assigned non-pref mmio.
*/
LIST_HEAD(save_head);
LIST_HEAD(local_fail_head);
struct pci_dev_resource *save_res;
struct pci_dev_resource *dev_res, *tmp_res, *dev_res2;
unsigned long fail_type;
resource_size_t add_align, align;
/* Check if optional add_size is there */
if (!realloc_head || list_empty(realloc_head))
goto requested_and_reassign;
/* Save original start, end, flags etc at first */
/*
* head 信息保存在 save_head中,在利用requested + add_size长度进行分配失败时
* 需要按照requested分配
*/
list_for_each_entry(dev_res, head, list) {
if (add_to_list(&save_head, dev_res->dev, dev_res->res, 0, 0)) {
free_list(&save_head);
goto requested_and_reassign;
}
}
/* Update res in head list with add_size in realloc_head list */
list_for_each_entry_safe(dev_res, tmp_res, head, list) {
/*
* 添加add_size,这里包含了为了支持热插拔而预留的空间
*/
dev_res->res->end += get_res_add_size(realloc_head,
dev_res->res);
/*
* There are two kinds of additional resources in the list:
* 1. bridge resource -- IORESOURCE_STARTALIGN
* 2. SR-IOV resource -- IORESOURCE_SIZEALIGN
* Here just fix the additional alignment for bridge
*/
if (!(dev_res->res->flags & IORESOURCE_STARTALIGN))
continue;
add_align = get_res_add_align(realloc_head, dev_res->res);
/*
* The "head" list is sorted by the alignment to make sure
* resources with bigger alignment will be assigned first.
* After we change the alignment of a dev_res in "head" list,
* we need to reorder the list by alignment to make it
* consistent.
*/
if (add_align > dev_res->res->start) {
resource_size_t r_size = resource_size(dev_res->res);
dev_res->res->start = add_align;
dev_res->res->end = add_align + r_size - 1;
list_for_each_entry(dev_res2, head, list) {
align = pci_resource_alignment(dev_res2->dev,
dev_res2->res);
if (add_align > align) {
list_move_tail(&dev_res->list,
&dev_res2->list);
break;
}
}
}
}
/* Try updated head list with add_size added */
assign_requested_resources_sorted(head, &local_fail_head);
/* all assigned with add_size ? */
if (list_empty(&local_fail_head)) {
/* Remove head list from realloc_head list */
list_for_each_entry(dev_res, head, list)
remove_from_list(realloc_head, dev_res->res);
free_list(&save_head);
free_list(head);
return;
}
/* check failed type */
fail_type = pci_fail_res_type_mask(&local_fail_head);
/* remove not need to be released assigned res from head list etc */
/*
* head中剩下的是需要释放的,因为剩余的空间类型和分配失败的属于同一类型
*/
list_for_each_entry_safe(dev_res, tmp_res, head, list)
if (dev_res->res->parent &&
!pci_need_to_release(fail_type, dev_res->res)) {
/* remove it from realloc_head list */
remove_from_list(realloc_head, dev_res->res);
remove_from_list(&save_head, dev_res->res);
list_del(&dev_res->list);
kfree(dev_res);
}
free_list(&local_fail_head);
/* Release assigned resource */
list_for_each_entry(dev_res, head, list)
if (dev_res->res->parent)
release_resource(dev_res->res);
/* Restore start/end/flags from saved list */
list_for_each_entry(save_res, &save_head, list) {
struct resource *res = save_res->res;
res->start = save_res->start;
res->end = save_res->end;
res->flags = save_res->flags;
}
free_list(&save_head);
requested_and_reassign:
/* Satisfy the must-have resource requests */
assign_requested_resources_sorted(head, fail_head);
/* Try to satisfy any additional optional resource
requests */
if (realloc_head)
reassign_resources_sorted(realloc_head, head);
free_list(head);
}
上述代码主要分为以下几个步骤:
- 将需要分配的资源信息保存在save_head中
- 将需要分配的资源信息与之前统计的资源信息相结合,将add_size加入到要分配的信息中,其中add_size中包含为了支持热插拔而分配出来的信息
assign_requested_resources_sorted
为设备分配空间,并将分配失败的记录在local_fail_head- 如果上面有分配失败的情况,需要将已经分配的与失败同类型的资源释放掉,比如都是I/O或者都是mmio资源
- 利用步骤1中保存在save_head中的信息重新分配,此时没有加入add_size以增加分配成功概率
- 在资源分配的过程中利用的就是struct resource结构维护资源关系,linux支持不使用bios的默认分配,在pci_device_add—>pci_reassigndev_resource_alignment中可以找到相关代码。利用resource_alignment参数实现。
4.linux下pcie热插拔实现
对于热插拔的支持,主要工作是分配bus号、mmio和I/O空间。主要实现在(drivers/pci/hotplug/pciehp_ctrl.c)board_added函数中,其中pciehp_configure_device完成了对设备的配置作用,具体代码如下:
int pciehp_configure_device(struct slot *p_slot)
{
struct pci_dev *dev;
struct pci_dev *bridge = p_slot->ctrl->pcie->port;
struct pci_bus *parent = bridge->subordinate;
int num, ret = 0;
struct controller *ctrl = p_slot->ctrl;
pci_lock_rescan_remove();
/*
* PCI_DEVFN(0, 0) pcie的缘故,只能挂一个节点
*/
dev = pci_get_slot(parent, PCI_DEVFN(0, 0));
if (dev) {
ctrl_err(ctrl, "Device %s already exists at %04x:%02x:00, cannot hot-add\n",
pci_name(dev), pci_domain_nr(parent), parent->number);
pci_dev_put(dev);
ret = -EEXIST;
goto out;
}
num = pci_scan_slot(parent, PCI_DEVFN(0, 0));
if (num == 0) {
ctrl_err(ctrl, "No new device found\n");
ret = -ENODEV;
goto out;
}
list_for_each_entry(dev, &parent->devices, bus_list)
if (pci_is_bridge(dev))
pci_hp_add_bridge(dev);
/*
* 分配资源,利用枚举过程中为支持热插拔预分配的空间
*/
pci_assign_unassigned_bridge_resources(bridge);
pcie_bus_configure_settings(parent);
pci_bus_add_devices(parent);
out:
pci_unlock_rescan_remove();
return ret;
}
int pci_hp_add_bridge(struct pci_dev *dev)
{
struct pci_bus *parent = dev->bus;
int pass, busnr, start = parent->busn_res.start;
int end = parent->busn_res.end;
/*
* 前面提到了在分配bus时,为了支持热插拔会多分配一些,这里
* 就是要找到空闲总线使用
*/
for (busnr = start; busnr <= end; busnr++) {
if (!pci_find_bus(pci_domain_nr(parent), busnr))
break;
}
if (busnr-- > end) {
printk(KERN_ERR "No bus number available for hot-added bridge %s\n",
pci_name(dev));
return -1;
}
/*
* 与之前的两次遍历原理相同
*/
for (pass = 0; pass < 2; pass++)
busnr = pci_scan_bridge(parent, dev, busnr, pass);
if (!dev->subordinate)
return -1;
return 0;
}
上面的代码逻辑和枚举时的逻辑类似,这里不再阐述。在分配bus、mmio和I/O空间时就用到了在枚举时为了支持热插拔预分配的空间。
5. PCI地址域
在访问PCI总线上设备时,利用的都是PCI域的地址,这里需要注意的是,PCI域的地址和CPU的地址空间不是等价的,无论是CPU访问PCI设备,还是PCI设备进行DMA操作,都需要借助Host主桥完成地址域的转换(PCI域到CPU域 | CPU域到PCI域)。在x86平台下,CPU域地址空间与PCI域地址空间是一一对应的,使人感觉起来就像只有一个域存在,不过对于像PPC这类处理器平台,PCI域和CPU域地址空间并不是一一对应的,需要借助inbound和outbound窗口管理映射关系,理解域的概念有助于更深刻的理解PCI总线。
6. x86地址空间布局
7 SR-IOV
对于其中的一个VF,在整个PCI拓扑中的总线号如下计算关系:
dev->bus->number + ((dev->devfn + dev->sriov->offset +dev->sriov->stride * vf_id) >> 8);
在当前PCI总线上的devfn计算如下:
(dev->devfn + dev->sriov->offset +dev->sriov->stride * vf_id) & 0xff;
对于每一个VF,不会使用PCI配置空间中的BAR地址,而是使用上图中的BAR空间,与PF中的BAR空间一样,这里也需要先获取BRA空间需要的长度,然后再分配空间。对于一个VF的BAR空间首地址计算是
VF_N starting address = VF_BASE + (N – 1) * (VF_BAR aperture size)
sriov_enable函数(drivers/pci/iov.c)使能了sr-iov功能,该函数主要工作如下:
- 验证参数合法性
- 按照上面提到的公式计算总线号及devfn,并判断计算出的总线号是否超出了上游桥的总线范围,在枚举过程中,会为SR-IOV预留总线号
- 为当前VF分配空间资源
参考文献
《PCI+EXPRESS体系结构导读》
https://resources.infosecinstitute.com/system-address-map-initialization-in-x86x64-architecture-part-1-pci-based-systems/
https://resources.infosecinstitute.com/system-address-map-initialization-x86x64-architecture-part-2-pci-express-based-systems/#gref