ARTICLE DETAIL

资讯详情

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

Linux 内核 PCI 驱动开发实战指南:从设备探测到 DMA、中断与资源管理

Linux 内核 PCI 驱动开发实战指南:从设备探测到 DMA、中断与资源管理 Linux 内核 PCI 驱动开发实战指南从设备探测到 DMA、中断与资源管理【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux本文基于内核文档 Documentation/PCI/pci.rst 整理展开完整覆盖其核心脉络pci_register_driver()驱动模型、ID 表与运行时 new_id 机制、设备初始化的八步流程、关闭设备的逆序清理、PCI 配置空间访问以及 MMIO Write Posting 等易踩坑细节。文中结合 include/linux/pci.h 与 drivers/pci/pci-driver.c 的源码实现加以印证。读完后你应能独立编写一个符合现代 PCI 驱动规范的探测、初始化与卸载流程并理解每一步背后的内核机制。为什么 PCI 驱动开发不平凡PCI 世界庞大且充满意外。由于每种 CPU 架构实现了不同的芯片组而 PCI 设备又有各自的往往是令人头疼的特性要求Linux 内核中的 PCI 支持远非想象中简单。内核文档明确指出文档都会随版本腐化当行为与描述不符时以源码为准。遇到 API 疑问时官方建议联系 Linux PCI 邮件列表讨论。文档同时推荐 Corbet、Rubini 和 Kroah-Hartman 合著的《Linux Device Drivers》第三版LDD3采用 Creative Commons 许可免费公开作为更完整的参考资料。PCI 驱动的整体结构PCI 驱动的设备发现方向与直觉相反并不是驱动去调用某个函数查找设备而是当 PCI 通用代码发现新设备时ID 表匹配上的驱动会被回调通知。驱动只需在初始化时调用pci_register_driver()并传入一个描述驱动的结构体PCI 层便接管了大部分探测工作。这种设计天然支持热插拔——热插 PCI、CardBus、Express-Card 都能被同一个驱动处理。一旦驱动获知某个 PCI 设备并取得控制权ownership通常需要执行如下初始化序列原文档列出的八步使能设备enable the device申请 MMIO/IO Port 资源设置 DMA mask 大小coherent 与 streaming 两种分配并初始化共享控制数据coherent 内存访问设备配置空间如需要注册中断处理程序request_irq()初始化非 PCI 部分如芯片中的 LAN/SCSI 逻辑使能 DMA/处理引擎设备使用完毕、模块需要卸载时则执行对称的清理序列禁止设备再产生中断释放中断free_irq()停止所有 DMA 活动释放 DMA 缓冲streaming 与 coherent 都要释放从其他子系统SCSI、netdev 等注销释放 MMIO/IO Port 资源禁用设备一个值得注意的兼容性细节如果内核没有配置 PCI 子系统CONFIG_PCI未设置上述大部分 PCI 函数会被定义为内联空函数或仅返回相应错误码以避免驱动中散落大量#ifdef。这一点可以在源码中直接得到印证——include/linux/pci.h 末尾提供了桩实现例如未配置 PCI 时的static inline int pci_enable_device(struct pci_dev *dev) { return -EIO; }约 L2184 行。pci_register_driver() 调用与驱动结构体PCI 驱动在初始化阶段调用pci_register_driver()并传入一个struct pci_driver结构体的指针。当前内核中该结构体定义见 include/linux/pci.h核心字段包括struct pci_driver { 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) */ int (*suspend)(struct pci_dev *dev, pm_message_t state); /* Device suspended */ int (*resume)(struct pci_dev *dev); /* Device woken up */ void (*shutdown)(struct pci_dev *dev); ... struct device_driver driver; struct pci_dynids dynids; bool driver_managed_dma; };其中probe在进程上下文调用可以睡眠remove同样运行于进程上下文可以睡眠见 include/linux/pci.h 的注释。suspend/resume用于电源管理其语义详见 Documentation/power/pci.rst。shutdown挂接在 reboot 通知链上用于停止空闲的 DMA 操作、配置 Wake-on-LAN 或改变重启前的电源状态。err_handler用于 PCI 错误恢复详见 Documentation/PCI/pci-error-recovery.rst。driver_managed_dma标志表示驱动不经内核 DMA API 自行管理 DMA如 VFIO 类驱动以便 IOMMU 层允许其自行建立 I/O 地址空间。ID 表struct pci_device_idid_table是一个以全零项结尾的struct pci_device_id数组且必须非 NULL否则 probe 不会被调用。文档建议 ID 表使用static const定义。多数驱动只需要用PCI_DEVICE()或PCI_DEVICE_CLASS()宏即可搭好这张表。以PCI_DEVICE()为例include/linux/pci.h#define PCI_DEVICE(vend,dev) \ .vendor (vend), .device (dev), \ .subvendor PCI_ANY_ID, .subdevice PCI_ANY_ID即 subvendor/subdevice 自动置为通配符PCI_ANY_ID。类似地还有带子系统的PCI_DEVICE_SUB(vend, dev, subvend, subdev)等宏。运行时动态添加 PCI IDnew_id文档给出了在运行时向已加载驱动追加 PCI ID 的标准做法echo vendor device subvendor subdevice class class_mask driver_data \ /sys/bus/pci/drivers/{driver}/new_id所有字段均为十六进制不带0x前缀。vendor 和 device 必填其余可选且可选字段的默认值有明确规则subvendor 与 subdevice 默认为PCI_ANY_IDFFFFFFFFclass 与 class_mask 默认为 0driver_data 默认为 0ULoverride_only 默认为 0driver_data 必须与驱动静态pci_device_id表中某个条目的 driver_data 一致如果表中所有条目的 driver_data 都非零则 driver_data 字段变为必填。这一规则并非只是文档约定内核在 sysfs 写入路径上做了强制校验。drivers/pci/pci-driver.c 中的new_id_store()用sscanf(buf, %x %x %x %x %x %x %lx, ...)解析字段少于 2 个字段返回-EINVAL随后遍历id_table若传入的 driver_data 与任何已有条目都不匹配就直接返回-EINVAL/* Only accept driver_data values that match an existing id_table entry */ if (ids) { retval -EINVAL; while (ids-vendor || ids-subvendor || ids-class_mask) { if (id.driver_data ids-driver_data) { retval 0; break; } ids; } if (retval) /* No match */ return retval; }动态 ID 会挂在驱动的dynids链表上匹配时优先查找动态 ID 再查静态表pci_match_device()drivers/pci/pci-driver.c其注释也明确提到 dynids 列表可能已被 sysfs new_id 文件扩充。ID 一旦加入驱动 probe 例程就会为所有尚未被认领、且出现在更新后pci_ids 列表中的 PCI 设备触发。驱动退出时只需调用pci_unregister_driver()PCI 层会自动对驱动所管理的每个设备调用remove回调。初始化/清理函数的属性标注文档建议在合适的位置为初始化和清理函数标注属性对应宏定义在linux/init.h属性含义__init初始化代码驱动初始化完成后即被丢弃__exit退出代码对非模块化驱动无效使用建议module_init()/module_exit()函数以及仅从它们调用的所有初始化函数应标注__init/__exit不要标注struct pci_driver本身不确定该用哪个标注时宁可不标——标错比不标更糟。手动查找 PCI 设备文档强调除非有非常充分的理由PCI 驱动都应使用pci_register_driver()接口。一个 PCI 设备由多个驱动分别控制是常见且合法的情形例如一块卡同时实现串口/并口/软驱控制等多个硬件服务。确需手动查找时文档给出三种构造方式按 vendor/device ID 搜索struct pci_dev *dev NULL; while (dev pci_get_device(VENDOR_ID, DEVICE_ID, dev)) configure_device(dev);按 class ID 搜索迭代方式类似pci_get_class(CLASS_ID, dev)按 vendor/device 加 subsystem vendor/device 搜索pci_get_subsys(VENDOR_ID, DEVICE_ID, SUBSYS_VENDOR_ID, SUBSYS_DEVICE_ID, dev)VENDOR_ID或DEVICE_ID的位置可以用常量PCI_ANY_ID作为通配符替换比如查找某厂商的所有设备。关键的引用计数约束这些函数是热插拔安全的它们对返回的pci_dev增加引用计数因此你最终可能是模块卸载时必须调用pci_dev_put()递减引用计数否则pci_dev结构体永远不会被释放。设备初始化步骤详解使能 PCI 设备在触碰任何设备寄存器之前驱动必须先调用pci_enable_device()使能设备。它会唤醒处于挂起状态的设备为设备分配 I/O 和内存区域如果 BIOS 没有做分配中断如果 BIOS 没有做当前内核源码给该函数标注了__must_checkinclude/linux/pci.hint __must_check pci_enable_device(struct pci_dev *dev);从接口层面强制提醒pci_enable_device()可能失败必须检查返回值。文档还保留了一条 OS BUG 警告内核在使能资源之前并不会先检查资源分配是否冲突。更合理的顺序本应是先pci_request_resources()再pci_enable_device()但目前驱动无法检测到两个设备被分配到同一地址范围的问题文档注明截至 2.6.19 该问题已讨论但未改变。DMA 相关的使能开关pci_set_master()置位 PCI_COMMAND 寄存器中的 bus master 位以启用 DMA若 BIOS 把 latency timer 设成了错误值它还会顺手修正。pci_clear_master()清掉 bus master 位以禁用 DMA。若设备支持 Memory-Write-Invalidate 事务调用pci_set_mwi()置位 Mem-Wr-Inval 并确保 cache line size 寄存器正确。并非所有架构/芯片组都支持必须检查返回值如果 Mem-Wr-Inval 只是锦上添花而非必需可改用pci_try_set_mwi()做尽力而为的启用。申请 MMIO/IO Port 资源一个关键原则内存MMIO与 I/O 端口地址不能直接读自 PCI 配置空间而应使用pci_dev结构体中的值——因为bus address可能已被架构/芯片组相关内核代码重映射为host physical地址。如何访问设备寄存器和设备内存文档指引参阅 Documentation/driver-api/io-mapping.rst。驱动需要调用pci_request_region()确认没有其他设备正在使用同一地址资源相对地pci_release_region()应当在pci_disable_device()之后调用。目的是防止两个设备在同一地址范围上发生碰撞。对不属于常规 PCI BAR 的地址资源可以用其通用版本request_mem_region()MMIO 范围和request_region()IO Port 范围。文档还提示参阅下文pci_request_selected_regions()并再次提醒上述OS BUG限制截至 2.6.19驱动只能在调用pci_enable_device()之后才能确定 MMIO 和 IO Port 资源是否可用。设置 DMA mask 大小文档在此给出重要提示如果本节内容读起来费解请转去读 Documentation/core-api/dma-api.rst——本节只是提醒驱动必须声明设备的 DMA 能力并非 DMA 接口的权威说明。核心要点所有驱动都应显式声明 PCI bus master 的 DMA 能力32 位或 64 位。对 streaming 数据具有超过 32 位 bus master 能力的设备驱动必须通过dma_set_mask()向内核注册这一能力。这通常允许在 System RAM 位于 4G物理地址之上的系统上实现更高效的 DMA。所有 PCI-X 和 PCIe 合规设备的驱动必须调用dma_set_mask()因为它们是 64 位 DMA 设备。同理若设备可以直接寻址 4G 物理地址之上的coherent memory驱动必须调用dma_set_coherent_mask()注册该能力同样涵盖所有 PCI-X 与 PCIe 设备。注意许多 64 位PCI设备PCI-X 之前的以及部分 PCI-X 设备负载streaming数据支持 64 位 DMA但控制coherent数据不支持。初始化共享控制数据DMA mask 设置完成之后驱动即可分配 coherent又称 shared内存。完整的 DMA API 描述见 Documentation/core-api/dma-api.rst本节的提醒在于这一步必须在向设备启用 DMA 之前完成。初始化设备寄存器部分驱动需要写入特定的 capability 字段或初始化/复位厂商专有寄存器例如清除挂起的中断。配置空间寄存器可以随时访问几乎如此——运行 BIST 时配置空间会消失但后果只是产生一次 PCI Bus Master Abort配置读返回垃圾值。注册中断处理程序虽然request_irq()是这里描述的最后一步它往往只是设备初始化过程中的又一个中间环节许多驱动会把它推迟到设备被打开使用时再注册。文档给出的三条硬性要求所有中断处理程序都应使用IRQF_SHARED注册并通过 devid 把 IRQ 映射到具体设备——因为所有 PCI 中断线都可能是共享的。request_irq()会把中断处理程序和 device handle 关联到某个中断号并同时使能中断。因此注册前必须确保设备已处于静默状态、没有挂起的中断。传统中断号对应从 PCI 设备到中断控制器的 IRQ 线而使用 MSI/MSI-X 后中断号是 CPU 的 vector。关于 MSI 与 MSI-X二者都是 PCI capability都是消息信号中断——通过向 Local APIC 做一次 DMA 写把中断投递给 CPU。根本区别在于多 vector 的分配方式MSI 需要连续的 vector 块MSI-X 可以分配若干独立的 vector。通过调用pci_alloc_irq_vectors()并传入PCI_IRQ_MSI和/或PCI_IRQ_MSIX标志在request_irq()之前可启用 MSI 能力此时 PCI 支持代码会把 CPU vector 数据写入 PCI 设备的能力寄存器。由于很多架构、芯片组或 BIOS 不支持 MSI/MSI-X仅带这两个标志的调用会失败因此应当始终同时指定PCI_IRQ_INTX作为回退。若 MSI/MSI-X 与 legacy INTx 使用不同的中断处理函数驱动在调用pci_alloc_irq_vectors()之后应依据pci_dev结构体中的msi_enabled与msix_enabled标志选择正确的那个。文档列出了使用 MSI 的两个真正好的理由MSI 按定义是独占的中断 vector——中断处理程序无需再验证是不是自己的设备触发了这次中断。MSI 避免了 DMA/IRQ 竞争条件——主机内存中的 DMA 写入在 MSI 被投递时保证对主机 CPU 可见。这对数据一致性与避免读到过期的控制数据都至关重要该保证允许驱动省掉用于冲刷 DMA 流的 MMIO 读。文档还指出可参考 InfiniBand 驱动或 tg3 网卡驱动源码中drivers/net下的 Ethernet 驱动作为 MSI/MSI-X 用法示例。PCI 设备关闭流程模块卸载或设备被拔出时多数以下步骤需要执行与初始化逆序对应禁止设备产生中断释放中断free_irq()停止所有 DMA 活动释放 DMA 缓冲streaming 与 coherent 两者从其他子系统注销使设备停止响应 MMIO/IO Port 地址释放 MMIO/IO Port 资源停止设备上的中断具体做法因芯片/设备而异。如果不做这一步当且仅当中断与其他设备共享时就会造成尖叫中断screaming interrupt共享中断处理程序被拔掉后其余使用同一 IRQ 线的设备仍需要中断保持使能若被拔掉的那个设备持续拉低 IRQ 线系统会假设是剩余某个设备触发了中断——而没有任何设备处理它系统就会挂住直到中断控制器在10 万轮迭代后判定该 IRQ 无人处理并将其屏蔽。共享 IRQ 一旦被屏蔽剩余设备也全部无法正常工作——局面相当糟糕。这也是在可用时优先使用 MSI/MSI-X 的又一个理由它们按定义是独占中断天然免疫尖叫中断问题。释放中断设备静默不再产生中断之后即可调用free_irq()。该函数会在任何挂起的 IRQ 处理完毕后返回把驱动的中断处理程序从该 IRQ 上拔掉并在没有其他使用者时释放该 IRQ。停止所有 DMA 活动文档用了极其重要来强调在尝试释放任何 DMA 控制数据之前必须停止所有 DMA 操作。否则可能造成内存损坏、挂起在某些芯片组上会直接硬崩溃。此外先停中断再停 DMA可以避免 IRQ 处理程序重启 DMA 引擎的竞争。文档坦承这步看似显而易见但过去多个成熟驱动都曾做错。释放 DMA 缓冲DMA 停止后先清理 streaming DMA——unmap 数据缓冲如有上游所有者则归还缓冲然后清理包含控制数据的 coherent 缓冲。unmapping 接口的细节见 Documentation/core-api/dma-api.rst。从其他子系统注销大多数底层 PCI 设备驱动支持另一个子系统如 USB、ALSA、SCSI、NetDev、InfiniBand 等。务必确认驱动没有丢失来自该子系统的资源如果丢失了典型症状是当子系统试图调用一个已经卸载的驱动时发生 Oopspanic。禁用设备与释放资源先对 MMIO 或 IO Port 资源执行io_unmap()然后调用pci_disable_device()——它是pci_enable_device()的对称逆操作。调用pci_disable_device()之后不得再访问设备寄存器。最后调用pci_release_region()把 MMIO/IO Port 范围标记为可用不做这一步通常会导致驱动无法重新加载。访问 PCI 配置空间文档给出的访问途径对struct pci_dev *表示的设备使用pci_read/write_config_byte/word/dword访问其配置空间。所有这些函数成功时返回 0失败时返回一个可由pcibios_strerror翻译为文本的错误码PCIBIOS_...。多数驱动假设对合法 PCI 设备的访问不会失败。若手上没有struct pci_dev可调用pci_bus_read/write_config_byte/word/dword按总线 设备 功能定位访问。访问标准配置头中的字段时请使用linux/pci.h中声明的符号化位置名与位定义不要使用魔法数字。需要访问扩展 PCI Capability 寄存器时调用pci_find_capability()查找指定 capability它会帮你定位对应的寄存器块。其他常用函数速查原文档Other interesting functions一节列出的一组函数完整保留如下函数说明pci_get_domain_bus_and_slot()按给定 domain、bus、slot 和编号查找对应pci_dev找到则其引用计数加一pci_set_power_state()设置 PCI 电源管理状态0D0 … 3D3pci_find_capability()在设备 capability 列表中查找指定 capabilitypci_resource_start()返回给定 PCI region 的 bus 起始地址pci_resource_end()返回给定 PCI region 的 bus 结束地址pci_resource_len()返回 PCI region 的字节长度pci_set_drvdata()为pci_dev设置驱动私有数据指针pci_get_drvdata()取回pci_dev的驱动私有数据指针pci_set_mwi()启用 Memory-Write-Invalidate 事务pci_clear_mwi()禁用 Memory-Write-Invalidate 事务综合建议Miscellaneous hints文档给出的三条通用建议均值得原样保留向用户展示 PCI 设备名称时例如驱动告知用户找到了哪块卡请使用pci_name(pci_dev)不要自行拼 bus/slot 号。始终使用指向pci_dev结构体的指针来引用 PCI 设备。所有 PCI 层函数都以这种方式标识设备这是唯一合理的做法。除极少数特殊用途外不要使用 bus/slot/function 编号——在多主总线系统上这些编号的语义可能相当复杂。不要在你的驱动里打开 Fast Back-to-Back 写。总线上所有设备都必须支持它才行这是平台与通用代码应当处理的事情而不是单个驱动的职责。Vendor 与 Device ID 的规范不要往 include/linux/pci_ids.h 里添加新的设备 ID 或厂商 ID除非该 ID 被多个驱动共享。可以在你自己的驱动里加私有定义如果有用的话或者直接写十六进制常量。Device ID 是任意的十六进制数由厂商控制通常只在一个地方使用——pci_device_id表。新的 vendor/device ID 应当提交到社区维护的 pci.ids 数据库pci-ids 项目文档中提到的pci.ids镜像文件由 pciutils 项目维护。已废弃函数对照表如果你把旧驱动移植到新的 PCI 接口可能会遇到下列函数。它们因为与热插拔、PCI domain 或合理的加锁机制不兼容已从内核中移除废弃函数替代函数pci_find_device()pci_get_device()pci_find_subsys()pci_get_subsys()pci_find_slot()pci_get_domain_bus_and_slot()pci_get_slot()pci_get_domain_bus_and_slot()文档同时指出替代方案之一仍是传统 PCI 驱动遍历 PCI 设备列表的写法——这依然可行但不被推荐。MMIO 空间与 Write Posting把驱动从 I/O Port 空间改造为 MMIO 空间时往往需要额外的改动必须处理写回写Write Posting。很多驱动文档举了 tg3、acenix、sym53c8xx_2 为例已经这样做了。两者时序语义的差别在于I/O Port 空间保证写事务在 CPU 继续执行之前已经到达 PCI 设备MMIO 空间允许 CPU 在事务到达设备之前继续执行。硬件爱好者称之为 Write Posting因为写完成的确认在事务到达目的地之前就已经贴回给 CPU 了。因此对时序敏感的代码应在 CPU 预期等待之处补一次读。经典的逐位敲入bit banging序列在 I/O Port 空间下是for (i 8; --i; val 1) { outb(val 1, ioport_reg); /* write bit */ udelay(10); }同一序列在 MMIO 空间下应改为for (i 8; --i; val 1) { writeb(val 1, mmio_reg); /* write bit */ readb(safe_mmio_reg); /* flush posted write */ udelay(10); }注意safe_mmio_reg必须是读之无副作用的寄存器不能干扰设备的正常运行。另一个需要留意的场景是复位 PCI 设备用 PCI 配置空间读来冲刷writel()。这样可以在所有平台上优雅地处理 PCI master abort 的情形——当你预期设备不会响应readl()时。多数 x86 平台允许 MMIO 读发生 master abort俗称 Soft Fail并返回垃圾值例如~0但许多 RISC 平台会直接崩溃俗称 Hard Fail。小结这篇指南的价值在于把 PCI 驱动开发中容易做错且后果严重的环节都摆到了台面上ID 匹配与动态 new_id 的 driver_data 约束、pci_enable_device()的错误检查与资源申请顺序的历史包袱、DMA mask 必须先于 coherent 内存声明、关闭路径上先停中断、再停 DMA、最后释放缓冲的严格次序以及 MMIO Write Posting 带来的时序陷阱。配合 include/linux/pci.h 的接口定义、drivers/pci/pci-driver.c 的匹配与 sysfs 实现以及 Documentation/core-api/dma-api.rst、Documentation/driver-api/io-mapping.rst、Documentation/power/pci.rst 等延伸阅读可以覆盖从驱动骨架到细节陷阱的完整知识链路。正如文档的告诫文档会随版本腐化行为不符时永远以源码为准。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表