lwIP를 RTOS 위에서 붙이다 보면 “락은 안 보이는데 멈춘다” 싶은 순간이 있다.
- 링크 이벤트 직후만 멈춘다
- 어떤 콜백에서 send를 한 뒤부터 응답이 없다
- CPU는 살아 있는데 네트워크만 굳는다
- watchdog이 걸릴 정도는 아닌데 패킷 흐름이 끊긴다
이런 경우는 메모리 부족보다 먼저
지금 내가 tcpip_thread 안에서 다시 tcpip_thread를 기다리고 있지 않은지를 보는 편이 빠르다.
오늘 메모는 lwIP에서 callback 안의 netconn/sockets 호출이 self-deadlock처럼 보이는 이유다.
핵심부터
RTOS + tcpip_thread 모델에서 많은 API는 내부적으로 이런 그림으로 돈다.
user task
-> 메시지를 tcpip_thread로 전달
-> tcpip_thread가 실제 처리
-> 완료될 때까지 caller가 기다림
이 구조 자체는 정상이다.
문제는 caller가 이미 tcpip_thread일 때다.
tcpip_thread 안의 callback
-> netconn/socket API 호출
-> 다시 tcpip_thread 처리 완료를 기다림
-> 자기 자신을 기다리는 모양이 됨
이때 구현과 옵션에 따라 정확한 형태는 다르지만,
현상은 대체로 멈춤, 긴 지연, 응답 정지로 보인다.
왜 낮은 부하에서는 지나가는가
초기 bring-up에서는 이런 코드가 바로 안 드러날 때가 있다.
- 호출 빈도가 낮다
- 타이밍상 다른 경로가 먼저 빠져나간다
- 테스트가 단순해 콜백 중첩이 적다
그러다가 링크 flap, 재연결, burst traffic, 상태 콜백 연쇄가 들어오면
그제서야 재현된다.
그래서 겉보기에는 “가끔 멈춘다”가 된다.
흔한 진입점 1: tcpip_callback 안에서 netconn API를 다시 부른다
이 패턴은 의도는 선해 보인다.
user task
-> tcpip_callback()으로 코어 컨텍스트 진입
-> callback 내부에서 netconn_send() 호출
겉으로는 “코어 스레드 안에서 처리하면 더 안전하겠지”처럼 보인다.
그런데 netconn_* 계열은 이미 자체 메시지 전달과 대기를 포함할 수 있다.
그러면 callback 안에서 또 동기 API를 부르며
메시지 패싱 위에 메시지 패싱을 겹치는 구조가 된다.
흔한 진입점 2: netif/status/link callback 안에서 socket send를 건다
링크 복구 시점에 알림 패킷 하나 보내고 싶어서
상태 콜백 안에서 바로 send()를 호출하는 코드도 자주 본다.
link callback
-> application notify packet send()
-> 그 뒤부터 간헐 정지
문제는 그 콜백이 어떤 컨텍스트에서 도는지
프로젝트 코드에서는 잊기 쉽다는 점이다.
특히 아래 상황이 위험하다.
- 링크 up/down 처리 중
- 주소 변경 callback 중
- DHCP 상태 변경 callback 중
이 시점에는 내부 상태 전환이 아직 끝나지 않았을 수 있다.
그래서 socket API를 바로 물면
데드락 비슷한 정지나 긴 대기처럼 보이기 쉽다.
흔한 진입점 3: 수신 callback 안에서 블로킹 호출을 섞는다
RAW API receive callback이나 이벤트 callback에서
“응답을 하나 바로 보내고 끝내자”는 구조도 많이 나온다.
여기서 low-level send 자체는 가능한 경우가 있어도,
중간에 아래 호출이 섞이면 위험해진다.
- 다른 netconn read/write
- socket recv/send with timeout
- semaphore 대기 후 lwIP 재호출
즉 문제는 “콜백 안에서 보냈다” 자체보다
콜백 안에서 다시 동기 대기 경로를 열어 버린 것에 가깝다.
증상이 ERR_MEM처럼 보일 때도 있다
self-deadlock이 항상 완전 정지로만 보이는 건 아니다.
- 송신 큐가 안 비워져
ERR_MEM처럼 보임 - 응답 task가 영원히 안 깨어 timeout처럼 보임
- mailbox가 쌓이면서 몇 초 뒤에만 회복됨
그래서 메모리 옵션만 늘리고 끝내면
근본 원인을 놓치기 쉽다.
특히 TCPIP_MBOX_SIZE를 키우면
문제가 늦게 드러질 뿐 사라지지 않는 경우가 있다.
로그를 이렇게 남기면 빨라진다
이 이슈는 패킷 캡처만으로는 잘 안 보인다.
나는 보통 아래 항목을 같이 본다.
- 문제가 난 함수가 어떤 태스크/스레드 문맥에서 호출됐는지
tcpip_callback진입/종료 시각- 그 안에서 호출한
netconn_*또는send()/recv()기록 - mailbox depth 또는 semaphore 대기 시간
- 링크/주소/DHCP 상태 callback 직후인지 여부
이 다섯 줄이 있으면
“네트워크가 멈췄다”보다
“콜백 안에서 자기 자신을 기다린다”는 그림이 빨리 나온다.
구현 쪽에서 무난한 패턴
안전하게 가려면 경계를 단순하게 두는 편이 낫다.
tcpip_thread callback
-> 상태만 갱신
-> 필요한 작업은 app worker queue에 예약
app worker/task
-> netconn/socket API 호출
즉 코어 컨텍스트에서는:
- 상태 표시
- 플래그 세팅
- 짧은 비블로킹 처리
까지만 하고,
동기 대기 가능성이 있는 API는 바깥 task로 빼는 편이 안전하다.
프로젝트가 RAW API 중심이라면 더 단순하다.
callback 안에서는 같은 규칙의 비블로킹 처리만 하고,
다른 레이어 API와 섞지 않는 편이 덜 아프다.
빠른 체크리스트
tcpip_callback, link/status callback, DHCP callback 안에서netconn_*또는send()/recv()를 부르는지 확인- 호출 함수가 실제로 어떤 스레드 문맥에서 도는지 로그로 확인
- 코어 스레드 안의 callback에서 semaphore, queue, event 대기가 있는지 확인
ERR_MEM이나 timeout이 사실은 송신 완료 정지의 결과가 아닌지 확인- 네트워크 알림 송신은 callback 안에서 직접 하지 말고 app worker로 넘기도록 분리했는지 확인
한 줄 요약
lwIP에서 콜백 직후 전체 네트워크가 멈춘다면 메모리보다 먼저 tcpip_thread 안에서 다시 netconn/sockets API를 기다리며 self-deadlock 비슷한 구조를 만들고 있지 않은지 확인하는 게 빠르다.
추천 키워드
lwIP, TCPIP, tcpip_thread, netconn, RTOS, embedded network
DevBJ | 오늘을살자, Log Today