修复Trap22快速复连丢中断致监听死亡:net层周期对账自愈(去抖+冷却),复现用例与回归全绿

This commit is contained in:
2026-09-23 02:27:10 +08:00
parent 3ac8eb807d
commit 77ee778605
3 changed files with 146 additions and 18 deletions
+40 -15
View File
@@ -560,28 +560,53 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报
- 真实缺陷见 **Trap 23**(上传中止后首笔复连上传被静默截断,已可信复现并修复);
- 发现:2026-08-26(阶段 8);结案:2026-09-23。
## Trap 22:重监听后快速重连丢失 CONNECT 中断 → 监听静默死亡
## Trap 22:快速复连丢 CONNECT/DISCONNECT 中断 → 监听静默死亡(2026-09-23 复现并修复)
### 现象
轻负载构建(Phase 6 only)+ 带历史的重监听 socket:上一条连接关闭后约 600ms 到达的第二条连接,芯片侧硬件完成 TCP 握手(PC 连接成功、数据可流入),但 MCU 侧**无 `standalone accept`**(socket 控制块滞留 LISTENING)→ `net_send` 全部被状态机拒绝(`echo: send -1 != recv N`),PC 侧回显 0 字节;接收路径仍可读出数据(RECV_OK 正常、缓冲可排空)。断开时 DISCONNECT 中断命中 `handle_disconnect_event` 的 LISTENING 分支 → 仅记 `WARNING: listen sockN got disconnect` 而无任何处置 → 芯片在 DISCONNECT 后**自动关闭 Socket**(FUN_PARA 默认行为)→ MCU 未再重开 → **监听静默死亡**:此后所有 SYN 被拒(connection refused),不自恢复(仅复位/网络重建可恢复)。
复现:Phase 6-only 构建 + `test/ch395f_socket_test.py tcp_client` 无间隔连续运行 → run1 全过、run2 失同步、run3 起全部 connection refused(2026-09-22 台架 A 组;出现 1/1,未做 10/10 规模复验)。
### 现象(生产固件实测)
单连接监听 socket 在快速复连后**永久失效**(后续 SYN 被拒或超时,10s 内不自恢复),而芯片/网栈其余部分完全正常(其他监听端口照常工作)。两种表现:
- **拒连(ConnectionRefused)**:芯片完成握手但 **CONNECT 中断未上报** → 控制块滞留 LISTENING;或本层因 **DISCONNECT 中断丢失**仍以为有客户端 → 两种情况都会让芯片侧那条连接**一直占住单连接监听**,后续 SYN 全被拒;且丢事件后本层收不到任何中断,无法靠事件自愈。
- 应用侧连锁:`net_send` 被拒(send -1);或应用读不到 EOF(其 `g_client` 永不释放,因而**永不调用 accept**)。
### 机制(实证边界;芯片侧微观机理未解)
1. 芯片硬件完成握手但 CONNECT 中断未上报(或 MCU 侧丢失)→ `handle_connect_event` 未执行 → 状态滞留 LISTENING。
2. **实证边界(2×2 台架)**:失同步仅现于"轻负载 + 重监听历史 + 快速重连(~600ms)"组合;重负载(Phase 6/9/11)下断连后 ~500ms 重连仍同步正常(B' 组 20/20 standalone accept、10/10 全连接成功);轻负载 + 15s 重连间隔亦全正常(A' 组 20/20)。即中断丢失窗口 ≈ "close → 重监听 → SYN" 间隔 < ~1s 且 netTask 相对空闲。芯片侧丢中断的具体原因未解(列为后续观察项,不阻塞下述修复)。
3. 后果链确定:状态滞留 LISTENING → `net_send` 被拒(send -1)→ 断开时 DISCONNECT 落 LISTENING 分支(现有代码仅告警)→ 芯片自动关闭 Socket → MCU 不再重开 → 监听死亡。
4. 与 Trap 21 的关系:Trap 21(TC-NET-604 "RX 窗口冻结")**已结案为非缺陷**(真实生产路径 227 轮全过;原台架"停摆"系测试程序自吞数据 + 判读工具误判,见 Trap 21);本条(Trap 22)是**独立且仍待修**的失效模式:连接状态失同步(缺 standalone accept + `send -1` + `WARNING: listen`),须与之分开跟踪。
### 复现(可信用例,纯客户端判据;工具 `MDK-ARM/tcp_reconnect_probe.py`)
- **确定性复现**:产品固件 + 待机 60s 后,对 UI 监听 `:7003` 以 300ms 间隔快速复连 → 修复前**首 2 次成功、之后永久拒连**(10 轮 2/10,10s 恢复窗口内无自恢复);
- **概率复现**:FTP 控制 `:21` 以 100ms 间隔连 50 次 → 修复前**第 29 轮起永久拒连**;
- 对照:FTP `:21` 在 0ms / 600ms 间隔下通过;`:8888`(对时)同期亦正常 → 触发与"复连间隔 <~1s + 轻负载(待机)"相关,与 2026-09-22 记录的边界一致。
### 修复(方案,未实施 → N-6)
1. `handle_disconnect_event` LISTENING 分支:回读芯片 socket 状态,非可监听状态(即芯片已自动关闭)时执行 `net_relisten_try` 重建(重 OPEN + TCP_LISTEN)——D20 陈旧事件修复的 LISTENING 分支姊妹情形(D20 只处理 ESTABLISHED 分支)。
2. 可选加固:重监听完成后清除该 socket 残留中断位(`handle_connect_event` 已有同模式),防陈旧位致下一次事件误判。
3. 应用侧规避(可立即执行):单连接 socket 断连后重连留 ≥1~2s 退避——客户端快速重试 / lftpd 快速 PASV 序列是生产主要风险场景。
### 机制(设备侧日志实证)
1. 芯片丢 **CONNECT** 中断 → 无 `standalone accept`,控制块滞留 LISTENING;或丢 **DISCONNECT** 中断 → 本层仍以为 ESTABLISHED;
2. 此后本层**收不到任何中断**(芯片静默持有/丢弃该连接),纯事件驱动无路可走——实测把 `handle_disconnect_event` 的 LISTENING 分支从"仅告警"改为"重建监听"后**仍然无效**(因为连 DISCONNECT 都没来);
3. 故必须**周期回读芯片状态对账**才能发现失同步。
### 修复(已实施,`Drivers/BSP/NET/net_socket.c`)
1. **周期对账 `net_reconcile_tick()`**(`net_process_messages` 每轮调用;周期 200ms):对在用 socket 回读芯片状态,修正三类失同步——
① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理(应用读到 EOF 后自动重监听);
② 本层 LISTENING 而芯片已完成握手 → 补做 standalone accept(复用 `handle_connect_event`);
③ 本层 LISTENING 而芯片监听已消失 → 重建监听(`net_relisten_try`)。
**去抖 + 冷却**:同一失配需连续 2 次(≈400ms)才动手;同一 socket 动手后冷却 1s。
2. `handle_disconnect_event` 的 LISTENING 分支:由"仅告警"改为"回读芯片状态——已不可监听即重建"(仍在监听则判陈旧事件丢弃)。
### 踩坑记录(务必先读)
最初的修法放在 **accept 路径**(每次 accept 时回读状态并自愈):单次状态读会把"刚 open/listen 尚未生效"的瞬态误判为失配 → 监听被反复重建直至 `relisten give up` → **三个监听口全部死亡(风暴)**。改为"周期对账 + 去抖 + 冷却"后消除。
**结论:凡基于单次状态读的重建动作,必须去抖。**
### 验证(修复前 → 修复后)
| 用例 | 修复前 | 修复后 |
|---|---|---|
| 待机后 UI `:7003` 快速复连 ×10 | 2/10(之后永久拒连) | **10/10** |
| FTP `:21` 间隔 100ms ×50 | 28/50(第 29 轮起死亡) | **50/50** |
| FTP `:21` 间隔 0ms / 600ms ×50 | 通过 | **50/50 / 50/50** |
| UI `:7003` 间隔 200ms ×20 | — | **20/20** |
| FTP 正常上传 10×64KB(SHA-256 往返) | 通过 | **10/10** |
| 上传中止 RST / FIN 回归(Trap 23) | 通过 | **5/5 + 5/5** |
| 设备侧 `relisten give up` / 重建风暴 | — | **0** |
自愈实测计数(整轮会话):`chip closed -> disconnect` **12** 次、`connection completed w/o CONNECT irq -> accept` **1** 次、`listen lost -> relisten` **1** 次——三类失同步均真实出现并被自愈;`relisten give up` 0 次。
### 对应文件
`Drivers/BSP/NET/net_socket.c`(handle_disconnect_event LISTENING 分支);`test/net_test_task.c`(复现:Phase 6-only 构建 + 快速 tcp_client);`docs/待处理内容.md` N-6(修复待办登记)。
`Drivers/BSP/NET/net_socket.c`(`net_reconcile_tick` / `handle_disconnect_event` LISTENING 分支);`MDK-ARM/tcp_reconnect_probe.py`(可信用例);`docs/待处理内容.md` N-6(已闭环)。
### 发现时间
2026-09-22(Trap 21 二次 2×2 复核台架副产品,A 组 run2)。
2026-09-22(台架偶发 1/1)→ 2026-09-23(生产固件确定性复现 + 当日修复闭环)。
## Trap 23:上传中止后首笔复连上传被静默截断(≤2048B)并误报成功