lwIP로 UDP 수신을 붙이면 처음에는 멀쩡한데
조금 지나서 갑자기 멈추는 경우가 있다.
- 부팅 직후에는 잘 받는다
- 몇 분 지나면 콜백 빈도가 줄어든다
- 더 지나면 아예 수신이 멈춘다
- 재부팅하면 다시 잠깐 살아난다
이때 링크나 ARP부터 의심하기 쉽다.
그런데 원인은 더 단순한 경우가 많다.
오늘 메모는 RAW UDP receive callback에서 pbuf_free()를 놓쳤을 때 보이는 전형적인 증상이다.
결론부터
RAW API의 UDP receive callback으로 넘어온 pbuf는
애플리케이션이 사용을 마치면 해제해야 한다.
그걸 놓치면 작은 패킷 누수가 쌓여
결국 PBUF_POOL이 먼저 마른다.
UDP packet arrives
-> lwIP allocates pbuf
-> udp recv callback called
-> app returns without pbuf_free()
-> pool element stays occupied
초반에는 티가 안 나도,
트래픽이 계속 들어오면 결국 수신 경로 전체에 영향을 준다.
왜 처음에는 정상처럼 보이나
PBUF_POOL은 보통 여러 개를 미리 잡아 둔다.
그래서 한두 개씩 새어도 바로 안 드러난다.
- 패킷 빈도가 낮으면 몇 시간 뒤에야 보인다
- 테스트가 짧으면 문제를 놓친다
- 로그에는 마지막 실패 순간만 찍혀서 원인이 멀어 보인다
결국 체감상으로는
“가끔 멈춘다”가 된다.
흔한 실수 1: 정상 경로에서만 free하고 예외 경로는 빠뜨린다
제일 자주 보는 패턴은 이거다.
static void udp_rx(void *arg, struct udp_pcb *pcb, struct pbuf *p,
const ip_addr_t *addr, u16_t port)
{
if (p == NULL) {
return;
}
if (!is_valid_packet(p)) {
return;
}
handle_packet(p);
pbuf_free(p);
}
겉으로는 free를 하고 있다.
하지만 invalid packet 경로에서는 빠져 있다.
이렇게 되면 노이즈 패킷, 길이 오류, 포맷 오류가 들어올수록
누수가 더 빨리 커진다.
흔한 실수 2: payload만 복사하고 원본 pbuf를 잊는다
이 경우도 많다.
callback 진입
-> app buffer로 payload 복사
-> queue에 app buffer 전달
-> pbuf 해제 누락
복사는 끝났으니 일이 끝난 것처럼 느껴진다.
하지만 원래 pbuf는 여전히 pool을 잡고 있다.
즉 ownership을 이렇게 분리해서 봐야 한다.
- 앱 버퍼 복사본의 수명
- lwIP가 넘긴 원본
pbuf의 수명
복사를 했다고 원본이 자동으로 해제되지는 않는다.
흔한 실수 3: 다른 태스크로 넘기는데 ref/free 규칙이 없다
callback에서 바로 파싱하지 않고
worker task로 넘기는 구조도 흔하다.
이때 규칙이 불분명하면 금방 꼬인다.
callback
-> pbuf pointer를 queue에 넣음
-> worker가 나중에 처리
여기서 중요한 것은 둘 중 하나를 명확히 하는 것이다.
A. callback이 ownership을 worker에 넘기고 worker가 free
또는
B. callback이 필요한 데이터만 복사하고 즉시 free
둘 다 애매하면 이런 문제가 나온다.
- callback도 안 free
- worker도 안 free
- 에러 경로에서만 free 누락
즉 “누가 마지막에 free하는가”가 코드에 분명해야 한다.
pbuf pool 고갈은 어떻게 보이나
증상은 꼭 “메모리 누수”처럼 드러나지 않는다.
- UDP callback이 점점 덜 불린다
pbuf_alloc(PBUF_RAW, ...)실패 로그가 뜬다- ARP, ICMP 같은 다른 수신도 같이 이상해진다
- driver는 RX interrupt가 오는데 상위로 못 올라간다
왜냐하면 PBUF_POOL은 UDP만의 자원이 아니라
RX 경로 전체에서 같이 쓰이는 경우가 많기 때문이다.
그래서 앱 하나의 해제 누락이
시스템 전체 네트워크 저하처럼 보일 수 있다.
구현 쪽에서 무난한 패턴
가장 단순한 구조는 아래 둘 중 하나다.
1. callback 안에서 필요한 만큼만 복사하고 즉시 free
udp recv callback
-> validate
-> copy payload to app buffer
-> pbuf_free(p)
-> signal worker
이 방식은 ownership이 단순해서 덜 헷갈린다.
2. pbuf 자체를 넘길 거면 ownership을 문서화
udp recv callback
-> queue pbuf pointer to worker
-> worker always frees after use
-> every error path frees too
이 방식은 복사 비용을 줄일 수 있지만,
queue full, parse fail, shutdown 경로까지 free 규칙이 필요하다.
로그를 이렇게 남기면 빨라진다
이 문제는 마지막 실패 한 줄만 보면 드라이버 이슈처럼 보인다.
나는 보통 아래 항목을 같이 본다.
- UDP recv callback 호출 횟수
pbuf_free()호출 횟수- queue full 또는 parse fail 경로 진입 횟수
PBUF_POOL사용량 또는 alloc fail 로그- worker가 실제로 free한 시각
이 다섯 줄이 있으면
“네트워크가 끊겼다”보다
“수신 버퍼가 회수되지 않았다”는 쪽이 먼저 보인다.
빠른 체크리스트
- UDP recv callback의 모든
return경로에서pbuf해제가 보장되는지 확인 - payload 복사 후 원본
pbuf를 따로 해제하는지 확인 - callback과 worker 사이에서 ownership 주체를 주석이나 이름으로 명확히 했는지 확인
- queue full, parse error, shutdown 같은 예외 경로에도 free가 있는지 확인
PBUF_POOL고갈 로그와 UDP 수신 정지 시점을 같이 기록하는지 확인
한 줄 요약
lwIP RAW UDP callback에서 수신이 몇 분 뒤 멈춘다면 링크보다 먼저 pbuf_free() 누락을 의심하고, 모든 정상/예외 경로에서 누가 마지막으로 pbuf를 해제하는지 분명히 두는 편이 빠르다.
추천 키워드
lwIP, UDP, pbuf, PBUF_POOL, RAW API, embedded network
DevBJ | 오늘을살자, Log Today