目录
四、深度扫描:pci_scan_root_bus() 与 pci_scan_slot()
第一部分:基石篇 - 理解PCIe与Linux内核 Linux设备驱动模型与PCI子系统 2.2 PCI子系统在内核中的角色与初始化
在上一讲中,我们学习了Linux设备驱动模型的“宪法”——bus, device, driver 三位一体的哲学。今天,我们将镜头聚焦于 PCI子系统,它是这套哲学在PCI/PCIe世界中的具体执行者。理解PCI子系统如何在内核启动时自动扫描总线、发现设备、并为驱动开发铺平道路,是成为高级驱动工程师的必经之路。
想象一下:当你按下电脑电源键,内核开始加载。此时,内存是空的,外设是“沉默”的。那么,内核是如何知道你的主板上插了几块PCIe卡?它们的厂商ID、设备ID是什么?内存空间(BAR)又映射在哪里?这一切的“寻宝图”绘制工作,就是由 PCI子系统 在启动早期默默完成的。我们将深入内核源码的“心脏地带”,揭示这段不为人知的初始化之旅。
一、PCI子系统的核心职责
PCI子系统是内核中负责管理和操作所有PCI/PCIe设备的软件框架。它的核心任务可以概括为:
- 总线枚举 (Bus Enumeration): 扫描系统中的所有PCI总线,发现并识别连接在上面的每一个设备。
- 资源分配 (Resource Assignment): 为每个设备的BAR(Base Address Register)分配系统内存和I/O端口地址,解决资源冲突。
- 设备抽象: 为每个发现的设备创建内核数据结构(
struct pci_dev),并将其注册到设备驱动模型中。 - 提供API: 为PCI驱动开发者提供一套标准的API(如
pci_ioremap_bar,pci_enable_device),简化驱动开发。 - 电源管理与热插拔: 支持PCIe设备的节能状态(D-states)和热插拔事件处理。
二、初始化的起点:从 start_kernel() 说起
内核的初始化是一个宏大的工程,PCI子系统的初始化只是其中一环。它的旅程始于 init/main.c 中的 start_kernel() 函数。
c
深色版本
asmlinkage __visible void __init start_kernel(void)
{
// ... 大量初始化 ...
setup_arch(&command_line); // 架构相关初始化 (x86, ARM等)
// ...
rest_init(); // 启动init进程
}
关键的入口在 setup_arch() 中,对于x86架构,它会调用 x86_init.resources.reserve_resources(),最终触发PCI子系统的初始化。
三、核心初始化函数:pci_subsys_init()
PCI子系统的主初始化函数是 pci_subsys_init(),它通常在内核启动的 subsys_initcall 阶段被调用。
c
深色版本
// drivers/pci/main.c
static int __init pci_subsys_init(void)
{
// 1. 注册PCI总线类型到设备驱动模型
bus_register(&pci_bus_type);
// 2. 注册PCI设备驱动
device_driver_register(&pci_dev_driver);
// 3. 注册PCI设备
device_register(&pciid_device);
// 4. 扫描并初始化PCI总线 (最关键的一步!)
if (pci_probe)
pci_scan_root_buses();
return 0;
}
subsys_initcall(pci_subsys_init);
让我们分解这四个关键步骤:
-
bus_register(&pci_bus_type)- 这是将PCI总线 (
struct bus_type) 注册到设备驱动模型 的核心步骤。 pci_bus_type定义了PCI总线的特性,包括其match(匹配函数)、probe(探测函数)、remove(移除函数) 等回调。- 注册后,
/sys/bus/pci/目录被创建,PCI总线正式成为设备驱动模型的一部分。
- 这是将PCI总线 (
-
device_driver_register(&pci_dev_driver)- 注册一个特殊的“通用”PCI驱动
pci_dev_driver。 - 这个驱动非常简单,它的
probe函数几乎什么都不做。它的存在是为了让PCI设备在没有找到专用驱动时,也能被“管理”起来,避免内核报错。
- 注册一个特殊的“通用”PCI驱动
-
device_register(&pciid_device)- 注册一个名为
pciid的虚拟设备。 - 它的主要作用是让
depmod工具在构建模块依赖时,知道pci模块的存在,确保PCI驱动能正确加载。
- 注册一个名为
-
pci_scan_root_buses()- “寻宝”的开始!- 这是整个PCI初始化过程中最核心、最复杂的一步。
- 它会遍历系统中所有的根总线 (Root Bus)。在x86系统中,根总线通常就是CPU直接连接的PCI总线(Bus 0)。
- 对于每一个根总线,它会调用
pci_scan_root_bus()进行深度扫描。
四、深度扫描:pci_scan_root_bus() 与 pci_scan_slot()
pci_scan_root_bus() 是设备发现的“侦察兵”。
c
深色版本
struct pci_bus *pci_scan_root_bus(...) {
struct pci_bus *bus;
// 1. 分配并初始化一个pci_bus结构
bus = pci_create_root_bus(...);
// 2. 扫描该总线上的所有设备 (Slot 0 to 31)
pci_scan_child_bus(bus);
// 3. 分配总线号、进行资源优化 (如重新分配BAR)
pci_bus_size_bridges(bus);
pci_bus_assign_resources(bus);
return bus;
}
-
pci_scan_child_bus(bus)- 这个函数会遍历总线上的32个设备槽位 (Slot 0-31)。
- 对于每个槽位,调用
pci_scan_slot()。
-
pci_scan_slot()- 在一个槽位上,PCI标准允许有最多8个功能 (Function)(多见于多功能设备,如带网卡和声卡的扩展卡)。
pci_scan_slot()会遍历 Function 0-7。- 对于每个 Function,调用
pci_scan_single_device()。
-
pci_scan_single_device()- 发现一个设备!- 这是发现单个PCI设备的最终函数。
- 它通过配置空间访问(通常使用
CONFIG_ADDRESS和CONFIG_DATAI/O端口,或在现代系统中使用增强配置访问机制 ECAM)读取设备的Vendor ID和Device ID。 - 如果Vendor ID不是
0xFFFF(无效值),则确认设备存在。 - 创建
struct pci_dev:为这个设备分配内存,填充其基本信息(bus, device, function, vendor, device, class等)。 - 读取并解析配置空间:读取BAR、中断引脚、中断线等信息,并存储在
pci_dev结构中。 - 将设备注册到设备驱动模型:调用
device_register(&pdev->dev)。这一步至关重要,它让这个PCI设备在/sys/devices/...下有了自己的“家”,并触发了驱动匹配过程。
五、与固件的交互:ACPI 与 DSDT
在现代x86系统中,PCI子系统的初始化离不开与固件 (Firmware) 的协作,特别是 ACPI (Advanced Configuration and Power Interface)。
- 作用:
- 提供总线信息:ACPI的DSDT (Differentiated System Description Table) 表描述了系统中有哪些PCI总线及其属性。
- 资源预留:ACPI表会声明哪些内存区域和IRQ是系统保留的,PCI子系统在分配资源时必须避开这些区域。
- 热插拔支持:ACPI提供机制来通知操作系统PCIe插槽的插入/拔出事件。
- 流程:
- 内核启动时,ACPI子系统解析DSDT等表。
- PCI子系统通过ACPI获取根总线列表和预分配的资源信息。
- 在
pci_scan_root_bus()时,这些信息被用来指导扫描和资源分配。
六、初始化完成后的状态
当 pci_subsys_init() 成功执行后,内核世界已经发生了翻天覆地的变化:
/sys/bus/pci/devices/目录下充满了0000:xx:yy.z格式的符号链接,每一个都代表一个被发现的PCI设备。- 每个设备的配置空间信息(BAR、中断等)已被读取并存储在
struct pci_dev中。 - 设备驱动模型开始工作:内核会遍历所有已注册的PCI驱动(
pci_driver),并尝试与新发现的设备进行匹配。 - 如果匹配成功,驱动的
probe()函数将被调用,驱动正式接管设备。
七、总结与下节预告
今天我们揭开了PCI子系统初始化的神秘面纱:
- 它始于
pci_subsys_init(),核心是注册总线和扫描设备。 pci_scan_root_buses()及其后续函数链是设备发现的“引擎”。- 配置空间访问是读取设备硬件信息的唯一途径。
- ACPI 在现代系统中扮演着不可或缺的“向导”角色。
- 最终,每一个PCI设备都被抽象为一个
struct pci_dev并注册到设备驱动模型,为驱动的加载铺平了道路。
理解这个过程,你就明白了为什么你的PCIe驱动不需要自己去“找”设备——内核已经帮你把“名单”和“地址”都准备好了。下一讲《2.3 编写PCI驱动框架:pci_driver, probe, remove》,我们将利用内核提供的这一切,动手编写我们的第一个完整的PCIe驱动框架。你将学习如何定义 pci_driver 结构体,实现 probe 和 remove 函数,并完成一个可以编译、加载、并在 dmesg 中看到“Hello PCI!”的驱动。这将是理论与实践的完美交汇,标志着你真正踏入了PCIe驱动开发的大门。
动手实践建议:
- 在QEMU虚拟机中运行
dmesg | grep -i pci,观察内核启动时PCI子系统的日志,寻找pci 0000:xx:yy.z: [firmware]: reserved或PCI host bridge等关键字。 - 对比
lspci的输出和/sys/bus/pci/devices/目录下的内容,理解它们的对应关系。 - (进阶)查阅Linux内核源码(如
drivers/pci/probe.c),找到pci_scan_single_device()函数,阅读其代码逻辑。
请务必结合 dmesg 日志来理解初始化流程。如果您对PCI子系统的某个初始化步骤有深入疑问,欢迎在评论区提出。
预告: 下一节《2.3 编写PCI驱动框架》,我们将动手编写第一个真正的PCIe驱动!
107

被折叠的 条评论
为什么被折叠?



