3. Linux PCI子系统概览

Linux PCI 子系统是内核中结构最清晰、层次最分明的总线框架之一。它把“主机控制器如何访问配置空间”“总线如何被编号”“设备如何被描述”“驱动如何匹配”“BAR 资源如何分配”这几件事拆成不同对象协作完成。理解 pci_host_bridge、pci_bus、pci_dev、pci_driver 四类核心对象及其枚举流程,是后续编写 PCIe 主机控制器驱动的基础。

一、四类核心对象与职责

PCI 子系统采用“桥—总线—设备—驱动”的分层模型。主机控制器驱动首先注册一个 pci_host_bridge,它代表一颗 PCI 主桥(在 PCIe 中通常对应 RC,即 Root Complex)。主桥向下引出一条根总线,内核用 pci_bus 描述这条总线及其子总线。总线上挂载的每个功能用 pci_dev 表示,而驱动侧用 pci_driver 描述“我能支持哪些设备”。

  • pci_host_bridge:主桥抽象,持有配置空间访问方法(pci_ops)、总线号范围、窗口资源(windows)、sysdata 私有数据,是枚举的起点。
  • pci_bus:一条 PCI 总线,含总线号、父桥、设备链表、资源窗口,可嵌套形成总线树。
  • pci_dev:一个 PCI 功能(function),含厂商/设备 ID、BAR、配置空间、所属总线、中断信息。
  • pci_driver:驱动对象,含 id_table、probe/remove、电源管理回调,通过 ID 匹配绑定到 pci_dev。
Linux PCI 子系统对象层次与枚举数据流 pci_host_bridge pci_ops / windows / sysdata 枚举起点 pci_bus (bus 0) 根总线 / 子总线树 pci_dev (00:00.0) RC 内部功能 pci_dev (01:00.0) 端点设备 pci_dev (02:00.0) 下游设备 pci_driver id_table → probe / remove ID 匹配绑定 资源分配 BAR / 总线号 / 窗口 枚举流程:扫描配置空间 → 创建 pci_dev → 分配总线号与 BAR → 注册设备 → 驱动匹配 probe

二、枚举流程:从主桥到设备

枚举是 PCI 子系统的“发现阶段”。主机控制器驱动在初始化时注册 pci_host_bridge,随后内核调用 pci_scan_root_bus_bridge() 或 pci_scan_root_bus() 开始扫描。核心步骤如下:

  1. 读取根总线 0 上的每个设备/功能,通过 pci_ops 访问配置空间,获取厂商 ID、设备 ID、类代码、头类型。
  2. 对每个有效功能创建 pci_dev,并挂到对应 pci_bus 的设备链表上。
  3. 若发现 PCI-to-PCI 桥,则为其下游总线分配新的总线号,递归扫描形成总线树。
  4. 调用 pci_assign_unassigned_resources() 为所有设备的 BAR 分配地址空间,并配置桥的窗口。
  5. 设备注册到设备模型后,pci_bus_match() 用 pci_driver.id_table 与设备 ID 匹配,匹配成功则调用驱动的 probe()。

关键点:枚举顺序决定了总线号、设备号和 BAR 地址的最终布局。主机控制器驱动必须保证 pci_ops 在枚举前已可用,否则配置空间读取失败,整条总线无法被发现。

三、资源管理:BAR、总线号与窗口

PCI 设备通过 BAR(Base Address Register)向系统申请 MMIO 或 I/O 空间。内核在枚举后统一分配资源,避免冲突。资源管理涉及三类对象:

资源类型 描述对象 分配方式 常见问题
MMIO BAR pci_dev.resource[] 从主桥窗口或父桥窗口切分 窗口过小导致分配失败
I/O BAR pci_dev.resource[] I/O 端口空间分配 PCIe 中通常不使用
总线号 pci_bus.number 枚举时递归分配 总线号耗尽或重复
桥窗口 pci_bus.window[] 根据下游设备需求汇总 窗口未对齐导致设备不可见

主机控制器驱动通常通过 pci_add_resource() 或 pci_host_bridge.windows 向内核声明可用窗口。若窗口声明错误,枚举时 BAR 分配会失败,日志中常见:

pci 0000:01:00.0: BAR 0: no space for [mem size 0x100000]
pci 0000:01:00.0: BAR 0: failed to assign
pci 0000:00:00.0: PCI bridge to [bus 01]: bridge window not assigned

四、驱动匹配与典型场景

pci_driver 通过 id_table 声明支持的设备。匹配时内核比较厂商 ID、设备 ID、子厂商 ID、子设备 ID 和类代码。匹配成功后调用 probe(),驱动通常在此完成 pci_enable_device()、pci_request_regions()、pci_iomap() 和中断申请。

static const struct pci_device_id my_pci_ids[] = {
    { PCI_DEVICE(0x1234, 0x5678) },
    { PCI_DEVICE_CLASS(PCI_CLASS_NETWORK_ETHERNET, 0xffffff) },
    { 0, }
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);

static struct pci_driver my_pci_driver = {
    .name     = "my_pcie",
    .id_table = my_pci_ids,
    .probe    = my_probe,
    .remove   = my_remove,
};
module_pci_driver(my_pci_driver);

调试枚举问题时,可查看 /sys/bus/pci/devices/、lspci -tv 和 dmesg | grep pci。若设备未出现,优先检查 pci_ops 读写是否正常、主桥窗口是否声明、链路训练是否完成。

五、常见误区与复盘

  • 误区一:认为 pci_host_bridge 就是 pci_bus。实际上主桥是控制器抽象,总线是枚举后形成的拓扑节点。
  • 误区二:在 probe() 中直接访问 BAR 地址而不先 pci_enable_device(),导致设备未使能、访问异常。
  • 误区三:忽略总线号分配范围,导致下游桥无法获得总线号,设备“消失”。
  • 复盘要点:枚举失败时按“配置空间访问 → 设备发现 → 总线号分配 → BAR 分配 → 驱动匹配”顺序逐层排查,能快速定位问题所在。

掌握这四类对象和枚举流程后,后续编写 PCIe 主机控制器驱动时,就能清楚知道自己的代码应挂在哪个层次:实现 pci_ops、注册 pci_host_bridge、声明窗口资源,剩下的枚举、资源分配和驱动匹配交给 PCI 子系统完成。