Skip to content
오늘을살자
Go back

lwIP에서 tcpip_thread 안에서 다시 기다리면 멈춘다: callback 안의 netconn/sockets 호출이 self-deadlock이 되는 이유

Edit page

lwIP를 RTOS 위에서 붙이다 보면 “락은 안 보이는데 멈춘다” 싶은 순간이 있다.

이런 경우는 메모리 부족보다 먼저
지금 내가 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를 부르며
메시지 패싱 위에 메시지 패싱을 겹치는 구조가 된다.

링크 복구 시점에 알림 패킷 하나 보내고 싶어서
상태 콜백 안에서 바로 send()를 호출하는 코드도 자주 본다.

link callback
-> application notify packet send()
-> 그 뒤부터 간헐 정지

문제는 그 콜백이 어떤 컨텍스트에서 도는지
프로젝트 코드에서는 잊기 쉽다는 점이다.

특히 아래 상황이 위험하다.

이 시점에는 내부 상태 전환이 아직 끝나지 않았을 수 있다.
그래서 socket API를 바로 물면
데드락 비슷한 정지나 긴 대기처럼 보이기 쉽다.

흔한 진입점 3: 수신 callback 안에서 블로킹 호출을 섞는다

RAW API receive callback이나 이벤트 callback에서
“응답을 하나 바로 보내고 끝내자”는 구조도 많이 나온다.

여기서 low-level send 자체는 가능한 경우가 있어도,
중간에 아래 호출이 섞이면 위험해진다.

즉 문제는 “콜백 안에서 보냈다” 자체보다
콜백 안에서 다시 동기 대기 경로를 열어 버린 것에 가깝다.

증상이 ERR_MEM처럼 보일 때도 있다

self-deadlock이 항상 완전 정지로만 보이는 건 아니다.

그래서 메모리 옵션만 늘리고 끝내면
근본 원인을 놓치기 쉽다.

특히 TCPIP_MBOX_SIZE를 키우면
문제가 늦게 드러질 뿐 사라지지 않는 경우가 있다.

로그를 이렇게 남기면 빨라진다

이 이슈는 패킷 캡처만으로는 잘 안 보인다.
나는 보통 아래 항목을 같이 본다.

  1. 문제가 난 함수가 어떤 태스크/스레드 문맥에서 호출됐는지
  2. tcpip_callback 진입/종료 시각
  3. 그 안에서 호출한 netconn_* 또는 send()/recv() 기록
  4. mailbox depth 또는 semaphore 대기 시간
  5. 링크/주소/DHCP 상태 callback 직후인지 여부

이 다섯 줄이 있으면
“네트워크가 멈췄다”보다
“콜백 안에서 자기 자신을 기다린다”는 그림이 빨리 나온다.

구현 쪽에서 무난한 패턴

안전하게 가려면 경계를 단순하게 두는 편이 낫다.

tcpip_thread callback
-> 상태만 갱신
-> 필요한 작업은 app worker queue에 예약

app worker/task
-> netconn/socket API 호출

즉 코어 컨텍스트에서는:

까지만 하고,
동기 대기 가능성이 있는 API는 바깥 task로 빼는 편이 안전하다.

프로젝트가 RAW API 중심이라면 더 단순하다.
callback 안에서는 같은 규칙의 비블로킹 처리만 하고,
다른 레이어 API와 섞지 않는 편이 덜 아프다.

빠른 체크리스트

  1. tcpip_callback, link/status callback, DHCP callback 안에서 netconn_* 또는 send()/recv()를 부르는지 확인
  2. 호출 함수가 실제로 어떤 스레드 문맥에서 도는지 로그로 확인
  3. 코어 스레드 안의 callback에서 semaphore, queue, event 대기가 있는지 확인
  4. ERR_MEM이나 timeout이 사실은 송신 완료 정지의 결과가 아닌지 확인
  5. 네트워크 알림 송신은 callback 안에서 직접 하지 말고 app worker로 넘기도록 분리했는지 확인

한 줄 요약

lwIP에서 콜백 직후 전체 네트워크가 멈춘다면 메모리보다 먼저 tcpip_thread 안에서 다시 netconn/sockets API를 기다리며 self-deadlock 비슷한 구조를 만들고 있지 않은지 확인하는 게 빠르다.

추천 키워드

lwIP, TCPIP, tcpip_thread, netconn, RTOS, embedded network


DevBJ | 오늘을살자, Log Today


Edit page
Share this post on:

Next Post
DoIP에서 Security Access 뒤 NRC 0x24가 뜬다: seed/key 이후 요청 순서와 세션 문맥을 같이 봐야 한다