fix(NET): 监听重建单一所有者收口 + 沉降窗 800ms 上限只留痕
CI / L0 静态门禁 (pre-commit) (push) Failing after 1m12s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 17s

T0 降噪 + T1 单一所有者:四处重建触发(recon/accept/eof/drop)与 pending
第五处统一走 net_listener_request(),由 net_listener_tick 单点执行;
沉降重试日志改"只留首末"(E0 口径),每次恢复少打 3 行重复行。

force open 降级为只留痕(E19 反事实 A/B 实测):芯片未回 CLOSED 时强行
open 注定吃 USE 错(TRAP-NET-09),放行与不放行的端口恢复时刻同一分布
(放行 +512 / 不放行 +505·533 / 降级后 +509 ms)——恢复由芯片何时回
CLOSED 决定。真正兜住"监听器永久失效"的是 D10 预算到期 + 对账形态④
重新登记,不是放行。新增 s_relisten_capped[] 使上限留痕只打一次,
隔离标志不清 → 下一轮继续排空残渣(构建 B 实测每 30ms 两行会刷屏)。

负向注入三轮打靶(E18,注入代码不入库):形态① 吞掉 DISCONNECT 后
4/4 救回、③ 空闲监听器静默失位两轮独立复现、⑤ 开盲区吞掉芯片自发中断
+ 收回 LISTEN 后 -> resync 命中且挂起 recv 被取消 → 三条均不可降级为
只留痕。顺带稳定复现 [ERR] relisten retry budget exhausted = 对端不配合
拆链的签名,非回归。

台架:boot PASS;ftp_abort_probe RST 10/10 + FIN 5/5;relisten_stress
:21 50/50(p50 507ms);n30_repro 30/30。构建 0 错 0 警 Code=212316;
netTask 静态最坏链 1920→1912 B;RAM 台账受管区已 --write 同步(RW +8B)。
文档:E17/E18/E19 三行 + §4.1 常态 0 命中归因 + §4.3 兜底归属改正;
登记簿 N-31 销项(结论沉淀进 §4.3 与 E19)。
This commit is contained in:
2026-10-14 15:57:10 +08:00
parent e95d2be30b
commit 0edbfaf08b
17 changed files with 356 additions and 224 deletions
+1 -1
View File
@@ -53,7 +53,7 @@
<text x="962.0" y="454.0" font-size="11.5" font-weight="700" fill="#d97706">net_send_pending_tick()</text>
<text x="962.0" y="472.0" font-size="11" fill="#64748b">推进挂起 send</text>
<rect x="1150.0" y="434.0" width="186.0" height="46" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-opacity="0.6"/>
<text x="1160.0" y="454.0" font-size="11.5" font-weight="700" fill="#7c3aed">net_listener_retry_tick()</text>
<text x="1160.0" y="454.0" font-size="11.5" font-weight="700" fill="#7c3aed">net_listener_tick()</text>
<text x="1160.0" y="472.0" font-size="11" fill="#64748b">重试对账</text>
<rect x="1348.0" y="434.0" width="186.0" height="46" rx="8" fill="#ffffff" stroke="#64748b" stroke-opacity="0.6"/>
<text x="1358.0" y="454.0" font-size="11.5" font-weight="700" fill="#64748b">排空消息队列</text>

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

+5 -1
View File
@@ -1,8 +1,11 @@
# 登记簿(N-x / S-x / T-x 长期观察项)
# 登记簿(长期观察项;存量 `N-x / S-x / T-x`,**新条目用语义标识**)
> 编号空间与"不回收"规则见 `docs/任务规划/README.md` §0;排期位置见 `docs/任务规划/README.md` §2 波次总览;
> 当前波次任务卡(带 DoD / 写范围 / 状态)在 `W1-采集保护收尾.md`。
> **本文件是低频文件**:新发现按类别进对应表;销项 = 删行 + 清引用(`docs/任务规划/README.md` §3)。
> **2026-10-14 起新条目的 `#` 列填「语义标识」**——一行短语直接说清是什么事(如
> `沉降窗 800ms 上限只留痕`、`RS485 采集接线`),**不再新增字母+数字编号**;存量编号冻结沿用,
> **已随销项消失的号永久作废**、不得复用(原因与撞号实测见 `docs/任务规划/README.md` §0)。
## 1. 网络(CH395F / net 层)
@@ -13,6 +16,7 @@
| N-4 | **pending 机制二期:ACCEPT 阻塞路径 pending 化**(SEND 部分已完成)——E-06、E-20、E-21 剩余分量共用该机制扩展;届时 L3/L5 基线再重锁、探针族扩充、看门狗接 selftest 留痕、模拟器差距清单 +1 | — | 一期(RECV)已落地;二期 **SEND 已于 2026-10-14 落地**(构建门禁过、台架复测待做),**ACCEPT 仍待做**(收益性质不同,优先级低——见 `Net层阻塞模型架构设计.md` §10 判据)。架构现状见 `Net层阻塞模型架构设计.md`,ACCEPT 设计基线见 `基线与待确认.md` §2.1 | `错误码传播审计.md` 第二档 #2 |
| N-16 | **A2/A4 专项场景台架验证待办**(正常路径已实测零回归):① PHY 拔线负向测试——断链需断 **DTU 侧**线缆或关**受管交换机端口**;② A2 芯片级 `net_init` 失败分支(需 SPI 故障注入);③ A4 短写压测(需远显客户端 + UI 推流压力) | 2026-10-03(台架拓扑 = CH395↔交换机,拔线操作受限) | 待专项台架窗口 | `实机测试实施指南.md` §10 |
| N-17 | **`time_sync` 30s 挂起客户端掉线 + 监听重建期间,网络任务心跳缺口达 800ms~1s(声明 stall 但未复位)。**待办**:定位监听重建为何阻塞 NET/FTP/UI;评估缩短重建重试(5×2s)或让掉线路径不阻塞其它任务 | 2026-10-03(V5 台架) | V5 长跑 44 次 `task stall: mask=0x00000001`,**100% 与 `e02_probe` 的 30s 挂起客户端触发 `time_sync: no TIME packet in 30s, drop conn` → 监听重建(约 4s)重合**;隔离实验(仅 FTP,120s)**0 次 stall** → 探针场景专属。缺口(~1s)< IWDG 最坏超时(1701ms),余量仅 ~0.7s。 | `实机测试实施指南.md` §4;`../子系统设计/看门狗设计说明.md` |
| N-30 | **relisten 沉降段统一到驱动 `sock_reset` 原语受阻**——"去重复实现"的收口已实测回退:原语版 `ftp_abort_probe` RST **0/10**、FIN **0/5**(中止后首笔复连仍被截断到 ≤2048B),另新增 `sock_reset_sync timeout (600ms)` 与 `close: sock_reset not done` 两条 `[ERR]`;同场 net 层自建序列 **10/10 + 5/5** | 2026-10-14(同一块板 A/B,判据 `test/host/ftp_abort_probe.py`;快复连 50/50、背靠背 30/30 两版都过 → 失效面精确落在"中止后首笔复连") | **现状 = `net_listener.c` 保留自建排渣序列**(排空 + CLOSE + 排事件 + 等 CLOSED,30 ms 重试轮持续清)。重开前置三条语义:① 隔离窗内**持续**排空/关闭(残渣在 CLOSE 之后才落进 RX,一次排空接不住)② **复用让位**(复位机不得拆上层刚在同一芯片 Socket 上重建的监听)③ `net_sock_reset_sync` 改"已在运行 = 加入等待"(`begin` 返 -1 ≠ 失败) | `../网络/网络自愈架构设计.md` §8 延后项 1、§5.2 E16;`../问题记录/CH395F_Trap_Records.md` TRAP-NET-18 |
## 2. 存储 / 日志管线
@@ -36,10 +36,10 @@
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 230,988 B (225.57 KB) | 1,048,576 B (1024 KB) | 22.0% |
| RW_IRAM1 | 98,136 B (95.84 KB) | 131,072 B (128 KB) | 74.9% |
| ER_IROM1 | 231,056 B (225.64 KB) | 1,048,576 B (1024 KB) | 22.0% |
| RW_IRAM1 | 98,144 B (95.84 KB) | 131,072 B (128 KB) | 74.9% |
| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
| **Flash (ROM 合计)** | **232,092 B (226.7 KB)** | 1,048,576 B (1024 KB) | **22.1%** |
| **Flash (ROM 合计)** | **232,168 B (226.7 KB)** | 1,048,576 B (1024 KB) | **22.1%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
@@ -110,7 +110,7 @@
| 任务 | 配置栈 | 字节 | 实际职责 | 2026-10-10 实测峰值 / 静态最坏链 |
|---|---|---|---|---|
| uiTask | `4096*4` | 16KB | LVGL 1bpp 离屏渲染(渲染缓冲在静态区,栈只走调用链) | 4016 B(余 75%)/ 静态 1336 B → 可大幅缩减 |
| netTask | `960*4` | 3.75KB | CH395F 轮询(DMA 搬运,栈压力小) | **1116 B(余 70%)**/ 静态 **1944 B**(2026-10-14 SEND pending 台架复测,含 `net_send_pending_tick` 新增栈帧;FTP 传输期峰 **1164 B**。前值 920 B 为一期 recv 挂起时实测,N32 监听模块化后复测 **1944 B** 不变,链尾经 net_listener_retry_tick→net_relisten_locked) |
| netTask | `960*4` | 3.75KB | CH395F 轮询(DMA 搬运,栈压力小) | **1116 B(余 70%)**/ 静态 **1912 B**(2026-10-14 E19 `force open` 降级后复测,链尾经 net_listener_tick→net_relisten_locked;前值 **1920 B** = 同日 E17 单一所有者收口后、**1944 B** = SEND pending 台架复测(含 `net_send_pending_tick` 新增栈帧),更早 920 B 为一期 recv 挂起时实测;FTP 传输期实测峰 **1164 B**) |
| ftpTask | `1024*4` | 4KB | lftpd FTP 服务 | 740 B(余 81%)/ 静态 1216 B |
| testTask | `2048*4` | 8KB | 测试套件(产品构建不创建;存储套件深链需要) | 未创建 / 静态 496 B |
| defaultTask | `1280*4` | 5KB | FatFS 挂载/探针 + 配置引擎 + syslog + 栈水位探针 | 2152 B(余 41%)/ 静态 **3016 B**(N-33 抬栈 1024→1280,2026-10-12) |
+3 -3
View File
@@ -15,14 +15,14 @@
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|---|---|---|---|---|
| ER_IROM1 (Flash) | 230,988 B | 1,048,576 B | 22.0% | 参考 |
| ER_IROM1 (Flash) | 231,056 B | 1,048,576 B | 22.0% | 参考 |
| RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS |
| UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 |
| RW_IRAM1 | 98,136 B | 131,072 B | 74.9% | PASS |
| RW_IRAM1 | 98,144 B | 131,072 B | 74.9% | PASS |
| 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 |
|---|---|---|---|
| 网络 | 13,682 B | 12 KB | ⚠ 超 1.4 KB |
| 网络 | 13,690 B | 12 KB | ⚠ 超 1.4 KB |
| 存储 | 7,056 B | 12 KB | OK |
| UI | 54,988 B | 16 KB | ⚠ 超 37.7 KB |
| RTOS 堆 | 57,376 B | 56 KB | 堆=配额 |
@@ -663,7 +663,7 @@ sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复
| **会话跑完一段后无限循环、peer 空等 300s** | **D16**:Phase 13 状态机迁移目标错 | 核对顺序 L5→L3→L10→L6→L7→DONE(§8 D16) |
| **`[LAT]` 段行缺失/截断、wait-idle 判不出空闲** | **D12-b**:热路径日志洪水 | 删除 net 热路径逐调用日志(§8 D12-b) |
| **开机日志连续丢行**(含必检标记 `net init: OK` 等)但系统健康 | **日志并发竞态**:多任务并发 `printf` → 非重入 `fputc`/`HAL_UART_Transmit` 丢整行、UART 状态短暂卡住(2026-09-22 台架实证:boot 时 testTask 启动行与 netTask 日志并发 → 连续 4 行丢失、~2s 后自恢复) | 必现证据行采用"静默采集 + 延迟到安静窗口打印"模式(范例:P0-a 验收行曾按此模式实装,验收完成后探针已移除);根治 = S8 日志非阻塞/串行化(§10.2) |
| **`relisten` 段 FAIL、断线固定 ~588ms** | **D10** 回归(退回 `osDelay(500)`) | 核对 `net_listener_try`/`net_listener_retry_tick` 非阻塞重试在位(§8 D10) |
| **`relisten` 段 FAIL、断线固定 ~588ms** | **D10** 回归(退回 `osDelay(500)`) | 核对 `net_listener_request`/`net_listener_tick` 非阻塞重试在位(§8 D10) |
| **远端显示整帧卡住十几秒后自恢复** | **E-02** 回归 | 核对 L3 pending 在位:`net_recv_locked` 阻塞分支登记 pending(哨兵 `NET_PENDING_REGISTERED`)+ `net_pending_tick` 在主循环 + 调用方剩余预算循环(lftpd/timeSync)(§8.3 要点 4) |
| **INIT 应答帧字节数字段恒为 0** | **D17** 回归 | 核对 `remo_send_init` 发满 8B(§8 D17) |
| **开机首个 FTP 会话第 2 个数据连接 425** | **D20**(已定位未修,§8.2) | 复现:`test/host/ftp_regression.py`(复位开边界);现场规避:客户端重试 425(瞬态 4xx);根治方向:数据 Socket 轮换 |
+4 -4
View File
@@ -63,7 +63,7 @@
| `net_types.h` | 类型定义(地址结构、Socket 控制块等) |
| `net_socket.h` | BSD Socket API 头文件,含 `net_msg_t` 消息类型定义 |
| `net_socket.c` | API 核心实现:消息队列处理与线程安全封装、内部 `_sock()` 版本;另含 L3 pending 化 recv、TRAP-NET-17 周期对账 + 监听重建**合一** reconciler(`net_reconcile_tick`,100ms,分级去抖+冷却,形态①②③④⑤/U11,自愈收敛阶段 3) |
| `net_listener.c`(**真机专属**) | **监听生命周期**(N-32 §10 模块化):`net_listener_try` / `net_listener_clear` / `net_listener_retry_tick`(D10 重试 30ms/2s)/ `net_listener_settle`(沉降/隔离 100ms/400ms);原 `net_listener_pump` 的重建决策已并入 `net_reconcile_tick` 形态③④(自愈收敛阶段 3,单一 100ms tick);内部头,模拟器不编译 |
| `net_listener.c`(**真机专属**) | **监听生命周期**(N-32 §10 模块化,E17 单一所有者收口):`net_listener_request`(**只登记意图**,5 个登记点)/ `net_listener_tick`(**唯一执行者**,门集 + D10 重试 30ms/2s)/ `net_listener_clear` / `net_listener_settle`(沉降/隔离 100ms/400ms);"该不该重建"的周期判据在 `net_reconcile_tick` 形态③④(自愈收敛阶段 3,单一 100ms tick);内部头,模拟器不编译 |
| `net_select.h` | select/poll API 头文件 |
| `net_select.c` | select/poll 实现(仅 MCU 端编译) |
| `net_socket_sim.c`(`simulator/sim/`) | Winsock2 后端(模拟器端,与 `net_socket.c` 互斥编译),详见 §2.3 |
@@ -190,7 +190,7 @@ int net_close_sock(net_sock_t *p_sock);
|---|---|---|---|
| **`auto_relisten`** | 数据通道正常 EOF / DISCONNECT | 重开 + 重新 LISTEN,回 LISTENING;应用层无需 close/重 listen | `net_listen()` 置位;`net_close()` 清 |
| **D10 非阻塞重监听重试** | 重开失败(TRAP-NET-09:芯片 close 后需沉降) | 单次尝试失败即登记待重试,每轮到点重试;超时限放弃并记 `relisten give up` | 重试间隔 30ms / 时限 2s(自首次尝试);同 fd 有在途挂起 recv / send 时跳过(L3 保险,防毁在途连接 / 截断在途传输) |
| **TRAP-NET-17 周期对账 + 监听重建(合一 `net_reconcile_tick`,自愈收敛阶段 3;对账①②⑤ + 原 pump③④/U11)** | 芯片 CONNECT/DISCONNECT 事件未收到(快速复连,读清消费竞态——E14 复核:非芯片侧丢失);静默回收 Socket(peer FIN,无 DISCONNECT 事件);监听型 fd 芯片监听失位 / 本层状态滞后 | 每 100ms 对在用 socket 读**驱动状态缓存**(`ch395f_sock_state_cached`,唯一刷新点 = `net_poll` 每轮全量刷新,E2 实测单次回读 18.1µs),修正**五类**失同步 + 一类被动重同步:① 本层 ESTABLISHED 而芯片已断 → 按断开处理(只置事实);② 本层 LISTENING 而芯片已握手 → 补 accept(N-30 幽灵甄别);③ 本层 LISTENING 而芯片监听失位 → 重建(`net_listener_try`);④ 本层 CLOSED(监听型 fd)而芯片不在 LISTEN → 重建;⑤ 本层 ESTABLISHED 而芯片已静默回收回 LISTEN(0x05/0x01)→ RESYNC 语义只修本层、不发芯片命令(自愈收敛阶段1 新增,原手动 API `net_resync_listen` 的语义并入);U11 本层 CLOSED 而芯片**正在** LISTEN → 只做状态重同步、不发芯片命令 | 周期 100ms;**分级去抖**:②=1 轮、③④⑤=2 轮、①=4 轮(M3 让位窗 400ms,**勿改小**)+ 动手后 **1s 冷却**(防"刚 open 未生效"瞬态重建风暴;U11 被动重同步不去抖、不占冷却);其它位置只置事实、不做重建动作 |
| **TRAP-NET-17 周期对账 + 监听重建(合一 `net_reconcile_tick`,自愈收敛阶段 3;对账①②⑤ + 原 pump③④/U11)** | 芯片 CONNECT/DISCONNECT 事件未收到(快速复连,读清消费竞态——E14 复核:非芯片侧丢失);静默回收 Socket(peer FIN,无 DISCONNECT 事件);监听型 fd 芯片监听失位 / 本层状态滞后 | 每 100ms 对在用 socket 读**驱动状态缓存**(`ch395f_sock_state_cached`,唯一刷新点 = `net_poll` 每轮全量刷新,E2 实测单次回读 18.1µs),修正**五类**失同步 + 一类被动重同步:① 本层 ESTABLISHED 而芯片已断 → 按断开处理(只置事实);② 本层 LISTENING 而芯片已握手 → 补 accept(N-30 幽灵甄别);③ 本层 LISTENING 而芯片监听失位 → 重建(`net_listener_request`);④ 本层 CLOSED(监听型 fd)而芯片不在 LISTEN → 重建;⑤ 本层 ESTABLISHED 而芯片已静默回收回 LISTEN(0x05/0x01)→ RESYNC 语义只修本层、不发芯片命令(自愈收敛阶段1 新增,原手动 API `net_resync_listen` 的语义并入);U11 本层 CLOSED 而芯片**正在** LISTEN → 只做状态重同步、不发芯片命令 | 周期 100ms;**分级去抖**:②=1 轮、③④⑤=2 轮、①=4 轮(M3 让位窗 400ms,**勿改小**)+ 动手后 **1s 冷却**(防"刚 open 未生效"瞬态重建风暴;U11 被动重同步不去抖、不占冷却);其它位置只置事实、不做重建动作 |
| **TRAP-NET-18 排渣隔离** | 异常释放(芯片 TIMEOUT,如客户端中止上传):在途数据落进 RX 缓冲,直接复监听 → 下条连接把 ~2048B 残渣当自己的数据读走(上传静默截断);正常 FIN 释放亦有残留 DISCONNECT 收尾问题 | 复监听前沉降/隔离:持续排空 RX 残渣 + 主动 close + 排空中断,等芯片回 CLOSED/TCP_CLOSED 才 open+listen;到期强制放行 | 沉降等级(`net_listener_settle`):异常释放 QUARANTINE **400ms** / 正常释放 GRACEFUL **100ms**(2026-09-24 起正常 FIN 亦需沉降);上限 **800ms** 强制放行(**必须 < 2s 时限**,否则监听被放弃) |
**行为结论**:监听器恢复从"一次性事件"变为"登记 → 到点重试"过程:
@@ -1013,7 +1013,7 @@ int net_drop_conn(int sockfd);
应用**放弃会话**(对时 30s 预算耗尽、协议超时)时主动拆掉当前 TCP 会话并**保留监听**——单连接模型下挂起但 TCP 仍活的客户端会永久占住监听(PC 内核自动 ACK keepalive,芯片僵尸判不死,定案见 `docs/网络/网络自愈架构设计.md` §5.2 E13),而应用又不得 `net_close` 监听 fd(毁监听);本 API 补上"拆连接不拆监听"的应用侧形态。
- **netTask 侧**:先结束本 fd 挂起 recv(排空/取消 + 必通知),再 `sock_reset` QUARANTINE(FIN + 排渣 + 排空事件 + 等 byte0=00H,TRAP-NET-18 异常释放语义,400ms 底);本层收口 CLOSED;`auto_relisten` fd 随即 `net_listener_try` 即时重监听(失败交 D10 重试 + reconciler 形态④)
- **netTask 侧**:先结束本 fd 挂起 recv(排空/取消 + 必通知),再 `sock_reset` QUARANTINE(FIN + 排渣 + 排空事件 + 等 byte0=00H,TRAP-NET-18 异常释放语义,400ms 底);本层收口 CLOSED;`auto_relisten` fd 随即 `net_listener_request` 登记重监听(下一轮 `net_listener_tick` 执行,30ms 节奏重试 + reconciler 形态④兜底)
- **幂等**:无活动会话(非 ESTABLISHED/CONNECTING)直接返 0,不动监听
- **EOF 路径不要调**:EOF 时 net 层 `auto_relisten` 已接管(§12.4),无需也不应再拆
- 经消息队列委托 netTask(同其它 `net_*` API,非 netTask 上下文调用);等待期 netTask 有界占用 ~400-600ms
@@ -1057,7 +1057,7 @@ void net_process_messages(void);
处理其他任务投递的请求(send/recv/connect/accept/close/drop_conn)。每次调用先跑 4 个**非阻塞** tick,再取队列消息(0 超时出队):
1. **D10 重监听重试**(`net_listener_retry_tick`):断连后监听器恢复的到点重试(30ms 间隔 / 2s 时限,§3.6)
1. **D10 重监听重试**(`net_listener_tick`):断连后监听器恢复的到点重试(30ms 间隔 / 2s 时限,§3.6)
2. **L3 挂起 recv 完成检查**(`net_pending_tick`):按 ①数据 ②到点(仅显式预算) ③断连 检查每个挂起请求并通知调用方;④ 挂起超时留痕探针(编译期选项,默认关):挂起超阈值只 `[ERR]` 留痕一次,不强制结束
3. **L3 挂起 send 推进**(`net_send_pending_tick`):按 ①断连 ②推进(每轮至多一块 `NET_SEND_CHUNK_MAX`) ③写完 ④无进展超时 推进挂起 send 并通知调用方(结果恒为已发字节数,短写由调用方检查)
4. **TRAP-NET-17 芯片/本层对账 + 监听重建(合一 `net_reconcile_tick`)**:100ms 周期,判据读驱动状态缓存(`ch395f_sock_state_cached`),修正**五类**失同步(①②③④⑤)+ U11 被动重同步——原 200ms 对账与 100ms 监听器 pump 已合并为单一 tick,**唯一重建入口**=形态③④(§3.6)
@@ -62,7 +62,7 @@ flowchart TB
LR["netRecvLocked<br/>快路径 / DONTWAIT / 登记挂起"]
LA["netAcceptLocked<br/>先 netPoll + 状态判定"]
LC["netCloseLocked<br/>结束挂起 + cmd_done 快路径 / sock_reset(GRACEFUL)"]
DC["netDropConnLocked<br/>拆会话保监听:sock_reset(QUARANTINE) + 即时重监听"]
DC["netDropConnLocked<br/>拆会话保监听:sock_reset(QUARANTINE) + 登记重监听"]
RLS["netListenerTry → netRelistenLocked<br/>单次尝试 + TRAP-NET-18 排渣隔离"]
end
EV["事件处理器<br/>handleConnect · handleDisconnect<br/>handleRecv · handleSend · handleTimeout"]
@@ -82,7 +82,7 @@ flowchart TB
T3 -->|"② 补 accept"| EV
T3 -->|"⑤ 静默回收:RESYNC 只修本层 + 取消挂起(不发芯片命令,自愈收敛阶段1 吸收原 net_resync_listen 语义)"| RSY["reconcile⑤ RESYNC 动作"]
T3 -->|"③④ 监听失位 / 孤儿监听即重建(原 pump,自愈收敛阶段 3)"| RLS
DC -->|"拆会话后即时重监听"| RLS
DC -->|"拆会话后登记重监听"| RLS
NP --> EV
EV --> FE
FE -.-> CB
@@ -107,7 +107,7 @@ flowchart TB
2. `net_process_messages()` 开头的 **4 个 tick 是网络层自愈主干**:没有任何应用消息时,重试 / 挂起 recv 完成 / 挂起 send 推进 / 状态对账 + 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。
3. 事件处理器**只由 `net_poll` 驱动**,例外是 `net_reconcile_tick`(分级去抖 + 1s 冷却)可再入 `handle_disconnect_event` / `handle_connect_event`——对账是事件未收到的自愈通道(TRAP-NET-17)。**注意职责边界(N-32 P2,自愈收敛阶段 3 合并后)**:对账与事件处理器**只置事实**(置 CLOSED / 登记沉降),**重建监听的动作统一由 `net_reconcile_tick` 形态③④ 做**(本层唯一重建入口)——"谁先看到 DISCONNECT 就谁重建"的竞争在结构上消失。
4. 回调 `event_cb` 在 **netTask 上下文**执行:回调内不得阻塞、不得发网络操作。
5. **模块归属**:`T1`(D10 重试)/ `RLS` 的 `net_listener_try` / `ST`(settle)在 `net_listener.c`(真机专属、模拟器不编译);挂起 recv 完成检查(`net_pending_tick`)、挂起 send 推进(`net_send_pending_tick`)与两张挂起表(`PR` / `SR`)在 `net_pending.c`(真机专属);其余 tick(含合并后的 `T3` reconciler)与 `_locked` 在 `net_socket.c`。
5. **模块归属**:`T1`(D10 重试)/ `RLS` 的 `net_listener_request` / `ST`(settle)在 `net_listener.c`(真机专属、模拟器不编译);挂起 recv 完成检查(`net_pending_tick`)、挂起 send 推进(`net_send_pending_tick`)与两张挂起表(`PR` / `SR`)在 `net_pending.c`(真机专属);其余 tick(含合并后的 `T3` reconciler)与 `_locked` 在 `net_socket.c`。
---
@@ -126,7 +126,7 @@ flowchart TB
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|---|---|---|---|
| `net_poll()` | 事件泵:GPIO 轮询 INT#(低=有待处理)→ `GET_GLOB_INT_STATUS_ALL`(读=清)→ 按位图逐 Socket 读 SINT → 分派 5 类事件;**每轮末尾**对在用 Socket 全量刷新**驱动状态缓存** + 步进 `ch395f_sock_reset_poll`(自愈收敛阶段 3) | `HAL_GPIO_ReadPin` → `ch395f_get_glob_int_status_all` → `ch395f_get_sock_int_status` ×n → `handle_connect/disconnect/recv/send/timeout_event` → `ch395f_get_sock_state` ×n(刷新缓存)+ `ch395f_sock_reset_poll` ×n | **RECV 中断是电平触发**:每次调用**只处理一批**(do-while(0)),处理完立即返回,否则主循环(echo/应用逻辑)饿死(TRAP-NET-08);SENBUF_FREE 只置 `send_ready` 不记事件;状态缓存**唯一刷新点**在此(reconciler 只读缓存,E2 实测单次回读 18.1µs);`sock_reset_poll` 步进使同步包装 600ms 放弃后的复位机不搁浅(IDLE 槽纯 RAM 判零 SPI) |
| `net_process_messages()` | 队列入口 + 自愈 tick 宿主:每轮**先**跑 4 个非阻塞 tick,再排空队列逐条分派,结果 `xTaskNotify` 回传 | `net_listener_retry_tick` → `net_pending_tick` → `net_send_pending_tick` → `net_reconcile_tick`(对账+重建合一)→ `osMessageQueueGet`(0 超时) ×n → switch 分派 → `xTaskNotify` | ① tick 放在队列**之前**:断连后即使没有任何应用消息,监听器也能自主恢复、挂起 recv / send 也能完成;② RECV 若返回 `NET_PENDING_REGISTERED`(哨兵 0x70000000)则**不发即时回执**(`do_notify=0`),完成时才通知;③ 失败结果把 `s_net_errno`(具体负错误码)压入通知值——调用方恢复 errno 后对外统一 -1(BSD 语义);④ 通知值**整个 32 位**都是结果(不压位域标签,否则 32768 这类大字节数会溢出为负);⑤ **对账①置 CLOSED 后形态③④同轮即可重建**(自愈收敛阶段 3 合并,原"pump 置于对账之后"的顺序依赖在单 tick 内自然成立) |
| `net_process_messages()` | 队列入口 + 自愈 tick 宿主:每轮**先**跑 4 个非阻塞 tick,再排空队列逐条分派,结果 `xTaskNotify` 回传 | `net_listener_tick` → `net_pending_tick` → `net_send_pending_tick` → `net_reconcile_tick`(对账+重建合一)→ `osMessageQueueGet`(0 超时) ×n → switch 分派 → `xTaskNotify` | ① tick 放在队列**之前**:断连后即使没有任何应用消息,监听器也能自主恢复、挂起 recv / send 也能完成;② RECV 若返回 `NET_PENDING_REGISTERED`(哨兵 0x70000000)则**不发即时回执**(`do_notify=0`),完成时才通知;③ 失败结果把 `s_net_errno`(具体负错误码)压入通知值——调用方恢复 errno 后对外统一 -1(BSD 语义);④ 通知值**整个 32 位**都是结果(不压位域标签,否则 32768 这类大字节数会溢出为负);⑤ **对账①置 CLOSED 后形态③④同轮即可重建**(自愈收敛阶段 3 合并,原"pump 置于对账之后"的顺序依赖在单 tick 内自然成立) |
### 4.3 经队列的公共 API 及其 netTask 侧实现
@@ -135,14 +135,14 @@ flowchart TB
| 公共 API | 消息 | netTask 侧实现 | 关键芯片命令 | 返回语义 | 设计意图 / 注意 |
|---|---|---|---|---|---|
| `net_listen` | `NET_MSG_LISTEN` | `net_listen_locked` | `set_proto_type` + `set_sour_port` + `open_socket`(**open 重试 3×,失败走 `net_sock_reset_sync` 确定性排渣**,自愈收敛阶段 3)+ `tcp_listen` + 清**本 Socket** 中断状态 | 0 / -1 | **初次**监听允许有界重试(启动期无竞争);成功后置 `auto_relisten=1`——监听常驻的源头;启动前只清本 Socket SINT(B3/F-NET-7:全局寄存器唯一读者 = `net_poll`,全量读清会焊死其它 Socket 事件位);open 失败重试原"close+盲等 500ms"已换 sock_reset(GRACEFUL 100ms 底,E4 定案复位后 open 必成) |
| `net_accept` | `NET_MSG_ACCEPT` | `net_accept_locked` | 无(先 `net_poll` 泵一次 + 状态判定) | 监听 fd 自身(单连接)/ -1(WOULDBLOCK) | 单连接:监听 Socket 在 CONNECT 后**自身即数据通道**,返回的就是它;CLOSED + auto_relisten 时触发**非阻塞** `net_listener_try`;**accept 路径不做自愈**——单次状态读会误判"刚 listen 未生效"致重建风暴(2026-09-23 实测),统一交给 `net_reconcile_tick` 形态③④(自愈收敛阶段 3 合并后的单一重建入口) |
| `net_accept` | `NET_MSG_ACCEPT` | `net_accept_locked` | 无(先 `net_poll` 泵一次 + 状态判定) | 监听 fd 自身(单连接)/ -1(WOULDBLOCK) | 单连接:监听 Socket 在 CONNECT 后**自身即数据通道**,返回的就是它;CLOSED + auto_relisten 时触发**非阻塞** `net_listener_request`;**accept 路径不做自愈**——单次状态读会误判"刚 listen 未生效"致重建风暴(2026-09-23 实测),统一交给 `net_reconcile_tick` 形态③④(自愈收敛阶段 3 合并后的单一重建入口) |
| `net_connect` | `NET_MSG_CONNECT` | `net_connect_locked` | `set_proto_type` + `open_socket`(open 重试 3× + `net_sock_reset_sync` 排渣)+ 清残留中断(只清本 Socket SINT,`ch395f_sock_events`)+ `set_sour_port` / `set_des_ip`(大端数组)/ `set_des_port` + `tcp_connect` | 0 = 请求已发出(**异步**)/ -1 | open **先于**寄存器写入(open 会复位寄存器);open 后清残留 DISCONNECT,防新连接秒断(TRAP-NET-12);目的 IP 字节序写反 = CONNECT TIMEOUT(TRAP-NET-14);返回 0 ≠ 已建立——调用方以 `net_sock_state` 轮询等 ESTABLISHED |
| `net_send` | `NET_MSG_SEND` | `net_send_locked` | `ch395f_write_send_buf`(分块)+ 等 SENBUF_FREE(`net_poll` 轮询,**有界自旋 ≤ `NET_SEND_SPIN_MS`**);超预算登记挂起 send(`net_send_pending_register`) | 实际字节数 / -1 | 分块 ≤ `NET_SEND_CHUNK_MAX` = 1KB(**硬件发送 FIFO 硬上限**,TRAP-NET-16:写穿 FIFO = 数据错位);`send_ready` 门控用 `net_poll` 轮询而非 osDelay,**自旋超 `NET_SEND_SPIN_MS`(2ms)即转挂起**(netTask 最坏停滞由 1s 压到 2ms);短写**必须**告警——静默截断会让对端永久等待剩余字节(2026-09-21 实证:UI 推流应答截断 → 客户端失联) |
| `net_recv` | `NET_MSG_RECV`(timeout=0→**无限等待**,自愈收敛阶段 1.5 删 30s 默认) | `net_recv_locked` | `net_poll` + `get_recv_len` / `read_recv_buf` | >0 / 0(EOF) / -1(errno = WOULDBLOCK / CONNRESET;阻塞形态无 TIMEDOUT 一档) | 快路径:数据已在 → 立即读出返回(允许部分读);无数据 + 阻塞(`blocking` 属性=1 且未 DONTWAIT)→ **登记挂起后立即返回**(L3,netTask 零占用),唤醒源=数据/EOF/close/网络重建;DONTWAIT / `blocking=0` + CLOSED/session_dead → EOF + 触发重监听;⚠ IWDG 受监控任务禁用无限形态(`net_socket.h` 文档契约) |
| `net_recv_nb` | `NET_MSG_RECV`(flags=DONTWAIT) | `net_recv_locked` | 同上 | >0 / 0 / -1(WOULDBLOCK 等) | 原 `NET_MSG_DONTWAIT` 路径转正的具名入口(自愈收敛阶段 1.5);不改变 `blocking` 属性;受 IWDG 监控 / 有周期职责任务的推荐默认(ui_net_sink / iec104_serv 已迁) |
| `net_recv_to` | `NET_MSG_RECV`(timeout>0) | `net_recv_locked` | 同上 | >0 / 0 / -1 / **-2(本片超时无数据)** | E-02 头阻塞修复配套 + **显式预算**入口(自愈收敛阶段 1.5,等价 Python settimeout,无隐藏默认值):调用方把长等待切成有界片(如剩余预算),**总超时语义不变、单次占用有界**;`timeout_ms=0` 直接拒(0 = 无限等待,与本接口"有界"存在理由相反);超时**只凭 -2 判定、不得依赖 errno**(`s_net_errno` 是跨任务共享全局量,随时被别的任务覆盖) |
| `net_close` | `NET_MSG_CLOSE` | `net_close_locked` | `ch395f_cmd_done(CLOSE)` 快路径(芯片已 CLOSED → 只清本 Socket 残留事件)/ `net_sock_reset_sync(GRACEFUL)` 确定性排渣(自愈收敛阶段 3) | 0 / -1 | **先**结束本 fd 挂起 recv(排空/取消 + 必通知,关不得让等待者悬挂);排空只碰本 Socket SINT,**不再读清全局中断寄存器**(F-NET-7:全局唯一读者 = `net_poll`);原"close + 500ms 盲等排空循环"已换 sock_reset(内置 LISTEN 态二段 CLOSE 补发,E2 补充发现);控制块整体清零(`last_peer_port` 幽灵基准跨 fd 保留,N-30) |
| `net_drop_conn` | `NET_MSG_DROP_CONN`(自愈收敛阶段 3 新增) | `net_drop_conn_locked` | `net_pending_settle_close` + `net_sock_reset_sync(QUARANTINE)` +(auto_relisten)`net_listener_try` | 0(含幂等)/ -1 | **拆会话保监听**(E-02 收口):应用放弃会话(对时 30s 预算耗尽 / 协议超时)而单连接监听被挂起对端永久占住时的应用侧出口;EOF 路径**不得**调(`auto_relisten` 已接管);非 ESTABLISHED/CONNECTING 直接返 0 不动监听;本层收口 CLOSED 不清 fd(保留 local_port/auto_relisten),监听型 fd 即时重监听、失败交 D10 + reconciler 形态④ |
| `net_drop_conn` | `NET_MSG_DROP_CONN`(自愈收敛阶段 3 新增) | `net_drop_conn_locked` | `net_pending_settle_close` + `net_sock_reset_sync(QUARANTINE)` +(auto_relisten)`net_listener_request` | 0(含幂等)/ -1 | **拆会话保监听**(E-02 收口):应用放弃会话(对时 30s 预算耗尽 / 协议超时)而单连接监听被挂起对端永久占住时的应用侧出口;EOF 路径**不得**调(`auto_relisten` 已接管);非 ESTABLISHED/CONNECTING 直接返 0 不动监听;本层收口 CLOSED 不清 fd(保留 local_port/auto_relisten),监听型 fd 登记重监听、失败交 D10 + reconciler 形态④ |
| ~~`net_relisten`~~ | ~~`NET_MSG_RELISTEN`~~ | **自愈收敛阶段1 已删除**(消息类型同删) | — | — | 原"无条件芯片侧重建"与 pump/D10 完全重叠,且对健康监听器是 footgun(open→USE→close 自毁 ~1s);生产零调用。历史实现见 `net_socket.c` git 历史 |
| ~~`net_resync_listen`~~ | ~~`NET_MSG_RESYNC_LISTEN`~~ | **自愈收敛阶段1 已删除**(语义并入对账⑤) | — | — | D14 静默回收(芯片自行回监听、不发 DISCONNECT)现由 `net_reconcile_tick` 判据⑤自动认领:本层 ESTABLISHED 且芯片 0x05/0x01 → 只修本层(state→LISTENING + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv),**不发芯片命令**(纪律原样保留,run8 实证 170s 拒绝);去抖/冷却沿用对账框架 |
@@ -169,7 +169,7 @@ flowchart TB
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|---|---|---|---|
| `net_pending_register` | 登记挂起(`net_recv_locked` 的"无数据 + 阻塞"路径) | 写 `s_net_pending[fd]` | 每 fd 至多一个:**覆盖会丢失前调用方通知 = 永久挂死**;执行任务自身(netTask)发起的等待直接拒绝(自等自通知 = 死锁,netTask 内须用 DONTWAIT) |
| `net_pending_tick` | 完成检查(`net_process_messages` 每轮调用,非阻塞) | ① `get_recv_len` + `read_recv_buf` ② 到点(仅显式预算) ③ `get_recv_len` 排空 + `net_listener_try` ④ `net_pending_complete` | 按 **①数据 ②到点(仅 `net_recv_to` 显式预算;无限登记无 deadline)③断连** 顺序检查(逐项对齐旧自旋语义);CONNECTING/LISTENING 等状态**保持等待**(CONNECT→ESTABLISHED 是合法迁移,不做状态推断取消,防误杀正在建链的等待);**挂起超时留痕探针**(编译期选项 `NET_PENDING_TRIPWIRE`,默认关):挂起超阈值 = 完成逻辑缺位 → 只 `[ERR]` 留痕一次,**绝不强制结束、绝不伪造 TIMEDOUT**——③ 与显式 cancel 是无限 recv 的全部唤醒责任方 |
| `net_pending_tick` | 完成检查(`net_process_messages` 每轮调用,非阻塞) | ① `get_recv_len` + `read_recv_buf` ② 到点(仅显式预算) ③ `get_recv_len` 排空 + `net_listener_request` ④ `net_pending_complete` | 按 **①数据 ②到点(仅 `net_recv_to` 显式预算;无限登记无 deadline)③断连** 顺序检查(逐项对齐旧自旋语义);CONNECTING/LISTENING 等状态**保持等待**(CONNECT→ESTABLISHED 是合法迁移,不做状态推断取消,防误杀正在建链的等待);**挂起超时留痕探针**(编译期选项 `NET_PENDING_TRIPWIRE`,默认关):挂起超阈值 = 完成逻辑缺位 → 只 `[ERR]` 留痕一次,**绝不强制结束、绝不伪造 TIMEDOUT**——③ 与显式 cancel 是无限 recv 的全部唤醒责任方 |
| `net_pending_complete` | 所有完成/取消路径的公共出口 | `xTaskNotify(caller, result)` + 清 active | 通知值 = 结果原值(>0 字节 / 0 EOF / 负错误码,与消息分派同一回传通道);失败结果同步 `s_net_errno`(netTask 上下文一致性);无在位挂起时静默返回(防重复通知) |
| `net_pending_settle_close` | close 前**结束**本 fd 挂起 | `get_recv_len` + `read_recv_buf` + `net_pending_complete` | 关闭不得丢已到达数据(芯片缓冲 close 前可读):有数据 → 按字节数通知;无数据 → 取消(CONNRESET);先于 `ch395f_close_socket` 执行 |
| `net_pending_cancel` / `cancel_all` | 显式取消(**必通知**) | `net_pending_complete` | 取消路径:陈旧 resync(单 fd)/ 网络重建(全部,`net_init` 开头,早于控制块清零)——"取消不通知 = 调用方永久挂死" |
@@ -177,18 +177,18 @@ flowchart TB
### 4.6 D10 非阻塞重监听重试子系统(`net_listener.c`)
**模型**:重开失败 = **登记待重试**(30ms 节奏 / 2s 时限),`net_listener_retry_tick` 到点重试——任何时刻不阻塞任务。旧实现 `osDelay(500)` 忙等:netTask 冻结 500ms(全网停摆)、实测恢复 588ms、期间重连被拒(M3 台架)。
**模型**:重开失败 = **登记待重试**(30ms 节奏 / 2s 时限),`net_listener_tick` 到点重试——任何时刻不阻塞任务。旧实现 `osDelay(500)` 忙等:netTask 冻结 500ms(全网停摆)、实测恢复 588ms、期间重连被拒(M3 台架)。
> **N-32 §10(2026-10-12)**:本子系统的函数已从 `net_socket.c` **抽出到 `net_listener.c`**(真机专属,模拟器不编译);并新增 `net_listener_settle` 取代外部直接写 `s_relisten_quarantine[]`(沉降等级化)。
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|---|---|---|---|
| `net_listener_settle` | 登记"本 fd 会话已结束,重建前需沉降/隔离"(**只置事实**) | 写 `s_relisten_quarantine[fd]`(等级:GRACEFUL 100ms / QUARANTINE 400ms) | N-32 P1:取代此前外部直接写 `s_relisten_quarantine[]`;由 `handle_disconnect_event`(GRACEFUL)/ `handle_timeout_event`(QUARANTINE)调用。**只置事实,动作交 reconciler 形态③④ / `net_listener_try`**;越界 fd 静默忽略 |
| `net_listener_try` | 尝试重建一次;失败登记重试(**不阻塞**) | `net_relisten_locked` → 成功 `net_relisten_done` / 失败置 `s_relisten_wait/next/dead` | 已在重试队列 → 直接返回 -1(**不重置重试时刻**):否则 10ms 一次的 accept 轮询会不断把重试时刻往后推 → 永不重试(饥饿) |
| `net_listener_settle` | 登记"本 fd 会话已结束,重建前需沉降/隔离"(**只置事实**) | 写 `s_relisten_quarantine[fd]`(等级:GRACEFUL 100ms / QUARANTINE 400ms) | N-32 P1:取代此前外部直接写 `s_relisten_quarantine[]`;由 `handle_disconnect_event`(GRACEFUL)/ `handle_timeout_event`(QUARANTINE)调用。**只置事实,动作只在 `net_listener_tick` 一处发生**;越界 fd 静默忽略 |
| `net_listener_request` | **登记**"本 fd 期望回到 LISTEN"(只置意图,**不碰芯片**) | 写 `s_relisten_req[fd]`(来源 +1)+ `next=0`(下一轮 tick 即尝试) | 取代原 `net_listener_try`(当场动作 → 4 个调用点各自补 `state == CLOSED` 门,U12 漏一处即事故;E17 收口后门上收到 tick)。已在请求中 → **不重置重试时刻**:否则 10ms 一次的 accept 轮询会把重试时刻不断往后推 → 永不重试(饥饿);越界 fd 静默忽略 |
| `net_relisten_locked` | 真正的 open + listen(**单次尝试**) | (沉降期)`net_drain_chip_rx` + `close_socket` + `get_sock_int_status` / `get_glob_int_status_all` + `get_socket_status` → `set_proto_type` + `set_sour_port` + `open_socket` + `tcp_listen` + 清中断 | ① **严禁 osDelay 忙等**:失败只清理并返回,沉降交给重试轮询;② **TRAP-NET-18 沉降门**:带沉降标记的 socket(异常释放 TIMEOUT → QUARANTINE 400ms;正常释放 DISCONNECT → GRACEFUL 100ms)→ 排空 RX 残渣 + 主动 close + 排空中断 + 等芯片回 CLOSED/TCP_CLOSED;**上限强制放行**(必须 < 2s 重试时限,否则沉降耗尽重试预算 → `relisten give up` 监听被弃——2026-09-23 实测修正);③ 成功只置 state=LISTENING,**不 memset**(fd 与 `auto_relisten` 保留——BSD 风格监听常驻) |
| `net_listener_retry_tick` | 每轮到点重试(D10) | `net_relisten_locked` → `net_relisten_done` / 顺延 / 放弃 | **同 fd 有在途挂起 recv / send 时跳过**(重开 Socket 会摧毁在途连接 / 截断在途传输,L3 保险);到 2s 时限 → 清状态 + `relisten give up` 错误日志(应用可见) |
| `net_listener_tick` | 监听重建的**唯一执行者**(netTask 每轮,非阻塞;原名 `net_listener_retry_tick` 只做 D10 到点重试) | `net_relisten_locked` → `net_relisten_done` / 顺延 30ms / 超预算清登记 + `[ERR]` | **门集集中这一处**:控制块有效 + `in_use` + 状态 ∈ {LISTENING, CLOSED} + 监听型(`auto_relisten` / STREAM / `local_port≠0`)+ **芯片已在 LISTEN 即视为请求已达成、静默了结、绝不 open**(U12 结构性防护,读驱动状态缓存零 SPI)+ **同 fd 有在途挂起 recv / send 时跳过**(重开 Socket 会摧毁在途连接 / 截断在途传输,L3 保险)+ 30ms 节奏 / 2s 预算(D10;超预算清登记并留痕,之后由 reconciler 形态③④ 重新登记,监听不永久失效) |
| `net_relisten_done` | 标记完成 | `net_listener_clear` +(探针构建)恢复计时样本 | 恢复时延 = 首次尝试 → 完成(L3 探针 `relisten` 段) |
| `net_listener_clear` | 取消该 fd 的待重试/沉降 | 清 `wait/next/dead` + `settle_dead/settle_start` + `quarantine` | 触发点:`net_socket`(新 fd 不继承)/ `net_close_locked`(关闭即取消);沉降/隔离状态必须一并清,否则残留标记会污染下一次复监听 |
| `net_listener_clear` | 取消该 fd 的重建请求 / 沉降 | 清 `req/next/dead` + `settle_dead/settle_start` + `quarantine` | 触发点:`net_socket`(新 fd 不继承)/ `net_close_locked`(关闭即取消)/ tick 内"登记已陈旧或已达成";沉降/隔离状态必须一并清,否则残留标记会污染下一次复监听 |
| `net_drain_chip_rx` | 排空芯片 RX 残留(256B 分块,≤16 次 = 4KB 上限) | `get_recv_len` + `read_recv_buf` | TRAP-NET-18:中止/异常断开时芯片 RX 缓冲残留上一条连接的数据(实测 ~2048B),不排空会被下一条连接当自己的数据读走(静默截断);分块读避免新增静态缓冲(内存预算紧张) |
### 4.7 监听重建(原 N-32 监听器 pump;自愈收敛阶段 3 已并入 `net_reconcile_tick` 形态③④/U11)
@@ -201,8 +201,8 @@ flowchart TB
| # | 本层状态 / 芯片状态 | 动作 | 来源 |
|---|---|---|---|
| ③ | 本层 LISTENING 而芯片监听**已失位** | 重建(`net_listener_try`) | 原对账③ / 原 pump① |
| ④ | 本层 CLOSED(会话已结束)而芯片**不在** LISTEN | 重建(`net_listener_try`) | 原对账④ / 原 pump② |
| ③ | 本层 LISTENING 而芯片监听**已失位** | 重建(`net_listener_request`) | 原对账③ / 原 pump① |
| ④ | 本层 CLOSED(会话已结束)而芯片**不在** LISTEN | 重建(`net_listener_request`) | 原对账④ / 原 pump② |
| U11 | 本层 CLOSED 而芯片**正在** LISTEN | **只做状态重同步**(CLOSED→LISTENING),**不发任何芯片命令** | U11 缺口 |
情形 U11 的理由:芯片监听器是**健康**的,重下发 open+listen 会因 `USE` 错误走进关闭分支、把正听着的 Socket 关掉(U12 同源事故);只把本层状态收回 LISTENING 即可(与对账⑤ RESYNC 同一纪律——原 `net_resync_listen`,自愈收敛阶段1 已退役)。
@@ -223,8 +223,8 @@ flowchart TB
|---|---|---|---|
| ① | 本层 ESTABLISHED 而芯片已无连接(sock/tcp = CLOSED) | 按断开处理 → CLOSED + 回调(应用读到 EOF 后由形态③④重建监听) | `handle_disconnect_event`(内置陈旧事件甄别) |
| ② | 本层 LISTENING 而芯片已完成握手(tcp = ESTABLISHED) | 补做 standalone accept 簿记 | `handle_connect_event` |
| ③ | 本层 LISTENING(监听型 fd)而芯片监听失位 | 重建 | `net_listener_try`(原 pump①) |
| ④ | 本层 CLOSED(监听型 fd)而芯片不在 LISTEN | 重建 | `net_listener_try`(原 pump②) |
| ③ | 本层 LISTENING(监听型 fd)而芯片监听失位 | 重建 | `net_listener_request`(原 pump①) |
| ④ | 本层 CLOSED(监听型 fd)而芯片不在 LISTEN | 重建 | `net_listener_request`(原 pump②) |
| ⑤ | 本层 ESTABLISHED 而芯片静默回收回 LISTEN(0x05/0x01,无 DISCONNECT 中断) | RESYNC 只修本层、不发芯片命令 | 原 `net_resync_listen` 语义(自愈收敛阶段1 并入) |
| U11 | 本层 CLOSED(监听型 fd)而芯片**正在** LISTEN | 只重同步 CLOSED→LISTENING,不发芯片命令 | 原 pump③ |
@@ -327,14 +327,14 @@ sequenceDiagram
N->>N: handle_timeout_event:net_listener_settle(QUARANTINE 400ms) + CLOSED
end
Note over N: 只置事实 —— 重建统一由 net_reconcile_tick 形态③④(100ms)决定
N->>C: net_listener_try → net_relisten_locked
N->>C: net_listener_request(登记)→ net_listener_tick → net_relisten_locked
opt 沉降期内(TRAP-NET-18)
N->>C: 排空 RX 残渣(256B 分块)+ close + 排空中断
C-->>N: 未到 CLOSED/TCP_CLOSED → 返回(30ms 后重试轮询复检)
end
N->>C: set_proto_type + set_sour_port + open_socket + tcp_listen
alt open 失败(TRAP-NET-09 沉降)
N->>N: 登记重试(30ms 节奏 / 2s 时限)→ net_listener_retry_tick 到点重试
N->>N: 登记重试(30ms 节奏 / 2s 时限)→ net_listener_tick 到点重试
else 成功
N->>N: state = LISTENING(auto_relisten 保留)
N-->>A: xTaskNotify(accept 返回 / recv 返回 0)
@@ -430,7 +430,7 @@ sequenceDiagram
| 想改什么 | 先看哪些函数 |
|---|---|
| 监听恢复策略(节奏/时限/沉降) | `net_listener_try` / `net_relisten_locked` / `net_listener_retry_tick`;常量 `NET_RELISTEN_*_MS`(`net_listener.c` 内);重建决策在 `net_reconcile_tick` 形态③④(下行) |
| 监听恢复策略(节奏/时限/沉降) | `net_listener_request` / `net_relisten_locked` / `net_listener_tick`;常量 `NET_RELISTEN_*_MS`(`net_listener.c` 内);重建决策在 `net_reconcile_tick` 形态③④(下行) |
| 阻塞接收等待语义 | `net_pending_register` / `net_pending_tick` / `net_pending_complete`;常量 `NET_PENDING_WATCHDOG_MS` |
| 阻塞发送等待语义(二期 SEND pending) | `net_send_pending_register` / `net_send_pending_tick` / `net_send_pending_complete`;常量 `NET_SEND_SPIN_MS`(有界自旋预算)/ `NET_SEND_TIMEOUT_MS`(无进展超时)/ `NET_SEND_CHUNK_MAX`(分块上限,`net_pending.h` 唯一定义处) |
| 事件未收到自愈 + 监听重建策略(合一 tick,自愈收敛阶段 3) | `net_reconcile_tick`(`NET_RECONCILE_PERIOD_MS` / `NET_RECONCILE_COOLDOWN_MS` / `NET_RECON_STABLE_ACCEPT|RELISTEN|DEATH`,①②③④⑤+U11,唯一重建入口=形态③④)+ `handle_disconnect_event` / `handle_connect_event` |
+18 -17
View File
@@ -80,7 +80,7 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线
| 区域 | 内容 |
|---|---|
| **左栏 · 调用方任务** | `net_recv` → 判"数据已在?" → **是**走快速路径;**否**(`timeout > 0`)登记挂起 recv 后在自己上下文睡任务通知。`net_send` 同构:快路径短暂自旋写块,超预算则登记挂起 send 后睡 |
| **中栏 · netTask 主循环** | ~1ms 节拍:`net_poll`(刷新 `readable` + 全量刷新驱动状态缓存)→ 4 个非阻塞 tick(`net_listener_retry_tick` D10 → `net_pending_tick` 结束挂起 recv → `net_send_pending_tick` 推进/结束挂起 send → `net_reconcile_tick` 对账 + 监听重建唯一入口)→ 排空消息队列 |
| **中栏 · netTask 主循环** | ~1ms 节拍:`net_poll`(刷新 `readable` + 全量刷新驱动状态缓存)→ 4 个非阻塞 tick(`net_listener_tick` D10 → `net_pending_tick` 结束挂起 recv → `net_send_pending_tick` 推进/结束挂起 send → `net_reconcile_tick` 对账 + 监听重建唯一入口)→ 排空消息队列 |
| **下带 · 判定** | recv 每轮按序判定:数据到达 / 到点(仅显式预算)/ EOF / 同 fd 后续操作 / 网络重建(**五路**);send 每轮按序判定:断连 / 推进 / 写完 / 无进展超时(**四路**);挂起超时留痕探针(编译期选项,默认关,见 §12)只留痕 |
| **回环** | 判定完成经 `xTaskNotify` 唤醒调用方(结果 = 字节数 / 负错误码) |
@@ -130,7 +130,7 @@ task_wd_beat() → 处理 SYS_EVT_NET_RECONFIG → net_poll() → net_process_me
| 顺序 | tick | 职责 | 与挂起模型的关系 |
|---|---|---|---|
| ① | `net_listener_retry_tick()` | D10 重监听重试(30ms/2s) | 跳过有在途挂起 recv / send 的 fd(§7.2 集成点 8 / 20) |
| ① | `net_listener_tick()` | D10 重监听重试(30ms/2s) | 跳过有在途挂起 recv / send 的 fd(§7.2 集成点 8 / 20) |
| ② | `net_pending_tick(s_net_socks)` | **结束挂起 recv**(数据/到点/断连) | recv 的主完成检查入口 |
| ③ | `net_send_pending_tick(s_net_socks)` | **推进/结束挂起 send**(断连/推进/写完/超时) | send 的主推进入口(二期) |
| ④ | `net_reconcile_tick()` | 100ms 对账 + 监听重建唯一入口 | 形态⑤(芯片静默回收)取消本 fd 挂起 recv / send(§7.2 集成点 7 / 17) |
@@ -150,7 +150,7 @@ task_wd_beat() → 处理 SYS_EVT_NET_RECONFIG → net_poll() → net_process_me
| 监听生命周期的模块化边界(`net_listener.c` 拆分依据) | `docs/网络/网络自愈架构设计.md` §11 |
交叉点只有两处:`net_reconcile_tick` 形态⑤ 取消挂起(§5.2 第 5 路)、
`net_listener_retry_tick` 的挂起 recv 保险(§7.2 集成点 8)。
`net_listener_tick` 的挂起 recv 保险(§7.2 集成点 8)。
---
@@ -241,7 +241,7 @@ flowchart TD
`net_close_locked` 对 `net_sock_t` 做**整块 `memset`**。若挂起状态置于其中,`close` 会
静默抹掉 pending——调用方永远等不到通知,**永久挂死**。独立表把"跨时间状态"与
socket 生命周期解耦,与 `net_listener_retry_tick` 的重试表同构。
socket 生命周期解耦,与 `net_listener_tick` 的重试表同构。
### 4.3 登记守卫(两条硬防线,recv / send 共用)
@@ -338,12 +338,13 @@ send 是"分块推进直到全部写完"(四路 + 显式取消)。两表可
⚠ **判据不可收窄**:只判 `CLOSED` 时,抢先 relisten 后第 1 路(要求 `ESTABLISHED`)与
第 3 路(要求 `CLOSED`)两头落空,挂起只能干等第 2 路;无限等待登记没有第 2 路,
**漏判 = 永久挂死**(留痕探针只留痕、不唤醒)。
- 动作:查芯片残留 → 有则按数据完成;无则内层 `state == CLOSED && auto_relisten` 时
`net_listener_try()` 即时重建,随后按 `released_abnormal` 决定 `CONNRESET`(异常释放)
或 `0`(正常 FIN)。
- ⚠ 内层 `state == CLOSED` 门是**必需的**,不是冗余:`net_listener_try` 无状态门,
对已 `LISTENING` 的 fd 再 `open+listen` 会因 `USE` 错误走进关闭分支、关掉正听着的 Socket。
- 该处**保留即时重建**(而非只交 reconciler):它只在 `p->active != 0`(有挂起 recv)时
- 动作:查芯片残留 → 有则按数据完成;无则 `auto_relisten` 时 `net_listener_request()`
**登记**重建(动作在下一轮 `net_listener_tick`,≤1ms),随后按 `released_abnormal` 决定
`CONNRESET`(异常释放)或 `0`(正常 FIN)。
- ⚠ 原内层 `state == CLOSED` 门已**上收到 `net_listener_tick`**(E17 单一所有者收口):
它防的是对已 `LISTENING` 的 fd 再 `open+listen` → `USE` 错误走进关闭分支、关掉正听着的
Socket(U12 同源);这是**执行侧安全不变量**,不再由每个登记点各自复述(漏一处即事故)。
- 该处**保留即时登记**(而非只交 reconciler):它只在 `p->active != 0`(有挂起 recv)时
到达,属"消费者在等这条连接结局"的**消费者驱动路径**;删掉会把 CLOSED 窗口拉长到
reconciler 周期 + 隔离期(~100ms),期间调用方高频 `net_accept` → `[WD] task stall`。
@@ -419,7 +420,7 @@ recv 与 send 各一组(send 组前缀 `net_send_pending_`),共 14 个函
| `net_pending_cancel(fd, err)` | netTask | — | — | 经 `complete` 通知 | `net_reconcile_tick` 形态⑤ |
| `net_pending_cancel_all(err)` | netTask | 芯片复位之前 | — | 逐 fd 通知 | `net_init` 顶部 |
| `net_pending_settle_close(fd, p_sock)` | netTask | `ch395f_close_socket` 之前 | — | 排空或取消 + 通知 | `net_close_locked` / `net_drop_conn_locked` 首行 |
| `net_pending_active(fd)` | **任意**(纯 RAM 读) | — | `1`/`0` | 无 | `net_listener_retry_tick` 挂起保险 |
| `net_pending_active(fd)` | **任意**(纯 RAM 读) | — | `1`/`0` | 无 | `net_listener_tick` 挂起保险 |
**send 组**(与 recv 组一一对称):
@@ -431,7 +432,7 @@ recv 与 send 各一组(send 组前缀 `net_send_pending_`),共 14 个函
| `net_send_pending_clear(fd)` | **任意** | — | — | 静默清槽(不通知) | `alloc_socket` / `net_close_locked` 尾部 |
| `net_send_pending_cancel(fd, err)` | netTask | — | — | 经 `complete` 通知(结果 = 已发字节数) | `net_close_locked` / `net_drop_conn_locked` / `net_reconcile_tick` 形态⑤ |
| `net_send_pending_cancel_all(err)` | netTask | 芯片复位之前 | — | 逐 fd 通知 | `net_init` 顶部 |
| `net_send_pending_active(fd)` | **任意**(纯 RAM 读) | — | `1`/`0` | 无 | `net_listener_retry_tick` 挂起 send 保险 |
| `net_send_pending_active(fd)` | **任意**(纯 RAM 读) | — | `1`/`0` | 无 | `net_listener_tick` 挂起 send 保险 |
> `net_pending_complete` / `net_send_pending_complete` 是各自方向**唯一**的通知出口,
> `clear` 是**唯一**静默清槽出口——任何新增路径必须二选一,不得自行写
@@ -487,7 +488,7 @@ recv 与 send 各一组(send 组前缀 `net_send_pending_`),共 14 个函
| 5 | 关闭结算 | `net_socket.c` `net_close_locked` 首行 | `net_pending_settle_close` | close 丢已到数据 / 调用方挂死 |
| 6 | 拆会话结算 | `net_socket.c` `net_drop_conn_locked` 首行 | `net_pending_settle_close` | 同上 |
| 7 | 静默回收取消 | `net_socket.c` `net_reconcile_tick` 形态⑤ | `net_pending_cancel(i, NET_ERR_CONNRESET)` | 芯片静默回收后挂起 recv 无唤醒源 |
| 8 | 重开 Socket 保险 | `net_listener.c` `net_listener_retry_tick` | `net_pending_active(i)` 命中则 `continue` | 重开 Socket **摧毁在途连接** |
| 8 | 重开 Socket 保险 | `net_listener.c` `net_listener_tick` | `net_pending_active(i)` 命中则 `continue` | 重开 Socket **摧毁在途连接** |
| 9 | 新 fd 清槽 | `net_socket.c` `alloc_socket` | `net_pending_clear(sockfd)` | fd 复用继承旧挂起 → 新会话立即 EOF / 结果错位 |
| 10 | close 幂等清槽 | `net_socket.c` `net_close_locked` 尾部 | `net_pending_clear(sockfd)` | 槽位残留(虽经 settle 已通知,仍须归零) |
| 11 | 唤醒通道(两段式) | `net_socket.c` `net_wait_result` | `xTaskNotifyWait` 零等待 + 阻塞 | 通知丢失 → 调用方挂死 |
@@ -504,7 +505,7 @@ recv 与 send 各一组(send 组前缀 `net_send_pending_`),共 14 个函
| 17 | 静默回收取消 | `net_socket.c` `net_reconcile_tick` 形态⑤ | `net_send_pending_cancel(i, NET_ERR_CONNRESET)` | 芯片静默回收后挂起 send 无唤醒源 |
| 18 | 重建前取消 | `net_socket.c` `net_init` 顶部(芯片复位前) | `net_send_pending_cancel_all(NET_ERR_CONNRESET)` | 重建前挂起的调用方**永久挂死** |
| 19 | 新 fd / close 清槽 | `net_socket.c` `alloc_socket` / `net_close_locked` 尾部 | `net_send_pending_clear(sockfd)` | fd 复用继承旧挂起 send → 新会话误写旧缓冲 |
| 20 | send 重开保险 | `net_listener.c` `net_listener_retry_tick` | `net_send_pending_active(i)` 命中则 `continue` | 重开 Socket **摧毁在途传输**(半写的数据流被截断) |
| 20 | send 重开保险 | `net_listener.c` `net_listener_tick` | `net_send_pending_active(i)` 命中则 `continue` | 重开 Socket **摧毁在途传输**(半写的数据流被截断) |
### 7.2 逐集成点要点
@@ -523,7 +524,7 @@ net_pending_capture_owner() ← 捕获执行者
**3–4 / 14–15. `net_process_messages` 开头与分派**:
```text
net_listener_retry_tick() ← D10
net_listener_tick() ← D10
net_pending_tick(s_net_socks) ← 挂起 recv 完成检查
net_send_pending_tick(s_net_socks) ← 挂起 send 推进/完成检查
net_reconcile_tick() ← 对账 + 重建
@@ -539,7 +540,7 @@ while (osMessageQueueGet(..., 0) == osOK) { ... }
`net_pending_settle_close(...)`——"关闭不得让等待中的调用方永久悬挂",且"关闭不丢已到达
数据(芯片缓冲在 close 前可读)"。
**8 / 20. 挂起保险(recv 与 send)**:`net_listener_retry_tick` 对每个待重试 fd,先做状态门
**8 / 20. 挂起保险(recv 与 send)**:`net_listener_tick` 对每个待重试 fd,先做状态门
(仅 `LISTENING`/`CLOSED` 才重试),再 `net_pending_active(i) || net_send_pending_active(i)`
命中即 `continue`——重开 Socket 会摧毁在途连接(recv)与在途传输(send 的半写数据流)。
@@ -754,5 +755,5 @@ while (osMessageQueueGet(..., 0) == osOK) { ... }
| `Drivers/BSP/NET/net_socket.c` | `net_send_locked` / `net_send` / `net_send_sock` | 快速路径(有界自旋)与挂起 send 登记入口 |
| `Drivers/BSP/NET/net_socket.c` | `net_process_messages` / `net_wait_result` | 分派(含 `NET_MSG_SEND` 透传 `caller`)、哨兵抑制、两段式唤醒 |
| `Drivers/BSP/NET/net_socket.c` | `net_close_locked` / `net_drop_conn_locked` / `alloc_socket` / `net_init` | 结算、清槽、取消、捕获执行者(recv + send 双侧) |
| `Drivers/BSP/NET/net_listener.c` | `net_listener_retry_tick` | 消费 `net_pending_active` / `net_send_pending_active` 作挂起保险 |
| `Drivers/BSP/NET/net_listener.c` | `net_listener_tick` | 消费 `net_pending_active` / `net_send_pending_active` 作挂起保险 |
| `App/task/net_task.c` | `net_task_func` | 主循环节拍(`net_poll` → `net_process_messages`) |
+78 -27
View File
@@ -98,7 +98,7 @@ EOF 只在 **recv 那一刻**本层为 CLOSED 才给出(`net_recv_locked`,`D
`net_accept_locked` CLOSED 分支、`net_recv_locked`、`net_pending_tick`、`NET_MSG_RELISTEN`、
`net_resync_listen`、`net_listen_locked`)+ 应用层 4 处(lftpd、time_sync、ui_net_sink、net_task 代次);
状态迁移点 15 个,其中 **15 个"无人负责"组合**(U1~U15,见 `docs/问题记录/CH395F_Trap_Records.md` B10 行)。
收敛后周期性决策已唯一化为 §4.1 形态③④、消费者驱动即时重建按 §4.2 保留(3 处)——
收敛后周期性决策已唯一化为 §4.1 形态③④、消费者驱动即时登记按 §4.2 保留(3 处)——
上段是欠债的**原始形状**,保留以说明"为什么需要 I2"。
三个已实测的派生失效(机理保留,供判读 I2/I3 被违反时的症状):
@@ -149,7 +149,7 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要
| 层 | 持有什么 | 关键接口 |
|---|---|---|
| 驱动层 `Drivers/BSP/CH395F/ch395f.c/.h` | 每 Socket 状态缓存 + 类型化回读/谓词;每 Socket 事件读即清护栏;复合复位状态机;命令完成判据(状态回读);链路态缓存 | `ch395f_sock_state_cached` / `ch395f_sock_events` / `ch395f_sock_reset_begin` / `ch395f_sock_reset_poll` / `ch395f_cmd_done` / `ch395f_glob_event_feed` / `ch395f_link_up_cached` |
| net 层 `net_socket.c` / `net_listener.c`(netTask 单所有者) | fd↔Socket 映射、消息队列串行化、L3 pending recv、事件分发与回调;会话投影与粘滞电平;单一 reconciler;监听生命周期(armed/D10/settle);同步复位包装 | `net_poll` / `net_reconcile_tick` / `net_listener_try` / `net_listener_settle` / `net_listener_retry_tick` / `net_sock_reset_sync` / `net_drop_conn` |
| net 层 `net_socket.c` / `net_listener.c`(netTask 单所有者) | fd↔Socket 映射、消息队列串行化、L3 pending recv、事件分发与回调;会话投影与粘滞电平;单一 reconciler;监听生命周期(armed/D10/settle);同步复位包装 | `net_poll` / `net_reconcile_tick` / `net_listener_request` / `net_listener_settle` / `net_listener_tick` / `net_sock_reset_sync` / `net_drop_conn` |
| 应用/服务层(`ui_net_sink` / `iec104_serv` / `lftpd` / `time_sync`) | 只管 fd 与数据;放弃会话走 `net_drop_conn`;EOF 不得 `net_close` 监听 fd | — |
### 3.2 主循环自愈三 tick
@@ -157,7 +157,7 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要
| tick | 节奏 | 职责 |
|---|---|---|
| `net_poll` | 主循环每轮(≈1ms) | INT# 引脚 → 全局事件汇聚(唯一读者)→ SINT 分派 → **驱动缓存全量刷新** → 链路位喂驱动 → 步进 `ch395f_sock_reset_poll` |
| `net_process_messages` | 每轮 | RPC 消息处理;开头每轮 `net_pending_tick`(挂起 recv 结束)+ `net_listener_retry_tick`(D10) |
| `net_process_messages` | 每轮 | RPC 消息处理;开头每轮 `net_pending_tick`(挂起 recv 结束)+ `net_listener_tick`(D10) |
| `net_reconcile_tick` | 100ms | 唯一周期对账 + 重建(§4.1) |
### 3.3 状态模型(会话投影)
@@ -183,12 +183,20 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要
|---|---|---|---|
| ① | ESTABLISHED ↔ 芯片 CLOSED / TCP_CLOSED | 复用 `handle_disconnect_event`(置事实 + GRACEFUL 沉降) | **4 轮**(≈400ms,M3 让位窗) |
| ② | LISTENING ↔ 芯片已握手(TCP=ESTABLISHED) | `GET_REMOT_IPP` 幽灵甄别 → 补 accept(`handle_connect_event`) | **1 轮**(E2:LISTEN 可见 ≤10ms 无长尾) |
| ③ | 监听型 fd 而芯片不在 LISTEN | `net_listener_try` 重建(走 I2 唯一入口) | 2 轮 |
| ③ | 监听型 fd 而芯片不在 LISTEN | 登记重建请求(动作在 I2 唯一执行者 `net_listener_tick`) | 2 轮 |
| ④ | 本层 CLOSED(auto_relisten)而芯片**正在** LISTEN(U11) | 只重同步本层为 LISTENING,**零芯片命令** | 2 轮 |
| ⑤ | ESTABLISHED ↔ 芯片已回 LISTEN(静默回收,F-NET-2) | RESYNC 语义只修本层(转登端口 + 取消挂起 recv),零命令 | 2 轮 |
表注:形态② 命中时的对端端口回读是本 tick 唯一按需 SPI。
- **为什么 ①③⑤ 常态 0 命中**(E18 负向注入打靶三轮,2026-10-14):消费者驱动的 pending
recv / recv EOF 比 100 ms 对账**更快**,多数场景先把本层收口成 CLOSED → 由形态④ 接手。
①③⑤ 的职责边界是**"没有人在等"的场景**,与 ④ 互补而非重复。三条都已主动打中并确认救回:
① 吞掉 DISCONNECT 后 **4/4 救回**;③ 空闲监听器被静默 close,两轮独立复现;⑤ 开盲区吞掉
芯片自发中断 + 把芯片收回 LISTEN 后命中 `-> resync`(零芯片命令、挂起 recv 被取消、端口
立刻可复连)。→ **三条均不可降级为只留痕**;⑤ 的"取消挂起 recv/send"尤其不可省,
否则消费者在死连接上干等到 deadline。
- **M3 可疑门**(`handle_disconnect_event` ESTABLISHED 分支):芯片 `0x05/0x00`
(Socket OPEN 但 TCP 槽 0x00——活连接应为 0x04,逻辑矛盾态)且 `lost_conn_seen==0`
且 `recv_bytes==0` 且**芯片 RX 有在途数据**(不可毁的消费者资产,与 closed-with-data
@@ -198,23 +206,39 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要
- 沿革:形态③④ = 原 N-32 `net_listener_pump`(阶段 3 并入);形态⑤ = 原
`net_resync_listen` API 语义(阶段 1 删 API 并入)。
### 4.2 监听生命周期
### 4.2 监听生命周期(单一所有者:登记 / 执行分离)
- **`auto_relisten`**:`net_listen` 成功即置位;所有重建路径的门控标志(I4)。
- **`net_listener_armed`**:唯一监听门控。⚠ **必须放行 ESTABLISHED**——它是单连接模型
的"待 accept"电平;漏放行时 i104 的 200ms 轮询抢不过形态② 去抖而整场饿死
(E10 实测 18/18 FAIL → 修复后 18/18 PASS,127s→8.3s)。
- **D10 `net_listener_retry_tick`**:30ms 间隔 / 2s 预算的非阻塞重试;L3 保险
(同 fd 有在途挂起 recv 则跳过);"预算耗尽"不是终态——reconciler 形态③④ 周期接管。
- **消费者驱动即时重建 ×3**(accept CLOSED 分支 / recv EOF / pending③):与周期兜底
- **登记侧 `net_listener_request(fd, reason)`**:只置意图(`reason` 仅用于留痕归因),
**不碰芯片**。登记点 5 处:reconciler 形态③④ / accept CLOSED 分支 / recv EOF /
`net_drop_conn` / 挂起 recv 断连(pending③)。已在请求中**不重置重试时刻**(防饥饿)。
- **执行侧 `net_listener_tick()`**(netTask 每轮;原名 `net_listener_retry_tick`,只做
D10 到点重试):**唯一**调用 `net_relisten_locked` 的地方,门集集中一处——
控制块有效 + `in_use` + 状态 ∈ {LISTENING, CLOSED} + 监听型(`auto_relisten` /
STREAM / `local_port≠0`)+ **芯片已在 LISTEN 即视为请求已达成、静默了结**
(U12 的结构性防护,读驱动状态缓存零 SPI)+ 挂起保险(L3)+ 30ms 节奏 / 2s 预算
(D10)。"预算耗尽"不是终态——reconciler 形态③④ 周期重新登记。
⚠ 收口前是 `net_listener_try`(当场动作、**无状态门**),故 4 个调用点各自补
`state == CLOSED` 门——漏一处即 U12 事故形态;门集上收后调用点不再需要(E17)。
- **消费者驱动即时登记 ×3**(accept CLOSED 分支 / recv EOF / pending③):与周期兜底
构成双层——**不可删**(P2a 实测:删消费者驱动 → CLOSED 窗口拉长 → `[WD] task stall`)。
收口后动作延后一轮(≤1ms)执行,恢复窗 p50 199~208ms 与收口前持平(E17)。
- `net_listener_settle(fd, mode)`:登记"会话已结束,重建前需沉降/隔离"——**只置事实**,
动作交形态③④ / `net_listener_try`(I2)。
动作只在 `net_listener_tick` 发生(I2)。
### 4.3 沉降与排渣隔离(TRAP-NET-18)
- 两档:**GRACEFUL 100ms 底**(正常释放)/ **QUARANTINE 400ms 底**(异常释放:芯片
TIMEOUT、FTP ABORT);800ms 强制放行(必须 < D10 的 2s 时限)。
TIMEOUT、FTP ABORT);**800ms 上限只留痕、不放行**(E19 反事实 A/B → 2026-10-14 降级)。
- **800ms 上限为什么不再"强制放行"**(E19 实测):芯片未回 CLOSED 时强行 `open` 注定吃
USE 错(TRAP-NET-09),放行与不放行的端口恢复时刻**同一分布**(放行 +512 / 不放行
+505·533 / 降级后实测 +509 ms)——恢复由"芯片何时回 CLOSED"决定。**"监听器不会永久失效"
的真正兜底 = D10 预算到期 + 对账形态④ 重新登记**,不是放行;勿再把本处当兜底。
降级后上限到点只打**一行** `keep waiting` 且**隔离标志不清**(下一轮继续排空残渣);
若每轮都留痕会 30ms 刷两行(构建 B 实测踩过,故加 `s_relisten_capped` 只留首末)。
- **隔离期持续排空 RX**:中止后残渣(实测 rxdrain=2048 = 芯片单 Socket RX 缓冲满容,
E4)会被下一条连接读走 → 隔离期边等沉降窗边排空。
- **GRACEFUL 无条件化**(M1,F-NET-15):旧 `recv_bytes≠0` 门是背靠背失效循环的放大器
@@ -249,6 +273,10 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要
→ 重读 PHY 更新 `link_up` 缓存 + 变化计数;reconciler 订阅留痕(运行期拔网线不再静默)。
- net 层同步包装 `net_sock_reset_sync`(listen/connect open 失败重试、close 快路径)
——忙等统一,close/connect 路径不再读清全局寄存器(I7)。
- **⚠ 监听重建(relisten)尚未接进本原语(E16)**:现序列 = 一次 CLOSE + CLOSE 前排空 +
等 CLOSED,**不等价**于 net 层旧隔离序列的"隔离窗内持续排空/关闭"(残渣在 CLOSE 之后
才落进 RX),也不与"同一芯片 Socket 被上层立即复用"共存(复位机会拆掉刚建的监听)。
承接前置条件与 A/B 实测数据见 §8 延后项 1;在此之前 `net_listener.c` 保留自建序列。
### 4.5 recv 语义与 pending(阶段 1.5)
@@ -267,7 +295,7 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要
### 4.6 `net_drop_conn`——应用主动拆会话保监听(E-02 收口)
- 语义:`net_pending_settle_close` 先结束挂起 recv(回传结果 + 唤醒调用方)→ `net_sock_reset_sync(QUARANTINE)`
→ 本层收口 CLOSED(不清 fd)→ `auto_relisten` 即时 `net_listener_try`
→ 本层收口 CLOSED(不清 fd)→ `auto_relisten` 即时登记重建请求(`net_listener_tick` 执行)
(失败交 D10 + 形态④)。
- **幂等**(非 ESTABLISHED/CONNECTING 直接返 0);**EOF 路径不得调**
(`auto_relisten` 已接管,再调属重复拆)。
@@ -340,7 +368,7 @@ Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少
| 契约要求 | 对应缺口 | 收口手段(现状) |
|---|---|---|
| 「会话结束」必须**谁都能看见**,不能只在边沿那一刻 | EOF 边沿化:抢先 relisten → recv 永久 `WOULDBLOCK` | 不变量 I:`net_sock_t.session_dead` 粘滞电平,`net_recv_locked` 判据 `state==CLOSED \|\| session_dead`(§3.3) |
| 关闭 Socket 必须**一定有人做**、且只做一次 | 重建所有者 12 个 + U12 的 LISTEN 失败不回 close | 不变量 II:重建决策唯一化为 `net_reconcile_tick` 形态③④ + `net_listener_try`(§4.1 / §4.2) |
| 关闭 Socket 必须**一定有人做**、且只做一次 | 重建所有者 12 个 + U12 的 LISTEN 失败不回 close | 不变量 II:重建**判据**唯一化 = `net_reconcile_tick` 形态③④;重建**动作**唯一化 = `net_listener_tick`(§4.1 / §4.2,E17) |
| 关闭姿势必须正确 | TRAP-NET-12:陈旧 DISCONNECT 锁存秒断下一条连接 | `sock_reset` 序列 = 排 RX → CLOSE → 排事件 → 等 `byte0=00H`(§4.4) |
| 异常释放须先排空残渣再关 | TRAP-NET-18:~2KB RX 残渣被下条连接当自己的数据读走 | 隔离期持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED`(§4.3) |
@@ -389,6 +417,10 @@ Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少
| **E13** | 产品固件 + PC 网卡抖动注入,尝试确定性复现 E8/E12 记录的"残渣风暴态" | 根因 = **僵尸会话**(对端静默死亡、芯片侧 ESTABLISHED 未清、单连接监听被占),而非跨烧录残渣;`RSTI←NRST` 推翻"跨烧录存活";自愈链端到端 30~90 s 有效 | §4.7;PASV 预清缺口由 `sock_reset` 收口 |
| **E14** | 48 轮直读 SINT 矩阵复核"丢中断"说法(drain-int 现场抓取 + SINT 补读 ×3 + 超时归类) | **芯片侧事件丢失 = 0**;"丢失"实为 drain 读清消费 3/48 + 门控竞态 2/48 + 主机同步 1/48;幽灵态本轮 0/48 未复现 | §1 事实 2 改写;生产 listen/relisten 排事件挪到 LISTEN 命令**之前**(根除自伤窗);TRAP-NET-17/-20 归因更正(机制保留不变) |
| **E15** | 幽灵态归因复核(WCH 官网 + 手册出处) | `0x04` + 旧端口 = 芯片**如实报告**旧连接拆链进行中,不是谎报:bit3=1 是自家配置(手册 §5.52②);拆链走 RFC 9293 §3.3.2 状态机(FIN_WAIT_1→FIN_WAIT_2→TIME_WAIT,2MSL 驻留);拆链窗实测 >2 s 与手册 §5.17 默认重传 12 次耗尽 ≥3 s 同量级;WCH 官方博客确认"断连即重连被拒"、官方方案 = 关闭完成后重开 | TRAP-NET-20 口径"谎报"→"如实报告";settle 等待 = 官方方案;幽灵甄别 = 强制放行路径的护栏;I3 就此加强 |
| **E16** | 把 relisten 隔离段收口到驱动 `sock_reset` 原语(删 net 层自建的排空/关闭/等待序列与 100/400 ms 参数副本),同一块板 A/B 跑 `ftp_abort_probe`(RST 10 + FIN 5)+ `relisten_stress`(:7003 300ms×10、:21 100ms×50)+ `n30_repro` 30 轮;失败后两轮补救:① 原语加"沉降窗后二次排空 RX" ② `net_sock_reset_sync` 改"已在运行=加入等待" | **收口失败、已回退**:原语版 RST **0/10**、FIN **0/5**(`followup` SHA 不匹配 = 首笔复连仍被截断),并新增 `sock_reset_sync timeout (600ms)` 与 `close: sock_reset not done` 两条 `[ERR]`;同场原序列 **10/10 + 5/5**、恢复窗 p50 **208 ms**(n=30,min 177 / max 280)。快复连与背靠背腿原语版也全过(50/50、10/10、30/30)→ **失效面精确落在"中止后首笔复连"**。根因两条:残渣在 CLOSE 之后才落进 RX(一次排空接不住、二次排空仍不等价于"每 30 ms 持续 CLOSE+排空");复位机"逼回 CLOSED"的目标与上层立即复用同一 Socket 冲突 | §8 延后项 1 改写为受阻 + 三条前置语义;§4.4 加承接警告;登记 **N-30** |
| **E17** | **单一所有者收口**(与 E16 相反方向的简化):`net_listener_try`(当场动作、**无状态门**,4 个调用点各自补 `state == CLOSED`)改为 `net_listener_request`(只登记意图)+ `net_listener_tick`(唯一执行者,门集含"芯片已在 LISTEN 即了结");顺带把沉降/重试留痕改为只留首末。同一块板全量回归 | `ftp_abort_probe` RST **10/10** + FIN **5/5**;`relisten_stress` :7003 10/10(p50 25 ms)、:21 50/50(p50 509 ms);`n30_repro` **30/30**;`e02_probe` queue/listener 双 PASS(恢复 513 ms ≈ 基线 500 ms);恢复窗 p50 **199~208 ms**(n=108,与 E16 基线 208 ms 持平)→ **延后一轮(≤1 ms)执行不伤即时性**;relisten 留痕 **5.4 → 2.5 行/次恢复**;`Code` +8 B | §4.2 改为"登记 / 执行分离";§9 事实表与 §11 API 表同步;`net_listener_try` 一名退役 |
| **E18** | **负向注入打靶 · 三轮**(任务分支 `task/N31-neg-inject`,注入代码 +232 行、`NET_FAULT_INJECT` 编译开关、**不入库**):① 吞掉 DISCONNECT(:2404,在 i104 的 200 ms accept 轮询窗内 RST → "芯片 CLOSED / 本层 ESTABLISHED / 无人在 recv");③ 绕过本层簿记直接 close 芯片监听(:7003 空闲监听器);⑤ 开盲区吞掉芯片自发的中断 + 等芯片真 CLOSED 再 open+listen + 确认落到 `0x05/0x01`(:8888,PC 侧 RST 打断 `FIN_WAIT_2`) | **三条全部命中**:① 吞事件后 `reconcile: chip closed -> disconnect` → `auto-relisten ok`,**4/4 救回**、端口 5/5 复验;③ 两轮独立复现 `was state=3` → `auto-relisten ok (recon)`;⑤ `reclaim DONE chip=0x05/0x01` → `chip re-listened w/o DISCONNECT -> resync` → 本层回 LISTENING,零芯片命令、端口立刻可复连、无 `task stall` | **①③⑤ 均不可降级为只留痕**:它们只在"没有人在等"时才有活干(pending/EOF 比 100 ms 对账快,常态先把状态收口成 ④ → **互补而非重复**);⑤ 的**取消挂起 recv/send** 尤其不可省,否则消费者在死连接上干等到 deadline。顺带量出"为什么常态打不中":①⑤ 的前提会被更快的消费者路径消解,⑤ 还要先越过芯片 `FIN_WAIT_2`(E15 的 2MSL 长尾,实测 972 轮 ≈1 s) |
| **E19** | **`force open` 反事实 A/B**(同一注入场景:本层已收口 CLOSED + 芯片停 `FIN_WAIT_2` `0x05/0x06` + **对端挂住不关** = E-02 场景;构建 A = 现状放行,构建 B = `NET_FORCE_OPEN_COUNTERFACTUAL` 只留痕不放行) | **800 ms 上限首次被打中**:`relisten quarantine timeout, force open (sock=0x05 tcp=0x06)`。A:对端挂住期间端口 **0/4** 可连、对端关闭后 **+512 ms** 恢复;B:同样 **0/4**、**+505 / +533 ms** 恢复 → **放行与不放行同一分布**;B 另暴露"抑制后每 30 ms 刷两行"的日志风暴(放行顺带关掉了沉降窗) | **`force open` 的动作在全部可产现场景内无可测收益**:端口恢复时刻由"芯片何时回 CLOSED"决定,真正兜住"监听器永久失效"的是 **D10 预算 + 形态④ 重新登记**(B 实测:抑制后仍持续重试并恢复)→ "防永久失效"的归属应改正。两条实测坑:① 本层直接改 `state` **不会**进沉降窗,窗只由 `net_listener_settle` 开启(注入踩过一次,导致连打三轮 0 命中);② `[ERR] relisten retry budget exhausted` 在此**被稳定复现** = 对端不配合拆链的签名,非回归。**决策已落地**:`force open` 降级为只留痕(`keep waiting` + `s_relisten_capped` 只留首末、隔离标志不清→继续排渣);真机注入复验 **+509 ms** 与 A/B 同分布,回归 RST **10/10**、FIN **5/5**、`relisten_stress` :21 **50/50**(p50 507)、`n30_repro` **30/30**,netTask 静态最坏链 1920→**1912 B** |
| **E-02**(审计编号) | `test/host/e02_probe.py` 台架定案 `net_drop_conn` 收口后的时序(阶段 3 产品固件) | 对端配合(收到 FIN 后其内核 close):`TIME_WAIT` ~150 ms → 形态④ relisten,恢复全程 ~500 ms;对端内核存活但不 close:芯片停 `FIN_WAIT_2` 长尾,**监听恢复受限于对端 close** | §4.6 |
## 6. 红线(明确不做的事)
@@ -396,7 +428,7 @@ Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少
1. **不改 EXTI/ISR**:全局事件寄存器整体读即清,ISR 读寄存器会把事件从 netTask 抢走
——正是 B3 事故形状。
2. **不启用多连接模式(FUN_PARA bit1)**:不支持多监听(A′ 出局);监听常驻只能软件承诺。
3. **不删消费者驱动即时重建 ×3**(P2a 实测删除 → `[WD] task stall`)。
3. **不删消费者驱动即时登记 ×3**(P2a 实测删除 → `[WD] task stall`)。
4. **不改单连接复用模型**:硬 TCP 栈原生主流形态(ioLibrary 无 accept、FreeRTOS reuse
同形),不是对 BSD 的偏离。
5. **SPI 分频 8、中断经 `net_poll` 集中读清**:PCB 约束 + I7(护栏是收窄读法,不是改路径)。
@@ -426,11 +458,22 @@ Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少
## 8. 延后项
1. **relisten 沉降段统一**:`net_relisten_locked` 隔离段仍是手拼序列(排空 RX + 主动
close + 读本 Socket SINT + 轮询状态 + 30ms 重试轮),未统一改走 `sock_reset` 原语。
判定:它不是忙等、时序是生产已调优的(TRAP-NET-18),改造收益只是去重复代码——
待下一次触碰该热路径的任务顺路处理(原语已具备承接条件:二段 CLOSE、quiescent
排空、等级化最短等待)。
1. **relisten 沉降段统一(受阻,2026-10-14 实测,见 §5.2 E16)**:曾按本项把
`net_relisten_locked` 的隔离段改走 `ch395f_sock_reset_begin/poll`(删 net 层自建的
排空+关闭+等待序列与 100/400 ms 参数副本),**已回退**——`ftp_abort_probe` RST **0/10**、
FIN **0/5**(首笔复连仍被截断),并新增两条 `[ERR]`;同场原序列 **10/10 + 5/5**。
原语要能承接,先补三条语义(缺一即回归):
① **窗口内持续清**:残渣在 CLOSE **之后**才落进 RX,只在 CLOSE 前排一次接不住;
补"窗后二次排空"仍 0/10——旧序列每 30 ms 重试轮都 CLOSE + 排空 + 排事件,
整个隔离窗持续清,原语的"一次 CLOSE + 一次排空"与之不等价;
② **复用让位**:复位机的目标是把本 Socket 逼回 CLOSED,而 FTP 数据通道每笔传输都在
同一芯片 Socket 上重建监听;复位机未了结时它会去拆刚建的监听(WAIT_CLOSE 的
二段 CLOSE 补发),并让 `net_sock_reset_sync` 600 ms 超时 → `close` 跳过排渣、
`listen` 的 open 重试环退化成无等待空转;
③ **槽位可加入**:每 Socket 只有一个复位槽位,`ch395f_sock_reset_begin` 返 -1
(已在运行)不等于失败——同步包装必须**加入**已在跑的复位机,否则 `net_close`
走 "proceed anyway"、排渣被跳过。
补齐 ①②③ 并各自回归后再接;登记 **N-30**(`docs/任务规划/登记簿.md`)。
2. **部分读→再查 recv_len 定向 TC**:旧"部分读归零"机理未证实(与手册 §5.34 及
TC-NET-201 相反),注释约束已降级为历史观察——以定向用例结案(陷阱记录「台架纪律」
节已挂该待办)。
@@ -457,7 +500,7 @@ Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少
| `net_connect_locked` | **无状态门**(可在 LISTENING 上改成 CONNECTING) |
| `net_recv_locked` 快路径 | **无状态门** |
| `net_listener_armed` | 唯一监听门控;**必须放行 ESTABLISHED**(§4.2) |
| `net_listener_try` | **无状态门**——对已 LISTENING 的 fd 再 open+listen 会因 USE 错误走进关闭分支、**关掉正听着新 SYN 的 Socket**(U12 同源事故);故两处调用点须自补内层 `state == CLOSED` 门 |
| `net_listener_tick`(唯一执行者) | 门集**只在这一处**:状态 ∈ {LISTENING, CLOSED} + 监听型 + **芯片已在 LISTEN 即视为达成、绝不 open**(U12 结构性防护)。登记侧 `net_listener_request` 因此**不需要**状态门——旧 `net_listener_try` 当场动作且**无状态门**,对已 LISTENING 的 fd 再 open+listen 会因 USE 错误走进关闭分支、**关掉正听着新 SYN 的 Socket**(U12 同源事故),故当时 4 个调用点须各自补内层 `state == CLOSED` 门(漏一处即事故);E17 收口后该门上收,调用点不再复述 |
### 9.3 三个服务的契约差异(历史成因,现状已对齐)
@@ -490,7 +533,7 @@ Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少
| # | 判据 | 收敛前 | 目标 | 结果 |
|---|---|---|---|---|
| 1 | 模块外调用「重建监听」的**入口** | 8 处散点直调 + 2 处直写状态数组 | **单一入口 `net_listener_try`** | 4 个调用点(形态③ / accept CLOSED 分支 / recv EOF / `net_drop_conn`),无散点 |
| 1 | 模块外调用「重建监听」的**入口** | 8 处散点直调 + 2 处直写状态数组 | **单一入口 `net_listener_request`**(登记)+ **单一执行者 `net_listener_tick`**(动作) | 5 个登记点(形态③④ / accept CLOSED 分支 / recv EOF / `net_drop_conn` / 挂起 recv 断连),**0 处模块外执行**(E17) |
| 2 | `s_relisten_*` 静态状态被**模块外直接读写** | 2 处外部写 | **0** | 0(外部写改为 `net_listener_settle`) |
| 3 | 依赖方向 | 无 | 新模块 → `net_socket.c` **单向** | 达成(不反向 include) |
@@ -504,7 +547,7 @@ Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少
| 搬入 | 理由 |
|---|---|
| `net_relisten_locked` / `net_listener_try` / `net_listener_retry_tick` / `net_listener_clear` / `net_relisten_done` | 「维持 fd 处于 LISTEN 态」的全部机制 |
| `net_relisten_locked` / `net_listener_request` / `net_listener_tick` / `net_listener_clear` / `net_relisten_done` | 「维持 fd 处于 LISTEN 态」的全部机制 |
| `net_drain_chip_rx` | 重建前的排空(只被 relisten 调用) |
| 7 个 `s_relisten_*` 数组 + 7 个 `NET_RELISTEN_*` 常量 | 状态与策略私有化 |
| ~~`net_listener_pump`~~ | 原唯一的周期性重建决策者——**阶段 3 已删除**,决策并入 `net_reconcile_tick` 形态③④(§4.1) |
@@ -525,7 +568,7 @@ Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少
> 要写它们(转登幽灵甄别基准),`net_lost_connect_probe` 与对账②也要读写。
> **这是本次重构无法消除的残余耦合**(单连接模型的固有结果),明确登记而非假装不存在。
### 11.3 对外接口(4 函数 + 1 枚举,全部窄接口)
### 11.3 对外接口(4 函数 + 2 枚举,全部窄接口)
```c
/* net_listener.h —— 只被 net_socket.c include(内部头,不进公开 net_socket.h,故 sim 无依赖) */
@@ -535,10 +578,18 @@ typedef enum {
NET_LISTENER_SETTLE_GRACEFUL = 2, /* 正常释放(DISCONNECT/FIN):沉降 100ms */
} net_listener_settle_t;
void net_listener_settle(int sockfd, net_listener_settle_t level); /* 只置事实,不动作 */
int net_listener_try(int sockfd); /* 尝试重建一次(不阻塞);已在重试队列返 -1 */
void net_listener_clear(int sockfd); /* 清本 fd 的重建/沉降状态(新 fd 或 close 时) */
void net_listener_retry_tick(void); /* D10 重试轮询(netTask 每轮调用,非阻塞) */
typedef enum { /* 登记来源:仅用于留痕归因,不改变动作 */
NET_LISTENER_REQ_RECON = 0, /* 对账形态③④:芯片侧监听失位 / 孤儿监听 */
NET_LISTENER_REQ_ACCEPT, /* 消费者 accept 撞上 CLOSED 的监听 fd */
NET_LISTENER_REQ_EOF, /* 消费者 recv 读到 EOF */
NET_LISTENER_REQ_DROP, /* net_drop_conn 拆会话保监听 */
NET_LISTENER_REQ_PENDING, /* 挂起 recv 因断连结束 */
} net_listener_req_t;
void net_listener_settle(int sockfd, net_listener_settle_t level); /* 只置事实,不动作 */
void net_listener_request(int sockfd, net_listener_req_t reason); /* 只登记意图,不碰芯片 */
void net_listener_clear(int sockfd); /* 清本 fd 的重建/沉降状态(新 fd 或 close 时) */
void net_listener_tick(void); /* 唯一执行者(netTask 每轮,非阻塞;30ms/2s) */
```
**反向依赖(新模块需要宿主提供的)**:`net_sock_t` 定义(`net_types.h`,共享头,不算反向依赖)、
@@ -557,7 +608,7 @@ void net_listener_retry_tick(void); /* D10 重试轮询(netTask 每轮调
1. **`net_set_errno` 由 `static` 提升为模块间可见** —— 否则新模块链接期报 `L6218E: Undefined symbol`;
`NET_ERR_RETURN_1` 宏随之迁到 `net_socket.h`(它依赖前者,不能只移宏)。
2. **新增 `net_pending_active(fd)`** —— `net_listener_retry_tick` 原有「本 fd 有在途挂起 recv 则跳过」
2. **新增 `net_pending_active(fd)`** —— `net_listener_tick` 原有「本 fd 有在途挂起 recv 则跳过」
(L3 保险,防重开 Socket 摧毁在途连接)直接读 `s_net_pending[i].active`;新模块不能碰该 static
数组,故加薄访问器。**语义原样保留**。
3. **`net_listener.c` 必须真机专属** —— 它依赖 `ch395f_*` SPI 驱动,而 sim 是 Winsock 后端
+3 -2
View File
@@ -155,7 +155,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修
3. **做法**:直接 `ch395f_close_socket()`;`disconnect` 纯属多余且会**制造** DISCONNECT 事件(bit3=1 下必须被消费/预清,否则污染下一条连接,TRAP-NET-12/23 实证);
4. **观测口径**:§5.22 第 1 字节只有 `00H/05H`,第 2 字节(TCP 态)**仅在 byte0=OPEN 时有意义**——关闭后轮询**第 1 字节**等 `00H` 再 open,别拿 byte1 当"卡住"判据。
- **边界(E-02 台架)**:`close` 无 RST 的适用条件——对端配合(收到 FIN 后其内核 close):芯片走 `TIME_WAIT` ~150ms → relisten → 新连接接入;**对端内核存活但不 close**:芯片停在 **`FIN_WAIT_2`(byte0=05H/byte1=06H)长尾**——没有 RST 就拆不掉,**监听恢复受限于对端 close**。不配合场景的兜底 = 芯片自身 TIMEOUT/KeepAlive 拆僵尸会话(判死窗 30~90s,`docs/网络/网络自愈架构设计.md` §5.2 E13)——**不是**应用可加速的路径。
- **应用侧出口 = `net_drop_conn(fd)`**(自愈收敛阶段 3):netTask 侧走驱动 `sock_reset` QUARANTINE(FIN+排渣+排空事件+等 byte0=00H,400ms 底)+ 本层收口 CLOSED + `auto_relisten` 即时重监听;**保监听不毁**(单连接模型不得 `net_close` 监听 fd)。对端不 close 时复位机等在 `net_poll` 步进里(`ch395f_sock_reset_poll`),对端 FIN 到达即自动走完——**不搁浅**。
- **应用侧出口 = `net_drop_conn(fd)`**(自愈收敛阶段 3):netTask 侧走驱动 `sock_reset` QUARANTINE(FIN+排渣+排空事件+等 byte0=00H,400ms 底)+ 本层收口 CLOSED + `auto_relisten` 登记重监听(≤1ms 后 `net_listener_tick` 执行);**保监听不毁**(单连接模型不得 `net_close` 监听 fd)。对端不 close 时复位机等在 `net_poll` 步进里(`ch395f_sock_reset_poll`),对端 FIN 到达即自动走完——**不搁浅**。
## TRAP-NET-09 · 沉降窗:PHY link up 后 / close 后不能立即 open
@@ -163,7 +163,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修
- **现象**:PHY_CHANGE 中断同一时刻 `ch395f_open_socket()` 返错(非 BUSY 超时),首次 listen 失败。
- **根因(定案)**:PHY link up 后芯片内部协议栈需额外时间完成初始化;同一 `net_poll()` 迭代内立即 `OPEN_SOCKET` 可能返 `ERR_BUSY` 或其他错误码。与 FUN_PARA 无关。
- **修复(现行形态)**:
1. `net_listen_locked`/`net_connect_locked` open 失败重试 ×3(D10 `net_listener_retry_tick` 重试环);
1. `net_listen_locked`/`net_connect_locked` open 失败重试 ×3(D10 `net_listener_tick` 重试环);
2. open 失败先走 `net_sock_reset_sync`(确定性排渣,内置二段 CLOSE,见下)再重试——最短等 100ms;
3. 启动错峰走**就绪屏障**(`SYS_EVT_NET_READY`/`FTP_READY`),不用魔法延时。
- **补充:LISTEN 态 Socket 的二段关闭**(E2/E4 芯片实验,本条"沉降才能重开"的前置形态):
@@ -189,6 +189,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修
1. **异常释放(TIMEOUT/RST)隔离**:`handle_timeout_event` 置 per-fd 隔离标记;`net_relisten_locked` 隔离期内持续排空芯片 RX(`net_drain_chip_rx()`)+ 主动 close + 排空该 socket 中断 + 等 `CLOSED/TCP_CLOSED`,满 `NET_RELISTEN_QUARANTINE_MS`(400ms) 才 open+listen;上限 `NET_RELISTEN_SETTLE_MS`(800ms) 强制放行——**必须小于** `NET_RELISTEN_DEADLINE_MS`(2000ms):取 2000ms 会耗尽重试预算 → give up → **监听器死亡**;
2. **正常释放(FIN)沉降**:`handle_disconnect_event` 登记(仅 `recv_bytes != 0` 时——纯连接即关与只发不收免沉降保复连时序),relisten 前最短等 `NET_RELISTEN_GRACEFUL_MS`(**100ms**,实测悬崖:0/30ms 无效、100ms 稳定通过);日志区分 `relisten settle`/`relisten quarantine`;
3. **应用侧硬化**:`net_sock_t.released_abnormal` 标记(TIMEOUT 置 1/DISCONNECT 置 0,新 fd 与新监听清零——新连接不得继承上一条的释放原因);`net_pending_tick` 断连结束时按标记以 `NET_ERR_CONNRESET` 或 EOF(0) 回传;`lftpd receive_file` 错误路径 **unlink 截断文件** + STOR 回 **450**。
- **收口尝试与边界(2026-10-14,E16)**:曾把上面修复 1 的排渣序列收口进驱动复合复位原语 `ch395f_sock_reset_*`(去重复实现与参数副本),**实测回退**:`ftp_abort_probe` RST **0/10**、FIN **0/5**(首笔复连仍被截断到 ≤2048B),并新增 `sock_reset_sync timeout (600ms)` / `close: sock_reset not done` 两条 `[ERR]`;同场 net 层自建序列 **10/10 + 5/5**(快复连 50/50、背靠背 30/30 两版都过 → 失效面精确落在"中止后首笔复连")。结论 = 原语缺**隔离窗内持续排空/关闭**与**复用让位**两条语义,补齐前 **net 层保留自建序列**,勿再直接把监听重建接进原语。前置语义与 A/B 数据 → `docs/网络/网络自愈架构设计.md` §8 延后项 1、§5.2 E16;登记 **N-30**。
- **协议边界(不可判定,留痕)**:FTP STOR 无长度声明,"在途 FIN 关闭"与"正常完成"**逐字节不可区分** → 优雅中止仍回 226;仅 RST/掉链(芯片 TIMEOUT)可识别 → 450。FIN 边界由客户端长度校验(SIZE/RETR 往返)兜底。
- **回归基线**:`--check-abort` RST **5/5**(450)+ FIN **5/5**(226 边界不变);followup 全过;正常往返 freeze SHA-256 全 MATCH;`:7003` sink 复用 RST×4/FIN×4 全过;`:8888` 真实收包写 RTC 成功。
- **与 TC-NET-604 的关系**:本条是独立、已可信复现并已修复的真实缺陷;TC-NET-604 已结案为非缺陷(判读教训见第六章台架纪律)。现场"上传失败/文件不完整/像卡死"类报告优先用本条判据复核。