lwIP RAW TCP를 붙이다 보면 송신은 되는데 종료에서 막히는 경우가 있다.
- 데이터 전송은 얼추 된다
- 마지막에 연결을 닫으려 한다
tcp_close()가 가끔ERR_MEM을 반환한다- 재시도하면 되기도 하고 안 되기도 한다
이때 ERR_MEM이라는 이름 때문에
메모리 풀 크기부터 올리기 쉽다.
그런데 실제로는 close 시점의 TCP 송신 상태가 아직 정리되지 않은 경우가 더 많다.
오늘 메모는 lwIP RAW TCP에서 tcp_close()가 ERR_MEM을 반환할 때 어디부터 봐야 하는지다.
결론부터
tcp_close()의 ERR_MEM은
항상 “RAM이 바닥났다”는 뜻이 아니다.
close는 단순 free가 아니라
남아 있는 송신 상태를 마무리하면서 FIN까지 실어 보내야 하는 동작이다.
app wants close
-> lwIP checks pcb state
-> unsent / unacked / queue state inspected
-> FIN path needs room and valid sequencing
따라서 종료 직전에 아직 정리되지 않은 세그먼트가 있으면
tcp_close()가 바로 성공하지 않을 수 있다.
왜 send는 되는데 close에서만 터지나
전송 중에는 애플리케이션이 이런 흐름을 자주 만든다.
tcp_write()
-> tcp_output()
-> last payload queued
-> immediately tcp_close()
겉으로는 “보낼 건 다 보냈다”고 느껴진다.
하지만 lwIP 내부에서는 아직:
unsent큐에 남아 있거나unacked에 대기 중이거나- 세그먼트 자원이 아직 회수되지 않았을 수 있다
즉 앱 기준 “끝”과
TCP 기준 “정상 종료 가능” 시점은 다를 수 있다.
흔한 패턴 1: 마지막 tcp_write() 직후 바로 close한다
가장 자주 보는 그림이다.
tcp_write(pcb, buf, len, TCP_WRITE_FLAG_COPY);
tcp_output(pcb);
tcp_close(pcb);
이 코드는 짧은 테스트에서는 지나갈 수 있다.
하지만 링크가 느리거나 ACK가 늦으면
마지막 데이터가 아직 확인되지 않았을 수 있다.
이때 tcp_close()는
“지금 당장 깔끔하게 닫기 어렵다”는 의미로 ERR_MEM을 줄 수 있다.
흔한 패턴 2: sent callback과 에러 경로를 분리하지 않았다
RAW API에서는 종료 시점을 콜백 기준으로 맞추는 편이 안전하다.
예를 들면:
application marks want_close = true
-> sent callback fires as ACK progresses
-> when pending bytes reach 0
-> tcp_close() attempted
그런데 이런 구분이 없으면
애플리케이션은 아직 unacked가 남은 시점에도 close를 던지게 된다.
결과는 보통 둘 중 하나다.
ERR_MEM이 반복된다- close retry 루프가 busy loop가 된다
흔한 패턴 3: close 재시도를 잘못된 문맥에서 한다
ERR_MEM이 떴다고 해서 아무 곳에서나 다시 호출하면 더 꼬인다.
특히 아래 문맥은 주의가 필요하다.
- ISR에서 직접 재시도
- 다른 태스크가 pcb를 동시에 건드림
- 에러 callback 이후 이미 무효가 된 pcb 재사용
close 실패는 “조금 뒤 다시 시도”가 맞을 수 있다.
하지만 그 재시도는 같은 TCP 문맥 안에서, pcb 수명이 유효할 때 해야 한다.
호출 문맥 자체가 섞이면
문제가 ERR_MEM이 아니라 코어 락과 상태 오염으로 바뀐다.
관련해서는 lwIP에서 ISR에서 바로 보내면 가끔 터진다: tcpip_thread로 넘기는 패턴 정리와
lwIP에서 tcpip_thread 안에서 다시 기다리면 멈춘다: callback 안의 netconn/sockets 호출이 self-deadlock이 되는 이유도 같이 보면 흐름이 더 잘 맞는다.
ERR_MEM인데 왜 메모리만 올려도 안 고쳐지나
송신 관련 자원은 여러 층에 걸쳐 있다.
TCP_SND_BUFTCP_SND_QUEUELENMEMP_NUM_TCP_SEG- pcb의
unsent/unacked상태
그래서 메모리를 조금 올려도
종료 시점이 너무 이르면 증상은 그대로 남는다.
즉 이 문제는 단순 용량보다
close를 언제 호출했는가가 더 중요한 경우가 많다.
버퍼와 세그먼트 자원 자체를 읽는 법은
lwIP TCP 버퍼 설정 읽는 법: TCP_SND_BUF, TCP_WND, MEM_SIZE, MEMP_NUM_TCP_SEG, PBUF_POOL_SIZE에서 더 자세히 볼 수 있다.
구현 쪽에서 무난한 패턴
가장 단순한 방향은
“보내기 끝”과 “닫기 시도”를 분리하는 것이다.
app enqueues last data
-> mark close_pending = true
-> sent callback tracks outstanding bytes
-> when no unsent and no unacked remain
-> tcp_close()
또는 close가 바로 안 되면:
tcp_close() == ERR_MEM
-> keep close_pending
-> retry from next safe callback
핵심은 tcp_close()를 즉시 성공해야 하는 API처럼 다루지 않는 것이다.
abort로 바로 끝내면 안 되나
테스트 장비나 일회성 연결에서는 tcp_abort()가 더 단순해 보일 수 있다.
하지만 의미가 다르다.
tcp_close()
-> graceful close, FIN path
tcp_abort()
-> immediate teardown, pending data loss possible
따라서 로그를 남기거나 상대가 마지막 payload를 반드시 받아야 하는 흐름이라면
ERR_MEM이 뜬다고 바로 abort로 바꾸기보다
왜 정상 종료 타이밍이 안 맞는지 먼저 보는 편이 낫다.
로그를 이렇게 남기면 빨라진다
이 문제는 ERR_MEM 한 줄만 남기면 원인을 못 좁힌다.
나는 보통 아래 항목을 같이 본다.
- 마지막
tcp_write()시각과 길이 tcp_sentcallback에서 남은 outstanding byte 변화tcp_close()호출 시 pcb state- close 직전
unsent/unacked존재 여부 ERR_MEM재시도가 어느 문맥에서 실행됐는지
이 다섯 줄이 있으면
“메모리가 부족한가?”보다
“아직 닫을 준비가 안 된 pcb를 너무 빨리 닫았나?”를 먼저 볼 수 있다.
빠른 체크리스트
- 마지막
tcp_write()직후 바로tcp_close()를 호출하는 경로가 있는지 확인 tcp_close()재시도를 sent/poll 같은 안전한 callback에서 하는지 확인ERR_MEM이 뜰 때 pcb의unsent또는unacked가 남아 있는지 확인- close 실패를 heap 부족으로 단정하고 설정값만 키우는지 점검
- 정상 종료가 꼭 필요하지 않은 경로와
tcp_abort()를 써도 되는 경로를 분리했는지 확인
한 줄 요약
lwIP RAW TCP에서 tcp_close()가 ERR_MEM을 반환하면 free RAM보다 먼저 아직 정리되지 않은 송신 큐와 ACK 대기 상태를 보고, 종료 시도를 sent/poll 기준으로 늦추는 편이 빠르다.
추천 키워드
lwIP, TCP, tcp_close, ERR_MEM, RAW API, embedded network
DevBJ | 오늘을살자, Log Today