上一篇我们把 PCI 总线的初始化流程、设备枚举和总线的底层数据结构捋了一遍,算是把“硬件怎么变成软件眼中的 pci_dev”这件事讲清楚了。这一篇我们把视角转到驱动侧,聚焦在 Linux 内核里 PCI 驱动的标准框架上:一个 PCI 驱动注册时会发生什么、驱动和设备是怎么匹配上的、probe 函数里为什么要按那个顺序写、BAR 空间怎么映射、中断怎么申请,以及最后再聊几个实际开发中一定会踩的坑。
适合看这篇的人包括:准备写自己的 PCIe 网卡/加速卡驱动的嵌入式工程师,正在读内核源码却总是绕晕在 bus_type 和 pci_driver 之间的内核初学者,以及已经在改驱动但经常遇到“probe 不进”、“资源报错”的运维和业务开发。我会尽量用实战视角来讲,不贴大段源码翻译,重点是让你知道“为什么代码要这么写”。
1. PCI 驱动框架的整体脉络:从 device 到 driver
1.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_init+pci_register_driver就完事了,完全不用手动遍历设备。
struct pci_driver里最重要的几个成员,你需要逐一理解:
| 成员 | 作用 | 必须注意的点 |
|---|---|---|
name | 驱动名字,会出现在/sys/bus/pci/drivers/下 | 不要起太长,最好见名知意 |
id_table | 驱动支持哪些设备的“匹配规则” | 如果为 NULL,这个驱动几乎什么也匹配不到 |
probe | 设备与驱动匹配成功后回调,驱动初始化 | 出错时要能返回错误码,并保证资源不回滚 |
remove | 设备移除或驱动卸载时回调,清理资源 | 与 probe 的操作顺序要成对 |
suspend/resume | 电源管理回调 | 若没做 PM 需求,可以置为 NULL |
shutdown | 系统关机时调用 | 有些设备需要特殊关机动作,没有就 NULL |
err_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。设备的完整 BDF(Bus: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 个 BAR(PCIe 里通常用 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) [size=16M]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: primary=xx, secondary=xx, subordinate=xx,如果secondary/subordinate范围快满了,就是 bus number 不够。如果看到Window [mem 0x...]只覆盖了一部分区域,而设备的 BAR 需求超出,那就是 MMIO 空间不足。
解决思路优先级:
- 在 BIOS/固件侧检查 PCIe root port 的资源分配策略。有些服务器 BIOS 默认把 MMIO 窗口设得比较小,但允许你调到 64GB 或更大。
- 使用内核参数调整总线重分配:
pci=assign-buses或pci=realloc。注意不同发行版对pci=realloc的默认支持不同,可以先在 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 数据读写不正常:映射正确但总是读到 FFFFFFFF
ioread32(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 和中断密集场景再深挖,我们下一篇接着聊。