尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Linux PCI驱动开发:从设备模型到DMA与中断管理

发布时间:2026/9/26 2:33:44

资讯中心
01
ARTICLE

Linux PCI驱动开发:从设备模型到DMA与中断管理

Linux PCI驱动开发:从设备模型到DMA与中断管理
1. 从整体框架看 Linux PCI 驱动先说个结论Linux PCI 驱动框架本质上是在内核的device/driver/bus三方模型基础上针对 PCI 总线做出的具体化实现。这个系列第二篇我打算跳过 PCI 枚举和总线初始化这些入门内容第一篇已经聊过重点讲讲拿到一个 PCI 设备之后驱动代码应该怎么组织各个回调函数到底在什么时机被调用以及你最容易踩坑的几个资源管理点。有朋友可能会问我直接打开内核里的某个网卡驱动照着抄行不行行但大概率抄不明白。因为你照抄的只是probe函数里那几行ioremap、request_irq、pci_set_master真正决定一个驱动能不能稳定跑起来的是框架在这些回调前后替你做了什么事以及你需要主动跟框架配合做的事。这篇文章就是把这些“框架替你做的事”和“你必须自己做的事”分清楚。适合看这篇文章的人有两类一类是刚开始写 PCI 设备驱动被pci_alloc_irq_vectors、dma_map_single这些 API 绕晕的同学另一类是已经能跑通 probe但遇到 DMA 错误、中断不触发、设备移除时死机需要把框架原理补上来的开发。下面从设备模型开始逐层往下拆。2. 总线、设备、驱动三者怎么挂钩2.1 device/driver/bus 模型在 PCI 总线上的形态内核里所有设备驱动的根都是struct bus_type。PCI 总线对应的就是pci_bus_type它定义在drivers/pci/bus.c里。这个结构决定了三件事设备怎么注册pci_bus_type.probe→ 实际调用pci_call_probe驱动怎么匹配设备pci_bus_type.match→ 实际执行 PCI ID 匹配热插拔和电源管理怎么接入pci_bus_type.pm→ 挂了一整套dev_pm_ops驱动注册流程是驱动调pci_register_driver→ 框架把pci_driver包装成device_driver挂到pci_bus_type的驱动链表上 → 框架遍历该总线上所有pci_dev调用match做 ID 匹配匹配成功就调probe。设备侧是同样的逻辑PCI 枚举阶段生成pci_dev注册到总线 → 框架反向遍历已注册驱动调用probe。所以“绑定”这件事不是你的驱动主动去拉的是框架撮合的。你只需要提供一个pci_device_id表和一个probe函数。2.2 pci_device_id 匹配的细节pci_device_id长这样static const struct pci_device_id foo_pci_ids[] { { PCI_DEVICE(PCI_VENDOR_ID_FOO, PCI_DEVICE_ID_FOO_BAR) }, { PCI_DEVICE_SUB(PCI_VENDOR_ID_FOO, PCI_DEVICE_ID_FOO_BAR, PCI_ANY_ID, PCI_ANY_ID) }, { 0, } }; MODULE_DEVICE_TABLE(pci, foo_pci_ids);PCI_DEVICE同时匹配 vendor 和 device ID。如果你的驱动要支持同厂商的一整类设备可以用PCI_DEVICE_CLASS按类别匹配但强烈不建议只用 class 匹配。原因是 class 太粗容易误抓设备比如显卡和网卡用同一个 class 就会被你抢走。很多驱动还会加MODULE_DEVICE_TABLE这不是给内核匹配用的是给depmod生成模块别名用的。如果没有它modprobe在设备出现时没法自动加载你的模块。这个细节很容易漏写自加载模块时必须带上。2.3 设备树/ACPI 和 PCI 的配合在 x86 平台上PCI 设备基本靠 ACPI 描述在 ARM 平台上很多时候还要配合设备树。但要注意设备树里对 PCI 的控制节点只描述控制器本身host bridge挂在 PCI 总线上的 endpoint 设备绝大部分不需要在设备树里写节点。它们靠 PCI 枚举发现ID 匹配决定由哪个驱动接管。真正需要在设备树里写的只是极少数情况硬件上有设备但 PCI 枚举因为某种原因发现不了需要给设备指定dma-coherent属性需要给设备附带一些非标准属性比如供电控制 GPIO。如果设备树里给 PCI 设备写节点要注意compatible不是用来匹配 PCI 驱动的PCI 驱动只认vendor/deviceID。设备树节点中可以用reg 0 0 0x00 0x00 0x0这种形式描述 BDF 号但大部分时候没必要。3. pci_driver 关键回调与完整生命周期3.1 结构体逐字段拆解struct pci_driver的完整定义在内核头文件里生产环境里最常用的字段是这些static struct pci_driver foo_driver { .name foo, .id_table foo_pci_ids, .probe foo_pci_probe, .remove foo_pci_remove, .shutdown foo_pci_shutdown, .err_handler foo_pci_err_handler, .driver.pm foo_pci_pm_ops, };几个容易误解的点.probe是绑定成功后被调用的入口但它不一定在进程上下文里被你想象的线程调用。当设备在启动阶段被发现时probe 是在设备初始化流程中同步调用的当驱动后期加载时probe 可能运行在kworker线程里。.remove在设备分离、驱动卸载、系统重启三个阶段都可能被调用。不要假设它总是在“驱动正常卸载”时被调用它可能是热拔插触发的也可能是系统关机流程触发的。.shutdown在系统重启/关机时调用跟.remove不是一回事。.shutdown只需要做“让设备停止工作”的最小操作比如写一个复位寄存器不需要完整释放资源。.err_handler是可选的用于高级错误恢复AER、DPC后面会细说。3.2 probe 函数的标准分工一个规范的 PCI probe 大概有以下几个阶段static int foo_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct foo_dev *fdev; int ret; /* 1. 启用设备 */ ret pci_enable_device(pdev); if (ret) return ret; /* 2. 申请资源BAR 映射、中断 */ ret pcim_enable_device(pdev); /* 或 pci_enable_device pcim_iomap_regions */ if (ret) goto err_disable; /* 3. 设置 DMA 掩码 */ ret dma_set_mask_and_coherent(pdev-dev, DMA_BIT_MASK(64)); if (ret) return ret; /* 4. 申请中断向量 */ ret pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_ALL_TYPES); if (ret 0) return ret; /* 5. 初始化私有数据 */ fdev kzalloc(sizeof(*fdev), GFP_KERNEL); if (!fdev) return -ENOMEM; pci_set_drvdata(pdev, fdev); fdev-pdev pdev; /* 6. 分配 DMA 缓冲、注册子系统 */ ret foo_setup_dma(fdev); if (ret) goto err_free_irq; ret misc_register(fdev-miscdev); /* 如字符设备 */ if (ret) goto err_free_dma; return 0; err_free_dma: foo_free_dma(fdev); err_free_irq: pci_free_irq_vectors(pdev); kfree(fdev); return ret; }这里有一个很关键的经验如果把pci_enable_device换成pcim_enable_device资源自动释放将变得简单很多。pcim_系列是 PCI 设备管理managedAPI框架会记录资源在设备分离或驱动卸载时自动释放。用它之后probe 里很多错误路径就不需要手写goto步步回退了remove 函数也能简化不少。3.3 membar 与 BAR 映射BARBase Address Register是配置空间里描述设备地址窗口的寄存器。设备出厂时 BAR 里写入的是预设的地址区间BIOS/内核枚举阶段会根据设备的需求分配实际地址并把分配好的地址写回 BAR。驱动里需要做的是把 BAR 指向的物理地址映射到内核虚拟地址ret pcim_iomap_regions(pdev, BIT(0) | BIT(2), foo); if (ret) return ret; fdev-bar0 pcim_iomap_table(pdev)[0]; fdev-bar2 pcim_iomap_table(pdev)[2];BIT(0)表示第 1 个 BAR索引从 0 开始BIT(2)表示第 3 个 BAR。用pcim_iomap_regions一步完成“申请资源 ioremap”出错自动回滚比手动pci_request_regionioremap省事得多。读bar0里的寄存器用ioread32/iowrite32系列。这里有个坑很多人图方便直接用readl/writel在某些体系结构上没问题但对于 PCI 设备应该用ioread32系列。原因在于ioread32会带字节序处理且明确区分 IO 端口空间和 MMIO 空间跨平台移植时不容易出问题。3.4 为什么不要自己调用探测函数有人 debug 时喜欢在另一个函数里直接调foo_pci_probe(pdev, id)试图绕过框架手动初始化设备。这个做法我没法说绝对不行但强烈反对。原因probe 内部依赖框架设置的dev-driver、电源状态、DMA 掩码等状态直接调用时这些状态可能还没就绪probe 失败时框架会清理它自己分配的资源你手动调用反而打乱资源管理流程一旦设备热插拔没有框架介入引用计数和 PM 状态会错乱。正确的验证方式是让框架正常触发 probe比如重新绑定echo 0000:03:00.0 /sys/bus/pci/drivers/foo/unbind echo 0000:03:00.0 /sys/bus/pci/drivers/foo/bind4. 资源管理地址空间、DMA 和常见报错4.1 资源树的概念PCI 总线的地址资源是树形结构。树的根是 CPU 域的地址空间即iomem_resource。往下是各 host bridge 分配的 PCI 域地址区间再往下是每个 PCI 设备 BAR 的struct resource。调试的时候你会用到这个命令cat /proc/iomem输出里能看到 PCI Bus 对应的地址范围比如80000000-8fffffff : PCI Bus 0000:00 80000000-80003fff : 0000:00:01.0 80004000-80007fff : 0000:00:02.0如果设备 BAR 没出现在/proc/iomem里说明资源没有被正确分配。常见的元凶是热插拔时 BIOS 没给新设备预留窗口内核启动参数pcirealloc没开而现有的 PCI bridge 窗口不够设备在 probe 前已经pci_request_region失败返回 -EBUSY。4.2 64 位 BAR 与地址窗口现代高性能设备尤其 GPU 和 NVMe普遍把大块 BAR 放在 64 位地址空间。驱动里要注意如果你看到 BAR 描述里flags有IORESOURCE_MEM_64从pci_resource_start拿到的可能是 64 位地址需要用pci_resource_len判断长度后再决定是否 kmap。写驱动时建议先用一个调试函数打印所有 BAR 情况for (i 0; i 6; i) { if (pci_resource_len(pdev, i) 0) continue; dev_info(pdev-dev, BAR%d: start0x%llx len0x%llx flags0x%lx\n, i, (unsigned long long)pci_resource_start(pdev, i), (unsigned long long)pci_resource_len(pdev, i), pci_resource_flags(pdev, i)); }这样能快速确认设备的地址窗口有没有被正确分配尤其遇到“发了一次写操作导致系统挂死”的情况先用这个排查地址是否对得上。4.3 DMA 映射框架与 IOMMU/SMMUDMA 是 PCI 驱动里最容易出问题的部分。核心概念是**设备访问内存用的地址不是 CPU 物理地址而是 DMA 地址。**在没有 IOMMU 的平台上DMA 地址等于物理地址有 IOMMU/SMMU 时DMA 地址经过页表转换才到物理地址。驱动要做的是把内核虚拟地址映射成 DMA 地址并把地址写给设备。映射 API 分两类/* 流式 DMA单次传输用完即走 */ dma_addr_t dma_handle; dma_handle dma_map_single(pdev-dev, cpu_addr, size, DMA_TO_DEVICE); // 校验 dma_mapping_error dma_unmap_single(pdev-dev, dma_handle, size, DMA_TO_DEVICE); /* 一致性 DMA长期使用需要 CPU 和设备同时访问 */ struct foo_dev *fdev; fdev-dma_buf dma_alloc_coherent(pdev-dev, size, fdev-dma_addr, GFP_KERNEL);用dma_alloc_coherent分配的内存在物理上是连续的所以不能分配太大。超过几 MB 就要考虑dma_alloc_noncoherent 页表拼接或者干脆改用流式 DMA scatter-gather。设置 DMA 掩码是很容易被忽视的一步。如果设备支持 64 位寻址就尽早设置ret dma_set_mask_and_coherent(pdev-dev, DMA_BIT_MASK(64)); if (ret) ret dma_set_mask_and_coherent(pdev-dev, DMA_BIT_MASK(32));不设置或设置错误dma_map 可能直接失败而且日志非常难懂。报错信息一般是DMAR: DRHD: handling fault status reg 3 DMAR: [DMA Read] Request device [00:04.0] fault addr ...看到这种地址转换失败先怀疑 IOMMU 没开或总线地址宽度设置错误。排查手段是查dmesg里的 ACPI DMAR 表以及lspci -v里设备 Capabilities 的 Max Payload。4.4 “insufficient PCI resources” 报错怎么查这个报错在内核日志里经常伴随热插拔或大 BAR 设备出现。根因就一句话**PCI bridge 的窗口资源不够分配新设备。**常见场景笔记本上接了第二个 NVMe 盘控制器root port的预分配窗口太小虚拟机里加了一张网卡但 QEMU 给 PCI 总线留的 MMIO 窗口不够一张卡上有多个设备每个设备都要独立 BAR总线窗口被耗尽。排查第一步是确认当前资源占用lspci -vvv -s 00:01.0重点看 bridge 窗口范围。然后看系统启动时有没有pcirealloccat /proc/cmdline如果没开 realloc且日志里有bus: 00-1f这样的总线范围信息可以试着在启动参数里加pcirealloc,assign-bussesrealloc 允许内核重新分配桥窗口assign-busses 重新分配总线号。这个方法在绝大多数 x86 平台上有效但在带 IOMMU 的服务器上要谨慎重新分配可能导致旧设备 DMA 地址失效最好先做一次备份再操作。另一个做法是直接用PCI_QUIRKS加大桥窗口但除非你自己维护内核不推荐。5. 中断路径从 INTx 到 MSI-MSX 的完整流程5.1 三种中断机制对比中断这块我直接给结论**新驱动一律用 MSI/MSI-X不要再用 INTx。**原因特性INTxMSIMSI-X中断数1 根线最多 4 个中断共享最多 32 个受 Message Address/Data 限制可支持数百个每个中断独立向量性能差需共享判断好直接触发处理函数最好多队列场景首选CPU 亲和无法精确控制可设置亲和性可独立设置每个向量亲和性支持度所有 PCI 设备都支持PCI 2.2 必备PCI 3.0 推荐MSI-X 最吸引人的地方是它给每个队列一个独立的中断向量多队列网卡、NVMe 都是这么干的。驱动里用pci_alloc_irq_vectors即可申请多个向量static int foo_setup_msix(struct pci_dev *pdev, int nvec) { int ret pci_alloc_irq_vectors(pdev, nvec, nvec, PCI_IRQ_MSIX); if (ret 0) return ret; if (ret ! nvec) return -ENOSPC; for (i 0; i nvec; i) { fdev-irq[i] pci_irq_vector(pdev, i); ret request_irq(fdev-irq[i], foo_irq_handler, IRQF_SHARED, foo, fdev); if (ret) goto err_free_irq; } return 0; }这里有两个细节pci_alloc_irq_vectors返回的是实际申请的向量数不一定等于你请求的。如果申请 4 个但设备只支持 2 个 MSI-X返回值就是 2。所以必须要用返回值重新设置队列数。不传PCI_IRQ_MSIX而是传PCI_IRQ_ALL_TYPES时框架会按 MSI-X MSI INTx 的顺序自动选择适合做兼容。但对多队列驱动我建议显式指定 MSI-X否则退化成 INTx 时驱动逻辑会出问题。5.2 中断处理函数与线程化中断处理函数尽量短。如果处理流程较长应该用request_threaded_irq或irq_threads。一个建议是默认把中断处理写成 再把耗时逻辑放到 workqueue 里而不是直接在线程化中断里做长耗时操作。线程化中断虽然好但它的优先级和普通进程不太一样某些系统配置下和 RT 调度器配合会引入意外的延迟。另外要处理共享中断的情况。即使你申请了 MSI-X理论上不会共享但驱动里仍然应该检查irqreturn_t的返回值。正确写法是static irqreturn_t foo_irq_handler(int irq, void *data) { struct foo_dev *fdev data; u32 status ioread32(fdev-bar0 INT_STATUS); if (!(status INT_PENDING)) return IRQ_NONE; /* 处理中断 */ return IRQ_HANDLED; }如果设备中断没发生返回 IRQ_NONE让框架继续找人。共享中断场景下返回 IRQ_HANDLED 但没做任何事属于“抢中断”问题排查起来非常痛苦。5.3 MSI 分配失败的应对MSI 分配失败一般有三种平台不支持x86 正常某些 ARM 板子 host controller 没实现 MSI 支持中断数量超过平台中断控制器限制pci_alloc_irq_vectors一次性申请的向量数超过设备能力。第二种场景在虚拟化环境常见比如 CPU 核数不多但设备要求 64 个向量。应对策略是先请求大向量数失败后降级最后回退到少量共享中断nvec pci_alloc_irq_vectors(pdev, 1, 64, PCI_IRQ_MSIX); if (nvec 0) nvec pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI); if (nvec 0) nvec 1; // 最后选择 INTx驱动逻辑要支持“队列数和中断向量数不对等”的情况不能假设每个队列一定有独立向量。6. 电源管理与设备移除6.1 D 状态切换和 dev_pm_opsPCI 设备的电源管理状态是 D0工作到 D3hot/D3cold深度休眠。框架在总线层面已经注册了pci_pm_*系列操作驱动只需要在.driver.pm里挂自己的回调static const struct dev_pm_ops foo_pm_ops { .suspend foo_pm_suspend, .resume foo_pm_resume, .runtime_suspend foo_pm_runtime_suspend, .runtime_resume foo_pm_runtime_resume, };注意pci_dev是挂在 PCI 总线上的电源管理回调最终会被pci_pm_*包装后调用和平台设备的 PM 回调不完全一样。你在suspend里不要做pci_save_state这种事总线层已经做过了你只需要保存驱动自己的上下文。6.2 运行时电源管理PCI 运行时 PM 的开关是echo auto /sys/bus/pci/devices/0000:03:00.0/power/control对应的驱动代码里需要实现runtime_suspend和runtime_resume。这两个回调的调用时机往往让新手困惑它不是一个稳定的周期调用而是由框架根据设备引用计数、状态变化自动触发的。建议如果你的设备比较“重”比如要保存几 MB 上下文才能恢复runtime_suspend里尽量只做硬件层面的挂起把数据保存留到suspend里做。运行时 PM 的退出条件很频繁比如一次 IO 就可能唤醒设备如果每次都做完整保存性能会崩。6.3 remove 流程和热插拔热插拔场景下remove 必须能随时中断所有 IO。如果设备正在 DMA 到内存你的 remove 里直接kfreeDMA 缓冲区很可能会触发 IOMMU 错误或内核崩溃。正确顺序应该是static void foo_pci_remove(struct pci_dev *pdev) { struct foo_dev *fdev pci_get_drvdata(pdev); // 1. 停掉业务的上下层 foo_stop_all_io(fdev); // 2. 注销子系统如 misc、netdev misc_deregister(fdev-miscdev); // 3. 强制取消或完成未完成的 DMA 请求 foo_cancel_pending_dma(fdev); // 4. 释放中断 pci_free_irq_vectors(pdev); // 5. 释放 DMA 缓冲和私有结构 foo_free_dma(fdev); kfree(fdev); }这里最忌讳的是把misc_deregister放在最后一步。如果设备节点先删掉了用户可能还持有一个打开的文件句柄这个句柄对应的 release 回调后面还要访问fdev这时你如果把它提前释放就会访问野指针。正确设计是先删节点等 release 被调用引用计数归零后再释放结构体。使用kref是标准解法但很多简单驱动不搞 kref直接在 remove 里释放这种情况下必须确保 spam 接口不会再被调用。6.4 shutdown 和 reboot系统重启时内核会调用shutdown。很多设备在 reboot 流程中如果没被正确关掉关机画面会卡住或系统无法重启。对 PCI 驱动shutdown里最需要做的是把设备“踢回 D0 或 D3”并停掉 DMA。有些网卡驱动里还要恢复系统启动时需要的 MMIO 窗口免得固件在 EFI 阶段找不到设备。经验上shutdown实现可以非常简单static void foo_pci_shutdown(struct pci_dev *pdev) { iowrite32(0, fdev-bar0 DMA_CTRL); // 关 DMA 引擎 pci_set_power_state(pdev, PCI_D3hot); }不用释放资源只关设备活动。记住一句话shutdown 是给硬件看的remove 是给软件看的。7. 错误处理与常用调试手段7.1 错误处理基础PCIe 的错误类型分几层UNCORRECTABLE不可纠正、CORRECTABLE可纠正还有致命的 AERAdvanced Error Reporting错误。如果设备支持 AER驱动可以实现struct pci_error_handlersstatic const struct pci_error_handlers foo_err_handler { .error_detected foo_error_detected, .mmio_enabled foo_mmio_enabled, .link_reset foo_link_reset, .slot_reset foo_slot_reset, .resume foo_resume_after_error, };流程是错误被检测到 →error_detected→ 尝试恢复 →slot_reset→resume。大部分驱动用不到整套机制但如果你在做容错系统这五个回调的骨架值得提前搭好。error_detected的返回值很重要它决定了框架下一步怎么做PCI_ERS_RESULT_CAN_RECOVER驱动认为可以恢复继续走后续恢复流程PCI_ERS_RESULT_NEED_RESET要求先做链路复位PCI_ERS_RESULT_DISCONNECT放弃按移除处理。7.2 调试三板斧说真的PCI 驱动调试 80% 时间花在确认“地址对不对、中断有没有触发、DMA 有没有完成”这三件事上。对应的三板斧第一板斧lspci -vvv看设备能力、BAR、MSI-X、链路状态。配合-xxx可以 dump 配置空间原始字节排查寄存器设置问题。第二板斧sysfsls /sys/bus/pci/devices/0000:03:00.0/ cat /sys/bus/pci/devices/0000:03:00.0/resourceresource文件可以直接得到 BAR 的物理地址范围比 lspci 更精确。irq文件能看到中断号。driver_override可以强制绑定某个驱动调试时很管用。第三板斧tracepoint内核无线追踪支持pci相关的 tracepoint包括pci_msi_*、pci_aer_*。用 perf 抓perf trace -e pci:* -a或者用 ftraceecho pci:* /sys/kernel/tracing/set_event echo 1 /sys/kernel/tracing/tracing_on这个方法在“中断确实没被触发”和“DMA 确实没完成”之间做区分时特别有效。7.3 常见问题速查现象常见原因排查手段probe 里pci_request_region返回 -EBUSY资源被别的驱动占了lspci -vvv看 BAR owner查dmesgioread32读到全 F设备未上电/PCIe 链路未训练成功/配置空间窗口错了lspci -t看链路pciehp状态中断完全不触发MSI 配置失败/共享中断处理不当/设备处于 D3cat /proc/interruptsperf traceDMA 写入后数据一直是旧值一致性 DMA 缺少屏障/地址给错用dma_alloc_coherent确认地址配合devmem系统在卸载模块时死机remove 释放了还在用的资源用kref保护按上面顺序释放复现热插拔时内核 oops没处理pci_dev引用计数在 remove 里加pci_dev_get/put对调8. 实操心得我写 PCI 驱动撞过的几个坑最后分享几个从实际项目里踩出来的经验供大家参考。第一个坑BAR 映射全部成功后寄存器和中断都正常但 DMA 就是不对。最后查到问题是dma_set_mask没调用。设备默认 mask 是 32 位但我的设备 BAR 和 DMA 地址全在 64 位区域pci_alloc_consistent给的 DMA 地址高位被截断设备拿到错误地址自然写不进。后来我把dma_set_mask_and_coherent(pdev-dev, DMA_BIT_MASK(64))提到 probe 最前面问题解决。所以写驱动时第一步就把 DMA 掩码设好不要留到后面某段代码里再补。第二个坑remove 里释放了struct foo_dev但用户态还握着 fd。fd 的 release 回调里访问了fdev-bar0直接野指针。后来我用kref引用计数misc_deregister后等 release 回调执行完再释放内存。这个坑在带/dev接口的设备上特别常见建议所有暴露字符节点的 PCI 驱动都按这个模式写。第三个坑热拔插时系统卡死。原因是我在 remove 里调用了pci_set_power_state(PDEV, PCI_D3hot)但这个设备在热拔插流程里已经被框架设定为 D3 了我再调一次导致状态机重复切换卡在硬件固件里。结论remove 函数里不要主动设置设备电源状态除非你真的知道框架没做这件事。第四个经验调试时善用 driver_override。如果你同一个设备节点上有多个驱动都匹配或者你想临时强制绑定自己装的驱动不要频繁modprobe/rmmod直接用echo 0000:03:00.0 /sys/bus/pci/devices/0000:03:00.0/driver_override echo 0000:03:00.0 /sys/bus/pci/drivers/foo/bind这个机制在调试多版本驱动时省了我大量时间。最后补充一个判断框架行为的技巧如果你想知道某个回调到底什么时候被调用可以在里面加一句dev_info(pdev-dev, %s called\n, __func__)然后依次触发绑定、卸载、睡眠、唤醒、热拔插观察调用顺序。这个日志是理解框架时序最直接的手段比我上面写一大段都管用。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。