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
+279 -8
View File
@@ -1,21 +1,24 @@
/*
* 模块名称:挂起 recv 请求表(阻塞 recv 挂起模型)—— 实现
* 模块名称:挂起请求表(阻塞 recv / send 挂起模型)—— 实现
* 文件说明:见 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)
* net_listener.h(net_listener_try)/ ch395f.h(RX 读出 / TX 分块写)/ 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 错误(错误码经通知值回传)
* 阻塞 recv / send 挂起模型(E-02 结构演进):阻塞等待 = 挂起请求 + 主循环节拍完成
* "无数据 / 发送缓冲未空 + 阻塞等待"不再自旋占用 netTask:登记 pending 后立即返回
* net_pending_tick / net_send_pending_tick(与 D10 重监听重试同节奏,
* net_process_messages 开头每轮调用)
* recv:按 ①数据 ②到点(仅显式预算) ③断连 顺序检查完成条件并通知调用方
* send:按 ①断连 ②推进写块 ③写完 ④无进展超时 顺序推进并通知调用方
* 关闭/网络重建/陈旧重同步走显式 cancel 路径(取消必通知,调用方永不挂死)
* 公开 API 返回契约:recv >0 字节数 / 0 EOF / -1 错误;send >=0 已发字节数(短写由
* 调用方按 ret != len 判定)/ -1 错误(错误码经通知值回传)
* 预算归调用方:timeout_ms=0 即无限等待(无默认预算);挂起超时留痕探针为
* 编译期选项(NET_PENDING_TRIPWIRE,默认 0 = 编译剔除)
*/
@@ -414,3 +417,271 @@ void net_pending_settle_close(int sockfd, net_sock_t *p_sock) {
/* 结束(必通知) */
net_pending_complete(sockfd, val);
}
/* ================= 挂起 send 请求表(SEND pending,二期) ================= */
typedef struct {
uint8_t active; /* 1 = 本 fd 有挂起 send */
const void *buf; /* 调用方缓冲基址(仅指针,须保持有效至完成) */
int len; /* 本次发送总长度(含登记前已写部分) */
int sent; /* 已写入芯片的字节数(进度;登记时可 > 0) */
uint32_t deadline; /* 无进展超时时刻 = 上次成功写块时刻 + NET_SEND_TIMEOUT_MS */
uint32_t registered; /* 登记时刻(tick;日志基准) */
TaskHandle_t caller; /* 调用方任务(完成通知目标) */
} net_send_pending_t;
/* 每 fd 一个挂起 send(与 recv 表独立:收发两方向互不阻塞) */
static net_send_pending_t s_net_send[NET_MAX_SOCKETS];
/*
* 函数功能:通知调用方并清除本 fd 的挂起 send(所有完成/取消路径的公共出口)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* result - 结果原值(本模型恒为**已发字节数** >= 0) int
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文;sockfd 合法且挂起在位(否则静默返回,防重复通知)
* 函数说明:send 结果恒 >= 0(短写语义),故不在此改写错误码——取消/超时的原因
* 由各调用路径在调用本函数**之前**经 net_set_errno 记录
*/
static void net_send_pending_complete(int sockfd, int result) {
/* 本 fd 挂起请求指针 */
net_send_pending_t *p;
/* 描述符越界:静默返回(防重复通知) */
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
return;
}
/* 取挂起请求 */
p = &s_net_send[sockfd];
/* 无在位挂起(已结束/已取消):静默返回 */
if (p->active == 0U) {
return;
}
/* 覆盖式写入通知值 = 已发字节数(32 位原值) */
xTaskNotify(p->caller, (uint32_t)result, eSetValueWithOverwrite);
/* 清除挂起状态 */
p->active = 0U;
}
/*
* 函数功能:登记一个挂起 send 请求(net_send_locked 的"等发送缓冲"超预算路径)
* 入口参数:见 net_pending.h
* 出口参数:无
* 返回值:NET_PENDING_REGISTERED - 登记成功;-1 - 拒绝(见头注释)
* 限定条件:netTask 上下文
* 函数说明:每 fd 至多一个挂起 send(禁止覆盖——覆盖丢失前调用方通知 = 永久挂死);
* 执行任务自身发起(net_send_sock 阻塞用法)无他人可完成 → 拒绝(自等死锁)
*/
int net_send_pending_register(int sockfd, const void *buf, int len, int sent,
TaskHandle_t caller) {
/* 挂起请求指针 */
net_send_pending_t *p;
/* 登记时刻(tick) */
uint32_t now;
/* 描述符越界 → BADF */
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 参数非法(缓冲无效 / 长度非正 / 进度越界)→ INVAL */
if ((buf == NULL) || (len <= 0) || (sent < 0) || (sent >= len)) {
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
/* 取挂起请求 */
p = &s_net_send[sockfd];
/* 同 fd 已有挂起 send:拒绝(覆盖将丢失前通知 = 该任务永久挂死) */
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;
/* 调用方发送缓冲基址(须保持有效至完成) */
p->buf = buf;
/* 本次发送总长度 */
p->len = len;
/* 进度起点(登记前已写入芯片的字节数) */
p->sent = sent;
/* 无进展超时起点(NET_SEND_TIMEOUT_MS = 0 表示不限,同快速路径口径) */
p->deadline = (NET_SEND_TIMEOUT_MS > 0) ? (now + (uint32_t)NET_SEND_TIMEOUT_MS) : 0U;
/* 登记时刻(日志基准) */
p->registered = now;
/* 完成通知目标 */
p->caller = caller;
/* 登记成功:分派跳过即时通知 */
return NET_PENDING_REGISTERED;
}
/*
* 函数功能:挂起 send 推进与完成检查(netTask 每轮 net_process_messages 调用一次,非阻塞)
* 入口参数:p_socks - Socket 控制块表首指针(net_socket.c 的 s_net_socks) net_sock_t *
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文
* 函数说明:1. 逐 fd 按序:①断连 → 以已发字节数结束(剩余字节不可能送达)
* 2. ②推进:发送缓冲空闲则写下一块(每轮至多一块——写完即清 send_ready,
* 下一块须等 SENBUF_FREE 中断重新置位,由 net_poll 刷新)
* 3. ③全部写完 → 以总长度结束;④无进展超时 → 以已发字节数结束
* 4. 结果恒为已发字节数(短写由调用方按 `ret != len` 判定)
* 5. 停滞计时按**无进展**刷新:大文件传输总时长必然超过单块预算,
* 按总时长判会误杀(与 recv 侧"到点"语义的差异点)
*/
void net_send_pending_tick(net_sock_t *p_socks) {
/* Socket 循环索引 */
int i;
for (i = 0; i < NET_MAX_SOCKETS; i++) {
/* 本 fd 挂起请求 */
net_send_pending_t *p = &s_net_send[i];
/* 本 fd 控制块 */
net_sock_t *p_sock = &p_socks[i];
/* 无挂起请求:跳过 */
if (p->active == 0U) {
continue;
}
/* fd 已关闭(正常路径在 close 内先取消;此处兜底,防任何路径漏掉通知) */
if (p_sock->in_use == 0U) {
net_send_pending_complete(i, p->sent);
continue;
}
/* ① 断连:本会话已结束 → 剩余字节不可能送达,按已发字节数结束。
* 判据 `state != ESTABLISHED`(同快速路径):state 离开 ESTABLISHED 后
* 即便再回到 ESTABLISHED,也是**新会话**(relisten + 新 CONNECT)——
* 旧会话的剩余字节绝不能投到新连接上。session_dead 一并覆盖
* (net_types.h:TIMEOUT/DISCONNECT 两条路径都同时置 CLOSED,此处为
* 与 recv 侧判据对齐的冗余保险) */
if ((p_sock->state != NET_SOCK_STATE_ESTABLISHED) || (p_sock->session_dead != 0U)) {
DBG_ERROR("sock%d send pending aborted (%d/%d): session down",
i, p->sent, p->len);
(void)net_set_errno(NET_ERR_CONNRESET);
net_send_pending_complete(i, p->sent);
continue;
}
/* ② 推进:发送缓冲空闲则写下一块 */
if ((p->sent < p->len) && (p_sock->send_ready != 0U)) {
/* 本次写块长度 = min(剩余, 硬件发送缓冲上限) */
int chunk = p->len - p->sent;
if (chunk > NET_SEND_CHUNK_MAX) {
chunk = NET_SEND_CHUNK_MAX;
}
/* 写入芯片发送 FIFO(与快速路径同一分块粒度,TRAP-NET-16) */
ch395f_write_send_buf((uint8_t)i, (uint8_t *)p->buf + p->sent, (uint16_t)chunk);
/* 标记发送缓冲占用中(等 SENBUF_FREE 中断重新置位) */
p_sock->send_ready = 0U;
/* 累计进度与统计 */
p->sent += chunk;
p_sock->send_bytes += (uint32_t)chunk;
/* 进度刷新:停滞判定按"无进展"而非"总时长" */
p->deadline = (NET_SEND_TIMEOUT_MS > 0)
? (HAL_GetTick() + (uint32_t)NET_SEND_TIMEOUT_MS) : 0U;
}
/* ③ 全部写完:以总长度结束(调用方见 ret == len 即成功) */
if (p->sent >= p->len) {
net_send_pending_complete(i, p->sent);
continue;
}
/* ④ 无进展超时:以已发字节数结束(短写;调用方须检查返回值) */
if ((NET_SEND_TIMEOUT_MS > 0) &&
((int32_t)(HAL_GetTick() - p->deadline) >= 0)) {
DBG_ERROR("sock%d send pending stall (%d/%d)", i, p->sent, p->len);
(void)net_set_errno(NET_ERR_TIMEDOUT);
net_send_pending_complete(i, p->sent);
continue;
}
}
}
/*
* 函数功能:清除本 fd 的挂起 send(不通知;用于"新 fd 不得继承"与 close 后的幂等清理)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* 出口参数:无
* 返回值:无
* 限定条件:无
* 函数说明:静默清理不发通知:close / 重建路径先经 cancel 通知,本调用仅保证状态归零(幂等)
*/
void net_send_pending_clear(int sockfd) {
/* 描述符越界:直接返回 */
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
return;
}
/* 挂起状态清零(静默,不发通知) */
s_net_send[sockfd].active = 0U;
}
/*
* 函数功能:取消本 fd 的挂起 send 并通知调用方(close / drop_conn / resync / 重建)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* err - 取消原因(记入 netTask 错误码槽位 + 日志) int
* 出口参数:无
* 返回值:无(无在位挂起时静默返回)
* 限定条件:netTask 上下文
* 函数说明:以**已发字节数**结束(已写入芯片的字节不回收——对端可能已收到;未发部分
* 放弃,由调用方按短写处理)。取消必通知,否则调用方永久挂死
*/
void net_send_pending_cancel(int sockfd, int err) {
/* 本 fd 挂起请求指针(只读:完成经 net_send_pending_complete 按 fd 操作) */
const net_send_pending_t *p;
/* 描述符越界:静默返回 */
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
return;
}
/* 取挂起请求 */
p = &s_net_send[sockfd];
/* 无在位挂起:静默返回 */
if (p->active == 0U) {
return;
}
/* 留痕:取消原因 + 进度(短写程度) */
DBG_ERROR("sock%d send pending cancel (%d/%d, err=%d)", sockfd, p->sent, p->len, err);
/* 原因记入 netTask 自己的错误码槽位(结果 >= 0 不会被 complete 代记) */
(void)net_set_errno(err);
/* 结束:以已发字节数通知 */
net_send_pending_complete(sockfd, p->sent);
}
/*
* 函数功能:取消全部挂起 send 并逐一通知(网络重建路径)
* 入口参数:err - 取消原因 int
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文;须在控制块清零与芯片复位之前调用
* 函数说明:网络重建(net_stack_bring_up → net_init)前调用,保证所有在位调用方均被唤醒
*/
void net_send_pending_cancel_all(int err) {
/* 逐 fd 遍历 */
for (int i = 0; i < NET_MAX_SOCKETS; i++) {
/* 取消并通知(未在位则静默跳过) */
net_send_pending_cancel(i, err);
}
}
/*
* 函数功能:查询本 fd 是否有在途挂起 send —— 宿主能力,供 net_listener 用
* 入口参数:sockfd - Socket 描述符 (0~7) int
* 出口参数:无
* 返回值:1 - 有挂起;0 - 无(含描述符越界) int
* 限定条件:任意上下文(纯 RAM 读,不发起 RPC)
* 函数说明:net_listener_retry_tick 用它跳过"重开 Socket 会摧毁在途传输"的 fd
* (挂起 send 保险;与 net_pending_active 的挂起 recv 保险并列)
*/
int net_send_pending_active(int sockfd) {
/* 描述符越界:视为无挂起 */
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
return 0;
}
return (s_net_send[sockfd].active != 0U) ? 1 : 0;
}