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。
二、枚举流程:从主桥到设备
枚举是 PCI 子系统的“发现阶段”。主机控制器驱动在初始化时注册 pci_host_bridge,随后内核调用 pci_scan_root_bus_bridge() 或 pci_scan_root_bus() 开始扫描。核心步骤如下:
- 读取根总线 0 上的每个设备/功能,通过
pci_ops访问配置空间,获取厂商 ID、设备 ID、类代码、头类型。 - 对每个有效功能创建
pci_dev,并挂到对应pci_bus的设备链表上。 - 若发现 PCI-to-PCI 桥,则为其下游总线分配新的总线号,递归扫描形成总线树。
- 调用
pci_assign_unassigned_resources()为所有设备的 BAR 分配地址空间,并配置桥的窗口。 - 设备注册到设备模型后,
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 子系统完成。