Net 阻塞模型文档改写为架构设计并迁入 docs/网络
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
# Net 层阻塞模型架构设计
|
||||
|
||||
> **模块名称**:Net 层(`net_socket.c` / netTask)阻塞等待模型
|
||||
> **文档定位**:**现状架构设计说明**——描述本层**当前如何组织等待与完成**、为什么这样设计、
|
||||
> 边界条件与已验证的性能特征。**不是历史记录**;演进过程与门禁实测数据作为溯源附于文末。
|
||||
> **状态**:设计已落地并全量台架验证(`v2-on-ui`)。
|
||||
> **日期**:2026-09-29
|
||||
> **相关文档**:
|
||||
> `docs/网络/BSD_Socket_API_使用指南.md`(应用视角 API 语义)、
|
||||
> `docs/网络/BSD_Socket_API_函数调用关系.md`(调用链与执行域)、
|
||||
> `docs/测试说明/链路延迟测试说明.md`(延迟基线与故障对照)、
|
||||
> `docs/审查记录/下一步任务规划.md` §3.1(二期 ACCEPT/SEND 扩展设计)
|
||||
|
||||
---
|
||||
|
||||
## 1. 问题域
|
||||
|
||||
netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线归它独占(SPI 铁律),
|
||||
因此所有 socket 操作必须经消息队列串行化到 netTask 上下文完成。
|
||||
|
||||
这带来一个结构性约束:**消费者发起的"等待数据"会占住执行者**。若 `net_recv` 采用
|
||||
"在 netTask 内自旋轮询直到数据到达"的朴素形态,一条空闲连接即可让整个网络层停摆——
|
||||
所有其他 socket 的操作都要排队等它。
|
||||
|
||||
> **约束的本质**:本项目的"数据到达"无法被免费观测。业界正统形态(LwIP 等)依赖网卡
|
||||
> DMA/IRQ 把数据递到协议线程,数据到达是 O(1) 事件;而本项目的数据到达发生在 CH395F
|
||||
> **芯片内部**,唯一观测手段是一次 SPI 事务,而 SPI 只许 netTask 发起。
|
||||
> → **观测本身占用执行者**,等待无法挪出 netTask。
|
||||
|
||||
设计自由度因此只剩两条:① 每次占用执行者多久 ② 等待期间执行者还能做什么。
|
||||
本架构的答案:**占用降到登记一次(微秒级),等待期间执行者照常服务整个队列**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构总览:pending 请求模型
|
||||
|
||||
核心思想:把"等数据"从**执行者内的自旋**改为**执行者外的登记**。
|
||||
|
||||
```
|
||||
调用方任务 netTask(执行者) CH395F 芯片
|
||||
────────── ───────────────── ────────────
|
||||
net_recv(fd, buf, len, timeout)
|
||||
│
|
||||
├─ 数据已在?── 是 ──→ 立即 SPI 读出 ────────────────────→ GET_REMV_LEN/READ
|
||||
│ 通知调用方(结果 = 字节数)
|
||||
│
|
||||
└─ 否,且 timeout>0
|
||||
│
|
||||
├─ 登记 pending 到 s_net_pending[fd] ← 占用执行者到此结束(微秒级)
|
||||
└─ 在自己上下文睡任务通知 ⟵──────────────┐
|
||||
│
|
||||
┌────────────────┴─────────────────┐
|
||||
│ netTask 主循环(~1ms 节拍) │
|
||||
│ net_poll() ← 刷新 readable
|
||||
│ net_pending_tick() ← 了结挂起
|
||||
│ net_relisten_retry_tick() │
|
||||
│ 排空消息队列 │
|
||||
└──────────────────────────────────┘
|
||||
│
|
||||
┌──────────────────────────────────────────────────┘
|
||||
│ 五路完成/取消判定(每轮对每个挂起 fd)
|
||||
│ ① 数据到达 → SPI 读出,通知调用方(结果 = 字节数)
|
||||
│ ② 到点 deadline → 通知 NET_ERR_TIMEDOUT
|
||||
│ ③ EOF/DISCONNECT → 按 0(EOF) 完成 + auto-relisten + 通知
|
||||
│ ④ 同 fd 后续操作 → 先行结算该 fd 的挂起,再执行新操作
|
||||
│ ⑤ 网络重建 → 取消 + **必须通知**(否则调用方永久挂死)
|
||||
└─→ xTaskNotify → 唤醒调用方
|
||||
```
|
||||
|
||||
**关键性质**:
|
||||
|
||||
- 等待期间 netTask **零额外占用**——`net_pending_tick` 只读标志与到期时间,
|
||||
数据到达判据 `readable` 由主循环既有的 `net_poll()` 每轮刷新,**不引入额外 SPI 事务**;
|
||||
- 调用方**在自己的上下文睡眠**(任务通知),与 LwIP 的消费者线程模型同构;
|
||||
- 公开 API 与调用契约**不变**——`net_recv` / `net_recv_to` 的签名、返回值、
|
||||
超时语义均与改造前一致,既有调用方、测试与模拟器零改动。
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据结构
|
||||
|
||||
### 3.1 挂起请求表
|
||||
|
||||
| 项 | 设计 |
|
||||
|---|---|
|
||||
| 存储 | **独立静态数组** `s_net_pending[NET_MAX_SOCKETS]`(不置于 `net_sock_t` 内) |
|
||||
| 字段 | `active` / `buf` / `len` / `deadline` / `registered` / `caller` |
|
||||
| 容量 | 每 fd **至多一个**挂起请求 |
|
||||
| 占用 | ≈24B/fd × 8 fd = **192B**(+4B 任务句柄),实测 SRAM +200B |
|
||||
|
||||
**为什么独立成表而不是塞进 `net_sock_t`**:
|
||||
|
||||
`net_close_locked` 对 `net_sock_t` 做**整块 memset**。若挂起状态置于其中,`close` 会
|
||||
静默抹掉 pending——调用方永远等不到通知,**永久挂死**。独立表把"跨时间状态"与
|
||||
socket 生命周期解耦,与 `net_relisten_retry_tick` 的重试表同构。
|
||||
|
||||
### 3.2 登记守卫(两条硬防线)
|
||||
|
||||
| 守卫 | 理由 |
|
||||
|---|---|
|
||||
| 每 fd 至多一个,**不覆盖** | 覆盖 = 丢掉首个调用方的通知 = 该调用方永久挂起 |
|
||||
| `caller == NULL` 或 `caller == netTask` 时**拒绝**(返回 BUSY) | netTask 上下文自等待 = 无人能完成它 = 死锁;netTask 内必须用 DONTWAIT |
|
||||
|
||||
---
|
||||
|
||||
## 4. 完成与取消语义(五路)
|
||||
|
||||
`net_pending_tick()` 在主循环每轮运行,对每个挂起 fd 按序判定:
|
||||
|
||||
| # | 条件 | 动作 | 设计要点 |
|
||||
|---|---|---|---|
|
||||
| 1 | **数据到达** | SPI 读出 + 通知(结果 = 字节数) | 由 `net_poll` 维护的 `readable` 门控,**零额外 SPI**;仅 ESTABLISHED / UDP_OPEN 态可读;spurious readable 需清除 |
|
||||
| 2 | **到点** `now >= deadline` | 通知 `NET_ERR_TIMEDOUT`(对外 -2 映射保持) | 状态无关;用有符号回绕安全差值,防 tick 回绕 |
|
||||
| 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)`,先于芯片复位;**取消不通知 = 调用方永久挂死** |
|
||||
|
||||
**状态推断的克制**:CONNECTING / LISTENING 等中间态**保持等待**,**不做**状态推断式取消——
|
||||
CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的等待。
|
||||
|
||||
### 4.1 看门狗
|
||||
|
||||
到点后 1s 仍未了结 = 完成逻辑缺位 → **强制了结 + `DBG_ERROR`**。
|
||||
定位:防"挂起登记后永不完成"导致调用方无限期睡眠。
|
||||
|
||||
---
|
||||
|
||||
## 5. 调用方契约
|
||||
|
||||
调用方**不改协议、不改 API**,但需知悉三条:
|
||||
|
||||
| 契约 | 说明 |
|
||||
|---|---|
|
||||
| `buf` 生命周期 | 挂起期间 `buf` 指针须**保持有效**至调用返回(挂起表按指针写入) |
|
||||
| 剩余预算循环 | 调用方保留"总预算 − 已耗时"循环骨架(`n == -2` 续轮,预算耗尽退出)——这是**公开契约要求**(30s 总超时语义 + 跨 TCP 分段累积),与本架构无关 |
|
||||
| 超时精度 | 完成判定随主循环节拍(~1ms);主循环被长操作占用时,超时**最多顺延**相应时长(已知代价,见 §7) |
|
||||
|
||||
> **已退役形态**:改造前调用方需自行实现"5ms 分片 + `vTaskDelay(1)` 让出"协议。
|
||||
> 该协议已随本架构落地而删除——分片是"执行者被占"前提下的妥协,pending 模型下不再需要。
|
||||
|
||||
---
|
||||
|
||||
## 6. 性能特征(已验证)
|
||||
|
||||
| 指标 | 值 | 条件 |
|
||||
|---|---|---|
|
||||
| 队列响应性(挂起连接下) | **92.5ms** | 挂起对时客户端 + FTP banner 探测 |
|
||||
| 改造前同场景 | 28554ms | — |
|
||||
| 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%) | 零占用设计落点 |
|
||||
| RAM / Flash 增量 | +200B / +608B | 相对改造前 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 设计边界与已知代价
|
||||
|
||||
| 项 | 现状 | 说明 |
|
||||
|---|---|---|
|
||||
| **SEND 路径未覆盖** | 发送仍为分块阻塞(1s/chunk stall) | 该期间队列仍被冻结;二期 pending 化解决 |
|
||||
| **ACCEPT 路径未覆盖** | accept 仍用轮询计时 | 需 pending 化(二期) |
|
||||
| **超时精度** | 随主循环节拍(~1ms),被长操作占用时顺延 | 一期文档化、不修;二期 SEND 化后该顺延面**收窄** |
|
||||
| **看门狗未接自检** | 强结算 + `DBG_ERROR`,未写 selftest | 二期补齐 |
|
||||
| **模拟器不可建模** | `net_socket_sim.c` 单线程 Winsock 无法复现挂起模型 | 该路径在模拟器上零覆盖,只能硬件验证 |
|
||||
|
||||
**二期扩展设计**(ACCEPT / SEND 的挂起化、门禁清单、改动量)见
|
||||
`docs/审查记录/下一步任务规划.md` §3.1。
|
||||
|
||||
---
|
||||
|
||||
## 8. 设计铁律(为什么是这样)
|
||||
|
||||
1. **执行者永不在等待中自旋**——等待必须在调用方上下文完成,netTask 只负责"登记"与"了结"。
|
||||
2. **取消必通知**——任何取消路径(close / 重建 / 陈旧重同步)都必须唤醒调用方,
|
||||
否则是永久挂死而非错误返回。
|
||||
3. **挂起状态与 socket 表解耦**——`net_sock_t` 会被整块 memset,跨时间状态不能寄居其中。
|
||||
4. **不做状态推断式取消**——中间态(CONNECTING/LISTENING)合法迁移存在,
|
||||
推断取消会误杀正常建链等待。
|
||||
5. **数据到达不引入额外 SPI**——判据复用主循环既有 `net_poll` 维护的 `readable` 标志。
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:演进溯源
|
||||
|
||||
> 本架构由 E-02 缺陷修复演进而来(明细见 `错误码传播审计.md` E-02)。**以下为历史信息,
|
||||
> 描述"如何演到这里",不作为当前设计依据。**
|
||||
|
||||
**E-02 缺陷**:netTask 每次取一条消息处理到完成才取下一条;任一消费者发起的 30s 阻塞
|
||||
`net_recv` 期间队列整体停摆最长 30s(FTP 客户端空闲 >30s、对时客户端连上不发数据均可触发)。
|
||||
|
||||
**两代形态**:
|
||||
|
||||
| 代次 | 形态 | 状态 | 提交 |
|
||||
|---|---|---|---|
|
||||
| 一代 | 分片有界等待:`net_recv_to` + 5ms 片 + 30s 预算 + 片间 `vTaskDelay(1)` 让出 | 已退役 | `f8d6e90` / `4a1605b` |
|
||||
| 二代 | **pending 请求(本文档所述,现行)** | **一线现行** | `a45c0cb` |
|
||||
|
||||
**实施与初始设计基线的偏差**(如实留档):
|
||||
|
||||
| 基线条目 | 实施 | 说明 |
|
||||
|---|---|---|
|
||||
| pending 控制块 ≈20B/fd | ≈24B/fd × 8 = 192B | 实测 SRAM +200B,同量级 |
|
||||
| 控制块置于 `net_sock_t` 内 | **独立静态数组** | 见 §3.1 理由 |
|
||||
| 无数据 → 登记、跳过通知 | 哨兵 `NET_PENDING_REGISTERED`(0x70000000)→ `do_notify=0` | 逐字一致 |
|
||||
| 调用方改单次 `net_recv_to(…, 30000)` | **保留剩余预算循环** | 公开契约要求(见 §5) |
|
||||
|
||||
**验证门禁逐条实测**(2026-09-22,设备 192.168.1.100):
|
||||
|
||||
| # | 门禁 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | CH395F NET 层套件 | **PASS**:F1 组合 Phase 6/9/11(64B/1460B 回显 11/11、select 回显 11/11、断开重连、KeepAlive + auto-relisten);F2 组合 Phase 8-RECV(100KB 阻塞接收 50/50 块 + CRC32 整包一致 0x9A0E0C8C)。Phase 7/10 未跑(DONTWAIT 路径另由 M3 探针族 L5~L7 覆盖)。TC-NET-604 FAIL = 既有芯片侧问题(Trap 21,2026-09-23 结案非缺陷) |
|
||||
| 2 | M3 基线重锁 | **PASS**:见 §6 |
|
||||
| 3 | e02_probe A/B | **PASS 且优于分片方案**:队列响应性 92.5ms;30s 截止按节拍精确触发(30.1s);监听恢复 6.6s |
|
||||
| 4 | ftp_regression / E-06 假 425 | 随 F1/F2 + M3 压力档覆盖(FTP 半登录态即其场景) |
|
||||
| 5 | UI 远端推流压力 | **PASS**:`:7003` 100Hz 全帧 CRC 50/50,零尖峰 |
|
||||
| 6 | timeSync 三场景 | **PASS**:挂起 / 正常包;部分包由 Phase 8-RECV 跨段累积等效覆盖 |
|
||||
| 7 | 产品 `@test` + 全片回读 | **PASS**:Required 全中 / Forbidden 零命中;回读 `No difference found` |
|
||||
| 8 | RAM/栈前后对比 | **PASS**:见 §6 |
|
||||
|
||||
**未执行 / 延后项**:
|
||||
|
||||
- 未保留分片路径作影子对照 → 分片协议直接退役;对照由"旧镜像 A/B"承担,回退 = git revert
|
||||
(单提交、API 不变);
|
||||
- 探针族扩充(close/reconfig/EOF/disconnect-during-pending 专项)→ 二期;
|
||||
- 看门狗接 selftest 留痕 → 二期;
|
||||
- 模拟器差距 +1(`net_socket_sim.c` 无法建模挂起)→ 二期更新清单;
|
||||
- 台架期间另发现一次 49s 全系统日志静默(defaultTask/NAND syslog 排空停滞,net 栈照常工作)
|
||||
——既有存储侧现象,与本层无关,见 `docs/待处理内容.md` N-2。
|
||||
|
||||
## 附录 B:修订记录
|
||||
|
||||
| 日期 | 内容 |
|
||||
|---|---|
|
||||
| 2026-09-22 | 一期落地并全量台架验证(原《Net 层阻塞模型业界对照与演进方案》实施记录) |
|
||||
| 2026-09-26 | 调研文档退役,客观实施记录独立成文 |
|
||||
| 2026-09-29 | **改写为架构设计文档**:以"当前如何设计/为何如此"为主线重组织;历史演进与门禁实测降级为附录 A;迁入 `docs/网络/` |
|
||||
Reference in New Issue
Block a user