Trap23两阶段修复: 异常释放400ms隔离+正常释放100ms沉降(仅收过数据), 修复中止后新传输首笔静默截断

This commit is contained in:
2026-09-25 01:57:03 +08:00
parent d0cbeb6b5b
commit a1f1f46262
2 changed files with 73 additions and 7 deletions
+36 -7
View File
@@ -1429,10 +1429,20 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags,
* NET_RELISTEN_SETTLE_MS 后强制放行,保证监听器不会永久失效。
* 正常 FIN 释放(handle_disconnect_event)不走此路径——既有行为零改动。
* 声明须早于 net_relisten_locked(其余重试状态见其后区块)。 */
#define NET_RELISTEN_QUARANTINE_MS 400U /* 隔离时长(等在途数据落尽 + 排空残渣) */
#define NET_RELISTEN_QUARANTINE_MS 400U /* 异常释放最短隔离(等在途数据落尽 + 排空残渣) */
#define NET_RELISTEN_GRACEFUL_MS 100U /* 正常释放最短沉降(等芯片完成上一条连接的收尾) */
#define NET_RELISTEN_SETTLE_MS 800U /* 隔离上限(须 < NET_RELISTEN_DEADLINE_MS,
* 否则隔离耗尽重试预算 → 监听被放弃,2026-09-23 实测) */
static uint8_t s_relisten_quarantine[NET_MAX_SOCKETS]; /* 1 = 本次释放为异常,需隔离 */
/* 重监听前"清芯片收尾状态"的两个等级(s_relisten_quarantine 取值):
* QUARANTINE - 异常释放(芯片 TIMEOUT):对端可能仍有在途数据,须清理 + 最短等 400ms;
* GRACEFUL - 正常释放(芯片 DISCONNECT):无在途数据,清理 + 最短等 100ms。
* 2026-09-24 台架实证:正常释放走的是既有"不复位芯片收尾状态"的路径,残留的 DISCONNECT
* 会在**复用同一 fd 的下一条连接**上被投递(accept 后约 10~20ms)→ 下一条上传被静默截断
* 到 2048B(一个芯片 RX 缓冲)且回报 226。只清理不等待(0ms)实测**无效**(4/4 仍截断),
* 说明芯片需要时间完成上一条连接的内部收尾;给足沉降即 4/4 通过。 */
#define NET_RELISTEN_SETTLE_QUARANTINE 1U
#define NET_RELISTEN_SETTLE_GRACEFUL 2U
static uint8_t s_relisten_quarantine[NET_MAX_SOCKETS]; /* 0=免清;1=异常释放;2=正常释放 */
static uint32_t s_relisten_settle_start[NET_MAX_SOCKETS]; /* 隔离起点(0 = 未开始) */
static uint32_t s_relisten_settle_dead[NET_MAX_SOCKETS]; /* 隔离上限到期时刻 */
@@ -1479,6 +1489,13 @@ static int net_relisten_locked(net_sock_t *p_sock) {
if (s_relisten_quarantine[sockfd] != 0U) {
uint8_t st[2] = {0, 0};
uint32_t now = HAL_GetTick();
/* 最短等待由释放来源决定(见 NET_RELISTEN_SETTLE_* 定义):异常释放等 400ms
* 让在途数据落尽;正常释放等 100ms 让芯片完成上一条连接的内部收尾。
* 日志按等级区分 quarantine(异常)/ settle(正常),便于台架分辨触发路径。 */
uint8_t lvl = s_relisten_quarantine[sockfd];
uint32_t hold = (lvl == NET_RELISTEN_SETTLE_QUARANTINE) ? NET_RELISTEN_QUARANTINE_MS
: NET_RELISTEN_GRACEFUL_MS;
const char *tag = (lvl == NET_RELISTEN_SETTLE_QUARANTINE) ? "quarantine" : "settle";
if (s_relisten_settle_start[sockfd] == 0U) {
s_relisten_settle_start[sockfd] = now;
@@ -1491,15 +1508,15 @@ static int net_relisten_locked(net_sock_t *p_sock) {
ch395f_get_socket_status((uint8_t)sockfd, st);
if (((st[0] != CH395F_SOCKET_CLOSED) || (st[1] != CH395F_TCP_CLOSED) ||
((uint32_t)(now - s_relisten_settle_start[sockfd]) < NET_RELISTEN_QUARANTINE_MS))) {
((uint32_t)(now - s_relisten_settle_start[sockfd]) < hold))) {
if ((int32_t)(now - s_relisten_settle_dead[sockfd]) < 0) {
DBG_INFO("sock%d relisten quarantine (sock=0x%02X tcp=0x%02X)",
sockfd, st[0], st[1]);
DBG_INFO("sock%d relisten %s (sock=0x%02X tcp=0x%02X)",
sockfd, tag, st[0], st[1]);
s_net_errno = NET_ERR_BUSY;
return -1; /* 交重试轮询(30ms 后复检) */
}
DBG_INFO("sock%d relisten quarantine timeout, force open (sock=0x%02X tcp=0x%02X)",
sockfd, st[0], st[1]);
DBG_INFO("sock%d relisten %s timeout, force open (sock=0x%02X tcp=0x%02X)",
sockfd, tag, st[0], st[1]);
}
s_relisten_quarantine[sockfd] = 0U; /* 隔离完成(或强制放行) */
s_relisten_settle_start[sockfd] = 0U;
@@ -2780,6 +2797,18 @@ static void handle_disconnect_event(net_sock_t *p_sock) {
/* 单连接:先不关 CH395F Socket,
* 留给 net_recv_locked 把剩余数据读出来再关,置 CLOSED 等待应用重新监听 */
DBG_INFO("sock%d DISCONNECTED, releasing (chip tcp=0x%02X)", p_sock->ch395_sock, st[1]);
/* Trap 23 推广(2026-09-24 台架实证):正常 DISCONNECT 释放留下的芯片收尾状态
* 会在复用同一 fd 的下一条连接上以"伪 DISCONNECT"形式投递(实测 accept 后约
* 10~20ms)→ 该连接被提前释放,应用把已到的 2048B 当整份文件并回报 226。
* 仅在**本连接收过数据**时登记沉降:实测触发源是"收过数据的数据通道被关闭",
* 而纯连接即关(tcp_reconnect_probe 的 0ms 复连用例)与只发不收的下载连接不残留
* —— 对它们免沉降即可保住 Trap 22 的快速复连时序(:21 @0ms ×50 全通过)。 */
if (p_sock->recv_bytes != 0U) {
int qfd = (int)(p_sock - s_net_socks);
if ((qfd >= 0) && (qfd < NET_MAX_SOCKETS)) {
s_relisten_quarantine[qfd] = NET_RELISTEN_SETTLE_GRACEFUL;
}
}
p_sock->state = NET_SOCK_STATE_CLOSED;
fire_event(p_sock, NET_EVENT_DISCONNECTED);
} else if (p_sock->state != NET_SOCK_STATE_CLOSED &&
+37
View File
@@ -664,3 +664,40 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报
- 修复后:RST 中止 **10/10**、优雅 FIN 中止 **5/5** 全部通过;
- 正常路径回归:30×64KB 零间隔 **30/30**、20×64KB **20/20**,SHA-256 全匹配;
- 启动门禁:`App init OK` / `net init: OK`、FORBID 0 行。
### 第二阶段:正常释放(FIN)路径——边界甄别与 100ms 沉降(2026-09-24)
**边界再甄别(现场"1~2s"时间假设被推翻)**:中止后 gap 500/2000/5000ms 台架复测 → 设备实存尺寸**全部仍 2048**(各 4/4,合计 12/12)→ **与时间无关**;监听重开本征即时(`t_accept` 30~47ms)。真实边界:
| 维度 | 结论 | 证据 |
|---|---|---|
| 触发 | 设备侧**收过数据**的数据连接被对端关闭(纯 FIN 即触发,不要求 RST) | 纯 `close()`(FIN)4/4 复现;RST 变体 5/5 |
| 受影响路径 | 仅**正常 DISCONNECT 释放路径**缺芯片状态沉降 | RST(TIMEOUT)路径已有一阶段 400ms 隔离保护 |
| 投递形式 | 芯片上一条连接的收尾残留,投递给**复用同一 fd 的下一条**连接(accept 后约 10~20ms 一条伪 DISCONNECT) | 设备日志:`standalone accept` → `DISCONNECTED, releasing (chip tcp=0x00)` |
| 影响面 | **一次性**(首条新连接中招,第二条起干净);同 fd 非 FTP 服务(`:7003` UI sink)不受影响(其 socket 不按传输复用) | 一次性 3/3 全对;`:7003` 复用 8/8 全过 |
| 时间 | 无关(gap 5s 仍失败)→ 现场"1~2s"只是人工重触的常见窗口,不是芯片机制 | gap 500/2000/5000ms 共 12/12 全部失败 |
**修复(第二阶段,`Drivers/BSP/NET/net_socket.c` +36/−7,netTask 串行上下文,零新增缓冲)**:
1. relisten 前沉降分两级:异常释放(芯片 TIMEOUT)=清理 + 最短等 400ms(`NET_RELISTEN_QUARANTINE_MS`,一阶段行为不变);正常释放(芯片 DISCONNECT)=清理 + 最短等 **100ms**(`NET_RELISTEN_GRACEFUL_MS`,新增);
2. 正常释放沉降**仅在本连接收过数据时登记**(`recv_bytes != 0`):纯连接即关(Trap 22 的 0ms 复连用例)与只发不收的下载连接无残留 → 免沉降保住快速复连时序;
3. 日志区分 `relisten settle`(正常)/ `relisten quarantine`(异常),便于台架分辨触发路径。
**沉降值实测悬崖**(只清理不等待 = 0ms):0ms **无效**(4/4 仍截断)→ 30ms 仍失败(4/4)→ **100ms 通过**(多轮稳定)→ 取 100ms(对 30ms 重试节拍 `NET_RELISTEN_RETRY_MS` 留 3× 余量)。
**验证(第二阶段后;设备日志 `UPLOAD '<path>' N bytes OK` 为权威真值)**:
| 用例 | 修复前 | 修复后 |
|---|---|---|
| FIN 关闭后**首笔**上传 ×8 | **0/8**(实存 2048 + 回报 226) | **8/8**(实存 4096,sha OK) |
| 纯 `close()` 变体 ×4 | 中招 | **4/4** |
| `ftp_abort_probe --graceful` ×5 | **0/5** | **5/5** |
| `ftp_abort_probe`(RST)×5 | 5/5(一阶段) | **5/5** |
| 无中止对照 ×4 / 一次性连发 3 笔 ×1 | 4/4 / 第 1 笔坏 | **4/4 / 3/3** |
| 正常往返 freeze 4096×4 / 64KB×2 | 通过 | **4/4 / 2/2**(t_stor 0.68→**0.18s**,t_retr 1.13→**0.19s**) |
| 非 FTP `:7003` sink 复用 ×8(RST+FIN) | 8/8 | **8/8** |
| Trap 22:`:21` 0ms×50 / 100ms×50,`:7003` 300ms×10,`:8888` 0ms×20 | 50/50 / 50/50 / 10/10 / 20/20 | **50/50 / 50/50 / 10/10 / 20/20** |
| 启动门禁 / 控制台 / 堆 | — | `App init OK` / `net init: OK`,FORBID **0**,`CONSOLE dropped 0`,`heap free=8776 min_ever=4816` |
**结论**:缺陷位于 **fd 复用生命周期**(芯片上一条连接的残留状态投递给下一条连接),不在 FTP 命令逻辑——lftpd 只是把它放大成误报 226。RST/TIMEOUT 半边由一阶段隔离处理,DISCONNECT 半边由本阶段沉降处理,合计覆盖全部"收过数据后关闭"的释放路径。已知残余边界:中止那一笔 STOR 自身仍回报 226(STOR 无长度声明、在途 FIN 与正常完成不可区分)——后续硬化见待办 #3。
**对应文件**:`Drivers/BSP/NET/net_socket.c`(`handle_disconnect_event` 沉降登记 + `net_relisten_locked` 两级沉降);`MDK-ARM/ftp_abort_probe.py` / `MDK-ARM/bench_diag_abort_gap.py`(边界甄别与回归判据)。