linux之PCIE 设备枚举流程分析

1.简介

当系统启动时或者有新的PCIe设备接入时,PCIe主机会扫描PCIe总线上的PCIe设备,读取设备配置空间信息,建立设备的拓扑关系,然后为设备分配资源(如内存空间、I/O空间、中断、总线编号等),最后根据设备的类型匹配驱动。下面以Linux内核为例,介绍PCIe主机枚举PCIe设备的流程。

2.pci_host_probe

Linux内核中,PCIe RC驱动调用pci_host_probe函数枚举PCIe总线上的所有设备。pci_host_probe的执行流程如下图所示,总共有5部分,具体如下:

  1. 调用pci_scan_root_bus_bridge函数扫描总线上的桥和设备。
    1. 首先注册Host主桥。主桥上有root bus,root bus的编号通常是0。然后设置msi_controller和msi_domain,用于MSI/MSI-X中断。最后将root bus的数据结构挂到pci_root_buses上,便于统一管理。
    2. 调用pci_scan_child_bus函数扫描设备,后面详细介绍。
  2. 确定连接到PCI总线上的PCI桥(包括标准PCI桥和PCI到PCIe桥)所需的地址空间大小,并分配相应的内存和I/O资源。最后,调整桥设备的子总线号,以确保正确的总线层次结构和设备访问路径。
  3. 给总线上的设备和桥分配资源,即设置配置空间的BAR、BASE、Limit等寄存器,后面详细介绍。
  4. 遍历PCIe总线上设备和桥,设置MPS、MRRS,可以参考PCIe Linux MRRS和MPS参数设置策略。
  5. 遍历PCIe总线上设备和桥,添加设备,调用PCIe总线提供的匹配方法匹配PCIe设备驱动。

3.pci_scan_child_bus

Linux内核调用pci_scan_child_bus函数,从root bus开始枚举PCIe总线上的所有设备。整个枚举流程分为三步,具体如下:

  1. 第一步:首先从root bus开始扫描。PCIe设备和功能号组合总共有256种,因此需要全部遍历一遍,devfn高5位是设备号,低3位是功能号,每次循坏扫描一个设备,默认扫描Fun0,如果设备是多功能设备,则会扫描其他Function。扫描设备的单个Function使用pci_scan_single_device函数,后面详细介绍。
  2. 第二步:若bus下面挂的桥被BIOS配置过,则扫描桥下面的设备,如此递归,直到扫面完整个总线,若桥没有被配置过或需要重新配置,则先跳过,在第三步重新配置。对于被BIOS配置过的桥,其Primary、Secondary、Subordinate已经设置,内核会创建pci_bus,为其预留bus编号,同时将bus资源管理起来。
  3. 第三步:遍历总线上的所有桥,设置所有没有配置的桥,即设置桥的Primary、Secondary、Subordinate,同时创建pci_bus,为其预留bus编号,同时将bus资源管理起来。

4.pci_scan_single_device

内核调用pci_scan_single_device函数扫描设备的单个Function,先扫描Function,再添加Function。执行的流程如下:

  1. 内核通过读取Function的VID来判断设备是否存在,超时时间为60000毫秒。若读取到的VID是0xffffffff、0x00000000、0x0000ffff、0xffff0000,则表示没有设备,若读取到的是0x1,说明返回了CRS的completion包,设备还没准备好,延时循环读取PCI_VENDOR_ID(等待设备准备好),若读到CRS的completion包,则延时翻倍。
  2. 扫描到Function,则分配pci_dev数据结构,每个Function对对应一个pci_dev数据结构。
  3. 初始化Function(设备)。
    1. 设置Function的bus_type为pci_bus_type,用于匹配和初始化PCIe设备驱动。
    2. 读取Function的port类型,通过PCI Express Capability registers获取,主要是确定Function是upstream还是downstream ports。
    3. 获取Function的配置空间大小。通过读偏移地址为256处的值,若读到有效的扩展空间头则为4096字节,否则为256字节。
    4. 解析非桥设备的PCIe配置空间头。具体有PCI_INTERRUPT_PIN、PCI_BASE_ADDRESS、PCI_SUBSYSTEM_VENDOR_ID、PCI_SUBSYSTEM_ID。PCI_BASE_ADDRESS会读取Function的BAR,详细的后面说明。
    5. 解析桥设备的PCIe配置空间头。具体有PCI_INTERRUPT_PIN、PCI_BASE_ADDRESS、PCI_EXP_SLTCAP、PCI_CAP_ID_SSVID、PCI_SSVID_VENDOR_ID和桥的地址空间。
  4. 配置、初始化和管理Function。主要是配置MPS、Extended Tags、ordering、LTR等,初始化MSI、PM、VPD、IOV、ATS、AER等,将pci_dev挂到pci_bus的devices链表,最后建立MSI IRQ domain。
4.1.解析BAR

内核使用pci_read_bases函数解析Function的BAR和ROM,包括桥的BAR0和BAR1,非桥设备的BAR0-BAR5。BAR的使用方法参考3.3节PCIe总线-配置空间介绍(三)pci_read_bases函数的执行流程如下所示,主要有:

  1. 先读BAR,读出的全为F,表示BAR不正常,意味着设备可能已经被移除,不再存在于总线上,或者设备已经进入了一种不能响应读操作的错误或低功耗状态。
  2. 向BAR写入0xFFFFFFFF,向ROM写入0xFFFF8000,然后再度BAR和ROM,以确定BAR和ROM的属性和大小。
  3. 将第一次读的BAR和ROM的默认值写回,恢复默认值。
  4. 根据第一次读取BAR的值,解析BAR的类型和属性。具体有IO空间IORESOURCE_IO,32位非预取存储器空间IORESOURCE_MEM、64位非预取存储器空间IORESOURCE_MEM_64(虽然BAR是64位,但实际上RC只会给BAR配置32位PCIe总线地址),预取存储器空间IORESOURCE_PREFETCH。内核会根据解析的BAR信息分配PCIe总线地址。
  5. 如果是64位BAR,则还需要解析高32位的BAR,步骤和第4步一样。
  6. 计算BAR所需地址空间大小。
  7. 将PCIe总线地址region转换成存储域地址resource,将存储域地址resource转换成PCIe总线地址Region,两者的起始地址必须相等。
4.2.解析桥windows

内核使用pci_read_bridge_windows函数解析桥设备的PCI_IO_BASE、PCI_PREF_MEMORY_BASE、PCI_PREF_BASE_UPPER32。若PCI_IO_BASE寄存器存在,则设置bridge->io_window = 1。若PCI_PREF_MEMORY_BASE寄存器存在,则设置bridge->pref_window = 1。若支持64位预取地址,且PCI_PREF_BASE_UPPER32寄存器存在,则设置bridge->pref_64_window = 1。PCI_MEMORY_BASE寄存器必须实现,这里可不用解析,后面直接分配地址。

复制代码
[drivers/pci/probe.c]
static void pci_read_bridge_windows(struct pci_dev *bridge)
{
	u16 io;
	u32 pmem, tmp;

	pci_read_config_word(bridge, PCI_IO_BASE, &io);
	if (!io) {
		pci_write_config_word(bridge, PCI_IO_BASE, 0xe0f0);
		pci_read_config_word(bridge, PCI_IO_BASE, &io);
		pci_write_config_word(bridge, PCI_IO_BASE, 0x0);
	}
	if (io)
		bridge->io_window = 1;

	/*
	 * DECchip 21050 pass 2 errata: the bridge may miss an address
	 * disconnect boundary by one PCI data phase.  Workaround: do not
	 * use prefetching on this device.
	 */
	if (bridge->vendor == PCI_VENDOR_ID_DEC && bridge->device == 0x0001)
		return;

	pci_read_config_dword(bridge, PCI_PREF_MEMORY_BASE, &pmem);
	if (!pmem) {
		pci_write_config_dword(bridge, PCI_PREF_MEMORY_BASE,
					       0xffe0fff0);
		pci_read_config_dword(bridge, PCI_PREF_MEMORY_BASE, &pmem);
		pci_write_config_dword(bridge, PCI_PREF_MEMORY_BASE, 0x0);
	}
	if (!pmem)
		return;

	bridge->pref_window = 1;

	if ((pmem & PCI_PREF_RANGE_TYPE_MASK) == PCI_PREF_RANGE_TYPE_64) {

		/*
		 * Bridge claims to have a 64-bit prefetchable memory
		 * window; verify that the upper bits are actually
		 * writable.
		 */
		pci_read_config_dword(bridge, PCI_PREF_BASE_UPPER32, &pmem);
		pci_write_config_dword(bridge, PCI_PREF_BASE_UPPER32,
				       0xffffffff);
		pci_read_config_dword(bridge, PCI_PREF_BASE_UPPER32, &tmp);
		pci_write_config_dword(bridge, PCI_PREF_BASE_UPPER32, pmem);
		if (tmp)
			bridge->pref_64_window = 1;
	}
}

5.分配资源(地址)

内核使用pci_bus_assign_resources函数给PCIe桥和非桥设备分配资源。详细的流程如下所示。针对每一条子总线,递归调用__pci_bus_assign_resources分配资源。一条子总线上的设备和桥分配资源的步骤如下:

  1. 遍历子总线上的所有设备,给设备需要的资源排序。
  2. 遍历子总线上设备需要的资源,调用pci_assign_resource函数分配资源,最终会调用到allocate_resource函数,先查找资源,再请求资源。这里的资源是PCIe总线地址。
  3. 分配好资源后,调用pci_update_resource函数将资源配置到设备的BAR或ROM中,同时回读BAR和ROM进行校验。将PCIe总线地址设置到BAR或者ROM寄存器当中。
  4. 如果是桥设备,则调用pci_setup_bridge初始化桥。桥的IORESOURCE_IO、IORESOURCE_MEM、IORESOURCE_PREFETCH资源范围,是桥下面所有设备相同资源类型之和。
相关推荐
lingran__8 小时前
Linux 基础常用指令万字详解(上)|文件目录命令,结合底层原理剖析
linux·运维·服务器·后端·centos·linux基础指令
轻揉小乔 真新人8 小时前
T-SQL查询进阶—理解SQL Server中的锁
服务器·数据库·sql
各类产品分享8 小时前
电力行业AR智能运维哪个品牌好
运维·ar·ar巡检
Dlrb12119 小时前
Linux驱动---sys文件系统自动添加设备节点、添加调试节点以及MISC设备驱动的解释
linux·imx6ull·misc·设备节点·sys文件系统
贾天佑忆月 迷失的昵9 小时前
客户端与服务器持续同步解析(轮询,comet,WebSocket)
运维·服务器·websocket
刘某的Cloud9 小时前
Galera Cluster mariadb 生产环境常见问题排查与运维指南
linux·运维·数据库·mariadb·集群高可用
70asunflower9 小时前
Linux 性能排查分析完全教程
linux·运维
zhangfeng11339 小时前
宝塔8 升级到 11导致 服务器无法访问外网dns失效了,无法访问bt.cn 解决方法 测试通过
运维·服务器
兵bing10 小时前
Docker Compose 配置文件归纳总结-千问
运维·docker·容器
dok1211 小时前
Johannes 《Linux内核模块与设备驱动开发:编写Linux驱动程序》 (11)kmalloc 动态内存分配和struct file中的私有数据
linux·驱动开发·算法