From f849c79b7a37ce991dd77537210835a3f47c6225 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Wed, 14 Oct 2026 12:05:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(NET):=20SEND=20pending=20=E4=BA=8C?= =?UTF-8?q?=E6=9C=9F=E8=90=BD=E5=9C=B0=20+=20=E6=9C=AB=E5=9D=97=E6=8C=82?= =?UTF-8?q?=E8=B5=B7=E8=B6=8A=E7=95=8C=E7=BC=BA=E9=99=B7=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=EF=BC=88=E5=8F=B0=E6=9E=B6=E5=A4=8D=E6=B5=8B=E9=97=AD=E7=8E=AF?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit net_pending.c/h 增设第二张挂起表 s_net_send[](与 recv 表完全独立,收发 互不阻塞);net_send_locked 快速路径改为有界自旋,单次等待超出 NET_SEND_SPIN_MS(2ms) 即登记挂起 send,由 net_send_pending_tick 按主循环 节拍分块推进,netTask 最坏停滞由 1s 压到 2ms。net_listener_retry_tick 的 挂起保险扩展到 send 侧;net_close_locked / 网络重建 / 状态对账三路均先 取消再动作(取消必通知,调用方永不挂死)。 台架复测(A/B 对照 + 插桩定位)发现并修复末块挂起越界缺陷:内层"等本块 发完(SENBUF_FREE)"位于写块之后,写完末块时 sent 已 == len,若该次等待越过 2ms 预算便会调用 net_send_pending_register,而登记函数按 sent >= len 拒收 并回 -1,把一次实际已完整交付芯片的发送报成失败。修复为内层等待开头判 sent >= len 即跳出(末块已交付、SENBUF_FREE 等待是纯保守自旋),并置于 超时/自旋判定之前;该守卫同时保证后续登记必伴随 sent < len。 复验:ftp_regression 连续 19 次 11/11;ftp_integrity_test 20 轮 x 64KB STOR/RETR SHA-256 20/20 一致(两轮),吞吐稳定约 80KB/s;设备日志零 write error;ST-LINK 回读 No difference found。 文档:Net层阻塞模型架构设计.md 补二期设计与新增 9.1 缺陷记录,netTask 栈实测峰值按本次台架 920B 订正为 1116/3840(FTP 传输期 1164);网络自愈 架构设计.md 吸收原监听生命周期文档、新增编号术语导读表,并把 5.2 "实验锚点速查"的 E 编号含义与 E-02 审计编号歧义写在表头(用户反馈该表 E 编号无法读懂)。 门禁:Keil 0 错 0 警;cppcheck / 编码规范 / 乱码零告警; check_mem_numbers PASS;check_doc_links --all --sections-strict 142 篇 PASS。 --- Drivers/BSP/NET/README.md | 9 +- Drivers/BSP/NET/net_config.h | 15 + Drivers/BSP/NET/net_listener.c | 17 +- Drivers/BSP/NET/net_listener.h | 9 +- Drivers/BSP/NET/net_pending.c | 287 ++++++- Drivers/BSP/NET/net_pending.h | 110 ++- Drivers/BSP/NET/net_socket.c | 132 +++- Drivers/BSP/NET/net_socket.h | 13 +- docs/README.md | 5 +- docs/images/Net层阻塞模型.svg | 58 +- docs/任务规划/基线与待确认.md | 25 +- docs/任务规划/登记簿.md | 2 +- docs/内存管理/RAM使用分析与优化方案.md | 8 +- docs/内存管理/RAM预算台账.md | 6 +- .../网络Socket监听生命周期设计说明.md | 587 -------------- docs/编号规范.md | 31 +- docs/网络/BSD_Socket_API_使用指南.md | 21 +- docs/网络/BSD_Socket_API_函数调用关系.md | 52 +- docs/网络/Net层阻塞模型架构设计.md | 727 +++++++++++++++--- docs/网络/网络自愈架构设计.md | 344 ++++++++- docs/问题记录/CH395F_Trap_Records.md | 4 +- scripts/ram_baseline.tsv | 2 +- test/board/ch395f_reuse_bench.c | 2 +- tools/arch_diagram/gen_net_pending_svg.py | 42 +- 24 files changed, 1626 insertions(+), 882 deletions(-) delete mode 100644 docs/子系统设计/网络Socket监听生命周期设计说明.md diff --git a/Drivers/BSP/NET/README.md b/Drivers/BSP/NET/README.md index 922da455..654a0936 100644 --- a/Drivers/BSP/NET/README.md +++ b/Drivers/BSP/NET/README.md @@ -8,7 +8,7 @@ | 文件 | 作用 | |---|---| | `net_socket.c/h` | **BSD Socket API 实现**:`net_socket` / `net_connect` / `net_send` / `net_recv` / `net_close` / `net_listen` / `net_accept`,含挂起 recv(pending)与错误码 | -| `net_pending.c/h` | **挂起 recv 请求表**(阻塞 recv 挂起模型:登记 / 每轮完成检查 / 取消 / close 结算;真机专属,模拟器走 `net_socket_sim.c`) | +| `net_pending.c/h` | **挂起 recv / send 请求表**(阻塞挂起模型:登记 / 每轮完成检查(recv)/ 推进(send)/ 取消 / close 结算;真机专属,模拟器走 `net_socket_sim.c`) | | `net_select.c/h` | `select`/`poll` 多路复用(基于芯片中断事件 + 周期轮询) | | `net_types.h` | 网络层公共类型定义 | | `net_config.h` | 网络层编译期配置(缓冲区大小、超时、对账周期等) | @@ -16,10 +16,13 @@ ## 核心机制 -### 阻塞模型(挂起 recv / pending) +### 阻塞模型(挂起 recv / send / pending) - 一期(RECV)已落地:`net_recv` 等支持带超时的阻塞(`pending` 挂起 + tick 唤醒) -- 二期(ACCEPT + SEND)待做 → `docs/任务规划/登记簿.md` N-4 +- 二期(SEND)已落地:`net_send` 快路径有界自旋(`NET_SEND_SPIN_MS` = 2ms),超预算登记 + 挂起 send,由 `net_send_pending_tick` 按主循环节拍分块推进;netTask 最坏停滞由 1s 压到 2ms +- 二期(ACCEPT)待做 → `docs/任务规划/登记簿.md` N-4 +- 架构细节:`docs/网络/Net层阻塞模型架构设计.md` ### 周期对账自愈(TRAP-NET-17 修复;自愈收敛阶段 3 合并 pump) diff --git a/Drivers/BSP/NET/net_config.h b/Drivers/BSP/NET/net_config.h index a6894723..92302d37 100644 --- a/Drivers/BSP/NET/net_config.h +++ b/Drivers/BSP/NET/net_config.h @@ -14,6 +14,9 @@ * 2026-10-13 自愈收敛阶段 1.5:删除 NET_RECV_TIMEOUT_MS(30s 默认预算)—— * recv 超时改为调用方策略(net_recv 无限阻塞 / * net_recv_to 显式预算 / net_select 首选),API 不再强加默认值 + * 2026-10-14 二期 SEND pending:新增 NET_SEND_SPIN_MS(发送快速路径有界自旋 + * 预算,超预算转挂起登记)——netTask 发送最坏停滞由 + * NET_SEND_TIMEOUT_MS(1s)压到本值,健康路径行为不变 */ #ifndef NET_CONFIG_H @@ -41,6 +44,18 @@ extern "C" { #define NET_SEND_TIMEOUT_MS 1000 /* 发送超时 */ #endif +/* + * 发送快速路径有界自旋预算(毫秒,SEND pending 配套) + * 分块发送时"等发送缓冲腾出"的正常代价实测仅 219µs(中位,max 378µs, + * 见《链路延迟测试说明.md》§6.2 chunk_wait)——留约 5 倍余量用有界自旋吃掉, + * 使**健康路径逐位不变**(多块帧仍在一次分派内发完,不额外引入 tick 延迟); + * 一旦单次等待超过本预算即登记挂起请求,把等待交回调用方上下文, + * netTask 的最坏停滞由 NET_SEND_TIMEOUT_MS(1s)压到本值。 + */ +#ifndef NET_SEND_SPIN_MS +#define NET_SEND_SPIN_MS 2 +#endif + /* * TCP 重传配置(由 net_socket.c 的 net_init() 在 ch395f_init() 之前下发) * P0-a(2026-09-22):周期 500→200ms——单次丢包代价 500→200ms(治 D1 症状), diff --git a/Drivers/BSP/NET/net_listener.c b/Drivers/BSP/NET/net_listener.c index bceb1ffa..3a5e6ecd 100644 --- a/Drivers/BSP/NET/net_listener.c +++ b/Drivers/BSP/NET/net_listener.c @@ -12,7 +12,7 @@ * 2026-10-13 王建锋 自愈收敛阶段 3:pump 并入 net_reconcile_tick(《网络自愈架构设计》§4.1) * * 职责与边界见 net_listener.h 头注释;设计依据见 - * docs/子系统设计/网络Socket监听生命周期设计说明.md §10。 + * docs/网络/网络自愈架构设计.md §11。 * * 模块内分层: * ① 策略常量与私有状态(s_relisten_*) @@ -33,7 +33,7 @@ #include "net_socket.h" /* net_sock_t / net_get_sock / NET_MAX_SOCKETS / net_set_errno */ #include "net_listener.h" -#include "net_pending.h" /* net_pending_active(挂起 recv 保险查询) */ +#include "net_pending.h" /* net_pending_active / net_send_pending_active(挂起保险查询) */ #include "ch395f.h" #include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */ #include "net_latency_test.h" /* 延迟探针接口(relisten 恢复计时采样) */ @@ -400,8 +400,8 @@ int net_listener_try(int sockfd) { * 返回值:无 * 限定条件:netTask 上下文(由 net_process_messages 每轮调用) * 函数说明:1. 到点重试待办的重监听;成功即报告探针样本 - * 2. 本 fd 有在途挂起 recv 时跳过(net_pending_active,挂起 recv 保险: - * 重开 Socket 会摧毁在途连接) + * 2. 本 fd 有在途挂起 recv / 挂起 send 时跳过(net_pending_active / + * net_send_pending_active,挂起保险:重开 Socket 会摧毁在途传输) * 3. 超过 NET_RELISTEN_DEADLINE_MS 停止 D10 快速重试并留痕 * (监听不永久失效——reconciler 100ms 周期仍按"芯片不在听"认领重建) */ @@ -430,8 +430,8 @@ void net_listener_retry_tick(void) { continue; } } - /* 本 fd 有在途挂起 recv:重开 Socket 会摧毁在途连接(挂起 recv 保险) */ - if (net_pending_active(i) != 0U) { + /* 本 fd 有在途挂起 recv / 挂起 send:重开 Socket 会摧毁在途连接(挂起保险) */ + if ((net_pending_active(i) != 0U) || (net_send_pending_active(i) != 0U)) { continue; } /* 未到点(tick 差值按有符号比较,抗回绕) */ @@ -460,8 +460,3 @@ void net_listener_retry_tick(void) { } } } - -/* 自愈收敛阶段 3(《网络自愈架构设计》§4.1):原"监听器 pump"(100ms 周期、去抖/冷却、 - * 情形①②③)已并入 net_socket.c 的 net_reconcile_tick 单一 100ms tick - * (判据读驱动状态缓存);本模块保留沉降/隔离、D10 重试、armed 查询与 - * net_listener_try(重建动作本身,由 reconciler 形态③④ 调用)。 */ diff --git a/Drivers/BSP/NET/net_listener.h b/Drivers/BSP/NET/net_listener.h index 9d140b90..86cee7db 100644 --- a/Drivers/BSP/NET/net_listener.h +++ b/Drivers/BSP/NET/net_listener.h @@ -10,14 +10,14 @@ * 修改记录: * 2026-10-12 王建锋 从 net_socket.c 抽出监听生命周期(N-32 §10 模块化) * - * 职责边界(详见 docs/子系统设计/网络Socket监听生命周期设计说明.md §10): + * 职责边界(详见 docs/网络/网络自愈架构设计.md §11): * 本模块只负责「让一个监听型 fd 保持/回到 LISTEN 态」。 * * **不属于本模块**(留在 net_socket.c): * - `net_listen_locked`(首次监听的**应用 API 路径**,含 BOUND/CREATED 状态门与缓冲) * - `net_accept_locked`(产出的是**连接**:LISTENING→ESTABLISHED 迁移) * - `net_lost_connect_probe`(做的是**补 accept**,即连接建立) - * - `net_listener_armed` / `net_resync_listen`(双端公开 API,sim 也实现) + * - `net_listener_armed`(**双端公开 API**,sim 也实现;纯 RAM 查询) * * ⚠ 本头文件**只被 net_socket.c include**(内部头),不进公开 net_socket.h —— * 故模拟器(simulator/sim/net_socket_sim.c)对本模块**零依赖**,CMakeLists 不编本文件。 @@ -90,7 +90,8 @@ void net_listener_retry_tick(void); * net_reconcile_tick 单一 100ms tick(判据读驱动状态缓存,《网络自愈架构设计》§4.1)。 * 本模块的 net_listener_try 由 reconciler 形态③④ 与 D10 重试轮询调用。 */ -/* 注:本模块消费的宿主能力 `net_pending_active()` 声明在 net_pending.h - * (它是 net_pending.c 的函数,须与配对头文件同处,见规范 S17 2.2)。 */ +/* 注:本模块消费的宿主能力 `net_pending_active()` / `net_send_pending_active()` + * 声明在 net_pending.h(它们是 net_pending.c 的函数,须与配对头文件同处, + * 见规范 S17 2.2)。 */ #endif /* NET_LISTENER_H */ diff --git a/Drivers/BSP/NET/net_pending.c b/Drivers/BSP/NET/net_pending.c index 0ed8a0fb..4e1575ce 100644 --- a/Drivers/BSP/NET/net_pending.c +++ b/Drivers/BSP/NET/net_pending.c @@ -1,21 +1,24 @@ /* - * 模块名称:挂起 recv 请求表(阻塞 recv 挂起模型)—— 实现 + * 模块名称:挂起请求表(阻塞 recv / send 挂起模型)—— 实现 * 文件说明:见 net_pending.h;架构与五路语义见 * docs/网络/Net层阻塞模型架构设计.md * 依赖(全部单向,无反向 include): * 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 读出)/ main.h(HAL_GetTick) + * net_listener.h(net_listener_try)/ ch395f.h(RX 读出 / TX 分块写)/ main.h(HAL_GetTick) * 限定条件:本文件真机专属——模拟器走 sim/net_socket_sim.c, * simulator/CMakeLists.txt 不编译本文件(同 net_listener.c 先例) * - * 阻塞 recv 挂起模型(E-02 结构演进):阻塞等待 = 挂起请求 + 主循环节拍完成 - * "无数据 + 阻塞等待"不再自旋占用 netTask:登记 pending 后立即返回 - * net_pending_tick(与 D10 重监听重试同节奏,net_process_messages 开头每轮调用) - * 按 ①数据 ②到点(仅显式预算) ③断连 顺序检查完成条件并通知调用方 - * 关闭/网络重建/陈旧重同步走显式 settle/cancel 路径(取消必通知,调用方永不挂死) - * 公开 API 返回契约:>0 字节数 / 0 EOF / -1 错误(错误码经通知值回传) + * 阻塞 recv / send 挂起模型(E-02 结构演进):阻塞等待 = 挂起请求 + 主循环节拍完成 + * "无数据 / 发送缓冲未空 + 阻塞等待"不再自旋占用 netTask:登记 pending 后立即返回 + * net_pending_tick / net_send_pending_tick(与 D10 重监听重试同节奏, + * net_process_messages 开头每轮调用) + * recv:按 ①数据 ②到点(仅显式预算) ③断连 顺序检查完成条件并通知调用方 + * send:按 ①断连 ②推进写块 ③写完 ④无进展超时 顺序推进并通知调用方 + * 关闭/网络重建/陈旧重同步走显式 cancel 路径(取消必通知,调用方永不挂死) + * 公开 API 返回契约:recv >0 字节数 / 0 EOF / -1 错误;send >=0 已发字节数(短写由 + * 调用方按 ret != len 判定)/ -1 错误(错误码经通知值回传) * 预算归调用方:timeout_ms=0 即无限等待(无默认预算);挂起超时留痕探针为 * 编译期选项(NET_PENDING_TRIPWIRE,默认 0 = 编译剔除) */ @@ -414,3 +417,271 @@ void net_pending_settle_close(int sockfd, net_sock_t *p_sock) { /* 结束(必通知) */ net_pending_complete(sockfd, val); } + +/* ================= 挂起 send 请求表(SEND pending,二期) ================= */ + +typedef struct { + uint8_t active; /* 1 = 本 fd 有挂起 send */ + const void *buf; /* 调用方缓冲基址(仅指针,须保持有效至完成) */ + int len; /* 本次发送总长度(含登记前已写部分) */ + int sent; /* 已写入芯片的字节数(进度;登记时可 > 0) */ + uint32_t deadline; /* 无进展超时时刻 = 上次成功写块时刻 + NET_SEND_TIMEOUT_MS */ + uint32_t registered; /* 登记时刻(tick;日志基准) */ + TaskHandle_t caller; /* 调用方任务(完成通知目标) */ +} net_send_pending_t; + +/* 每 fd 一个挂起 send(与 recv 表独立:收发两方向互不阻塞) */ +static net_send_pending_t s_net_send[NET_MAX_SOCKETS]; + +/* + * 函数功能:通知调用方并清除本 fd 的挂起 send(所有完成/取消路径的公共出口) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * result - 结果原值(本模型恒为**已发字节数** >= 0) int + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文;sockfd 合法且挂起在位(否则静默返回,防重复通知) + * 函数说明:send 结果恒 >= 0(短写语义),故不在此改写错误码——取消/超时的原因 + * 由各调用路径在调用本函数**之前**经 net_set_errno 记录 + */ +static void net_send_pending_complete(int sockfd, int result) { + /* 本 fd 挂起请求指针 */ + net_send_pending_t *p; + + /* 描述符越界:静默返回(防重复通知) */ + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return; + } + /* 取挂起请求 */ + p = &s_net_send[sockfd]; + /* 无在位挂起(已结束/已取消):静默返回 */ + if (p->active == 0U) { + return; + } + /* 覆盖式写入通知值 = 已发字节数(32 位原值) */ + xTaskNotify(p->caller, (uint32_t)result, eSetValueWithOverwrite); + /* 清除挂起状态 */ + p->active = 0U; +} + +/* + * 函数功能:登记一个挂起 send 请求(net_send_locked 的"等发送缓冲"超预算路径) + * 入口参数:见 net_pending.h + * 出口参数:无 + * 返回值:NET_PENDING_REGISTERED - 登记成功;-1 - 拒绝(见头注释) + * 限定条件:netTask 上下文 + * 函数说明:每 fd 至多一个挂起 send(禁止覆盖——覆盖丢失前调用方通知 = 永久挂死); + * 执行任务自身发起(net_send_sock 阻塞用法)无他人可完成 → 拒绝(自等死锁) + */ +int net_send_pending_register(int sockfd, const void *buf, int len, int sent, + TaskHandle_t caller) { + /* 挂起请求指针 */ + net_send_pending_t *p; + /* 登记时刻(tick) */ + uint32_t now; + + /* 描述符越界 → BADF */ + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + /* 参数非法(缓冲无效 / 长度非正 / 进度越界)→ INVAL */ + if ((buf == NULL) || (len <= 0) || (sent < 0) || (sent >= len)) { + return NET_ERR_RETURN_1(NET_ERR_INVAL); + } + /* 取挂起请求 */ + p = &s_net_send[sockfd]; + + /* 同 fd 已有挂起 send:拒绝(覆盖将丢失前通知 = 该任务永久挂死) */ + if (p->active != 0U) { + return NET_ERR_RETURN_1(NET_ERR_BUSY); + } + /* netTask 自身发起:无他人可完成(tick 跑在 netTask 内)→ 拒绝(自等死锁) */ + if ((caller == NULL) || (caller == s_owner_task)) { + return NET_ERR_RETURN_1(NET_ERR_BUSY); + } + + /* 当前 tick(停滞计时基准) */ + now = HAL_GetTick(); + /* 标记挂起在位 */ + p->active = 1U; + /* 调用方发送缓冲基址(须保持有效至完成) */ + p->buf = buf; + /* 本次发送总长度 */ + p->len = len; + /* 进度起点(登记前已写入芯片的字节数) */ + p->sent = sent; + /* 无进展超时起点(NET_SEND_TIMEOUT_MS = 0 表示不限,同快速路径口径) */ + p->deadline = (NET_SEND_TIMEOUT_MS > 0) ? (now + (uint32_t)NET_SEND_TIMEOUT_MS) : 0U; + /* 登记时刻(日志基准) */ + p->registered = now; + /* 完成通知目标 */ + p->caller = caller; + /* 登记成功:分派跳过即时通知 */ + return NET_PENDING_REGISTERED; +} + +/* + * 函数功能:挂起 send 推进与完成检查(netTask 每轮 net_process_messages 调用一次,非阻塞) + * 入口参数:p_socks - Socket 控制块表首指针(net_socket.c 的 s_net_socks) net_sock_t * + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文 + * 函数说明:1. 逐 fd 按序:①断连 → 以已发字节数结束(剩余字节不可能送达) + * 2. ②推进:发送缓冲空闲则写下一块(每轮至多一块——写完即清 send_ready, + * 下一块须等 SENBUF_FREE 中断重新置位,由 net_poll 刷新) + * 3. ③全部写完 → 以总长度结束;④无进展超时 → 以已发字节数结束 + * 4. 结果恒为已发字节数(短写由调用方按 `ret != len` 判定) + * 5. 停滞计时按**无进展**刷新:大文件传输总时长必然超过单块预算, + * 按总时长判会误杀(与 recv 侧"到点"语义的差异点) + */ +void net_send_pending_tick(net_sock_t *p_socks) { + /* Socket 循环索引 */ + int i; + + for (i = 0; i < NET_MAX_SOCKETS; i++) { + /* 本 fd 挂起请求 */ + net_send_pending_t *p = &s_net_send[i]; + /* 本 fd 控制块 */ + net_sock_t *p_sock = &p_socks[i]; + + /* 无挂起请求:跳过 */ + if (p->active == 0U) { + continue; + } + + /* fd 已关闭(正常路径在 close 内先取消;此处兜底,防任何路径漏掉通知) */ + if (p_sock->in_use == 0U) { + net_send_pending_complete(i, p->sent); + continue; + } + + /* ① 断连:本会话已结束 → 剩余字节不可能送达,按已发字节数结束。 + * 判据 `state != ESTABLISHED`(同快速路径):state 离开 ESTABLISHED 后 + * 即便再回到 ESTABLISHED,也是**新会话**(relisten + 新 CONNECT)—— + * 旧会话的剩余字节绝不能投到新连接上。session_dead 一并覆盖 + * (net_types.h:TIMEOUT/DISCONNECT 两条路径都同时置 CLOSED,此处为 + * 与 recv 侧判据对齐的冗余保险) */ + if ((p_sock->state != NET_SOCK_STATE_ESTABLISHED) || (p_sock->session_dead != 0U)) { + DBG_ERROR("sock%d send pending aborted (%d/%d): session down", + i, p->sent, p->len); + (void)net_set_errno(NET_ERR_CONNRESET); + net_send_pending_complete(i, p->sent); + continue; + } + + /* ② 推进:发送缓冲空闲则写下一块 */ + if ((p->sent < p->len) && (p_sock->send_ready != 0U)) { + /* 本次写块长度 = min(剩余, 硬件发送缓冲上限) */ + int chunk = p->len - p->sent; + if (chunk > NET_SEND_CHUNK_MAX) { + chunk = NET_SEND_CHUNK_MAX; + } + /* 写入芯片发送 FIFO(与快速路径同一分块粒度,TRAP-NET-16) */ + ch395f_write_send_buf((uint8_t)i, (uint8_t *)p->buf + p->sent, (uint16_t)chunk); + /* 标记发送缓冲占用中(等 SENBUF_FREE 中断重新置位) */ + p_sock->send_ready = 0U; + /* 累计进度与统计 */ + p->sent += chunk; + p_sock->send_bytes += (uint32_t)chunk; + /* 进度刷新:停滞判定按"无进展"而非"总时长" */ + p->deadline = (NET_SEND_TIMEOUT_MS > 0) + ? (HAL_GetTick() + (uint32_t)NET_SEND_TIMEOUT_MS) : 0U; + } + + /* ③ 全部写完:以总长度结束(调用方见 ret == len 即成功) */ + if (p->sent >= p->len) { + net_send_pending_complete(i, p->sent); + continue; + } + + /* ④ 无进展超时:以已发字节数结束(短写;调用方须检查返回值) */ + if ((NET_SEND_TIMEOUT_MS > 0) && + ((int32_t)(HAL_GetTick() - p->deadline) >= 0)) { + DBG_ERROR("sock%d send pending stall (%d/%d)", i, p->sent, p->len); + (void)net_set_errno(NET_ERR_TIMEDOUT); + net_send_pending_complete(i, p->sent); + continue; + } + } +} + +/* + * 函数功能:清除本 fd 的挂起 send(不通知;用于"新 fd 不得继承"与 close 后的幂等清理) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:无 + * 限定条件:无 + * 函数说明:静默清理不发通知:close / 重建路径先经 cancel 通知,本调用仅保证状态归零(幂等) + */ +void net_send_pending_clear(int sockfd) { + /* 描述符越界:直接返回 */ + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return; + } + /* 挂起状态清零(静默,不发通知) */ + s_net_send[sockfd].active = 0U; +} + +/* + * 函数功能:取消本 fd 的挂起 send 并通知调用方(close / drop_conn / resync / 重建) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * err - 取消原因(记入 netTask 错误码槽位 + 日志) int + * 出口参数:无 + * 返回值:无(无在位挂起时静默返回) + * 限定条件:netTask 上下文 + * 函数说明:以**已发字节数**结束(已写入芯片的字节不回收——对端可能已收到;未发部分 + * 放弃,由调用方按短写处理)。取消必通知,否则调用方永久挂死 + */ +void net_send_pending_cancel(int sockfd, int err) { + /* 本 fd 挂起请求指针(只读:完成经 net_send_pending_complete 按 fd 操作) */ + const net_send_pending_t *p; + + /* 描述符越界:静默返回 */ + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return; + } + /* 取挂起请求 */ + p = &s_net_send[sockfd]; + /* 无在位挂起:静默返回 */ + if (p->active == 0U) { + return; + } + /* 留痕:取消原因 + 进度(短写程度) */ + DBG_ERROR("sock%d send pending cancel (%d/%d, err=%d)", sockfd, p->sent, p->len, err); + /* 原因记入 netTask 自己的错误码槽位(结果 >= 0 不会被 complete 代记) */ + (void)net_set_errno(err); + /* 结束:以已发字节数通知 */ + net_send_pending_complete(sockfd, p->sent); +} + +/* + * 函数功能:取消全部挂起 send 并逐一通知(网络重建路径) + * 入口参数:err - 取消原因 int + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文;须在控制块清零与芯片复位之前调用 + * 函数说明:网络重建(net_stack_bring_up → net_init)前调用,保证所有在位调用方均被唤醒 + */ +void net_send_pending_cancel_all(int err) { + /* 逐 fd 遍历 */ + for (int i = 0; i < NET_MAX_SOCKETS; i++) { + /* 取消并通知(未在位则静默跳过) */ + net_send_pending_cancel(i, err); + } +} + +/* + * 函数功能:查询本 fd 是否有在途挂起 send —— 宿主能力,供 net_listener 用 + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:1 - 有挂起;0 - 无(含描述符越界) int + * 限定条件:任意上下文(纯 RAM 读,不发起 RPC) + * 函数说明:net_listener_retry_tick 用它跳过"重开 Socket 会摧毁在途传输"的 fd + * (挂起 send 保险;与 net_pending_active 的挂起 recv 保险并列) + */ +int net_send_pending_active(int sockfd) { + /* 描述符越界:视为无挂起 */ + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return 0; + } + return (s_net_send[sockfd].active != 0U) ? 1 : 0; +} diff --git a/Drivers/BSP/NET/net_pending.h b/Drivers/BSP/NET/net_pending.h index 93e822dd..d9d1b5e6 100644 --- a/Drivers/BSP/NET/net_pending.h +++ b/Drivers/BSP/NET/net_pending.h @@ -1,12 +1,14 @@ /* - * 模块名称:挂起 recv 请求表(阻塞 recv 挂起模型) - * 文件说明:阻塞 recv 的"挂起请求"数据结构与生命周期:登记 / 每轮完成检查 / + * 模块名称:挂起请求表(阻塞 recv / send 挂起模型) + * 文件说明:阻塞 recv / send 的"挂起请求"数据结构与生命周期:登记 / 每轮完成检查 / * 取消(close、网络重建、对账 resync)/ 幂等清理。 + * recv 侧 = 数据到达即完成;send 侧 = 分块推进至全部写完(进度刷新停滞计时)。 * 架构与五路语义见 docs/网络/Net层阻塞模型架构设计.md - * 依赖:net_types.h(net_sock_t);消费方 = net_socket.c(消息分派 / close / - * 重建 / 对账) - * 限定条件:除 net_pending_active / net_pending_clear(纯 RAM 读)外, - * 全部函数须 netTask 上下文 + * 依赖:net_types.h(net_sock_t);ch395f.h(send 侧分块写 + 缓冲常量); + * 消费方 = net_socket.c(消息分派 / close / 重建 / 对账)、 + * net_listener.c(挂起保险查询) + * 限定条件:除 net_pending_active / net_send_pending_active / *_clear(纯 RAM 读) + * 外,全部函数须 netTask 上下文 */ #ifndef NET_PENDING_H #define NET_PENDING_H @@ -15,6 +17,7 @@ #include "FreeRTOS.h" #include "task.h" #include "net_types.h" +#include "ch395f.h" /* 内部哨兵:pending 已登记(消息分派据此跳过即时通知;取值不与字节数空间冲突) */ #define NET_PENDING_REGISTERED 0x70000000 @@ -22,12 +25,21 @@ /* 挂起超时留痕探针:排查"调用方挂死唤不醒"时置 1——挂起超 NET_RECV_TRIPWIRE_MS * 只留一条 [ERR](哪个 fd/多少毫秒/什么基准),绝不强制结束、绝不伪造返回值。 * 开与关语义完全一致,差别只在留痕;默认 0 = 产品构建零成本。 - * 见 docs/网络/Net层阻塞模型架构设计.md §9 */ + * 见 docs/网络/Net层阻塞模型架构设计.md §12 */ #define NET_PENDING_TRIPWIRE 0 #if NET_PENDING_TRIPWIRE #define NET_RECV_TRIPWIRE_MS 60000U /* 挂起超时留痕阈值(ms) */ #endif +/* 单次写入 CH395F 发送 FIFO 的最大字节数:受硬件发送缓冲 + * (CH395F_SEND_BUF_SIZE_BYTES = 1KB)与 SPI DMA 载荷 + * (CH395F_SPI_DMA_MAX_PAYLOAD = 2KB)双重限制,以硬件发送缓冲为硬上限 + * (TRAP-NET-16:单次写 2KB/4KB 会写穿 1KB 发送 FIFO)。 + * ⚠ 唯一定义处:发送快速路径(net_send_locked)与挂起 send 推进 + * (net_send_pending_tick)**必须**共用同一粒度,否则两条路径的分块行为漂移。 */ +#define NET_SEND_CHUNK_MAX ((int)CH395F_SEND_BUF_SIZE_BYTES < (int)CH395F_SPI_DMA_MAX_PAYLOAD ? \ + (int)CH395F_SEND_BUF_SIZE_BYTES : (int)CH395F_SPI_DMA_MAX_PAYLOAD) + /* * 函数功能:捕获执行任务句柄(net_init 顶部调用一次;登记守卫据此拒绝自等待死锁) * 入口参数:无 @@ -116,4 +128,88 @@ void net_pending_settle_close(int sockfd, net_sock_t *p_sock); */ int net_pending_active(int sockfd); +/* ================= 挂起 send 请求表(SEND pending,二期) ================= + * 与 recv 表**完全独立**(各自一张表、各占一槽):收发两个方向互不阻塞, + * 同一 fd 可同时有一个挂起 recv 与一个挂起 send(今天的分片实现本就支持该并发)。 + * 语义差异:recv 是"等一次到达即完成",send 是"分块推进直到全部写完", + * 故 send 的停滞计时按**无进展**刷新(大文件传输总时长必然超过单块预算)。 + */ + +/* + * 函数功能:登记一个挂起 send 请求(net_send_locked 的"等发送缓冲"超预算路径) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * buf - 调用方发送缓冲基址(须保持有效至完成;本函数不拷贝数据) const void * + * len - 本次发送**总**长度(含登记前已写入芯片的部分) int + * sent - 登记前已写入芯片的字节数(进度起点,0 ≤ sent < len) int + * caller - 调用方任务句柄(完成通知目标) TaskHandle_t + * 出口参数:无 + * 返回值:NET_PENDING_REGISTERED - 登记成功(分派跳过即时通知) + * -1 - 拒绝(参数非法 / 同 fd 已有挂起 send / 执行任务自身发起) + * 限定条件:netTask 上下文 + * 函数说明:每 fd 至多一个挂起 send(禁止覆盖——覆盖丢失前调用方通知 = 永久挂死); + * 执行任务自身发起(net_send_sock 阻塞用法)无他人可完成 → 拒绝(自等死锁)。 + * ⚠ **调用方铁律**:`sent < len` 是调用侧必须保证的**前置条件**,不是本函数的 + * 兜底。`sent == len` 表示"全部字节已交付芯片、无剩余块可挂起"——此时超预算 + * 应**直接返回 sent**,不得调用本函数;本函数会按参数非法拒绝并回 -1, + * 把一次完整发送报成失败(net_send_locked 末块等待 2026-10-14 台架实证, + * 见《Net层阻塞模型架构设计.md》 §9.1)。 + */ +int net_send_pending_register(int sockfd, const void *buf, int len, int sent, + TaskHandle_t caller); + +/* + * 函数功能:挂起 send 推进与完成检查(netTask 每轮 net_process_messages 调用一次,非阻塞) + * 入口参数:p_socks - Socket 控制块表首指针(net_socket.c 的 s_net_socks) net_sock_t * + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文 + * 函数说明:逐 fd 按序判定:①断连(state 非 ESTABLISHED / session_dead)→ 以已发 + * 字节数结束;②推进——发送缓冲空闲则写下一块(每轮至多一块,写完即清 + * send_ready,下一块等 SENBUF_FREE 由 net_poll 重新置位);③全部写完 → + * 以总长度结束;④无进展超时 → 以已发字节数结束。结果均为**已发字节数** + * (短写由调用方按 `ret != len` 判定,与分片实现契约一致)。 + */ +void net_send_pending_tick(net_sock_t *p_socks); + +/* + * 函数功能:清除本 fd 的挂起 send(不通知;"新 fd 不得继承"与 close 后的幂等清理) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:无 + * 限定条件:无 + */ +void net_send_pending_clear(int sockfd); + +/* + * 函数功能:取消本 fd 的挂起 send 并通知调用方(close / drop_conn / 对账 resync / 重建) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * err - 取消原因(记入 netTask 错误码槽位 + 日志) int + * 出口参数:无 + * 返回值:无(无在位挂起时静默返回) + * 限定条件:netTask 上下文 + * 函数说明:以**已发字节数**结束(已写入芯片的字节不回收——对端可能已收到; + * 未发部分放弃,由调用方按短写处理),原因记 errno 与 [ERR] 日志 + */ +void net_send_pending_cancel(int sockfd, int err); + +/* + * 函数功能:取消全部挂起 send 并逐一通知(网络重建路径) + * 入口参数:err - 取消原因 int + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文;须在控制块清零与芯片复位之前调用 + */ +void net_send_pending_cancel_all(int err); + +/* + * 函数功能:查询本 fd 是否有在途挂起 send —— 宿主能力,供 net_listener 用 + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:1 - 有挂起;0 - 无(含描述符越界) int + * 限定条件:任意上下文(纯 RAM 读,不发起 RPC) + * 函数说明:与 net_pending_active 同用途——net_listener_retry_tick 据此跳过 + * "重开 Socket 会摧毁在途传输"的 fd(挂起 send 保险) + */ +int net_send_pending_active(int sockfd); + #endif /* NET_PENDING_H */ diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 90d4b99f..9e2c02d3 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -183,7 +183,8 @@ static void handle_send_event(net_sock_t *p_sock); static void handle_timeout_event(net_sock_t *p_sock); static void fire_event(net_sock_t *p_sock, net_event_t event); static void parse_ip_string(const char *str, uint8_t *ip_arr); -static int net_send_locked(net_sock_t *p_sock, const void *buf, int len); +static int net_send_locked(net_sock_t *p_sock, const void *buf, int len, + TaskHandle_t caller); static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, uint32_t timeout_ms, TaskHandle_t caller); static int net_close_locked(net_sock_t *p_sock); @@ -226,8 +227,10 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { ch395f_status_t ret; /* 网络重建前取消全部挂起请求并通知调用方(调用方阻塞等回执, - * 取消不通知 = 永久挂死);必须早于控制块清零与芯片复位 */ + * 取消不通知 = 永久挂死);必须早于控制块清零与芯片复位。 + * recv / send 两张表各自独立,须分别取消 */ net_pending_cancel_all(NET_ERR_CONNRESET); + net_send_pending_cancel_all(NET_ERR_CONNRESET); /* 清零 Socket 控制块 */ memset(s_net_socks, 0, sizeof(s_net_socks)); @@ -774,6 +777,8 @@ static void net_reconcile_tick(void) { p_sock->lost_conn_seen = 0U; /* 取消本 fd 在死连接上的挂起 recv(必通知,防调用方挂死) */ net_pending_cancel(i, NET_ERR_CONNRESET); + /* 挂起 send 同样取消(会话已被芯片收走,剩余字节不可能送达) */ + net_send_pending_cancel(i, NET_ERR_CONNRESET); } else if ((bad == 3) || (bad == 4)) { /* 类型③④:监听失位/孤儿监听 → 重建(本层唯一重建入口; * 非阻塞:失败由 D10 重试轮询接管) */ @@ -791,9 +796,9 @@ static void net_reconcile_tick(void) { /* - * 函数功能:netTask 主循环核心 —— 自愈 tick 宿主 + 消息队列分派器。每轮 - * 先跑三个非阻塞自愈 tick(D10 重监听 / 挂起 recv / - * TRAP-NET-17 状态对账),再非阻塞排空队列按 msg.type 分派到 _locked 实现。 + * 函数功能:netTask 主循环核心 —— 自愈 tick 宿主 + 消息队列分派器。每轮先跑四个非阻塞 + * 自愈 tick(D10 重监听 / 挂起 recv / 挂起 send / TRAP-NET-17 状态对账), + * 再非阻塞排空队列按 msg.type 分派到 _locked 实现。 * 入口参数:无 * 出口参数:无 * 返回值:无(void);消息结果经 xTaskNotify 通知值回传(成功=字节数/描述符, @@ -801,18 +806,15 @@ 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(NET_RELISTEN_RETRY_MS) - * 节拍重试 open+listen,2s 总时限到期放弃留痕;本 fd 有在途挂起 recv 时跳过(挂起 recv 保险) - * 3. net_pending_tick:按节拍结束挂起 recv(数据/到点[仅显式预算]/断连); - * 挂起超时留痕探针(编译期选项,默认关):挂起超阈值只 DBG_ERROR 留痕一次,绝不强制结束/伪造返回值 - * 4. net_reconcile_tick(TRAP-NET-17 + pump 合并,自愈收敛阶段 3):每 100ms 对账 - * ①②⑤ + ③④ 重建 + U11 重同步,判据读驱动状态缓存(net_poll 每轮 - * 全量刷新,E2 背书);去抖分级(②=1 轮/③④⑤=2 轮/①=4 轮,M3 让位窗) - * + 1s 冷却;重建失败进 D10 重试队列,reconciler 不重复催 - * 5. net_msg_t 按值入队,netTask 侧副本调用方不可见 → 结果只能搭任务通知值回传 - * 6. 32 位整体即结果,不做位域压缩 - * 7. RECV 特例:无数据+阻塞 → 登记 pending 哨兵 NET_PENDING_REGISTERED(0x70000000), - * do_notify=0,回执推迟到 net_pending_tick 完成时 + * 2. net_listener_retry_tick(D10):断连后按 30ms 节拍重试 open+listen, + * 2s 时限到期放弃留痕;本 fd 有在途挂起 recv / send 时跳过(挂起保险) + * 3. net_pending_tick:按节拍结束挂起 recv(数据/到点[仅显式预算]/断连) + * 4. net_send_pending_tick:按节拍推进挂起 send(断连/推进/写完/无进展超时) + * 5. net_reconcile_tick(TRAP-NET-17 + pump 合并,自愈收敛阶段 3):100ms 对账 + * ①②⑤ + ③④ 重建 + U11 重同步,判据读驱动状态缓存;去抖分级 + 1s 冷却 + * 6. net_msg_t 按值入队 → 结果只能搭任务通知值回传(32 位整体即结果,不做位域压缩) + * 7. RECV / SEND 特例:超预算登记 pending 哨兵 NET_PENDING_REGISTERED(0x70000000), + * do_notify=0,回执推迟到 net_pending_tick / net_send_pending_tick 完成时 */ void net_process_messages(void) { net_msg_t msg; @@ -822,11 +824,13 @@ void net_process_messages(void) { return; } - /* 三个非阻塞自愈 tick 置于消息处理之前:断连后即使无应用消息,监听器也能自主恢复 */ + /* 四个非阻塞自愈 tick 置于消息处理之前:断连后即使无应用消息,监听器也能自主恢复 */ /* D10 重监听重试(TRAP-NET-09):沉降后重开芯片监听器 */ net_listener_retry_tick(); /* 挂起 recv 完成检查:数据到达 / 到点 / 断连时通知调用方 */ net_pending_tick(s_net_socks); + /* 挂起 send 推进与完成检查(SEND pending):断连 / 推进写块 / 写完 / 无进展超时 */ + net_send_pending_tick(s_net_socks); /* TRAP-NET-17 状态对账 + 监听器 pump(自愈收敛阶段 3 合并单一 100ms tick): * 判据读驱动缓存(net_poll 每轮全量刷新),①②⑤ 失同步 + ③④ 重建 + * U11 重同步全部在此(本层唯一周期重建入口,N-32 P2 单一所有者) */ @@ -852,8 +856,14 @@ void net_process_messages(void) { msg.result = -1; } else { /* 真正写入芯片:1KB/块等 SENBUF_FREE 中断(§5.32/§5.23①), - * 单块停滞 1s 超时,中途断连立即 CONNRESET;短写留痕,调用方须检查返回值 */ - msg.result = net_send_locked(p_sock, msg.buf, msg.len); + * 单块等待超出快速路径预算(NET_SEND_SPIN_MS)即登记挂起请求、 + * netTask 零占用返回;中途断连立即 CONNRESET;短写留痕, + * 调用方须检查返回值 */ + msg.result = net_send_locked(p_sock, msg.buf, msg.len, msg.caller); + if (msg.result == NET_PENDING_REGISTERED) { + /* 挂起已登记:不发即时回执,完成时才通知(防"登记成功"误报为结果) */ + do_notify = 0; + } } break; } @@ -1074,6 +1084,8 @@ int net_socket(int domain, int type) { net_listener_clear(sockfd); /* 新 fd 不得继承上一次挂起的 recv 请求 */ net_pending_clear(sockfd); + /* 新 fd 不得继承上一次挂起的 send 请求 */ + net_send_pending_clear(sockfd); /* 返回描述符(控制块已就绪) */ return sockfd; @@ -1918,30 +1930,38 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a } /* - * SPI DMA 单次事务最大有效载荷(取 CH395F 驱动公开常量) + * 单次写入 CH395F 发送 FIFO 的最大字节数 NET_SEND_CHUNK_MAX 已移入 + * Drivers/BSP/NET/net_pending.h —— 发送快速路径(本文件)与挂起 send 推进 + * (net_pending.c 的 net_send_pending_tick)必须共用同一定义,否则两条路径的 + * 分块粒度漂移(TRAP-NET-16:单次写超 1KB 发送 FIFO 会写穿)。 */ -#define NET_DMA_MAX_PAYLOAD ((int)CH395F_SPI_DMA_MAX_PAYLOAD) -/* 单次写入 CH395F 发送 FIFO 的最大字节数:受硬件发送缓冲(CH395F_SEND_BUF_SIZE_BYTES=1KB) - * 与 SPI DMA 载荷(NET_DMA_MAX_PAYLOAD=2KB) 双重限制,以硬件发送缓冲为硬上限(见 TRAP-NET-16), - * 否则单次写 2KB/4KB 会写穿 1KB 发送 FIFO。 */ -#define NET_SEND_CHUNK_MAX ((int)CH395F_SEND_BUF_SIZE_BYTES < NET_DMA_MAX_PAYLOAD ? \ - (int)CH395F_SEND_BUF_SIZE_BYTES : NET_DMA_MAX_PAYLOAD) /* * 函数功能:发送数据(内部锁定版本,必须在 netTask 上下文调用) * 入口参数:p_sock - Socket 控制块指针 net_sock_t * * buf - 待发送数据缓冲区指针 const void * * len - 数据长度 int > 0 + * caller - 调用方任务句柄(挂起 send 的完成通知目标) TaskHandle_t * 出口参数:无 - * 返回值:实际发送字节数(>= 0);-1 表示参数无效或 Socket 未连接 int + * 返回值:实际发送字节数(>= 0);-1 表示参数无效或 Socket 未连接; + * NET_PENDING_REGISTERED 表示已登记挂起 send(分派据此跳过即时通知) int * 限定条件:p_sock 非空;Socket 处于 ESTABLISHED 状态;netTask 上下文 * 函数说明:1. 按 NET_SEND_CHUNK_MAX(硬件发送缓冲上限,见 TRAP-NET-16)分块写入 CH395F 发送 FIFO - * 2. 发送前等待 send_ready 置位,超时返回已发送字节数 - * 3. 每写完一块等待 SENBUF_FREE 中断(send_ready 清零后重新置位,§5.32/§5.23①) + * 2. 等待发送缓冲腾出采用**有界自旋**:单次等待 ≤ NET_SEND_SPIN_MS + * (健康路径实测 219µs 中位,5 倍余量)→ 多块帧仍在一次分派内发完, + * 健康路径行为与分片时代逐位一致 + * 3. 单次等待超出预算 → 登记挂起 send(进度 = 已发字节数)并立即返回, + * 由 net_send_pending_tick 按主循环节拍继续推进 —— + * netTask 最坏停滞由 1s 压到 NET_SEND_SPIN_MS。 + * ⚠ 仅**写块前**的等待可挂起:末块写完后 sent == len,此时超预算必须 + * 直接返回 sent 而非登记(登记函数按 sent >= len 拒收,会把完整发送 + * 报成 -1;2026-10-14 台架实证,详见《Net层阻塞模型架构设计.md》 §9.1) * 4. 发送中途断连(状态非 ESTABLISHED)→ 立即失败并返回已发字节 * (errno=CONNRESET,不再空等超时) + * 5. 未写完即返回(含挂起完成)均为**短写**:调用方须按 ret != len 判定 */ -static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) { +static int net_send_locked(net_sock_t *p_sock, const void *buf, int len, + TaskHandle_t caller) { /* 累计实际发送字节数 */ int sent = 0; /* 当前等待阶段起始时刻(tick) */ @@ -1979,13 +1999,22 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) { (void)net_set_errno(NET_ERR_CONNRESET); break; } - /* 停滞超时:NET_SEND_TIMEOUT_MS 后 send_ready 仍未置位 */ + /* 停滞超时:NET_SEND_TIMEOUT_MS 后 send_ready 仍未置位(仅快速路径可见; + * 挂起路径的停滞判定由 net_send_pending_tick 负责) */ if (NET_SEND_TIMEOUT_MS > 0 && (HAL_GetTick() - tick_start) >= NET_SEND_TIMEOUT_MS) { DBG_ERROR("sock%d send stall: send_ready not set (%d/%d)", sockfd, sent, len); (void)net_set_errno(NET_ERR_TIMEDOUT); break; } + /* 快速路径预算用尽(仍无进展):登记挂起 send,把等待交回调用方上下文。 + * 先于上面的 1s 停滞判定生效,故健康/轻微拥塞路径不会走到 1s 分支。 + * 登记失败(同 fd 已有挂起 / netTask 自身调用)→ 返回 -1,调用方见负值。 + * 不变量:此处位于 **写块之前**,且循环条件 `sent < len` 尚未被改写 + * → `sent < len` 恒成立,登记必获受理(与末块等待的差异点)。 */ + if ((HAL_GetTick() - tick_start) >= (uint32_t)NET_SEND_SPIN_MS) { + return net_send_pending_register(sockfd, buf, len, sent, caller); + } /* 轮询一次(更新 send_ready/状态),下一轮再试 */ net_poll(); continue; @@ -2017,11 +2046,29 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) { } /* 轮询:处理 SEND_OK/SENBUF_FREE 中断 */ net_poll(); - /* 等待超时:本块放弃(外层统一处理) */ + /* 末块已全部交付(sent == len):无剩余块可写,SENBUF_FREE 等待是 + * **纯粹的保守自旋**(原本是为"等缓冲腾出再写下一块"服务)——直接跳出, + * 把等待顺延为下一批的责任。同时也是下面挂起判定的前置守卫: + * net_send_pending_register 会按 `sent >= len` 拒收并回 -1, + * 把一次完整发送报成失败(2026-10-14 台架实证:RETR 1KB 单块帧 + * `write error, ret=-1`)。本检查置于**超时/自旋判定之前**, + * 保证末块路径不因预算/超时判定产生任何额外延迟。 */ + if (sent >= len) { + break; + } + /* 等待超时:本块放弃(外层统一处理)。置于自旋判定之前, + * 保证 NET_SEND_SPIN_MS 被配置得比 NET_SEND_TIMEOUT_MS 大时仍以超时收口 */ if (NET_SEND_TIMEOUT_MS > 0 && (HAL_GetTick() - tick_start) >= NET_SEND_TIMEOUT_MS) { break; } + /* 快速路径预算用尽(仍无进展):登记挂起 send(进度 = 已发字节数), + * 由 net_send_pending_tick 续传;健康路径(219µs 量级)不会到达此处。 + * 此处位于**写块之后**,但上面的 `sent >= len` 守卫已保证伴随 + * `sent < len`(仍有剩余块)才具到达资格,登记必获受理。 */ + if ((HAL_GetTick() - tick_start) >= (uint32_t)NET_SEND_SPIN_MS) { + return net_send_pending_register(sockfd, buf, len, sent, caller); + } } #if defined(TEST_LATENCY_PROBE) /* 探针已武装:累计本段等待样本 */ @@ -2160,6 +2207,8 @@ static int net_close_locked(net_sock_t *p_sock) { /* 先结束本 fd 的挂起 recv(排空/取消 + 必通知),再关芯片 Socket * ——关闭不得让等待中的调用方永久悬挂 */ net_pending_settle_close((int)(p_sock - s_net_socks), p_sock); + /* 挂起 send 同样先结束(以已发字节数通知;未发部分放弃),再关芯片 Socket */ + net_send_pending_cancel((int)(p_sock - s_net_socks), NET_ERR_CONNRESET); /* 排渣 + 关闭 + 等 CLOSED 一体(见函数头注释 1); * 已 CLOSED 的 fd 只清本 Socket 残留事件(防下一连接误判"刚建立就断开") */ @@ -2179,6 +2228,8 @@ static int net_close_locked(net_sock_t *p_sock) { net_listener_clear(sockfd); /* 关闭即清除该 fd 的挂起状态(幂等) */ net_pending_clear(sockfd); + /* 挂起 send 同样幂等清理(cancel 已在上方通知,此处仅保证状态归零) */ + net_send_pending_clear(sockfd); /* 关闭成功 */ return 0; @@ -2208,6 +2259,8 @@ static int net_drop_conn_locked(net_sock_t *p_sock) { /* 先结束本 fd 挂起 recv(排空/取消 + 必通知),再拆芯片会话 */ net_pending_settle_close((int)(p_sock - s_net_socks), p_sock); + /* 挂起 send 同样先结束(以已发字节数通知;未发部分放弃),再拆芯片会话 */ + net_send_pending_cancel((int)(p_sock - s_net_socks), NET_ERR_CONNRESET); /* 确定性拆除(异常释放 → QUARANTINE 400ms 底,TRAP-NET-18 语义) */ if (net_sock_reset_sync(sockfd, CH395F_RESET_QUARANTINE) != 0) { @@ -2837,17 +2890,22 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, * buf - 待发送数据缓冲区指针 const void * * len - 数据长度 int > 0 * 出口参数:无 - * 返回值:实际发送字节数(>= 0);-1 表示参数无效 int + * 返回值:实际发送字节数(>= 0);-1 表示参数无效 / 需等待但被拒(BUSY) int * 限定条件:p_sock 非空;netTask 上下文(避免向自身发送消息造成死锁) - * 函数说明:直接调用 net_send_locked 完成发送 + * 函数说明:1. 直接调用 net_send_locked 完成发送 + * 2. SEND pending 化后:发送缓冲未及时腾出(超 NET_SEND_SPIN_MS)时, + * 挂起登记会被"拒绝执行者自等待"守卫拒绝(netTask 内无人能完成它) + * → 返回 -1(BUSY)。本接口因此只适合"发送缓冲可用"的场合; + * netTask 内需要长时间等待的发送,须自行分片或改用挂起模型 */ int net_send_sock(net_sock_t *p_sock, const void *buf, int len) { /* 控制块指针为空 → BADF */ if (p_sock == NULL) { return NET_ERR_RETURN_1(NET_ERR_BADF); } - /* 直接委托锁定实现(netTask 上下文,无队列往返) */ - return net_send_locked(p_sock, buf, len); + /* 直接委托锁定实现(netTask 上下文,无队列往返); + * caller = netTask 自身 → 挂起登记必被拒(自等死锁守卫) */ + return net_send_locked(p_sock, buf, len, xTaskGetCurrentTaskHandle()); } /* * 函数功能:接收数据(内部版本,绕过消息队列,供 netTask 直接使用) diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index 93677a92..ed36bbf1 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -226,9 +226,18 @@ int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen); * buf - 数据缓冲区 * len - 数据长度 * flags - 通常为 0 - * 返回值:实际发送字节数,失败返回 -1 + * 返回值:实际写入字节数——**= len 为全部写入;< len 为短写,调用方必须检查**; + * 失败返回 -1(errno 已置位) * 限定条件:TCP 连接已建立,指针非空 - * 函数说明:阻塞等待发送缓冲区空闲,拷贝数据后触发发送 + * 函数说明:1. 快路径:短暂自旋等待发送缓冲区空闲(预算 NET_SEND_SPIN_MS,默认 2ms), + * 拷贝数据后触发发送——健康链路下(实测 219us 量级)行为与旧版逐字节一致 + * 2. 慢路径:自旋超预算后把请求**挂起登记**给 netTask,由 + * net_send_pending_tick 在发送缓冲区空闲时续传(分片 NET_SEND_CHUNK_MAX), + * 调用方在任务通知上睡眠——**不再占用 netTask 执行者**(消除 E-02 队头阻塞) + * 3. 短写语义:无论快路径超时还是挂起完成,只要存在未写完字节即按已发数返回, + * 不假装全部成功;调用方按短写重试剩余部分(见 BSD_Socket_API 使用指南 §3.5) + * 4. ⚠ 禁止在 IWDG required_mask 内任务做**无预算**的整块发送;挂起路径本身 + * 受 NET_SEND_TIMEOUT_MS 无进展超时兜底,但仍建议调用方自带超时预算 */ int net_send(int sockfd, const void *buf, int len, int flags); diff --git a/docs/README.md b/docs/README.md index ce9a63cc..ea575e50 100644 --- a/docs/README.md +++ b/docs/README.md @@ -94,8 +94,8 @@ docs/ | [`IEC104上行设计说明.md`](子系统设计/IEC104上行设计说明.md) | **B4 上行规约**:IEC 60870-5-104 被控站(TCP 2404)范围/点表(V0.1 有效子集)/链路流程(k/w/t1~t3、总召、COV、遥控两段式、定值)/netTask 接入决策/预算与测试方案;协议核心纯逻辑 + L0.5 全栈单测 | | [`网络/BSD_Socket_API_使用指南.md`](网络/BSD_Socket_API_使用指南.md) | **上层视图**:BSD Socket API 公开语义、示例、常见问题 | | [`网络/BSD_Socket_API_函数调用关系.md`](网络/BSD_Socket_API_函数调用关系.md) | **内部视图**:netTask / 消息队列 / L3 挂起 / 重试对账的调用关系与设计意图 | -| [`网络/Net层阻塞模型架构设计.md`](网络/Net层阻塞模型架构设计.md) | **等待模型**:pending 请求架构、五路完成语义、设计边界与性能特征 | -| [`网络/网络自愈架构设计.md`](网络/网络自愈架构设计.md) | **网络自愈架构**:现状权威描述——设计原则(不变量 I1~I8)、分层职责与三 tick、reconciler 形态①~⑤ + M3 门、驱动原语、recv 语义、KeepAlive、参数表与 E 锚点速查、红线与延后项;审计过程(T1~T4/E0~E13)已归档 | +| [`网络/Net层阻塞模型架构设计.md`](网络/Net层阻塞模型架构设计.md) | **等待模型**:recv/send 挂起请求架构、数据结构与挂起槽状态机、recv 五路 + send 四路完成语义、模块接口契约(14 函数)、实现落点(20 集成点)、调用方契约、设计边界与性能特征 | +| [`网络/网络自愈架构设计.md`](网络/网络自愈架构设计.md) | **网络自愈架构 + 监听生命周期**:现状权威描述——根因三层证据与 A′ 出局、设计原则(不变量 I1~I8)、分层职责与三 tick、reconciler 形态①~⑤ + M3 门、驱动原语、FUN_PARA bit3 契约、recv 语义、KeepAlive、参数表与 E 锚点速查、红线、代码级事实清点、决策记录 D1~D7、`net_listener.c` 模块化边界、延后项;审计与 B10 根治过程已归档 | --- @@ -272,6 +272,7 @@ docs/ | 日期 | 变更 | 说明 | |---|---|---| +| 2026-10-14 | 网络文档合并 | 《网络Socket监听生命周期设计说明.md》(B10 根治,587 行)并入 [`网络/网络自愈架构设计.md`](网络/网络自愈架构设计.md):原理与规范层论证 → §1.1~§1.4、FUN_PARA bit3 契约 → §4.9、代码级事实清点 → §9、决策记录 → §10、`net_listener.c` 模块化边界 → §11,原「引用与归档约定」顺延为 §12;过程类内容(分阶段勾选清单、台架验收记录)不迁、留 git 历史;原文档删除。全库引用同步 13 处(`net_listener.c`/`.h` 注释、Net层阻塞模型、BSD_Socket_API 两篇、陷阱记录 B10、编号规范 §4.2/§5.2/§7.2);本索引 §5.1 补登记 | | 2026-10-14 | E14"丢中断"复核与归因更正 | 48 轮直读 SINT 矩阵复核历史"丢中断"说法:**芯片侧事件丢失 = 0**("丢失"= drain 读清消费 3/48 + 门控竞态 2/48 + 主机同步 1/48,幽灵态 0/48 未复现);根除性修复 = listen/relisten 路径排事件挪至 LISTEN 命令**之前**(吞新会话 CONNECT 的生产侧自伤窗);全局读收口(全局寄存器唯一读者=`net_poll`,I7 护栏);全库 17 文件约 40 处"丢中断"归因更正为"事件未收到/读清消费竞态",`docs/网络/网络自愈架构设计.md` §5.2 新增 E14 锚点;台架保留复核探针(drain-int/SINT 补读×3/超时归类);同日 E15 幽灵态归因复核(官网+手册实锤:0x04+旧端口 = 芯片**如实报告**旧连接拆链进行中——bit3=1 保留现场为自家配置、拆链窗与 §5.17 重传默认值吻合、WCH 官方博客确认断连即重连被拒且官方方案 = 关闭完成后重开,即 settle 等待) | | 2026-10-13 | 网络自愈架构文档瘦身与改名 | 合并文档 `网络/N34-自愈审计.md`(2062 行审计全记录)瘦身为**终版设计描述**(281 行:不变量 I1~I8 / 架构与机制详解 / 参数与 E 锚点速查 / 红线 / 延后项),并经 `rename_ref.py` 改名 [`网络/网络自愈架构设计.md`](网络/网络自愈架构设计.md)(全库 16 文件 30 处引用同步);审计过程(T1~T4、E0~E13)归档 git 历史,考古命令见该文档「引用与归档约定」 | | 2026-10-13 | 网络自愈审计归档 | 原 `docs/网络/N34-自愈审计/` 目录 7 份文档(根因总览 / net 层盘点 / 驱动层审计 / 主流对照 / 汇总裁决 / 实施方案 / 实验方案)**合并为单文档 [`网络/网络自愈架构设计.md`](网络/网络自愈架构设计.md)**,原目录删除;全库引用同步(`docs/` 4 篇 + 驱动/网络代码注释 + 配图生成器与 SVG)。本索引 §5.1 补登记 | diff --git a/docs/images/Net层阻塞模型.svg b/docs/images/Net层阻塞模型.svg index 205dc5a0..74eca71d 100644 --- a/docs/images/Net层阻塞模型.svg +++ b/docs/images/Net层阻塞模型.svg @@ -1,7 +1,7 @@ - - + + Net 层阻塞模型 · pending 请求架构 -把「等数据」从执行者内的自旋,改为执行者外的登记 —— 调用方在自己上下文睡眠,netTask 零占用地服务整个队列 +把「等数据 / 等发送缓冲」从执行者内的自旋,改为执行者外的登记 —— 调用方在自己上下文睡眠,netTask 零占用地服务整个队列 调用方任务 @@ -32,7 +32,7 @@ 与 LwIP 消费者线程模型同构 · 取消必通知,永不挂死 是 否,timeout > 0 - + 立即 SPI 读出 GET_REMV_LEN / READ → 通知调用方(结果 = 字节数) @@ -41,24 +41,27 @@ 登记即返回 (微秒级) - + netTask 主循环(~1ms 节拍)—— 挂起期间照常服务整个队列 - -net_poll() + +net_poll() 刷新 readable - -net_pending_tick() -结束挂起 - -net_listener_retry_tick() -重试对账 - -排空消息队列 -服务所有消费者 - -net_pending_tick():五路完成 / 取消判定(每轮对每个挂起 fd 按序) - - + +net_pending_tick() +结束挂起 recv + +net_send_pending_tick() +推进挂起 send + +net_listener_retry_tick() +重试对账 + +排空消息队列 +服务所有消费者 + +挂起 recv:五路完成 / 取消判定(net_pending_tick 每轮对每个挂起 fd 按序) + + ① 数据到达 → SPI 读出,通知调用方(结果 = 字节数) @@ -75,12 +78,13 @@ 网络重建(芯片复位) → 取消 + 必须通知(否则调用方永久挂死) 挂起超时留痕探针(编译期选项,默认关):挂起超阈值仍未结束 → 只 DBG_ERROR 留痕(不强制结束、不伪造返回值) - -xTaskNotify -唤醒调用方(结果 = 字节数 / 负错误码) - - - +挂起 send 四路:①断连(以已发字节数结束)②推进(每轮至多一块 ≤ NET_SEND_CHUNK_MAX)③写完(以总长度结束)④无进展超时(短写)—— net_send_pending_tick 每轮判定 + +xTaskNotify +唤醒调用方(结果 = 字节数 / 负错误码) + + + -关键性质:挂起期间 netTask 零额外占用(判据复用主循环既有 net_poll 的 readable,不引入额外 SPI 事务);公开 API 不变,既有调用方 / 测试 / 模拟器零改动。 +关键性质:挂起期间 netTask 零额外占用(recv 判据复用 net_poll 的 readable,不引入额外 SPI;send 有界自旋 ≤ NET_SEND_SPIN_MS,netTask 最坏停滞由 1s 压到 2ms);公开 API 不变。 diff --git a/docs/任务规划/基线与待确认.md b/docs/任务规划/基线与待确认.md index 19387487..4f943266 100644 --- a/docs/任务规划/基线与待确认.md +++ b/docs/任务规划/基线与待确认.md @@ -11,21 +11,21 @@ ## 2. 设计基线(供后续接入参考) -### 2.1 pending 二期设计基线(ACCEPT + SEND) +### 2.1 pending 二期设计基线(SEND 已落地 / ACCEPT 待办) -> **来源**:一期(RECV)架构现状见 `docs/网络/Net层阻塞模型架构设计.md`(演进史查 git 历史); +> **来源**:recv/send 架构现状见 `docs/网络/Net层阻塞模型架构设计.md`(演进史查 git 历史); > 本节仅保留**二期仍要用的设计基线**。 **一期已验证的机制骨架**(二期沿用,勿重新发明):独立静态 pending 表 + `net_pending_tick` -主循环按节拍结束挂起(五路:数据/到点/断连/同 fd 串行/重建取消必通知)+ 登记守卫(每 fd 至多一个、 -不覆盖、拒绝 netTask 自等待)+ 1s 看门狗强结算。 +主循环按节拍结束挂起(recv 五路:数据/到点/断连/同 fd 串行/重建取消必通知)+ 登记守卫(每 fd 至多一个、 +不覆盖、拒绝 netTask 自等待)+ 挂起超时留痕探针(tripwire,只留痕不伪造返回值)。 **二期扩展点**: -| 项 | 目标 | 对应缺陷 | -|---|---|---| -| **ACCEPT pending** | accept 等待改为"登记 → CONNECT 事件完成",退役 100ms/200ms 轮询循环 | E-06(accept 轮询计时失真) | -| **SEND pending** | 发送续传改为"登记 → send_ready 完成",消除 1s/分片 stall 对队列的冻结 | E-02 残余(SEND 位置) | +| 项 | 目标 | 对应缺陷 | 状态 | +|---|---|---|---| +| **SEND pending** | 发送续传改为"登记 → send_ready 完成",消除 1s/分片 stall 对队列的冻结 | E-02 残余(SEND 位置) | ✅ **2026-10-14 已落地**(`net_send_pending_tick` + 有界自旋 `NET_SEND_SPIN_MS`;架构见 `docs/网络/Net层阻塞模型架构设计.md` §4.5/§5.5/§7) | +| **ACCEPT pending** | accept 等待改为"登记 → CONNECT 事件完成",退役 100ms/200ms 轮询循环 | E-06(accept 轮询计时失真) | ⬜ 待做 | **二期门禁**(一期门禁的加强版): @@ -39,10 +39,13 @@ 8. 看门狗接 selftest 留痕 + 模拟器差距清单 +1(`net_socket_sim.c` 无法建模 pending)。 **已知代价(延续一期,需文档化)**:pending 完成判定随主循环节拍运行,被长操作占用时超时 -顺延(~1ms 级);二期把 SEND 也 pending 化后,该顺延面反而**收窄**。 +顺延(~1ms 级);**SEND 已 pending 化(2026-10-14)**,该顺延面已收窄——发送停滞不再顺延整层; +剩余 ACCEPT 化不改变此结论。 -**改动量估算**:再 ~300-500 行,集中在 `net_socket.c`;公开 API 不变。风险评级**中** -(执行器核心 + D 系列前科)——**大的是验证,不是代码**。 +**改动量估算**:SEND 实际落地约 300 行(`net_pending.c` +287 行、`net_socket.c` 10 处集成点、 +`net_config.h` 新增 `NET_SEND_SPIN_MS`);公开 API 不变。风险评级**中**(执行器核心 + D 系列前科) +——**大的是验证,不是代码**。SEND 已完成构建门禁(0 错误 0 警告)与静态核对,**台架复测待做** +(netTask 栈峰值须重测)。剩余 ACCEPT 预计再 ~100-200 行。 ### 2.2 错误码通道四要素(CH395F 范式) diff --git a/docs/任务规划/登记簿.md b/docs/任务规划/登记簿.md index 97aa909a..6f39d341 100644 --- a/docs/任务规划/登记簿.md +++ b/docs/任务规划/登记簿.md @@ -10,7 +10,7 @@ |---|---|---|---|---| | N-2 | **49s 全系统日志静默(defaultTask/NAND syslog 排空停滞)**——串口日志 49s 无行,期间 net 栈照常处理外部连接 → 日志管线停滞,队列满后各任务日志行被丢;系统功能不受损 | 2026-09-22(L3 台架 F2,gen 181) | **A/B 归因:既有存储侧现象**。**2026-10-03 降级为低风险观察项**:① 丢弃可观测性**代码已具备**(`console_log.c` `s_con_drop` + `[ERR] CONSOLE dropped`;`sys_log_mcu.c` 队列满 `DBG_ERROR`;UART TX 僵尸通道自愈);② V5 30 min **未复现 49s**(>10s 静默全为 ~28s、均在 `e02_probe` 窗口内=N-17)。**残余待办**:与 S8(非阻塞日志)合并评估(C3) | `错误码传播审计.md` E-02 阶段二 | | N-3 | **L5 队列往返基线(1223/988µs)的 L3 后重验**——L5 测 DONTWAIT 队列全程;L3 对该路径逐位不变 → 机制上无影响,但基线数值属分片时代,严格重锁需跑一轮 Phase 13(需 PC peer) | 2026-09-22(L3 收尾留档) | 低风险:10ms/100ms 阈值余量 >8×;下次 Phase 13 窗口顺带重锁即可 | `链路延迟测试说明.md` §6.4 L5 注¹ | -| N-4 | **pending 机制二期:ACCEPT + SEND 阻塞路径 pending 化**——E-06、E-20、E-21 剩余分量共用该机制扩展;届时 L3/L5 基线再重锁、探针族扩充、看门狗接 selftest 留痕、模拟器差距清单 +1 | — | 一期(RECV)已落地并入主线;架构现状见 `Net层阻塞模型架构设计.md`,二期设计基线见 `基线与待确认.md` §2.1 | `错误码传播审计.md` 第二档 #2 | +| 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` | diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 5da3b59c..8bd9ed43 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 230,048 B (224.66 KB) | 1,048,576 B (1024 KB) | 21.9% | -| RW_IRAM1 | 97,912 B (95.62 KB) | 131,072 B (128 KB) | 74.7% | +| 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% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **231,152 B (225.7 KB)** | 1,048,576 B (1024 KB) | **22.0%** | +| **Flash (ROM 合计)** | **232,092 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 搬运,栈压力小) | 920 B(余 76%)/ 静态 **1944 B**(2026-10-12 N32 监听模块化后复测;自愈收敛阶段 3 合并后复测 **1944 B** 不变,链尾经 net_listener_retry_tick→net_relisten_locked) | +| 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) | | 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 baf1ca5a..eac40606 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,14 +15,14 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 230,048 B | 1,048,576 B | 21.9% | 参考 | +| ER_IROM1 (Flash) | 230,988 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 | 97,912 B | 131,072 B | 74.7% | PASS | +| RW_IRAM1 | 98,136 B | 131,072 B | 74.9% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| -| 网络 | 13,458 B | 12 KB | ⚠ 超 1.1 KB | +| 网络 | 13,682 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/子系统设计/网络Socket监听生命周期设计说明.md b/docs/子系统设计/网络Socket监听生命周期设计说明.md deleted file mode 100644 index d4c6f015..00000000 --- a/docs/子系统设计/网络Socket监听生命周期设计说明.md +++ /dev/null @@ -1,587 +0,0 @@ -# 网络 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-NET-15/-20 定案为保留排空现场)⇒ 断开后芯片必然停在 `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-NET-09 沉降窗 | - -另有一条**探针失效**:`net_can_accept`(`:1453`)要求 ESTABLISHED 才返 1, -而 `Drivers/BSP/UI/ui_net_sink.c:512` 用它门控 accept → :7003 **永不进 `net_accept`** -→ TRAP-NET-20 的 `net_lost_connect_probe`(`:1250-1289`)对这个端口**从不运行**, -CONNECT 事件未收到时只剩对账②。:21/:8888 因为无条件轮询 accept,探针才活着。 - -### 2.4 FUN_PARA bit3=1 的必要性与安全边界(2026-10-12 评估,结论:**必须保留 0x08**) - -> 用户提问:「bit3 在本项目设置为 1 的必要性大吗?会导致这么多问题;如果设为 1, -> 如何安全处理?」本节以手册原文 + 实测证据回答,并把它并入决策记录 D4。 - -**① 必要性 = 硬性必需,不是可调项。** 手册对 bit3 的分流写得很直白: - -- `docs/芯片手册/CH395F.md` §5.52 注释②:「此位为 0 表示在产生 SINT_STAT_DISCONNECT 或者 SINT_STAT_TIM_OUT 中断后, - CH395 会主动关闭 Socket。……**关闭 Socket 就意味着 CH395 会将此 Socket 内部缓冲区和 - 一些相关变量清除**,在某些应用中,如果 CH395 内部自行关闭 Socket 可能会导致残留的数据 - 被强行清除。这种情况下可以将该位置 1,**由单片机将数据读出来,然后再关闭 Socket**。」 -- `docs/芯片手册/CH395F.md` §5.23 L572 同向:「为 0 则……主动将 Socket 状态置为关闭状态**并将相关缓冲区全部清除**; - 反之则不会对 Socket 状态和相关缓冲区做任何操作,以方便外部单片机……**将残留的数据读出**。」 - -实测现场(TRAP-NET-20 三次定案的判别探针):`st=0x05/0x00` 且 `recv_len=1027`—— -Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少**。这正是 bit3=1 按规格 -保留现场,不是芯片异常(`docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-20「定案二」)。 - -**② bit3=0 的代价是「硬件保证的数据丢失」,且不可在软件层补救。** -本产品最典型的流量形态是「发完即 FIN」:ftplib `storbinary` 语义 = `sendall` 发完立即关 -数据连接、回控制通道等 226。芯片把最后一段数据落进 RX 的同时抛出 DISCONNECT: - -- bit3=1:RX 数据在,MCU 来得及读(TRAP-NET-18 的「排空 RX 再 close」修复才成立); -- bit3=0:芯片**当场**关 Socket 并清缓冲 → MCU 永远读不到尾部 → 文件被截断却仍回报 226 - (静默损坏)。这是 bit3=0 的**必然结果**,不是概率问题——软件再聪明也拿不回已被硬件擦掉的数据。 - -即:改 bit3=0 等于用「一个硬件保证、不可修的数据丢失」去换「一个软件侧可修的关闭义务」, -严格更差。:8888 对时包、:7003 远显推流同样是「数据 + 随即关闭」形态,一并受害。 - -**③ bit3=0 也并不能真正消除孤儿态。** 芯片自动关以**收到** DISCONNECT/TIM_OUT 为前提, -而 TRAP-NET-17 已实证快速复连场景芯片会**丢**这两个中断(`docs/问题记录/CH395F_Trap_Records.md` -B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket 照样存在。 -它只是降低出现概率,代价却是上面第②条。 - -**④ 所以「这么多问题」不归 bit3=1。** 断开后停 `0x05/0x00` 是 bit3=1 的**合法后果**(D4); -真正造成 B10 永久拒连、消费者卡死、EOF 被吞没的,是软件侧没把「该由 MCU 关 Socket」这条 -义务落到唯一所有者——重建所有者 12 个、无人负责组合 15 个(§2.3),以及 U12 的「relisten -的 LISTEN 失败不 close 芯片 Socket」与 open 失败路径不对称。bit3=1 只是把义务显式化, -把原本被芯片掩盖的架构欠债照了出来。 - -**⑤ 安全处理 = 把 bit3=1 的契约在软件侧收口**(即 §4 的两条不变量,逐条对应上面的缺口): - -| 契约要求 | 对应缺口 | 收口手段 | -|---|---|---| -| 「会话结束」必须被**谁都能看见**,不能只在边沿那一刻 | EOF 边沿化:抢先 relisten → recv 永久 `WOULDBLOCK`(§2.3) | 不变量 I:`net_sock_t.session_dead` 粘滞电平,`net_recv_locked` DONTWAIT 判据改 `state==CLOSED \|\| session_dead`(P1) | -| 关闭 Socket 必须**一定有人做**、且只做一次 | 重建所有者 12 个 + U12 的 LISTEN 失败不回 close | 不变量 II:`net_listener_pump` 单点重建 + 补 U12 的 close 对称(P2)(自愈收敛阶段 3:pump 已并入 `net_reconcile_tick` 形态③④,单一 100ms tick——不变量 II 不变,见 §10.10) | -| 关闭姿势必须正确 | TRAP-NET-12:陈旧 DISCONNECT 锁存秒断下一条连接 | `net_close_locked` 既有「close → 等 `CLOSED` → 排空本 Socket 中断」,是 bit3=1 下唯一正确的关闭序列 | -| 异常释放须先排空残渣再关 | TRAP-NET-18:~2KB RX 残渣被下条连接当自己的数据读走 | `net_relisten_locked` 隔离期(400 ms)持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED` | - -> **命名注(自愈收敛阶段 1.5,2026-10-13)**:上表与下文所说的「DONTWAIT」机制本体不变 -> (`NET_MSG_DONTWAIT` 仍是 `net_recv_locked` 的非阻塞路径与 EOF 判据载体),但对外 -> 入口已主流化:具名 API `net_recv_nb()`(单次非阻塞)+ `net_setblocking(fd, on)` -> (模式挂控制块 `blocking` 属性);`net_recv()` 默认改为**无限阻塞**(30s 默认预算 -> 删除),受 IWDG 监控任务的有界等待契约见 `docs/网络/BSD_Socket_API_使用指南.md` -> §10.1 与 `docs/网络/网络自愈架构设计.md` §4.5。本文所有「DONTWAIT 判据 / -> 返 WOULDBLOCK」表述按新命名逐字仍成立,不逐处改写。 - -**⑥ 本次(P0)已落地的口径修正**:`net_socket.c:3627` / `:3641` 原注释写「DISCONNECT 后 -芯片自动关闭 Socket(FUN_PARA 默认)」——与实际 `0x08` **相反**。这类注释正是 U12 这类 -「跳过显式 close」缺陷的观念源头,已改正并留痕。 - -## 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-NET-02/-11。 - -且 FUN_PARA **初始化后不可改**(`docs/芯片手册/CH395F.md` §5.52 L795⑥),这是开机一次性决策,不能运行时降级。 -**结论:A′ 出局;根治必须在单连接复用模型内做。** - -## 4. 根治原理:两条不变量 - -- **不变量 I(电平化)**:「这条会话已结束」必须是**可重复查询、幂等**的状态, - 而不是"恰好在那一刻观察到才算"。谁先看到不重要,**谁都饿不着**。 -- **不变量 II(单一所有者)**:监听器的重建**只有一个决定者**(net 层的 listener pump)。 - 其它所有位置只**置事实**,不**做动作**。(自愈收敛阶段 3 起该决定者 = `net_reconcile_tick` 形态③④,pump 已并入,见 §10.10) - -两条不变量成立后,"谁重建""会不会错过"这两类问题在结构上消失, -对账④从"唯一救命稻草"降级为"事件面未收到时的补漏"(TRAP-NET-17 实证事件面不是可靠唯一信源——E14 复核:事件非芯片侧丢失、被读清消费——故对账**必须保留**)。 - -## 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-NET-18 5/5×2)。 - -### P2 单一所有者(中改,消灭多所有者)—— P2a ✅ 2026-10-12 / P2b 建议暂缓 - -1. 新增 `net_listener_pump()`(100 ms 周期),接管**周期性**重建,是唯一做周期性 - `net_relisten_try` 的地方。✅ 已落地(自愈收敛阶段 3 已并入 `net_reconcile_tick` 形态③④,单一 100ms tick,见 §10.10) -2. ⚠ **原第 2 条已被实测推翻**:原计划「删除 `net_accept_locked` / `net_recv_locked` / - `net_pending_tick` 三处即时重建」。实测删掉会把 CLOSED 窗口从 ~0 拉长到 ~100 ms, - 调用方高频空转 `net_accept`(RPC 无上界)→ uiTask 最坏单轮 172 ms → 1034 ms + `[WD] task stall`。 - **故保留这三处**(消费者动作触发,非周期性抢先),只删对账③④ 两处**周期性**路径。 - 调用点 7 → 5。✅ 已按修正版落地(详见 §9) -3. P2b:消灭 netTask 内的同步忙等(U6):`net_close_locked` 的 500 ms `osDelay` 循环、 - `net_listen_locked` 的 3×500 ms,改用既有 D10 非阻塞重试模式。 - **建议暂缓** —— 实测 `task stall` 在忙等未改造时同样存在,危害是理论性的(详见 §9) -4. 补 U11:本层 CLOSED 而芯片**正在** LISTEN → 只做状态重同步,不重下发命令。✅ 已落地 -5. 验收:`:21` 0ms×50 仍 50/50;`netp`(`net_poll` 段最长耗时,µs)不劣化。✅ 见 §9 - (⚠ 原判据「close/listen 风暴期间无 `[WD] task stall`」**不可用**:该现象在基线同样存在, - 见 §9 的 P1/P2a 对比数据) - -### 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-NET-17 实证事件面不是可靠唯一信源,不可删)。 - -## 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-NET-17 实证事件面不是可靠唯一信源)。 -- **中断所有权**:全局寄存器整体读即清、无按位清除(`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 时挂起 recv 必被结束(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-NET-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-NET-09 沉降窗。 - -## 8. 决策记录 - -| # | 决定 | 依据 | -|---|---|---| -| D1 | **A′(启用芯片多连接模式)出局**,根治必须在单连接复用模型内做 | `docs/问题记录/CH395F_Trap_Records.md` 附 FTP 改造清单第 4 条「多连接模式不支持多监听」;本产品 3 常驻监听 + 动态 PASV;§9.2.6 L1295 池按源端口匹配会互串;历史实测 TRAP-NET-02/-11;FUN_PARA 初始化后不可改(`docs/芯片手册/CH395F.md` §5.52 L795⑥) | -| D2 | 采用**形态 B 深化**(电平化 + 单一所有者),**不做 P3** | P3 需控制面重写、RAM/Flash 上升,只有"同端口并发多连接"需求才值得 | -| D3 | 对账④**保留**(`4f2cc8b` 不撤回),其副作用由 P1 消除 | TRAP-NET-17 实证事件面不是可靠唯一信源(E14 复核:非芯片侧丢失,是读清消费)→ 周期对账不可删;④ 已把 `:7003` 从 0/10 永久死拉到 10/10 | -| D4 | `FUN_PARA` **保持 `0x08`**,不因孤儿态改配置(2026-10-12 复核确认) | bit3=1 是 TRAP-NET-15/-20 定案的"保留排空现场"契约;bit3=0 会令芯片在 DISCONNECT/TIM_OUT 时**当场清缓冲**(`docs/芯片手册/CH395F.md` §5.52② / §5.23 L572)→「发完即 FIN」流量的尾部数据必被硬件擦除、**不可补救**;且 bit3=0 也挡不住事件面未收到(TRAP-NET-17)→ 孤儿态照样有。孤儿态是 bit3=1 的合法后果,应在软件层收口而非回退契约。完整论证见 §2.4 | -| 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(零风险)** -- [x] 修 `Drivers/BSP/NET/net_socket.c:3627`、`:3641` 的 FUN_PARA 口径错误注释(实际 `0x08`,芯片**不**自动关)—— 2026-10-12 已改并留痕(§2.4⑥),`@build` 0 错 0 警 -- [x] 把 U11 / U12 / U14 / U15 登记进 `docs/问题记录/CH395F_Trap_Records.md`——**实际落点改 《网络自愈架构设计》**:U11/U12/U14 机制归属见 `docs/网络/网络自愈架构设计.md` §4.1;U15 经 E6 裁决(命中=0)登记观察项(`docs/网络/网络自愈架构设计.md` §4.8) -- [x] 在 B10 行确认已记「④ 扩大 EOF 吞没窗口」——2026-10-13 复核:新 TRAP-NET 文档 B10 行在位(✅ 已修 + 副作用注记 + `net_drop_conn` 根治出口) - -**P1(边沿→电平)** -- [x] `net_types.h` 的 `net_sock_t` 增 `uint8_t session_dead;` -- [x] 置位:`handle_disconnect_event` 释放分支、`handle_timeout_event` ESTABLISHED 分支、对账①(复用前者)、`net_connect_locked:1685` -- [x] 清零:`handle_connect_event`(新 CONNECT,两支)、`net_close`/`net_socket` 的 memset 与显式清零;**`net_relisten_locked` 不清**(关键,已加注释钉住契约) -- [x] `net_recv_locked` DONTWAIT 判据改 `state==CLOSED || session_dead` -- [x] `ui_net_sink.c:563` 加 `g_client != g_listen` 护栏;EOF/错误后不再自行 `bind_listen` -- [x] 新增 `net_listener_armed(fd)`(LISTENING 或 CLOSED+auto_relisten),替换 `ui_net_sink.c:512` 的 `net_can_accept` 门控(MCU + `net_socket_sim.c` 双端实现) -- [x] **超出清单的必要扩展(设计时未预见)**:`net_pending_tick` ③ 断连判据同步改 `CLOSED || session_dead`。不改则 lftpd 的**阻塞** recv 仍会被抢先 relisten 饿死——①要求 ESTABLISHED、③要求 CLOSED,抢先 relisten 后两头落空,只能干等 deadline 超时。判据放宽后,两处 relisten 调用均须补内层 `state == CLOSED` 门:`net_relisten_try` 无状态门,对已 LISTENING 的 fd 再 open+listen 会因 USE 错误走进关闭分支、**关掉正听着新 SYN 的 Socket**(U12 同源事故) -- [x] 全量回归矩阵(§7.1 那一套)—— 2026-10-12 真机全绿,见下「P1 台架验收记录」 -- [!] 「抢先 relisten」腿 —— **外部注入不可构造,已放弃**:见下「验收腿的否定结论」 -- [x] `@test` PASS → 回读校验(`No difference found`) - -**P1 台架验收记录(2026-10-12,`@build` Code=197096 / 基线 197084,+12 B)** - -| 腿 | 结果 | 说明 | -|---|---|---| -| `@test` 启动闭环 | **PASS** | 启动锚点齐、无 `[ERR]`、reset cause ×1、7.2 s 到 net ready | -| `ftp_regression` | **11/11** | 含 STOR 32KB —— 「发完即 FIN」链路,`session_dead` 直接作用面 | -| TRAP-NET-18 `--check-abort`(RST) | **5/5** | 450 + 5xx 正确回报,无误报 226 | -| TRAP-NET-18 `--graceful`(FIN) | **5/5** | 226 正常 | -| `:21` 快速复连 0 ms ×50 | **50/50** | TRAP-NET-17 基线不回归 | -| `:21` 快速复连 100 ms ×50 | **50/50** | 同上 | -| `:8888` 0 ms ×20 | **20/20** | 对时服务不回归 | -| `:7003` 300 ms ×10 | **10/10** | B10 原始失败场景(修复前 0/10) | -| 设备侧孤儿对账 `orphan listener` | **0 次** | 探针时段经 serial-assistant `clear` 划界后统计 | - -> 台架观测用 **`serial-assistant` skill**(daemon 独占串口、日志即接口)—— -> 设备侧簿记(`standalone accept` / `auto-relisten ok` / `DISCONNECTED, releasing` / -> `orphan listener`)直接 `log --grep` 取增量,无需自建串口客户端。 -> ⚠ 自建 pyserial 客户端会撞 `PermissionError(拒绝访问)`:daemon 独占绑定串口。 - -**验收腿的否定结论(重要,勿再重复踩)**:设计文档原计划的新增腿是 -「连上 :7003 → 拖住 uiTask 使 relisten 先发生 → 断连 → 断言 sink 仍得 EOF」。 -2026-10-12 实现后发现**真机外部无法构造**,且据此写的探针**无判别力**: - -- 外部唯一可注入的是「A 发完数据立刻 FIN + 立即 B 抢占」。实测该形态下 - **P1 版与对照组(撤掉 `g_client != g_listen` 护栏)双双 PASS**,设备侧 - `accept/relisten/disconnect` 簿记与 `orphan` 计数完全一致 → 探针测不到它声称要测的东西。 -- 机理:真实「抢先」要求 relisten 发生在**消费者下一次 recv 之前**;而对账④ 周期 - 200 ms + 去抖(`NET_RECONCILE_STABLE_N`),`ui_net_sink` 轮询 100 ms —— - 正常时序下消费者总是先 recv 到 EOF,对账抢不到。**只有 uiTask 被拖住**才可能出现, - 而那是设备内部状态。 -- 处置:按 TRAP-NET-20「不留未证明的复杂度」**删除该探针**;该场景登记为**残余验证项**, - 需设备内注入点(参照 `TEST_LATENCY_PROBE` 的编译开关模式)才能在真机复现。 -- 因此 P1 的**正确性依据**是:① 代码级论证(判据由边沿改电平、置位/清零完备、 - 两处 relisten 补状态门);② 上表全量回归矩阵**不回归**;③ 不变量 I 的结构性论证。 - 「抢先」的前瞻场景**未被真机证明**,如实标注,不冒充已验收。 - -**P1 落成状态(2026-10-12)**:代码已实现、`@build` 0 错 0 警(`Code=197096`,较基线 +12 B)、 -`mem-numbers` 快照已 `--write` 刷新、除下述基线问题外静态门禁全绿。 -⚠ **已知基线问题(非本改动引入)**:`doc-links` 在**任何从当前 `v2-on-ui` 切出的工作树**上都会报 -2 处失效引用——`RAM使用分析与优化方案.md` 引用了两个**尚未提交**的文件(RAM 基线 `.tsv` 与 -RAM 预算台账 `.md`,属另一在途任务)。这两个文件在主工作树磁盘上存在故主线门禁呈绿, -工作树里没有故必报红(机理见 `docs/环境与部署/并行任务工作树方案.md` §10.2)。 -待该任务合入后自动消解,**不应**由本分支代为提交他人半成品。 - -**P2(单一所有者)—— 已拆为 P2a / P2b** - -**P2a(✅ 2026-10-12 落地,提交 `7cb5af0`)** -- [x] 新增 `net_listener_pump()`(100 ms 周期 + 去抖 + 冷却),接管**周期性**重建, - 覆盖:① LISTENING 失位 ② CLOSED 不在 LISTEN ③ CLOSED 但芯片**正在** LISTEN(U11) - (自愈收敛阶段 3 已并入 `net_reconcile_tick` 形态③④/U11,单一 100ms tick,见 §10.10) -- [x] 删对账③④ 及其孤儿态认领门槛(对账只剩①②;认领收窄为 ESTABLISHED/LISTENING) -- [x] 补 U11:本层 CLOSED 而芯片正在 LISTEN → **只做状态重同步**(含 `last_peer_port` - 转登 + `lost_conn_seen` 清零,同 `net_resync_listen`),不发任何芯片命令 -- [x] `net_listener_armed` 收紧:CLOSED 态仅在 `readable≠0`(有残留)时放行 -- [x] 验收:`:21` 0ms×50 / 100ms×50 各 **50/50**;`:8888` **20/20**;`:7003` 300ms×10 - **10/10**;`ftp_regression` **11/11**;Trap23 abort/graceful 各 **5/5**; - 设备侧 `orphan listener` 增量 **0**(P1 基线同条件为 **2**)→ 目标达成 - -- [ ] **P2b(同步忙等改造)—— 建议暂缓**:`net_close_locked` 的 500 ms `osDelay` 循环、 - `net_listen_locked` 的 3×500 ms。**实测依据**:全量历史 `task stall` 在忙等未改造时 - 同样存在(P1 基线同条件 53 条),忙等的危害是**理论性**的;而 `net_close` 是每次会话 - 结束都走的热路径。按 TRAP-NET-20「不留未证明的复杂度」,为一个未发生的故障去动热路径, - 性价比存疑 —— 建议等有 `task stall` 与忙等的**因果证据**后再做。 - (**自愈收敛阶段 3 已实施 2026-10-13**:不是"为忙等而改",而是驱动 `sock_reset` 原语落地后 - 顺路收口——`net_sock_reset_sync` 确定性排渣替代盲等(GRACEFUL 100ms 底 / 600ms 封顶), - 并删除 `net_close_locked` / `net_connect_locked` 的全局中断读清(F-NET-7),见 §10.10) - -> ⚠ **P2a 与设计文档原清单的关键差异(实测推翻原计划,务必据实执行)**: -> 原清单第 2 条要求把 `net_recv_locked` / `net_pending_tick` / `net_accept_locked` -> **三处即时重建一并删除**。台架实测证明**删掉会引入回归**: -> - CLOSED 窗口从 ~0 拉长到 pump 周期 + 隔离期(~100 ms); -> - 期间调用方高频发起 `net_accept`(RPC:入队 + `xTaskNotifyWait` 回执**无上界**) -> → 队列积压 → 实测 uiTask 最坏单轮 **172 ms → 1034 ms**,`[WD] task stall` 出现; -> - 故**保留这三处**:它们是「**消费者动作触发**」(消费者正在等这条连接的结局), -> 不是周期性「抢先」;真正该收敛的只有**周期性**路径(对账③④)。 -> - 收敛后 `net_relisten_try` 调用点 **7 → 5**(pump + 显式消息 + 3 处消费者驱动)。 - -> ⚠ **如实记录的观测**:`task stall` 是**既有现象**(P1 基线同条件 53 条 vs P2a 32 条, -> 未恶化);uiTask 最坏单轮的历史最大值 P2a 偏高(1054 ms vs P1 172 ms),但 `uil` -> (最近一次)为 10999 µs 即**当前正常**,判为一次性卡顿,**留待观察**。 - -**回滚点**:P0 纯注释可直接 revert;P1 若回归失败 revert 单提交即可(④ 仍在,退回到"端口活着但慢消费者可能卡死"的已知状态);P2a 独立成提交(`7cb5af0`),revert 后回到"对账③④ 兜底 + 12 所有者"的 P1 状态;P2b 未做。 - ---- - -## 10. 监听生命周期模块化边界设计(**已执行 2026-10-12**,提交 `c1ab902`) - -> **动机**:真机 net 层 `net_socket.c` 4126 行,而模拟器做同样的事只有 807 行(5 倍); -> `relisten` 一词在文件里出现 110 次。用户诉求 = **高内聚低耦合**。 -> P2a 把「周期性重建」收敛到 `net_listener_pump`(所有者 7→5)之后,搬移边界才**第一次清晰**—— -> 这正是 P2a 相对"直接抽模块"的价值:先收敛,再搬移。(自愈收敛阶段 3 起 pump 已并入 `net_reconcile_tick` 形态③④,见 §10.10) -> -> 本节只定**边界与接口**,不含任何代码;执行须过 §10.6 的验证门。 - -### 10.1 目标与可验证判据 - -「高内聚」在本项目落成**三条可机械检验**的判据(不是感觉): - -| # | 判据 | 现状 | 目标 | -|---|---|---|---| -| 1 | 模块外调用「重建监听」的**入口数** | 5(pump / 显式消息 / recv / accept / pending) | **1**(新模块导出) | -| 2 | `s_relisten_*` 静态状态被**模块外直接读写** | 2 处外部写 | **0** | -| 3 | 依赖方向 | 无 | 新模块 → `net_socket.c` **单向**,无反向 include | - -⚠ **明确不承诺**:本重构**不消除 fd 别名**(同一 Socket 既当监听器又当连接)。 -那是 CH395F 单连接模型 + bit1 多连接模式**不支持多监听**(Trap 记录改造清单第 4 条)锁死的, -属硬件上限(§3 D1)。重构只提升**组织度**,不改变架构上限——**别指望补丁消失,只指望补丁不再重复**。 - -### 10.2 现状清点(2026-10-12,`net_socket.c` 4126 行) - -| 类别 | 数量 | 明细 | -|---|---|---| -| 纯监听生命周期函数 | **7** | `net_relisten_locked`(139) / `net_listener_pump`(107) / `net_relisten_retry_tick`(47) / `net_relisten_try`(45) / `net_drain_chip_rx`(32) / `net_relisten_clear`(28) / `net_relisten_done`(16) | -| 静态状态数组 | **7** | `s_relisten_quarantine` / `_settle_start` / `_settle_dead` / `_wait` / `_next` / `_dead` / `_mark` | -| 常量 | **8** | `NET_RELISTEN_{QUARANTINE,GRACEFUL,SETTLE}_MS` / `NET_RELISTEN_SETTLE_{QUARANTINE,GRACEFUL}` / `NET_RELISTEN_{RETRY,DEADLINE}_MS` / `NET_LISTENER_PUMP_PERIOD_MS` | -| **搬移量小计** | **≈470 行** | 函数块 414 + 常量/状态注释块 55 | - -> ⚠ **自愈收敛阶段 3(2026-10-13)追注**:上表是 2026-10-12 搬移时的历史清点,原文保留。此后 `net_listener_pump` 已**删除**(重建决策并入 `net_socket.c` 的 `net_reconcile_tick` 形态③④/U11,单一 100ms tick),`NET_LISTENER_PUMP_PERIOD_MS` 等 pump 常量同删、新增 `NET_RECONCILE_PERIOD_MS` / `NET_RECON_STABLE_*`——见 §10.10。 - -**跨边界引用点**(这个数字决定接口面大小,是本设计的核心依据): - -| 引用 | 处数 | 位置 | -|---|---|---| -| `s_relisten_quarantine[fd] = ` 外部**写** | **2** | `handle_disconnect_event`(GRACEFUL)、`handle_timeout_event`(QUARANTINE) | -| `net_relisten_try()` 外部调用 | **4** | `NET_MSG_RELISTEN` 处理、`net_recv_locked`、`net_accept_locked`、`net_pending_tick` | -| `net_relisten_clear()` 外部调用 | **2** | `net_socket`(新 fd)、`net_close_locked` | -| 自愈 tick 调用 | **2** | `net_process_messages`(`retry_tick` + `pump`) | - -> **关键观察**:状态数组的**外部写只有 2 处**、且都是「登记沉降等级」这一个语义。 -> 这说明模块边界天然干净——**接口面只需 4 个函数**,不需要暴露任何数组。 - -### 10.3 边界划分 - -**搬入新模块** `Drivers/BSP/NET/net_listener.c`(**真机专属**,sim 不编): - -| 搬入 | 理由 | -|---|---| -| `net_relisten_locked` / `net_relisten_try` / `net_relisten_retry_tick` / `net_relisten_clear` / `net_relisten_done` | 「维持 fd 处于 LISTEN 态」的全部机制 | -| `net_listener_pump` | 唯一的周期性重建决策者(P2a 成果)(自愈收敛阶段 3 已并入 `net_reconcile_tick` 形态③④,单一 100ms tick,见 §10.10) | -| `net_drain_chip_rx` | 重建前的排空(只被 relisten 调用) | -| 7 个 `s_relisten_*` + 8 个 `NET_RELISTEN_*` / `PUMP_PERIOD` 常量 | 状态与策略私有化 | - -**留在 `net_socket.c`**(并说明理由,避免"看着像就搬"): - -| 留下 | 理由 | -|---|---| -| `net_listen_locked` | 是**应用 API 路径**(首次监听,含 §9 待议的忙等 P2b),且要处理 BOUND/CREATED 状态门与缓冲——属"连接层建监听",不是"维持监听" | -| `net_accept_locked` | 产出的是**连接**(LISTENING→ESTABLISHED 迁移),属连接层;它只**调用**新模块 | -| `net_lost_connect_probe` | 做的是"补 accept"(连接建立),不是重建监听 | -| `net_listener_armed` | **双端公开 API**(sim 也实现);纯 RAM 查询,很薄,留在原处避免 sim 缺符号 | -| ~~`net_resync_listen`~~ | **自愈收敛阶段1 已删除**:其"只改本层状态"语义并入对账⑤(静默回收形态自动认领,纪律"不发芯片命令"随动作沉淀进 `net_reconcile_tick`),手动 API 面退役 | -| `net_reconcile_tick` | ①②⑤ 是**连接**失同步自愈(③④ 已在 P2a 移交 pump;⑤ 为 自愈收敛阶段1 新增,吸收原 `net_resync_listen` 语义)(自愈收敛阶段 3:③④/U11 已**回到本 tick**,单一 100ms tick 覆盖①②③④⑤/U11,见 §10.10) | -| `net_sock_t` 全部字段 | 数据归 `net_types.h`(双端共享核心),**不随模块走** | - -> ⚠ `net_sock_t` 的 `last_peer_port` / `lost_conn_seen` 是**跨层字段**:新模块的 -> `net_relisten_locked` 要写它们(转登幽灵甄别基准),`net_lost_connect_probe` 与对账② -> 也要读写。**这是本次重构无法消除的残余耦合**(单连接模型的固有结果), -> 明确登记而非假装不存在。 - -### 10.4 接口设计(4 函数 + 1 枚举,全部窄接口) - -```c -/* net_listener.h —— 只被 net_socket.c include(内部头,不进公开 net_socket.h,故 sim 无依赖) */ - -/* 沉降/隔离等级(替代外部直接写 s_relisten_quarantine) */ -typedef enum { - NET_LISTENER_SETTLE_GRACEFUL = 2, /* 正常释放(DISCONNECT):等芯片收尾 100ms */ - NET_LISTENER_SETTLE_QUARANTINE = 1, /* 异常释放(TIMEOUT/RST):等在途落尽 + 排空残渣 400ms */ -} net_listener_settle_t; - -/* 登记「本 fd 的会话已结束,重建前需沉降」—— 由 handle_disconnect/timeout_event 调用 */ -void net_listener_settle(int sockfd, net_listener_settle_t level); - -/* 尝试重建一次(不阻塞);已在重试队列则返回 -1 —— 替代 4 处 net_relisten_try */ -int net_listener_try(net_sock_t *p_sock); - -/* 清本 fd 的重建/沉降状态(新 fd 或 close 时)—— 替代 2 处 net_relisten_clear */ -void net_listener_clear(int sockfd); - -/* 自愈节拍:D10 重试轮询 + 监听器 pump(合二为一,调用方只记一个入口) */ -void net_listener_tick(void); -``` - -**反向依赖(新模块需要 `net_socket.c` 提供的)**: -- `net_sock_t` 定义 + 字段(`net_types.h`,共享头,**不算反向依赖**) -- `s_net_socks` 数组 —— **问题点**:它是 `net_socket.c` 的 static。 - **解法**:新模块**不反推 fd**,一律用 `p_sock->ch395_sock`(值等于 fd,`net_socket` 初始化时保证) - 或调用方传入的 `sockfd`。这样**完全不需要访问 `s_net_socks`**,依赖面进一步收窄。 -- `net_set_errno` / `NET_ERR_RETURN_1`(错误码) -- `ch395f_*`(驱动,本来就是外部依赖) -- `HAL_GetTick` / `lat_probe_*`(TEST_LATENCY_PROBE 构建) - -### 10.5 构建与注册 - -| 项 | 动作 | -|---|---| -| `MDK-ARM/STM32F407-Demo.uvprojx` | 加 `` 条目;`Drivers/BSP/NET` 已在 IncludePath(同目录,**无需新增路径**) | -| `simulator/CMakeLists.txt` | **不动** —— 新模块真机专属;sim 用 `sim/net_socket_sim.c` 自洽(无孤儿态、无 relisten) | -| 注册校验 | 跑 `registration-check`(双端注册反向校验)确认没有单边注册 | - -> ⚠ 按项目约定,新 `.c` 文件的注册走 **`new-source-file` skill**(uvprojx + CMakeLists 双端), -> 不要手工编辑工程文件。 - -### 10.6 验证门(执行时必须逐条过) - -1. **纯搬移可证**:搬移前后 `@build` 的 `Code`/`RO-data`/`RW-data`/`ZI-data` **完全不变** - (只挪位置、不加逻辑;任何数字变化都说明搬错了)。 -2. `@test` PASS + 回读 `No difference found`。 -3. 全量回归矩阵:`:21` 0ms×50 / 100ms×50、`:8888` 0ms×20、`:7003` 300ms×10、 - `ftp_regression` 11/11、Trap23 abort/graceful 各 5/5。 -4. 设备侧 `orphan listener` 增量 **0**(P2a 已达成的基线,不得回退)。 -5. `net_listener.c` **不 include** `net_socket.c` 的任何符号(除 `net_types.h`)—— - 用 `grep -n '#include' net_listener.c` 人工核对。 -6. 判据 §10.1 三条**逐条量化复核**(入口数 / 外部写 / 依赖方向)。 - -### 10.7 明确不做(边界外,避免范围失控) - -- **不动** `net_listen_locked` 的忙等(那是 P2b,已判暂缓)。 -- **不动** `net_sock_t` 字段布局(`last_peer_port` / `lost_conn_seen` 的跨层耦合本次保留)。 -- **不动** sim 后端(D6:双模型分裂暂不消除;P1 已让**应用层契约**对齐)。 -- **不做** P3 形态 A(listener 一等对象 + accept 返回虚拟句柄)—— 硬件不支持多监听,已出局(D1)。 -- **不引入**第二套并行机制(沿用「同一能力只有一个实现」的收敛原则)。 - -### 10.8 执行顺序建议 - -1. **先搬状态与常量**(7 数组 + 8 常量)→ 编译验证(此时功能等价,只换位置); -2. **再搬 7 个函数**,同步把 8 处外部引用改成 4 个窄接口; -3. **最后合并 tick**(`net_listener_tick` 内部调 retry + pump),`net_process_messages` 少一个调用点; -4. 每步独立提交、独立 `@build`;**第 3 步后**才跑完整台架矩阵(前两步是纯组织变更,可只跑 `@build` + `@test`)。 - -### 10.9 执行结果(2026-10-12,提交 `c1ab902`) - -**执行顺序调整(与 §10.8 的差异,据实记录)**:§10.8 的「先搬状态与常量」**单独做不可行**—— -状态数组被 `net_relisten_*` 函数直接读写,只搬状态不搬函数会**编译失败**。 -故第 1 步改为「**状态 + 常量 + 7 函数一次性纯搬移**」,第 3 步(合并 tick)**未做** -(收益仅少一个调用点,却会破坏「尺寸不变」这条硬判据,性价比不足)。 - -| 判据 | 结果 | -|---|---| -| `net_socket.c` 行数 | 4126 → **3656**(-471) | -| 新增模块 | `net_listener.c` / `net_listener.h`(真机专属) | -| **RW-data** | 980 → **980**(不变 ✓) | -| **ZI-data** | 143060 → **143060**(不变 ✓) | -| Code / RO-data | 197236 → 197228(-8)/ 17012 → 17000(-12) | -| 接口 | 8 处散点直调 + 2 处直接写数组 → **4 个窄接口** | -| `s_net_socks` 依赖 | **已消除**(改用 `net_get_sock` + `ch395_sock`) | -| 静态门禁 | 全绿(cppcheck / C 规范 / 双端注册 / 主机侧单测 12 套 / mem-numbers) | -| `@test` + 回读 | PASS + `No difference found` | -| 全量回归 | `:21` 0ms×50 与 100ms×50 各 **50/50**;`:8888` **20/20**;`:7003` **10/10**;`ftp_regression` **11/11**;Trap23 abort/graceful 各 **5/5** | -| 设备侧 `orphan listener` | **0**(保持 P2a 基线) | - -> **关于「尺寸完全不变」判据的修订**:实际执行发现该判据**过严** —— 接口改造 -> (fd 改传参、`net_set_errno` 提升可见性、新增 `net_pending_active`/`net_listener_settle`) -> **必然**改变代码生成。故修订为:**RW-data / ZI-data 必须完全不变**(数据总量不变), -> **Code / RO-data 允许极小变化**(本次各 -8 / -12 字节,相对 471 行搬移可忽略)。 - -**执行中暴露的三处「非纯搬移」(必要,均已注释留痕)**: - -1. **`net_set_errno` 由 `static` 提升为模块间可见** —— 它是 `static`,新模块链接期 - 报 `L6218E: Undefined symbol`;`NET_ERR_RETURN_1` 宏随之迁到 `net_socket.h` - (它依赖前者,不能只移宏)。 -2. **新增 `net_pending_active(fd)`** —— `net_listener_retry_tick` 原有「本 fd 有在途 - 挂起 recv 则跳过」(L3 保险,防重开 Socket 摧毁在途连接)直接读 `s_net_pending[i].active`; - 新模块不能碰该 static 数组,故加薄访问器。**语义原样保留**。 -3. **`net_listener.c` 必须真机专属** —— 它依赖 `ch395f_*` SPI 驱动,而 sim 是 Winsock - 后端(无 `ch395f_*`、无孤儿态、无重监听概念)→ **无法**在 sim 编译。故: - 只注册 Keil 端;并在 `scripts/check_registration.py` 的 `SIM_EXEMPT` **显式点名** - (该脚本既定的豁免原则正是「显式清单 + 每行理由,不整目录一刀切」,本例遵此)。 - -**教训(供后续搬移复用)**: -- 搬移前先查**跨边界引用面**(本次发现「状态数组外部写仅 2 处」),它直接决定接口面与风险; -- 「纯搬移可证」判据要选**对改造不敏感的量**(RW/ZI 数据总量),否则会被接口改造的 - 正常开销误报; -- 改完文件**必须重新 `git add`** —— pre-commit 校验的是**暂存区快照**,漏 add 会让 - 「本地已修好」与「提交时仍报错」自相矛盾(本次踩到,白跑两轮)。 - -### 10.10 自愈收敛阶段 3:pump 并入 reconciler(**已执行 2026-10-13**) - -自愈架构审计(`docs/网络/网络自愈架构设计.md` §4.1 表注留痕沿革)指出:`net_reconcile_tick`(200ms,①②⑤)与 -`net_listener_pump`(100ms,③④/U11)是**同一件事的两个半片**——都在"读芯片状态 → 比对镜像 → 分级处置", -拆两个 tick 只是历史演化(P2a 从对账里拆出去),既多一份去抖/冷却状态机,又让"对账① 置 CLOSED 后 -要等下一个 pump 轮才重建"多一跳延迟。阶段 3 合并为**单一 100ms `net_reconcile_tick`**: - -- **`net_listener_pump()` 删除**;三种情形作为**形态③④ + U11** 回到 `net_reconcile_tick`, - 认领门槛按原 pump 口径以 `listener_fd` 门(`auto_relisten` + TCP + 已绑端口)恢复—— - **不变量 II(单一所有者)不变**,重建决策仍只此一处; -- **判据改读驱动状态缓存** `ch395f_sock_state_cached`(唯一刷新点 = `net_poll` 每轮全量刷新, - E2 实测单次回读 18.1µs)——reconciler 不再自发状态 SPI,形态② 命中时的对端端口回读是 - 本 tick 唯一按需 SPI; -- **分级去抖**(100ms/轮):②=1 轮(`NET_RECON_STABLE_ACCEPT`)、③④⑤=2 轮 - (`NET_RECON_STABLE_RELISTEN`)、①=4 轮(`NET_RECON_STABLE_DEATH`,M3 让位窗 400ms 不变); - 冷却 1s(`NET_RECONCILE_COOLDOWN_MS`);U11 被动重同步不去抖不占冷却; -- 常量 `NET_LISTENER_PUMP_PERIOD_MS` / `NET_LISTENER_STABLE_N` / `NET_LISTENER_COOLDOWN_MS` 删除; - 主循环自愈 tick 由 4 个回到 **3 个**(retry / pending / reconcile); -- 同阶段顺路收口 P2b(§9 曾"建议暂缓"):`net_listen_locked` / `net_connect_locked` 的 open 失败 - 重试与 `net_close_locked` 的排空循环改走驱动 `sock_reset` 原语(`net_sock_reset_sync`, - GRACEFUL 100ms 底 / 600ms 封顶),并删除 close/connect 路径的全局中断读清(F-NET-7)。 - -**台架回归(2026-10-13,最终构建 Code=211348)**:`:7003` 50/50、`:21` 100ms/0ms/600ms 各 50/50、 -TRAP-NET-18 RST 10/10 + FIN 5/5、TRAP-NET-20 n30 11/11、iec104 18/18、`ftp_regression` 11/11、E-02 双 PASS; -netTask 静态最坏链 1944 B 不变(链尾改经 `net_listener_retry_tick`→`net_relisten_locked`)。 diff --git a/docs/编号规范.md b/docs/编号规范.md index f4ba8481..20c3ce89 100644 --- a/docs/编号规范.md +++ b/docs/编号规范.md @@ -36,7 +36,7 @@ | **跨文档同号不同义** | `Trap 02` 在 CH395F 是"缓冲区重叠"、在 GD5F 是"测试缓冲越界";`B10` 在规划是"送检窗口"、在陷阱审计是"孤儿监听" | | **语义进编号 → 事实一变编号就撒谎** | `P1-x` 把严重度写进号里,而审计中大量条目事后降级(`E-59` P0→P1、dhara `is_free` P1→P2):改号则引用全断,不改则号实不符 | | **孤儿编号(代码有、文档无)** | `I-1`/`I-2`、`Task #32` 会话号、`P1-01~P1-09`、`TC-GD5F-1100/1101/1102`、`TC-STO-06`、`TC-AH-*`、`TC-PE-*`(前两项项目已在 `docs/测试说明/实机测试实施指南.md` 自行承认) | -| **悬空引用(引用了不存在的号)** | `Trap 19` 从未存在却被 `docs/测试说明/CH395F_Test_Guide.md` 引用;`N-5/N-8/N-19/N-30/N-31` 销项删行后仍被引用 | +| **悬空引用(引用了不存在的号)** | `Trap 19` 从未存在却被 `docs/测试说明/CH395F_Test_Guide.md` 引用;`N-5/N-8/N-19/N-30/N-31/N-32` 销项删行后仍被引用(`N-32` = 监听生命周期模块化,被 `docs/网络/` 三篇与代码注释多处引用) | | **同一事物多种字面** | `Trap 07` / `TRAP-07` / `陷阱 1`;`gen 181` / `gen-288` / `gen68`;`TC-NET-704` / `TC704` / `TC-203` | | **规范自身就不自洽** | `docs/测试说明/测试文档模板规范.md` §6.3 同一行并列 `TC-STO-03`(2 位)与 `TC-RTC-001`(3 位);`docs/测试说明/测试文档模板规范.md` §6.2 已废止 `PHY` 模块码,而阶段 ID 至今仍是 `TM-PHY-01` | | **机器校验只覆盖一个族** | 31 个 pre-commit 钩子中只有 `TC` 有 `scripts/check_test_traceability.py`,且**未挂钩子、warn-only、正则 `TC-[A-Za-z0-9]+-\d+` 匹配不到 `TC-AI-01A`**;`E/N/S/B/C/D/A/U/O/gen/Trap` 全部裸奔 | @@ -45,7 +45,7 @@ --- -## 2. 六条设计原则(强制) +## 2. 七条设计原则(强制) | # | 原则 | 理由 | |---|---|---| @@ -55,6 +55,7 @@ | **P-4** | **形态唯一**:`<前缀>-<序号>`;前缀 1~3 位大写字母;序号位数按类别固定并左补零;子项只允许一级 `-<小写字母>`。**禁止**裸形(`S3`)、空格形(`Trap 07`)、中文形(`陷阱 1`)、圈号(`①②③④`) | 连字符与补零不得承载语义;字面唯一才能被机器判定 | | **P-5** | **只增不复用**;每个类别有「下一个可用号」游标(`[计划] scripts/id_registry.json`),新增先取游标 | 并行任务与多文档同时新增时防撞号(`docs/问题记录/项目审查与优化建议报告.md` 变更记录自述"并轨时未做全局唯一性检查"就是没有游标) | | **P-6** | **引用必须机器可判定**:跨文档引用用行内代码,首次出现紧邻属主文档名;文档内编号**必须**带文档名 | 与 `§N.M` 同一条路:能校验的约定才算存在 | +| **P-7** | **编号不语义,引用必语义**:编号本体保持无语义并冻结(同 P-2);但**正文引用必须带语义括注**——`TRAP-NET-18(异常释放后排空 RX 残渣)`、`docs/网络/网络自愈架构设计.md` §4.6(应用主动拆会话保监听);**禁止裸 ID 单独承担信息**。语义放括注,事实变了只改括注、编号不变 | 读者不该为看懂一句话去跳三篇文档。可读性由**引用方式**解决,不由**编号命名**解决——命名带语义会重蹈 `P1-x` 覆辙(§1.2)。落地见 §5.6 | --- @@ -90,8 +91,9 @@ |---|---|---|---| | `FR-[A-Z0-9]+-\d{2}` | 功能需求 | `docs/产品需求/产品需求设计文档.md` §5 | 各域独立递增;**新增域写进该文 §1.2 术语与缩写** | | `NFR-[A-Z0-9]+-\d{2}` | 非功能需求(**新建**,现 §7 全是散文无号) | `docs/产品需求/产品需求设计文档.md` §7 | 从 `NFR-<域>-01` 起 | -| `N-\d{2}` | **登记簿条目**(技术债 / 长期观察 / 硬件改版项;含义由"网络项"重定义) | `docs/任务规划/登记簿.md` | 在册最大 `N-33` → 游标 `N-34`;原 `S-x`/`T-x` 并入本族(§7.1) | +| `N-\d{2}` | **登记簿条目**(技术债 / 长期观察 / 硬件改版项;含义由"网络项"重定义) | `docs/任务规划/登记簿.md` | **曾用**最大 `N-33`(`N-30`~`N-33` 均已销项、登记簿行已删,**现存至 `N-29`**)→ 游标 `N-34`(号不回收);原 `S-x`/`T-x` 并入本族(§7.1) | | `E-\d{2}` | 错误码/返回值传播审计发现 | `docs/问题记录/错误码传播审计.md` | **冻结**:`E-63` 为最大号,**不再新增**;新发现一律 `DEF` | +| `E\d+` | **自愈实验编号**(N34 台架实验,**无连字符**) | `docs/网络/网络自愈架构设计.md` §5.2 | **已封闭**:`E0`~`E15`,**不再新增**;原始实验记录已归档(该文档 §12)。⚠ 与上一行靠**连字符**区分:带连字符 = 审计发现 | | `TC-[A-Z]{1,4}-\d{3,4}` | 测试用例 | 各测试文档 | 3 位起;分阶段套件序号 = 阶段号×100 + 项号(阶段 ≥10 自然进位 4 位,`TC-NET-1001` 合规);后缀只允许 `-<小写字母>` | | `TM-[A-Z]{1,4}-\d{2}` | 测试阶段 | 各测试文档 | 模块码取 §6 统一表(`TM-PHY-01` → `TM-NET-01`) | | `DP-[A-Z]{1,4}-\d{2}` | 设计原则 | 各测试文档 | **加模块码**(现 `DP-01` 跨文档会撞) | @@ -104,7 +106,7 @@ |---|---|---|---|---| | `DEF-` | 缺陷 / 问题发现 | `P0-x`~`P3-x`(在册项)、新发现 | `[计划] docs/问题记录/缺陷登记簿.md` | `DEF-001` | | `WI-` | 工作项 / 任务卡 | `B\d`(功能主线)、`C\d`(结构债)、`D\d`(验证窗口)、`W1-0`、`S1~S14`、`A-1~A-7`、`P1-10`、报告 `R-x`(内存优化)、`F-x`、`U1~U5` 的未闭环项 | `docs/任务规划/` 波次与排期文件(类别做列) | `WI-001` | -| `DR-` | 决策记录 | `docs/子系统设计/网络Socket监听生命周期设计说明.md` 的 `D1~D10` | `[计划] docs/架构设计/决策记录登记簿.md` | `DR-001` | +| `DR-` | 决策记录 | `docs/网络/网络自愈架构设计.md` 的 `D1~D7` | `[计划] docs/架构设计/决策记录登记簿.md` | `DR-001` | | `OQ-` | 待确认 / 开放问题 | `B0-a/b/c`、`待确认-N` | `docs/任务规划/基线与待确认.md` | `OQ-001` | | `RSK-` | 风险(**文档内编号**) | 保护规划 `R1~R8`、看门狗 `R1~R7` | 各文档风险表(引用必须带文档名) | 各文档自 `RSK-01` | | `TRAP--` | 陷阱记录 | `Trap 07`、`TRAP-07`、`陷阱 1` | `docs/问题记录/<模块>_Trap_Records.md` | 保留原号(`TRAP-NET-01~21`——NET 域 2026-10-13 全库迁移并重排连续,沿革见该文档、`TRAP-GD5F-01~06`、`TRAP-RTC-01~`) | @@ -115,7 +117,7 @@ | `GEN-` | 台架日志代次 | `gen 181`、`gen-288`、`gen68` | `docs/测试说明/串口助手设计说明.md` | 数字不变,仅改字面 | | `RUN--` | 台架轮次 | `round 1~9` | `docs/测试说明/实机测试实施指南.md` | 日期制 | | `MS-` | 里程碑 | `M1~M3` | 波次文件 | `MS-01` | -| `RC-` | net 周期对账类型 | `①②③④`(唯一被代码逻辑引用的圈号链) | `docs/子系统设计/网络Socket监听生命周期设计说明.md` | `RC-01`~`RC-04` | +| `RC-` | net 周期对账类型 | `①②③④⑤`(唯一被代码逻辑引用的圈号链) | `docs/网络/网络自愈架构设计.md` | `RC-01`~`RC-05` | | `PAT-` | 缺陷模式(九类) | `docs/问题记录/错误码传播审计.md` §1.1 的 `P1~P9` | 同文 | `PAT-01`~`PAT-09` | | `ST-` | 专项内部实施阶段(**文档内编号**) | 保护规划 §7.1 的 `P0~P5`、方案阶段 `P0~P3` | 各专项文档 | 各文档自 `ST-0` | | `UL-` | UI 标准化四层(**文档内编号**) | 报告 UI 四层 `L0~L3` | `docs/UI设计/UI_设计架构.md` | `UL-0` | @@ -149,6 +151,8 @@ ### 5.1 新增编号 +0. **先问"能不能不加号"**:能用 § 小节引用、表格列、语义标题表达清楚的,就不要分配编号—— + 编号是"确需跨文档长期引用"时才付的成本(P-1 / P-3 / P-7)。确需新增才往下走。 1. 查 §4 总表确定类别与属主登记处;**表里没有的类别 → 先改本规范,不得自造前缀**。 2. 到属主登记处取游标号(`[计划] scripts/id_registry.json` 记录每类下一个可用号),登记处新增一行。 3. 严重度 / 状态 / 波次 / 出处写进表格列,**不进编号**。 @@ -159,7 +163,7 @@ |---|---| | 全局编号,跨文档首次出现 | `` `docs/任务规划/登记簿.md` N-21 ``(紧邻,同 `§N.M` 规则) | | 全局编号,同段后续 | `` `N-21` `` | -| 文档内编号 | **必须** `` `docs/子系统设计/网络Socket监听生命周期设计说明.md` DR-003 `` | +| 文档内编号 | **必须** `` `docs/网络/网络自愈架构设计.md` DR-003 `` | | 尚未创建的条目文件 | 加 `[计划] ` 前缀(`docs/文档规范.md` §3.3) | | 引用报错样例/历史字面 | 该行加 `[示例]` 标记 | @@ -179,6 +183,16 @@ - 号段用尽 → 扩位(`N-99`→`N-100`),**不换字母**。 - 历史文档(审计报告、评审报告、已闭环陷阱)不回改,走别名表。 +### 5.6 编号密集文档的对照节(强制) + +一篇文档若引用 **≥3 个编号族**(如 `I*` + `E*` + `F-*` + `M*` + `U*` + `TRAP-*`),**必须**在正文之前 +加一节「导读:本文用到的编号与术语」——每个记号一行,给出「是什么 + 在哪查」,让读者不必跳出本文拼语义。 + +- **首次实践**:`docs/网络/网络自愈架构设计.md` 的 `## 导读:本文用到的编号与术语`。 +- 对**已封闭 / 已归档**的编号族尤其必要:属主登记表可能已删行(`N-30`~`N-33` 销项、`E0`~`E15` 随审计归档), + 此时该文档即其**唯一可查处**。 +- 与 P-7 配套:**正文引用带语义括注**(局部可读)+ **对照节给编号族总览**(全局可查),两者合起来才算看得懂。 + --- ## 6. 模块码表(`TC` / `TM` / `DP` / `TRAP` 共用) @@ -209,7 +223,7 @@ | 旧族 | 新族 | 规则 | |---|---|---| -| 登记簿 `N-2`…`N-33` | `N-02`…`N-33` | 仅补零,数字不变 | +| 登记簿 `N-2`…`N-33`(`N-30`~`N-33` 已销项,现存至 `N-29`) | `N-02`…`N-33` | 仅补零,数字不变;号不回收,游标仍 `N-34` | | 登记簿 `S-3/S-4/S-5`、`T-7` | `N-34`… | 续号并入,别名表保留旧号 | | 主线 `B0~B10`、结构债 `C1~C6`、验证窗口 `D2~D7`、`W1-0` | `WI-001`… | 按波次顺序连续重编,映射表逐条列出并落别名表 | | 复审 `S1~S14`、`A-1~A-7`、报告 `R-x`/`F-x`/`U1~U5` 未闭环项 | `WI-*` | 同上;已闭环项留历史文档 | @@ -236,7 +250,7 @@ | 孤儿 | 处置 | |---|---| -| `I-1`/`I-2`(`ui_net_sink.c`) | 在 `docs/子系统设计/网络Socket监听生命周期设计说明.md` 补定义行,或就地展开成事实描述 | +| `I-1`/`I-2`(`ui_net_sink.c`) | 就地展开成事实描述(原属主《网络Socket监听生命周期设计说明》已并入 `docs/网络/网络自愈架构设计.md` §9.3,不再补编号定义行) | | `Task #32` 会话号及其小节引用 | 会话号,改写为可解析引用(结论已沉淀进 `docs/子系统设计/看门狗设计说明.md` §11) | | `P1-01~P1-09`(`ch395f_test_task.c`) | 改指 `TC-NET-101`…,或补文档条目 | | `TC-GD5F-1100/1101/1102`、`TC-STO-06` | 补进 `docs/测试说明/GD5F2GQ5UE_Test_Guide.md` | @@ -311,4 +325,5 @@ git grep -n '<旧字面量>' # 零残留 | 日期 | 变更 | 说明 | |---|---|---| +| 2026-10-14 | 新增 P-7 与引用可读性规则 | §2 加 **P-7「编号不语义,引用必语义」**(编号本体保持无语义并冻结,但正文引用必须带语义括注、禁裸 ID 独担信息)→ 六条原则变**七条**;§5.1 加前置步"先问能不能不加号";新增 **§5.6「编号密集文档的对照节(强制)」**(≥3 个编号族必须加「导读:编号与术语」);§4.1 补登记 **`E\d+`**(N34 自愈实验编号族,已封闭)并把 `N-\d{2}` 的在册范围订正为「曾用最大 `N-33`,现存至 `N-29`」;§1.2 悬空引用示例补 `N-32`;§7.1 同步订正。起因:`docs/网络/网络自愈架构设计.md` 合并与导读节实践暴露"裸编号引用不可读",用户裁定以后尽量用语义标识 | | (规划稿) | 建立本文档 | 全库扫描 1,385 个不同编号 / 19,306 处出现后定稿的两层编号体系(全局编号 + 文档内编号)、六条原则、目标前缀总表、三批迁移映射与 `check_ids.py` 门禁设计;用户决策:`E-xx` 冻结保留、严重度档位词 `P0~P3` 不变、文档落 `docs/编号规范.md`、阶段 1 三批全做 | diff --git a/docs/网络/BSD_Socket_API_使用指南.md b/docs/网络/BSD_Socket_API_使用指南.md index 4ce888a1..a859ce45 100644 --- a/docs/网络/BSD_Socket_API_使用指南.md +++ b/docs/网络/BSD_Socket_API_使用指南.md @@ -158,7 +158,7 @@ sequenceDiagram | **`netTask` 外(任意应用任务)** | 公共 API:`net_send()` / `net_recv()` / `net_recv_to()` / `net_accept()` / `net_connect()` / `net_close()` / `net_listen()` / `net_drop_conn()`——请求入队列,netTask 串行执行,调用方阻塞等回执。**自愈无显式 API**(自愈收敛阶段1:`net_relisten()` / `net_resync_listen()` 已退役,静默回收/监听失位由对账⑤与 reconciler 形态③④自动认领,见 §3.6) | | **`netTask` 内** | 内部 API `net_send_sock()` / `net_recv_sock()` / `net_close_sock()` / `net_listen_locked()` | | **`net_socket()` / `net_bind()`** | 直接调用,无需队列;实际在就绪屏障后的服务任务(timeSyncTask / ftpTask / uiTask)中创建监听 | -| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环调用;`net_process_messages()` 每轮先跑 **3 个非阻塞 tick**(D10 重监听重试 → L3 挂起 recv 完成 → TRAP-NET-17 对账+监听重建合一 reconciler 100ms),再处理队列 | +| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环调用;`net_process_messages()` 每轮先跑 **4 个非阻塞 tick**(D10 重监听重试 → L3 挂起 recv 完成 → L3 挂起 send 推进 → TRAP-NET-17 对账+监听重建合一 reconciler 100ms),再处理队列 | ### 3.4 内部 API 说明 @@ -178,7 +178,8 @@ int net_close_sock(net_sock_t *p_sock); - **`netTask` 不可向自身发消息**:`netTask` 中直接调 `net_send()` 会死锁(等待自己处理消息),必须在 `netTask` 中使用 `net_send_sock()` 等内部版本 - **阻塞 recv 不再独占 netTask(L3 pending 化,2026-09-22)**:无数据时公共 `net_recv()` / `net_recv_to()` **登记挂起请求后返回**(每 fd 至多一个,禁止覆盖——覆盖即丢失前调用方通知 = 永久挂死);完成由 `net_pending_tick()` 按主循环节拍检查(①数据 ②到点[仅显式预算] ③断连)并经任务通知唤醒调用方——等待期间调用方在自己上下文睡通知,netTask 零占用(消除 E-02 头阻塞)。副作用:`buf` 指针须保持有效至返回;close / 网络重建 / 陈旧重同步会**显式取消并通知**(`NET_ERR_CONNRESET` 等),调用方永不挂死 - **recv 语义主流化(自愈收敛阶段 1.5,2026-10-13)**:对齐 POSIX/Python——**模式归使用者、超时归调用方、API 不强加默认预算**。`net_recv()` 默认**无限阻塞**(`NET_RECV_TIMEOUT_MS` 30s 默认已删),唤醒源 = 数据 / EOF / close / 网络重建(CONNRESET),会话死亡由对账① ~400ms 判死兜底;非阻塞具名入口 `net_recv_nb()`(原 `NET_MSG_DONTWAIT` 路径转正),模式属性 `net_setblocking(fd, on)` 挂 socket 控制块;超时首选 `net_select(...)`,`net_recv_to(...)` 保留为**显式预算**便捷入口。内部看门狗降级为 **tripwire**:挂起超 60s 只 `[ERR]` 留痕一条(完成逻辑缺位探针),不再伪造 TIMEDOUT 返回值。⚠ **文档契约(嵌入式特有)**:IWDG `required_mask` 内任务**禁止无限阻塞 recv**——必须 `net_select` 超时或 `net_recv_to` 显式预算("超时归使用者"对受监控任务是义务不是选项) -- **队列阻塞语义**:只有 `osMessageQueuePut` 失败(8 长度固定队列,极端情形)时公共 API 阻塞;recv 挂起等待**不计入**队列占用(完成前队列中无消息滞留) +- **阻塞 send 不再独占 netTask(L3 send pending 化,2026-10-14)**:`net_send()` 快路径**有界自旋**等发送缓冲腾出(预算 `NET_SEND_SPIN_MS` = 2ms;健康链路实测块间等待 med 219µs ≪ 2ms,故正常发送不会触发挂起),超预算即登记挂起 send 返回;推进由 `net_send_pending_tick()` 按主循环节拍分块完成(①断连 ②推进 ③写完 ④无进展超时),调用方睡任务通知——netTask 最坏停滞由 `NET_SEND_TIMEOUT_MS`(1s)压到 2ms。返回值恒为**已写入字节数**:`= len` 全部写入,`< len` 为**短写**,调用方**必须**检查并重试剩余(断连/超时/取消均以已发字节数结束,不会伪造全部成功)。停滞计时按**无进展**刷新(每次成功写块重置),大文件传输不会被误判超时 +- **队列阻塞语义**:只有 `osMessageQueuePut` 失败(8 长度固定队列,极端情形)时公共 API 阻塞;recv / send 挂起等待**不计入**队列占用(完成前队列中无消息滞留) - **哪些操作要求 netTask 已运行**:`net_listen()` / `net_accept()` / `net_connect()` / `net_send()` / `net_recv()` / `net_close()` 全经消息队列 → 只能在就绪屏障后的任务调用;`net_socket()` / `net_bind()` 为直接调用,但同样建议屏障后;**`net_listener_armed()` / `net_can_read()` 是纯 RAM 读控制块**(§10.1),不入队、不等回执,**任意任务可安全周期调用**(用途 = 门控,把"无谓的 RPC"降为 0) ### 3.6 芯片/本层状态对账与重监听自愈(TRAP-NET-09/-17/-18) @@ -188,7 +189,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 时跳过(L3 保险,防毁在途连接) | +| **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-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 时限**,否则监听被放弃) | @@ -247,7 +248,7 @@ int main(void) - 更新所有 Socket 的连接状态(CONNECT / DISCONNECT / RECV / TIMEOUT) - 触发事件回调 - 处理数据接收 - - `net_process_messages()` 每轮先跑 3 个**非阻塞**检查再取队列:D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 唤醒,E-02 修复)→ TRAP-NET-17 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,事件未收到/静默回收/监听失位自愈,§3.6) + - `net_process_messages()` 每轮先跑 4 个**非阻塞**检查再取队列:D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 唤醒,E-02 修复)→ L3 挂起 send 推进(阻塞 send 唤醒)→ TRAP-NET-17 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,事件未收到/静默回收/监听失位自愈,§3.6) - **监听 Socket 建立**:由就绪屏障后的服务任务(timeSyncTask / ftpTask / uiTask)经队列 API 建立(`net_socket` → `net_bind` → `net_listen`);断连后 net 层自动重监听(§3.6),服务层只需循环 `net_accept`,无需维护重监听逻辑 ```c @@ -1054,11 +1055,12 @@ Socket 已创建或已绑定时可用;port 为 0(动态分配)时返回已 void net_process_messages(void); ``` -处理其他任务投递的请求(send/recv/connect/accept/close/drop_conn)。每次调用先跑 3 个**非阻塞** tick,再取队列消息(0 超时出队): +处理其他任务投递的请求(send/recv/connect/accept/close/drop_conn)。每次调用先跑 4 个**非阻塞** tick,再取队列消息(0 超时出队): 1. **D10 重监听重试**(`net_listener_retry_tick`):断连后监听器恢复的到点重试(30ms 间隔 / 2s 时限,§3.6) 2. **L3 挂起 recv 完成检查**(`net_pending_tick`):按 ①数据 ②到点(仅显式预算) ③断连 检查每个挂起请求并通知调用方;④ 挂起超时留痕探针(编译期选项,默认关):挂起超阈值只 `[ERR]` 留痕一次,不强制结束 -3. **TRAP-NET-17 芯片/本层对账 + 监听重建(合一 `net_reconcile_tick`)**:100ms 周期,判据读驱动状态缓存(`ch395f_sock_state_cached`),修正**五类**失同步(①②③④⑤)+ U11 被动重同步——原 200ms 对账与 100ms 监听器 pump 已合并为单一 tick,**唯一重建入口**=形态③④(§3.6) +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) > `net_msg_t.timeout_ms`:L3 显式预算(`net_recv_to()`),0=**无限等待**(自愈收敛阶段 1.5 删除 `NET_RECV_TIMEOUT_MS` 30s 默认预算——超时归调用方)。消息类型 7 种(send/recv/close/connect/listen/accept/drop_conn)——自愈类消息 `NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 已随 API 退役删除(自愈收敛阶段1)。 @@ -1275,7 +1277,7 @@ static int read_time_frame(int conn_sock, uint8_t buf[FRAME_LEN]) { **是的**,且 `net_poll()` 与 `net_process_messages()` 缺一不可: - `net_poll()`:GPIO 轮询 INT#,**每轮批处理一批**中断,更新各 Socket 状态、触发事件回调(RECV 为电平触发,每轮只处理一批,避免饿死主循环)。 -- `net_process_messages()`:除串行处理队列请求外,每轮先跑 3 个**非阻塞**检查——D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 的唤醒)→ TRAP-NET-17 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,事件未收到/静默回收/监听失位自愈,§3.6)。 +- `net_process_messages()`:除串行处理队列请求外,每轮先跑 4 个**非阻塞**检查——D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 的唤醒)→ L3 挂起 send 推进(阻塞 send 的唤醒)→ TRAP-NET-17 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,事件未收到/静默回收/监听失位自愈,§3.6)。 建议主循环周期 ≤ 10ms(本项目 1ms)。若 netTask 主循环停摆(断点、任务被挂起),监听器不会恢复、阻塞 recv 不会完成——这是软件侧"监听死机"的最常见原因。 @@ -1458,10 +1460,11 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po | 日期 | 变更 | |------|------| +| 2026-10-14 | **二期 SEND pending 化**(架构见 `docs/网络/Net层阻塞模型架构设计.md` §4.5/§5.5/§7):`net_send` 快路径改为**有界自旋**(`NET_SEND_SPIN_MS` = 2ms,健康链路块间等待 med 219µs ≪ 2ms,行为不变),超预算登记挂起 send,由新增 `net_send_pending_tick` 按主循环节拍分块推进(①断连 ②推进 ③写完 ④无进展超时),调用方睡任务通知——netTask 最坏停滞由 `NET_SEND_TIMEOUT_MS`(1s)压到 2ms。返回值恒为**已写入字节数**(短写须检查);`NET_SEND_TIMEOUT_MS` 降级为**无进展**超时(每次成功写块刷新)。主循环 tick 由 3 个增至 **4 个**(新增 `net_send_pending_tick`,与 `net_pending_tick` 并列);§3.3 / §3.5 / §3.6 / §10.2 / §12 同步 | | 2026-10-13 | **自愈收敛阶段 3 自愈合并 + E-02 收口**(实施规格现沉淀 `docs/网络/网络自愈架构设计.md` §4.1):`net_listener_pump` **删除**,与 `net_reconcile_tick` 合并为**单一 100ms tick**(覆盖原对账①②⑤ + 原 pump③④/U11;判据读**驱动状态缓存** `ch395f_sock_state_cached`,唯一刷新点 = `net_poll` 每轮全量刷新,E2 实测单次回读 18.1µs;**分级去抖** ②=1 轮 / ③④⑤=2 轮 / ①=4 轮(M3 让位窗 400ms)+ 1s 冷却;§2.2 / §3.3 / §3.6(五层 → **四层**)/ §10.2 / §12.1 / §12.10 同步,主循环自愈 tick 由 4 个回到 **3 个**;常量 `NET_LISTENER_PUMP_PERIOD_MS` / `NET_LISTENER_STABLE_N` / `NET_LISTENER_COOLDOWN_MS` 删除);**忙等统一**——`net_listen_locked` / `net_connect_locked` open 失败重试"close+盲等 500ms"与 `net_close_locked` 500ms 排空循环改走驱动 `sock_reset` 原语(`net_sock_reset_sync`,GRACEFUL 100ms 底),`net_close_locked` 不再读清全局中断寄存器(F-NET-7:全局寄存器唯一读者 = `net_poll`);新增 **`net_drop_conn(sockfd)`**(§10.1,E-02 收口:拆会话**保监听**、幂等、EOF 路径不得调;新消息类型 `NET_MSG_DROP_CONN`;timeSync 30s 预算耗尽路径接入;模拟器实现为关数据连接);`net_poll` 每轮对在用 Socket 步进 `ch395f_sock_reset_poll`(同步包装 600ms 放弃后复位机不搁浅);`net_sock_state_t` 注释声明为"本层会话投影"(芯片事实源 = 驱动缓存,`net_types.h`);台架回归全绿(最终构建 Code=211348,E-02 双 PASS,见 `docs/网络/网络自愈架构设计.md` §5.2 E-02) | | 2026-10-13 | **自愈收敛阶段 1.5 recv 语义主流化**(规格现沉淀 `docs/网络/网络自愈架构设计.md` §4.5):`net_recv()` 默认**无限阻塞**(删 `NET_RECV_TIMEOUT_MS` 30s 默认预算,唤醒源=数据/EOF/close/网络重建 CONNRESET);新增 `net_recv_nb()`(`NET_MSG_DONTWAIT` 路径转正)与 `net_setblocking(fd, on)`(模式挂 socket 控制块属性,`net_sock_t.blocking`,默认阻塞);`net_recv_to()` 保留为**显式预算**便捷入口(`timeout_ms=0` 语义从"默认 30s"改为"无限"故仍拒 INVAL);内部 recv 看门狗降级为 **tripwire**(挂起超 60s 只 `[ERR]` 留痕一次,不再"到点 +1s 强制了结"伪造 TIMEDOUT);⚠ 新增**文档契约**:IWDG `required_mask` 内任务禁止无限阻塞 recv(必须 `net_select` 超时或 `net_recv_to` 显式预算);`ui_net_sink` / `iec104_serv` 迁移 `net_recv_nb`,lftpd / timeSync 维持显式预算;模拟器 `net_socket_sim.c` 镜像三 API(blocking 属性包装层模拟) | -| 2026-10-13 | **自愈收敛阶段1 API 面收敛**(审计过程已归档 `docs/网络/网络自愈架构设计.md` §9,现状见其 §4):删除手动自愈 API `net_relisten()` / `net_resync_listen()`(生产零调用;后者语义并入对账**⑤**——本层 ESTABLISHED 而芯片 0x05/0x01 静默回收 → RESYNC 只修本层,§3.6 对账由两类扩为三类①②⑤;前者与 pump/D10 重叠且对健康监听器是 footgun)与 `net_can_accept()`(只认 ESTABLISHED 的旧门控挡死丢 CONNECT 探针,监听门控统一 `net_listener_armed()`,§10.1 门控节改写);`NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 消息类型删除;timeSync 会话收尾不再 `net_close`(同 fd 续 accept,监听跨会话常驻);lftpd 应用层阻塞重试退役(就绪屏障已消除 TRAP-NET-09 启动形态) | -| 2026-10-12 | **N-32 监听生命周期模块化 + 单一所有者**:监听生命周期抽为 `net_listener.c`(§2.2 文件表新增该行,真机专属);主循环自愈 tick 由 3 个增至 **4 个**(新增 `net_listener_pump`,100ms,**唯一重建入口**);`net_reconcile_tick` 对账收窄为 ①②,③④ 移交 pump;`handle_disconnect_event` / `handle_timeout_event` 改为 `net_listener_settle(GRACEFUL 100ms / QUARANTINE 400ms)` **只置事实**、不再自行重建;§3.3 / §3.6(四层自愈 → **五层**)/ §12.10 同步。权威设计见 `docs/子系统设计/网络Socket监听生命周期设计说明.md` §10 | +| 2026-10-13 | **自愈收敛阶段1 API 面收敛**(审计过程已归档 `docs/网络/网络自愈架构设计.md` §12,现状见其 §4):删除手动自愈 API `net_relisten()` / `net_resync_listen()`(生产零调用;后者语义并入对账**⑤**——本层 ESTABLISHED 而芯片 0x05/0x01 静默回收 → RESYNC 只修本层,§3.6 对账由两类扩为三类①②⑤;前者与 pump/D10 重叠且对健康监听器是 footgun)与 `net_can_accept()`(只认 ESTABLISHED 的旧门控挡死丢 CONNECT 探针,监听门控统一 `net_listener_armed()`,§10.1 门控节改写);`NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 消息类型删除;timeSync 会话收尾不再 `net_close`(同 fd 续 accept,监听跨会话常驻);lftpd 应用层阻塞重试退役(就绪屏障已消除 TRAP-NET-09 启动形态) | +| 2026-10-12 | **N-32 监听生命周期模块化 + 单一所有者**:监听生命周期抽为 `net_listener.c`(§2.2 文件表新增该行,真机专属);主循环自愈 tick 由 3 个增至 **4 个**(新增 `net_listener_pump`,100ms,**唯一重建入口**);`net_reconcile_tick` 对账收窄为 ①②,③④ 移交 pump;`handle_disconnect_event` / `handle_timeout_event` 改为 `net_listener_settle(GRACEFUL 100ms / QUARANTINE 400ms)` **只置事实**、不再自行重建;§3.3 / §3.6(四层自愈 → **五层**)/ §12.10 同步。权威设计见 `docs/网络/网络自愈架构设计.md` §11 | | 2026-10-10 | 新增 **`net_can_accept()` / `net_can_read()` 就绪门控 API**(§10.1):纯 RAM 读控制块、**不走消息队列、不等 netTask 回执**,供实时性敏感任务在发 RPC 前判断"有没有活"(动因 = `ui_net_sink_poll()` 每 10 ms 一次 `net_accept` 把 uiTask 拖成最坏 ≈50 ms;原拟登记 N-19 / C7,当日结案不入登记簿;设计与实测见 `docs/子系统设计/看门狗设计说明.md` §11(原分析文档已并入该节));§3.5 补"netTask 运行要求"例外条;⚠ 记录两条使用约束——**不可作为唯一判据**(`readable` 只由 RECV_OK 中断置位,须保留低频兜底 recv)与**模拟器后端恒返 1**(`net_socket_sim.c`,Winsock 无 RPC 成本) | | 2026-09-23 | 同步 L3 pending 化、D10 非阻塞重监听、TRAP-NET-17 对账、TRAP-NET-18 排渣隔离后的现状:新增 `net_recv_to` / `net_sock_state` / `net_resync_listen` / `net_relisten`(§10.1)及 `net_getsockname`;§3 新增 3.6(对账与自愈四机制)、3.3/3.5 改写(recv 挂起模型、队列语义、netTask 运行要求);§4/§5 示例更新(监听由就绪屏障后服务任务建立、`net_sock_state` 轮询);§10.2 改述 `net_process_messages()` 每轮节奏(重监听重试 → 挂起 recv 完成 → 200ms 对账 → 队列),补 `timeout_ms`/新消息类型说明;新增 §11.2 分片阻塞读(`net_recv_to`);§12.1/12.3/12.4/12.9 更新,新增 §12.10(自愈行为数据与排障判据,含 TC-NET-604 结案) | | 2026-09-21 | **修正公开 API 的错误码回传**:`net_msg_t` 按值拷入队列 → netTask 对消息的写入调用方不可见,改用 `xTaskNotify()` 通知值回传具体错误码(负值),公开 API 恢复 errno 后统一返回 -1(此前被覆盖成 `NET_ERR`,使 `net_get_errno() != NET_ERR_WOULDBLOCK` 的"空轮询"判断恒成立 → `ui_net_sink` 连上即 `net_close`);§12.4 补单连接模型断开处理(EOF 时不得 `net_close` 监听 socket);§12.9 补错误码回传约束 | diff --git a/docs/网络/BSD_Socket_API_函数调用关系.md b/docs/网络/BSD_Socket_API_函数调用关系.md index 1bf673df..82c4a865 100644 --- a/docs/网络/BSD_Socket_API_函数调用关系.md +++ b/docs/网络/BSD_Socket_API_函数调用关系.md @@ -5,10 +5,10 @@ > > **范围**:MCU 端 `Drivers/BSP/NET/net_socket.c`(约 3660 行)+ 其拆出的 > `Drivers/BSP/NET/net_listener.c`(约 550 行,**监听生命周期**,N-32 §10 模块化)+ -> `Drivers/BSP/NET/net_pending.c`(约 390 行,**挂起 recv 请求表**,2026-10-14 拆出)。 +> `Drivers/BSP/NET/net_pending.c`(约 690 行,**挂起 recv / send 请求表**;send 组为 2026-10-14 二期新增)。 > 模拟器端后端 `simulator/sim/net_socket_sim.c`(Winsock)是另一套实现,见 `docs/网络/BSD_Socket_API_使用指南.md` §2.3;`net_select.c` 不在此展开,见 `docs/网络/BSD_Socket_API_使用指南.md` §7 / §10.5。 > `net_listener.c` / `net_pending.c` 真机专属(模拟器不编译);设计依据分别见 -> `docs/子系统设计/网络Socket监听生命周期设计说明.md` §10 与 +> `docs/网络/网络自愈架构设计.md` §11 与 > `docs/网络/Net层阻塞模型架构设计.md`。 > > **交叉引用约定**:机制标 Trap 号 → `docs/问题记录/CH395F_Trap_Records.md`;用法语义引用使用指南 §;关键参数值与 `net_config.h` / `net_socket.c` 内宏一致。 @@ -25,7 +25,7 @@ | 执行域 | 运行位置 / 节奏 | 允许的操作 | 典型调用者 | |---|---|---|---| -| **netTask 主循环** | 独立 FreeRTOS 任务,1ms/tick | **唯一**可碰芯片(SPI2)的地方;3 个非阻塞 tick(D10 重试 / L3 挂起 / TRAP-NET-17 对账+监听重建合一 reconciler);队列分派;事件处理器;用户回调 | `net_poll` / `net_process_messages` 及其下游全部 `_locked` / `_tick` / `handle_*` | +| **netTask 主循环** | 独立 FreeRTOS 任务,1ms/tick | **唯一**可碰芯片(SPI2)的地方;4 个非阻塞 tick(D10 重试 / L3 挂起 recv / L3 挂起 send / TRAP-NET-17 对账+监听重建合一 reconciler);队列分派;事件处理器;用户回调 | `net_poll` / `net_process_messages` 及其下游全部 `_locked` / `_tick` / `handle_*` | | **应用任务侧** | ftpTask / uiTask / timeSyncTask / testTask 等 | 经队列的公共 API(入队 + `xTaskNotifyWait` 等回执);纯 RAM 直调 API;**禁止直碰芯片** | 各服务任务 | | **直调快路径** | 同步调用,不经过队列 | `net_socket` / 查询族 / 回调注册(纯 RAM);**直碰 SPI 的三例外**:`net_bind`(UDP) / `net_sendto` / `net_recvfrom` | 初始化阶段、netTask 上下文、测试套件(硬件层测试模式下 netTask 自挂起,testTask 独占芯片) | @@ -48,16 +48,17 @@ flowchart TB XN["xTaskNotify / xTaskNotifyWait
(结果回传的唯一通道)"] subgraph NT["netTask 主循环 — 1ms/tick;芯片操作唯一执行者"] - NPM["netProcessMessages
先跑 3 个 tick,再排空队列分派"] + NPM["netProcessMessages
先跑 4 个 tick,再排空队列分派"] NP["netPoll
INT# GPIO 轮询 → 全局中断 → 各 Socket SINT → 状态缓存全量刷新 + sock_reset 步进 → 每轮一批"] T1["① netListenerRetryTick
D10 重监听重试:30ms / 2s 时限"] T2["② netPendingTick
L3 挂起 recv:①数据 ②到点(仅显式预算) ③断连 ④tripwire 60s 只留痕"] - T3["③ netReconcileTick
TRAP-NET-17 对账 + 监听重建合一(自愈收敛阶段 3):100ms / 分级去抖 / 冷却
读驱动状态缓存(①②③④⑤ + U11)"] + T2B["③ netSendPendingTick
L3 挂起 send:①断连 ②推进(每轮≤1块) ③写完 ④无进展超时"] + T3["④ netReconcileTick
TRAP-NET-17 对账 + 监听重建合一(自愈收敛阶段 3):100ms / 分级去抖 / 冷却
读驱动状态缓存(①②③④⑤ + U11)"] DISP["队列分派 switch
SEND · RECV · CLOSE
CONNECT · LISTEN · ACCEPT · DROP_CONN"] subgraph LK["_locked / 内部分派目标(仅 netTask 上下文)"] LL["netListenLocked
初次监听(open 重试 3× + sock_reset 排渣)"] LN["netConnectLocked
open 重试 3×(sock_reset 排渣)+ tcpConnect"] - LS["netSendLocked
分块 ≤1KB + sendReady 门控"] + LS["netSendLocked
分块 ≤1KB + sendReady 门控;超 NET_SEND_SPIN_MS 登记挂起 send"] LR["netRecvLocked
快路径 / DONTWAIT / 登记挂起"] LA["netAcceptLocked
先 netPoll + 状态判定"] LC["netCloseLocked
结束挂起 + cmd_done 快路径 / sock_reset(GRACEFUL)"] @@ -67,14 +68,15 @@ flowchart TB EV["事件处理器
handleConnect · handleDisconnect
handleRecv · handleSend · handleTimeout"] ST["netListenerSettle
只置沉降/隔离事实(100ms / 400ms)"] FE["fireEvent → 用户回调"] - PR["s_net_pending
(每 fd 至多一个挂起)"] + PR["s_net_pending
(每 fd 至多一个挂起 recv)"] + SR["s_net_send
(每 fd 至多一个挂起 send)"] CH[("ch395f.* 驱动
SPI2 串行事务")] end PUB -->|入队| MQ MQ --> DISP DISP --> LL & LN & LS & LR & LA & LC & DC & RLS - NPM --> T1 & T2 & T3 & NP + NPM --> T1 & T2 & T2B & T3 & NP T1 --> RLS T3 -->|"① 已断"| EV T3 -->|"② 补 accept"| EV @@ -89,6 +91,9 @@ flowchart TB LR -->|无数据+阻塞| PR PR --> T2 T2 -->|xTaskNotify| XN + LS -->|等发送缓冲超预算| SR + SR --> T2B + T2B -->|xTaskNotify| XN XN -->|唤醒| PUB NP --> CH LL & LN & LS & LR & LA & LC & DC & RLS --> CH @@ -98,11 +103,11 @@ flowchart TB **读图要点**: -1. 应用侧只有两种"等":① 入队后 `xTaskNotifyWait` 等回执(同步语义);② 挂起 recv 的等待(同一通道,只是"完成"由 `netPendingTick` 判定,不由 netTask 立即处理)。 -2. `net_process_messages()` 开头的 **3 个 tick 是网络层自愈主干**:没有任何应用消息时,重试 / 挂起完成 / 状态对账 + 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。 +1. 应用侧只有两种"等":① 入队后 `xTaskNotifyWait` 等回执(同步语义);② 挂起 recv / send 的等待(同一通道,只是"完成"由 `netPendingTick` / `netSendPendingTick` 判定,不由 netTask 立即处理)。 +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`)与挂起表在 `net_pending.c`(真机专属);其余 tick(含合并后的 `T3` reconciler)与 `_locked` 在 `net_socket.c`。 +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`。 --- @@ -121,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 宿主:每轮**先**跑 3 个非阻塞 tick,再排空队列逐条分派,结果 `xTaskNotify` 回传 | `net_listener_retry_tick` → `net_pending_tick` → `net_reconcile_tick`(对账+重建合一)→ `osMessageQueueGet`(0 超时) ×n → switch 分派 → `xTaskNotify` | ① tick 放在队列**之前**:断连后即使没有任何应用消息,监听器也能自主恢复、挂起 recv 也能完成;② 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_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 内自然成立) | ### 4.3 经队列的公共 API 及其 netTask 侧实现 @@ -132,7 +137,7 @@ flowchart TB | `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_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` 轮询) | 实际字节数 / -1 | 分块 ≤ `NET_SEND_CHUNK_MAX` = 1KB(**硬件发送 FIFO 硬上限**,TRAP-NET-16:写穿 FIFO = 数据错位);`send_ready` 门控用 `net_poll` 轮询而非 osDelay;短写**必须**告警——静默截断会让对端永久等待剩余字节(2026-09-21 实证:UI 推流应答截断 → 客户端失联) | +| `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` 是跨任务共享全局量,随时被别的任务覆盖) | @@ -181,7 +186,7 @@ flowchart TB | `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_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 时跳过**(重开 Socket 会摧毁在途连接,L3 保险);到 2s 时限 → 清状态 + `relisten give up` 错误日志(应用可见) | +| `net_listener_retry_tick` | 每轮到点重试(D10) | `net_relisten_locked` → `net_relisten_done` / 顺延 / 放弃 | **同 fd 有在途挂起 recv / send 时跳过**(重开 Socket 会摧毁在途连接 / 截断在途传输,L3 保险);到 2s 时限 → 清状态 + `relisten give up` 错误日志(应用可见) | | `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_drain_chip_rx` | 排空芯片 RX 残留(256B 分块,≤16 次 = 4KB 上限) | `get_recv_len` + `read_recv_buf` | TRAP-NET-18:中止/异常断开时芯片 RX 缓冲残留上一条连接的数据(实测 ~2048B),不排空会被下一条连接当自己的数据读走(静默截断);分块读避免新增静态缓冲(内存预算紧张) | @@ -265,12 +270,20 @@ sequenceDiagram Note over A: net_wait_result:零等待拾取 → 无则阻塞 Q->>N: NET_MSG_SEND N->>C: net_send_locked:write_send_buf(≤1KB/块) - loop 每块 - N->>C: 等 SENBUF_FREE(net_poll 轮询,不睡) + alt 健康路径(块间等待 < NET_SEND_SPIN_MS=2ms) + loop 每块 + N->>C: 等 SENBUF_FREE(net_poll 有界自旋,不睡) + end + else 超预算(对端零窗口 / 不读) + N->>N: net_send_pending_register(每 fd 至多 1;do_notify=0) + Note over A,N: netTask 立即解放;A 在自己上下文睡通知 + loop 每个主循环节拍(1ms) + N->>C: net_send_pending_tick:①断连 ②推进 ③写完 ④无进展超时 + end end C-->>N: SENBUF_FREE / SEND_OK(net_poll 置 send_ready) N->>A: xTaskNotify(实际字节数 / 负错误码) - A->>A: 返回字节数 / -1(errno 已恢复) + A->>A: 返回字节数 / -1(errno 已恢复;短写按 ret < len 处理) ``` ### 5.2 阻塞接收无数据(L3 pending 路径) @@ -401,7 +414,7 @@ sequenceDiagram | `ch395f_open_socket` / `ch395f_close_socket` | `net_bind`(UDP)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked`(含沉降期主动 close);`net_close_locked` 的 CLOSE 已改经 `ch395f_cmd_done` / `ch395f_sock_reset_*`(自愈收敛阶段 3) | TRAP-NET-09:close 后需沉降才能重开 | | `ch395f_tcp_listen` / `ch395f_tcp_connect` | `net_listen_locked` / `net_relisten_locked`;`net_connect_locked` | — | | `ch395f_set_proto_type` / `set_sour_port` / `set_des_ip` / `set_des_port` | `net_bind`(UDP)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked`、`net_sendto` | open 之后写端口/IP(open 会复位寄存器);set_des_ip 期望**大端数组** | -| `ch395f_write_send_buf` | `net_send_locked` / `net_sendto` | 单次 ≤1KB(硬件 FIFO 上限,TRAP-NET-16) | +| `ch395f_write_send_buf` | `net_send_locked` / `net_send_pending_tick`(二期挂起 send 推进)/ `net_sendto` | 单次 ≤1KB(硬件 FIFO 上限,TRAP-NET-16) | | `ch395f_get_recv_len` / `ch395f_read_recv_buf` | `net_recv_locked`、`net_pending_tick`、`net_pending_settle_close`、`net_recvfrom`、`net_drain_chip_rx`、`handle_disconnect_event`(M3 可疑门 RX 非空判据) | `handle_recv_event` **故意不读**(防竞争,旧机理未证实) | | `ch395f_get_socket_status` | `handle_disconnect_event`(两处甄别——事件路径**主动 SPI**,缓存必然滞后于刚到达的中断)、`net_relisten_locked`(沉降门)、`net_connect_locked`(诊断回读)。⚠ **周期对账不再直接调**——改读缓存(下行) | 输出 **2 字节**([0]=Socket 状态 [1]=TCP 状态),缓冲必须给 2 字节 | | `ch395f_get_sock_state` / `ch395f_sock_state_cached` | 刷新**仅 `net_poll`** 每轮对在用 Socket 调 `get_sock_state`(唯一刷新点);`sock_state_cached` 读缓存 = `net_reconcile_tick`(形态①②③④⑤/U11 判据) | 自愈收敛阶段 3:芯片状态唯一事实源;E2 实测单次回读 18.1µs,缓存新鲜度 ≤1ms | @@ -419,6 +432,7 @@ sequenceDiagram |---|---| | 监听恢复策略(节奏/时限/沉降) | `net_listener_try` / `net_relisten_locked` / `net_listener_retry_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` | | 新增一个公共 API | 模式:`net_socket.h` 声明(`NET_NO_OS` 条件编译)→ `net_msg_type_t` 加类型(如需要)→ `net_process_messages` 加 dispatch 分支 → netTask 侧 `_locked` 实现;调用侧复制"参数检查 → 入队 → `net_wait_result` → errno 恢复"骨架 | | 缓冲布局 / MSS / 重传 / KeepAlive 参数 | `net_init` + `net_config.h` + `ch395f.h`(`CH395F_RECV/SEND_BLOCKS`、`CH395F_TCP_MSS`);改完必须 0 错 0 警构建 + 全测试套件回归 | @@ -430,7 +444,7 @@ sequenceDiagram ## 9. 相关文档 - `docs/网络/BSD_Socket_API_使用指南.md` —— 公开 API 语义、示例、常见问题(本文的上半部) -- `docs/子系统设计/网络Socket监听生命周期设计说明.md` —— **监听生命周期权威设计**(N-32:边沿→电平、单一所有者重建(自愈收敛阶段 3 起为 `net_reconcile_tick` 形态③④)、`net_listener.c` 模块化边界 §10) +- `docs/网络/网络自愈架构设计.md` —— **监听生命周期权威设计**(N-32:边沿→电平、单一所有者重建(自愈收敛阶段 3 起为 `net_reconcile_tick` 形态③④)、`net_listener.c` 模块化边界 §11) - `docs/问题记录/CH395F_Trap_Records.md` —— TRAP-NET-01~23 机制细节(本文引用处) - `docs/网络/Net层阻塞模型架构设计.md` —— 阻塞等待模型的现状架构设计(pending 请求模型 / 五路完成语义) - `docs/问题记录/错误码传播审计.md` —— E-02 头阻塞与错误码回传全审计 diff --git a/docs/网络/Net层阻塞模型架构设计.md b/docs/网络/Net层阻塞模型架构设计.md index 77510422..b297b6f0 100644 --- a/docs/网络/Net层阻塞模型架构设计.md +++ b/docs/网络/Net层阻塞模型架构设计.md @@ -1,16 +1,35 @@ # Net 层阻塞模型架构设计 -> **模块名称**:Net 层挂起 recv 模型(`net_pending.c` / netTask 主循环节拍) +> **模块名称**:Net 层挂起 recv / send 模型(`Drivers/BSP/NET/net_pending.c` + netTask 主循环节拍) > **文档定位**:**现状架构设计说明**——描述本层**当前如何组织等待与完成**、为什么这样设计、 -> 边界条件与已验证的性能特征。**不是历史记录**:历史演进、门禁实测记录与修订日志 -> 一律不保留在文档内(需要时查 git 历史)。 -> **状态**:设计已落地并全量台架验证(`v2-on-ui`)。 -> **日期**:2026-09-29 +> 数据结构与接口契约、实现落点与集成步骤、边界条件与已验证的性能特征。**不是历史记录**: +> 历史演进、门禁实测记录与修订日志一律不保留在文档内(需要时查 git 历史)。 +> **状态**:recv 挂起(一期)与 send 挂起(二期)设计均已落地;recv 全量台架验证, +> send 于 2026-10-14 完成台架复测(含本次复测发现并修复的末块挂起越界缺陷,见 §9.1)。 +> **日期**:2026-10-14 > **相关文档**: > `docs/网络/BSD_Socket_API_使用指南.md`(应用视角 API 语义)、 > `docs/网络/BSD_Socket_API_函数调用关系.md`(调用链与执行域)、 +> `docs/网络/网络自愈架构设计.md`(周期对账与监听生命周期——与本模型的分工见 §2.4;模块化边界见 §11)、 > `docs/测试说明/链路延迟测试说明.md`(延迟基线与故障对照)、 -> `docs/任务规划/基线与待确认.md` §2.1(二期 ACCEPT/SEND 扩展设计) +> `docs/问题记录/错误码传播审计.md`(E-02 队头阻塞审计)、 +> `docs/任务规划/基线与待确认.md` §2.1(二期扩展设计:SEND 已落地、ACCEPT 待办) + +## 目录 + +- [1. 问题域](#1-问题域) +- [2. 架构总览](#2-架构总览) +- [3. 一次阻塞 recv 的全过程](#3-一次阻塞-recv-的全过程) +- [4. 数据结构](#4-数据结构) +- [5. 完成与取消语义](#5-完成与取消语义) +- [6. 模块接口契约](#6-模块接口契约) +- [7. 实现落点(集成步骤)](#7-实现落点集成步骤) +- [8. 调用方契约](#8-调用方契约) +- [9. 性能特征](#9-性能特征) +- [10. 设计边界与已知代价](#10-设计边界与已知代价) +- [11. 设计铁律](#11-设计铁律) +- [12. 排查辅助:挂起超时留痕探针](#12-排查辅助挂起超时留痕探针) +- [13. 源码索引](#13-源码索引) --- @@ -29,11 +48,28 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线 > → **观测本身占用执行者**,等待无法挪出 netTask。 设计自由度因此只剩两条:① 每次占用执行者多久 ② 等待期间执行者还能做什么。 -本架构的答案:**占用降到登记一次(微秒级),等待期间执行者照常服务整个队列**。 +三条候选路线的取舍: + +| 路线 | 单次占用执行者 | 等待期间执行者 | 落地结论 | +|---|---|---|---| +| A 执行者内自旋 | 整个等待期(可无限) | **完全停摆** | ❌ 队头阻塞(实测一条空闲 FTP 连接冻结远端显示 17.7s) | +| B 有界分片轮询(`net_recv_to` 分片) | ≤ `timeout_ms` | 分片间隙可用 | ⚠️ 止血:仍周期性占用,调用方须自管总预算 | +| C **挂起登记(本模型)** | 微秒级(登记一次) | 照常服务整个队列 | ✅ 结构性消灭 | + +路线 A→B→C 的落地证据与量化见 `docs/问题记录/错误码传播审计.md` E-02。 +路线 B 的入口 `net_recv_to` 在路线 C 下**继续保留**,但语义已从"分期占用执行者" +变为"调用方显式预算"(见 §6.2、§8)。 + +> **SEND 是同一问题的第二个面**:`net_send` 写入 CH395F 发送 FIFO 后需等 `send_ready` +> (由 `SENBUF_FREE` 中断经 `net_poll` 重新置位),旧的 `net_send_locked` 在 netTask 内 +> **忙轮询等它**,单次停滞上限 `NET_SEND_TIMEOUT_MS`(1 s)——同属执行者占用型队头阻塞, +> 量级比 recv 小一个数量级,但同样会冻结整个消息队列并顺延挂起 recv 的唤醒。二期把 +> send 也纳入挂起模型(§4.5、§5.5),**快路径保留有界自旋**(健康链路 219 µs ≪ 2 ms 预算, +> 行为不变),超预算才登记挂起。收益判据仍是"执行者被占多久"(§10)。 --- -## 2. 架构总览:pending 请求模型 +## 2. 架构总览 核心思想:把"等数据"从**执行者内的自旋**改为**执行者外的登记**。 @@ -43,75 +79,89 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线 | 区域 | 内容 | |---|---| -| **左栏 · 调用方任务** | `net_recv` → 判"数据已在?" → **是**走快速路径;**否**(`timeout > 0`)登记 pending 后在自己上下文睡任务通知 | -| **中栏 · netTask 主循环** | ~1ms 节拍:`net_poll`(刷新 readable + 全量刷新驱动状态缓存)→ 3 个非阻塞 tick(`net_listener_retry_tick` D10 → `net_pending_tick` 结束挂起 → `net_reconcile_tick` TRAP-NET-17 对账 + 监听重建唯一入口)→ 排空消息队列 | -| **下带 · 五路判定** | 每轮对每个挂起 fd 按序判定:数据到达 / 到点(仅显式预算)/ EOF / 同 fd 后续操作 / 网络重建;挂起超时留痕探针(编译期选项,默认关,见 §9)只留痕 | +| **左栏 · 调用方任务** | `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` 对账 + 监听重建唯一入口)→ 排空消息队列 | +| **下带 · 判定** | recv 每轮按序判定:数据到达 / 到点(仅显式预算)/ EOF / 同 fd 后续操作 / 网络重建(**五路**);send 每轮按序判定:断连 / 推进 / 写完 / 无进展超时(**四路**);挂起超时留痕探针(编译期选项,默认关,见 §12)只留痕 | | **回环** | 判定完成经 `xTaskNotify` 唤醒调用方(结果 = 字节数 / 负错误码) | -**关键性质**: - -- 等待期间 netTask **零额外占用**——`net_pending_tick` 只读标志与到期时间, - 数据到达判据 `readable` 由主循环既有的 `net_poll()` 每轮刷新,**不引入额外 SPI 事务**; -- 调用方**在自己的上下文睡眠**(任务通知),与 LwIP 的消费者线程模型同构; -- 公开 API 与调用契约**稳定**——`net_recv`(默认无限阻塞)/ `net_recv_nb` + - `net_setblocking` / `net_recv_to`(唯一显式预算入口)的签名与返回值不变, - 既有调用方、测试与模拟器零改动;超时归属语义详见 - `docs/网络/网络自愈架构设计.md` §4.5 与 `docs/网络/BSD_Socket_API_使用指南.md` §3.5。 - > 图形由 `tools/arch_diagram/gen_net_pending_svg.py` 生成(改流程后重跑;见 > `docs/架构设计/图形绘制流程.md`)。 ---- +### 2.1 关键性质 -## 3. 数据结构 +- 等待期间 netTask **零额外占用**——`net_pending_tick` 只读标志与到期时间, + 数据到达判据 `readable` 由主循环既有的 `net_poll()` 每轮刷新,**不引入额外 SPI 事务**; +- **挂起 send 的占用有界且非阻塞**——`net_send_pending_tick` 每轮至多写一块(`≤ NET_SEND_CHUNK_MAX`), + 写完即返回,绝不 `osDelay` 忙等;故 netTask 最坏停滞由 `NET_SEND_TIMEOUT_MS`(1 s) + 压到 `NET_SEND_SPIN_MS`(2 ms); +- 调用方**在自己的上下文睡眠**(任务通知),与 LwIP 的消费者线程模型同构; +- 公开 API 与调用契约**稳定**——`net_recv`(默认无限阻塞)/ `net_recv_nb` + + `net_setblocking` / `net_recv_to`(唯一显式预算入口)/ `net_send` 的签名不变; + 既有调用方、测试与模拟器零改动;`net_send` 的返回值语义(已发字节数,短写须检查) + 与挂起化前一致,超时归属语义详见 + `docs/网络/网络自愈架构设计.md` §4.5 与 `docs/网络/BSD_Socket_API_使用指南.md` §3.5。 -### 3.1 挂起请求表 +### 2.2 模块边界 -| 项 | 设计 | -|---|---| -| 存储 | **独立静态数组** `s_net_pending[NET_MAX_SOCKETS]`(不置于 `net_sock_t` 内) | -| 字段 | `active` / `infinite` / `buf` / `len` / `deadline` / `registered` / `caller`(`infinite`:0 预算 = 无限等待;留痕开启时另有 `tripwire_logged`:保证只留痕一次) | -| 容量 | 每 fd **至多一个**挂起请求 | -| 占用 | ≈24B/fd × 8 fd = **192B**(+4B 任务句柄),实测 SRAM +200B | +本模型是 `net_socket.c` 内部的**一层**,实现独立成模块,只被少数确定位置消费: -**为什么独立成表而不是塞进 `net_sock_t`**: +| 文件 | 持有 | 对外接口 | +|---|---|---| +| `Drivers/BSP/NET/net_pending.c` / `.h` | 挂起 recv 表 `s_net_pending[]`、挂起 send 表 `s_net_send[]`、执行者句柄 `s_owner_task`、全部完成/取消逻辑 | 14 个函数(recv 组 9 + send 组 7,含 2 个 static,§6.1) | +| `Drivers/BSP/NET/net_socket.c` | 消息队列与分派、`net_recv_locked` / `net_send_locked` 的挂起登记、close/drop_conn/重建/对账的结算调用 | 调用方(§7) | +| `Drivers/BSP/NET/net_listener.c` | 监听器重开 | 消费 `net_pending_active()` / `net_send_pending_active()` 作"挂起保险" | +| `App/task/net_task.c` | 主循环节拍(`net_poll` → `net_process_messages` → `osDelay(1)`) | 提供 tick 节拍 | -`net_close_locked` 对 `net_sock_t` 做**整块 memset**。若挂起状态置于其中,`close` 会 -静默抹掉 pending——调用方永远等不到通知,**永久挂死**。独立表把"跨时间状态"与 -socket 生命周期解耦,与 `net_listener_retry_tick` 的重试表同构。 +依赖方向**单向**:`net_pending.c` → `net_types.h` / `net_socket.h` / `net_listener.h` / +`ch395f.h`(send 侧分块写与缓冲常量)/ `main.h`;无反向 include。`net_listener.c` 经 +`net_pending.h` 反向消费 `net_pending_active()` / `net_send_pending_active()`,二者均为 +**纯 RAM 读**,任意上下文安全。 -### 3.2 登记守卫(两条硬防线) +### 2.3 与主循环的关系 -| 守卫 | 理由 | -|---|---| -| 每 fd 至多一个,**不覆盖** | 覆盖 = 丢掉首个调用方的通知 = 该调用方永久挂起 | -| `caller == NULL` 或 `caller == netTask` 时**拒绝**(返回 BUSY) | netTask 上下文自等待 = 无人能完成它 = 死锁;netTask 内必须用 DONTWAIT | +netTask 主循环(`App/task/net_task.c` → `net_task_func`)每轮: ---- +```text +task_wd_beat() → 处理 SYS_EVT_NET_RECONFIG → net_poll() → net_process_messages() → osDelay(1) +``` -## 4. 挂起的 recv 会被什么结束(5 种触发条件) +`net_process_messages()`(`Drivers/BSP/NET/net_socket.c`)内部**先跑四个非阻塞 tick, +再排空消息队列**,顺序固定: -挂起 recv 被结束的方式只有 5 种,下文简称**五路**——①②③ 由 `net_pending_tick()` -每轮对每个挂起 fd 按序判定,④⑤ 由显式操作触发: - -| # | 条件 | 动作 | 设计要点 | +| 顺序 | tick | 职责 | 与挂起模型的关系 | |---|---|---|---| -| 1 | **数据到达** | SPI 读出 + 通知(结果 = 字节数) | 由 `net_poll` 维护的 `readable` 门控,**零额外 SPI**;仅 ESTABLISHED / UDP_OPEN 态可读;spurious readable 需清除 | -| 2 | **到点** `now >= deadline`(仅显式预算登记;无限等待登记无 deadline) | 通知 `NET_ERR_TIMEDOUT`(对外 -2 映射保持) | 状态无关;用有符号回绕安全差值,防 tick 回绕;这是 `net_recv_to` 调用方策略的正常兑现,不是伪造 | -| 3 | **CLOSED**(EOF/DISCONNECT) | 排空:有数据则读出;否则按 0(EOF) 完成 + `auto_relisten` + 通知 | CLOSE 先行完成挂起 = `net_close` 的 settle 钩子(排空→数据否则 CONNRESET) | -| 4 | **同 fd 后续操作**:close / drop_conn / 对账 resync(芯片静默回收) | 先结束挂起 recv(排空 → 有数据按字节数完成;否则取消 CONNRESET),再执行新操作 | per-fd 串行:pending 模型唯一留下的次序面就在本 fd 内,其他 fd 不受影响。同 fd 第二个 recv 被登记守卫拒绝(BUSY);send 无需结算(收发不冲突,SPI 由 netTask 串行) | -| 5 | **网络重建**(`net_stack_bring_up`,芯片复位) | **取消 + 必须通知** | `net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;**取消不通知 = 调用方永久挂死** | +| ① | `net_listener_retry_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) | -**状态推断的克制**:CONNECTING / LISTENING 等中间态**保持等待**,**不做**状态推断式取消—— -CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的等待。 +四个 tick 均非阻塞、**绝不 `osDelay` 忙等**;tick 先于消息分派,保证"断连后即使无应用 +消息,监听器与挂起 recv / send 也能自主收敛"。②③ 相对顺序无关(两张独立表), +但都必须在 ④ 之前——对账可能取消同一 fd 的两侧挂起。 -### 4.1 一次阻塞 recv 从调用到返回的全过程 +### 2.4 与网络自愈架构的分工 -一句话链:**登记(μs 级)→ 调用方自己睡 → 每轮判定(只读标志与到期时间)→ 公共出口(通知+清槽)→ 两段式等待取结果**。以 `net_recv(sockfd, buf, len)` 阻塞路径为例: +两篇文档描述同一段代码的两个正交面,边界如下: + +| 关注点 | 归属文档 | +|---|---| +| "一次阻塞 recv 如何被唤醒/结束"(本模型:登记、五路完成、接口、集成点) | **本文** | +| "芯片与本层状态失同步后如何周期收敛"(对账五形态、沉降隔离、D10 重试、KeepAlive) | `docs/网络/网络自愈架构设计.md` | +| 监听生命周期的模块化边界(`net_listener.c` 拆分依据) | `docs/网络/网络自愈架构设计.md` §11 | + +交叉点只有两处:`net_reconcile_tick` 形态⑤ 取消挂起(§5.2 第 5 路)、 +`net_listener_retry_tick` 的挂起 recv 保险(§7.2 集成点 8)。 + +--- + +## 3. 一次阻塞 recv 的全过程 + +一句话链:**登记(μs 级)→ 调用方自己睡 → 每轮判定(只读标志与到期时间)→ +公共出口(通知 + 清槽)→ 两段式等待取结果**。以 `net_recv(sockfd, buf, len, 0)` 阻塞路径为例: ```mermaid flowchart TD - A["调用方任务:net_recv(sockfd, buf, len)"] --> B["消息入队,随后在
自己上下文睡(两段式等待)"] + A["调用方任务:net_recv(sockfd, buf, len, 0)"] --> B["消息入队,随后在
自己上下文睡(两段式等待)"] B -.-> C subgraph NT["netTask —— 唯一执行者(~1ms 节拍)"] @@ -133,75 +183,539 @@ flowchart TD I["通知调用方(同一回传通道):通知值 = 结果原值
挂起路径经公共出口 net_pending_complete:清槽、防重复通知"] I --> J["调用方醒来取结果
0 = EOF 也是有效通知(以 pdTRUE 判定)"] - K["取消路径:close / drop_conn / 网络重建"] -. "先结束挂起、再动芯片" .-> I + K["取消路径:close / drop_conn / 网络重建 / 对账 resync"] -. "先结束挂起、再动芯片" .-> I ``` -1. **入队**(调用方任务):`net_recv` 把请求打包进消息入 `netMsgQueue`,随后进入两段式等待(步骤 6)。netTask 不被占用。 -2. **快速路径判定**(netTask,`net_recv_locked`):数据已在(`readable` / recv_len>0)→ 直接读出、经通知值回传字节数——不产生挂起,常见路径到此为止。 -3. **登记**(netTask):无数据且阻塞模式 → `net_pending_register` 过两条守卫(§3.2:每 fd 至多一个不覆盖;拒绝 netTask 自等待)→ 记 `buf`/`len`/`caller` 与预算(`net_recv_to` → `deadline`;无限等待 → `infinite=1` 无 deadline)→ 返回"已登记"哨兵,netTask 立即处理下一条消息。 -4. **每轮判定**(netTask,`net_pending_tick`):对每个挂起 fd 按序 ①数据 → ②到点 → ③断连(判据见上表;③ 的判据是 `state==CLOSED || session_dead`,漏 ③ = 无限 recv 永久挂死,勿动)。命中任一条件进入公共出口;挂起超时留痕探针只留痕不完成(编译期选项,默认关,见 §9)。 -5. **公共完成出口**(`net_pending_complete`,所有完成/取消路径的唯一出口):`xTaskNotify` 覆盖式写入**通知值 = 结果原值**(>0 字节数 / 0 EOF / 负错误码)→ 清 `active`;无在位挂起则静默返回(防重复通知)。失败结果同步记入 netTask 自己的 errno 槽位(按任务槽位,非跨任务共享全局)。 -6. **唤醒与取结果**(调用方任务):所有 `net_*` API 的回执共用同一条任务通知通道,**两段式等待**防"入队→等待入口"间隙内通知丢失——先零等待拾取(清除掩码全 1;以 pdTRUE/pdFALSE 判定已落地,不能判值≠0——合法的 0 结果=EOF 也是有效通知),未拾取到再阻塞等待。通知值 32 位整体作为结果,不切分位域(大单次收发不被截断)。 -7. **取消路径**(五路中的 ④⑤,共同规则"**先结束挂起、再动芯片**"): - - **同 fd 后续操作**(close / drop_conn):`net_pending_settle_close` 先结算——芯片 RX 有数据(bit3=1 契约:断连后残数据留缓冲待 MCU 读)→ 按字节数完成;无数据 → 取消(CONNRESET)——然后才执行 close/拆会话。对账发现芯片静默回收(resync)时走 `net_pending_cancel`(连接已被芯片收走,无排空窗口,直接取消必通知)。 - - **网络重建**:`net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;取消必通知,否则调用方永久挂死。 -8. **buf 生命周期**:挂起期间 `buf` 指针须保持有效至调用返回(挂起表按指针写入,§5)。 +1. **入队**(调用方任务):`net_recv` 把请求打包进消息入 `netMsgQueue`,随后进入两段式 + 等待(步骤 6)。netTask 不被占用。 +2. **快速路径判定**(netTask,`net_recv_locked`):函数入口先 `net_poll()` 一次刷新状态机 + 与可读标志,再查 `ch395f_get_recv_len`。数据已在(`recv_len > 0`)→ 直接读出、经通知值 + 回传字节数——不产生挂起,常见路径到此为止。 +3. **登记**(netTask):无数据且阻塞模式 → `net_pending_register` 过两条守卫(§4.3)→ 记 + `buf`/`len`/`caller` 与预算(`net_recv_to` → `deadline`;无限等待 → `infinite=1` 无 + deadline)→ 返回哨兵 `NET_PENDING_REGISTERED`,netTask 立即处理下一条消息。 +4. **每轮判定**(netTask,`net_pending_tick`):对每个挂起 fd 按序 ①数据 → ②到点 → + ③断连(判据见 §5);命中任一条件进入公共出口。挂起超时留痕探针只留痕不完成 + (编译期选项,默认关,见 §12)。 +5. **公共完成出口**(`net_pending_complete`,所有完成/取消路径的**唯一**出口): + `xTaskNotify` 覆盖式写入**通知值 = 结果原值**(>0 字节数 / 0 EOF / 负错误码)→ 清 + `active`;无在位挂起则静默返回(防重复通知)。失败结果同步记入 netTask 自己的 errno + 槽位(按任务槽位,非跨任务共享全局)。 +6. **唤醒与取结果**(调用方任务):所有 `net_*` API 的回执共用同一条任务通知通道, + **两段式等待**防"入队→等待入口"间隙内通知丢失——先零等待拾取(清除掩码全 1; + 以 `pdTRUE`/`pdFALSE` 判定已落地,**不能判值≠0**——合法的 0 结果 = EOF 也是有效通知), + 未拾取到再阻塞等待。通知值 32 位整体作为结果,不切分位域(大单次收发不被截断)。 +7. **取消路径**(第 4/5 路,共同规则"**先结束挂起、再动芯片**"): + - **同 fd 后续操作**(close / drop_conn):`net_pending_settle_close` 先结算——芯片 RX + 有数据(bit3=1 契约:断连后残数据留缓冲待 MCU 读)→ 按字节数完成;无数据 → 取消 + (CONNRESET)——然后才执行 close/拆会话。对账发现芯片静默回收(resync)时走 + `net_pending_cancel`(连接已被芯片收走,无排空窗口,直接取消必通知)。 + - **网络重建**:`net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位; + 取消必通知,否则调用方永久挂死。 +8. **`buf` 生命周期**:挂起期间 `buf` 指针须保持有效至调用返回(挂起表按指针写入,§8)。 --- -## 5. 调用方契约 +## 4. 数据结构 -调用方**不改协议、不改 API**,但需知悉三条: +### 4.1 挂起 recv 请求表 `net_pending_t` + +| 字段 | 类型 | 含义 | 生命周期 | +|---|---|---|---| +| `active` | `uint8_t` | 1 = 本 fd 有挂起 recv | 登记置 1;`net_pending_complete` / `clear` 清 0 | +| `infinite` | `uint8_t` | 1 = 无限等待(无 deadline) | 登记按 `to_ms == 0` 定;每轮 ② 判定据此跳过 | +| `tripwire_logged` | `uint8_t` | 1 = 本轮挂起已留痕(保证只留一条) | **仅 `NET_PENDING_TRIPWIRE` 编译期存在**;登记清 0 | +| `buf` | `void *` | 调用方接收缓冲(仅指针,不拷贝) | 登记写入;调用方保证有效至返回 | +| `len` | `int` | 缓冲容量 | 登记写入 | +| `deadline` | `uint32_t` | 截止时刻(tick)= 登记时刻 + 本次预算 | 无限等待时置 0(可观测性用,② 判定不看) | +| `registered` | `uint32_t` | 登记时刻(tick) | 留痕探针基准 | +| `caller` | `TaskHandle_t` | 调用方任务(完成通知目标) | 登记写入 | + +- **存储**:**独立静态数组** `s_net_pending[NET_MAX_SOCKETS]`(不置于 `net_sock_t` 内)。 +- **容量**:每 fd **至多一个**挂起请求。 +- **占用**:结构 24 B/fd × 8 fd = **192 B**(`tripwire_logged` 落在对齐填充里,开启探针 + 不增加占用);另 `s_owner_task` 4 B。实测 SRAM +200 B。 +- **`infinite` 与 `deadline` 正交**:`to_ms == 0` 即"0 预算 = 无限等待"(无默认预算)。 + +### 4.2 为什么独立成表而不是塞进 `net_sock_t` + +`net_close_locked` 对 `net_sock_t` 做**整块 `memset`**。若挂起状态置于其中,`close` 会 +静默抹掉 pending——调用方永远等不到通知,**永久挂死**。独立表把"跨时间状态"与 +socket 生命周期解耦,与 `net_listener_retry_tick` 的重试表同构。 + +### 4.3 登记守卫(两条硬防线,recv / send 共用) + +recv 与 send 两张表的登记函数(`net_pending_register` / `net_send_pending_register`) +执行**完全相同**的两条守卫: + +| 守卫 | 判据 | 理由 | +|---|---|---| +| 每 fd 至多一个,**不覆盖** | `p->active != 0` → 返回 `BUSY` | 覆盖 = 丢掉首个调用方的通知 = 该调用方永久挂起 | +| 拒绝执行者自等待 | `caller == NULL` 或 `caller == s_owner_task` → 返回 `BUSY` | netTask 上下文自等待 = 无人能完成它 = 死锁;netTask 内必须用 `NET_MSG_DONTWAIT` / 非阻塞发送 | + +`s_owner_task` 由 `net_pending_capture_owner()` 在 `net_init` 顶部捕获(= 调用者, +NET 层为 netTask,硬件层为 testTask)。 + +### 4.4 挂起槽状态机 + +```mermaid +stateDiagram-v2 + [*] --> IDLE + IDLE --> WAITING: net_pending_register(过两守卫) + WAITING --> WAITING: tick 每轮判定未命中
(含中间态 CONNECTING/LISTENING 保持等待) + WAITING --> IDLE: net_pending_complete
(数据 / 到点 / EOF / 取消) + IDLE --> IDLE: complete 对非在位槽位静默返回(防重复通知) + note right of WAITING + 等待期零执行者占用: + tick 只读标志与到期时间 + end note +``` + +槽位只有两个状态:**IDLE**(无挂起)与 **WAITING**(有挂起)。所有出口都汇聚到 +`net_pending_complete`(或幂等的 `net_pending_clear`),因此**不存在"完成两次"或 +"漏通知"的中间态**。挂起 send 槽位(§4.5)同构,只是"完成条件"由"数据到达"换为"全部写完"。 + +### 4.5 挂起 send 请求表 `net_send_pending_t` + +| 字段 | 类型 | 含义 | 生命周期 | +|---|---|---|---| +| `active` | `uint8_t` | 1 = 本 fd 有挂起 send | 登记置 1;`net_send_pending_complete` / `clear` 清 0 | +| `buf` | `const void *` | 调用方发送缓冲基址(**仅指针,不拷贝**) | 登记写入;调用方保证有效至返回 | +| `len` | `int` | 本次发送**总**长度(含登记前已写入芯片的部分) | 登记写入 | +| `sent` | `int` | 已写入芯片的字节数(**进度**;登记时可 > 0) | 每次成功写块累加;完成时即结果值 | +| `deadline` | `uint32_t` | 无进展超时时刻 = **上次成功写块时刻** + `NET_SEND_TIMEOUT_MS` | 每次成功写块刷新;`NET_SEND_TIMEOUT_MS == 0` 时置 0(不限) | +| `registered` | `uint32_t` | 登记时刻(tick) | 日志基准 | +| `caller` | `TaskHandle_t` | 调用方任务(完成通知目标) | 登记写入 | + +- **存储**:**独立静态数组** `s_net_send[NET_MAX_SOCKETS]`,与 `s_net_pending[]` **完全独立** + (各自一张表、各占一槽)。 +- **容量**:每 fd **至多一个**挂起 send。 +- **占用**:结构 28 B/fd × 8 fd = **224 B**(实测 RW_IRAM1 +224 B,与 `s_net_pending` 的 192 B 并列)。 +- **与 recv 表可并存**:同一 fd 可同时有一个挂起 recv 与一个挂起 send——收发两个方向互不阻塞 + (SPI 由 netTask 串行,本就支持该并发)。 +- **`deadline` 按无进展刷新**:这是与 recv 表 `deadline`(一次预算、不刷新)的**关键差异**, + 理由见 §5.5。 + +--- + +## 5. 完成与取消语义 + +recv 与 send 两张挂起表**各自独立**,完成语义分别定义:recv 是"等一次到达即完成"(五路), +send 是"分块推进直到全部写完"(四路 + 显式取消)。两表可同一 fd 并存,互不阻塞(§4.5)。 + +### 5.1 挂起 recv 五路总表 + +挂起 recv 被结束的方式只有 5 种,下文简称**五路**——①②③ 由 `net_pending_tick()` +每轮对每个挂起 fd 按序判定,④⑤ 由显式操作触发。 + +| # | 条件 | 动作 | 设计要点 | +|---|---|---|---| +| 1 | **数据到达** | SPI 读出 + 通知(结果 = 字节数) | 由 `net_poll` 维护的 `readable` 门控,**零额外 SPI**;仅 `ESTABLISHED` / `UDP_OPEN` 态可读;spurious readable 需清除 | +| 2 | **到点** `now >= deadline`(仅显式预算登记) | 通知 `NET_ERR_TIMEDOUT`(对外 -2 映射保持) | 状态无关;用有符号回绕安全差值,防 tick 回绕;这是 `net_recv_to` 调用方策略的正常兑现,不是伪造 | +| 3 | **断连**(`CLOSED` 或 `session_dead`) | 排空:有数据则读出;否则按 `released_abnormal` 给 `CONNRESET` 或 0(EOF),并 `auto_relisten` + 通知 | CLOSE 先行完成挂起 = `net_close` 的 settle 钩子 | +| 4 | **同 fd 后续操作**:close / drop_conn / 对账 resync | 先结束挂起 recv,再执行新操作 | per-fd 串行;同 fd 第二个 recv 被登记守卫拒绝(BUSY);send 无需结算(收发不冲突,SPI 由 netTask 串行) | +| 5 | **网络重建**(`net_stack_bring_up`,芯片复位) | **取消 + 必须通知** | `net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;**取消不通知 = 调用方永久挂死** | + +### 5.2 挂起 recv 逐路判据与动作 + +**第 1 路 · 数据到达**(`net_pending_tick` 内): + +- 门控:`state == ESTABLISHED || state == UDP_OPEN` 且 `readable != 0`; +- 命中后 `ch395f_get_recv_len` → 有数据则 `min(可用, len)` 读出、累加 `recv_bytes`、 + 清 `readable`、以字节数完成;`avail == 0`(虚假标志)只清 `readable` 继续等待。 +- **零额外 SPI 的来源**:`readable` 由 `net_poll` 每轮刷新(INT# 电平触发链保证缓冲有数据时 + 每轮重读 SINT 重位置位),`net_pending_tick` 不发起探测事务。 + +**第 2 路 · 到点**: + +- 判据 `(p->infinite == 0) && ((int32_t)(now - p->deadline) >= 0)`;有符号差值抗 tick 回绕。 +- 无限等待登记(`infinite == 1`)**无 deadline,永不因时间结束**——这是调用方策略。 + +**第 3 路 · 断连**: + +- 判据 `state == CLOSED || session_dead != 0`(`session_dead` 是"会话已结束"的**粘滞电平**, + 定义见 `Drivers/BSP/NET/net_types.h`)。 + ⚠ **判据不可收窄**:只判 `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)时 + 到达,属"消费者在等这条连接结局"的**消费者驱动路径**;删掉会把 CLOSED 窗口拉长到 + reconciler 周期 + 隔离期(~100ms),期间调用方高频 `net_accept` → `[WD] task stall`。 + +**第 4 路 · 同 fd 后续操作**: + +- `net_close_locked` / `net_drop_conn_locked` 的**首行**调 `net_pending_settle_close` + (有数据先送达、无数据取消,**必通知**),再动芯片; +- 对账形态⑤(芯片静默回收回 LISTEN)走 `net_pending_cancel(i, CONNRESET)`——连接已被芯片 + 收走,无排空窗口,直接取消。 + +**第 5 路 · 网络重建**: + +- `net_init` 顶部(控制块清零与芯片复位**之前**)`net_pending_cancel_all(NET_ERR_CONNRESET)`; +- 遍历 8 个 fd 逐一通知,未在位者静默跳过。 + +### 5.3 为什么 recv 判据顺序是 ①→②→③ + +1. **数据优先于断连**:断连时芯片缓冲可能仍有残数据(bit3=1 契约),先判数据可让 + 最后一次读到完整内容,避免"断连即丢尾包"; +2. **数据优先于到点**:数据与到点同时成立时交付数据(更贴近调用方意图,且 + `net_recv_to` 的调用方以"本片有数据"为准继续累计预算); +3. **到点优先于断连**:显式预算登记在超时时刻应兑现超时语义,即便此刻连接恰好已断—— + 调用方拿到 `-2` 后按自己的预算循环决定是否续轮,下一轮会拿到断连结果。 + +### 5.4 recv 状态推断的克制 + +`CONNECTING` / `LISTENING` 等中间态**保持等待**,**不做**状态推断式取消—— +`CONNECT → ESTABLISHED` 是合法迁移,推断取消会误杀正在建链的等待。陈旧 resync +(芯片静默回收)由显式 `net_pending_cancel` 处理,不靠推断。 + +### 5.5 挂起 send 的完成与取消(四路 + 显式取消) + +send 与 recv 语义不同:recv 是"等一次到达即完成",send 是"**分块推进直到全部写完**"。 +因此 `net_send_pending_tick()` 每轮对每个挂起 send 按序判定 **①断连 → ②推进 → ③写完 → ④无进展超时**, +另有显式取消路径(close / drop_conn / resync / 重建)。 + +| # | 条件 | 动作 | 设计要点 | +|---|---|---|---| +| 1 | **断连**(`state != ESTABLISHED` 或 `session_dead`) | 以**已发字节数**结束 | 剩余字节不可能送达;判据同快速路径——离开 `ESTABLISHED` 后即便再回来也是**新会话**,旧字节绝不投到新连接;`session_dead` 为冗余保险 | +| 2 | **推进**:`sent < len` 且 `send_ready != 0` | 写下一块(`min(剩余, NET_SEND_CHUNK_MAX)`),清 `send_ready`,刷新无进展计时 | **每轮至多一块**——写完即清 `send_ready`,下一块须等 `SENBUF_FREE` 中断经 `net_poll` 重新置位;与快速路径共用分块粒度(TRAP-NET-16) | +| 3 | **全部写完**(`sent >= len`) | 以**总长度**结束 | 调用方见 `ret == len` 即成功 | +| 4 | **无进展超时**(`now >= deadline`) | 以**已发字节数**结束(短写)+ `TIMEDOUT` | 停滞计时按**无进展**刷新(每次成功写块重置)——大文件传输总时长必然超过单块预算,按总时长判会误杀;这与 recv 侧"到点"是**有意的语义差异** | +| — | **显式取消**:close / drop_conn / resync / 重建 | 以**已发字节数**结束,原因记 `net_set_errno` + `[ERR]` | 已写入芯片的字节**不回收**(对端可能已收到);未发部分放弃,由调用方按短写处理。**取消必通知**(同 recv 铁律) | + +**为什么 send 结果恒为"已发字节数"而非错误码**:`net_send` 的契约是"返回实际写入字节数", +短写是**一等公民**(`ret < len` 即短写,调用方重试剩余)。断连/超时/取消都不伪造"全部成功", +也不把结果改写为负错误码——原因经 `net_set_errno` 记录在 netTask 自己的槽位,调用方以 +`ret != len` 判定短写(详见 `docs/网络/BSD_Socket_API_使用指南.md` §3.5)。 + +**为什么 send 用"无进展"而非"总时长"计时**:recv 的"到点"是调用方**显式预算**的兑现(一次等待 +一个 deadline 就够);send 的等待是**多次块间等待的累积**,一个 100 KB 传输在健康链路下也要 +经历上百次 `send_ready` 等待,总时长远超单块预算。若按总时长判,正常大传输会被误杀成超时—— +故 `deadline` 每次成功写块后重置为"当前 + `NET_SEND_TIMEOUT_MS`",只判"块间停滞"。 + +--- + +## 6. 模块接口契约 + +### 6.1 接口总表 + +全部声明在 `Drivers/BSP/NET/net_pending.h`,实现见 `Drivers/BSP/NET/net_pending.c`。 +recv 与 send 各一组(send 组前缀 `net_send_pending_`),共 14 个函数。 + +**公共 / recv 组**: + +| 函数 | 上下文 | 前置条件 | 返回 | 副作用 | 调用者 | +|---|---|---|---|---|---| +| `net_pending_capture_owner(void)` | 执行任务 | 创建消息队列前 | — | 捕获 `s_owner_task` | `net_init` 顶部 | +| `net_pending_register(fd, buf, len, to_ms, caller)` | netTask | 无在位挂起;`caller` 非空且非执行者 | `NET_PENDING_REGISTERED` / `-1`(BUSY/BADF) | 占槽、写字段 | `net_recv_locked` 阻塞无数据路径 | +| `net_pending_tick(p_socks)` | netTask | `p_socks = s_net_socks` | — | 完成命中的挂起(通知 + 清槽) | `net_process_messages` 开头 | +| `net_pending_complete(fd, result)`(static) | netTask | — | — | 通知调用方、清槽、失败记 errno | 本模块内部所有完成/取消路径 | +| `net_pending_clear(fd)` | **任意** | — | — | 静默清槽(不通知) | `alloc_socket` / `net_close_locked` 尾部 | +| `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` 挂起保险 | + +**send 组**(与 recv 组一一对称): + +| 函数 | 上下文 | 前置条件 | 返回 | 副作用 | 调用者 | +|---|---|---|---|---|---| +| `net_send_pending_register(fd, buf, len, sent, caller)` | netTask | 无在位挂起 send;`caller` 非空且非执行者;`0 ≤ sent < len` | `NET_PENDING_REGISTERED` / `-1`(BUSY/BADF/INVAL) | 占槽、写字段 | `net_send_locked` 超预算路径 | +| `net_send_pending_tick(p_socks)` | netTask | `p_socks = s_net_socks` | — | 推进/完成命中的挂起 send(通知 + 清槽) | `net_process_messages` 开头 | +| `net_send_pending_complete(fd, result)`(static) | netTask | — | — | 通知调用方、清槽 | 本模块内部所有 send 完成/取消路径 | +| `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_pending_complete` / `net_send_pending_complete` 是各自方向**唯一**的通知出口, +> `clear` 是**唯一**静默清槽出口——任何新增路径必须二选一,不得自行写 +> `s_net_pending[fd].active = 0` / `s_net_send[fd].active = 0`。 + +### 6.2 关键契约细则 + +- **通知值即结果**:`xTaskNotify(caller, (uint32_t)result, eSetValueWithOverwrite)`, + 32 位整体不切分;`result < 0` 时先 `net_set_errno(result)`(记入 netTask 任务槽位)。 +- **失败码回传路径(两条)**:快速路径由 `net_process_messages` 回传该条消息**自身**的 + 错误码;挂起路径由 `net_pending_complete` 记 errno 并把负值作为通知值——两处取的都是 + netTask 自己的 errno 槽位(E-03 后按任务隔离),不受其他任务影响。 +- **`NET_PENDING_REGISTERED` 哨兵**:`0x70000000`,取值不与字节数空间冲突;分派器据此 + 置 `do_notify = 0`,把回执推迟到真正的完成时刻,避免"登记成功"被误报为处理结果。 +- **`net_recv_to` 与 `timeout_ms = 0`**:`net_recv_to` 拒收 `timeout_ms == 0`(`INVAL`), + 因为 0 在 netTask 侧意味着"无限等待",与该接口"有界等待"的存在理由相反。 +- **`-2` 映射**:`net_recv_to` 见到 `NET_ERR_TIMEDOUT` 返回 `-2`(**先于**通用 `< 0` 分支 + 判断,否则 `-9` 会被吞成 `-1`);超时只凭返回码判定,不依赖 errno(无跨任务竞态)。 +- **send 短写契约**:`net_send` 返回值恒为**已写入字节数**(`≥ 0`);`= len` 为全部写入, + `< len` 为短写,调用方**必须**检查并重试剩余(`ui_net_sink` / `iec104_serv` 按 + `ret != total` 判短写;`lftpd_inet_write` 循环 `while (n > 0)`、`n <= 0` 视为致命)。 + 断连/超时/取消均以已发字节数结束,**不伪造"全部成功"、不改写为负错误码**——原因记 + netTask 自己的 errno 槽位。 +- **`net_send_sock`(netTask 内部版)**:其阻塞用法会把 `caller` 置为 netTask 自身, + 登记必被"拒绝执行者自等待"守卫拒(`BUSY`)→ 返回 `-1`。故该接口只适合"发送缓冲可用" + 的场合;netTask 内需长时间等待的发送须自行分片(见 §7.2 集成点 13)。 + +### 6.3 编译期开关 + +| 宏 | 位置 | 默认 | 作用 | +|---|---|---|---| +| `NET_PENDING_TRIPWIRE` | `Drivers/BSP/NET/net_pending.h` | `0`(产品构建编译剔除,零成本) | 开启挂起超时留痕探针(见 §12) | +| `NET_RECV_TRIPWIRE_MS` | 同上(仅探针开启时定义) | `60000` | 留痕阈值 | +| `NET_SEND_SPIN_MS` | `Drivers/BSP/NET/net_config.h` | `2` | **发送快速路径**的有界自旋预算(ms):单次"等发送缓冲腾出"超过本值即登记挂起 send,把 netTask 最坏停滞由 `NET_SEND_TIMEOUT_MS`(1 s)压到本值。健康链路实测块间等待 med 219 µs / max 378 µs(≈5× 余量),故正常发送不会触发挂起 | +| `NET_SEND_TIMEOUT_MS` | `Drivers/BSP/NET/net_config.h` | `1000` | 发送**无进展**超时(ms):快速路径的块间停滞上限;挂起路径的 `deadline` 亦按本值刷新。`0` = 不限 | + +--- + +## 7. 实现落点(集成步骤) + +本节是"**改/查本模型时看哪里**"的落地清单:每个集成点若缺失或改动,后果都是确定的。 + +### 7.1 集成点总表 + +**recv 组**(编号 1–12 沿用一期): + +| # | 集成点 | 文件 / 函数 | 调用 | 缺失后果 | +|---|---|---|---|---| +| 1 | 执行者句柄捕获 | `net_socket.c` `net_init` 顶部 | `net_pending_capture_owner()` | 自等待守卫失效(`s_owner_task` 恒 NULL)→ netTask 内阻塞 recv/send 登记成功 → **死锁** | +| 2 | 重建前取消 | `net_socket.c` `net_init` 顶部(芯片复位前) | `net_pending_cancel_all(NET_ERR_CONNRESET)` | 重建前挂起的调用方**永久挂死** | +| 3 | 每轮完成检查 | `net_socket.c` `net_process_messages` 开头 | `net_pending_tick(s_net_socks)` | 挂起**永不完成** | +| 4 | 挂起登记与回执抑制 | `net_socket.c` `net_process_messages` 的 `NET_MSG_RECV` 分支 | `net_recv_locked` 返回哨兵 → `do_notify = 0` | "登记成功"被当结果回执(误报) | +| 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 **摧毁在途连接** | +| 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` 零等待 + 阻塞 | 通知丢失 → 调用方挂死 | +| 12 | 双端注册 | `MDK-ARM/STM32F407-Demo.uvprojx` + `simulator/CMakeLists.txt` | `net_pending.c` 只入真机 | 编译缺失 / 模拟器误编(§7.4) | + +**send 组**(编号 13–19,二期;与 recv 组一一对称): + +| # | 集成点 | 文件 / 函数 | 调用 | 缺失后果 | +|---|---|---|---|---| +| 13 | 发送有界自旋 | `net_socket.c` `net_send_locked` 两个等待循环 | 超 `NET_SEND_SPIN_MS` 即 `net_send_pending_register(...)` | netTask 停滞回到 1 s(队头阻塞);须在 1 s 超时判定**之后**再判自旋预算,保证 `SPIN > TIMEOUT` 误配时仍以超时收口 | +| 14 | 每轮推进 | `net_socket.c` `net_process_messages` 开头(`net_pending_tick` 之后) | `net_send_pending_tick(s_net_socks)` | 挂起 send **永不推进 / 永不完成** | +| 15 | send 登记与回执抑制 | `net_socket.c` `net_process_messages` 的 `NET_MSG_SEND` 分支 | `net_send_locked` 返回哨兵 → `do_notify = 0` | "登记成功"被当结果回执(误报) | +| 16 | 关闭 / 拆会话取消 | `net_socket.c` `net_close_locked` / `net_drop_conn_locked` | `net_send_pending_cancel(fd, NET_ERR_CONNRESET)` | 挂起 send 的调用方**永久挂死**(已发字节数收不回来) | +| 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 **摧毁在途传输**(半写的数据流被截断) | + +### 7.2 逐集成点要点 + +**1–2. `net_init` 顶部顺序**(`Drivers/BSP/NET/net_socket.c`): + +```text +net_pending_cancel_all(NET_ERR_CONNRESET) ← 必须最早(芯片复位前) +memset(s_net_socks, 0, ...) ← 控制块清零 +net_pending_capture_owner() ← 捕获执行者 +... ch395f_check_exist / ch395f_reset ... ← 芯片复位 +``` + +顺序不可交换:取消必须先于清零与复位,否则挂起调用方既收不到通知、其 `caller` +也可能随控制块一起失效。 + +**3–4 / 14–15. `net_process_messages` 开头与分派**: + +```text +net_listener_retry_tick() ← D10 +net_pending_tick(s_net_socks) ← 挂起 recv 完成检查 +net_send_pending_tick(s_net_socks) ← 挂起 send 推进/完成检查 +net_reconcile_tick() ← 对账 + 重建 +while (osMessageQueueGet(..., 0) == osOK) { ... } +``` + +`NET_MSG_RECV` 分支:`msg.result = net_recv_locked(...)`;`NET_MSG_SEND` 分支: +`msg.result = net_send_locked(..., msg.caller)`;两者若等于 `NET_PENDING_REGISTERED` +则 `do_notify = 0`(**不发即时回执**)。send 分支还须把 `msg.caller`(调用方任务句柄) +透传给 `net_send_locked`——挂起登记的目标。四个 tick 均先于消息分派。 + +**5–6. 结算先于拆链**:`net_close_locked` / `net_drop_conn_locked` 的**第一行**就是 +`net_pending_settle_close(...)`——"关闭不得让等待中的调用方永久悬挂",且"关闭不丢已到达 +数据(芯片缓冲在 close 前可读)"。 + +**8 / 20. 挂起保险(recv 与 send)**:`net_listener_retry_tick` 对每个待重试 fd,先做状态门 +(仅 `LISTENING`/`CLOSED` 才重试),再 `net_pending_active(i) || net_send_pending_active(i)` +命中即 `continue`——重开 Socket 会摧毁在途连接(recv)与在途传输(send 的半写数据流)。 + +**11. 两段式等待**(`net_wait_result`,所有 `net_*` API 共用): + +- 第一段零等待拾取,消费"入队 → 等待入口"间隙内已落地的通知; +- 判定必须用返回值 `pdTRUE`/`pdFALSE`,**不能判值 ≠ 0**(0 = EOF 是合法通知); +- 清除掩码必须全 1(传 0 = 不清除,会残留上次结果被误读为本次结果); +- 禁止在本路径加逐调用日志(曾占日志 98.6%,冲掉 `[LAT]` 证据行)。 + +**13. 发送有界自旋**(`net_send_locked`):两个等待循环(首块等待 + 每块发完等待)在 +`net_poll()` 之后判两件事,**顺序不可交换**:先判 `NET_SEND_TIMEOUT_MS`(1 s 停滞 → 记 +`TIMEDOUT` 收口),再判 `NET_SEND_SPIN_MS`(超预算 → `net_send_pending_register(...)` 返回 +哨兵)。把超时判定放在自旋判定**之前**,是为了保证 `NET_SEND_SPIN_MS > NET_SEND_TIMEOUT_MS` +的误配下仍以超时收口,不会"永远登记不出去"。健康路径块间等待 med 219 µs ≪ 2 ms,**永不触发** +挂起——故快速路径行为与挂起化前逐字节一致。 + +**14–19. send 组生命周期**:`net_send_pending_tick` 每轮按 ①断连 → ②推进 → ③写完 → ④超时 +(§5.5)推进;`net_close_locked` / `net_drop_conn_locked` / `net_reconcile_tick` 形态⑤ / +`net_init` 顶部四处调 `net_send_pending_cancel(_all)`(**必通知**,否则调用方永久挂死); +`alloc_socket` / `net_close_locked` 尾部调 `net_send_pending_clear`(静默归零)。 + +### 7.3 改动检查清单 + +**新增一种"结束挂起 recv"的条件**: + +- [ ] 在 `net_pending_tick` 的 ①②③ 判定链中插入,或走显式 `net_pending_cancel*`; +- [ ] 所有出口**必须**经 `net_pending_complete`(不得直接清 `active`); +- [ ] 若新条件是"取消",**必须通知**(取消不通知 = 永久挂死); +- [ ] 复核第 1 路的 `readable` 门控与第 3 路的判据未被新条件破坏(两头落空即挂死); +- [ ] 更新本文 §5 与 `docs/网络/BSD_Socket_API_使用指南.md` §3.5。 + +**新增一个使用阻塞 recv 的消费者**: + +- [ ] 确认该任务**不在** IWDG `required_mask` 内;若在,**禁止无限阻塞**,必须 + `net_select` 超时或 `net_recv_to` 显式预算(契约见 §8); +- [ ] `buf` 保持有效至调用返回; +- [ ] 保留"总预算 − 已耗时"循环骨架(`n == -2` 续轮)。 + +**新增一种"结束挂起 send"的条件**: + +- [ ] 在 `net_send_pending_tick` 的 ①②③④ 判定链中插入,或走显式 `net_send_pending_cancel*`; +- [ ] 所有出口**必须**经 `net_send_pending_complete`(不得直接清 `active`); +- [ ] 若新条件是"取消",**必须通知**(取消不通知 = 永久挂死); +- [ ] 结果恒为**已发字节数**(短写语义),**不得**改写为负错误码——原因走 `net_set_errno`; +- [ ] 复核"无进展计时刷新"未被新条件破坏(按总时长判会误杀大传输); +- [ ] 更新本文 §5.5 与 `docs/网络/BSD_Socket_API_使用指南.md` §3.5。 + +**新增一个 socket fd 生命周期路径**:任何 `memset(p_sock)` / 释放 fd 的路径都须 +配套 `net_pending_clear(fd)`(新 fd 不得继承)或 `net_pending_settle_close`(关闭前结算), +**两个方向都要**(recv 与 send 各自一张表)——漏掉 send 侧会让新 fd 误写旧缓冲。 + +### 7.4 双端注册与模拟器边界 + +- **真机**:`Drivers/BSP/NET/net_pending.c` 必须注册进 `MDK-ARM/STM32F407-Demo.uvprojx` + 的 NET 分组(`net_listener.c` 旁)。 +- **模拟器**:`simulator/CMakeLists.txt` **不编译** `net_pending.c` 与 `net_listener.c` + ——模拟器走 `simulator/sim/net_socket_sim.c`(单线程 Winsock,无法复现挂起模型内部机制; + 外部语义已由包装层按 blocking 属性/预算模拟)。故挂起表 / tick / 通知路径在模拟器上 + **零覆盖**,只能硬件验证。 + +--- + +## 8. 调用方契约 + +调用方**不改协议、不改 API**,但需知悉五条: | 契约 | 说明 | |---|---| -| `buf` 生命周期 | 挂起期间 `buf` 指针须**保持有效**至调用返回(挂起表按指针写入) | +| `buf` 生命周期 | 挂起期间 `buf` 指针须**保持有效**至调用返回(挂起表按指针写入,recv / send 同) | +| **send 短写** | `net_send` 返回值恒为已写入字节数:`= len` 全部写入,`< len` 为短写,调用方**必须**检查并重试剩余(`ui_net_sink` / `iec104_serv` 按 `ret != total` 判短写;`lftpd_inet_write` 循环 `while (n > 0)`、`n <= 0` 视为致命)。挂起 send 以已发字节数结束,不会伪造全部成功 | | 剩余预算循环 | 调用方保留"总预算 − 已耗时"循环骨架(`n == -2` 续轮,预算耗尽退出)——这是**公开契约要求**(30s 总超时语义 + 跨 TCP 分段累积),与本架构无关 | -| 超时精度 | 完成判定随主循环节拍(~1ms);主循环被长操作占用时,超时**最多顺延**相应时长(已知代价,见 §7) | +| IWDG 内禁无限阻塞 | IWDG `required_mask` 内任务**禁止无限阻塞 recv**——必须 `net_select` 超时或 `net_recv_to` 显式预算("超时归使用者"对受监控任务是义务不是选项,否则"对端连上不发数据" = IWDG 复位) | +| 超时精度 | 完成判定随主循环节拍(~1ms);主循环被长操作占用时,超时**最多顺延**相应时长(已知代价,见 §10) | --- -## 6. 性能特征(已验证) +## 9. 性能特征 -| 指标 | 值 | 条件 | +| 指标 | 值 | 来源 / 条件 | |---|---|---| -| 队列响应性(挂起连接下) | **92.5ms** | 挂起对时客户端 + FTP banner 探测 | -| M3 静默档帧周期 | med 58.9 / p95 69.9 / max 72.1ms | 零尖峰 | -| M3 压力档帧周期 | med 60.1 / p95 66.4 / max 69.3ms | 零尖峰,p99 尾部 133.5→69.3ms | -| netTask 栈峰值 | 488B / 4096(余量 88%) | 零占用设计落点 | +| 队列响应性(挂起连接下) | **92.5 ms** | `test/host/e02_probe.py`:挂起对时客户端 + FTP banner 探测 | +| M3 静默档帧周期 | med 58.9 / p95 69.9 / max 72.1 ms | `docs/测试说明/链路延迟测试说明.md` §6.3(零尖峰) | +| M3 压力档帧周期 | med 60.1 / p95 66.4 / max 69.3 ms | 同上(零尖峰;p99 尾部 133.5 → 69.3 ms) | +| netTask 栈实测峰值 | **1116 B / 3840 B(余 70%)**,FTP 传输期峰 **1164 B**;静态最坏链 ≈1.9 KB | `docs/内存管理/RAM使用分析与优化方案.md` §2.3;2026-10-14 台架复测(含 `net_send_pending_tick` 新增栈帧) | + +**结构性收益(为什么这些数字成立)**:压力档下 MCU 侧 `prod_cycle_mcu` 6706 µs 纹丝不动 +(min 6698 / max 6720)——队头阻塞工况不再向 UI 帧周期传导;`net_pending_tick` 在无挂起 +时是纯指针读(零 SPI),故 L5 队列往返段与挂起化前逐位不变。 + +**SEND pending 的结构性收益**(二期,`net_send_pending_tick`):发送停滞期 netTask 不再 +被冻结——netTask 最坏停滞由 `NET_SEND_TIMEOUT_MS`(1 s)压到 `NET_SEND_SPIN_MS`(2 ms), +且停滞不再顺延 `net_pending_tick` 的挂起 recv 唤醒。健康链路块间等待 med 219 µs / max 378 µs +(≈5× 余量),故**正常发送完全不触发挂起**,快路径逐字节不变。 +静态代价:`s_net_send[]` **+224 B**(RW_IRAM1,8×28 B,见 §4.5)。 + +> ✅ **2026-10-14 台架复测已补测**:SEND pending 改动后 netTask 栈峰值 **1116 B / 3840 B** +> (开机态)~ **1164 B**(FTP 传输期),较此前 920 B 增加 ≈200 B(`net_send_pending_tick` +> 新增一层调用帧),余量 69~70%,远低于 1.9 KB 静态最坏链。复测同步发现并修复了 +> 末块挂起越界缺陷(见 §9.1)。 + +### 9.1 末块挂起越界缺陷(2026-10-14 台架复测发现并修复) + +**现象**:SEND pending 二期上线后台架复测,`ftp_regression.py` 间歇性 1/11(部分步骤 +`write error, ret=-1`),偶可 11/11;基线固件同测 6/6 恒过 → 判定为**本次改动引入的回归**。 + +**根因**:`net_send_locked` 的内层"等本块发完(SENBUF_FREE)"循环位于**写块之后**。 +写完**末块**时 `sent` 已累加到 `len`,若该次 SENBUF_FREE 等待超过 `NET_SEND_SPIN_MS` +(2 ms),代码径自调用 `net_send_pending_register(..., sent, ...)`。而登记函数的参数 +校验含 `sent >= len → INVAL` 拒绝(本模型下 `sent == len` 意味着**无剩余块可挂起**), +返回 -1 被 `net_send_locked` 直接透传 → 一次**实际已完整交付芯片**的发送被报成失败。 +`ftp_regression` 的 RETR 1KB 是单块帧(一次即 `sent == len`),故首当其冲。 + +**修复**:内层等待循环**开头**判 `sent >= len` → 直接跳出等待返回 `sent` +(== `len`,完整交付)——末块写完后已无下一块,"等 SENBUF_FREE"是纯保守自旋, +其等待顺延为下一批的责任。该守卫**置于超时/自旋判定之前**,末块路径零额外延迟; +同时它保证后面的 `net_send_pending_register` 调用必伴随 `sent < len`,登记必获受理。 +外层写块前的挂起判定不受影响(循环条件 `sent < len` 未被改写)。 + +**为什么此前没暴露**:健康链路块间等待 med 219 µs ≪ 2 ms,末块等待极少越过预算;只有 +在末块 SENBUF_FREE 变慢(FTP 数据连接刚建立、客户端读速不匹配等)时才触发,故表现为 +间歇性。**教训**:自旋预算与"末块"边界是两个正交维度,二者的组合必须显式覆盖—— +写完末块后"还差多少没写"的答案已是 0,此时任何"等缓冲区"型预算分支都不该继续走。 + +**复测结论**(修复后):`ftp_regression` 连续 **19 次 11/11**(含守卫重排版后重烧 5 次)、 +`ftp_integrity_test` 20 轮 × 64 KB STOR/RETR SHA-256 **20/20 一致**(两轮,吞吐稳定 +≈80 KB/s)、设备日志零 write error、无 forbidden 标记、ST-LINK 回读 `No difference found`。 --- -## 7. 设计边界与已知代价 +## 10. 设计边界与已知代价 | 项 | 现状 | 说明 | |---|---|---| -| **SEND 路径未覆盖** | 发送仍为分块阻塞(1s/chunk stall) | 该期间队列仍被冻结;二期 pending 化解决 | -| **ACCEPT 路径未覆盖** | accept 仍用轮询计时 | 需 pending 化(二期) | -| **超时精度** | 随主循环节拍(~1ms),被长操作占用时顺延 | 一期文档化、不修;二期 SEND 化后该顺延面**收窄** | +| **SEND 路径**(执行者占用问题)✅ **二期已覆盖** | `net_send_locked` 快路径有界自旋 ≤ `NET_SEND_SPIN_MS`(2 ms);超预算登记挂起 send,由 `net_send_pending_tick` 按主循环节拍分块推进(`NET_SEND_CHUNK_MAX` = 1 KB/块),调用方在自己上下文睡 | 停滞期 netTask **不再冻结**:挂起 send 每轮至多写一块,`net_pending_tick` 与消息队列照常推进——挂起 recv 的最坏唤醒延迟不再被 ≈1 s 顺延。健康链路块间等待 med 219 µs ≪ 2 ms,快速路径行为与挂起化前逐字节一致。`NET_SEND_TIMEOUT_MS` 降级为**无进展**超时(每次成功写块刷新,见 §5.5) | +| **ACCEPT 路径未覆盖**(调用方轮询问题) | `net_accept_locked` 单次 `net_poll` 后**立即**返回 `ESTABLISHED` 或 `WOULDBLOCK`——**不占用 netTask**(0 停滞) | 代价不在执行者,而在**调用方轮询**:没有"阻塞到有连接"的入口,各服务(`ui_net_sink` / `iec104_serv` 100 ms / `time_sync` / `lftpd`)各自手写轮询循环,每轮一次完整 RPC 往返(入队 + 通知 + 唤醒),并带来一个轮询周期级的 accept 延迟抖动。pending 化的收益是**消除轮询 churn 与抖动**,不是解队头阻塞 | +| **超时精度** | 随主循环节拍(~1ms),被长操作占用时顺延 | 一期文档化、不修;SEND 化后该顺延面**收窄**(发送停滞不再顺延整层) | | **挂起超时留痕探针** | 编译期选项 `NET_PENDING_TRIPWIRE` 默认关(排查挂死时开);未接自检 selftest | 二期补齐自检 | -| **模拟器不可建模** | `net_socket_sim.c` 单线程 Winsock 无法复现**挂起模型内部机制**(外部语义已由包装层按 blocking 属性/预算模拟) | 挂起表/tick/通知路径在模拟器上零覆盖,只能硬件验证;挂起模型实现独立在 `net_pending.c`(真机专属,CMakeLists 不编译;**监听生命周期 `net_listener.c` 同此**) | +| **模拟器不可建模** | `net_socket_sim.c` 单线程 Winsock 无法复现**挂起模型内部机制**(外部语义已由包装层按 blocking 属性/预算模拟) | 挂起表 / tick / 通知路径在模拟器上零覆盖,只能硬件验证(§7.4) | +| **单 fd 单挂起** | 每 fd 至多一个挂起 recv,第二个被 `BUSY` 拒绝 | 由登记守卫保证;同 fd 并发 recv 本就无意义(per-fd 串行语义) | -**二期扩展设计**(ACCEPT / SEND 的挂起化、门禁清单、改动量)见 -`docs/任务规划/基线与待确认.md` §2.1。 +> **SEND 与 ACCEPT 的 pending 化收益性质不同,勿混为一谈**。判据只有一个: +> **执行者被占多久**——SEND 挂起化前 ≤1 s/次(占用),ACCEPT 0(不占用)。 +> - SEND 是**执行者占用**问题,与 RECV 同源:pending 化直接解队头阻塞,收益真实, +> **二期已落地**(§5.5、§7.1 send 组); +> - ACCEPT 是**调用方轮询**问题:执行者本就零占用,"accept 慢"来自调用方轮询周期, +> 不是 netTask 卡住——pending 化换的是 RPC 开销与延迟抖动,**不解决任何队头阻塞**。 +> 故 ACCEPT 优先级低于 SEND,**仍未落地**(登记项见 `docs/任务规划/登记簿.md` N-4)。 + +**二期扩展设计**(ACCEPT 挂起化、门禁清单、改动量)见 +`docs/任务规划/基线与待确认.md` §2.1。SEND 的挂起化已按该设计落地,见本文 §4.5 / §5.5 / §7.1。 --- -## 8. 设计铁律(为什么是这样) +## 11. 设计铁律(为什么是这样) 1. **执行者永不在等待中自旋**——等待必须在调用方上下文完成,netTask 只负责"登记"与"结束"。 + send 侧的"快路径有界自旋"是该铁律的**受控例外**:自旋被 `NET_SEND_SPIN_MS` 硬限(2 ms), + 超限即转为挂起登记,故执行者占用有界。 2. **取消必通知**——任何取消路径(close / 重建 / 陈旧重同步)都必须唤醒调用方, - 否则是永久挂死而非错误返回。 -3. **挂起状态与 socket 表解耦**——`net_sock_t` 会被整块 memset,跨时间状态不能寄居其中。 -4. **不做状态推断式取消**——中间态(CONNECTING/LISTENING)合法迁移存在, + 否则是永久挂死而非错误返回(recv / send 同)。 +3. **挂起状态与 socket 表解耦**——`net_sock_t` 会被整块 `memset`,跨时间状态不能寄居其中。 +4. **不做状态推断式取消**——中间态(`CONNECTING`/`LISTENING`)合法迁移存在, 推断取消会误杀正常建链等待。 5. **数据到达不引入额外 SPI**——判据复用主循环既有 `net_poll` 维护的 `readable` 标志。 +6. **唯一完成出口**——所有完成/取消都经 `net_pending_complete`(recv)/ `net_send_pending_complete` + (send);任何路径不得自行清 `active`。 +7. **send 结果恒为已发字节数**——短写是一等公民;断连 / 超时 / 取消**不伪造**全部成功、 + **不把结果改写为负错误码**,原因只经 `net_set_errno` 留痕。send 停滞计时按**无进展** + 刷新(每次成功写块重置),绝不按总时长判(会误杀大传输)。 --- -## 9. 排查辅助:挂起超时留痕探针(编译期选项,默认关) +## 12. 排查辅助:挂起超时留痕探针(编译期选项,默认关) -五路判定 + 显式 cancel 是挂起 recv 的**全部唤醒责任**(§4)。一个挂起请求若超过阈值 +五路判定 + 显式 cancel 是挂起 recv 的**全部唤醒责任**(§5)。一个挂起请求若超过阈值 (60s,`NET_RECV_TRIPWIRE_MS`)还没被任何一路结束,只有一种解释:**完成路径漏了它** (代码缺陷)——而不是"数据还没到":无限等待本来就是调用方的合法策略,不该被强行打断。 @@ -210,6 +724,35 @@ flowchart TD "强制超时"兜底:强制结束会把"唤醒路径有 bug"这个**缺陷**伪装成一次**普通超时**—— 调用方拿到假结果,缺陷永远潜伏(旧实现正是"到点 +1s 伪造 TIMEDOUT",已废除)。 -开关:`NET_PENDING_TRIPWIRE`(`net_socket.c`,默认 0 = 编译剔除、产品零成本)—— -排查"调用方挂死唤不醒"时置 1 重烧。**开与关语义完全一致**:探针只留痕,不参与完成。 -代码里该机制名为 tripwire(绊线:挂住 = 踩到缺陷 wiring)。 +开关:`NET_PENDING_TRIPWIRE`(`Drivers/BSP/NET/net_pending.h`,默认 0 = 编译剔除、产品 +零成本)——排查"调用方挂死唤不醒"时置 1 重烧。**开与关语义完全一致**:探针只留痕, +不参与完成。代码里该机制名为 tripwire(绊线:挂住 = 踩到缺陷 wiring)。 +判据基准:无限登记按 `registered` 计,显式登记按 `deadline` 计。 + +--- + +## 13. 源码索引 + +| 文件 | 符号 | 职责 | +|---|---|---| +| `Drivers/BSP/NET/net_pending.h` | `NET_PENDING_REGISTERED` / `NET_PENDING_TRIPWIRE` / `NET_SEND_CHUNK_MAX` | 哨兵、编译期开关、分块上限(唯一定义处) | +| `Drivers/BSP/NET/net_config.h` | `NET_SEND_SPIN_MS` / `NET_SEND_TIMEOUT_MS` | 发送自旋预算与无进展超时 | +| `Drivers/BSP/NET/net_pending.c` | `net_pending_capture_owner` | 捕获执行者句柄(防自等待,recv/send 共用) | +| `Drivers/BSP/NET/net_pending.c` | `net_pending_register` | 登记挂起 recv(两守卫) | +| `Drivers/BSP/NET/net_pending.c` | `net_pending_tick` | 每轮 recv 完成检查(①②③ + 探针) | +| `Drivers/BSP/NET/net_pending.c` | `net_pending_complete`(static) | recv 唯一通知出口(通知 + 清槽 + 记 errno) | +| `Drivers/BSP/NET/net_pending.c` | `net_pending_clear` | recv 幂等静默清槽 | +| `Drivers/BSP/NET/net_pending.c` | `net_pending_cancel` / `net_pending_cancel_all` | recv 显式取消(resync / 重建) | +| `Drivers/BSP/NET/net_pending.c` | `net_pending_settle_close` | recv 关闭前结算(排空或取消) | +| `Drivers/BSP/NET/net_pending.c` | `net_pending_active` | recv 挂起查询(纯 RAM 读,监听器保险) | +| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_register` | 登记挂起 send(两守卫;记录进度起点 `sent`) | +| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_tick` | 每轮 send 推进/完成检查(①断连②推进③写完④超时) | +| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_complete`(static) | send 唯一通知出口(结果 = 已发字节数) | +| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_clear` / `net_send_pending_cancel` / `net_send_pending_cancel_all` | send 静默清槽 / 显式取消 / 重建取消 | +| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_active` | send 挂起查询(纯 RAM 读,监听器保险) | +| `Drivers/BSP/NET/net_socket.c` | `net_recv_locked` / `net_recv` / `net_recv_nb` / `net_recv_to` | 快速路径与挂起 recv 登记入口 | +| `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` 作挂起保险 | +| `App/task/net_task.c` | `net_task_func` | 主循环节拍(`net_poll` → `net_process_messages`) | diff --git a/docs/网络/网络自愈架构设计.md b/docs/网络/网络自愈架构设计.md index 9a98b2f3..7a6e333b 100644 --- a/docs/网络/网络自愈架构设计.md +++ b/docs/网络/网络自愈架构设计.md @@ -1,13 +1,49 @@ # 网络自愈架构设计 > **定位**:本文是 CH395F 网络自愈架构的**现状权威描述**——设计结构、原理、关键参数及其实证锚点。 -> 文中「收敛阶段 0~3」指 2026-10 自愈架构的分阶段实施(过程已归档,见 §9)。 +> 文中「收敛阶段 0~3」指 2026-10 自愈架构的分阶段实施(过程已归档,见 §12)。 +> **本文已吸收原《网络Socket监听生命周期设计说明》(B10 根治,2026-10-14 合并)**: +> 其原理与规范层论证并入 §1.1~§1.4、FUN_PARA bit3 契约并入 §4.9、代码级事实并入 §9、 +> 决策记录并入 §10、模块化边界并入 §11;原文档已删除,过程全文在 git 历史 +> (`git log --oneline -- docs/子系统设计/网络Socket监听生命周期设计说明.md`)。 > 架构审计过程(T1~T4 三路审计与 39 条编号发现、E0~E15 实验方法与原始记录、分阶段实施清单) > 已完成使命并删除——过程全文在 git 历史(`git show 1a35b36^:docs/网络/N34-自愈审计/`,目录合并前 > 7 份文档)。实证结论已沉淀进:`docs/问题记录/CH395F_Trap_Records.md`(TRAP-NET-01~21 与 B 系列)、 -> `docs/子系统设计/网络Socket监听生命周期设计说明.md`(D1~D10 与模块化边界)、`docs/芯片手册/CH395F.md`。 +> `docs/芯片手册/CH395F.md`。 > 仍被代码与文档引用的 12 条编号发现保留在 §7 速查表维持可寻址。 +## 导读:本文用到的编号与术语 + +本文跨越 2026-10 的多轮审计与实验,正文里混用了**多套编号**与**机制代号**。 +它们都是**冻结标识**——代码注释、`test/host/` 探针脚本、`docs/问题记录/` 都直接引用, +**看懂即可,不要重排或改名**(改号即断链)。查下表: + +| 记号 | 是什么 | 在哪查 | +|---|---|---| +| `I1`~`I8` | 本架构的**不变量**(Invariant)——违反即有实测后果 | §2 | +| `E0`~`E15`、`E-02` | **台架实验**编号(E = Experiment):每个实验是「假设 → 方法 → 判据 → 门禁哪个实施决策」 | §5.2(原始记录已归档,见 §12) | +| `F-NET-nn` / `F-DRV-nn` / `F-MS-nn` | 审计的**编号发现**:NET 层 / 驱动层 / 状态镜像层 | §7(只保留仍被引用的 12 条) | +| `M1`~`M5` | 实验固件里的**候选修改代号**:M1 沉降无条件化 · M2 门控换 `net_listener_armed` · M3 可疑门 · M4/M4-lite 删沉降段全局读即清 · M5 `sock_reset` quiescent 排空 | §4.1、§4.3 | +| `形态①`~`形态⑤` | `net_reconcile_tick` 的五类**失同步判据** | §4.1 | +| `U1`~`U15` | 状态迁移的**"无人负责"组合**(审计枚举出的架构欠债) | `docs/问题记录/CH395F_Trap_Records.md` B10 行 | +| `TRAP-NET-nn`、`B3`/`B10` | **陷阱记录**条目与 B 系列(待修/已修清单) | 同上 | +| `N-nn` | `docs/任务规划/登记簿.md` 的**任务/发现编号** | 该登记簿(⚠ `N-32` 等**已销项**——登记簿行按"规划只留待办、完成项删除"约定已删,本文引用处即其最终事实描述) | +| `D1`~`D7` | 本文**决策记录** | §10 | +| `T1`~`T4` | 审计的**三个视角**(已归档,勿再引用) | git 历史(§12) | +| `P0`~`P3`、`阶段 0`~`阶段 3` | 已完成的**实施阶段**(前者 = B10 根治,后者 = 自愈收敛),其勾选清单勿再引用 | §12 | +| `TSL` / `TSC` / `TSD` | SPI 命令与数据之间的**时序间隔**要求 | `docs/芯片手册/CH395F.md` §7.4 | +| `n30` | 压测脚本 `n30_repro.py`(:21 背靠背 STOR 30 轮),自愈回归的**主判据** | `test/host/` | +| 沉降窗 / 隔离期 | 释放会话后、重建监听前的**最短等待**(GRACEFUL 100 ms / QUARANTINE 400 ms) | §4.3 | +| 让位窗 | M3 可疑门交给形态① 电平二次确认的 **400 ms** | §4.1 | +| 镜像 | net 层持有的 8 态 `net_sock_state_t`——**会话投影**,不是芯片事实源 | §3.3 | + +> ⚠ **字母 `E` 有两套编号,靠连字符区分**: +> - `E0`~`E15`(**无**连字符)= 本文的**自愈实验**编号,逐条见 §5.2; +> - `E-02`(**带**连字符)= `docs/问题记录/错误码传播审计.md` 的**审计发现**编号—— +> §5.2 末行引用它是为了记录该发现的**收口台架**,**不是**实验编号 +> (证据:`test/host/e02_probe.py` 文档串自述"错误码传播审计.md E-02 的 timeSync 触发形态"); +> - 该审计编号族冻结于 `E-63`,不再新增。 + ## 1. 为什么是这个架构 三个芯片事实决定架构形态(全部台架实证,见 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-09/-17/-18/-20 @@ -26,6 +62,69 @@ 组合:net 层持有损 8 态镜像并作为决策依据 + 状态迁移只靠中断事件 + 重建与事务脱钩。 现架构(§2~§4)即按此收敛的结果。 +### 1.1 芯片层事实:LISTEN 不是能力,是一个会被消耗掉的槽位状态 + +| 事实 | 出处 | +|---|---| +| 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`,bit3 `SOCKET_CLOSE`=1, +TRAP-NET-15/-20 定案为"保留排空现场")⇒ **断开后芯片必然停在 `0x05/0x00`**。 +探针看到的"孤儿芯片态"不是芯片异常,而是本仓这个配置的**直接且合法的后果**; +bit3=1 的必要性与安全边界见 §4.9。 + +### 1.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), +于是"关掉一条连接"和"毁掉一个监听器"在类型上无法区分——这正是红线 8 +(「EOF 时不得 `net_close` 监听 fd」,§6)存在的唯一原因。 +**需要靠铁律维持的正确性,就是架构欠债的凭证。** + +### 1.3 实现层:fd 别名 → 边沿通知 → 多所有者 + +EOF 只在 **recv 那一刻**本层为 CLOSED 才给出(`net_recv_locked`,`Drivers/BSP/NET/net_socket.c`); +若该 fd 已被别的路径 relisten 回 LISTENING,同一个 recv 恒返 `WOULDBLOCK`。 +这是一个**边沿**,不是一个**电平**——正是不变量 I2/I3 要消除的形状。 + +由此派生出"谁重建监听"的**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 行)。 +收敛后周期性决策已唯一化为 §4.1 形态③④、消费者驱动即时重建按 §4.2 保留(3 处)—— +上段是欠债的**原始形状**,保留以说明"为什么需要 I2"。 + +三个已实测的派生失效(机理保留,供判读 I2/I3 被违反时的症状): + +| 失效 | 机理 | 现状 | +|---|---|---| +| 孤儿监听(B10) | 边沿没人接 → net 层等应用重建、应用等 net 层给 EOF、对账只裁 ESTABLISHED/LISTENING 直接跳过、芯片不会自己回 LISTEN → **四方互等** | 已收口(§4.1 形态④ + §4.6 `net_drop_conn`) | +| 消费者卡死 | 抢先 relisten 把慢消费者的 EOF **永久**吞掉 → `g_client` 不清、新连接被旧会话静默吸收 | 已由 `session_dead` 粘滞电平消除(§3.3 不变量 I) | +| 监听被误关 | `net_can_read` 在非 ESTABLISHED(**含 LISTENING**)返 1 → 消费者 recv 得 `WOULDBLOCK` → 判"真错误"后无护栏 `net_close` 等于**关掉监听 fd** | 已加 `g_client != g_listen` 护栏(`Drivers/BSP/UI/ui_net_sink.c`);落 TRAP-NET-09 沉降窗 | + +### 1.4 为什么不走"芯片原生多连接模式"(A′ 出局) + +芯片确实自带 lwIP 式模型:FUN_PARA bit1(≥0x44,本仓实测 0x4A)下 +「服务器的 Socket **仅用来监听**,单片机需要分配其它 Socket 用于该连接」(`docs/芯片手册/CH395F.md` §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 中断从不触发」、 + 「多个 Socket 数据互相覆盖」——即 TRAP-NET-02/-11。 + +且 FUN_PARA **初始化后不可改**(`docs/芯片手册/CH395F.md` §5.52 L795⑥),这是开机一次性决策, +不能运行时降级。**结论:A′ 出局;根治只能在单连接复用模型内做**(红线 2,§6)。 + ## 2. 设计原则(不变量) | # | 不变量 | 违反的后果(均实测) | @@ -196,6 +295,55 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要 - **E6 裁决(2026-10-13,~4h 常态 + 每小时 :7003×10 + E1 压测):命中=0 → 内化不 立项**,上层盲收保留现状(I8);将来出现数据面失配再武装重裁。 +### 4.9 FUN_PARA bit3=1 的必要性与安全边界(D4) + +> 结论:**必须保留 `0x08`**。本节以手册原文 + 实测证据回答"bit3=1 引出这么多问题, +> 是否该改回 0",并给出软件侧的收口位置。 + +**① 必要性 = 硬性必需,不是可调项。** 手册对 bit3 的分流写得很直白: + +- `docs/芯片手册/CH395F.md` §5.52 注释②:「此位为 0 表示在产生 SINT_STAT_DISCONNECT 或者 SINT_STAT_TIM_OUT 中断后, + CH395 会主动关闭 Socket。……**关闭 Socket 就意味着 CH395 会将此 Socket 内部缓冲区和 + 一些相关变量清除**,在某些应用中,如果 CH395 内部自行关闭 Socket 可能会导致残留的数据 + 被强行清除。这种情况下可以将该位置 1,**由单片机将数据读出来,然后再关闭 Socket**。」 +- `docs/芯片手册/CH395F.md` §5.23 L572 同向:「为 0 则……主动将 Socket 状态置为关闭状态**并将相关缓冲区全部清除**; + 反之则不会对 Socket 状态和相关缓冲区做任何操作,以方便外部单片机……**将残留的数据读出**。」 + +实测现场(TRAP-NET-20 判别探针):`st=0x05/0x00` 且 `recv_len=1027`—— +Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少**。这正是 bit3=1 按规格 +保留现场,不是芯片异常(`docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-20「定案二」)。 + +**② bit3=0 的代价是「硬件保证的数据丢失」,且不可在软件层补救。** +本产品最典型的流量形态是「发完即 FIN」:ftplib `storbinary` 语义 = `sendall` 发完立即关 +数据连接、回控制通道等 226。芯片把最后一段数据落进 RX 的同时抛出 DISCONNECT: + +- bit3=1:RX 数据在,MCU 来得及读(TRAP-NET-18 的「排空 RX 再 close」修复才成立); +- bit3=0:芯片**当场**关 Socket 并清缓冲 → MCU 永远读不到尾部 → 文件被截断却仍回报 226 + (静默损坏)。这是 bit3=0 的**必然结果**,不是概率问题。 + +即:改 bit3=0 等于用「一个硬件保证、不可修的数据丢失」去换「一个软件侧可修的关闭义务」, +严格更差。:8888 对时包、:7003 远显推流同样是「数据 + 随即关闭」形态,一并受害。 + +**③ bit3=0 也并不能真正消除孤儿态。** 芯片自动关以**收到** DISCONNECT/TIM_OUT 为前提, +而 TRAP-NET-17(E14 复核)已实证快速复连场景这两个中断会被读清消费掉 +(`docs/问题记录/CH395F_Trap_Records.md` B10 行)→ 事件丢了,bit3=0 同样停在已关/半关态, +孤儿 Socket 照样存在。它只是降低出现概率,代价却是第②条。 + +**④ 所以「这么多问题」不归 bit3=1。** 断开后停 `0x05/0x00` 是 bit3=1 的**合法后果**(D4); +真正造成 B10 永久拒连、消费者卡死、EOF 被吞没的,是软件侧没把「该由 MCU 关 Socket」这条 +义务落到唯一所有者(§1.3 的 12 所有者 / 15 组合),以及 U12 的「relisten 的 LISTEN 失败 +不 close 芯片 Socket」与 open 失败路径不对称。bit3=1 只是把义务显式化,把原本被芯片 +掩盖的架构欠债照了出来。 + +**⑤ 安全处理 = 把 bit3=1 的契约在软件侧收口**(逐条对应上面的缺口): + +| 契约要求 | 对应缺口 | 收口手段(现状) | +|---|---|---| +| 「会话结束」必须**谁都能看见**,不能只在边沿那一刻 | 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) | +| 关闭姿势必须正确 | TRAP-NET-12:陈旧 DISCONNECT 锁存秒断下一条连接 | `sock_reset` 序列 = 排 RX → CLOSE → 排事件 → 等 `byte0=00H`(§4.4) | +| 异常释放须先排空残渣再关 | TRAP-NET-18:~2KB RX 残渣被下条连接当自己的数据读走 | 隔离期持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED`(§4.3) | + ## 5. 关键参数与实证锚点 ### 5.1 参数表 @@ -212,27 +360,36 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要 | recv tripwire | 60s | 阶段 1.5(完成逻辑缺位探针,不改语义) | | 恢复窗(正常断连→监听可用) | 0.3~1s | E1 发现 A:释放只置事实 → 等周期 tick → SYN 静默丢 → 客户端 1s RTO → `:7003` 复连 p50≈1020ms | -### 5.2 实验锚点速查(E 编号被代码与陷阱记录引用,结论一句话) +### 5.2 实验锚点速查(E 编号 = 台架实验序号) -| E | 结论 | 落点 | -|---|---|---| -| E0 | 常态 3.5h 对账零命中(冷路径);D10 逐轮留痕是主要日志噪声源 | reconciler 热路径成本可接受;重试日志只留首末 | -| E1 | `:7003` 复连 50/50 p50≈1020ms(恢复窗 0.3~1s);背靠背 STOR 4/50 失效循环(发现 B) | P0 止血包;`n30_repro` 纳入常备回归 | -| E5 | 静默回收形态 ~110 次连接零复现(代码缺口真实、触发罕见) | 阶段 1 顺带修(形态⑤),不单独立项 | -| E7/E7b | M1+M2+M4-lite:n30 8%→96.7%(116/120);M1 不带 M4-lite 会放大 F-NET-7 | P0 转正(`9ac9879`);M1/M4-lite 同批纪律 | -| E8 | 故障率 2%~54% 状态依赖(残渣存量);风暴三机制(F-NET-17) | M3/M5 设计 + "沉降让位救回"红线 | -| E9 | M3 判据三轮迭代定案(资产门版);风暴免疫 + 零误伤 | §4.1 M3 门(`ae17d36`) | -| E10 | 阶段 0/1 转正全绿;**armed 必须放行 ESTABLISHED**(i104 18/18,127s→8.3s) | §4.2 门控 | -| E11 | 服务器侧 KeepAlive 真实可用;判死 ≈30.0s(手册公式不成立);静默存活不误杀 | §4.7 转正 | -| E12 | TSL 规格偏离:460 轮/≈58MB 双向 SHA-256 无可复现危害、两臂无差异 | §6 红线 6;B 变体留作待用补丁 | -| E13 | 风暴态根因 = 僵尸会话(确定性复现);RSTI←NRST 推翻"跨烧录存活";自愈链端到端有效 30~90s | §4.7;PASV 预清缺口由 `sock_reset` 收口 | -| E2 | 单次回读 18.1µs;LISTEN 可见 ≤10ms 无长尾;LISTEN 态 CLOSE 二段(byte0 留 05H) | 缓存刷新策略;② 去抖 1 轮;`sock_reset` 二段补发 | -| E3 | OPEN/LISTEN/CLOSE 命令状态无判别力 + 陈旧 0x17 跨命令存活;SET 类完全不更新 | `cmd_done` 判据 = 状态回读 | -| E4 | 复位序列 A(排RX→CLOSE→排事件→等00H)20/20 vs C(什么都不做)0/20 级联自持;rxdrain=2048 | §4.4 序列定案;两模式都必须排 RX | -| E6 | U15 命中=0(~4h 常态 + 压测;用户裁决提前收线) | §4.8 观察项 | -| E-02 | `drop_conn` 时序定案:配合对端 TIME_WAIT ~150ms→形态④;不配合 FIN_WAIT_2 长尾 | §4.6 | -| E14 | "丢中断"复核(2026-10-14,48 轮直读 SINT):芯片侧事件丢失 = 0;"丢失"全部 = drain 读清消费(3/48,drain-int 现场抓获)+ 门控竞态(2/48)+ 主机同步(1/48);幽灵态本轮 0/48 未复现 | §1 事实 2 改写;TRAP-NET-17/-20 归因更正(机制保留不变);生产 listen/relisten 排事件挪至 LISTEN 命令**前**(根除自伤窗) | -| E15 | 幽灵态归因复核(2026-10-14,官方出处):0x04+旧端口 = 芯片**如实报告**旧连接拆链进行中——① bit3 `SOCKET_CLOSE=1` 为我方设置(版本门控,§5.52②:DISCONNECT/TIM_OUT 后芯片按设计不动状态与缓冲);② 拆链走 RFC 9293 §3.3.2 状态机:主动关闭 FIN_WAIT_1→FIN_WAIT_2→TIME_WAIT(2MSL 驻留;TIME_WAIT 协议目的=防旧连接迟到报文伤新连接——"断连即重连被拒"的协议根源),芯片自动走完(手册 §5.22);§5.17 默认重传 12 次、实测耗尽 ≥3s,与拆链窗实测 >2s 同量级;③ WCH 官方博客确认"断连后立即重连被拒、需程序侧特殊处理" | TRAP-NET-20"谎报"→"如实报告";settle 等待 = 官方方案;幽灵甄别 = 强制放行路径的护栏;I3 就此加强 | +> **`E` 是什么**:`E` + 序号 = **台架实验编号**(E = Experiment),不是错误码、也不是小节号。 +> 每个实验遵循「假设 → 方法 → 判据 → 它门禁哪个实施决策」四段式;下表给出方法与结论, +> 原始记录(假设、原始日志、统计)随审计过程归档,取用命令见 §12。 +> ⚠ **这些编号是冻结标识**:`net_socket.c` / `net_listener.c` 注释、`test/host/e02_probe.py`、 +> `test/host/e6_soak.py`、`docs/问题记录/CH395F_Trap_Records.md` 都直接引用它们,**重排或改名会断链**。 +> ⚠ 本表**末行 `E-02` 不是实验编号**——它是 `docs/问题记录/错误码传播审计.md` 的**审计发现**编号 +> (`E-` 族,冻结于 `E-63`),列在此处只为记录该发现的**收口台架**。 +> 区分规则:**带连字符 = 审计发现,无连字符 = 自愈实验**。 + +| 编号 | 实验做了什么(方法) | 结论 | 落点 | +|---|---|---|---| +| **E0** | 直接 grep 生产常态串口日志(gen=149 窗口,开机后 ~3.5 h,零台架成本) | 常态下对账**零命中**(冷路径);日志噪声主要来自 D10 重试的逐轮留痕 | reconciler 热路径成本可接受;重试日志只留首末 | +| **E1** | 现网固件 + PC 侧压测:`n30_repro.py` 背靠背 STOR、`ftp_regression`、`:7003` 100 ms×50 快速复连、iec104 连断 ×10 | `:7003` 复连 50/50、p50≈1020 ms(恢复窗 0.3~1 s);背靠背 STOR 出现 4/50 失效循环 | 促成止血;`n30_repro` 纳入常备回归 | +| **E2** | 裸机实验固件(`ch395f_reuse_bench.c` + DWT 周期计数器):测 `GET_SOCKET_STATUS` 单次 SPI 耗时;LISTEN 命令后 0/10/50/100 ms 回读,看首次观察到 TCP=01H 的延迟 | 单次回读 **18.1 µs**(每轮全量刷新成本可忽略);LISTEN 可见延迟 ≤10 ms 无长尾;**LISTEN 态 CLOSE 是二段**——一次只拆监听,byte0 恒留 05H 不自落 | 驱动缓存刷新策略;reconciler 形态② 去抖降到 1 轮;`sock_reset` 内置二段补发 | +| **E3** | 同场固件,对 OPEN / LISTEN / CLOSE / SET_* 五条命令各 5 轮 ×2 遍 = 50 样本,看 `GET_CMD_STATUS` 有无判别力 | OPEN / LISTEN / CLOSE **无判别力**,且陈旧错误码 0x17 跨命令持续存活;SET 类**完全不更新**状态 | 命令完成判据改用**状态回读**(`ch395f_cmd_done`) | +| **E4** | 裸机 3 变体 ×10 轮 ×2 遍 = 60 轮:先造污染会话(发 3000B 后 RST),按变体复位后**立即**重开监听并上传 4096B 图案载荷,逐字节校验 | 变体 A(排空 RX→CLOSE→排空事件→等 byte0=00H)**20/20**;变体 C(什么都不做)**0/20** 级联自持;实测 `rxdrain=2048`(芯片单 Socket RX 缓冲满容) | §4.4 复位序列定案;两档沉降**都必须排 RX** | +| **E5** | :7003 上 connect→发 1B→对端 FIN→等 3 s→复连 ×10,查"芯片静默回收回 LISTEN 却不发 DISCONNECT"能否复现 | 20 次 FIN **全部**产生 `DISCONNECTED`;静默回收形态在 E1+E5 累计 ~110 次连接中断里**零复现** | 缺口代码上真实但触发罕见 → 顺带修(对账形态⑤),不单独立项 | +| **E6** | 生产路径插桩固件(`NET_U15_PROBE=1` + `test/host/e6_soak.py`):常态运行 + 每小时 :7003×10 + 压测,探"`GET_RECV_LEN>0` 但 `!readable`"失配 | 命中 **0**(~4 h,用户裁决提前收线,原规格 24 h) | U15 登记**观察项**、不立项(I8);上层盲收保留现状 | +| **E7 / E7b** | 实验固件加候选修改 M1(GRACEFUL 沉降无条件化)+ M2(门控换 `net_listener_armed`),再叠加/去掉 M4-lite(删沉降段全局读即清),跑 `n30` 压测 | M1+M2+M4-lite 把 `n30` 成功率从 **8% → 96.7%**(116/120);**M1 单独上会放大 F-NET-7** | 转正(`9ac9879`);立下「M1 与 M4-lite 必须同批」纪律 | +| **E8** | 同固件、同负载,**只变设备状态**跑 `n30` 矩阵(全新启动 / 探针+压测累积后 / 仅 ST-Link 复位) | 故障率在 **2%~54%** 间随状态摆动 → 故障率 ∝ 芯片侧残留存量;抓到"风暴签名"三机制(F-NET-17) | M3 可疑门 + `sock_reset` quiescent 排空;"沉降让位救回"红线 | +| **E9** | M3 原型三轮迭代(可疑门判据:芯片 `0x05/0x00` + `lost_conn_seen==0` + `recv_bytes==0` + 芯片 RX 有在途数据) | 判据定案为**资产门版**;风暴态免疫 + 零误伤 | §4.1 的 M3 门(`ae17d36`) | +| **E10** | 阶段 0 九项 + 阶段 1 六项转正后的全量台架验证 | 全绿;暴露 **`net_listener_armed` 必须放行 ESTABLISHED**——漏放行时 i104 的 200 ms 轮询抢不过形态② 去抖而整场饿死(18/18 FAIL,127 s→8.3 s) | §4.2 门控定案 | +| **E11** | 临时固件开启服务器侧 KeepAlive(`CMD_SET_KEEPALIVE_SN`,60s/5s/3),用"禁用 PC 网卡"造"链路在、对端静默" | 真实可用;判死 **≈30.0 s**(早于手册公式 75 s,以实测为准);静默但**存活**的对端不误杀 | §4.7 转正 | +| **E12** | SPI 缓存区 TSL 时序 A/B:手册要求"长度与数据之间"留 TSL(读 0.4 µs / 写 0.2 µs),本仓把命令头与数据打包进同一 DMA 突发(间隔=0),对照两臂 | 460 轮 / ≈58 MB 双向 SHA-256 **无可复现危害**、两臂无差异 | §6 红线 6;"两段+TSL"变体留作**待用补丁** | +| **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 就此加强 | +| **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. 红线(明确不做的事) @@ -280,10 +437,145 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要 3. **U15 观察项**:若将来出现数据面失配(RX>0 但 `!readable`),重新武装探针重裁(§4.8)。 4. **链路事件业务消费**:缓存与留痕已就绪;PHY_CHANGE → 自动重配的触发待产品需求。 -## 9. 引用与归档约定 +## 9. 代码级事实清点 + +> 判读本架构或做决策时需要的**源码事实**。⚠ 只给符号名、不固化行号——行号随重构漂移 +> (本仓曾出现文档内两处行数快照自相矛盾)。行数快照(2026-10-14): +> `net_socket.c` 3702 行、`net_listener.c` 467 行、`net_listener.h` 97 行。 + +### 9.1 会话投影的状态写入面 + +`net_sock_t.state` 的**全仓唯一写者**是 `net_socket.c`(13 处显式赋值 + 2 处 `memset`); +`net_types.h` 只定义、不写。因此"镜像说 A、芯片是 B"的窗口只可能由这一个文件制造—— +这是 I1(镜像降级为会话投影)收敛成本低的根本原因。 + +### 9.2 关键状态门(做决策必须知道) + +| 函数 | 状态门 | +|---|---| +| `net_listen_locked` | **只允许 BOUND / CREATED**——对已 CLOSED 的监听 fd 直接 `net_listen` 返 INVAL | +| `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` 门 | + +### 9.3 三个服务的契约差异(历史成因,现状已对齐) + +| 服务 | 曾认为的连接结束信号 | net 层实际给什么 | 监听重建者 | 是否留无人重建组合 | +|---|---|---|---|---| +| `lftpd` 控制 :21 | `read_line!=0`(EOF→-1) | 阻塞 pending 保证 CLOSED 时挂起 recv 必被结束(FIN→0 / 异常→CONNRESET) | net 层;应用**永不** close 控制 fd | 无(契约一致,**正面样板**) | +| `time_sync` :8888 | EOF / 预算耗尽 / 错误,随后**无条件** `net_close(conn==ls)` | 同上 | 应用自己每会话全量重建 | 不留孤儿,但违反红线 8 → close→重建沉降窗(TRAP-NET-09)+ 最坏 ~10 s 端口下线;**已改走 `net_drop_conn`**(§4.6) | +| `ui_net_sink` :7003 | `net_recv` 返 0 或真错误;重建条件 `net_get_sock(g_listen)==NULL`(fd 不释放 → **恒假**) | 该 0 只在 recv 时刻 `state==CLOSED` 才给 | 名义 net 层,实际自身条件在 MCU 恒假 | **有**(B10 与"消费者卡死"都出自这里);**已收口**(§1.3 / §4.1) | +| 模拟器后端 | `recv` 0 恒可得、fd 分离 | Winsock 原生 | 无需 | 无——且对上述全部失效模式**零覆盖**(D6:双模型分裂暂不消除) | + +## 10. 决策记录 + +| # | 决定 | 依据 | +|---|---|---| +| D1 | **A′(启用芯片多连接模式)出局**,根治必须在单连接复用模型内做 | §1.4:多监听不支持 / 池按源端口匹配会互串 / 历史实测 TRAP-NET-02、-11 / FUN_PARA 初始化后不可改 | +| D2 | 采用**形态 B 深化**(电平化 + 单一所有者),**不做 P3**(listener 一等对象 + `accept` 返回虚拟句柄) | P3 需控制面重写 15~25 函数、RAM/Flash 上升,只有"同端口并发多连接"需求才值得 | +| D3 | 周期对账**保留**,其副作用由不变量 I 消除 | TRAP-NET-17 实证事件面不是可靠唯一信源(E14 复核:非芯片侧丢失,是读清消费)→ 周期对账不可删 | +| D4 | `FUN_PARA` **保持 `0x08`**,不因孤儿态改配置 | bit3=1 是 TRAP-NET-15/-20 定案的"保留排空现场"契约;bit3=0 会令芯片在 DISCONNECT/TIM_OUT 时**当场清缓冲** →「发完即 FIN」的尾部数据必被硬件擦除、**不可补救**。完整论证见 §4.9 | +| D5 | 顺序 **P0 → P1 → P2**,每阶段独立提交、独立回归、可单独回滚 | P1 不动 fd 模型却同时消掉 B10 与"消费者卡死"两类事故,性价比最高 | +| D6 | 真机/模拟器**双模型分裂暂不消除** | 消除即等于做 P3;先靠不变量 I/II 把应用侧契约对齐 | +| D7 | 同族残留**不在监听生命周期根治范围**,各自单列:U1(CONNECTING 滞留)、U11、U12、U14、U15,以及审计项 B4~B9 | 与监听生命周期不同因,混做会让回归面失控 | + +## 11. 监听生命周期的模块化边界(`net_listener.c`) + +> **动机**:真机 net 层 `net_socket.c` 一度 4126 行,而模拟器做同样的事只有 807 行(5 倍); +> `relisten` 一词在文件里出现 110 次。诉求 = **高内聚低耦合**。 +> 先由 §4.1 把"周期性重建"收敛到唯一决策者,搬移边界才第一次清晰——**先收敛,再搬移**。 + +### 11.1 可机械检验的判据 + +| # | 判据 | 收敛前 | 目标 | 结果 | +|---|---|---|---|---| +| 1 | 模块外调用「重建监听」的**入口** | 8 处散点直调 + 2 处直写状态数组 | **单一入口 `net_listener_try`** | 4 个调用点(形态③ / accept CLOSED 分支 / recv EOF / `net_drop_conn`),无散点 | +| 2 | `s_relisten_*` 静态状态被**模块外直接读写** | 2 处外部写 | **0** | 0(外部写改为 `net_listener_settle`) | +| 3 | 依赖方向 | 无 | 新模块 → `net_socket.c` **单向** | 达成(不反向 include) | + +⚠ **明确不承诺**:本重构**不消除 fd 别名**(同一 Socket 既当监听器又当连接)。 +那是 CH395F 单连接模型 + bit1 多连接模式**不支持多监听**(§1.4)锁死的,属硬件上限。 +重构只提升**组织度**,不改变架构上限——**别指望补丁消失,只指望补丁不再重复**。 + +### 11.2 边界划分 + +**搬入 `Drivers/BSP/NET/net_listener.c`**(**真机专属**,sim 不编): + +| 搬入 | 理由 | +|---|---| +| `net_relisten_locked` / `net_listener_try` / `net_listener_retry_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) | + +**留在 `net_socket.c`**(并说明理由,避免"看着像就搬"): + +| 留下 | 理由 | +|---|---| +| `net_listen_locked` | 是**应用 API 路径**(首次监听,含 BOUND/CREATED 状态门与缓冲)——属"连接层建监听",不是"维持监听" | +| `net_accept_locked` | 产出的是**连接**(LISTENING→ESTABLISHED 迁移),属连接层;它只**调用**新模块 | +| `net_lost_connect_probe` | 做的是"补 accept"(连接建立),不是重建监听 | +| `net_listener_armed` | **双端公开 API**(sim 也实现);纯 RAM 查询、很薄,留在原处避免 sim 缺符号 | +| ~~`net_resync_listen`~~ | **阶段 1 已删除**:其"只改本层状态"语义并入对账⑤(§4.1),手动 API 面退役 | +| `net_reconcile_tick` | ①②⑤ 是**连接**失同步自愈;③④/U11 亦已回归本 tick(阶段 3,§4.1) | +| `net_sock_t` 全部字段 | 数据归 `net_types.h`(双端共享核心),**不随模块走** | + +> ⚠ `net_sock_t` 的 `last_peer_port` / `lost_conn_seen` 是**跨层字段**:`net_relisten_locked` +> 要写它们(转登幽灵甄别基准),`net_lost_connect_probe` 与对账②也要读写。 +> **这是本次重构无法消除的残余耦合**(单连接模型的固有结果),明确登记而非假装不存在。 + +### 11.3 对外接口(4 函数 + 1 枚举,全部窄接口) + +```c +/* net_listener.h —— 只被 net_socket.c include(内部头,不进公开 net_socket.h,故 sim 无依赖) */ + +typedef enum { + NET_LISTENER_SETTLE_QUARANTINE = 1, /* 异常释放(TIMEOUT/RST):隔离 400ms + 排空 RX 残渣 */ + 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 每轮调用,非阻塞) */ +``` + +**反向依赖(新模块需要宿主提供的)**:`net_sock_t` 定义(`net_types.h`,共享头,不算反向依赖)、 +`net_set_errno` / `NET_ERR_RETURN_1`、`net_pending_active`(§11.4)、`ch395f_*` 驱动、`HAL_GetTick`。 +`net_listener.c` **不访问** `s_net_socks`——一律用 `p_sock->ch395_sock` 或调用方传入的 `sockfd`。 + +### 11.4 落地结果与三处「非纯搬移」 + +`net_socket.c` 4126 → 3656 行(-471);新增 `net_listener.c` / `net_listener.h`。 +`RW-data` 980 → 980、`ZI-data` 143060 → 143060 **完全不变**(数据总量不变); +`Code` / `RO-data` 各 -8 / -12 字节(相对 471 行搬移可忽略)。 +接口由「8 处散点直调 + 2 处直接写数组」收敛为 **4 个窄接口**;`s_net_socks` 依赖**已消除**。 +`@test` PASS + 回读 `No difference found`。 + +执行中暴露三处**必要**的"非纯搬移"(均已在代码注释留痕,是当前代码形状的成因): + +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 则跳过」 + (L3 保险,防重开 Socket 摧毁在途连接)直接读 `s_net_pending[i].active`;新模块不能碰该 static + 数组,故加薄访问器。**语义原样保留**。 +3. **`net_listener.c` 必须真机专属** —— 它依赖 `ch395f_*` SPI 驱动,而 sim 是 Winsock 后端 + (无 `ch395f_*`、无孤儿态、无重监听概念)→ **无法**在 sim 编译。故只注册 Keil 端, + 并在 `scripts/check_registration.py` 的 `SIM_EXEMPT` **显式点名**(遵"显式清单 + 每行理由"原则)。 + +> **判据修订留痕**:原定「纯搬移可证 = Code / RO-data / RW-data / ZI-data 完全不变」**过严**—— +> 接口改造必然改变代码生成。修订为:**RW-data / ZI-data 必须完全不变**(数据总量不变), +> `Code` / `RO-data` 允许极小变化。 + +## 12. 引用与归档约定 - 本文引用格式:`docs/网络/网络自愈架构设计.md` §N.M(紧邻写法);E 编号引 §5.2 (如"§5.2 E12"),F 编号引 §7。 -- **勿引用已删除的过程章节**(T1~T4 审计表、实验方法与原始记录、分阶段勾选清单)。 - 过程考古:`git show 1a35b36^:docs/网络/N34-自愈审计/`(目录合并前 7 份文档)或 - `git log --oneline -- docs/网络/N34-自愈审计/`。 +- **勿引用已删除的过程章节**:N34 审计表(T1~T4、实验方法与原始记录、分阶段勾选清单), + 以及原《网络Socket监听生命周期设计说明》的 P0/P1/P2 勾选清单与台架验收记录。 + 过程考古: + - N34 自愈审计:`git show 1a35b36^:docs/网络/N34-自愈审计/`(目录合并前 7 份文档)或 + `git log --oneline -- docs/网络/N34-自愈审计/`; + - 监听生命周期设计说明(本文 §1.1~§1.4 / §4.9 / §9~§11 的来源): + `git log --oneline -- docs/子系统设计/网络Socket监听生命周期设计说明.md`。 diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index 78f6c14c..2df4224c 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -275,7 +275,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修 | B7 | 低 | `ch395f_test_task.c` Phase 5 | 单次写 4093B 超 1KB 发送缓冲与 2048 DMA 载荷 → 整块被丢;注释仍写旧上限。写路径金丝雀(A6)会打 `[ERR]` | ⏳ 待修(chunk 改 ≤1024 或分块) | | B8 | 低 | `ch395f_test_task.c` Phase 4 | 把 sock6 缓冲重排到块 24-27,与 `net_init` 固定布局中 sock4 的 24-27 **交叠**(§5.45 禁止) | ⏳ 待修 | | B9 | 低 | `ch395f_reuse_bench.c`、`net_socket.c` | bench 用 `get_phy_status()!=0` 判链路(01H=断开也成立,应 `!= 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` 释放分支 | **孤儿监听 → 端口永久拒连**:释放分支把状态置 CLOSED 等"应用重新监听",但 `ui_net_sink` 只在 `net_recv` 返 0(EOF)时才清 `g_client`,而 EOF 已被事件分支消费 → net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN,**四方互等**。对账①③都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外** | ✅ 已修:对账新增**形态④**认领孤儿(`in_use`+`state==CLOSED`+`auto_relisten`+`type==STREAM`+`local_port!=0`,芯片侧不在 LISTEN)→ `net_relisten_try`。⚠ `auto_relisten` 是决定性门:少了它出向连接会被误判孤儿 relisten 成监听器。⚠ ④ 的副作用:`net_recv` 的 EOF 是**边沿**——④抢先 relisten 会把慢消费者的 EOF 永久吞掉。**根治出口 = `net_drop_conn` + 生命周期设计**(自愈收敛阶段 3 已落):原理与分阶段方案 → `docs/子系统设计/网络Socket监听生命周期设计说明.md` | +| B10 | **中** | `net_reconcile_tick` 认领范围 + `handle_disconnect_event` 释放分支 | **孤儿监听 → 端口永久拒连**:释放分支把状态置 CLOSED 等"应用重新监听",但 `ui_net_sink` 只在 `net_recv` 返 0(EOF)时才清 `g_client`,而 EOF 已被事件分支消费 → net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN,**四方互等**。对账①③都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外** | ✅ 已修:对账新增**形态④**认领孤儿(`in_use`+`state==CLOSED`+`auto_relisten`+`type==STREAM`+`local_port!=0`,芯片侧不在 LISTEN)→ `net_relisten_try`。⚠ `auto_relisten` 是决定性门:少了它出向连接会被误判孤儿 relisten 成监听器。⚠ ④ 的副作用:`net_recv` 的 EOF 是**边沿**——④抢先 relisten 会把慢消费者的 EOF 永久吞掉。**根治出口 = `net_drop_conn` + 生命周期设计**(自愈收敛阶段 3 已落):原理与决策记录 → `docs/网络/网络自愈架构设计.md` §1.1~§1.4、§4.6、§10 | ## 台架注入陷阱(方法论,持续补充) @@ -309,7 +309,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修 # 附录 A · 驱动关键点速查(细节以对应条目为准) - **初始化顺序**:`SET_MAC → SET_IP/GWIP/MASK → INIT_CH395 → SET_PHY`(TRAP-NET-19;IP 族必须在 INIT 前——INIT 读取并锁定寄存器值;SET_PHY 在 INIT 后重建物理链路);`CMD_PING_ENABLE` 不需显式调用。 -- **SPI 通信**:每次事务 `ch395f_spi_begin()/end()` 包裹;分频 8(PCB 约束);大数据走 SPI2 DMA(`CH395F_SPI_DMA_BUF_SIZE`=2048+4=2052,`ch395f.h:148`;历史值 1500/4100 已作废)。⚠ **TSL 规格偏离(E12 登记)**:手册 §7.4 要求缓存区命令"长度与数据之间"留 TSL(读 0.4us/写 0.2us),本仓两个缓存区函数把命令头与数据打包进**同一 DMA 突发**(间隔=0)属规格偏离;**实测未发现危害**(460 轮/≈58MB 双向 SHA-256 仅 1 次未复现异常,"两段+TSL"变体与现状无差异)→ 依 TRAP-NET-20 铁律暂不改产品路径,偏离与证据登记于此、变体留作待用补丁(A/B 结论锚点 `docs/网络/网络自愈架构设计.md` §5.2 E12,过程全文见其 §9 归档)。 +- **SPI 通信**:每次事务 `ch395f_spi_begin()/end()` 包裹;分频 8(PCB 约束);大数据走 SPI2 DMA(`CH395F_SPI_DMA_BUF_SIZE`=2048+4=2052,`ch395f.h:148`;历史值 1500/4100 已作废)。⚠ **TSL 规格偏离(E12 登记)**:手册 §7.4 要求缓存区命令"长度与数据之间"留 TSL(读 0.4us/写 0.2us),本仓两个缓存区函数把命令头与数据打包进**同一 DMA 突发**(间隔=0)属规格偏离;**实测未发现危害**(460 轮/≈58MB 双向 SHA-256 仅 1 次未复现异常,"两段+TSL"变体与现状无差异)→ 依 TRAP-NET-20 铁律暂不改产品路径,偏离与证据登记于此、变体留作待用补丁(A/B 结论锚点 `docs/网络/网络自愈架构设计.md` §5.2 E12,过程全文见其 §12 归档)。 - **TCP 参数**(必须在 INIT_CH395 之前):重传 `SET_RETRAN_COUNT`(默认 12,最大 20)×`SET_RETRAN_PERIOD`(默认 500ms,最大 1000ms);KeepAlive 参数 500ms 倍数、IDLE>INTVL(TRAP-NET-06)。 - **KeepAlive 定案链**:默认关闭 → 服务器侧应在 `SINT_STAT_CONNECT` 后开启(§5.56)→ 链路层死亡实测 +2s 即被重传(§5.17 8×200ms)覆盖、与 KeepAlive 无关 → **"链路在、对端静默"场景实测判死 ≈30s**(60s/5s/3,早于手册公式 75s,以实测为准;短参数 6s/2s/2 → ≈5.1s 证明参数生效;静默但存活对端不误杀)→ **已转正**:`handle_connect_event` LISTENING 分支开启 60s/5s/3;消费链路 TIM_OUT → `handle_timeout_event` QUARANTINE → relisten → 端口恢复(方法学与数据见 `docs/网络/网络自愈架构设计.md` §4.7 与 §5.2 E11/E13)。 - **Socket 缓冲**:默认只给 Socket 0~3;Socket 4~7 必须 `open_socket` 前显式分配(TRAP-NET-02);现行固定布局 6 块/Socket。 diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 9cefea4d..4f4a0f70 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 211324 18680 1104 154232 230004 21.9 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 diff --git a/test/board/ch395f_reuse_bench.c b/test/board/ch395f_reuse_bench.c index 18c53677..c05417d2 100644 --- a/test/board/ch395f_reuse_bench.c +++ b/test/board/ch395f_reuse_bench.c @@ -19,7 +19,7 @@ * 含"命令未发时"对照读,复核 Trap 记录的 SET 类无判别力 + 陈旧 BUSY 残留); * E4 = sock_reset 内部顺序 A/B/C 变体 ×10 轮(轮间换端口)+ 立即复连载荷完整性自证 * (配套 test/host/ch395f_reuse_host.py --mode e4)。 - * 判据与结果锚点 → docs/网络/网络自愈架构设计.md §5.2 E2/E3/E4(过程全文已归档,见该文档 §9)。 + * 判据与结果锚点 → docs/网络/网络自愈架构设计.md §5.2 E2/E3/E4(过程全文已归档,见该文档 §12)。 * * close_mode:0=主机先关(芯片等 DISCONNECT);1=芯片先关;2=无数据连上即关 * verify:0=无修复;1=listen 后确认 LISTEN 重试;2=缓冲重分配; diff --git a/tools/arch_diagram/gen_net_pending_svg.py b/tools/arch_diagram/gen_net_pending_svg.py index f27215b8..329584b0 100644 --- a/tools/arch_diagram/gen_net_pending_svg.py +++ b/tools/arch_diagram/gen_net_pending_svg.py @@ -2,14 +2,14 @@ # -*- coding: utf-8 -*- """生成 docs/images/Net层阻塞模型.svg —— pending 请求模型架构图(纯标准库)。 -结构:左右两栏 + 主循环横带 + 五路判定带 - 左 = 调用方任务(net_recv → 数据已在? → 登记挂起 / 睡任务通知) +结构:左右两栏 + 主循环横带 + recv 五路 / send 四路判定带 + 左 = 调用方任务(net_recv → 数据已在? → 登记挂起 / 睡任务通知;net_send 同构) 右 = netTask 立即 SPI 读出(快速路径)+ CH395F 芯片 - 中 = netTask 主循环(~1ms 节拍:net_poll / net_pending_tick / ...) - 下 = 五路完成/取消判定 → xTaskNotify 唤醒调用方 + 中 = netTask 主循环(~1ms 节拍:net_poll / net_pending_tick / net_send_pending_tick / ...) + 下 = recv 五路 + send 四路完成/取消判定 → xTaskNotify 唤醒调用方 -目的:把"等数据"从执行者内的自旋改为执行者外的登记——登记后调用方在自己上下文 -睡眠,netTask 零占用地继续服务整个队列。 +目的:把"等数据 / 等发送缓冲"从执行者内的自旋改为执行者外的登记——登记后调用方在自己 +上下文睡眠,netTask 零占用地(send 为有界自旋 ≤ NET_SEND_SPIN_MS)继续服务整个队列。 用法(仓库根目录,任意 python3): python tools/arch_diagram/gen_net_pending_svg.py @@ -25,8 +25,8 @@ ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__) OUT = os.path.join(ROOT, "docs", "images", "Net层阻塞模型.svg") # ----------------------------- 画布与样式 ----------------------------- -W = 1440 -H = 1030 +W = 1620 +H = 1074 PAD = 40 FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" @@ -74,7 +74,8 @@ FAST_PATH = ("立即 SPI 读出", LOOP_STEPS = [ ("net_poll()", "刷新 readable", C_CHIP), - ("net_pending_tick()", "结束挂起", C_TICK), + ("net_pending_tick()", "结束挂起 recv", C_TICK), + ("net_send_pending_tick()", "推进挂起 send", C_TICK), ("net_listener_retry_tick()", "重试对账", C_ACTOR), ("排空消息队列", "服务所有消费者", "#64748b"), ] @@ -88,8 +89,8 @@ PHASES = [ ] WATCHDOG = "挂起超时留痕探针(编译期选项,默认关):挂起超阈值仍未结束 → 只 DBG_ERROR 留痕(不强制结束、不伪造返回值)" -FOOT = ("关键性质:挂起期间 netTask 零额外占用(判据复用主循环既有 net_poll 的 readable," - "不引入额外 SPI 事务);公开 API 不变,既有调用方 / 测试 / 模拟器零改动。") +FOOT = ("关键性质:挂起期间 netTask 零额外占用(recv 判据复用 net_poll 的 readable,不引入额外 SPI;" + "send 有界自旋 ≤ NET_SEND_SPIN_MS,netTask 最坏停滞由 1s 压到 2ms);公开 API 不变。") # ----------------------------- 生成 ----------------------------- @@ -132,8 +133,8 @@ def build(): % (PAD, TITLE_FONT, esc("Net 层阻塞模型 · pending 请求架构"))) add('%s' % (PAD, SUB_FONT, - esc("把「等数据」从执行者内的自旋,改为执行者外的登记 —— 调用方在自己上下文睡眠," - "netTask 零占用地服务整个队列"))) + esc("把「等数据 / 等发送缓冲」从执行者内的自旋,改为执行者外的登记 —— 调用方在自己" + "上下文睡眠,netTask 零占用地服务整个队列"))) # ---- 图例 ---- lx = PAD @@ -216,20 +217,20 @@ def build(): cy = lw_y + 42 add('' % (cx, cy, cw, color)) - add('%s' + add('%s' % (cx + 10, cy + 20, color, esc(name))) add('%s' % (cx + 10, cy + 38, esc(note))) - # ---- 五路判定带 ---- + # ---- recv 五路 / send 四路判定带 ---- ph_y = LANE_Y + LANE_H + 26 - ph_h = 234 + ph_h = 264 add('' % (PAD, ph_y, W - 2 * PAD, ph_h)) add('%s' % (PAD + 18, ph_y + 28, PANEL_TITLE_FONT, - esc("net_pending_tick():五路完成 / 取消判定(每轮对每个挂起 fd 按序)"))) + esc("挂起 recv:五路完成 / 取消判定(net_pending_tick 每轮对每个挂起 fd 按序)"))) # 主循环 → 五路 line(lw_x + lw_w / 2, lw_y + lw_h, lw_x + lw_w / 2, ph_y - 2, C_TICK, head=True) @@ -249,6 +250,13 @@ def build(): add('%s' % (PAD + 56, wd_y + 8, C_CANCEL, esc(WATCHDOG))) + # send 四路(net_send_pending_tick,二期) + sg_y = wd_y + 30 + add('%s' + % (PAD + 26, sg_y + 8, C_TICK, + esc("挂起 send 四路:①断连(以已发字节数结束)②推进(每轮至多一块 ≤ NET_SEND_CHUNK_MAX)" + "③写完(以总长度结束)④无进展超时(短写)—— net_send_pending_tick 每轮判定"))) + # xTaskNotify 回环 → 调用方睡眠盒 nb_w, nb_h = 250, 62 nb_x = W - PAD - 30 - nb_w