docs(net): 监听生命周期根治设计——证据、决策与执行清单

新增 docs/子系统设计/网络Socket监听生命周期设计说明.md(9 节):

原理三层证据——芯片层:LISTEN 与 ESTABLISHED 是同一 TCP 状态字节的互斥
取值(手册 §5.22 L535-545、§5.29 L609「直到连接成功后」),全手册无任何
"连接结束自动恢复监听"条款;且本仓 fun_para=0x08(bit3 SOCKET_CLOSE=1,
Trap 18/25 定案)使断开后芯片停在 0x05/0x00,即 B10 看到的"孤儿芯片态"
是本仓配置的合法后果,而非芯片异常。规范层:RFC 1122 §4.2.2.18 明文被动
OPEN MUST NOT affect any previously created connection record,POSIX
accept() 原文 socket 不变,lwIP 把监听器所有权写进 tcp_close() 文档——
本仓需要靠"EOF 不得 close 监听 fd"铁律维持正确性,即架构欠债的凭证。
实现层:EOF 是时序敏感边沿(只在 recv 那一刻 state==CLOSED 才给),导致
重建所有者散成 12 个(栈内 8 + 应用 4)、15 个迁移点、15 个无人负责组合。

关键决定 D1~D7:不启用芯片原生多连接模式(A′ 出局——FTP 改造清单第 4 条
已定案"多连接模式不支持多监听",本产品有 21/8888/7003 三个常驻监听加会
话期动态 PASV,且历史上踩过 Trap 02/11,FUN_PARA 初始化后不可改);采用
"会话结束电平化 + 监听重建单一所有者"的形态 B 深化,不做控制面重写;对
账④保留不撤回(Trap 22 实证事件会丢,周期对账不可删),其副作用由 P1
消除;fun_para 保持 0x08,不回退 Trap 25 的排空契约。

证据附录含 B10 探针日志、A/B 归因数据(基线 5/1/1/5 vs B3 3/7/2/1,均值
3.0 vs 3.25,统计不可区分)、④ 修复后完整回归矩阵、官方 EXAM5/EXAM10 所
有权模型(SDK 行号)、状态机 15 迁移点与关键判据、三服务契约不一致表。

Trap_Records B10 残余改写为指向该文档,并明确记录 ④ 的副作用:抢先
relisten 会把慢消费者的 EOF 永久吞掉,g_client 不清、新客户端数据被旧会
话静默吸收——④ 修好了端口拒连却撑大了另一侧窗口,P1 必须补。

登记行 N-32 已写入 docs/待处理内容.md 工作区,但该文件与并发在途的 N-27
改动混杂,本次不提交,留待其归属方一并入库。
This commit is contained in:
2026-10-12 11:17:48 +08:00
parent 4f2cc8bc90
commit 09f8a6fb20
2 changed files with 227 additions and 1 deletions
@@ -0,0 +1,226 @@
# 网络 Socket 监听生命周期设计说明(B10 根治)
> 定位:回答"为什么 CH395F 上监听会被毁、端口会永久拒连",并给出根治路线。
> 事实来源:`docs/芯片手册/CH395F.md`、`docs/问题记录/CH395F_Trap_Records.md`、
> `Drivers/BSP/NET/net_socket.c`(3921 行)、官方 CH395EVT V2.0 例程(主机侧核对)。
> 本文只定原理与方案,不含已落地代码;落地按 §5 分阶段,每阶段独立可回滚。
## 1. 一句话根因
**监听器是一个长期服务对象,却被实现成一个短暂的芯片 Socket;而"这条会话结束了"这个事实,
是用一个对时序敏感的边沿来通知的。**
由此必然派生两类事故:① 没人接住边沿 → 监听器无人重建(B10 孤儿监听);
② 别人抢先接住 → 消费者永远收不到结束信号(会话卡死、新连接被旧会话静默吸收)。
## 2. 原理:三层证据
### 2.1 芯片层——"监听"不是能力,是一个会被消耗掉的槽位状态
| 事实 | 出处 |
|---|---|
| LISTEN 与 ESTABLISHED 是同一 TCP 状态字节的**互斥取值**(01H/04H) | `docs/芯片手册/CH395F.md` §5.22 L535-545 |
| 「Socket 会一直检测连接事件,**直到连接成功后**……每个 Socket 只可以建立一个连接」 | `docs/芯片手册/CH395F.md` §5.29 L609 |
| 连接结束后芯片**永不**自动回 LISTEN——全手册无此条款;重连必须"重新打开 Socket 再 CONNECT" | `docs/芯片手册/CH395F.md` §5.30 L615、§9.2.5 L1266 |
| 断开后芯片行为由 FUN_PARA bit3 分流:=0 芯片自动关 Socket(→`0x00/0x00`);**=1 芯片什么都不做(→`0x05/0x00`),MCU 必须自己 CLOSE** | `docs/芯片手册/CH395F.md` §5.23 L572、§5.52 L795② |
⚠ **本仓 `ch395f_set_fun_para(0x08)`(`Drivers/BSP/NET/net_socket.c:275`,bit3 SOCKET_CLOSE=1,
Trap 18/25 定案为保留排空现场)⇒ 断开后芯片必然停在 `0x05/0x00`。
B10 探针看到的"孤儿芯片态"不是芯片异常,而是本仓这个配置的**直接且合法的后果**。**
⚠ 口径错误待修:`Drivers/BSP/NET/net_socket.c:3627` 与 `:3641` 的函数头注释写
「CH395F 在 DISCONNECT 后自动关闭 Socket(FUN_PARA 默认)」——与实际 `0x08` 相反。
### 2.2 规范层——本仓的做法与 TCP/POSIX 规范正面冲突
- RFC 1122 §4.2.2.18:被动 OPEN「**MUST NOT affect any previously created connection record**」。
- POSIX `accept()`:「The original socket remains open and can accept more connections」。
- lwIP 把"监听器归谁"直接写进 `tcp_close()` 文档:只有 socket 层创建的监听器才会被连带关闭,
raw API 自建的不会——**所有权是 API 契约的一部分,不是调用方的自觉**。
本仓让同一个 fd 兼任"监听器"和"连接"两个角色(handle/resource aliasing),
于是"关掉一条连接"和"毁掉一个监听器"在类型上无法区分——
这正是 AGENTS 里「应用层不得在 EOF 时 `net_close` 该 fd」这条铁律存在的唯一原因。
**需要靠铁律维持的正确性,就是架构欠债的凭证。**
### 2.3 实现层——别名导致"边沿通知",进而导致多所有者
`net_recv` 的 EOF 只在 **recv 那一刻 `state==CLOSED`** 才给出
(`Drivers/BSP/NET/net_socket.c:1880-1888`);若该 fd 已被别的路径 relisten 回 LISTENING,
同一个 recv 恒返 `WOULDBLOCK`(`:1890`)。这是一个**边沿**,不是一个**电平**。
于是"谁重建监听"散成 **12 个所有者**:栈内 8 处(对账③④、`handle_disconnect_event` 监听分支、
`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 行)。
三个已实测的派生失效:
| 失效 | 机理 | 现状 |
|---|---|---|
| 孤儿监听(B10) | 边沿没人接,四方互等 | 对账④兜底 ≈1 s(已提交 `4f2cc8b`) |
| 消费者卡死 | ④/D10 抢先 relisten → recv 永久 `WOULDBLOCK` → `g_client` 不清、新连接被旧会话静默吸收 | **④ 扩大了此窗口**,未修 |
| 监听被误关 | `net_can_read` 在非 ESTABLISHED(含 LISTENING)返 1 → recv 得 `WOULDBLOCK` → `Drivers/BSP/UI/ui_net_sink.c:563` 无护栏 `net_close(g_client)` = 关掉监听 fd | 靠随后 `bind_listen` 自救,落 Trap 09 沉降窗 |
另有一条**探针失效**:`net_can_accept`(`:1453`)要求 ESTABLISHED 才返 1,
而 `Drivers/BSP/UI/ui_net_sink.c:512` 用它门控 accept → :7003 **永不进 `net_accept`**
→ Trap 25 的 `net_lost_connect_probe`(`:1250-1289`)对这个端口**从不运行**,
丢 CONNECT 只剩对账②。:21/:8888 因为无条件轮询 accept,探针才活着。
## 3. 为什么不能走"芯片原生多连接模式"(A′)
芯片确实自带 lwIP 式模型:FUN_PARA bit1(≥0x44,本仓实测 0x4A)下
「服务器的 Socket **仅用来监听**,单片机需要分配其它 Socket 用于该连接」(§9.2.6 L1295)。
但本产品**不可用**,三条硬证据:
1. **不支持多监听**——`docs/问题记录/CH395F_Trap_Records.md` 附 FTP 改造清单第 4 条
「CH395F 多连接模式不支持多监听(类型:架构)」,处置即 `fun_para=0x00`。
本产品有 21/8888/7003 **三个常驻监听** + 会话期动态 PASV 监听。
2. 池分配按**源端口匹配**(`docs/芯片手册/CH395F.md` §9.2.6 L1295),多监听器的池会互相串。
3. 历史实测:「多连接模式下最多接受 3 个客户端(Socket 1~3),Socket 4~7 的 CONNECT 中断从不触发」(:10)、
「多个 Socket 数据互相覆盖」(:35)——即 Trap 02/11。
且 FUN_PARA **初始化后不可改**(`docs/芯片手册/CH395F.md` §5.52 L795⑥),这是开机一次性决策,不能运行时降级。
**结论:A′ 出局;根治必须在单连接复用模型内做。**
## 4. 根治原理:两条不变量
- **不变量 I(电平化)**:「这条会话已结束」必须是**可重复查询、幂等**的状态,
而不是"恰好在那一刻观察到才算"。谁先看到不重要,**谁都饿不着**。
- **不变量 II(单一所有者)**:监听器的重建**只有一个决定者**(net 层的 listener pump)。
其它所有位置只**置事实**,不**做动作**。
两条不变量成立后,"谁重建""会不会错过"这两类问题在结构上消失,
对账④从"唯一救命稻草"降级为"事件丢失时的补漏"(Trap 22 实证事件会丢,故对账**必须保留**)。
## 5. 分阶段方案
### P0 口径与登记(零风险,纯文档/注释)
1. 修 `net_socket.c:3627`、`:3641` 的 FUN_PARA 口径错误注释(实际 `0x08`,芯片**不**自动关)。
2. 把 U11(本层 CLOSED + 芯片正在 LISTEN,④判据缺口)、U12(relisten 的 LISTEN 失败不 close 芯片 Socket,
与 open 失败路径不对称)、U14(`NET_SOCK_STATE_TCP_CLIENT_RECONNECTING` 死枚举)、
U15(丢 RECV_OK 无人对账)登记进 `docs/问题记录/CH395F_Trap_Records.md`。
3. 在 B10 行补记残余:**④ 扩大了"EOF 被吞没"窗口**(见 §2.3)。
### P1 边沿 → 电平(小改,收益最大)
1. `net_sock_t` 增 1 字节 `session_dead`(粘滞"本会话已结束"):
- **置位**:`handle_disconnect_event` 释放分支、`handle_timeout_event` ESTABLISHED 分支、
对账①(复用前者)、`net_connect_locked` 命令被拒。
- **清零**:新的 CONNECT 成功(`handle_connect_event`)、`net_close`/`net_socket`(memset)。
- **不清**:`net_relisten_locked`——**这正是关键**,重建监听与"应用是否已确认会话结束"解耦。
2. `net_recv_locked` DONTWAIT:判据由 `state==CLOSED` 改为 `state==CLOSED || session_dead`
→ EOF **可重复获得**,任何消费者都不会再饿着。
3. `ui_net_sink` 三处收口:
- `:563` 真错误分支加 `g_client != g_listen` 护栏(与 `:552` 一致,MCU 上不关监听);
- EOF/错误后**不再**自行 `bind_listen`(重建交回 net 层,落实不变量 II);
- accept 门控从 `net_can_accept`(ESTABLISHED)改为新的 `net_listener_armed(fd)`
(LISTENING 或 CLOSED+auto_relisten)→ 让 :7003 重新吃到 `lost_connect_probe`。
4. 验收(新增腿,专打"抢先 relisten"):客户端连上 :7003 → 拖住 uiTask 使 relisten 先发生 →
断连 → 断言 sink 仍拿到 EOF、且**下一个客户端能被服务**(当前会被旧会话静默吸收)。
回归矩阵沿用 B10 那套(`:7003` 10/10、`:21` 50/50×2、`:8888` 20/20、`ftp_regression` 11/11、Trap 23 5/5×2)。
### P2 单一所有者(中改,消灭多所有者)
1. 新增 `net_listener_pump()`,与 `net_reconcile_tick` 同处 netTask 自愈节拍,
是**唯一**对监听型 Socket 调 `net_relisten_try` 的地方。
2. 删除散点 relisten 调用:`net_accept_locked:1346`、`net_recv_locked:1884`、`net_pending_tick:2451`
——它们只报告 EOF,重建由 pump 统一做(消除"抢先"这件事本身)。
3. 消灭 netTask 内的同步忙等(U6):`net_close_locked:2597-2616` 的 500 ms `osDelay` 循环、
`net_listen_locked:1115-1126` 的 3×500 ms,改用既有 D10 非阻塞重试模式。
4. 补 U11:本层 CLOSED 而芯片**正在** LISTEN → 只做状态重同步(等价 `NET_MSG_RESYNC_LISTEN`),不重下发命令。
5. 验收:close/listen 风暴期间无 `[WD] task stall`;`:21` 0ms×50 仍 50/50;
`netp`(`net_poll` 段最长耗时,µs)不劣化。
### P3 远期(**不建议现在做**,仅登记方向)
形态 A:listener 成为一等对象 + `accept` 返回虚拟句柄(与 `simulator/sim/net_socket_sim.c`
已是同一模型,消除真机/模拟器双模型分裂)。代价:控制面重写 15~25 函数、RAM +<1 KB、Flash +2~6 KB、
回归面覆盖全部服务与板级套件。只有当产品需要"同一端口并发多连接"时才值得立项。
## 6. 顺序与回滚
P0 → P1 → P2 严格串行,每阶段独立提交、独立回归、可单独回滚。
P1 是性价比最高的一步:它不改变 fd 模型,却把 B10 与"消费者卡死"两类事故同时从结构上消掉。
P2 之后,对账④保留但只作事件丢失的补漏(Trap 22 实证事件会丢,不可删)。
## 7. 证据附录(2026-10-12 实测与调研,供后续执行时复核)
### 7.1 B10 现场与修复实测
| 项 | 数据 |
|---|---|
| 孤儿态探针(临时,测完已删) | `TEMP orphan sock2 state=0 port=7003 chip=0x05/0x00` 每秒一条持续 12 s+;`:7003 300ms×10` → `SUMMARY rounds=10 ok=0 fail=10 unrecovered=10`;+30 s 仍拒连 |
| 判"是否 B3 引起"的 A/B | 基线 `794a688` = 5/1/1/5(均值 3.0)vs B3 构建 = 3/7/2/1(均值 3.25)→ **统计不可区分**;每个样本前硬复位 + 等 12 s 干净启动 |
| ④ 修复后 | `:7003 300ms×10` **10/10**(+20 s 存活);`:8888 0ms×20` 20/20;`:21 0ms×50` 50/50;`:21 100ms×50` 50/50;`ftp_regression` 11/11 ×2;`ftp_abort_probe --check-abort` RST 5/5、`--graceful` FIN 5/5;`@test` PASS(600 ms、回读无差异);`Code` 197008→197084 |
| 救援日志(生效证明) | `sock2 reconcile: orphan listener (chip=0x05/0x00) -> relisten` → `auto-relisten ok` / `relisten retry ok` |
| 台架方法论坑 | ① 一轮把监听打死不自恢复 → **每腿前必须硬复位**,否则 A/B 被污染(本次第一腿就是这么废的);② 判固件回归前先核对主机侧工具凭据(`ftp_abort_probe.py` 用 anonymous 登录,N-19 后 530 拒 → 假 0/5) |
### 7.2 官方例程的所有权模型(CH395EVT V2.0,主机路径 `D:\Code\DTU\使用的芯片手册\CH395EVT\EVT\`,不在仓内)
- **EXAM5(多连接)**:`main.c:296 SetFunPapr(0x02)`(bit1=1);Socket0 `OPEN+TCPListen` 后再不碰(:117-127),Socket1~7 只配端口/协议**不 OPEN**(:129-133);DISCONNECT/TIM_OUT **只打印、不 close 不重 LISTEN**(:203-212)——恢复完全由芯片自动,**只在多连接模式成立**。
- **EXAM10(单连接,FTP Server)**:DISCONNECT → `CH395.C:234` 显式 `CloseSocket` → `:237-238` **立即重新 OPEN + 重新 TCP_LISTEN**;TIM_OUT 同路径(`:250`/`:254`);调用者是中断处理器;close→reopen **无延时无重试**,失败 `mStopIfError` 挂死。
- **结论**:单连接模式下官方**零自动重监听**,且原生做法是**在事件现场当场重建**;本仓对账④是周期兜底,方向一致但不可互替(Trap 22 实证事件会丢)。
- **中断所有权**:全局寄存器整体读即清、无按位清除(`docs/芯片手册/CH395F.md` §5.15 L441);CONNECT/DISCONNECT/TIM_OUT 是**事件位**(可丢)、RECV 是**条件位**(可补)→ 任何"只靠事件重建监听"的方案天然不完备。
### 7.3 状态机事实(`Drivers/BSP/NET/net_socket.c` 3921 行)
- `->state =` 赋值 **13 处**(:815 :953 :1069 :1072 :1139 :1685 :1693 :2075 :3577 :3611 :3698 :3765 :3787)+ 2 处 memset(:249 全表、:2622 单块);`state` 的**全仓唯一写者**是本文件。
- **重建所有者 12 个**:栈内 8(对账③④、`handle_disconnect_event` 监听分支、`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 代次)。
- 做决策必须知道的判据:`net_listen_locked:1102-1104` 状态门**只允许 BOUND/CREATED**(对已 CLOSED 的监听 fd 直接 `net_listen` 返 INVAL);`net_connect_locked:1583` **无状态门**(可在 LISTENING 上改成 CONNECTING);`net_recv_locked:1862-1873` 快路径**无状态门**;`net_can_accept:1453` 要求 ESTABLISHED。
- M6(`handle_disconnect_event:3659-3701` 置 CLOSED)之后,**唯一不依赖应用调用的收口是对账④**;`:1346`/`:1884`/`:2451` 三条内部路径全部以"应用先发起一次 accept/recv"为前提,而 `ui_net_sink.c:512`/`:536` 的门控恰好把这两类调用在 CLOSED 态下全部掐断——这就是四方互等的成因。
### 7.4 三个服务的契约互不一致
| 服务 | 认为的连接结束信号 | net 层实际给什么 | 监听重建者 | 是否留无人重建组合 |
|---|---|---|---|---|
| `lftpd` 控制 :21 | `read_line!=0`(EOF→-1,`lftpd_inet.c:154-156`) | 阻塞 pending 保证 CLOSED 必了结(FIN→0 / 异常→CONNRESET,`net_socket.c:2457-2462`) | net 层(`:2449-2451`),应用**永不** close 控制 fd(`lftpd.c:746-750`) | 无(契约一致,是正面样板) |
| `time_sync` :8888 | EOF/30 s 预算/错误,随后**无条件** `net_close(conn==ls)`(`time_sync.c:182`) | 同上 | 应用自己每会话全量重建(`:224`) | 不留孤儿,但违反「EOF 不得 close 监听 fd」→ close→重建沉降窗(Trap 09)+ 最坏 ~10 s 端口下线 |
| `ui_net_sink` :7003 | `net_recv` 返 0 或真错误;重建条件 `net_get_sock(g_listen)==NULL`(`:557`/`:566`) | 该 0 只在 recv 时刻 `state==CLOSED` 才给 | 名义 net 层,实际自身条件在 MCU **恒假**(fd 不释放) | **有**(B10 与"消费者卡死"都出自这里) |
| 模拟器后端 | `recv` 0 恒可得、fd 分离(`net_socket_sim.c:293-336`) | Winsock 原生 | 无需 | 无——且对上述全部失效模式**零覆盖** |
### 7.5 ④ 副作用的判据链(P1 要消除的对象)
`net_can_read:1477-1478` 在非 ESTABLISHED(**含 LISTENING**)返 1 → 消费者去 recv →
DONTWAIT 在 LISTENING 下返 `WOULDBLOCK`(`:1890`)→ `ui_net_sink.c:560-569` 判为"真错误"→
`:563` **无 `g_client != g_listen` 护栏**的 `net_close(g_client)` 在 MCU 上等于**关掉监听 fd**
(与同文件 `:134` 注释"严禁"自相矛盾)→ 靠随后 `bind_listen` 自救,落 Trap 09 沉降窗。
## 8. 决策记录
| # | 决定 | 依据 |
|---|---|---|
| D1 | **A′(启用芯片多连接模式)出局**,根治必须在单连接复用模型内做 | `docs/问题记录/CH395F_Trap_Records.md` 附 FTP 改造清单第 4 条「多连接模式不支持多监听」;本产品 3 常驻监听 + 动态 PASV;§9.2.6 L1295 池按源端口匹配会互串;历史实测 Trap 02/11;FUN_PARA 初始化后不可改(`docs/芯片手册/CH395F.md` §5.52 L795⑥) |
| D2 | 采用**形态 B 深化**(电平化 + 单一所有者),**不做 P3** | P3 需控制面重写、RAM/Flash 上升,只有"同端口并发多连接"需求才值得 |
| D3 | 对账④**保留**(`4f2cc8b` 不撤回),其副作用由 P1 消除 | Trap 22 实证事件会丢 → 周期对账不可删;④ 已把 `:7003` 从 0/10 永久死拉到 10/10 |
| D4 | `FUN_PARA` **保持 `0x08`**,不因孤儿态改配置 | bit3=1 是 Trap 18/25 定案的"保留排空现场"契约;孤儿态是它的合法后果,应在软件层收口而非回退契约 |
| D5 | 顺序 **P0 → P1 → P2**,每阶段独立提交、独立回归、可单独回滚 | P1 不动 fd 模型却消掉两类事故,性价比最高 |
| D6 | 真机/模拟器**双模型分裂暂不消除** | 消除即等于做 P3;先靠 P1 把应用侧契约对齐 |
| D7 | 同族残留**不在本次根治范围**,各自单列:U1(CONNECTING 滞留,B2 残余)、U11、U12、U14、U15,以及审计项 B4/B5/B6/B7/B8/B9 | 与监听生命周期不同因,混做会让回归面失控 |
## 9. 执行清单(后续按此执行,逐项勾选)
**P0(零风险)**
- [ ] 修 `Drivers/BSP/NET/net_socket.c:3627`、`:3641` 的 FUN_PARA 口径错误注释(实际 `0x08`,芯片**不**自动关)
- [ ] 把 U11 / U12 / U14 / U15 登记进 `docs/问题记录/CH395F_Trap_Records.md`
- [ ] 在 B10 行确认已记「④ 扩大 EOF 吞没窗口」(已记,执行时复核)
**P1(边沿→电平)**
- [ ] `net_types.h` 的 `net_sock_t` 增 `uint8_t session_dead;`
- [ ] 置位:`handle_disconnect_event` 释放分支、`handle_timeout_event` ESTABLISHED 分支、对账①(复用前者)、`net_connect_locked:1685`
- [ ] 清零:`handle_connect_event`(新 CONNECT)、`net_close`/`net_socket` 的 memset;**`net_relisten_locked` 不清**(关键)
- [ ] `net_recv_locked` DONTWAIT 判据改 `state==CLOSED || session_dead`
- [ ] `ui_net_sink.c:563` 加 `g_client != g_listen` 护栏;EOF/错误后不再自行 `bind_listen`
- [ ] 新增 `net_listener_armed(fd)`(LISTENING 或 CLOSED+auto_relisten),替换 `ui_net_sink.c:512` 的 `net_can_accept` 门控
- [ ] 验收:新增"抢先 relisten"腿(连上 :7003 → 拖住 uiTask → 断连 → 断言 sink 仍得 EOF **且下一个客户端被服务**)+ 全量回归矩阵(§7.1 那一套)
- [ ] `@build` 0/0 → `@test` PASS → 回读校验 → 停下等提交指令
**P2(单一所有者)**
- [ ] 新增 `net_listener_pump()`,与 `net_reconcile_tick` 同节拍,成为**唯一**调 `net_relisten_try` 的监听重建入口
- [ ] 删散点 relisten:`net_accept_locked:1346`、`net_recv_locked:1884`、`net_pending_tick:2451`
- [ ] 消灭 netTask 内同步忙等:`net_close_locked:2597-2616`(500 ms `osDelay` 循环)、`net_listen_locked:1115-1126`(3×500 ms)→ 改 D10 非阻塞重试
- [ ] 补 U11:本层 CLOSED 而芯片**正在** LISTEN → 只做状态重同步,不重下发命令
- [ ] 验收:close/listen 风暴期间无 `[WD] task stall`;`:21 0ms×50` 仍 50/50;`netp`(`net_poll` 段最长耗时 µs)不劣化
**回滚点**:P0 纯注释可直接 revert;P1 若回归失败 revert 单提交即可(④ 仍在,退回到"端口活着但慢消费者可能卡死"的已知状态);P2 涉及入口收口,须按阶段各自成提交、各自可 revert。
+1 -1
View File
@@ -947,7 +947,7 @@ Trap 22 的周期对账(200ms 节拍 + 400ms 去抖 + 1s 冷却)设计目标
| B7 | 低 | `ch395f_test_task.c:1379/1504` | Phase 5 单次写 4093 B 超 1 KB 发送缓冲与 2048 DMA 载荷 → 整块被丢;注释仍写"4096 B = 发送缓冲上限"(2026-09-22 布局解耦前的旧事实)。A6 修后会打 `[ERR]` 而非静默 stall | ⏳ 待修(chunk 改 ≤1024 或分块) |
| B8 | 低 | `ch395f_test_task.c:1290-1292` | Phase 4 把 sock6 缓冲重排到块 24-27,与 `net_init` 固定布局中 sock4 的 24-27 **交叠**(§5.45 禁止) | ⏳ 待修 |
| B9 | 低 | `ch395f_reuse_bench.c:93`、`net_socket.c:39`、`net_socket.c:262` | bench 用 `get_phy_status() != 0` 判链路(01H=断开也成立,`net_task.c:122` 的 `!= CH395F_PHY_DISCONN` 才对);`NET_PHY_CHECK_INTERVAL_MS` 死宏;无芯片版本门控(§5.16 <0x44 不支持 ALL 版、§5.52 bit3/4 需 ≥0x46,实测 0x4A 无影响) | ⏳ 待修(前两项死代码/卫生) |
| B10 | **中** | `net_reconcile_tick` 认领范围 + `handle_disconnect_event` 释放分支(`net_socket.c:3663-3686`) | **孤儿监听 → 端口永久拒连**。断开时释放分支把状态置 `CLOSED` 并注释「留给 `net_recv_locked` 把剩余数据读出来再关,**等待应用重新监听**」——即由应用负责重建;但 `ui_net_sink` 只在 `net_recv` 返 0(EOF)时才清 `g_client`,而 EOF 已被这个事件分支消费掉 → **net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN**,四方互等。实测(临时探针):`TEMP orphan sock2 state=0 port=7003 chip=0x05/0x00` 每秒一条持续 12 s+,`:7003 300ms×10` **0/10**、+30 s 仍拒连。与 Trap 22 的关系:Trap 22 的①③两类都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外**,所以那套对账全程沉默 | ✅ 已修(2026-10-12):`net_reconcile_tick` 新增**类型④**认领孤儿(`in_use` + `state==CLOSED` + `auto_relisten` + `type==STREAM` + `local_port!=0`,芯片侧不在 LISTEN)→ 复用 `net_relisten_try`(自带"已在重试队列则不重复登记"保护,与 D10 重试队列、Trap 23 沉降隔离兼容)。⚠ **`auto_relisten` 是决定性门**:少了它,"本层 CLOSED + 已绑端口"的**出向连接**(板级 Phase 7 TC701~703 用 `net_connect` 绑本地口)会被误判孤儿并被 relisten 成监听器。**回归**:`:7003 300ms×10` **0/10 → 10/10**(+20 s 存活);`:21` 0ms×50 **50/50**、100ms×50 **50/50**;`:8888` 0ms×20 **20/20**;`ftp_regression` **11/11 ×2**;Trap 23 RST **5/5**、FIN **5/5**;`@test` PASS(600 ms、回读无差异);`Code` 197008→197084。⚠ **残余(未根治)**:生产者仍在——释放分支依旧把状态停在 `CLOSED` 等应用重建,④是**兜底**(实测救援后端口 1 s 内恢复,日志 `reconcile: orphan listener … -> relisten` 可见);根治要在释放分支对 `auto_relisten` 直接重监听,但那会破坏「让 `net_recv_locked` 读干剩余数据」的 Trap 25 closed-with-data 契约,须单列评估 |
| B10 | **中** | `net_reconcile_tick` 认领范围 + `handle_disconnect_event` 释放分支(`net_socket.c:3663-3686`) | **孤儿监听 → 端口永久拒连**。断开时释放分支把状态置 `CLOSED` 并注释「留给 `net_recv_locked` 把剩余数据读出来再关,**等待应用重新监听**」——即由应用负责重建;但 `ui_net_sink` 只在 `net_recv` 返 0(EOF)时才清 `g_client`,而 EOF 已被这个事件分支消费掉 → **net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN**,四方互等。实测(临时探针):`TEMP orphan sock2 state=0 port=7003 chip=0x05/0x00` 每秒一条持续 12 s+,`:7003 300ms×10` **0/10**、+30 s 仍拒连。与 Trap 22 的关系:Trap 22 的①③两类都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外**,所以那套对账全程沉默 | ✅ 已修(2026-10-12):`net_reconcile_tick` 新增**类型④**认领孤儿(`in_use` + `state==CLOSED` + `auto_relisten` + `type==STREAM` + `local_port!=0`,芯片侧不在 LISTEN)→ 复用 `net_relisten_try`(自带"已在重试队列则不重复登记"保护,与 D10 重试队列、Trap 23 沉降隔离兼容)。⚠ **`auto_relisten` 是决定性门**:少了它,"本层 CLOSED + 已绑端口"的**出向连接**(板级 Phase 7 TC701~703 用 `net_connect` 绑本地口)会被误判孤儿并被 relisten 成监听器。**回归**:`:7003 300ms×10` **0/10 → 10/10**(+20 s 存活);`:21` 0ms×50 **50/50**、100ms×50 **50/50**;`:8888` 0ms×20 **20/20**;`ftp_regression` **11/11 ×2**;Trap 23 RST **5/5**、FIN **5/5**;`@test` PASS(600 ms、回读无差异);`Code` 197008→197084。⚠ **残余(未根治,已立项根治)**:生产者仍在——释放分支依旧把状态停在 `CLOSED` 等应用重建,④是**兜底**(实测救援后端口 1 s 内恢复,日志 `reconcile: orphan listener … -> relisten` 可见)。⚠ **④ 有副作用**:`net_recv` 的 EOF 是**边沿**(只在 recv 那一刻 `state==CLOSED` 才给 0,`:1880-1888`;已被别处 relisten 回 LISTENING 则恒 `WOULDBLOCK`,`:1890`)→ ④ 抢先 relisten 会把慢消费者的 EOF 永久吞掉,`g_client` 不清、新客户端数据被旧会话静默吸收。**原理与分阶段根治方案 → `docs/子系统设计/网络Socket监听生命周期设计说明.md`**(含:芯片层为何永不自动回 LISTEN、本仓 `fun_para=0x08` 使 `0x05/0x00` 成为合法常驻态、RFC 1122 §4.2.2.18 与 POSIX accept 的规范冲突、12 个重建所有者与 U1~U15 无人负责组合、以及"芯片原生多连接模式(A′)为何对本产品不可用"的决定性证据) |
### 文档侧待修清单(本轮只改了最要紧的几处,其余登记在此)