feat(NET): SEND pending 二期落地 + 末块挂起越界缺陷修复(台架复测闭环)

net_pending.c/h 增设第二张挂起表 s_net_send[](与 recv 表完全独立,收发
互不阻塞);net_send_locked 快速路径改为有界自旋,单次等待超出
NET_SEND_SPIN_MS(2ms) 即登记挂起 send,由 net_send_pending_tick 按主循环
节拍分块推进,netTask 最坏停滞由 1s 压到 2ms。net_listener_retry_tick 的
挂起保险扩展到 send 侧;net_close_locked / 网络重建 / 状态对账三路均先
取消再动作(取消必通知,调用方永不挂死)。

台架复测(A/B 对照 + 插桩定位)发现并修复末块挂起越界缺陷:内层"等本块
发完(SENBUF_FREE)"位于写块之后,写完末块时 sent 已 == len,若该次等待越过
2ms 预算便会调用 net_send_pending_register,而登记函数按 sent >= len 拒收
并回 -1,把一次实际已完整交付芯片的发送报成失败。修复为内层等待开头判
sent >= len 即跳出(末块已交付、SENBUF_FREE 等待是纯保守自旋),并置于
超时/自旋判定之前;该守卫同时保证后续登记必伴随 sent < len。

复验:ftp_regression 连续 19 次 11/11;ftp_integrity_test 20 轮 x 64KB
STOR/RETR SHA-256 20/20 一致(两轮),吞吐稳定约 80KB/s;设备日志零
write error;ST-LINK 回读 No difference found。

文档:Net层阻塞模型架构设计.md 补二期设计与新增 9.1 缺陷记录,netTask
栈实测峰值按本次台架 920B 订正为 1116/3840(FTP 传输期 1164);网络自愈
架构设计.md 吸收原监听生命周期文档、新增编号术语导读表,并把 5.2
"实验锚点速查"的 E 编号含义与 E-02 审计编号歧义写在表头(用户反馈该表
E 编号无法读懂)。

门禁:Keil 0 错 0 警;cppcheck / 编码规范 / 乱码零告警;
check_mem_numbers PASS;check_doc_links --all --sections-strict 142 篇 PASS。
This commit is contained in:
2026-10-14 12:05:21 +08:00
parent df13dec479
commit f849c79b7a
24 changed files with 1626 additions and 882 deletions
+103 -7
View File
@@ -1,12 +1,14 @@
/*
* 模块名称:挂起 recv 请求表(阻塞 recv 挂起模型)
* 文件说明:阻塞 recv 的"挂起请求"数据结构与生命周期:登记 / 每轮完成检查 /
* 模块名称:挂起请求表(阻塞 recv / send 挂起模型)
* 文件说明:阻塞 recv / send 的"挂起请求"数据结构与生命周期:登记 / 每轮完成检查 /
* 取消(close、网络重建、对账 resync)/ 幂等清理。
* recv 侧 = 数据到达即完成;send 侧 = 分块推进至全部写完(进度刷新停滞计时)。
* 架构与五路语义见 docs/网络/Net层阻塞模型架构设计.md
* 依赖:net_types.h(net_sock_t);消费方 = net_socket.c(消息分派 / close /
* 重建 / 对账)
* 限定条件:除 net_pending_active / net_pending_clear(纯 RAM 读)外,
* 全部函数须 netTask 上下文
* 依赖:net_types.h(net_sock_t);ch395f.h(send 侧分块写 + 缓冲常量);
* 消费方 = net_socket.c(消息分派 / close / 重建 / 对账)、
* net_listener.c(挂起保险查询)
* 限定条件:除 net_pending_active / net_send_pending_active / *_clear(纯 RAM 读)
* 外,全部函数须 netTask 上下文
*/
#ifndef NET_PENDING_H
#define NET_PENDING_H
@@ -15,6 +17,7 @@
#include "FreeRTOS.h"
#include "task.h"
#include "net_types.h"
#include "ch395f.h"
/* 内部哨兵:pending 已登记(消息分派据此跳过即时通知;取值不与字节数空间冲突) */
#define NET_PENDING_REGISTERED 0x70000000
@@ -22,12 +25,21 @@
/* 挂起超时留痕探针:排查"调用方挂死唤不醒"时置 1——挂起超 NET_RECV_TRIPWIRE_MS
* 只留一条 [ERR](哪个 fd/多少毫秒/什么基准),绝不强制结束、绝不伪造返回值。
* 开与关语义完全一致,差别只在留痕;默认 0 = 产品构建零成本。
* 见 docs/网络/Net层阻塞模型架构设计.md §9 */
* 见 docs/网络/Net层阻塞模型架构设计.md §12 */
#define NET_PENDING_TRIPWIRE 0
#if NET_PENDING_TRIPWIRE
#define NET_RECV_TRIPWIRE_MS 60000U /* 挂起超时留痕阈值(ms) */
#endif
/* 单次写入 CH395F 发送 FIFO 的最大字节数:受硬件发送缓冲
* (CH395F_SEND_BUF_SIZE_BYTES = 1KB)与 SPI DMA 载荷
* (CH395F_SPI_DMA_MAX_PAYLOAD = 2KB)双重限制,以硬件发送缓冲为硬上限
* (TRAP-NET-16:单次写 2KB/4KB 会写穿 1KB 发送 FIFO)。
* ⚠ 唯一定义处:发送快速路径(net_send_locked)与挂起 send 推进
* (net_send_pending_tick)**必须**共用同一粒度,否则两条路径的分块行为漂移。 */
#define NET_SEND_CHUNK_MAX ((int)CH395F_SEND_BUF_SIZE_BYTES < (int)CH395F_SPI_DMA_MAX_PAYLOAD ? \
(int)CH395F_SEND_BUF_SIZE_BYTES : (int)CH395F_SPI_DMA_MAX_PAYLOAD)
/*
* 函数功能:捕获执行任务句柄(net_init 顶部调用一次;登记守卫据此拒绝自等待死锁)
* 入口参数:无
@@ -116,4 +128,88 @@ void net_pending_settle_close(int sockfd, net_sock_t *p_sock);
*/
int net_pending_active(int sockfd);
/* ================= 挂起 send 请求表(SEND pending,二期) =================
* 与 recv 表**完全独立**(各自一张表、各占一槽):收发两个方向互不阻塞,
* 同一 fd 可同时有一个挂起 recv 与一个挂起 send(今天的分片实现本就支持该并发)。
* 语义差异:recv 是"等一次到达即完成",send 是"分块推进直到全部写完",
* 故 send 的停滞计时按**无进展**刷新(大文件传输总时长必然超过单块预算)。
*/
/*
* 函数功能:登记一个挂起 send 请求(net_send_locked 的"等发送缓冲"超预算路径)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* buf - 调用方发送缓冲基址(须保持有效至完成;本函数不拷贝数据) const void *
* len - 本次发送**总**长度(含登记前已写入芯片的部分) int
* sent - 登记前已写入芯片的字节数(进度起点,0 ≤ sent < len) int
* caller - 调用方任务句柄(完成通知目标) TaskHandle_t
* 出口参数:无
* 返回值:NET_PENDING_REGISTERED - 登记成功(分派跳过即时通知)
* -1 - 拒绝(参数非法 / 同 fd 已有挂起 send / 执行任务自身发起)
* 限定条件:netTask 上下文
* 函数说明:每 fd 至多一个挂起 send(禁止覆盖——覆盖丢失前调用方通知 = 永久挂死);
* 执行任务自身发起(net_send_sock 阻塞用法)无他人可完成 → 拒绝(自等死锁)。
* ⚠ **调用方铁律**:`sent < len` 是调用侧必须保证的**前置条件**,不是本函数的
* 兜底。`sent == len` 表示"全部字节已交付芯片、无剩余块可挂起"——此时超预算
* 应**直接返回 sent**,不得调用本函数;本函数会按参数非法拒绝并回 -1,
* 把一次完整发送报成失败(net_send_locked 末块等待 2026-10-14 台架实证,
* 见《Net层阻塞模型架构设计.md》 §9.1)。
*/
int net_send_pending_register(int sockfd, const void *buf, int len, int sent,
TaskHandle_t caller);
/*
* 函数功能:挂起 send 推进与完成检查(netTask 每轮 net_process_messages 调用一次,非阻塞)
* 入口参数:p_socks - Socket 控制块表首指针(net_socket.c 的 s_net_socks) net_sock_t *
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文
* 函数说明:逐 fd 按序判定:①断连(state 非 ESTABLISHED / session_dead)→ 以已发
* 字节数结束;②推进——发送缓冲空闲则写下一块(每轮至多一块,写完即清
* send_ready,下一块等 SENBUF_FREE 由 net_poll 重新置位);③全部写完 →
* 以总长度结束;④无进展超时 → 以已发字节数结束。结果均为**已发字节数**
* (短写由调用方按 `ret != len` 判定,与分片实现契约一致)。
*/
void net_send_pending_tick(net_sock_t *p_socks);
/*
* 函数功能:清除本 fd 的挂起 send(不通知;"新 fd 不得继承"与 close 后的幂等清理)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* 出口参数:无
* 返回值:无
* 限定条件:无
*/
void net_send_pending_clear(int sockfd);
/*
* 函数功能:取消本 fd 的挂起 send 并通知调用方(close / drop_conn / 对账 resync / 重建)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* err - 取消原因(记入 netTask 错误码槽位 + 日志) int
* 出口参数:无
* 返回值:无(无在位挂起时静默返回)
* 限定条件:netTask 上下文
* 函数说明:以**已发字节数**结束(已写入芯片的字节不回收——对端可能已收到;
* 未发部分放弃,由调用方按短写处理),原因记 errno 与 [ERR] 日志
*/
void net_send_pending_cancel(int sockfd, int err);
/*
* 函数功能:取消全部挂起 send 并逐一通知(网络重建路径)
* 入口参数:err - 取消原因 int
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文;须在控制块清零与芯片复位之前调用
*/
void net_send_pending_cancel_all(int err);
/*
* 函数功能:查询本 fd 是否有在途挂起 send —— 宿主能力,供 net_listener 用
* 入口参数:sockfd - Socket 描述符 (0~7) int
* 出口参数:无
* 返回值:1 - 有挂起;0 - 无(含描述符越界) int
* 限定条件:任意上下文(纯 RAM 读,不发起 RPC)
* 函数说明:与 net_pending_active 同用途——net_listener_retry_tick 据此跳过
* "重开 Socket 会摧毁在途传输"的 fd(挂起 send 保险)
*/
int net_send_pending_active(int sockfd);
#endif /* NET_PENDING_H */