☰
Wi-Fi驱动异常排查:PCIe BAR读全FF背后的MSI中断重映射问题
2026/10/18 16:31:35 网站建设 项目流程

如果你在调 Wi-Fi 驱动时遇到过read return 0xffffffff、BAR 空间里读出全 FF、或者/proc/interrupts里对应设备那列中断计数纹丝不动,那你大概已经体会过这种感觉:驱动还挂在设备上、配置空间也还活着,但设备和主机之间其实已经断了。这篇笔记记录的是一次 Wi-Fi 驱动异常排查,我一开始只是发现 PCIe BAR 空间异常,最后却不得不把 MSI 中断机制从协议层一路翻到 IOMMU 的中断重映射实现层。文中没有从头讲教科书原理,而是按我实际动手的顺序,把现象、排除链路、根因和修复都摊开来讲,希望能给正在调 PCIe 设备驱动的朋友省掉一些弯路。

1. 故障现场:满速打流十分钟,W 卡直接“失联”

事情发生在实验室那台 x86 测试机上。当时我们在做某 Wi-Fi 6E 模组(下文统称 W 卡)的长时间吞吐验证,内核版本是 6.1,网卡挂在 M.2 槽位上,PCI 编号固定为0000:03:00.0。测试方法很简单:开 8 条双向数据流,跑 30 分钟,看有没有断流、丢包或者复位。

结果第 11 分钟,观察终端突然冒出大量超时,ping 从正常变成 100% 丢包,iw dev也扫不到任何 AP。第一反应是射频或固件出问题了,因为这种模块在长时间高负载下偶尔会有底层固件 crash 的毛病。但接下来看到的日志让我觉得事情没那么简单:

[1384.611005] wcard 0000:03:00.0: request timeout, response: 0x00000000 [1384.611029] wcard 0000:03:00.0: queue 0 stuck for 2000 ms. [1384.612111] wcard 0000:03:00.0: Hardware error detected. restarting...

紧接着 dmesg 里开始刷一种更诡异的日志,夹杂在驱动自己的超时信息中间:

[1384.612145] DMAR: [INTR-REMAP] Request device [0000:03:00.0] fault index 0x1f [fault reason 0x26] [1384.612201] DMAR: [INTR-REMAP] Request device [0000:03:00.0] fault index 0x1f [fault reason 0x26]

这种DMAR / INTR-REMAP的日志平时很容易被当成“平台层面的杂音”忽略掉,但它在满速打流的场景里成百上千条地刷,绝不是偶然。我先是怀疑电源管理策略,像 ASPM、D3cold 切换这类老问题,于是关掉了相关的 kernel 参数重新复测,结果第 11 分钟照样崩。复现了三次之后,我确认这八成不是射频层问题,而是 PCIe 设备链路本身处于一个“主机服务不了设备”的怪状态。

更有意思的是,故障发生之后,ip link show wlp3s0显示链路一直是 down 的。执行modprobe -r再重新加载驱动,有时能恢复,有时一加载就继续挂;真正能 100% 恢复现场的手段只有整机重启。这种“只有重启才能救回来”的症状,往往意味着硬件状态没有被驱动正常复位——不是配置参数没设对,而是某个底层状态已经脏了,驱动再怎么重来也抹不掉。

我当时的结论很简单:先别管射频和固件,把 PCIe 配置空间和 BAR 资源先盘一遍,看看设备在主机眼里到底是什么状态。

2. BAR 空间异常排查:寄存器读不回,但配置空间依然完好

2.1 先确认 BAR 本身没被系统改坏

要判断“BAR 空间异常”,第一步是确认 BAR 寄存器在配置空间里还正不正常。PCIe 设备通过配置空间里偏移0x10到0x24的六个 BAR(Base Address Register)声明自己需要哪些 MMIO 窗,这些对 CPU 来说就是一组“门牌地址”。看门牌用lspci -vvv就够了:

03:00.0 Network controller: W-card NW-7300 Subsystem: W-card NW-7300 Control: I/O- Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx- Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx- Interrupt: pin A routed to IRQ 289 Region 0: Memory at ef100000 (32-bit, non-prefetchable) [size=16K] Region 1: Memory at ef120000 (32-bit, non-prefetchable) [size=4096] Capabilities: [c8] MSI-X: Enable+ Count=16 Masked- Vector table: BAR 0 offset 0x00001000 PBA: BAR 0 offset 0x00003000

这里Region 0、Region 1的地址和大小都很正常,Control行里Mem+表示 memory decode 开着,BusMaster+表示设备允许发起 DMA 和 MSI 写事务。也就是说,从配置空间看,BAR 的“门牌”没有丢,地址映射也没有被内核重新分配掉。于是我把怀疑点挪到真实访问上。

2.2 真正读 BAR 的时候,设备没有回包

配置空间正常不代表 MMIO 读写正常。BAR 里的地址只是一个窗口,CPU 往这个地址发一个 PCIe 读事务,要设备真正响应并返回 read completion,数据才算拿到。如果设备端没有回包,表现就取决于平台:有的桥路会一直让读侧挂死,有的会返回 0xFFFFFFFF,还有的会直接触发 PCIe 错误上报。

我当时先用devmem2尝试读 BAR 0 的物理地址0xef100000,终端直接卡住,Ctrl+C 都难响应;再试dd读/sys/bus/pci/devices/0000:03:00.0/resource0,也是一样的症状。在另一台同型号机器上复测,则是干净利落地返回了全0xFF。两个表现本质一样:设备端没有返回正常完成。

于是我把现象准确地定为:

  • 配置空间可读,BAR 内容没被改写;
  • Mem+已经置位,但 MMIO 窗口无响应;
  • 设备还在 PCIe 总线上,没有没电或掉链路。

2.3 一个常见的认知陷阱:BAR 全 FF 不等于“资源冲突”

很多人在这一步会误判成“BAR 空间被别的驱动占用了”或者“地址分配冲突”。地址冲突的症状通常是lspci显示[disabled]、resource0文件不存在或大小不对,或者内核报can't reserve region。而我们遇到的情况不是资源分配问题——BAR 配置完好,但后端不响应。这更像设备内部的状态机已经烂掉,比如固件卡死、设备停在复位流程中、MMIO decode 被设备自己关掉,或者它根本没进入能回包的 D0 状态。

要区分这两种情况,关键是看“门牌”和“屋里是否有人”两个层面:配置空间读就是看门牌,MMIO 读就是看屋里人对不对你的敲门有反应。门牌在、人不应,叫设备无响应;门牌被改掉、资源被收回,才叫资源冲突。前者往往是设备或驱动状态机问题,后者才是内核资源管理问题。

但这里还有一个更隐蔽的问题:为什么设备会处于“门开着但屋里没人”的状态?我一开始以为是设备固件自主复位的副作用,直到我注意到/proc/interrupts里那列纹丝不动的计数器,才意识到问题很可能出在中断投递上。

3. MSI 中断机制拆解:先搞懂“往内存写一笔”怎么变成一次中断

3.1 从 INTx 到 MSI:为什么网卡不再喜欢“拉线”

传统 PCI 时代,设备靠 INTx 物理引脚申请中断,也就是每个功能一根硬线,电平或者边沿触发,由中断控制器把信号送到 CPU。这种方式有三个天然痛点:中断向量数量少、多个设备往往共享一根线、每次中断的触发开销和扰动都不小。共享中断尤其麻烦,处理时不确认设备确实发生了中断,就得逐个轮询所有挂在线上的驱动,对高吞吐网卡来说完全不可接受。

MSI(Message Signaled Interrupts)把“拉一根线”换成了“写一笔内存”。设备生成一个 PCIe memory write 事务,目标是某个预先约定好的特殊地址,数据里带上预分配的中断向量信息。这个“写内存”不是普通 DMA,它不会真的往某块缓冲里塞数据,而是被中断控制器识别并转换成一次 CPU 中断。对设备来说,发中断变成一次普通的内存写,不用再受引脚数量和共享限制;对主机来说,MSI 天然按设备甚至按队列分配向量,不再需要共享。

MSI 和 MSI-X 的区别在于扩展性。MSI 最多支持 32 个向量,所有向量共用一个目标地址段;MSI-X 则允许每个向量独立设置自己的 address 和 data,所以驱动可以给每个 DMA 队列分配独立的中断向量。W 卡这种 16 队列的设备,用的就是 MSI-X。

3.2 MSI-X 表为什么和 BAR 空间是“同生共死”的关系

MSI-X 有一块关键的硬件结构叫 MSI-X Table(向量表),它就放在某根 BAR 指向的 MMIO 空间里。lspci -vvv里那行Vector table: BAR 0 offset 0x00001000就是在说:BAR 0 这段 16KB 的 MMIO 空间里,从偏移0x1000开始放着一张向量表。每个表项 16 字节,结构如下:

偏移内容说明
+0x00Message Address 低 32 位中断目标地址,通常落在中断控制器区域
+0x04Message Address 高 32 位64 位地址的高半部分
+0x08Message Data中断数据,配合地址确定向量
+0x0CVector Control第 0 位是 Mask,置 1 则屏蔽该向量

注意这个细节:向量表是设备普通的 MMIO 空间,不是配置空间的一部分。驱动或硬件可以把整张表读出来、改掉、甚至填成垃圾,PCI 配置空间完全不会察觉。标题里的“BAR 空间异常”,在我这次故障里其实就是这张表出了问题——不是 BAR 地址丢了,而是 BAR 背后的设备状态连同这张表都陷入了一个不闭环的状态。

3.3 IOMMU 的中断重映射:为什么 MSI 不是“写进去就完事”

在启用了 DMA 重映射的平台(x86 上常见的中断重映射机制,或 Arm 平台类似实现),MSI 并没有直接中断 CPU,而是先经过 IOMMU 做一次翻译。设备发出的 MSI 写事务会被 IOMMU 截获,IOMMU 根据写地址里的“中断句柄”去查一张中断重映射表(IRTE),再把真正的向量和 CPU 投递信息填入原本的 MSI 消息,最终触发中断。这样平台可以把虚拟机、分区安全、设备隔离做得比较干净,同时也能做到按设备重映射中断。

重点来了:IRTE 表项是主机侧内核管理的内存结构。当一个设备的中断被释放,比如驱动卸载、解绑、或者复位流程里调用了pci_free_irq_vectors(),内核会把对应的 IRTE 表项标记为无效,最常见的就是把表项里的 present 位清 0。如果这时设备还在用老的 MSI-X 向量表往外发中断,IOMMU 查到这条表项发现“无效”,就会拒绝投递,然后在 dmesg 里打印一条 interrupt-remapping 故障。这就是我前面看到DMAR: [INTR-REMAP] ... fault index 0x1f的真正含义:故障索引0x1f对应的就是设备 MSI 消息里携带的句柄,而 fault reason0x26在大多数内核版本里翻译过来正是“该 IRTE 已不在服务状态(present 位被清)”。

到这里,整个因果链已经隐隐约约出来了:主机端的 IRTE 被释放了,但设备端的 MSI-X 表没有同步清掉,设备还在往一个已经被回收的“号码”上打电话。呼叫永远无人接听,设备自然陷入无人服务的状态。

4. 排查链路全程复盘:从 DMAR 到中断计数为 0 的关键证据

4.1 证据一:/proc/interrupts 里零增长的中断计数

拿到 DMAR 日志后,我做的第一件事是打开/proc/interrupts,找 W 卡对应的那行。结果非常“干净”:

CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 289: 0 0 0 0 0 0 PCI-MSI-X 0000:03:00.0 wcard-0

中断编号是 289,设备名也绑定在wcard-0上,但六个 CPU 的计数全部是 0。一个正在跑 8 条数据流、忙得不行的网卡,居然一个中断都没收到。这说明 MSI 消息要么根本没有发出来,要么发了但被 IOMMU 拦掉了。结合 DMAR 日志,答案显然是后者:消息一直在发,只是每次都被 fault 拦截,根本不送不到 CPU。

中断计数是排查这类问题最便宜的探针。如果某个设备在负载下中断数纹丝不动,先别怀疑“它是不是没用中断”,更常见的是“它想用但投递不到”。

4.2 证据二:lspci 里的 MSI-X 表位置,以及它和 fault index 的对上号

再确认一次lspci -vvv,MSI-X 是 Enable 状态,表在 BAR 0 offset0x1000。于是我用devmem2绕开驱动,直接读物理地址0xef101000处的向量表内容:

0xef101000: f0 1f 02 00 00 00 00 00 00 20 00 00 01 00 00 00 0xef101010: f0 1f 02 00 00 00 00 00 00 20 00 00 01 00 00 00

看到第一行后我就懂了。每个表项的 16 字节里,低 32 位 Message Address 是0x00021ff0,Data 是0x0020。在中断重映射开启时,地址中的低若干位就是 IRTE 索引的来源,0x1f赫然就在其中。这和 dmesg 里反复出现的fault index 0x1f完全对应上了:设备还在用这个老句柄发中断,而内核早就把这一项 IRTE 释放了。

换句话说,设备的“电话号码本”里还存着一个旧号码,主机这一侧却已经把号码注销了。这种情况常见于设备复位时序和驱动卸载流程互相打架:驱动发现固件异常,触发设备复位和重新初始化,过程中先把内核的 MSI-X 向量释放掉;但设备这个复位流程是先发生后向内存字节的——它复位完成、固件开始跑以后,向量表里还是老数据,于是固件按老编号发 MSI,被 IOMMU 挡下。

4.3 证据三:把故障时间线排出来,因果就清楚了

把日志按时间排序之后,故障链非常清楚:

时间事件状态
T0固件队列 0 超时,触发内部复位设备开始复位,MMIO 窗口暂时不可靠
T0+0.1s驱动走 teardown,释放 MSI-X 向量内核把 IRTE present 位清 0
T0+0.3s设备固件重启,从旧向量表发 MSIIOMMU fault,中断未投递
T0+1s驱动轮询 BAR 0 等待 FW ready读回 0xffffffff / 读 hang
T0+10sDMAR fault 持续刷屏设备永远等不到主机服务,彻底卡死

这里要特别强调一点:严格来说,BAR 空间读失败是“果”,不是“因”。设备在复位过程中 MMIO 本来就可能短暂不可用,这本来可以恢复;真正让它恢复不了的,是 MSI 投递链路被主机端提前切断,而设备侧的向量表没有同步刷新,导致固件永远收不到主机给它“继续跑”的许可中断。于是整个设备就死在这个不闭环的状态里,BAR 读全 FF 只是这个死循环的外部表现。

4.4 为什么手动 unbind/rebind 时好时坏

我试过用echo "0000:03:00.0" > .../drivers/wcard/unbind再 bind,有时候能救回来,有时候不行。原因也在这个向量表上:单纯的内核 bind/unbind 并不会触发一次真正的 PCIe 热复位或功能复位,设备侧 MSI-X 表里的脏数据不会被硬件主动清除。救回来的那次,多半是驱动 probe 流程里重新写了整张表,或者设备正好被链路电源管理打到了一个会更彻底复位的状态;救不回来的时候,就是设备已经处于“向量表怎么都写不进正确值”的复位死区。

这也是我为什么后来直接上remove+rescan,而不是反复 shave 驱动绑定。remove会把设备从内核 PCI 设备模型中摘掉,rescan再重新枚举一次,相当于让设备经历一次“消失再出现”,大多数情况下 BIOS 侧和内核侧都会从干净状态重新配置它。

5. 修复方案与代码级规避:先把中断卸干净,再做硬件复位

5.1 现场抢救:remove + rescan 的正确姿势

如果以后你在类似故障现场,最快的抢救办法是:

echo 1 > /sys/bus/pci/devices/0000:03:00.0/remove sleep 1 echo 1 > /sys/bus/pci/rescan

remove会走一次完整的设备移除流程:驱动 detach、中断释放、DMA 资源回收;rescan会触发新的枚举,重新分配 BAR、重新注册 IRQ。执行完之后检查三件事:lspci能看到设备、/proc/interrupts出现新的中断号、并且中断计数开始增长。如果rescan后中断号还是 289 且计数仍为 0,那基本说明平台侧的 IOMMU 状态也要参与复位,这时候重启是唯一干净路径,不用浪费时间反复绑驱动。

另外,如果目标设备是主板上靠近某些会被平台固件接管功能的槽位,建议在抢救之前先把正在使用的应用和网络接口 stop 掉,避免 remove 过程中应用层不停访问已消失的设备节点,引发不必要的奇怪报错。

5.2 驱动层面的根治:teardown 顺序决定一切

根因既然是“主机先释放 IRTE,设备还在发老向量”,那驱动侧的修法就很明确:在硬件复位之前,先把设备侧中断生成彻底关掉,再释放内核 IRQ,最后才碰硬件复位。我在这个项目里给驱动补了一段 quiesce 流程,核心顺序如下:

static void wcard_quiesce_before_reset(struct pci_dev *pdev) { struct wcard_priv *priv = pci_get_drvdata(pdev); /* 1. 先把设备侧的中断源全部 mask 掉,清掉 pending */ wcard_write32(priv, WCARD_INT_MASK, 0); wcard_write32(priv, WCARD_INT_CLEAR, ~0U); mb(); /* 确保 posted write 落达 */ /* 2. 关掉总线主控,防止 DMA 和 MSI 继续往外发 */ pci_clear_master(pdev); /* 3. 现在才释放内核 IRQ,IOMMU 的 IRTE 随之清掉 */ pci_free_irq_vectors(pdev); /* 4. 最后做硬件级复位 */ pci_reset_function_locked(pdev); }

顺序为什么重要?你可以把它想成拆水管:必须先在设备的“总阀”把中断生成关掉,再放掉内核侧的向量资源。如果反过来,IRTE 已经清了,设备还开着中断源,那么设备随时可能用老句柄发 MSI,正好撞上 IOMMU 的 fault 逻辑。仅这一步,就把故障复现率从“必现”压到了“不再出现”。

顺带说一句:如果你维护的驱动里存在 firmware 自主复位后重新写 MSI-X 表的逻辑,务必在复位完成后显式地把整张向量表重新写一遍,而不是只信一个“上一个驱动已经写好了”的前提。硬件复位会把设备内部大部分寄存器清回默认值,但往往不会保证把 MMIO 类型的 MSI-X 表一并清干净,这就是脏状态的温床。

5.3 用中断重映射开关做二分验证

为了确认 fault 确实出在 IRTE 这一层,而不是 MSI 写入本身有别的毛病,我试过在 grub 命令行临时加intremap=off启动一次系统复测。结果很有意思:设备的吞吐恢复正常,DMAR fault 一条都不再出现,故障时间点推后甚至消失。这说明问题不是设备写错了地址——而是主机侧重映射表状态和设备的实际状态不同步。复测完成后我把参数去掉,恢复正常启动方式。

这个方法仅建议拿来做二分定位,不建议生产环境长期关闭中断重映射。重映射既保护了设备隔离,也是很多虚拟机直通场景的必需品。我们的修复最终是落在驱动 teardown 顺序上,而不是靠关掉平台保护。

6. 这次排错留下的避坑清单与方法论

这次排查最值钱的不是“某个驱动改了几行”,而是形成了一套下次可以直接照做的判断流程。我把它整理成清单,供同样在调 PCIe / Wi-Fi 驱动的朋友参考:

  1. 先盘配置空间,再碰 MMIO。任何“BAR 异常”都先看lspci -vvv的 Region 和 Control 位。Mem+、地址值、size 都在,说明是设备无响应;地址没了或 size 变成 0,才是资源分配问题。

  2. 中断计数是全场最便宜的探针。设备在高负载下/proc/interrupts对应行计数为 0,基本可以断定中断投递链路断了。别急着怀疑设备模式,先怀疑“设备想发、但发不到”。

  3. DMAR / IOMMU fault 不是噪声。它明确告诉你主机侧哪个中间件把哪一笔中断或 DMA 拦住了。fault index 往往是设备地址编码的关键线索,拿它去和设备 MSI-X 表内容比对,能精准对上号。

  4. 见到 0xFFFFFFFF 别急着回写。设备无响应时,MMIO 读回全 1 只是“没有完成”的一种表现。如果此时驱动盲目写寄存器,可能把硬件推到更深的未知状态。先弄清设备处于什么电源状态、是否在复位中,比乱写更安全。

  5. MSI-X 表在 BAR 里,和配置空间不在一个血统。配置空间里 MSI-X cap 显示 Enable 只是表象,真正的向量数据躺在 BAR 内存区。所以排查时一定要把lspci -vvv的 Vector table 位置换算成物理地址,直接把表 dump 出来看。

  6. bind/unbind 和 remove/rescan 不是一回事。前者只在内核软件层重走驱动生命周期,后者才会让设备经历一次完整的 PCIe 枚举消失再出现。遇到硬件状态脏了,优先remove+rescan。

  7. teardown 顺序永远遵守“先关设备源,再释放主机资源”。不管你的设备是网卡还是 NVMe,只要涉及 MSI/MSI-X,中断源的 mask/disable 一定要早于free_irq,否则就可能留下一个“设备还在打电话、号码已经注销”的窗口期。

最后再说一个实操习惯:我现在跑 Wi-Fi 长时间测试,都会在后台开一个循环,每两秒抓一次/proc/interrupts里设备中断号的增量,顺便监控 dmesg 里 DMAR 关键字。系统在真正断流之前,中断计数早就会先出现异常,这个“提前量”非常重要。很多难复现的偶发问题,就是靠这种看似笨拙的观察才抓到了第一现场。其实排错本身并不神秘,难的是在正确的时间看到正确的证据。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询