diff --git a/App/time_sync.c b/App/time_sync.c index 9e566339..0f090458 100644 --- a/App/time_sync.c +++ b/App/time_sync.c @@ -25,16 +25,15 @@ #define TIME_SYNC_PORT 8888 /* - * E-02 头阻塞修复(timeSync 侧;lftpd 侧见 lftpd_inet.c): + * E-02 头阻塞修复 + L3 pending 化(timeSync 侧;lftpd 侧见 lftpd_inet.c): * 旧实现用单次 30s 阻塞 net_recv(默认 NET_RECV_TIMEOUT_MS)等时间包——等待期间 * netTask(全部 socket 操作的唯一执行者)被独占,其余任务的网络操作(远端显示 / * FTP)整体停摆(docs/问题记录/错误码传播审计.md E-02)。 - * 修法:等待切成 ≤TIME_SYNC_NET_SLICE_MS 的片(与 lftpd 同片长 = 其他任务最坏 - * 等待,见 docs/测试说明/链路延迟测试说明.md §8.3),调用方累计总预算 - * TIME_SYNC_TOTAL_BUDGET_MS——总超时语义与旧 30s 一致,任何时刻 netTask 占用 - * 有界;片间 vTaskDelay(1) 让出 CPU,防空闲忙循环饿死低优先级任务。 + * 2026-09-22 先落地分片有界读(5ms 片),同日 L3 pending 化后改为"挂起请求": + * 每次等待 = 一次挂起登记(netTask 零占用,完成按主循环节拍检查),调用方 + * 单次超时 = 剩余总预算(总等待 ≤ TIME_SYNC_TOTAL_BUDGET_MS,语义与旧 30s + * 一致);等待期间调用方在自己上下文睡任务通知,旧分片协议(片长/片间让出)退役。 */ -#define TIME_SYNC_NET_SLICE_MS 5U #define TIME_SYNC_TOTAL_BUDGET_MS 30000U /* @@ -88,10 +87,10 @@ static int time_sync_open_listener(void) { * 函数功能:处理一个已建立的时间同步连接 * 入口参数:conn_sock - 已建立的连接 Socket 描述符(单连接模式下即监听 Socket 自身) * 返回值:无 - * 函数说明:分片有界读取 PC 发来的时间包("TIME"+uint32 大端 本地时间),解析后 - * 写回 RTC,然后关闭连接。读取用 net_recv_to 5ms 分片(E-02):单片不 - * 长期占用 netTask,总预算 30s 与旧实现语义一致;跨片累积,容忍时间包 - * 被 TCP 分段送达 + * 函数说明:pending 读取 PC 发来的时间包("TIME"+uint32 大端 本地时间),解析后 + * 写回 RTC,然后关闭连接。每次等待 = 一次挂起请求(netTask 零占用, + * L3 pending 化),单次超时 = 剩余总预算(≤30s,语义与旧 30s 阻塞一致); + * 跨调用累积,容忍时间包被 TCP 分段送达 */ static void time_sync_handle_conn(int conn_sock) { uint8_t buf[16]; @@ -99,9 +98,16 @@ static void time_sync_handle_conn(int conn_sock) { TickType_t t0 = xTaskGetTickCount(); while (got < (int)sizeof(buf)) { - /* 分片有界读(E-02):单片 netTask 占用 ≤ TIME_SYNC_NET_SLICE_MS */ - int n = net_recv_to(conn_sock, buf + got, (int)(sizeof(buf) - got), - TIME_SYNC_NET_SLICE_MS); + /* 单次预算 = 剩余总预算(L3 pending:等待期 netTask 零占用,调用方 + * 在自己上下文睡任务通知,旧片间 vTaskDelay(1) 让出退役) */ + TickType_t elapsed = xTaskGetTickCount() - t0; + uint32_t rem = (elapsed >= (TickType_t)pdMS_TO_TICKS(TIME_SYNC_TOTAL_BUDGET_MS)) ? 0U + : (uint32_t)((TickType_t)TIME_SYNC_TOTAL_BUDGET_MS - elapsed); + if (rem == 0U) { + DBG_INFO("time_sync: no TIME packet in 30s, drop conn"); + break; /* 总预算耗尽(语义同旧 30s 超时) */ + } + int n = net_recv_to(conn_sock, buf + got, (int)(sizeof(buf) - got), rem); if (n > 0) { got += n; if (got >= 8) { @@ -112,18 +118,10 @@ static void time_sync_handle_conn(int conn_sock) { if (n == 0) { break; /* 对端关闭(EOF) */ } - if (n != -2) { - break; /* 真错误 */ + if (n == -2) { + continue; /* 本次预算等满无数据 → 下轮 rem=0 退出 */ } - /* 本片超时无数据:总预算未耗尽则继续等。 - * **必须显式让出 CPU**:若某片因 net_poll 自身耗时在首轮就返回 -2, - * 本任务(Normal)立即重排队会形成忙循环 → 饿死 BelowNormal 的 uiTask - * (lftpd 侧同因,见链路延迟测试说明.md §8.3 第 3 条) */ - if ((xTaskGetTickCount() - t0) >= pdMS_TO_TICKS(TIME_SYNC_TOTAL_BUDGET_MS)) { - DBG_INFO("time_sync: no TIME packet in 30s, drop conn"); - break; /* 总预算耗尽(语义同旧 30s 超时) */ - } - vTaskDelay(pdMS_TO_TICKS(1)); + break; /* 真错误 */ } if (got >= 8) { diff --git a/Drivers/BSP/NET/lftpd/lftpd_inet.c b/Drivers/BSP/NET/lftpd/lftpd_inet.c index 9c68ed1f..f8a18e0b 100644 --- a/Drivers/BSP/NET/lftpd/lftpd_inet.c +++ b/Drivers/BSP/NET/lftpd/lftpd_inet.c @@ -6,18 +6,17 @@ #include /* - * E-02 头阻塞修复(2026-09-22 M3 台架实证): + * E-02 头阻塞修复 + L3 pending 化(2026-09-22,M3 台架实证): * netTask 是全部 socket 操作的唯一执行者;lftpd 原先用默认 30s 的**阻塞** * net_recv 等待对端下一行/下一块数据 → 只要客户端连上却不发完整行,netTask * 就被长期占用,其余任务(含远端显示)的网络操作整体冻结(实测一次空闲 FTP * 登录让 LCDMEM 整帧等待 17.7s)。 - * 修法:把等待切成 ≤LFTPD_NET_SLICE_MS 的片(每片 netTask 占用有界), - * 调用方累计总预算 LFTPD_TOTAL_BUDGET_MS —— **总超时语义与旧实现一致**, - * 但任何时刻都不再有"秒级冻结"。单片超时由 net_recv_to 返回 -2 区分(无 errno 竞态)。 - * 分片长度即"其他任务最坏等待":实测 100ms 片下受压帧仍达 ~750ms(多片排队), - * 收到 5ms 后受压帧降到 ~10ms 量级(见 m3_data 修复后数据)。 + * 2026-09-22 先落地分片有界读(5ms 片;实测 100ms 片下受压帧仍达 ~750ms, + * 收到 5ms 后降到 ~10ms 量级,见 m3_data 修复后数据),同日 L3 pending 化 + * 后改为"挂起请求":每次等待 = 一次挂起登记(netTask 零占用,完成按主循环 + * 节拍检查),调用方单次超时 = 剩余总预算(总等待 ≤ LFTPD_TOTAL_BUDGET_MS, + * **总超时语义与旧实现一致**),旧分片协议退役。 */ -#define LFTPD_NET_SLICE_MS 5U #define LFTPD_TOTAL_BUDGET_MS 30000U int lftpd_inet_listen(int port) @@ -120,25 +119,23 @@ 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) { - /* 分片有界读(E-02):单片 netTask 占用 ≤ LFTPD_NET_SLICE_MS */ + /* 单次预算 = 剩余总预算(L3 pending:等待期 netTask 零占用,调用方 + * 在自己上下文睡任务通知,旧片间 vTaskDelay(1) 让出退役) */ + 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); + if (rem == 0U) { + return -1; /* 总预算耗尽(语义同旧 30s 超时) */ + } int read_len = net_recv_to(socket, buffer + total_read_len, (int)(buffer_len - total_read_len - 1), - LFTPD_NET_SLICE_MS); + rem); if (read_len == 0) { return -1; /* 对端关闭 */ } if (read_len == -2) { - /* 本片无数据:累计总预算,未超则继续等。 - * **必须显式让出 CPU**:若某片因 net_poll 自身耗时而在首轮就返回 -2, - * 本任务(Normal)立即重排队会形成忙循环 → 饿死 BelowNormal 的 uiTask - * → 远端显示请求数秒无人拾取(2026-09-22 M3 实测 2.4~3.4s 周期尖刺)。 - * vTaskDelay(1) 保证低优先级任务每毫秒都有运行机会。 */ - if ((xTaskGetTickCount() - t0) >= pdMS_TO_TICKS(LFTPD_TOTAL_BUDGET_MS)) { - return -1; /* 总预算耗尽(语义同旧 30s 超时) */ - } - vTaskDelay(pdMS_TO_TICKS(1)); - continue; + continue; /* 本次预算等满无数据 → 下轮 rem=0 退出 */ } if (read_len < 0) { return read_len; /* 真错误 */ @@ -200,22 +197,25 @@ int lftpd_inet_write(int socket, const void *buf, int len) int lftpd_inet_read(int socket, void *buf, int len) { - /* 分片有界读(E-02):总预算 30s 不变,但每片 netTask 占用 ≤ LFTPD_NET_SLICE_MS, - * 避免空闲数据连接(客户端开了 PASV 却不发数据)冻结全网 */ + /* L3 pending 读:总预算 30s 不变,等待期 netTask 零占用 + * (空闲数据连接不再冻结全网;旧分片协议随 L3 pending 化退役) */ TickType_t t0 = xTaskGetTickCount(); for (;;) { - int n = net_recv_to(socket, buf, len, LFTPD_NET_SLICE_MS); + 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); + if (rem == 0U) { + return -1; /* 总预算耗尽(语义同旧 30s 超时) */ + } + int n = net_recv_to(socket, buf, len, rem); if (n >= 0) { return n; /* >0 数据 / 0 EOF */ } if (n == -1) { return -1; /* 真错误 */ } - if ((xTaskGetTickCount() - t0) >= pdMS_TO_TICKS(LFTPD_TOTAL_BUDGET_MS)) { - return -1; /* 总预算耗尽(语义同旧 30s 超时) */ - } - vTaskDelay(pdMS_TO_TICKS(1)); /* 让出 CPU:防空闲时忙循环饿死低优先级任务 */ + /* n == -2:本次预算等满无数据 → 下轮 rem=0 退出 */ } } diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 91f75d8b..dcdac04b 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -68,7 +68,7 @@ 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_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, - uint32_t timeout_ms); + uint32_t timeout_ms, TaskHandle_t caller); static int net_close_locked(net_sock_t *p_sock); static int net_relisten_locked(net_sock_t *p_sock); /* D10 非阻塞重监听:单次尝试 + 延迟重试(实现见 net_relisten_locked 之后) */ @@ -78,6 +78,37 @@ static void net_relisten_retry_tick(void); 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 错误(错误码经通知值回传)。 + */ +#define NET_PENDING_REGISTERED 0x70000000 /* 内部哨兵:pending 已登记(分派跳过即时通知;取值不与字节数空间冲突) */ +#define NET_PENDING_WATCHDOG_MS 1000U /* 看门狗:到点后 1s 仍未了结 = 完成逻辑缺位 → 强制了结 + 错误日志 */ + +typedef struct { + uint8_t active; /* 1 = 本 fd 有挂起 recv */ + void *buf; /* 调用方缓冲(仅指针,须保持有效至完成) */ + int len; /* 缓冲容量 */ + uint32_t deadline; /* 截止时刻(tick)= 登记时刻 + 本次预算 */ + uint32_t registered; /* 登记时刻(tick;看门狗/日志) */ + 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 * @@ -112,10 +143,15 @@ 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); + /* 清零 Socket 控制块 */ memset(s_net_socks, 0, sizeof(s_net_socks)); s_net_errno = 0; s_net_initialized = 0; + s_net_task_handle = xTaskGetCurrentTaskHandle(); /* 执行任务 = net_init 调用者(NET 层 = netTask / 硬件层 = testTask) */ /* 检查 CH395F 芯片 */ if (ch395f_check_exist() != CH395F_STATUS_OK) { @@ -356,10 +392,14 @@ void net_process_messages(void) { } /* D10:待重试的自动重监听(Trap 09 沉降后重开),非阻塞、每轮最多一次尝试。 - * 放在消息处理之前:断连后即使没有任何应用消息,监听器也能自主恢复。 */ + * 放在消息处理之前:断连后即使没有任何应用消息,监听器也能自主恢复。 + * L3:挂起 recv 的完成检查同位(两 tick 共享主循环节拍,均为 O(每 fd) 非阻塞)。 */ net_relisten_retry_tick(); + net_pending_tick(); while (osMessageQueueGet(netMsgQueueHandle, &msg, NULL, 0) == osOK) { + int do_notify = 1; + switch (msg.type) { case NET_MSG_SEND: { net_sock_t *p_sock = net_get_sock(msg.sockfd); @@ -380,8 +420,13 @@ void net_process_messages(void) { s_net_errno = NET_ERR_BADF; msg.result = -1; } else { + /* L3:无数据 + 阻塞等待 = 登记挂起请求(netTask 零占用), + * 完成由 net_pending_tick 按主循环节拍通知调用方 */ msg.result = net_recv_locked(p_sock, msg.buf, msg.len, msg.flags, - msg.timeout_ms); + msg.timeout_ms, msg.caller); + if (msg.result == NET_PENDING_REGISTERED) { + do_notify = 0; /* 挂起已登记:不发即时回执,完成时才通知 */ + } } break; } @@ -426,6 +471,7 @@ void net_process_messages(void) { * close/重监听命令——会摧毁芯片侧仍健康的监听器 → 连接永拒 * (run8 实证 170s 拒绝)。新连接的 CONNECT 中断会把两侧重新同步。 */ p_sock->state = NET_SOCK_STATE_LISTENING; + net_pending_cancel(msg.sockfd, NET_ERR_CONNRESET); /* L3:死连接上的挂起 recv 立即取消(必通知) */ msg.result = 0; } break; @@ -453,8 +499,9 @@ void net_process_messages(void) { /* 结果回传:成功=字节数/描述符;失败=具体错误码(如 NET_ERR_WOULDBLOCK)。 * 注意消息体按值拷入队列——调用方看不到 netTask 对 msg 的修改,结果只能搭 * 通知值这趟车带回;**整个 32 位都是结果**(若压入序列号等高 16 位标签, - * 单次结果上限会被压到 32767,而 TEST_BUF_SIZE=32768 等调用点会溢出为负)。 */ - { + * 单次结果上限会被压到 32767,而 TEST_BUF_SIZE=32768 等调用点会溢出为负)。 + * L3:挂起 recv(do_notify=0)不发即时回执,由 net_pending_tick 完成时通知。 */ + if (do_notify != 0) { uint32_t notify_val = (uint32_t)msg.result; if (msg.result < 0) { @@ -533,6 +580,7 @@ int net_socket(int domain, int type) { p_sock->send_ready = 1; p_sock->readable = 0; net_relisten_clear(sockfd); /* 新 fd 不得继承上一次的待重试状态 */ + net_pending_clear(sockfd); /* 新 fd 不得继承上一次挂起的 recv 请求(L3) */ return sockfd; } @@ -1074,21 +1122,26 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) { /* * 函数功能:接收数据(内部锁定版本,必须在 netTask 上下文调用) - * 入口参数:p_sock - Socket 控制块指针 net_sock_t * - * buf - 接收缓冲区指针 void * - * len - 缓冲区大小 int > 0 - * flags - 标志位,NET_MSG_DONTWAIT 表示非阻塞 int + * 入口参数:p_sock - Socket 控制块指针 net_sock_t * + * buf - 接收缓冲区指针 void * + * len - 缓冲区大小 int > 0 + * flags - 标志位,NET_MSG_DONTWAIT 表示非阻塞 int + * timeout_ms - 本次等待预算(0 = 默认 NET_RECV_TIMEOUT_MS) uint32_t + * caller - 调用方任务句柄(挂起请求的完成通知目标) TaskHandle_t * 出口参数:buf - 填充接收到的数据 - * 返回值:实际接收字节数(> 0);0 表示对端关闭;-1 表示出错/超时/非阻塞无数据 int + * 返回值:实际接收字节数(> 0);0 表示对端关闭;-1 表示出错/非阻塞无数据 int + * NET_PENDING_REGISTERED 表示已登记挂起请求(分派据此跳过即时通知) * 限定条件:p_sock 非空;netTask 上下文 - * 函数说明:1. 循环调用 net_poll() 检查接收长度,有数据即读出 - * 2. 非阻塞模式下无数据立即返回 -1(NET_ERR_WOULDBLOCK) - * 3. CLOSED 状态尝试排空接收缓冲区残余数据(Trap 13:FIN 可能先于数据到达) + * 函数说明:1. 数据已在(快速路径)→ 立即读出返回,行为与旧实现一致 + * 2. 非阻塞模式无数据立即返回 -1(NET_ERR_WOULDBLOCK) + * 3. 无数据 + 阻塞等待 → 登记挂起请求(L3 pending 化,E-02 结构演进): + * netTask 零占用立即返回;完成条件(数据/到点/断连)由 + * net_pending_tick 按主循环节拍检查并通知 caller; + * 关闭/网络重建/陈旧重同步由对应路径显式了结(取消必通知)。 + * CLOSED 状态语义(排空 + EOF + 自动重监听)逐项保留在 tick 内 */ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, - uint32_t timeout_ms) { - uint32_t tick_start; - uint32_t to_ms; + uint32_t timeout_ms, TaskHandle_t caller) { int sockfd = (int)(p_sock - s_net_socks); if (buf == NULL || len <= 0) { @@ -1096,58 +1149,37 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, return -1; } - /* 超时来源:调用方指定(分片有界读,E-02 修复)优先,否则用默认值 */ - to_ms = (timeout_ms != 0U) ? timeout_ms : (uint32_t)NET_RECV_TIMEOUT_MS; - tick_start = HAL_GetTick(); + /* 超时来源:调用方指定(单次预算)优先,否则用默认值(均作为挂起截止) */ + uint32_t to_ms = (timeout_ms != 0U) ? timeout_ms : (uint32_t)NET_RECV_TIMEOUT_MS; - while (1) { - net_poll(); + net_poll(); - int recv_len = ch395f_get_recv_len(sockfd); - if (recv_len > 0) { - int read_len = (recv_len > len) ? len : recv_len; - ch395f_read_recv_buf(sockfd, (uint8_t *)buf, (uint16_t)read_len); - p_sock->recv_bytes += (uint32_t)read_len; - p_sock->readable = 0; /* 已消费数据(注:部分读受 Trap 13 限制,下一次 RECV_OK 会重新置位) */ - return read_len; - } - - if (flags & NET_MSG_DONTWAIT) { - /* 非阻塞且当前无数据:若连接已断开则视为 EOF(0) 并自动重监听, - * 否则返回 WOULDBLOCK(-1) 表示暂时无数据 */ - if (p_sock->state == NET_SOCK_STATE_CLOSED) { - if (p_sock->auto_relisten) { - (void)net_relisten_try(p_sock); /* 非阻塞:失败由重试轮询接管 */ - } - return 0; - } - s_net_errno = NET_ERR_WOULDBLOCK; - return -1; - } - - if ((to_ms > 0U) && - ((HAL_GetTick() - tick_start) >= to_ms)) { - s_net_errno = NET_ERR_TIMEDOUT; - return -1; - } + int recv_len = ch395f_get_recv_len(sockfd); + if (recv_len > 0) { + int read_len = (recv_len > len) ? len : recv_len; + ch395f_read_recv_buf(sockfd, (uint8_t *)buf, (uint16_t)read_len); + p_sock->recv_bytes += (uint32_t)read_len; + p_sock->readable = 0; /* 已消费数据(注:部分读受 Trap 13 限制,下一次 RECV_OK 会重新置位) */ + return read_len; + } + if (flags & NET_MSG_DONTWAIT) { + /* 非阻塞且当前无数据:若连接已断开则视为 EOF(0) 并自动重监听, + * 否则返回 WOULDBLOCK(-1) 表示暂时无数据 */ if (p_sock->state == NET_SOCK_STATE_CLOSED) { - /* 断开前尝试排空 CH395F 接收缓冲区(Trap 13:FIN 可能比数据先到) */ - int rem = ch395f_get_recv_len(sockfd); - if (rem > 0) { - int rd = (rem > len) ? len : rem; - ch395f_read_recv_buf(sockfd, (uint8_t *)buf, (uint16_t)rd); - p_sock->recv_bytes += (uint32_t)rd; - p_sock->readable = 0; - return rd; - } - /* EOF:单连接模式自动回到 LISTEN,应用下次 net_accept 即接到新连接 */ if (p_sock->auto_relisten) { (void)net_relisten_try(p_sock); /* 非阻塞:失败由重试轮询接管 */ } return 0; } + s_net_errno = NET_ERR_WOULDBLOCK; + return -1; } + + /* 无数据 + 阻塞等待:登记挂起请求,立即返回(netTask 任何时刻都不被占用)。 + * 调用方在自己上下文睡任务通知(LwIP netconn 模型);等待期间 netTask + * 继续主循环(事件泵 + 队列),其他消费者零头阻塞。 */ + return net_pending_register(sockfd, buf, len, to_ms, caller); } /* @@ -1303,6 +1335,9 @@ static void net_relisten_retry_tick(void) { if (s_relisten_wait[i] == 0U) { continue; } + if (s_net_pending[i].active != 0U) { + continue; /* 本 fd 有在途挂起 recv:重开 Socket 会摧毁在途连接(L3 保险) */ + } if ((int32_t)(now - s_relisten_next[i]) < 0) { continue; /* 未到点(tick 差值按有符号比较,抗回绕) */ } @@ -1320,6 +1355,228 @@ static void net_relisten_retry_tick(void) { } } +/* ===== L3 挂起 recv 请求的实现(一期,2026-09-22) ===== + * 完成条件按 ①数据 ②到点 ③断连 顺序检查(逐项对齐旧自旋循环语义): + * ① 数据通道(ESTABLISHED/UDP_OPEN)内 readable 门控 SPI 读——常见无数据路径 + * 零额外 SPI 事务(INT# 电平触发链保证缓冲有数据时每轮重读 SINT 重位置位) + * ② 到点(与状态无关,旧自旋亦在任何状态下检查超时)→ NET_ERR_TIMEDOUT + * ③ CLOSED:先排空(Trap 13:FIN 可能先于数据)→ 有数据按数据完成, + * 否则 EOF(0) + 自动重监听 + * 其他状态(CONNECTING/LISTENING/…)保持等待状态迁移(CONNECT→ESTABLISHED + * 是合法路径;陈旧 resync 由显式取消处理)——不做状态推断取消,避免误杀 + * 正在建链的等待。看门狗兜底:到点后仍未了结 = 完成逻辑缺位 → 强制了结留痕。 */ + +/* + * 函数功能:通知调用方并清除本 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) { + net_pending_t *p; + + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return; + } + p = &s_net_pending[sockfd]; + if (p->active == 0U) { + return; + } + if (result < 0) { + s_net_errno = result; + } + 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 - 本次等待预算(截止 = 登记时刻 + to_ms) 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; + uint32_t now; + + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + s_net_errno = NET_ERR_BADF; + return -1; + } + p = &s_net_pending[sockfd]; + + if (p->active != 0U) { + s_net_errno = NET_ERR_BUSY; + return -1; + } + if ((caller == NULL) || (caller == s_net_task_handle)) { + s_net_errno = NET_ERR_BUSY; + return -1; + } + + now = HAL_GetTick(); + p->active = 1U; + p->buf = buf; + p->len = len; + p->deadline = now + to_ms; + p->registered = now; + p->caller = caller; + return NET_PENDING_REGISTERED; +} + +/* + * 函数功能:挂起 recv 完成检查(netTask 每轮 net_process_messages 调用一次,非阻塞) + * 返回值:无 + * 限定条件:netTask 上下文 + */ +static void net_pending_tick(void) { + uint32_t now = HAL_GetTick(); + int i; + + for (i = 0; i < NET_MAX_SOCKETS; i++) { + net_pending_t *p = &s_net_pending[i]; + net_sock_t *p_sock = &s_net_socks[i]; + + if (p->active == 0U) { + continue; + } + + 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) { + 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; /* 虚假标志:缓冲无数据,继续等待 */ + } + } + + /* ② 到点(tick 差值按有符号比较,抗回绕) */ + if ((int32_t)(now - p->deadline) >= 0) { + s_net_errno = NET_ERR_TIMEDOUT; + net_pending_complete(i, NET_ERR_TIMEDOUT); + continue; + } + + /* ③ 断连:排空 → 数据或 EOF(0) + 自动重监听 */ + if (p_sock->state == NET_SOCK_STATE_CLOSED) { + int rem = ch395f_get_recv_len((uint8_t)i); + if (rem > 0) { + 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 { + if (p_sock->auto_relisten != 0U) { + (void)net_relisten_try(p_sock); /* 非阻塞:失败由重试轮询接管 */ + } + net_pending_complete(i, 0); + } + continue; + } + + /* 其他状态:保持等待(见本区块头注释) */ + + /* ④ 看门狗:到点 + 裕量仍未了结 = 完成逻辑缺位 → 强制了结并留痕 */ + if ((int32_t)(now - p->deadline) >= (int32_t)NET_PENDING_WATCHDOG_MS) { + DBG_ERROR("sock%d pending recv stuck %ums past deadline (force settle)", + i, (unsigned)((int32_t)now - (int32_t)p->deadline)); + s_net_errno = NET_ERR_TIMEDOUT; + net_pending_complete(i, NET_ERR_TIMEDOUT); + } + } +} + +/* + * 函数功能:清除本 fd 的挂起请求(不通知;用于"新 fd 不得继承"与 close 后的幂等清理) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 返回值:无 + * 限定条件:无 + */ +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 上下文 + */ +static void net_pending_cancel(int sockfd, int err) { + net_pending_complete(sockfd, err); /* complete 内部校验范围与在位 */ +} + +/* + * 函数功能:取消全部挂起请求并逐一通知(网络重建路径) + * 入口参数:err - 通知的错误码 int + * 返回值:无 + * 限定条件:netTask 上下文;须在控制块清零与芯片复位之前调用 + */ +static void net_pending_cancel_all(int err) { + 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; + int avail; + + if (s_net_pending[sockfd].active == 0U) { + return; + } + val = NET_ERR_CONNRESET; + avail = ch395f_get_recv_len((uint8_t)sockfd); + if (avail > 0) { + 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(内部版本,不经过消息队列) @@ -1334,6 +1591,10 @@ static int net_close_locked(net_sock_t *p_sock) { int sockfd = (int)(p_sock - s_net_socks); uint32_t tick = HAL_GetTick(); + /* L3:先了结本 fd 的挂起 recv(排空/取消 + 必通知),再关芯片 Socket + * ——关闭不得让等待中的调用方永久悬挂 */ + net_pending_settle_close(p_sock); + ch395f_close_socket(sockfd); /* 排空上一条连接残留的 DISCONNECT 中断(单连接模式仅有 Socket 0, @@ -1357,6 +1618,7 @@ static int net_close_locked(net_sock_t *p_sock) { memset(p_sock, 0, sizeof(net_sock_t)); net_relisten_clear(sockfd); /* 关闭即取消该 fd 的待重试(D10 重试队列) */ + net_pending_clear(sockfd); /* 关闭即清除该 fd 的挂起状态(L3;幂等) */ return 0; } @@ -1814,13 +2076,16 @@ int net_send_sock(net_sock_t *p_sock, const void *buf, int len) { * -1 - 接收失败 int * 限定条件:必须在 netTask 上下文中调用;p_sock->in_use == 1 * 函数说明:直接操作 Socket 控制块,不经过消息队列。避免 netTask 向自身发送消息造成死锁 + * (L3 后:netTask 上下文的阻塞等待会被挂起登记拒绝——自通知死锁, + * 本函数阻塞用法仅对 DONTWAIT 有效) */ int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags) { if (p_sock == NULL) { s_net_errno = NET_ERR_BADF; return -1; } - return net_recv_locked(p_sock, buf, len, flags, 0U); /* 0 = 默认 NET_RECV_TIMEOUT_MS */ + return net_recv_locked(p_sock, buf, len, flags, 0U, /* 0 = 默认 NET_RECV_TIMEOUT_MS */ + xTaskGetCurrentTaskHandle()); } /* * 函数功能:关闭 Socket(内部版本,绕过消息队列,供 netTask 直接使用)