리눅스 네트워킹 스택 — sk_buff에서 소켓까지

·16 min read
linuxnetworkingkerneltcp
Series:Linux/networking#2

리눅스 네트워킹 스택 — sk_buff에서 소켓까지

리눅스의 네트워킹 스택은 커널에서 가장 복잡한 서브시스템 중 하나다. 네트워크 인터페이스 카드(NIC)에 도착한 패킷이 애플리케이션의 recv() 호출까지 전달되는 과정을 단계별로 추적해 본다.

sk_buff: 패킷의 컨테이너

sk_buff(socket buffer)는 네트워킹 스택에서 패킷을 표현하는 핵심 구조체다. 패킷 데이터 자체뿐만 아니라 프로토콜 헤더 포인터, 라우팅 정보, 타임스탬프 등 다양한 메타데이터를 포함한다.

struct sk_buff {    struct sk_buff      *next, *prev;    struct sock         *sk;    struct net_device   *dev;    unsigned char       *head, *data, *tail, *end;    __u16               transport_header;    __u16               network_header;    __u16               mac_header;    // ... 수십 개의 필드};

head부터 end까지가 할당된 버퍼 공간이고, data부터 tail까지가 현재 유효한 패킷 데이터다. 프로토콜 계층을 올라가며 skb_pull()로 헤더를 벗기고, 내려가며 skb_push()로 헤더를 추가한다.

제로카피와 scatter-gather

성능 최적화를 위해 sk_buff는 실제 데이터를 연속된 메모리에 두지 않을 수 있다. skb_shared_info 구조체의 frags[] 배열을 통해 여러 페이지에 분산된 데이터를 참조할 수 있으며, 이것이 scatter-gather I/O의 기반이다.

sendfile() 시스템 콜이나 TCP zero-copy 전송은 이 메커니즘을 활용한다.

패킷 수신 경로

패킷이 NIC에 도착하면 다음과 같은 경로를 거친다.

NAPI와 인터럽트 처리

현대 NIC 드라이버는 NAPI(New API)를 사용한다. 첫 패킷 도착 시 하드웨어 인터럽트가 발생하고, 이후 인터럽트를 비활성화한 채 폴링 모드로 전환하여 배치 처리한다.

// NAPI 폴링 함수 (드라이버 구현)static int my_poll(struct napi_struct *napi, int budget){    int work_done = 0;    while (work_done < budget) {        struct sk_buff *skb = get_next_packet(priv);        if (!skb)            break;        skb->protocol = eth_type_trans(skb, netdev);        napi_gro_receive(napi, skb);        work_done++;    }    if (work_done < budget)        napi_complete_done(napi, work_done);    return work_done;}

budget은 한 번의 폴링에서 처리할 최대 패킷 수로, 기본값은 64다. 처리할 패킷이 남아있으면 소프트IRQ 컨텍스트에서 다시 폴링이 스케줄된다.

GRO와 패킷 병합

napi_gro_receive()는 Generic Receive Offload를 수행한다. 같은 플로우에 속하는 작은 패킷들을 하나의 큰 패킷으로 병합하여 상위 스택의 처리 오버헤드를 줄인다.

병합된 패킷은 netif_receive_skb()를 통해 프로토콜 핸들러로 전달된다.

넷필터와 라우팅

IP 계층에서 패킷은 넷필터 훅 포인트를 거친다. iptablesnftables 규칙이 이 훅에 등록되어 패킷 필터링, NAT, 마킹 등을 수행한다.

# nftables로 패킷 흐름 확인sudo nft add rule inet filter input \    meta nfproto ipv4 tcp dport 80 counter accept# 카운터로 매칭된 패킷 수 확인sudo nft list ruleset

주요 훅 포인트는 PREROUTINGINPUT(로컬 전달) 또는 FORWARD(포워딩) → POSTROUTING 순서로 실행된다.

라우팅 테이블 조회

ip_rcv()에서 넷필터 PREROUTING 훅을 거친 후, ip_route_input()으로 라우팅 결정이 이루어진다. FIB(Forwarding Information Base)를 조회하여 패킷이 로컬로 전달될지, 다른 인터페이스로 포워딩될지 결정한다.

TCP 계층 처리

로컬 전달 패킷은 tcp_v4_rcv()에서 소켓을 찾고, 시퀀스 번호 검증, 윈도우 관리, ACK 처리 등을 수행한다. 최종적으로 데이터는 소켓의 수신 버퍼(sk->sk_receive_queue)에 적재된다.

애플리케이션이 recv()를 호출하면 tcp_recvmsg()가 수신 큐에서 데이터를 유저스페이스 버퍼로 복사한다. 큐가 비어있으면 태스크는 슬립 상태에 들어간다.

TCP 튜닝 파라미터

네트워크 성능 튜닝에 자주 사용되는 커널 파라미터들이 있다.

# 소켓 버퍼 크기 (최소/기본/최대)sysctl net.ipv4.tcp_rmem="4096 131072 6291456"sysctl net.ipv4.tcp_wmem="4096 16384  4194304"# TCP 혼잡 제어 알고리즘sysctl net.ipv4.tcp_congestion_control=bbr# 커넥션 추적 테이블 크기sysctl net.netfilter.nf_conntrack_max=262144

고대역폭 환경에서는 버퍼 크기를 키우고 BBR 혼잡 제어를 사용하는 것이 일반적인 최적화 패턴이다.