#tech 수정: 2026-05-21

lwIP에서 ISR에서 바로 보내면 가끔 터진다: tcpip_thread로 넘기는 패턴 정리

이더넷 IRQ에서 패킷 받자마자 lwIP API를 호출하면, 어떤 보드에서는 잘 돌아가고 어떤 보드에서는 랜덤하게 죽는다. 원인은 대부분 컨텍스트/락 문제다. ISR에서 할 일과 tcpip_thread로 넘길 일을 분리하는 패턴을 짧게 정리한다.

lwIP를 RTOS 위에 올리고 나면 꼭 한 번은 이런 형태를 겪는다.

  • 낮은 부하에서는 멀쩡하다
  • 부하가 올라가면 간헐적으로 멈춘다
  • 재현이 어렵다 (가끔은 며칠 돌다가 죽는다)
  • 스택을 보면 netif/udp/tcp 쪽에서 터져 있다

그리고 로그를 보면 이런 코드가 슬쩍 보인다.

  • IRQ(ETH IRQ)에서 udp_send() / tcp_write()를 호출
  • 혹은 RX 인터럽트에서 netif->input() 체인을 끝까지 태움

결론부터 말하면,
ISR에서 lwIP 코어를 “정면으로” 건드리면 랜덤 이슈가 나기 쉽다.

왜 ISR 호출이 위험한가 (실무 관점)

lwIP는 크게 두 가지 모드로 쓰인다.

  • RAW API: 빠르지만, 호출 컨텍스트 규칙을 안 지키면 바로 깨짐
  • netconn/sockets API: 내부적으로 메시지 패싱이 들어가고, 규칙을 조금 더 지켜줌

RTOS + tcpip_thread 구성에서는
lwIP 내부 상태가 사실상 tcpip_thread 중심으로 돌아간다고 보면 된다.

이 상태에서 ISR이 중간에 치고 들어오면 문제는 보통 이 두 가지다.

  1. 락 없이 코어 구조체를 만짐 (race)
  2. ISR에서 블로킹/메모리 할당/큐 포화가 발생 (타이밍 꼬임)

그래서 “가끔만 터지는” 형태가 된다.

목표: ISR에서는 최소만, 나머지는 tcpip_thread로

ISR에서 할 일은 진짜 최소로 잡는 게 편하다.

  • DMA descriptor 정리
  • RX frame 길이/포인터 확보
  • 드라이버 큐에 넣고 빠르게 return

그리고 lwIP API 호출, pbuf 생성/체인, 프로토콜 처리는
tcpip_thread 컨텍스트로 넘겨서 처리한다.

가장 흔한 구조: ISR → queue → net task → tcpip_thread

구조는 대략 이렇다.

ETH IRQ
  → (frame pointer/len만) RX queue push
    → net task가 queue pop
      → tcpip_callback()로 lwIP 컨텍스트에 작업 전달

이 패턴의 장점은 단순하다.

  • ISR이 짧아짐 (시스템 전체 안정성↑)
  • lwIP 코어 접근이 한 방향으로 정리됨 (재현 어려운 버그↓)

코드 스케치: tcpip_callback으로 넘기기

아래는 “개념”만 잡는 용도다.
프로젝트마다 드라이버/메모리 모델이 다르니 그대로 붙여넣기용은 아니다.

struct rx_item {
  void *buf;
  uint16_t len;
};

static void lwip_rx_in_tcpip_thread(void *arg) {
  struct rx_item *item = (struct rx_item *)arg;

  // 여기서 pbuf를 만들고(혹은 REF로 감싸고) netif->input으로 넘긴다.
  // pbuf 생명주기(REF 사용 시 free 콜백)가 핵심 포인트.

  // struct pbuf *p = pbuf_alloc(PBUF_RAW, item->len, PBUF_POOL);
  // memcpy(p->payload, item->buf, item->len);
  // if (netif->input(p, &gnetif) != ERR_OK) pbuf_free(p);
}

void ethernet_rx_task(void *arg) {
  for (;;) {
    struct rx_item item;
    // queue_pop(&item);  // IRQ가 넣어둔 프레임
    tcpip_callback(lwip_rx_in_tcpip_thread, &item);
  }
}

여기서 포인트는 두 가지다.

  • tcpip_callback으로 들어간 함수는 tcpip_thread 컨텍스트에서 돈다
  • pbuf를 “어디서 만들고 어디서 free 할지”를 명확히 해야 한다

자주 깨지는 지점 4개

1) tcpip_callback에 넘긴 인자가 스택 변수다

위 스케치처럼 &item을 넘기면,
콜백이 실행되기 전에 item이 바뀌거나 사라질 수 있다.

실무에서는 보통 이렇게 한다.

  • 큐에서 꺼낸 프레임은 “프레임 버퍼 포인터”로 넘김 (버퍼는 풀에서 관리)
  • 혹은 별도의 고정 풀에서 rx_item을 할당해서 넘김

2) ISR에서 pbuf_alloc()를 한다

이건 생각보다 많이 본다.
보드/힙/락 구현에 따라 랜덤하게 죽기 딱 좋다.

가능하면 pbuf_alloc()은 tcpip_thread 쪽에서 하자.

3) LWIP_TCPIP_CORE_LOCKING을 켜놓고 규칙을 섞어쓴다

core locking을 켰다고 해서 “아무데서나 호출”이 되는 게 아니다.

  • 어떤 API는 코어 락이 필요
  • 어떤 API는 호출 컨텍스트 자체가 제한

프로젝트에서는 한 가지 모델로 정리하는 게 덜 아프다.

  • tcpip_callback / tcpip_try_callback 기반으로 코어 접근 단일화
  • 혹은 LOCK_TCPIP_CORE() 규칙을 코드 전반에 강제

4) TCPIP_MBOX_SIZE가 작아서 콜백이 드롭된다

콜백/메시지는 결국 mbox로 쌓인다.
부하가 올라가면 “가끔만” 콜백이 누락될 수 있다.

이때 증상은 보통 이렇다.

  • RX는 들어오는데 상위가 처리 못함
  • UDP는 간헐적으로 끊기는 것처럼 보임
  • 타이밍에 따라 멀쩡해 보이기도 함

이런 경우에는 mbox/큐의 수용량과
net task 우선순위를 같이 봐야 한다.

빠른 디버깅 체크리스트

  1. lwIP API가 ISR/드라이버 컨텍스트에서 호출되는지부터 grep
  2. LWIP_ASSERT / LWIP_STATS를 켜서 “어디가 막히는지” 숫자로 보기
  3. tcpip_thread 우선순위/스택, TCPIP_MBOX_SIZE를 같이 확인
  4. pbuf 누수/고갈이면 memp/pbuf 통계부터 본다
  5. “가끔만”이면 대부분 컨텍스트/락/큐 포화다 (확률 이슈)

결론

lwIP는 성능도 좋고 유연한데, RTOS 환경에서는 “호출 컨텍스트 규칙”을 어기면 랜덤 이슈가 나온다. ISR에서는 최소만 하고, tcpip_thread로 넘기는 구조로 정리하면 재현 어려운 네트워크 버그가 크게 줄어든다.


광고