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
+97 -8
View File
@@ -959,6 +959,9 @@ int net_socket(int domain, int type) {
p_sock->readable = 0;
/* 新 fd 不得继承上一条连接的释放原因(Trap 23 待办 #3) */
p_sock->released_abnormal = 0;
/* N-32(P1):新 fd 不得继承"上一条会话已结束"电平(否则新建即 EOF,
* 消费侧连数据都收不到)——与 released_abnormal 同理,显式清零 */
p_sock->session_dead = 0;
/* 新 fd 不得继承上一次的待重试状态(D10) */
net_relisten_clear(sockfd);
/* 新 fd 不得继承上一次挂起的 recv 请求(L3) */
@@ -1453,6 +1456,49 @@ int net_can_accept(int sockfd) {
return (p_sock->state == NET_SOCK_STATE_ESTABLISHED) ? 1 : 0;
}
/*
* 函数功能:查询监听 Socket 是否处于"应该尝试 accept"的就绪态(纯 RAM 读,不发起 RPC)
* 入口参数:sockfd - 监听 Socket 描述符 int
* 出口参数:无
* 返回值:1 - 就绪(LISTENING,或 CLOSED + auto_relisten = 断连待重建态);
* 0 - 未就绪(fd 未分配 / 非监听型 / 正处隔离期以外的不就绪态) int
* 限定条件:任意任务上下文;net_init() 已调用
* 函数说明:1. N-32(P1 不变量 II):取代 net_can_accept()(要求 ESTABLISHED)作为
* ui_net_sink 的 accept 门控。原门控下 :7003 永不进 net_accept →
* net_lost_connect_probe(丢 CONNECT 中断补 accept)对该端口从不运行
* (设计文档 §2.3 末"探针失效")
* 2. 为什么放行 LISTENING:探针与"closed-but-readable 接受"(:1338)
* 只在 net_accept_locked 内部发生,门控挡在外面就永远到不了;
* LISTENING 态 accept 由探测补 accept 或返 WOULDBLOCK,均安全
* 3. 为什么放行 CLOSED + auto_relisten:单连接模型断连后 fd 停在
* CLOSED 等 relisten(D10 非阻塞重试 / 对账④),此窗口内新 SYN
* 可能已被芯片接住(对账②)→ 放行让 net_accept_locked 的状态门
* 自行裁决(含 RX 有数据的 drain_pending 接受)
* 4. 只读控制块,不入队、不等通知 → uiTask 不被 netTask 拖住
* 5. 与 net_can_accept 语义差异是**有意的**:本函数表达"这个监听器
* 还在服役",而非"这一轮一定有连接"
*/
int net_listener_armed(int sockfd) {
/* Socket 控制块(纯 RAM 读,可能为 NULL = 描述符越界或未分配) */
const net_sock_t *p_sock = net_get_sock(sockfd);
if (p_sock == NULL) {
return 0;
}
/* 正常监听态 */
if (p_sock->state == NET_SOCK_STATE_LISTENING) {
return 1;
}
/* 断连待重建态:监听器仍在服役(auto_relisten + 已绑端口),
* 芯片侧交给 relisten / 对账恢复;同 net_reconcile_tick ④ 的认领口径 */
if ((p_sock->state == NET_SOCK_STATE_CLOSED) &&
(p_sock->auto_relisten != 0U) &&
(p_sock->local_port != 0U)) {
return 1;
}
return 0;
}
/*
* 函数功能:查询 Socket 是否"值得发一次 recv"(纯 RAM 读,不发起 RPC)
* 入口参数:sockfd - Socket 描述符 int
@@ -1682,6 +1728,10 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a
if ((status != CH395F_ERR_SUCCESS) && (status != CH395F_ERR_BUSY)) {
DBG_ERROR("[CONN] sock%d TCP_CONNECT rejected by chip: 0x%02X", sockfd, status);
(void)ch395f_close_socket((uint8_t)sockfd);
/* N-32(P1 不变量 I):命令级失败同样是"本会话已结束"——此前这里只置
* CLOSED,而 CONNECTING 态从未置 session_dead,挂起的 recv 只能干等
* deadline(B2 审计发现的"停在 CONNECTING 干等 30s 超时才暴露"同源) */
p_sock->session_dead = 1U;
p_sock->state = NET_SOCK_STATE_CLOSED;
return NET_ERR_RETURN_1(NET_ERR_INVAL);
}
@@ -1876,10 +1926,20 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags,
if (flags & NET_MSG_DONTWAIT) {
/* 非阻塞且当前无数据:若连接已断开则视为 EOF(0) 并自动重监听,
* 否则返回 WOULDBLOCK(-1) 表示暂时无数据 */
/* 连接已断开(CLOSED):按 EOF 处理 */
if (p_sock->state == NET_SOCK_STATE_CLOSED) {
/* 启用自动重监听:先把监听器重新置回 LISTEN */
if (p_sock->auto_relisten) {
/* 连接已断开(CLOSED)**或**本会话已结束(session_dead):按 EOF 处理。
* 判据从纯 `state == CLOSED` 放宽为 `|| session_dead`(N-32 P1 不变量 I):
* 抢先 relisten 后 state 已是 LISTENING 而会话确实已结束,旧判据在此
* 恒返 WOULDBLOCK → EOF 永久失踪(新连接被旧会话静默吸收)。
* 有数据时走上方快路径(recv_len>0 优先),本判据只在"无数据"时生效,
* 故不会把新连接的起始数据误判为 EOF。 */
if ((p_sock->state == NET_SOCK_STATE_CLOSED) || (p_sock->session_dead != 0U)) {
/* 启用自动重监听:先把监听器重新置回 LISTEN。
* ⚠ 内层 `state == CLOSED` 门是**必需的**,不是冗余:
* `net_relisten_try` 无状态门,对已 LISTENING 的 fd 再 open+listen
* 会让 ch395f_open_socket 返 USE,其失败分支会 ch395f_close_socket
* —— 把正听着新 SYN 的 Socket 当场关掉(U12 同源事故)。
* session_dead + 已回 LISTENING = 重建已由别处完成,这里只报 EOF。 */
if ((p_sock->state == NET_SOCK_STATE_CLOSED) && (p_sock->auto_relisten != 0U)) {
/* 非阻塞:失败由 D10 重试轮询接管 */
(void)net_relisten_try(p_sock);
}
@@ -2073,6 +2133,11 @@ static int net_relisten_locked(net_sock_t *p_sock) {
/* 状态置回 LISTENING(fd 保留) */
p_sock->state = NET_SOCK_STATE_LISTENING;
/* N-32(P1,设计文档 §5 P1.1 的关键约定):此处**刻意不清 session_dead**。
* 重建监听只是"芯片侧恢复就位",与"应用是否已确认上一条会话结束"是两件事;
* 清了它,抢先 relisten 之后消费者就再也得不到 EOF(B10"消费者卡死"原型)。
* session_dead 只由新 CONNECT 成功(handle_connect_event)或 memset
* (net_close/net_socket)清零。 */
/* 新监听不得继承上一条连接的释放原因(Trap 23 待办 #3) */
p_sock->released_abnormal = 0U;
/* N-30(Trap 25):清零前把对端端口转登为幽灵甄别基准 */
@@ -2428,8 +2493,12 @@ static void net_pending_tick(void) {
continue;
}
/* ③ 断连:排空 → 数据或 EOF(0) + 自动重监听 */
if (p_sock->state == NET_SOCK_STATE_CLOSED) {
/* ③ 断连:排空 → 数据或 EOF(0) + 自动重监听。
* 判据同 net_recv_locked(N-32 P1 不变量 I):`state == CLOSED ||
* session_dead`。只判 CLOSED 时,抢先 relisten 后 ①(要求 ESTABLISHED)
* 与 ③(要求 CLOSED) 两头落空 → 挂起的 recv 只能干等 ② deadline 超时
* (lftpd 阻塞 recv 的"消费者卡死"原型)。 */
if ((p_sock->state == NET_SOCK_STATE_CLOSED) || (p_sock->session_dead != 0U)) {
/* 查询残留字节数 */
int rem = ch395f_get_recv_len((uint8_t)i);
/* 有残留数据:按数据完成 */
@@ -2445,8 +2514,12 @@ static void net_pending_tick(void) {
/* 了结:以字节数通知 */
net_pending_complete(i, rd);
} else {
/* 启用自动重监听:先把监听器重新置回 LISTEN */
if (p_sock->auto_relisten != 0U) {
/* 启用自动重监听:先把监听器重新置回 LISTEN。
* ⚠ 内层 `state == CLOSED` 门同 net_recv_locked:session_dead
* 但已回 LISTENING 时重建已由别处完成,再 open+listen 会因
* USE 错误走进关闭分支、关掉正听着的 Socket(U12 同源) */
if ((p_sock->state == NET_SOCK_STATE_CLOSED) &&
(p_sock->auto_relisten != 0U)) {
/* 非阻塞:失败由 D10 重试轮询接管 */
(void)net_relisten_try(p_sock);
}
@@ -3575,6 +3648,10 @@ static void handle_connect_event(net_sock_t *p_sock) {
p_sock->remote_port = (uint16_t)(remote[4] | (remote[5] << 8));
/* 状态 → ESTABLISHED(监听自身转数据通道) */
p_sock->state = NET_SOCK_STATE_ESTABLISHED;
/* N-32(P1):新会话开始 —— 清"上一条会话已结束"电平。必须在置
* ESTABLISHED 之后立刻做:否则抢先 relisten 后到新连接的窗口内
* recv 仍会误得 EOF(把新客户端踢开) */
p_sock->session_dead = 0U;
/* 新开 Socket 发送缓冲按空闲计 */
p_sock->send_ready = 1;
@@ -3609,6 +3686,9 @@ static void handle_connect_event(net_sock_t *p_sock) {
p_sock->remote_port = (uint16_t)(remote[4] | (remote[5] << 8));
/* 状态 → ESTABLISHED */
p_sock->state = NET_SOCK_STATE_ESTABLISHED;
/* N-32(P1):新会话开始 —— 清"上一条会话已结束"电平(同 LISTENING
* 分支:必须在转 ESTABLISHED 后立刻做,防抢先 relisten 窗口误 EOF) */
p_sock->session_dead = 0U;
/* 使能 KeepAlive(客户端连接保活) */
ch395f_set_keepalive_enable(p_sock->ch395_sock, 1);
@@ -3695,6 +3775,11 @@ static void handle_disconnect_event(net_sock_t *p_sock) {
}
/* 正常释放(对端 FIN):断连了结回 EOF(0) */
p_sock->released_abnormal = 0U;
/* N-32(P1 不变量 I):置"本会话已结束"电平。须在置 CLOSED **之前**,
* 使 session_dead 覆盖"CLOSED 等待 recv 排空"整个窗口——排空期间
* recv 的 DONTWAIT 快路径先返数据(recv_len>0 优先),数据尽后判据
* 落到本标志 → 仍得 EOF,不因排空慢而漏报 */
p_sock->session_dead = 1U;
/* N-30(Trap 25):登记本连接对端端口(幽灵态甄别基准,须在状态
* 迁移前记录——remote_port 此刻仍有效,relisten 清零) */
p_sock->last_peer_port = p_sock->remote_port;
@@ -3787,6 +3872,10 @@ static void handle_timeout_event(net_sock_t *p_sock) {
p_sock->released_abnormal = 1U;
/* N-30(Trap 25):异常释放同样登记对端端口(幽灵甄别基准) */
p_sock->last_peer_port = p_sock->remote_port;
/* N-32(P1 不变量 I):异常结束同样是"本会话已结束"电平(TIMEOUT/RST
* 已在下方 requarantine,本标志只表达"会话结束了"这一事实,与
* released_abnormal(结束得好不好看)正交) */
p_sock->session_dead = 1U;
/* 状态 → CLOSED */
p_sock->state = NET_SOCK_STATE_CLOSED;
} else if (p_sock->state == NET_SOCK_STATE_LISTENING) {
+22
View File
@@ -172,6 +172,28 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen);
*/
int net_can_accept(int sockfd);
/*
* 函数功能:查询监听 Socket 是否处于"应该尝试 accept"的就绪态(**纯 RAM 读,不发起 RPC**)
* 入口参数:sockfd - 监听 Socket 描述符 int
* 出口参数:无
* 返回值:1 - 就绪(LISTENING,或 CLOSED + auto_relisten = 断连待重建态);0 - 不就绪 int
* 限定条件:net_init() 已调用;任意任务上下文
* 函数说明:1. N-32(P1 不变量 II):取代 `net_can_accept()`(只认 ESTABLISHED)作为
* ui_net_sink 的 accept 门控——原门控使 :7003 永不进 `net_accept()`,
* `net_lost_connect_probe`(丢 CONNECT 中断补 accept,N-30 实证的
* "数据被吞/425"主根因修复)对该端口等于从未运行
* 2. 放行 LISTENING:探针与 "closed-but-readable 接受" 都发生在
* `net_accept_locked` **内部**,门控挡在外面就永远到不了;LISTENING 态
* accept 要么被探针补中、要么返 WOULDBLOCK,两者均安全
* 3. 放行 CLOSED + auto_relisten:单连接模型断连后 fd 停在 CLOSED 等
* relisten(D10 非阻塞重试 / 对账④),此窗口内新 SYN 可能已被芯片
* 接住 → 放行让 `net_accept_locked` 的状态门自行裁决
* 4. 与 `net_can_accept` 的语义差异**有意为之**:本函数回答"这个监听器
* 还在服役吗",而非"这一轮一定有连接"
* 5. 跨任务读单字,同 `net_can_accept` 的最坏情形论证
*/
int net_listener_armed(int sockfd);
/*
* 函数功能:查询 Socket 是否"值得发一次 recv"(**纯 RAM 读,不发起 RPC**)
* 入口参数:sockfd - Socket 描述符 int
+16
View File
@@ -189,6 +189,22 @@ typedef struct {
* 节拍 = accept 轮询 100ms;新监听/补 accept 后清零) */
uint8_t lost_conn_seen;
/* N-32 / B10 根治 P1(2026-10-12,设计文档 §2.3 不变量 I):本会话"已结束"的
* **电平**(粘滞),取代 `state == CLOSED` 这一**边沿**。
* 为什么必须电平化:单连接复用模型下同一个 fd 既是监听器又是连接(handle
* aliasing),"关掉一条连接"与"毁掉一个监听器"在类型上无法区分;而"会话结束"
* 原先只能靠 `state == CLOSED` 这一刻被观察到——一旦别的路径抢先 relisten
* 回 LISTENING(对账④/`net_accept_locked` 的 CLOSED 分支),消费者再 recv
* 就恒得 WOULDBLOCK,EOF 永远失踪(B10"消费者卡死"与新连接被旧会话静默吸收)。
* session_dead 让"会话已结束"可重复查询、幂等:谁先看到不重要,谁都饿不着。
* 置位:handle_disconnect_event 释放分支 / handle_timeout_event ESTABLISHED
* 分支 / net_connect_locked 命令被拒 / 对账①(复用前者,:654)。
* 清零:新 CONNECT 成功(handle_connect_event 两支)、net_close/net_socket
* (memset 整块清零)、net_relisten_locked 的 peer 簿记**不清**——
* 这是关键:重建监听与"应用是否已确认会话结束"必须解耦(design §5 P1.1)。
* 跨 fd 保留语义同 released_abnormal(幽灵/陈旧事件属芯片 Socket 级)。 */
uint8_t session_dead;
/* 统计信息 */
uint32_t recv_bytes; /* 累计接收字节数 */
uint32_t send_bytes; /* 累计发送字节数 */
+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 真消失:交回主循环重试 */
}
}
}
@@ -36,10 +36,10 @@
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 214,088 B (209.07 KB) | 1,048,576 B (1024 KB) | 20.4% |
| ER_IROM1 | 214,100 B (209.08 KB) | 1,048,576 B (1024 KB) | 20.4% |
| RW_IRAM1 | 94,768 B (92.55 KB) | 131,072 B (128 KB) | 72.3% |
| RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% |
| **Flash (ROM 合计)** | **215,056 B (210.0 KB)** | 1,048,576 B (1024 KB) | **20.5%** |
| **Flash (ROM 合计)** | **215,068 B (210.0 KB)** | 1,048,576 B (1024 KB) | **20.5%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
@@ -260,14 +260,23 @@ DONTWAIT 在 LISTENING 下返 `WOULDBLOCK`(`:1890`)→ `ui_net_sink.c:560-56
- [ ] 在 B10 行确认已记「④ 扩大 EOF 吞没窗口」(已记,执行时复核)
**P1(边沿→电平)**
- [ ] `net_types.h` 的 `net_sock_t` 增 `uint8_t session_dead;`
- [ ] 置位:`handle_disconnect_event` 释放分支、`handle_timeout_event` ESTABLISHED 分支、对账①(复用前者)、`net_connect_locked:1685`
- [ ] 清零:`handle_connect_event`(新 CONNECT)、`net_close`/`net_socket` 的 memset;**`net_relisten_locked` 不清**(关键)
- [ ] `net_recv_locked` DONTWAIT 判据改 `state==CLOSED || session_dead`
- [ ] `ui_net_sink.c:563` 加 `g_client != g_listen` 护栏;EOF/错误后不再自行 `bind_listen`
- [ ] 新增 `net_listener_armed(fd)`(LISTENING 或 CLOSED+auto_relisten),替换 `ui_net_sink.c:512` 的 `net_can_accept` 门控
- [x] `net_types.h` 的 `net_sock_t` 增 `uint8_t session_dead;`
- [x] 置位:`handle_disconnect_event` 释放分支、`handle_timeout_event` ESTABLISHED 分支、对账①(复用前者)、`net_connect_locked:1685`
- [x] 清零:`handle_connect_event`(新 CONNECT,两支)、`net_close`/`net_socket` 的 memset 与显式清零;**`net_relisten_locked` 不清**(关键,已加注释钉住契约)
- [x] `net_recv_locked` DONTWAIT 判据改 `state==CLOSED || session_dead`
- [x] `ui_net_sink.c:563` 加 `g_client != g_listen` 护栏;EOF/错误后不再自行 `bind_listen`
- [x] 新增 `net_listener_armed(fd)`(LISTENING 或 CLOSED+auto_relisten),替换 `ui_net_sink.c:512` 的 `net_can_accept` 门控(MCU + `net_socket_sim.c` 双端实现)
- [x] **超出清单的必要扩展(设计时未预见)**:`net_pending_tick` ③ 断连判据同步改 `CLOSED || session_dead`。不改则 lftpd 的**阻塞** recv 仍会被抢先 relisten 饿死——①要求 ESTABLISHED、③要求 CLOSED,抢先 relisten 后两头落空,只能干等 deadline 超时。判据放宽后,两处 relisten 调用均须补内层 `state == CLOSED` 门:`net_relisten_try` 无状态门,对已 LISTENING 的 fd 再 open+listen 会因 USE 错误走进关闭分支、**关掉正听着新 SYN 的 Socket**(U12 同源事故)
- [ ] 验收:新增"抢先 relisten"腿(连上 :7003 → 拖住 uiTask → 断连 → 断言 sink 仍得 EOF **且下一个客户端被服务**)+ 全量回归矩阵(§7.1 那一套)
- [ ] `@build` 0/0 → `@test` PASS → 回读校验 → 停下等提交指令
- [ ] `@test` PASS → 回读校验 → 停下等提交指令
**P1 落成状态(2026-10-12)**:代码已实现、`@build` 0 错 0 警(`Code=197096`,较基线 +12 B)、
`mem-numbers` 快照已 `--write` 刷新、除下述基线问题外静态门禁全绿。
⚠ **已知基线问题(非本改动引入)**:`doc-links` 在**任何从当前 `v2-on-ui` 切出的工作树**上都会报
2 处失效引用——`RAM使用分析与优化方案.md:277/:585` → `scripts/ram_baseline.tsv`、
`docs/内存管理/RAM预算台账.md`。这两个文件属另一在途任务、尚未提交,主工作树磁盘上有故主线
门禁呈绿(见 `docs/环境与部署/并行任务工作树方案.md` §10.2)。待该任务合入后自动消解,
**不应**由本分支代为提交他人半成品。
**P2(单一所有者)**
- [ ] 新增 `net_listener_pump()`,与 `net_reconcile_tick` 同节拍,成为**唯一**调 `net_relisten_try` 的监听重建入口
+18
View File
@@ -351,6 +351,24 @@ int net_can_accept(int sockfd) {
return 1;
}
/*
* 函数功能:查询监听 Socket 是否处于"应该尝试 accept"的就绪态(与 MCU 端同名 API)
* 入口参数:sockfd - 监听 socket 描述符
* 返回值:1 - 可尝试 accept;0 - 描述符无效
* 限定条件:socket 已创建
* 函数说明:同 net_can_accept——Winsock accept 本无 RPC 成本 → 恒返回 1,
* 使上层门控在模拟器上等价于"改前行为"。
* ⚠ MCU 端该函数额外放行 CLOSED+auto_relisten(单连接复用的断连待重建态),
* 那是芯片 fd 别名模型的产物;Winsock 后端 fd 与监听器分离
* (设计文档 D6:双模型分裂暂不消除),无此中间态,恒 1 即为正确语义。
*/
int net_listener_armed(int sockfd) {
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) {
return 0;
}
return 1;
}
/*
* 函数功能:查询 Socket 是否"值得发一次 recv"(与 MCU 端同名 API)
* 入口参数:sockfd - socket 描述符