Skip to content
오늘을살자
Go back

lwIP RAW TCP에서 close가 가끔 실패한다: tcp_close()의 ERR_MEM은 free RAM보다 송신 잔여 상태를 먼저 봐야 한다

Edit page

lwIP RAW TCP를 붙이다 보면 송신은 되는데 종료에서 막히는 경우가 있다.

이때 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 내부에서는 아직:

즉 앱 기준 “끝”과
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를 던지게 된다.

결과는 보통 둘 중 하나다.

흔한 패턴 3: close 재시도를 잘못된 문맥에서 한다

ERR_MEM이 떴다고 해서 아무 곳에서나 다시 호출하면 더 꼬인다.

특히 아래 문맥은 주의가 필요하다.

close 실패는 “조금 뒤 다시 시도”가 맞을 수 있다.
하지만 그 재시도는 같은 TCP 문맥 안에서, pcb 수명이 유효할 때 해야 한다.

호출 문맥 자체가 섞이면
문제가 ERR_MEM이 아니라 코어 락과 상태 오염으로 바뀐다.

관련해서는 lwIP에서 ISR에서 바로 보내면 가끔 터진다: tcpip_thread로 넘기는 패턴 정리
lwIP에서 tcpip_thread 안에서 다시 기다리면 멈춘다: callback 안의 netconn/sockets 호출이 self-deadlock이 되는 이유도 같이 보면 흐름이 더 잘 맞는다.

ERR_MEM인데 왜 메모리만 올려도 안 고쳐지나

송신 관련 자원은 여러 층에 걸쳐 있다.

그래서 메모리를 조금 올려도
종료 시점이 너무 이르면 증상은 그대로 남는다.

즉 이 문제는 단순 용량보다
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 한 줄만 남기면 원인을 못 좁힌다.

나는 보통 아래 항목을 같이 본다.

  1. 마지막 tcp_write() 시각과 길이
  2. tcp_sent callback에서 남은 outstanding byte 변화
  3. tcp_close() 호출 시 pcb state
  4. close 직전 unsent / unacked 존재 여부
  5. ERR_MEM 재시도가 어느 문맥에서 실행됐는지

이 다섯 줄이 있으면
“메모리가 부족한가?”보다
“아직 닫을 준비가 안 된 pcb를 너무 빨리 닫았나?”를 먼저 볼 수 있다.

빠른 체크리스트

  1. 마지막 tcp_write() 직후 바로 tcp_close()를 호출하는 경로가 있는지 확인
  2. tcp_close() 재시도를 sent/poll 같은 안전한 callback에서 하는지 확인
  3. ERR_MEM이 뜰 때 pcb의 unsent 또는 unacked가 남아 있는지 확인
  4. close 실패를 heap 부족으로 단정하고 설정값만 키우는지 점검
  5. 정상 종료가 꼭 필요하지 않은 경로와 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


Edit page
Share this post on:

Next Post
DoIP에서 한동안 가만히 두면 첫 요청만 timeout 난다: 살아 있는 것처럼 보이는 TCP socket을 먼저 의심하자