ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux PCI驱动框架深度解析:从设备匹配到probe资源分配

Linux PCI驱动框架深度解析:从设备匹配到probe资源分配 1. PCI驱动框架的整体设计思路聊到Linux下的PCI驱动很多人第一反应是“这不就是填个pci_driver结构体然后pci_register_driver完事吗”。如果你只是写一个简单的采集卡驱动这么理解倒也没大错。但一旦你碰到多function设备、SR-IOV、热插拔、或者需要和DMA、中断子系统深度配合的场景就会发现这套框架远比表面看起来要复杂。我这些年调试过的PCI相关问题从枚举失败到BAR空间映射冲突从MSI中断收不到到DMA地址不对几乎每一个坑都和“框架怎么设计的”这件事有关。这一篇接着上一部分的内容往下走重点放在驱动如何与PCI设备完成匹配、probe阶段到底发生了什么、以及资源分配背后的那套逻辑。如果你之前只停留在“照着模板改改”的阶段那这部分内容应该能帮你把很多模糊的地方补上。1.1 为什么PCI驱动要分成“设备”和“驱动”两半Linux设备模型的核心思想就是设备与驱动分离。PCI子系统也不例外。总线bus负责把设备device和驱动driver撮合到一起这个撮合的过程叫匹配match。具体到PCI上内核用pci_bus_type这个总线类型来管理。每枚举到一个PCI设备内核就创建一个pci_dev结构体挂到对应总线下面。驱动那边则注册一个pci_driver里面带着一张id_table相当于一张“我能支持的设备清单”。总线在两边都就绪的时候逐条比对id_table里的条目和设备的vendor/device ID匹配上了就调用驱动的probe函数。这么设计的好处很直接同一份驱动代码可以支持多个不同型号的设备只要它们的寄存器布局兼容反过来一个设备也可以被多个驱动声明支持最终由匹配优先级和加载顺序决定谁先接管。这种灵活性在服务器网卡、存储控制器这类产品线上特别常见——厂商往往用同一套驱动框架覆盖好几代芯片。注意匹配成功不代表驱动一定能正常工作。probe里如果返回错误总线会把设备和驱动解绑然后继续尝试匹配下一个驱动。这个机制经常被忽略但调试“驱动加载了却没反应”的问题时非常关键。1.2pci_driver结构体里哪些字段真正重要很多人注册驱动时习惯把结构体填得满满当当其实常用的就那么几个字段。我按实际使用频率排个序name驱动名字会出现在/sys/bus/pci/drivers/下面也是lspci -k显示的那个名字。起名要唯一别和已有驱动撞车。id_table匹配表核心中的核心。每个条目包含vendor、device、subvendor、subdevice、class、class_mask等字段。probe匹配成功后调用负责初始化硬件、申请资源、注册字符设备或网络设备等。remove设备移除或驱动卸载时调用负责释放资源。注意这个函数不能失败返回值会被忽略。suspend/resume电源管理相关不做电源管理的设备可以不填。driver.pm指向dev_pm_ops现代驱动更推荐用这个而不是老的suspend/resume。id_table的匹配规则值得单独说。内核里pci_match_one_device函数的逻辑大致是如果条目的vendor和device都是PCI_ANY_ID那就靠class和class_mask来匹配否则优先比对vendor/device再看subvendor/subdevice。这里有个容易踩的坑——class_mask写错会导致匹配范围过大或过小。比如你想匹配所有存储控制器class应该写PCI_CLASS_STORAGE_XXXclass_mask写0xffffff如果只想匹配某个子类mask就要相应调整。2. probe函数里的资源分配细节probe是整个驱动里最核心的函数设备能不能用、性能好不好基本在这一步就定下来了。我见过太多驱动在probe里偷懒结果运行起来各种诡异问题。这一章把probe里该做的事按顺序拆开讲。2.1 第一步永远是pci_enable_device这个函数做两件事唤醒设备如果它处于低功耗状态以及使能设备的I/O和内存空间访问。不调用它后面所有对BAR空间的读写都是未定义行为轻则读到全F重则直接触发总线错误。调用之后建议紧接着用pci_set_master把设备设成总线主控模式。只有设了master设备才能发起DMA。很多新手写的驱动能读写寄存器但DMA死活不工作十有八九就是漏了这一句。ret pci_enable_device(pdev); if (ret) { dev_err(pdev-dev, enable device failed\n); return ret; } pci_set_master(pdev);提示如果设备支持PCIe的ASPM或者需要特定的链路状态可能还需要调用pci_set_power_state和pci_enable_wake。这些在普通PCI设备上不常用但PCIe设备上要留意。2.2 BAR空间映射pci_iomap还是ioremapBARBase Address Register是设备暴露给CPU的窗口。PCI设备最多有6个BAR每个BAR要么是内存空间MMIO要么是I/O空间。现代设备基本都用MMIOI/O空间已经很少见了。映射BAR有两种常见做法pci_iomap(pdev, bar, len)推荐用法。它会自动处理I/O空间和内存空间的差异返回一个可以传给ioread32/iowrite32的指针。pci_resource_startioremap老式做法只适用于内存空间。现在新代码不建议这么写。映射之前一定要用pci_resource_len检查BAR长度用pci_resource_flags确认是I/O还是内存。我遇到过设备BAR长度报告为0的情况直接映射会返回NULL然后解引用就崩了。bar0 pci_iomap(pdev, 0, pci_resource_len(pdev, 0)); if (!bar0) { dev_err(pdev-dev, cannot map BAR0\n); goto err_disable; }映射完成后不要假设BAR里的寄存器布局和手册完全一致。有些设备在上电后需要先写一个解锁序列或者BAR里前几个字节是只读的ID区域。稳妥的做法是先读一遍关键寄存器确认值符合预期再继续。2.3 DMA掩码设置别让设备访问到非法地址pci_set_dma_mask和pci_set_consistent_dma_mask这两个函数决定了设备能发起DMA的地址范围。如果设备是32位的就必须设成DMA_BIT_MASK(32)如果是64位设备可以设成DMA_BIT_MASK(64)。这里有个经典问题在64位系统上如果驱动不设置DMA掩码内核默认给的是32位掩码。对于支持64位DMA的设备来说这会导致它只能访问4GB以下的物理内存性能白白浪费。反过来如果设备只支持32位DMA但驱动设了64位掩码设备发起DMA时高位地址会被截断数据就写到错误的地方去了。ret pci_set_dma_mask(pdev, DMA_BIT_MASK(64)); if (ret) { ret pci_set_dma_mask(pdev, DMA_BIT_MASK(32)); if (ret) { dev_err(pdev-dev, no usable DMA mask\n); goto err_unmap; } } pci_set_consistent_dma_mask(pdev, DMA_BIT_MASK(32));注意最后一行一致性DMA掩码通常比流式DMA掩码更严格因为一致性内存需要在启动时就预留好。很多设备的一致性DMA只能到32位所以即使流式DMA支持64位一致性DMA也要单独设成32位。2.4 中断申请MSI、MSI-X还是传统INTx中断是PCI驱动里另一个容易出问题的地方。传统INTx是共享的多个设备可以共用一个中断线所以处理函数里必须判断中断是不是自己产生的。MSI和MSI-X则是设备专属的不会共享处理起来更简单延迟也更低。申请中断的推荐顺序是先试MSI-X再试MSI最后退回INTx。nvec pci_alloc_irq_vectors(pdev, 1, 32, PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_LEGACY); if (nvec 0) { dev_err(pdev-dev, failed to allocate IRQ vectors\n); goto err_dma; }pci_alloc_irq_vectors这个函数会自动帮你选择可用的中断类型返回分配到的向量数量。拿到向量号之后用pci_irq_vector(pdev, i)获取具体的中断号然后request_irq注册处理函数。注意MSI-X支持每个向量独立屏蔽MSI则是共享一个屏蔽位。如果你的设备需要精细的中断控制优先用MSI-X。另外MSI-X的向量数量在设备手册里会写明不要申请超过硬件支持的数量。3. 从枚举到probe的完整实操流程前面讲的是“应该怎么做”这一章用一个虚拟的PCI采集卡设备把从系统启动到驱动probe完成的整个过程串一遍。你可以跟着这个流程在自己的机器上验证。3.1 系统启动时的PCI枚举内核启动时PCI子系统会扫描所有总线读取每个设备的配置空间创建pci_dev结构体。这个过程叫枚举。枚举完成后你可以在/sys/bus/pci/devices/下面看到所有设备目录名是domain:bus:slot.function的格式。ls /sys/bus/pci/devices/ # 输出示例 # 0000:00:00.0 0000:00:01.0 0000:00:1f.0 ...每个目录下面有一堆属性文件常用的有文件含义vendor厂商IDdevice设备IDclass设备类别resourceBAR空间信息irq分配到的中断号driver当前绑定的驱动符号链接用lspci -vvv可以一次性看到这些信息比逐个读文件方便得多。3.2 手动绑定和解除绑定驱动调试驱动的时候经常需要手动把设备从驱动上解绑或者把驱动绑到指定设备上。这两个操作通过sysfs就能完成# 查看设备当前绑定的驱动 ls -l /sys/bus/pci/devices/0000:01:00.0/driver # 解除绑定 echo 0000:01:00.0 /sys/bus/pci/drivers/my_driver/unbind # 重新绑定 echo 0000:01:00.0 /sys/bus/pci/drivers/my_driver/bind这个技巧在测试remove和probe路径时特别有用。你可以反复绑定解绑观察驱动有没有正确释放资源。如果解绑后dmesg里出现内存泄漏警告那说明remove函数写得不干净。3.3 probe函数的完整执行顺序把前面几节的内容串起来一个规范的probe函数应该按这个顺序执行pci_enable_device— 使能设备pci_set_master— 设置总线主控pci_set_dma_mask/pci_set_consistent_dma_mask— 设置DMA掩码pci_iomap— 映射BAR空间pci_alloc_irq_vectors— 申请中断向量request_irq— 注册中断处理函数初始化硬件寄存器注册字符设备/网络设备/其他子系统接口保存私有数据到pci_set_drvdata每一步失败都要有对应的错误处理用goto跳到相应的清理标签。这是内核代码的标准写法虽然看起来有点啰嗦但能保证资源不泄漏。static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct my_priv *priv; int ret; ret pci_enable_device(pdev); if (ret) return ret; pci_set_master(pdev); ret pci_set_dma_mask(pdev, DMA_BIT_MASK(64)); if (ret) { ret pci_set_dma_mask(pdev, DMA_BIT_MASK(32)); if (ret) goto err_disable; } priv devm_kzalloc(pdev-dev, sizeof(*priv), GFP_KERNEL); if (!priv) { ret -ENOMEM; goto err_disable; } priv-bar0 pci_iomap(pdev, 0, 0); if (!priv-bar0) { ret -EIO; goto err_disable; } ret pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI | PCI_IRQ_LEGACY); if (ret 0) goto err_unmap; ret request_irq(pci_irq_vector(pdev, 0), my_isr, 0, my_driver, priv); if (ret) goto err_irq; pci_set_drvdata(pdev, priv); return 0; err_irq: pci_free_irq_vectors(pdev); err_unmap: pci_iounmap(pdev, priv-bar0); err_disable: pci_disable_device(pdev); return ret; }提示用devm_系列函数如devm_kzalloc、devm_request_irq可以省掉一部分清理代码内核会在设备移除时自动释放。但pci_iomap没有对应的devm_版本还是需要手动pci_iounmap。4. 常见问题与排查技巧实录这一章是我这些年踩过的坑和帮别人排查过的问题的汇总。每个问题都给出症状、原因和解决方法你可以当成速查表用。4.1 设备枚举不到或BAR全F症状lspci看不到设备或者能看到设备但读BAR返回全F。常见原因设备供电不足或链路训练失败。PCIe设备需要链路训练成功才能被枚举如果金手指接触不良或者供电不稳链路就起不来。BIOS/UEFI里没有分配总线号。有些主板默认关闭了某些插槽需要在BIOS里手动开启。设备固件没有正确加载。某些FPGA类设备需要先加载bitstream才会出现在总线上。排查方法# 查看PCIe链路状态 lspci -vvv | grep -A2 LnkSta # 查看内核枚举日志 dmesg | grep -i pci如果LnkSta显示Speed 2.5GT/s, Width x0说明链路没训练成功基本是硬件问题。4.2 probe返回错误但看不到具体原因症状驱动加载了但设备没反应dmesg里只有一句“probe failed”。原因很多驱动在错误路径上只打印了笼统的错误信息没有把具体的错误码和失败步骤打出来。解决方法在probe的每个错误分支加上详细的日志包括错误码和当前步骤。更好的做法是用dev_err而不是printk这样日志里会带上设备名方便定位。dev_err(pdev-dev, failed to map BAR0, ret%d\n, ret);另外/sys/bus/pci/drivers/my_driver/下面有bind和unbind文件可以手动触发绑定观察dmesg输出。4.3 MSI中断收不到症状设备明明产生了中断但驱动的中断处理函数没被调用。常见原因MSI使能了但设备端没有正确配置MSI地址和data。MSI的地址和data是内核在pci_alloc_irq_vectors时写进设备配置空间的如果设备固件在初始化时覆盖了这些值中断就丢了。中断被屏蔽了。MSI有一个全局屏蔽位在配置空间的Message Control寄存器里。中断处理函数返回了IRQ_NONE内核认为这个中断不是该设备产生的直接忽略。排查方法# 查看中断统计 cat /proc/interrupts | grep my_driver # 查看MSI配置 lspci -vvv -s 01:00.0 | grep -A5 MSI如果/proc/interrupts里对应中断号计数一直是0说明中断根本没到CPU。这时候要检查设备端的MSI配置有没有被覆盖。4.4 DMA地址错误导致数据损坏症状DMA传输偶尔成功偶尔失败或者数据写到内存的随机位置。原因DMA掩码设置不当或者使用了不正确的DMA API。排查方法确认pci_set_dma_mask的返回值如果设置失败说明设备不支持该地址宽度。用dma_alloc_coherent申请一致性DMA内存不要用kmalloc然后手动转物理地址。流式DMA要用dma_map_single/dma_unmap_single配对不要漏掉unmap。dma_addr_t dma_handle; void *cpu_addr dma_alloc_coherent(pdev-dev, size, dma_handle, GFP_KERNEL); if (!cpu_addr) { dev_err(pdev-dev, DMA alloc failed\n); return -ENOMEM; } // 把dma_handle写进设备寄存器注意dma_alloc_coherent返回的地址在设备视角和CPU视角可能不同不要假设它们相等。设备寄存器里要写dma_handleCPU访问用cpu_addr。4.5 驱动卸载后资源没释放症状反复加载卸载驱动后dmesg出现内存泄漏警告或者第二次加载失败。原因remove函数没有释放所有申请的资源。排查方法检查remove里有没有调用pci_iounmap、pci_free_irq_vectors、pci_disable_device。用devm_系列函数可以自动释放大部分资源但pci_iomap和pci_alloc_irq_vectors需要手动释放。在remove里加日志确认每个释放步骤都执行到了。static void my_remove(struct pci_dev *pdev) { struct my_priv *priv pci_get_drvdata(pdev); free_irq(pci_irq_vector(pdev, 0), priv); pci_free_irq_vectors(pdev); pci_iounmap(pdev, priv-bar0); pci_disable_device(pdev); dev_info(pdev-dev, removed\n); }4.6 常见问题速查表问题现象可能原因排查命令设备枚举不到链路训练失败、BIOS未分配总线号lspci -vvv、dmesgBAR读取全F设备未使能、BAR未映射pci_enable_device返回值probe失败无日志错误路径缺少日志加dev_errMSI中断收不到MSI配置被覆盖、中断屏蔽/proc/interrupts、lspci -vvvDMA数据损坏掩码错误、API使用不当检查dma_alloc_coherent卸载后泄漏remove未释放资源检查remove函数5. 几个容易被忽略的进阶细节前面讲的都是基础这一章补充几个在实际项目中经常遇到但文档里很少提的点。5.1 SR-IOV场景下的PF和VF驱动SR-IOVSingle Root I/O Virtualization允许一个物理功能PF派生出多个虚拟功能VF。每个VF在总线上看起来都是一个独立的PCI设备有自己的vendor/device ID。PF驱动负责配置VF的数量和资源VF驱动则像普通PCI驱动一样工作。这里的关键点是PF驱动必须在VF驱动之前加载因为VF设备只有在PF使能了SR-IOV之后才会出现。如果你发现VF设备枚举不到先检查PF驱动有没有正确调用pci_enable_sriov。ret pci_enable_sriov(pdev, num_vfs); if (ret) dev_err(pdev-dev, failed to enable SRIOV\n);VF的vendor/device ID通常和PF不同需要在id_table里单独列出。有些设备还支持VF和PF共用同一个驱动通过pci_is_virtfn判断当前是PF还是VF。5.2 热插拔支持PCIe支持热插拔设备可以在系统运行时插入或拔出。要支持热插拔驱动需要处理remove被意外调用的情况并且不能假设设备一直存在。热插拔的触发流程是用户按下插槽的attention button或者通过sysfs写入power控制文件内核通知ACPI或PCIe热插拔控制器然后调用驱动的remove。如果驱动正在处理DMA或者持有锁remove可能会阻塞导致热插拔失败。提示在remove里不要做耗时操作尽量快速释放资源并返回。如果必须等待硬件状态用msleep而不是忙等。5.3 配置空间访问的注意事项PCI配置空间有256字节PCI或4KBPCIe。前64字节是标准头部后面的部分是设备特定的。访问配置空间用pci_read_config_byte/pci_write_config_byte等函数不要直接操作pci_dev-config。配置空间访问可能失败尤其是在设备已经被移除的情况下。所有配置空间读写都要检查返回值u16 val; ret pci_read_config_word(pdev, PCI_VENDOR_ID, val); if (ret) dev_err(pdev-dev, config read failed\n);另外PCIe设备的扩展配置空间超过256字节的部分需要用pci_read_config_dword配合PCI_CFG_SPACE_EXP_SIZE来访问普通函数只能访问前256字节。5.4 电源管理suspend和resume的正确写法如果设备支持电源管理suspend里要保存设备状态、停止DMA、禁用中断resume里要恢复寄存器、重新使能中断和DMA。顺序很重要suspend时先停DMA再禁中断resume时先使能中断再启动DMA。static int my_suspend(struct device *dev) { struct pci_dev *pdev to_pci_dev(dev); struct my_priv *priv pci_get_drvdata(pdev); disable_irq(pci_irq_vector(pdev, 0)); // 停止DMA // 保存寄存器状态 pci_save_state(pdev); pci_disable_device(pdev); return 0; } static int my_resume(struct device *dev) { struct pci_dev *pdev to_pci_dev(dev); struct my_priv *priv pci_get_drvdata(pdev); pci_enable_device(pdev); pci_restore_state(pdev); // 恢复寄存器 // 启动DMA enable_irq(pci_irq_vector(pdev, 0)); return 0; }pci_save_state和pci_restore_state会保存和恢复标准配置空间但设备特定的寄存器需要驱动自己处理。如果设备在suspend期间掉电resume后需要重新初始化所有寄存器。6. 调试工具和实用命令汇总最后整理一些日常调试PCI驱动时最常用的命令和工具放在手边随时查。6.1 lspci的常用参数# 显示所有设备的基本信息 lspci # 显示详细信息包括BAR、中断、链路状态 lspci -vvv # 只显示指定设备 lspci -s 01:00.0 -vvv # 以树形结构显示 lspci -t # 显示设备的内核驱动 lspci -klspci -vvv的输出里重点看这几项Region 0到Region 5是BAR空间Interrupt是中断信息LnkSta是PCIe链路状态Capabilities里的MSI和MSI-X是中断能力。6.2 sysfs里的关键文件# 查看设备资源 cat /sys/bus/pci/devices/0000:01:00.0/resource # 查看驱动绑定情况 ls -l /sys/bus/pci/devices/0000:01:00.0/driver # 查看驱动支持的设备列表 cat /sys/bus/pci/drivers/my_driver/new_idresource文件的每一行对应一个BAR格式是start end flags。flags里如果是0x...0200表示内存空间0x...0100表示I/O空间。6.3 内核日志过滤# 只看PCI相关日志 dmesg | grep -i pci # 实时查看日志 dmesg -w # 查看驱动加载日志 dmesg | grep my_driver如果日志太多可以用dmesg -l err只看错误级别。调试时建议把CONFIG_DYNAMIC_DEBUG打开用dynamic_debug控制特定文件的日志输出。6.4 性能分析工具# 查看中断分布 cat /proc/interrupts # 查看DMA映射情况 cat /sys/kernel/debug/dma-api/dump # 查看PCIe带宽 lspci -vvv | grep -i lnksta/sys/kernel/debug/dma-api/dump需要内核打开CONFIG_DMA_API_DEBUG它会列出所有活跃的DMA映射帮你发现泄漏的映射。我个人在实际调试中最深的体会是PCI驱动的问题九成以上出在资源分配和中断配置上。寄存器读写反而很少出错因为手册上写得清清楚楚。所以每次遇到问题先检查pci_enable_device有没有调用、BAR有没有映射成功、DMA掩码设对了没有、中断向量申请到了没有。把这四步确认一遍大部分问题都能定位到。另外dmesg永远是你最好的朋友遇到问题先看日志比瞎猜快得多。
返回列表