侧边栏壁纸
  • 累计撰写 53 篇文章
  • 累计收到 3 条评论

P4-03 · 从 0 写一个最小 PCIe 内核驱动:24 行 probe 一切,然后逐行升级

PacketForge
2026-08-18 / 0 评论 / 0 阅读 /
对应路线图:Phase 4 (Day 120-140) | 内核 v6.6 | 实验代码:code/dpu_dev/01_pci/,引用均标 文件:行号
上一篇:P4-02 MSI-X | 下一篇:P5-01 DMA/IOMMU

一句话定位

PCIe 驱动的最小骨架不是操作寄存器,而是参与内核的"设备-驱动匹配协议":填一张 pci_device_id 表 + 一个 probe 回调,driver core 就会在设备出现时替你调它。code/dpu_dev/01_pci/pci_scan.c 全文件只有 24 行,却能 probe 整条总线上的所有设备——这是每个网卡驱动(ixgbe、mlx5)的同款起点。

真实源码精读

① 全文件带读:code/dpu_dev/01_pci/pci_scan.c:1-24

#include <linux/module.h>
  #include <linux/pci.h>

  static int probe(struct pci_dev *pdev, const struct pci_device_id *id)
  {
      printk("pci_scan: %04x:%04x (rev %02x) BAR0=%llx IRQ=%d\n",
             pdev->vendor, pdev->device, pdev->revision,
             pci_resource_start(pdev, 0), pdev->irq);
      return 0;
  }

  static struct pci_device_id ids[] = {
      { PCI_ANY_ID, PCI_ANY_ID, PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0 },
      { 0, }
  };
  MODULE_DEVICE_TABLE(pci, ids);

  static struct pci_driver drv = {
      .name = "pci_scan",
      .id_table = ids,
      .probe = probe,
  };
  module_pci_driver(drv);
  MODULE_LICENSE("GPL");

逐行拆:

  • probe 回调(:4-10):签名 (struct pci_dev *, const struct pci_device_id *) 是 PCI 子系统的硬约定。这里读的 vendor/device/revision 正是 P4-01 里 pci_scan_device 从 Config Space 填进 pci_dev 的(probe.c:2507-2508);pci_resource_start(pdev, 0) 是 BAR0 物理地址(宏定义 pci.h:2107);pdev->irq 是内核分配的 IRQ 号(pci.h:420)。返回 0 = "这个设备我接管了"。
  • ids 表(:12-16):全 PCI_ANY_ID 表示匹配所有设备——所以本模块会 probe 系统里每一个 PCI 设备,只能在学习机上玩。真正的驱动用 PCI_DEVICE(0x8086, 0x10fb) 这类精确匹配(宏在 pci.h:962)。表项结构在 mod_devicetable.h:44-50:vendor/device/subvendor/subdevice/class/class_mask 六维过滤 + driver_data 私有数据。
  • module_pci_driver(drv)(:23):展开成 module_driver(drv, pci_register_driver, pci_unregister_driver)(pci.h:1598-1599),自动生成 init/exit,替换手写 module_init/module_exit 的样板代码。

② 匹配与 probe 触发:struct pci_driver 与 driver core(pci.h:928-943)

struct pci_driver {
    struct list_head    node;
    const char        *name;
    const struct pci_device_id *id_table;    /* Must be non-NULL for probe to be called */
    int  (*probe)(struct pci_dev *dev, const struct pci_device_id *id);    /* New device inserted */
    void (*remove)(struct pci_dev *dev);    /* Device removed (NULL if not a hot-plug capable driver) */
    // ...省略 suspend/resume/shutdown/sriov/err_handler ...
    struct device_driver    driver;
    // ...
};

id_table 注释说得很直白:没有它 probe 就不会被调用。注册入口 __pci_register_driver(pci-driver.c:1451-1467)只做一件事:填 drv->driver.bus = &pci_bus_type 然后 driver_register()——后面就是通用设备模型的 match → probe 机器了。设备侧由 pci_device_probe(pci-driver.c:444-467)接棒:

static int pci_device_probe(struct device *dev)
{
    int error;
    struct pci_dev *pci_dev = to_pci_dev(dev);
    struct pci_driver *drv = to_pci_driver(dev->driver);
    // ...
    pci_assign_irq(pci_dev);

    error = pcibios_alloc_irq(pci_dev);
    // ...
    pci_dev_get(pci_dev);
    error = __pci_device_probe(drv, pci_dev);
    // ...
}

注意顺序:先 pci_assign_irq 分配 IRQ,再进驱动的 probe——所以 probe 里 pdev->irq 一定已经有值。

③ remove 与卸载:内核兜底(pci-driver.c:469-510 节选)

static void pci_device_remove(struct device *dev)
{
    struct pci_dev *pci_dev = to_pci_dev(dev);
    struct pci_driver *drv = pci_dev->driver;

    if (drv->remove) {
        // ... runtime PM barrier ...
        drv->remove(pci_dev);
        // ...
    }
    pcibios_free_irq(pci_dev);
    pci_dev->driver = NULL;
    // ...
}

drv->remove 是驱动自己的清理窗口(放走 IRQ、unmap、还资源);窗口之外的事内核兜底:pcibios_free_irq 收尾 IRQ(:486)、解绑 driver = NULL(:487)。rmmod 会经 pci_unregister_driver(pci-driver.c:1480)对每个被该驱动接管的设备各调一次 remove。

④ 升级路线:把"只读"变成"拥有"

pci_scan.c 只读不占,等于借设备看了一眼。要真正"拥有"一个 BAR(比如网卡的 MMIO 寄存器区),probe 里按序做四件事(API 均已核实存在):

步骤API位置作用
1pci_enable_device(pdev)pci.c:2191打开 IO/MEM decode,唤醒设备
2pci_request_regions(pdev, name)pci.c:4273声明 resource 所有权(/proc/iomem 可见)
3pci_ioremap_bar(pdev, bar)pci.c:252映射 BAR 到内核虚拟地址
4request_irq(pci_irq_vector(pdev, 0), handler, ...)interrupt.h:165挂中断(配合 P4-02 的 MSI-X)

remove 里严格逆序:free_irq(interrupt.h:196)→ iounmap → pci_release_regions(pci.c:4254)→ pci_disable_device(pci.c:2379)。

🐧 内核机制对照:驱动协议 vs 用户态"野路子"

环节内核驱动(本模块)DPDK 用户态(bus/pci)
设备发现driver core 匹配 id_table → probe自己扫 /sys/bus/pci/devices/
"接管"动作probe 返回 0,设备归属本驱动bind 到 vfio-pci/uio 驱动后拿 fd
BAR 访问ioremap → 内核虚拟地址mmap,pci_common_uio.c:178-179
中断request_irq + 上半部/NAPI轮询或 epoll 中断线程
资源冲突防护pci_request_regions 互斥依赖内核驱动让位(bound to VFIO)

DPDK 不是"没有驱动协议",而是把协议放进了用户态:内核里那个 vfio-pci 驱动替它完成了 probe/接管,用户态只负责 mmap 和轮询。

面试 3 问

  1. probe 什么时候被调用? → 设备注册或驱动注册后 driver core 完成 match 即调;前提是 id_table 非 NULL(pci.h:931 注释);入口 pci_device_probe(pci-driver.c:444)先 pci_assign_irq 再调驱动的 probe(pci-driver.c:453-460)。
  2. PCI_ANY_ID 表项的风险? → 匹配所有设备(pci_scan.c:12-13),probe 返回 0 会接管每一个设备(pci_scan.c:9),真实驱动必须用 PCI_DEVICE 精确匹配(pci.h:962-964)。
  3. remove 里不释放资源会怎样? → IRQ 方面内核会兜底 pcibios_free_irq(pci-driver.c:486),但 ioremap 的地址和 request_regions 占用的 resource 泄漏,再加载驱动会 EBUSY——所以 remove 必须逆序归还(见上表)。

🔧 动手实验

cd code/dpu_dev/01_pci
make                                  # Makefile:1-2,obj-m += pci_scan.o
sudo insmod pci_scan.ko
dmesg | tail -30                      # 每个 PCI 设备一行:vendor:device rev BAR0 IRQ
sudo rmmod pci_scan

升级练习(在实验机上,不要在主力机上):给 probe 加 pci_enable_device + pci_request_regions,验证 /proc/iomem 里出现 pci_scan 名字段;再试 pci_ioremap_bar 读 4 字节网卡寄存器与 lspci -xxxx 对拍。数据落点 perf-data/P4/03-pci-scan-dmesg.txt(dmesg 原文 + iomem 段 + 升级版对比)。


下一篇:P5-01 · DMA/IOVA/IOMMU:设备怎么看到内存

0

评论 (0)

取消