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 枚举流程
- Root Bridge 初始化:ACPI 或 Device Tree 告知内核,哪里是 PCI 总线的根。
- 总线扫描:内核从总线 0 开始,递归扫描。读 Vendor ID,如果不是 0xFFFF,说明有设备。
- 分配资源:为每个设备分配 MMIO 地址空间、IO 端口和中断号。这一步靠的是 PCI 桥的窗口配置。
- 创建 pci_dev 对象:每个找到的功能,都变成一个 pci_dev 结构体实例,挂到 bus 链表上。
为什么会这样?
因为硬件设计上,每个 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。
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 驱动中的典型用法
我来给你画个简图,把上面这些串起来:
我当年踩过最深的坑是 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。
嗯,这一节先到这儿。