lwIP UDP接收异常:PBUF_POOL_SIZE次后失效但pbuf已释放
首先,咱们直接戳破核心问题:你看到的"pbuf池耗尽"现象是真的,但不是因为发送用的p_out没释放,而是接收路径的pbuf完全没被你释放!这是lwIP开发中非常容易踩的坑,结合你的代码和现象,我来一步步拆解修复方案:
1. 最致命的问题:接收回调未释放pbuf
lwIP在把接收到的数据包传给你的rcv_app_msg回调时,会把pbuf的管理权完全交给用户。如果回调里不调用pbuf_free(p),这个pbuf会一直占用池里的节点——每次接收响应都会耗掉一个pbuf,直到PBUF_POOL_SIZE次后,池里没有可用资源,lwIP就无法再处理新的接收数据包了(Wireshark能抓到是因为硬件收到了,但lwIP拿不到内存来解析)。
修复你的接收回调:
void rcv_app_msg(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { // callback from the ISR rx_flag = 1; // 必须释放接收到的pbuf,否则pbuf池会被彻底耗尽 if (p != NULL) { pbuf_free(p); } }
2. 发送pbuf的payload赋值错误
你直接写p_out->payload = msg;是完全错误的:pbuf_alloc分配的RAM是独立的,你需要把msg的内容拷贝到pbuf的payload内存里,而不是直接赋值指针(否则pbuf指向的是msg的地址,若msg是栈变量或被覆盖,会出现数据混乱,同时lwIP无法正确管理这段内存)。
修复发送逻辑:
p_out = pbuf_alloc(PBUF_TRANSPORT, sz, PBUF_RAM); if ( p_out == NULL ) { fatal_error("%s : could not allocate out pbuf of %d\n", __func__, sz); } // 替换直接赋值指针为内存拷贝 memcpy(p_out->payload, msg, sz); udp_sendto(pcb_out, p_out, IP_ADDR_BROADCAST, APP_PORT_NUM);
3. 重复配置UDP PCB导致的冗余问题
你在send_app_broadcast_msg和app_inc_pcb_refcount里重复调用udp_bind、udp_connect、udp_recv,这会导致端口重复绑定、回调重复注册,可能引发未知的异常行为。
修改send_app_broadcast_msg,移除重复的PCB配置代码:
void send_app_broadcast_msg(char *msg) { uint32_t sz = strlen(msg); if ( sz > APP_MAX_MSG_SIZE ) { fatal_error("%s : requested message too large (%d, %d) \n", __func__, sz, APP_MAX_MSG_SIZE); } xprintf("sending broadcast msg \"%s\"... ", msg); app_inc_pcb_refcount(); // 这里已经完成了PCB的初始化、绑定、回调注册 if ( pcb_out == NULL ) { fatal_error("%s : could not allocate out pcb\n", __func__); } ip_set_option(pcb_out, SOF_BROADCAST); // 移除下面重复的配置代码 // udp_bind(pcb_out, IP4_ADDR_ANY, APP_PORT_NUM); // udp_connect(pcb_out, IP4_ADDR_ANY, APP_PORT_NUM); // udp_recv(pcb_out, rcv_app_msg, NULL); p_out = pbuf_alloc(PBUF_TRANSPORT, sz, PBUF_RAM); if ( p_out == NULL ) { fatal_error("%s : could not allocate out pbuf of %d\n", __func__, sz); } memcpy(p_out->payload, msg, sz); udp_sendto(pcb_out, p_out, IP_ADDR_BROADCAST, APP_PORT_NUM); }
4. 引用计数的拼写错误
你在app_inc_pcb_refcount里写了pixee_pcb_refcount++;,但实际定义的变量是app_pcb_refcount,这个拼写错误会导致引用计数失效,可能引发PCB被错误释放的问题:
static void app_inc_pcb_refcount(void) { if (app_pcb_refcount == 0) { if ( pcb_out != NULL ) { fatal_error( "%s : memory leak\n", __func__ ); } pcb_out = udp_new(); if (pcb_out == NULL) { fatal_error( "%s : could not allocate pcb\n", __func__ ); } udp_bind(pcb_out, IP4_ADDR_ANY, APP_PORT_NUM); udp_connect(pcb_out, IP4_ADDR_ANY, APP_PORT_NUM); udp_recv(pcb_out, rcv_app_msg, NULL); } // 修复拼写错误 app_pcb_refcount++; return; }
5. 释放p_out后的无效指针访问
在app_check_timeouts里,你先调用pbuf_free(p_out),然后还访问p_out->ref——这是未定义行为,因为pbuf_free后p_out已经是无效指针了。修改为:
if (end_flag) { // 先获取ref计数,再释放pbuf u8_t ref_count = p_out->ref; uint8_t c = pbuf_free(p_out); xprintf("p_out=%p ref=%d, c=%d\n", (void*)p_out, ref_count, c); app_dec_pcb_refcount(); p_out = NULL; pcb_out = NULL; timeout_ctr = 0; rx_flag = 0; signal_state = SIGNAL_STATE_IDLE; }
为什么之前的现象和PBUF_POOL_SIZE强相关?
每次接收服务器响应时,lwIP都会从pbuf池分配一个节点来存储数据包,然后传给你的回调函数。你之前没有释放这些接收用的pbuf,所以每通信一次就耗掉一个池节点,当次数达到PBUF_POOL_SIZE时,池被彻底耗尽,lwIP无法再接收新的数据包,自然就会超时。
把这些修复点全部落实后,即使PBUF_POOL_SIZE设为5,也能持续正常通信,不会再出现超时问题。
内容的提问来源于stack exchange,提问作者danmcb

