@@ -125,6 +125,78 @@ EOF 只在 **recv 那一刻**本层为 CLOSED 才给出(`net_recv_locked`, `D
且 FUN_PARA **初始化后不可改 ** ( `docs/芯片手册/CH395F.md` §5.52 L795⑥),这是开机一次性决策,
不能运行时降级。**结论:A′ 出局;根治只能在单连接复用模型内做**(红线 2,§6)。
### 1.5 与主流 offload 驱动的代码级对照("是不是过度设计"的正面回答)
> 2026-10-14 补。此前本节只有"主流方案无一维护第二份 socket 状态镜像"一句**结论**,
> 没有代码级对照——于是"沉降/重试这套机器是不是我们想多了"会被反复问。这张表就是答案。
**对照对象 ** : WIZnet **ioLibrary ** `Ethernet/socket.c` ( W5100~W6300 官方驱动,TCP offload
芯片的事实标准)。其关闭/重开路径原文:
``` c
int8_t close ( uint8_t sn ) {
setSn_CR ( sn , Sn_CR_CLOSE ) ;
while ( getSn_CR ( sn ) ) ; /* 等命令被芯片处理 */
setSn_IR ( sn , 0xFF ) ; /* ① 复用前清中断 */
sock_is_sending & = ~ ( 1 < < sn ) ; /* ② per-socket 记账清零 */
sock_remained_size [ sn ] = 0 ;
sock_pack_info [ sn ] = PACK_NONE ;
while ( getSn_SR ( sn ) ! = SOCK_CLOSED ) ; /* ③ 等芯片状态真落地才返回 */
}
int8_t disconnect ( uint8_t sn ) {
. . .
while ( getSn_SR ( sn ) ! = SOCK_CLOSED ) { /* ③ 同样等 */
if ( getSn_IR ( sn ) & Sn_IR_TIMEOUT ) { close ( sn ) ; return SOCKERR_TIMEOUT ; }
} /* ④ 有界超时兜底 */
}
int8_t socket ( . . . ) { close ( sn ) ; . . . while ( getSn_SR ( sn ) = = SOCK_CLOSED ) ; } /* 重开前先关干净 */
int8_t listen ( . . . ) { while ( getSn_SR ( sn ) ! = SOCK_LISTEN ) { close ( sn ) ; return SOCKERR_SOCKCLOSED ; } }
```
⇒ * * ① 复用前排渣 ② per-socket 记账 ③ 等状态落地 ④ 有界超时——本仓一条不少**:
分别对应 listen 前排清 SINT(§4.10)、`s_relisten_*[]` 、沉降窗(§4.3)、D10 预算(§4.2)。
**这套形状不是本仓发明,是 offload 芯片驱动的通用形态。 **
**那本仓多出来的体积从哪来——三条约束的兑换券 ** :
| 约束 | ioLibrary | 本仓 | 复杂度落点 |
|---|---|---|---|
| **不能阻塞 ** | `while (getSn_SR(sn) != SOCK_CLOSED);` 就地死等,`close()` 里连超时都没有 | netTask 绝不能冻结(IWDG + 另外 3 个常驻监听 + 消息队列同任务) | ⚠ **头号来源 ** :他们一个 `while()` 的事,本仓必须翻译成**可续跑状态机**——`s_relisten_*[]` 8 个数组**全部是"把 while 换成状态"的代价**,无一是额外逻辑 |
| **监听器归谁 ** | 8 个独立硬件 socket,关数据 socket 碰不到监听 socket | 单 socket 复用(§1.1) | "监听器需重新武装"这个概念在 ioLibrary 里**根本不存在** |
| **谁做 TCP 收尾 ** | 硬件自动:W5500 数据手册明写断开成功 → `SOCK_CLOSED` ,否则 **TCP 超时后也变 `SOCK_CLOSED` ** | `bit3=1` 下芯片按设计**什么都不做**,等 MCU 关(§1.1) | 沉降被 WIZnet 芯片自己吸收,CH395F 明确甩给软件 |
| **清中断的代价 ** | `setSn_IR(sn, 0xFF)` 随便清(每 socket 独立 W1C) | 全局寄存器整体读即清、无按位手段 | 他们一行清干净,本仓必须守 §4.10.5 三条准入判据 |
⚠ **ioLibrary 那两条死等不是"更简单",是"把代价挪到运行时" ** :官方论坛有"系统挂死、SPI
驱动在 `close()` 后永远等不到回复"与"`close()` 只轮询 `Sn_CR` 不轮询 `Sn_SR` "的质疑帖。
本仓没有死等的自由(上表第一行),复杂度只是换了个地方存着。
**lwIP / Zephyr / FreeRTOS+TCP 为什么没有这套机器 ** :完整 TCP 栈跑在 MCU 里,
TIME_WAIT/2MSL 藏在栈内部,应用永远看不见 FIN_WAIT_2;监听器所有权是显式 API( lwIP raw API
把"`tcp_close()` 只对 socket 层创建的监听器生效"写进文档)。⇒ **它们把复杂度付在 RAM/CPU
上,本仓付在驱动代码上**——用 offload 芯片就是为了省那颗栈,**复杂度不会消失,只会搬家**
(§1.2 那句"需要靠铁律维持的正确性就是架构欠债的凭证"的另一面:欠债要还,但还债的货币
由芯片选型决定)。
**本仓可安全减的部分(诚实清单) ** ——该文件的**内部组织度**(搬移边界 / 窄接口 / 可机械
检验判据)见 §11,本节只管"这台机器该不该存在":`net_listener.c` 实测 **386 行 = 注释 155 +
代码 204**(原 508 = 注释 277 + 代码 204; 2026-10-14 注释精简 − 44%,20 条陷阱的现场叙述迁入
本文,**代码逐行未改**)——**7 个函数**(4 公开 + 3 内部)/ 8 个 per-socket 数组,
**代码量本来就不大,过去大的是 20 条陷阱的现场注释 ** ;精简后那些叙述在本文件里。
可省:`s_relisten_settle_dead[]` (恒 = `settle_start + NET_RELISTEN_SETTLE_MS` ,
可就地算)、`s_relisten_capped[]` (可压成 `quarantine` 的一个 bit)——合计 16 B。
**不可省 ** :沉降窗(E4 变体 C 带残渣关闭 0/20)、D10( = ioLibrary 的 TIMEOUT 等价物)、
listen 前排清(§4.10)、登记/执行分离(E17,12 个所有者收敛成 1)。
**真要大幅减掉这套机器,唯一杠杆是芯片 ** ——而 §1.4 已用三条硬证据否掉多连接模式,且
`FUN_PARA` 初始化后不可改。WCH 官方博客原话(E15 已引):"客户端主动断连并立即重新发起
连接请求,此时 CH395 会拒绝该连接请求,**因此需要程序中做一些特殊处理**"——官方口径的
"特殊处理"就是本文件。
**出处 ** : ioLibrary `Ethernet/socket.c` ( github.com/Wiznet/ioLibrary_Driver);
W5500 数据手册 W5500_ds_v110e.pdf( `Sn_SR` 断开 / TCP 超时条款);WIZnet maker 论坛
帖 13028(close 后永久等待致挂死)与 10305(轮询口径质疑);lwIP 2.0.x Raw API 文档;
WCH 官方博客 cnblogs sw111/18451802。
## 2. 设计原则(不变量)
| # | 不变量 | 违反的后果(均实测) |
@@ -158,7 +230,7 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要
|---|---|---|
| `net_poll` | 主循环每轮(≈1ms) | INT# 引脚 → 全局事件汇聚(唯一读者)→ SINT 分派 → **驱动缓存全量刷新 ** → 链路位喂驱动 → 步进 `ch395f_sock_reset_poll` |
| `net_process_messages` | 每轮 | RPC 消息处理;开头每轮 `net_pending_tick` (挂起 recv 结束)+ `net_listener_tick` ( D10) |
| `net_reconcile_tick` | 100ms | 唯一周期对账 + 重建 (§4.1) |
| `net_reconcile_tick` | 100ms | 唯一周期**对账**——判据在此,重建动作在 §4.2 唯一执行者 (§4.1) |
### 3.3 状态模型(会话投影)
@@ -177,25 +249,108 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要
### 4.1 `net_reconcile_tick`——单一 100ms tick(唯一周期性自愈入口)
#### 4.1.1 为什么需要它:状态有两个所有者
| 所有者 | 是什么 | 谁更新 |
|---|---|---|
| **本层 ** `p_sock->state` | 软件枚举(`CLOSED=0` / `LISTENING=3` / `ESTABLISHED=5` )= 会话投影,**非芯片事实源**(I1) | **中断事件流 ** : CONNECT / DISCONNECT / RECV_OK |
| **芯片 ** `c.open` + `c.tcp_state` | CH395F 两个真实状态字节,`net_poll` 每轮刷进驱动缓存(新鲜度 ≤1ms) | 芯片自己的 TCP 状态机 |
主路径是事件流:芯片状态一变就发中断、本层跟随。**中断没送到 netTask**(被门控丢弃 /
时序窗口错过 / 芯片压根不发——形态⑤ 台架实证)→ 两侧脱钩,只有两类坏结果:
- **连接已死而本层以为活着** → 消费者挂在 recv 上,而对端再也不会发数据;
- **监听器已没而本层以为在听** → 端口对 SYN 不响应,表现是"连接被拒"。
本 tick = 每 100ms 把两侧摆在一起比对的那只手,**5 种失配组合 = 5 个形态**。
#### 4.1.2 读判据前必须知道的四个词
- `chip_listening` = `(open != CLOSED) && (tcp_state == LISTEN)` ,即 `0x05/0x01` 。
⚠ * * `0x05/0x00` (Socket 开着、TCP 已关的二段关闭中间态)不算在听**——这个区分很关键。
- `listener_fd` = `auto_relisten && type==STREAM && local_port!=0` :监听型 fd 门。少了它,
**出向连接 ** (本层 CLOSED + 已绑端口)会被误判成监听器而被 relisten。
- **去抖三档**(连续失配几轮才动手,一轮 = 100ms):② **1 轮 ** ( E2: LISTEN 可见 ≤10ms
无长尾)|③④⑤ **2 轮 ** |① **4 轮 ** (≈400ms,M3 让位窗——① 动作最重,要给正常事件流
先自己收口的机会)。动手后置 **1s 冷却**防风暴;U11 被动重同步**不去抖、不占冷却 ** 。
- **判据在对账、动作在别处**(I2):③④ 只 `net_listener_request()` 登记意图,真正执行
重建的是 `net_listener_tick` 一处(§4.2)。
#### 4.1.3 形态表(判据 → 动作)
判据读驱动缓存(零 SPI;例外见表注),统一 1s 冷却:
| 形态 | 判据(本层 ↔ 芯片) | 动作 | 去抖 |
|---|---|---|---|
| ① | ESTABLISHED ↔ 芯片 CLOSED / TCP_CLOSED | 复用 `handle_disconnect_event` (置事实 + GRACEFUL 沉降) | **4 轮 ** (≈400ms, M3 让位窗) |
| ② | LISTENING ↔ 芯片已握手(TCP=ESTABLISHED) | `GET_REMOT_IPP` 幽灵甄别 → 补 accept( `handle_connect_event` ) | **1 轮 ** ( E2: LISTEN 可见 ≤10ms 无长尾) |
| ③ | 监听型 fd 而芯片不在 LISTEN | 登记重建请求(动作在 I2 唯一执行者 `net_listener_tick` ) | 2 轮 |
| ④ | 本层 CLOSED( auto_relisten)而芯片**正 在** LISTEN(U11) | 只重同步本层为 LISTENING, **零芯片命令** | 2 轮 |
| ⑤ | ESTABLISHED ↔ 芯片已回 LISTEN(静默回收,F-NET-2) | RESYNC 语义只修本层(转登端口 + 取消挂起 recv),零命令 | 2 轮 |
| 形态 | 判据(本层 ↔ 芯片) | 动作 | 去抖 | 芯片命令 |
|---|---|---|---|---|
| ① | ESTABLISHED ↔ 芯片 CLOSED / TCP_CLOSED | 复用 `handle_disconnect_event` (置事实 + GRACEFUL 沉降) | **4 轮 ** (≈400ms, M3 让位窗) | 无(置事实) |
| ② | LISTENING ↔ 芯片已握手(TCP=ESTABLISHED) | `GET_REMOT_IPP` 幽灵甄别 → 补 accept( `handle_connect_event` ) | **1 轮 ** ( E2: LISTEN 可见 ≤10ms 无长尾) | 1 次 SPI 读 |
| ③ | LISTENING ↔ 监听型 fd 而芯片不在 LISTEN | 登记重建请求(动作在 I2 唯一执行者 `net_listener_tick` ) | 2 轮 | 无(转交) |
| ④ | 本层 CLOSED( auto_relisten)而芯片**不 在** LISTEN | 登记重建请求(同 ③,原 pump②) | 2 轮 | 无(转交) |
| ⑤ | ESTABLISHED ↔ 芯片已回 LISTEN(静默回收,F-NET-2) | RESYNC 语义只修本层(转登端口 + 取消挂起 recv/send ),零命令 | 2 轮 | **零 ** |
| U11 | 本层 CLOSED ↔ 芯片**正在** LISTEN | 只重同步本层为 LISTENING(转登端口) | **不去抖、不占冷却 ** | **零 ** |
⚠ **U11 不是形态④ ** (本表 2026-10-14 修正:旧表把 U11 误标成 ④、且**漏了真 ④ 的行**,
还把 U11 的去抖写成 2 轮)。两者判据同族(本层 CLOSED + 监听型 fd)而结论相反:
**芯片在听 = 本层滞后 → 只改状态;芯片不在听 = 监听真没了 → 登记重建 ** 。搞混的代价就是
U12 同源事故——对正听着的 Socket 重下发 open+listen 会吃 USE 错、走进关闭分支,
把健康监听器关掉。
表注:形态② 命中时的对端端口回读是本 tick 唯一按需 SPI。
- **为什么 ①③⑤ 常态 0 命中**(E18 负向注入打靶三轮,2026-10-14):消费者驱动的 pending
recv / recv EOF 比 100 ms 对账**更快**,多数场景先把本层收口成 CLOSED → 由形态④ 接手。
①③⑤ 的职责边界是**"没有人在等"的场景**,与 ④ 互补而非重复。三条都已主动打中并确认救回:
① 吞掉 DISCONNECT 后 **4/4 救回 ** ;③ 空闲监听器被静默 close,两轮独立复现;⑤ 开盲区吞掉
芯片自发中断 + 把芯片收回 LISTEN 后命中 `-> resync` (零芯片命令、挂起 recv 被取消、端口
立刻可复连)。→ **三条均不可降级为只留痕 ** ;⑤ 的"取消挂起 recv/send"尤其不可省,
否则消费者在死连接上干等到 deadline。
#### 4.1.4 每个形态为什么长这样、怎么验的
* * ① 连接已失**:成因 = 连接被拆而 DISCONNECT 没被 netTask 收到,本层停在 ESTABLISHED、
消费者挂在 recv 上等永远不会来的数据。动作复用断开状态机(含陈旧事件甄别 + 状态迁移 +
事件派发),**只置事实、不做重建**(N-32,重建交 ③④)。验法 = 注入吞掉该端口的 DISCONNECT
分派(前提:在 i104 的 200ms accept 轮询窗内由 PC 发 RST,让芯片真的拆链)→
`reconcile: chip closed (0x05/0x00) -> disconnect` 。日志里 `open` 仍是 `0x05` 、
`tcp_state=0x00` → 判据是**"或"**,故成立(E18 第 2 轮 4/4 救回)。
⚠ 第 1 轮曾**误判命中**:拿 PC 侧"能连上"当判据,而设备日志里根本没有 `chip closed` 行
(干活的是 ③)→ **判据是设备日志,不是端口可达性 ** ( E18 方法论第一条)。
* * ② 握手已完成、事件没收到**:成因 = 芯片做完三次握手而 CONNECT 没送到,本层以为在听、
应用不会 accept,数据全堆在芯片 RX,应用侧**完全看不见**这条连接。⚠ **幽灵甄别(N-30 /
TRAP-NET-20)是本形态最凶处**:拆链收尾窗口里芯片状态会**如实**显示 ESTABLISHED(旧连接
未拆净,E15 契约,非谎报),此时补 accept 就把新数据发给已死的旧连接 = 生产 `LIST#4`
数据被吞的直接成因;故仅当"对端端口非零且 = `last_peer_port` "时拦截,`peer==0` 不拦截
(对端端口提交有滞后,不得因此挡掉救援)。去抖 1 轮 = 最激进,因为它是"白捡的救援"、代价小。
⚠ * * ② 不在 E18 靶单上**(需吞 CONNECT 事件)→ 其依据是幽灵甄别 + E2/E10 台架记录,
**勿写成"已负向注入验证" ** 。
* * ③ 监听器失位**:成因 = 芯片上的监听器没了(close / 超时 / 回收)且没发事件,本层还以为
在听 → 端口拒连。验法 = 绕过本层全部簿记,直接对**空闲**监听器(`:7003` 远显,无连接、
无消费者 = 最干净的靶)发 `ch395f_close_socket()` → `chip not listening (0x00/0xFF,
was state=3) -> relisten` ( `was state=3` 即 LISTENING)。E18 第 1、2 轮两次独立复现。
* * ④ 孤儿监听**:成因 = 会话结束、本层已收口 CLOSED,但监听没重建成功(或重建事件丢了)。
与 ③ 的差别**只在本层状态**:③ 是"以为还在听",④ 是"知道会话结束了、但监听器本该在"。
**它是常态唯一在干活的形态 ** (归因见 §4.1.5)。④ 不是靶,却被 E18 第 1 轮的失败反复
误打中(没吞掉芯片自发 DISCONNECT → 本层塌成 CLOSED → 活落到 ④)——这些"打歪了"的样本
正是 §4.1.5 结论的来源。
* * ⑤ 芯片静默回收**:成因 = 芯片**自己**把 Socket 收回 LISTEN 且**不发 DISCONNECT 中断**
(run8 台架实证),本层挂着一个已不存在的"活连接"。动作纪律 = **零芯片命令 ** :只改本层
state、转登 `last_peer_port` 、清 `lost_conn_seen` 、**取消挂起 recv/send**(必通知,防调用方
挂死)。为什么绝不能顺手 close + 重 open:芯片那个监听器是**健康的**,重下发会摧毁它 →
端口永久拒绝,**run8 实测 170s 拒连**(与 U11 同一条纪律)。验法 = 四步机:开盲区吞掉芯片
自发的一切中断 + close → **等芯片真回 CLOSED ** → 绕过本层 open+listen → 确认落到
`0x05/0x01` 而本层仍 ESTABLISHED → `chip re-listened w/o DISCONNECT -> resync` 。
两次失败各暴露一个硬事实(详见 §5.2 E18):状态机漏 `step=1` → 每 ~1ms 重复 close →
**SPI 风暴 ** → `[ERR][WD] task stall` ;等待预算 200 轮太短——芯片 close 后停在
**FIN_WAIT_2( `0x05/0x06`)实测 972 轮 ≈1s ** ( E15 的 2MSL 长尾),靠 PC 侧 RST 才打断。
这条实测同时是 §4.3 沉降上限"为什么必须存在"的时间尺度证据。
#### 4.1.5 为什么 ①③⑤ 常态 0 命中
消费者驱动的 pending recv / recv EOF 比 100 ms 对账**更快**,多数场景先把本层收口成
CLOSED → 由形态④ 接手。关键在两条快路径的判据(`net_pending.c` 的挂起 recv 结束判定 +
`net_socket.c` 的 recv 快路径):**只看本层 `state` 、从不读芯片**——所以只要"有人在等",
事件流就先赢,①③⑤ 永远轮不到。推论就是职责边界:**①③⑤ 管的是"没有人在等"的场景**
(空闲监听器被静默 close、连接被静默拆掉而无人 recv、芯片静默回收),与 ④ **互补而非
重复**。怎么打中的见 §4.1.4、证据见 §5.2 E18。→ **三条均不可降级为只留痕 ** ;⑤ 的
"取消挂起 recv/send"尤其不可省,否则消费者在死连接上干等到 deadline。
#### 4.1.6 M3 可疑门与沿革
- **M3 可疑门**( `handle_disconnect_event` ESTABLISHED 分支):芯片 `0x05/0x00`
( Socket OPEN 但 TCP 槽 0x00——活连接应为 0x04,逻辑矛盾态)且 `lost_conn_seen==0`
@@ -372,6 +527,70 @@ Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少
| 关闭姿势必须正确 | TRAP-NET-12:陈旧 DISCONNECT 锁存秒断下一条连接 | `sock_reset` 序列 = 排 RX → CLOSE → 排事件 → 等 `byte0=00H` (§4.4) |
| 异常释放须先排空残渣再关 | TRAP-NET-18:~2KB RX 残渣被下条连接当自己的数据读走 | 隔离期持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED` (§4.3) |
### 4.10 读即清信源审计(谁在读、会不会互吞)
> 2026-10-14 全库审计(grep `get_sock_int_status` / `glob_int_status` / `int_status_sn`)。
> 口径原本散在 **B3、F-NET-15 M4-lite、E14、TRAP-NET-12/-18** 四处代码注释里,**没有总表**——
> 而"想再加一处读清"的人不会先去读那四处。本节就是那张挡人的表。
#### 4.10.1 两个读即清寄存器与两级门控
| 寄存器 | 命令 | 宽度 | 语义 | 合法读者 |
|---|---|---|---|---|
| 全局中断状态 | `GET_GLOB_INT_STATUS_ALL` | 16 位 | 含**全部 8 个 Socket** 的事件位(Socket n = bit(n+4));**整体读即清,芯片无按位清除手段** | * * `net_poll` 唯一**( AGENTS 模块约定) |
| 本 Socket 中断状态 SINT | `GET_INT_STATUS_SN` | 8 位 | 单个 Socket 的事件位;读即清 | `net_poll` 分派 + §4.10.2 的定点排渣 |
`net_poll` 是**两级门控**:先读全局位,某 Socket 位为 0 就 `continue` 、**不去读它的 SINT**。
⇒ 任何"顺手全量清一遍"的代码,等于**把别的在用 Socket 的门一并焊死**——它们的
CONNECT/DISCONNECT 边沿**永久丢失**( RECV 属条件位、下轮可补回,TRAP-NET-08)。
这就是 B3 那类代码的真实危害,也是"全局寄存器只有一个读者"这条约定的全部理由。
#### 4.10.2 现存读清点全量清单(产品代码 6 处)
| 位置 | 时机 | 会不会吞掉有用事件 |
|---|---|---|
| `net_poll` 全局读清(`net_socket.c` ) | 事件汇聚入口 | 契约本体——读完立刻按位分派,不吞 |
| `net_poll` 分派读 SINT(同上) | 受全局位门控 | 不吞 |
| `net_listen_locked` 预清 | * * `tcp_listen` 之前** | 芯片此时还没在听,**产生不了 CONNECT** |
| `net_relisten_locked` 预清(`net_listener.c` ) | * * `tcp_listen` 之前**(E14 定案改法) | 同上 |
| 沉降/隔离窗每 30ms( `net_listener.c` ) | `close` 之后、`open` 之前 | 吞的是上一条会话残留 DISCONNECT/TIMEOUT = **本意排渣 ** |
| `net_connect_locked` / `net_close_locked` 预清、`sock_reset` 第 3 步(驱动) | 出向连接前 / 关闭前 / 复位排空至 quiescent | "本 Socket 即将被重新使用"的定点排渣(TRAP-NET-12/-18 两级预清) |
**共同点:全部只碰本 Socket,且都落在"这个 Socket 即将被重新使用"的边界上。 **
⇒ 当前**没有已知的"事件被读清互吞"活口**( `test/board` 台架里的读清点不算——它们剥离了
net 层,本就以"我自己是唯一读者"为前提)。
#### 4.10.3 已根除的三条互吞实例(历史,勿复活)
| 已删的东西 | 危害 | 定性 |
|---|---|---|
| **B3 ** : `net_listen_locked` / `net_relisten_locked` 遍历 **8 个 Socket 全清 + 再读清全局 ** | 见 §4.10.1——别的在用 Socket 边沿永久丢失;而后者是**每次断连的热路径** | git 考古:多连接时代调试遗留被原样搬进单连接模型(e862009bc → 319d00e3 → db64e6acf),**不属于任何 Trap 的修复** |
| **F-NET-15 M4-lite ** :沉降段的全局读即清 | 每 30ms 一轮就搁浅其它 Socket 的 CONNECT/DISCONNECT/RECV_OK; M1 使沉降成为每次断连热路径后,放大成 **FTP 控制/数据通道互杀 ** ( E7 实测 lftpd 5s 超时) | 已删,注释留在 `net_listener.c` 隔离段 |
| **E14 ** :排事件放在 `tcp_listen` **之后 ** | listen 后读清把**快速复连新连接的 CONNECT** 当场吞掉(生产与台架同源根除) | **已挪到 LISTEN 之前 ** ( `net_listener.c` / `net_listen_locked` 同改法) |
#### 4.10.4 一处「看着像缺陷、其实是配对消费」
`handle_connect_event` 末尾有一处 `(void)ch395f_get_sock_int_status()` ,注释只写"清除残留
TIMEOUT",极易被误读成"随手丢弃整字节"。查调用链才定性:它有**三个调用点**,其中
**形态② 补 accept 这条路径根本没读过 SINT ** (CONNECT 事件本来就没送到),芯片里那个
CONNECT 位还锁着——软件已补做 accept,**必须清掉**,否则 `net_poll` 下一轮看到全局位会
**二次分派同一个 CONNECT ** 。
残留风险只剩 net_poll 路径:从分派读 SINT 到此处是**微秒级窗口**,其间锁存进同一字节的
DISCONNECT 会被 `(void)` 丢掉。但 RECV_OK 是条件位(下轮补回)、DISCONNECT 由形态① 在
400ms 内兜住 → **有界,不建议改动 ** 。⚠ 真要动它,必须先读懂形态② 那条路径再动手。
#### 4.10.5 再加一处读清的三条准入判据
1. **只碰本 Socket ** ——绝不碰全局寄存器(唯一读者 = `net_poll` )。
2. **必须在 `tcp_listen` 之前 ** (E14:之后读清会吞掉新连接的 CONNECT)。
3. **必须说清两件事 ** :吞掉的是**谁的什么位**(上一条会话残留 = 合法;本 Socket 新事件 =
违规),以及万一吞错**由哪条兜底**(对账形态 / 条件位自愈)。
**结构性残余 ** :两级门控存在"全局位没置、SINT 却有位"的**理论**可能。E14 的 48 轮里
2/48 属**门控观察伪象**( SINT 30ms 内其实已捕获),**不是已证实的漏读**。兜底仍是 I3:
**事件只负责提前唤醒,正确性靠回读 + 有界超时 ** 。
## 5. 关键参数与实证锚点
### 5.1 参数表
@@ -483,8 +702,10 @@ Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少
## 9. 代码级事实清点
> 判读本架构或做决策时需要的**源码事实**。⚠ 只给符号名、不固化行号——行号随重构漂移
> (本仓曾出现文档内两处行数快照自相矛盾)。行数快照(2026-10-14):
> `net_socket.c` 3702 行、`net_listener.c` 467 行、`net_listener.h` 97 行。
> (本仓曾出现文档内两处行数快照自相矛盾)。行数快照(2026-10-14,两次模块化之后 ):
> `net_socket.c` 3119 行、`net_events.c` 663 行、`net_listener.c` 386 行、`net_listener.h` 126 行。
> ⚠ **这类快照当天就会漂移**(本节上一版写的 3702/467/97 已被同日的 §11.5 作废)——
> 它是"读代码前的地图",**不是判据**;要判据请数符号,别数行。
### 9.1 会话投影的状态写入面
@@ -528,6 +749,9 @@ Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少
> **动机**:真机 net 层 `net_socket.c` 一度 4126 行,而模拟器做同样的事只有 807 行(5 倍);
> `relisten` 一词在文件里出现 110 次。诉求 = **高内聚低耦合**。
> 先由 §4.1 把"周期性重建"收敛到唯一决策者,搬移边界才第一次清晰——**先收敛,再搬移**。
>
> ⚠ **本节只回答"组织度"**(搬什么、边界在哪、接口多窄)。若问的是"这套沉降 / 重试机器
> 本身是不是我们想多了"——那是**合法性**问题,见 §1.5 与 ioLibrary 的代码级对照。
### 11.1 可机械检验的判据
@@ -549,7 +773,7 @@ Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少
|---|---|
| `net_relisten_locked` / `net_listener_request` / `net_listener_tick` / `net_listener_clear` / `net_relisten_done` | 「维持 fd 处于 LISTEN 态」的全部机制 |
| `net_drain_chip_rx` | 重建前的排空(只被 relisten 调用) |
| 7 个 `s_relisten_*` 数组 + 7 个 `NET_RELISTEN_*` 常量 | 状态与策略私有化 |
| 8 个 `s_relisten_*` 数组 + 7 个 `NET_RELISTEN_*` 常量 | 状态与策略私有化(第 8 个 `s_relisten_capped` 是 E19 降级时新增,属 §1.5「可省清单」) |
| ~~`net_listener_pump`~~ | 原唯一的周期性重建决策者——**阶段 3 已删除**,决策并入 `net_reconcile_tick` 形态③④(§4.1) |
**留在 `net_socket.c` ** (并说明理由,避免"看着像就搬"):
@@ -619,6 +843,42 @@ void net_listener_tick(void); /* 唯一执行者(netTask 每轮,
> 接口改造必然改变代码生成。修订为:**RW-data / ZI-data 必须完全不变**(数据总量不变),
> `Code` / `RO-data` 允许极小变化。
### 11.5 同族第二模块:`net_events.c`( 2026-10-14,事件处理 + 周期对账)
§11 把"动作"搬进 `net_listener.c` 后,`net_socket.c` 里剩下的**判据**( `net_reconcile_tick`
形态①②③④⑤/U11)与**边沿事件处理**(5 个 `handle_*_event` )仍是同一职责的两半——
一个周期面、一个事件面,都在做"芯片状态面 ↔ 本层镜像"的收敛。本次把它们合成**一个**模块。
**结果 ** : `net_socket.c` 3720 → 3119 行(−601);新增 `net_events.c` 663 / `net_events.h` 46。
`RW-data` 1112 → 1112、`ZI-data` 154456 → 154456 **完全不变 ** ; `Code` 212316 → 212296(−20 B,
拆 TU 的链接器布局差)。`@test` PASS: 635ms 到 `net init: OK` , `:8888` /`:2404` /FTP 均建立,
`[ERR]` /Fault/relisten/reconcile 命中 **0 ** 条。
**为什么是一个模块而不是两个 ** (与原计划不同,值得记):剥掉注释后实测**对账区真实调用
事件处理器 2 处**(形态② 补 accept 走 `handle_connect_event` ),且 `NET_RECON_STABLE_DEATH`
两边共用。拆成 `net_events.c` + `net_reconcile.c` 会造出跨模块调用;合起来这 2 处变模块内
直调,**跨模块导出为零**。⚠ 依赖扫描**必须先剥注释**——未剥时 `alloc_socket` /
`net_close_locked` / `net_relisten_locked` 全被误报成依赖(其实只是注释里的提及),
足以把人引向错误的边界。
**三处「非纯搬移」 ** (沿用 §11.4 的口径,均已留痕):
1. **fd 取法替换 ** : `p_sock - s_net_socks` → `p_sock->ch395_sock` ( `net_socket()` 里
`ch395_sock = sockfd` ,恒等;模拟器同构造)。⚠ 随之**收窄**两处沉降登记的范围门
`(qfd >= 0) && (qfd < MAX)` → `qfd < MAX` : `ch395_sock` 是 `uint8_t` , `(int)` 后恒非负,
留着被 cppcheck 判 `knownConditionTrueFalse` 拦提交。**勿加回**。
2. **新增 `net_sock_raw()` ** :对账必须看到 `in_use==0` / `state==CLOSED` 的槽位(形态④
认领孤儿监听正依赖 CLOSED 态),而 `net_get_sock()` 对未在用槽位返回 NULL 会漏掉这一类。
声明放**配对头** `net_socket.h` 而非内部头 `net_events.h` ——规范 **S17 2.2 ** 要求非 static
函数声明在配对头文件(同 §11.4 第 1 条 `net_set_errno` 先例)。
3. **真机专属 + `SIM_EXEMPT` 显式点名 ** :同 §11.4 第 3 条(依赖 `ch395f_*` SPI 与驱动状态
缓存,Winsock 后端无"事件未收到/静默回收"概念,无失同步可收敛)。
**搬移完整性的机械证明 ** :剥注释后取**代码行多重集**, `net_socket.c` + `net_events.c` 之和
与原 `net_socket.c` 相比,差异**恰好等于**上面声明的改动集(3 处 fd 取法 + 1 处数组访问 +
8 处 `static` 去除 + 6 行前向声明 + 新文件自身 15 行头 + `net_sock_raw` 三行),**无一处意外**。
搬移类改动只有配上这种证明才敢合入——否则"注释和代码混在一起改"无法区分意外丢失。
## 12. 引用与归档约定
- 本文引用格式:`docs/网络/网络自愈架构设计.md` §N.M(紧邻写法);E 编号引 §5.2