上一篇我们把 PCI 总线的初始化流程、设备枚举和总线的底层数据结构捋了一遍算是把“硬件怎么变成软件眼中的 pci_dev”这件事讲清楚了。这一篇我们把视角转到驱动侧聚焦在 Linux 内核里 PCI 驱动的标准框架上一个 PCI 驱动注册时会发生什么、驱动和设备是怎么匹配上的、probe 函数里为什么要按那个顺序写、BAR 空间怎么映射、中断怎么申请以及最后再聊几个实际开发中一定会踩的坑。适合看这篇的人包括准备写自己的 PCIe 网卡/加速卡驱动的嵌入式工程师正在读内核源码却总是绕晕在 bus_type 和 pci_driver 之间的内核初学者以及已经在改驱动但经常遇到“probe 不进”、“资源报错”的运维和业务开发。我会尽量用实战视角来讲不贴大段源码翻译重点是让你知道“为什么代码要这么写”。1. PCI 驱动框架的整体脉络从 device 到 driver1.1 总线、设备、驱动三者到底是怎么咬合的很多人第一次看内核里的 PCI 驱动会被一堆结构体劝退pci_driver、pci_dev、pci_bus、pci_bus_type、device_driver、resource…… 其实核心关系就一句话PCI 总线是一种bus_type它负责把挂在同一条总线上的一堆“设备”和一堆“驱动”做配对。你可以把整个模型类比成一个招聘市场struct pci_dev像是求职者的简历里面写了这个人的身份证号vendor ID、device ID、住址bus/device/function 编号、能力范围class code还有他有哪些电话和邮箱可以联系BAR 地址空间。struct pci_driver像是招聘岗位的 JD里面明说自己需要什么样的人id_table招到人之后谁来带probe离职时怎么交接remove。pci_bus_type就是中介平台。平台维护了两个名单已登记简历库和已发布岗位库。每来一份新简历平台就拿着简历的身份证号去岗位库里配对每发布一个新岗位平台也会拿岗位要求去简历库反向扫描。在内核代码里这个“平台”就是全局注册的pci_bus_type。它的match方法决定了设备与驱动是否互相认可。只要有pci_dev或pci_driver注册进来总线核心就会调用pci_device_match走一遍匹配逻辑。之所以要单独建一套模型而不是用简单的一对一指针是因为 Linux 设备模型要解决“热插拔、驱动模块加载/卸载、电源管理、sysfs 接口、设备生命周期”等一系列问题。PCI 驱动框架不是凭空设计的它只是 Linux device model 在 PCI 总线上的具体实现。理解了这一点后面看代码就不会觉得是一堆散装函数了。1.2 驱动注册入口pci_register_driver 背后到底做了什么几乎每个 PCI 驱动的入口函数里都有这样一行return pci_register_driver(mydrv);但大多数人都没去看过它展开后的样子。pci_register_driver是个宏实际调用的是__pci_register_driver()。这个函数做了三件关键的事把struct pci_driver里的driver子结构体中的bus指针设为pci_bus_type。调用内核通用的driver_register()把驱动注册进设备模型。通过driver_attach()/bus_probe_device()触发一次匹配。由于通用模型已经处理好生命周期所以pci_register_driver本身不需要知道具体设备在哪个总线上只需要标记自己属于 PCI。这也是为什么很多老驱动用module_initpci_register_driver就完事了完全不用手动遍历设备。struct pci_driver里最重要的几个成员你需要逐一理解成员作用必须注意的点name驱动名字会出现在/sys/bus/pci/drivers/下不要起太长最好见名知意id_table驱动支持哪些设备的“匹配规则”如果为 NULL这个驱动几乎什么也匹配不到probe设备与驱动匹配成功后回调驱动初始化出错时要能返回错误码并保证资源不回滚remove设备移除或驱动卸载时回调清理资源与 probe 的操作顺序要成对suspend/resume电源管理回调若没做 PM 需求可以置为 NULLshutdown系统关机时调用有些设备需要特殊关机动作没有就 NULLerr_handler高级错误恢复回调主要用于 PCIe AER普通驱动可以不用但服务器场景建议补上写id_table时最常见的错误是手写 vendor/device 数字。建议大家用内核提供的宏static const struct pci_device_id my_pci_ids[] { { PCI_DEVICE(0x10ee, 0x1234) }, { PCI_DEVICE(0x1234, 0x5678) }, { /* 终结项 */ } }; MODULE_DEVICE_TABLE(pci, my_pci_ids);PCI_DEVICE(vendor, device)宏展开后实际上会填充class_mask为全 F表示精确匹配厂商和设备 ID。如果你还需要按 class code 匹配比如“匹配所有网络控制器”就要用PCI_DEVICE_CLASS并注意 class 码要左移 8 位。MODULE_DEVICE_TABLE(pci, ...)这一步非常关键它不仅给模块加载工具提供自动生成别名也让内核在没有驱动实体时知道你支持哪些设备。很多新手不写这一行导致modprobe加载后明明设备在系统里却没有任何反应。2. PCI 设备枚举与资源分配的内幕2.1 枚举从哪开始不只是 BIOS 的事PCI 设备在 Linux 启动早期就被扫描出来了。扫描的总线源头是PCI Root Port也就是热词里频繁出现的pci express root port。系统上电后由固件充当最初的“路由器”把各总线上的设备信息暴露出来Linux 内核的 PCI 子系统会从 Root Port 出发递归扫描下级总线遇到有设备的总线就继续往下级枚举。所以你会看到lspci输出里第一行往往是 Host bridge接着是一种类似00:1c.0 PCI bridge的设备这个就是 Root Port 或者 PCIe Switch Upstream Port。设备的完整 BDFBus:Device.Function编号就是扫描过程中按位置顺次分配的。枚举的结果是生成一棵pci_bus树每个物理 PCI 功能都会对应一个struct pci_dev节点。林林总总的设备都会被加入到 bus 的devices链表中同时通过device_add()挂到 sysfs 里。你在/sys/bus/pci/devices/0000:03:00.0/下看到的目录就是在这一步生成的。设备枚举完成之后下一步是资源分配。每个 PCI 设备都有若干块地址空间用于与 CPU 通信。这些空间就是 BAR。固件或内核需要为每个 BAR 分配物理地址范围并且要保证各 BDF 之间的地址不冲突。如果系统里没有正确分配资源后面驱动是怎么 probe 都不会成功的因为pci_resource_start()返回的全是 0。从这里开始就能理解为什么总是有人遇到pci out of resources。多见于 PCIe 交换机级联或插了多张高 BAR 占用设备时桥的窗口资源不够。这个问题在后面的排查章节再展开。2.2 BAR 空间你拿什么和硬件对话每一个 PCI 功能最多有 6 个 BARPCIe 里通常用 2~6 个。BAR 分两种类型内存空间memory和 I/O 空间I/O。现代 PCIe 设备绝大多数只用内存空间I/O 空间基本是历史遗留。你可以用lspci -vvv查看某设备每个 BAR 的地址范围和属性lspci -vvv -s 03:00.0这里你会看到类似Region 0: Memory at dl80000000 (64-bit, prefetchable) [size16M]16M就是这块 BAR 申请的地址空间大小。在驱动里我们通过这几组函数读取 BAR 信息pci_resource_start(dev, bar)BAR 的物理起始地址pci_resource_end(dev, bar)BAR 的物理结束地址pci_resource_len(dev, bar)BAR 的大小pci_resource_flags(dev, bar)BAR 的属性IO/MEM是否可预取是否 64 位拿到物理地址后CPU 并不是直接拿这个地址读写的。因为内核映使用的是虚拟地址所以必须先做一次“映射”ioremap()或者推荐用devm_ioremap_resource()。devm_ioremap_resource()不仅帮你做了 ioremap还会顺便把 BAR 资源通过devm_request_mem_region()请求掉并且如果 BAR 非法就直接报错。对于驱动开发者来说这就是最省心的选择。BAR 的分配历史悠久内核默认会用“尽量从 32 位地址空间偷空间不够再用 64 位”的算法。所以你在 dmesg 里经常看到 PCI bridge window 的值比如pci 0000:03:00.0: BAR 0: assigned [mem 0xd0000000-0xdfffffff 64bit pref]这个就是内核给这个设备的 BAR 分配物理地址的现场。BAR 的分配时机通常发生在设备枚举阶段的pci_assign_unassigned_resources()中。如果系统里有设备没被分配资源或者分配失败那这颗设备在驱动看来就和一个“没地址的哑巴”一样。3. 设备与驱动的匹配一次决定命运的握手3.1 匹配时机与匹配优先级合适的人遇合适的岗位匹配动作会发生在两个时间点当一个设备被注册到总线时总线会遍历已经注册的驱动列表看有没有驱动想接这个活。当一个驱动被注册到总线时总线会遍历已经存在的设备列表看有没有设备能被驱动接受。无论从哪个方向触发最终都会调用pci_bus_match()-pci_match_device()。这里的核心逻辑很简单遍历驱动id_table中的每一项。对比设备的 vendor ID、device ID。如果前两项不匹配再看 class code 是否满足class_mask 会过滤 class 字段。只要有一项满足就算匹配成功。有几个细节会坑到人多数驱动的id_table精确匹配 vendor/device所以如果你的设备跟别人共用 vendor ID 而 device ID 不同那必须是精确匹配自己的 device ID。MODULE_DEVICE_TABLE生成的模块别名会与 udev 的modalias对应。当你插上一个新设备时udev 根据 modalias 自动加载驱动模块所以如果 id_table 写错了模块可能根本不会被加载。如果id_table里有多个表项而驱动只写了PCI_DEVICE(0x1234, 0x5678)那后面就算有相同的 vendor 但不同 device ID 的设备插上也不会 probe。此外如果驱动使用了PCI_DEVICE_CLASS这种按 class 匹配的模糊规则匹配优先级低于精确匹配而且内核有机制防止一个设备被多个驱动“抢”probe只会调用一次。一旦匹配成功设备和驱动就进入“绑定”状态。3.2 probe 函数驱动真正启动的地方一旦匹配成功内核会调用驱动中的probe(struct pci_dev *pdev, const struct pci_device_id *id)。这个函数怎么组织直接决定了驱动稳不稳。我见过很多半路出家的驱动开发直接把所有初始化堆在一个函数里也不管失败回滚最后问题频出。这里给出一份经过实战检验的“黄金顺序”分配并初始化私有数据结构使用devm_kzalloc最好自带生命周期管理忘了释放也不至于泄漏。调用pci_enable_device(pdev)。这一步会开启设备的主控权限、启用 I/O 和 MEM 空间访问并 refcount 计数。注意这一步必须在读写 BAR 之前做否则读寄存器可能全是 0xFFFFFFFF。调用pci_request_regions(pdev, driver_name)。这一步向内核声明“这个设备的 BAR 我要用”防止别的驱动或代码误操作。与pci_enable_device搭配时业界习惯是先 enable 再 request regions。如果request_regions失败一定要 goto 到错误处理。映射需要的 BAR。使用pcim_iomap(pdev, bar, pci_resource_len())或devm_ioremap_resource()。后者更安全会自动生成资源冲突检查。注册中断。现代 PCIe 强烈建议使用 MSI/MSI-X而不是传统 INTx。详见后文。初始化 DMA 相关结构如dma_set_mask_and_coherent。DMA 掩码要按硬件能力设置别默认设 32 位导致无法访问高地址空间。注册字符设备或其他子系统接口比如misc_register()或v4l2_device_register()。这样用户态才能通过/dev节点访问设备。最后调用pci_set_drvdata(pdev, private_data)或pci_set_drvdata(pdev, ...)保存私有数据同时跟pci_get_drvdata()配套使用。在probe失败时一定要把已经成功的步骤逆序回滚。比如你已经 enable 了设备但申请区域失败要调用pci_disable_device()已经映射了 BAR 但中断申请失败要iounmap。用 devm 系列可以减少很多回滚但pci_enable_device不是 devm 的所以如果后面失败仍要手动pci_disable_device()。一个经验法则是把probe当作一个小型状态机每一步成功后才“置位”一旦后续出错按位逆序清理。不要图省事否则你会在 module reload 时体会什么叫“双重释放”或“资源泄漏”。4. 实操写一个可以用的 PCI 字符设备驱动4.1 最小驱动骨架与 Makefile纸上谈兵没有意义我们直接写一个最简单的 PCI 字符设备驱动。它的功能探测到指定的 PCI 设备后在/dev下创建一个节点用户可以通过read/write直接读写 BAR0 映射后的内存。这算是所有 PCIe 加速卡、采集卡驱动的最骨架形态。驱动主体#include linux/module.h #include linux/kernel.h #include linux/pci.h #include linux/miscdevice.h #include linux/fs.h #include linux/uaccess.h #include linux/io.h struct my_pci_dev { void __iomem *bar0; struct pci_dev *pdev; struct miscdevice misc; }; static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *off) { struct my_pci_dev *d container_of(file-private_data, struct my_pci_dev, misc); u32 val; size_t max pci_resource_len(d-pdev, 0); if (*off max || count max - *off) count max - *off; val ioread32(d-bar0 *off); if (copy_to_user(buf, val, min(count, sizeof(val)))) return -EFAULT; *off count; return count; } static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct my_pci_dev *d; int ret; d devm_kzalloc(pdev-dev, sizeof(*d), GFP_KERNEL); if (!d) return -ENOMEM; ret pci_enable_device(pdev); if (ret) return ret; ret pci_request_regions(pdev, my_pci_drv); if (ret) goto err_disable; d-bar0 pci_iomap(pdev, 0, pci_resource_len(pdev, 0)); if (!d-bar0) { ret -EIO; goto err_release; } d-pdev pdev; pci_set_drvdata(pdev, d); d-misc.minor MISC_DYNAMIC_MINOR; d-misc.name my_pci_dev; d-misc.fops my_fops; d-misc.parent pdev-dev; ret misc_register(d-misc); if (ret) goto err_unmap; return 0; err_unmap: pci_iounmap(pdev, d-bar0); err_release: pci_release_regions(pdev); err_disable: pci_disable_device(pdev); return ret; } static void my_pci_remove(struct pci_dev *pdev) { struct my_pci_dev *d pci_get_drvdata(pdev); misc_deregister(d-misc); pci_iounmap(pdev, d-bar0); pci_release_regions(pdev); pci_disable_device(pdev); } static const struct pci_device_id my_pci_ids[] { { PCI_DEVICE(0x10ee, 0x1234) }, // 替换成你的实际设备 { } }; MODULE_DEVICE_TABLE(pci, my_pci_ids); static struct pci_driver my_pci_driver { .name my_pci_drv, .id_table my_pci_ids, .probe my_pci_probe, .remove my_pci_remove, }; static int __init my_init(void) { return pci_register_driver(my_pci_driver); } static void __exit my_exit(void) { pci_unregister_driver(my_pci_driver); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE(GPL);注意probe里的错误处理顺序我自己写的时候漏掉过pci_disable_device导致卸载后再加载时报“Device in use”。这已经是老生常谈了但每次都能在社区里看到新人踩中。Makefile 其实就几行obj-m my_pci_drv.o KDIR : /lib/modules/$(shell uname -r)/build all: make -C $(KDIR) M$(PWD) modules clean: make -C $(KDIR) M$(PWD) clean把上述文件放到同一个目录执行make之后用insmod my_pci_drv.ko加载。如果一切正常dmesg里应该能看到匹配和 probe 成功的日志/sys/bus/pci/drivers/my_pci_drv/下出现对应设备的 symlink。如果系统根本没有这个硬件设备也可以用pci_stub之类的占位方式测试或者直接改 id_table 匹配你手上的开发板。4.2 让设备在 /dev 下出现字符设备与 PCI 联姻很多初学者会有疑问probe只是内核空间初始化完了用户态怎么访问答案就是通过注册字符设备或其它子系统接口。我这里用miscdevice是因为简单适合快速给设备挂一个/dev/my_pci_dev节点。misc_register()会自动申请一个次设备号你不用手动指定主次设备号也就避免和系统里的/dev/mem、/dev/null冲撞。misc_deregister()时同样会释放节点。在file_operations里你需要定义一个.open函数来把文件端点绑定到驱动私有数据上。如果像上面那样直接使用container_of(file-private_data, ...)需要先在 open 里执行static int my_open(struct inode *inode, struct file *file) { struct my_pci_dev *d container_of(inode-i_cdev, struct my_pci_dev, misc); file-private_data d; return 0; }如果你用的是miscdevice也可以通过container_of(inode-i_misc, struct my_pci_dev, misc)而不是inode-i_cdev来解析因为 misc 设备有独立的i_misc机制。这里非常容易出现空指针建议多写一层 defensive check。之后用户态就能用open(/dev/my_pci_dev)读到设备 BAR0 的内容了。如果你测试时打开 /dev 下没有节点优先去查udev规则和dmesg大概率是模块加载失败而不是节点没生成。4.3 中断与 MSI-X 处理要点PCI 设备通常需要中断通知 CPU。传统 PCI 使用 INTx但 INTx 在多个设备共享一条中断线时很容易产生共享中断风暴且在每个 CPU 上的亲和性也不理想。现代 PCIe 设备基本都支持 MSI 或 MSI-X。MSI 给设备分配一组中断号CPU 通过写内存直接触发中断处理省去了 IRQ 线的争抢。内核推荐使用通用 API 而不是手动调用pci_enable_msix()。从 4.x 内核开始最标准的做法是int nvec pci_alloc_irq_vectors(pdev, 1, 4, PCI_IRQ_MSIX); if (nvec 0) nvec pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI); if (nvec 0) return -EIO; int irq pci_irq_vector(pdev, queue_index); ret request_irq(irq, my_isr, 0, my_pci_drv, dev_id);pci_alloc_irq_vectors可以指定最少和最多向量数内核会优先分配 MSI-X不行则回退到 MSI不行再用 legacy INTx前提是传PCI_IRQ_LEGACY标志。建议参数里加上PCI_IRQ_LEGACY以保证在老设备上也能工作。申请中断要特别注意必须在pci_enable_device()之后因为设备空间还没开启时中断控制器侧可能没就绪。中断处理函数的参数里的dev_id建议直接传pci_dev或私有数据结构不要在 ISR 里到处用全局变量。如果采用的是多队列设备每队列一个中断号每个中断号对应一个dev_id不要混淆。释放中断的顺序要在 remove 函数里先free_irq再pci_release_regions否则可能在中断风暴时访问已释放的资源。MSI-X 的另一个坑是必须保证设备 BAR 空间映射完成后再分配中断因为 MSI-X table 有可能就放在 BAR 空间里。如果 BAR 还没有映射中断控制器无法初始化 MSI-X 表格pci_alloc_irq_vectors会失败。所以我在上面的 probe 顺序里把 BAR 映射放在中断申请之前就是这个原因。5. 常见问题与排查技巧实录5.1 “Error: Insufficient PCI resources detected!!!” 到底在报什么这个报错在热词里出现的频率极高尤其是插了多卡或 PCIe 交换环境。字面意思就是“PCI 资源不够”。这个资源主要指两类东西Bus number 不够系统最多 256 个总线号0xff每个 PCIe Root Port 或 Switch 下游端口都要分配一段 bus range。桥接设备一多总线号就耗尽。MMIO 窗口不够每个 PCIe 桥Root Port、Switch都有可配置的 MMIO 窗口。桥下的所有设备 BAR 必须落在桥的 window 范围内。如果桥只配了 32 位窗口而桥下挂了很多 64 位 BAR 的设备或设备 BAR 总量超过窗口大小就会分配失败。你可以用这些命令快速定位lspci -tv lspci -vvv cat /proc/iomem cat /proc/interruptslspci -vvv输出中会写明每个桥的Bus: primaryxx, secondaryxx, subordinatexx如果secondary/subordinate范围快满了就是 bus number 不够。如果看到Window [mem 0x...]只覆盖了一部分区域而设备的 BAR 需求超出那就是 MMIO 空间不足。解决思路优先级在 BIOS/固件侧检查 PCIe root port 的资源分配策略。有些服务器 BIOS 默认把 MMIO 窗口设得比较小但允许你调到 64GB 或更大。使用内核参数调整总线重分配pciassign-buses或pcirealloc。注意不同发行版对pcirealloc的默认支持不同可以先在 GRUB 里临时加参数测试。如果设备支持优先把 BAR 放在 64 位地址空间。可以在驱动里通过pci_set_mem_64bar()或 BIOS 分配时让端口的窗口变大。关掉不用的 onboard 设备释放资源。这个报错还有一个变体是“pci out of resources condit”常见于 BIOS 配置的内存映射 I/O 槽位不足。有时候两次插拔后资源没有完全回收重启后反而好了——这不是玄学而是固件的 resource reclaim 存在缺陷。真遇到这种情况建议配合硬件厂商更新固件。5.2 probe 没被调用的排查步骤驱动insmod成功也打印了pci_register_driver完成但probe就是不被调用。这种情况十有八九是设备与id_table不匹配。我一般按顺序排查先用lspci -nn -s BDF拿到该设备的 vendor/device/class。对比模块的 id_table。可以通过/sys/bus/pci/drivers/my_pci_drv/下是否有对应设备 symlink 判断是否绑定。如果 symlink 不存在基本就是匹配没成功。查模块别名。运行modinfo my_pci_drv.ko看alias行是否包含pci:v000010EEd00001234*这类字符串。如果没有这行检查你是否写了MODULE_DEVICE_TABLE(pci, ...)。如果匹配没问题但 probe 仍没执行再看设备是否已经被其他驱动绑定。可以通过/sys/bus/pci/devices/BDF/driver查看到的是哪个驱动。如果有其他驱动占了而你的id_table也匹配那总线只会绑定其中一个通常在设备枚举阶段已绑定驱动优先。你可以手动解绑或者用drivers_probe接口强制绑定echo 0000:03:00.0 /sys/bus/pci/devices/0000:03:00.0/driver/unbind echo my_pci_drv /sys/bus/pci/drivers_probe当然这只是一种调试手段真正部署还是得靠 udev 和 modalias 自动匹配。5.3 数据读写不正常映射正确但总是读到 FFFFFFFFioread32(d-bar0 offset)返回全 F通常原因有三个设备没使能。确认是否调用了pci_enable_device()并且lspci -vvv里DeviceControl没有显示Disable字样。BAR 地址为 0。检查lspci -vvv中Region是否显示[disabled]如果 BAR 没被正确分配则ioremap后的虚拟地址指向无效物理空间。设备本身没有正确配置比如某些 FPGA 逻辑需要经过 DMA 初始化才能对外响应。可以先用devmem或者busybox devmem读写物理地址做验证。比如将 BAR 物理地址读出的值对比ioread32读出的值。另外还有一个常见坑CPU 地址与 DMA 总线地址混淆。CPU 访问 reg 时用的是映射后的虚拟地址而 DMA 引擎操作的地址必须传递给 FPGА 或网卡的 DMA 能力这个地址是物理地址且需要满足 DMA mask。如果你直接把ioremap的虚拟地址交给硬件 DMA结果必然是零正确数据。总线地址通常可以通过dma_map_single/dma_map_page获得 IOVA而不是裸奔物理地址。遇到数据异常我建议先用lspci -nn和lspci -vvv确认设备资源再用最简单的寄存器读写排除法。把问题切成两半是地址/映射问题还是中断/DMA问题。全套驱动调试中dmseg永远是最优先信息源。真到万不得已再开ftrace或者tracepoint。6. 调试技巧与经验收尾写到这里PCI 驱动框架的基本闭环讲完了。最后再分享两个实操中会救命的经验。第一个经验是关于devm 系列 API 的使用。早期内核驱动要自己管理很多资源释放一不留神就在remove里留下悬空指针。现在用devm_kzalloc、devm_ioremap_resource、devm_request_threaded_irq之后大量释放逻辑可以省略但要记住pci_enable_device和pci_release_regions不是 devm 的必须手动配对。我在给一块 PCIe SSD 写驱动时就因为偷懒少写pci_disable_device导致连续 insmod/rmmod 三次就报 device busy。从此以后我把probe和remove的每一步都做成一张成对清单宁可多写几行也不敢再漏。第二个经验是关于利用 sysfs 做快调查。很多驱动同事一上来就写加载日志却不知道/sys/bus/pci/devices/下每个设备都有resource、config、enable、driver等接口cat /sys/bus/pci/devices/0000:03:00.0/resource cat /sys/bus/pci/devices/0000:03:00.0/config echo 1 /sys/bus/pci/devices/0000:03:00.0/enable通过它们可以在不写任何代码的情况下判断设备的 resource 分配和配置空间是否正常。尤其resource文件能清楚看到每个 BAR 是否被分配、当前物理地址、大小和标志。这比反复改内打印和 insmod/rmmod 高效得多。PCI 驱动框架本身不复杂复杂的是它跟设备模型、电源管理、DMA、中断、sysfs 的交织。只要你把pci_dev、pci_driver、总线匹配、BAR 映射这五六个概念啃透剩下的都只是细节。希望这篇二能帮你把框架从知识变成经验。如果后面想围绕 DMA 和中断密集场景再深挖我们下一篇接着聊。
