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
+95 -37
View File
@@ -183,7 +183,8 @@ static void handle_send_event(net_sock_t *p_sock);
static void handle_timeout_event(net_sock_t *p_sock);
static void fire_event(net_sock_t *p_sock, net_event_t event);
static void parse_ip_string(const char *str, uint8_t *ip_arr);
static int net_send_locked(net_sock_t *p_sock, const void *buf, int len);
static int net_send_locked(net_sock_t *p_sock, const void *buf, int len,
TaskHandle_t caller);
static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags,
uint32_t timeout_ms, TaskHandle_t caller);
static int net_close_locked(net_sock_t *p_sock);
@@ -226,8 +227,10 @@ static bool s_mac_is_zero(const uint8_t *p_mac) {
ch395f_status_t ret;
/* 网络重建前取消全部挂起请求并通知调用方(调用方阻塞等回执,
* 取消不通知 = 永久挂死);必须早于控制块清零与芯片复位 */
* 取消不通知 = 永久挂死);必须早于控制块清零与芯片复位。
* recv / send 两张表各自独立,须分别取消 */
net_pending_cancel_all(NET_ERR_CONNRESET);
net_send_pending_cancel_all(NET_ERR_CONNRESET);
/* 清零 Socket 控制块 */
memset(s_net_socks, 0, sizeof(s_net_socks));
@@ -774,6 +777,8 @@ static void net_reconcile_tick(void) {
p_sock->lost_conn_seen = 0U;
/* 取消本 fd 在死连接上的挂起 recv(必通知,防调用方挂死) */
net_pending_cancel(i, NET_ERR_CONNRESET);
/* 挂起 send 同样取消(会话已被芯片收走,剩余字节不可能送达) */
net_send_pending_cancel(i, NET_ERR_CONNRESET);
} else if ((bad == 3) || (bad == 4)) {
/* 类型③④:监听失位/孤儿监听 → 重建(本层唯一重建入口;
* 非阻塞:失败由 D10 重试轮询接管) */
@@ -791,9 +796,9 @@ static void net_reconcile_tick(void) {
/*
* 函数功能:netTask 主循环核心 —— 自愈 tick 宿主 + 消息队列分派器。每轮
* 先跑三个非阻塞自愈 tick(D10 重监听 / 挂起 recv /
* TRAP-NET-17 状态对账),再非阻塞排空队列按 msg.type 分派到 _locked 实现。
* 函数功能:netTask 主循环核心 —— 自愈 tick 宿主 + 消息队列分派器。每轮先跑四个非阻塞
* 自愈 tick(D10 重监听 / 挂起 recv / 挂起 send / TRAP-NET-17 状态对账),
* 再非阻塞排空队列按 msg.type 分派到 _locked 实现。
* 入口参数:无
* 出口参数:无
* 返回值:无(void);消息结果经 xTaskNotify 通知值回传(成功=字节数/描述符,
@@ -801,18 +806,15 @@ static void net_reconcile_tick(void) {
* 限定条件:必须在 netTask 上下文中调用(独占 CH395F 与 s_net_socks 访问权);
* 由 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 时跳过(挂起 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 让位窗)
* + 1s 冷却;重建失败进 D10 重试队列,reconciler 不重复催
* 5. net_msg_t 按值入队,netTask 侧副本调用方不可见 → 结果只能搭任务通知值回传
* 6. 32 位整体即结果,不做位域压缩
* 7. RECV 特例:无数据+阻塞 → 登记 pending 哨兵 NET_PENDING_REGISTERED(0x70000000),
* do_notify=0,回执推迟到 net_pending_tick 完成时
* 2. net_listener_retry_tick(D10):断连后按 30ms 节拍重试 open+listen,
* 2s 时限到期放弃留痕;本 fd 有在途挂起 recv / send 时跳过(挂起保险)
* 3. net_pending_tick:按节拍结束挂起 recv(数据/到点[仅显式预算]/断连)
* 4. net_send_pending_tick:按节拍推进挂起 send(断连/推进/写完/无进展超时)
* 5. net_reconcile_tick(TRAP-NET-17 + pump 合并,自愈收敛阶段 3):100ms 对账
* ①②⑤ + ③④ 重建 + U11 重同步,判据读驱动状态缓存;去抖分级 + 1s 冷却
* 6. net_msg_t 按值入队 → 结果只能搭任务通知值回传(32 位整体即结果,不做位域压缩)
* 7. RECV / SEND 特例:超预算登记 pending 哨兵 NET_PENDING_REGISTERED(0x70000000),
* do_notify=0,回执推迟到 net_pending_tick / net_send_pending_tick 完成时
*/
void net_process_messages(void) {
net_msg_t msg;
@@ -822,11 +824,13 @@ void net_process_messages(void) {
return;
}
/* 三个非阻塞自愈 tick 置于消息处理之前:断连后即使无应用消息,监听器也能自主恢复 */
/* 四个非阻塞自愈 tick 置于消息处理之前:断连后即使无应用消息,监听器也能自主恢复 */
/* D10 重监听重试(TRAP-NET-09):沉降后重开芯片监听器 */
net_listener_retry_tick();
/* 挂起 recv 完成检查:数据到达 / 到点 / 断连时通知调用方 */
net_pending_tick(s_net_socks);
/* 挂起 send 推进与完成检查(SEND pending):断连 / 推进写块 / 写完 / 无进展超时 */
net_send_pending_tick(s_net_socks);
/* TRAP-NET-17 状态对账 + 监听器 pump(自愈收敛阶段 3 合并单一 100ms tick):
* 判据读驱动缓存(net_poll 每轮全量刷新),①②⑤ 失同步 + ③④ 重建 +
* U11 重同步全部在此(本层唯一周期重建入口,N-32 P2 单一所有者) */
@@ -852,8 +856,14 @@ void net_process_messages(void) {
msg.result = -1;
} else {
/* 真正写入芯片:1KB/块等 SENBUF_FREE 中断(§5.32/§5.23①),
* 单块停滞 1s 超时,中途断连立即 CONNRESET;短写留痕,调用方须检查返回值 */
msg.result = net_send_locked(p_sock, msg.buf, msg.len);
* 单块等待超出快速路径预算(NET_SEND_SPIN_MS)即登记挂起请求、
* netTask 零占用返回;中途断连立即 CONNRESET;短写留痕,
* 调用方须检查返回值 */
msg.result = net_send_locked(p_sock, msg.buf, msg.len, msg.caller);
if (msg.result == NET_PENDING_REGISTERED) {
/* 挂起已登记:不发即时回执,完成时才通知(防"登记成功"误报为结果) */
do_notify = 0;
}
}
break;
}
@@ -1074,6 +1084,8 @@ int net_socket(int domain, int type) {
net_listener_clear(sockfd);
/* 新 fd 不得继承上一次挂起的 recv 请求 */
net_pending_clear(sockfd);
/* 新 fd 不得继承上一次挂起的 send 请求 */
net_send_pending_clear(sockfd);
/* 返回描述符(控制块已就绪) */
return sockfd;
@@ -1918,30 +1930,38 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a
}
/*
* SPI DMA 单次事务最大有效载荷(取 CH395F 驱动公开常量)
* 单次写入 CH395F 发送 FIFO 的最大字节数 NET_SEND_CHUNK_MAX 已移入
* Drivers/BSP/NET/net_pending.h —— 发送快速路径(本文件)与挂起 send 推进
* (net_pending.c 的 net_send_pending_tick)必须共用同一定义,否则两条路径的
* 分块粒度漂移(TRAP-NET-16:单次写超 1KB 发送 FIFO 会写穿)。
*/
#define NET_DMA_MAX_PAYLOAD ((int)CH395F_SPI_DMA_MAX_PAYLOAD)
/* 单次写入 CH395F 发送 FIFO 的最大字节数:受硬件发送缓冲(CH395F_SEND_BUF_SIZE_BYTES=1KB)
* 与 SPI DMA 载荷(NET_DMA_MAX_PAYLOAD=2KB) 双重限制,以硬件发送缓冲为硬上限(见 TRAP-NET-16),
* 否则单次写 2KB/4KB 会写穿 1KB 发送 FIFO。 */
#define NET_SEND_CHUNK_MAX ((int)CH395F_SEND_BUF_SIZE_BYTES < NET_DMA_MAX_PAYLOAD ? \
(int)CH395F_SEND_BUF_SIZE_BYTES : NET_DMA_MAX_PAYLOAD)
/*
* 函数功能:发送数据(内部锁定版本,必须在 netTask 上下文调用)
* 入口参数:p_sock - Socket 控制块指针 net_sock_t *
* buf - 待发送数据缓冲区指针 const void *
* len - 数据长度 int > 0
* caller - 调用方任务句柄(挂起 send 的完成通知目标) TaskHandle_t
* 出口参数:无
* 返回值:实际发送字节数(>= 0);-1 表示参数无效或 Socket 未连接 int
* 返回值:实际发送字节数(>= 0);-1 表示参数无效或 Socket 未连接;
* NET_PENDING_REGISTERED 表示已登记挂起 send(分派据此跳过即时通知) int
* 限定条件:p_sock 非空;Socket 处于 ESTABLISHED 状态;netTask 上下文
* 函数说明:1. 按 NET_SEND_CHUNK_MAX(硬件发送缓冲上限,见 TRAP-NET-16)分块写入 CH395F 发送 FIFO
* 2. 发送前等待 send_ready 置位,超时返回已发送字节数
* 3. 每写完一块等待 SENBUF_FREE 中断(send_ready 清零后重新置位,§5.32/§5.23①)
* 2. 等待发送缓冲腾出采用**有界自旋**:单次等待 ≤ NET_SEND_SPIN_MS
* (健康路径实测 219µs 中位,5 倍余量)→ 多块帧仍在一次分派内发完,
* 健康路径行为与分片时代逐位一致
* 3. 单次等待超出预算 → 登记挂起 send(进度 = 已发字节数)并立即返回,
* 由 net_send_pending_tick 按主循环节拍继续推进 ——
* netTask 最坏停滞由 1s 压到 NET_SEND_SPIN_MS。
* ⚠ 仅**写块前**的等待可挂起:末块写完后 sent == len,此时超预算必须
* 直接返回 sent 而非登记(登记函数按 sent >= len 拒收,会把完整发送
* 报成 -1;2026-10-14 台架实证,详见《Net层阻塞模型架构设计.md》 §9.1)
* 4. 发送中途断连(状态非 ESTABLISHED)→ 立即失败并返回已发字节
* (errno=CONNRESET,不再空等超时)
* 5. 未写完即返回(含挂起完成)均为**短写**:调用方须按 ret != len 判定
*/
static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) {
static int net_send_locked(net_sock_t *p_sock, const void *buf, int len,
TaskHandle_t caller) {
/* 累计实际发送字节数 */
int sent = 0;
/* 当前等待阶段起始时刻(tick) */
@@ -1979,13 +1999,22 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) {
(void)net_set_errno(NET_ERR_CONNRESET);
break;
}
/* 停滞超时:NET_SEND_TIMEOUT_MS 后 send_ready 仍未置位 */
/* 停滞超时:NET_SEND_TIMEOUT_MS 后 send_ready 仍未置位(仅快速路径可见;
* 挂起路径的停滞判定由 net_send_pending_tick 负责) */
if (NET_SEND_TIMEOUT_MS > 0 &&
(HAL_GetTick() - tick_start) >= NET_SEND_TIMEOUT_MS) {
DBG_ERROR("sock%d send stall: send_ready not set (%d/%d)", sockfd, sent, len);
(void)net_set_errno(NET_ERR_TIMEDOUT);
break;
}
/* 快速路径预算用尽(仍无进展):登记挂起 send,把等待交回调用方上下文。
* 先于上面的 1s 停滞判定生效,故健康/轻微拥塞路径不会走到 1s 分支。
* 登记失败(同 fd 已有挂起 / netTask 自身调用)→ 返回 -1,调用方见负值。
* 不变量:此处位于 **写块之前**,且循环条件 `sent < len` 尚未被改写
* → `sent < len` 恒成立,登记必获受理(与末块等待的差异点)。 */
if ((HAL_GetTick() - tick_start) >= (uint32_t)NET_SEND_SPIN_MS) {
return net_send_pending_register(sockfd, buf, len, sent, caller);
}
/* 轮询一次(更新 send_ready/状态),下一轮再试 */
net_poll();
continue;
@@ -2017,11 +2046,29 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) {
}
/* 轮询:处理 SEND_OK/SENBUF_FREE 中断 */
net_poll();
/* 等待超时:本块放弃(外层统一处理) */
/* 末块已全部交付(sent == len):无剩余块可写,SENBUF_FREE 等待是
* **纯粹的保守自旋**(原本是为"等缓冲腾出再写下一块"服务)——直接跳出,
* 把等待顺延为下一批的责任。同时也是下面挂起判定的前置守卫:
* net_send_pending_register 会按 `sent >= len` 拒收并回 -1,
* 把一次完整发送报成失败(2026-10-14 台架实证:RETR 1KB 单块帧
* `write error, ret=-1`)。本检查置于**超时/自旋判定之前**,
* 保证末块路径不因预算/超时判定产生任何额外延迟。 */
if (sent >= len) {
break;
}
/* 等待超时:本块放弃(外层统一处理)。置于自旋判定之前,
* 保证 NET_SEND_SPIN_MS 被配置得比 NET_SEND_TIMEOUT_MS 大时仍以超时收口 */
if (NET_SEND_TIMEOUT_MS > 0 &&
(HAL_GetTick() - tick_start) >= NET_SEND_TIMEOUT_MS) {
break;
}
/* 快速路径预算用尽(仍无进展):登记挂起 send(进度 = 已发字节数),
* 由 net_send_pending_tick 续传;健康路径(219µs 量级)不会到达此处。
* 此处位于**写块之后**,但上面的 `sent >= len` 守卫已保证伴随
* `sent < len`(仍有剩余块)才具到达资格,登记必获受理。 */
if ((HAL_GetTick() - tick_start) >= (uint32_t)NET_SEND_SPIN_MS) {
return net_send_pending_register(sockfd, buf, len, sent, caller);
}
}
#if defined(TEST_LATENCY_PROBE)
/* 探针已武装:累计本段等待样本 */
@@ -2160,6 +2207,8 @@ static int net_close_locked(net_sock_t *p_sock) {
/* 先结束本 fd 的挂起 recv(排空/取消 + 必通知),再关芯片 Socket
* ——关闭不得让等待中的调用方永久悬挂 */
net_pending_settle_close((int)(p_sock - s_net_socks), p_sock);
/* 挂起 send 同样先结束(以已发字节数通知;未发部分放弃),再关芯片 Socket */
net_send_pending_cancel((int)(p_sock - s_net_socks), NET_ERR_CONNRESET);
/* 排渣 + 关闭 + 等 CLOSED 一体(见函数头注释 1);
* 已 CLOSED 的 fd 只清本 Socket 残留事件(防下一连接误判"刚建立就断开") */
@@ -2179,6 +2228,8 @@ static int net_close_locked(net_sock_t *p_sock) {
net_listener_clear(sockfd);
/* 关闭即清除该 fd 的挂起状态(幂等) */
net_pending_clear(sockfd);
/* 挂起 send 同样幂等清理(cancel 已在上方通知,此处仅保证状态归零) */
net_send_pending_clear(sockfd);
/* 关闭成功 */
return 0;
@@ -2208,6 +2259,8 @@ static int net_drop_conn_locked(net_sock_t *p_sock) {
/* 先结束本 fd 挂起 recv(排空/取消 + 必通知),再拆芯片会话 */
net_pending_settle_close((int)(p_sock - s_net_socks), p_sock);
/* 挂起 send 同样先结束(以已发字节数通知;未发部分放弃),再拆芯片会话 */
net_send_pending_cancel((int)(p_sock - s_net_socks), NET_ERR_CONNRESET);
/* 确定性拆除(异常释放 → QUARANTINE 400ms 底,TRAP-NET-18 语义) */
if (net_sock_reset_sync(sockfd, CH395F_RESET_QUARANTINE) != 0) {
@@ -2837,17 +2890,22 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags,
* buf - 待发送数据缓冲区指针 const void *
* len - 数据长度 int > 0
* 出口参数:无
* 返回值:实际发送字节数(>= 0);-1 表示参数无效 int
* 返回值:实际发送字节数(>= 0);-1 表示参数无效 / 需等待但被拒(BUSY) int
* 限定条件:p_sock 非空;netTask 上下文(避免向自身发送消息造成死锁)
* 函数说明:直接调用 net_send_locked 完成发送
* 函数说明:1. 直接调用 net_send_locked 完成发送
* 2. SEND pending 化后:发送缓冲未及时腾出(超 NET_SEND_SPIN_MS)时,
* 挂起登记会被"拒绝执行者自等待"守卫拒绝(netTask 内无人能完成它)
* → 返回 -1(BUSY)。本接口因此只适合"发送缓冲可用"的场合;
* netTask 内需要长时间等待的发送,须自行分片或改用挂起模型
*/
int net_send_sock(net_sock_t *p_sock, const void *buf, int len) {
/* 控制块指针为空 → BADF */
if (p_sock == NULL) {
return NET_ERR_RETURN_1(NET_ERR_BADF);
}
/* 直接委托锁定实现(netTask 上下文,无队列往返) */
return net_send_locked(p_sock, buf, len);
/* 直接委托锁定实现(netTask 上下文,无队列往返);
* caller = netTask 自身 → 挂起登记必被拒(自等死锁守卫) */
return net_send_locked(p_sock, buf, len, xTaskGetCurrentTaskHandle());
}
/*
* 函数功能:接收数据(内部版本,绕过消息队列,供 netTask 直接使用)