feat(NET): N34 阶段 1.5 recv 语义主流化——无限默认+具名非阻塞+模式属性

核心语义(实施方案 §3.5):
- net_recv 默认无限阻塞:删除 NET_RECV_TIMEOUT_MS(30s 隐藏默认预算),
  超时归调用方策略;唤醒源=数据/EOF/close/网络重建(CONNRESET),
  会话死亡由对账① ~400ms 判死兜底,无"对端消失永久挂死"窗口
- 新增 net_recv_nb(NET_MSG_DONTWAIT 路径转正具名)与
  net_setblocking(fd,on)(阻塞模式挂控制块属性,对齐 O_NONBLOCK/
  Python setblocking;直写单字节,net_set_event_cb 先例)
- net_types.h net_sock_t 新增 blocking 属性;net_socket() 默认置 1
  (close memset 清零后显式重置,同 released_abnormal 纪律)
- 原 1s 强制了结看门狗降级为 60s tripwire:只 [ERR] 留痕一次,
  绝不伪造 TIMEDOUT 返回值(旧实现掩盖完成路径缺陷)
- net_recvfrom 删 30s 自旋超时、同判据对齐(零生产调用者,注释警示)
- 文档契约:IWDG required_mask 内任务禁止无限阻塞 recv(BSD 指南
  §3.5/§10.1 + net_socket.h 注释)

调用点审计(§3.5.5):零生产调用点采用无限默认——iec104_serv/
ui_net_sink 转 net_recv_nb(受监控任务逐位等价);lftpd/time_sync
已是显式预算 net_recv_to 不动;32 轮盲收兜底未动(阶段 3 E6 门禁)

模拟器双端镜像 net_recv_nb/net_recv_to/net_setblocking(包装层
Sleep(1) 轮询模拟阻塞);差距清单 N-3/N-6 登记义务已履行

门禁与台架:
- @build 0/0:Code 209580→209780(+200B) ZI +32B;RAM 受管区官方
  脚本同步;ci_local L0 32/32 + L0.5 951/951
- 台架全回归:relisten :7003 50/50、iec104 18/18、对时正常、
  ftp_regression 11/11、Trap23 RST 5/5+FIN 5/5、ftp_integrity
  20/20 字节一致、n30 0/10、@test PASS
- 模拟器 build_sim OK + check_remo_sink 黄金参考 0/25600 位差

队友 p15-recv-semantics 实施,Lead 全量审阅 + 独立复跑门禁与回归
This commit is contained in:
2026-10-13 11:23:19 +08:00
parent 8f902e09fd
commit d346e4ee84
17 changed files with 487 additions and 147 deletions
+15 -8
View File
@@ -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。