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
+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);
}