diff --git a/App/task/iec104_serv.c b/App/task/iec104_serv.c index ad14d399..0d094598 100644 --- a/App/task/iec104_serv.c +++ b/App/task/iec104_serv.c @@ -258,7 +258,10 @@ static void s_serv_poll(void) { s_liveness_ctr = 0u; } - n = net_recv(s_client, s_rx, (int)sizeof(s_rx), NET_MSG_DONTWAIT); + /* N34 阶段 1.5:DONTWAIT 转正为具名 API net_recv_nb——本任务在 IWDG + * required_mask 内且有 100ms 周期职责(喂狗/链路节拍/上送),禁止无限 + * 阻塞 recv(文档契约见 net_socket.h net_recv 注释),非阻塞轮询是义务 */ + n = net_recv_nb(s_client, s_rx, (int)sizeof(s_rx)); if (n > 0) { iec104_link_on_rx(&s_link, s_rx, (uint16_t)n, now); } else if (n == 0) { diff --git a/Drivers/BSP/NET/net_config.h b/Drivers/BSP/NET/net_config.h index d932bfb3..0570aad2 100644 --- a/Drivers/BSP/NET/net_config.h +++ b/Drivers/BSP/NET/net_config.h @@ -11,6 +11,9 @@ * 实际生效值 60s/5s/3 并在 net_socket.c 引用(原宏为无效遗留值) * 2026-09-22 新增 NET_RETRANS_COUNT/NET_RETRANS_PERIOD_MS(P0-a:重传周期 * 500→200ms 治 D1 症状),net_socket.c 的字面量 8/500 移入本文件 + * 2026-10-13 N34 阶段 1.5:删除 NET_RECV_TIMEOUT_MS(30s 默认预算)—— + * recv 超时改为调用方策略(net_recv 无限阻塞 / + * net_recv_to 显式预算 / net_select 首选),API 不再强加默认值 */ #ifndef NET_CONFIG_H @@ -30,15 +33,14 @@ extern "C" { /* * 超时时间配置(毫秒) * 0 = 无限等待(阻塞模式) + * 注:recv 侧无编译期默认预算(N34 阶段 1.5 删除 NET_RECV_TIMEOUT_MS)—— + * 超时是调用方策略:net_recv() 无限阻塞、net_recv_to() 显式预算、 + * net_select() 首选机制。 */ #ifndef NET_SEND_TIMEOUT_MS #define NET_SEND_TIMEOUT_MS 1000 /* 发送超时 */ #endif -#ifndef NET_RECV_TIMEOUT_MS -#define NET_RECV_TIMEOUT_MS 30000 /* 接收超时(30秒,0=无限等待) */ -#endif - /* * TCP 重传配置(由 net_socket.c 的 net_init() 在 ch395f_init() 之前下发) * P0-a(2026-09-22):周期 500→200ms——单次丢包代价 500→200ms(治 D1 症状), diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 0d7f0fd6..9581781b 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -174,19 +174,23 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a /* L3 pending 化(一期,E-02 结构演进):阻塞等待 = 挂起请求 + 主循环节拍完成 * "无数据 + 阻塞等待"不再自旋占用 netTask:登记 pending 后立即返回 * net_pending_tick(与 D10 重监听重试同节奏,net_process_messages 开头每轮调用) - * 按 ①数据 ②到点 ③断连 顺序检查完成条件并通知调用方(语义逐项对齐旧自旋循环) + * 按 ①数据 ②到点(仅显式预算) ③断连 顺序检查完成条件并通知调用方(语义逐项对齐旧自旋循环) * 关闭/网络重建/陈旧重同步走显式 settle/cancel 路径(取消必通知,调用方永不挂死) * 公开 API 返回契约不变:>0 字节数 / 0 EOF / -1 错误(错误码经通知值回传) + * N34 阶段 1.5:预算归调用方——timeout_ms=0 即无限等待(30s 默认预算已删), + * 原"到点 +1s 强制了结"的看门狗降级为 tripwire(只留痕、绝不伪造返回值) */ #define NET_PENDING_REGISTERED 0x70000000 /* 内部哨兵:pending 已登记(分派跳过即时通知;取值不与字节数空间冲突) */ -#define NET_PENDING_WATCHDOG_MS 1000U /* 看门狗:到点后 1s 仍未了结 = 完成逻辑缺位 → 强制了结 + 错误日志 */ +#define NET_RECV_TRIPWIRE_MS 60000U /* tripwire:挂起超 60s = 完成逻辑缺位探针 → 只 [ERR] 留痕一次,绝不强制了结 */ typedef struct { uint8_t active; /* 1 = 本 fd 有挂起 recv */ + uint8_t infinite; /* 1 = 无限等待(无 deadline,N34 阶段 1.5:0 预算 = 永等) */ + uint8_t tripwire_logged; /* 1 = 本轮挂起已打过 tripwire(保证只留痕一条) */ void *buf; /* 调用方缓冲(仅指针,须保持有效至完成) */ int len; /* 缓冲容量 */ - uint32_t deadline; /* 截止时刻(tick)= 登记时刻 + 本次预算 */ - uint32_t registered; /* 登记时刻(tick;看门狗/日志) */ + uint32_t deadline; /* 截止时刻(tick)= 登记时刻 + 本次预算(infinite=1 时无意义) */ + uint32_t registered; /* 登记时刻(tick;tripwire/日志基准) */ TaskHandle_t caller; /* 调用方任务(完成通知目标) */ } net_pending_t; @@ -694,8 +698,8 @@ static void net_reconcile_tick(void) { * (NET_RELISTEN_RETRY_MS)节拍重试 open+listen;2s(NET_RELISTEN_DEADLINE_MS) * 总时限到期放弃留痕 * 3. D10:本 fd 有在途挂起 recv 时跳过(L3 保险) - * 4. net_pending_tick(L3):按节拍了结挂起 recv(数据/到点/断连) - * 5. L3 看门狗:到点后 1s(NET_PENDING_WATCHDOG_MS)未了结 → 强制了结 + DBG_ERROR 留痕 + * 4. net_pending_tick(L3):按节拍了结挂起 recv(数据/到点[仅显式预算]/断连) + * 5. L3 tripwire(N34 阶段 1.5):挂起超 60s 只 DBG_ERROR 留痕一次,不再强制了结/伪造返回值 * 6. net_reconcile_tick(Trap 22):每 200ms(NET_RECONCILE_PERIOD_MS)对账①② * 7. 对账以连续 2 次失配去抖 + 1s 冷却,自愈两类失同步 * 8. net_listener_pump(N-32 P2):每 100ms 监听器 pump,本层**唯一**重建入口 @@ -933,6 +937,10 @@ int net_socket(int domain, int type) { p_sock->send_ready = 1; /* 无可读数据 */ p_sock->readable = 0; + /* N34 阶段 1.5:阻塞模式默认 = 阻塞(BSD/Python 语义;非阻塞用 + * net_setblocking(fd, 0) 显式设置)。close 的 memset 会清零本字段, + * 新 fd 必须在此显式置 1(同 released_abnormal 的"不得继承"纪律) */ + p_sock->blocking = 1; /* 新 fd 不得继承上一条连接的释放原因(Trap 23 待办 #3) */ p_sock->released_abnormal = 0; /* N-32(P1):新 fd 不得继承"上一条会话已结束"电平(否则新建即 EOF, @@ -1865,16 +1873,17 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) { * buf - 接收缓冲区指针 void * * len - 缓冲区大小 int > 0 * flags - 标志位,NET_MSG_DONTWAIT 表示非阻塞 int - * timeout_ms - 本次等待预算(0 = 默认 NET_RECV_TIMEOUT_MS) uint32_t + * timeout_ms - 本次等待预算(0 = 无限等待,N34 阶段 1.5 起无默认值)uint32_t * caller - 调用方任务句柄(挂起请求的完成通知目标) TaskHandle_t * 出口参数:buf - 填充接收到的数据 * 返回值:实际接收字节数(> 0);0 表示对端关闭;-1 表示出错/非阻塞无数据 int * NET_PENDING_REGISTERED 表示已登记挂起请求(分派据此跳过即时通知) * 限定条件:p_sock 非空;netTask 上下文 * 函数说明:1. 数据已在(快速路径)→ 立即读出返回,行为与旧实现一致 - * 2. 非阻塞模式无数据立即返回 -1(NET_ERR_WOULDBLOCK) + * 2. 非阻塞(NET_MSG_DONTWAIT 单次覆盖 **或** socket blocking 属性为 0, + * N34 阶段 1.5 模式属性)无数据立即返回 -1(NET_ERR_WOULDBLOCK) * 3. 无数据 + 阻塞等待 → 登记挂起请求(L3 pending 化,E-02 结构演进) - * 4. netTask 零占用立即返回;完成条件(数据/到点/断连)由 net_pending_tick 按主循环节拍检查 + * 4. netTask 零占用立即返回;完成条件(数据/到点[仅显式预算]/断连)由 net_pending_tick 按主循环节拍检查 * 5. 并通知 caller;关闭/网络重建/陈旧重同步由对应路径显式了结(取消必通知) * 6. CLOSED 状态语义(排空 + EOF + 自动重监听)逐项保留在 tick 内 */ @@ -1888,8 +1897,9 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, return NET_ERR_RETURN_1(NET_ERR_INVAL); } - /* 超时来源:调用方指定(单次预算)优先,否则用默认值(均作为挂起截止) */ - uint32_t to_ms = (timeout_ms != 0U) ? timeout_ms : (uint32_t)NET_RECV_TIMEOUT_MS; + /* 超时来源:只有调用方显式预算(net_recv_to);0 = 无限等待 + * (N34 阶段 1.5:NET_RECV_TIMEOUT_MS 30s 默认预算已删,API 不强加隐藏值) */ + uint32_t to_ms = timeout_ms; /* 先轮询一次:刷新状态机与可读标志 */ net_poll(); @@ -1911,8 +1921,9 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, return read_len; } - /* 非阻塞模式(DONTWAIT) */ - if (flags & NET_MSG_DONTWAIT) { + /* 非阻塞模式:NET_MSG_DONTWAIT 单次覆盖,或 socket 的 blocking 属性为 0 + * (net_setblocking(fd, 0),N34 阶段 1.5 模式挂控制块属性) */ + if ((flags & NET_MSG_DONTWAIT) || (p_sock->blocking == 0U)) { /* 非阻塞且当前无数据:若连接已断开则视为 EOF(0) 并自动重监听, * 否则返回 WOULDBLOCK(-1) 表示暂时无数据 */ /* 连接已断开(CLOSED)**或**本会话已结束(session_dead):按 EOF 处理。 @@ -1961,16 +1972,18 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, * 2. 清空整个控制块(memset 0),释放该 Socket 资源 */ -/* ===== L3 挂起 recv 请求的实现(一期,2026-09-22) ===== - * 完成条件按 ①数据 ②到点 ③断连 顺序检查(逐项对齐旧自旋循环语义) +/* ===== L3 挂起 recv 请求的实现(一期,2026-09-22;N34 阶段 1.5 语义主流化) ===== + * 完成条件按 ①数据 ②到点(仅显式预算) ③断连 顺序检查(逐项对齐旧自旋循环语义) * ① 数据通道(ESTABLISHED/UDP_OPEN)内 readable 门控 SPI 读——常见无数据路径零额外 SPI 事务 * (INT# 电平触发链保证缓冲有数据时每轮重读 SINT 重位置位) - * ② 到点(与状态无关,旧自旋亦在任何状态下检查超时)→ NET_ERR_TIMEDOUT + * ② 到点(仅当登记时带显式预算 net_recv_to;无限等待登记无 deadline)→ NET_ERR_TIMEDOUT + * ——这是调用方策略的正常兑现,不是伪造 * ③ CLOSED:先排空(bit3=1 芯片断连后残数据留缓冲待 MCU 读——Trap 25 排空契约; * 旧注归 Trap 13 已随其复核注降级)→ 有数据按数据完成,否则 EOF(0) + 自动重监听 * 其他状态(CONNECTING/LISTENING/…)保持等待状态迁移(CONNECT→ESTABLISHED 是合法路径; * 陈旧 resync 由显式取消处理)——不做状态推断取消,避免误杀正在建链的等待 - * 看门狗兜底:到点后仍未了结 = 完成逻辑缺位 → 强制了结留痕 + * tripwire(N34 阶段 1.5 降级):挂起超 60s = "完成逻辑缺位"探针 → 只 [ERR] 留痕一次, + * **绝不强制了结、绝不伪造 TIMEDOUT 返回值**(唤醒责任在 ①②③ 与显式 cancel 路径) */ /* @@ -2012,7 +2025,7 @@ static void net_pending_complete(int sockfd, int result) { * 入口参数:sockfd - Socket 描述符 (0~7) int * buf - 调用方接收缓冲(须保持有效至完成) void * * len - 缓冲容量 int - * to_ms - 本次等待预算(截止 = 登记时刻 + to_ms) uint32_t + * to_ms - 本次等待预算(0 = 无限等待,N34 阶段 1.5) uint32_t * caller - 调用方任务句柄(完成通知目标) TaskHandle_t * 出口参数:无 * 返回值:NET_PENDING_REGISTERED - 登记成功(分派跳过即时通知) @@ -2045,17 +2058,21 @@ static int net_pending_register(int sockfd, void *buf, int len, return NET_ERR_RETURN_1(NET_ERR_BUSY); } - /* 当前 tick(截止/看门狗基准) */ + /* 当前 tick(截止/tripwire 基准) */ now = HAL_GetTick(); /* 标记挂起在位 */ p->active = 1U; + /* 预算形态:0 = 无限等待(无 deadline,tick ② 跳过;tripwire 以登记时刻计) */ + p->infinite = (to_ms == 0U) ? 1U : 0U; + /* tripwire 留痕标记清零(每轮挂起只留痕一条) */ + p->tripwire_logged = 0U; /* 调用方接收缓冲(须保持有效至完成) */ p->buf = buf; /* 缓冲容量 */ p->len = len; - /* 截止时刻 = 登记时刻 + 预算 */ - p->deadline = now + to_ms; - /* 登记时刻(看门狗基准) */ + /* 截止时刻 = 登记时刻 + 预算(无限等待时不启用,置 0 仅为可观测性) */ + p->deadline = (to_ms == 0U) ? 0U : (now + to_ms); + /* 登记时刻(tripwire 基准) */ p->registered = now; /* 完成通知目标 */ p->caller = caller; @@ -2069,8 +2086,10 @@ static int net_pending_register(int sockfd, void *buf, int len, * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文 - * 函数说明:1. 逐 fd 检查完成条件:数据到达 / 到点(deadline)/ 断连 - * 2. 完成即经 net_pending_complete 通知并清除;看门狗(NET_PENDING_WATCHDOG_MS)超时强制了结 + * 函数说明:1. 逐 fd 检查完成条件:数据到达 / 到点(仅显式预算)/ 断连 + * 2. 完成即经 net_pending_complete 通知并清除 + * 3. tripwire(N34 阶段 1.5):挂起超 NET_RECV_TRIPWIRE_MS 只 [ERR] 留痕 + * 一次("完成逻辑缺位"探针),绝不强制了结、绝不伪造返回值 */ static void net_pending_tick(void) { /* 当前 tick */ @@ -2122,8 +2141,10 @@ static void net_pending_tick(void) { } } - /* ② 到点(tick 差值按有符号比较,抗回绕) */ - if ((int32_t)(now - p->deadline) >= 0) { + /* ② 到点(仅显式预算登记;无限等待登记无 deadline——N34 阶段 1.5: + * 超时是调用方策略,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; @@ -2133,7 +2154,9 @@ static void net_pending_tick(void) { * 判据同 net_recv_locked(N-32 P1 不变量 I):`state == CLOSED || * session_dead`。只判 CLOSED 时,抢先 relisten 后 ①(要求 ESTABLISHED) * 与 ③(要求 CLOSED) 两头落空 → 挂起的 recv 只能干等 ② deadline 超时 - * (lftpd 阻塞 recv 的"消费者卡死"原型)。 */ + * (lftpd 阻塞 recv 的"消费者卡死"原型)。N34 阶段 1.5 后该形态更致命: + * 无限等待登记没有 ②,漏 ③ = 永久挂死(只剩 tripwire 留痕)—— + * ③ 与显式 cancel 是无限 recv 的全部唤醒责任方,勿动判据。 */ if ((p_sock->state == NET_SOCK_STATE_CLOSED) || (p_sock->session_dead != 0U)) { /* 查询残留字节数 */ int rem = ch395f_get_recv_len((uint8_t)i); @@ -2179,13 +2202,23 @@ static void net_pending_tick(void) { /* 其他状态:保持等待(见本区块头注释) */ - /* ④ 看门狗:到点 + 裕量仍未了结 = 完成逻辑缺位 → 强制了结并留痕 */ - 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)); - /* 强制了结:超时(错误码由 net_pending_complete 记入本任务槽位) */ - net_pending_complete(i, NET_ERR_TIMEDOUT); + /* ④ tripwire(N34 阶段 1.5 降级,原"看门狗强制了结"):挂起超 60s = + * "完成逻辑缺位"探针 → 只 [ERR] 留痕一次,**绝不强制了结、绝不伪造 + * TIMEDOUT 返回值**(旧实现到点 +1s 兜底伪造 TIMEDOUT,掩盖完成路径 + * 缺陷;新语义下无限等待本就无预算,伪造返回值反而违背调用方意图)。 + * 判据:无限登记按登记时刻计;显式登记按 deadline 计(② 正常不会让它 + * 越过 60s,能到这里本身就是完成逻辑缺位)。 */ + 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; + } } } } @@ -2437,17 +2470,26 @@ int net_send(int sockfd, const void *buf, int len, int flags) { } /* - * 函数功能:接收数据(线程安全版本,通过消息队列委托给 netTask) + * 函数功能:接收数据(线程安全版本,通过消息队列委托给 netTask; + * **默认无限阻塞**——N34 阶段 1.5 语义主流化,30s 默认预算已删) * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS * buf - 接收缓冲区指针 void * * len - 缓冲区大小 int > 0 - * flags - 标志位,0=阻塞,NET_MSG_DONTWAIT=非阻塞 int + * flags - 标志位,0=按 socket 阻塞属性等待,NET_MSG_DONTWAIT=单次非阻塞 int * 出口参数:无 * 返回值:实际接收字节数 int >= 0 || 0 || -1 * 0 - 对端关闭连接 int - * -1 - 接收失败(参数无效、队列满等) int + * -1 - 接收失败(参数无效、非阻塞无数据=WOULDBLOCK、 + * close/网络重建/陈旧重同步取消=CONNRESET 等,errno 可查) int * 限定条件:Socket 处于 ESTABLISHED 状态;net_init() + MX_FREERTOS_Init() 已执行;非 netTask 上下文 - * 函数说明:将接收请求放入消息队列,阻塞等待 netTask 处理完毕并返回结果。netTask 内请使用 net_recv_sock() + * 函数说明:1. 将接收请求放入消息队列,等待 netTask 处理完毕并返回结果。netTask 内请使用 net_recv_sock() + * 2. 无数据 + 阻塞(blocking 属性为默认值 1 且未置 DONTWAIT)→ 登记挂起 + * **无限等待**;唤醒源 = 数据 / EOF / close / 网络重建(CONNRESET); + * 会话死亡由对账① ~400ms 判死兜底,不存在"对端消失后永久挂死"窗口 + * 3. ⚠ 文档契约(嵌入式特有):IWDG required_mask 内任务禁止无限阻塞 + * recv——必须 net_select 超时或 net_recv_to 显式预算(超时归使用者 + * 对受监控任务是义务不是选项),否则"对端连上不发数据"= IWDG 复位 + * 4. 超时首选 net_select(...);net_recv_to(...) 为显式预算便捷入口 */ int net_recv(int sockfd, void *buf, int len, int flags) { /* 一次性消息结构体(队列按值存结构副本) */ @@ -2471,9 +2513,9 @@ int net_recv(int sockfd, void *buf, int len, int flags) { msg.buf = buf; /* 缓冲容量 */ msg.len = len; - /* 标志:0=阻塞,NET_MSG_DONTWAIT=非阻塞 */ + /* 标志:0=按 socket 阻塞属性,NET_MSG_DONTWAIT=单次非阻塞 */ msg.flags = flags; - /* 0 = 默认超时预算(netTask 侧用 NET_RECV_TIMEOUT_MS) */ + /* 0 = 无限等待(N34 阶段 1.5:无默认预算;显式预算走 net_recv_to) */ msg.timeout_ms = 0U; /* 调用方任务句柄(netTask 凭它回传回执) */ msg.caller = xTaskGetCurrentTaskHandle(); @@ -2499,7 +2541,28 @@ int net_recv(int sockfd, void *buf, int len, int flags) { } /* - * 函数功能:接收数据(带调用方指定超时的阻塞版,用于分片有界读) + * 函数功能:接收数据(非阻塞具名入口——N34 阶段 1.5 把 NET_MSG_DONTWAIT 路径转正, + * 对齐 BSD recv(MSG_DONTWAIT) / Python recv 非阻塞模式的"名如其义") + * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS + * buf - 接收缓冲区指针 void * + * len - 缓冲区大小 int > 0 + * 出口参数:buf - 填充接收到的数据 + * 返回值:实际接收字节数(> 0);0 = 对端关闭(net 层已自动重监听); int >= 0 || -1 + * -1 = 错误(无数据 = NET_ERR_WOULDBLOCK,其余 errno 可查) int + * 限定条件:非 netTask 上下文;net 层已初始化 + * 函数说明:1. 等价 net_recv(sockfd, buf, len, NET_MSG_DONTWAIT):单次强制非阻塞, + * 不改变 socket 的 blocking 属性(模式用 net_setblocking) + * 2. 受 IWDG 监控 / 有周期职责的任务(netTask 除外)的推荐默认入口; + * 需要"等但不无限等"用 net_select / net_recv_to + */ +int net_recv_nb(int sockfd, void *buf, int len) { + return net_recv(sockfd, buf, len, NET_MSG_DONTWAIT); +} + +/* + * 函数功能:接收数据(带调用方指定超时的阻塞版,用于分片有界读; + * N34 阶段 1.5 起为**显式预算**便捷入口,等价 Python settimeout, + * 不再有隐藏默认值) * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS * buf / len - 接收缓冲与容量 void * / int > 0 * timeout_ms - 单次占用 netTask 的最长等待(毫秒,>0) uint32_t @@ -2507,10 +2570,11 @@ int net_recv(int sockfd, void *buf, int len, int flags) { * 返回值:>0 字节数;0 = 对端关闭(EOF);-1 = 真错误(errno 可查);**-2 = 超时无数据** * 限定条件:非 netTask 上下文;net 层已初始化 * 函数说明:1. E-02 修复配套接口:lftpd 等"等对端下一行/下一块数据"的调用方 - * 若用默认 30s 阻塞 recv,会把 netTask(全部 socket 操作的唯一执行者)长期占住 + * 若无限阻塞 recv,会把 netTask(全部 socket 操作的唯一执行者)长期占住 * 2. 导致其余任务的网络操作整体冻结(M3 实测一次空闲 FTP 登录冻结远端显示 17.7s) * 3. 用本接口把等待切成 ≤timeout_ms 的片,调用方自行累计总预算:总超时语义不变、单次占用有界 * 4. 超时用独立返回码 -2(而非靠 errno 区分)→ 无跨任务 errno 竞态 + * 5. ⚠ IWDG required_mask 内任务必须走本接口或 net_select(net_recv 契约) */ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { /* 一次性消息结构体:队列按值存结构副本(仅指针、不拷数据),入队后由 netTask 取出处理 */ @@ -2528,8 +2592,9 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { return NET_ERR_RETURN_1(NET_ERR); } - /* ---- 3. 分片长度检查:timeout_ms = 0 在 netTask 侧意味着"改用默认 - * NET_RECV_TIMEOUT_MS(30 秒)",恰是本接口要规避的长阻塞,直接拒绝 ---- */ + /* ---- 3. 分片长度检查:timeout_ms = 0 在 netTask 侧意味着"**无限等待**" + * (N34 阶段 1.5 删除 30s 默认预算),恰与本接口"有界等待"的存在 + * 理由相反,直接拒绝 ---- */ if (timeout_ms == 0U) { /* 分片长度必须 > 0 */ return NET_ERR_RETURN_1(NET_ERR_INVAL); @@ -2780,14 +2845,14 @@ int net_sendto(int sockfd, const void *buf, int len, int flags, * 限定条件:Socket 已创建且为 UDP 类型(SOCK_DGRAM);net_init() 已调用;有数据可用 * 函数说明:1. 轮询检查芯片接收缓冲(net_poll + get_recv_len),有数据即返回 * 2. UDP 数据报前 8 字节为报文头(源端口+源 IP),据此填充 src_addr - * 3. 无数据时:DONTWAIT 立即返回 -1(WOULDBLOCK);默认等待 NET_RECV_TIMEOUT_MS 后返回 TIMEDOUT + * 3. 无数据时(N34 阶段 1.5,与 net_recv 同构):DONTWAIT / 非阻塞属性 + * 立即返回 -1(WOULDBLOCK);阻塞模式**无限等待**(30s 默认预算已删; + * 本接口为轮询自旋实现且无生产调用者,受 IWDG 监控的任务勿用无限形态) */ int net_recvfrom(int sockfd, void *buf, int len, int flags, struct net_sockaddr *src_addr, int *addrlen) { /* Socket 控制块指针 */ net_sock_t *p_sock; - /* 起始时刻(超时判定基准,tick) */ - uint32_t tick_start; /* 参数检查 */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { @@ -2807,10 +2872,7 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, return NET_ERR_RETURN_1(NET_ERR_INVAL); } - /* 记录起始时刻(超时基准) */ - tick_start = HAL_GetTick(); - - /* 轮询直到:有数据 / 非阻塞 / 超时 */ + /* 轮询直到:有数据 / 非阻塞(N34 阶段 1.5:阻塞 = 无限等待,无默认预算) */ while (1) { /* 轮询一次:更新状态机与可读标志 */ net_poll(); @@ -2862,18 +2924,16 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, } } - /* 非阻塞模式:无数据立即返回 */ - if (flags & NET_MSG_DONTWAIT) { + /* 非阻塞模式:NET_MSG_DONTWAIT 单次覆盖,或 socket blocking 属性为 0 + * (N34 阶段 1.5,与 net_recv_locked 同判据):无数据立即返回 */ + if ((flags & NET_MSG_DONTWAIT) || (p_sock->blocking == 0U)) { /* 置 WOULDBLOCK */ return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK); } - /* 超时检查 */ - if (NET_RECV_TIMEOUT_MS > 0 && - (HAL_GetTick() - tick_start) >= NET_RECV_TIMEOUT_MS) { - /* 置 TIMEDOUT */ - return NET_ERR_RETURN_1(NET_ERR_TIMEDOUT); - } + /* 阻塞模式:无数据继续轮询等待(N34 阶段 1.5:无限等待,无默认预算—— + * 旧 NET_RECV_TIMEOUT_MS 30s 兜底已删;唤醒源 = 数据,与 net_recv 的 + * 差异是本接口为自旋实现,勿在受 IWDG 监控任务中使用无限形态) */ } } @@ -2916,7 +2976,8 @@ int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags) { return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 直接委托锁定实现(netTask 上下文,无队列往返); - * 0 = 默认 NET_RECV_TIMEOUT_MS;caller = netTask 自身(自调用) */ + * timeout_ms = 0(无限等待语义,但 netTask 自调用会被挂起登记拒绝 → + * 本接口实际仅 DONTWAIT / 非阻塞属性可用);caller = netTask 自身 */ return net_recv_locked(p_sock, buf, len, flags, 0U, xTaskGetCurrentTaskHandle()); } @@ -2975,6 +3036,46 @@ int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg) { /* 注册成功 */ return 0; } + +/* + * 函数功能:设置 Socket 阻塞/非阻塞**模式属性**(N34 阶段 1.5,模式挂控制块属性, + * 对齐 BSD O_NONBLOCK / Python setblocking) + * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS + * on - 1 = 阻塞(net_socket 默认);0 = 非阻塞 int + * 出口参数:无 + * 返回值:0 - 设置成功 int + * -1 - 失败(描述符越界 / 未占用) int + * 限定条件:socket 已创建(in_use == 1);任意上下文(同 net_set_event_cb 先例) + * 函数说明:1. 属性对 net_recv / net_recv_sock 生效:非阻塞下无数据立即 + * NET_ERR_WOULDBLOCK;EOF 判定语义不变(CLOSED/session_dead 无数据 + * 仍返回 0 并触发自动重监听) + * 2. 直写控制块单字节(ARM 原子写,先例 net_set_event_cb):与 netTask + * 竞态的最坏结果 = 一次在途 recv 用旧模式,无撕裂、无需过消息队列 + * 3. 与超时预算正交:模式是粘滞属性;单次预算用 net_recv_to / net_select + */ +int net_setblocking(int sockfd, int on) { + /* Socket 控制块指针 */ + net_sock_t *p_sock; + + /* 参数检查:描述符范围 */ + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { + /* 描述符越界 → BADF */ + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + + /* 取控制块 */ + p_sock = &s_net_socks[sockfd]; + /* 控制块未在用 → BADF */ + if (!p_sock->in_use) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + + /* 写模式属性:非 0 = 阻塞,0 = 非阻塞(单字节原子写) */ + p_sock->blocking = (on != 0) ? 1U : 0U; + + /* 设置成功 */ + return 0; +} /* * 函数功能:获取 **本任务** 最后一次 net_*() 调用失败时的精确错误码 * 入口参数:无 diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index 3915b3a0..bbd4a319 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -48,7 +48,8 @@ typedef struct { void *buf; /* 数据缓冲区(send/recv) */ int len; /* 数据长度/缓冲区大小 */ int flags; /* 接收标志 */ - uint32_t timeout_ms; /* 接收超时(入队方向;0 = 用默认 NET_RECV_TIMEOUT_MS) */ + uint32_t timeout_ms; /* 接收超时(入队方向;0 = 无限等待,无默认预算—— + * N34 阶段 1.5 删除 NET_RECV_TIMEOUT_MS 30s 默认) */ TaskHandle_t caller; /* 调用方任务句柄 */ int result; /* 操作结果 */ /* 连接参数 */ @@ -230,19 +231,55 @@ int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen); int net_send(int sockfd, const void *buf, int len, int flags); /* - * 函数功能:接收数据(TCP) + * 函数功能:接收数据(TCP,**默认无限阻塞**——N34 阶段 1.5 语义主流化) * 入口参数:sockfd - socket 描述符 * buf - 接收缓冲区 * len - 缓冲区大小 - * flags - 通常为 0,可设置 NET_MSG_DONTWAIT 非阻塞 + * flags - 通常为 0;NET_MSG_DONTWAIT 单次强制非阻塞 * 返回值:实际接收字节数,0=对端关闭,-1=错误 * 限定条件:TCP 连接已建立 - * 函数说明:在阻塞模式下,如果没有数据,函数会等待; - * 在非阻塞模式下,如果没有数据,返回 -1 并设置 - * net_errno 为 NET_ERR_WOULDBLOCK + * 函数说明:1. 无数据时**无限等待**(无默认超时预算,对齐 POSIX/Python:模式归 + * 使用者、超时归调用方);唤醒源 = 数据 / EOF / close / 网络重建 + * (NET_ERR_CONNRESET)——会话死亡由对账①在 ~400ms 内判死兜底, + * 不存在"对端消失后永久挂死"的窗口 + * 2. 模式属性:net_setblocking(fd, 0) 后本接口无数据立即返回 + * NET_ERR_WOULDBLOCK;NET_MSG_DONTWAIT 仅单次覆盖该属性 + * 3. ⚠ 文档契约(嵌入式特有):IWDG required_mask 内任务**禁止无限 + * 阻塞 recv**——必须 net_select 超时或 net_recv_to 显式预算, + * 否则"对端连上不发数据"= IWDG 复位(超时归使用者对受监控任务是义务) + * 4. 超时首选 net_select(r,w,e,timeout_ms);net_recv_to 为显式预算便捷入口 */ int net_recv(int sockfd, void *buf, int len, int flags); +/* + * 函数功能:接收数据(非阻塞具名入口,原 NET_MSG_DONTWAIT 路径转正——N34 阶段 1.5) + * 入口参数:sockfd - socket 描述符 + * buf - 接收缓冲区 + * len - 缓冲区大小 + * 返回值:实际接收字节数;0 = 对端关闭(net 层已自动重监听); + * -1 = 错误(无数据 = NET_ERR_WOULDBLOCK,其余 errno 可查) + * 限定条件:非 netTask 上下文(netTask 内用 net_recv_sock + DONTWAIT) + * 函数说明:等价 net_recv(sockfd, buf, len, NET_MSG_DONTWAIT); + * 不改变 socket 的 blocking 属性(模式用 net_setblocking 设置) + */ +int net_recv_nb(int sockfd, void *buf, int len); + +/* + * 函数功能:设置 Socket 阻塞/非阻塞**模式属性**(对齐 BSD O_NONBLOCK / Python + * setblocking——N34 阶段 1.5,模式挂 socket 控制块属性) + * 入口参数:sockfd - socket 描述符 + * on - 1 = 阻塞(net_socket 默认);0 = 非阻塞 + * 返回值:0 成功;-1 失败(描述符无效/未占用) + * 限定条件:socket 已创建 + * 函数说明:1. 属性对 net_recv / net_recv_sock 生效:非阻塞模式下无数据立即 + * 返回 NET_ERR_WOULDBLOCK(EOF 判定语义不变:CLOSED/session_dead + * 无数据仍返回 0 并触发自动重监听) + * 2. 直写控制块单字节(先例 net_set_event_cb),任意上下文可调; + * 最坏一次在途 recv 用旧模式,无撕裂风险 + * 3. 与超时预算正交:模式是粘滞属性,单次预算用 net_recv_to/net_select + */ +int net_setblocking(int sockfd, int on); + /* * 函数功能:接收数据(**带调用方指定超时**的阻塞版,用于分片有界读) * 入口参数:sockfd - Socket 描述符 @@ -250,10 +287,14 @@ int net_recv(int sockfd, void *buf, int len, int flags); * timeout_ms - 单次调用在 netTask 内的最长等待(毫秒,>0) * 返回值:>0 实际字节数;0 = 对端关闭(EOF);-1 = 真错误(errno 可查);**-2 = 超时无数据** * 限定条件:非 netTask 上下文;Socket 已连接 - * 函数说明:为"不能长时间占用 netTask"的调用方(如 lftpd 控制行/数据读)提供 - * **分片**阻塞:每次调用最多占用 netTask timeout_ms,返回 -2 表示这段 - * 时间内无数据,调用方自行累计总预算并决定是否继续(E-02 头阻塞修复)。 - * 超时用独立返回码而非 errno → 无跨任务 errno 竞态。 + * 函数说明:1. **显式预算**便捷入口(等价 Python settimeout,N34 阶段 1.5 起 + * 不再有隐藏默认值):net_recv 的无限阻塞 + 本接口的显式预算构成 + * 超时语义的全部来源;超时首选仍是 net_select + * 2. E-02 修复配套:lftpd 等"等对端下一行/下一块数据"的调用方 + * 若无限阻塞 recv,会把 netTask(全部 socket 操作的唯一执行者)长期占住 + * 3. 用本接口把等待切成 ≤timeout_ms 的片,调用方自行累计总预算:总超时语义不变、单次占用有界 + * 4. 超时用独立返回码 -2(而非靠 errno 区分)→ 无跨任务 errno 竞态 + * 5. ⚠ IWDG required_mask 内任务必须走本接口或 net_select(见 net_recv 契约) */ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); @@ -277,12 +318,16 @@ int net_sendto(int sockfd, const void *buf, int len, int flags, * 入口参数:sockfd - socket 描述符 * buf - 接收缓冲区 * len - 缓冲区大小 - * flags - 通常为 0 + * flags - 通常为 0;NET_MSG_DONTWAIT 单次强制非阻塞 * src_addr - 输出:发送方地址 * addrlen - 输入输出:地址长度 * 返回值:实际接收字节数,失败返回 -1 * 限定条件:socket 已创建,指针非空 - * 函数说明:读取 CH395F 接收缓冲区,获取对端地址 + * 函数说明:1. 读取 CH395F 接收缓冲区,获取对端地址 + * 2. 阻塞语义与 net_recv 同(N34 阶段 1.5):blocking 属性下无数据 + * 无限等待,DONTWAIT / 非阻塞属性立即 NET_ERR_WOULDBLOCK; + * 本接口为轮询自旋实现(历史遗留,UDP 专用),受 IWDG 监控的 + * 任务同样禁止对其无限等待 */ int net_recvfrom(int sockfd, void *buf, int len, int flags, struct net_sockaddr *src_addr, int *addrlen); diff --git a/Drivers/BSP/NET/net_types.h b/Drivers/BSP/NET/net_types.h index 3a218ed1..35cfbd42 100644 --- a/Drivers/BSP/NET/net_types.h +++ b/Drivers/BSP/NET/net_types.h @@ -47,7 +47,9 @@ extern "C" { /* * 接收标志定义 */ -#define NET_MSG_DONTWAIT 0x01 /* 非阻塞模式 */ +#define NET_MSG_DONTWAIT 0x01 /* 非阻塞模式(单次覆盖 socket 的 blocking 属性; + * 具名入口见 net_recv_nb() / net_setblocking(), + * N34 阶段 1.5) */ /* * 网络字节序转换宏 @@ -171,6 +173,15 @@ typedef struct { /* 单连接模式自动重监听:连接断开后由 net 层自动回到 LISTEN(BSD 风格监听常驻) */ uint8_t auto_relisten; + /* N34 阶段 1.5(2026-10-13):阻塞/非阻塞**模式属性**(对齐 BSD O_NONBLOCK / + * Python setblocking 的"模式归使用者"模型)。 + * 1 = 阻塞(net_socket() 默认);0 = 非阻塞(net_setblocking(fd, 0))。 + * net_recv() 无 NET_MSG_DONTWAIT 时按本属性决定"无数据 → 登记挂起等待" + * 还是"立即 WOULDBLOCK"。单字节写(ARM 原子),写入先例同 event_cb + * (net_setblocking 直写控制块,最坏一次 recv 用旧模式,无撕裂风险)。 + * 注意与"超时预算"正交:模式是属性(粘滞),预算是单次参数(net_recv_to)。 */ + uint8_t blocking; + /* 释放来源标记(Trap 23 待办 #3):1 = 异常释放(芯片 TIMEOUT,对端 RST/掉链), * 0 = 正常释放(芯片 DISCONNECT,对端 FIN)或新连接。L3 挂起 recv 的了结断连 * 分支据此决定回 EOF(0) 还是 NET_ERR_CONNRESET:异常结束上报应用(lftpd STOR) diff --git a/Drivers/BSP/UI/ui_net_sink.c b/Drivers/BSP/UI/ui_net_sink.c index ac59a58b..3699f152 100644 --- a/Drivers/BSP/UI/ui_net_sink.c +++ b/Drivers/BSP/UI/ui_net_sink.c @@ -545,8 +545,10 @@ void ui_net_sink_poll(void) { return; } - /* 有客户端时非阻塞收帧(I-1 门控 + I-2 兜底): - * 无数据可读 → 多数轮次直接返回,省掉一次 net_recv 往返(该往返内部 + /* 有客户端时非阻塞收帧(I-1 门控 + I-2 兜底;N34 阶段 1.5:DONTWAIT 转正 + * 为具名 API net_recv_nb——uiTask 在 IWDG required_mask 内且有 10ms UI 推流 + * 周期职责,禁止无限阻塞 recv,非阻塞轮询是义务): + * 无数据可读 → 多数轮次直接返回,省掉一次 recv 往返(该往返内部 * osMessageQueuePut(osWaitForever) + xTaskNotifyWait(portMAX_DELAY),无上界); * 但每 RM_LIVENESS_DIV 轮无条件收一次,防 RECV_OK 中断丢失把数据永久藏住 */ if (net_can_read(g_client) == 0) { @@ -556,7 +558,7 @@ void ui_net_sink_poll(void) { } else { s_liveness_ctr = 0U; } - n = net_recv(g_client, g_rx, (int)sizeof(g_rx), NET_MSG_DONTWAIT); + n = net_recv_nb(g_client, g_rx, (int)sizeof(g_rx)); if (n > 0) { for (int i = 0; i < n; i++) { rm_parser_feed((uint8_t)g_rx[i]); diff --git a/Drivers/BSP/UI/ui_net_sink.h b/Drivers/BSP/UI/ui_net_sink.h index f5ca105d..bc2b1536 100644 --- a/Drivers/BSP/UI/ui_net_sink.h +++ b/Drivers/BSP/UI/ui_net_sink.h @@ -45,8 +45,10 @@ int ui_net_sink_start(uint16_t port); * 返回值:无 * 限定条件:ui_net_sink_start() 已成功调用 * 函数说明:1. 无客户端时先门控(net_listener_armed,纯 RAM 读)再尝试接受连接 - * 2. 有客户端时先门控(net_can_read)再用 DONTWAIT 收帧并解析 + * 2. 有客户端时先门控(net_can_read)再用 net_recv_nb 非阻塞收帧并解析 * (KEEPLIVE/INIT/KEY/LCDMEM);门控判为无数据时仍每 32 轮兜底收一次 + * (N34 阶段 1.5:DONTWAIT 转正为具名 API;uiTask 受 IWDG 监控, + * 非阻塞是契约义务——见 net_socket.h net_recv 文档契约) * 3. 对端关闭或异常时复位客户端,等待下次重监听 * ⚠ **不是"零成本非阻塞"**:门控只消除无谓往返;真有活时仍是 * "入队 + 等 netTask 回执"(osMessageQueuePut(osWaitForever) + diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 3ad954e9..0ecfa972 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 228,172 B (222.82 KB) | 1,048,576 B (1024 KB) | 21.8% | -| RW_IRAM1 | 97,864 B (95.57 KB) | 131,072 B (128 KB) | 74.7% | +| ER_IROM1 | 228,372 B (223.02 KB) | 1,048,576 B (1024 KB) | 21.8% | +| RW_IRAM1 | 97,896 B (95.60 KB) | 131,072 B (128 KB) | 74.7% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **229,276 B (223.9 KB)** | 1,048,576 B (1024 KB) | **21.9%** | +| **Flash (ROM 合计)** | **229,476 B (224.1 KB)** | 1,048,576 B (1024 KB) | **21.9%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index f3a05d8b..1056b566 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,14 +15,14 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 228,172 B | 1,048,576 B | 21.8% | 参考 | +| ER_IROM1 (Flash) | 228,372 B | 1,048,576 B | 21.8% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 97,864 B | 131,072 B | 74.7% | PASS | +| RW_IRAM1 | 97,896 B | 131,072 B | 74.7% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| -| 网络 | 13,410 B | 12 KB | ⚠ 超 1.1 KB | +| 网络 | 13,442 B | 12 KB | ⚠ 超 1.1 KB | | 存储 | 7,056 B | 12 KB | OK | | UI | 54,988 B | 16 KB | ⚠ 超 37.7 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | diff --git a/docs/子系统设计/网络Socket监听生命周期设计说明.md b/docs/子系统设计/网络Socket监听生命周期设计说明.md index bfc402ba..00850856 100644 --- a/docs/子系统设计/网络Socket监听生命周期设计说明.md +++ b/docs/子系统设计/网络Socket监听生命周期设计说明.md @@ -116,6 +116,14 @@ B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket | 关闭姿势必须正确 | Trap 14/17:陈旧 DISCONNECT 锁存秒断下一条连接 | `net_close_locked` 既有「close → 等 `CLOSED` → 排空本 Socket 中断」,是 bit3=1 下唯一正确的关闭序列 | | 异常释放须先排空残渣再关 | Trap 23:~2KB RX 残渣被下条连接当自己的数据读走 | `net_relisten_locked` 隔离期(400 ms)持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED` | +> **命名注(N34 阶段 1.5,2026-10-13)**:上表与下文所说的「DONTWAIT」机制本体不变 +> (`NET_MSG_DONTWAIT` 仍是 `net_recv_locked` 的非阻塞路径与 EOF 判据载体),但对外 +> 入口已主流化:具名 API `net_recv_nb()`(单次非阻塞)+ `net_setblocking(fd, on)` +> (模式挂控制块 `blocking` 属性);`net_recv()` 默认改为**无限阻塞**(30s 默认预算 +> 删除),受 IWDG 监控任务的有界等待契约见 `docs/网络/BSD_Socket_API_使用指南.md` +> §10.1 与 `docs/网络/N34-自愈审计/实施方案.md` §3.5。本文所有「DONTWAIT 判据 / +> 返 WOULDBLOCK」表述按新命名逐字仍成立,不逐处改写。 + **⑥ 本次(P0)已落地的口径修正**:`net_socket.c:3627` / `:3641` 原注释写「DISCONNECT 后 芯片自动关闭 Socket(FUN_PARA 默认)」——与实际 `0x08` **相反**。这类注释正是 U12 这类 「跳过显式 close」缺陷的观念源头,已改正并留痕。 diff --git a/docs/测试说明/模拟器差距清单.md b/docs/测试说明/模拟器差距清单.md index 7bb8905d..f7f4266d 100644 --- a/docs/测试说明/模拟器差距清单.md +++ b/docs/测试说明/模拟器差距清单.md @@ -42,10 +42,10 @@ |---|---|---|---|---|---| | N-1 | **无 pending 机制** | `net_accept`/`net_send` 直接失败或阻塞于宿主 socket;无"挂起队列"概念 | 异步 TCP 状态机,`accept`/`send` 可处于 pending,由 `net_poll` 推进 | **E-06**(accept 轮询计时失真)、**E-20**(lftpd 控制通道 errno 分类)、**E-21**(timeSync errno 分级)等在模拟器上无法复现 | `@test` + FTP 套件;N-4 二期专门建模 | | N-2 | **代次号恒为 0** | `net_get_generation()` 恒返 0(无"网络重建"概念) | 每次 `net_stack_bring_up` / 重配置递增,各 TCP 服务据此重建监听 | "网络重建后服务是否重建监听" 的逻辑**在模拟器上恒走同一分支** | `@test` 网络重配置用例(UI 改 IP 免重启生效) | -| N-3 | **`net_process_messages` / `net_recv_to` 等缺失** | 未实现(MCU 专属:CH395F 消息队列驱动、带超时接收) | 由 `netMsgQueue` 串行化 SPI 事务、支持毫秒级超时接收 | 依赖"带超时接收"与"消息泵"的上层逻辑在模拟器上**不可编译/不可运行** | 板端 CH395F / NET 套件 | +| N-3 | ~~`net_recv_to` 缺失~~(**2026-10-13 N34 阶段 1.5 已消除**:sim 以 `GetTickCount64` + `Sleep(1)` 轮询兑现预算,含 -2 返回码与 `timeout_ms=0` 拒 INVAL);`net_process_messages` 仍缺失 | `net_recv_to` 已实现(包装层模拟);消息泵未实现(MCU 专属:CH395F 消息队列驱动) | 由 `netMsgQueue` 串行化 SPI 事务、支持毫秒级超时接收 | 依赖"消息泵"的上层逻辑在模拟器上**不可编译/不可运行**;带超时接收的**外部语义**已可双端共测,但预算兑现精度为 Sleep(1) 粒度 | 板端 CH395F / NET 套件 | | N-4 | **Socket 失效语义不同** | 以 `in_use` 判活(`net_socket_sim.c` 设计要点 4) | 监听 Socket 随客户端一同失效,由 net 层 `auto_relisten`/pump 自动重建(N34 阶段1 后无手动 `net_relisten`) | "客户端断线 → 监听失效 → 重连" 的黏滞状态在模拟器上不出现 | `check_remo_sink.py` 覆盖重连;真机 `@test` | | N-5 | **单连接 backlog** | `NET_SIM_BACKLOG = 1` | CH395F 多 socket 并发 | 多客户端并发场景(FTP + 推流 + timeSync 同时)不可在模拟器验证 | 板端 NET 套件 + 台架并发场景 | -| N-6 | **非阻塞语义模拟** | 创建即 `FIONBIO`,使 `DONTWAIT` 语义与 MCU 对齐(设计要点 2) | 中断驱动 + 状态机,天然非阻塞 | ——(此项是**对齐**而非差距,登记以说明"为何模拟器能测非阻塞路径") | —— | +| N-6 | **非阻塞语义模拟** | 创建即 `FIONBIO`(Winsock 句柄恒非阻塞,UI 主循环安全底线);**阻塞/非阻塞按控制块 `blocking` 属性在包装层模拟**(N34 阶段 1.5:`net_recv` 无限轮询等待 / `net_recv_nb` / `NET_MSG_DONTWAIT` 立即 WOULDBLOCK,`net_setblocking` 双端同名) | 中断驱动 + 状态机 + L3 挂起 recv(等待睡任务通知,netTask 零占用) | 外部语义**对齐**(登记以说明"为何模拟器能测非阻塞路径与显式预算路径");内部挂起模型(N-1)仍不建模 | —— | ### 2.2 日志与配置 @@ -109,4 +109,5 @@ | 日期 | 变更 | |---|---| +| 2026-10-13 | **N34 阶段 1.5 recv 语义主流化**:`net_socket_sim.c` 镜像 `net_recv_nb` / `net_recv_to` / `net_setblocking`,阻塞语义改按控制块 `blocking` 属性在包装层模拟(Winsock 句柄仍恒 FIONBIO)——N-3 的 `net_recv_to` 缺失部分**已消除**(划线留档),N-6 改写为"模式属性 + 包装层模拟";内部挂起模型(N-1)不变 | | 2026-10-02 | 初版(S13):收录 6 条网络差距(N-1~N-6)、4 条日志/配置差距(L-1~L-4)、7 条硬件专属差距(H-1~H-7)、4 条"有意对齐"项。证据来源:`net_socket_sim.c` 文件头设计要点 + 双端导出函数逐一对比(MCU 有而 sim 缺 9 个)+ 各子系统文档既有记载汇总。§3 给出 6 条"必须回真机"决策规则 | diff --git a/docs/测试说明/链路延迟测试说明.md b/docs/测试说明/链路延迟测试说明.md index e7974a17..d22c0403 100644 --- a/docs/测试说明/链路延迟测试说明.md +++ b/docs/测试说明/链路延迟测试说明.md @@ -91,7 +91,7 @@ sequenceDiagram | TCP 重传 | **8 次 × 200ms = 1.6s 窗口**(固定周期,非 RTT 自适应;P0-a 2026-09-22 实装,原 8×500ms=4s) | `NET_RETRANS_COUNT/PERIOD_MS`(`net_config.h`) | | SPI2 速率 | APB1 42MHz ÷ 8 ≈ **5.25 Mbit/s** → 1KB ≈ **1.6ms**(分频 8 为 PCB 约束,勿"优化") | `Src/spi.c` | | 发送等待超时 | 1s(`NET_SEND_TIMEOUT_MS`) | `net_config.h` | -| 接收超时 | 30s(`NET_RECV_TIMEOUT_MS`) | `net_config.h` | +| 接收超时 | **无默认预算**(N34 阶段 1.5:`net_recv` 无限阻塞,超时归调用方——`net_select` / `net_recv_to`;原 30s `NET_RECV_TIMEOUT_MS` 已删) | `net_config.h`(宏已移除) | | 调试串口 | USART1 115200,**逐字节阻塞发送**(`fputc` = `HAL_UART_Transmit` 单字节) | `Src/usart.c` | | LVGL 刷新周期 / 色深 | 20ms / 1bpp(`RENDER_MODE_FULL`) | `lv_conf.h`、`ui_port.c` | diff --git a/docs/网络/BSD_Socket_API_使用指南.md b/docs/网络/BSD_Socket_API_使用指南.md index 95e7dd84..400d0414 100644 --- a/docs/网络/BSD_Socket_API_使用指南.md +++ b/docs/网络/BSD_Socket_API_使用指南.md @@ -41,9 +41,11 @@ | 特性 | Linux Socket | 本实现 | |------|--------------|--------| | Socket 数量 | 无限制 | 最多 8 个 | -| 非阻塞模式 | 支持 | 通过 `net_poll()` 实现 | +| 阻塞默认 | 默认阻塞(无隐藏超时) | **同**(N34 阶段 1.5:`net_recv()` 无限阻塞,超时归调用方——`net_select` / `net_recv_to`) | +| 非阻塞模式 | `O_NONBLOCK` / `MSG_DONTWAIT` | `net_setblocking(fd, 0)`(模式属性)/ `net_recv_nb()` / `NET_MSG_DONTWAIT`(单次) | | 错误码 | errno | `net_get_errno()` | | 头文件 | `` | `"net_socket.h"` | +| 无限阻塞约束 | 无 | ⚠ IWDG `required_mask` 内任务**禁止**(嵌入式特有契约,§10.1) | --- @@ -79,7 +81,7 @@ PC 模拟器(`simulator/`,CMake + SDL2)与 MCU 端共用同一份上层网 | 底层后端 | CH395F(SPI2) | Winsock2(链接 `ws2_32`) | | `net_init()` | 芯片初始化 / 复位 / 配置 IP·MAC / 分配 Socket 缓冲 | 仅确保 `WSAStartup`(幂等);ip/mask/gw/mac 四参数全部忽略 | | fd 模型 | 控制块索引 0~7 | 相同(并行数组 `s_ws[]` 存 Winsock 句柄,不侵入共享的 `net_sock_t`) | -| 并发 / 阻塞 | 消息队列 + `netTask` 串行化 | 单线程直调;所有 Socket 创建即非阻塞(FIONBIO),主循环直接轮询 | +| 并发 / 阻塞 | 消息队列 + `netTask` 串行化 | 单线程直调;Winsock 句柄创建即非阻塞(FIONBIO),阻塞语义按控制块 `blocking` 属性在包装层模拟(`net_recv` 轮询等待 / `net_recv_nb` 立即返回,N34 阶段 1.5 与 MCU 端对齐) | | `net_poll()` | CH395F 中断轮询 + 状态机更新 | 空操作(返回 0,非阻塞 I/O 直接反映事件) | | `net_accept()` | 单连接:监听 Socket 自身即数据通道(返回监听 Socket 自身) | Winsock 语义:返回独立于监听 Socket 的新 fd(监听 Socket 保持 `LISTENING`) | | 事件回调 | `net_poll` 检测后主动触发 | 仅存储,不主动触发(上层自行非阻塞轮询感知) | @@ -174,7 +176,8 @@ int net_close_sock(net_sock_t *p_sock); - **消息队列由 CubeMX 创建**:在 `MX_FREERTOS_Init()`(`osKernelInitialize()` 之后)中调用 `osMessageQueueNew(8, sizeof(net_msg_t), NULL)` 创建 - **队列句柄全局可见**:通过 `extern osMessageQueueId_t netMsgQueueHandle` 引用 - **`netTask` 不可向自身发消息**:`netTask` 中直接调 `net_send()` 会死锁(等待自己处理消息),必须在 `netTask` 中使用 `net_send_sock()` 等内部版本 -- **阻塞 recv 不再独占 netTask(L3 pending 化,2026-09-22)**:无数据时公共 `net_recv()` / `net_recv_to()` **登记挂起请求后返回**(每 fd 至多一个,禁止覆盖——覆盖即丢失前调用方通知 = 永久挂死);完成由 `net_pending_tick()` 按主循环节拍检查(①数据 ②到点 ③断连,看门狗 1s)并经任务通知唤醒调用方——等待期间调用方在自己上下文睡通知,netTask 零占用(消除 E-02 头阻塞)。副作用:`buf` 指针须保持有效至返回;close / 网络重建 / 陈旧重同步会**显式取消并通知**(`NET_ERR_CONNRESET` 等),调用方永不挂死 +- **阻塞 recv 不再独占 netTask(L3 pending 化,2026-09-22)**:无数据时公共 `net_recv()` / `net_recv_to()` **登记挂起请求后返回**(每 fd 至多一个,禁止覆盖——覆盖即丢失前调用方通知 = 永久挂死);完成由 `net_pending_tick()` 按主循环节拍检查(①数据 ②到点[仅显式预算] ③断连)并经任务通知唤醒调用方——等待期间调用方在自己上下文睡通知,netTask 零占用(消除 E-02 头阻塞)。副作用:`buf` 指针须保持有效至返回;close / 网络重建 / 陈旧重同步会**显式取消并通知**(`NET_ERR_CONNRESET` 等),调用方永不挂死 +- **recv 语义主流化(N34 阶段 1.5,2026-10-13)**:对齐 POSIX/Python——**模式归使用者、超时归调用方、API 不强加默认预算**。`net_recv()` 默认**无限阻塞**(`NET_RECV_TIMEOUT_MS` 30s 默认已删),唤醒源 = 数据 / EOF / close / 网络重建(CONNRESET),会话死亡由对账① ~400ms 判死兜底;非阻塞具名入口 `net_recv_nb()`(原 `NET_MSG_DONTWAIT` 路径转正),模式属性 `net_setblocking(fd, on)` 挂 socket 控制块;超时首选 `net_select(...)`,`net_recv_to(...)` 保留为**显式预算**便捷入口。内部看门狗降级为 **tripwire**:挂起超 60s 只 `[ERR]` 留痕一条(完成逻辑缺位探针),不再伪造 TIMEDOUT 返回值。⚠ **文档契约(嵌入式特有)**:IWDG `required_mask` 内任务**禁止无限阻塞 recv**——必须 `net_select` 超时或 `net_recv_to` 显式预算("超时归使用者"对受监控任务是义务不是选项) - **队列阻塞语义**:只有 `osMessageQueuePut` 失败(8 长度固定队列,极端情形)时公共 API 阻塞;recv 挂起等待**不计入**队列占用(完成前队列中无消息滞留) - **哪些操作要求 netTask 已运行**:`net_listen()` / `net_accept()` / `net_connect()` / `net_send()` / `net_recv()` / `net_close()` 全经消息队列 → 只能在就绪屏障后的任务调用;`net_socket()` / `net_bind()` 为直接调用,但同样建议屏障后;**`net_listener_armed()` / `net_can_read()` 是纯 RAM 读控制块**(§10.1),不入队、不等回执,**任意任务可安全周期调用**(用途 = 门控,把"无谓的 RPC"降为 0) @@ -919,7 +922,7 @@ int net_send(int sockfd, const void *buf, int len, int flags); | `flags` | 通常为 0 | | 返回值 | 实际发送字节数,失败返回 -1 | -#### net_recv - 接收数据 +#### net_recv - 接收数据(默认无限阻塞) ```c int net_recv(int sockfd, void *buf, int len, int flags); @@ -930,12 +933,43 @@ int net_recv(int sockfd, void *buf, int len, int flags); | `sockfd` | socket 描述符 | | `buf` | 接收缓冲区(须保持有效至调用返回) | | `len` | 缓冲区大小 | -| `flags` | 0=阻塞,`NET_MSG_DONTWAIT`=非阻塞 | -| 返回值 | 实际接收字节数 (>0);`0`=对端关闭(net 层随即自动重监听);`-1`=错误(非阻塞无数据=`NET_ERR_WOULDBLOCK`;阻塞预算内无数据=`NET_ERR_TIMEDOUT`,默认 30s `NET_RECV_TIMEOUT_MS`;close/网络重建/陈旧重同步取消=`NET_ERR_CONNRESET`) | +| `flags` | 0=按 socket 阻塞属性等待,`NET_MSG_DONTWAIT`=单次强制非阻塞 | +| 返回值 | 实际接收字节数 (>0);`0`=对端关闭(net 层随即自动重监听);`-1`=错误(非阻塞无数据=`NET_ERR_WOULDBLOCK`;close/网络重建/陈旧重同步取消=`NET_ERR_CONNRESET`;**阻塞形态无"预算内无数据"一档——默认无限等待,超时是调用方策略**) | -> **L3 pending 化(2026-09-22)**:无数据时阻塞接收**不占 netTask**——登记挂起请求(每 fd 至多一个,禁止覆盖:覆盖会丢失前调用方通知 = 永久挂死)后**立即返回**;完成按主循环节拍检查(数据/到点/断连,看门狗 1s)并经任务通知唤醒调用方。等待期间调用方在自己上下文睡通知,netTask 全程零占用(消除 E-02 头阻塞)。 +> **语义主流化(N34 阶段 1.5,2026-10-13)**:对齐 POSIX/Python——`net_recv()` 默认**无限阻塞**(旧 `NET_RECV_TIMEOUT_MS` 30s 默认预算已删,API 不再强加隐藏值)。唤醒源 = 数据 / EOF / close / 网络重建(`NET_ERR_CONNRESET`);会话死亡由对账①在 ~400ms 内判死兜底(§3.6),不存在"对端消失后永久挂死"窗口。需要"等但不无限等":首选 `net_select(r,w,e,timeout_ms)`(§7.1),或 `net_recv_to()` 显式预算。 +> +> **L3 pending 化(2026-09-22)**:无数据时阻塞接收**不占 netTask**——登记挂起请求(每 fd 至多一个,禁止覆盖:覆盖会丢失前调用方通知 = 永久挂死)后**立即返回**;完成按主循环节拍检查(①数据 ②到点[仅显式预算] ③断连)并经任务通知唤醒调用方。等待期间调用方在自己上下文睡通知,netTask 全程零占用(消除 E-02 头阻塞)。内部 tripwire(原看门狗降级):挂起超 60s 只 `[ERR]` 留痕一条("完成逻辑缺位"探针),**绝不伪造 TIMEDOUT 返回值**。 +> +> ⚠ **文档契约(嵌入式特有,Python 没有)**:IWDG `required_mask` 内任务(当前产品掩码 `0x1F7`:default/net/ftp/ui/timeSync/adc/iec104/protect)**禁止无限阻塞 recv**——必须 `net_select` 超时或 `net_recv_to` 显式预算并周期喂心跳,否则"对端连上不发数据"这种完全正常的场景 = IWDG 复位。 -#### net_recv_to - 接收数据(调用方指定超时,分片阻塞版) +#### net_recv_nb - 接收数据(非阻塞具名入口) + +```c +int net_recv_nb(int sockfd, void *buf, int len); +``` + +| 参数 | 说明 | +|------|------| +| `sockfd` / `buf` / `len` | 同 `net_recv` | +| 返回值 | >0 字节数;`0`=对端关闭;`-1`=无数据(`NET_ERR_WOULDBLOCK`)或真错误(`net_get_errno` 可查) | + +原 `NET_MSG_DONTWAIT` 路径转正(N34 阶段 1.5):等价 `net_recv(sockfd, buf, len, NET_MSG_DONTWAIT)`,单次强制非阻塞、不改变 socket 的 blocking 属性。受 IWDG 监控 / 有周期职责的任务的推荐默认入口(生产用户:`ui_net_sink` :7003、`iec104_serv` :2404)。 + +#### net_setblocking - 设置阻塞模式属性 + +```c +int net_setblocking(int sockfd, int on); +``` + +| 参数 | 说明 | +|------|------| +| `sockfd` | socket 描述符 | +| `on` | 1=阻塞(`net_socket()` 默认);0=非阻塞 | +| 返回值 | 0 成功;-1 失败(描述符无效/未占用) | + +模式挂 socket 控制块属性(对齐 BSD `O_NONBLOCK` / Python `setblocking`):非阻塞模式下 `net_recv()` 无数据立即返回 `NET_ERR_WOULDBLOCK`(EOF 判定不变:CLOSED/session_dead 无数据仍返回 0 并触发自动重监听)。与超时预算**正交**——模式是粘滞属性,单次预算用 `net_recv_to` / `net_select`。直写控制块单字节(先例 `net_set_event_cb`),任意上下文可调。 + +#### net_recv_to - 接收数据(调用方指定超时,显式预算分片版) ```c int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); @@ -944,10 +978,10 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); | 参数 | 说明 | |------|------| | `sockfd` / `buf` / `len` | 同 `net_recv` | -| `timeout_ms` | 本片等待预算(ms,**必须 >0**;0 会被解释为"默认 30s",恰是本接口要规避的长阻塞,直接拒 `NET_ERR_INVAL`) | +| `timeout_ms` | 本片等待预算(ms,**必须 >0**;0 = `net_recv` 的无限等待语义,恰与本接口"有界"存在理由相反,直接拒 `NET_ERR_INVAL`——N34 阶段 1.5 起本接口是**显式预算**入口,不再有"默认 30s"隐藏值) | | 返回值 | >0=字节数(允许部分读,TCP 分段);`0`=对端关闭(排空残留后);`-1`=真错误(`net_get_errno` 可查);**-2=本片预算内无数据**(**不是 errno**,不得依赖 errno 判超时——`s_net_errno` 是跨任务共享全局量) | -调用方累计自身总预算:见到 -2 发起下一片,直到收齐 / EOF / 总预算耗尽。生产用途:lftpd 数据行读、timeSync 帧读(E-02 头阻塞修复,完整例 §11.2)。 +调用方累计自身总预算:见到 -2 发起下一片,直到收齐 / EOF / 总预算耗尽。生产用途:lftpd 数据行读、timeSync 帧读(E-02 头阻塞修复,完整例 §11.2);受 IWDG 监控任务的长等待必须走本接口(或 `net_select`)+ 每片喂心跳。 #### net_sendto - 发送 UDP 数据 @@ -1007,11 +1041,11 @@ void net_process_messages(void); 处理其他任务投递的请求(send/recv/connect/accept/close)。每次调用先跑 4 个**非阻塞** tick,再取队列消息(0 超时出队): 1. **D10 重监听重试**(`net_listener_retry_tick`):断连后监听器恢复的到点重试(30ms 间隔 / 2s 时限,§3.6) -2. **L3 挂起 recv 完成检查**(`net_pending_tick`):按 ①数据 ②到点 ③断连 检查每个挂起请求并通知调用方(看门狗 1s) +2. **L3 挂起 recv 完成检查**(`net_pending_tick`):按 ①数据 ②到点(仅显式预算) ③断连 检查每个挂起请求并通知调用方;④ tripwire:挂起超 60s 只 `[ERR]` 留痕一次,不强制了结(N34 阶段 1.5) 3. **Trap 22 芯片/本层对账**(`net_reconcile_tick`):200ms 周期修正**三类**失同步(①②⑤,§3.6) 4. **N-32 监听器 pump**(`net_listener_pump`):100ms 周期,**唯一重建入口**(③④;本层 CLOSED 而芯片在听时只重同步,不发芯片命令,§3.6) -> `net_msg_t.timeout_ms`:L3 分片预算(`net_recv_to()`),0=默认 `NET_RECV_TIMEOUT_MS`(30s)。消息类型仅 6 种(send/recv/close/connect/listen/accept)——自愈类消息 `NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 已随 API 退役删除(N34 阶段1)。 +> `net_msg_t.timeout_ms`:L3 显式预算(`net_recv_to()`),0=**无限等待**(N34 阶段 1.5 删除 `NET_RECV_TIMEOUT_MS` 30s 默认预算——超时归调用方)。消息类型仅 6 种(send/recv/close/connect/listen/accept)——自愈类消息 `NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 已随 API 退役删除(N34 阶段1)。 #### net_send_sock - 发送数据(内部版本) @@ -1155,9 +1189,9 @@ void tcp_client_reconnect(void) net_send(sock, "heartbeat", 9, 0); } - /* 检查接收 */ + /* 检查接收(N34 阶段 1.5:非阻塞具名入口 net_recv_nb) */ char buf[128]; - int len = net_recv(sock, buf, sizeof(buf) - 1, NET_MSG_DONTWAIT); + int len = net_recv_nb(sock, buf, sizeof(buf) - 1); if (len > 0) { buf[len] = '\0'; printf("收到: %s\r\n", buf); @@ -1246,18 +1280,18 @@ static int read_time_frame(int conn_sock, uint8_t buf[FRAME_LEN]) { ### 12.4 如何处理连接断开? -**方式一:阻塞接收** +**方式一:阻塞接收(无限等待——⚠ 仅限不受 IWDG 监控且无周期职责的任务,§10.1 文档契约)** ```c int len = net_recv(sock, buf, sizeof(buf), 0); if (len <= 0) { - /* 连接断开或错误 */ + /* 连接断开或错误(EOF / CONNRESET / 真错误,net_get_errno 可查) */ net_close(sock); } ``` -**方式二:非阻塞接收** +**方式二:非阻塞接收(`net_recv_nb`,N34 阶段 1.5 转正)** ```c -int len = net_recv(sock, buf, sizeof(buf), NET_MSG_DONTWAIT); +int len = net_recv_nb(sock, buf, sizeof(buf)); if (len < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) { /* 没有数据 */ } else if (len <= 0) { @@ -1407,6 +1441,7 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po | 日期 | 变更 | |------|------| +| 2026-10-13 | **N34 阶段 1.5 recv 语义主流化**(规格 `docs/网络/N34-自愈审计/实施方案.md` §3.5):`net_recv()` 默认**无限阻塞**(删 `NET_RECV_TIMEOUT_MS` 30s 默认预算,唤醒源=数据/EOF/close/网络重建 CONNRESET);新增 `net_recv_nb()`(`NET_MSG_DONTWAIT` 路径转正)与 `net_setblocking(fd, on)`(模式挂 socket 控制块属性,`net_sock_t.blocking`,默认阻塞);`net_recv_to()` 保留为**显式预算**便捷入口(`timeout_ms=0` 语义从"默认 30s"改为"无限"故仍拒 INVAL);内部 recv 看门狗降级为 **tripwire**(挂起超 60s 只 `[ERR]` 留痕一次,不再"到点 +1s 强制了结"伪造 TIMEDOUT);⚠ 新增**文档契约**:IWDG `required_mask` 内任务禁止无限阻塞 recv(必须 `net_select` 超时或 `net_recv_to` 显式预算);`ui_net_sink` / `iec104_serv` 迁移 `net_recv_nb`,lftpd / timeSync 维持显式预算;模拟器 `net_socket_sim.c` 镜像三 API(blocking 属性包装层模拟) | | 2026-10-13 | **N34 阶段1 API 面收敛**(审计见 `docs/网络/N34-自愈审计/README.md`):删除手动自愈 API `net_relisten()` / `net_resync_listen()`(生产零调用;后者语义并入对账**⑤**——本层 ESTABLISHED 而芯片 0x05/0x01 静默回收 → RESYNC 只修本层,§3.6 对账由两类扩为三类①②⑤;前者与 pump/D10 重叠且对健康监听器是 footgun)与 `net_can_accept()`(只认 ESTABLISHED 的旧门控挡死丢 CONNECT 探针,监听门控统一 `net_listener_armed()`,§10.1 门控节改写);`NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 消息类型删除;timeSync 会话收尾不再 `net_close`(同 fd 续 accept,监听跨会话常驻);lftpd 应用层阻塞重试退役(就绪屏障已消除 Trap 09 启动形态) | | 2026-10-12 | **N-32 监听生命周期模块化 + 单一所有者**:监听生命周期抽为 `net_listener.c`(§2.2 文件表新增该行,真机专属);主循环自愈 tick 由 3 个增至 **4 个**(新增 `net_listener_pump`,100ms,**唯一重建入口**);`net_reconcile_tick` 对账收窄为 ①②,③④ 移交 pump;`handle_disconnect_event` / `handle_timeout_event` 改为 `net_listener_settle(GRACEFUL 100ms / QUARANTINE 400ms)` **只置事实**、不再自行重建;§3.3 / §3.6(四层自愈 → **五层**)/ §12.10 同步。权威设计见 `docs/子系统设计/网络Socket监听生命周期设计说明.md` §10 | | 2026-10-10 | 新增 **`net_can_accept()` / `net_can_read()` 就绪门控 API**(§10.1):纯 RAM 读控制块、**不走消息队列、不等 netTask 回执**,供实时性敏感任务在发 RPC 前判断"有没有活"(动因 = `ui_net_sink_poll()` 每 10 ms 一次 `net_accept` 把 uiTask 拖成最坏 ≈50 ms;原拟登记 N-19 / C7,当日结案不入登记簿;设计与实测见 `docs/子系统设计/看门狗设计说明.md` §11(原分析文档已并入该节));§3.5 补"netTask 运行要求"例外条;⚠ 记录两条使用约束——**不可作为唯一判据**(`readable` 只由 RECV_OK 中断置位,须保留低频兜底 recv)与**模拟器后端恒返 1**(`net_socket_sim.c`,Winsock 无 RPC 成本) | diff --git a/docs/网络/BSD_Socket_API_函数调用关系.md b/docs/网络/BSD_Socket_API_函数调用关系.md index 2c92f01c..24c53a6b 100644 --- a/docs/网络/BSD_Socket_API_函数调用关系.md +++ b/docs/网络/BSD_Socket_API_函数调用关系.md @@ -48,7 +48,7 @@ flowchart TB NPM["netProcessMessages
先跑 4 个 tick,再排空队列分派"] NP["netPoll
INT# GPIO 轮询 → 全局中断 → 各 Socket SINT → 每轮一批"] T1["① netListenerRetryTick
D10 重监听重试:30ms / 2s 时限"] - T2["② netPendingTick
L3 挂起 recv:①数据 ②到点 ③断连 ④看门狗 1s"] + T2["② netPendingTick
L3 挂起 recv:①数据 ②到点(仅显式预算) ③断连 ④tripwire 60s 只留痕"] T3["③ netReconcileTick
Trap 22 对账:200ms / 去抖 / 冷却(①②⑤)"] T4["④ netListenerPump
N-32 唯一重建入口:100ms / 去抖 / 冷却"] DISP["队列分派 switch
SEND · RECV · CLOSE
CONNECT · LISTEN · ACCEPT"] @@ -129,8 +129,9 @@ flowchart TB | `net_accept` | `NET_MSG_ACCEPT` | `net_accept_locked` | 无(先 `net_poll` 泵一次 + 状态判定) | 监听 fd 自身(单连接)/ -1(WOULDBLOCK) | 单连接:监听 Socket 在 CONNECT 后**自身即数据通道**,返回的就是它;CLOSED + auto_relisten 时触发**非阻塞** `net_listener_try`;**accept 路径不做自愈**——单次状态读会误判"刚 listen 未生效"致重建风暴(2026-09-23 实测),统一交给 `net_listener_pump`(N-32 单一重建入口) | | `net_connect` | `NET_MSG_CONNECT` | `net_connect_locked` | `set_proto_type` + `open_socket`(3×500ms 重试)+ 清残留中断 + `set_sour_port` / `set_des_ip`(大端数组)/ `set_des_port` + `tcp_connect` | 0 = 请求已发出(**异步**)/ -1 | open **先于**寄存器写入(open 会复位寄存器);open 后清残留 DISCONNECT,防新连接秒断(Trap 14/17);目的 IP 字节序写反 = CONNECT TIMEOUT(Trap 16);返回 0 ≠ 已建立——调用方以 `net_sock_state` 轮询等 ESTABLISHED | | `net_send` | `NET_MSG_SEND` | `net_send_locked` | `ch395f_write_send_buf`(分块)+ 等 SENBUF_FREE(`net_poll` 轮询) | 实际字节数 / -1 | 分块 ≤ `NET_SEND_CHUNK_MAX` = 1KB(**硬件发送 FIFO 硬上限**,Trap 20:写穿 FIFO = 数据错位);`send_ready` 门控用 `net_poll` 轮询而非 osDelay;短写**必须**告警——静默截断会让对端永久等待剩余字节(2026-09-21 实证:UI 推流应答截断 → 客户端失联) | -| `net_recv` | `NET_MSG_RECV`(timeout=0→默认 30s) | `net_recv_locked` | `net_poll` + `get_recv_len` / `read_recv_buf` | >0 / 0(EOF) / -1(errno = WOULDBLOCK / TIMEDOUT / CONNRESET) | 快路径:数据已在 → 立即读出返回(允许部分读);无数据 + 阻塞 → **登记挂起后立即返回**(L3,netTask 零占用);DONTWAIT + CLOSED → EOF + 触发重监听 | -| `net_recv_to` | `NET_MSG_RECV`(timeout>0) | `net_recv_locked` | 同上 | >0 / 0 / -1 / **-2(本片超时无数据)** | E-02 头阻塞修复配套:调用方把长等待切成有界片(如 5ms/剩余预算),**总超时语义不变、单次占用有界**;`timeout_ms=0` 直接拒(会被解释成默认 30s,恰是长阻塞);超时**只凭 -2 判定、不得依赖 errno**(`s_net_errno` 是跨任务共享全局量,随时被别的任务覆盖) | +| `net_recv` | `NET_MSG_RECV`(timeout=0→**无限等待**,N34 阶段 1.5 删 30s 默认) | `net_recv_locked` | `net_poll` + `get_recv_len` / `read_recv_buf` | >0 / 0(EOF) / -1(errno = WOULDBLOCK / CONNRESET;阻塞形态无 TIMEDOUT 一档) | 快路径:数据已在 → 立即读出返回(允许部分读);无数据 + 阻塞(`blocking` 属性=1 且未 DONTWAIT)→ **登记挂起后立即返回**(L3,netTask 零占用),唤醒源=数据/EOF/close/网络重建;DONTWAIT / `blocking=0` + CLOSED/session_dead → EOF + 触发重监听;⚠ IWDG 受监控任务禁用无限形态(`net_socket.h` 文档契约) | +| `net_recv_nb` | `NET_MSG_RECV`(flags=DONTWAIT) | `net_recv_locked` | 同上 | >0 / 0 / -1(WOULDBLOCK 等) | 原 `NET_MSG_DONTWAIT` 路径转正的具名入口(N34 阶段 1.5);不改变 `blocking` 属性;受 IWDG 监控 / 有周期职责任务的推荐默认(ui_net_sink / iec104_serv 已迁) | +| `net_recv_to` | `NET_MSG_RECV`(timeout>0) | `net_recv_locked` | 同上 | >0 / 0 / -1 / **-2(本片超时无数据)** | E-02 头阻塞修复配套 + **显式预算**入口(N34 阶段 1.5,等价 Python settimeout,无隐藏默认值):调用方把长等待切成有界片(如剩余预算),**总超时语义不变、单次占用有界**;`timeout_ms=0` 直接拒(0 = 无限等待,与本接口"有界"存在理由相反);超时**只凭 -2 判定、不得依赖 errno**(`s_net_errno` 是跨任务共享全局量,随时被别的任务覆盖) | | `net_close` | `NET_MSG_CLOSE` | `net_close_locked` | `close_socket` + 等 CLOSED(5ms 节奏 / ≤500ms)+ 排空 Socket/全局中断 | 0 / -1 | **先**了结本 fd 挂起 recv(排空/取消 + 必通知,关不得让等待者悬挂);关后排空残留 DISCONNECT(Trap 14/17:否则下一连接被"刚建立就断开"秒断);控制块整体清零 | | ~~`net_relisten`~~ | ~~`NET_MSG_RELISTEN`~~ | **N34 阶段1 已删除**(消息类型同删) | — | — | 原"无条件芯片侧重建"与 pump/D10 完全重叠,且对健康监听器是 footgun(open→USE→close 自毁 ~1s);生产零调用。历史实现见 `net_socket.c` git 历史 | | ~~`net_resync_listen`~~ | ~~`NET_MSG_RESYNC_LISTEN`~~ | **N34 阶段1 已删除**(语义并入对账⑤) | — | — | D14 静默回收(芯片自行回监听、不发 DISCONNECT)现由 `net_reconcile_tick` 判据⑤自动认领:本层 ESTABLISHED 且芯片 0x05/0x01 → 只修本层(state→LISTENING + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv),**不发芯片命令**(纪律原样保留,run8 实证 170s 拒绝);去抖/冷却沿用对账框架 | @@ -141,7 +142,8 @@ flowchart TB |---|---|---|---| | `net_socket(domain,type)` | 分配空闲控制块(`alloc_socket` 线性扫描 `in_use=0`)→ CREATED | `alloc_socket` + `net_listener_clear` + `net_pending_clear` | 新 fd 不得继承上一次的待重试/挂起状态(复用描述符的经典坑);domain 仅 `NET_AF_INET` | | `net_bind(sockfd,addr,addrlen)` | TCP:仅记录本地端口(port=0 → 动态端口 49152 起递增)→ BOUND,**无芯片命令**;UDP:完整开 Socket → UDP_OPEN | UDP 路径:`set_proto_type(UDP)` + `set_sour_port` + `set_des_ip(0xFFFFFFFF)` + `set_des_port(0)` + `open_socket`(Socket 4~7 兜底重设缓冲) | TCP 的 bind 只是"记账",真正的芯片动作在 listen/connect;UDP 直碰 SPI(§2 三例外之一);动态端口回绕 65535→49152 | -| `net_sendto` / `net_recvfrom` | UDP 收发(直碰 SPI,不经队列) | `set_des_ip` / `set_des_port` + `write_send_buf`(分块 1KB + `net_poll` 门控);`net_poll` + `get_recv_len` + `read_recv_buf` | 同 §2 上下文约束;recv 时 UDP Server 模式前 8 字节是固定头(源 IP + 端口 + 长度),先剥头填 `src_addr` 再读数据 | +| `net_sendto` / `net_recvfrom` | UDP 收发(直碰 SPI,不经队列) | `set_des_ip` / `set_des_port` + `write_send_buf`(分块 1KB + `net_poll` 门控);`net_poll` + `get_recv_len` + `read_recv_buf` | 同 §2 上下文约束;recv 时 UDP Server 模式前 8 字节是固定头(源 IP + 端口 + 长度),先剥头填 `src_addr` 再读数据;recvfrom 阻塞语义与 net_recv 同构(N34 阶段 1.5:blocking 属性 / DONTWAIT,无默认预算;轮询自旋实现,生产零调用) | +| `net_setblocking(sockfd,on)` | 设置阻塞**模式属性**(N34 阶段 1.5,直写控制块,不经队列) | 写 `s_net_socks[fd].blocking` | 对齐 BSD `O_NONBLOCK` / Python `setblocking`:"模式归使用者、超时归调用方";单字节原子写(先例 `net_set_event_cb`),最坏一次在途 recv 用旧模式;`net_socket()` 默认置 1(close 的 memset 清零后新 fd 必须显式重置) | | `net_wait_result(kind,sockfd)` | 调用侧两段式等待:**零等待拾取** → 阻塞等回执 | `xTaskNotifyWait` ×2 | ① 零等待段兜住"入队→等待入口"间隙落地的通知(2026-09-22 台架实证丢通知);② **清除掩码必须全 1**(传 0 = 不清 → 上次结果残留被重读,run3~11 挂死/错位的真根因);③ 必须判 **pdTRUE** 而非 `v!=0`(合法结果 0 = EOF 会被误判"无通知"→ 永久挂死);④ 32 位整体作结果(32768 大字节数不截断);⑤ 本路径**禁止**加逐调用日志(曾占日志 98.6%、冲掉 [LAT] 证据行,run14 实证) | | `net_sock_state(sockfd)` | 跨任务只读状态快照(单字节读,无锁) | 无 | 描述符无效/未占用 → -1;值可能滞后 netTask 最新状态——用于**轮询**(客户端等 ESTABLISHED),不替代 accept/connect | | `net_get_sock(sockfd)` | 返回控制块指针 | 无 | netTask 上下文内用;跨任务只可读单字节 `state`(即 `net_sock_state`),不得读结构体其他成员 | @@ -157,7 +159,7 @@ flowchart TB | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| | `net_pending_register` | 登记挂起(`net_recv_locked` 的"无数据 + 阻塞"路径) | 写 `s_net_pending[fd]` | 每 fd 至多一个:**覆盖会丢失前调用方通知 = 永久挂死**;执行任务自身(netTask)发起的等待直接拒绝(自等自通知 = 死锁,netTask 内须用 DONTWAIT) | -| `net_pending_tick` | 完成检查(`net_process_messages` 每轮调用,非阻塞) | ① `get_recv_len` + `read_recv_buf` ② 到点 ③ `get_recv_len` 排空 + `net_listener_try` ④ `net_pending_complete` | 按 **①数据 ②到点 ③断连** 顺序检查(逐项对齐旧自旋语义);CONNECTING/LISTENING 等状态**保持等待**(CONNECT→ESTABLISHED 是合法迁移,不做状态推断取消,防误杀正在建链的等待);**看门狗**:到点后 1s 仍未了结 = 完成逻辑缺位 → 强制了结 + 错误日志 | +| `net_pending_tick` | 完成检查(`net_process_messages` 每轮调用,非阻塞) | ① `get_recv_len` + `read_recv_buf` ② 到点(仅显式预算) ③ `get_recv_len` 排空 + `net_listener_try` ④ `net_pending_complete` | 按 **①数据 ②到点(仅 `net_recv_to` 显式预算;无限登记无 deadline)③断连** 顺序检查(逐项对齐旧自旋语义);CONNECTING/LISTENING 等状态**保持等待**(CONNECT→ESTABLISHED 是合法迁移,不做状态推断取消,防误杀正在建链的等待);**tripwire**(N34 阶段 1.5,原看门狗降级):挂起超 60s = 完成逻辑缺位 → 只 `[ERR]` 留痕一次,**绝不强制了结、绝不伪造 TIMEDOUT**——③ 与显式 cancel 是无限 recv 的全部唤醒责任方 | | `net_pending_complete` | 所有完成/取消路径的公共出口 | `xTaskNotify(caller, result)` + 清 active | 通知值 = 结果原值(>0 字节 / 0 EOF / 负错误码,与消息分派同一回传通道);失败结果同步 `s_net_errno`(netTask 上下文一致性);无在位挂起时静默返回(防重复通知) | | `net_pending_settle_close` | close 前**了结**本 fd 挂起 | `get_recv_len` + `read_recv_buf` + `net_pending_complete` | 关闭不得丢已到达数据(芯片缓冲 close 前可读):有数据 → 按字节数通知;无数据 → 取消(CONNRESET);先于 `ch395f_close_socket` 执行 | | `net_pending_cancel` / `cancel_all` | 显式取消(**必通知**) | `net_pending_complete` | 取消路径:陈旧 resync(单 fd)/ 网络重建(全部,`net_init` 开头,早于控制块清零)——"取消不通知 = 调用方永久挂死" | diff --git a/docs/网络/Net层阻塞模型架构设计.md b/docs/网络/Net层阻塞模型架构设计.md index fd301095..96720d1d 100644 --- a/docs/网络/Net层阻塞模型架构设计.md +++ b/docs/网络/Net层阻塞模型架构设计.md @@ -44,7 +44,7 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线 |---|---| | **左栏 · 调用方任务** | `net_recv` → 判"数据已在?" → **是**走快速路径;**否**(`timeout > 0`)登记 pending 后在自己上下文睡任务通知 | | **中栏 · netTask 主循环** | ~1ms 节拍:`net_poll`(刷新 readable)→ 4 个非阻塞 tick(`net_listener_retry_tick` D10 → `net_pending_tick` 了结挂起 → `net_reconcile_tick` Trap 22 对账 → `net_listener_pump` 监听重建唯一入口)→ 排空消息队列 | -| **下带 · 五路判定** | 每轮对每个挂起 fd 按序判定:数据到达 / 到点 / EOF / 同 fd 后续操作 / 网络重建;含 1s 看门狗 | +| **下带 · 五路判定** | 每轮对每个挂起 fd 按序判定:数据到达 / 到点(仅显式预算)/ EOF / 同 fd 后续操作 / 网络重建;tripwire 60s 只留痕(N34 阶段 1.5 由 1s 看门狗降级) | | **回环** | 判定完成经 `xTaskNotify` 唤醒调用方(结果 = 字节数 / 负错误码) | **关键性质**: @@ -54,6 +54,10 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线 - 调用方**在自己的上下文睡眠**(任务通知),与 LwIP 的消费者线程模型同构; - 公开 API 与调用契约**不变**——`net_recv` / `net_recv_to` 的签名、返回值、 超时语义均与改造前一致,既有调用方、测试与模拟器零改动。 + (**N34 阶段 1.5 更新**:本条描述 L3 改造当时口径;2026-10-13 起 `net_recv` + 默认预算 30s 删除、改为无限阻塞,`net_recv_to` 成为唯一显式预算入口—— + 挂起模型与"等待不占 netTask"性质不变,变的是超时归属:见 + `docs/网络/N34-自愈审计/实施方案.md` §3.5 与 BSD 指南 §3.5。) > 图形由 `tools/arch_diagram/gen_net_pending_svg.py` 生成(改流程后重跑;见 > `docs/架构设计/图形绘制流程.md`)。 @@ -67,7 +71,7 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线 | 项 | 设计 | |---|---| | 存储 | **独立静态数组** `s_net_pending[NET_MAX_SOCKETS]`(不置于 `net_sock_t` 内) | -| 字段 | `active` / `buf` / `len` / `deadline` / `registered` / `caller` | +| 字段 | `active` / `infinite` / `tripwire_logged` / `buf` / `len` / `deadline` / `registered` / `caller`(`infinite`/`tripwire_logged` 为 N34 阶段 1.5 新增:0 预算 = 无限等待、tripwire 只留痕一次) | | 容量 | 每 fd **至多一个**挂起请求 | | 占用 | ≈24B/fd × 8 fd = **192B**(+4B 任务句柄),实测 SRAM +200B | @@ -93,7 +97,7 @@ socket 生命周期解耦,与 `net_listener_retry_tick` 的重试表同构。 | # | 条件 | 动作 | 设计要点 | |---|---|---|---| | 1 | **数据到达** | SPI 读出 + 通知(结果 = 字节数) | 由 `net_poll` 维护的 `readable` 门控,**零额外 SPI**;仅 ESTABLISHED / UDP_OPEN 态可读;spurious readable 需清除 | -| 2 | **到点** `now >= deadline` | 通知 `NET_ERR_TIMEDOUT`(对外 -2 映射保持) | 状态无关;用有符号回绕安全差值,防 tick 回绕 | +| 2 | **到点** `now >= deadline`(仅显式预算登记;无限等待登记无 deadline——N34 阶段 1.5) | 通知 `NET_ERR_TIMEDOUT`(对外 -2 映射保持) | 状态无关;用有符号回绕安全差值,防 tick 回绕;这是 `net_recv_to` 调用方策略的正常兑现,不是伪造 | | 3 | **CLOSED**(EOF/DISCONNECT) | 排空:有数据则读出;否则按 0(EOF) 完成 + `auto_relisten` + 通知 | CLOSE 先行完成挂起 = `net_close` 的 settle 钩子(排空→数据否则 CONNRESET) | | 4 | **同 fd 后续操作** | 先结算挂起,再执行新操作 | per-fd 串行——冻结面从"全网"缩到"本 fd" | | 5 | **网络重建**(`net_stack_bring_up`,芯片复位) | **取消 + 必须通知** | `net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;**取消不通知 = 调用方永久挂死** | @@ -101,10 +105,13 @@ socket 生命周期解耦,与 `net_listener_retry_tick` 的重试表同构。 **状态推断的克制**:CONNECTING / LISTENING 等中间态**保持等待**,**不做**状态推断式取消—— CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的等待。 -### 4.1 看门狗 +### 4.1 tripwire(原"看门狗",N34 阶段 1.5 降级) -到点后 1s 仍未了结 = 完成逻辑缺位 → **强制了结 + `DBG_ERROR`**。 -定位:防"挂起登记后永不完成"导致调用方无限期睡眠。 +挂起超 60s(`NET_RECV_TRIPWIRE_MS`)= "完成逻辑缺位"探针 → **只 `DBG_ERROR` 留痕一次, +绝不强制了结、绝不伪造 TIMEDOUT 返回值**(旧设计"到点 +1s 强制了结"会掩盖完成路径 +缺陷,且与"无限等待"的新语义矛盾——伪造返回值违背调用方意图)。 +定位:留痕成本极低的开发期探针;唤醒责任在 ①数据 ②到点(仅显式预算)③断连 与 +显式 cancel 路径(close / 网络重建 / 陈旧重同步),见 §4 五路。 --- @@ -143,8 +150,8 @@ CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的 | **SEND 路径未覆盖** | 发送仍为分块阻塞(1s/chunk stall) | 该期间队列仍被冻结;二期 pending 化解决 | | **ACCEPT 路径未覆盖** | accept 仍用轮询计时 | 需 pending 化(二期) | | **超时精度** | 随主循环节拍(~1ms),被长操作占用时顺延 | 一期文档化、不修;二期 SEND 化后该顺延面**收窄** | -| **看门狗未接自检** | 强结算 + `DBG_ERROR`,未写 selftest | 二期补齐 | -| **模拟器不可建模** | `net_socket_sim.c` 单线程 Winsock 无法复现挂起模型 | 该路径在模拟器上零覆盖,只能硬件验证;**监听生命周期 `net_listener.c` 亦为真机专属**(内部头只被 `net_socket.c` include,CMakeLists 不编译) | +| **tripwire 未接自检** | 只 `DBG_ERROR` 留痕(N34 阶段 1.5 起不再强制了结),未写 selftest | 二期补齐 | +| **模拟器不可建模** | `net_socket_sim.c` 单线程 Winsock 无法复现**挂起模型内部机制**(外部语义已由包装层按 blocking 属性/预算模拟,N34 阶段 1.5) | 挂起表/tick/通知路径在模拟器上零覆盖,只能硬件验证;**监听生命周期 `net_listener.c` 亦为真机专属**(内部头只被 `net_socket.c` include,CMakeLists 不编译) | **二期扩展设计**(ACCEPT / SEND 的挂起化、门禁清单、改动量)见 `docs/任务规划/基线与待确认.md` §2.1。 diff --git a/simulator/sim/net_socket_sim.c b/simulator/sim/net_socket_sim.c index db67b30e..81eef003 100644 --- a/simulator/sim/net_socket_sim.c +++ b/simulator/sim/net_socket_sim.c @@ -10,8 +10,10 @@ * * 设计要点: * 1. fd 采用与 MCU 端一致的"控制块索引"模型(0 ~ NET_MAX_SOCKETS-1) - * 2. 所有 Socket 创建即置为非阻塞(ioctlsocket FIONBIO),使 net_recv / - * net_accept 的 DONTWAIT 语义与 MCU 端一致(无数据返回 -1 + NET_ERR_WOULDBLOCK) + * 2. Winsock 句柄创建即置非阻塞(ioctlsocket FIONBIO),阻塞语义在包装层 + * 按控制块 blocking 属性模拟(N34 阶段 1.5:模式归使用者——blocking=1 + * 时 recv 轮询等待(Sleep(1)),=0 或 NET_MSG_DONTWAIT 立即 WOULDBLOCK), + * 使 net_recv / net_recv_nb / net_recv_to / net_accept 的语义与 MCU 端一致 * 3. 代次号在模拟器上恒为 0(无"网络重建"概念),net_get_generation 恒返回 0 * 4. net_get_sock 以 in_use 判活,供上层判断"监听 Socket 是否随客户端一同失效" * 5. Winsock 句柄存放于并行数组 s_ws[],不侵入共享的 net_sock_t 结构体 @@ -213,6 +215,9 @@ int net_socket(int domain, int type) { s_socks[idx].type = (uint8_t)type; s_socks[idx].state = NET_SOCK_STATE_CREATED; s_socks[idx].ch395_sock = (uint8_t)idx; + /* N34 阶段 1.5:阻塞模式默认 = 阻塞(与 MCU 端 net_socket 一致; + * Winsock 句柄仍恒非阻塞,阻塞语义由包装层按本属性模拟,见文件头要点 2) */ + s_socks[idx].blocking = 1; s_ws[idx] = ws; s_net_errno = NET_OK; @@ -329,6 +334,8 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) { s_socks[nidx].type = (uint8_t)NET_SOCK_STREAM; s_socks[nidx].state = NET_SOCK_STATE_ESTABLISHED; s_socks[nidx].ch395_sock = (uint8_t)nidx; + /* N34 阶段 1.5:accept 出的新连接同样默认阻塞模式(与 MCU 端一致) */ + s_socks[nidx].blocking = 1; s_ws[nidx] = cws; s_net_errno = NET_OK; @@ -451,32 +458,18 @@ int net_send(int sockfd, const void *buf, int len, int flags) { } /* - * 函数功能:接收数据(TCP) - * 入口参数:sockfd - socket 描述符 - * buf - 接收缓冲区 + * 函数功能:单次非阻塞 recv(内部公共体:立即返回,不等待) + * 入口参数:sockfd - socket 描述符(已校验) + * buf - 接收缓冲区(已校验) * len - 缓冲区大小 - * flags - 通常为 0,可设 NET_MSG_DONTWAIT(Socket 本就非阻塞) * 出口参数:buf - 填充接收数据 - * 返回值:实际接收字节数;0 = 对端关闭;-1 = 无数据(NET_ERR_WOULDBLOCK)或错误 - * 限定条件:socket 已建立 - * 函数说明:与 MCU 端语义一致——>0 有数据,0 对端关闭,<0 且 errno 区分 - * "暂无数据"与"真错误" + * 返回值:>0 字节数;0 = 对端关闭;-1 = 无数据(WOULDBLOCK) 或错误 + * 限定条件:Winsock 句柄为非阻塞(FIONBIO) + * 函数说明:net_recv 的非阻塞路径与 net_recv_nb 共用;错误码映射与旧实现一致 */ -int net_recv(int sockfd, void *buf, int len, int flags) { - int r; +static int recv_once(int sockfd, void *buf, int len) { + int r = recv(s_ws[sockfd], (char *)buf, len, 0); - (void)flags; - - if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { - s_net_errno = NET_ERR_BADF; - return -1; - } - if (buf == NULL || len < 0) { - s_net_errno = NET_ERR_INVAL; - return -1; - } - - r = recv(s_ws[sockfd], (char *)buf, len, 0); if (r > 0) { s_socks[sockfd].recv_bytes += (uint32_t)r; s_net_errno = NET_OK; @@ -494,6 +487,110 @@ int net_recv(int sockfd, void *buf, int len, int flags) { return -1; } +/* + * 函数功能:阻塞语义模拟(内部公共体:轮询等待数据/EOF/错误) + * 入口参数:sockfd - socket 描述符(已校验) + * buf / len - 接收缓冲与容量(已校验) + * has_budget - 1 = 有预算(net_recv_to);0 = 无限等待(net_recv 阻塞) + * budget_ms - 预算毫秒(has_budget=0 时忽略) + * 出口参数:buf - 填充接收数据 + * 返回值:>0 字节数;0 = 对端关闭;-1 = 错误;-2 = 预算耗尽无数据(仅 has_budget) + * 限定条件:Winsock 句柄为非阻塞;调用方为 UI/服务线程(Sleep(1) 粒度可接受) + * 函数说明:与 MCU 端挂起 recv 的对外契约对齐——MCU 端等待发生在调用方任务 + * (睡任务通知),模拟器端等价地睡 Sleep(1) 轮询;EOF/错误立即返回 + */ +static int recv_wait(int sockfd, void *buf, int len, int has_budget, unsigned long long budget_ms) { + unsigned long long t0 = GetTickCount64(); + + for (;;) { + int r = recv_once(sockfd, buf, len); + + if (r != -1 || s_net_errno != NET_ERR_WOULDBLOCK) { + return r; /* 数据 / EOF / 真错误:立即返回 */ + } + if (has_budget && (GetTickCount64() - t0) >= budget_ms) { + s_net_errno = NET_ERR_TIMEDOUT; + return -2; /* 预算耗尽(对齐 MCU net_recv_to 的 -2) */ + } + Sleep(1); + } +} + +/* + * 函数功能:接收数据(TCP,**默认无限阻塞**——N34 阶段 1.5 与 MCU 端语义对齐) + * 入口参数:sockfd - socket 描述符 + * buf - 接收缓冲区 + * len - 缓冲区大小 + * flags - 通常为 0;NET_MSG_DONTWAIT 单次强制非阻塞 + * 出口参数:buf - 填充接收数据 + * 返回值:实际接收字节数;0 = 对端关闭;-1 = 无数据(NET_ERR_WOULDBLOCK,仅非阻塞)或错误 + * 限定条件:socket 已建立 + * 函数说明:与 MCU 端语义一致——blocking 属性(net_setblocking)决定无数据时 + * 无限等待还是立即 WOULDBLOCK;DONTWAIT 单次覆盖属性 + */ +int net_recv(int sockfd, void *buf, int len, int flags) { + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (buf == NULL || len < 0) { + s_net_errno = NET_ERR_INVAL; + return -1; + } + + /* 非阻塞:DONTWAIT 单次覆盖,或 socket 的 blocking 属性为 0 */ + if ((flags & NET_MSG_DONTWAIT) || s_socks[sockfd].blocking == 0U) { + return recv_once(sockfd, buf, len); + } + /* 阻塞:无限等待(无默认预算,N34 阶段 1.5) */ + return recv_wait(sockfd, buf, len, 0, 0ULL); +} + +/* + * 函数功能:接收数据(非阻塞具名入口,与 MCU 端同名 API 对齐——N34 阶段 1.5) + * 入口参数:sockfd / buf / len - 同 net_recv + * 返回值:>0 字节数;0 = 对端关闭;-1 = 无数据(WOULDBLOCK) 或错误 + * 限定条件:socket 已建立 + * 函数说明:等价 net_recv(sockfd, buf, len, NET_MSG_DONTWAIT),不改变 blocking 属性 + */ +int net_recv_nb(int sockfd, void *buf, int len) { + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (buf == NULL || len < 0) { + s_net_errno = NET_ERR_INVAL; + return -1; + } + return recv_once(sockfd, buf, len); +} + +/* + * 函数功能:接收数据(**显式预算**便捷入口,与 MCU 端同名 API 对齐——N34 阶段 1.5) + * 入口参数:sockfd / buf / len - 同 net_recv + * timeout_ms - 本次等待预算(ms,必须 >0;0 = 无限等待语义 + * 属 net_recv 的职责,此处拒 INVAL,与 MCU 端一致) + * 返回值:>0 字节数;0 = 对端关闭;-1 = 真错误;-2 = 预算耗尽无数据 + * 限定条件:socket 已建立 + * 函数说明:模拟器以 GetTickCount64 + Sleep(1) 轮询兑现预算 + */ +int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (buf == NULL || len < 0) { + s_net_errno = NET_ERR_INVAL; + return -1; + } + if (timeout_ms == 0U) { + /* 0 = 无限等待(net_recv 的语义),与本接口"有界"存在理由相反 → 拒绝 */ + s_net_errno = NET_ERR_INVAL; + return -1; + } + return recv_wait(sockfd, buf, len, 1, (unsigned long long)timeout_ms); +} + /* * 函数功能:发送 UDP 数据报(含目标地址) * 入口参数:sockfd - socket 描述符 @@ -634,6 +731,28 @@ int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg) { return 0; } +/* + * 函数功能:设置 Socket 阻塞/非阻塞**模式属性**(与 MCU 端同名 API 对齐——N34 阶段 1.5) + * 入口参数:sockfd - socket 描述符 + * on - 1 = 阻塞(net_socket/net_accept 默认);0 = 非阻塞 + * 返回值:0 成功;-1 失败(描述符无效/未占用) + * 限定条件:socket 已创建 + * 函数说明:Winsock 句柄恒非阻塞(FIONBIO,UI 主循环安全底线),本属性只驱动 + * 包装层的等待策略(net_recv 无限轮询 / 立即 WOULDBLOCK), + * 语义与 MCU 端"模式挂控制块属性"一致 + */ +int net_setblocking(int sockfd, int on) { + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + + s_socks[sockfd].blocking = (on != 0) ? 1U : 0U; + + s_net_errno = NET_OK; + return 0; +} + /* * 函数功能:获取 socket 最后错误码 * 入口参数:无 diff --git a/test/host/latency_peer.py b/test/host/latency_peer.py index ab6d5467..21326609 100644 --- a/test/host/latency_peer.py +++ b/test/host/latency_peer.py @@ -310,7 +310,9 @@ class RemoPeer: # --- FTP head-of-line stress (E-02) --- def ftp_stress_worker(self, start_evt, stop_evt, hold_s): """Wait for the stress window to open, then hold an idle FTP login: - lftpd read_line -> blocking net_recv(0) inside netTask (NET_RECV_TIMEOUT_MS) + lftpd read_line -> blocking net_recv(0) inside netTask (old 30s default + budget NET_RECV_TIMEOUT_MS, removed in N34 phase 1.5; today the wait is + an L3 pending recv -> this scenario must NOT freeze the queue anymore) -> every queued message (incl. the LCDMEM response) waits behind it (E-02).""" if not start_evt.wait(120.0): sys.stdout.write("[peer] ftp stress: window never opened\n")