diff --git a/Drivers/BSP/NET/lftpd/lftpd_inet.c b/Drivers/BSP/NET/lftpd/lftpd_inet.c index 109e2bbc..dadfd3af 100644 --- a/Drivers/BSP/NET/lftpd/lftpd_inet.c +++ b/Drivers/BSP/NET/lftpd/lftpd_inet.c @@ -112,10 +112,12 @@ int lftpd_inet_accept_timeout(int socket, int timeout_ms) if (s >= 0) { return s; } - /* 数据连接已被对端关闭时提前退出,不等待超时 */ - net_sock_t *p_sock = net_get_sock(socket); - if (p_sock == NULL || p_sock->state != NET_SOCK_STATE_LISTENING) { - DBG_ERROR("data socket closed, accept aborted"); + /* F-NET-15 M2(N34 实验 2026-10-12):镜像门控退役——原直读 p_sock->state 在 + * relisten 忙期(CLOSED 瞬态)误判"已关闭",扔掉 ~4.9s 等待预算;armed 门控 + * 认"监听器在服役(LISTENING)或待重建且 RX 有残留(closed-with-data 救回窗)", + * 与 :7003 的 N-32 P1 同型修复 */ + if (net_listener_armed(socket) == 0) { + DBG_ERROR("data listener disarmed, accept aborted"); return -1; } lftpd_task_wd_beat(); /* 看门狗:每轮喂心跳(数据连接等待最长 5s) */ diff --git a/Drivers/BSP/NET/net_listener.c b/Drivers/BSP/NET/net_listener.c index 941d9b2a..46e98389 100644 --- a/Drivers/BSP/NET/net_listener.c +++ b/Drivers/BSP/NET/net_listener.c @@ -174,8 +174,13 @@ static int net_relisten_locked(int sockfd) { ch395f_close_socket((uint8_t)sockfd); /* 读即清:本 Socket 中断状态寄存器 */ (void)ch395f_get_sock_int_status((uint8_t)sockfd); - /* 读即清:全局中断状态寄存器 */ - (void)ch395f_get_glob_int_status_all(); + /* F-NET-15 M4-lite(N34 实验 2026-10-12):删除沉降段的全局读即清—— + * ① 违反模块约定「所有 CH395F 中断在 net_poll 内集中读取并清除」; + * ② 全局寄存器整体读即清无按位手段(B3 实证),每 30ms 重试一轮即搁浅 + * 其它在用 Socket 的 CONNECT/DISCONNECT/RECV_OK(net_poll 门控焊死); + * ③ M1 使沉降成为每次断连热路径后,该搁浅被放大为 FTP 控制/数据通道 + * 互杀(E7 实测:lftpd 5s 超时、closed-with-data 救回全部迟到)。 + * 本 Socket 残留全局位交 net_poll 正常读到时 SINT 已为 0,无害了结 */ /* 读芯片当前状态(两字节) */ ch395f_get_socket_status((uint8_t)sockfd, st); diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index d565980c..e736f5b9 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -3420,8 +3420,13 @@ static void handle_disconnect_event(net_sock_t *p_sock) { /* Trap 23 推广(2026-09-24 台架实证):正常 DISCONNECT 释放留下的芯片收尾状态 * 会在复用同一 fd 的下一条连接上以"伪 DISCONNECT"形式投递(实测 accept 后约 10~20ms)→ 该连接被提前释放,应用把已到的 2048B 当整份文件并回报 226; * 仅**本连接收过数据**时登记沉降(实测触发源 = 收过数据的数据通道被关闭),纯连接即关/只发不收的连接免沉降,保住 Trap 22 的快速复连时序(:21 @0ms ×50 全通过) */ - /* 本连接收过数据:登记沉降(同 fd 下次重监听前多等 400/800ms 沉降窗口) */ - if (p_sock->recv_bytes != 0U) { + /* F-NET-15 M1(N34 实验 2026-10-12):沉降**无条件**登记。原 recv_bytes!=0 门的 + * 受害者由事件时序定义(accept 后 ~20ms 被陈旧 DISCONNECT 杀 → recv_bytes==0 是 + * 构造性必然),门对"残渣循环自持"形态失效(n30_repro 背靠背 9/10 失败,E1 实测); + * 门原保护目标(:21 @0ms 快复连)已被 N-32 pump 延迟化(恢复窗实测 0.3~1s)架空, + * +100ms 沉降落在窗内不可见。沉降窗口同时让残渣 DISCONNECT 在 state==CLOSED + * (无害、仅留痕)时被消费,断开"杀下一条 → 新拆链 → 再杀"的繁殖链 */ + { /* 反推描述符(沉降按 fd 登记) */ int qfd = (int)(p_sock - s_net_socks); /* 描述符范围保护 */ diff --git a/test/host/relisten_stress.py b/test/host/relisten_stress.py new file mode 100644 index 00000000..a92502ef --- /dev/null +++ b/test/host/relisten_stress.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""relisten_stress.py - listener reconnect stress (N34 E1/E5 bench leg). + +Connect -> optional hold -> immediate close, repeated at a fixed gap. +Counts refusals and connect latency distribution; non-ok rounds listed. +Pure stdlib, ASCII output only. + +Usage: + python relisten_stress.py --port 7003 --n 50 --gap-ms 100 + python relisten_stress.py --port 7003 --n 20 --gap-ms 0 --hold-ms 50 +Exit codes: 0 = zero refusals / 1 = refusals seen / 2 = usage. +""" +import argparse +import socket +import sys +import time + + +def main(): + ap = argparse.ArgumentParser(description="listener reconnect stress") + ap.add_argument("--host", default="192.168.1.100") + ap.add_argument("--port", type=int, required=True) + ap.add_argument("--n", type=int, default=50) + ap.add_argument("--gap-ms", type=int, default=100) + ap.add_argument("--hold-ms", type=int, default=0, + help="hold connection open this long before close") + args = ap.parse_args() + + results = [] + for i in range(args.n): + t0 = time.time() + try: + s = socket.create_connection((args.host, args.port), timeout=3) + lat = (time.time() - t0) * 1000.0 + if args.hold_ms: + time.sleep(args.hold_ms / 1000.0) + try: + s.shutdown(socket.SHUT_RDWR) + except OSError: + pass + s.close() + results.append((i, "ok", lat)) + except ConnectionRefusedError: + results.append((i, "refused", (time.time() - t0) * 1000.0)) + except Exception as e: + results.append((i, type(e).__name__, (time.time() - t0) * 1000.0)) + if args.gap_ms: + time.sleep(args.gap_ms / 1000.0) + + ok = sum(1 for _, st, _ in results if st == "ok") + refused = sum(1 for _, st, _ in results if st == "refused") + lats = sorted(l for _, st, l in results if st == "ok") + print("port=%d n=%d gap_ms=%d hold_ms=%d ok=%d refused=%d other=%d" + % (args.port, args.n, args.gap_ms, args.hold_ms, + ok, refused, args.n - ok - refused)) + if lats: + print("connect latency ms: min=%.0f p50=%.0f p90=%.0f max=%.0f" + % (lats[0], lats[len(lats) // 2], lats[int(len(lats) * 0.9)], lats[-1])) + for i, st, l in results: + if st != "ok": + print(" round %d: %s (%.0f ms)" % (i, st, l)) + return 1 if refused else 0 + + +if __name__ == "__main__": + sys.exit(main())