diff --git a/Drivers/BSP/NET/net_listener.c b/Drivers/BSP/NET/net_listener.c new file mode 100644 index 00000000..941d9b2a --- /dev/null +++ b/Drivers/BSP/NET/net_listener.c @@ -0,0 +1,554 @@ +/* + * 模块名称:Network Listener Lifecycle + * 模块功能:监听器生命周期(真机专属)—— 维持 fd 处于 LISTEN 态的全部机制: + * 重建(open+listen)、重建前的沉降/隔离(Trap 23)、排空 RX 残渣、 + * D10 非阻塞重试、周期性自愈 pump(唯一的周期性重建决策者)。 + * 适用平台:STM32F407ZGTx + CH395F 以太网芯片 + * 作者:王建锋 + * 创建日期:2026-10-12 + * 修改记录: + * 2026-10-12 王建锋 从 net_socket.c 整体抽出监听生命周期(N-32 §10 模块化) + * + * 职责与边界见 net_listener.h 头注释;设计依据见 + * docs/子系统设计/网络Socket监听生命周期设计说明.md §10。 + * + * 模块内分层: + * ① 策略常量与私有状态(s_relisten_*) + * ② 机制:net_drain_chip_rx(排空)/ net_relisten_locked(重建一次) + * ③ 对外接口:settle / try / clear / retry_tick / pump + * + * 依赖(全部为单向,无反向 include): + * net_types.h(net_sock_t)/ net_socket.h(net_get_sock / NET_MAX_SOCKETS / + * net_set_errno / NET_ERR_* / net_pending_active)/ ch395f.h(驱动)/ + * dbg_log.h / main.h(HAL_GetTick) + */ +#include +#include + +#include "FreeRTOS.h" +#include "task.h" +#include "cmsis_os.h" + +#include "net_socket.h" /* net_sock_t / net_get_sock / NET_MAX_SOCKETS / net_set_errno */ +#include "net_listener.h" +#include "ch395f.h" +#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */ +#include "net_latency_test.h" /* L3 探针接口(relisten 恢复计时采样) */ +#include "main.h" + +#include "dbg_log.h" + +/* ===== ① 策略常量与私有状态 ===== */ + +/* Trap 23(2026-09-23 生产路径实测):异常释放(芯片 TIMEOUT,例如客户端中止上传) + * 后的**隔离期**。实测时序:中止时对端仍有在途数据 → 芯片 TIMEOUT(此时 MCU 侧 recv=0) + * 在途段随后落进芯片 RX 缓冲(约 2KB 残渣) + * 若立即 open+listen,残渣被下一条连接当作自己的数据读走 + * 新上传只收 2KB 就被判定结束、回报成功(静默截断) + * 处置:异常释放的 socket 先隔离 NET_RELISTEN_QUARANTINE_MS:期间持续排空 RX、主动 close 并排空中断 + * 等芯片回到 CLOSED/TCP_CLOSED;隔离期满才 open+listen + * 上限 NET_RELISTEN_SETTLE_MS 后强制放行,保证监听器不会永久失效 + * 正常 FIN 释放(handle_disconnect_event)不走此路径——既有行为零改动 + */ +#define NET_RELISTEN_QUARANTINE_MS 400U /* 异常释放最短隔离(等在途数据落尽 + 排空残渣) */ +#define NET_RELISTEN_GRACEFUL_MS 100U /* 正常释放最短沉降(等芯片完成上一条连接的收尾) */ +#define NET_RELISTEN_SETTLE_MS 800U /* 隔离上限(须 < NET_RELISTEN_DEADLINE_MS, + * 否则隔离耗尽重试预算 → 监听被放弃,2026-09-23 实测) */ +/* 重监听前"清芯片收尾状态"的两个等级(s_relisten_quarantine 取值,取值语义见 net_listener.h) + * 2026-09-24 台架实证:正常释放走的是既有"不复位芯片收尾状态"的路径 + * 残留的 DISCONNECT 会在复用同一 fd 的下一条连接上被投递(accept 后约 10~20ms) + * 下一条上传被静默截断到 2048B(一个芯片 RX 缓冲)且回报 226 + * 只清理不等待(0ms)实测无效(4/4 仍截断):芯片需要时间完成上一条连接的内部收尾 + * 给足沉降即 4/4 通过 + */ +#define NET_RELISTEN_SETTLE_QUARANTINE 1U +#define NET_RELISTEN_SETTLE_GRACEFUL 2U + +static uint8_t s_relisten_quarantine[NET_MAX_SOCKETS]; /* 0=免清;1=异常释放;2=正常释放 */ +static uint32_t s_relisten_settle_start[NET_MAX_SOCKETS]; /* 隔离起点(0 = 未开始) */ +static uint32_t s_relisten_settle_dead[NET_MAX_SOCKETS]; /* 隔离上限到期时刻 */ + +/* ===== Trap 09 非阻塞重监听重试(D10 修复,2026-09-22 M3 实测) ===== + * 旧实现:open 失败 → close → osDelay(500) → 重试,netTask 忙等 500ms(全网冻结) + * 且实测恢复 588ms、期间客户端重连被拒 + * 新实现:首次失败即登记待重试,由 netTask 每轮 net_process_messages() 到点重试 + * 恢复时间 ≈ NET_RELISTEN_RETRY_MS 量级,任何时刻都不阻塞任务 + */ +#define NET_RELISTEN_RETRY_MS 30U /* 重试间隔(Trap 09 沉降窗口;实测 100ms 首次必成, + * 收紧到 30ms 以缩短恢复时间——未成则下一轮到点再来) */ +#define NET_RELISTEN_DEADLINE_MS 2000U /* 放弃时限(自首次尝试起) */ + +static uint8_t s_relisten_wait[NET_MAX_SOCKETS]; /* 1 = 有待重试的重监听 */ +static uint32_t s_relisten_next[NET_MAX_SOCKETS]; /* 下次重试时刻(tick) */ +static uint32_t s_relisten_dead[NET_MAX_SOCKETS]; /* 放弃时刻(tick) */ +#if defined(TEST_LATENCY_PROBE) +static lat_tmark_t s_relisten_mark[NET_MAX_SOCKETS]; /* 恢复计时起点(首次尝试) */ +#endif + +/* ===== ② 机制 ===== */ + +/* + * 函数功能:清空芯片该 socket 的 RX 残留(Trap 23) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文;socket 即将 close/open(数据已无意义,丢弃) + * 函数说明:1. 中止/异常断开时芯片 RX 缓冲可能残留上一条连接的数据(实测残留约一个缓冲 = 2048B) + * 2. 残渣带入下一条连接会使其在约 2KB 处被芯片拆链(上传被静默截断) + * 3. 关闭前主动读空即清除;分块读避免新增静态缓冲 + * (2026-10-09 实测:ARMCC 把本 scratch 与同函数内生命周期不重叠的 + * 192B 日志缓冲叠放,局部缓冲边际栈成本仅 ~16B,静态化反亏 256B BSS,勿再"优化") + */ +static void net_drain_chip_rx(int sockfd) { + /* 复用分块缓冲(256B/块,避免新增静态缓冲) */ + uint8_t scratch[256]; + + /* 上限 16×256 = 4KB(缓冲仅 2KB),防死循环 */ + for (int guard = 0; guard < 16; guard++) { + /* 查询芯片 RX 可用字节数 */ + int n = (int)ch395f_get_recv_len((uint8_t)sockfd); + /* 已排空 → 结束 */ + if (n <= 0) { + break; + } + /* 单块以缓冲容量为上限 */ + if (n > (int)sizeof(scratch)) { + n = (int)sizeof(scratch); + } + /* 丢弃该块(读即清) */ + ch395f_read_recv_buf((uint8_t)sockfd, scratch, (uint16_t)n); + } +} + +/* + * 函数功能:单连接模式断连后自动重新进入 LISTEN(不清空控制块,保留 fd) + * 入口参数:sockfd - 已断开(CLOSED)的 TCP 监听型 Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:0 - 成功,-1 - 失败 + * 限定条件:netTask 上下文;控制块 in_use == 1;保留 local_port/auto_relisten + * 函数说明:1. 复用原本地端口与协议,重新 OPEN + TCP_LISTEN + * 2. 仅将 state 置回 LISTENING,不 memset(fd 与 auto_relisten 保持不变) + * 3. 实现 BSD 风格"监听 Socket 常驻",应用层无需手动重监听 + * 4. **单次尝试**:失败立即返回(不 osDelay 忙等) + * 5. Trap 09 沉降交给 net_listener_try / net_listener_retry_tick 非阻塞重试(见下) + * 6. 旧实现在此 osDelay(500) 忙等,netTask 冻结 500ms、实测恢复 588ms(M3 台架 D10) + * 7. N-32 §10:fd 由参数传入(不再用 `p_sock - s_net_socks` 反推), + * 控制块经 net_get_sock() 取 —— 本模块**不访问** net_socket.c 的 static 数组 + */ +static int net_relisten_locked(int sockfd) { + /* 芯片命令返回状态 */ + uint8_t status; + /* Socket 控制块(fd 由调用方保证有效) */ + net_sock_t *p_sock = net_get_sock(sockfd); + + /* 控制块无效:拒绝(不应发生,调用方已判过) */ + if (p_sock == NULL) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + + /* 释放隔离期(Trap 23):重监听前先清芯片收尾状态 */ + if (s_relisten_quarantine[sockfd] != 0U) { + /* 芯片状态两字节:[0]=Socket,[1]=TCP */ + uint8_t st[2] = {0, 0}; + /* 当前 tick */ + uint32_t now = HAL_GetTick(); + /* 隔离等级(1=异常释放 2=正常释放) */ + uint8_t lvl = s_relisten_quarantine[sockfd]; + /* 最短等待由释放来源决定(NET_RELISTEN_SETTLE_*):异常释放 400ms(等在途数据落尽) + * 正常释放 100ms(等芯片内部收尾) */ + uint32_t hold = (lvl == NET_RELISTEN_SETTLE_QUARANTINE) ? NET_RELISTEN_QUARANTINE_MS + : NET_RELISTEN_GRACEFUL_MS; + /* 日志标签(台架分辨触发路径) */ + const char *tag = (lvl == NET_RELISTEN_SETTLE_QUARANTINE) ? "quarantine" : "settle"; + + /* 首次进入隔离期:记录起点与上限 */ + if (s_relisten_settle_start[sockfd] == 0U) { + /* 隔离起点 = 本 tick */ + s_relisten_settle_start[sockfd] = now; + /* 隔离上限 = 起点 + 800ms */ + s_relisten_settle_dead[sockfd] = now + NET_RELISTEN_SETTLE_MS; + } + /* 隔离期内持续排空 RX 残渣 */ + net_drain_chip_rx(sockfd); + /* 主动关:清旧连接收尾状态 */ + ch395f_close_socket((uint8_t)sockfd); + /* 读即清:本 Socket 中断状态寄存器 */ + (void)ch395f_get_sock_int_status((uint8_t)sockfd); + /* 读即清:全局中断状态寄存器 */ + (void)ch395f_get_glob_int_status_all(); + /* 读芯片当前状态(两字节) */ + ch395f_get_socket_status((uint8_t)sockfd, st); + + /* 芯片尚未回到 CLOSED/TCP_CLOSED,或未满最短等待 */ + if (((st[0] != CH395F_SOCKET_CLOSED) || (st[1] != CH395F_TCP_CLOSED) || + ((uint32_t)(now - s_relisten_settle_start[sockfd]) < hold))) { + /* 未到上限:交重试轮询(30ms 后复检) */ + if ((int32_t)(now - s_relisten_settle_dead[sockfd]) < 0) { + DBG_INFO("sock%d relisten %s (sock=0x%02X tcp=0x%02X)", + sockfd, tag, st[0], st[1]); + /* 置 BUSY(结果回传调用方) */ + (void)net_set_errno(NET_ERR_BUSY); + /* 交重试轮询(30ms 后复检) */ + return -1; + } + /* 隔离上限已至:强制放行(防监听器永久失效) */ + DBG_INFO("sock%d relisten %s timeout, force open (sock=0x%02X tcp=0x%02X)", + sockfd, tag, st[0], st[1]); + } + /* 隔离完成(或强制放行) */ + s_relisten_quarantine[sockfd] = 0U; + /* 清隔离起点(回到"未开始") */ + s_relisten_settle_start[sockfd] = 0U; + /* 清上限时刻 */ + s_relisten_settle_dead[sockfd] = 0U; + } + + /* 重置协议类型为 TCP */ + ch395f_set_proto_type((uint8_t)sockfd, CH395F_PROTO_TYPE_TCP); + /* 本地端口(沿用原监听) */ + ch395f_set_sour_port((uint8_t)sockfd, p_sock->local_port); + + /* 打开芯片侧 Socket */ + status = ch395f_open_socket((uint8_t)sockfd); + /* open 失败 → BUSY(Trap 09:芯片需沉降时间才能重开) */ + if (status != CH395F_ERR_SUCCESS) { + /* Trap 09:断开后芯片需沉降时间才能重新 open。此处只清理并返回 + * (严禁 osDelay 忙等)→ 由重试轮询到点再来。 */ + /* 先关闭,交重试轮询(30ms 后复检) */ + ch395f_close_socket((uint8_t)sockfd); + /* 置 BUSY(结果回传调用方) */ + (void)net_set_errno(NET_ERR_BUSY); + /* 交重试轮询(30ms 后复检) */ + return -1; + } + /* 下发 TCP LISTEN 命令 */ + status = ch395f_tcp_listen((uint8_t)sockfd); + /* LISTEN 失败 → BUSY(同样交重试) */ + if (status != CH395F_ERR_SUCCESS) { + /* 置 BUSY(结果回传调用方) */ + return NET_ERR_RETURN_1(NET_ERR_BUSY); + } + + /* 状态置回 LISTENING(fd 保留) */ + p_sock->state = NET_SOCK_STATE_LISTENING; + /* N-32(P1,设计文档 §5 P1.1 的关键约定):此处**刻意不清 session_dead**。 + * 重建监听只是"芯片侧恢复就位",与"应用是否已确认上一条会话结束"是两件事; + * 清了它,抢先 relisten 之后消费者就再也得不到 EOF(B10"消费者卡死"原型)。 + * session_dead 只由新 CONNECT 成功(handle_connect_event)或 memset + * (net_close/net_socket)清零。 */ + /* 新监听不得继承上一条连接的释放原因(Trap 23 待办 #3) */ + p_sock->released_abnormal = 0U; + /* N-30(Trap 25):清零前把对端端口转登为幽灵甄别基准 */ + p_sock->last_peer_port = p_sock->remote_port; + /* N-30:新监听会话,丢中断去抖计数清零 */ + p_sock->lost_conn_seen = 0U; + /* 清对端端口(新连接待用) */ + p_sock->remote_port = 0; + /* 清对端 IP */ + p_sock->remote_ip = 0; + /* 清对端 IP 数组 */ + memset(p_sock->remote_ip_arr, 0, sizeof(p_sock->remote_ip_arr)); + /* 新开 Socket 发送缓冲按占用计(由 SEND_OK 清除) */ + p_sock->send_ready = 0; + /* 清接收字节统计(新连接) */ + p_sock->recv_bytes = 0; + /* auto_relisten 保持为 1,下次断连仍自动重监听 */ + + /* 读即清:仅本 Socket 的中断状态(防上一条会话残留误触发)。 + * 原为"全 8 个 Socket + 全局"读清,与 net_listen_locked 同源(B3,机制 + * 与 git 考古见该处注释):本处是每次断连 auto-relisten 的热路径,全量清 + * 会持续吞掉其它在用 Socket 的 CONNECT/DISCONNECT 等边沿事件。全局中断 + * 寄存器交 net_poll 集中读清(AGENTS 模块约定)。 */ + (void)ch395f_get_sock_int_status((uint8_t)sockfd); + + /* 留痕:监听器重建完成 */ + DBG_INFO("sock%d auto-relisten ok", sockfd); + /* 重监听成功 */ + return 0; +} + +/* ===== ③ 对外接口 ===== */ + +/* + * 函数功能:登记「本 fd 的会话已结束,重建监听前需沉降/隔离」(契约见 net_listener.h) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * level - 沉降等级(net_listener_settle_t,由释放来源决定) + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文;由 handle_disconnect_event / handle_timeout_event 调用 + * 函数说明:1. 只置事实,不做动作 —— 重建由 net_listener_pump / net_listener_try 负责 + * 2. 越界 sockfd 静默忽略(与旧直接写 s_relisten_quarantine 行为一致) + */ +void net_listener_settle(int sockfd, net_listener_settle_t level) { + /* 描述符越界:静默忽略(与旧直接写数组行为一致,不设错误码) */ + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return; + } + /* 登记沉降/隔离等级 —— 只置事实,重建由 pump / try 负责 */ + s_relisten_quarantine[sockfd] = (uint8_t)level; +} + +/* + * 函数功能:清除本 fd 的待重监听重试状态(关闭/新建 fd 时调用) + * 入口参数:sockfd - Socket 描述符 (0~7) + * 出口参数:无 + * 返回值:无 + * 限定条件:无 + * 函数说明:1. 清"待重试"标志与重试时刻,fd 关闭/新建时调用(防残留重试) + */ +void net_listener_clear(int sockfd) { + /* 描述符越界:直接返回(本函数不设错误码) */ + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return; + } + /* 清"待重试"标志 */ + s_relisten_wait[sockfd] = 0U; + /* 清下次重试时刻 */ + s_relisten_next[sockfd] = 0U; + /* 清放弃时刻 */ + s_relisten_dead[sockfd] = 0U; + /* Trap 23:一并清沉降/隔离状态 */ + s_relisten_settle_dead[sockfd] = 0U; + /* 清沉降起点 */ + s_relisten_settle_start[sockfd] = 0U; + /* 一并清隔离标志:关闭/重建即确定性释放, + * 防同 fd 下一生成继承上轮的 400/800ms + * 隔离等待(2026-09-24 台架发现跨轮残留) */ + s_relisten_quarantine[sockfd] = 0U; +} + +/* + * 函数功能:标记重监听完成(清重试状态;探针构建下报告恢复计时样本) + * 入口参数:sockfd - Socket 描述符 (0~7) + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文 + * 函数说明:1. 重监听成功收尾:清重试状态;探针构建(TEST_LATENCY_PROBE)下上报恢复计时样本 + */ +static void net_relisten_done(int sockfd) { + /* 清重试状态(复用清除函数) */ + net_listener_clear(sockfd); +#if defined(TEST_LATENCY_PROBE) + /* 上报恢复计时样本(首次尝试 → 成功) */ + lat_probe_add_seg("relisten", lat_mark_us(&s_relisten_mark[sockfd])); +#endif +} + +/* + * 函数功能:尝试重建监听一次(**不阻塞**);失败则登记"待重试"(契约见 net_listener.h) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:0 - 成功(监听器已就绪);-1 - 本次失败或已在重试队列中 int + * 限定条件:netTask 上下文;fd 为监听型(auto_relisten) + * 函数说明:1. 已在重试队列时直接返回 -1(不重复尝试、不重置重试时刻)—— + * 否则每 10ms 一次的 accept 轮询会不断把重试时刻往后推, + * 导致永不重试(重试饥饿) + * 2. N-32 §10:替代此前散落在 net_socket.c 各处的 net_relisten_try() + */ +int net_listener_try(int sockfd) { + /* 当前 tick */ + uint32_t now = HAL_GetTick(); + + /* 描述符越界 → BADF */ + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + /* 已在重试队列:不重复尝试、不重置时刻(防重试饥饿) */ + if (s_relisten_wait[sockfd] != 0U) { + (void)net_set_errno(NET_ERR_BUSY); + /* 已在重试队列:由 tick 负责 */ + return -1; + } +#if defined(TEST_LATENCY_PROBE) + /* 恢复计时自首次尝试起 */ + lat_mark(&s_relisten_mark[sockfd]); +#endif + /* 本次尝试成功 */ + if (net_relisten_locked(sockfd) == 0) { + /* 标记完成(清状态 + 探针样本) */ + net_relisten_done(sockfd); + /* 监听器已就绪 */ + return 0; + } + /* 登记"待重试" */ + s_relisten_wait[sockfd] = 1U; + /* 下次重试时刻 = now + 30ms */ + s_relisten_next[sockfd] = now + NET_RELISTEN_RETRY_MS; + /* 放弃时刻 = now + 2s */ + s_relisten_dead[sockfd] = now + NET_RELISTEN_DEADLINE_MS; + /* 本次失败(重试交 tick) */ + return -1; +} + +/* + * 函数功能:D10 重试轮询(netTask 每轮调用一次,非阻塞) + * 入口参数:无 + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文(由 net_process_messages 每轮调用) + * 函数说明:1. 到点重试待办的重监听;成功即报告探针样本 + * 2. 本 fd 有在途挂起 recv 时跳过(net_pending_active,L3 保险: + * 重开 Socket 会摧毁在途连接) + * 3. 超过 NET_RELISTEN_DEADLINE_MS 放弃并记错误日志(防监听器永久失效) + */ +void net_listener_retry_tick(void) { + /* 当前 tick */ + uint32_t now = HAL_GetTick(); + /* Socket 循环索引 */ + int i; + + for (i = 0; i < NET_MAX_SOCKETS; i++) { + /* 本 fd 无待重试重监听:跳过 */ + if (s_relisten_wait[i] == 0U) { + continue; + } + /* 本 fd 有在途挂起 recv:重开 Socket 会摧毁在途连接(L3 保险) */ + if (net_pending_active(i) != 0U) { + continue; + } + /* 未到点(tick 差值按有符号比较,抗回绕) */ + if ((int32_t)(now - s_relisten_next[i]) < 0) { + continue; + } + /* 已到放弃时限 */ + if ((int32_t)(now - s_relisten_dead[i]) >= 0) { + /* 清重试状态 */ + net_listener_clear(i); + /* 放弃并记错误日志 */ + DBG_ERROR("sock%d relisten give up (Trap 09 not cleared)", i); + continue; + } + /* 重试成功 */ + if (net_relisten_locked(i) == 0) { + /* 标记完成(清状态 + 探针样本) */ + net_relisten_done(i); + /* 留痕:重试成功 */ + DBG_INFO("sock%d relisten retry ok", i); + } else { + /* 仍未成:置下次重试时刻 */ + s_relisten_next[i] = now + NET_RELISTEN_RETRY_MS; + } + } +} + +/* 监听器 pump 周期(对账 200ms 的 1/2:比它快、又不至于像 50ms 那样 + * 频繁 close/open 芯片 Socket 与消费者收发撞车——2026-10-12 台架实测 + * 50ms 会把 uiTask 最坏单轮从 172ms 推到 1034ms) */ +#define NET_LISTENER_PUMP_PERIOD_MS 100U +/* pump 去抖与冷却(沿用原对账③④ 的实测结论,取值不变): + * 2026-09-23 实证——无去抖时监听被反复重建直至放弃 */ +#define NET_LISTENER_STABLE_N 2U /* 连续失配次数(去抖,≈200ms) */ +#define NET_LISTENER_COOLDOWN_MS 1000U /* 同一 socket 两次动手的最小间隔(防抖成风暴) */ + +/* + * 函数功能:监听器 pump(N-32 P2「单一所有者」)—— 本层**唯一**的周期性 + * 重建监听决策者 + * 入口参数:无 + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文(由 net_process_messages 每轮调用) + * 函数说明:1. 设计意图(设计文档 §4 不变量 II):其它所有位置只**置事实** + * (session_dead / 状态迁移 / 事件派发),不**做动作**——于是 + * "谁先看到 DISCONNECT 就谁重建"这类竞争在结构上消失 + * 2. 覆盖三种情形(前两种原属对账③④,第三种为 U11 缺口): + * ① 本层 LISTENING 而芯片监听已失位 → 重建(原对账③) + * ② 本层 CLOSED(会话已结束)而芯片不在 LISTEN → 重建(原对账④) + * ③ 本层 CLOSED 而芯片**正在** LISTEN → **只做状态重同步** + * (U11):芯片监听器是健康的,重下发 open+listen 会因 USE + * 错误走进关闭分支、把正听着的 Socket 关掉(U12 同源事故); + * 只把本层状态收回 LISTENING,**不发任何芯片命令** + * 3. 去抖/冷却沿用对账的实测结论(2026-09-23 实证:无去抖时监听被 + * 反复重建直至放弃) + * 4. session_dead **不在此清零**(与 net_relisten_locked 同一契约: + * 重建监听 ≠ 应用已确认会话结束;清零只发生在 handle_connect_event) + */ +void net_listener_pump(void) { + /* 上次轮次时刻 */ + static uint32_t s_last = 0U; + /* 连续失配计数(去抖) */ + static uint8_t s_bad[NET_MAX_SOCKETS]; + /* 冷却到期时刻(防抖成风暴) */ + static uint32_t s_cool[NET_MAX_SOCKETS]; + /* 当前 tick */ + uint32_t now = HAL_GetTick(); + /* Socket 循环索引 */ + int i; + + /* 未到 pump 周期:直接返回 */ + if ((uint32_t)(now - s_last) < NET_LISTENER_PUMP_PERIOD_MS) { + return; + } + s_last = now; + + for (i = 0; i < NET_MAX_SOCKETS; i++) { + /* 取控制块 */ + net_sock_t *p_sock = net_get_sock(i); + /* 芯片侧状态两字节 */ + uint8_t st[2] = {0, 0}; + /* 芯片当前是否在 LISTEN */ + int chip_listening; + + /* 控制块无效:跳过 */ + if (p_sock == NULL) { + s_bad[i] = 0U; + continue; + } + + /* 认领门槛:仅监听型 fd(在用 + 自动重监听 + TCP + 已绑端口)。 + * auto_relisten 是决定性门:少了它,"本层 CLOSED + 已绑端口"的出向 + * 连接(板级 Phase 7 TC701~703 用 net_connect 绑本地口)会被误判为 + * 监听器并被 relisten(同对账④的既定口径) */ + if ((p_sock->in_use == 0U) || + (p_sock->auto_relisten == 0U) || + (p_sock->type != NET_SOCK_STREAM) || + (p_sock->local_port == 0U)) { + s_bad[i] = 0U; + continue; + } + /* 只处理与监听生命周期相关的两个状态 */ + if ((p_sock->state != NET_SOCK_STATE_LISTENING) && + (p_sock->state != NET_SOCK_STATE_CLOSED)) { + s_bad[i] = 0U; + continue; + } + + /* 回读芯片状态(每监听型 fd 一条命令) */ + ch395f_get_socket_status((uint8_t)i, st); + chip_listening = ((st[0] != CH395F_SOCKET_CLOSED) && + (st[1] == CH395F_TCP_LISTEN)); + + /* 芯片在听 */ + if (chip_listening) { + s_bad[i] = 0U; + /* 本层 CLOSED 而芯片在听 = U11(本层状态滞后)→ 只重同步 */ + if (p_sock->state == NET_SOCK_STATE_CLOSED) { + DBG_INFO("sock%d pump: chip listening, resync CLOSED->LISTENING (U11)", i); + /* 只改本层状态,**不发任何芯片命令**(同 net_resync_listen 的 + * 既定纪律,理由见该函数注释)。session_dead 不清 */ + p_sock->state = NET_SOCK_STATE_LISTENING; + /* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 去抖清零 + * —— 芯片监听器未被重开,芯片侧对端信息可能仍是上一条连接的, + * 必须转登为基准,否则对账② 的幽灵拦截会失效(同 net_resync_listen) */ + p_sock->last_peer_port = p_sock->remote_port; + p_sock->lost_conn_seen = 0U; + } + continue; + } + + /* 芯片不在听:情形①②同一处置——重建(本层唯一重建入口) */ + if (s_bad[i] < NET_LISTENER_STABLE_N) { + s_bad[i]++; + } + /* 去抖未满或处于冷却期:本轮不动手 */ + if ((s_bad[i] < NET_LISTENER_STABLE_N) || + ((int32_t)(now - s_cool[i]) < 0)) { + continue; + } + s_bad[i] = 0U; + s_cool[i] = now + NET_LISTENER_COOLDOWN_MS; + DBG_INFO("sock%d pump: chip not listening (0x%02X/0x%02X, was state=%d) -> relisten", + i, st[0], st[1], (int)p_sock->state); + /* 非阻塞:失败由 D10 重试轮询接管 */ + (void)net_listener_try(i); + } +} diff --git a/Drivers/BSP/NET/net_listener.h b/Drivers/BSP/NET/net_listener.h new file mode 100644 index 00000000..597cd352 --- /dev/null +++ b/Drivers/BSP/NET/net_listener.h @@ -0,0 +1,103 @@ +/* + * 模块名称:Network Listener Lifecycle (Header) + * 模块功能:监听器生命周期模块的对外接口(**真机专属**)—— 维持 fd 处于 + * LISTEN 态的全部机制:重建、重建前的沉降/隔离(Trap 23)、排空 RX + * 残渣、D10 非阻塞重试、周期性自愈 pump。 + * 适用平台:STM32F407ZGTx + CH395F 以太网芯片 + * 作者:王建锋 + * 创建日期:2026-10-12 + * 修改记录: + * 2026-10-12 王建锋 从 net_socket.c 抽出监听生命周期(N-32 §10 模块化) + * + * 职责边界(详见 docs/子系统设计/网络Socket监听生命周期设计说明.md §10): + * 本模块只负责「让一个监听型 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_socket.c include**(内部头),不进公开 net_socket.h —— + * 故模拟器(simulator/sim/net_socket_sim.c)对本模块**零依赖**,CMakeLists 不编本文件。 + */ +#ifndef NET_LISTENER_H +#define NET_LISTENER_H + +#include "net_types.h" + +/* + * 沉降/隔离等级:重建监听前"清芯片收尾状态"的强度 + * (替代此前外部直接写 s_relisten_quarantine[]) + * QUARANTINE - 异常释放(芯片 TIMEOUT / 对端 RST):对端可能仍有在途数据, + * 须排空残渣 + 最短等 400 ms + * GRACEFUL - 正常释放(芯片 DISCONNECT / 对端 FIN):无在途数据, + * 清理 + 最短等 100 ms(等芯片完成上一条连接的内部收尾) + */ +typedef enum { + NET_LISTENER_SETTLE_QUARANTINE = 1, /* 异常释放:隔离 400 ms + 排空 RX 残渣 */ + NET_LISTENER_SETTLE_GRACEFUL = 2, /* 正常释放:沉降 100 ms */ +} net_listener_settle_t; + +/* + * 函数功能:登记「本 fd 的会话已结束,重建监听前需沉降/隔离」 + * 入口参数:sockfd - Socket 描述符 (0~7) int + * level - 沉降等级(释放来源决定,见 net_listener_settle_t) + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文;由 handle_disconnect_event / handle_timeout_event 调用 + * 函数说明:1. 只置事实,不做动作 —— 重建由 net_listener_pump / net_listener_try 负责 + * 2. 越界 sockfd 静默忽略(不设错误码,与旧直接写数组行为一致) + */ +void net_listener_settle(int sockfd, net_listener_settle_t level); + +/* + * 函数功能:尝试重建监听一次(**不阻塞**);失败则登记"待重试" + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:0 - 成功(监听器已就绪);-1 - 本次失败或已在重试队列中 int + * 限定条件:netTask 上下文;fd 为监听型(auto_relisten) + * 函数说明:1. 已在重试队列时直接返回 -1(不重复尝试、不重置重试时刻)—— + * 否则每 10 ms 一次的 accept 轮询会不断把重试时刻往后推, + * 导致永不重试(重试饥饿) + * 2. 替代此前散落在 net_socket.c 各处的 net_relisten_try() + */ +int net_listener_try(int sockfd); + +/* + * 函数功能:清除本 fd 的待重试/沉降状态(关闭或新建 fd 时调用) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:无 + * 限定条件:无(越界静默返回) + * 函数说明:防同 fd 下一生成继承上轮的重试队列或 400/800 ms 隔离等待 + */ +void net_listener_clear(int sockfd); + +/* + * 函数功能:D10 重试轮询(netTask 每轮调用,非阻塞) + * 入口参数:无 + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文(由 net_process_messages 每轮调用) + * 函数说明:到点重试待办的重监听;成功即报告探针样本;超时放弃并记错误日志 + */ +void net_listener_retry_tick(void); + +/* + * 函数功能:监听器 pump —— 本层**唯一**的周期性重建决策者 + * 入口参数:无 + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文(由 net_process_messages 每轮调用) + * 函数说明:1. 覆盖三种情形:① LISTENING 而芯片失位 → 重建 + * ② CLOSED 而芯片不在 LISTEN → 重建 + * ③ CLOSED 而芯片**正在** LISTEN → 只做状态重同步(U11,不发芯片命令) + * 2. 100 ms 周期 + 去抖 + 冷却(详见实现处注释) + */ +void net_listener_pump(void); + +/* 注:本模块消费的宿主能力 `net_pending_active()` 声明在 net_socket.h + * (它是 net_socket.c 的函数,须与配对头文件同处,见规范 S17 2.2)。 */ + +#endif /* NET_LISTENER_H */ diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index ff19f383..b6a6858a 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -18,6 +18,7 @@ #include "cmsis_os.h" #include "net_socket.h" +#include "net_listener.h" #include "ch395f.h" #include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */ #include "net_latency_test.h" /* L3 探针接口(chunk_wait 采样) */ @@ -140,16 +141,15 @@ static uint32_t net_errno_slot(void) { * 出口参数:无 * 返回值:err 原值(便于写成 `return net_set_errno(NET_ERR_XXX);`) * 限定条件:无 - * 函数说明:只写本任务的槽位,其他任务读不到、也改不了 → 消除跨任务覆写竞态 + * 函数说明:1. 只写本任务的槽位,其他任务读不到、也改不了 → 消除跨任务覆写竞态 + * 2. N-32 §10 模块化:由 static 提升为**模块间可见**(net_listener.c + * 也需要设 errno),声明与 NET_ERR_RETURN_1 宏见 net_socket.h */ -static int net_set_errno(int err) { +int net_set_errno(int err) { s_net_errno_slots[net_errno_slot()].err = err; return err; } -/* 「记本任务错误码 + 统一失败返回 -1」的紧凑写法,取代原 `s_net_errno = X; return -1;` */ -#define NET_ERR_RETURN_1(err) (net_set_errno(err), -1) - /* @@ -168,11 +168,6 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len); 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); -static int net_relisten_locked(net_sock_t *p_sock); -/* D10 非阻塞重监听:单次尝试 + 延迟重试(实现见 net_relisten_locked 之后) */ -static void net_relisten_clear(int sockfd); -static int net_relisten_try(net_sock_t *p_sock); -static void net_relisten_retry_tick(void); static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen); static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int addrlen); @@ -535,11 +530,12 @@ int net_poll(void) { * 函数说明:1. 芯片在快速复连场景会丢 CONNECT/DISCONNECT 中断;丢事件后本层无法靠事件自愈 * 2. 典型:对端连接占着单连接监听的"后备"(芯片 TCP 仍 ESTABLISHED),后续 SYN 全被拒 * 3. 应用因未收到 DISCONNECT 仍以为有客户端,永不调用 accept,事件驱动自愈无路可走 - * 4. 故周期回读芯片状态,修正三类失同步: - * 5. ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理(应用读到 EOF 后自动重监听) + * 4. 故周期回读芯片状态,修正**两类**失同步: + * 5. ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理(只置事实;重建交 pump) * 6. ② 本层 LISTENING 而芯片已完成握手 → 补做 standalone accept(复用 handle_connect_event) - * 7. ③ 本层 LISTENING 而芯片监听已消失 → 重建监听(net_relisten_try) - * 7b.④ 本层 CLOSED 但 fd 在用且已绑端口(孤儿监听)→ 重建监听(B10) + * 7. ⚠ N-32 P2(2026-10-12):原③(本层 LISTENING 丢监听)与④(孤儿监听) + * 已**整体移交** `net_listener_pump` —— 本函数内不再有任何重建动作 + * (单一所有者);认领门槛相应收窄为 ESTABLISHED/LISTENING * 8. 单次状态读可能落在"刚 open/listen 尚未生效"的瞬态 * 9. 同一失配需连续 NET_RECONCILE_STABLE_N 次才动手(2026-09-23 实测:无去抖时监听被反复重建直至放弃) * 10. 同一 socket 动手后冷却 NET_RECONCILE_COOLDOWN_MS,防去抖成风暴 @@ -576,23 +572,13 @@ static void net_reconcile_tick(void) { /* 失同步类型:1=连接已失 2=握手已成丢中断 3=监听已失 */ int bad = 0; - /* 对账认领范围:ESTABLISHED / LISTENING,外加"孤儿监听"(④)。 - * 孤儿 = fd 在用、**监听型**(auto_relisten,仅 net_listen_locked:1135 置位) - * 且已绑端口的 Socket 停在 CLOSED:断开事件已被 handle_disconnect_event - * 消费(置 CLOSED、不再向应用上报 EOF),应用那边还在等 net_recv 的 0 - * 才肯重建 —— 两边都不动手,芯片也不会自己回 LISTEN,端口就此永久拒连 - * (B10,2026-10-12 实测 :7003 芯片侧 0x05/0x00 = Socket 开着但不在 - * LISTEN,30s 不自恢复)。 - * ⚠ auto_relisten 是这里的**决定性门**:少了它,"本层 CLOSED + 已绑端口" - * 的出向连接(板级 Phase 7 TC701~703 用 net_connect 绑本地口)会被误判 - * 孤儿并被 relisten 成监听器 */ + /* 对账认领范围:**仅 ESTABLISHED / LISTENING**。 + * ⚠ 原先还认领"孤儿监听"(CLOSED + auto_relisten + 已绑端口)以驱动对账④; + * N-32 P2 把③④整体移交给 net_listener_pump(唯一重建入口),本函数只剩 + * ①②——两者都以 ESTABLISHED/LISTENING 为前提,故认领门槛相应收窄。 */ if ((p_sock->in_use == 0U) || ((p_sock->state != NET_SOCK_STATE_ESTABLISHED) && - (p_sock->state != NET_SOCK_STATE_LISTENING) && - !((p_sock->state == NET_SOCK_STATE_CLOSED) && - (p_sock->auto_relisten != 0U) && - (p_sock->type == NET_SOCK_STREAM) && - (p_sock->local_port != 0U)))) { + (p_sock->state != NET_SOCK_STATE_LISTENING))) { s_bad[i] = 0U; continue; } @@ -616,17 +602,9 @@ static void net_reconcile_tick(void) { if (!((peer != 0U) && (peer == p_sock->last_peer_port))) { bad = 2; } - } else if ((p_sock->state == NET_SOCK_STATE_LISTENING) && - ((st[0] == CH395F_SOCKET_CLOSED) || (st[1] != CH395F_TCP_LISTEN))) { - /* ③ 本层 LISTENING 而芯片监听已消失 → 重建监听 */ - bad = 3; - } else if ((p_sock->state == NET_SOCK_STATE_CLOSED) && - ((st[0] == CH395F_SOCKET_CLOSED) || (st[1] != CH395F_TCP_LISTEN))) { - /* ④ 孤儿监听:本层 CLOSED + fd 在用 + **监听型** + 已绑端口,芯片侧不在 - * LISTEN(监听型这一条由上方认领门槛保证)→ 与③同一处置,但触发源不同 - * (③是监听态丢监听,④是压根没人重建) */ - bad = 4; } + /* ③④(本层 LISTENING 丢监听 / 本层 CLOSED 孤儿监听)已移交 + * net_listener_pump —— N-32 P2 单一所有者,本函数不再做重建动作 */ /* 无失同步:清零计数,转下一 socket */ if (bad == 0) { @@ -647,31 +625,22 @@ static void net_reconcile_tick(void) { s_cool[i] = now + NET_RECONCILE_COOLDOWN_MS; if (bad == 1) { - /* 类型①:按断开处理(应用读到 EOF 后自动重监听) */ + /* 类型①:按断开处理(应用读到 EOF 后由 pump 重建监听) */ DBG_INFO("sock%d reconcile: chip closed (sock=0x%02X tcp=0x%02X) -> disconnect", i, st[0], st[1]); - /* 复用断开状态机(含陈旧事件甄别 + 状态迁移 + 事件派发) */ + /* 复用断开状态机(含陈旧事件甄别 + 状态迁移 + 事件派发); + * 只置事实,重建由 net_listener_pump 统一做(N-32 P2) */ handle_disconnect_event(p_sock); - } else if (bad == 2) { + } else { /* 类型②:补做 standalone accept */ DBG_INFO("sock%d reconcile: connection completed w/o CONNECT irq -> accept", i); /* 复用 CONNECT 事件的簿记路径 */ handle_connect_event(p_sock); - } else if (bad == 3) { - /* 类型③:重建监听(单次尝试,失败进 D10 重试队列) */ - DBG_INFO("sock%d reconcile: listen lost (sock=0x%02X tcp=0x%02X) -> relisten", - i, st[0], st[1]); - (void)net_relisten_try(p_sock); - } else { - /* 类型④:孤儿监听 —— 补一次重建(B10)。net_relisten_try 自带 - * "已在重试队列则不重复登记"保护,与 D10 队列 / Trap 23 沉降隔离兼容 */ - DBG_INFO("sock%d reconcile: orphan listener (chip=0x%02X/0x%02X) -> relisten", - i, st[0], st[1]); - (void)net_relisten_try(p_sock); } } } + /* * 函数功能:netTask 主循环核心 —— 自愈 tick 宿主 + 消息队列分派器。每轮 * 先跑三个非阻塞自愈 tick(D10 重监听 / L3 挂起 recv / @@ -706,11 +675,14 @@ void net_process_messages(void) { /* 三个非阻塞自愈 tick 置于消息处理之前:断连后即使无应用消息,监听器也能自主恢复 */ /* D10 重监听重试(Trap 09):沉降后重开芯片监听器 */ - net_relisten_retry_tick(); + net_listener_retry_tick(); /* L3 挂起 recv 完成检查:数据到达 / 到点 / 断连时通知调用方 */ net_pending_tick(); - /* Trap 22 状态对账:周期回读芯片状态,自愈丢失的中断 */ + /* Trap 22 状态对账:周期回读芯片状态,自愈丢失的中断(①②;③④ 已归 pump) */ net_reconcile_tick(); + /* N-32 P2:监听器 pump —— 本层唯一重建入口(置于对账之后:对账①② + * 可能刚把会话置 CLOSED,pump 紧接着就能看到并重建) */ + net_listener_pump(); /* 队列排空:超时 0 非阻塞取,每条处理一轮,直到本轮队列为空 */ while (osMessageQueueGet(netMsgQueueHandle, &msg, NULL, 0) == osOK) { @@ -824,16 +796,17 @@ void net_process_messages(void) { break; } case NET_MSG_RELISTEN: { - /* 取控制块;NULL = fd 越界或已关闭 */ - net_sock_t *p_sock = net_get_sock(msg.sockfd); - if (p_sock == NULL) { + /* 描述符有效性检查(NULL = fd 越界或已关闭)→ 无效即 BADF; + * 有效则交 net_listener_try(它内部自行取控制块,本处不再持有指针, + * N-32 §10 入口收敛) */ + if (net_get_sock(msg.sockfd) == NULL) { /* 描述符无效 → BADF,结果 -1 */ net_set_errno(NET_ERR_BADF); msg.result = -1; } else { /* 无条件芯片侧重监听:单次尝试,失败登记非阻塞重试 * (芯片已在监听 → 下一轮到点再来;两种陈旧场景都收敛到"芯片监听器在位") */ - msg.result = net_relisten_try(p_sock); + msg.result = net_listener_try(msg.sockfd); } break; } @@ -959,8 +932,11 @@ int net_socket(int domain, int type) { p_sock->readable = 0; /* 新 fd 不得继承上一条连接的释放原因(Trap 23 待办 #3) */ p_sock->released_abnormal = 0; + /* N-32(P1):新 fd 不得继承"上一条会话已结束"电平(否则新建即 EOF, + * 消费侧连数据都收不到)——与 released_abnormal 同理,显式清零 */ + p_sock->session_dead = 0; /* 新 fd 不得继承上一次的待重试状态(D10) */ - net_relisten_clear(sockfd); + net_listener_clear(sockfd); /* 新 fd 不得继承上一次挂起的 recv 请求(L3) */ net_pending_clear(sockfd); @@ -1341,9 +1317,12 @@ static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen DBG_INFO("sock%d closed-with-data accept (drain pending)", sockfd); } else { /* 连接已断开(CLOSED)且无残留数据:先把 Socket 重新置回 LISTEN, - * 下一次 accept 即可等待新连接(BSD 风格监听常驻) */ - /* 非阻塞:失败由 D10 重试轮询接管 */ - (void)net_relisten_try(p_listen_sock); + * 下一次 accept 即可等待新连接(BSD 风格监听常驻)。 + * N-32 P2 修正:**保留本处即时重建** —— 它由消费者调 accept 触发 + * (消费者正在等新连接),不是周期性抢先;删掉会把 CLOSED 窗口 + * 拉长、导致调用方高频空转 accept(实测 [WD] task stall)。 + * 非阻塞:失败由 D10 重试轮询接管 */ + (void)net_listener_try(sockfd); } } if (drain_pending == 0) { @@ -1453,6 +1432,60 @@ int net_can_accept(int sockfd) { return (p_sock->state == NET_SOCK_STATE_ESTABLISHED) ? 1 : 0; } +/* + * 函数功能:查询监听 Socket 是否处于"应该尝试 accept"的就绪态(纯 RAM 读,不发起 RPC) + * 入口参数:sockfd - 监听 Socket 描述符 int + * 出口参数:无 + * 返回值:1 - 就绪(LISTENING,或 CLOSED + auto_relisten = 断连待重建态); + * 0 - 未就绪(fd 未分配 / 非监听型 / 正处隔离期以外的不就绪态) int + * 限定条件:任意任务上下文;net_init() 已调用 + * 函数说明:1. N-32(P1 不变量 II):取代 net_can_accept()(要求 ESTABLISHED)作为 + * ui_net_sink 的 accept 门控。原门控下 :7003 永不进 net_accept → + * net_lost_connect_probe(丢 CONNECT 中断补 accept)对该端口从不运行 + * (设计文档 §2.3 末"探针失效") + * 2. 为什么放行 LISTENING:探针与"closed-but-readable 接受"(:1338) + * 只在 net_accept_locked 内部发生,门控挡在外面就永远到不了; + * LISTENING 态 accept 由探测补 accept 或返 WOULDBLOCK,均安全 + * 3. 为什么放行 CLOSED + auto_relisten:单连接模型断连后 fd 停在 + * CLOSED 等 relisten(D10 非阻塞重试 / 对账④),此窗口内新 SYN + * 可能已被芯片接住(对账②)→ 放行让 net_accept_locked 的状态门 + * 自行裁决(含 RX 有数据的 drain_pending 接受) + * 4. 只读控制块,不入队、不等通知 → uiTask 不被 netTask 拖住 + * 5. 与 net_can_accept 语义差异是**有意的**:本函数表达"这个监听器 + * 还在服役",而非"这一轮一定有连接" + */ +int net_listener_armed(int sockfd) { + /* Socket 控制块(纯 RAM 读,可能为 NULL = 描述符越界或未分配) */ + const net_sock_t *p_sock = net_get_sock(sockfd); + + if (p_sock == NULL) { + return 0; + } + /* 正常监听态:放行。net_lost_connect_probe(丢 CONNECT 补 accept)与常规 + * accept 都发生在 net_accept_locked 内部,门控挡在外面就永远到不了 */ + if (p_sock->state == NET_SOCK_STATE_LISTENING) { + return 1; + } + /* 断连待重建态:**仅当芯片 RX 可能仍有残留数据**时放行,让 + * net_accept_locked 的 "closed-but-readable" 接受(N-30,对端"发完即 FIN") + * 仍能拿到该连接。 + * ⚠ 无残留时必须返回 0 —— 否则调用方会在整个 CLOSED 窗口内每轮都发起 + * net_accept(RPC:入队 + xTaskNotifyWait 回执**无上界**)→ 队列积压 → + * 调用方任务喂不上狗。2026-10-12 P2a 台架实测:删掉即时 relisten 后 CLOSED + * 窗口被拉长到 pump 周期 + 隔离期(~100ms),:21 0ms×50 期间出现 48 条 + * `[WD] task stall`(mask=0x67/req=0x77,缺 bit4=UI)。 + * 重建本身由 net_listener_pump 负责,调用方**无需**在此催促。 + * readable 由 net_poll 在 RECV_OK 置位,"对端发完即 FIN"场景下 CLOSED 之后 + * 仍为 1(无人清除),可作"可能仍有残留"的纯 RAM 判据(不发起 SPI)。 */ + if ((p_sock->state == NET_SOCK_STATE_CLOSED) && + (p_sock->auto_relisten != 0U) && + (p_sock->local_port != 0U) && + (p_sock->readable != 0U)) { + return 1; + } + return 0; +} + /* * 函数功能:查询 Socket 是否"值得发一次 recv"(纯 RAM 读,不发起 RPC) * 入口参数:sockfd - Socket 描述符 int @@ -1682,6 +1715,10 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a if ((status != CH395F_ERR_SUCCESS) && (status != CH395F_ERR_BUSY)) { DBG_ERROR("[CONN] sock%d TCP_CONNECT rejected by chip: 0x%02X", sockfd, status); (void)ch395f_close_socket((uint8_t)sockfd); + /* N-32(P1 不变量 I):命令级失败同样是"本会话已结束"——此前这里只置 + * CLOSED,而 CONNECTING 态从未置 session_dead,挂起的 recv 只能干等 + * deadline(B2 审计发现的"停在 CONNECTING 干等 30s 超时才暴露"同源) */ + p_sock->session_dead = 1U; p_sock->state = NET_SOCK_STATE_CLOSED; return NET_ERR_RETURN_1(NET_ERR_INVAL); } @@ -1876,12 +1913,27 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, if (flags & NET_MSG_DONTWAIT) { /* 非阻塞且当前无数据:若连接已断开则视为 EOF(0) 并自动重监听, * 否则返回 WOULDBLOCK(-1) 表示暂时无数据 */ - /* 连接已断开(CLOSED):按 EOF 处理 */ - if (p_sock->state == NET_SOCK_STATE_CLOSED) { - /* 启用自动重监听:先把监听器重新置回 LISTEN */ - if (p_sock->auto_relisten) { + /* 连接已断开(CLOSED)**或**本会话已结束(session_dead):按 EOF 处理。 + * 判据从纯 `state == CLOSED` 放宽为 `|| session_dead`(N-32 P1 不变量 I): + * 抢先 relisten 后 state 已是 LISTENING 而会话确实已结束,旧判据在此 + * 恒返 WOULDBLOCK → EOF 永久失踪(新连接被旧会话静默吸收)。 + * 有数据时走上方快路径(recv_len>0 优先),本判据只在"无数据"时生效, + * 故不会把新连接的起始数据误判为 EOF。 */ + if ((p_sock->state == NET_SOCK_STATE_CLOSED) || (p_sock->session_dead != 0U)) { + /* 启用自动重监听:先把监听器重新置回 LISTEN。 + * ⚠ 内层 `state == CLOSED` 门是**必需的**,不是冗余: + * `net_relisten_try` 无状态门,对已 LISTENING 的 fd 再 open+listen + * 会让 ch395f_open_socket 返 USE,其失败分支会 ch395f_close_socket + * —— 把正听着新 SYN 的 Socket 当场关掉(U12 同源事故)。 + * session_dead + 已回 LISTENING = 重建已由别处完成,这里只报 EOF。 + * N-32 P2 修正(2026-10-12 台架实证):**保留本处即时重建**。 + * 它是"消费者动作触发"(消费者正在等这条连接的结局),不是周期性 + * "抢先";删掉它会把 CLOSED 窗口从 ~0 拉长到 pump 周期+隔离期 + * (~100ms),期间调用方高频发起 net_accept(RPC 无上界)→ + * `[WD] task stall`(实测 uis 往返 1063ms)。周期性抢先由 pump 收敛即可。 */ + if ((p_sock->state == NET_SOCK_STATE_CLOSED) && (p_sock->auto_relisten != 0U)) { /* 非阻塞:失败由 D10 重试轮询接管 */ - (void)net_relisten_try(p_sock); + (void)net_listener_try(sockfd); } /* EOF:返回 0(非错误) */ return 0; @@ -1906,360 +1958,6 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, * 函数说明:1. 调用 ch395f_close_socket 关闭 CH395F 硬件 Socket * 2. 清空整个控制块(memset 0),释放该 Socket 资源 */ -/* Trap 23(2026-09-23 生产路径实测):异常释放(芯片 TIMEOUT,例如客户端中止上传) - * 后的**隔离期**。实测时序:中止时对端仍有在途数据 → 芯片 TIMEOUT(此时 MCU 侧 recv=0) - * 在途段随后落进芯片 RX 缓冲(约 2KB 残渣) - * 若立即 open+listen,残渣被下一条连接当作自己的数据读走 - * 新上传只收 2KB 就被判定结束、回报成功(静默截断) - * 处置:异常释放的 socket 先隔离 NET_RELISTEN_QUARANTINE_MS:期间持续排空 RX、主动 close 并排空中断 - * 等芯片回到 CLOSED/TCP_CLOSED;隔离期满才 open+listen - * 上限 NET_RELISTEN_SETTLE_MS 后强制放行,保证监听器不会永久失效 - * 正常 FIN 释放(handle_disconnect_event)不走此路径——既有行为零改动 - * 声明须早于 net_relisten_locked(其余重试状态见其后区块) - */ -#define NET_RELISTEN_QUARANTINE_MS 400U /* 异常释放最短隔离(等在途数据落尽 + 排空残渣) */ -#define NET_RELISTEN_GRACEFUL_MS 100U /* 正常释放最短沉降(等芯片完成上一条连接的收尾) */ -#define NET_RELISTEN_SETTLE_MS 800U /* 隔离上限(须 < NET_RELISTEN_DEADLINE_MS, - * 否则隔离耗尽重试预算 → 监听被放弃,2026-09-23 实测) */ -/* 重监听前"清芯片收尾状态"的两个等级(s_relisten_quarantine 取值) - * QUARANTINE - 异常释放(芯片 TIMEOUT):对端可能仍有在途数据,须清理 + 最短等 400ms - * GRACEFUL - 正常释放(芯片 DISCONNECT):无在途数据,清理 + 最短等 100ms - * 2026-09-24 台架实证:正常释放走的是既有"不复位芯片收尾状态"的路径 - * 残留的 DISCONNECT 会在复用同一 fd 的下一条连接上被投递(accept 后约 10~20ms) - * 下一条上传被静默截断到 2048B(一个芯片 RX 缓冲)且回报 226 - * 只清理不等待(0ms)实测无效(4/4 仍截断):芯片需要时间完成上一条连接的内部收尾 - * 给足沉降即 4/4 通过 - */ -#define NET_RELISTEN_SETTLE_QUARANTINE 1U -#define NET_RELISTEN_SETTLE_GRACEFUL 2U -static uint8_t s_relisten_quarantine[NET_MAX_SOCKETS]; /* 0=免清;1=异常释放;2=正常释放 */ -static uint32_t s_relisten_settle_start[NET_MAX_SOCKETS]; /* 隔离起点(0 = 未开始) */ -static uint32_t s_relisten_settle_dead[NET_MAX_SOCKETS]; /* 隔离上限到期时刻 */ - -/* - * 函数功能:清空芯片该 socket 的 RX 残留(Trap 23) - * 入口参数:sockfd - Socket 描述符 (0~7) int - * 出口参数:无 - * 返回值:无 - * 限定条件:netTask 上下文;socket 即将 close/open(数据已无意义,丢弃) - * 函数说明:1. 中止/异常断开时芯片 RX 缓冲可能残留上一条连接的数据(实测残留约一个缓冲 = 2048B) - * 2. 残渣带入下一条连接会使其在约 2KB 处被芯片拆链(上传被静默截断) - * 3. 关闭前主动读空即清除;分块读避免新增静态缓冲 - * (2026-10-09 实测:ARMCC 把本 scratch 与同函数内生命周期不重叠的 - * 192B 日志缓冲叠放,局部缓冲边际栈成本仅 ~16B,静态化反亏 256B BSS,勿再"优化") - */ -static void net_drain_chip_rx(int sockfd) { - /* 复用分块缓冲(256B/块,避免新增静态缓冲) */ - uint8_t scratch[256]; - - /* 上限 16×256 = 4KB(缓冲仅 2KB),防死循环 */ - for (int guard = 0; guard < 16; guard++) { - /* 查询芯片 RX 可用字节数 */ - int n = (int)ch395f_get_recv_len((uint8_t)sockfd); - /* 已排空 → 结束 */ - if (n <= 0) { - break; - } - /* 单块以缓冲容量为上限 */ - if (n > (int)sizeof(scratch)) { - n = (int)sizeof(scratch); - } - /* 丢弃该块(读即清) */ - ch395f_read_recv_buf((uint8_t)sockfd, scratch, (uint16_t)n); - } -} - -/* - * 函数功能:单连接模式断连后自动重新进入 LISTEN(不清空控制块,保留 fd) - * 入口参数:p_sock - 已断开(CLOSED)的 TCP 监听型 Socket 控制块 net_sock_t * - * 出口参数:无 - * 返回值:0 - 成功,-1 - 失败 - * 限定条件:netTask 上下文;p_sock->in_use == 1;保留 local_port/auto_relisten - * 函数说明:1. 复用原本地端口与协议,重新 OPEN + TCP_LISTEN - * 2. 仅将 state 置回 LISTENING,不 memset(fd 与 auto_relisten 保持不变) - * 3. 实现 BSD 风格"监听 Socket 常驻",应用层无需手动重监听 - * 4. **单次尝试**:失败立即返回(不 osDelay 忙等) - * 5. Trap 09 沉降交给 net_relisten_try / net_relisten_retry_tick 非阻塞重试(见下) - * 6. 旧实现在此 osDelay(500) 忙等,netTask 冻结 500ms、实测恢复 588ms(M3 台架 D10) - */ -static int net_relisten_locked(net_sock_t *p_sock) { - /* 芯片命令返回状态 */ - uint8_t status; - /* 由控制块指针反推描述符 */ - int sockfd = (int)(p_sock - s_net_socks); - - /* 释放隔离期(Trap 23):重监听前先清芯片收尾状态 */ - if (s_relisten_quarantine[sockfd] != 0U) { - /* 芯片状态两字节:[0]=Socket,[1]=TCP */ - uint8_t st[2] = {0, 0}; - /* 当前 tick */ - uint32_t now = HAL_GetTick(); - /* 隔离等级(1=异常释放 2=正常释放) */ - uint8_t lvl = s_relisten_quarantine[sockfd]; - /* 最短等待由释放来源决定(NET_RELISTEN_SETTLE_*):异常释放 400ms(等在途数据落尽) - * 正常释放 100ms(等芯片内部收尾) */ - uint32_t hold = (lvl == NET_RELISTEN_SETTLE_QUARANTINE) ? NET_RELISTEN_QUARANTINE_MS - : NET_RELISTEN_GRACEFUL_MS; - /* 日志标签(台架分辨触发路径) */ - const char *tag = (lvl == NET_RELISTEN_SETTLE_QUARANTINE) ? "quarantine" : "settle"; - - /* 首次进入隔离期:记录起点与上限 */ - if (s_relisten_settle_start[sockfd] == 0U) { - /* 隔离起点 = 本 tick */ - s_relisten_settle_start[sockfd] = now; - /* 隔离上限 = 起点 + 800ms */ - s_relisten_settle_dead[sockfd] = now + NET_RELISTEN_SETTLE_MS; - } - /* 隔离期内持续排空 RX 残渣 */ - net_drain_chip_rx(sockfd); - /* 主动关:清旧连接收尾状态 */ - ch395f_close_socket((uint8_t)sockfd); - /* 读即清:本 Socket 中断状态寄存器 */ - (void)ch395f_get_sock_int_status((uint8_t)sockfd); - /* 读即清:全局中断状态寄存器 */ - (void)ch395f_get_glob_int_status_all(); - /* 读芯片当前状态(两字节) */ - ch395f_get_socket_status((uint8_t)sockfd, st); - - /* 芯片尚未回到 CLOSED/TCP_CLOSED,或未满最短等待 */ - if (((st[0] != CH395F_SOCKET_CLOSED) || (st[1] != CH395F_TCP_CLOSED) || - ((uint32_t)(now - s_relisten_settle_start[sockfd]) < hold))) { - /* 未到上限:交重试轮询(30ms 后复检) */ - if ((int32_t)(now - s_relisten_settle_dead[sockfd]) < 0) { - DBG_INFO("sock%d relisten %s (sock=0x%02X tcp=0x%02X)", - sockfd, tag, st[0], st[1]); - /* 置 BUSY(结果回传调用方) */ - (void)net_set_errno(NET_ERR_BUSY); - /* 交重试轮询(30ms 后复检) */ - return -1; - } - /* 隔离上限已至:强制放行(防监听器永久失效) */ - DBG_INFO("sock%d relisten %s timeout, force open (sock=0x%02X tcp=0x%02X)", - sockfd, tag, st[0], st[1]); - } - /* 隔离完成(或强制放行) */ - s_relisten_quarantine[sockfd] = 0U; - /* 清隔离起点(回到"未开始") */ - s_relisten_settle_start[sockfd] = 0U; - /* 清上限时刻 */ - s_relisten_settle_dead[sockfd] = 0U; - } - - /* 重置协议类型为 TCP */ - ch395f_set_proto_type((uint8_t)sockfd, CH395F_PROTO_TYPE_TCP); - /* 本地端口(沿用原监听) */ - ch395f_set_sour_port((uint8_t)sockfd, p_sock->local_port); - - /* 打开芯片侧 Socket */ - status = ch395f_open_socket((uint8_t)sockfd); - /* open 失败 → BUSY(Trap 09:芯片需沉降时间才能重开) */ - if (status != CH395F_ERR_SUCCESS) { - /* Trap 09:断开后芯片需沉降时间才能重新 open。此处只清理并返回 - * (严禁 osDelay 忙等)→ 由重试轮询到点再来。 */ - /* 先关闭,交重试轮询(30ms 后复检) */ - ch395f_close_socket((uint8_t)sockfd); - /* 置 BUSY(结果回传调用方) */ - (void)net_set_errno(NET_ERR_BUSY); - /* 交重试轮询(30ms 后复检) */ - return -1; - } - /* 下发 TCP LISTEN 命令 */ - status = ch395f_tcp_listen((uint8_t)sockfd); - /* LISTEN 失败 → BUSY(同样交重试) */ - if (status != CH395F_ERR_SUCCESS) { - /* 置 BUSY(结果回传调用方) */ - return NET_ERR_RETURN_1(NET_ERR_BUSY); - } - - /* 状态置回 LISTENING(fd 保留) */ - p_sock->state = NET_SOCK_STATE_LISTENING; - /* 新监听不得继承上一条连接的释放原因(Trap 23 待办 #3) */ - p_sock->released_abnormal = 0U; - /* N-30(Trap 25):清零前把对端端口转登为幽灵甄别基准 */ - p_sock->last_peer_port = p_sock->remote_port; - /* N-30:新监听会话,丢中断去抖计数清零 */ - p_sock->lost_conn_seen = 0U; - /* 清对端端口(新连接待用) */ - p_sock->remote_port = 0; - /* 清对端 IP */ - p_sock->remote_ip = 0; - /* 清对端 IP 数组 */ - memset(p_sock->remote_ip_arr, 0, sizeof(p_sock->remote_ip_arr)); - /* 新开 Socket 发送缓冲按占用计(由 SEND_OK 清除) */ - p_sock->send_ready = 0; - /* 清接收字节统计(新连接) */ - p_sock->recv_bytes = 0; - /* auto_relisten 保持为 1,下次断连仍自动重监听 */ - - /* 读即清:仅本 Socket 的中断状态(防上一条会话残留误触发)。 - * 原为"全 8 个 Socket + 全局"读清,与 net_listen_locked 同源(B3,机制 - * 与 git 考古见该处注释):本处是每次断连 auto-relisten 的热路径,全量清 - * 会持续吞掉其它在用 Socket 的 CONNECT/DISCONNECT 等边沿事件。全局中断 - * 寄存器交 net_poll 集中读清(AGENTS 模块约定)。 */ - (void)ch395f_get_sock_int_status((uint8_t)sockfd); - - /* 留痕:监听器重建完成 */ - DBG_INFO("sock%d auto-relisten ok", sockfd); - /* 重监听成功 */ - return 0; -} - -/* ===== Trap 09 非阻塞重监听重试(D10 修复,2026-09-22 M3 实测) ===== - * 旧实现:open 失败 → close → osDelay(500) → 重试,netTask 忙等 500ms(全网冻结) - * 且实测恢复 588ms、期间客户端重连被拒 - * 新实现:首次失败即登记待重试,由 netTask 每轮 net_process_messages() 到点重试 - * 恢复时间 ≈ NET_RELISTEN_RETRY_MS 量级,任何时刻都不阻塞任务 - */ -#define NET_RELISTEN_RETRY_MS 30U /* 重试间隔(Trap 09 沉降窗口;实测 100ms 首次必成, - * 收紧到 30ms 以缩短恢复时间——未成则下一轮到点再来) */ -#define NET_RELISTEN_DEADLINE_MS 2000U /* 放弃时限(自首次尝试起) */ - -static uint8_t s_relisten_wait[NET_MAX_SOCKETS]; /* 1 = 有待重试的重监听 */ -static uint32_t s_relisten_next[NET_MAX_SOCKETS]; /* 下次重试时刻(tick) */ -static uint32_t s_relisten_dead[NET_MAX_SOCKETS]; /* 放弃时刻(tick) */ -#if defined(TEST_LATENCY_PROBE) -static lat_tmark_t s_relisten_mark[NET_MAX_SOCKETS]; /* 恢复计时起点(首次尝试) */ -#endif - -/* - * 函数功能:清除本 fd 的待重监听重试状态(关闭/新建 fd 时调用) - * 入口参数:sockfd - Socket 描述符 (0~7) - * 出口参数:无 - * 返回值:无 - * 限定条件:无 - * 函数说明:1. 清"待重试"标志与重试时刻,fd 关闭/新建时调用(防残留重试) - */ -static void net_relisten_clear(int sockfd) { - /* 描述符越界:直接返回(本函数不设错误码) */ - if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { - return; - } - /* 清"待重试"标志 */ - s_relisten_wait[sockfd] = 0U; - /* 清下次重试时刻 */ - s_relisten_next[sockfd] = 0U; - /* 清放弃时刻 */ - s_relisten_dead[sockfd] = 0U; - /* Trap 23:一并清沉降/隔离状态 */ - s_relisten_settle_dead[sockfd] = 0U; - /* 清沉降起点 */ - s_relisten_settle_start[sockfd] = 0U; - /* 一并清隔离标志:关闭/重建即确定性释放, - * 防同 fd 下一生成继承上轮的 400/800ms - * 隔离等待(2026-09-24 台架发现跨轮残留) */ - s_relisten_quarantine[sockfd] = 0U; -} - -/* - * 函数功能:标记重监听完成(清重试状态;探针构建下报告恢复计时样本) - * 入口参数:sockfd - Socket 描述符 (0~7) - * 出口参数:无 - * 返回值:无 - * 限定条件:netTask 上下文 - * 函数说明:1. 重监听成功收尾:清重试状态;探针构建(TEST_LATENCY_PROBE)下上报恢复计时样本 - */ -static void net_relisten_done(int sockfd) { - /* 清重试状态(复用清除函数) */ - net_relisten_clear(sockfd); -#if defined(TEST_LATENCY_PROBE) - /* 上报恢复计时样本(首次尝试 → 成功) */ - lat_probe_add_seg("relisten", lat_mark_us(&s_relisten_mark[sockfd])); -#endif -} - -/* - * 函数功能:尝试重监听一次;失败则登记"待重试"(**不阻塞** netTask) - * 入口参数:p_sock - CLOSED 的监听型 Socket 控制块 net_sock_t * - * 出口参数:无 - * 返回值:0 - 成功(监听器已就绪);-1 - 本次失败或已在重试队列中 - * 限定条件:netTask 上下文 - * 函数说明:已有待重试时直接返回 -1(不重复尝试、不重置重试时刻)——否则每 10ms - * 一次的 accept 轮询会不断把重试时刻往后推,导致永不重试(饥饿)。 - */ -static int net_relisten_try(net_sock_t *p_sock) { - /* 由控制块指针反推描述符 */ - int sockfd = (int)(p_sock - s_net_socks); - /* 当前 tick */ - uint32_t now = HAL_GetTick(); - - /* 描述符越界 → BADF */ - if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { - return NET_ERR_RETURN_1(NET_ERR_BADF); - } - /* 已在重试队列:不重复尝试、不重置时刻(防重试饥饿) */ - if (s_relisten_wait[sockfd] != 0U) { - (void)net_set_errno(NET_ERR_BUSY); - /* 已在重试队列:由 tick 负责 */ - return -1; - } -#if defined(TEST_LATENCY_PROBE) - /* 恢复计时自首次尝试起 */ - lat_mark(&s_relisten_mark[sockfd]); -#endif - /* 本次尝试成功 */ - if (net_relisten_locked(p_sock) == 0) { - /* 标记完成(清状态 + 探针样本) */ - net_relisten_done(sockfd); - /* 监听器已就绪 */ - return 0; - } - /* 登记"待重试" */ - s_relisten_wait[sockfd] = 1U; - /* 下次重试时刻 = now + 30ms */ - s_relisten_next[sockfd] = now + NET_RELISTEN_RETRY_MS; - /* 放弃时刻 = now + 2s */ - s_relisten_dead[sockfd] = now + NET_RELISTEN_DEADLINE_MS; - /* 本次失败(重试交 tick) */ - return -1; -} - -/* - * 函数功能:重试轮询(netTask 每轮调用一次,非阻塞) - * 入口参数:无 - * 出口参数:无 - * 返回值:无 - * 限定条件:netTask 上下文(net_process_messages 开头调用) - * 函数说明:到点重试待办的自动重监听;成功即报告探针样本;超时放弃并记错误日志 - */ -static void net_relisten_retry_tick(void) { - /* 当前 tick */ - uint32_t now = HAL_GetTick(); - /* Socket 循环索引 */ - int i; - - for (i = 0; i < NET_MAX_SOCKETS; i++) { - /* 本 fd 无待重试重监听:跳过 */ - if (s_relisten_wait[i] == 0U) { - continue; - } - /* 本 fd 有在途挂起 recv:重开 Socket 会摧毁在途连接(L3 保险) */ - if (s_net_pending[i].active != 0U) { - continue; - } - /* 未到点(tick 差值按有符号比较,抗回绕) */ - if ((int32_t)(now - s_relisten_next[i]) < 0) { - continue; - } - /* 已到放弃时限 */ - if ((int32_t)(now - s_relisten_dead[i]) >= 0) { - /* 清重试状态 */ - net_relisten_clear(i); - /* 放弃并记错误日志 */ - DBG_ERROR("sock%d relisten give up (Trap 09 not cleared)", i); - continue; - } - /* 重试成功 */ - if (net_relisten_locked(&s_net_socks[i]) == 0) { - /* 标记完成(清状态 + 探针样本) */ - net_relisten_done(i); - /* 留痕:重试成功 */ - DBG_INFO("sock%d relisten retry ok", i); - } else { - /* 仍未成:置下次重试时刻 */ - s_relisten_next[i] = now + NET_RELISTEN_RETRY_MS; - } - } -} /* ===== L3 挂起 recv 请求的实现(一期,2026-09-22) ===== * 完成条件按 ①数据 ②到点 ③断连 顺序检查(逐项对齐旧自旋循环语义) @@ -2428,8 +2126,12 @@ static void net_pending_tick(void) { continue; } - /* ③ 断连:排空 → 数据或 EOF(0) + 自动重监听 */ - if (p_sock->state == NET_SOCK_STATE_CLOSED) { + /* ③ 断连:排空 → 数据或 EOF(0) + 自动重监听。 + * 判据同 net_recv_locked(N-32 P1 不变量 I):`state == CLOSED || + * session_dead`。只判 CLOSED 时,抢先 relisten 后 ①(要求 ESTABLISHED) + * 与 ③(要求 CLOSED) 两头落空 → 挂起的 recv 只能干等 ② deadline 超时 + * (lftpd 阻塞 recv 的"消费者卡死"原型)。 */ + if ((p_sock->state == NET_SOCK_STATE_CLOSED) || (p_sock->session_dead != 0U)) { /* 查询残留字节数 */ int rem = ch395f_get_recv_len((uint8_t)i); /* 有残留数据:按数据完成 */ @@ -2445,10 +2147,17 @@ static void net_pending_tick(void) { /* 了结:以字节数通知 */ net_pending_complete(i, rd); } else { - /* 启用自动重监听:先把监听器重新置回 LISTEN */ - if (p_sock->auto_relisten != 0U) { - /* 非阻塞:失败由 D10 重试轮询接管 */ - (void)net_relisten_try(p_sock); + /* 启用自动重监听:先把监听器重新置回 LISTEN。 + * ⚠ 内层 `state == CLOSED` 门同 net_recv_locked:session_dead + * 但已回 LISTENING 时重建已由别处完成,再 open+listen 会因 + * USE 错误走进关闭分支、关掉正听着的 Socket(U12 同源)。 + * N-32 P2 修正:**保留本处即时重建** —— 它只在有**挂起 recv** + * 时到达(`p->active != 0`),属"消费者在等这条连接结局"的 + * 消费者驱动路径,不是周期性抢先(同 net_recv_locked 的理由)。 */ + if ((p_sock->state == NET_SOCK_STATE_CLOSED) && + (p_sock->auto_relisten != 0U)) { + /* 非阻塞:失败由 D10 重试轮询接管(N-32 §10:入口收敛到 net_listener_try) */ + (void)net_listener_try(i); } /* Trap 23 待办 #3:异常释放(TIMEOUT/RST)不是正常 EOF(对端未发正常 FIN) * → 以连接重置了结:应用(lftpd STOR)对异常结束回 5xx 而非误报 226; @@ -2478,6 +2187,24 @@ static void net_pending_tick(void) { } } +/* + * 函数功能:查询本 fd 是否有在途挂起 recv(L3 pending)—— 宿主能力,供 net_listener 用 + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:1 - 有挂起;0 - 无(含描述符越界) int + * 限定条件:任意上下文(纯 RAM 读,不发起 RPC) + * 函数说明:1. N-32 §10 模块化:net_listener_retry_tick 用它跳过"重开 Socket + * 会摧毁在途连接"的 fd(L3 保险,原为直接读 s_net_pending[i].active) + * 2. 声明在 net_listener.h(本模块提供、监听器模块消费) + */ +int net_pending_active(int sockfd) { + /* 描述符越界:视为无挂起 */ + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return 0; + } + return (s_net_pending[sockfd].active != 0U) ? 1 : 0; +} + /* * 函数功能:清除本 fd 的挂起请求(不通知;用于"新 fd 不得继承"与 close 后的幂等清理) * 入口参数:sockfd - Socket 描述符 (0~7) int @@ -2622,7 +2349,7 @@ static int net_close_locked(net_sock_t *p_sock) { memset(p_sock, 0, sizeof(net_sock_t)); p_sock->last_peer_port = keep_peer; /* 关闭即取消该 fd 的待重试(D10 重试队列) */ - net_relisten_clear(sockfd); + net_listener_clear(sockfd); /* 关闭即清除该 fd 的挂起状态(L3;幂等) */ net_pending_clear(sockfd); @@ -3575,6 +3302,10 @@ static void handle_connect_event(net_sock_t *p_sock) { p_sock->remote_port = (uint16_t)(remote[4] | (remote[5] << 8)); /* 状态 → ESTABLISHED(监听自身转数据通道) */ p_sock->state = NET_SOCK_STATE_ESTABLISHED; + /* N-32(P1):新会话开始 —— 清"上一条会话已结束"电平。必须在置 + * ESTABLISHED 之后立刻做:否则抢先 relisten 后到新连接的窗口内 + * recv 仍会误得 EOF(把新客户端踢开) */ + p_sock->session_dead = 0U; /* 新开 Socket 发送缓冲按空闲计 */ p_sock->send_ready = 1; @@ -3609,6 +3340,9 @@ static void handle_connect_event(net_sock_t *p_sock) { p_sock->remote_port = (uint16_t)(remote[4] | (remote[5] << 8)); /* 状态 → ESTABLISHED */ p_sock->state = NET_SOCK_STATE_ESTABLISHED; + /* N-32(P1):新会话开始 —— 清"上一条会话已结束"电平(同 LISTENING + * 分支:必须在转 ESTABLISHED 后立刻做,防抢先 relisten 窗口误 EOF) */ + p_sock->session_dead = 0U; /* 使能 KeepAlive(客户端连接保活) */ ch395f_set_keepalive_enable(p_sock->ch395_sock, 1); @@ -3624,7 +3358,11 @@ static void handle_connect_event(net_sock_t *p_sock) { * 出口参数:无 * 返回值:无 * 限定条件:net_poll() 检测到 DISCONNECT 中断后调用;netTask 上下文 - * 函数说明:1. CH395F 在 DISCONNECT 后自动关闭 Socket(FUN_PARA 默认),需重新 OPEN + LISTEN + * 函数说明:1. ⚠ FUN_PARA bit3=1(本仓 net_init 置 0x08):DISCONNECT/TIM_OUT 后芯片 + * **不**关 Socket、**不清**缓冲(停 0x05/0x00)——残留数据须由本层读出后,再由 + * 本层 ch395f_close_socket 关闭;芯片不代劳(§5.52② / §5.23)。旧注释写"CH395F 在 + * DISCONNECT 后自动关闭 Socket(FUN_PARA 默认)"与实际 0x08 相反,2026-10-12 修正: + * 它会误导维护者跳过显式 close(= U12 的成因之一)。之后需重新 OPEN + LISTEN * 2. 单连接:置 CLOSED,交由 net_recv_locked 排空剩余数据后再关 * 3. 触发 NET_EVENT_DISCONNECTED 回调 */ @@ -3638,20 +3376,21 @@ static void handle_disconnect_event(net_sock_t *p_sock) { /* 监听态:DISCONNECT 落在监听器上 */ if (p_sock->state == NET_SOCK_STATE_LISTENING) { - /* Trap 22 / N-6:监听态收到 DISCONNECT——芯片断开后自动关闭该 Socket(FUN_PARA 默认),不重开则监听永久死亡(此后所有 SYN 被拒); + /* Trap 22 / N-6:监听态收到 DISCONNECT——bit3=1(0x08)下芯片**不**关该 Socket(停 0x05/0x00,芯片 TCP 侧已 CLOSED);不重开则此后所有 SYN 全被拒 = 监听永久死亡; * 触发 = 断连后 <~1s 快速复连时 CONNECT 中断未上报(控制块滞留 LISTENING)后该连接断开(2026-09-23 生产固件复现); - * → 回读芯片状态:监听已失位即重建(D10 非阻塞重试),仍在监听判陈旧丢弃 */ + * N-32 P2:本分支**只回读留痕、不再 relisten** —— 重建统一交 + * net_listener_pump(每 50 ms 回读芯片状态,监听失位即重建)。原处直接 + * relisten 是 12 个所有者之一,且位于 DISCONNECT 事件路径,可能"抢先" + * 于消费者 recv(P1 已消除其后果,P2 消除其可能) */ /* 回读缓冲(芯片状态 2 字节) */ uint8_t st[2] = {0, 0}; - /* 回读芯片状态 */ + /* 回读芯片状态(仅为留痕:重建与否由 pump 裁决) */ ch395f_get_socket_status((uint8_t)sockfd, st); - /* 监听已失位(芯片已关闭):留痕并重建监听 */ + /* 监听已失位(芯片已关闭) */ if ((st[0] == CH395F_SOCKET_CLOSED) || (st[1] != CH395F_TCP_LISTEN)) { - DBG_INFO("sock%d listen lost (chip sock=0x%02X tcp=0x%02X), relisten now", + DBG_INFO("sock%d listen lost (chip sock=0x%02X tcp=0x%02X), pump will relisten", sockfd, st[0], st[1]); - /* 非阻塞:失败由 D10 重试轮询接管 */ - (void)net_relisten_try(p_sock); } else { /* 仍在监听:判陈旧事件丢弃 */ DBG_INFO("sock%d stale listen DISCONNECT ignored (chip tcp=0x%02X)", sockfd, st[1]); @@ -3686,11 +3425,16 @@ static void handle_disconnect_event(net_sock_t *p_sock) { /* 描述符范围保护 */ if ((qfd >= 0) && (qfd < NET_MAX_SOCKETS)) { /* 登记优雅沉降(NET_RELISTEN_SETTLE_GRACEFUL) */ - s_relisten_quarantine[qfd] = NET_RELISTEN_SETTLE_GRACEFUL; + net_listener_settle(qfd, NET_LISTENER_SETTLE_GRACEFUL); } } /* 正常释放(对端 FIN):断连了结回 EOF(0) */ p_sock->released_abnormal = 0U; + /* N-32(P1 不变量 I):置"本会话已结束"电平。须在置 CLOSED **之前**, + * 使 session_dead 覆盖"CLOSED 等待 recv 排空"整个窗口——排空期间 + * recv 的 DONTWAIT 快路径先返数据(recv_len>0 优先),数据尽后判据 + * 落到本标志 → 仍得 EOF,不因排空慢而漏报 */ + p_sock->session_dead = 1U; /* N-30(Trap 25):登记本连接对端端口(幽灵态甄别基准,须在状态 * 迁移前记录——remote_port 此刻仍有效,relisten 清零) */ p_sock->last_peer_port = p_sock->remote_port; @@ -3774,7 +3518,7 @@ static void handle_timeout_event(net_sock_t *p_sock) { /* 描述符范围保护 */ if ((qfd >= 0) && (qfd < NET_MAX_SOCKETS)) { /* 登记异常释放隔离(重监听前等待在途数据落尽 + RX 排空) */ - s_relisten_quarantine[qfd] = 1U; + net_listener_settle(qfd, NET_LISTENER_SETTLE_QUARANTINE); } } /* Trap 23 待办 #3:标记异常释放——L3 挂起 recv 排空断连后以 @@ -3783,6 +3527,10 @@ static void handle_timeout_event(net_sock_t *p_sock) { p_sock->released_abnormal = 1U; /* N-30(Trap 25):异常释放同样登记对端端口(幽灵甄别基准) */ p_sock->last_peer_port = p_sock->remote_port; + /* N-32(P1 不变量 I):异常结束同样是"本会话已结束"电平(TIMEOUT/RST + * 已在下方 requarantine,本标志只表达"会话结束了"这一事实,与 + * released_abnormal(结束得好不好看)正交) */ + p_sock->session_dead = 1U; /* 状态 → CLOSED */ p_sock->state = NET_SOCK_STATE_CLOSED; } else if (p_sock->state == NET_SOCK_STATE_LISTENING) { diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index b9ca27a2..d9f200dc 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -172,6 +172,29 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen); */ int net_can_accept(int sockfd); +/* + * 函数功能:查询监听 Socket 是否处于"应该尝试 accept"的就绪态(**纯 RAM 读,不发起 RPC**) + * 入口参数:sockfd - 监听 Socket 描述符 int + * 出口参数:无 + * 返回值:1 - 就绪(LISTENING,或 CLOSED + auto_relisten = 断连待重建态);0 - 不就绪 int + * 限定条件:net_init() 已调用;任意任务上下文 + * 函数说明:1. N-32(P1 不变量 II):取代 `net_can_accept()`(只认 ESTABLISHED)作为 + * ui_net_sink 的 accept 门控——原门控使 :7003 永不进 `net_accept()`, + * `net_lost_connect_probe`(丢 CONNECT 中断补 accept,N-30 实证的 + * "数据被吞/425"主根因修复)对该端口等于从未运行 + * 2. 放行 LISTENING:探针与 "closed-but-readable 接受" 都发生在 + * `net_accept_locked` **内部**,门控挡在外面就永远到不了 + * 3. 放行 CLOSED+auto_relisten **且 readable≠0**(有残留数据): + * 保住 N-30 的 "closed-but-readable" 接受;**无残留时必须返回 0**, + * 否则调用方会在整个 CLOSED 窗口内每轮发起 `net_accept`(RPC 无上界) + * → 队列积压 → 喂不上狗(2026-10-12 P2a 台架实测 48 条 task stall)。 + * 重建由 `net_listener_pump` 负责,调用方无需催促 + * 4. 与 `net_can_accept` 的语义差异**有意为之**:本函数回答"这个监听器 + * 这一轮**值不值得**发起一次 accept",而非"一定有连接" + * 5. 跨任务读单字,同 `net_can_accept` 的最坏情形论证 + */ +int net_listener_armed(int sockfd); + /* * 函数功能:查询 Socket 是否"值得发一次 recv"(**纯 RAM 读,不发起 RPC**) * 入口参数:sockfd - Socket 描述符 int @@ -412,6 +435,34 @@ void net_process_messages(void); */ net_sock_t *net_get_sock(int sockfd); +/* + * 函数功能:记录**本任务**最后一次失败的精确错误码(net 层内部通用工具) + * 入口参数:err - 错误码(NET_ERR_xxx,负值) + * 出口参数:无 + * 返回值:err 原值(便于写成 `return net_set_errno(NET_ERR_XXX);`) + * 限定条件:无 + * 函数说明:1. 只写本任务的槽位,其他任务读不到、也改不了 → 消除跨任务覆写竞态 + * 2. N-32 §10 模块化:由 net_socket.c 的 static 提升为**模块间可见**, + * 供 net_listener.c 等同目录内部模块复用(定义仍在 net_socket.c) + */ +int net_set_errno(int err); + +/* 「记本任务错误码 + 统一失败返回 -1」的紧凑写法(取代 `s_net_errno = X; return -1;`) + * N-32 §10:原定义在 net_socket.c,随 net_set_errno 一并提升到本头文件 */ +#define NET_ERR_RETURN_1(err) (net_set_errno(err), -1) + +/* + * 函数功能:查询本 fd 是否有在途挂起 recv(L3 pending)—— 供 net_listener.c 消费 + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:1 - 有挂起;0 - 无(含描述符越界) int + * 限定条件:任意上下文(纯 RAM 读,不发起 RPC) + * 函数说明:1. N-32 §10 模块化:net_listener_retry_tick 用它跳过"重开 Socket + * 会摧毁在途连接"的 fd(L3 保险,原为直接读 s_net_pending[i].active) + * 2. 定义在 net_socket.c(配对头即本文件) + */ +int net_pending_active(int sockfd); + /* * 函数功能:获取 Socket 本地地址(IP + Port) * 入口参数:sockfd - socket 描述符 diff --git a/Drivers/BSP/NET/net_types.h b/Drivers/BSP/NET/net_types.h index 4cf2f7a9..3a218ed1 100644 --- a/Drivers/BSP/NET/net_types.h +++ b/Drivers/BSP/NET/net_types.h @@ -189,6 +189,22 @@ typedef struct { * 节拍 = accept 轮询 100ms;新监听/补 accept 后清零) */ uint8_t lost_conn_seen; + /* N-32 / B10 根治 P1(2026-10-12,设计文档 §2.3 不变量 I):本会话"已结束"的 + * **电平**(粘滞),取代 `state == CLOSED` 这一**边沿**。 + * 为什么必须电平化:单连接复用模型下同一个 fd 既是监听器又是连接(handle + * aliasing),"关掉一条连接"与"毁掉一个监听器"在类型上无法区分;而"会话结束" + * 原先只能靠 `state == CLOSED` 这一刻被观察到——一旦别的路径抢先 relisten + * 回 LISTENING(对账④/`net_accept_locked` 的 CLOSED 分支),消费者再 recv + * 就恒得 WOULDBLOCK,EOF 永远失踪(B10"消费者卡死"与新连接被旧会话静默吸收)。 + * session_dead 让"会话已结束"可重复查询、幂等:谁先看到不重要,谁都饿不着。 + * 置位:handle_disconnect_event 释放分支 / handle_timeout_event ESTABLISHED + * 分支 / net_connect_locked 命令被拒 / 对账①(复用前者,:654)。 + * 清零:新 CONNECT 成功(handle_connect_event 两支)、net_close/net_socket + * (memset 整块清零)、net_relisten_locked 的 peer 簿记**不清**—— + * 这是关键:重建监听与"应用是否已确认会话结束"必须解耦(design §5 P1.1)。 + * 跨 fd 保留语义同 released_abnormal(幽灵/陈旧事件属芯片 Socket 级)。 */ + uint8_t session_dead; + /* 统计信息 */ uint32_t recv_bytes; /* 累计接收字节数 */ uint32_t send_bytes; /* 累计发送字节数 */ diff --git a/Drivers/BSP/UI/ui_net_sink.c b/Drivers/BSP/UI/ui_net_sink.c index 74d22bcb..ac59a58b 100644 --- a/Drivers/BSP/UI/ui_net_sink.c +++ b/Drivers/BSP/UI/ui_net_sink.c @@ -23,6 +23,13 @@ * "RX 非空但 readable=0",故每 32 轮无条件 recv 一次(≈320 ms); * 门控放行时若监听控制块已消失则置 g_listen=-1 触发重建, * 避免"门控把端口永久不再接受新客户端"这一新卡死路径 + * 2026-10-12 王建锋 N-32(B10 根治 P1,设计文档 §2.3 不变量 II):accept 门控 + * 由 net_can_accept()(只认 ESTABLISHED)改判 + * net_listener_armed()(LISTENING 或 CLOSED+auto_relisten)—— + * 旧门控在断连待重建窗口恒 0,:7003 永不进 net_accept, + * net_lost_connect_probe 对该端口等于从未运行;同时 EOF / + * 真错误两条路径不再自行 bind_listen(重建交回 net 层, + * 消灭第三/第四个重建所有者),仅 fd 真消失时置 -1 */ #include "ui_net_sink.h" @@ -469,11 +476,15 @@ int ui_net_sink_start(uint16_t port) { * 限定条件:ui_net_sink_start() 已成功调用 * 函数说明: 1. 网络代次变化(重建)时关闭旧监听/客户端并重建 * 2. 无监听(启动失败)时重试启动 - * 3. 无客户端时**先门控**(net_can_accept,纯 RAM 读)再 accept,成功则复位状态机 + * 3. 无客户端时**先门控**(net_listener_armed,纯 RAM 读)再 accept,成功则复位状态机。 + * 门控判据 = "监听器还在服役"(LISTENING 或 CLOSED+auto_relisten)而非 + * "本轮必然有连接"——后者会让断连待重建窗口内永不进 net_accept, + * 把丢 CONNECT 中断的补 accept 探测永久挡在门外(N-32) * 4. 有客户端时**先门控**(net_can_read)再非阻塞 recv 逐字节喂入状态机; * 门控判为"无数据"时仍每 RM_LIVENESS_DIV 轮无条件收一次(兜底) - * 5. 对端关闭/真错误时关闭客户端;若监听 Socket 随客户端失效 - * (MCU 单连接模型)则重建监听,等待重连 + * 5. 对端关闭/真错误时关闭客户端。**不再自行重建监听**(N-32 不变量 II): + * recv 的 EOF/错误分支已触发 net 层 auto_relisten,另有对账④兜底; + * 仅 g_listen 的 fd 真消失(net_get_sock 返 NULL)才置 -1 交回 2 重启 * ⚠ **门控只消除"无谓的往返",不消除"往返本身无上界"**:一旦真有活要干 * (accept / recv / 3204 B 的 LCDMEM 应答),仍是"入队 + 等 netTask 回执", * 最坏 ≈4 s。宿主 = uiTask 主循环;"搬出 uiTask"已完整评估并**决定不做** @@ -507,10 +518,15 @@ void ui_net_sink_poll(void) { return; } - /* 无客户端时尝试接受连接(I-1 门控:无连接可接受 → 0 次 RPC) */ + /* 无客户端时尝试接受连接(N-32 P1 不变量 II:门控从 net_can_accept〔只认 + * ESTABLISHED〕换成 net_listener_armed〔LISTENING 或 CLOSED+auto_relisten〕) + * 原因:MCU 单连接模型下断连后 fd 会停在 CLOSED 等 relisten,旧门控在该窗口 + * 内恒 0 → :7003 永不进 net_accept → net_lost_connect_probe 对该端口等于 + * 从未运行(丢 CONNECT 中断无第二信源);armed 放行后由 net_accept_locked + * 的状态门自行裁决(含 RX 有数据的 closed-but-readable 接受) */ if (g_client < 0) { - if (net_can_accept(g_listen) == 0) { - /* 监听控制块已消失(网络重建 / 异常释放)→ 置 -1 交回上方 + if (net_listener_armed(g_listen) == 0) { + /* 监听器已不在服役(fd 消失 / 非监听型)→ 置 -1 交回上方 * g_listen < 0 分支重建监听。改前靠"每轮都 accept"撞出 BADF 来暴露, * 加了门控就必须显式兜这一条,否则端口永久不再接受新客户端 */ if (net_get_sock(g_listen) == NULL) { @@ -547,24 +563,40 @@ void ui_net_sink_poll(void) { } } else if (n == 0) { /* 对端关闭:单连接模型下监听通道与数据通道是同一个 Socket,net 层已在 - * net_recv_locked 的 CLOSED 分支自动把该 Socket 放回 LISTEN;此处绝不能再 + * net_recv_locked 的 EOF 分支自动把该 Socket 放回 LISTEN;此处绝不能再 * net_close 它,否则监听一并被毁(后续连接被拒),且重建监听受 Trap 09 时序限制 */ if (g_client != g_listen) { net_close(g_client); /* 多连接后端(PC 模拟器):数据通道独立,需显式关闭 */ } g_client = -1; rm_parser_reset(); - if (net_get_sock(g_listen) == NULL && bind_listen() < 0) { - g_listen = -1; /* 重建失败:交回主循环重试,避免拿失效 fd 反复 accept */ + /* N-32(P1 不变量 II):重建**不再**由本处自行 bind_listen。 + * ① net_recv_locked 的 EOF 分支已触发 auto_relisten(D10 非阻塞重试); + * ② 对账④还会兜底补一次 relisten(Trap 22 实证事件会丢,两层不可互替)。 + * 本处再做一遍 = 第三个重建所有者,正是 §2.3 那 12 个所有者的同类。 + * 仅当 fd 真消失(net_get_sock 返 NULL = 网络重建)才置 -1,交回上方 + * 分支重新走 start → bind_listen */ + if (net_get_sock(g_listen) == NULL) { + g_listen = -1; /* fd 真消失:交回主循环重试 */ } } else { /* n < 0:区分"暂无数据"(继续等待)与"真错误"(断开并视情况重建监听) */ if (net_get_errno() != NET_ERR_WOULDBLOCK) { - net_close(g_client); + /* N-32(P1):加 `g_client != g_listen` 护栏,与上面 n==0 分支 + * 的 :552 一致——MCU 单连接模型下 g_client 可能就是 g_listen 本身, + * 无护栏的 net_close 会直接关掉监听 fd(:563,与 :134 注释"严禁" + * 自相矛盾),端口随 Trap 09 沉降窗短死。PC 模拟器多连接后端下 + * 两者恒不等,护栏不改变其行为 */ + if (g_client != g_listen) { + net_close(g_client); /* 多连接后端(PC 模拟器):数据通道独立,需显式关闭 */ + } g_client = -1; rm_parser_reset(); - if (net_get_sock(g_listen) == NULL && bind_listen() < 0) { - g_listen = -1; /* 重建失败:交回主循环重试 */ + /* N-32(P1 不变量 II):同 EOF 分支——不再自行 bind_listen, + * 重建交回 net 层(recv 的错误分支同样会走 relisten)。仅 fd 真消失 + * 时置 -1 交回主循环走 start */ + if (net_get_sock(g_listen) == NULL) { + g_listen = -1; /* fd 真消失:交回主循环重试 */ } } } diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index 3b1975b6..293a6a46 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -1583,6 +1583,11 @@ 1 ..\Drivers\BSP\NET\net_socket.c + + net_listener.c + 1 + ..\Drivers\BSP\NET\net_listener.c + net_cfg.c 1 diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 4a2ca0ca..92673e24 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 223,560 B (218.32 KB) | 1,048,576 B (1024 KB) | 21.3% | -| RW_IRAM1 | 96,096 B (93.84 KB) | 131,072 B (128 KB) | 73.3% | +| ER_IROM1 | 223,724 B (218.48 KB) | 1,048,576 B (1024 KB) | 21.3% | +| RW_IRAM1 | 96,144 B (93.89 KB) | 131,072 B (128 KB) | 73.4% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **224,596 B (219.3 KB)** | 1,048,576 B (1024 KB) | **21.4%** | +| **Flash (ROM 合计)** | **224,772 B (219.5 KB)** | 1,048,576 B (1024 KB) | **21.4%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index fea41a60..f4683a58 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,14 +15,14 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 223,560 B | 1,048,576 B | 21.3% | 参考 | +| ER_IROM1 (Flash) | 223,724 B | 1,048,576 B | 21.3% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 96,096 B | 131,072 B | 73.3% | PASS | +| RW_IRAM1 | 96,144 B | 131,072 B | 73.4% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| -| 网络 | 13,366 B | 12 KB | ⚠ 超 1.1 KB | +| 网络 | 13,410 B | 12 KB | ⚠ 超 1.1 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 index f72aa7b1..18f92d38 100644 --- a/docs/子系统设计/网络Socket监听生命周期设计说明.md +++ b/docs/子系统设计/网络Socket监听生命周期设计说明.md @@ -67,6 +67,59 @@ B10 探针看到的"孤儿芯片态"不是芯片异常,而是本仓这个配 → Trap 25 的 `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 25 三次定案的判别探针):`st=0x05/0x00` 且 `recv_len=1027`—— +Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少**。这正是 bit3=1 按规格 +保留现场,不是芯片异常(`docs/问题记录/CH395F_Trap_Records.md` §877-878)。 + +**② bit3=0 的代价是「硬件保证的数据丢失」,且不可在软件层补救。** +本产品最典型的流量形态是「发完即 FIN」:ftplib `storbinary` 语义 = `sendall` 发完立即关 +数据连接、回控制通道等 226。芯片把最后一段数据落进 RX 的同时抛出 DISCONNECT: + +- bit3=1:RX 数据在,MCU 来得及读(Trap 23 的「排空 RX 再 close」修复才成立); +- bit3=0:芯片**当场**关 Socket 并清缓冲 → MCU 永远读不到尾部 → 文件被截断却仍回报 226 + (静默损坏)。这是 bit3=0 的**必然结果**,不是概率问题——软件再聪明也拿不回已被硬件擦掉的数据。 + +即:改 bit3=0 等于用「一个硬件保证、不可修的数据丢失」去换「一个软件侧可修的关闭义务」, +严格更差。:8888 对时包、:7003 远显推流同样是「数据 + 随即关闭」形态,一并受害。 + +**③ bit3=0 也并不能真正消除孤儿态。** 芯片自动关以**收到** DISCONNECT/TIM_OUT 为前提, +而 Trap 22 已实证快速复连场景芯片会**丢**这两个中断(`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) | +| 关闭姿势必须正确 | Trap 14/17:陈旧 DISCONNECT 锁存秒断下一条连接 | `net_close_locked` 既有「close → 等 `CLOSED` → 排空本 Socket 中断」,是 bit3=1 下唯一正确的关闭序列 | +| 异常释放须先排空残渣再关 | Trap 23:~2KB RX 残渣被下条连接当自己的数据读走 | `net_relisten_locked` 隔离期(400 ms)持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED` | + +**⑥ 本次(P0)已落地的口径修正**:`net_socket.c:3627` / `:3641` 原注释写「DISCONNECT 后 +芯片自动关闭 Socket(FUN_PARA 默认)」——与实际 `0x08` **相反**。这类注释正是 U12 这类 +「跳过显式 close」缺陷的观念源头,已改正并留痕。 + ## 3. 为什么不能走"芯片原生多连接模式"(A′) 芯片确实自带 lwIP 式模型:FUN_PARA bit1(≥0x44,本仓实测 0x4A)下 @@ -121,17 +174,22 @@ B10 探针看到的"孤儿芯片态"不是芯片异常,而是本仓这个配 断连 → 断言 sink 仍拿到 EOF、且**下一个客户端能被服务**(当前会被旧会话静默吸收)。 回归矩阵沿用 B10 那套(`:7003` 10/10、`:21` 50/50×2、`:8888` 20/20、`ftp_regression` 11/11、Trap 23 5/5×2)。 -### P2 单一所有者(中改,消灭多所有者) +### P2 单一所有者(中改,消灭多所有者)—— P2a ✅ 2026-10-12 / P2b 建议暂缓 -1. 新增 `net_listener_pump()`,与 `net_reconcile_tick` 同处 netTask 自愈节拍, - 是**唯一**对监听型 Socket 调 `net_relisten_try` 的地方。 -2. 删除散点 relisten 调用:`net_accept_locked:1346`、`net_recv_locked:1884`、`net_pending_tick:2451` - ——它们只报告 EOF,重建由 pump 统一做(消除"抢先"这件事本身)。 -3. 消灭 netTask 内的同步忙等(U6):`net_close_locked:2597-2616` 的 500 ms `osDelay` 循环、 - `net_listen_locked:1115-1126` 的 3×500 ms,改用既有 D10 非阻塞重试模式。 -4. 补 U11:本层 CLOSED 而芯片**正在** LISTEN → 只做状态重同步(等价 `NET_MSG_RESYNC_LISTEN`),不重下发命令。 -5. 验收:close/listen 风暴期间无 `[WD] task stall`;`:21` 0ms×50 仍 50/50; - `netp`(`net_poll` 段最长耗时,µs)不劣化。 +1. 新增 `net_listener_pump()`(100 ms 周期),接管**周期性**重建,是唯一做周期性 + `net_relisten_try` 的地方。✅ 已落地 +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 远期(**不建议现在做**,仅登记方向) @@ -194,7 +252,7 @@ DONTWAIT 在 LISTENING 下返 `WOULDBLOCK`(`:1890`)→ `ui_net_sink.c:560-56 | D1 | **A′(启用芯片多连接模式)出局**,根治必须在单连接复用模型内做 | `docs/问题记录/CH395F_Trap_Records.md` 附 FTP 改造清单第 4 条「多连接模式不支持多监听」;本产品 3 常驻监听 + 动态 PASV;§9.2.6 L1295 池按源端口匹配会互串;历史实测 Trap 02/11;FUN_PARA 初始化后不可改(`docs/芯片手册/CH395F.md` §5.52 L795⑥) | | D2 | 采用**形态 B 深化**(电平化 + 单一所有者),**不做 P3** | P3 需控制面重写、RAM/Flash 上升,只有"同端口并发多连接"需求才值得 | | D3 | 对账④**保留**(`4f2cc8b` 不撤回),其副作用由 P1 消除 | Trap 22 实证事件会丢 → 周期对账不可删;④ 已把 `:7003` 从 0/10 永久死拉到 10/10 | -| D4 | `FUN_PARA` **保持 `0x08`**,不因孤儿态改配置 | bit3=1 是 Trap 18/25 定案的"保留排空现场"契约;孤儿态是它的合法后果,应在软件层收口而非回退契约 | +| D4 | `FUN_PARA` **保持 `0x08`**,不因孤儿态改配置(2026-10-12 复核确认) | bit3=1 是 Trap 18/25 定案的"保留排空现场"契约;bit3=0 会令芯片在 DISCONNECT/TIM_OUT 时**当场清缓冲**(`docs/芯片手册/CH395F.md` §5.52② / §5.23 L572)→「发完即 FIN」流量的尾部数据必被硬件擦除、**不可补救**;且 bit3=0 也挡不住丢中断(Trap 22)→ 孤儿态照样有。孤儿态是 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 | 与监听生命周期不同因,混做会让回归面失控 | @@ -202,25 +260,288 @@ DONTWAIT 在 LISTENING 下返 `WOULDBLOCK`(`:1890`)→ `ui_net_sink.c:560-56 ## 9. 执行清单(后续按此执行,逐项勾选) **P0(零风险)** -- [ ] 修 `Drivers/BSP/NET/net_socket.c:3627`、`:3641` 的 FUN_PARA 口径错误注释(实际 `0x08`,芯片**不**自动关) +- [x] 修 `Drivers/BSP/NET/net_socket.c:3627`、`:3641` 的 FUN_PARA 口径错误注释(实际 `0x08`,芯片**不**自动关)—— 2026-10-12 已改并留痕(§2.4⑥),`@build` 0 错 0 警 - [ ] 把 U11 / U12 / U14 / U15 登记进 `docs/问题记录/CH395F_Trap_Records.md` - [ ] 在 B10 行确认已记「④ 扩大 EOF 吞没窗口」(已记,执行时复核) **P1(边沿→电平)** -- [ ] `net_types.h` 的 `net_sock_t` 增 `uint8_t session_dead;` -- [ ] 置位:`handle_disconnect_event` 释放分支、`handle_timeout_event` ESTABLISHED 分支、对账①(复用前者)、`net_connect_locked:1685` -- [ ] 清零:`handle_connect_event`(新 CONNECT)、`net_close`/`net_socket` 的 memset;**`net_relisten_locked` 不清**(关键) -- [ ] `net_recv_locked` DONTWAIT 判据改 `state==CLOSED || session_dead` -- [ ] `ui_net_sink.c:563` 加 `g_client != g_listen` 护栏;EOF/错误后不再自行 `bind_listen` -- [ ] 新增 `net_listener_armed(fd)`(LISTENING 或 CLOSED+auto_relisten),替换 `ui_net_sink.c:512` 的 `net_can_accept` 门控 -- [ ] 验收:新增"抢先 relisten"腿(连上 :7003 → 拖住 uiTask → 断连 → 断言 sink 仍得 EOF **且下一个客户端被服务**)+ 全量回归矩阵(§7.1 那一套) -- [ ] `@build` 0/0 → `@test` PASS → 回读校验 → 停下等提交指令 +- [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`) -**P2(单一所有者)** -- [ ] 新增 `net_listener_pump()`,与 `net_reconcile_tick` 同节拍,成为**唯一**调 `net_relisten_try` 的监听重建入口 -- [ ] 删散点 relisten:`net_accept_locked:1346`、`net_recv_locked:1884`、`net_pending_tick:2451` -- [ ] 消灭 netTask 内同步忙等:`net_close_locked:2597-2616`(500 ms `osDelay` 循环)、`net_listen_locked:1115-1126`(3×500 ms)→ 改 D10 非阻塞重试 -- [ ] 补 U11:本层 CLOSED 而芯片**正在** LISTEN → 只做状态重同步,不重下发命令 -- [ ] 验收:close/listen 风暴期间无 `[WD] task stall`;`:21 0ms×50` 仍 50/50;`netp`(`net_poll` 段最长耗时 µs)不劣化 +**P1 台架验收记录(2026-10-12,`@build` Code=197096 / 基线 197084,+12 B)** -**回滚点**:P0 纯注释可直接 revert;P1 若回归失败 revert 单提交即可(④ 仍在,退回到"端口活着但慢消费者可能卡死"的已知状态);P2 涉及入口收口,须按阶段各自成提交、各自可 revert。 +| 腿 | 结果 | 说明 | +|---|---|---| +| `@test` 启动闭环 | **PASS** | 启动锚点齐、无 `[ERR]`、reset cause ×1、7.2 s 到 net ready | +| `ftp_regression` | **11/11** | 含 STOR 32KB —— 「发完即 FIN」链路,`session_dead` 直接作用面 | +| Trap 23 `--check-abort`(RST) | **5/5** | 450 + 5xx 正确回报,无误报 226 | +| Trap 23 `--graceful`(FIN) | **5/5** | 226 正常 | +| `:21` 快速复连 0 ms ×50 | **50/50** | Trap 22 基线不回归 | +| `: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 25「不留未证明的复杂度」**删除该探针**;该场景登记为**残余验证项**, + 需设备内注入点(参照 `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) +- [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 25「不留未证明的复杂度」,为一个未发生的故障去动热路径, + 性价比存疑 —— 建议等有 `task stall` 与忙等的**因果证据**后再做。 + +> ⚠ **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 相对"直接抽模块"的价值:先收敛,再搬移。 +> +> 本节只定**边界与接口**,不含任何代码;执行须过 §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 | + +**跨边界引用点**(这个数字决定接口面大小,是本设计的核心依据): + +| 引用 | 处数 | 位置 | +|---|---|---| +| `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 成果) | +| `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` | 公开 API + 消息分派;它**只改本层状态**,本就不调 relisten | +| `net_reconcile_tick` | ①② 是**连接**失同步自愈(③④ 已在 P2a 移交 pump) | +| `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 会让 + 「本地已修好」与「提交时仍报错」自相矛盾(本次踩到,白跑两轮)。 diff --git a/scripts/check_registration.py b/scripts/check_registration.py index 97d8e33b..8e300965 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -113,6 +113,7 @@ SIM_EXEMPT = [ ("Drivers/BSP/NET/lftpd/lftpd_string.c", "MCU FTP 字符串工具"), ("Drivers/BSP/NET/net_select.c", "MCU socket 选择层"), ("Drivers/BSP/NET/net_socket.c", "MCU socket 后端,模拟器另有 net_socket_sim.c"), + ("Drivers/BSP/NET/net_listener.c", "MCU 监听器生命周期(依赖 ch395f_* SPI 驱动与 net_socket.c 静态状态),模拟器无对应(Winsock 后端无孤儿态/重监听概念)"), ("Drivers/BSP/RS485/rs485.c", "MCU RS-485 驱动"), ("Drivers/BSP/SD2506/sd2506.c", "MCU RTC 驱动"), ("Drivers/BSP/TPAFE5160/tpafe5160.c", "MCU 计量芯片驱动"), diff --git a/simulator/sim/net_socket_sim.c b/simulator/sim/net_socket_sim.c index 8c339e22..47ce668f 100644 --- a/simulator/sim/net_socket_sim.c +++ b/simulator/sim/net_socket_sim.c @@ -351,6 +351,24 @@ int net_can_accept(int sockfd) { return 1; } +/* + * 函数功能:查询监听 Socket 是否处于"应该尝试 accept"的就绪态(与 MCU 端同名 API) + * 入口参数:sockfd - 监听 socket 描述符 + * 返回值:1 - 可尝试 accept;0 - 描述符无效 + * 限定条件:socket 已创建 + * 函数说明:同 net_can_accept——Winsock accept 本无 RPC 成本 → 恒返回 1, + * 使上层门控在模拟器上等价于"改前行为"。 + * ⚠ MCU 端该函数额外放行 CLOSED+auto_relisten(单连接复用的断连待重建态), + * 那是芯片 fd 别名模型的产物;Winsock 后端 fd 与监听器分离 + * (设计文档 D6:双模型分裂暂不消除),无此中间态,恒 1 即为正确语义。 + */ +int net_listener_armed(int sockfd) { + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + return 0; + } + return 1; +} + /* * 函数功能:查询 Socket 是否"值得发一次 recv"(与 MCU 端同名 API) * 入口参数:sockfd - socket 描述符