龙芯 3B6000M xHCI "HC died" 排查:ZLP 规避与未解的第二触发路径
一台基于龙芯 3B6000M 的 CPE,5G 上网使用中会突然整机断网——而且不只断 5G,挂在同一个 USB 控制器下的有线网卡、蓝牙、摄像头全部瞬间消失。查 dmesg,每次都是同样的四连:
cdc_mbim wwp0s25u3: NETDEV WATCHDOG: transmit queue 0 timed out
xhci_hcd 0000:00:19.0: xHCI host not responding to stop endpoint command
xhci_hcd 0000:00:19.0: xHCI host controller not responding, assume dead
xhci_hcd 0000:00:19.0: HC died; cleaning up
这篇文章记录从 2026-09-09 首次留档到 09-29 的完整排查:如何定位故障链、如何用协议边界构造秒级复现器、如何用一个几行的内核补丁把故障频率压下去两个量级,以及 26.5 小时后故障在白名单生效状态下复发时,我们学到了什么。
1. 现场与基线
排查第一步是把环境钉死,避免后面每个假设都在变动的地基上验证:
| 项目 | 值 |
|---|---|
| 平台 | Loongson 3B6000M/4-EVB,片上 xHCI(非独立桥片) |
| xHCI 控制器 | 0000:00:19.0,PCI ID 0014:7a34 rev 01,传统 PIC IRQ |
| 固件 | Loongson-FwSdk-V5.0.0431-stable202602 |
| 系统 | Loongnix 25,内核 6.6.52-loong64(6.6.52-1.11-lnd.15) |
| 5G 模组 | MEIG SRM821,VID:PID 2dee:4d53,SuperSpeed |
| 驱动/接口 | cdc_mbim,/dev/cdc-wdm0 + wwp0s25u3 |
| 数据端点 | EP 0x81 IN / EP 0x01 OUT,bulk,wMaxPacketSize=1024,bMaxBurst=0 |
两个先决条件直接决定了排查的形状:
xhci_hcd/xhci_pci均为内建(=y),不能靠替换.ko测补丁,每一个内核侧假设的验证都是一次完整构建 + 装机;- 该 PCI function 没有
reset_method,不支持 FLR。unbind/bind 能让设备重新枚举,但它不是硬复位。
恢复手段:对 0000:00:19.0 做 xHCI unbind/bind,约 4 秒后 SRM821 重新枚举,蜂窝连接、同控制器下其他 USB 设备一并恢复。但 09-16 一次自动恢复留下了重要观察——重绑定后 USB 设备和 ModemManager 对象都回来了,journal 里却跟着出现:
WARN Event TRB for slot 2 ep 1 with no TDs queued?
ERROR Transfer event TRB DMA ptr not part of current TD
cdc_mbim NETDEV WATCHDOG: transmit queue 0 timed out
设备"存在"和传输环"健康"是两回事。任何只检查对象存在、不验证数据面的恢复逻辑,都会把这种半死状态误判为成功。这个认识后来直接写进了 watchdog 的改进项。
2. 定位故障链:第一现场不在 watchdog
用 debugfs 把 slot 映射到设备:出问题的 slot 对应 SRM821,interface 1 的数据端点是 bulk EP 0x01(OUT)。所以日志里反复出现的 slot N ep 1,指的是 MBIM 上行 bulk OUT 路径。据此还原完整故障链:
bulk OUT Transfer Event 异常(ring 存在,但 td_list 为空)
→ TX URB 无法完成
→ usbnet_tx_timeout()
→ unlink_urbs() → xhci_urb_dequeue()
→ Stop Endpoint 命令超时
→ xhci_halt() → xhci_hc_died()
时序上有个关键细节:WARN Event TRB ... no TDs 比 TX watchdog 早 9~11 秒。TX watchdog 只是下游连锁的第一声报警,第一现场是 Transfer Event 与软件 TD 状态的失步——看门狗响起时,案子已经发生十秒了。
这一步同时排除了两个候选:
- runtime PM:故障瞬间控制器 PCI 运行时电源态为 active/on,runtime suspended 时间为 0,无证据;
- 上游
66cb618bf0bb("prevent potential failure in handle_tx_event() for Transfer events without TRB"):它处理的是ep_ring == NULL,本案ep_ring存在、td_list为空,不是同一路径;且该补丁历史上在稳定分支的回移曾造成严重回归,直接排除重新应用。
3. 第一轮假设:stable 补丁缺失 → 回移 A/B
6.6.52 落后于后续 stable 的 xHCI 修复。与本故障路径最相关的一组共 9 个:
| commit | 主题 |
|---|---|
53cd1bb1f8eb | Mitigate failed set dequeue pointer commands(6.6.58) |
809dd30ee901 | Fix Link TRB DMA in command ring stopped completion event(6.6.60) |
ae0ef0d756d2 | Fix TD invalidation under pending Set TR Dequeue(6.6.64) |
61329b25dc1d | Retry Stop Endpoint on buggy controllers |
f1ece345ad2c | Limit Stop Endpoint retries(6.6.70) |
6cd8e621a689 | Make Stop Endpoint error handling generic(6.6.70) |
8b2e38f2a9b7 | Avoid redundant Stop Endpoint commands(6.6.70) |
6299fb6190fe | Set DESI bits in ERDP correctly(6.6.90) |
f377434945f5 | Use more than one Event Ring segment(6.6.90) |
前 7 个正对 Stop Endpoint 超时这条终点路径,后 2 个涉及事件环容量与事件指针——当时只是"顺带拉齐基线",后来证明它们才是最有意思的(见第 7 节)。
补丁回移的构建纪律花了不少时间,但事后看每一项都必要:
- 不动原工作树,隔离 worktree + 独立分支,基线与被测机在装的包精确对齐;
- 标准
bindeb-pkg出 image + headers,保证机上 4 个 DKMS 模块能跟着重建; - 与 stock 的
Module.symvers逐行比对:21662 个导出符号中只有 7 个 xHCI 符号 CRC 变化,逐一核对在装 DKMS 模块均不引用它们; - 旧内核保留在 GRUB 高级菜单,随时回退。
结果:09-17 装机,当天同签名故障再次复现。这 9 个补丁单独上机,没有解决问题。但这次"失败"的 A/B 并不白做——它把"内核 6.6.52 落后于 stable"这个最省事的解释关掉了,逼着排查转向设备与协议侧。
4. 第二轮假设:ZLP——为什么恰好是 bulk OUT
重新看参数:
dwNtbOutMaxSize = 16384
tx_max = 16384
wNtbOutMaxDatagrams = 16
bulk OUT wMaxPacketSize = 1024
三个事实拼在一起:
- USB 规范要求用 short packet 终止传输;传输长度恰为 wMaxPacketSize 整数倍时,必须补发一个零长度包(ZLP)示意结束。
- MBIM/NCM 规范本不需要 ZLP——NTB 头部自带长度,接收方数字节即可。但内核
cdc_mbim出于历史兼容(早期模块固件依赖 ZLP),对非白名单设备默认开FLAG_SEND_ZLP。 16384 % 1024 == 0。usbnet据此设置URB_ZERO_PACKET,xHCI 把一个 16384 字节的 bulk OUT URB 建成两个 TD:一个数据 TD,一个 ZLP TD。
第一现场恰好落在这个端点——ZLP 路径是目前最佳候选。注意量词:到这里它还只是"待验证的触发条件",不是根因。
复现器:瞄准协议边界,而不是堆流量
网络栈给了现成的扳机。NCM 聚合在大吞吐上行时会追求打满 tx_max,把碎片包拼凑并 pad 到 16384——于是 ZLP TD 稳定出现。udp_blast.py 把这件事做得精确:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, 25, b'wwp0s25u3\0') # SO_BINDTODEVICE
# 1136B 负载 + 28B IP/UDP 头 = 1164B
# 14 个包聚合 = 16296B,再加 88B NCM 头 = 16384B,精准打满 tx_max → 强制 ZLP
payload = b'x' * 1136
while True:
s.sendto(payload, ('114.114.114.114', 53))
stock 内核上这个脚本秒级必杀:不出 3 秒 HC died,如果 SSH 恰好挂在同控制器的网卡上,会话当场死掉,只能靠串口或旁路网络看现场。
反过来也解释了为什么日常 ping 打不死:NCM 头(NTH16/NDP16)加上为对齐插入的填充具有随机性,小包极难命中 1024 的整数倍;而大流量下载/上传会主动聚合打满——这正是"日常大流量容易死"的机制解释。
规避 A/B:一个字节的差分
echo 16383 > /sys/class/net/wwp0s25u3/cdc_ncm/tx_max
tx_max 变 16383 后不再整除 1024,ZLP 从此不出现。同负载下传输平顺、数据面正常;恢复 16384 则故障如约而至。一进一出,触发条件闭环——ZLP 是实证的高频触发条件。中间机制(模组固件如何被 ZLP 影响)是推断,未做 usbmon 直接观测;实证的是主机侧 ZLP TD 路径与故障的强耦合。
5. 固化规避:cdc_mbim 白名单补丁
tx_max 是运行时参数,重新枚举或重启即失效。固化方案是一个只动设备匹配表的小补丁:在 cdc_mbim.c 的 mbim_devs[] 中为 2dee:4d53 增加精确的 USB_DEVICE_AND_INTERFACE_INFO 条目,driver_info 指向不带 FLAG_SEND_ZLP 的结构——与内核里 Ericsson MBIM 设备的"ZLP 合规白名单"同一思路。
生效后的行为变化:驱动不再对该模组设 ZLP;usbnet 走 pad 路径,把满 16384 的 NTB 尾部补 1 字节成 16385,不再是 1024 的倍数,ZLP 从源头消失。
补丁进入 09-20 构建的自建内核(6.6.52-1.12+lnd.17)。注意该内核不含第 3 节的 9 个补丁——这一点用两条二进制证据钉死,后面要用:
modinfo cdc_mbim的 alias 表存在精确条目usb:v2DEEp4D53ddcdscdpic02isc0Eip00in*,只有匹配表里有对应条目才会生成它;/proc/kallsyms中xhci_process_cancelled_tds缺席——该符号由 9 补丁链中的ae0ef0d756d2引入,基线本无,缺席即证明该链未合入。
顺带一个取证教训:这台机器上 /boot/System.map 只有 83 字节,是打包 profile 的残缺产物,从它 grep 得到的"无符号"结论是假的。符号存在性以 kallsyms 为准。
6. 26.5 小时后,故障回来了
boot -1:09-28 09:03 → 09-29 11:36,白名单生效状态,日常使用流量(非灌包,事后专门确认过),一次完整 HC died:
11:36:13 WARN Event TRB for slot 3 ep 1 with no TDs queued?
11:36:22 NETDEV WATCHDOG: transmit queue 0 timed out 6076 ms
11:36:27 xHCI host not responding to stop endpoint command
11:36:27 xHCI host controller not responding, assume dead
11:36:27 HC died; cleaning up (usb3/usb4 总线设备全灭)
签名与补丁前逐条一致:同一端点(bulk OUT)、同一首案(WARN no TDs 早于 watchdog 约 9 秒)、同一终点。而当天早些时候,同一台机器用 test.py(udp_blast 的计量版)灌了 529MB / 5 分钟,没有触发。
看似矛盾,其实合理:test.py 的负载形态是 1136B×14 精确打满 16384 NTB——它瞄准的恰是白名单已经阻断的 ZLP 路径。规避生效之后,原来的复现器就失明了。日常使用反而复现,说明存在第二条触发路径,正常上行流量就能踩中,不需要极限聚合。这对严重性评估也是坏消息:问题从"压测场景"回到了"真实日常场景"。
到这里,证据支持的结论(注意量词与核验状态):
- 「ZLP 是唯一触发条件」——被证伪;
- 「白名单大幅降低频率」——成立,但是量级判断而非严格故障率:补丁前灌包秒级~分钟级必杀、日常使用反复故障(间隔无精确记录);补丁后 529MB 灌包未触发、日常 26.5 小时一次(单次观察)。严格的每 GB / 每小时故障率需要暴露量数据支撑,被动采样器已部署在积累;
- 「白名单后 TX 零 ZLP」——高置信推断,usbmon 实测未做;
- 9 补丁(无白名单)复现,白名单(无 9 补丁)复现——组合从未测过,第二路径是否恰好落在 9 补丁覆盖范围内,现有数据无法区分。
7. 假设排序与判别实验
补丁前后触发条件完全不同(ZLP 满包 vs 日常流量)、终点签名完全相同——这个组合指向公共基础设施。当前假设按优先级:
- 事件环事件丢失(单段事件环 + ERDP DESI 缺陷)→ Stop Endpoint 完成事件丢失 → 驱动等到超时,误判 HC died。依据:lnd.17 实测
ERSTSZ=1(单段事件环,与 kallsyms 判定互证);WARN no-TD与事件迟到/错位的形态吻合。若成立,部分场景的 "HC died" 其实是误杀,控制器可能根本没死,正确修复是事件环扩展补丁——而不是换硬件。 - 3B6000M xHCI 硬件/微码缺陷(Stop Endpoint 状态机或 DMA 一致性毛刺)。依据:命令超时在 stock / 9 补丁 / lnd.17 三种内核上一致出现。同族平台有 DMA 一致性前科——前科提高先验,但不是本案证据。
- 模组固件对非 ZLP 传输形态假死(降级为第三候选):WARN no-TD 是主机侧失步证据,不指向模组,仅作诱因保留。
判别实验分被动、主动两路。被动:HC died watcher 自动抓取控制器 MMIO(经 sysfs resource0 mmap 读;/dev/mem 受 STRICT_DEVMEM 限制不可用),下次故障瞬间留下寄存器状态:
| 现场读数 | 指向 |
|---|---|
| USBSTS.CNR=1 且 USBCMD.RUN=1 | 控制器真死(假设 2) |
| 寄存器正常(RUN=1/CNR=0/HCH=0)+ ERDP 滞后或 EHB=1 | 事件丢失(假设 1) |
| HCH=1 | 需结合 CRCR.CA 区分软/硬 halt |
健康基线已在故障前采好:USBCMD=0x00000005(RUN=1 INTE=1),USBSTS=0x00000000,ERSTSZ=0x1,ERDP 无 pending 事件。主动:单独回移两个事件环补丁(6299fb6190fe + f377434945f5)构建判别内核做日常观察——若复现消失,假设 1 获强支持。
还有一条现实约束塑造了所有后续设计:被测机走 5G 资费,死循环灌包约 150GB/天不可接受。后续验证一律零流量/限流量:
- 被动暴露量采样:systemd timer 每 10 分钟把
wwp0s25u3计数写入 journal,日常使用自然积累分母; - 零流量触发器:libusb 绕过网络栈,直接对 EP
0x01构造「1024 整数倍 ±ZLP / SG 多段」传输,形态完全可控、零资费(执行需临时断数据面,时机另定); - HC died 自动取证器:如上,盯 journal 关键字命中即抓寄存器 + dmesg + 网卡计数。
8. 结论与复盘
已确立:
- 完整故障链与端点映射:bulk OUT Transfer Event 与软件 TD 失步 → TX watchdog → Stop Endpoint 超时 → HC died → 同控制器全部设备消失;
- ZLP 是实证的高频触发条件(秒级复现器 +
tx_max=16383规避闭环); - 白名单补丁大幅降低故障频率,但未消除;
- 9 个 stable xHCI 补丁单独上机无效;
- 存在第二触发路径,日常上行流量即可触发。
未确立(下一步):第二路径根因(三个假设待 MMIO 取证与判别内核裁决);组合修复(9 补丁 + 白名单,唯一未测组合)是否足够;严格故障率(等被动暴露量数据);白名单后零 ZLP 的 usbmon 直接观测。
几条可迁移的经验:
- 时序定位第一现场:WARN 比 watchdog 早 9~11 秒,把注意力从"网络超时"拉回"传输环失步",后面所有假设都建立在这个偏移上;
- 复现器要瞄准协议边界,不是堆流量:精确打满 NTB 的 1136B×14,比盲目跑 iperf 有效一个数量级;
- 规避生效后复现器会失明:
test.py对第二路径失效是结构性必然,不能当成"修好了"的证据——需要被动暴露量统计和不同维度的取证器接棒; - 二进制证据优先于文档叙述:modinfo alias / kallsyms 钉死"机器上装的是什么",残缺的 System.map 差点给出相反答案;
- 修复组合要显式测试:两套修复各自单独无效,不回答组合是否有效——这个空位只能靠实验填。
评论