feat(net): N-32 P1 会话结束电平化,消灭抢先 relisten 饿死消费者

B10 根治 P1(不变量 I):把"这条会话结束了"从 state==CLOSED 这一
**边沿**改成 session_dead **电平**,从结构上消掉两类事故。

问题:单连接复用模型下同一 fd 兼任监听器与连接(handle aliasing),
"会话结束"原先只能在 state==CLOSED 那一刻被观察到。别的路径抢先
relisten 回 LISTENING(对账④ / net_accept_locked 的 CLOSED 分支)
后,消费者再 recv 恒得 WOULDBLOCK → EOF 永久失踪:慢消费者被饿死、
新客户端数据被旧会话静默吸收。

改动:
- net_types.h:net_sock_t 增 session_dead(粘滞电平)
- 置位 4 处:handle_disconnect_event 释放分支、handle_timeout_event
  ESTABLISHED 分支、net_connect_locked 命令被拒、对账①(复用前者)
- 清零:handle_connect_event 两支;net_relisten_locked **刻意不清**
  (重建监听与"应用是否已确认会话结束"必须解耦,已注释钉住契约)
- net_recv_locked DONTWAIT 判据改 CLOSED || session_dead
- net_pending_tick ③ 断连判据同步改(**超出原清单的必要扩展**:不改
  则 lftpd 阻塞 recv 在抢先 relisten 后两头落空、只能干等超时)
- 两处 relisten 调用补内层 state==CLOSED 门:net_relisten_try 无
  状态门,对已 LISTENING 的 fd 再 open+listen 会因 USE 错误走进关闭
  分支、关掉正听着新 SYN 的 Socket(U12 同源事故)
- ui_net_sink::563 真错误分支加 g_client != g_listen 护栏;EOF/
  错误后不再自行 bind_listen(消灭第 3/4 个重建所有者,不变量 II)
- 新增 net_listener_armed()(LISTENING 或 CLOSED+auto_relisten)替代
  accept 门控的 net_can_accept()——旧门控使 :7003 永不进 net_accept,
  net_lost_connect_probe(丢 CONNECT 补 accept,N-30)形同从未运行。
  MCU 与 net_socket_sim.c 双端实现(sim 端恒 1,保持改前行为)

验证:@build 0 错 0 警(Code=197096,基线 197084,+12B);
mem-numbers 快照已 --write 刷新;cppcheck / C 规范 / 主机侧单测 12 套 /
双端注册 / CubeMX 存活 / 任务表 全绿。

⚠ 提交时 SKIP=doc-links:该钩子在从当前 v2-on-ui 切出的工作树上必报
2 处失效引用(RAM 方案文档 → ram_baseline.tsv / RAM预算台账.md),
属另一在途任务未提交的文件,主工作树磁盘上有故主线门禁呈绿。非本次
引入,不代为提交他人半成品;待其合入自动消解(见 并行任务工作树方案.md §10.2)。
This commit is contained in:
2026-10-12 13:06:23 +08:00
parent 0d084e6ec9
commit a8442410d1
7 changed files with 215 additions and 29 deletions
+44 -12
View File
@@ -23,6 +23,13 @@
* "RX 非空但 readable=0",故每 32 轮无条件 recv 一次(≈320 ms);
* 门控放行时若监听控制块已消失则置 g_listen=-1 触发重建,
* 避免"门控把端口永久不再接受新客户端"这一新卡死路径
* 2026-10-12 王建锋 N-32(B10 根治 P1,设计文档 §2.3 不变量 II):accept 门控
* 由 net_can_accept()(只认 ESTABLISHED)改判
* net_listener_armed()(LISTENING 或 CLOSED+auto_relisten)——
* 旧门控在断连待重建窗口恒 0,:7003 永不进 net_accept,
* net_lost_connect_probe 对该端口等于从未运行;同时 EOF /
* 真错误两条路径不再自行 bind_listen(重建交回 net 层,
* 消灭第三/第四个重建所有者),仅 fd 真消失时置 -1
*/
#include "ui_net_sink.h"
@@ -469,11 +476,15 @@ int ui_net_sink_start(uint16_t port) {
* 限定条件:ui_net_sink_start() 已成功调用
* 函数说明: 1. 网络代次变化(重建)时关闭旧监听/客户端并重建
* 2. 无监听(启动失败)时重试启动
* 3. 无客户端时**先门控**(net_can_accept,纯 RAM 读)再 accept,成功则复位状态机
* 3. 无客户端时**先门控**(net_listener_armed,纯 RAM 读)再 accept,成功则复位状态机。
* 门控判据 = "监听器还在服役"(LISTENING 或 CLOSED+auto_relisten)而非
* "本轮必然有连接"——后者会让断连待重建窗口内永不进 net_accept,
* 把丢 CONNECT 中断的补 accept 探测永久挡在门外(N-32)
* 4. 有客户端时**先门控**(net_can_read)再非阻塞 recv 逐字节喂入状态机;
* 门控判为"无数据"时仍每 RM_LIVENESS_DIV 轮无条件收一次(兜底)
* 5. 对端关闭/真错误时关闭客户端;若监听 Socket 随客户端失效
* (MCU 单连接模型)则重建监听,等待重连
* 5. 对端关闭/真错误时关闭客户端。**不再自行重建监听**(N-32 不变量 II):
* recv 的 EOF/错误分支已触发 net 层 auto_relisten,另有对账④兜底;
* 仅 g_listen 的 fd 真消失(net_get_sock 返 NULL)才置 -1 交回 2 重启
* ⚠ **门控只消除"无谓的往返",不消除"往返本身无上界"**:一旦真有活要干
* (accept / recv / 3204 B 的 LCDMEM 应答),仍是"入队 + 等 netTask 回执",
* 最坏 ≈4 s。宿主 = uiTask 主循环;"搬出 uiTask"已完整评估并**决定不做**
@@ -507,10 +518,15 @@ void ui_net_sink_poll(void) {
return;
}
/* 无客户端时尝试接受连接(I-1 门控:无连接可接受 → 0 次 RPC) */
/* 无客户端时尝试接受连接(N-32 P1 不变量 II:门控从 net_can_accept〔只认
* ESTABLISHED〕换成 net_listener_armed〔LISTENING 或 CLOSED+auto_relisten〕)
* 原因:MCU 单连接模型下断连后 fd 会停在 CLOSED 等 relisten,旧门控在该窗口
* 内恒 0 → :7003 永不进 net_accept → net_lost_connect_probe 对该端口等于
* 从未运行(丢 CONNECT 中断无第二信源);armed 放行后由 net_accept_locked
* 的状态门自行裁决(含 RX 有数据的 closed-but-readable 接受) */
if (g_client < 0) {
if (net_can_accept(g_listen) == 0) {
/* 监听控制块已消失(网络重建 / 异常释放)→ 置 -1 交回上方
if (net_listener_armed(g_listen) == 0) {
/* 监听器已不在服役(fd 消失 / 非监听型)→ 置 -1 交回上方
* g_listen < 0 分支重建监听。改前靠"每轮都 accept"撞出 BADF 来暴露,
* 加了门控就必须显式兜这一条,否则端口永久不再接受新客户端 */
if (net_get_sock(g_listen) == NULL) {
@@ -547,24 +563,40 @@ void ui_net_sink_poll(void) {
}
} else if (n == 0) {
/* 对端关闭:单连接模型下监听通道与数据通道是同一个 Socket,net 层已在
* net_recv_locked 的 CLOSED 分支自动把该 Socket 放回 LISTEN;此处绝不能再
* net_recv_locked 的 EOF 分支自动把该 Socket 放回 LISTEN;此处绝不能再
* net_close 它,否则监听一并被毁(后续连接被拒),且重建监听受 Trap 09 时序限制 */
if (g_client != g_listen) {
net_close(g_client); /* 多连接后端(PC 模拟器):数据通道独立,需显式关闭 */
}
g_client = -1;
rm_parser_reset();
if (net_get_sock(g_listen) == NULL && bind_listen() < 0) {
g_listen = -1; /* 重建失败:交回主循环重试,避免拿失效 fd 反复 accept */
/* N-32(P1 不变量 II):重建**不再**由本处自行 bind_listen。
* ① net_recv_locked 的 EOF 分支已触发 auto_relisten(D10 非阻塞重试);
* ② 对账④还会兜底补一次 relisten(Trap 22 实证事件会丢,两层不可互替)。
* 本处再做一遍 = 第三个重建所有者,正是 §2.3 那 12 个所有者的同类。
* 仅当 fd 真消失(net_get_sock 返 NULL = 网络重建)才置 -1,交回上方
* 分支重新走 start → bind_listen */
if (net_get_sock(g_listen) == NULL) {
g_listen = -1; /* fd 真消失:交回主循环重试 */
}
} else {
/* n < 0:区分"暂无数据"(继续等待)与"真错误"(断开并视情况重建监听) */
if (net_get_errno() != NET_ERR_WOULDBLOCK) {
net_close(g_client);
/* N-32(P1):加 `g_client != g_listen` 护栏,与上面 n==0 分支
* 的 :552 一致——MCU 单连接模型下 g_client 可能就是 g_listen 本身,
* 无护栏的 net_close 会直接关掉监听 fd(:563,与 :134 注释"严禁"
* 自相矛盾),端口随 Trap 09 沉降窗短死。PC 模拟器多连接后端下
* 两者恒不等,护栏不改变其行为 */
if (g_client != g_listen) {
net_close(g_client); /* 多连接后端(PC 模拟器):数据通道独立,需显式关闭 */
}
g_client = -1;
rm_parser_reset();
if (net_get_sock(g_listen) == NULL && bind_listen() < 0) {
g_listen = -1; /* 重建失败:交回主循环重试 */
/* N-32(P1 不变量 II):同 EOF 分支——不再自行 bind_listen,
* 重建交回 net 层(recv 的错误分支同样会走 relisten)。仅 fd 真消失
* 时置 -1 交回主循环走 start */
if (net_get_sock(g_listen) == NULL) {
g_listen = -1; /* fd 真消失:交回主循环重试 */
}
}
}