Files
TCP2UART/CH390_RX_PBUF泄漏修复复盘.md
T

246 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CH390 / lwIP 固定次数 ping 失败问题修复复盘
## 1. 问题现象
在 TCP2UART 固件运行后,设备初期可以正常 ARP 和 ping,但连续 ping 一段时间后不再响应。
典型现象:
- 设备 IP`192.168.31.100`
- 设备 MAC`02:00:00:00:00:01`
- 对端/网关 IP`192.168.31.1`
- 对端/网关 MAC`00:e0:4c:28:1e:60`
- 失败后设备仍持续发送 TCP SYN/RST 或 client timeout 相关流量,说明 TX、任务调度和应用层并未整体死机。
- 失败后对端继续向设备 MAC 发送 ICMP/ARP,但设备不再回复。
关键抓包:
- `WiresharkLog/04290150.pcapng`
- `seq=1884..1891` 共 8 次 ping reply 正常。
- 第 9 次 `seq=1892` 开始无 reply。
- `WiresharkLog/04290206.pcapng`
- 曾把 `PBUF_POOL_SIZE` / `MEMP_NUM_TCPIP_MSG_INPKT` 从 8 临时扩大到 16。
- 成功 ping 从 8 次变为 `seq=1900..1915` 共 16 次。
- 第 17 次 `seq=1916` 开始无 reply。
这个“成功次数随池大小移动”的现象证明:问题不是 CH390 随机丢包,也不是 PHY/TX 死掉,而是每次成功处理 ping 后都有某个 pbuf 引用没有释放,最终耗尽 `PBUF_POOL`
## 2. 排查过程中的重要结论
### 2.1 CH390 RX 读包路径曾存在风险,但不是最终根因
早期排查时发现 CH390 RX 路径与参考驱动存在若干不一致,已修正:
- `ch390_receive_packet()` 按参考序列读取 RX ready:先读 `MRCMDX` dummy,再读 `MRCMDX1`
- 校验 RX header 的 `Head` 字节必须为 `CH390_PKT_RDY`
- CH390 RX SRAM 中的 `rx_len` 包含 Ethernet FCS,交给 lwIP 前需要减去 4 字节。
- `ch390_rx_reset()` 显式写 `MPTRCR_RST_RX` 复位 RX memory pointer。
这些修正确保 CH390 RX FIFO 读包更接近参考实现,但无法解释“固定 8 次/16 次后失败”。
### 2.2 扩大 lwIP 池只能延迟问题
曾临时将如下配置从 8 提到 16
```c
#define PBUF_POOL_SIZE 16
#define MEMP_NUM_PBUF 16
#define MEMP_NUM_TCPIP_MSG_INPKT 16
```
结果成功 ping 次数也从 8 变成 16。这说明扩大池子只是延迟耗尽,不能作为根修复。
最终已恢复为 8
```c
#define PBUF_POOL_SIZE 8
#define MEMP_NUM_PBUF 8
#define MEMP_NUM_TCPIP_MSG_INPKT 8
```
### 2.3 `tcpip_input()` 异步队列不是最终根因
项目启用了 lwIP core locking。为避免每个 RX 包占用 `MEMP_TCPIP_MSG_INPKT`,配置已改为同步输入:
```c
#define LWIP_TCPIP_CORE_LOCKING 1
#define LWIP_TCPIP_CORE_LOCKING_INPUT 1
```
这样 `tcpip_input()` 会在 core lock 下同步调用 `ethernet_input()`,不再通过 `TCPIP_MSG_INPKT` 邮箱异步排队。
但用户后续验证仍然固定 8 次后停止,且每次成功 ping 都已经有 reply,因此说明 RX 包确实已经进入 ICMP 处理路径,问题更可能是 reply 输出路径增加了 pbuf 引用但未释放。
## 3. 最终根因
最终根因位于:
```text
Drivers/LwIP/src/netif/ethernet.c
```
`ethernet_output()` 实现:
```c
q = pbuf_alloc(PBUF_RAW_TX, SIZEOF_ETH_HDR, PBUF_RAM);
if (q == NULL) {
LINK_STATS_INC(link.memerr);
LINK_STATS_INC(link.drop);
return ERR_MEM;
}
pbuf_chain(q, p);
ethhdr = (struct eth_hdr *)q->payload;
SMEMCPY(&ethhdr->dest, dst, sizeof(struct eth_addr));
SMEMCPY(&ethhdr->src, src, sizeof(struct eth_addr));
ethhdr->type = lwip_htons(eth_type);
return netif->linkoutput(netif, q);
```
问题在 `pbuf_chain(q, p)`
lwIP 的 `pbuf_chain()` 会执行:
```c
pbuf_ref(t);
```
也就是给被挂接的原始 pbuf `p` 引用计数加 1。
ICMP echo reply 路径会复用 RX pbuf
```text
ethernetif_poll()
-> tcpip_input()
-> ethernet_input()
-> ip4_input()
-> icmp_input()
-> ip4_output_if()
-> etharp_output()
-> ethernet_output()
```
`icmp_input()` 末尾本身会 `pbuf_free(p)`,这部分是正确的。但在原实现中,`ethernet_output()` 通过 `pbuf_chain(q, p)``p` 额外加了一次引用,却没有在 `linkoutput()` 返回后释放临时 header pbuf `q`
因此每次 ping 的引用计数变化是:
```text
RX pbuf 初始 ref = 1
pbuf_chain(q, p) 后 ref = 2
icmp_input() 末尾 pbuf_free(p) 后 ref = 1
=> p 永远没有回到 0PBUF_POOL 泄漏 1 个
```
所以:
- `PBUF_POOL_SIZE=8` 时,8 次 ping reply 后耗尽。
- 临时扩大到 16 时,16 次 ping reply 后耗尽。
## 4. 修复方案
修复 `ethernet_output()`,在同步 `linkoutput()` 完成后释放临时 header pbuf 链:
```c
err_t ethernet_output(struct netif *netif,
struct pbuf *p,
const struct eth_addr *src,
const struct eth_addr *dst,
u16_t eth_type)
{
struct pbuf *q;
struct eth_hdr *ethhdr;
err_t err;
LWIP_ASSERT("netif != NULL", netif != NULL);
LWIP_ASSERT("p != NULL", p != NULL);
LWIP_ASSERT("src != NULL", src != NULL);
LWIP_ASSERT("dst != NULL", dst != NULL);
q = pbuf_alloc(PBUF_RAW_TX, SIZEOF_ETH_HDR, PBUF_RAM);
if (q == NULL) {
LINK_STATS_INC(link.memerr);
LINK_STATS_INC(link.drop);
return ERR_MEM;
}
pbuf_chain(q, p);
ethhdr = (struct eth_hdr *)q->payload;
SMEMCPY(&ethhdr->dest, dst, sizeof(struct eth_addr));
SMEMCPY(&ethhdr->src, src, sizeof(struct eth_addr));
ethhdr->type = lwip_htons(eth_type);
err = netif->linkoutput(netif, q);
pbuf_free(q);
return err;
}
```
为什么这里可以释放 `q`
- 本项目 `low_level_output()` 是同步发送。
- 它会立即遍历 pbuf 链,把数据复制到 `s_tx_buffer`
- 随后调用 `ch390_runtime_send_packet()` 把连续 buffer 发给 CH390。
- `low_level_output()` 返回后不再持有 pbuf 指针。
因此 `ethernet_output()``linkoutput()` 返回后释放 `q` 是正确的。
`pbuf_free(q)` 会同时:
- 释放临时 Ethernet header pbuf `q`
- 解除 `pbuf_chain()` 对原始 RX pbuf `p` 增加的引用;
- 之后 `icmp_input()` 末尾的 `pbuf_free(p)` 可以真正把 RX pbuf 归还 `PBUF_POOL`
## 5. 不要做的错误修复
### 5.1 不要在 `netif->input()` 成功后手动释放 pbuf
驱动层当前逻辑是正确的:
```c
input_err = ch390_netif.input(p, &ch390_netif);
if (input_err != ERR_OK) {
pbuf_free(p);
}
```
`netif->input()` 返回 `ERR_OK` 时,pbuf ownership 已经交给 lwIP。此时驱动不能再 `pbuf_free(p)`,否则会造成 double-free 或 use-after-free。
### 5.2 不要只扩大 `PBUF_POOL_SIZE`
扩大池子只会让失败次数从 8 变 16、32……不会修复泄漏。
### 5.3 不要继续优先怀疑 CH390 PHY/TX
抓包中失败后设备仍持续发送 TCP SYN/RST,说明 TX 和任务仍活着。固定次数失败更符合 pbuf 引用泄漏。
## 6. 验证结果
修复后 Keil 构建通过:
```text
"TCP2UART\TCP2UART.axf" - 0 Error(s), 0 Warning(s).
Program Size: Code=93376 RO-data=2768 RW-data=456 ZI-data=56032
```
用户烧录验证后确认问题已修复。
## 7. 后续排查建议
如后续再次出现固定次数网络停止,优先检查:
1. 是否存在 `pbuf_chain()` / `pbuf_ref()` 后没有配对 `pbuf_free()` 的路径。
2. 是否有 ARP pending queue 长时间持有 pbuf。
3. 是否有 TCP `recvmbox` / 应用桥接队列背压长期持有 pbuf。
4. 是否有人在 `netif->input()` 成功后错误释放 pbuf,导致内存破坏。
推荐排查点:
- `Drivers/LwIP/src/netif/ethernet.c`
- `Drivers/LwIP/src/core/ipv4/icmp.c`
- `Drivers/LwIP/src/core/ipv4/etharp.c`
- `Drivers/LwIP/src/core/pbuf.c`
- `Drivers/LwIP/src/netif/ethernetif.c`