From 0edbfaf08bf352af2b1e4991666ab28f3d3258f1 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Wed, 14 Oct 2026 15:57:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(NET):=20=E7=9B=91=E5=90=AC=E9=87=8D?= =?UTF-8?q?=E5=BB=BA=E5=8D=95=E4=B8=80=E6=89=80=E6=9C=89=E8=80=85=E6=94=B6?= =?UTF-8?q?=E5=8F=A3=20+=20=E6=B2=89=E9=99=8D=E7=AA=97=20800ms=20=E4=B8=8A?= =?UTF-8?q?=E9=99=90=E5=8F=AA=E7=95=99=E7=97=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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)。 --- Drivers/BSP/NET/net_listener.c | 210 +++++++++++++--------- Drivers/BSP/NET/net_listener.h | 65 +++++-- Drivers/BSP/NET/net_pending.c | 23 ++- Drivers/BSP/NET/net_pending.h | 4 +- Drivers/BSP/NET/net_socket.c | 59 +++--- docs/images/Net层阻塞模型.svg | 2 +- docs/任务规划/登记簿.md | 6 +- docs/内存管理/RAM使用分析与优化方案.md | 8 +- docs/内存管理/RAM预算台账.md | 6 +- docs/测试说明/链路延迟测试说明.md | 2 +- docs/网络/BSD_Socket_API_使用指南.md | 8 +- docs/网络/BSD_Socket_API_函数调用关系.md | 38 ++-- docs/网络/Net层阻塞模型架构设计.md | 35 ++-- docs/网络/网络自愈架构设计.md | 105 ++++++++--- docs/问题记录/CH395F_Trap_Records.md | 5 +- scripts/ram_baseline.tsv | 2 +- tools/arch_diagram/gen_net_pending_svg.py | 2 +- 17 files changed, 356 insertions(+), 224 deletions(-) diff --git a/Drivers/BSP/NET/net_listener.c b/Drivers/BSP/NET/net_listener.c index 3a5e6ecd..b289fd68 100644 --- a/Drivers/BSP/NET/net_listener.c +++ b/Drivers/BSP/NET/net_listener.c @@ -2,22 +2,26 @@ * 模块名称:Network Listener Lifecycle * 模块功能:监听器生命周期(真机专属)—— 维持 fd 处于 LISTEN 态的全部机制: * 重建(open+listen)、重建前的沉降/隔离(TRAP-NET-18)、排空 RX 残渣、 - * D10 非阻塞重试(周期性重建决策者=自愈收敛阶段 3 合并后的 - * net_reconcile_tick,见 net_socket.c)。 + * D10 非阻塞重试。**本模块是唯一的执行者**:外部只登记请求,动作只在本 + * 模块的 net_listener_tick 一处发生(周期性"该不该重建"的判据仍在 + * net_reconcile_tick 形态③④,见 net_socket.c)。 * 适用平台:STM32F407ZGTx + CH395F 以太网芯片 * 作者:王建锋 * 创建日期:2026-10-12 * 修改记录: * 2026-10-12 王建锋 从 net_socket.c 整体抽出监听生命周期(N-32 §10 模块化) * 2026-10-13 王建锋 自愈收敛阶段 3:pump 并入 net_reconcile_tick(《网络自愈架构设计》§4.1) + * 2026-10-14 王建锋 单一所有者收口(《网络自愈架构设计》§4.2 I2 加强):try 当场动作 + * + 四处调用点各自带门 → request 登记意图 + tick 唯一执行; + * 沉降留痕改"只留首末"(实测每次恢复 3 行 → 1 行) * * 职责与边界见 net_listener.h 头注释;设计依据见 * docs/网络/网络自愈架构设计.md §11。 * * 模块内分层: * ① 策略常量与私有状态(s_relisten_*) - * ② 机制:net_drain_chip_rx(排空)/ net_relisten_locked(重建一次) - * ③ 对外接口:settle / try / clear / retry_tick + * ② 机制:net_drain_chip_rx(排空)/ net_relisten_locked(重建一次,只由 tick 调) + * ③ 对外接口:settle / request / clear / tick * * 依赖(全部为单向,无反向 include): * net_types.h(net_sock_t)/ net_socket.h(net_get_sock / NET_MAX_SOCKETS / @@ -70,20 +74,21 @@ static uint8_t s_relisten_quarantine[NET_MAX_SOCKETS]; /* 0=免清;1=异常释放;2=正常释放 */ static uint32_t s_relisten_settle_start[NET_MAX_SOCKETS]; /* 隔离起点(0 = 未开始) */ static uint32_t s_relisten_settle_dead[NET_MAX_SOCKETS]; /* 隔离上限到期时刻 */ +static uint8_t s_relisten_capped[NET_MAX_SOCKETS]; /* 1=上限留痕已打(只留首末,防 30ms 刷屏) */ /* ===== TRAP-NET-09 非阻塞重监听重试(D10 修复,2026-09-22 M3 实测) ===== * 旧实现:open 失败 → close → osDelay(500) → 重试,netTask 忙等 500ms(全网冻结) * 且实测恢复 588ms、期间客户端重连被拒 - * 新实现:首次失败即登记待重试,由 netTask 每轮 net_process_messages() 到点重试 + * 新实现:外部只登记请求(net_listener_request),由 netTask 每轮 net_listener_tick 执行 * 恢复时间 ≈ NET_RELISTEN_RETRY_MS 量级,任何时刻都不阻塞任务 */ #define NET_RELISTEN_RETRY_MS 30U /* 重试间隔(TRAP-NET-09 沉降窗口;实测 100ms 首次必成, * 收紧到 30ms 以缩短恢复时间——未成则下一轮到点再来) */ #define NET_RELISTEN_DEADLINE_MS 2000U /* 放弃时限(自首次尝试起) */ -static uint8_t s_relisten_wait[NET_MAX_SOCKETS]; /* 1 = 有待重试的重监听 */ -static uint32_t s_relisten_next[NET_MAX_SOCKETS]; /* 下次重试时刻(tick) */ -static uint32_t s_relisten_dead[NET_MAX_SOCKETS]; /* 放弃时刻(tick) */ +static uint8_t s_relisten_req[NET_MAX_SOCKETS]; /* 0=无请求;非 0=请求来源+1(net_listener_req_t) */ +static uint32_t s_relisten_next[NET_MAX_SOCKETS]; /* 下次尝试时刻(tick;0=立即) */ +static uint32_t s_relisten_dead[NET_MAX_SOCKETS]; /* 放弃时刻(tick;0=尚未起算) */ #if defined(TEST_LATENCY_PROBE) static lat_tmark_t s_relisten_mark[NET_MAX_SOCKETS]; /* 恢复计时起点(首次尝试) */ #endif @@ -133,7 +138,8 @@ static void net_drain_chip_rx(int sockfd) { * 2. 仅将 state 置回 LISTENING,不 memset(fd 与 auto_relisten 保持不变) * 3. 实现 BSD 风格"监听 Socket 常驻",应用层无需手动重监听 * 4. **单次尝试**:失败立即返回(不 osDelay 忙等) - * 5. TRAP-NET-09 沉降交给 net_listener_try / net_listener_retry_tick 非阻塞重试(见下) + * 5. **只由 net_listener_tick 调用**(唯一执行者);TRAP-NET-09 沉降由它的 + * 30ms 节奏重试(见文件末 net_listener_tick) * 6. 旧实现在此 osDelay(500) 忙等,netTask 冻结 500ms、实测恢复 588ms(M3 台架 D10) * 7. N-32 §10:fd 由参数传入(不再用 `p_sock - s_net_socks` 反推), * 控制块经 net_get_sock() 取 —— 本模块**不访问** net_socket.c 的 static 数组 @@ -157,6 +163,8 @@ static int net_relisten_locked(int sockfd) { uint32_t now = HAL_GetTick(); /* 隔离等级(1=异常释放 2=正常释放) */ uint8_t lvl = s_relisten_quarantine[sockfd]; + /* 是否隔离期首轮:留痕只打首轮(E0 结论「重试日志只留首末」,2026-10-14 落地) */ + uint8_t first = (s_relisten_settle_start[sockfd] == 0U) ? 1U : 0U; /* 最短等待由释放来源决定(NET_RELISTEN_SETTLE_*):异常释放 400ms(等在途数据落尽) * 正常释放 100ms(等芯片内部收尾) */ uint32_t hold = (lvl == NET_RELISTEN_SETTLE_QUARANTINE) ? NET_RELISTEN_QUARANTINE_MS @@ -192,23 +200,42 @@ static int net_relisten_locked(int sockfd) { ((uint32_t)(now - s_relisten_settle_start[sockfd]) < hold))) { /* 未到上限:交重试轮询(30ms 后复检) */ if ((int32_t)(now - s_relisten_settle_dead[sockfd]) < 0) { - DBG_INFO("sock%d relisten %s (sock=0x%02X tcp=0x%02X)", - sockfd, tag, st[0], st[1]); + /* 只留首轮一行:实测每次恢复原本要打 3 行同样内容(E0 已定"重试日志 + * 只留首末");末行由下面的 force open 或成功行给出,中间轮次静默 */ + if (first != 0U) { + DBG_INFO("sock%d relisten %s (sock=0x%02X tcp=0x%02X)", + sockfd, tag, st[0], st[1]); + } /* 置 BUSY(结果回传调用方) */ (void)net_set_errno(NET_ERR_BUSY); /* 交重试轮询(30ms 后复检) */ return -1; } - /* 隔离上限已至:强制放行(防监听器永久失效) */ - DBG_INFO("sock%d relisten %s timeout, force open (sock=0x%02X tcp=0x%02X)", - sockfd, tag, st[0], st[1]); + /* 隔离上限已至:**只留痕,不强行放行**(E19 反事实 A/B 实测,2026-10-14): + * 芯片未回 CLOSED 时强行 open 注定吃 USE 错(TRAP-NET-09),放行与不放行的 + * 端口恢复时刻同一分布(+512 vs +505/533 ms)——恢复由"芯片何时回 CLOSED"决定。 + * 故继续留在隔离期边等边排渣,交 D10 重试轮询。**"监听器不会永久失效"的真正 + * 兜底 = D10 预算到期 + 对账形态④ 重新登记**(构建 B 实测:抑制后仍持续重试 + * 并最终恢复),不是这里放行——勿再把本处当兜底。 + * ⚠ 上限留痕只打一次:每 30ms 重试一轮若都留痕会刷屏(构建 B 实测两行/30ms) */ + if (s_relisten_capped[sockfd] == 0U) { + s_relisten_capped[sockfd] = 1U; + DBG_INFO("sock%d relisten %s timeout, keep waiting (sock=0x%02X tcp=0x%02X)", + sockfd, tag, st[0], st[1]); + } + /* 置 BUSY(结果回传调用方) */ + (void)net_set_errno(NET_ERR_BUSY); + /* 交重试轮询(30ms 后复检);隔离标志**不清** → 下轮继续排空残渣 */ + return -1; } - /* 隔离完成(或强制放行) */ + /* 隔离完成(芯片已回 CLOSED 且已满最短等待) */ s_relisten_quarantine[sockfd] = 0U; /* 清隔离起点(回到"未开始") */ s_relisten_settle_start[sockfd] = 0U; /* 清上限时刻 */ s_relisten_settle_dead[sockfd] = 0U; + /* 清上限留痕标志(下一轮隔离可重新留一次痕) */ + s_relisten_capped[sockfd] = 0U; } /* 重置协议类型为 TCP */ @@ -273,8 +300,8 @@ static int net_relisten_locked(int sockfd) { p_sock->recv_bytes = 0; /* auto_relisten 保持为 1,下次断连仍自动重监听 */ - /* 留痕:监听器重建完成 */ - DBG_INFO("sock%d auto-relisten ok", sockfd); + /* 成功留痕由唯一执行者 net_listener_tick 打(带登记来源):本函数此前也打一行, + * 与 tick 的"retry ok"是同一事实的两行(实测每次恢复 2 行 → 合并为 1 行) */ /* 重监听成功 */ return 0; } @@ -288,8 +315,7 @@ static int net_relisten_locked(int sockfd) { * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文;由 handle_disconnect_event / handle_timeout_event 调用 - * 函数说明:1. 只置事实,不做动作 —— 重建由 net_reconcile_tick(形态③④)/ - * net_listener_try 负责 + * 函数说明:1. 只置事实,不做动作 —— 动作只在本模块 net_listener_tick 一处发生 * 2. 越界 sockfd 静默忽略 */ void net_listener_settle(int sockfd, net_listener_settle_t level) { @@ -307,15 +333,15 @@ void net_listener_settle(int sockfd, net_listener_settle_t level) { * 出口参数:无 * 返回值:无 * 限定条件:无 - * 函数说明:1. 清"待重试"标志与重试时刻,fd 关闭/新建时调用(防残留重试) + * 函数说明:1. 清重建请求登记与重试时刻,fd 关闭/新建时调用(防残留请求) */ void net_listener_clear(int sockfd) { /* 描述符越界:直接返回(本函数不设错误码) */ if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { return; } - /* 清"待重试"标志 */ - s_relisten_wait[sockfd] = 0U; + /* 清重建请求登记 */ + s_relisten_req[sockfd] = 0U; /* 清下次重试时刻 */ s_relisten_next[sockfd] = 0U; /* 清放弃时刻 */ @@ -324,6 +350,8 @@ void net_listener_clear(int sockfd) { s_relisten_settle_dead[sockfd] = 0U; /* 清沉降起点 */ s_relisten_settle_start[sockfd] = 0U; + /* 清上限留痕标志(防同 fd 下一生成继承"已留痕"而静默) */ + s_relisten_capped[sockfd] = 0U; /* 一并清隔离标志:关闭/重建即确定性释放, * 防同 fd 下一生成继承上轮的 400/800ms * 隔离等待(2026-09-24 台架发现跨轮残留) */ @@ -348,98 +376,104 @@ static void net_relisten_done(int sockfd) { } /* - * 函数功能:尝试重建监听一次(**不阻塞**);失败则登记"待重试"(契约见 net_listener.h) + * 函数功能:登记「本 fd 期望回到 LISTEN」(**只登记意图,不碰芯片**;契约见 net_listener.h) * 入口参数:sockfd - Socket 描述符 (0~7) int + * reason - 登记来源(net_listener_req_t,仅用于留痕归因) * 出口参数:无 - * 返回值:0 - 成功(监听器已就绪);-1 - 本次失败或已在重试队列中 int - * 限定条件:netTask 上下文;fd 为监听型(auto_relisten) - * 函数说明:1. 已在重试队列时直接返回 -1(不重复尝试、不重置重试时刻)—— - * 否则每 10ms 一次的 accept 轮询会不断把重试时刻往后推, - * 导致永不重试(重试饥饿) - * 2. N-32 §10:替代此前散落在 net_socket.c 各处的 net_relisten_try() + * 返回值:无 + * 限定条件:netTask 上下文(API 路径与自愈 tick 内) + * 函数说明:1. **取代原 net_listener_try**:try 是"当场动作",于是每个调用点都得 + * 自带状态门(U12 事故正是四处各防各的、漏一处就把活监听拆掉); + * request 只置意图,门集与"芯片在听就不许 open"收进 net_listener_tick + * 2. 已在请求中:不动重试时刻 —— 否则 10ms 一次的 accept 轮询会不断把 + * 重试时刻往后推 → 永不重试(重试饥饿,原 try 的同名防护保留) + * 3. 非阻塞:实际重建发生在下一轮 net_listener_tick(≤1ms) + * 4. 越界 sockfd 静默忽略(无返回值,不设错误码) */ -int net_listener_try(int sockfd) { - /* 当前 tick */ - uint32_t now = HAL_GetTick(); - - /* 描述符越界 → BADF */ +void net_listener_request(int sockfd, net_listener_req_t reason) { + /* 描述符越界:静默忽略 */ if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { - return NET_ERR_RETURN_1(NET_ERR_BADF); + return; } - /* 已在重试队列:不重复尝试、不重置时刻(防重试饥饿) */ - if (s_relisten_wait[sockfd] != 0U) { - (void)net_set_errno(NET_ERR_BUSY); - /* 已在重试队列:由 tick 负责 */ - return -1; + /* 已在请求中:保持既有节奏,不重置(防重试饥饿) */ + if (s_relisten_req[sockfd] != 0U) { + return; } -#if defined(TEST_LATENCY_PROBE) - /* 恢复计时自首次尝试起 */ - lat_mark(&s_relisten_mark[sockfd]); -#endif - /* 本次尝试成功 */ - if (net_relisten_locked(sockfd) == 0) { - /* 标记完成(清状态 + 探针样本) */ - net_relisten_done(sockfd); - /* 监听器已就绪 */ - return 0; - } - /* 登记"待重试" */ - s_relisten_wait[sockfd] = 1U; - /* 下次重试时刻 = now + 30ms */ - s_relisten_next[sockfd] = now + NET_RELISTEN_RETRY_MS; - /* 放弃时刻 = now + 2s */ - s_relisten_dead[sockfd] = now + NET_RELISTEN_DEADLINE_MS; - /* 本次失败(重试交 tick) */ - return -1; + /* 登记来源(+1:0 保留给"无请求") */ + s_relisten_req[sockfd] = (uint8_t)((uint8_t)reason + 1U); + /* 立即尝试(下一轮 tick);放弃时限自首次尝试起算,故此处清零 */ + s_relisten_next[sockfd] = 0U; + s_relisten_dead[sockfd] = 0U; } /* - * 函数功能:D10 重试轮询(netTask 每轮调用一次,非阻塞) + * 函数功能:监听重建的**唯一执行者**(netTask 每轮调用一次,非阻塞) * 入口参数:无 * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文(由 net_process_messages 每轮调用) - * 函数说明:1. 到点重试待办的重监听;成功即报告探针样本 - * 2. 本 fd 有在途挂起 recv / 挂起 send 时跳过(net_pending_active / - * net_send_pending_active,挂起保险:重开 Socket 会摧毁在途传输) - * 3. 超过 NET_RELISTEN_DEADLINE_MS 停止 D10 快速重试并留痕 - * (监听不永久失效——reconciler 100ms 周期仍按"芯片不在听"认领重建) + * 函数说明:1. 遍历已登记请求的 fd,过**一处**门集后执行 open+listen;原名 + * net_listener_retry_tick(只做 D10 到点重试),现在它是唯一执行路径 + * 2. 门集(此前散在 4 个调用点,各防各的):控制块有效 + in_use + + * 状态 ∈ {LISTENING, CLOSED}(0-4 口径:已转 ESTABLISHED = 新会话抢先, + * 继续跑会 open→close 摧毁活连接)+ 监听型(auto_relisten / STREAM / + * local_port≠0,与原 pump 认领口径一致) + * 3. **芯片已在 LISTEN = 请求已达成,静默了结、绝不发 open**:U12 事故 + * (对在听新 SYN 的 Socket 再 open → 返 USE → 失败分支 close → 当场 + * 拆掉活监听)的结构性防护;读驱动状态缓存(net_poll 每轮刷新),零 SPI + * 4. 挂起保险:本 fd 有在途挂起 recv / send 时跳过(重开 Socket 会摧毁 + * 在途传输,net_pending_active / net_send_pending_active) + * 5. 节奏 30ms / 预算 2s(自首次尝试起);超预算清登记并留痕——监听不会 + * 永久失效,reconciler 100ms 周期仍按"芯片不在听"重新登记(形态③④) */ -void net_listener_retry_tick(void) { +void net_listener_tick(void) { /* 当前 tick */ uint32_t now = HAL_GetTick(); /* Socket 循环索引 */ int i; + /* 登记来源名(留痕分辨是谁登记的;与 net_listener_req_t 同序) */ + static const char *const k_req_name[] = { "recon", "accept", "eof", "drop", "pending" }; for (i = 0; i < NET_MAX_SOCKETS; i++) { - /* 本 fd 无待重试重监听:跳过 */ - if (s_relisten_wait[i] == 0U) { + /* 本 fd 无重建请求:跳过 */ + if (s_relisten_req[i] == 0U) { continue; } - /* 0-4(自愈收敛阶段0)状态门:仅本层仍 LISTENING/CLOSED 才重试。若已转 - * ESTABLISHED(新会话抢先到达),在途重试登记即陈旧——继续跑会 - * open→close 摧毁活连接(认领口径与 reconciler 形态③④ 一致) */ + /* --- 门集:不满足即"登记已陈旧",清掉让位 --- */ { /* 取控制块 */ const net_sock_t *p_sock = net_get_sock(i); - if ((p_sock == NULL) || + /* 控制块失效 / 未在用 / 状态不允许 / 非监听型 fd:陈旧登记 */ + if ((p_sock == NULL) || (p_sock->in_use == 0U) || ((p_sock->state != NET_SOCK_STATE_LISTENING) && - (p_sock->state != NET_SOCK_STATE_CLOSED))) { - /* 陈旧登记:清重试状态让位 */ + (p_sock->state != NET_SOCK_STATE_CLOSED)) || + (p_sock->auto_relisten == 0U) || + (p_sock->type != NET_SOCK_STREAM) || + (p_sock->local_port == 0U)) { + /* 陈旧登记:清状态让位 */ net_listener_clear(i); continue; } + /* 芯片已在听:请求已达成(见函数说明 3),静默了结 */ + { + ch395f_sock_state_t c = ch395f_sock_state_cached((uint8_t)i); + if ((c.open != CH395F_SOCKET_CLOSED) && + (c.tcp_state == CH395F_TCP_LISTEN)) { + net_listener_clear(i); + continue; + } + } } /* 本 fd 有在途挂起 recv / 挂起 send:重开 Socket 会摧毁在途连接(挂起保险) */ if ((net_pending_active(i) != 0U) || (net_send_pending_active(i) != 0U)) { continue; } - /* 未到点(tick 差值按有符号比较,抗回绕) */ - if ((int32_t)(now - s_relisten_next[i]) < 0) { + /* 未到点(tick 差值按有符号比较,抗回绕;0 = 立即尝试) */ + if ((s_relisten_next[i] != 0U) && ((int32_t)(now - s_relisten_next[i]) < 0)) { continue; } - /* 已到放弃时限 */ - if ((int32_t)(now - s_relisten_dead[i]) >= 0) { + /* 已到放弃时限(0 = 尚未起算,见下) */ + if ((s_relisten_dead[i] != 0U) && ((int32_t)(now - s_relisten_dead[i]) >= 0)) { /* 清重试状态 */ net_listener_clear(i); /* D10 快速重试预算耗尽留痕:后续交 reconciler 形态③④ 周期接管 @@ -448,14 +482,26 @@ void net_listener_retry_tick(void) { DBG_ERROR("sock%d relisten retry budget exhausted, reconciler takes over", i); continue; } - /* 重试成功 */ + /* 首次尝试:起算放弃时限与恢复计时起点 */ + if (s_relisten_dead[i] == 0U) { + s_relisten_dead[i] = now + NET_RELISTEN_DEADLINE_MS; +#if defined(TEST_LATENCY_PROBE) + /* 恢复计时自首次尝试起 */ + lat_mark(&s_relisten_mark[i]); +#endif + } + /* 执行一次重建(本函数是 net_relisten_locked 的唯一调用者) */ if (net_relisten_locked(i) == 0) { + /* 登记来源名(越界兜底:枚举扩充时不得越界读) */ + uint8_t idx = (uint8_t)(s_relisten_req[i] - 1U); + const char *name = (idx < (uint8_t)(sizeof(k_req_name) / sizeof(k_req_name[0]))) + ? k_req_name[idx] : "?"; + /* 留痕:监听器重建完成(带登记来源;成功留痕已从 net_relisten_locked 收并到此一处) */ + DBG_INFO("sock%d auto-relisten ok (%s)", i, name); /* 标记完成(清状态 + 探针样本) */ net_relisten_done(i); - /* 留痕:重试成功 */ - DBG_INFO("sock%d relisten retry ok", i); } else { - /* 仍未成:置下次重试时刻 */ + /* 仍未成:顺延 30ms 再来 */ s_relisten_next[i] = now + NET_RELISTEN_RETRY_MS; } } diff --git a/Drivers/BSP/NET/net_listener.h b/Drivers/BSP/NET/net_listener.h index 86cee7db..069c7990 100644 --- a/Drivers/BSP/NET/net_listener.h +++ b/Drivers/BSP/NET/net_listener.h @@ -2,13 +2,17 @@ * 模块名称:Network Listener Lifecycle (Header) * 模块功能:监听器生命周期模块的对外接口(**真机专属**)—— 维持 fd 处于 * LISTEN 态的全部机制:重建、重建前的沉降/隔离(TRAP-NET-18)、排空 RX - * 残渣、D10 非阻塞重试(周期重建决策者=自愈收敛阶段 3 合并后的 - * net_reconcile_tick,见 net_socket.c)。 + * 残渣、D10 非阻塞重试。**本模块是"把 fd 放回 LISTEN"的唯一执行者**: + * 外部只登记请求(net_listener_request),动作只在 net_listener_tick 一处发生; + * "该不该重建"的周期判据仍在 net_reconcile_tick(形态③④,见 net_socket.c)。 * 适用平台:STM32F407ZGTx + CH395F 以太网芯片 * 作者:王建锋 * 创建日期:2026-10-12 * 修改记录: * 2026-10-12 王建锋 从 net_socket.c 抽出监听生命周期(N-32 §10 模块化) + * 2026-10-14 王建锋 单一所有者收口:net_listener_try(各调用点自带门、当场动作) + * 改为 net_listener_request(只登记意图)+ net_listener_tick + * (唯一执行者,门集与芯片在听判据集中一处) * * 职责边界(详见 docs/网络/网络自愈架构设计.md §11): * 本模块只负责「让一个监听型 fd 保持/回到 LISTEN 态」。 @@ -47,24 +51,38 @@ typedef enum { * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文;由 handle_disconnect_event / handle_timeout_event 调用 - * 函数说明:1. 只置事实,不做动作 —— 重建由 net_reconcile_tick(形态③④)/ - * net_listener_try 负责 + * 函数说明:1. 只置事实,不做动作 —— 动作只在 net_listener_tick 一处发生 * 2. 越界 sockfd 静默忽略(不设错误码) */ void net_listener_settle(int sockfd, net_listener_settle_t level); /* - * 函数功能:尝试重建监听一次(**不阻塞**);失败则登记"待重试" - * 入口参数:sockfd - Socket 描述符 (0~7) int - * 出口参数:无 - * 返回值:0 - 成功(监听器已就绪);-1 - 本次失败或已在重试队列中 int - * 限定条件:netTask 上下文;fd 为监听型(auto_relisten) - * 函数说明:1. 已在重试队列时直接返回 -1(不重复尝试、不重置重试时刻)—— - * 否则每 10 ms 一次的 accept 轮询会不断把重试时刻往后推, - * 导致永不重试(重试饥饿) - * 2. 替代此前散落在 net_socket.c 各处的 net_relisten_try() + * 重建请求来源:只用于留痕归因(日志里分辨是谁登记的),不改变动作 + * —— 单一所有者的意义正在于此:来源再多,执行只有一条路径、一套门 */ -int net_listener_try(int sockfd); +typedef enum { + NET_LISTENER_REQ_RECON = 0U, /* 对账形态③④:芯片侧监听失位 / 孤儿监听 */ + NET_LISTENER_REQ_ACCEPT = 1U, /* 消费者 accept 撞上 CLOSED 的监听 fd */ + NET_LISTENER_REQ_EOF = 2U, /* 消费者 recv 读到 EOF */ + NET_LISTENER_REQ_DROP = 3U, /* net_drop_conn 拆会话保监听 */ + NET_LISTENER_REQ_PENDING = 4U, /* 挂起 recv 因断连结束 */ +} net_listener_req_t; + +/* + * 函数功能:登记「本 fd 期望回到 LISTEN」(**只登记意图,不碰芯片**) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * reason - 登记来源(net_listener_req_t,仅留痕归因) + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文(API 路径与自愈 tick 内) + * 函数说明:1. **取代原 net_listener_try**:try 是"当场动作",故每个调用点都得 + * 自带状态门(U12 事故就是这么四处各防各的);request 只置意图, + * 门集与"芯片已在听就不许再 open"的判据收进 net_listener_tick 一处 + * 2. 已在请求中时**不动重试时刻**:否则 10ms 一次的 accept 轮询会把 + * 重试时刻不断往后推 → 永不重试(重试饥饿,原 try 的同名防护保留) + * 3. 非阻塞:实际重建在下一轮 net_listener_tick(≤1ms)里做 + */ +void net_listener_request(int sockfd, net_listener_req_t reason); /* * 函数功能:清除本 fd 的待重试/沉降状态(关闭或新建 fd 时调用) @@ -77,18 +95,29 @@ int net_listener_try(int sockfd); void net_listener_clear(int sockfd); /* - * 函数功能:D10 重试轮询(netTask 每轮调用,非阻塞) + * 函数功能:监听重建的**唯一执行者**(netTask 每轮调用,非阻塞) * 入口参数:无 * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文(由 net_process_messages 每轮调用) - * 函数说明:到点重试待办的重监听;成功即报告探针样本;超时放弃并记错误日志 + * 函数说明:1. 遍历已登记请求的 fd,按**一处**门集裁决后执行 open+listen: + * 控制块有效 + in_use + 状态 ∈ {LISTENING, CLOSED} + 监听型 + * (auto_relisten / STREAM / local_port≠0)+ 无在途挂起 recv/send + * + 到点(30ms 节奏)+ 未超预算(2s) + * 2. **芯片已在 LISTEN 即视为请求已达成**(读驱动状态缓存,零 SPI)—— + * 这是 U12 事故的结构性防护:绝不对正在听新 SYN 的 Socket 再 open + * (旧形态下 open 返 USE → 失败分支 close → 当场拆掉活监听) + * 3. 原名 net_listener_retry_tick(只做 D10 到点重试);现在它是唯一 + * 执行路径,故按同族 tick 命名(net_pending_tick / net_reconcile_tick) + * 4. 超预算清登记并 [ERR] 留痕:监听不会永久失效——reconciler 100ms + * 周期仍会按"芯片不在听"重新登记(形态③④) */ -void net_listener_retry_tick(void); +void net_listener_tick(void); /* 自愈收敛阶段 3:原 `net_listener_pump`(周期重建决策者)已并入 net_socket.c 的 * net_reconcile_tick 单一 100ms tick(判据读驱动状态缓存,《网络自愈架构设计》§4.1)。 - * 本模块的 net_listener_try 由 reconciler 形态③④ 与 D10 重试轮询调用。 */ + * 本模块的 net_listener_tick 是执行者,登记方 = reconciler 形态③④ + 各消费者路径 + * (accept / recv EOF / drop_conn / 挂起 recv 断连),全部走 net_listener_request。 */ /* 注:本模块消费的宿主能力 `net_pending_active()` / `net_send_pending_active()` * 声明在 net_pending.h(它们是 net_pending.c 的函数,须与配对头文件同处, diff --git a/Drivers/BSP/NET/net_pending.c b/Drivers/BSP/NET/net_pending.c index 4e1575ce..74ffeade 100644 --- a/Drivers/BSP/NET/net_pending.c +++ b/Drivers/BSP/NET/net_pending.c @@ -6,7 +6,7 @@ * net_types.h(net_sock_t / NET_ERR_* / NET_SOCK_STATE_*)/ * net_socket.h(NET_MAX_SOCKETS / net_set_errno / NET_ERR_RETURN_1 / * net_pending_active 声明)/ net_pending.h(本模块接口)/ - * net_listener.h(net_listener_try)/ ch395f.h(RX 读出 / TX 分块写)/ main.h(HAL_GetTick) + * net_listener.h(net_listener_request / net_listener_tick)/ ch395f.h(RX 读出 / TX 分块写)/ main.h(HAL_GetTick) * 限定条件:本文件真机专属——模拟器走 sim/net_socket_sim.c, * simulator/CMakeLists.txt 不编译本文件(同 net_listener.c 先例) * @@ -255,17 +255,16 @@ void net_pending_tick(net_sock_t *p_socks) { /* 结束:以字节数通知 */ net_pending_complete(i, rd); } else { - /* 启用自动重监听:先把监听器重新置回 LISTEN。 - * ⚠ 内层 `state == CLOSED` 门同 net_recv_locked:session_dead - * 但已回 LISTENING 时重建已由别处完成,再 open+listen 会因 - * USE 错误走进关闭分支、关掉正听着的 Socket(U12 同源)。 - * N-32 P2 修正:**保留本处即时重建** —— 它只在有**挂起 recv** + /* 启用自动重监听:登记重建请求(动作在 net_listener_tick 一处)。 + * 原内层 `state == CLOSED` 门已**上收到 tick**——它防的是"对已回 + * LISTENING 的 fd 再 open+listen → 因 USE 错误走进关闭分支、关掉 + * 正听着的 Socket"(U12 同源),属执行侧安全不变量,不由登记点复述。 + * N-32 P2 修正:**保留本处即时登记** —— 它只在有**挂起 recv** * 时到达(`p->active != 0`),属"消费者在等这条连接结局"的 * 消费者驱动路径,不是周期性抢先(同 net_recv_locked 的理由)。 */ - if ((p_sock->state == NET_SOCK_STATE_CLOSED) && - (p_sock->auto_relisten != 0U)) { - /* 非阻塞:失败由 D10 重试轮询接管(N-32 §10:入口收敛到 net_listener_try) */ - (void)net_listener_try(i); + if (p_sock->auto_relisten != 0U) { + /* 非阻塞:下一轮 net_listener_tick 执行(入口收敛到 request) */ + net_listener_request(i, NET_LISTENER_REQ_PENDING); } /* TRAP-NET-18 待办 #3:异常释放(TIMEOUT/RST)不是正常 EOF(对端未发正常 FIN) * → 按连接重置结束:应用(lftpd STOR)对异常结束回 5xx 而非误报 226; @@ -313,7 +312,7 @@ void net_pending_tick(net_sock_t *p_socks) { * 出口参数:无 * 返回值:1 - 有挂起;0 - 无(含描述符越界) int * 限定条件:任意上下文(纯 RAM 读,不发起 RPC) - * 函数说明:1. N-32 §10 模块化:net_listener_retry_tick 用它跳过"重开 Socket + * 函数说明:1. N-32 §10 模块化:net_listener_tick 用它跳过"重开 Socket * 会摧毁在途连接"的 fd(挂起 recv 保险,原为直接读 s_net_pending[i].active) * 2. 声明在 net_pending.h(配对头,规范 S17 2.2) */ @@ -675,7 +674,7 @@ void net_send_pending_cancel_all(int err) { * 出口参数:无 * 返回值:1 - 有挂起;0 - 无(含描述符越界) int * 限定条件:任意上下文(纯 RAM 读,不发起 RPC) - * 函数说明:net_listener_retry_tick 用它跳过"重开 Socket 会摧毁在途传输"的 fd + * 函数说明:net_listener_tick 用它跳过"重开 Socket 会摧毁在途传输"的 fd * (挂起 send 保险;与 net_pending_active 的挂起 recv 保险并列) */ int net_send_pending_active(int sockfd) { diff --git a/Drivers/BSP/NET/net_pending.h b/Drivers/BSP/NET/net_pending.h index d9d1b5e6..96e5176c 100644 --- a/Drivers/BSP/NET/net_pending.h +++ b/Drivers/BSP/NET/net_pending.h @@ -123,7 +123,7 @@ void net_pending_settle_close(int sockfd, net_sock_t *p_sock); * 出口参数:无 * 返回值:1 - 有挂起;0 - 无(含描述符越界) int * 限定条件:任意上下文(纯 RAM 读,不发起 RPC) - * 函数说明:N-32 §10 模块化:net_listener_retry_tick 用它跳过"重开 Socket + * 函数说明:N-32 §10 模块化:net_listener_tick 用它跳过"重开 Socket * 会摧毁在途连接"的 fd(挂起 recv 保险) */ int net_pending_active(int sockfd); @@ -207,7 +207,7 @@ void net_send_pending_cancel_all(int err); * 出口参数:无 * 返回值:1 - 有挂起;0 - 无(含描述符越界) int * 限定条件:任意上下文(纯 RAM 读,不发起 RPC) - * 函数说明:与 net_pending_active 同用途——net_listener_retry_tick 据此跳过 + * 函数说明:与 net_pending_active 同用途——net_listener_tick 据此跳过 * "重开 Socket 会摧毁在途传输"的 fd(挂起 send 保险) */ int net_send_pending_active(int sockfd); diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 9e2c02d3..22dc07be 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -780,11 +780,11 @@ static void net_reconcile_tick(void) { /* 挂起 send 同样取消(会话已被芯片收走,剩余字节不可能送达) */ net_send_pending_cancel(i, NET_ERR_CONNRESET); } else if ((bad == 3) || (bad == 4)) { - /* 类型③④:监听失位/孤儿监听 → 重建(本层唯一重建入口; - * 非阻塞:失败由 D10 重试轮询接管) */ + /* 类型③④:监听失位/孤儿监听 → **登记**重建请求(判据在此,动作在 + * net_listener_tick 一处;非阻塞,30ms 节奏重试) */ DBG_INFO("sock%d reconcile: chip not listening (0x%02X/0x%02X, was state=%d) -> relisten", i, c.open, c.tcp_state, (int)p_sock->state); - (void)net_listener_try(i); + net_listener_request(i, NET_LISTENER_REQ_RECON); } else { /* 类型②:补做 standalone accept */ DBG_INFO("sock%d reconcile: connection completed w/o CONNECT irq -> accept", i); @@ -806,8 +806,8 @@ static void net_reconcile_tick(void) { * 限定条件:必须在 netTask 上下文中调用(独占 CH395F 与 s_net_socks 访问权); * 由 net_task.c 在 net_stack_bring_up 之后每次 1ms 循环调用。 * 函数说明:1. 四个自愈 tick 均非阻塞(绝不 osDelay 忙等,netTask 永不冻结),先于消息分派执行 - * 2. net_listener_retry_tick(D10):断连后按 30ms 节拍重试 open+listen, - * 2s 时限到期放弃留痕;本 fd 有在途挂起 recv / send 时跳过(挂起保险) + * 2. net_listener_tick(监听重建**唯一执行者**):消费各路径登记的请求, + * 30ms 节奏 / 2s 预算;门集与"芯片在听就不 open"(U12 防护)都在这一处 * 3. net_pending_tick:按节拍结束挂起 recv(数据/到点[仅显式预算]/断连) * 4. net_send_pending_tick:按节拍推进挂起 send(断连/推进/写完/无进展超时) * 5. net_reconcile_tick(TRAP-NET-17 + pump 合并,自愈收敛阶段 3):100ms 对账 @@ -825,8 +825,8 @@ void net_process_messages(void) { } /* 四个非阻塞自愈 tick 置于消息处理之前:断连后即使无应用消息,监听器也能自主恢复 */ - /* D10 重监听重试(TRAP-NET-09):沉降后重开芯片监听器 */ - net_listener_retry_tick(); + /* 监听重建唯一执行者(TRAP-NET-09 D10 节奏;消费各路径登记的请求) */ + net_listener_tick(); /* 挂起 recv 完成检查:数据到达 / 到点 / 断连时通知调用方 */ net_pending_tick(s_net_socks); /* 挂起 send 推进与完成检查(SEND pending):断连 / 推进写块 / 写完 / 无进展超时 */ @@ -1516,13 +1516,13 @@ static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen drain_pending = 1; DBG_INFO("sock%d closed-with-data accept (drain pending)", sockfd); } else { - /* 连接已断开(CLOSED)且无残留数据:先把 Socket 重新置回 LISTEN, - * 下一次 accept 即可等待新连接(BSD 风格监听常驻)。 - * N-32 P2 修正:**保留本处即时重建** —— 它由消费者调 accept 触发 + /* 连接已断开(CLOSED)且无残留数据:登记重建请求,下一次 accept + * 即可等待新连接(BSD 风格监听常驻)。 + * N-32 P2 修正:**保留本处即时登记** —— 它由消费者调 accept 触发 * (消费者正在等新连接),不是周期性抢先;删掉会把 CLOSED 窗口 * 拉长、导致调用方高频空转 accept(实测 [WD] task stall)。 - * 非阻塞:失败由 D10 重试轮询接管 */ - (void)net_listener_try(sockfd); + * 单一所有者收口后:动作发生在下一轮 net_listener_tick(≤1ms),非阻塞 */ + net_listener_request(sockfd, NET_LISTENER_REQ_ACCEPT); } } if (drain_pending == 0) { @@ -2155,20 +2155,20 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, * 有数据时走上方快路径(recv_len>0 优先),本判据只在"无数据"时生效, * 故不会把新连接的起始数据误判为 EOF。 */ if ((p_sock->state == NET_SOCK_STATE_CLOSED) || (p_sock->session_dead != 0U)) { - /* 启用自动重监听:先把监听器重新置回 LISTEN。 - * ⚠ 内层 `state == CLOSED` 门是**必需的**,不是冗余: - * `net_listener_try` 无状态门,对已 LISTENING 的 fd 再 open+listen - * 会让 ch395f_open_socket 返 USE,其失败分支会 ch395f_close_socket - * —— 把正听着新 SYN 的 Socket 当场关掉(U12 同源事故)。 - * session_dead + 已回 LISTENING = 重建已由别处完成,这里只报 EOF。 - * N-32 P2 修正(2026-10-12 台架实证):**保留本处即时重建**。 + /* 启用自动重监听:登记重建请求(动作在 net_listener_tick 一处)。 + * ⚠ 原此处内层 `state == CLOSED` 门已**上收到 tick**:它防的是"对已 + * LISTENING 的 fd 再 open+listen → ch395f_open_socket 返 USE → 失败分支 + * ch395f_close_socket → 把正听着新 SYN 的 Socket 当场关掉"(U12 同源 + * 事故)。这是**执行侧安全不变量**,不该由每个登记点各自复述—— + * 漏一处即事故;收口后 tick 另加"芯片已在听即了结",结构上不可能再犯。 + * N-32 P2 修正(2026-10-12 台架实证):**保留本处即时登记**—— * 它是"消费者动作触发"(消费者正在等这条连接的结局),不是周期性 - * "抢先";删掉它会把 CLOSED 窗口从 ~0 拉长到 pump 周期+隔离期 + * "抢先";删掉它会把 CLOSED 窗口从 ~0 拉长到 reconciler 周期+隔离期 * (~100ms),期间调用方高频发起 net_accept(RPC 无上界)→ - * `[WD] task stall`(实测 uis 往返 1063ms)。周期性抢先由 reconciler 收敛即可。 */ - if ((p_sock->state == NET_SOCK_STATE_CLOSED) && (p_sock->auto_relisten != 0U)) { - /* 非阻塞:失败由 D10 重试轮询接管 */ - (void)net_listener_try(sockfd); + * `[WD] task stall`(实测 uis 往返 1063ms)。周期性兜底由 reconciler 收敛。 */ + if (p_sock->auto_relisten != 0U) { + /* 非阻塞:下一轮 net_listener_tick 执行,30ms 节奏重试 */ + net_listener_request(sockfd, NET_LISTENER_REQ_EOF); } /* EOF:返回 0(非错误) */ return 0; @@ -2247,9 +2247,10 @@ static int net_close_locked(net_sock_t *p_sock) { * "拆连接不拆监听"的应用侧形态 * 2. 拆除=sock_reset QUARANTINE(TRAP-NET-18 异常释放语义:FIN + 排渣 + * 排空事件(quiescent) + 等 byte0=00H,内置二段 CLOSE 补发) - * 3. auto_relisten(监听型 fd):收口 CLOSED 后即时 net_listener_try - * (芯片已 CLOSED 直接 open 必成,E4 定案;失败交 D10 重试 + - * reconciler 形态④);非 auto_relisten(client fd):收口 CLOSED, + * 3. auto_relisten(监听型 fd):收口 CLOSED 后登记重建请求(动作在 + * net_listener_tick,≤1ms 后执行;芯片已 CLOSED 直接 open 必成, + * E4 定案;未成则 30ms 节奏重试 + reconciler 形态④兜底); + * 非 auto_relisten(client fd):收口 CLOSED, * 是否重连由调用方决定 * 4. 挂起 recv 先结束(settle_close:排空尝试或取消,必通知) */ @@ -2275,8 +2276,8 @@ static int net_drop_conn_locked(net_sock_t *p_sock) { p_sock->lost_conn_seen = 0U; if (p_sock->auto_relisten) { - /* 监听型 fd:即时重建(非阻塞;失败 D10 重试轮询接管) */ - (void)net_listener_try(sockfd); + /* 监听型 fd:登记重建请求(非阻塞;下一轮 net_listener_tick 执行) */ + net_listener_request(sockfd, NET_LISTENER_REQ_DROP); } /* 拆除完成 */ diff --git a/docs/images/Net层阻塞模型.svg b/docs/images/Net层阻塞模型.svg index 74eca71d..c8db9be2 100644 --- a/docs/images/Net层阻塞模型.svg +++ b/docs/images/Net层阻塞模型.svg @@ -53,7 +53,7 @@ net_send_pending_tick() 推进挂起 send -net_listener_retry_tick() +net_listener_tick() 重试对账 排空消息队列 diff --git a/docs/任务规划/登记簿.md b/docs/任务规划/登记簿.md index 6f39d341..029bd38f 100644 --- a/docs/任务规划/登记簿.md +++ b/docs/任务规划/登记簿.md @@ -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. 存储 / 日志管线 diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 8bd9ed43..e46b391c 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| 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%** | 各区的容量与常量来源: @@ -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) | diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index eac40606..71ec7bca 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,14 +15,14 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤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 | 堆=配额 | diff --git a/docs/测试说明/链路延迟测试说明.md b/docs/测试说明/链路延迟测试说明.md index b9de1881..95586c04 100644 --- a/docs/测试说明/链路延迟测试说明.md +++ b/docs/测试说明/链路延迟测试说明.md @@ -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 轮换 | diff --git a/docs/网络/BSD_Socket_API_使用指南.md b/docs/网络/BSD_Socket_API_使用指南.md index a859ce45..8f47286e 100644 --- a/docs/网络/BSD_Socket_API_使用指南.md +++ b/docs/网络/BSD_Socket_API_使用指南.md @@ -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) diff --git a/docs/网络/BSD_Socket_API_函数调用关系.md b/docs/网络/BSD_Socket_API_函数调用关系.md index 82c4a865..0830d919 100644 --- a/docs/网络/BSD_Socket_API_函数调用关系.md +++ b/docs/网络/BSD_Socket_API_函数调用关系.md @@ -62,7 +62,7 @@ flowchart TB LR["netRecvLocked
快路径 / DONTWAIT / 登记挂起"] LA["netAcceptLocked
先 netPoll + 状态判定"] LC["netCloseLocked
结束挂起 + cmd_done 快路径 / sock_reset(GRACEFUL)"] - DC["netDropConnLocked
拆会话保监听:sock_reset(QUARANTINE) + 即时重监听"] + DC["netDropConnLocked
拆会话保监听:sock_reset(QUARANTINE) + 登记重监听"] RLS["netListenerTry → netRelistenLocked
单次尝试 + TRAP-NET-18 排渣隔离"] end EV["事件处理器
handleConnect · handleDisconnect
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` | diff --git a/docs/网络/Net层阻塞模型架构设计.md b/docs/网络/Net层阻塞模型架构设计.md index b297b6f0..1eab1db4 100644 --- a/docs/网络/Net层阻塞模型架构设计.md +++ b/docs/网络/Net层阻塞模型架构设计.md @@ -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`) | diff --git a/docs/网络/网络自愈架构设计.md b/docs/网络/网络自愈架构设计.md index 7a6e333b..66a22089 100644 --- a/docs/网络/网络自愈架构设计.md +++ b/docs/网络/网络自愈架构设计.md @@ -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 后端 diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index 2df4224c..e333f0b2 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -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 已结案为非缺陷(判读教训见第六章台架纪律)。现场"上传失败/文件不完整/像卡死"类报告优先用本条判据复核。 diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 4f4a0f70..936b2d3f 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -1,4 +1,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-12 209492 18332 1104 154184 227824 21.7 97864 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;console_log.o=4120;ch395f.o=4106;default_task.o=2736;sys_log_mcu.o=1924 2026-10-13 211456 18760 1104 154232 230216 22.0 97912 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924 -2026-10-14 212304 18680 1104 154456 230984 22.0 98136 74.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924 +2026-10-14 212316 18740 1112 154456 231056 22.0 98144 74.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924 diff --git a/tools/arch_diagram/gen_net_pending_svg.py b/tools/arch_diagram/gen_net_pending_svg.py index 329584b0..aee5e7e6 100644 --- a/tools/arch_diagram/gen_net_pending_svg.py +++ b/tools/arch_diagram/gen_net_pending_svg.py @@ -76,7 +76,7 @@ LOOP_STEPS = [ ("net_poll()", "刷新 readable", C_CHIP), ("net_pending_tick()", "结束挂起 recv", C_TICK), ("net_send_pending_tick()", "推进挂起 send", C_TICK), - ("net_listener_retry_tick()", "重试对账", C_ACTOR), + ("net_listener_tick()", "重试对账", C_ACTOR), ("排空消息队列", "服务所有消费者", "#64748b"), ]