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:
+635
-92
@@ -1,16 +1,35 @@
|
||||
# Net 层阻塞模型架构设计
|
||||
|
||||
> **模块名称**:Net 层挂起 recv 模型(`net_pending.c` / netTask 主循环节拍)
|
||||
> **模块名称**:Net 层挂起 recv / send 模型(`Drivers/BSP/NET/net_pending.c` + netTask 主循环节拍)
|
||||
> **文档定位**:**现状架构设计说明**——描述本层**当前如何组织等待与完成**、为什么这样设计、
|
||||
> 边界条件与已验证的性能特征。**不是历史记录**:历史演进、门禁实测记录与修订日志
|
||||
> 一律不保留在文档内(需要时查 git 历史)。
|
||||
> **状态**:设计已落地并全量台架验证(`v2-on-ui`)。
|
||||
> **日期**:2026-09-29
|
||||
> 数据结构与接口契约、实现落点与集成步骤、边界条件与已验证的性能特征。**不是历史记录**:
|
||||
> 历史演进、门禁实测记录与修订日志一律不保留在文档内(需要时查 git 历史)。
|
||||
> **状态**:recv 挂起(一期)与 send 挂起(二期)设计均已落地;recv 全量台架验证,
|
||||
> send 于 2026-10-14 完成台架复测(含本次复测发现并修复的末块挂起越界缺陷,见 §9.1)。
|
||||
> **日期**:2026-10-14
|
||||
> **相关文档**:
|
||||
> `docs/网络/BSD_Socket_API_使用指南.md`(应用视角 API 语义)、
|
||||
> `docs/网络/BSD_Socket_API_函数调用关系.md`(调用链与执行域)、
|
||||
> `docs/网络/网络自愈架构设计.md`(周期对账与监听生命周期——与本模型的分工见 §2.4;模块化边界见 §11)、
|
||||
> `docs/测试说明/链路延迟测试说明.md`(延迟基线与故障对照)、
|
||||
> `docs/任务规划/基线与待确认.md` §2.1(二期 ACCEPT/SEND 扩展设计)
|
||||
> `docs/问题记录/错误码传播审计.md`(E-02 队头阻塞审计)、
|
||||
> `docs/任务规划/基线与待确认.md` §2.1(二期扩展设计:SEND 已落地、ACCEPT 待办)
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 问题域](#1-问题域)
|
||||
- [2. 架构总览](#2-架构总览)
|
||||
- [3. 一次阻塞 recv 的全过程](#3-一次阻塞-recv-的全过程)
|
||||
- [4. 数据结构](#4-数据结构)
|
||||
- [5. 完成与取消语义](#5-完成与取消语义)
|
||||
- [6. 模块接口契约](#6-模块接口契约)
|
||||
- [7. 实现落点(集成步骤)](#7-实现落点集成步骤)
|
||||
- [8. 调用方契约](#8-调用方契约)
|
||||
- [9. 性能特征](#9-性能特征)
|
||||
- [10. 设计边界与已知代价](#10-设计边界与已知代价)
|
||||
- [11. 设计铁律](#11-设计铁律)
|
||||
- [12. 排查辅助:挂起超时留痕探针](#12-排查辅助挂起超时留痕探针)
|
||||
- [13. 源码索引](#13-源码索引)
|
||||
|
||||
---
|
||||
|
||||
@@ -29,11 +48,28 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线
|
||||
> → **观测本身占用执行者**,等待无法挪出 netTask。
|
||||
|
||||
设计自由度因此只剩两条:① 每次占用执行者多久 ② 等待期间执行者还能做什么。
|
||||
本架构的答案:**占用降到登记一次(微秒级),等待期间执行者照常服务整个队列**。
|
||||
三条候选路线的取舍:
|
||||
|
||||
| 路线 | 单次占用执行者 | 等待期间执行者 | 落地结论 |
|
||||
|---|---|---|---|
|
||||
| A 执行者内自旋 | 整个等待期(可无限) | **完全停摆** | ❌ 队头阻塞(实测一条空闲 FTP 连接冻结远端显示 17.7s) |
|
||||
| B 有界分片轮询(`net_recv_to` 分片) | ≤ `timeout_ms` | 分片间隙可用 | ⚠️ 止血:仍周期性占用,调用方须自管总预算 |
|
||||
| C **挂起登记(本模型)** | 微秒级(登记一次) | 照常服务整个队列 | ✅ 结构性消灭 |
|
||||
|
||||
路线 A→B→C 的落地证据与量化见 `docs/问题记录/错误码传播审计.md` E-02。
|
||||
路线 B 的入口 `net_recv_to` 在路线 C 下**继续保留**,但语义已从"分期占用执行者"
|
||||
变为"调用方显式预算"(见 §6.2、§8)。
|
||||
|
||||
> **SEND 是同一问题的第二个面**:`net_send` 写入 CH395F 发送 FIFO 后需等 `send_ready`
|
||||
> (由 `SENBUF_FREE` 中断经 `net_poll` 重新置位),旧的 `net_send_locked` 在 netTask 内
|
||||
> **忙轮询等它**,单次停滞上限 `NET_SEND_TIMEOUT_MS`(1 s)——同属执行者占用型队头阻塞,
|
||||
> 量级比 recv 小一个数量级,但同样会冻结整个消息队列并顺延挂起 recv 的唤醒。二期把
|
||||
> send 也纳入挂起模型(§4.5、§5.5),**快路径保留有界自旋**(健康链路 219 µs ≪ 2 ms 预算,
|
||||
> 行为不变),超预算才登记挂起。收益判据仍是"执行者被占多久"(§10)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构总览:pending 请求模型
|
||||
## 2. 架构总览
|
||||
|
||||
核心思想:把"等数据"从**执行者内的自旋**改为**执行者外的登记**。
|
||||
|
||||
@@ -43,75 +79,89 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线
|
||||
|
||||
| 区域 | 内容 |
|
||||
|---|---|
|
||||
| **左栏 · 调用方任务** | `net_recv` → 判"数据已在?" → **是**走快速路径;**否**(`timeout > 0`)登记 pending 后在自己上下文睡任务通知 |
|
||||
| **中栏 · netTask 主循环** | ~1ms 节拍:`net_poll`(刷新 readable + 全量刷新驱动状态缓存)→ 3 个非阻塞 tick(`net_listener_retry_tick` D10 → `net_pending_tick` 结束挂起 → `net_reconcile_tick` TRAP-NET-17 对账 + 监听重建唯一入口)→ 排空消息队列 |
|
||||
| **下带 · 五路判定** | 每轮对每个挂起 fd 按序判定:数据到达 / 到点(仅显式预算)/ EOF / 同 fd 后续操作 / 网络重建;挂起超时留痕探针(编译期选项,默认关,见 §9)只留痕 |
|
||||
| **左栏 · 调用方任务** | `net_recv` → 判"数据已在?" → **是**走快速路径;**否**(`timeout > 0`)登记挂起 recv 后在自己上下文睡任务通知。`net_send` 同构:快路径短暂自旋写块,超预算则登记挂起 send 后睡 |
|
||||
| **中栏 · netTask 主循环** | ~1ms 节拍:`net_poll`(刷新 `readable` + 全量刷新驱动状态缓存)→ 4 个非阻塞 tick(`net_listener_retry_tick` D10 → `net_pending_tick` 结束挂起 recv → `net_send_pending_tick` 推进/结束挂起 send → `net_reconcile_tick` 对账 + 监听重建唯一入口)→ 排空消息队列 |
|
||||
| **下带 · 判定** | recv 每轮按序判定:数据到达 / 到点(仅显式预算)/ EOF / 同 fd 后续操作 / 网络重建(**五路**);send 每轮按序判定:断连 / 推进 / 写完 / 无进展超时(**四路**);挂起超时留痕探针(编译期选项,默认关,见 §12)只留痕 |
|
||||
| **回环** | 判定完成经 `xTaskNotify` 唤醒调用方(结果 = 字节数 / 负错误码) |
|
||||
|
||||
**关键性质**:
|
||||
|
||||
- 等待期间 netTask **零额外占用**——`net_pending_tick` 只读标志与到期时间,
|
||||
数据到达判据 `readable` 由主循环既有的 `net_poll()` 每轮刷新,**不引入额外 SPI 事务**;
|
||||
- 调用方**在自己的上下文睡眠**(任务通知),与 LwIP 的消费者线程模型同构;
|
||||
- 公开 API 与调用契约**稳定**——`net_recv`(默认无限阻塞)/ `net_recv_nb` +
|
||||
`net_setblocking` / `net_recv_to`(唯一显式预算入口)的签名与返回值不变,
|
||||
既有调用方、测试与模拟器零改动;超时归属语义详见
|
||||
`docs/网络/网络自愈架构设计.md` §4.5 与 `docs/网络/BSD_Socket_API_使用指南.md` §3.5。
|
||||
|
||||
> 图形由 `tools/arch_diagram/gen_net_pending_svg.py` 生成(改流程后重跑;见
|
||||
> `docs/架构设计/图形绘制流程.md`)。
|
||||
|
||||
---
|
||||
### 2.1 关键性质
|
||||
|
||||
## 3. 数据结构
|
||||
- 等待期间 netTask **零额外占用**——`net_pending_tick` 只读标志与到期时间,
|
||||
数据到达判据 `readable` 由主循环既有的 `net_poll()` 每轮刷新,**不引入额外 SPI 事务**;
|
||||
- **挂起 send 的占用有界且非阻塞**——`net_send_pending_tick` 每轮至多写一块(`≤ NET_SEND_CHUNK_MAX`),
|
||||
写完即返回,绝不 `osDelay` 忙等;故 netTask 最坏停滞由 `NET_SEND_TIMEOUT_MS`(1 s)
|
||||
压到 `NET_SEND_SPIN_MS`(2 ms);
|
||||
- 调用方**在自己的上下文睡眠**(任务通知),与 LwIP 的消费者线程模型同构;
|
||||
- 公开 API 与调用契约**稳定**——`net_recv`(默认无限阻塞)/ `net_recv_nb` +
|
||||
`net_setblocking` / `net_recv_to`(唯一显式预算入口)/ `net_send` 的签名不变;
|
||||
既有调用方、测试与模拟器零改动;`net_send` 的返回值语义(已发字节数,短写须检查)
|
||||
与挂起化前一致,超时归属语义详见
|
||||
`docs/网络/网络自愈架构设计.md` §4.5 与 `docs/网络/BSD_Socket_API_使用指南.md` §3.5。
|
||||
|
||||
### 3.1 挂起请求表
|
||||
### 2.2 模块边界
|
||||
|
||||
| 项 | 设计 |
|
||||
|---|---|
|
||||
| 存储 | **独立静态数组** `s_net_pending[NET_MAX_SOCKETS]`(不置于 `net_sock_t` 内) |
|
||||
| 字段 | `active` / `infinite` / `buf` / `len` / `deadline` / `registered` / `caller`(`infinite`:0 预算 = 无限等待;留痕开启时另有 `tripwire_logged`:保证只留痕一次) |
|
||||
| 容量 | 每 fd **至多一个**挂起请求 |
|
||||
| 占用 | ≈24B/fd × 8 fd = **192B**(+4B 任务句柄),实测 SRAM +200B |
|
||||
本模型是 `net_socket.c` 内部的**一层**,实现独立成模块,只被少数确定位置消费:
|
||||
|
||||
**为什么独立成表而不是塞进 `net_sock_t`**:
|
||||
| 文件 | 持有 | 对外接口 |
|
||||
|---|---|---|
|
||||
| `Drivers/BSP/NET/net_pending.c` / `.h` | 挂起 recv 表 `s_net_pending[]`、挂起 send 表 `s_net_send[]`、执行者句柄 `s_owner_task`、全部完成/取消逻辑 | 14 个函数(recv 组 9 + send 组 7,含 2 个 static,§6.1) |
|
||||
| `Drivers/BSP/NET/net_socket.c` | 消息队列与分派、`net_recv_locked` / `net_send_locked` 的挂起登记、close/drop_conn/重建/对账的结算调用 | 调用方(§7) |
|
||||
| `Drivers/BSP/NET/net_listener.c` | 监听器重开 | 消费 `net_pending_active()` / `net_send_pending_active()` 作"挂起保险" |
|
||||
| `App/task/net_task.c` | 主循环节拍(`net_poll` → `net_process_messages` → `osDelay(1)`) | 提供 tick 节拍 |
|
||||
|
||||
`net_close_locked` 对 `net_sock_t` 做**整块 memset**。若挂起状态置于其中,`close` 会
|
||||
静默抹掉 pending——调用方永远等不到通知,**永久挂死**。独立表把"跨时间状态"与
|
||||
socket 生命周期解耦,与 `net_listener_retry_tick` 的重试表同构。
|
||||
依赖方向**单向**:`net_pending.c` → `net_types.h` / `net_socket.h` / `net_listener.h` /
|
||||
`ch395f.h`(send 侧分块写与缓冲常量)/ `main.h`;无反向 include。`net_listener.c` 经
|
||||
`net_pending.h` 反向消费 `net_pending_active()` / `net_send_pending_active()`,二者均为
|
||||
**纯 RAM 读**,任意上下文安全。
|
||||
|
||||
### 3.2 登记守卫(两条硬防线)
|
||||
### 2.3 与主循环的关系
|
||||
|
||||
| 守卫 | 理由 |
|
||||
|---|---|
|
||||
| 每 fd 至多一个,**不覆盖** | 覆盖 = 丢掉首个调用方的通知 = 该调用方永久挂起 |
|
||||
| `caller == NULL` 或 `caller == netTask` 时**拒绝**(返回 BUSY) | netTask 上下文自等待 = 无人能完成它 = 死锁;netTask 内必须用 DONTWAIT |
|
||||
netTask 主循环(`App/task/net_task.c` → `net_task_func`)每轮:
|
||||
|
||||
---
|
||||
```text
|
||||
task_wd_beat() → 处理 SYS_EVT_NET_RECONFIG → net_poll() → net_process_messages() → osDelay(1)
|
||||
```
|
||||
|
||||
## 4. 挂起的 recv 会被什么结束(5 种触发条件)
|
||||
`net_process_messages()`(`Drivers/BSP/NET/net_socket.c`)内部**先跑四个非阻塞 tick,
|
||||
再排空消息队列**,顺序固定:
|
||||
|
||||
挂起 recv 被结束的方式只有 5 种,下文简称**五路**——①②③ 由 `net_pending_tick()`
|
||||
每轮对每个挂起 fd 按序判定,④⑤ 由显式操作触发:
|
||||
|
||||
| # | 条件 | 动作 | 设计要点 |
|
||||
| 顺序 | tick | 职责 | 与挂起模型的关系 |
|
||||
|---|---|---|---|
|
||||
| 1 | **数据到达** | SPI 读出 + 通知(结果 = 字节数) | 由 `net_poll` 维护的 `readable` 门控,**零额外 SPI**;仅 ESTABLISHED / UDP_OPEN 态可读;spurious readable 需清除 |
|
||||
| 2 | **到点** `now >= deadline`(仅显式预算登记;无限等待登记无 deadline) | 通知 `NET_ERR_TIMEDOUT`(对外 -2 映射保持) | 状态无关;用有符号回绕安全差值,防 tick 回绕;这是 `net_recv_to` 调用方策略的正常兑现,不是伪造 |
|
||||
| 3 | **CLOSED**(EOF/DISCONNECT) | 排空:有数据则读出;否则按 0(EOF) 完成 + `auto_relisten` + 通知 | CLOSE 先行完成挂起 = `net_close` 的 settle 钩子(排空→数据否则 CONNRESET) |
|
||||
| 4 | **同 fd 后续操作**:close / drop_conn / 对账 resync(芯片静默回收) | 先结束挂起 recv(排空 → 有数据按字节数完成;否则取消 CONNRESET),再执行新操作 | per-fd 串行:pending 模型唯一留下的次序面就在本 fd 内,其他 fd 不受影响。同 fd 第二个 recv 被登记守卫拒绝(BUSY);send 无需结算(收发不冲突,SPI 由 netTask 串行) |
|
||||
| 5 | **网络重建**(`net_stack_bring_up`,芯片复位) | **取消 + 必须通知** | `net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;**取消不通知 = 调用方永久挂死** |
|
||||
| ① | `net_listener_retry_tick()` | D10 重监听重试(30ms/2s) | 跳过有在途挂起 recv / send 的 fd(§7.2 集成点 8 / 20) |
|
||||
| ② | `net_pending_tick(s_net_socks)` | **结束挂起 recv**(数据/到点/断连) | recv 的主完成检查入口 |
|
||||
| ③ | `net_send_pending_tick(s_net_socks)` | **推进/结束挂起 send**(断连/推进/写完/超时) | send 的主推进入口(二期) |
|
||||
| ④ | `net_reconcile_tick()` | 100ms 对账 + 监听重建唯一入口 | 形态⑤(芯片静默回收)取消本 fd 挂起 recv / send(§7.2 集成点 7 / 17) |
|
||||
|
||||
**状态推断的克制**:CONNECTING / LISTENING 等中间态**保持等待**,**不做**状态推断式取消——
|
||||
CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的等待。
|
||||
四个 tick 均非阻塞、**绝不 `osDelay` 忙等**;tick 先于消息分派,保证"断连后即使无应用
|
||||
消息,监听器与挂起 recv / send 也能自主收敛"。②③ 相对顺序无关(两张独立表),
|
||||
但都必须在 ④ 之前——对账可能取消同一 fd 的两侧挂起。
|
||||
|
||||
### 4.1 一次阻塞 recv 从调用到返回的全过程
|
||||
### 2.4 与网络自愈架构的分工
|
||||
|
||||
一句话链:**登记(μs 级)→ 调用方自己睡 → 每轮判定(只读标志与到期时间)→ 公共出口(通知+清槽)→ 两段式等待取结果**。以 `net_recv(sockfd, buf, len)` 阻塞路径为例:
|
||||
两篇文档描述同一段代码的两个正交面,边界如下:
|
||||
|
||||
| 关注点 | 归属文档 |
|
||||
|---|---|
|
||||
| "一次阻塞 recv 如何被唤醒/结束"(本模型:登记、五路完成、接口、集成点) | **本文** |
|
||||
| "芯片与本层状态失同步后如何周期收敛"(对账五形态、沉降隔离、D10 重试、KeepAlive) | `docs/网络/网络自愈架构设计.md` |
|
||||
| 监听生命周期的模块化边界(`net_listener.c` 拆分依据) | `docs/网络/网络自愈架构设计.md` §11 |
|
||||
|
||||
交叉点只有两处:`net_reconcile_tick` 形态⑤ 取消挂起(§5.2 第 5 路)、
|
||||
`net_listener_retry_tick` 的挂起 recv 保险(§7.2 集成点 8)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 一次阻塞 recv 的全过程
|
||||
|
||||
一句话链:**登记(μs 级)→ 调用方自己睡 → 每轮判定(只读标志与到期时间)→
|
||||
公共出口(通知 + 清槽)→ 两段式等待取结果**。以 `net_recv(sockfd, buf, len, 0)` 阻塞路径为例:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["调用方任务:net_recv(sockfd, buf, len)"] --> B["消息入队,随后在<br/>自己上下文睡(两段式等待)"]
|
||||
A["调用方任务:net_recv(sockfd, buf, len, 0)"] --> B["消息入队,随后在<br/>自己上下文睡(两段式等待)"]
|
||||
B -.-> C
|
||||
|
||||
subgraph NT["netTask —— 唯一执行者(~1ms 节拍)"]
|
||||
@@ -133,75 +183,539 @@ flowchart TD
|
||||
I["通知调用方(同一回传通道):通知值 = 结果原值<br/>挂起路径经公共出口 net_pending_complete:清槽、防重复通知"]
|
||||
I --> J["调用方醒来取结果<br/>0 = EOF 也是有效通知(以 pdTRUE 判定)"]
|
||||
|
||||
K["取消路径:close / drop_conn / 网络重建"] -. "先结束挂起、再动芯片" .-> I
|
||||
K["取消路径:close / drop_conn / 网络重建 / 对账 resync"] -. "先结束挂起、再动芯片" .-> I
|
||||
```
|
||||
|
||||
1. **入队**(调用方任务):`net_recv` 把请求打包进消息入 `netMsgQueue`,随后进入两段式等待(步骤 6)。netTask 不被占用。
|
||||
2. **快速路径判定**(netTask,`net_recv_locked`):数据已在(`readable` / recv_len>0)→ 直接读出、经通知值回传字节数——不产生挂起,常见路径到此为止。
|
||||
3. **登记**(netTask):无数据且阻塞模式 → `net_pending_register` 过两条守卫(§3.2:每 fd 至多一个不覆盖;拒绝 netTask 自等待)→ 记 `buf`/`len`/`caller` 与预算(`net_recv_to` → `deadline`;无限等待 → `infinite=1` 无 deadline)→ 返回"已登记"哨兵,netTask 立即处理下一条消息。
|
||||
4. **每轮判定**(netTask,`net_pending_tick`):对每个挂起 fd 按序 ①数据 → ②到点 → ③断连(判据见上表;③ 的判据是 `state==CLOSED || session_dead`,漏 ③ = 无限 recv 永久挂死,勿动)。命中任一条件进入公共出口;挂起超时留痕探针只留痕不完成(编译期选项,默认关,见 §9)。
|
||||
5. **公共完成出口**(`net_pending_complete`,所有完成/取消路径的唯一出口):`xTaskNotify` 覆盖式写入**通知值 = 结果原值**(>0 字节数 / 0 EOF / 负错误码)→ 清 `active`;无在位挂起则静默返回(防重复通知)。失败结果同步记入 netTask 自己的 errno 槽位(按任务槽位,非跨任务共享全局)。
|
||||
6. **唤醒与取结果**(调用方任务):所有 `net_*` API 的回执共用同一条任务通知通道,**两段式等待**防"入队→等待入口"间隙内通知丢失——先零等待拾取(清除掩码全 1;以 pdTRUE/pdFALSE 判定已落地,不能判值≠0——合法的 0 结果=EOF 也是有效通知),未拾取到再阻塞等待。通知值 32 位整体作为结果,不切分位域(大单次收发不被截断)。
|
||||
7. **取消路径**(五路中的 ④⑤,共同规则"**先结束挂起、再动芯片**"):
|
||||
- **同 fd 后续操作**(close / drop_conn):`net_pending_settle_close` 先结算——芯片 RX 有数据(bit3=1 契约:断连后残数据留缓冲待 MCU 读)→ 按字节数完成;无数据 → 取消(CONNRESET)——然后才执行 close/拆会话。对账发现芯片静默回收(resync)时走 `net_pending_cancel`(连接已被芯片收走,无排空窗口,直接取消必通知)。
|
||||
- **网络重建**:`net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;取消必通知,否则调用方永久挂死。
|
||||
8. **buf 生命周期**:挂起期间 `buf` 指针须保持有效至调用返回(挂起表按指针写入,§5)。
|
||||
1. **入队**(调用方任务):`net_recv` 把请求打包进消息入 `netMsgQueue`,随后进入两段式
|
||||
等待(步骤 6)。netTask 不被占用。
|
||||
2. **快速路径判定**(netTask,`net_recv_locked`):函数入口先 `net_poll()` 一次刷新状态机
|
||||
与可读标志,再查 `ch395f_get_recv_len`。数据已在(`recv_len > 0`)→ 直接读出、经通知值
|
||||
回传字节数——不产生挂起,常见路径到此为止。
|
||||
3. **登记**(netTask):无数据且阻塞模式 → `net_pending_register` 过两条守卫(§4.3)→ 记
|
||||
`buf`/`len`/`caller` 与预算(`net_recv_to` → `deadline`;无限等待 → `infinite=1` 无
|
||||
deadline)→ 返回哨兵 `NET_PENDING_REGISTERED`,netTask 立即处理下一条消息。
|
||||
4. **每轮判定**(netTask,`net_pending_tick`):对每个挂起 fd 按序 ①数据 → ②到点 →
|
||||
③断连(判据见 §5);命中任一条件进入公共出口。挂起超时留痕探针只留痕不完成
|
||||
(编译期选项,默认关,见 §12)。
|
||||
5. **公共完成出口**(`net_pending_complete`,所有完成/取消路径的**唯一**出口):
|
||||
`xTaskNotify` 覆盖式写入**通知值 = 结果原值**(>0 字节数 / 0 EOF / 负错误码)→ 清
|
||||
`active`;无在位挂起则静默返回(防重复通知)。失败结果同步记入 netTask 自己的 errno
|
||||
槽位(按任务槽位,非跨任务共享全局)。
|
||||
6. **唤醒与取结果**(调用方任务):所有 `net_*` API 的回执共用同一条任务通知通道,
|
||||
**两段式等待**防"入队→等待入口"间隙内通知丢失——先零等待拾取(清除掩码全 1;
|
||||
以 `pdTRUE`/`pdFALSE` 判定已落地,**不能判值≠0**——合法的 0 结果 = EOF 也是有效通知),
|
||||
未拾取到再阻塞等待。通知值 32 位整体作为结果,不切分位域(大单次收发不被截断)。
|
||||
7. **取消路径**(第 4/5 路,共同规则"**先结束挂起、再动芯片**"):
|
||||
- **同 fd 后续操作**(close / drop_conn):`net_pending_settle_close` 先结算——芯片 RX
|
||||
有数据(bit3=1 契约:断连后残数据留缓冲待 MCU 读)→ 按字节数完成;无数据 → 取消
|
||||
(CONNRESET)——然后才执行 close/拆会话。对账发现芯片静默回收(resync)时走
|
||||
`net_pending_cancel`(连接已被芯片收走,无排空窗口,直接取消必通知)。
|
||||
- **网络重建**:`net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;
|
||||
取消必通知,否则调用方永久挂死。
|
||||
8. **`buf` 生命周期**:挂起期间 `buf` 指针须保持有效至调用返回(挂起表按指针写入,§8)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 调用方契约
|
||||
## 4. 数据结构
|
||||
|
||||
调用方**不改协议、不改 API**,但需知悉三条:
|
||||
### 4.1 挂起 recv 请求表 `net_pending_t`
|
||||
|
||||
| 字段 | 类型 | 含义 | 生命周期 |
|
||||
|---|---|---|---|
|
||||
| `active` | `uint8_t` | 1 = 本 fd 有挂起 recv | 登记置 1;`net_pending_complete` / `clear` 清 0 |
|
||||
| `infinite` | `uint8_t` | 1 = 无限等待(无 deadline) | 登记按 `to_ms == 0` 定;每轮 ② 判定据此跳过 |
|
||||
| `tripwire_logged` | `uint8_t` | 1 = 本轮挂起已留痕(保证只留一条) | **仅 `NET_PENDING_TRIPWIRE` 编译期存在**;登记清 0 |
|
||||
| `buf` | `void *` | 调用方接收缓冲(仅指针,不拷贝) | 登记写入;调用方保证有效至返回 |
|
||||
| `len` | `int` | 缓冲容量 | 登记写入 |
|
||||
| `deadline` | `uint32_t` | 截止时刻(tick)= 登记时刻 + 本次预算 | 无限等待时置 0(可观测性用,② 判定不看) |
|
||||
| `registered` | `uint32_t` | 登记时刻(tick) | 留痕探针基准 |
|
||||
| `caller` | `TaskHandle_t` | 调用方任务(完成通知目标) | 登记写入 |
|
||||
|
||||
- **存储**:**独立静态数组** `s_net_pending[NET_MAX_SOCKETS]`(不置于 `net_sock_t` 内)。
|
||||
- **容量**:每 fd **至多一个**挂起请求。
|
||||
- **占用**:结构 24 B/fd × 8 fd = **192 B**(`tripwire_logged` 落在对齐填充里,开启探针
|
||||
不增加占用);另 `s_owner_task` 4 B。实测 SRAM +200 B。
|
||||
- **`infinite` 与 `deadline` 正交**:`to_ms == 0` 即"0 预算 = 无限等待"(无默认预算)。
|
||||
|
||||
### 4.2 为什么独立成表而不是塞进 `net_sock_t`
|
||||
|
||||
`net_close_locked` 对 `net_sock_t` 做**整块 `memset`**。若挂起状态置于其中,`close` 会
|
||||
静默抹掉 pending——调用方永远等不到通知,**永久挂死**。独立表把"跨时间状态"与
|
||||
socket 生命周期解耦,与 `net_listener_retry_tick` 的重试表同构。
|
||||
|
||||
### 4.3 登记守卫(两条硬防线,recv / send 共用)
|
||||
|
||||
recv 与 send 两张表的登记函数(`net_pending_register` / `net_send_pending_register`)
|
||||
执行**完全相同**的两条守卫:
|
||||
|
||||
| 守卫 | 判据 | 理由 |
|
||||
|---|---|---|
|
||||
| 每 fd 至多一个,**不覆盖** | `p->active != 0` → 返回 `BUSY` | 覆盖 = 丢掉首个调用方的通知 = 该调用方永久挂起 |
|
||||
| 拒绝执行者自等待 | `caller == NULL` 或 `caller == s_owner_task` → 返回 `BUSY` | netTask 上下文自等待 = 无人能完成它 = 死锁;netTask 内必须用 `NET_MSG_DONTWAIT` / 非阻塞发送 |
|
||||
|
||||
`s_owner_task` 由 `net_pending_capture_owner()` 在 `net_init` 顶部捕获(= 调用者,
|
||||
NET 层为 netTask,硬件层为 testTask)。
|
||||
|
||||
### 4.4 挂起槽状态机
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> IDLE
|
||||
IDLE --> WAITING: net_pending_register(过两守卫)
|
||||
WAITING --> WAITING: tick 每轮判定未命中<br/>(含中间态 CONNECTING/LISTENING 保持等待)
|
||||
WAITING --> IDLE: net_pending_complete<br/>(数据 / 到点 / EOF / 取消)
|
||||
IDLE --> IDLE: complete 对非在位槽位静默返回(防重复通知)
|
||||
note right of WAITING
|
||||
等待期零执行者占用:
|
||||
tick 只读标志与到期时间
|
||||
end note
|
||||
```
|
||||
|
||||
槽位只有两个状态:**IDLE**(无挂起)与 **WAITING**(有挂起)。所有出口都汇聚到
|
||||
`net_pending_complete`(或幂等的 `net_pending_clear`),因此**不存在"完成两次"或
|
||||
"漏通知"的中间态**。挂起 send 槽位(§4.5)同构,只是"完成条件"由"数据到达"换为"全部写完"。
|
||||
|
||||
### 4.5 挂起 send 请求表 `net_send_pending_t`
|
||||
|
||||
| 字段 | 类型 | 含义 | 生命周期 |
|
||||
|---|---|---|---|
|
||||
| `active` | `uint8_t` | 1 = 本 fd 有挂起 send | 登记置 1;`net_send_pending_complete` / `clear` 清 0 |
|
||||
| `buf` | `const void *` | 调用方发送缓冲基址(**仅指针,不拷贝**) | 登记写入;调用方保证有效至返回 |
|
||||
| `len` | `int` | 本次发送**总**长度(含登记前已写入芯片的部分) | 登记写入 |
|
||||
| `sent` | `int` | 已写入芯片的字节数(**进度**;登记时可 > 0) | 每次成功写块累加;完成时即结果值 |
|
||||
| `deadline` | `uint32_t` | 无进展超时时刻 = **上次成功写块时刻** + `NET_SEND_TIMEOUT_MS` | 每次成功写块刷新;`NET_SEND_TIMEOUT_MS == 0` 时置 0(不限) |
|
||||
| `registered` | `uint32_t` | 登记时刻(tick) | 日志基准 |
|
||||
| `caller` | `TaskHandle_t` | 调用方任务(完成通知目标) | 登记写入 |
|
||||
|
||||
- **存储**:**独立静态数组** `s_net_send[NET_MAX_SOCKETS]`,与 `s_net_pending[]` **完全独立**
|
||||
(各自一张表、各占一槽)。
|
||||
- **容量**:每 fd **至多一个**挂起 send。
|
||||
- **占用**:结构 28 B/fd × 8 fd = **224 B**(实测 RW_IRAM1 +224 B,与 `s_net_pending` 的 192 B 并列)。
|
||||
- **与 recv 表可并存**:同一 fd 可同时有一个挂起 recv 与一个挂起 send——收发两个方向互不阻塞
|
||||
(SPI 由 netTask 串行,本就支持该并发)。
|
||||
- **`deadline` 按无进展刷新**:这是与 recv 表 `deadline`(一次预算、不刷新)的**关键差异**,
|
||||
理由见 §5.5。
|
||||
|
||||
---
|
||||
|
||||
## 5. 完成与取消语义
|
||||
|
||||
recv 与 send 两张挂起表**各自独立**,完成语义分别定义:recv 是"等一次到达即完成"(五路),
|
||||
send 是"分块推进直到全部写完"(四路 + 显式取消)。两表可同一 fd 并存,互不阻塞(§4.5)。
|
||||
|
||||
### 5.1 挂起 recv 五路总表
|
||||
|
||||
挂起 recv 被结束的方式只有 5 种,下文简称**五路**——①②③ 由 `net_pending_tick()`
|
||||
每轮对每个挂起 fd 按序判定,④⑤ 由显式操作触发。
|
||||
|
||||
| # | 条件 | 动作 | 设计要点 |
|
||||
|---|---|---|---|
|
||||
| 1 | **数据到达** | SPI 读出 + 通知(结果 = 字节数) | 由 `net_poll` 维护的 `readable` 门控,**零额外 SPI**;仅 `ESTABLISHED` / `UDP_OPEN` 态可读;spurious readable 需清除 |
|
||||
| 2 | **到点** `now >= deadline`(仅显式预算登记) | 通知 `NET_ERR_TIMEDOUT`(对外 -2 映射保持) | 状态无关;用有符号回绕安全差值,防 tick 回绕;这是 `net_recv_to` 调用方策略的正常兑现,不是伪造 |
|
||||
| 3 | **断连**(`CLOSED` 或 `session_dead`) | 排空:有数据则读出;否则按 `released_abnormal` 给 `CONNRESET` 或 0(EOF),并 `auto_relisten` + 通知 | CLOSE 先行完成挂起 = `net_close` 的 settle 钩子 |
|
||||
| 4 | **同 fd 后续操作**:close / drop_conn / 对账 resync | 先结束挂起 recv,再执行新操作 | per-fd 串行;同 fd 第二个 recv 被登记守卫拒绝(BUSY);send 无需结算(收发不冲突,SPI 由 netTask 串行) |
|
||||
| 5 | **网络重建**(`net_stack_bring_up`,芯片复位) | **取消 + 必须通知** | `net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;**取消不通知 = 调用方永久挂死** |
|
||||
|
||||
### 5.2 挂起 recv 逐路判据与动作
|
||||
|
||||
**第 1 路 · 数据到达**(`net_pending_tick` 内):
|
||||
|
||||
- 门控:`state == ESTABLISHED || state == UDP_OPEN` 且 `readable != 0`;
|
||||
- 命中后 `ch395f_get_recv_len` → 有数据则 `min(可用, len)` 读出、累加 `recv_bytes`、
|
||||
清 `readable`、以字节数完成;`avail == 0`(虚假标志)只清 `readable` 继续等待。
|
||||
- **零额外 SPI 的来源**:`readable` 由 `net_poll` 每轮刷新(INT# 电平触发链保证缓冲有数据时
|
||||
每轮重读 SINT 重位置位),`net_pending_tick` 不发起探测事务。
|
||||
|
||||
**第 2 路 · 到点**:
|
||||
|
||||
- 判据 `(p->infinite == 0) && ((int32_t)(now - p->deadline) >= 0)`;有符号差值抗 tick 回绕。
|
||||
- 无限等待登记(`infinite == 1`)**无 deadline,永不因时间结束**——这是调用方策略。
|
||||
|
||||
**第 3 路 · 断连**:
|
||||
|
||||
- 判据 `state == CLOSED || session_dead != 0`(`session_dead` 是"会话已结束"的**粘滞电平**,
|
||||
定义见 `Drivers/BSP/NET/net_types.h`)。
|
||||
⚠ **判据不可收窄**:只判 `CLOSED` 时,抢先 relisten 后第 1 路(要求 `ESTABLISHED`)与
|
||||
第 3 路(要求 `CLOSED`)两头落空,挂起只能干等第 2 路;无限等待登记没有第 2 路,
|
||||
**漏判 = 永久挂死**(留痕探针只留痕、不唤醒)。
|
||||
- 动作:查芯片残留 → 有则按数据完成;无则内层 `state == CLOSED && auto_relisten` 时
|
||||
`net_listener_try()` 即时重建,随后按 `released_abnormal` 决定 `CONNRESET`(异常释放)
|
||||
或 `0`(正常 FIN)。
|
||||
- ⚠ 内层 `state == CLOSED` 门是**必需的**,不是冗余:`net_listener_try` 无状态门,
|
||||
对已 `LISTENING` 的 fd 再 `open+listen` 会因 `USE` 错误走进关闭分支、关掉正听着的 Socket。
|
||||
- 该处**保留即时重建**(而非只交 reconciler):它只在 `p->active != 0`(有挂起 recv)时
|
||||
到达,属"消费者在等这条连接结局"的**消费者驱动路径**;删掉会把 CLOSED 窗口拉长到
|
||||
reconciler 周期 + 隔离期(~100ms),期间调用方高频 `net_accept` → `[WD] task stall`。
|
||||
|
||||
**第 4 路 · 同 fd 后续操作**:
|
||||
|
||||
- `net_close_locked` / `net_drop_conn_locked` 的**首行**调 `net_pending_settle_close`
|
||||
(有数据先送达、无数据取消,**必通知**),再动芯片;
|
||||
- 对账形态⑤(芯片静默回收回 LISTEN)走 `net_pending_cancel(i, CONNRESET)`——连接已被芯片
|
||||
收走,无排空窗口,直接取消。
|
||||
|
||||
**第 5 路 · 网络重建**:
|
||||
|
||||
- `net_init` 顶部(控制块清零与芯片复位**之前**)`net_pending_cancel_all(NET_ERR_CONNRESET)`;
|
||||
- 遍历 8 个 fd 逐一通知,未在位者静默跳过。
|
||||
|
||||
### 5.3 为什么 recv 判据顺序是 ①→②→③
|
||||
|
||||
1. **数据优先于断连**:断连时芯片缓冲可能仍有残数据(bit3=1 契约),先判数据可让
|
||||
最后一次读到完整内容,避免"断连即丢尾包";
|
||||
2. **数据优先于到点**:数据与到点同时成立时交付数据(更贴近调用方意图,且
|
||||
`net_recv_to` 的调用方以"本片有数据"为准继续累计预算);
|
||||
3. **到点优先于断连**:显式预算登记在超时时刻应兑现超时语义,即便此刻连接恰好已断——
|
||||
调用方拿到 `-2` 后按自己的预算循环决定是否续轮,下一轮会拿到断连结果。
|
||||
|
||||
### 5.4 recv 状态推断的克制
|
||||
|
||||
`CONNECTING` / `LISTENING` 等中间态**保持等待**,**不做**状态推断式取消——
|
||||
`CONNECT → ESTABLISHED` 是合法迁移,推断取消会误杀正在建链的等待。陈旧 resync
|
||||
(芯片静默回收)由显式 `net_pending_cancel` 处理,不靠推断。
|
||||
|
||||
### 5.5 挂起 send 的完成与取消(四路 + 显式取消)
|
||||
|
||||
send 与 recv 语义不同:recv 是"等一次到达即完成",send 是"**分块推进直到全部写完**"。
|
||||
因此 `net_send_pending_tick()` 每轮对每个挂起 send 按序判定 **①断连 → ②推进 → ③写完 → ④无进展超时**,
|
||||
另有显式取消路径(close / drop_conn / resync / 重建)。
|
||||
|
||||
| # | 条件 | 动作 | 设计要点 |
|
||||
|---|---|---|---|
|
||||
| 1 | **断连**(`state != ESTABLISHED` 或 `session_dead`) | 以**已发字节数**结束 | 剩余字节不可能送达;判据同快速路径——离开 `ESTABLISHED` 后即便再回来也是**新会话**,旧字节绝不投到新连接;`session_dead` 为冗余保险 |
|
||||
| 2 | **推进**:`sent < len` 且 `send_ready != 0` | 写下一块(`min(剩余, NET_SEND_CHUNK_MAX)`),清 `send_ready`,刷新无进展计时 | **每轮至多一块**——写完即清 `send_ready`,下一块须等 `SENBUF_FREE` 中断经 `net_poll` 重新置位;与快速路径共用分块粒度(TRAP-NET-16) |
|
||||
| 3 | **全部写完**(`sent >= len`) | 以**总长度**结束 | 调用方见 `ret == len` 即成功 |
|
||||
| 4 | **无进展超时**(`now >= deadline`) | 以**已发字节数**结束(短写)+ `TIMEDOUT` | 停滞计时按**无进展**刷新(每次成功写块重置)——大文件传输总时长必然超过单块预算,按总时长判会误杀;这与 recv 侧"到点"是**有意的语义差异** |
|
||||
| — | **显式取消**:close / drop_conn / resync / 重建 | 以**已发字节数**结束,原因记 `net_set_errno` + `[ERR]` | 已写入芯片的字节**不回收**(对端可能已收到);未发部分放弃,由调用方按短写处理。**取消必通知**(同 recv 铁律) |
|
||||
|
||||
**为什么 send 结果恒为"已发字节数"而非错误码**:`net_send` 的契约是"返回实际写入字节数",
|
||||
短写是**一等公民**(`ret < len` 即短写,调用方重试剩余)。断连/超时/取消都不伪造"全部成功",
|
||||
也不把结果改写为负错误码——原因经 `net_set_errno` 记录在 netTask 自己的槽位,调用方以
|
||||
`ret != len` 判定短写(详见 `docs/网络/BSD_Socket_API_使用指南.md` §3.5)。
|
||||
|
||||
**为什么 send 用"无进展"而非"总时长"计时**:recv 的"到点"是调用方**显式预算**的兑现(一次等待
|
||||
一个 deadline 就够);send 的等待是**多次块间等待的累积**,一个 100 KB 传输在健康链路下也要
|
||||
经历上百次 `send_ready` 等待,总时长远超单块预算。若按总时长判,正常大传输会被误杀成超时——
|
||||
故 `deadline` 每次成功写块后重置为"当前 + `NET_SEND_TIMEOUT_MS`",只判"块间停滞"。
|
||||
|
||||
---
|
||||
|
||||
## 6. 模块接口契约
|
||||
|
||||
### 6.1 接口总表
|
||||
|
||||
全部声明在 `Drivers/BSP/NET/net_pending.h`,实现见 `Drivers/BSP/NET/net_pending.c`。
|
||||
recv 与 send 各一组(send 组前缀 `net_send_pending_`),共 14 个函数。
|
||||
|
||||
**公共 / recv 组**:
|
||||
|
||||
| 函数 | 上下文 | 前置条件 | 返回 | 副作用 | 调用者 |
|
||||
|---|---|---|---|---|---|
|
||||
| `net_pending_capture_owner(void)` | 执行任务 | 创建消息队列前 | — | 捕获 `s_owner_task` | `net_init` 顶部 |
|
||||
| `net_pending_register(fd, buf, len, to_ms, caller)` | netTask | 无在位挂起;`caller` 非空且非执行者 | `NET_PENDING_REGISTERED` / `-1`(BUSY/BADF) | 占槽、写字段 | `net_recv_locked` 阻塞无数据路径 |
|
||||
| `net_pending_tick(p_socks)` | netTask | `p_socks = s_net_socks` | — | 完成命中的挂起(通知 + 清槽) | `net_process_messages` 开头 |
|
||||
| `net_pending_complete(fd, result)`(static) | netTask | — | — | 通知调用方、清槽、失败记 errno | 本模块内部所有完成/取消路径 |
|
||||
| `net_pending_clear(fd)` | **任意** | — | — | 静默清槽(不通知) | `alloc_socket` / `net_close_locked` 尾部 |
|
||||
| `net_pending_cancel(fd, err)` | netTask | — | — | 经 `complete` 通知 | `net_reconcile_tick` 形态⑤ |
|
||||
| `net_pending_cancel_all(err)` | netTask | 芯片复位之前 | — | 逐 fd 通知 | `net_init` 顶部 |
|
||||
| `net_pending_settle_close(fd, p_sock)` | netTask | `ch395f_close_socket` 之前 | — | 排空或取消 + 通知 | `net_close_locked` / `net_drop_conn_locked` 首行 |
|
||||
| `net_pending_active(fd)` | **任意**(纯 RAM 读) | — | `1`/`0` | 无 | `net_listener_retry_tick` 挂起保险 |
|
||||
|
||||
**send 组**(与 recv 组一一对称):
|
||||
|
||||
| 函数 | 上下文 | 前置条件 | 返回 | 副作用 | 调用者 |
|
||||
|---|---|---|---|---|---|
|
||||
| `net_send_pending_register(fd, buf, len, sent, caller)` | netTask | 无在位挂起 send;`caller` 非空且非执行者;`0 ≤ sent < len` | `NET_PENDING_REGISTERED` / `-1`(BUSY/BADF/INVAL) | 占槽、写字段 | `net_send_locked` 超预算路径 |
|
||||
| `net_send_pending_tick(p_socks)` | netTask | `p_socks = s_net_socks` | — | 推进/完成命中的挂起 send(通知 + 清槽) | `net_process_messages` 开头 |
|
||||
| `net_send_pending_complete(fd, result)`(static) | netTask | — | — | 通知调用方、清槽 | 本模块内部所有 send 完成/取消路径 |
|
||||
| `net_send_pending_clear(fd)` | **任意** | — | — | 静默清槽(不通知) | `alloc_socket` / `net_close_locked` 尾部 |
|
||||
| `net_send_pending_cancel(fd, err)` | netTask | — | — | 经 `complete` 通知(结果 = 已发字节数) | `net_close_locked` / `net_drop_conn_locked` / `net_reconcile_tick` 形态⑤ |
|
||||
| `net_send_pending_cancel_all(err)` | netTask | 芯片复位之前 | — | 逐 fd 通知 | `net_init` 顶部 |
|
||||
| `net_send_pending_active(fd)` | **任意**(纯 RAM 读) | — | `1`/`0` | 无 | `net_listener_retry_tick` 挂起 send 保险 |
|
||||
|
||||
> `net_pending_complete` / `net_send_pending_complete` 是各自方向**唯一**的通知出口,
|
||||
> `clear` 是**唯一**静默清槽出口——任何新增路径必须二选一,不得自行写
|
||||
> `s_net_pending[fd].active = 0` / `s_net_send[fd].active = 0`。
|
||||
|
||||
### 6.2 关键契约细则
|
||||
|
||||
- **通知值即结果**:`xTaskNotify(caller, (uint32_t)result, eSetValueWithOverwrite)`,
|
||||
32 位整体不切分;`result < 0` 时先 `net_set_errno(result)`(记入 netTask 任务槽位)。
|
||||
- **失败码回传路径(两条)**:快速路径由 `net_process_messages` 回传该条消息**自身**的
|
||||
错误码;挂起路径由 `net_pending_complete` 记 errno 并把负值作为通知值——两处取的都是
|
||||
netTask 自己的 errno 槽位(E-03 后按任务隔离),不受其他任务影响。
|
||||
- **`NET_PENDING_REGISTERED` 哨兵**:`0x70000000`,取值不与字节数空间冲突;分派器据此
|
||||
置 `do_notify = 0`,把回执推迟到真正的完成时刻,避免"登记成功"被误报为处理结果。
|
||||
- **`net_recv_to` 与 `timeout_ms = 0`**:`net_recv_to` 拒收 `timeout_ms == 0`(`INVAL`),
|
||||
因为 0 在 netTask 侧意味着"无限等待",与该接口"有界等待"的存在理由相反。
|
||||
- **`-2` 映射**:`net_recv_to` 见到 `NET_ERR_TIMEDOUT` 返回 `-2`(**先于**通用 `< 0` 分支
|
||||
判断,否则 `-9` 会被吞成 `-1`);超时只凭返回码判定,不依赖 errno(无跨任务竞态)。
|
||||
- **send 短写契约**:`net_send` 返回值恒为**已写入字节数**(`≥ 0`);`= len` 为全部写入,
|
||||
`< len` 为短写,调用方**必须**检查并重试剩余(`ui_net_sink` / `iec104_serv` 按
|
||||
`ret != total` 判短写;`lftpd_inet_write` 循环 `while (n > 0)`、`n <= 0` 视为致命)。
|
||||
断连/超时/取消均以已发字节数结束,**不伪造"全部成功"、不改写为负错误码**——原因记
|
||||
netTask 自己的 errno 槽位。
|
||||
- **`net_send_sock`(netTask 内部版)**:其阻塞用法会把 `caller` 置为 netTask 自身,
|
||||
登记必被"拒绝执行者自等待"守卫拒(`BUSY`)→ 返回 `-1`。故该接口只适合"发送缓冲可用"
|
||||
的场合;netTask 内需长时间等待的发送须自行分片(见 §7.2 集成点 13)。
|
||||
|
||||
### 6.3 编译期开关
|
||||
|
||||
| 宏 | 位置 | 默认 | 作用 |
|
||||
|---|---|---|---|
|
||||
| `NET_PENDING_TRIPWIRE` | `Drivers/BSP/NET/net_pending.h` | `0`(产品构建编译剔除,零成本) | 开启挂起超时留痕探针(见 §12) |
|
||||
| `NET_RECV_TRIPWIRE_MS` | 同上(仅探针开启时定义) | `60000` | 留痕阈值 |
|
||||
| `NET_SEND_SPIN_MS` | `Drivers/BSP/NET/net_config.h` | `2` | **发送快速路径**的有界自旋预算(ms):单次"等发送缓冲腾出"超过本值即登记挂起 send,把 netTask 最坏停滞由 `NET_SEND_TIMEOUT_MS`(1 s)压到本值。健康链路实测块间等待 med 219 µs / max 378 µs(≈5× 余量),故正常发送不会触发挂起 |
|
||||
| `NET_SEND_TIMEOUT_MS` | `Drivers/BSP/NET/net_config.h` | `1000` | 发送**无进展**超时(ms):快速路径的块间停滞上限;挂起路径的 `deadline` 亦按本值刷新。`0` = 不限 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 实现落点(集成步骤)
|
||||
|
||||
本节是"**改/查本模型时看哪里**"的落地清单:每个集成点若缺失或改动,后果都是确定的。
|
||||
|
||||
### 7.1 集成点总表
|
||||
|
||||
**recv 组**(编号 1–12 沿用一期):
|
||||
|
||||
| # | 集成点 | 文件 / 函数 | 调用 | 缺失后果 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 执行者句柄捕获 | `net_socket.c` `net_init` 顶部 | `net_pending_capture_owner()` | 自等待守卫失效(`s_owner_task` 恒 NULL)→ netTask 内阻塞 recv/send 登记成功 → **死锁** |
|
||||
| 2 | 重建前取消 | `net_socket.c` `net_init` 顶部(芯片复位前) | `net_pending_cancel_all(NET_ERR_CONNRESET)` | 重建前挂起的调用方**永久挂死** |
|
||||
| 3 | 每轮完成检查 | `net_socket.c` `net_process_messages` 开头 | `net_pending_tick(s_net_socks)` | 挂起**永不完成** |
|
||||
| 4 | 挂起登记与回执抑制 | `net_socket.c` `net_process_messages` 的 `NET_MSG_RECV` 分支 | `net_recv_locked` 返回哨兵 → `do_notify = 0` | "登记成功"被当结果回执(误报) |
|
||||
| 5 | 关闭结算 | `net_socket.c` `net_close_locked` 首行 | `net_pending_settle_close` | close 丢已到数据 / 调用方挂死 |
|
||||
| 6 | 拆会话结算 | `net_socket.c` `net_drop_conn_locked` 首行 | `net_pending_settle_close` | 同上 |
|
||||
| 7 | 静默回收取消 | `net_socket.c` `net_reconcile_tick` 形态⑤ | `net_pending_cancel(i, NET_ERR_CONNRESET)` | 芯片静默回收后挂起 recv 无唤醒源 |
|
||||
| 8 | 重开 Socket 保险 | `net_listener.c` `net_listener_retry_tick` | `net_pending_active(i)` 命中则 `continue` | 重开 Socket **摧毁在途连接** |
|
||||
| 9 | 新 fd 清槽 | `net_socket.c` `alloc_socket` | `net_pending_clear(sockfd)` | fd 复用继承旧挂起 → 新会话立即 EOF / 结果错位 |
|
||||
| 10 | close 幂等清槽 | `net_socket.c` `net_close_locked` 尾部 | `net_pending_clear(sockfd)` | 槽位残留(虽经 settle 已通知,仍须归零) |
|
||||
| 11 | 唤醒通道(两段式) | `net_socket.c` `net_wait_result` | `xTaskNotifyWait` 零等待 + 阻塞 | 通知丢失 → 调用方挂死 |
|
||||
| 12 | 双端注册 | `MDK-ARM/STM32F407-Demo.uvprojx` + `simulator/CMakeLists.txt` | `net_pending.c` 只入真机 | 编译缺失 / 模拟器误编(§7.4) |
|
||||
|
||||
**send 组**(编号 13–19,二期;与 recv 组一一对称):
|
||||
|
||||
| # | 集成点 | 文件 / 函数 | 调用 | 缺失后果 |
|
||||
|---|---|---|---|---|
|
||||
| 13 | 发送有界自旋 | `net_socket.c` `net_send_locked` 两个等待循环 | 超 `NET_SEND_SPIN_MS` 即 `net_send_pending_register(...)` | netTask 停滞回到 1 s(队头阻塞);须在 1 s 超时判定**之后**再判自旋预算,保证 `SPIN > TIMEOUT` 误配时仍以超时收口 |
|
||||
| 14 | 每轮推进 | `net_socket.c` `net_process_messages` 开头(`net_pending_tick` 之后) | `net_send_pending_tick(s_net_socks)` | 挂起 send **永不推进 / 永不完成** |
|
||||
| 15 | send 登记与回执抑制 | `net_socket.c` `net_process_messages` 的 `NET_MSG_SEND` 分支 | `net_send_locked` 返回哨兵 → `do_notify = 0` | "登记成功"被当结果回执(误报) |
|
||||
| 16 | 关闭 / 拆会话取消 | `net_socket.c` `net_close_locked` / `net_drop_conn_locked` | `net_send_pending_cancel(fd, NET_ERR_CONNRESET)` | 挂起 send 的调用方**永久挂死**(已发字节数收不回来) |
|
||||
| 17 | 静默回收取消 | `net_socket.c` `net_reconcile_tick` 形态⑤ | `net_send_pending_cancel(i, NET_ERR_CONNRESET)` | 芯片静默回收后挂起 send 无唤醒源 |
|
||||
| 18 | 重建前取消 | `net_socket.c` `net_init` 顶部(芯片复位前) | `net_send_pending_cancel_all(NET_ERR_CONNRESET)` | 重建前挂起的调用方**永久挂死** |
|
||||
| 19 | 新 fd / close 清槽 | `net_socket.c` `alloc_socket` / `net_close_locked` 尾部 | `net_send_pending_clear(sockfd)` | fd 复用继承旧挂起 send → 新会话误写旧缓冲 |
|
||||
| 20 | send 重开保险 | `net_listener.c` `net_listener_retry_tick` | `net_send_pending_active(i)` 命中则 `continue` | 重开 Socket **摧毁在途传输**(半写的数据流被截断) |
|
||||
|
||||
### 7.2 逐集成点要点
|
||||
|
||||
**1–2. `net_init` 顶部顺序**(`Drivers/BSP/NET/net_socket.c`):
|
||||
|
||||
```text
|
||||
net_pending_cancel_all(NET_ERR_CONNRESET) ← 必须最早(芯片复位前)
|
||||
memset(s_net_socks, 0, ...) ← 控制块清零
|
||||
net_pending_capture_owner() ← 捕获执行者
|
||||
... ch395f_check_exist / ch395f_reset ... ← 芯片复位
|
||||
```
|
||||
|
||||
顺序不可交换:取消必须先于清零与复位,否则挂起调用方既收不到通知、其 `caller`
|
||||
也可能随控制块一起失效。
|
||||
|
||||
**3–4 / 14–15. `net_process_messages` 开头与分派**:
|
||||
|
||||
```text
|
||||
net_listener_retry_tick() ← D10
|
||||
net_pending_tick(s_net_socks) ← 挂起 recv 完成检查
|
||||
net_send_pending_tick(s_net_socks) ← 挂起 send 推进/完成检查
|
||||
net_reconcile_tick() ← 对账 + 重建
|
||||
while (osMessageQueueGet(..., 0) == osOK) { ... }
|
||||
```
|
||||
|
||||
`NET_MSG_RECV` 分支:`msg.result = net_recv_locked(...)`;`NET_MSG_SEND` 分支:
|
||||
`msg.result = net_send_locked(..., msg.caller)`;两者若等于 `NET_PENDING_REGISTERED`
|
||||
则 `do_notify = 0`(**不发即时回执**)。send 分支还须把 `msg.caller`(调用方任务句柄)
|
||||
透传给 `net_send_locked`——挂起登记的目标。四个 tick 均先于消息分派。
|
||||
|
||||
**5–6. 结算先于拆链**:`net_close_locked` / `net_drop_conn_locked` 的**第一行**就是
|
||||
`net_pending_settle_close(...)`——"关闭不得让等待中的调用方永久悬挂",且"关闭不丢已到达
|
||||
数据(芯片缓冲在 close 前可读)"。
|
||||
|
||||
**8 / 20. 挂起保险(recv 与 send)**:`net_listener_retry_tick` 对每个待重试 fd,先做状态门
|
||||
(仅 `LISTENING`/`CLOSED` 才重试),再 `net_pending_active(i) || net_send_pending_active(i)`
|
||||
命中即 `continue`——重开 Socket 会摧毁在途连接(recv)与在途传输(send 的半写数据流)。
|
||||
|
||||
**11. 两段式等待**(`net_wait_result`,所有 `net_*` API 共用):
|
||||
|
||||
- 第一段零等待拾取,消费"入队 → 等待入口"间隙内已落地的通知;
|
||||
- 判定必须用返回值 `pdTRUE`/`pdFALSE`,**不能判值 ≠ 0**(0 = EOF 是合法通知);
|
||||
- 清除掩码必须全 1(传 0 = 不清除,会残留上次结果被误读为本次结果);
|
||||
- 禁止在本路径加逐调用日志(曾占日志 98.6%,冲掉 `[LAT]` 证据行)。
|
||||
|
||||
**13. 发送有界自旋**(`net_send_locked`):两个等待循环(首块等待 + 每块发完等待)在
|
||||
`net_poll()` 之后判两件事,**顺序不可交换**:先判 `NET_SEND_TIMEOUT_MS`(1 s 停滞 → 记
|
||||
`TIMEDOUT` 收口),再判 `NET_SEND_SPIN_MS`(超预算 → `net_send_pending_register(...)` 返回
|
||||
哨兵)。把超时判定放在自旋判定**之前**,是为了保证 `NET_SEND_SPIN_MS > NET_SEND_TIMEOUT_MS`
|
||||
的误配下仍以超时收口,不会"永远登记不出去"。健康路径块间等待 med 219 µs ≪ 2 ms,**永不触发**
|
||||
挂起——故快速路径行为与挂起化前逐字节一致。
|
||||
|
||||
**14–19. send 组生命周期**:`net_send_pending_tick` 每轮按 ①断连 → ②推进 → ③写完 → ④超时
|
||||
(§5.5)推进;`net_close_locked` / `net_drop_conn_locked` / `net_reconcile_tick` 形态⑤ /
|
||||
`net_init` 顶部四处调 `net_send_pending_cancel(_all)`(**必通知**,否则调用方永久挂死);
|
||||
`alloc_socket` / `net_close_locked` 尾部调 `net_send_pending_clear`(静默归零)。
|
||||
|
||||
### 7.3 改动检查清单
|
||||
|
||||
**新增一种"结束挂起 recv"的条件**:
|
||||
|
||||
- [ ] 在 `net_pending_tick` 的 ①②③ 判定链中插入,或走显式 `net_pending_cancel*`;
|
||||
- [ ] 所有出口**必须**经 `net_pending_complete`(不得直接清 `active`);
|
||||
- [ ] 若新条件是"取消",**必须通知**(取消不通知 = 永久挂死);
|
||||
- [ ] 复核第 1 路的 `readable` 门控与第 3 路的判据未被新条件破坏(两头落空即挂死);
|
||||
- [ ] 更新本文 §5 与 `docs/网络/BSD_Socket_API_使用指南.md` §3.5。
|
||||
|
||||
**新增一个使用阻塞 recv 的消费者**:
|
||||
|
||||
- [ ] 确认该任务**不在** IWDG `required_mask` 内;若在,**禁止无限阻塞**,必须
|
||||
`net_select` 超时或 `net_recv_to` 显式预算(契约见 §8);
|
||||
- [ ] `buf` 保持有效至调用返回;
|
||||
- [ ] 保留"总预算 − 已耗时"循环骨架(`n == -2` 续轮)。
|
||||
|
||||
**新增一种"结束挂起 send"的条件**:
|
||||
|
||||
- [ ] 在 `net_send_pending_tick` 的 ①②③④ 判定链中插入,或走显式 `net_send_pending_cancel*`;
|
||||
- [ ] 所有出口**必须**经 `net_send_pending_complete`(不得直接清 `active`);
|
||||
- [ ] 若新条件是"取消",**必须通知**(取消不通知 = 永久挂死);
|
||||
- [ ] 结果恒为**已发字节数**(短写语义),**不得**改写为负错误码——原因走 `net_set_errno`;
|
||||
- [ ] 复核"无进展计时刷新"未被新条件破坏(按总时长判会误杀大传输);
|
||||
- [ ] 更新本文 §5.5 与 `docs/网络/BSD_Socket_API_使用指南.md` §3.5。
|
||||
|
||||
**新增一个 socket fd 生命周期路径**:任何 `memset(p_sock)` / 释放 fd 的路径都须
|
||||
配套 `net_pending_clear(fd)`(新 fd 不得继承)或 `net_pending_settle_close`(关闭前结算),
|
||||
**两个方向都要**(recv 与 send 各自一张表)——漏掉 send 侧会让新 fd 误写旧缓冲。
|
||||
|
||||
### 7.4 双端注册与模拟器边界
|
||||
|
||||
- **真机**:`Drivers/BSP/NET/net_pending.c` 必须注册进 `MDK-ARM/STM32F407-Demo.uvprojx`
|
||||
的 NET 分组(`net_listener.c` 旁)。
|
||||
- **模拟器**:`simulator/CMakeLists.txt` **不编译** `net_pending.c` 与 `net_listener.c`
|
||||
——模拟器走 `simulator/sim/net_socket_sim.c`(单线程 Winsock,无法复现挂起模型内部机制;
|
||||
外部语义已由包装层按 blocking 属性/预算模拟)。故挂起表 / tick / 通知路径在模拟器上
|
||||
**零覆盖**,只能硬件验证。
|
||||
|
||||
---
|
||||
|
||||
## 8. 调用方契约
|
||||
|
||||
调用方**不改协议、不改 API**,但需知悉五条:
|
||||
|
||||
| 契约 | 说明 |
|
||||
|---|---|
|
||||
| `buf` 生命周期 | 挂起期间 `buf` 指针须**保持有效**至调用返回(挂起表按指针写入) |
|
||||
| `buf` 生命周期 | 挂起期间 `buf` 指针须**保持有效**至调用返回(挂起表按指针写入,recv / send 同) |
|
||||
| **send 短写** | `net_send` 返回值恒为已写入字节数:`= len` 全部写入,`< len` 为短写,调用方**必须**检查并重试剩余(`ui_net_sink` / `iec104_serv` 按 `ret != total` 判短写;`lftpd_inet_write` 循环 `while (n > 0)`、`n <= 0` 视为致命)。挂起 send 以已发字节数结束,不会伪造全部成功 |
|
||||
| 剩余预算循环 | 调用方保留"总预算 − 已耗时"循环骨架(`n == -2` 续轮,预算耗尽退出)——这是**公开契约要求**(30s 总超时语义 + 跨 TCP 分段累积),与本架构无关 |
|
||||
| 超时精度 | 完成判定随主循环节拍(~1ms);主循环被长操作占用时,超时**最多顺延**相应时长(已知代价,见 §7) |
|
||||
| IWDG 内禁无限阻塞 | IWDG `required_mask` 内任务**禁止无限阻塞 recv**——必须 `net_select` 超时或 `net_recv_to` 显式预算("超时归使用者"对受监控任务是义务不是选项,否则"对端连上不发数据" = IWDG 复位) |
|
||||
| 超时精度 | 完成判定随主循环节拍(~1ms);主循环被长操作占用时,超时**最多顺延**相应时长(已知代价,见 §10) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 性能特征(已验证)
|
||||
## 9. 性能特征
|
||||
|
||||
| 指标 | 值 | 条件 |
|
||||
| 指标 | 值 | 来源 / 条件 |
|
||||
|---|---|---|
|
||||
| 队列响应性(挂起连接下) | **92.5ms** | 挂起对时客户端 + FTP banner 探测 |
|
||||
| M3 静默档帧周期 | med 58.9 / p95 69.9 / max 72.1ms | 零尖峰 |
|
||||
| M3 压力档帧周期 | med 60.1 / p95 66.4 / max 69.3ms | 零尖峰,p99 尾部 133.5→69.3ms |
|
||||
| netTask 栈峰值 | 488B / 4096(余量 88%) | 零占用设计落点 |
|
||||
| 队列响应性(挂起连接下) | **92.5 ms** | `test/host/e02_probe.py`:挂起对时客户端 + FTP banner 探测 |
|
||||
| M3 静默档帧周期 | med 58.9 / p95 69.9 / max 72.1 ms | `docs/测试说明/链路延迟测试说明.md` §6.3(零尖峰) |
|
||||
| M3 压力档帧周期 | med 60.1 / p95 66.4 / max 69.3 ms | 同上(零尖峰;p99 尾部 133.5 → 69.3 ms) |
|
||||
| netTask 栈实测峰值 | **1116 B / 3840 B(余 70%)**,FTP 传输期峰 **1164 B**;静态最坏链 ≈1.9 KB | `docs/内存管理/RAM使用分析与优化方案.md` §2.3;2026-10-14 台架复测(含 `net_send_pending_tick` 新增栈帧) |
|
||||
|
||||
**结构性收益(为什么这些数字成立)**:压力档下 MCU 侧 `prod_cycle_mcu` 6706 µs 纹丝不动
|
||||
(min 6698 / max 6720)——队头阻塞工况不再向 UI 帧周期传导;`net_pending_tick` 在无挂起
|
||||
时是纯指针读(零 SPI),故 L5 队列往返段与挂起化前逐位不变。
|
||||
|
||||
**SEND pending 的结构性收益**(二期,`net_send_pending_tick`):发送停滞期 netTask 不再
|
||||
被冻结——netTask 最坏停滞由 `NET_SEND_TIMEOUT_MS`(1 s)压到 `NET_SEND_SPIN_MS`(2 ms),
|
||||
且停滞不再顺延 `net_pending_tick` 的挂起 recv 唤醒。健康链路块间等待 med 219 µs / max 378 µs
|
||||
(≈5× 余量),故**正常发送完全不触发挂起**,快路径逐字节不变。
|
||||
静态代价:`s_net_send[]` **+224 B**(RW_IRAM1,8×28 B,见 §4.5)。
|
||||
|
||||
> ✅ **2026-10-14 台架复测已补测**:SEND pending 改动后 netTask 栈峰值 **1116 B / 3840 B**
|
||||
> (开机态)~ **1164 B**(FTP 传输期),较此前 920 B 增加 ≈200 B(`net_send_pending_tick`
|
||||
> 新增一层调用帧),余量 69~70%,远低于 1.9 KB 静态最坏链。复测同步发现并修复了
|
||||
> 末块挂起越界缺陷(见 §9.1)。
|
||||
|
||||
### 9.1 末块挂起越界缺陷(2026-10-14 台架复测发现并修复)
|
||||
|
||||
**现象**:SEND pending 二期上线后台架复测,`ftp_regression.py` 间歇性 1/11(部分步骤
|
||||
`write error, ret=-1`),偶可 11/11;基线固件同测 6/6 恒过 → 判定为**本次改动引入的回归**。
|
||||
|
||||
**根因**:`net_send_locked` 的内层"等本块发完(SENBUF_FREE)"循环位于**写块之后**。
|
||||
写完**末块**时 `sent` 已累加到 `len`,若该次 SENBUF_FREE 等待超过 `NET_SEND_SPIN_MS`
|
||||
(2 ms),代码径自调用 `net_send_pending_register(..., sent, ...)`。而登记函数的参数
|
||||
校验含 `sent >= len → INVAL` 拒绝(本模型下 `sent == len` 意味着**无剩余块可挂起**),
|
||||
返回 -1 被 `net_send_locked` 直接透传 → 一次**实际已完整交付芯片**的发送被报成失败。
|
||||
`ftp_regression` 的 RETR 1KB 是单块帧(一次即 `sent == len`),故首当其冲。
|
||||
|
||||
**修复**:内层等待循环**开头**判 `sent >= len` → 直接跳出等待返回 `sent`
|
||||
(== `len`,完整交付)——末块写完后已无下一块,"等 SENBUF_FREE"是纯保守自旋,
|
||||
其等待顺延为下一批的责任。该守卫**置于超时/自旋判定之前**,末块路径零额外延迟;
|
||||
同时它保证后面的 `net_send_pending_register` 调用必伴随 `sent < len`,登记必获受理。
|
||||
外层写块前的挂起判定不受影响(循环条件 `sent < len` 未被改写)。
|
||||
|
||||
**为什么此前没暴露**:健康链路块间等待 med 219 µs ≪ 2 ms,末块等待极少越过预算;只有
|
||||
在末块 SENBUF_FREE 变慢(FTP 数据连接刚建立、客户端读速不匹配等)时才触发,故表现为
|
||||
间歇性。**教训**:自旋预算与"末块"边界是两个正交维度,二者的组合必须显式覆盖——
|
||||
写完末块后"还差多少没写"的答案已是 0,此时任何"等缓冲区"型预算分支都不该继续走。
|
||||
|
||||
**复测结论**(修复后):`ftp_regression` 连续 **19 次 11/11**(含守卫重排版后重烧 5 次)、
|
||||
`ftp_integrity_test` 20 轮 × 64 KB STOR/RETR SHA-256 **20/20 一致**(两轮,吞吐稳定
|
||||
≈80 KB/s)、设备日志零 write error、无 forbidden 标记、ST-LINK 回读 `No difference found`。
|
||||
|
||||
---
|
||||
|
||||
## 7. 设计边界与已知代价
|
||||
## 10. 设计边界与已知代价
|
||||
|
||||
| 项 | 现状 | 说明 |
|
||||
|---|---|---|
|
||||
| **SEND 路径未覆盖** | 发送仍为分块阻塞(1s/chunk stall) | 该期间队列仍被冻结;二期 pending 化解决 |
|
||||
| **ACCEPT 路径未覆盖** | accept 仍用轮询计时 | 需 pending 化(二期) |
|
||||
| **超时精度** | 随主循环节拍(~1ms),被长操作占用时顺延 | 一期文档化、不修;二期 SEND 化后该顺延面**收窄** |
|
||||
| **SEND 路径**(执行者占用问题)✅ **二期已覆盖** | `net_send_locked` 快路径有界自旋 ≤ `NET_SEND_SPIN_MS`(2 ms);超预算登记挂起 send,由 `net_send_pending_tick` 按主循环节拍分块推进(`NET_SEND_CHUNK_MAX` = 1 KB/块),调用方在自己上下文睡 | 停滞期 netTask **不再冻结**:挂起 send 每轮至多写一块,`net_pending_tick` 与消息队列照常推进——挂起 recv 的最坏唤醒延迟不再被 ≈1 s 顺延。健康链路块间等待 med 219 µs ≪ 2 ms,快速路径行为与挂起化前逐字节一致。`NET_SEND_TIMEOUT_MS` 降级为**无进展**超时(每次成功写块刷新,见 §5.5) |
|
||||
| **ACCEPT 路径未覆盖**(调用方轮询问题) | `net_accept_locked` 单次 `net_poll` 后**立即**返回 `ESTABLISHED` 或 `WOULDBLOCK`——**不占用 netTask**(0 停滞) | 代价不在执行者,而在**调用方轮询**:没有"阻塞到有连接"的入口,各服务(`ui_net_sink` / `iec104_serv` 100 ms / `time_sync` / `lftpd`)各自手写轮询循环,每轮一次完整 RPC 往返(入队 + 通知 + 唤醒),并带来一个轮询周期级的 accept 延迟抖动。pending 化的收益是**消除轮询 churn 与抖动**,不是解队头阻塞 |
|
||||
| **超时精度** | 随主循环节拍(~1ms),被长操作占用时顺延 | 一期文档化、不修;SEND 化后该顺延面**收窄**(发送停滞不再顺延整层) |
|
||||
| **挂起超时留痕探针** | 编译期选项 `NET_PENDING_TRIPWIRE` 默认关(排查挂死时开);未接自检 selftest | 二期补齐自检 |
|
||||
| **模拟器不可建模** | `net_socket_sim.c` 单线程 Winsock 无法复现**挂起模型内部机制**(外部语义已由包装层按 blocking 属性/预算模拟) | 挂起表/tick/通知路径在模拟器上零覆盖,只能硬件验证;挂起模型实现独立在 `net_pending.c`(真机专属,CMakeLists 不编译;**监听生命周期 `net_listener.c` 同此**) |
|
||||
| **模拟器不可建模** | `net_socket_sim.c` 单线程 Winsock 无法复现**挂起模型内部机制**(外部语义已由包装层按 blocking 属性/预算模拟) | 挂起表 / tick / 通知路径在模拟器上零覆盖,只能硬件验证(§7.4) |
|
||||
| **单 fd 单挂起** | 每 fd 至多一个挂起 recv,第二个被 `BUSY` 拒绝 | 由登记守卫保证;同 fd 并发 recv 本就无意义(per-fd 串行语义) |
|
||||
|
||||
**二期扩展设计**(ACCEPT / SEND 的挂起化、门禁清单、改动量)见
|
||||
`docs/任务规划/基线与待确认.md` §2.1。
|
||||
> **SEND 与 ACCEPT 的 pending 化收益性质不同,勿混为一谈**。判据只有一个:
|
||||
> **执行者被占多久**——SEND 挂起化前 ≤1 s/次(占用),ACCEPT 0(不占用)。
|
||||
> - SEND 是**执行者占用**问题,与 RECV 同源:pending 化直接解队头阻塞,收益真实,
|
||||
> **二期已落地**(§5.5、§7.1 send 组);
|
||||
> - ACCEPT 是**调用方轮询**问题:执行者本就零占用,"accept 慢"来自调用方轮询周期,
|
||||
> 不是 netTask 卡住——pending 化换的是 RPC 开销与延迟抖动,**不解决任何队头阻塞**。
|
||||
> 故 ACCEPT 优先级低于 SEND,**仍未落地**(登记项见 `docs/任务规划/登记簿.md` N-4)。
|
||||
|
||||
**二期扩展设计**(ACCEPT 挂起化、门禁清单、改动量)见
|
||||
`docs/任务规划/基线与待确认.md` §2.1。SEND 的挂起化已按该设计落地,见本文 §4.5 / §5.5 / §7.1。
|
||||
|
||||
---
|
||||
|
||||
## 8. 设计铁律(为什么是这样)
|
||||
## 11. 设计铁律(为什么是这样)
|
||||
|
||||
1. **执行者永不在等待中自旋**——等待必须在调用方上下文完成,netTask 只负责"登记"与"结束"。
|
||||
send 侧的"快路径有界自旋"是该铁律的**受控例外**:自旋被 `NET_SEND_SPIN_MS` 硬限(2 ms),
|
||||
超限即转为挂起登记,故执行者占用有界。
|
||||
2. **取消必通知**——任何取消路径(close / 重建 / 陈旧重同步)都必须唤醒调用方,
|
||||
否则是永久挂死而非错误返回。
|
||||
3. **挂起状态与 socket 表解耦**——`net_sock_t` 会被整块 memset,跨时间状态不能寄居其中。
|
||||
4. **不做状态推断式取消**——中间态(CONNECTING/LISTENING)合法迁移存在,
|
||||
否则是永久挂死而非错误返回(recv / send 同)。
|
||||
3. **挂起状态与 socket 表解耦**——`net_sock_t` 会被整块 `memset`,跨时间状态不能寄居其中。
|
||||
4. **不做状态推断式取消**——中间态(`CONNECTING`/`LISTENING`)合法迁移存在,
|
||||
推断取消会误杀正常建链等待。
|
||||
5. **数据到达不引入额外 SPI**——判据复用主循环既有 `net_poll` 维护的 `readable` 标志。
|
||||
6. **唯一完成出口**——所有完成/取消都经 `net_pending_complete`(recv)/ `net_send_pending_complete`
|
||||
(send);任何路径不得自行清 `active`。
|
||||
7. **send 结果恒为已发字节数**——短写是一等公民;断连 / 超时 / 取消**不伪造**全部成功、
|
||||
**不把结果改写为负错误码**,原因只经 `net_set_errno` 留痕。send 停滞计时按**无进展**
|
||||
刷新(每次成功写块重置),绝不按总时长判(会误杀大传输)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 排查辅助:挂起超时留痕探针(编译期选项,默认关)
|
||||
## 12. 排查辅助:挂起超时留痕探针(编译期选项,默认关)
|
||||
|
||||
五路判定 + 显式 cancel 是挂起 recv 的**全部唤醒责任**(§4)。一个挂起请求若超过阈值
|
||||
五路判定 + 显式 cancel 是挂起 recv 的**全部唤醒责任**(§5)。一个挂起请求若超过阈值
|
||||
(60s,`NET_RECV_TRIPWIRE_MS`)还没被任何一路结束,只有一种解释:**完成路径漏了它**
|
||||
(代码缺陷)——而不是"数据还没到":无限等待本来就是调用方的合法策略,不该被强行打断。
|
||||
|
||||
@@ -210,6 +724,35 @@ flowchart TD
|
||||
"强制超时"兜底:强制结束会把"唤醒路径有 bug"这个**缺陷**伪装成一次**普通超时**——
|
||||
调用方拿到假结果,缺陷永远潜伏(旧实现正是"到点 +1s 伪造 TIMEDOUT",已废除)。
|
||||
|
||||
开关:`NET_PENDING_TRIPWIRE`(`net_socket.c`,默认 0 = 编译剔除、产品零成本)——
|
||||
排查"调用方挂死唤不醒"时置 1 重烧。**开与关语义完全一致**:探针只留痕,不参与完成。
|
||||
代码里该机制名为 tripwire(绊线:挂住 = 踩到缺陷 wiring)。
|
||||
开关:`NET_PENDING_TRIPWIRE`(`Drivers/BSP/NET/net_pending.h`,默认 0 = 编译剔除、产品
|
||||
零成本)——排查"调用方挂死唤不醒"时置 1 重烧。**开与关语义完全一致**:探针只留痕,
|
||||
不参与完成。代码里该机制名为 tripwire(绊线:挂住 = 踩到缺陷 wiring)。
|
||||
判据基准:无限登记按 `registered` 计,显式登记按 `deadline` 计。
|
||||
|
||||
---
|
||||
|
||||
## 13. 源码索引
|
||||
|
||||
| 文件 | 符号 | 职责 |
|
||||
|---|---|---|
|
||||
| `Drivers/BSP/NET/net_pending.h` | `NET_PENDING_REGISTERED` / `NET_PENDING_TRIPWIRE` / `NET_SEND_CHUNK_MAX` | 哨兵、编译期开关、分块上限(唯一定义处) |
|
||||
| `Drivers/BSP/NET/net_config.h` | `NET_SEND_SPIN_MS` / `NET_SEND_TIMEOUT_MS` | 发送自旋预算与无进展超时 |
|
||||
| `Drivers/BSP/NET/net_pending.c` | `net_pending_capture_owner` | 捕获执行者句柄(防自等待,recv/send 共用) |
|
||||
| `Drivers/BSP/NET/net_pending.c` | `net_pending_register` | 登记挂起 recv(两守卫) |
|
||||
| `Drivers/BSP/NET/net_pending.c` | `net_pending_tick` | 每轮 recv 完成检查(①②③ + 探针) |
|
||||
| `Drivers/BSP/NET/net_pending.c` | `net_pending_complete`(static) | recv 唯一通知出口(通知 + 清槽 + 记 errno) |
|
||||
| `Drivers/BSP/NET/net_pending.c` | `net_pending_clear` | recv 幂等静默清槽 |
|
||||
| `Drivers/BSP/NET/net_pending.c` | `net_pending_cancel` / `net_pending_cancel_all` | recv 显式取消(resync / 重建) |
|
||||
| `Drivers/BSP/NET/net_pending.c` | `net_pending_settle_close` | recv 关闭前结算(排空或取消) |
|
||||
| `Drivers/BSP/NET/net_pending.c` | `net_pending_active` | recv 挂起查询(纯 RAM 读,监听器保险) |
|
||||
| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_register` | 登记挂起 send(两守卫;记录进度起点 `sent`) |
|
||||
| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_tick` | 每轮 send 推进/完成检查(①断连②推进③写完④超时) |
|
||||
| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_complete`(static) | send 唯一通知出口(结果 = 已发字节数) |
|
||||
| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_clear` / `net_send_pending_cancel` / `net_send_pending_cancel_all` | send 静默清槽 / 显式取消 / 重建取消 |
|
||||
| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_active` | send 挂起查询(纯 RAM 读,监听器保险) |
|
||||
| `Drivers/BSP/NET/net_socket.c` | `net_recv_locked` / `net_recv` / `net_recv_nb` / `net_recv_to` | 快速路径与挂起 recv 登记入口 |
|
||||
| `Drivers/BSP/NET/net_socket.c` | `net_send_locked` / `net_send` / `net_send_sock` | 快速路径(有界自旋)与挂起 send 登记入口 |
|
||||
| `Drivers/BSP/NET/net_socket.c` | `net_process_messages` / `net_wait_result` | 分派(含 `NET_MSG_SEND` 透传 `caller`)、哨兵抑制、两段式唤醒 |
|
||||
| `Drivers/BSP/NET/net_socket.c` | `net_close_locked` / `net_drop_conn_locked` / `alloc_socket` / `net_init` | 结算、清槽、取消、捕获执行者(recv + send 双侧) |
|
||||
| `Drivers/BSP/NET/net_listener.c` | `net_listener_retry_tick` | 消费 `net_pending_active` / `net_send_pending_active` 作挂起保险 |
|
||||
| `App/task/net_task.c` | `net_task_func` | 主循环节拍(`net_poll` → `net_process_messages`) |
|
||||
|
||||
Reference in New Issue
Block a user