4. Linux 内核 PCI 子系统:设备枚举、驱动绑定与 pci_dev

这一章咱们聊聊 Linux 内核里 PCI 子系统那点事。

说实话,很多面试者一上来就背源码。

背得挺溜,但一问「设备是怎么被发现的?」就卡壳。

我个人习惯,先看大局,再扣细节。

4.1 PCI 设备枚举:从硬件到软件

设备枚举,说白了就是内核怎么把物理设备一个个找出来。

这个过程,在系统启动时就开始了。

4.1.1 硬件层:PCI 配置空间

每个 PCI 功能,都有 256 字节的配置空间。

前 64 字节是标准头部,里面藏着 Vendor ID、Device ID、Class Code 这些关键信息。

我当年调一块 FPGA 板卡时,就遇到 Vendor ID 读出来全是 0xFFFF,那说明设备根本不存在。

4.1.2 软件层:Linux 枚举流程

  1. Root Bridge 初始化:ACPI 或 Device Tree 告知内核,哪里是 PCI 总线的根。
  2. 总线扫描:内核从总线 0 开始,递归扫描。读 Vendor ID,如果不是 0xFFFF,说明有设备。
  3. 分配资源:为每个设备分配 MMIO 地址空间、IO 端口和中断号。这一步靠的是 PCI 桥的窗口配置。
  4. 创建 pci_dev 对象:每个找到的功能,都变成一个 pci_dev 结构体实例,挂到 bus 链表上。
小提示: 总线上如果有一个设备是 Multi-Function 的,它的每个 Function 都会被枚举成独立的 pci_dev。

为什么会这样?

因为硬件设计上,每个 Function 都有自己的配置空间。所以内核在扫描时,会检查 Header Type 寄存器的第 7 位,来判断是不是 Multi-Function 设备。

4.2 驱动绑定:pci_driver 模型

设备找到了,接下来就是找个驱动来管它。

4.2.1 pci_driver 结构体

写一个 PCIe 驱动,核心就是要实现一个 pci_driver。

static struct pci_driver my_driver = {
    .name        = "my_pcie_driver",
    .id_table    = my_ids,        // 告诉内核,你要管哪些设备
    .probe       = my_probe,      // 设备找到了,该初始化了
    .remove      = my_remove,     // 设备拔掉或卸载,该清理了
    .suspend     = my_suspend,    // 省电时调用
    .resume      = my_resume,     // 唤醒时调用
};

这里面,id_table 是关键。它是一个 struct pci_device_id 数组,里面填写你支持的 Vendor ID、Device ID。

注意: 如果 id_table 不匹配,你的 probe 永远不会被调用。我曾经在移植驱动时,忘了更新 id_table,结果 debug 了整整两天。

4.2.2 绑定过程

当 pci_register_driver() 被调用时,内核会做两件事:

  • 把这个驱动添加到 pci_bus_type 的驱动链表上。
  • 遍历所有已经枚举出来的 pci_dev,看看有没有匹配的。

匹配规则很简单:对比 pci_dev→vendor 和 device 是否在驱动 id_table 里。

如果匹配,就调用 pci_dev→driver→probe( pci_dev, id )。

嗯,这里要注意,probe 函数成功返回后,设备和驱动之间的绑定就正式建立了。

4.3 pci_dev 结构体:你真正要打交道的东西

写 DMA 驱动时,你几乎天天跟 pci_dev 打交道。

它包含了设备的所有关键信息。

我把最常用的字段整理了一下,做成表格,方便你记忆:

字段 类型 含义 DMA 场景
vendor unsigned int 厂商 ID,来自配置空间 识别设备用
device unsigned int 设备 ID 识别设备用
devfn unsigned int 设备号+功能号,高 5 位设备号,低 3 位功能号 确定具体哪个功能
bus struct pci_bus * 所属的总线对象 获取总线号、域号
resource[] struct resource 6 个 BAR 空间,每个 BAR 存储基址和长度 找到设备的内存和 IO 窗口
irq unsigned int 中断号 注册中断处理函数
is_busmaster unsigned int 标志位:是否启用总线 Master DMA 操作必须置 1
dma_mask u64 设备支持的最大 DMA 地址 分配 DMA buffer 时决定使用何种 API
dev struct device 内嵌的通用设备对象 用作 devm_* 系列 API 的参数

这里我想重点说下 resource[]。

BAR 空间是设备与主机通信的窗口。DMA 引擎的控制寄存器、状态寄存器、描述符环表,通常都映射到某个 BAR 里。

获取 BAR 的物理地址,用 pci_resource_start( pdev, bar ),长度用 pci_resource_len( pdev, bar )。

4.4 pci_dev 在 DMA 驱动中的典型用法

我来给你画个简图,把上面这些串起来:

PCIe DMA 驱动核心流程中的 pci_dev 使用 pci_dev 在 DMA 驱动中的核心流程 PCI 设备枚举 pci_dev 创建 (pci_scan_device) 驱动匹配 (probe 调用) probe 函数内核心动作 ① 读 pci_resource_start/len ② 配置 DMA 掩码 (dma_set_mask) ③ 注册中断 (request_irq) pci_dev 关键字段在 DMA 中的作用 → devfn:定位设备所属功能 → bus:确定设备所在总线(域:总线:设备:功能) → resource[]:BAR 的物理地址和大小 → dma_mask:决定能否使用 64-bit 地址 → is_busmaster:DMA 传输必须为 1 → irq:MSI/MSI-X 或 INTx 中断号 DMA 传输就绪 (pci_dev 一直存活可用)

我当年踩过最深的坑是 dma_mask 设置不当。

有一次设备只支持 32-bit DMA 地址,但我没设 dma_mask,内核给分配了 64-bit 的 DMA buffer,结果设备读写全乱套了。

后来养成了习惯:probe 里第一件事就是调用 dma_set_mask_and_coherent()。

4.5 总结一下

为了面试能答得好,我建议你重点掌握以下几点:

  • 枚举过程:内核怎么从硬件走到 pci_dev,知道 Vendor ID 0xFFFF 表示空槽。
  • 驱动绑定:pci_driver 的 id_table 怎么用,probe 何时被调用。
  • pci_dev 核心字段:特别是 resource[]、irq、dma_mask 和 is_busmaster。
  • 常见陷阱:比如忘记初始化 DMA 掩码、BAR 地址映射错误、中断未释放导致 rmmod 时 panic。

嗯,这一节先到这儿。