基于Linux的PCIe设备驱动开发入门到精通--2.2 PCI子系统在内核中的角色与初始化

目录

一、PCI子系统的核心职责

二、初始化的起点:从 start_kernel() 说起

三、核心初始化函数:pci_subsys_init()

四、深度扫描:pci_scan_root_bus() 与 pci_scan_slot()

五、与固件的交互:ACPI 与 DSDT

六、初始化完成后的状态

七、总结与下节预告


第一部分:基石篇 - 理解PCIe与Linux内核 Linux设备驱动模型与PCI子系统 2.2 PCI子系统在内核中的角色与初始化


在上一讲中,我们学习了Linux设备驱动模型的“宪法”——bus, device, driver 三位一体的哲学。今天,我们将镜头聚焦于 PCI子系统,它是这套哲学在PCI/PCIe世界中的具体执行者。理解PCI子系统如何在内核启动时自动扫描总线、发现设备、并为驱动开发铺平道路,是成为高级驱动工程师的必经之路。

想象一下:当你按下电脑电源键,内核开始加载。此时,内存是空的,外设是“沉默”的。那么,内核是如何知道你的主板上插了几块PCIe卡?它们的厂商ID、设备ID是什么?内存空间(BAR)又映射在哪里?这一切的“寻宝图”绘制工作,就是由 PCI子系统 在启动早期默默完成的。我们将深入内核源码的“心脏地带”,揭示这段不为人知的初始化之旅。

一、PCI子系统的核心职责

PCI子系统是内核中负责管理和操作所有PCI/PCIe设备的软件框架。它的核心任务可以概括为:

  1. 总线枚举 (Bus Enumeration): 扫描系统中的所有PCI总线,发现并识别连接在上面的每一个设备。
  2. 资源分配 (Resource Assignment): 为每个设备的BAR(Base Address Register)分配系统内存和I/O端口地址,解决资源冲突。
  3. 设备抽象: 为每个发现的设备创建内核数据结构(struct pci_dev),并将其注册到设备驱动模型中。
  4. 提供API: 为PCI驱动开发者提供一套标准的API(如 pci_ioremap_barpci_enable_device),简化驱动开发。
  5. 电源管理与热插拔: 支持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);

让我们分解这四个关键步骤:

  1. bus_register(&pci_bus_type)

    • 这是将PCI总线 (struct bus_type注册到设备驱动模型 的核心步骤。
    • pci_bus_type 定义了PCI总线的特性,包括其 match (匹配函数)、probe (探测函数)、remove (移除函数) 等回调。
    • 注册后,/sys/bus/pci/ 目录被创建,PCI总线正式成为设备驱动模型的一部分。
  2. device_driver_register(&pci_dev_driver)

    • 注册一个特殊的“通用”PCI驱动 pci_dev_driver
    • 这个驱动非常简单,它的 probe 函数几乎什么都不做。它的存在是为了让PCI设备在没有找到专用驱动时,也能被“管理”起来,避免内核报错。
  3. device_register(&pciid_device)

    • 注册一个名为 pciid 的虚拟设备。
    • 它的主要作用是让 depmod 工具在构建模块依赖时,知道 pci 模块的存在,确保PCI驱动能正确加载。
  4. 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;
}
  1. pci_scan_child_bus(bus)

    • 这个函数会遍历总线上的32个设备槽位 (Slot 0-31)。
    • 对于每个槽位,调用 pci_scan_slot()
  2. pci_scan_slot()

    • 在一个槽位上,PCI标准允许有最多8个功能 (Function)(多见于多功能设备,如带网卡和声卡的扩展卡)。
    • pci_scan_slot() 会遍历 Function 0-7。
    • 对于每个 Function,调用 pci_scan_single_device()
  3. pci_scan_single_device() - 发现一个设备!

    • 这是发现单个PCI设备的最终函数。
    • 它通过配置空间访问(通常使用 CONFIG_ADDRESS 和 CONFIG_DATA I/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插槽的插入/拔出事件。
  • 流程:
    1. 内核启动时,ACPI子系统解析DSDT等表。
    2. PCI子系统通过ACPI获取根总线列表和预分配的资源信息。
    3. 在 pci_scan_root_bus() 时,这些信息被用来指导扫描和资源分配。
六、初始化完成后的状态

pci_subsys_init() 成功执行后,内核世界已经发生了翻天覆地的变化:

  1. /sys/bus/pci/devices/ 目录下充满了 0000:xx:yy.z 格式的符号链接,每一个都代表一个被发现的PCI设备。
  2. 每个设备的配置空间信息(BAR、中断等)已被读取并存储在 struct pci_dev 中。
  3. 设备驱动模型开始工作:内核会遍历所有已注册的PCI驱动(pci_driver),并尝试与新发现的设备进行匹配。
  4. 如果匹配成功,驱动的 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 结构体,实现 proberemove 函数,并完成一个可以编译、加载、并在 dmesg 中看到“Hello PCI!”的驱动。这将是理论与实践的完美交汇,标志着你真正踏入了PCIe驱动开发的大门。

动手实践建议:

  1. 在QEMU虚拟机中运行 dmesg | grep -i pci,观察内核启动时PCI子系统的日志,寻找 pci 0000:xx:yy.z: [firmware]: reserved 或 PCI host bridge 等关键字。
  2. 对比 lspci 的输出和 /sys/bus/pci/devices/ 目录下的内容,理解它们的对应关系。
  3. (进阶)查阅Linux内核源码(如 drivers/pci/probe.c),找到 pci_scan_single_device() 函数,阅读其代码逻辑。

请务必结合 dmesg 日志来理解初始化流程。如果您对PCI子系统的某个初始化步骤有深入疑问,欢迎在评论区提出。

预告: 下一节《2.3 编写PCI驱动框架》,我们将动手编写第一个真正的PCIe驱动!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小蘑菇二号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值