merge: task/N32-funpara-bit3-safety 合入 v2-on-ui
任务 N32(9 个提交)经 task-worktree 回收: - merge: N32 并入 v2-on-ui 主干(解 RAM 快照冲突并重生成) - docs(net): N-32 §10 模块化执行结果与判据修订 - refactor(net): N-32 §10 监听生命周期抽为 net_listener.c(纯搬移) - docs(net): N-32 新增 §10 监听生命周期模块化边界设计(未执行) - docs(net): N-32 P2a 落地记录 + 修正设计文档的 P2 原计划 - feat(net): N-32 P2a 监听器 pump 单一所有者(周期性重建收敛) - docs(net): N-32 P1 台架验收记录 + 验收腿否定结论 - feat(net): N-32 P1 会话结束电平化,消灭抢先 relisten 饿死消费者 - docs(net): 修正 FUN_PARA bit3 口径注释 + 补必要性论证
This commit is contained in:
@@ -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 <string.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
+187
-439
@@ -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) {
|
||||
|
||||
@@ -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 描述符
|
||||
|
||||
@@ -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; /* 累计发送字节数 */
|
||||
|
||||
@@ -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 真消失:交回主循环重试 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1583,6 +1583,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Drivers\BSP\NET\net_socket.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>net_listener.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Drivers\BSP\NET\net_listener.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>net_cfg.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| 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%** |
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
各区的容量与常量来源:
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 | 门禁(≤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 | 堆=配额 |
|
||||
|
||||
@@ -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] = <level>` 外部**写** | **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` | 加 `<File>` 条目;`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 会让
|
||||
「本地已修好」与「提交时仍报错」自相矛盾(本次踩到,白跑两轮)。
|
||||
|
||||
@@ -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 计量芯片驱动"),
|
||||
|
||||
@@ -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 描述符
|
||||
|
||||
Reference in New Issue
Block a user