Skip to content
오늘을살자
Go back

lwIP TCP 버퍼 설정 읽는 법: TCP_SND_BUF, TCP_WND, MEM_SIZE, MEMP_NUM_TCP_SEG, PBUF_POOL_SIZE

Edit page
lwIP TCP 송수신 버퍼, MEM heap, MEMP tcp_seg pool, RX pbuf pool의 관계를 표현한 구조도

lwIP 설정을 볼 때 TCP_SND_BUF, TCP_WND, MEM_SIZE, MEMP_NUM_TCP_SEG, PBUF_POOL_SIZE가 한꺼번에 나오면 전부 “버퍼 크기”처럼 보인다.

하지만 실제로는 서로 다른 층을 조절한다.

TCP_SND_BUF       -> 연결별 TCP 송신 byte budget
TCP_WND           -> 연결별 TCP 수신 window
MEM_SIZE          -> lwIP mem_malloc() heap
MEMP_NUM_TCP_SEG  -> 전체 TCP segment descriptor pool
PBUF_POOL_SIZE    -> RX packet buffer 개수

이 글은 특정 프로젝트의 실제 숫자를 공개하기 위한 글이 아니다.

공개 가능한 형태로 일반화해서, lwIP 버퍼 설정을 읽을 때 어떤 값을 어떤 관점으로 봐야 하는지 정리한다.

먼저 결론

단일 TCP 연결만 보면 TCP_SND_BUFTCP_WNDTCP_MSS의 몇 배인지가 첫 번째 감각이다.

예를 들어 둘 다 10 * TCP_MSS 수준이면 이렇게 볼 수 있다.

TCP_SND_BUF / TCP_MSS ~= 10
TCP_WND     / TCP_MSS ~= 10

즉 연결 하나가 송신 쪽으로 대략 10 MSS 정도를 큐에 둘 수 있고, 수신 쪽으로도 상대에게 10 MSS 정도의 window를 광고할 수 있다는 뜻이다.

하지만 전체 시스템 관점에서는 이것만으로 부족하다.

MEMP_NUM_TCP_SEG는 모든 TCP 연결이 공유하는 global pool이고, PBUF_POOL_SIZE는 RX packet을 담는 pool 개수이며, MEM_SIZE는 별도의 heap이다.

그래서 튜닝은 항상 아래처럼 나눠서 봐야 한다.

송신 byte 예산       -> TCP_SND_BUF
수신 window 예산     -> TCP_WND
송신 segment 개수    -> MEMP_NUM_TCP_SEG
복사/동적 할당 heap  -> MEM_SIZE
RX packet 개수       -> PBUF_POOL_SIZE

TCP_SND_BUF: 송신 데이터를 얼마나 큐에 둘 수 있나

TCP_SND_BUF는 TCP 연결 하나가 아직 ACK 받지 못한 송신 데이터를 얼마나 들고 있을 수 있는지 정하는 byte budget이다.

애플리케이션이 tcp_write()로 데이터를 넣는다고 해서 그 데이터가 즉시 wire로 나가는 것은 아니다.

상대방 window, congestion state, ACK 수신 여부, 재전송 상태에 따라 lwIP 안에서 잠시 대기할 수 있다.

app tcp_write()
      |
      v
TCP send queue
      |
      v
ACK received -> free queued data

보통 설명할 때는 TCP_MSS 기준 배수로 보는 것이 편하다.

TCP_SND_BUF = TCP_MSS * N

여기서 N이 10이라면 연결 하나가 대략 10 MSS 정도의 송신 데이터를 큐에 둘 수 있다는 감각을 잡을 수 있다.

값이 크면 throughput에는 유리하다.

대신 queued data가 늘어날 수 있고, copy 송신 경로에서는 heap이나 pbuf 사용량도 같이 압박받는다.

값이 작으면 RAM은 아낄 수 있지만, 빠르게 데이터를 밀어 넣는 코드에서 tcp_write()ERR_MEM을 더 자주 반환할 수 있다.

TCP_WND: 상대에게 얼마나 받을 수 있다고 말할 것인가

TCP_WND는 수신 window다.

상대방에게 “나는 지금 이만큼 더 받을 수 있다”고 광고하는 값이다.

이 값도 TCP_MSS 기준 배수로 보는 것이 좋다.

TCP_WND = TCP_MSS * N

N이 10이면 상대는 ACK를 기다리지 않고도 대략 10 MSS 정도를 연속으로 보낼 수 있다.

수신 throughput을 생각하면 큰 값이 좋아 보인다.

하지만 수신한 데이터는 결국 pbuf나 application receive buffer 어딘가에 머문다.

특히 RAW API에서는 애플리케이션이 데이터를 처리한 뒤 tcp_recved()를 호출해야 window가 다시 열린다.

remote sender
  sends up to TCP_WND
       |
       v
lwIP RX path holds pbufs
       |
       v
application consumes data
       |
       v
tcp_recved() opens window again

그래서 TCP_WND를 키울 때는 PBUF_POOL_SIZE, application 처리 주기, receive callback 구조를 같이 봐야 한다.

MEM_SIZE: lwIP 전체 메모리가 아니라 mem_malloc heap

MEM_SIZE는 lwIP 내부 mem_malloc() heap 크기다.

이 값은 중요하지만, lwIP 전체 RAM 사용량과 같은 뜻은 아니다.

기본적인 정적 pool 구성에서는 MEMP_NUM_* 객체와 PBUF_POOL이 별도 영역으로 잡힐 수 있다.

lwIP memory, simplified

MEM heap      -> MEM_SIZE
MEMP pools    -> MEMP_NUM_*
PBUF pool     -> PBUF_POOL_SIZE * PBUF_POOL_BUFSIZE
RTOS objects  -> mailbox, semaphore, task stack
driver memory -> DMA descriptor, RX/TX buffer

MEM_SIZE가 주로 압박받는 경우는 다음과 같다.

그래서 ERR_MEM이 났다고 해서 무조건 MEM_SIZE만 올리면 안 된다.

tcp_seg pool이 부족한지, pbuf pool이 부족한지, heap이 부족한지를 먼저 분리해야 한다.

MEMP_NUM_TCP_SEG: byte가 아니라 segment descriptor 개수

MEMP_NUM_TCP_SEG는 TCP segment descriptor 개수다.

이 값은 “송신 데이터 byte 수”가 아니라 struct tcp_seg 같은 메타데이터 pool의 개수라고 보는 편이 안전하다.

TCP 송신 큐에는 실제 데이터 버퍼뿐 아니라 그 데이터를 TCP segment로 관리하기 위한 descriptor가 필요하다.

queued TCP data
  |
  +-- pbuf / payload storage
  |
  +-- tcp_seg descriptor

중요한 점은 MEMP_NUM_TCP_SEG가 global pool이라는 것이다.

연결마다 따로 주어지는 값이 아니라, active TCP 연결들이 나눠 쓴다.

예를 들어 설명용으로 tcp_seg pool이 “수십 개” 수준이고, 연결 하나가 최대 10 MSS 정도를 큐에 둘 수 있다고 하자.

그러면 단일 연결에서는 충분해 보이더라도, 여러 연결이 동시에 송신하면 MEMP_NUM_TCP_SEG가 먼저 병목이 될 수 있다.

active sender #1  -> uses some tcp_seg
active sender #2  -> uses some tcp_seg
active sender #3  -> uses some tcp_seg
...
global tcp_seg pool is shared

이 상황에서는 TCP_SND_BUF byte budget이 남아 있어도 tcp_write()ERR_MEM을 반환할 수 있다.

그래서 송신 병목을 볼 때는 항상 세 가지를 같이 본다.

TCP_SND_BUF
TCP_SND_QUEUELEN
MEMP_NUM_TCP_SEG

PBUF_POOL_SIZE: RX packet buffer의 개수

PBUF_POOL_SIZE는 byte 크기가 아니라 pbuf pool element 개수다.

실제 RAM 사용량은 PBUF_POOL_BUFSIZE와 곱해서 봐야 한다.

PBUF_POOL memory ~= PBUF_POOL_SIZE * PBUF_POOL_BUFSIZE + overhead

임베디드 Ethernet에서 PBUF_POOL_BUFSIZE는 full-size frame을 담기 위해 1.5KB 근처가 되는 구성이 흔하다.

이 경우 pool 개수가 “수백 개” 수준이면 전체 RAM 사용량은 꽤 커질 수 있다.

hundreds of pbufs * around full-frame size
  -> hundreds of KiB class memory budget

물론 실제 값은 alignment, struct pbuf 크기, pool overhead, DMA buffer 분리 여부에 따라 달라진다.

그래서 공개 글에서는 특정 장비의 정확한 pool 숫자보다, 다음 관계를 기억하는 편이 더 안전하고 유용하다.

PBUF_POOL_SIZE is count
PBUF_POOL_BUFSIZE is size per buffer
actual RAM is count * size + overhead

단일 연결에서는 무난해도 다중 연결에서는 다르다

TCP_SND_BUFTCP_WND10 * TCP_MSS 정도로 잡은 설정은 단일 TCP 연결 기준으로는 이해하기 쉽다.

송신도 대략 10 MSS, 수신도 대략 10 MSS의 window 감각이 생긴다.

하지만 다중 연결에서는 그림이 달라진다.

per-connection:
  TCP_SND_BUF
  TCP_WND

global/shared:
  MEMP_NUM_TCP_SEG
  PBUF_POOL_SIZE
  MEM_SIZE
  TCPIP mailbox

즉 연결별 window는 충분해도, global pool이 작으면 전체 부하에서 먼저 막힐 수 있다.

반대로 pbuf pool이 넉넉해 보여도, copy 송신이 많으면 MEM_SIZE 쪽에서 먼저 막힐 수 있다.

증상별로 먼저 볼 값

튜닝할 때는 “어떤 값이 크냐”보다 “어떤 증상이 나오냐”를 먼저 봐야 한다.

증상먼저 볼 값
tcp_write()ERR_MEM 반환TCP_SND_BUF, TCP_SND_QUEUELEN, MEMP_NUM_TCP_SEG, MEM_SIZE
송신이 ACK 뒤에만 조금씩 진행TCP_SND_BUF, 상대 window, congestion/retransmission
수신이 몇 KB 뒤 멈춤TCP_WND, tcp_recved(), application consume 속도
RX packet drop 증가PBUF_POOL_SIZE, driver RX descriptor, tcpip_thread 처리 속도
pbuf allocation 실패PBUF_POOL_SIZE, pbuf free 누락, receive backlog
연결 생성 실패MEMP_NUM_TCP_PCB, accept backlog, socket/netconn pool

여기서 중요한 것은 같은 ERR_MEM이라도 원인이 하나가 아니라는 점이다.

heap 부족, tcp_seg descriptor 부족, pbuf 부족이 모두 비슷한 증상으로 보일 수 있다.

공개 가능한 설정 설명 방식

실제 제품이나 내부 장비 설정을 글로 쓸 때는 숫자를 그대로 공개하지 않는 편이 좋다.

대신 다음처럼 일반화하면 충분히 설명이 된다.

TCP_SND_BUF       = about 10 * TCP_MSS
TCP_WND           = about 10 * TCP_MSS
MEM_SIZE          = tens of KiB class
MEMP_NUM_TCP_SEG  = several tens of descriptors
PBUF_POOL_SIZE    = hundred-class pool count

이렇게 쓰면 구조와 튜닝 포인트는 전달되면서도, 특정 제품의 실제 RAM budget이나 동시 연결 목표를 그대로 노출하지 않는다.

기술 글에서는 정확한 제품 설정값보다 관계식과 병목 방향이 더 중요할 때가 많다.

실무 체크리스트

lwIP 버퍼 설정을 볼 때는 아래 순서로 확인한다.

  1. TCP_SND_BUF / TCP_MSS가 몇 MSS인지 본다.
  2. TCP_WND / TCP_MSS가 몇 MSS인지 본다.
  3. TCP_SND_QUEUELENMEMP_NUM_TCP_SEG가 송신 burst를 버틸 수 있는지 본다.
  4. PBUF_POOL_SIZE * PBUF_POOL_BUFSIZE로 RX pool RAM 예산을 대략 계산한다.
  5. MEM_SIZE가 전체 lwIP RAM이 아니라는 점을 확인한다.
  6. 동시 연결 수와 active sender 수를 분리해서 본다.
  7. 가능하면 LWIP_STATS, MEM_STATS, MEMP_STATS, PBUF_STATS로 peak usage를 측정한다.

숫자를 먼저 키우는 것보다 peak usage를 보는 편이 안전하다.

RAM이 작은 MCU에서는 특히 그렇다.

한 줄 요약

TCP_SND_BUFTCP_WND는 연결별 TCP byte budget이고, MEMP_NUM_TCP_SEG, PBUF_POOL_SIZE, MEM_SIZE는 서로 다른 공유 메모리 압박 지점이다. lwIP 튜닝은 이 값들을 하나의 “버퍼 크기”로 보지 않고, 송신 byte, 수신 window, segment descriptor, heap, RX pbuf pool로 나눠 봐야 한다.


Edit page
Share this post on:

Next Post
lwIP RAW UDP callback 뒤에 수신이 멈춘다: pbuf_free를 놓치면 PBUF_POOL이 먼저 마른다