refactor(net): 挂起 recv 模型拆出 net_pending.c/h + 留痕探针编译门控 + E15 归因修正
CI / L0 静态门禁 (pre-commit) (push) Failing after 2m14s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 22s

- E15 归因修正:幽灵状态 0x04+旧端口 = 芯片如实报告拆除中的旧连接
  (文档化行为,非芯片故障;bit3=1 为我方设置(ver>=0x46)、
  RFC 9293 §3.3.2 Fig5 拆除顺序、WCH 官方博客方案 = close 完成后重开监听)
- 术语统一:全库"了结"→"结束"(约 45 处);阻塞模型 §4 首次出现处定义"五路"
- 注释清理:删孤儿函数头注释、考古叙事改现在时、去不可检索内部编码"L3"
  (33 处 6 文件)、banner 去重(唯一理由移到分支现场)
- 挂起超时留痕探针:编译期开关 NET_PENDING_TRIPWIRE(默认 0 = 编译剔除,
  省约 150B Flash;排查调用方挂死时置 1,只留痕绝不强制结束)
- 拆模块:net_pending.c/h(363/94 行)承载挂起 recv 模型,
  net_socket.c 4059→3661 行;tick 收控制块表参数、settle_close 双参显式传;
  net_pending_active 声明移配对头(S17 2.2);仅注册 uvprojx +
  check_registration MCU-only 白名单(同 net_listener.c 机制,
  模拟器走 net_socket_sim.c 按外部语义模拟)
- 文档:阻塞模型新增全过程小节(单主干流程图)+ §9 排查辅助;
  NET README / 函数调用关系 / 自愈架构 / BSD 指南同步;引用关系图重生成
- 台架回归:启动闭环 PASS([ERR]/Fault 零命中、回读一致)、
  :7003 重连探针 10/10、FTP 50 连接 STOR 50/50、回归窗口串口日志无异常
- RAM:ER_IROM1 230004B(探针门控后约 -150B),台账已回填
This commit is contained in:
2026-10-14 09:53:38 +08:00
parent a12367981d
commit df13dec479
30 changed files with 3396 additions and 2993 deletions
+2 -3
View File
@@ -143,9 +143,8 @@ extern "C" {
* CH395F_SPI_DMA_MAX_PAYLOAD: 单次 DMA 事务最大有效载荷(总缓冲 - 4 字节命令头)
* 缓冲按"一条命令读空整个 Socket 接收缓冲(2KB)"定容——排空契约(TRAP-NET-20,
* bit3=1 残数据由 MCU 读出)的形态本身就是整缓冲单读。
* (历史注记:旧注释把定容理由归给"部分读致 recv_len 归零、剩余数据不可见",该机理
* 2026-10-11 复核**未证实**且与手册 §5.34 相左,已降级为未证实历史观察,结案见
* `CH395F_Trap_Records.md` 台架纪律;定容不依赖该观察。)
* (旧解释"部分读致 recv_len 归零、剩余数据不可见"未证实且与手册 §5.34 相左,
* 定容不依赖该解释;结案见 `CH395F_Trap_Records.md` 台架纪律。)
* 2026-09-22:与 MSS 解耦(此前 = 2×MSS+4,MSS 下调会把 DMA 缓冲缩到装不下整缓冲
* → 整缓冲读会被截断丢弃)。
*/
+3 -2
View File
@@ -7,7 +7,8 @@
| 文件 | 作用 |
|---|---|
| `net_socket.c/h` | **BSD Socket API 实现**:`net_socket` / `net_connect` / `net_send` / `net_recv` / `net_close` / `net_listen` / `net_accept`,含 L3 挂起(pending)与错误码 |
| `net_socket.c/h` | **BSD Socket API 实现**:`net_socket` / `net_connect` / `net_send` / `net_recv` / `net_close` / `net_listen` / `net_accept`,含挂起 recv(pending)与错误码 |
| `net_pending.c/h` | **挂起 recv 请求表**(阻塞 recv 挂起模型:登记 / 每轮完成检查 / 取消 / close 结算;真机专属,模拟器走 `net_socket_sim.c`) |
| `net_select.c/h` | `select`/`poll` 多路复用(基于芯片中断事件 + 周期轮询) |
| `net_types.h` | 网络层公共类型定义 |
| `net_config.h` | 网络层编译期配置(缓冲区大小、超时、对账周期等) |
@@ -15,7 +16,7 @@
## 核心机制
### 阻塞模型(L3 pending)
### 阻塞模型(挂起 recv / pending)
- 一期(RECV)已落地:`net_recv` 等支持带超时的阻塞(`pending` 挂起 + tick 唤醒)
- 二期(ACCEPT + SEND)待做 → `docs/任务规划/登记簿.md` N-4
+11 -14
View File
@@ -6,16 +6,13 @@
#include <string.h>
/*
* E-02 头阻塞修复 + L3 pending 化(2026-09-22,M3 台架实证):
* netTask 是全部 socket 操作的唯一执行者;lftpd 原先用默认 30s 的**阻塞**
* net_recv 等待对端下一行/下一块数据 → 只要客户端连上却不发完整行,netTask
* 就被长期占用,其余任务(含远端显示)的网络操作整体冻结(实测一次空闲 FTP
* 登录让 LCDMEM 整帧等待 17.7s)。
* 2026-09-22 先落地分片有界读(5ms 片;实测 100ms 片下受压帧仍达 ~750ms,
* 收到 5ms 后降到 ~10ms 量级,见 m3_data 修复后数据),同日 L3 pending 化
* 后改为"挂起请求":每次等待 = 一次挂起登记(netTask 零占用,完成按主循环
* 节拍检查),调用方单次超时 = 剩余总预算(总等待 ≤ LFTPD_TOTAL_BUDGET_MS,
* **总超时语义与旧实现一致**),旧分片协议退役。
* E-02 头阻塞修复 + recv pending 化(M3 台架实证):
* netTask 是全部 socket 操作的唯一执行者;lftpd 若用**阻塞** net_recv 等待对端
* 下一行/下一块数据 → 只要客户端连上却不发完整行,netTask 就被长期占用,
* 其余任务(含远端显示)的网络操作整体冻结(实测一次空闲 FTP 登录让 LCDMEM
* 整帧等待 17.7s)。
* 现形态:每次等待 = 一次挂起登记(netTask 零占用,完成按主循环节拍检查),
* 调用方单次超时 = 剩余总预算(总等待 ≤ LFTPD_TOTAL_BUDGET_MS)。
*/
#define LFTPD_TOTAL_BUDGET_MS 30000U
@@ -133,8 +130,8 @@ int lftpd_inet_read_line(int socket, char *buffer, size_t buffer_len)
int total_read_len = 0;
while (total_read_len < (int)buffer_len) {
/* 单次预算 = 剩余总预算(L3 pending:等待期 netTask 零占用,调用方
* 在自己上下文睡任务通知,旧片间 vTaskDelay(1) 让出退役) */
/* 单次预算 = 剩余总预算(pending:等待期 netTask 零占用,调用方
* 在自己上下文睡任务通知) */
TickType_t elapsed = xTaskGetTickCount() - t0;
uint32_t rem = (elapsed >= (TickType_t)pdMS_TO_TICKS(LFTPD_TOTAL_BUDGET_MS)) ? 0U
: (uint32_t)((TickType_t)LFTPD_TOTAL_BUDGET_MS - elapsed);
@@ -215,8 +212,8 @@ int lftpd_inet_write(int socket, const void *buf, int len)
int lftpd_inet_read(int socket, void *buf, int len)
{
/* L3 pending 读:总预算 30s 不变,等待期 netTask 零占用
* (空闲数据连接不再冻结全网;旧分片协议随 L3 pending 化退役) */
/* pending 读:总预算 30s 不变,等待期 netTask 零占用
* (空闲数据连接不再冻结全网) */
TickType_t t0 = xTaskGetTickCount();
for (;;) {
+7 -6
View File
@@ -33,9 +33,10 @@
#include "net_socket.h" /* net_sock_t / net_get_sock / NET_MAX_SOCKETS / net_set_errno */
#include "net_listener.h"
#include "net_pending.h" /* net_pending_active(挂起 recv 保险查询) */
#include "ch395f.h"
#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */
#include "net_latency_test.h" /* L3 探针接口(relisten 恢复计时采样) */
#include "net_latency_test.h" /* 延迟探针接口(relisten 恢复计时采样) */
#include "main.h"
#include "dbg_log.h"
@@ -182,7 +183,7 @@ static int net_relisten_locked(int sockfd) {
* 其它在用 Socket 的 CONNECT/DISCONNECT/RECV_OK(net_poll 门控焊死);
* ③ M1 使沉降成为每次断连热路径后,该搁浅被放大为 FTP 控制/数据通道
* 互杀(E7 实测:lftpd 5s 超时、closed-with-data 救回全部迟到)。
* 本 Socket 残留全局位交 net_poll 正常读到时 SINT 已为 0,无害了结 */
* 本 Socket 残留全局位交 net_poll 正常读到时 SINT 已为 0,自然收尾 */
/* 读芯片当前状态(两字节) */
ch395f_get_socket_status((uint8_t)sockfd, st);
@@ -289,10 +290,10 @@ static int net_relisten_locked(int sockfd) {
* 限定条件:netTask 上下文;由 handle_disconnect_event / handle_timeout_event 调用
* 函数说明:1. 只置事实,不做动作 —— 重建由 net_reconcile_tick(形态③④)/
* net_listener_try 负责
* 2. 越界 sockfd 静默忽略(与旧直接写 s_relisten_quarantine 行为一致)
* 2. 越界 sockfd 静默忽略
*/
void net_listener_settle(int sockfd, net_listener_settle_t level) {
/* 描述符越界:静默忽略(与旧直接写数组行为一致,不设错误码) */
/* 描述符越界:静默忽略,不设错误码 */
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
return;
}
@@ -399,7 +400,7 @@ int net_listener_try(int sockfd) {
* 返回值:无
* 限定条件:netTask 上下文(由 net_process_messages 每轮调用)
* 函数说明:1. 到点重试待办的重监听;成功即报告探针样本
* 2. 本 fd 有在途挂起 recv 时跳过(net_pending_active,L3 保险:
* 2. 本 fd 有在途挂起 recv 时跳过(net_pending_active,挂起 recv 保险:
* 重开 Socket 会摧毁在途连接)
* 3. 超过 NET_RELISTEN_DEADLINE_MS 停止 D10 快速重试并留痕
* (监听不永久失效——reconciler 100ms 周期仍按"芯片不在听"认领重建)
@@ -429,7 +430,7 @@ void net_listener_retry_tick(void) {
continue;
}
}
/* 本 fd 有在途挂起 recv:重开 Socket 会摧毁在途连接(L3 保险) */
/* 本 fd 有在途挂起 recv:重开 Socket 会摧毁在途连接(挂起 recv 保险) */
if (net_pending_active(i) != 0U) {
continue;
}
+3 -3
View File
@@ -49,7 +49,7 @@ typedef enum {
* 限定条件:netTask 上下文;由 handle_disconnect_event / handle_timeout_event 调用
* 函数说明:1. 只置事实,不做动作 —— 重建由 net_reconcile_tick(形态③④)/
* net_listener_try 负责
* 2. 越界 sockfd 静默忽略(不设错误码,与旧直接写数组行为一致)
* 2. 越界 sockfd 静默忽略(不设错误码)
*/
void net_listener_settle(int sockfd, net_listener_settle_t level);
@@ -90,7 +90,7 @@ void net_listener_retry_tick(void);
* net_reconcile_tick 单一 100ms tick(判据读驱动状态缓存,《网络自愈架构设计》§4.1)。
* 本模块的 net_listener_try 由 reconciler 形态③④ 与 D10 重试轮询调用。 */
/* 注:本模块消费的宿主能力 `net_pending_active()` 声明在 net_socket.h
* (它是 net_socket.c 的函数,须与配对头文件同处,见规范 S17 2.2)。 */
/* 注:本模块消费的宿主能力 `net_pending_active()` 声明在 net_pending.h
* (它是 net_pending.c 的函数,须与配对头文件同处,见规范 S17 2.2)。 */
#endif /* NET_LISTENER_H */
+416
View File
@@ -0,0 +1,416 @@
/*
* 模块名称:挂起 recv 请求表(阻塞 recv 挂起模型)—— 实现
* 文件说明:见 net_pending.h;架构与五路语义见
* docs/网络/Net层阻塞模型架构设计.md
* 依赖(全部单向,无反向 include):
* net_types.h(net_sock_t / NET_ERR_* / NET_SOCK_STATE_*)/
* net_socket.h(NET_MAX_SOCKETS / net_set_errno / NET_ERR_RETURN_1 /
* net_pending_active 声明)/ net_pending.h(本模块接口)/
* net_listener.h(net_listener_try)/ ch395f.h(RX 读出)/ main.h(HAL_GetTick)
* 限定条件:本文件真机专属——模拟器走 sim/net_socket_sim.c,
* simulator/CMakeLists.txt 不编译本文件(同 net_listener.c 先例)
*
* 阻塞 recv 挂起模型(E-02 结构演进):阻塞等待 = 挂起请求 + 主循环节拍完成
* "无数据 + 阻塞等待"不再自旋占用 netTask:登记 pending 后立即返回
* net_pending_tick(与 D10 重监听重试同节奏,net_process_messages 开头每轮调用)
* 按 ①数据 ②到点(仅显式预算) ③断连 顺序检查完成条件并通知调用方
* 关闭/网络重建/陈旧重同步走显式 settle/cancel 路径(取消必通知,调用方永不挂死)
* 公开 API 返回契约:>0 字节数 / 0 EOF / -1 错误(错误码经通知值回传)
* 预算归调用方:timeout_ms=0 即无限等待(无默认预算);挂起超时留痕探针为
* 编译期选项(NET_PENDING_TRIPWIRE,默认 0 = 编译剔除)
*/
#include <stddef.h>
#include "FreeRTOS.h"
#include "task.h"
#include "net_types.h"
#include "net_socket.h"
#include "net_pending.h"
#include "net_listener.h"
#include "ch395f.h"
#include "dbg_log.h"
#include "main.h"
typedef struct {
uint8_t active; /* 1 = 本 fd 有挂起 recv */
uint8_t infinite; /* 1 = 无限等待(无 deadline;0 预算 = 永等) */
#if NET_PENDING_TRIPWIRE
uint8_t tripwire_logged; /* 1 = 本轮挂起已留痕(保证只留一条) */
#endif
void *buf; /* 调用方缓冲(仅指针,须保持有效至完成) */
int len; /* 缓冲容量 */
uint32_t deadline; /* 截止时刻(tick)= 登记时刻 + 本次预算(infinite=1 时无意义) */
uint32_t registered; /* 登记时刻(tick;留痕探针/日志基准) */
TaskHandle_t caller; /* 调用方任务(完成通知目标) */
} net_pending_t;
/* 每 fd 一个挂起(禁止覆盖:覆盖丢失前调用方通知 → 挂死) */
static net_pending_t s_net_pending[NET_MAX_SOCKETS];
/* 执行任务(net_init 捕获;防自通知死锁) */
static TaskHandle_t s_owner_task;
/*
* 函数功能:捕获执行任务句柄(登记守卫据此拒绝自等待死锁)
* 入口参数:无
* 出口参数:无
* 返回值:无
* 限定条件:执行任务上下文(NET 层 = netTask / 硬件层 = testTask),net_init 顶部
*/
void net_pending_capture_owner(void) {
s_owner_task = xTaskGetCurrentTaskHandle();
}
/*
* 函数功能:通知调用方并清除本 fd 的挂起请求(所有完成/取消路径的公共出口)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* result - 32 位结果原值(>0 字节数 / 0 EOF / 负错误码) int
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文;sockfd 合法且挂起在位(否则静默返回,防重复通知)
* 函数说明:通知值 = 结果本身(与消息分派同一回传通道,调用方直接按通知值
* 取结果);失败记入 netTask 自己的 errno 槽位(E-03)
*/
static void net_pending_complete(int sockfd, int result) {
/* 本 fd 挂起请求指针 */
net_pending_t *p;
/* 描述符越界:静默返回(防重复通知) */
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
return;
}
/* 取挂起请求 */
p = &s_net_pending[sockfd];
/* 无在位挂起(已结束/已取消):静默返回 */
if (p->active == 0U) {
return;
}
/* 失败结果:记入 netTask 自己的错误码槽位(E-03;不再写入跨任务共享全局) */
if (result < 0) {
(void)net_set_errno(result);
}
/* 覆盖式写入通知值 = 结果本身(32 位原值) */
xTaskNotify(p->caller, (uint32_t)result, eSetValueWithOverwrite);
/* 清除挂起状态 */
p->active = 0U;
}
/*
* 函数功能:登记一个挂起 recv 请求(net_recv_locked 的阻塞无数据路径)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* buf - 调用方接收缓冲(须保持有效至完成) void *
* len - 缓冲容量 int
* to_ms - 本次等待预算(0 = 无限等待) uint32_t
* caller - 调用方任务句柄(完成通知目标) TaskHandle_t
* 出口参数:无
* 返回值:NET_PENDING_REGISTERED - 登记成功(分派跳过即时通知)
* -1 - 拒绝(同 fd 已有挂起 / 执行任务自身发起)
* 限定条件:netTask 上下文
* 函数说明:每 fd 至多一个挂起(禁止覆盖——覆盖丢失前调用方通知 = 该任务永久挂死);
* 执行任务自身发起的阻塞等待无他人可完成(tick 跑在执行任务内)→ 自等自
* 通知 = 死锁,必须拒绝(netTask 上下文调用方须用 DONTWAIT)
*/
int net_pending_register(int sockfd, void *buf, int len,
uint32_t to_ms, TaskHandle_t caller) {
/* 挂起请求指针 */
net_pending_t *p;
/* 登记时刻(tick) */
uint32_t now;
/* 描述符越界 → BADF */
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 取挂起请求 */
p = &s_net_pending[sockfd];
/* 同 fd 已有挂起:拒绝(覆盖将丢失前通知 = 该任务永久挂死) */
if (p->active != 0U) {
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* netTask 自身发起:无他人可完成(tick 跑在 netTask 内)→ 拒绝(自等死锁) */
if ((caller == NULL) || (caller == s_owner_task)) {
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* 当前 tick(截止/留痕探针基准) */
now = HAL_GetTick();
/* 标记挂起在位 */
p->active = 1U;
/* 预算形态:0 = 无限等待(无 deadline,tick ② 跳过;留痕探针以登记时刻计) */
p->infinite = (to_ms == 0U) ? 1U : 0U;
#if NET_PENDING_TRIPWIRE
/* 留痕标记清零(每轮挂起只留一条) */
p->tripwire_logged = 0U;
#endif
/* 调用方接收缓冲(须保持有效至完成) */
p->buf = buf;
/* 缓冲容量 */
p->len = len;
/* 截止时刻 = 登记时刻 + 预算(无限等待时不启用,置 0 仅为可观测性) */
p->deadline = (to_ms == 0U) ? 0U : (now + to_ms);
/* 登记时刻(留痕探针基准) */
p->registered = now;
/* 完成通知目标 */
p->caller = caller;
/* 登记成功:分派跳过即时通知 */
return NET_PENDING_REGISTERED;
}
/*
* 函数功能:挂起 recv 完成检查(netTask 每轮 net_process_messages 调用一次,非阻塞)
* 入口参数:p_socks - Socket 控制块表首指针(net_socket.c 的 s_net_socks) net_sock_t *
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文
* 函数说明:1. 逐 fd 检查完成条件:数据到达 / 到点(仅显式预算)/ 断连
* 2. 完成即经 net_pending_complete 通知并清除
* 3. 挂起超时留痕探针(编译期选项 NET_PENDING_TRIPWIRE,默认关):挂起超阈值
* 只 [ERR] 留痕一次("完成逻辑缺位"探针),绝不强制结束、绝不伪造返回值
*/
void net_pending_tick(net_sock_t *p_socks) {
/* 当前 tick */
uint32_t now = HAL_GetTick();
/* Socket 循环索引 */
int i;
for (i = 0; i < NET_MAX_SOCKETS; i++) {
/* 本 fd 挂起请求 */
net_pending_t *p = &s_net_pending[i];
/* 本 fd 控制块 */
net_sock_t *p_sock = &p_socks[i];
/* 无挂起请求:跳过 */
if (p->active == 0U) {
continue;
}
/* fd 已关闭 */
if (p_sock->in_use == 0U) {
/* fd 已关闭(正常路径在 close 内先结束;此处兜底,防任何路径漏掉通知) */
net_pending_complete(i, NET_ERR_CONNRESET);
continue;
}
/* ① 数据到达(数据优先)——readable 门控:常见无数据路径零额外 SPI 事务
* (INT# 电平触发链保证缓冲有数据时每轮重读 SINT 重位置位) */
if ((p_sock->state == NET_SOCK_STATE_ESTABLISHED) ||
(p_sock->state == NET_SOCK_STATE_UDP_OPEN)) {
/* 可读标志置位 */
if (p_sock->readable != 0U) {
/* 查询芯片侧可用字节数 */
int avail = ch395f_get_recv_len((uint8_t)i);
/* 有数据 */
if (avail > 0) {
/* 本次读取量 = min(可用, 缓冲容量) */
int rd = (avail > p->len) ? p->len : avail;
/* 读入调用方缓冲区 */
ch395f_read_recv_buf((uint8_t)i, (uint8_t *)p->buf, (uint16_t)rd);
/* 累计接收字节统计 */
p_sock->recv_bytes += (uint32_t)rd;
/* 数据已消费,清可读标志 */
p_sock->readable = 0U;
/* 结束:以字节数通知 */
net_pending_complete(i, rd);
continue;
}
/* 虚假标志:缓冲无数据,继续等待 */
p_sock->readable = 0U;
}
}
/* ② 到点(仅显式预算登记;无限等待登记无 deadline):
* 超时是调用方策略,net_recv_to 的预算在此兑现,不是伪造。
* tick 差值按有符号比较,抗回绕) */
if ((p->infinite == 0U) && ((int32_t)(now - p->deadline) >= 0)) {
/* 结束:超时(错误码由 net_pending_complete 记入本任务槽位) */
net_pending_complete(i, NET_ERR_TIMEDOUT);
continue;
}
/* ③ 断连:排空 → 数据或 EOF(0) + 自动重监听。
* 判据同 net_recv_locked(N-32 P1 不变量 I):`state == CLOSED ||
* session_dead`。只判 CLOSED 时,抢先 relisten 后 ①(要求 ESTABLISHED)
* 与 ③(要求 CLOSED) 两头落空 → 挂起的 recv 只能干等 ② deadline 超时
* (lftpd 阻塞 recv 的"消费者卡死"原型)。无限等待登记没有 ②,
* 漏 ③ = 永久挂死(留痕探针只留痕、不唤醒)——
* ③ 与显式 cancel 是无限 recv 的全部唤醒责任方,勿动判据。 */
if ((p_sock->state == NET_SOCK_STATE_CLOSED) || (p_sock->session_dead != 0U)) {
/* 查询残留字节数 */
int rem = ch395f_get_recv_len((uint8_t)i);
/* 有残留数据:按数据完成 */
if (rem > 0) {
/* 本次读取量 = min(残留, 缓冲容量) */
int rd = (rem > p->len) ? p->len : rem;
/* 读入调用方缓冲区 */
ch395f_read_recv_buf((uint8_t)i, (uint8_t *)p->buf, (uint16_t)rd);
/* 累计接收字节统计 */
p_sock->recv_bytes += (uint32_t)rd;
/* 数据已消费,清可读标志 */
p_sock->readable = 0U;
/* 结束:以字节数通知 */
net_pending_complete(i, rd);
} else {
/* 启用自动重监听:先把监听器重新置回 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-NET-18 待办 #3:异常释放(TIMEOUT/RST)不是正常 EOF(对端未发正常 FIN)
* → 按连接重置结束:应用(lftpd STOR)对异常结束回 5xx 而非误报 226;
* 正常 FIN(DISCONNECT)仍回 0(在途 FIN 与正常完成不可区分——协议边界,见 TRAP-NET-18 记录) */
/* 异常释放:按连接重置结束(回 5xx,不误报 226) */
if (p_sock->released_abnormal != 0U) {
/* 异常释放(错误码由 net_pending_complete 记入本任务槽位) */
net_pending_complete(i, NET_ERR_CONNRESET);
} else {
/* 正常 EOF:以 0 结束 */
net_pending_complete(i, 0);
}
}
continue;
}
/* 其他状态(CONNECTING/LISTENING/…):保持等待状态迁移——CONNECT→ESTABLISHED
* 是合法迁移,不做状态推断取消,避免误杀正在建链的等待;陈旧 resync 由显式 cancel 处理 */
/* ④ 挂起超时留痕探针(编译期选项 NET_PENDING_TRIPWIRE,默认关):挂起超
* 阈值 = "完成逻辑缺位"探针 → 只留一条 [ERR],**绝不强制结束、绝不伪造
* TIMEDOUT 返回值**(强制结束会把完成路径缺陷伪装成普通超时,调用方
* 拿到假结果、缺陷永远潜伏)。判据:无限登记按登记时刻计;显式登记按
* deadline 计(② 正常不会让它越过阈值,能到这里本身就是完成逻辑缺位)。 */
#if NET_PENDING_TRIPWIRE
if (p->tripwire_logged == 0U) {
/* 探针基准时刻:无限 = 登记时刻;显式 = 截止时刻 */
uint32_t base = (p->infinite != 0U) ? p->registered : p->deadline;
if ((int32_t)(now - base) >= (int32_t)NET_RECV_TRIPWIRE_MS) {
/* 留痕(挂起已持续毫秒数,自基准起算) */
DBG_ERROR("sock%d recv pending %ums (base=%s): completion source missing",
i, (unsigned)((int32_t)now - (int32_t)base),
(p->infinite != 0U) ? "infinite" : "deadline");
/* 只留痕一次(每轮挂起一条,防日志风暴) */
p->tripwire_logged = 1U;
}
}
#endif
}
}
/*
* 函数功能:查询本 fd 是否有在途挂起 recv —— 宿主能力,供 net_listener 用
* 入口参数:sockfd - Socket 描述符 (0~7) int
* 出口参数:无
* 返回值:1 - 有挂起;0 - 无(含描述符越界) int
* 限定条件:任意上下文(纯 RAM 读,不发起 RPC)
* 函数说明:1. N-32 §10 模块化:net_listener_retry_tick 用它跳过"重开 Socket
* 会摧毁在途连接"的 fd(挂起 recv 保险,原为直接读 s_net_pending[i].active)
* 2. 声明在 net_pending.h(配对头,规范 S17 2.2)
*/
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
* 出口参数:无
* 返回值:无
* 限定条件:无
* 函数说明:1. 静默清理不发通知:close 路径先经 settle 通知,本调用仅保证状态归零(幂等)
* 2. 新建 fd 时调用,防复用槽位继承上一条连接的挂起请求
*/
void net_pending_clear(int sockfd) {
/* 描述符越界:直接返回 */
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
return;
}
/* 挂起状态清零(静默,不发通知) */
s_net_pending[sockfd].active = 0U;
}
/*
* 函数功能:取消本 fd 的挂起请求并通知调用方(显式路径:陈旧 resync 等)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* err - 通知的错误码(如 NET_ERR_CONNRESET) int
* 出口参数:无
* 返回值:无(无在位挂起时静默返回)
* 限定条件:netTask 上下文
* 函数说明:经 net_pending_complete 以指定错误码通知调用方(取消必通知,防挂死)
*/
void net_pending_cancel(int sockfd, int err) {
/* complete 内部校验范围与在位(未在位则静默返回) */
net_pending_complete(sockfd, err);
}
/*
* 函数功能:取消全部挂起请求并逐一通知(网络重建路径)
* 入口参数:err - 通知的错误码 int
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文;须在控制块清零与芯片复位之前调用
* 函数说明:网络重建(net_stack_bring_up)前调用,保证所有在位调用方均被唤醒
*/
void net_pending_cancel_all(int err) {
/* 逐 fd 遍历 */
for (int i = 0; i < NET_MAX_SOCKETS; i++) {
/* 取消并通知(未在位则静默跳过) */
net_pending_complete(i, err);
}
}
/*
* 函数功能:关闭前先结束本 fd 的挂起 recv——把结果回传并唤醒调用方(有数据先
* 送达,无数据则取消),再关闭 Socket(per-fd 串行:先完成、再关闭)
* 入口参数:sockfd - 将被关闭的描述符 (0~7) int
* p_sock - 将被关闭的 Socket 控制块指针(与 sockfd 对应) net_sock_t *
* 出口参数:无
* 返回值:无(无在位挂起时静默返回)
* 限定条件:netTask 上下文;须在 ch395f_close_socket 之前调用
* 函数说明:先排空尝试——关闭不丢已到达数据(芯片缓冲在 close 前可读);
* 有数据按字节数通知,否则取消(NET_ERR_CONNRESET)
*/
void net_pending_settle_close(int sockfd, net_sock_t *p_sock) {
/* 通知用结果值(默认为取消) */
int val;
/* 芯片 RX 可用字节数 */
int avail;
/* 描述符越界或无在位挂起:静默返回 */
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
return;
}
if (s_net_pending[sockfd].active == 0U) {
return;
}
/* 默认结果 = 取消(CONNRESET) */
val = NET_ERR_CONNRESET;
/* 查询芯片 RX 可用字节(缓冲内数据在 close 前可读) */
avail = ch395f_get_recv_len((uint8_t)sockfd);
/* 数据已到达:排空,关闭不丢数据 */
if (avail > 0) {
/* 本次读取量 = min(可用, 缓冲容量) */
int rd = (avail > s_net_pending[sockfd].len) ? s_net_pending[sockfd].len : avail;
/* 读入调用方缓冲区 */
ch395f_read_recv_buf((uint8_t)sockfd, (uint8_t *)s_net_pending[sockfd].buf, (uint16_t)rd);
/* 累计接收字节统计 */
p_sock->recv_bytes += (uint32_t)rd;
/* 数据已消费,清可读标志 */
p_sock->readable = 0U;
/* 有数据:以字节数通知 */
val = rd;
}
/* 结束(必通知) */
net_pending_complete(sockfd, val);
}
+119
View File
@@ -0,0 +1,119 @@
/*
* 模块名称:挂起 recv 请求表(阻塞 recv 挂起模型)
* 文件说明:阻塞 recv 的"挂起请求"数据结构与生命周期:登记 / 每轮完成检查 /
* 取消(close、网络重建、对账 resync)/ 幂等清理。
* 架构与五路语义见 docs/网络/Net层阻塞模型架构设计.md
* 依赖:net_types.h(net_sock_t);消费方 = net_socket.c(消息分派 / close /
* 重建 / 对账)
* 限定条件:除 net_pending_active / net_pending_clear(纯 RAM 读)外,
* 全部函数须 netTask 上下文
*/
#ifndef NET_PENDING_H
#define NET_PENDING_H
#include <stdint.h>
#include "FreeRTOS.h"
#include "task.h"
#include "net_types.h"
/* 内部哨兵:pending 已登记(消息分派据此跳过即时通知;取值不与字节数空间冲突) */
#define NET_PENDING_REGISTERED 0x70000000
/* 挂起超时留痕探针:排查"调用方挂死唤不醒"时置 1——挂起超 NET_RECV_TRIPWIRE_MS
* 只留一条 [ERR](哪个 fd/多少毫秒/什么基准),绝不强制结束、绝不伪造返回值。
* 开与关语义完全一致,差别只在留痕;默认 0 = 产品构建零成本。
* 见 docs/网络/Net层阻塞模型架构设计.md §9 */
#define NET_PENDING_TRIPWIRE 0
#if NET_PENDING_TRIPWIRE
#define NET_RECV_TRIPWIRE_MS 60000U /* 挂起超时留痕阈值(ms) */
#endif
/*
* 函数功能:捕获执行任务句柄(net_init 顶部调用一次;登记守卫据此拒绝自等待死锁)
* 入口参数:无
* 出口参数:无
* 返回值:无
* 限定条件:执行任务(netTask / 硬件层 = testTask)上下文,创建消息队列前
*/
void net_pending_capture_owner(void);
/*
* 函数功能:登记一个挂起 recv 请求(net_recv_locked 的阻塞无数据路径)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* buf - 调用方接收缓冲(须保持有效至完成) void *
* len - 缓冲容量 int
* to_ms - 本次等待预算(0 = 无限等待) uint32_t
* caller - 调用方任务句柄(完成通知目标) TaskHandle_t
* 出口参数:无
* 返回值:NET_PENDING_REGISTERED - 登记成功(分派跳过即时通知)
* -1 - 拒绝(同 fd 已有挂起 / 执行任务自身发起;错误码经 NET_ERR_RETURN_1)
* 限定条件:netTask 上下文
* 函数说明:每 fd 至多一个挂起(禁止覆盖——覆盖丢失前调用方通知 = 该任务永久挂死);
* 执行任务自身发起的阻塞等待无他人可完成 → 自等自通知 = 死锁,必须拒绝
*/
int net_pending_register(int sockfd, void *buf, int len,
uint32_t to_ms, TaskHandle_t caller);
/*
* 函数功能:挂起 recv 完成检查(netTask 每轮 net_process_messages 调用一次,非阻塞)
* 入口参数:p_socks - Socket 控制块表首指针(net_socket.c 的 s_net_socks) net_sock_t *
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文
* 函数说明:逐 fd 按序判定:①数据到达 ②到点(仅显式预算) ③断连(排空→数据或
* EOF/CONNRESET);命中即经公共出口通知并清槽;另有编译期留痕探针
* (NET_PENDING_TRIPWIRE,默认关)
*/
void net_pending_tick(net_sock_t *p_socks);
/*
* 函数功能:清除本 fd 的挂起请求(不通知;"新 fd 不得继承"与 close 后的幂等清理)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* 出口参数:无
* 返回值:无
* 限定条件:无
*/
void net_pending_clear(int sockfd);
/*
* 函数功能:取消本 fd 的挂起请求并通知调用方(显式路径:对账 resync 等)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* err - 通知的错误码(如 NET_ERR_CONNRESET) int
* 出口参数:无
* 返回值:无(无在位挂起时静默返回)
* 限定条件:netTask 上下文
*/
void net_pending_cancel(int sockfd, int err);
/*
* 函数功能:取消全部挂起请求并逐一通知(网络重建路径)
* 入口参数:err - 通知的错误码 int
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文;须在控制块清零与芯片复位之前调用
*/
void net_pending_cancel_all(int err);
/*
* 函数功能:关闭前先结束本 fd 的挂起 recv——有数据先送达(按字节数),无数据则
* 取消(CONNRESET),然后调用方才执行 close/拆会话(per-fd 串行)
* 入口参数:sockfd - 将被关闭的描述符 (0~7) int
* p_sock - 将被关闭的 Socket 控制块指针(与 sockfd 对应) net_sock_t *
* 出口参数:无
* 返回值:无(无在位挂起时静默返回)
* 限定条件:netTask 上下文;须在 ch395f_close_socket 之前调用
*/
void net_pending_settle_close(int sockfd, net_sock_t *p_sock);
/*
* 函数功能:查询本 fd 是否有在途挂起 recv —— 宿主能力,供 net_listener 消费
* 入口参数:sockfd - Socket 描述符 (0~7) int
* 出口参数:无
* 返回值:1 - 有挂起;0 - 无(含描述符越界) int
* 限定条件:任意上下文(纯 RAM 读,不发起 RPC)
* 函数说明:N-32 §10 模块化:net_listener_retry_tick 用它跳过"重开 Socket
* 会摧毁在途连接"的 fd(挂起 recv 保险)
*/
int net_pending_active(int sockfd);
#endif /* NET_PENDING_H */
+40 -444
View File
@@ -19,9 +19,10 @@
#include "net_socket.h"
#include "net_listener.h"
#include "net_pending.h"
#include "ch395f.h"
#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */
#include "net_latency_test.h" /* L3 探针接口(chunk_wait 采样) */
#include "net_latency_test.h" /* 延迟探针接口(chunk_wait 采样) */
#include "main.h"
/*
@@ -104,7 +105,7 @@ static struct {
* 函数说明:1. 已存在的槽位(owner 命中)直接复用;空槽(owner == NULL)首次占用
* 2. 快路径**只读不写**:并发读者绝不会看到"owner 已写、err 未写"的中间态
* 3. 槽位与任务不强绑定释放:任务句柄被回收复用时,新任务沿用该槽位的
* err 初值无碍(语义本就允许残留码,与旧全局变量行为一致)
* err 初值无碍(语义本就允许残留码)
*/
static uint32_t net_errno_slot(void) {
/* 任务句柄(未启动内核/中断上下文时为 NULL) */
@@ -190,41 +191,6 @@ static int net_drop_conn_locked(net_sock_t *p_sock);
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);
/* L3 pending 化(一期,E-02 结构演进):阻塞等待 = 挂起请求 + 主循环节拍完成
* "无数据 + 阻塞等待"不再自旋占用 netTask:登记 pending 后立即返回
* net_pending_tick(与 D10 重监听重试同节奏,net_process_messages 开头每轮调用)
* 按 ①数据 ②到点(仅显式预算) ③断连 顺序检查完成条件并通知调用方(语义逐项对齐旧自旋循环)
* 关闭/网络重建/陈旧重同步走显式 settle/cancel 路径(取消必通知,调用方永不挂死)
* 公开 API 返回契约不变:>0 字节数 / 0 EOF / -1 错误(错误码经通知值回传)
* 自愈收敛阶段 1.5:预算归调用方——timeout_ms=0 即无限等待(30s 默认预算已删),
* 原"到点 +1s 强制了结"的看门狗降级为 tripwire(只留痕、绝不伪造返回值)
*/
#define NET_PENDING_REGISTERED 0x70000000 /* 内部哨兵:pending 已登记(分派跳过即时通知;取值不与字节数空间冲突) */
#define NET_RECV_TRIPWIRE_MS 60000U /* tripwire:挂起超 60s = 完成逻辑缺位探针 → 只 [ERR] 留痕一次,绝不强制了结 */
typedef struct {
uint8_t active; /* 1 = 本 fd 有挂起 recv */
uint8_t infinite; /* 1 = 无限等待(无 deadline,自愈收敛阶段 1.5:0 预算 = 永等) */
uint8_t tripwire_logged; /* 1 = 本轮挂起已打过 tripwire(保证只留痕一条) */
void *buf; /* 调用方缓冲(仅指针,须保持有效至完成) */
int len; /* 缓冲容量 */
uint32_t deadline; /* 截止时刻(tick)= 登记时刻 + 本次预算(infinite=1 时无意义) */
uint32_t registered; /* 登记时刻(tick;tripwire/日志基准) */
TaskHandle_t caller; /* 调用方任务(完成通知目标) */
} net_pending_t;
static net_pending_t s_net_pending[NET_MAX_SOCKETS]; /* 每 fd 一个挂起(禁止覆盖:覆盖丢失前调用方通知 → 挂死) */
static TaskHandle_t s_net_task_handle; /* 执行任务(net_init 捕获;防自通知死锁) */
static void net_pending_tick(void);
static void net_pending_complete(int sockfd, int result);
static int net_pending_register(int sockfd, void *buf, int len,
uint32_t to_ms, TaskHandle_t caller);
static void net_pending_clear(int sockfd);
static void net_pending_cancel(int sockfd, int err);
static void net_pending_cancel_all(int err);
static void net_pending_settle_close(net_sock_t *p_sock);
/*
* 函数功能:判断 6 字节 MAC 是否全零
* 入口参数:p_mac - MAC 指针(不可为 NULL) const uint8_t *
@@ -259,7 +225,7 @@ static bool s_mac_is_zero(const uint8_t *p_mac) {
int net_init(const char *ip, const char *mask, const char *gateway, const uint8_t *mac) {
ch395f_status_t ret;
/* L3:网络重建前取消全部挂起请求并通知调用方(调用方阻塞等回执,
/* 网络重建前取消全部挂起请求并通知调用方(调用方阻塞等回执,
* 取消不通知 = 永久挂死);必须早于控制块清零与芯片复位 */
net_pending_cancel_all(NET_ERR_CONNRESET);
@@ -267,7 +233,7 @@ static bool s_mac_is_zero(const uint8_t *p_mac) {
memset(s_net_socks, 0, sizeof(s_net_socks));
net_set_errno(NET_OK);
s_net_initialized = 0;
s_net_task_handle = xTaskGetCurrentTaskHandle(); /* 执行任务 = net_init 调用者(NET 层 = netTask / 硬件层 = testTask) */
net_pending_capture_owner(); /* 执行任务 = net_init 调用者(NET 层 = netTask / 硬件层 = testTask) */
/* 检查 CH395F 芯片 */
if (ch395f_check_exist() != CH395F_STATUS_OK) {
@@ -649,7 +615,7 @@ int net_poll(void) {
* U11 本层 CLOSED(监听型 fd)而芯片在听 → 只重同步,**不发芯片命令**
* (重下发因 USE 错误关掉正听着的 Socket,U12 同源事故)
* 4. 去抖分级(100ms/轮;E2:LISTEN 可见 100% <=10ms 无长尾):②=1 轮
* (幽灵甄别挡拆链窗假 ESTABLISHED);③④⑤=2 轮(2026-09-23 实证:
* (幽灵甄别挡拆链窗的旧连接 ESTABLISHED);③④⑤=2 轮(2026-09-23 实证:
* 无去抖时监听被反复重建直至放弃);①=4 轮(**勿改小**:F-NET-17 M3
* 可疑电平让位再入,400ms 让位窗=在途投递不可毁的最后租约,E9a 实证)
* 5. 动手后冷却 NET_RECONCILE_COOLDOWN_MS 防风暴(U11 被动重同步不占)
@@ -727,7 +693,8 @@ static void net_reconcile_tick(void) {
} else if (p_sock->state == NET_SOCK_STATE_LISTENING) {
if (c.tcp_state == CH395F_TCP_ESTABLISHED) {
/* ② 本层 LISTENING 而芯片已完成握手(CONNECT 事件未收到)→ 补做 accept
* N-30(TRAP-NET-20)幽灵态甄别:拆链收尾窗口内芯片假报 ESTABLISHED,
* N-30(TRAP-NET-20)幽灵态甄别:拆链收尾窗口内旧连接未拆净,状态如实
* 显示 ESTABLISHED(E15:bit3=1 保留现场契约 + TCP 拆链状态机,非谎报),
* 回读对端端口 = 上一条连接端口 → 不补 accept(补了会把数据发给
* 已死的旧连接 = 生产 LIST#4 数据被吞的直接成因),等拆净或新 SYN。
* 这是本 tick 唯一的按需 SPI(仅形态②命中时发) */
@@ -825,7 +792,7 @@ static void net_reconcile_tick(void) {
/*
* 函数功能:netTask 主循环核心 —— 自愈 tick 宿主 + 消息队列分派器。每轮
* 先跑三个非阻塞自愈 tick(D10 重监听 / L3 挂起 recv /
* 先跑三个非阻塞自愈 tick(D10 重监听 / 挂起 recv /
* TRAP-NET-17 状态对账),再非阻塞排空队列按 msg.type 分派到 _locked 实现。
* 入口参数:无
* 出口参数:无
@@ -835,9 +802,9 @@ static void net_reconcile_tick(void) {
* 由 net_task.c 在 net_stack_bring_up 之后每次 1ms 循环调用。
* 函数说明:1. 四个自愈 tick 均非阻塞(绝不 osDelay 忙等,netTask 永不冻结),先于消息分派执行
* 2. net_listener_retry_tick(D10):断连后按 30ms(NET_RELISTEN_RETRY_MS)
* 节拍重试 open+listen,2s 总时限到期放弃留痕;本 fd 有在途挂起 recv 时跳过(L3 保险)
* 3. net_pending_tick(L3):按节拍了结挂起 recv(数据/到点[仅显式预算]/断连);
* L3 tripwire(自愈收敛阶段 1.5):挂起超 60s 只 DBG_ERROR 留痕一次,不再强制了结/伪造返回值
* 节拍重试 open+listen,2s 总时限到期放弃留痕;本 fd 有在途挂起 recv 时跳过(挂起 recv 保险)
* 3. net_pending_tick:按节拍结束挂起 recv(数据/到点[仅显式预算]/断连);
* 挂起超时留痕探针(编译期选项,默认关):挂起超阈值只 DBG_ERROR 留痕一次,绝不强制结束/伪造返回值
* 4. net_reconcile_tick(TRAP-NET-17 + pump 合并,自愈收敛阶段 3):每 100ms 对账
* ①②⑤ + ③④ 重建 + U11 重同步,判据读驱动状态缓存(net_poll 每轮
* 全量刷新,E2 背书);去抖分级(②=1 轮/③④⑤=2 轮/①=4 轮,M3 让位窗)
@@ -858,8 +825,8 @@ void net_process_messages(void) {
/* 三个非阻塞自愈 tick 置于消息处理之前:断连后即使无应用消息,监听器也能自主恢复 */
/* D10 重监听重试(TRAP-NET-09):沉降后重开芯片监听器 */
net_listener_retry_tick();
/* L3 挂起 recv 完成检查:数据到达 / 到点 / 断连时通知调用方 */
net_pending_tick();
/* 挂起 recv 完成检查:数据到达 / 到点 / 断连时通知调用方 */
net_pending_tick(s_net_socks);
/* TRAP-NET-17 状态对账 + 监听器 pump(自愈收敛阶段 3 合并单一 100ms tick):
* 判据读驱动缓存(net_poll 每轮全量刷新),①②⑤ 失同步 + ③④ 重建 +
* U11 重同步全部在此(本层唯一周期重建入口,N-32 P2 单一所有者) */
@@ -917,7 +884,7 @@ void net_process_messages(void) {
net_set_errno(NET_ERR_BADF);
msg.result = -1;
} else {
/* 先了结本 fd 挂起 recv(排空或取消,必通知),再关芯片侧 Socket;
/* 先结束本 fd 挂起 recv(排空或取消,必通知),再关芯片侧 Socket;
* 单连接模型监听 fd 断开后自动恢复监听,应用不得对它在 EOF 时 close */
msg.result = net_close_locked(p_sock);
}
@@ -1105,7 +1072,7 @@ int net_socket(int domain, int type) {
p_sock->session_dead = 0;
/* 新 fd 不得继承上一次的待重试状态(D10) */
net_listener_clear(sockfd);
/* 新 fd 不得继承上一次挂起的 recv 请求(L3) */
/* 新 fd 不得继承上一次挂起的 recv 请求 */
net_pending_clear(sockfd);
/* 返回描述符(控制块已就绪) */
@@ -1334,7 +1301,7 @@ int net_listen_locked(int sockfd) {
* 可补回,见 TRAP-NET-08)。
* 全局寄存器交 net_poll 集中读清(AGENTS 模块约定:所有 CH395F 中断经
* net_poll 集中读取并清除);本 Socket 残留的全局位被 net_poll 读到时
* SINT 已为 0,无害了结。TRAP-NET-12/-18 指定的两级预清落在
* SINT 已为 0,自然收尾。TRAP-NET-12/-18 指定的两级预清落在
* net_connect_locked / net_close_locked / relisten 隔离段,均为单 Socket,
* 本次未动。 */
(void)ch395f_get_sock_int_status((uint8_t)sockfd);
@@ -1431,7 +1398,8 @@ int net_listen(int sockfd) {
* 永远等不到 → 数据被吞 / 425。固定沉降延时、Socket 轮换、listen 后 close 重试、
* 缓冲重分配均已被实验证伪;唯一有效 = "中断 + 状态轮询"双信源(修复组 15/15
* 全过,机制触发 5 次全部救回)。→ CH395F_Trap_Records.md TRAP-NET-20
* 幽灵态甄别:拆链窗口内芯片也会直接假报 ESTABLISHED(旧连接未拆净),此时回读
* 幽灵态甄别:拆链窗口内旧连接未拆净,状态如实为 ESTABLISHED(E15:bit3=1 保留现场
* 契约,非谎报),此时回读
* 对端端口 = 上一条连接端口;新连接临时端口几乎必然不同 → 端口相同不 accept,
* 等旧连接拆净 / 新 SYN 到达(新连接若事件也未收到,后续轮询以新端口命中)。 */
@@ -1549,7 +1517,7 @@ static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen
/* 当前无连接 → WOULDBLOCK */
return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK);
}
/* drain_pending:落入下方 accept 返回(对端簿记仍在控制块,排空后 EOF 了结) */
/* drain_pending:落入下方 accept 返回(对端簿记仍在控制块,排空后以 EOF 收尾) */
}
/* 填充对端地址(若调用方需要) */
@@ -1720,7 +1688,7 @@ int net_can_read(int sockfd) {
if (p_sock == NULL) {
return 1;
}
/* 连接已非 ESTABLISHED(CLOSED/LISTENING/…):需要一次 recv 了结 EOF/错误 */
/* 连接已非 ESTABLISHED(CLOSED/LISTENING/…):需要发一次 recv 取回 EOF/错误 */
if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) {
return 1;
}
@@ -2033,8 +2001,8 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) {
p_sock->send_bytes += (uint32_t)chunk;
#if defined(TEST_LATENCY_PROBE)
/* L3 探针起点:发送缓冲写入刚完成
* (样本 = 写缓冲 → SENBUF_FREE 等待,见 链路延迟测试说明 §3 L3) */
/* 延迟探针起点(chunk_wait):发送缓冲写入刚完成
* (样本 = 写缓冲 → SENBUF_FREE 等待,见 链路延迟测试说明 §3) */
uint32_t c_l3 = lat_cycles_now();
#endif
@@ -2086,12 +2054,12 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) {
* 返回值:实际接收字节数(> 0);0 表示对端关闭;-1 表示出错/非阻塞无数据 int
* NET_PENDING_REGISTERED 表示已登记挂起请求(分派据此跳过即时通知)
* 限定条件:p_sock 非空;netTask 上下文
* 函数说明:1. 数据已在(快速路径)→ 立即读出返回,行为与旧实现一致
* 函数说明:1. 数据已在(快速路径)→ 立即读出返回
* 2. 非阻塞(NET_MSG_DONTWAIT 单次覆盖 **或** socket blocking 属性为 0,
* 自愈收敛阶段 1.5 模式属性)无数据立即返回 -1(NET_ERR_WOULDBLOCK)
* 3. 无数据 + 阻塞等待 → 登记挂起请求(L3 pending 化,E-02 结构演进)
* 3. 无数据 + 阻塞等待 → 登记挂起请求(pending 化,E-02 结构演进)
* 4. netTask 零占用立即返回;完成条件(数据/到点[仅显式预算]/断连)由 net_pending_tick 按主循环节拍检查
* 5. 并通知 caller;关闭/网络重建/陈旧重同步由对应路径显式了结(取消必通知)
* 5. 并通知 caller;关闭/网络重建/陈旧重同步由对应路径显式结束(取消必通知)
* 6. CLOSED 状态语义(排空 + EOF + 自动重监听)逐项保留在 tick 内
*/
static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags,
@@ -2165,380 +2133,10 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags,
/* 无数据 + 阻塞等待:登记挂起请求,立即返回(netTask 任何时刻都不被占用)。
* 调用方在自己上下文睡任务通知(LwIP netconn 模型);等待期间 netTask
* 继续主循环(事件泵 + 队列),其他消费者零头阻塞。 */
/* 登记挂起请求(L3):返回值即登记结果 */
/* 登记挂起请求:返回值即登记结果 */
return net_pending_register(sockfd, buf, len, to_ms, caller);
}
/*
* 函数功能:关闭 Socket(内部锁定版本,必须在 netTask 上下文调用)
* 入口参数:p_sock - Socket 控制块指针 net_sock_t *
* 出口参数:无
* 返回值:0 - 关闭成功 int
* 限定条件:p_sock 非空;netTask 上下文
* 函数说明:1. 调用 ch395f_close_socket 关闭 CH395F 硬件 Socket
* 2. 清空整个控制块(memset 0),释放该 Socket 资源
*/
/* ===== L3 挂起 recv 请求的实现(一期,2026-09-22;自愈收敛阶段 1.5 语义主流化) =====
* 完成条件按 ①数据 ②到点(仅显式预算) ③断连 顺序检查(逐项对齐旧自旋循环语义)
* ① 数据通道(ESTABLISHED/UDP_OPEN)内 readable 门控 SPI 读——常见无数据路径零额外 SPI 事务
* (INT# 电平触发链保证缓冲有数据时每轮重读 SINT 重位置位)
* ② 到点(仅当登记时带显式预算 net_recv_to;无限等待登记无 deadline)→ NET_ERR_TIMEDOUT
* ——这是调用方策略的正常兑现,不是伪造
* ③ CLOSED:先排空(bit3=1 芯片断连后残数据留缓冲待 MCU 读——TRAP-NET-20 排空契约;
* 旧注归"部分读归零"机理、已随复核注降级)→ 有数据按数据完成,否则 EOF(0) + 自动重监听
* 其他状态(CONNECTING/LISTENING/…)保持等待状态迁移(CONNECT→ESTABLISHED 是合法路径;
* 陈旧 resync 由显式取消处理)——不做状态推断取消,避免误杀正在建链的等待
* tripwire(自愈收敛阶段 1.5 降级):挂起超 60s = "完成逻辑缺位"探针 → 只 [ERR] 留痕一次,
* **绝不强制了结、绝不伪造 TIMEDOUT 返回值**(唤醒责任在 ①②③ 与显式 cancel 路径)
*/
/*
* 函数功能:通知调用方并清除本 fd 的挂起请求(所有完成/取消路径的公共出口)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* result - 32 位结果原值(>0 字节数 / 0 EOF / 负错误码) int
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文;sockfd 合法且挂起在位(否则静默返回,防重复通知)
* 函数说明:通知值 = 结果本身(与消息分派同一回传通道,调用方 net_recv/net_recv_to
* 的映射逻辑不变);失败结果同步 errno 保持 netTask 上下文一致性
*/
static void net_pending_complete(int sockfd, int result) {
/* 本 fd 挂起请求指针 */
net_pending_t *p;
/* 描述符越界:静默返回(防重复通知) */
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
return;
}
/* 取挂起请求 */
p = &s_net_pending[sockfd];
/* 无在位挂起(已了结/已取消):静默返回 */
if (p->active == 0U) {
return;
}
/* 失败结果:记入 netTask 自己的错误码槽位(E-03;不再写入跨任务共享全局) */
if (result < 0) {
(void)net_set_errno(result);
}
/* 覆盖式写入通知值 = 结果本身(32 位原值) */
xTaskNotify(p->caller, (uint32_t)result, eSetValueWithOverwrite);
/* 清除挂起状态 */
p->active = 0U;
}
/*
* 函数功能:登记一个挂起 recv 请求(net_recv_locked 的阻塞无数据路径)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* buf - 调用方接收缓冲(须保持有效至完成) void *
* len - 缓冲容量 int
* to_ms - 本次等待预算(0 = 无限等待,自愈收敛阶段 1.5) uint32_t
* caller - 调用方任务句柄(完成通知目标) TaskHandle_t
* 出口参数:无
* 返回值:NET_PENDING_REGISTERED - 登记成功(分派跳过即时通知)
* -1 - 拒绝(同 fd 已有挂起 / 执行任务自身发起)
* 限定条件:netTask 上下文
* 函数说明:每 fd 至多一个挂起(禁止覆盖——覆盖丢失前调用方通知 = 该任务永久挂死);
* 执行任务自身发起的阻塞等待无他人可完成(tick 跑在执行任务内)→ 自等自
* 通知 = 死锁,必须拒绝(netTask 上下文调用方须用 DONTWAIT)
*/
static int net_pending_register(int sockfd, void *buf, int len,
uint32_t to_ms, TaskHandle_t caller) {
/* 挂起请求指针 */
net_pending_t *p;
/* 登记时刻(tick) */
uint32_t now;
/* 描述符越界 → BADF */
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 取挂起请求 */
p = &s_net_pending[sockfd];
/* 同 fd 已有挂起:拒绝(覆盖将丢失前通知 = 该任务永久挂死) */
if (p->active != 0U) {
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* netTask 自身发起:无他人可完成(tick 跑在 netTask 内)→ 拒绝(自等死锁) */
if ((caller == NULL) || (caller == s_net_task_handle)) {
return NET_ERR_RETURN_1(NET_ERR_BUSY);
}
/* 当前 tick(截止/tripwire 基准) */
now = HAL_GetTick();
/* 标记挂起在位 */
p->active = 1U;
/* 预算形态:0 = 无限等待(无 deadline,tick ② 跳过;tripwire 以登记时刻计) */
p->infinite = (to_ms == 0U) ? 1U : 0U;
/* tripwire 留痕标记清零(每轮挂起只留痕一条) */
p->tripwire_logged = 0U;
/* 调用方接收缓冲(须保持有效至完成) */
p->buf = buf;
/* 缓冲容量 */
p->len = len;
/* 截止时刻 = 登记时刻 + 预算(无限等待时不启用,置 0 仅为可观测性) */
p->deadline = (to_ms == 0U) ? 0U : (now + to_ms);
/* 登记时刻(tripwire 基准) */
p->registered = now;
/* 完成通知目标 */
p->caller = caller;
/* 登记成功:分派跳过即时通知 */
return NET_PENDING_REGISTERED;
}
/*
* 函数功能:挂起 recv 完成检查(netTask 每轮 net_process_messages 调用一次,非阻塞)
* 入口参数:无
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文
* 函数说明:1. 逐 fd 检查完成条件:数据到达 / 到点(仅显式预算)/ 断连
* 2. 完成即经 net_pending_complete 通知并清除
* 3. tripwire(自愈收敛阶段 1.5):挂起超 NET_RECV_TRIPWIRE_MS 只 [ERR] 留痕
* 一次("完成逻辑缺位"探针),绝不强制了结、绝不伪造返回值
*/
static void net_pending_tick(void) {
/* 当前 tick */
uint32_t now = HAL_GetTick();
/* Socket 循环索引 */
int i;
for (i = 0; i < NET_MAX_SOCKETS; i++) {
/* 本 fd 挂起请求 */
net_pending_t *p = &s_net_pending[i];
/* 本 fd 控制块 */
net_sock_t *p_sock = &s_net_socks[i];
/* 无挂起请求:跳过 */
if (p->active == 0U) {
continue;
}
/* fd 已关闭 */
if (p_sock->in_use == 0U) {
/* fd 已关闭(正常路径在 close 内先了结;此处兜底,防任何路径漏结) */
net_pending_complete(i, NET_ERR_CONNRESET);
continue;
}
/* ① 数据到达(数据优先,与旧自旋一致) */
if ((p_sock->state == NET_SOCK_STATE_ESTABLISHED) ||
(p_sock->state == NET_SOCK_STATE_UDP_OPEN)) {
/* 可读标志置位 */
if (p_sock->readable != 0U) {
/* 查询芯片侧可用字节数 */
int avail = ch395f_get_recv_len((uint8_t)i);
/* 有数据 */
if (avail > 0) {
/* 本次读取量 = min(可用, 缓冲容量) */
int rd = (avail > p->len) ? p->len : avail;
/* 读入调用方缓冲区 */
ch395f_read_recv_buf((uint8_t)i, (uint8_t *)p->buf, (uint16_t)rd);
/* 累计接收字节统计 */
p_sock->recv_bytes += (uint32_t)rd;
/* 数据已消费,清可读标志 */
p_sock->readable = 0U;
/* 了结:以字节数通知 */
net_pending_complete(i, rd);
continue;
}
/* 虚假标志:缓冲无数据,继续等待 */
p_sock->readable = 0U;
}
}
/* ② 到点(仅显式预算登记;无限等待登记无 deadline——自愈收敛阶段 1.5:
* 超时是调用方策略,net_recv_to 的预算在此兑现,不是伪造。
* tick 差值按有符号比较,抗回绕) */
if ((p->infinite == 0U) && ((int32_t)(now - p->deadline) >= 0)) {
/* 了结:超时(错误码由 net_pending_complete 记入本任务槽位) */
net_pending_complete(i, NET_ERR_TIMEDOUT);
continue;
}
/* ③ 断连:排空 → 数据或 EOF(0) + 自动重监听。
* 判据同 net_recv_locked(N-32 P1 不变量 I):`state == CLOSED ||
* session_dead`。只判 CLOSED 时,抢先 relisten 后 ①(要求 ESTABLISHED)
* 与 ③(要求 CLOSED) 两头落空 → 挂起的 recv 只能干等 ② deadline 超时
* (lftpd 阻塞 recv 的"消费者卡死"原型)。自愈收敛阶段 1.5 后该形态更致命:
* 无限等待登记没有 ②,漏 ③ = 永久挂死(只剩 tripwire 留痕)——
* ③ 与显式 cancel 是无限 recv 的全部唤醒责任方,勿动判据。 */
if ((p_sock->state == NET_SOCK_STATE_CLOSED) || (p_sock->session_dead != 0U)) {
/* 查询残留字节数 */
int rem = ch395f_get_recv_len((uint8_t)i);
/* 有残留数据:按数据完成 */
if (rem > 0) {
/* 本次读取量 = min(残留, 缓冲容量) */
int rd = (rem > p->len) ? p->len : rem;
/* 读入调用方缓冲区 */
ch395f_read_recv_buf((uint8_t)i, (uint8_t *)p->buf, (uint16_t)rd);
/* 累计接收字节统计 */
p_sock->recv_bytes += (uint32_t)rd;
/* 数据已消费,清可读标志 */
p_sock->readable = 0U;
/* 了结:以字节数通知 */
net_pending_complete(i, rd);
} else {
/* 启用自动重监听:先把监听器重新置回 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-NET-18 待办 #3:异常释放(TIMEOUT/RST)不是正常 EOF(对端未发正常 FIN)
* → 以连接重置了结:应用(lftpd STOR)对异常结束回 5xx 而非误报 226;
* 正常 FIN(DISCONNECT)仍回 0(在途 FIN 与正常完成不可区分——协议边界,见 TRAP-NET-18 记录) */
/* 异常释放:以连接重置了结(回 5xx,不误报 226) */
if (p_sock->released_abnormal != 0U) {
/* 异常释放(错误码由 net_pending_complete 记入本任务槽位) */
net_pending_complete(i, NET_ERR_CONNRESET);
} else {
/* 正常 EOF:以 0 了结 */
net_pending_complete(i, 0);
}
}
continue;
}
/* 其他状态:保持等待(见本区块头注释) */
/* ④ tripwire(自愈收敛阶段 1.5 降级,原"看门狗强制了结"):挂起超 60s =
* "完成逻辑缺位"探针 → 只 [ERR] 留痕一次,**绝不强制了结、绝不伪造
* TIMEDOUT 返回值**(旧实现到点 +1s 兜底伪造 TIMEDOUT,掩盖完成路径
* 缺陷;新语义下无限等待本就无预算,伪造返回值反而违背调用方意图)。
* 判据:无限登记按登记时刻计;显式登记按 deadline 计(② 正常不会让它
* 越过 60s,能到这里本身就是完成逻辑缺位)。 */
if (p->tripwire_logged == 0U) {
/* 探针基准时刻:无限 = 登记时刻;显式 = 截止时刻 */
uint32_t base = (p->infinite != 0U) ? p->registered : p->deadline;
if ((int32_t)(now - base) >= (int32_t)NET_RECV_TRIPWIRE_MS) {
/* 留痕(挂起已持续毫秒数,自基准起算) */
DBG_ERROR("sock%d recv pending %ums (base=%s): completion source missing",
i, (unsigned)((int32_t)now - (int32_t)base),
(p->infinite != 0U) ? "infinite" : "deadline");
/* 只留痕一次(每轮挂起一条,防日志风暴) */
p->tripwire_logged = 1U;
}
}
}
}
/*
* 函数功能:查询本 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
* 出口参数:无
* 返回值:无
* 限定条件:无
* 函数说明:1. 静默清理不发通知:close 路径先经 settle 通知,本调用仅保证状态归零(幂等)
* 2. 新建 fd 时调用,防复用槽位继承上一条连接的挂起请求
*/
static void net_pending_clear(int sockfd) {
/* 描述符越界:直接返回 */
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
return;
}
/* 挂起状态清零(静默,不发通知) */
s_net_pending[sockfd].active = 0U;
}
/*
* 函数功能:取消本 fd 的挂起请求并通知调用方(显式路径:陈旧 resync 等)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* err - 通知的错误码(如 NET_ERR_CONNRESET) int
* 出口参数:无
* 返回值:无(无在位挂起时静默返回)
* 限定条件:netTask 上下文
* 函数说明:经 net_pending_complete 以指定错误码通知调用方(取消必通知,防挂死)
*/
static void net_pending_cancel(int sockfd, int err) {
/* complete 内部校验范围与在位(未在位则静默返回) */
net_pending_complete(sockfd, err);
}
/*
* 函数功能:取消全部挂起请求并逐一通知(网络重建路径)
* 入口参数:err - 通知的错误码 int
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文;须在控制块清零与芯片复位之前调用
* 函数说明:网络重建(net_stack_bring_up)前调用,保证所有在位调用方均被唤醒
*/
static void net_pending_cancel_all(int err) {
/* 逐 fd 遍历 */
for (int i = 0; i < NET_MAX_SOCKETS; i++) {
/* 取消并通知(未在位则静默跳过) */
net_pending_complete(i, err);
}
}
/*
* 函数功能:关闭前了结本 fd 的挂起 recv(per-fd 串行:先完成、再关闭)
* 入口参数:p_sock - 将被关闭的 Socket 控制块指针 net_sock_t *
* 出口参数:无
* 返回值:无(无在位挂起时静默返回)
* 限定条件:netTask 上下文;须在 ch395f_close_socket 之前调用
* 函数说明:先排空尝试——关闭不丢已到达数据(芯片缓冲在 close 前可读);
* 有数据按字节数通知,否则取消(NET_ERR_CONNRESET)
*/
static void net_pending_settle_close(net_sock_t *p_sock) {
/* 由控制块指针反推描述符 */
int sockfd = (int)(p_sock - s_net_socks);
/* 通知用结果值(默认为取消) */
int val;
/* 芯片 RX 可用字节数 */
int avail;
/* 无在位挂起:静默返回 */
if (s_net_pending[sockfd].active == 0U) {
return;
}
/* 默认结果 = 取消(CONNRESET) */
val = NET_ERR_CONNRESET;
/* 查询芯片 RX 可用字节(缓冲内数据在 close 前可读) */
avail = ch395f_get_recv_len((uint8_t)sockfd);
/* 数据已到达:排空,关闭不丢数据 */
if (avail > 0) {
/* 本次读取量 = min(可用, 缓冲容量) */
int rd = (avail > s_net_pending[sockfd].len) ? s_net_pending[sockfd].len : avail;
/* 读入调用方缓冲区 */
ch395f_read_recv_buf((uint8_t)sockfd, (uint8_t *)s_net_pending[sockfd].buf, (uint16_t)rd);
/* 累计接收字节统计 */
p_sock->recv_bytes += (uint32_t)rd;
/* 数据已消费,清可读标志 */
p_sock->readable = 0U;
/* 有数据:以字节数通知 */
val = rd;
}
/* 了结(必通知) */
net_pending_complete(sockfd, val);
}
/*
* 函数功能:关闭 Socket(内部版本,不经过消息队列)
* 入口参数:p_sock - 要关闭的 Socket 控制块指针 net_sock_t *
@@ -2559,9 +2157,9 @@ static int net_close_locked(net_sock_t *p_sock) {
/* 由控制块指针反推描述符 */
int sockfd = (int)(p_sock - s_net_socks);
/* L3:先了结本 fd 的挂起 recv(排空/取消 + 必通知),再关芯片 Socket
/* 先结束本 fd 的挂起 recv(排空/取消 + 必通知),再关芯片 Socket
* ——关闭不得让等待中的调用方永久悬挂 */
net_pending_settle_close(p_sock);
net_pending_settle_close((int)(p_sock - s_net_socks), p_sock);
/* 排渣 + 关闭 + 等 CLOSED 一体(见函数头注释 1);
* 已 CLOSED 的 fd 只清本 Socket 残留事件(防下一连接误判"刚建立就断开") */
@@ -2579,7 +2177,7 @@ static int net_close_locked(net_sock_t *p_sock) {
p_sock->last_peer_port = keep_peer;
/* 关闭即取消该 fd 的待重试(D10 重试队列) */
net_listener_clear(sockfd);
/* 关闭即清除该 fd 的挂起状态(L3;幂等) */
/* 关闭即清除该 fd 的挂起状态(幂等) */
net_pending_clear(sockfd);
/* 关闭成功 */
@@ -2602,14 +2200,14 @@ static int net_close_locked(net_sock_t *p_sock) {
* (芯片已 CLOSED 直接 open 必成,E4 定案;失败交 D10 重试 +
* reconciler 形态④);非 auto_relisten(client fd):收口 CLOSED,
* 是否重连由调用方决定
* 4. 挂起 recv 先了结(settle_close:排空尝试或取消,必通知)
* 4. 挂起 recv 先结束(settle_close:排空尝试或取消,必通知)
*/
static int net_drop_conn_locked(net_sock_t *p_sock) {
/* 由控制块指针反推描述符 */
int sockfd = (int)(p_sock - s_net_socks);
/* L3:先了结本 fd 挂起 recv(排空/取消 + 必通知),再拆芯片会话 */
net_pending_settle_close(p_sock);
/* 先结束本 fd 挂起 recv(排空/取消 + 必通知),再拆芯片会话 */
net_pending_settle_close((int)(p_sock - s_net_socks), p_sock);
/* 确定性拆除(异常释放 → QUARANTINE 400ms 底,TRAP-NET-18 语义) */
if (net_sock_reset_sync(sockfd, CH395F_RESET_QUARANTINE) != 0) {
@@ -3263,7 +2861,7 @@ int net_send_sock(net_sock_t *p_sock, const void *buf, int len) {
* -1 - 接收失败 int
* 限定条件:必须在 netTask 上下文中调用;p_sock->in_use == 1
* 函数说明:1. 直接操作 Socket 控制块,不经过消息队列。避免 netTask 向自身发送消息造成死锁
* 2. L3 后:netTask 上下文的阻塞等待会被挂起登记拒绝(自通知死锁)
* 2. pending 化后:netTask 上下文的阻塞等待会被挂起登记拒绝(自通知死锁)
* 3. 本函数阻塞用法仅对 DONTWAIT 有效
*/
int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags) {
@@ -3669,9 +3267,8 @@ static void handle_connect_event(net_sock_t *p_sock) {
* 限定条件:net_poll() 检测到 DISCONNECT 中断后调用;netTask 上下文
* 函数说明: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
* 本层 ch395f_close_socket 关闭;芯片不代劳(§5.52② / §5.23),不得跳过
* 显式 close(U12 成因之一)。之后需重新 OPEN + LISTEN
* 2. 单连接:置 CLOSED,交由 net_recv_locked 排空剩余数据后再关
* 3. 触发 NET_EVENT_DISCONNECTED 回调
*/
@@ -3771,7 +3368,7 @@ static void handle_disconnect_event(net_sock_t *p_sock) {
net_listener_settle(qfd, NET_LISTENER_SETTLE_GRACEFUL);
}
}
/* 正常释放(对端 FIN):断连了结回 EOF(0) */
/* 正常释放(对端 FIN):断连时挂起项以 EOF(0) 结束 */
p_sock->released_abnormal = 0U;
/* N-32(P1 不变量 I):置"本会话已结束"电平。须在置 CLOSED **之前**,
* 使 session_dead 覆盖"CLOSED 等待 recv 排空"整个窗口——排空期间
@@ -3804,8 +3401,7 @@ static void handle_disconnect_event(net_sock_t *p_sock) {
* 返回值:无
* 限定条件:net_poll() 检测到 RECV_OK 中断后调用;netTask 上下文
* 函数说明:1. 仅触发 NET_EVENT_DATA_RECEIVED 回调通知上层
* 2. 不在此读取 recv_len:单一读取点归 net_recv_locked(省一次 SPI
* 查询;旧"部分读归零"时代的"读取冲突"顾虑已随复核注降级,结构保留)
* 2. 不在此读取 recv_len:单一读取点归 net_recv_locked(省一次 SPI 查询)
*/
static void handle_recv_event(net_sock_t *p_sock) {
/* RECV_OK 中断已触发,直接通知上层。不在此读 recv_len,
@@ -3865,8 +3461,8 @@ static void handle_timeout_event(net_sock_t *p_sock) {
net_listener_settle(qfd, NET_LISTENER_SETTLE_QUARANTINE);
}
}
/* TRAP-NET-18 待办 #3:标记异常释放——L3 挂起 recv 排空断连后以
* NET_ERR_CONNRESET 了结(而非 EOF),应用(lftpd STOR)回报 5xx
/* TRAP-NET-18 待办 #3:标记异常释放——挂起 recv 排空断连后以
* NET_ERR_CONNRESET 结束(而非 EOF),应用(lftpd STOR)回报 5xx
* 而非误报 226(见 net_pending_tick 断连分支) */
p_sock->released_abnormal = 1U;
/* N-30(TRAP-NET-20):异常释放同样登记对端端口(幽灵甄别基准) */
+1 -13
View File
@@ -193,7 +193,7 @@ int net_listener_armed(int sockfd);
* 函数功能:查询 Socket 是否"值得发一次 recv"(**纯 RAM 读,不发起 RPC**)
* 入口参数:sockfd - Socket 描述符 int
* 出口参数:无
* 返回值:1 - 应发 recv(有数据可读,或连接已非 ESTABLISHED 需了结);0 - 不必发 int
* 返回值:1 - 应发 recv(有数据可读,或连接已非 ESTABLISHED 需发 recv 取回 EOF/错误);0 - 不必发 int
* 限定条件:net_init() 已调用;任意任务上下文
* 函数说明:1. ESTABLISHED → 看 readable(net_poll 在 RECV_OK 置位、读空清零)
* 2. **非 ESTABLISHED 一律返回 1**:连接已消失时必须让上层真的 recv 一次
@@ -492,18 +492,6 @@ int net_set_errno(int err);
* 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 描述符
+3 -2
View File
@@ -188,7 +188,7 @@ typedef struct {
uint8_t blocking;
/* 释放来源标记(TRAP-NET-18 待办 #3):1 = 异常释放(芯片 TIMEOUT,对端 RST/掉链),
* 0 = 正常释放(芯片 DISCONNECT,对端 FIN)或新连接。L3 挂起 recv 的了结断连
* 0 = 正常释放(芯片 DISCONNECT,对端 FIN)或新连接。挂起 recv 结束时的断连
* 分支据此决定回 EOF(0) 还是 NET_ERR_CONNRESET:异常结束上报应用(lftpd STOR)
* 为错误 → 回 5xx 而非误报 226。由 handle_timeout_event / handle_disconnect_event
* 置位,新连接建立时(net_socket / relisten 成功)清零——新连接不得继承上一条
@@ -196,7 +196,8 @@ typedef struct {
uint8_t released_abnormal;
/* N-30(2026-10-11 裸机芯片台架实证):本 Socket 上一条被释放连接的**对端端口**。
* 芯片拆链收尾窗口内 Socket 会假报 ESTABLISHED(幽灵态),此时回读的对端端口
* 芯片拆链收尾窗口内旧连接未拆净,Socket 如实为 ESTABLISHED(幽灵态,E15:
* bit3=1 保留现场契约,非谎报),此时回读的对端端口
* = 旧连接端口;新连接的临时端口几乎必然不同。accept 路径补 accept 与对账②
* 均以此甄别幽灵态(见 net_socket.c TRAP-NET-20)。跨 fd 释放保留(幽灵属芯片
* Socket 级,与 fd 复用无关),新连接断开时刷新。 */
+5
View File
@@ -1603,6 +1603,11 @@
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\NET\net_listener.c</FilePath>
</File>
<File>
<FileName>net_pending.c</FileName>
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\NET\net_pending.c</FilePath>
</File>
<File>
<FileName>net_cfg.c</FileName>
<FileType>1</FileType>
+1 -1
View File
@@ -272,7 +272,7 @@ docs/
| 日期 | 变更 | 说明 |
|---|---|---|
| 2026-10-14 | E14"丢中断"复核与归因更正 | 48 轮直读 SINT 矩阵复核历史"丢中断"说法:**芯片侧事件丢失 = 0**("丢失"= drain 读清消费 3/48 + 门控竞态 2/48 + 主机同步 1/48,幽灵态 0/48 未复现);根除性修复 = listen/relisten 路径排事件挪至 LISTEN 命令**之前**(吞新会话 CONNECT 的生产侧自伤窗);全局读收口(全局寄存器唯一读者=`net_poll`,I7 护栏);全库 17 文件约 40 处"丢中断"归因更正为"事件未收到/读清消费竞态",`docs/网络/网络自愈架构设计.md` §5.2 新增 E14 锚点;台架保留复核探针(drain-int/SINT 补读×3/超时归类) |
| 2026-10-14 | E14"丢中断"复核与归因更正 | 48 轮直读 SINT 矩阵复核历史"丢中断"说法:**芯片侧事件丢失 = 0**("丢失"= drain 读清消费 3/48 + 门控竞态 2/48 + 主机同步 1/48,幽灵态 0/48 未复现);根除性修复 = listen/relisten 路径排事件挪至 LISTEN 命令**之前**(吞新会话 CONNECT 的生产侧自伤窗);全局读收口(全局寄存器唯一读者=`net_poll`,I7 护栏);全库 17 文件约 40 处"丢中断"归因更正为"事件未收到/读清消费竞态",`docs/网络/网络自愈架构设计.md` §5.2 新增 E14 锚点;台架保留复核探针(drain-int/SINT 补读×3/超时归类);同日 E15 幽灵态归因复核(官网+手册实锤:0x04+旧端口 = 芯片**如实报告**旧连接拆链进行中——bit3=1 保留现场为自家配置、拆链窗与 §5.17 重传默认值吻合、WCH 官方博客确认断连即重连被拒且官方方案 = 关闭完成后重开,即 settle 等待) |
| 2026-10-13 | 网络自愈架构文档瘦身与改名 | 合并文档 `网络/N34-自愈审计.md`(2062 行审计全记录)瘦身为**终版设计描述**(281 行:不变量 I1~I8 / 架构与机制详解 / 参数与 E 锚点速查 / 红线 / 延后项),并经 `rename_ref.py` 改名 [`网络/网络自愈架构设计.md`](网络/网络自愈架构设计.md)(全库 16 文件 30 处引用同步);审计过程(T1~T4、E0~E13)归档 git 历史,考古命令见该文档「引用与归档约定」 |
| 2026-10-13 | 网络自愈审计归档 | 原 `docs/网络/N34-自愈审计/` 目录 7 份文档(根因总览 / net 层盘点 / 驱动层审计 / 主流对照 / 汇总裁决 / 实施方案 / 实验方案)**合并为单文档 [`网络/网络自愈架构设计.md`](网络/网络自愈架构设计.md)**,原目录删除;全库引用同步(`docs/` 4 篇 + 驱动/网络代码注释 + 配图生成器与 SVG)。本索引 §5.1 补登记 |
| 2026-10-12 | 新增环境与部署 | 新建 `docs/环境与部署/并行任务工作树方案.md`(**多任务并行开发机制规格**:`git worktree` 隔离、目录与共享/独占分配、`task-worktree` / `@task` skill 设计、单写者文档与合并残留审计、台架锁;**已落地并端到端实测**);本索引 §9.2 补登记 |
+2 -2
View File
@@ -48,7 +48,7 @@
<text x="566.0" y="472.0" font-size="11" fill="#64748b">刷新 readable</text>
<rect x="758.5" y="434.0" width="190.5" height="46" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.6"/>
<text x="768.5" y="454.0" font-size="12.5" font-weight="700" fill="#d97706">net_pending_tick()</text>
<text x="768.5" y="472.0" font-size="11" fill="#64748b">了结挂起</text>
<text x="768.5" y="472.0" font-size="11" fill="#64748b">结束挂起</text>
<rect x="961.0" y="434.0" width="190.5" height="46" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-opacity="0.6"/>
<text x="971.0" y="454.0" font-size="12.5" font-weight="700" fill="#7c3aed">net_listener_retry_tick()</text>
<text x="971.0" y="472.0" font-size="11" fill="#64748b">重试对账</text>
@@ -74,7 +74,7 @@
<text x="66" y="940.0" font-size="14" font-weight="700" fill="#dc2626">⑤</text>
<text x="96" y="940.0" font-size="12.5" font-weight="700" fill="#0f172a">网络重建(芯片复位)</text>
<text x="270" y="940.0" font-size="12.5" fill="#475569">→ 取消 + 必须通知(否则调用方永久挂死)</text>
<text x="96" y="969" font-size="12" font-weight="700" fill="#dc2626">看门狗:到点后 1s 仍未了结 = 完成逻辑缺位 → 强制了结 + DBG_ERROR</text>
<text x="96" y="969" font-size="12" font-weight="700" fill="#dc2626">挂起超时留痕探针(编译期选项,默认关):挂起超阈值仍未结束 → 只 DBG_ERROR 留痕(不强制结束、不伪造返回值)</text>
<rect x="1120" y="812" width="250" height="62" rx="10" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.55" stroke-width="1.6"/>
<text x="1134" y="838" font-size="13.5" font-weight="700" fill="#2563eb">xTaskNotify</text>
<text x="1134" y="858" font-size="11.5" fill="#64748b">唤醒调用方(结果 = 字节数 / 负错误码)</text>

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

+17
View File
@@ -77,6 +77,8 @@ digraph refgraph_full {
"Drivers/BSP/NET/net_config.h" [fillcolor="#dbeafe", color="#1d4ed8"];
"Drivers/BSP/NET/net_listener.c" [fillcolor="#dbeafe", color="#1d4ed8"];
"Drivers/BSP/NET/net_listener.h" [fillcolor="#dbeafe", color="#1d4ed8"];
"Drivers/BSP/NET/net_pending.c" [fillcolor="#dbeafe", color="#1d4ed8"];
"Drivers/BSP/NET/net_pending.h" [fillcolor="#dbeafe", color="#1d4ed8"];
"Drivers/BSP/NET/net_select.c" [fillcolor="#dbeafe", color="#1d4ed8"];
"Drivers/BSP/NET/net_select.h" [fillcolor="#dbeafe", color="#1d4ed8"];
"Drivers/BSP/NET/net_socket.c" [fillcolor="#dbeafe", color="#1d4ed8"];
@@ -505,6 +507,20 @@ digraph refgraph_full {
"Drivers/BSP/NET/net_listener.c" -> "test/board/test_config.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_listener.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed];
"Drivers/BSP/NET/net_listener.h" -> "Drivers/BSP/NET/net_types.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_pending.c" -> "Drivers/BSP/CH395F/ch395f.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_pending.c" -> "Drivers/BSP/NET/net_listener.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_pending.c" -> "Drivers/BSP/NET/net_pending.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_pending.c" -> "Drivers/BSP/NET/net_socket.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_pending.c" -> "Drivers/BSP/NET/net_types.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_pending.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_pending.c" -> "Inc/main.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_pending.c" -> "Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h" [color="#94a3b8", penwidth=0.7];
"Drivers/BSP/NET/net_pending.c" -> "Middlewares/Third_Party/FreeRTOS/Source/include/task.h" [color="#94a3b8", penwidth=0.7];
"Drivers/BSP/NET/net_pending.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed];
"Drivers/BSP/NET/net_pending.h" -> "Drivers/BSP/NET/net_types.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_pending.h" -> "Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h" [color="#94a3b8", penwidth=0.7];
"Drivers/BSP/NET/net_pending.h" -> "Middlewares/Third_Party/FreeRTOS/Source/include/task.h" [color="#94a3b8", penwidth=0.7];
"Drivers/BSP/NET/net_pending.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed];
"Drivers/BSP/NET/net_select.c" -> "Drivers/BSP/CH395F/ch395f.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_select.c" -> "Drivers/BSP/NET/net_select.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_select.c" -> "Drivers/BSP/NET/net_socket.h" [color="#2563eb", penwidth=1.0];
@@ -517,6 +533,7 @@ digraph refgraph_full {
"Drivers/BSP/NET/net_select.h" -> "Drivers/BSP/NET/net_types.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_socket.c" -> "Drivers/BSP/CH395F/ch395f.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_socket.c" -> "Drivers/BSP/NET/net_listener.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_socket.c" -> "Drivers/BSP/NET/net_pending.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_socket.c" -> "Drivers/BSP/NET/net_socket.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_socket.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0];
"Drivers/BSP/NET/net_socket.c" -> "Inc/main.h" [color="#2563eb", penwidth=1.0];
+109 -5
View File
@@ -341,6 +341,14 @@
"path": "Drivers/BSP/NET/net_listener.h",
"class": "fp"
},
{
"path": "Drivers/BSP/NET/net_pending.c",
"class": "fp"
},
{
"path": "Drivers/BSP/NET/net_pending.h",
"class": "fp"
},
{
"path": "Drivers/BSP/NET/net_select.c",
"class": "fp"
@@ -7619,6 +7627,90 @@
"kind": "fp-fp",
"include": "net_types.h"
},
{
"from": "Drivers/BSP/NET/net_pending.c",
"to": "外部/系统头文件",
"kind": "fp-ext",
"include": "stddef.h"
},
{
"from": "Drivers/BSP/NET/net_pending.c",
"to": "Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h",
"kind": "fp-tp",
"include": "FreeRTOS.h"
},
{
"from": "Drivers/BSP/NET/net_pending.c",
"to": "Middlewares/Third_Party/FreeRTOS/Source/include/task.h",
"kind": "fp-tp",
"include": "task.h"
},
{
"from": "Drivers/BSP/NET/net_pending.c",
"to": "Drivers/BSP/NET/net_types.h",
"kind": "fp-fp",
"include": "net_types.h"
},
{
"from": "Drivers/BSP/NET/net_pending.c",
"to": "Drivers/BSP/NET/net_socket.h",
"kind": "fp-fp",
"include": "net_socket.h"
},
{
"from": "Drivers/BSP/NET/net_pending.c",
"to": "Drivers/BSP/NET/net_pending.h",
"kind": "fp-fp",
"include": "net_pending.h"
},
{
"from": "Drivers/BSP/NET/net_pending.c",
"to": "Drivers/BSP/NET/net_listener.h",
"kind": "fp-fp",
"include": "net_listener.h"
},
{
"from": "Drivers/BSP/NET/net_pending.c",
"to": "Drivers/BSP/CH395F/ch395f.h",
"kind": "fp-fp",
"include": "ch395f.h"
},
{
"from": "Drivers/BSP/NET/net_pending.c",
"to": "Drivers/BSP/dbg_log.h",
"kind": "fp-fp",
"include": "dbg_log.h"
},
{
"from": "Drivers/BSP/NET/net_pending.c",
"to": "Inc/main.h",
"kind": "fp-fp",
"include": "main.h"
},
{
"from": "Drivers/BSP/NET/net_pending.h",
"to": "外部/系统头文件",
"kind": "fp-ext",
"include": "stdint.h"
},
{
"from": "Drivers/BSP/NET/net_pending.h",
"to": "Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h",
"kind": "fp-tp",
"include": "FreeRTOS.h"
},
{
"from": "Drivers/BSP/NET/net_pending.h",
"to": "Middlewares/Third_Party/FreeRTOS/Source/include/task.h",
"kind": "fp-tp",
"include": "task.h"
},
{
"from": "Drivers/BSP/NET/net_pending.h",
"to": "Drivers/BSP/NET/net_types.h",
"kind": "fp-fp",
"include": "net_types.h"
},
{
"from": "Drivers/BSP/NET/net_select.c",
"to": "外部/系统头文件",
@@ -7733,6 +7825,12 @@
"kind": "fp-fp",
"include": "net_listener.h"
},
{
"from": "Drivers/BSP/NET/net_socket.c",
"to": "Drivers/BSP/NET/net_pending.h",
"kind": "fp-fp",
"include": "net_pending.h"
},
{
"from": "Drivers/BSP/NET/net_socket.c",
"to": "Drivers/BSP/CH395F/ch395f.h",
@@ -41763,6 +41861,12 @@
"stdbool.h",
"string.h"
],
"Drivers/BSP/NET/net_pending.c": [
"stddef.h"
],
"Drivers/BSP/NET/net_pending.h": [
"stdint.h"
],
"Drivers/BSP/NET/net_select.c": [
"stdio.h",
"string.h"
@@ -43501,11 +43605,11 @@
]
},
"stats": {
"files_indexed": 1491,
"includes_parsed": 5939,
"resolved": 4792,
"unresolved": 1147,
"nodes_fp": 211,
"files_indexed": 1493,
"includes_parsed": 5954,
"resolved": 4805,
"unresolved": 1149,
"nodes_fp": 213,
"nodes_tp": 1266,
"ext_names": 351
},
File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 506 KiB

After

Width:  |  Height:  |  Size: 512 KiB

+5 -5
View File
@@ -82,12 +82,12 @@ digraph refgraph_module {
"BSP/GD5F2GQ5UE" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×1"];
"BSP/GD5F2GQ5UE" -> "dhara" [color="#2563eb", penwidth=1.2, label="×2"];
"BSP/GD5F2GQ5UE" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×4"];
"BSP/NET" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×2"];
"BSP/NET" -> "BSP/CH395F" [color="#2563eb", penwidth=1.2, label="×3"];
"BSP/NET" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×11"];
"BSP/NET" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×3"];
"BSP/NET" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×3"];
"BSP/NET" -> "BSP/CH395F" [color="#2563eb", penwidth=1.2, label="×4"];
"BSP/NET" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×15"];
"BSP/NET" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×4"];
"BSP/NET" -> "test" [color="#2563eb", penwidth=1.2, label="×4"];
"BSP/NET" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×4"];
"BSP/NET" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×6"];
"BSP/NETCFG" -> "App" [color="#2563eb", penwidth=1.2, label="×2"];
"BSP/NETCFG" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×1"];
"BSP/NETCFG" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×4"];
+5 -5
View File
@@ -513,28 +513,28 @@
<title>BSP/NET&#45;&gt;BSP(公共)</title>
<path fill="none" stroke="#2563eb" stroke-width="1.2" d="M1366.53,-1486.55C1371.67,-1482.5 1376.67,-1477.93 1380.75,-1473 1392.66,-1458.6 1385.78,-1448.45 1398.75,-1435 1417.52,-1415.54 1429.05,-1420 1452.75,-1407 1500.15,-1380.99 1523.25,-1387.42 1558,-1346 1607.98,-1286.42 1639.96,-1044.77 1649.85,-959.76"/>
<polygon fill="#2563eb" stroke="#2563eb" stroke-width="1.2" points="1653.31,-960.31 1650.97,-949.97 1646.36,-959.51 1653.31,-960.31"/>
<text xml:space="preserve" text-anchor="middle" x="1514.38" y="-1402.27" font-family="Microsoft YaHei" font-size="10.00" fill="#334155">×2</text>
<text xml:space="preserve" text-anchor="middle" x="1514.38" y="-1402.27" font-family="Microsoft YaHei" font-size="10.00" fill="#334155">×3</text>
</g>
<!-- BSP/NET&#45;&gt;BSP/CH395F -->
<g id="edge56" class="edge">
<title>BSP/NET&#45;&gt;BSP/CH395F</title>
<path fill="none" stroke="#2563eb" stroke-width="1.2" d="M1368.34,-1486.5C1373.07,-1482.56 1377.45,-1478.04 1380.75,-1473 1400.78,-1442.42 1375.3,-1420.79 1398.75,-1392.75 1415.45,-1372.77 1430.53,-1383.59 1452.75,-1370 1464.34,-1362.91 1476.01,-1353.7 1485.97,-1345.06"/>
<polygon fill="#2563eb" stroke="#2563eb" stroke-width="1.2" points="1488.11,-1347.85 1493.24,-1338.57 1483.45,-1342.62 1488.11,-1347.85"/>
<text xml:space="preserve" text-anchor="middle" x="1425.75" y="-1397.5" font-family="Microsoft YaHei" font-size="10.00" fill="#334155">×3</text>
<text xml:space="preserve" text-anchor="middle" x="1425.75" y="-1397.5" font-family="Microsoft YaHei" font-size="10.00" fill="#334155">×4</text>
</g>
<!-- BSP/NET&#45;&gt;FreeRTOS -->
<g id="edge57" class="edge">
<title>BSP/NET&#45;&gt;FreeRTOS</title>
<path fill="none" stroke="#2563eb" stroke-width="1.2" d="M1349.64,-1523.41C1371.83,-1556.12 1418.69,-1626.84 1452.75,-1689.75 1471.36,-1724.13 1489.77,-1765.26 1501.33,-1792.2"/>
<polygon fill="#2563eb" stroke="#2563eb" stroke-width="1.2" points="1498,-1793.31 1505.13,-1801.14 1504.44,-1790.57 1498,-1793.31"/>
<text xml:space="preserve" text-anchor="middle" x="1425.75" y="-1694.5" font-family="Microsoft YaHei" font-size="10.00" fill="#334155">×11</text>
<text xml:space="preserve" text-anchor="middle" x="1425.75" y="-1694.5" font-family="Microsoft YaHei" font-size="10.00" fill="#334155">×15</text>
</g>
<!-- BSP/NET&#45;&gt;Src/Inc (CubeMX) -->
<g id="edge58" class="edge">
<title>BSP/NET&#45;&gt;Src/Inc (CubeMX)</title>
<path fill="none" stroke="#2563eb" stroke-width="1.2" d="M1329.66,-1523.2C1315.28,-1572.05 1276.62,-1702.66 1273.25,-1706 1264.72,-1714.47 870.58,-1827.86 858.75,-1830 813.65,-1838.17 762.2,-1841.67 722,-1843.13"/>
<polygon fill="#2563eb" stroke="#2563eb" stroke-width="1.2" points="722.14,-1839.63 712.26,-1843.45 722.37,-1846.62 722.14,-1839.63"/>
<text xml:space="preserve" text-anchor="middle" x="982" y="-1802.03" font-family="Microsoft YaHei" font-size="10.00" fill="#334155">×3</text>
<text xml:space="preserve" text-anchor="middle" x="982" y="-1802.03" font-family="Microsoft YaHei" font-size="10.00" fill="#334155">×4</text>
</g>
<!-- BSP/NET&#45;&gt;test -->
<g id="edge59" class="edge">
@@ -548,7 +548,7 @@
<title>BSP/NET&#45;&gt;外部/系统头文件</title>
<path fill="none" stroke="#94a3b8" stroke-width="0.7" d="M1369.51,-1508.86C1459.7,-1519.56 1714.78,-1549.82 1843.01,-1565.03"/>
<polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="0.7" points="1842.5,-1568.5 1852.84,-1566.2 1843.32,-1561.55 1842.5,-1568.5"/>
<text xml:space="preserve" text-anchor="middle" x="1654.12" y="-1550.91" font-family="Microsoft YaHei" font-size="10.00" fill="#334155">×4</text>
<text xml:space="preserve" text-anchor="middle" x="1654.12" y="-1550.91" font-family="Microsoft YaHei" font-size="10.00" fill="#334155">×6</text>
</g>
<!-- BSP/NETCFG&#45;&gt;App -->
<g id="edge61" class="edge">

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

+1 -1
View File
@@ -17,7 +17,7 @@
> 本节仅保留**二期仍要用的设计基线**。
**一期已验证的机制骨架**(二期沿用,勿重新发明):独立静态 pending 表 + `net_pending_tick`
主循环节拍了结(五路:数据/到点/断连/同 fd 串行/重建取消必通知)+ 登记守卫(每 fd 至多一个、
主循环按节拍结束挂起(五路:数据/到点/断连/同 fd 串行/重建取消必通知)+ 登记守卫(每 fd 至多一个、
不覆盖、拒绝 netTask 自等待)+ 1s 看门狗强结算。
**二期扩展点**:
@@ -36,10 +36,10 @@
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 230,208 B (224.81 KB) | 1,048,576 B (1024 KB) | 22.0% |
| ER_IROM1 | 230,048 B (224.66 KB) | 1,048,576 B (1024 KB) | 21.9% |
| RW_IRAM1 | 97,912 B (95.62 KB) | 131,072 B (128 KB) | 74.7% |
| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
| **Flash (ROM 合计)** | **231,312 B (225.9 KB)** | 1,048,576 B (1024 KB) | **22.1%** |
| **Flash (ROM 合计)** | **231,152 B (225.7 KB)** | 1,048,576 B (1024 KB) | **22.0%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
+1 -1
View File
@@ -15,7 +15,7 @@
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|---|---|---|---|---|
| ER_IROM1 (Flash) | 230,208 B | 1,048,576 B | 22.0% | 参考 |
| ER_IROM1 (Flash) | 230,048 B | 1,048,576 B | 21.9% | 参考 |
| RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS |
| UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 |
| RW_IRAM1 | 97,912 B | 131,072 B | 74.7% | PASS |
@@ -241,7 +241,7 @@ P2 之后,对账④保留但只作事件面未收到的补漏(TRAP-NET-17
| 服务 | 认为的连接结束信号 | net 层实际给什么 | 监听重建者 | 是否留无人重建组合 |
|---|---|---|---|---|
| `lftpd` 控制 :21 | `read_line!=0`(EOF→-1,`lftpd_inet.c:154-156`) | 阻塞 pending 保证 CLOSED 必了结(FIN→0 / 异常→CONNRESET,`net_socket.c:2457-2462`) | net 层(`:2449-2451`),应用**永不** close 控制 fd(`lftpd.c:746-750`) | 无(契约一致,是正面样板) |
| `lftpd` 控制 :21 | `read_line!=0`(EOF→-1,`lftpd_inet.c:154-156`) | 阻塞 pending 保证 CLOSED 时挂起 recv 必被结束(FIN→0 / 异常→CONNRESET,`net_socket.c:2457-2462`) | net 层(`:2449-2451`),应用**永不** close 控制 fd(`lftpd.c:746-750`) | 无(契约一致,是正面样板) |
| `time_sync` :8888 | EOF/30 s 预算/错误,随后**无条件** `net_close(conn==ls)`(`time_sync.c:182`) | 同上 | 应用自己每会话全量重建(`:224`) | 不留孤儿,但违反「EOF 不得 close 监听 fd」→ close→重建沉降窗(TRAP-NET-09)+ 最坏 ~10 s 端口下线 |
| `ui_net_sink` :7003 | `net_recv` 返 0 或真错误;重建条件 `net_get_sock(g_listen)==NULL`(`:557`/`:566`) | 该 0 只在 recv 时刻 `state==CLOSED` 才给 | 名义 net 层,实际自身条件在 MCU **恒假**(fd 不释放) | **有**(B10 与"消费者卡死"都出自这里) |
| 模拟器后端 | `recv` 0 恒可得、fd 分离(`net_socket_sim.c:293-336`) | Winsock 原生 | 无需 | 无——且对上述全部失效模式**零覆盖** |
+2 -2
View File
@@ -1012,7 +1012,7 @@ int net_drop_conn(int sockfd);
应用**放弃会话**(对时 30s 预算耗尽、协议超时)时主动拆掉当前 TCP 会话并**保留监听**——单连接模型下挂起但 TCP 仍活的客户端会永久占住监听(PC 内核自动 ACK keepalive,芯片僵尸判不死,定案见 `docs/网络/网络自愈架构设计.md` §5.2 E13),而应用又不得 `net_close` 监听 fd(毁监听);本 API 补上"拆连接不拆监听"的应用侧形态。
- **netTask 侧**:先了结本 fd 挂起 recv(排空/取消 + 必通知),再 `sock_reset` QUARANTINE(FIN + 排渣 + 排空事件 + 等 byte0=00H,TRAP-NET-18 异常释放语义,400ms 底);本层收口 CLOSED;`auto_relisten` fd 随即 `net_listener_try` 即时重监听(失败交 D10 重试 + reconciler 形态④)
- **netTask 侧**:先结束本 fd 挂起 recv(排空/取消 + 必通知),再 `sock_reset` QUARANTINE(FIN + 排渣 + 排空事件 + 等 byte0=00H,TRAP-NET-18 异常释放语义,400ms 底);本层收口 CLOSED;`auto_relisten` fd 随即 `net_listener_try` 即时重监听(失败交 D10 重试 + reconciler 形态④)
- **幂等**:无活动会话(非 ESTABLISHED/CONNECTING)直接返 0,不动监听
- **EOF 路径不要调**:EOF 时 net 层 `auto_relisten` 已接管(§12.4),无需也不应再拆
- 经消息队列委托 netTask(同其它 `net_*` API,非 netTask 上下文调用);等待期 netTask 有界占用 ~400-600ms
@@ -1057,7 +1057,7 @@ void net_process_messages(void);
处理其他任务投递的请求(send/recv/connect/accept/close/drop_conn)。每次调用先跑 3 个**非阻塞** tick,再取队列消息(0 超时出队):
1. **D10 重监听重试**(`net_listener_retry_tick`):断连后监听器恢复的到点重试(30ms 间隔 / 2s 时限,§3.6)
2. **L3 挂起 recv 完成检查**(`net_pending_tick`):按 ①数据 ②到点(仅显式预算) ③断连 检查每个挂起请求并通知调用方;④ tripwire:挂起超 60s 只 `[ERR]` 留痕一次,不强制了结(自愈收敛阶段 1.5)
2. **L3 挂起 recv 完成检查**(`net_pending_tick`):按 ①数据 ②到点(仅显式预算) ③断连 检查每个挂起请求并通知调用方;④ 挂起超时留痕探针(编译期选项,默认关):挂起超阈值只 `[ERR]` 留痕一次,不强制结束
3. **TRAP-NET-17 芯片/本层对账 + 监听重建(合一 `net_reconcile_tick`)**:100ms 周期,判据读驱动状态缓存(`ch395f_sock_state_cached`),修正**五类**失同步(①②③④⑤)+ U11 被动重同步——原 200ms 对账与 100ms 监听器 pump 已合并为单一 tick,**唯一重建入口**=形态③④(§3.6)
> `net_msg_t.timeout_ms`:L3 显式预算(`net_recv_to()`),0=**无限等待**(自愈收敛阶段 1.5 删除 `NET_RECV_TIMEOUT_MS` 30s 默认预算——超时归调用方)。消息类型 7 种(send/recv/close/connect/listen/accept/drop_conn)——自愈类消息 `NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 已随 API 退役删除(自愈收敛阶段1)。
@@ -3,10 +3,13 @@
> **定位**:本文档是 `docs/网络/BSD_Socket_API_使用指南.md`(下称"使用指南")的**内部视图**伴读文档。
> 使用指南回答"**上层该怎么调**"(公开语义 / 示例 / 常见问题);本文回答"**内部函数各自做什么、互相怎么调用、为什么这么设计**"(调用关系 / 设计意图 / 踩坑)。
>
> **范围**:MCU 端 `Drivers/BSP/NET/net_socket.c`(约 3670 行)+ 其拆出的
> `Drivers/BSP/NET/net_listener.c`(约 550 行,**监听生命周期**,N-32 §10 模块化)。
> **范围**:MCU 端 `Drivers/BSP/NET/net_socket.c`(约 3660 行)+ 其拆出的
> `Drivers/BSP/NET/net_listener.c`(约 550 行,**监听生命周期**,N-32 §10 模块化)+
> `Drivers/BSP/NET/net_pending.c`(约 390 行,**挂起 recv 请求表**,2026-10-14 拆出)。
> 模拟器端后端 `simulator/sim/net_socket_sim.c`(Winsock)是另一套实现,见 `docs/网络/BSD_Socket_API_使用指南.md` §2.3;`net_select.c` 不在此展开,见 `docs/网络/BSD_Socket_API_使用指南.md` §7 / §10.5。
> `net_listener.c` **只被 `net_socket.c` include**(内部头,不进公开 `net_socket.h`),模拟器零依赖、不编译;其设计依据见 `docs/子系统设计/网络Socket监听生命周期设计说明.md` §10。
> `net_listener.c` / `net_pending.c` 真机专属(模拟器不编译);设计依据分别见
> `docs/子系统设计/网络Socket监听生命周期设计说明.md` §10 与
> `docs/网络/Net层阻塞模型架构设计.md`。
>
> **交叉引用约定**:机制标 Trap 号 → `docs/问题记录/CH395F_Trap_Records.md`;用法语义引用使用指南 §;关键参数值与 `net_config.h` / `net_socket.c` 内宏一致。
@@ -57,7 +60,7 @@ flowchart TB
LS["netSendLocked<br/>分块 ≤1KB + sendReady 门控"]
LR["netRecvLocked<br/>快路径 / DONTWAIT / 登记挂起"]
LA["netAcceptLocked<br/>先 netPoll + 状态判定"]
LC["netCloseLocked<br/>了结挂起 + cmd_done 快路径 / sock_reset(GRACEFUL)"]
LC["netCloseLocked<br/>结束挂起 + cmd_done 快路径 / sock_reset(GRACEFUL)"]
DC["netDropConnLocked<br/>拆会话保监听:sock_reset(QUARANTINE) + 即时重监听"]
RLS["netListenerTry → netRelistenLocked<br/>单次尝试 + TRAP-NET-18 排渣隔离"]
end
@@ -99,7 +102,7 @@ flowchart TB
2. `net_process_messages()` 开头的 **3 个 tick 是网络层自愈主干**:没有任何应用消息时,重试 / 挂起完成 / 状态对账 + 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。
3. 事件处理器**只由 `net_poll` 驱动**,例外是 `net_reconcile_tick`(分级去抖 + 1s 冷却)可再入 `handle_disconnect_event` / `handle_connect_event`——对账是事件未收到的自愈通道(TRAP-NET-17)。**注意职责边界(N-32 P2,自愈收敛阶段 3 合并后)**:对账与事件处理器**只置事实**(置 CLOSED / 登记沉降),**重建监听的动作统一由 `net_reconcile_tick` 形态③④ 做**(本层唯一重建入口)——"谁先看到 DISCONNECT 就谁重建"的竞争在结构上消失。
4. 回调 `event_cb` 在 **netTask 上下文**执行:回调内不得阻塞、不得发网络操作。
5. **模块归属**:`T1`(D10 重试)/ `RLS` 的 `net_listener_try` / `ST`(settle)在 `net_listener.c`(真机专属、模拟器不编译);其余 tick(含合并后的 `T3` reconciler)与 `_locked` 在 `net_socket.c`。
5. **模块归属**:`T1`(D10 重试)/ `RLS` 的 `net_listener_try` / `ST`(settle)在 `net_listener.c`(真机专属、模拟器不编译);挂起 recv 完成检查(`net_pending_tick`)与挂起表在 `net_pending.c`(真机专属);其余 tick(含合并后的 `T3` reconciler)与 `_locked` 在 `net_socket.c`。
---
@@ -133,7 +136,7 @@ flowchart TB
| `net_recv` | `NET_MSG_RECV`(timeout=0→**无限等待**,自愈收敛阶段 1.5 删 30s 默认) | `net_recv_locked` | `net_poll` + `get_recv_len` / `read_recv_buf` | >0 / 0(EOF) / -1(errno = WOULDBLOCK / CONNRESET;阻塞形态无 TIMEDOUT 一档) | 快路径:数据已在 → 立即读出返回(允许部分读);无数据 + 阻塞(`blocking` 属性=1 且未 DONTWAIT)→ **登记挂起后立即返回**(L3,netTask 零占用),唤醒源=数据/EOF/close/网络重建;DONTWAIT / `blocking=0` + CLOSED/session_dead → EOF + 触发重监听;⚠ IWDG 受监控任务禁用无限形态(`net_socket.h` 文档契约) |
| `net_recv_nb` | `NET_MSG_RECV`(flags=DONTWAIT) | `net_recv_locked` | 同上 | >0 / 0 / -1(WOULDBLOCK 等) | 原 `NET_MSG_DONTWAIT` 路径转正的具名入口(自愈收敛阶段 1.5);不改变 `blocking` 属性;受 IWDG 监控 / 有周期职责任务的推荐默认(ui_net_sink / iec104_serv 已迁) |
| `net_recv_to` | `NET_MSG_RECV`(timeout>0) | `net_recv_locked` | 同上 | >0 / 0 / -1 / **-2(本片超时无数据)** | E-02 头阻塞修复配套 + **显式预算**入口(自愈收敛阶段 1.5,等价 Python settimeout,无隐藏默认值):调用方把长等待切成有界片(如剩余预算),**总超时语义不变、单次占用有界**;`timeout_ms=0` 直接拒(0 = 无限等待,与本接口"有界"存在理由相反);超时**只凭 -2 判定、不得依赖 errno**(`s_net_errno` 是跨任务共享全局量,随时被别的任务覆盖) |
| `net_close` | `NET_MSG_CLOSE` | `net_close_locked` | `ch395f_cmd_done(CLOSE)` 快路径(芯片已 CLOSED → 只清本 Socket 残留事件)/ `net_sock_reset_sync(GRACEFUL)` 确定性排渣(自愈收敛阶段 3) | 0 / -1 | **先**了结本 fd 挂起 recv(排空/取消 + 必通知,关不得让等待者悬挂);排空只碰本 Socket SINT,**不再读清全局中断寄存器**(F-NET-7:全局唯一读者 = `net_poll`);原"close + 500ms 盲等排空循环"已换 sock_reset(内置 LISTEN 态二段 CLOSE 补发,E2 补充发现);控制块整体清零(`last_peer_port` 幽灵基准跨 fd 保留,N-30) |
| `net_close` | `NET_MSG_CLOSE` | `net_close_locked` | `ch395f_cmd_done(CLOSE)` 快路径(芯片已 CLOSED → 只清本 Socket 残留事件)/ `net_sock_reset_sync(GRACEFUL)` 确定性排渣(自愈收敛阶段 3) | 0 / -1 | **先**结束本 fd 挂起 recv(排空/取消 + 必通知,关不得让等待者悬挂);排空只碰本 Socket SINT,**不再读清全局中断寄存器**(F-NET-7:全局唯一读者 = `net_poll`);原"close + 500ms 盲等排空循环"已换 sock_reset(内置 LISTEN 态二段 CLOSE 补发,E2 补充发现);控制块整体清零(`last_peer_port` 幽灵基准跨 fd 保留,N-30) |
| `net_drop_conn` | `NET_MSG_DROP_CONN`(自愈收敛阶段 3 新增) | `net_drop_conn_locked` | `net_pending_settle_close` + `net_sock_reset_sync(QUARANTINE)` +(auto_relisten)`net_listener_try` | 0(含幂等)/ -1 | **拆会话保监听**(E-02 收口):应用放弃会话(对时 30s 预算耗尽 / 协议超时)而单连接监听被挂起对端永久占住时的应用侧出口;EOF 路径**不得**调(`auto_relisten` 已接管);非 ESTABLISHED/CONNECTING 直接返 0 不动监听;本层收口 CLOSED 不清 fd(保留 local_port/auto_relisten),监听型 fd 即时重监听、失败交 D10 + reconciler 形态④ |
| ~~`net_relisten`~~ | ~~`NET_MSG_RELISTEN`~~ | **自愈收敛阶段1 已删除**(消息类型同删) | — | — | 原"无条件芯片侧重建"与 pump/D10 完全重叠,且对健康监听器是 footgun(open→USE→close 自毁 ~1s);生产零调用。历史实现见 `net_socket.c` git 历史 |
| ~~`net_resync_listen`~~ | ~~`NET_MSG_RESYNC_LISTEN`~~ | **自愈收敛阶段1 已删除**(语义并入对账⑤) | — | — | D14 静默回收(芯片自行回监听、不发 DISCONNECT)现由 `net_reconcile_tick` 判据⑤自动认领:本层 ESTABLISHED 且芯片 0x05/0x01 → 只修本层(state→LISTENING + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv),**不发芯片命令**(纪律原样保留,run8 实证 170s 拒绝);去抖/冷却沿用对账框架 |
@@ -161,9 +164,9 @@ flowchart TB
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|---|---|---|---|
| `net_pending_register` | 登记挂起(`net_recv_locked` 的"无数据 + 阻塞"路径) | 写 `s_net_pending[fd]` | 每 fd 至多一个:**覆盖会丢失前调用方通知 = 永久挂死**;执行任务自身(netTask)发起的等待直接拒绝(自等自通知 = 死锁,netTask 内须用 DONTWAIT) |
| `net_pending_tick` | 完成检查(`net_process_messages` 每轮调用,非阻塞) | ① `get_recv_len` + `read_recv_buf` ② 到点(仅显式预算) ③ `get_recv_len` 排空 + `net_listener_try` ④ `net_pending_complete` | 按 **①数据 ②到点(仅 `net_recv_to` 显式预算;无限登记无 deadline)③断连** 顺序检查(逐项对齐旧自旋语义);CONNECTING/LISTENING 等状态**保持等待**(CONNECT→ESTABLISHED 是合法迁移,不做状态推断取消,防误杀正在建链的等待);**tripwire**(自愈收敛阶段 1.5,原看门狗降级):挂起超 60s = 完成逻辑缺位 → 只 `[ERR]` 留痕一次,**绝不强制了结、绝不伪造 TIMEDOUT**——③ 与显式 cancel 是无限 recv 的全部唤醒责任方 |
| `net_pending_tick` | 完成检查(`net_process_messages` 每轮调用,非阻塞) | ① `get_recv_len` + `read_recv_buf` ② 到点(仅显式预算) ③ `get_recv_len` 排空 + `net_listener_try` ④ `net_pending_complete` | 按 **①数据 ②到点(仅 `net_recv_to` 显式预算;无限登记无 deadline)③断连** 顺序检查(逐项对齐旧自旋语义);CONNECTING/LISTENING 等状态**保持等待**(CONNECT→ESTABLISHED 是合法迁移,不做状态推断取消,防误杀正在建链的等待);**挂起超时留痕探针**(编译期选项 `NET_PENDING_TRIPWIRE`,默认关):挂起超阈值 = 完成逻辑缺位 → 只 `[ERR]` 留痕一次,**绝不强制结束、绝不伪造 TIMEDOUT**——③ 与显式 cancel 是无限 recv 的全部唤醒责任方 |
| `net_pending_complete` | 所有完成/取消路径的公共出口 | `xTaskNotify(caller, result)` + 清 active | 通知值 = 结果原值(>0 字节 / 0 EOF / 负错误码,与消息分派同一回传通道);失败结果同步 `s_net_errno`(netTask 上下文一致性);无在位挂起时静默返回(防重复通知) |
| `net_pending_settle_close` | close 前**了结**本 fd 挂起 | `get_recv_len` + `read_recv_buf` + `net_pending_complete` | 关闭不得丢已到达数据(芯片缓冲 close 前可读):有数据 → 按字节数通知;无数据 → 取消(CONNRESET);先于 `ch395f_close_socket` 执行 |
| `net_pending_settle_close` | close 前**结束**本 fd 挂起 | `get_recv_len` + `read_recv_buf` + `net_pending_complete` | 关闭不得丢已到达数据(芯片缓冲 close 前可读):有数据 → 按字节数通知;无数据 → 取消(CONNRESET);先于 `ch395f_close_socket` 执行 |
| `net_pending_cancel` / `cancel_all` | 显式取消(**必通知**) | `net_pending_complete` | 取消路径:陈旧 resync(单 fd)/ 网络重建(全部,`net_init` 开头,早于控制块清零)——"取消不通知 = 调用方永久挂死" |
| `net_pending_clear` | 清除**不通知** | 清 `active` | 仅用于"新 fd 不得继承"(`net_socket`)与 close 后幂等清理(`net_close_locked`)——这两处前驱通知已由 settle 路径发出 |
@@ -374,7 +377,7 @@ sequenceDiagram
| 5 | **基于状态读的重建动作必须去抖 + 冷却**:分级门槛 ②=1 轮 / ③④⑤=2 轮 / ①=4 轮(100ms/轮)+ 冷却 1s | 2026-09-23:无去抖 → 监听反复重建直至 `relisten give up`,三服务口全死 |
| 6 | **异常释放(TIMEOUT)后复监听前必须隔离**:排空 RX 残渣 + 等 CLOSED/TCP_CLOSED;隔离上限 < 重试放弃时限 | TRAP-NET-18:~2KB 残渣被下条连接当自己的数据 → 首笔上传截断 2048B 并误报 226 成功;上限=2s 时隔离耗尽重试预算 → 监听被弃 |
| 7 | **RECV 电平触发中断每轮只处理一批** | TRAP-NET-08:连续循环 → 主循环饿死 |
| 8 | **close 前先了结挂起 recv + 排空残留 DISCONNECT** | TRAP-NET-12:锁存的陈旧 DISCONNECT 秒断下一连接 |
| 8 | **close 前先结束挂起 recv + 排空残留 DISCONNECT** | TRAP-NET-12:锁存的陈旧 DISCONNECT 秒断下一连接 |
| 9 | **两段式等待防丢失通知**:零等待拾取 + 阻塞;清除掩码全 1;以 pdTRUE 判定 | run3~11:丢失通知 → 挂死 / 结果错位;0 结果误判"无通知" → 永久挂死 |
| 10 | **网络重建先取消并通知全部挂起,再复位芯片** | 取消不通知 = 调用方永久挂死 |
| 11 | **监听重建只有一个所有者**(`net_reconcile_tick` 形态③④,自愈收敛阶段 3 合并后;原 `net_listener_pump` 已删);其它位置**只置事实**(置 CLOSED / 登记沉降),不做重建动作 | N-32 P2 / B10:多所有者导致"谁先看到 DISCONNECT 就谁重建"的竞争与重建风暴 |
+68 -16
View File
@@ -1,6 +1,6 @@
# Net 层阻塞模型架构设计
> **模块名称**:Net 层(`net_socket.c` / netTask)阻塞等待模型
> **模块名称**:Net 层挂起 recv 模型(`net_pending.c` / netTask 主循环节拍)
> **文档定位**:**现状架构设计说明**——描述本层**当前如何组织等待与完成**、为什么这样设计、
> 边界条件与已验证的性能特征。**不是历史记录**:历史演进、门禁实测记录与修订日志
> 一律不保留在文档内(需要时查 git 历史)。
@@ -44,8 +44,8 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线
| 区域 | 内容 |
|---|---|
| **左栏 · 调用方任务** | `net_recv` → 判"数据已在?" → **是**走快速路径;**否**(`timeout > 0`)登记 pending 后在自己上下文睡任务通知 |
| **中栏 · netTask 主循环** | ~1ms 节拍:`net_poll`(刷新 readable + 全量刷新驱动状态缓存)→ 3 个非阻塞 tick(`net_listener_retry_tick` D10 → `net_pending_tick` 了结挂起 → `net_reconcile_tick` TRAP-NET-17 对账 + 监听重建唯一入口)→ 排空消息队列 |
| **下带 · 五路判定** | 每轮对每个挂起 fd 按序判定:数据到达 / 到点(仅显式预算)/ EOF / 同 fd 后续操作 / 网络重建;tripwire 60s 只留痕 |
| **中栏 · netTask 主循环** | ~1ms 节拍:`net_poll`(刷新 readable + 全量刷新驱动状态缓存)→ 3 个非阻塞 tick(`net_listener_retry_tick` D10 → `net_pending_tick` 结束挂起 → `net_reconcile_tick` TRAP-NET-17 对账 + 监听重建唯一入口)→ 排空消息队列 |
| **下带 · 五路判定** | 每轮对每个挂起 fd 按序判定:数据到达 / 到点(仅显式预算)/ EOF / 同 fd 后续操作 / 网络重建;挂起超时留痕探针(编译期选项,默认关,见 §9)只留痕 |
| **回环** | 判定完成经 `xTaskNotify` 唤醒调用方(结果 = 字节数 / 负错误码) |
**关键性质**:
@@ -70,7 +70,7 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线
| 项 | 设计 |
|---|---|
| 存储 | **独立静态数组** `s_net_pending[NET_MAX_SOCKETS]`(不置于 `net_sock_t` 内) |
| 字段 | `active` / `infinite` / `tripwire_logged` / `buf` / `len` / `deadline` / `registered` / `caller`(`infinite`:0 预算 = 无限等待;`tripwire_logged`:tripwire 只留痕一次) |
| 字段 | `active` / `infinite` / `buf` / `len` / `deadline` / `registered` / `caller`(`infinite`:0 预算 = 无限等待;留痕开启时另有 `tripwire_logged`:保证只留痕一次) |
| 容量 | 每 fd **至多一个**挂起请求 |
| 占用 | ≈24B/fd × 8 fd = **192B**(+4B 任务句柄),实测 SRAM +200B |
@@ -89,28 +89,63 @@ socket 生命周期解耦,与 `net_listener_retry_tick` 的重试表同构。
---
## 4. 完成与取消语义(五路)
## 4. 挂起的 recv 会被什么结束(5 种触发条件)
`net_pending_tick()` 在主循环每轮运行,对每个挂起 fd 按序判定:
挂起 recv 被结束的方式只有 5 种,下文简称**五路**——①②③ 由 `net_pending_tick()`
每轮对每个挂起 fd 按序判定,④⑤ 由显式操作触发:
| # | 条件 | 动作 | 设计要点 |
|---|---|---|---|
| 1 | **数据到达** | SPI 读出 + 通知(结果 = 字节数) | 由 `net_poll` 维护的 `readable` 门控,**零额外 SPI**;仅 ESTABLISHED / UDP_OPEN 态可读;spurious readable 需清除 |
| 2 | **到点** `now >= deadline`(仅显式预算登记;无限等待登记无 deadline) | 通知 `NET_ERR_TIMEDOUT`(对外 -2 映射保持) | 状态无关;用有符号回绕安全差值,防 tick 回绕;这是 `net_recv_to` 调用方策略的正常兑现,不是伪造 |
| 3 | **CLOSED**(EOF/DISCONNECT) | 排空:有数据则读出;否则按 0(EOF) 完成 + `auto_relisten` + 通知 | CLOSE 先行完成挂起 = `net_close` 的 settle 钩子(排空→数据否则 CONNRESET) |
| 4 | **同 fd 后续操作** | 先结算挂起,再执行新操作 | per-fd 串行——冻结面从"全网"缩到"本 fd" |
| 4 | **同 fd 后续操作**:close / drop_conn / 对账 resync(芯片静默回收) | 先结束挂起 recv(排空 → 有数据按字节数完成;否则取消 CONNRESET),再执行新操作 | per-fd 串行:pending 模型唯一留下的次序面就在本 fd 内,其他 fd 不受影响。同 fd 第二个 recv 被登记守卫拒绝(BUSY);send 无需结算(收发不冲突,SPI 由 netTask 串行) |
| 5 | **网络重建**(`net_stack_bring_up`,芯片复位) | **取消 + 必须通知** | `net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;**取消不通知 = 调用方永久挂死** |
**状态推断的克制**:CONNECTING / LISTENING 等中间态**保持等待**,**不做**状态推断式取消——
CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的等待。
### 4.1 tripwire
### 4.1 一次阻塞 recv 从调用到返回的全过程
挂起超 60s(`NET_RECV_TRIPWIRE_MS`)= "完成逻辑缺位"探针 → **只 `DBG_ERROR` 留痕一次,
绝不强制了结、绝不伪造 TIMEDOUT 返回值**——强制了结会掩盖完成路径缺陷,伪造返回值
违背调用方意图。
定位:留痕成本极低的开发期探针;唤醒责任在 ①数据 ②到点(仅显式预算)③断连 与
显式 cancel 路径(close / 网络重建 / 陈旧重同步),见 §4 五路。
一句话链:**登记(μs 级)→ 调用方自己睡 → 每轮判定(只读标志与到期时间)→ 公共出口(通知+清槽)→ 两段式等待取结果**。以 `net_recv(sockfd, buf, len)` 阻塞路径为例:
```mermaid
flowchart TD
A["调用方任务:net_recv(sockfd, buf, len)"] --> B["消息入队,随后在<br/>自己上下文睡(两段式等待)"]
B -.-> C
subgraph NT["netTask —— 唯一执行者(~1ms 节拍)"]
C{"数据已在?<br/>readable / recv_len > 0"}
C -- "是(常见)" --> D["直接读出"]
C -- "否 + 阻塞模式" --> E["登记挂起请求(μs 级)<br/>守卫:每 fd 至多一个、拒绝 netTask 自等待"]
E --> F["随即返回,去处理下一条消息<br/>—— 等待不占用执行者"]
F --> G["每轮检查每个挂起 fd"]
G -- "未命中 → 等下一轮" --> G
G -- "数据到达" --> H1["SPI 读出"]
G -- "到点(仅显式预算)" --> H2["结果 = TIMEDOUT"]
G -- "断连(CLOSED / session_dead)" --> H3["排空:有残留 → 读出<br/>无残留 → EOF(0) / 异常 CONNRESET"]
H1 --> I
H2 --> I
H3 --> I
end
D --> I
I["通知调用方(同一回传通道):通知值 = 结果原值<br/>挂起路径经公共出口 net_pending_complete:清槽、防重复通知"]
I --> J["调用方醒来取结果<br/>0 = EOF 也是有效通知(以 pdTRUE 判定)"]
K["取消路径:close / drop_conn / 网络重建"] -. "先结束挂起、再动芯片" .-> I
```
1. **入队**(调用方任务):`net_recv` 把请求打包进消息入 `netMsgQueue`,随后进入两段式等待(步骤 6)。netTask 不被占用。
2. **快速路径判定**(netTask,`net_recv_locked`):数据已在(`readable` / recv_len>0)→ 直接读出、经通知值回传字节数——不产生挂起,常见路径到此为止。
3. **登记**(netTask):无数据且阻塞模式 → `net_pending_register` 过两条守卫(§3.2:每 fd 至多一个不覆盖;拒绝 netTask 自等待)→ 记 `buf`/`len`/`caller` 与预算(`net_recv_to` → `deadline`;无限等待 → `infinite=1` 无 deadline)→ 返回"已登记"哨兵,netTask 立即处理下一条消息。
4. **每轮判定**(netTask,`net_pending_tick`):对每个挂起 fd 按序 ①数据 → ②到点 → ③断连(判据见上表;③ 的判据是 `state==CLOSED || session_dead`,漏 ③ = 无限 recv 永久挂死,勿动)。命中任一条件进入公共出口;挂起超时留痕探针只留痕不完成(编译期选项,默认关,见 §9)。
5. **公共完成出口**(`net_pending_complete`,所有完成/取消路径的唯一出口):`xTaskNotify` 覆盖式写入**通知值 = 结果原值**(>0 字节数 / 0 EOF / 负错误码)→ 清 `active`;无在位挂起则静默返回(防重复通知)。失败结果同步记入 netTask 自己的 errno 槽位(按任务槽位,非跨任务共享全局)。
6. **唤醒与取结果**(调用方任务):所有 `net_*` API 的回执共用同一条任务通知通道,**两段式等待**防"入队→等待入口"间隙内通知丢失——先零等待拾取(清除掩码全 1;以 pdTRUE/pdFALSE 判定已落地,不能判值≠0——合法的 0 结果=EOF 也是有效通知),未拾取到再阻塞等待。通知值 32 位整体作为结果,不切分位域(大单次收发不被截断)。
7. **取消路径**(五路中的 ④⑤,共同规则"**先结束挂起、再动芯片**"):
- **同 fd 后续操作**(close / drop_conn):`net_pending_settle_close` 先结算——芯片 RX 有数据(bit3=1 契约:断连后残数据留缓冲待 MCU 读)→ 按字节数完成;无数据 → 取消(CONNRESET)——然后才执行 close/拆会话。对账发现芯片静默回收(resync)时走 `net_pending_cancel`(连接已被芯片收走,无排空窗口,直接取消必通知)。
- **网络重建**:`net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;取消必通知,否则调用方永久挂死。
8. **buf 生命周期**:挂起期间 `buf` 指针须保持有效至调用返回(挂起表按指针写入,§5)。
---
@@ -144,8 +179,8 @@ CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的
| **SEND 路径未覆盖** | 发送仍为分块阻塞(1s/chunk stall) | 该期间队列仍被冻结;二期 pending 化解决 |
| **ACCEPT 路径未覆盖** | accept 仍用轮询计时 | 需 pending 化(二期) |
| **超时精度** | 随主循环节拍(~1ms),被长操作占用时顺延 | 一期文档化、不修;二期 SEND 化后该顺延面**收窄** |
| **tripwire 未接自检** | 只 `DBG_ERROR` 留痕(不再强制了结),未写 selftest | 二期补齐 |
| **模拟器不可建模** | `net_socket_sim.c` 单线程 Winsock 无法复现**挂起模型内部机制**(外部语义已由包装层按 blocking 属性/预算模拟) | 挂起表/tick/通知路径在模拟器上零覆盖,只能硬件验证;**监听生命周期 `net_listener.c` 亦为真机专属**(内部头只被 `net_socket.c` include,CMakeLists 不编译) |
| **挂起超时留痕探针** | 编译期选项 `NET_PENDING_TRIPWIRE` 默认关(排查挂死时开);未接自检 selftest | 二期补齐自检 |
| **模拟器不可建模** | `net_socket_sim.c` 单线程 Winsock 无法复现**挂起模型内部机制**(外部语义已由包装层按 blocking 属性/预算模拟) | 挂起表/tick/通知路径在模拟器上零覆盖,只能硬件验证;挂起模型实现独立在 `net_pending.c`(真机专属,CMakeLists 不编译;**监听生命周期 `net_listener.c` 同此**) |
**二期扩展设计**(ACCEPT / SEND 的挂起化、门禁清单、改动量)见
`docs/任务规划/基线与待确认.md` §2.1。
@@ -154,10 +189,27 @@ CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的
## 8. 设计铁律(为什么是这样)
1. **执行者永不在等待中自旋**——等待必须在调用方上下文完成,netTask 只负责"登记"与"了结"。
1. **执行者永不在等待中自旋**——等待必须在调用方上下文完成,netTask 只负责"登记"与"结束"。
2. **取消必通知**——任何取消路径(close / 重建 / 陈旧重同步)都必须唤醒调用方,
否则是永久挂死而非错误返回。
3. **挂起状态与 socket 表解耦**——`net_sock_t` 会被整块 memset,跨时间状态不能寄居其中。
4. **不做状态推断式取消**——中间态(CONNECTING/LISTENING)合法迁移存在,
推断取消会误杀正常建链等待。
5. **数据到达不引入额外 SPI**——判据复用主循环既有 `net_poll` 维护的 `readable` 标志。
---
## 9. 排查辅助:挂起超时留痕探针(编译期选项,默认关)
五路判定 + 显式 cancel 是挂起 recv 的**全部唤醒责任**(§4)。一个挂起请求若超过阈值
(60s,`NET_RECV_TRIPWIRE_MS`)还没被任何一路结束,只有一种解释:**完成路径漏了它**
(代码缺陷)——而不是"数据还没到":无限等待本来就是调用方的合法策略,不该被强行打断。
探针的处理:`net_pending_tick` 对这样的 fd **只打一条 `[ERR]` 日志**(每 fd 一次,
防刷屏),然后让它继续等——**绝不强制结束、绝不伪造 TIMEDOUT 返回值**。为什么不用
"强制超时"兜底:强制结束会把"唤醒路径有 bug"这个**缺陷**伪装成一次**普通超时**——
调用方拿到假结果,缺陷永远潜伏(旧实现正是"到点 +1s 伪造 TIMEDOUT",已废除)。
开关:`NET_PENDING_TRIPWIRE`(`net_socket.c`,默认 0 = 编译剔除、产品零成本)——
排查"调用方挂死唤不醒"时置 1 重烧。**开与关语义完全一致**:探针只留痕,不参与完成。
代码里该机制名为 tripwire(绊线:挂住 = 踩到缺陷 wiring)。
+10 -10
View File
@@ -2,7 +2,7 @@
> **定位**:本文是 CH395F 网络自愈架构的**现状权威描述**——设计结构、原理、关键参数及其实证锚点。
> 文中「收敛阶段 0~3」指 2026-10 自愈架构的分阶段实施(过程已归档,见 §9)。
> 架构审计过程(T1~T4 三路审计与 39 条编号发现、E0~E14 实验方法与原始记录、分阶段实施清单)
> 架构审计过程(T1~T4 三路审计与 39 条编号发现、E0~E15 实验方法与原始记录、分阶段实施清单)
> 已完成使命并删除——过程全文在 git 历史(`git show 1a35b36^:docs/网络/N34-自愈审计/`,目录合并前
> 7 份文档)。实证结论已沉淀进:`docs/问题记录/CH395F_Trap_Records.md`(TRAP-NET-01~21 与 B 系列)、
> `docs/子系统设计/网络Socket监听生命周期设计说明.md`(D1~D10 与模块化边界)、`docs/芯片手册/CH395F.md`。
@@ -15,12 +15,8 @@
1. **芯片永不自动回 LISTEN**——单连接复用模型(监听与数据通道复用同一 Socket),
"监听常驻"只能是软件承诺(I4)。
2. **事件面不是可靠唯一信源**——旧"芯片丢中断"说法已被 E14 复核**推翻**(2026-10-14,
48 轮直读 SINT 矩阵:芯片侧事件丢失 = 0,SINT 锁存可靠)。历史"丢失"全部是
**读清消费竞态**(台架 post-listen drain 吞掉即时到达的 CONNECT,drain-int 探针
现场抓获 3/48;生产 B3 类游离全局读已清除)+ **门控观察伪象**(2/48,SINT 30ms 内
即捕获)。但结论不变:事件可被其它合法读者消费,且状态面存在事件覆盖不了的幽灵
(E7b)→ 正确性必须靠回读芯片事实 + 有界超时兜底(I3)。
2. **事件面不是可靠唯一信源**——事件可被其它合法读者消费,且状态面存在事件覆盖不了的幽灵
(E7b;E15:幽灵 = 芯片如实报告旧连接拆链进行中,非谎报)→ 正确性必须靠回读芯片事实 + 有界超时兜底(I3)。
3. **close 有沉降窗、RX 残渣有害**——带非空 RX 关闭会拆链停滞、吞下一会话 SYN 且自持
(E4 变体 C 0/20);沉降窗不定长(TRAP-NET-09/-20)→ 复用前清理必须内联进事务(I5)。
@@ -62,7 +58,7 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要
| tick | 节奏 | 职责 |
|---|---|---|
| `net_poll` | 主循环每轮(≈1ms) | INT# 引脚 → 全局事件汇聚(唯一读者)→ SINT 分派 → **驱动缓存全量刷新** → 链路位喂驱动 → 步进 `ch395f_sock_reset_poll` |
| `net_process_messages` | 每轮 | RPC 消息处理;开头每轮 `net_pending_tick`(挂起 recv 了结)+ `net_listener_retry_tick`(D10) |
| `net_process_messages` | 每轮 | RPC 消息处理;开头每轮 `net_pending_tick`(挂起 recv 结束)+ `net_listener_retry_tick`(D10) |
| `net_reconcile_tick` | 100ms | 唯一周期对账 + 重建(§4.1) |
### 3.3 状态模型(会话投影)
@@ -161,14 +157,17 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要
socket 属性);`net_recv_to()` = 显式预算便捷入口(`timeout_ms=0` 仍拒 INVAL)。
- 唤醒源 = 数据 / EOF / close / 网络重建 CONNRESET;会话死亡兜底 = 形态①(≈400ms)。
- **L3 pending recv**:阻塞 recv 不独占 netTask(挂起登记 + 主循环节拍完成);
**tripwire**:挂起超 60s 只 `[ERR]` 留痕一次(完成逻辑缺位探针),不伪造 TIMEDOUT。
**挂起超时留痕探针**(编译期开关 `NET_PENDING_TRIPWIRE`,默认关):挂起超阈值只
`[ERR]` 留痕一次(完成逻辑缺位),不伪造 TIMEDOUT。
- **术语**:"结束挂起 recv" = `net_pending_complete` 把结果(字节数 / EOF(0) / 错误码)
回传给调用任务并唤醒它,挂起槽随之释放(旧称"了结",2026-10-14 更名)。
- **文档契约**(嵌入式特有):IWDG `required_mask` 内任务**禁止无限阻塞 recv**——
必须 `net_select` 超时或 `net_recv_to` 显式预算。
- 上层 32 轮盲收(`ui_net_sink` / `iec104_serv`)保留现状(E6 裁决,§4.8)。
### 4.6 `net_drop_conn`——应用主动拆会话保监听(E-02 收口)
- 语义:`net_pending_settle_close` 先了结挂起 recv → `net_sock_reset_sync(QUARANTINE)`
- 语义:`net_pending_settle_close` 先结束挂起 recv(回传结果 + 唤醒调用方)→ `net_sock_reset_sync(QUARANTINE)`
→ 本层收口 CLOSED(不清 fd)→ `auto_relisten` 即时 `net_listener_try`
(失败交 D10 + 形态④)。
- **幂等**(非 ESTABLISHED/CONNECTING 直接返 0);**EOF 路径不得调**
@@ -233,6 +232,7 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要
| E6 | U15 命中=0(~4h 常态 + 压测;用户裁决提前收线) | §4.8 观察项 |
| E-02 | `drop_conn` 时序定案:配合对端 TIME_WAIT ~150ms→形态④;不配合 FIN_WAIT_2 长尾 | §4.6 |
| E14 | "丢中断"复核(2026-10-14,48 轮直读 SINT):芯片侧事件丢失 = 0;"丢失"全部 = drain 读清消费(3/48,drain-int 现场抓获)+ 门控竞态(2/48)+ 主机同步(1/48);幽灵态本轮 0/48 未复现 | §1 事实 2 改写;TRAP-NET-17/-20 归因更正(机制保留不变);生产 listen/relisten 排事件挪至 LISTEN 命令**前**(根除自伤窗) |
| E15 | 幽灵态归因复核(2026-10-14,官方出处):0x04+旧端口 = 芯片**如实报告**旧连接拆链进行中——① bit3 `SOCKET_CLOSE=1` 为我方设置(版本门控,§5.52②:DISCONNECT/TIM_OUT 后芯片按设计不动状态与缓冲);② 拆链走 RFC 9293 §3.3.2 状态机:主动关闭 FIN_WAIT_1→FIN_WAIT_2→TIME_WAIT(2MSL 驻留;TIME_WAIT 协议目的=防旧连接迟到报文伤新连接——"断连即重连被拒"的协议根源),芯片自动走完(手册 §5.22);§5.17 默认重传 12 次、实测耗尽 ≥3s,与拆链窗实测 >2s 同量级;③ WCH 官方博客确认"断连后立即重连被拒、需程序侧特殊处理" | TRAP-NET-20"谎报"→"如实报告";settle 等待 = 官方方案;幽灵甄别 = 强制放行路径的护栏;I3 就此加强 |
## 6. 红线(明确不做的事)
+3 -2
View File
@@ -124,7 +124,8 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修
- **状态**:✅(回归 4 轮 11/11;全局寄存器读即清见第六章 B3)
- **现象**:同一会话内**第 2 次及以后**的数据连接(LIST/STOR/RETR)两类失败,概率随上一条数据连接的负载与间隔变化:① **吞数据**——新连接建立成功、服务端 `net_send` 返回成功,客户端 20s 收不到任何字节;② **425**——新连接被接受后 ~10-20ms 出现 DISCONNECT(`chip tcp=0x00`),accept 失败。
- **定案实验(裸机芯片台架,剥离 OS/net/FTP)**:`test/board/ch395f_reuse_bench.c` 内核启动前直接调 `ch395f_*` 受控矩阵 + 主机侧 `test/host/ch395f_reuse_host.py`。**剥离后 bug 依旧复现** → 纯芯片操作时序问题。关键现场:`post-listen st=0x05/0x04`(listen 成功但 TCP 态停在旧连接 ESTABLISHED=幽灵态)、`unexpected int=0x10`(只等来旧连接滞后 DISCONNECT)、`no CONNECT within 10s`(芯片状态已 0x04 但事件始终未到等待环——E14 复核:事件已被 drain 读清消费,非芯片丢失)。
- **芯片级规律**:CH395F 拆链收尾窗口**变长且不可观测**(状态显示 CLOSED 持续 1s+ 也不代表拆完,实测可 >2s 且时长不定)。窗口内重新 `open+listen` 三种形态:① 握手完成但 **CONNECT 事件未收到**(E14 复核:事件被台架 post-listen drain 或读清竞态吞掉,非芯片侧丢失)→ 吞数据;② 状态直接假报 0x04 幽灵态(回读对端端口=旧连接端口);③ 旧连接滞后 DISCONNECT 打到新监听 → relisten 毁掉新连接 → 425。
- **芯片级规律**:CH395F 拆链收尾窗口**变长且不可观测**(状态显示 CLOSED 持续 1s+ 也不代表拆完,实测可 >2s 且时长不定)。窗口内重新 `open+listen` 三种形态:① 握手完成但 **CONNECT 事件未收到**(E14 复核:事件被台架 post-listen drain 或读清竞态吞掉,非芯片侧丢失)→ 吞数据;② 状态显示 0x04 幽灵态——旧连接拆链进行中,状态如实如此(E15 见下条;回读对端端口=旧连接端口);③ 旧连接滞后 DISCONNECT 打到新监听 → relisten 毁掉新连接 → 425。
- **归因更正(E15,2026-10-14,官方出处)**:旧"芯片谎报 0x04"措辞推翻——芯片**如实报告旧连接拆链进行中**:① `FUN_PARA` bit3 `SOCKET_CLOSE=1` 为我方设置(`net_init` 版本门控 ≥0x46;§5.52②/§5.23:DISCONNECT/TIM_OUT 后芯片**按设计不动 Socket 状态与缓冲**,等 MCU 关闭——"保留现场"是自家配置的文档化行为);② 拆链走 TCP/IP 协议状态机(RFC 9293 §3.3.2 图 5;芯片状态码表见手册 §5.22,明言"其他状态芯片会自动处理并更新"):主动关闭 FIN_WAIT_1→FIN_WAIT_2→TIME_WAIT(对端同时关经 CLOSING、同时关闭直达),TIME_WAIT 驻留 2MSL 才落 CLOSED——这就是拆链窗;TIME_WAIT 的协议目的即"防旧连接迟到报文影响新连接"(RFC 9293 §3.3.2),正是"断连即重连被拒"的协议层根源。重传默认 12 次(§5.17),实测重传耗尽 ≥3s(`test/board/ch395f_test_task.c` 超时分类实测)——与拆链窗实测 >2s 同量级;③ WCH 官方博客(《【WCH以太网接口系列芯片---CH395】CH395 TCP Server模式下客户端重连设置》,cnblogs sw111/18451802)明言"客户端主动断连并立即重新发起连接请求,此时 CH395 会拒绝该连接请求,因此需要程序中做一些特殊处理",官方方案 = 关闭完成后再 open+listen——即我们的 settle/quarantine 等待;幽灵甄别是"强制放行"路径的护栏(防把拆链中的连接当新连接 accept)。状态面从来是真——I3 就此加强。
- **已证伪的修复**(受控实验全部无效,列出防重踩):close 后固定沉降 100~3000ms / Socket 轮换(跨 socket 毒化)/ listen 后 close+重试 / 缓冲重分配 / close 前 TX 排空延时 / 禁用对账 tick(对账不是元凶)。
- **定案一:双信源原则——中断只是快路径,状态寄存器才是真相**:
1. **accept 路径内联探针**(`net_lost_connect_probe`,`net_accept_locked` 每 100ms 挂探针):芯片已 ESTABLISHED 而 CONNECT 事件未到 → 连续 2 轮去抖后补 accept;
@@ -187,7 +188,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修
- **修复(三阶段,`net_socket.c` + `lftpd.c`)**:
1. **异常释放(TIMEOUT/RST)隔离**:`handle_timeout_event` 置 per-fd 隔离标记;`net_relisten_locked` 隔离期内持续排空芯片 RX(`net_drain_chip_rx()`)+ 主动 close + 排空该 socket 中断 + 等 `CLOSED/TCP_CLOSED`,满 `NET_RELISTEN_QUARANTINE_MS`(400ms) 才 open+listen;上限 `NET_RELISTEN_SETTLE_MS`(800ms) 强制放行——**必须小于** `NET_RELISTEN_DEADLINE_MS`(2000ms):取 2000ms 会耗尽重试预算 → give up → **监听器死亡**;
2. **正常释放(FIN)沉降**:`handle_disconnect_event` 登记(仅 `recv_bytes != 0` 时——纯连接即关与只发不收免沉降保复连时序),relisten 前最短等 `NET_RELISTEN_GRACEFUL_MS`(**100ms**,实测悬崖:0/30ms 无效、100ms 稳定通过);日志区分 `relisten settle`/`relisten quarantine`;
3. **应用侧硬化**:`net_sock_t.released_abnormal` 标记(TIMEOUT 置 1/DISCONNECT 置 0,新 fd 与新监听清零——新连接不得继承上一条的释放原因);`net_pending_tick` 断连了结按标记以 `NET_ERR_CONNRESET` 或 EOF(0) 回传;`lftpd receive_file` 错误路径 **unlink 截断文件** + STOR 回 **450**。
3. **应用侧硬化**:`net_sock_t.released_abnormal` 标记(TIMEOUT 置 1/DISCONNECT 置 0,新 fd 与新监听清零——新连接不得继承上一条的释放原因);`net_pending_tick` 断连结束时按标记以 `NET_ERR_CONNRESET` 或 EOF(0) 回传;`lftpd receive_file` 错误路径 **unlink 截断文件** + STOR 回 **450**。
- **协议边界(不可判定,留痕)**:FTP STOR 无长度声明,"在途 FIN 关闭"与"正常完成"**逐字节不可区分** → 优雅中止仍回 226;仅 RST/掉链(芯片 TIMEOUT)可识别 → 450。FIN 边界由客户端长度校验(SIZE/RETR 往返)兜底。
- **回归基线**:`--check-abort` RST **5/5**(450)+ FIN **5/5**(226 边界不变);followup 全过;正常往返 freeze SHA-256 全 MATCH;`:7003` sink 复用 RST×4/FIN×4 全过;`:8888` 真实收包写 RTC 成功。
- **与 TC-NET-604 的关系**:本条是独立、已可信复现并已修复的真实缺陷;TC-NET-604 已结案为非缺陷(判读教训见第六章台架纪律)。现场"上传失败/文件不完整/像卡死"类报告优先用本条判据复核。
+1
View File
@@ -117,6 +117,7 @@ SIM_EXEMPT = [
("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/NET/net_pending.c", "MCU 挂起 recv 模型(依赖 ch395f_* SPI 读出与 FreeRTOS 通知),模拟器另有 net_socket_sim.c 按外部语义模拟"),
("Drivers/BSP/RS485/rs485.c", "MCU RS-485 驱动"),
("Drivers/BSP/SD2506/sd2506.c", "MCU RTC 驱动"),
("Drivers/BSP/TPAFE5160/tpafe5160.c", "MCU 计量芯片驱动"),
+1 -1
View File
@@ -1,4 +1,4 @@
date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
2026-10-12 209492 18332 1104 154184 227824 21.7 97864 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;console_log.o=4120;ch395f.o=4106;default_task.o=2736;sys_log_mcu.o=1924
2026-10-13 211456 18760 1104 154232 230216 22.0 97912 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924
2026-10-14 211448 18760 1104 154232 230208 22.0 97912 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924
2026-10-14 211324 18680 1104 154232 230004 21.9 97912 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924
1 date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
2 2026-10-12 209492 18332 1104 154184 227824 21.7 97864 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;console_log.o=4120;ch395f.o=4106;default_task.o=2736;sys_log_mcu.o=1924
3 2026-10-13 211456 18760 1104 154232 230216 22.0 97912 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924
4 2026-10-14 211448 211324 18760 18680 1104 154232 230208 230004 22.0 21.9 97912 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924
+2 -2
View File
@@ -74,7 +74,7 @@ FAST_PATH = ("立即 SPI 读出",
LOOP_STEPS = [
("net_poll()", "刷新 readable", C_CHIP),
("net_pending_tick()", "了结挂起", C_TICK),
("net_pending_tick()", "结束挂起", C_TICK),
("net_listener_retry_tick()", "重试对账", C_ACTOR),
("排空消息队列", "服务所有消费者", "#64748b"),
]
@@ -87,7 +87,7 @@ PHASES = [
("⑤", "网络重建(芯片复位)", "取消 + 必须通知(否则调用方永久挂死)", C_CANCEL),
]
WATCHDOG = "看门狗:到点后 1s 仍未了结 = 完成逻辑缺位 → 强制了结 + DBG_ERROR"
WATCHDOG = "挂起超时留痕探针(编译期选项,默认关):挂起超阈值仍未结束 → 只 DBG_ERROR 留痕(不强制结束、不伪造返回值)"
FOOT = ("关键性质:挂起期间 netTask 零额外占用(判据复用主循环既有 net_poll 的 readable,"
"不引入额外 SPI 事务);公开 API 不变,既有调用方 / 测试 / 模拟器零改动。")