sk_buff 자료구조

Linux 커널 네트워크 스택(Network Stack)의 핵심 자료구조 sk_buff: 메모리 레이아웃, 데이터 조작 함수, clone/copy, 소켓(struct sock) 연동, 소켓 옵션, raw socket, 체크섬(Checksum) 오프로드, GSO/GRO, zero-copy 전송까지 종합 분석합니다. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅(Debugging) 절차까지 실무 관점으로 다룹니다.

전제 조건: 네트워크 스택네트워크 디바이스 드라이버 문서를 먼저 읽으세요. 고성능 패킷(Packet) 경로는 큐 구조, 메모리 배치, 드롭 위치가 성능을 좌우하므로 드라이버 경계를 먼저 이해하는 것이 중요합니다.
일상 비유: 이 주제는 고속 톨게이트 차선 분리와 비슷합니다. 일반 차선(커널 스택)과 하이패스 차선(XDP/DPDK)을 구분해 보면 왜 지연(Latency)과 처리량(Throughput)이 달라지는지 명확해집니다.

핵심 요약

  • 메모리 레이아웃head, data, tail, end 4개 포인터로 버퍼(Buffer) 관리. skb_push/pull/put로 데이터 영역 조작.
  • 참조 모델clone은 메타데이터만 복사하고 버퍼 공유, copy는 완전 복사. 참조 카운트(Reference Count) 관리가 핵심.
  • 소켓 메모리sk_rmem_alloc/sk_wmem_alloctruesize 기반 소켓 버퍼 제한 구현.
  • 헤더 포인터mac_header, network_header, transport_header로 L2/L3/L4 헤더 오프셋(Offset) 추적.
  • 수명주기 — 할당 → 프로토콜 처리 → 소켓 전달 → 사용자 복사 → 해제. 각 단계에서 다른 함수와 상태 변화.
  • skb 확장skb_ext로 conntrack, IPsec secpath, bridge NF 등 가변 메타데이터를 skb에 동적 연결. 5.x+에서 메모리 효율 향상.
  • page_pool — 최신 고성능 드라이버(6.x+)는 page_pool로 DMA 매핑(Mapping) 캐시(Cache)와 페이지(Page) 재활용(Recycling)을 구현해 할당/해제 비용 최소화.
  • XDP 인터페이스xdp_buff는 skb 할당 이전 단계로 동작. XDP_PASS 시 build_skb()를 통해 sk_buff로 변환되어 일반 스택 진입.

단계별 이해

  1. 구조체(Struct) 이해
    4개 포인터(head, data, tail, end)와 3개 헤더 포인터(mac, network, transport)의 관계를 먼저 익힙시다. 이게 sk_buff의 핵심입니다.
  2. 데이터 조작 함수
    skb_push(헤더 추가), skb_pull(헤더 제거), skb_put(데이터 추가)의 동작을 코드로 직접 연습합니다.
  3. 할당 함수 선택
    alloc_skb(일반), netdev_alloc_skb(드라이버 수신), napi_alloc_skb(NAPI), page_pool_alloc_pages(6.x 고성능) 차이점을 파악합니다.
  4. 수명주기 추적
    수신 경로(NIC → 드라이버 → L2 → L3 → L4 → 소켓)와 전송 경로의 함수 호출 체인을 따라가며 이해합니다.
  5. 확장 시스템 학습
    skb_ext, page_pool, XDP ↔ sk_buff 변환 등 현대 커널의 최적화 메커니즘을 파악해 고성능 드라이버 코드를 읽는 눈을 키웁니다.
  6. 실전 디버깅 연습
    perf trace -e skb:kfree_skb로 드롭 원인을 추적하고, /proc/net/softnet_stat으로 CPU별 처리량을 분석하며, dropwatch로 병목(Bottleneck) 지점을 찾아봅니다.
관련 표준: IEEE 802.3 (Ethernet 프레임), RFC 791 (IPv4), RFC 8200 (IPv6) — sk_buff는 이 표준들이 정의하는 패킷 구조를 커널 내부에서 표현합니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

개요

struct sk_buff(흔히 skb라 부름)는 Linux 네트워크 스택에서 모든 패킷을 표현하는 자료구조입니다. 수신 패킷이든 전송 패킷이든, L2 프레임이든 L4 세그먼트든, 커널 내에서 네트워크 데이터가 이동할 때는 항상 sk_buff가 사용됩니다.

O'Reilly의 "Understanding Linux Network Internals"에서 Christian Benvenuti는 sk_buff를 "네트워크 스택의 왕(king of networking)"이라 표현했습니다. 이 자료구조를 이해하지 않고는 Linux 네트워크 코드를 읽을 수 없습니다.

struct sk_buff 주요 필드

/* include/linux/skbuff.h (주요 필드만 발췌) */
struct sk_buff {
    union {
        struct {
            struct sk_buff      *next;     /* 리스트 내 다음 skb */
            struct sk_buff      *prev;     /* 리스트 내 이전 skb */
        };
        struct rb_node rbnode;           /* TCP retransmit queue용 */
    };
    struct sock        *sk;             /* 소속 소켓 */
    struct net_device  *dev;            /* 수신/전송 네트워크 디바이스 */

    unsigned int        len;            /* 전체 데이터 길이 (linear + frags) */
    unsigned int        data_len;       /* 비선형(paged) 데이터 길이 */
    __u16               mac_len;        /* MAC 헤더 길이 */
    __u16               hdr_len;        /* 클론 시 writable 헤더 길이 */

    __be16              protocol;       /* 패킷 프로토콜 (ETH_P_IP 등) */
    __u32               priority;       /* QoS 우선순위 */

    sk_buff_data_t      transport_header; /* L4 헤더 오프셋 */
    sk_buff_data_t      network_header;   /* L3 헤더 오프셋 */
    sk_buff_data_t      mac_header;       /* L2 헤더 오프셋 */

    sk_buff_data_t      tail;            /* 데이터 끝 */
    sk_buff_data_t      end;             /* 할당된 버퍼 끝 */
    unsigned char      *head;           /* 할당된 버퍼 시작 */
    unsigned char      *data;           /* 실제 데이터 시작 */

    unsigned int        truesize;       /* 실제 메모리 사용량 */
    refcount_t          users;          /* 참조 카운트 */
};
코드 설명
  • next/prev, rbnodeinclude/linux/skbuff.h에서 정의된 union으로, 일반 큐(sk_buff_head)에서는 next/prev 이중 연결 리스트를, TCP 재전송 큐에서는 rb_node를 사용합니다. 메모리를 절약하면서 두 가지 큐 방식을 지원합니다.
  • sk, devsk는 이 패킷이 속한 소켓을 가리키며 메모리 과금(truesize 기반)에 사용됩니다. dev는 수신 시 입력 인터페이스, 전송 시 출력 인터페이스를 가리킵니다. 라우팅 결정 후 dev가 변경될 수 있습니다.
  • len, data_lenlen은 linear + paged 전체 데이터 길이이고, data_len은 비선형(frags[] + frag_list) 부분만의 길이입니다. linear 영역 크기는 skb_headlen(skb) = len - data_len으로 계산합니다.
  • transport/network/mac_headerL4/L3/L2 헤더의 head 기준 오프셋입니다. skb_reset_transport_header() 등으로 현재 data 위치를 기록하며, tcp_hdr(skb), ip_hdr(skb), eth_hdr(skb) 매크로가 이 오프셋을 사용해 헤더 포인터를 반환합니다.
  • head, data, tail, end4개 포인터가 버퍼 레이아웃의 핵심입니다. head~data가 headroom, data~tail이 실제 데이터, tail~end가 tailroom입니다. end 직후에 skb_shared_info가 배치됩니다.
  • truesize, userstruesize는 소켓 메모리 추적에 사용되는 실제 메모리 소비량(sk_buff 구조체 + 데이터 버퍼)입니다. users는 sk_buff 자체의 참조 카운트로, 0이 되면 __kfree_skb()가 호출됩니다.

자주 사용되는 추가 필드

위의 핵심 필드 외에도, sk_buff에는 프로토콜 처리와 성능 최적화에 쓰이는 중요한 필드들이 있습니다:

struct sk_buff {
    /* ... 위의 핵심 필드들 ... */

    char                cb[48];         /* 프로토콜별 제어 블록 (Control Buffer) */
    __u32               hash;           /* 패킷 해시 (RSS, flow steering) */
    __u8                pkt_type:3;     /* PACKET_HOST, PACKET_BROADCAST 등 */
    __u8                ip_summed:2;    /* 체크섬 오프로드 상태 */
    __u32               mark;           /* netfilter/tc 마킹 (iptables -j MARK) */
    __u16               queue_mapping;  /* 멀티큐 NIC 큐 인덱스 */
    unsigned int        napi_id;        /* NAPI 구조체 ID (busy polling) */
    union {
        __u32           tstamp;         /* 수신 타임스탬프 */
        u64             skb_mstamp_ns;  /* 고해상도 타임스탬프 */
    };
    __u8                cloned:1;       /* clone 여부 */
    __u8                nohdr:1;        /* 페이로드 참조만 (헤더 없음) */
    __u8                peeked:1;       /* MSG_PEEK으로 이미 확인됨 */
};
필드크기용도접근 방법
cb[48]48바이트프로토콜 레이어가 임시 데이터 저장 (TCP: tcp_skb_cb)TCP_SKB_CB(skb), IPCB(skb)
hash32비트수신 패킷의 flow hash (RSS, RPS에 활용)skb_get_hash(skb)
pkt_type3비트패킷 수신 유형: HOST, BROADCAST, MULTICAST, OTHERHOST직접 접근
ip_summed2비트체크섬 오프로드 모드 (NONE/UNNECESSARY/COMPLETE/PARTIAL)직접 접근
mark32비트netfilter, tc, 라우팅(Routing) 결정에 사용되는 패킷 마크직접 접근
queue_mapping16비트멀티큐 NIC에서 TX/RX 큐 선택skb_get_queue_mapping(skb)
napi_id32비트NAPI 인스턴스 식별 (SO_BUSY_POLL 연동)직접 접근
ℹ️

cb[48]은 각 프로토콜 레이어가 자신만의 구조체로 캐스팅하여 사용합니다. 예를 들어 TCP는 TCP_SKB_CB(skb)struct tcp_skb_cb에 시퀀스 번호, 타임스탬프 등을 저장합니다. 한 레이어가 cb를 사용하면 다음 레이어에서 덮어쓰므로, 레이어 간 데이터 전달에는 사용하지 마세요.

sk_buff 비트필드 및 플래그 완전 맵

sk_buff에는 수십 개의 1비트 플래그가 내장되어 있습니다. 이 플래그들은 네트워킹 서브시스템의 다양한 계층이 설정하며, 패킷의 현재 상태와 처리 방향을 결정합니다. 아래는 카테고리별로 분류한 완전한 목록입니다.

플래그 카테고리별 SVG 비트맵

sk_buff 비트필드 카테고리 맵 네트워킹 오프로드 터널/캡슐화 TC/QoS 보안 타임스탬프 기타 네트워킹 플래그 cloned 데이터 공유 중 peeked MSG_PEEK 사용 nohdr 헤더 클론 완료 fclone 빠른 클론 풀 pkt_type PACKET_* 유형 ooo_okay OOO 허용 l4_hash L4 해시 유효 sw_hash SW 계산 해시 오프로드 플래그 ip_summed 체크섬 상태(2b) csum_valid 체크섬 검증됨 csum_complete_sw SW 완전 체크섬 csum_not_inet non-IP 체크섬 remcsum_offload 원격 체크섬 오프로드 offload_fwd_mark 오프로드 포워딩 slow_gro GRO 슬로우패스 wifi_acked WiFi ACK 완료 터널/캡슐화 플래그 encapsulation 캡슐화 패킷 encap_hdr_csum 내부 헤더 체크섬 inner_protocol_type 내부 프로토콜 유형 dst_pending_confirm dst 확인 대기 ndisc_nodetype NDisc 노드 유형 ipvs_property IPVS 처리 중 no_fcs FCS 없음 TC/QoS tc_skip_classify 분류 건너뜀 tc_at_ingress 인그레스 TC redirected TC 리다이렉트 from_ingress 인그레스 출처 보안 decrypted 복호화 완료 nf_skip_egress Netfilter 이그레스 스킵 타임스탬프 mono_delivery_time 단조 전달 시각 tstamp_type 타임스탬프 유형 기타 플래그 scm_io_uring io_uring SCM csum_level 체크섬 레벨(2b) pfmemalloc 메모리 압박 할당 pp_recycle 페이지 풀 재사용 active_extensions 확장 활성 여부 * 위 비트들은 struct sk_buff 내 여러 바이트에 분산 배치됩니다. 실제 비트 위치는 커널 버전마다 다를 수 있습니다.
sk_buff 비트필드 카테고리 분류 맵

네트워킹 플래그 상세

플래그비트 수설정 주체의미 및 사용 시점
cloned1skb_clone() 데이터 버퍼를 다른 sk_buff와 공유 중임을 표시합니다. 설정 시 쓰기 전에 skb_copy()가 필요합니다.
peeked1소켓 수신 경로 MSG_PEEK 플래그로 소켓 버퍼를 들여다볼 때 설정됩니다. 실제 수신 시 참조 카운트 이중 감소를 방지합니다.
nohdr1skb_header_release() 클론된 skb의 헤더 영역이 더 이상 수정되지 않음을 표시합니다. pskb_expand_head() 생략을 허용합니다.
fclone2skb_fclone_busy() 빠른 클론(fast clone) 풀에서 할당된 skb임을 표시합니다. SKB_FCLONE_ORIG, SKB_FCLONE_CLONE, SKB_FCLONE_UNAVAILABLE 값을 가집니다.
pkt_type3드라이버, eth_type_trans() 패킷 수신 유형입니다. PACKET_HOST(자신), PACKET_BROADCAST, PACKET_MULTICAST, PACKET_OTHERHOST(도청) 등을 구분합니다.
ooo_okay1TCP 송신 경로 이 패킷이 순서 외(Out-Of-Order) 전송을 허용함을 표시합니다. MPTCP, SCTP 멀티스트리밍에서 활용됩니다.
l4_hash1skb_set_hash() L4 5-튜플 기반 해시가 hash 필드에 유효함을 표시합니다. RSS(Receive Side Scaling) 큐 선택에 사용됩니다.
sw_hash1소프트웨어 해시 계산 경로 하드웨어가 아닌 소프트웨어가 계산한 해시임을 표시합니다. 하드웨어 RSS와 구분하기 위해 사용합니다.

오프로드 플래그 상세

플래그비트 수설정 주체의미
ip_summed2드라이버, IP 스택 CHECKSUM_NONE: 체크섬 미검증. CHECKSUM_UNNECESSARY: HW 검증 완료. CHECKSUM_COMPLETE: HW가 전체 합산 제공. CHECKSUM_PARTIAL: 송신 시 HW에 위임.
csum_valid1IP/TCP 수신 경로 체크섬이 소프트웨어에 의해 검증되었음을 표시합니다. GRO(Generic Receive Offload) 병합 시 중요합니다.
csum_complete_sw1소프트웨어 체크섬 경로 하드웨어 없이 소프트웨어가 CHECKSUM_COMPLETE 수준의 체크섬을 계산하였음을 표시합니다.
csum_not_inet1SCTP, FCOE 등 인터넷 표준(RFC 1071) 체크섬이 아님을 표시합니다. SCTP CRC-32c, FCoE CRC32 등 별도 알고리즘 사용 시 설정합니다.
remcsum_offload1UDP 터널 오프로드 원격 체크섬 오프로드(Remote Checksum Offload) 동작 중임을 표시합니다. VXLAN, Geneve 등 UDP 터널에서 사용합니다.
offload_fwd_mark1하드웨어 스위치 드라이버 하드웨어가 이미 패킷을 포워딩하였음을 표시합니다. 소프트웨어 브리지가 재처리하지 않도록 합니다.
slow_gro1GRO 수신 경로 이 패킷이 GRO 병합에 적합하지 않아 슬로우패스로 처리됨을 표시합니다.
wifi_acked1mac80211 WiFi 레이어에서 ACK를 수신하였음을 표시합니다. TCP 소켓의 SO_WIFI_STATUS 소켓 옵션 응답에 사용됩니다.

터널·캡슐화 플래그 상세

플래그의미
encapsulation 이 skb가 터널 캡슐화 패킷임을 표시합니다. 내부 패킷의 헤더 오프셋(inner_*_header)이 유효합니다.
encap_hdr_csum 내부 헤더의 체크섬이 계산되어 있음을 표시합니다. 캡슐화 해제 시 재계산을 생략할 수 있습니다.
inner_protocol_type 내부 프로토콜 유형 식별자입니다. SKB_INNER_PROTOCOL_TYPE_IANA_L3 또는 SKB_INNER_PROTOCOL_TYPE_ETH_TYPE 값을 가집니다.
dst_pending_confirm 라우팅 캐시 항목(dst_entry)의 유효성 확인이 필요함을 표시합니다. ARP/NDP 갱신이 필요한 경로에서 설정됩니다.
ndisc_nodetype IPv6 이웃 발견(NDP) 노드 유형을 구분합니다. NDISC_NODETYPE_UNSPEC, NDISC_NODETYPE_HOST, NDISC_NODETYPE_NODEFAULT, NDISC_NODETYPE_DEFAULT.
ipvs_property IPVS(IP Virtual Server)가 처리 중인 패킷임을 표시합니다. Netfilter 훅에서 IPVS 내부 패킷을 구분하기 위해 사용합니다.
no_fcs 이더넷 FCS(Frame Check Sequence)가 포함되지 않음을 표시합니다. 일부 가상 드라이버나 내부 인터페이스에서 설정합니다.

TC/QoS, 보안, 타임스탬프 플래그 상세

카테고리플래그의미
TC/QoS tc_skip_classify TC 분류기를 건너뛰도록 요청합니다. 루프백이나 이미 분류된 패킷에 설정합니다.
tc_at_ingress 인그레스(수신) 방향의 TC 훅에서 처리 중임을 표시합니다. sch_handle_ingress()에서 설정합니다.
redirected TC BPF 프로그램이 패킷을 다른 인터페이스로 리다이렉트하였음을 표시합니다.
from_ingress 리다이렉트된 패킷이 인그레스 경로에서 왔음을 표시합니다. skb_redirect_from_ingress()에서 참조합니다.
보안 decrypted TLS 오프로드 또는 IPsec에 의해 복호화가 완료되었음을 표시합니다. 상위 레이어가 중복 복호화를 시도하지 않도록 합니다.
nf_skip_egress Netfilter 이그레스 훅을 건너뛰도록 요청합니다. 브리지-라우터 혼합 경로에서 중복 처리를 방지합니다.
타임스탬프 mono_delivery_time tstamp 필드가 CLOCK_REALTIME이 아닌 단조 시계(CLOCK_MONOTONIC) 기반 전달 예정 시각을 담고 있음을 표시합니다. EDT(Earliest Departure Time) 스케줄링에 사용합니다.
tstamp_type 타임스탬프 유형을 구분합니다 (수신 시각 vs 예약 전송 시각). 커널 6.x에서 추가되었습니다.
기타 pfmemalloc 메모리 압박 상황(PFMEMALLOC)에서 할당된 skb임을 표시합니다. 일반 소켓의 수신 큐 입큐를 거부하여 메모리 고갈을 방지합니다.
pp_recycle 페이지 풀(Page Pool)에서 할당된 데이터 페이지를 재활용할 수 있음을 표시합니다. XDP/AF_XDP 제로-카피 경로에서 사용합니다.
scm_io_uring io_uring의 SCM(Socket Control Message) 경로를 통해 전달됨을 표시합니다.
ip_summed 플래그 실제 사용 예시 (net/ipv4/tcp_input.c)
/* 수신 체크섬 상태에 따른 분기 처리 */
static inline int tcp_checksum_complete(struct sk_buff *skb)
{
    return !skb_csum_unnecessary(skb) &&
           __tcp_checksum_complete(skb);
}

/* ip_summed 값에 따른 체크섬 불필요 여부 판단 */
static inline bool skb_csum_unnecessary(const struct sk_buff *skb)
{
    /* CHECKSUM_UNNECESSARY: HW 검증 완료
     * CHECKSUM_COMPLETE: 합산값으로 검증 가능 (csum_valid 추가 확인)
     * CHECKSUM_PARTIAL: 송신 시 HW 위임, 수신에서는 미사용 */
    return ((skb->ip_summed == CHECKSUM_UNNECESSARY) ||
            (skb->ip_summed == CHECKSUM_COMPLETE && skb->csum_valid));
}

/* 드라이버에서 HW 체크섬 결과 설정 */
static void my_driver_rx_checksum(struct sk_buff *skb,
                                   struct hw_desc *desc)
{
    if (desc->csum_ok) {
        skb->ip_summed = CHECKSUM_UNNECESSARY;
    } else if (desc->csum_provided) {
        skb->ip_summed = CHECKSUM_COMPLETE;
        skb->csum = csum_unfold((__force __sum16)desc->csum_value);
    } else {
        skb->ip_summed = CHECKSUM_NONE;
    }
}

체크섬 오프로드와 ip_summed

ip_summed 필드는 NIC 하드웨어의 체크섬 오프로드 상태를 나타내는 핵심 플래그입니다:

의미 (RX)의미 (TX)
CHECKSUM_NONEHW 미지원, SW 검증 필요SW가 체크섬 계산 완료
CHECKSUM_UNNECESSARYHW 검증 완료, 유효함체크섬 불필요 (loopback 등)
CHECKSUM_COMPLETEHW가 전체 체크섬 제공사용 안 함
CHECKSUM_PARTIAL사용 안 함HW에 체크섬 계산 위임
/* 수신: 드라이버에서 체크섬 상태 설정 */
void my_driver_rx(struct sk_buff *skb, bool csum_ok)
{
    if (csum_ok) {
        skb->ip_summed = CHECKSUM_UNNECESSARY;  /* SW 검증 생략 */
    } else {
        skb->ip_summed = CHECKSUM_NONE;         /* SW 검증 필요 */
    }
}

/* 전송: CHECKSUM_PARTIAL 설정 예 */
skb->ip_summed = CHECKSUM_PARTIAL;
skb->csum_start = skb_transport_header(skb) - skb->head;
skb->csum_offset = offsetof(struct udphdr, check);
경험적 팁: 실제 네트워크 드라이버 개발 시, CHECKSUM_UNNECESSARY를 설정하면 TCP/UDP 프로토콜 스택에서 __skb_checksum_validate_needed()를 건너뛰어 CPU 사이클을 크게 절약합니다. 하지만 일부 buggy NIC에서는 가짜 양수(false positive)가 발생할 수 있어, 문제 발생 시 ethtool -K eth0 rx-checksumming off로 비활성화하고 테스트하세요.

skb 할당과 해제

sk_buff 할당 함수는 사용 상황에 따라 여러 변형이 있습니다:

함수컨텍스트특징
alloc_skb(size, gfp)일반 (프로세스(Process)/softirq)기본 할당 함수. kmalloc으로 linear 버퍼 할당
netdev_alloc_skb(dev, len)NAPI/irq 수신 경로NET_SKB_PAD headroom 자동 확보, per-CPU 캐시 활용
napi_alloc_skb(napi, len)NAPI poll 내부NAPI 전용 per-CPU 페이지 캐시(Page Cache), 최적 성능
build_skb(data, frag_size)사전 할당 버퍼이미 할당된 버퍼에 skb 메타데이터만 생성
__alloc_skb(size, gfp, flags)내부 APISKB_ALLOC_FCLONE, SKB_ALLOC_RX 등 플래그 지정
/* 일반적인 전송 경로 할당 */
struct sk_buff *skb = alloc_skb(MAX_HEADER + payload_len, GFP_KERNEL);
if (!skb)
    return -ENOMEM;
skb_reserve(skb, MAX_HEADER);  /* L2/L3/L4 헤더용 headroom */

/* NAPI 수신 경로 할당 (드라이버 내) */
struct sk_buff *skb = napi_alloc_skb(napi, 256);  /* 헤더만 linear */
/* 페이로드는 page fragment로 추가 */
skb_add_rx_frag(skb, 0, page, offset, size, truesize);

/* build_skb: XDP, 고성능 드라이버에서 사용 */
void *buf = page_address(page);
struct sk_buff *skb = build_skb(buf, PAGE_SIZE);
if (!skb) {
    put_page(page);
    return;
}
skb_reserve(skb, headroom);

해제 함수도 상황에 따라 구분됩니다:

함수용도tracepoint
kfree_skb(skb)패킷 드롭 (에러/필터링)skb:kfree_skb 발생 (원인 추적 가능)
consume_skb(skb)정상적 소비 완료skb:consume_skb 발생
dev_kfree_skb_any(skb)드라이버 (irq/process 모두)컨텍스트에 따라 지연 해제 가능
dev_consume_skb_any(skb)드라이버 정상 소비irq-safe한 consume_skb
kfree_skb_reason(skb, reason)드롭 원인 명시 (6.x+)드롭 원인을 enum으로 기록
⚠️

kfree_skb()consume_skb()의 차이는 기능적으로 동일하지만, tracepoint가 다릅니다. 정상 경로에서 kfree_skb()를 사용하면 드롭 모니터링 도구가 오탐을 보고합니다. 반드시 의미에 맞는 함수를 사용하세요.

__kfree_skb() 내부 해제 경로 소스 분석

sk_buff의 해제는 단순한 메모리 반환이 아닙니다. 참조 카운트 관리, 소멸자 콜백, 프래그먼트 정리, 클론 처리까지 여러 단계를 거칩니다. 커널 6.x에는 해제 이유를 추적하는 kfree_skb_reason() API도 추가되었습니다.

해제 경로 전체 흐름

kfree_skb() / kfree_skb_reason() refcount 1이면 __kfree_skb() 호출 __kfree_skb(skb) trace_kfree_skb() 트레이스포인트 skb_release_all(skb) head_state + data 순차 해제 skb_release_head_state(skb) ① dst_release(skb_dst(skb)) ② destructor(skb) 콜백 ③ nf_conntrack_put / secpath_put skb_release_data(skb) ① skb_zcopy_clear() — 제로카피 콜백 ② dataref 감소 → 0이면 진행 ③ frags[] 페이지 참조 해제 destructor 콜백 sock_wfree() — 송신 버퍼 반환 sock_rfree() — 수신 버퍼 반환 frag_list 처리 kfree_skb_list() 재귀 호출 각 frag skb 재귀 해제 frags[] 해제 skb_frag_unref() put_page() 각 페이지 skb_free_head(skb) kfree(skb->head) 또는 vfree() fclone 처리 fclone_ref 감소 → 0이면 부모 skb 해제 SKB_FCLONE_ORIG: kmem_cache_free() ↓ fclone? kmem_cache_free(skbuff_cache, skb) sk_buff 구조체 슬랩 캐시 반환 진입/제어 상태 해제 데이터 해제 메모리 반환 클론 처리
__kfree_skb() 내부 해제 경로 전체 흐름도

skb_release_head_state() 상세 분석

skb_release_head_state()sk_buff가 보유한 외부 참조를 모두 해제합니다. 실제 메모리 반환보다 앞서 수행되어야 하는 정리 작업들을 담당합니다.

skb_release_head_state() 소스 전체 분석 (net/core/skbuff.c)
static void skb_release_head_state(struct sk_buff *skb)
{
    /* ① 라우팅 캐시 참조 해제
     * skb_dst(skb)는 dst 포인터와 noref 비트를 분리해서 반환.
     * noref가 설정되지 않은 경우만 dst_release() 호출. */
    skb_dst_drop(skb);

#ifdef CONFIG_XFRM
    /* ② IPsec 보안 경로(secpath) 참조 해제 */
    if (skb_sec_path(skb))
        secpath_put(skb_sec_path(skb));
#endif

    /* ③ 소멸자 콜백 실행
     * sock_wfree: 소켓 송신 큐 버퍼 카운터 감소 + 대기 프로세스 깨움
     * sock_rfree: 소켓 수신 큐 버퍼 카운터 감소
     * tcp_wfree: TCP 송신 + TSQ(TCP Small Queues) 처리
     * skb_kfree_head: skb_orphan 이후 간단 해제
     */
    if (skb->destructor) {
        WARN_ON(in_hardirq());
        skb->destructor(skb);
    }

#if IS_ENABLED(CONFIG_NF_CONNTRACK)
    /* ④ Netfilter 연결 추적(conntrack) 참조 해제 */
    nf_conntrack_put(skb_nfct(skb));
#endif

    skb_ext_put(skb);   /* ⑤ skb 확장 블록(tc_skb_ext 등) 해제 */
}

skb_release_data() 상세 분석

skb_release_data()는 선형 데이터 버퍼와 비선형 영역(페이지 프래그먼트, frag_list)을 정리합니다. 데이터가 여러 sk_buff에 의해 공유되는 경우 dataref 참조 카운트를 확인한 후 실제 해제 여부를 결정합니다.

skb_release_data() 소스 전체 분석 (net/core/skbuff.c)
static void skb_release_data(struct sk_buff *skb,
                               enum skb_drop_reason reason,
                               bool zerocopy_clean)
{
    struct skb_shared_info *shinfo = skb_shinfo(skb);
    int i;

    /* ① 제로-카피 완료 콜백 처리
     * ubuf_info가 있으면 MSG_ZEROCOPY 전송 완료 통지 */
    if (skb_zcopy(skb)) {
        bool skip_unref = shinfo->flags & SKBFL_DONT_ORPHAN;
        skb_zcopy_clear(skb, zerocopy_clean);
        if (skip_unref)
            return;
    }

    /* ② 공유 데이터 참조 카운트 확인
     * dataref가 0이 되지 않으면 다른 sk_buff가 아직 사용 중 */
    if (!skb_data_unref(skb, shinfo))
        return;   /* 다른 클론이 있으면 중단 */

    /* ③ skb_shared_info의 hw_timestamps 등 정리 */
    skb_zcopy_clear(skb, true);

    /* ④ 페이지 프래그먼트(frags[]) 참조 해제
     * nr_frags개의 각 페이지에 put_page() 수행 */
    for (i = 0; i < shinfo->nr_frags; i++)
        skb_frag_unref(skb, i);

    /* ⑤ frag_list 연결 skb들 재귀 해제
     * 각 frag skb에 대해 kfree_skb() 재귀 호출 */
    if (skb_has_frag_list(skb)) {
        struct sk_buff *frag, *tmp;
        skb_walk_frags(skb, frag) {
            tmp = frag;
            frag = frag->next;
            kfree_skb_reason(tmp, reason);
        }
    }

    /* ⑥ 선형 데이터 버퍼(head) 해제
     * 할당 크기에 따라 kfree() 또는 vfree() 선택 */
    skb_free_head(skb);
}

static void skb_free_head(struct sk_buff *skb)
{
    unsigned char *head = skb->head;

    if (skb->head_frag) {
        /* 페이지 기반 head 버퍼: put_page() */
        if (skb_pp_recycle(skb, head))
            return;  /* 페이지 풀 재사용 */
        skb_free_frag(head);
    } else {
        kfree(head);  /* kmalloc 기반 head 버퍼 */
    }
}

fclone skb 해제 경로

fclone(fast clone)은 TCP 송신 경로에서 자주 발생하는 skb_clone()을 최적화하기 위해, 원본 sk_buff와 클론 슬롯을 하나의 슬랩 객체에 같이 할당하는 방식입니다. skb_fclone_busy(sk, skb)로 클론 중인지 확인하며, 둘 다 해제되어야 슬랩 객체가 반환됩니다.

fclone 해제 경로 분석 (net/core/skbuff.c)
static void __kfree_skb(struct sk_buff *skb)
{
    skb_release_all(skb);
    kfree_skbmem(skb);
}

static void kfree_skbmem(struct sk_buff *skb)
{
    struct sk_buff_fclones *fclones;

    switch (skb->fclone) {
    case SKB_FCLONE_UNAVAILABLE:
        /* 일반 skb: skbuff_cache 슬랩에 반환 */
        kmem_cache_free(skbuff_cache, skb);
        return;

    case SKB_FCLONE_ORIG:
        /* 원본 fclone skb: 클론 슬롯 확인 */
        fclones = container_of(skb, struct sk_buff_fclones, skb1);

        /* 클론이 아직 살아있으면 원본만 DEAD 표시 후 종료.
         * 클론이 나중에 해제될 때 슬랩 객체 반환. */
        if (skb_fclone_busy(skb->sk, skb))
            break;

        fclones->skb1.fclone = SKB_FCLONE_UNAVAILABLE;
        goto fastpath;

    case SKB_FCLONE_CLONE:
        /* 클론 skb: 원본이 DEAD이면 함께 슬랩 반환 */
        fclones = container_of(skb, struct sk_buff_fclones, skb2);

        /* 원본 refcount 확인: 원본이 아직 살아있으면 클론만 해제 */
        if (!refcount_dec_and_test(&fclones->fclone_ref))
            return;
        break;
    }

fastpath:
    /* fclone 쌍 전체를 skbuff_fclone_cache에 반환 */
    kmem_cache_free(skbuff_fclone_cache, fclones);
}

kfree_skb_reason() — 6.x 해제 이유 추적

리눅스 커널 5.17부터 도입된 kfree_skb_reason()은 패킷이 드롭될 때 그 이유를 enum skb_drop_reason으로 기록합니다. 이 정보는 trace_kfree_skb 트레이스포인트를 통해 perf, bpftrace, ftrace로 관측할 수 있으며, 네트워크 패킷 드롭 디버깅에 매우 유용합니다.

열거값드롭 원인발생 위치
SKB_NOT_DROPPED_YET0 아직 드롭되지 않음 (정상 경로)기본값
SKB_DROP_REASON_NOT_SPECIFIED1 이유 미지정 (구형 코드 경로)레거시 kfree_skb()
SKB_DROP_REASON_NO_SOCKET2 대상 소켓 없음UDP/TCP 수신 경로
SKB_DROP_REASON_PKT_TOO_SMALL3 패킷 크기 부족각 프로토콜 헤더 검증
SKB_DROP_REASON_TCP_CSUM4 TCP 체크섬 오류tcp_v4_rcv()
SKB_DROP_REASON_SOCKET_FILTER5 소켓 필터(BPF) 드롭sk_filter_trim_cap()
SKB_DROP_REASON_UDP_CSUM6 UDP 체크섬 오류udp_lib_checksum_complete()
SKB_DROP_REASON_NETFILTER_DROP7 Netfilter 훅 드롭NF_DROP 반환 시
SKB_DROP_REASON_OTHERHOST8 목적지 MAC 불일치 (PACKET_OTHERHOST)ip_rcv()
SKB_DROP_REASON_IP_CSUM9 IP 헤더 체크섬 오류ip_rcv()
SKB_DROP_REASON_IP_INHDR10 IP 헤더 길이 오류ip_rcv()
SKB_DROP_REASON_IP_RPFILTER11 역방향 경로 필터링(rp_filter)fib_validate_source()
SKB_DROP_REASON_UNICAST_IN_L2_MULTICAST12 L2 멀티캐스트 프레임에 유니캐스트 IP브리지 수신 경로
SKB_DROP_REASON_XFRM_POLICY13 IPsec 정책 위반xfrm_policy_check()
SKB_DROP_REASON_IP_NOPROTO14 지원하지 않는 IP 프로토콜 번호ip_local_deliver_finish()
SKB_DROP_REASON_SOCKET_RCVBUFF15 소켓 수신 버퍼 가득 참udp_queue_rcv_skb()
SKB_DROP_REASON_PROTO_MEM16 프로토콜 메모리 한도 초과sk_rmem_schedule()
SKB_DROP_REASON_TCP_AUTH_HDR17 TCP 인증 헤더 오류TCP-AO 처리 경로
SKB_DROP_REASON_MAXN 열거 범위 끝경계 검사용
kfree_skb_reason() 실제 사용 및 tracing 예시
/* 커널 코드: 드롭 원인 지정 해제 */
static int udp_rcv(struct sk_buff *skb)
{
    return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);
}

static int __udp4_lib_rcv(struct sk_buff *skb, ...)
{
    /* ... */
    if (udp4_csum_init(skb, uh, proto))
        goto csum_error;
    /* ... */

csum_error:
    kfree_skb_reason(skb, SKB_DROP_REASON_UDP_CSUM);
    return 0;
}

/* --- bpftrace로 드롭 이유 실시간 추적 --- */
/*
 * sudo bpftrace -e '
 *   tracepoint:skb:kfree_skb {
 *     @reason[args->reason] = count();
 *   }
 *   interval:s:5 { print(@reason); clear(@reason); }
 * '
 */

/* --- perf로 kfree_skb 이벤트 기록 --- */
/*
 * perf record -e skb:kfree_skb -ag -- sleep 10
 * perf script | awk '{print $NF}' | sort | uniq -c | sort -rn
 */

/* trace_kfree_skb 트레이스포인트 정의 (include/trace/events/skb.h) */
TRACE_EVENT(kfree_skb,
    TP_PROTO(struct sk_buff *skb, void *location,
             enum skb_drop_reason reason),
    TP_ARGS(skb, location, reason),
    TP_STRUCT__entry(
        __field(void *, skbaddr)
        __field(void *, location)
        __field(unsigned short, protocol)
        __field(enum skb_drop_reason, reason)
    ),
    TP_fast_assign(
        __entry->skbaddr  = skb;
        __entry->location = location;
        __entry->protocol = ntohs(skb->protocol);
        __entry->reason   = reason;
    ),
    TP_printk("skbaddr=%p protocol=%u location=%p reason: %s",
              __entry->skbaddr, __entry->protocol,
              __entry->location,
              __print_symbolic(__entry->reason,
                               DEFINE_DROP_REASON(...)
              ))
);

skb_release_all()의 호출 순서가 중요한 이유

skb_release_head_state()skb_release_data()보다 먼저 호출되어야 합니다. 소멸자 콜백(destructor)인 sock_wfree()tcp_wfree()는 소켓의 송신 버퍼 카운터를 갱신하고 대기 중인 프로세스를 깨울 수 있습니다. 이때 소켓이 여전히 유효한 상태여야 하므로, 데이터 메모리 해제보다 소켓 참조 정리가 선행되어야 합니다.

마찬가지로 nf_conntrack_put()은 연결 추적 항목의 참조를 감소시켜 타임아웃 타이머가 돌아가도록 합니다. 이 작업 역시 데이터 버퍼와는 독립적으로 먼저 수행해야 레이스 컨디션이 발생하지 않습니다.

메모리 레이아웃

sk_buff의 데이터 영역은 4개의 핵심 포인터로 관리됩니다:

sk_buff 메모리 레이아웃 headroom 데이터 영역 (len - data_len) tailroom head data tail end skb_ shared_ info (frags[])
head~end: 할당된 버퍼 전체, data~tail: 현재 유효 데이터, end 뒤에 skb_shared_info

아래 다이어그램은 두 부분으로 구성됩니다. 상단struct sk_buff의 포인터 필드가 할당된 버퍼의 어느 영역을 가리키는지 보여주고, 하단skb_reserve/skb_put/skb_push/skb_pull 호출 시 포인터가 어떻게 이동하는지 단계별로 나타냅니다:

struct sk_buff 포인터 필드 → 데이터 버퍼 매핑 struct sk_buff unsigned char *head unsigned char *data __u16 mac_header __u16 network_header __u16 transport_header sk_buff_data_t tail sk_buff_data_t end 기타 필드 *next, *prev, len, data_len truesize, mac_len, ... 할당된 버퍼 headroom Link layer header (L2) Network layer header (L3) Transport layer header (L4) payload data tailroom struct skb_shared_info *frag_list, frags[], gso_size data size shared info ※ truesize = sizeof(struct sk_buff) + 할당 버퍼 크기 ※ skb_headlen = len − data_len (linear 영역), skb_is_nonlinear = data_len > 0 불변식: head ≤ data ≤ tail ≤ end 데이터 포인터 조작 흐름 head data, tail end headroom (비어 있음) skb_s_i ① skb_reserve 변경: data, tail을 rsv만큼 → 이동하여 headroom 생성 (전송 시 헤더 추가 공간 확보) head data tail end headroom payload data tailroom skb_s_i ② skb_put 변경: tail을 len만큼 → 이동하여 payload 데이터 영역 확장 head data tail end (이전) hr hdr payload data tailroom skb_s_i ③ skb_push 변경: data를 hdr만큼 ← 이동하여 headroom에 헤더 prepend (headroom 축소) head data tail end (이전) headroom payload data tailroom skb_s_i ④ skb_pull 변경: data를 hdr만큼 → 이동하여 처리된 헤더를 strip (headroom 확장, 수신 경로)
상단: struct sk_buff 포인터 필드와 버퍼 영역의 매핑. 하단: ①reserve(headroom 확보) → ②put(tail→, payload 확장) → ③push(←data, 헤더 prepend) → ④pull(data→, 헤더 strip) 포인터 이동 흐름

이 레이아웃은 불변식 head ≤ data ≤ tail ≤ end를 항상 유지합니다. 이 조건이 깨지면 커널 패닉(Kernel Panic) 또는 메모리 손상으로 이어집니다. skb_push()/skb_pull()/skb_put() 계열 함수는 호출 전에 이 불변식을 검증하며, 위반 시 skb_over_panic / skb_under_panic을 트리거합니다.

메모리 관련 주요 매크로(Macro)

sk_buff 버퍼 크기를 계산할 때 자주 사용되는 매크로입니다:

매크로정의 (개념)용도
SKB_DATA_ALIGN(X) ALIGN(X, SMP_CACHE_BYTES) SMP 캐시 라인(Cache Line) 단위(보통 64B)로 올림 정렬. end 포인터 계산에 사용.
SKB_WITH_OVERHEAD(X) X - SKB_DATA_ALIGN(sizeof(struct skb_shared_info)) 할당 크기 X에서 skb_shared_info를 뺀 실제 사용 가능한 linear 데이터 크기.
SKB_TRUESIZE(X) SKB_DATA_ALIGN(X + sizeof(struct sk_buff)) + SKB_DATA_ALIGN(sizeof(struct skb_shared_info)) X바이트 데이터를 담는 sk_buff를 할당할 때 실제로 필요한 총 메모리 크기. skb->truesize 초기값으로 사용.
SKB_MAX_HEAD(X) SKB_WITH_OVERHEAD(PAGE_SIZE - X) 헤더용 headroom X를 예약한 뒤 한 페이지 내에서 사용할 수 있는 linear 데이터 최대 크기.
truesize 사용 예: alloc_skb(size, gfp)는 내부적으로 SKB_TRUESIZE(size)skb->truesize를 초기화합니다. 소켓의 수신 버퍼 제한(sk_rmem_alloc)은 이 값을 누적해 추적하므로, 페이지 프래그먼트를 직접 추가할 때는 skb_add_rx_frag()truesize 인자를 실제 할당 크기(예: PAGE_SIZE)로 정확히 넘겨야 합니다.

데이터 조작 함수

sk_buff의 데이터 영역은 head, data, tail, end 4개 포인터로 관리됩니다. 이 포인터들을 조작하는 함수들은 Linux 네트워크 코드에서 가장 빈번하게 사용되는 핵심 API입니다. 할당 직후의 빈 skb에서 프로토콜 헤더와 페이로드(Payload)를 채워가는 과정, 수신 패킷에서 헤더를 벗겨가는 과정 모두 이 함수들의 조합으로 이루어집니다.

skb_reserve — headroom 확보

/* include/linux/skbuff.h */
static inline void skb_reserve(struct sk_buff *skb, int len);

skb_reserve()datatail 포인터를 동시에 len만큼 뒤로 이동시켜 headroom(헤더 추가 공간)을 확보합니다. 반드시 할당 직후, 데이터를 넣기 전에만 호출해야 합니다.

/* 사용 예: 전송 경로에서 L2/L3/L4 헤더를 위한 headroom 확보 */
struct sk_buff *skb = alloc_skb(MAX_HEADER + payload_len, GFP_KERNEL);
if (!skb)
    return -ENOMEM;
skb_reserve(skb, MAX_HEADER);  /* data=tail=head+MAX_HEADER */
/* 이후 skb_put()으로 payload, skb_push()로 헤더 추가 */

/* 사용 예: 드라이버 수신 경로에서 NET_IP_ALIGN 정렬 */
struct sk_buff *skb = netdev_alloc_skb(dev, len + NET_IP_ALIGN);
skb_reserve(skb, NET_IP_ALIGN);  /* IP 헤더 4바이트 정렬 (x86 외) */
⚠️

주의: skb_reserve()는 이미 데이터가 들어 있는 skb에 호출하면 data 영역이 페이로드를 덮어쓰게 됩니다. skb->len == 0인 상태에서만 호출하세요. 할당 직후가 유일한 안전한 타이밍입니다.

skb_put 계열 — 데이터 끝에 추가

skb_put() 계열 함수는 tail 포인터를 뒤로 이동하여 데이터 영역을 확장합니다. 주로 전송 경로에서 페이로드를 추가하거나, 수신 드라이버에서 수신된 데이터 크기를 설정할 때 사용됩니다.

함수프로토타입설명
skb_putvoid *skb_put(skb, len)tail += len, 이전 tail 반환. 경계 검사 수행 — tailroom 부족 시 skb_over_panic
__skb_putvoid *__skb_put(skb, len)경계 검사 없는 고속 버전. tailroom이 충분함을 확신할 때만 사용 (드라이버 fast path)
skb_put_datavoid *skb_put_data(skb, data, len)skb_put() + memcpy(). 한 번에 데이터 복사까지 수행 (4.13+)
skb_put_zerovoid *skb_put_zero(skb, len)skb_put() + memset(0). 패딩이나 초기화된 헤더 추가 시 (4.13+)
skb_put_u8u8 *skb_put_u8(skb, val)1바이트 추가. 프로토콜 필드를 하나씩 조립할 때 (4.18+)
/* 기본 사용: 페이로드 추가 */
unsigned char *p = skb_put(skb, payload_len);
memcpy(p, payload_data, payload_len);

/* skb_put_data: 위 두 줄을 한 줄로 (4.13+, 권장) */
skb_put_data(skb, payload_data, payload_len);

/* skb_put_zero: 패딩 영역을 0으로 채우기 */
skb_put_zero(skb, 4);  /* 4바이트 zero 패딩 추가 */

/* __skb_put: 드라이버에서 DMA 수신 버퍼 크기 설정 시 */
__skb_put(skb, rx_size);  /* 이미 할당 시 충분한 공간 확보됨 */

/* skb_put_u8: 1바이트 필드 추가 */
skb_put_u8(skb, MY_PROTO_VERSION);  /* version 필드 */
skb_put_u8(skb, MY_MSG_TYPE);       /* type 필드 */

/* 구조체 단위로 헤더 추가하는 패턴 */
struct my_hdr *hdr = skb_put_zero(skb, sizeof(*hdr));
hdr->version = 1;
hdr->payload_len = htons(data_len);
코드 설명
  • skb_put()include/linux/skbuff.h에서 tail 포인터를 len만큼 뒤로 이동하고 이전 tail 위치를 반환합니다. 반환 포인터에 memcpy()로 데이터를 채우는 것이 기본 패턴입니다. 내부에서 SKB_LINEAR_ASSERT(skb)와 tailroom 검사를 수행합니다.
  • skb_put_data()4.13+에서 추가된 편의 함수로, skb_put() + memcpy()를 한 번에 수행합니다. 호출 체인: skb_put_data()skb_put()skb_tail_pointer() 갱신 → memcpy(). 새 코드에서는 이 함수를 사용하는 것이 권장됩니다.
  • skb_put_zero()skb_put() + memset(0) 조합입니다. Ethernet 최소 프레임 크기 충족을 위한 패딩이나, 보안상 초기화가 필요한 헤더 필드에 사용됩니다. 구조체 포인터를 반환하므로 이후 필드별 설정이 가능합니다.
  • __skb_put()경계 검사를 생략하는 고속 버전입니다. 드라이버에서 alloc_skb() 직후처럼 할당 크기가 보장된 상황에서만 사용합니다. 잘못 사용하면 skb_shared_info 영역을 덮어써 커널 크래시를 유발합니다.
  • 구조체 헤더 패턴skb_put_zero()의 반환값을 구조체 포인터로 캐스팅해 필드를 개별 설정하는 관용 패턴입니다. skb_put_zero()가 먼저 0으로 초기화하므로, 설정하지 않는 필드는 자동으로 0이 됩니다.
⚠️

주의: skb_put()skb_tailroom(skb) < len이면 skb_over_panic()을 호출하여 커널 패닉(Kernel Panic)을 발생시킵니다. 반드시 skb_tailroom(skb)으로 남은 공간을 확인하거나, 할당 시 충분한 크기를 지정하세요. __skb_put()은 검사를 하지 않으므로 더 위험합니다.

skb_push 계열 — 헤더 추가 (앞쪽 확장)

skb_push() 계열 함수는 data 포인터를 앞으로 이동하여 headroom에서 공간을 가져옵니다. 전송 경로에서 프로토콜 헤더를 추가할 때 사용됩니다 (L4 → L3 → L2 순서).

함수프로토타입설명
skb_pushvoid *skb_push(skb, len)data -= len, 새 data 포인터 반환. 경계 검사 수행 — headroom 부족 시 skb_under_panic
__skb_pushvoid *__skb_push(skb, len)경계 검사 없는 고속 버전. headroom이 보장될 때만 사용
/* 전형적인 전송 경로: L4 → L3 → L2 순서로 헤더 추가 */

/* 1. UDP 헤더 추가 */
struct udphdr *uh = skb_push(skb, sizeof(*uh));
skb_reset_transport_header(skb);
uh->source = sport;
uh->dest = dport;
uh->len = htons(skb->len);
uh->check = 0;

/* 2. IP 헤더 추가 */
struct iphdr *iph = skb_push(skb, sizeof(*iph));
skb_reset_network_header(skb);
iph->version = 4;
iph->ihl = 5;
iph->tot_len = htons(skb->len);
/* ... 나머지 IP 필드 설정 ... */

/* 3. Ethernet 헤더 추가 */
struct ethhdr *eth = skb_push(skb, ETH_HLEN);
skb_reset_mac_header(skb);
ether_addr_copy(eth->h_dest, dst_mac);
ether_addr_copy(eth->h_source, src_mac);
eth->h_proto = htons(ETH_P_IP);

/* __skb_push: 터널 드라이버에서 outer 헤더 추가 시 */
/* headroom은 skb_cow_head()로 이미 확보된 상태 */
struct vxlanhdr *vxh = (struct vxlanhdr *)__skb_push(skb, sizeof(*vxh));
코드 설명
  • UDP 헤더 (L4)skb_push(skb, sizeof(*uh))data 포인터를 UDP 헤더 크기만큼 앞으로 이동합니다. skb_reset_transport_header(skb)는 현재 data 위치를 transport_header에 기록하여 이후 udp_hdr(skb)가 올바른 위치를 반환하게 합니다.
  • IP 헤더 (L3)L4 위에 skb_push()로 IP 헤더를 쌓습니다. skb_reset_network_header(skb)network_header를 설정하면 ip_hdr(skb)가 동작합니다. iph->tot_lenskb->len을 넣으면 UDP 헤더 + 페이로드를 포함한 전체 IP 패킷 길이가 됩니다.
  • Ethernet 헤더 (L2)마지막으로 ETH_HLEN(14바이트) 크기의 Ethernet 헤더를 push합니다. skb_reset_mac_header(skb)mac_header를 설정합니다. 이 시점에서 data는 L2 프레임 시작을 가리키고, skb->len은 전체 프레임 크기입니다.
  • __skb_push (VXLAN)터널 드라이버에서 outer 헤더를 추가할 때 사용됩니다. skb_cow_head()로 headroom과 unclone을 먼저 확보했으므로 경계 검사 없는 __skb_push()가 안전합니다. VXLAN 드라이버(drivers/net/vxlan/vxlan_core.c)의 실제 패턴입니다.
⚠️

주의: skb_push()는 headroom이 부족하면 skb_under_panic()으로 커널 패닉을 발생시킵니다. 터널 캡슐화처럼 추가 헤더가 필요한 경우, 반드시 skb_cow_head()pskb_expand_head()로 headroom을 먼저 확보하세요.

skb_pull 계열 — 헤더 제거 (앞쪽 축소)

skb_pull() 계열 함수는 data 포인터를 뒤로 이동하여 이미 처리된 헤더를 건너뜁니다. 수신 경로에서 프로토콜 레이어를 올라갈 때 사용됩니다 (L2 → L3 → L4 순서).

함수프로토타입설명
skb_pullvoid *skb_pull(skb, len)data += len. len > skb->len이면 NULL 반환 (BUG_ON 대신 안전 실패)
__skb_pullvoid *__skb_pull(skb, len)경계 검사 없는 고속 버전. len이 유효함을 보장할 때만 사용
pskb_pullvoid *pskb_pull(skb, len)비선형(paged) 데이터까지 처리. linear 영역이 부족하면 frags에서 linear로 당겨옴
skb_pull_datavoid *skb_pull_data(skb, len)pskb_may_pull + skb_pull 결합. 실패 시 NULL 반환 (5.14+)
/* 수신 경로: eth_type_trans()가 L2 헤더를 처리 */
skb->protocol = eth_type_trans(skb, dev);
/* 내부적으로 skb_pull(skb, ETH_HLEN) 수행 → data가 L3 시작을 가리킴 */

/* IP 프로토콜 핸들러에서: IP 헤더 건너뛰기 */
skb_pull(skb, ip_hdrlen(skb));
skb_reset_transport_header(skb);
/* 이제 data가 L4 (TCP/UDP) 헤더를 가리킴 */

/* pskb_pull: 비선형 skb에서 안전하게 헤더 제거 */
/* GRO로 병합된 패킷은 페이로드가 frags에 있을 수 있음 */
if (!pskb_pull(skb, header_len))
    goto drop;

/* skb_pull_data: 검증 + 당기기를 한 번에 (5.14+, 권장) */
struct my_hdr *hdr;
if (!skb_pull_data(skb, sizeof(*hdr)))
    goto drop;  /* linear + paged 모두 검사 후 pull */
ℹ️

skb_pull()pskb_pull()의 핵심 차이: skb_pull()은 linear 데이터만 건너뛰며 len > skb->len이면 NULL을 반환합니다. pskb_pull()은 필요 시 paged fragments에서 데이터를 linear 영역으로 복사해 옵니다. GRO/GSO로 인한 비선형 패킷이 일반적인 현대 커널에서는 pskb_pull()이나 skb_pull_data() 사용을 권장합니다.

길이/공간 조회 함수

skb의 현재 데이터 크기와 남은 공간을 조회하는 인라인 함수들입니다. 데이터 추가 전 공간 확인, 비선형 여부 판단 등에 필수적으로 사용됩니다.

함수반환값설명
skb_headroom(skb)skb->data - skb->headdata 앞의 빈 공간 (헤더 추가 가능 공간). skb_push() 전에 확인
skb_tailroom(skb)skb->end - skb->tailtail 뒤의 빈 공간 (데이터 추가 가능 공간). skb_put() 전에 확인
skb_headlen(skb)skb->len - skb->data_lenlinear 영역의 데이터 크기 (head~tail 사이 실제 데이터)
skb_is_nonlinear(skb)skb->data_len != 0paged fragments가 있는지 여부. true면 linear 데이터 외에 frags 존재
skb_pagelen(skb)headlen + frags 합frag_list 제외한 데이터 크기 (linear + frags[]만)
skb->len전체 데이터 길이linear + data_len (모든 paged data + frag_list 포함)
skb->data_len비선형 데이터 크기frags[] + frag_list에 있는 데이터 합계
skb_availroom(skb)즉시 사용 가능한 tailroomcloned skb는 tailroom이 있어도 0 반환 (쓸 수 없으므로)
/* 공간 확인 후 안전하게 데이터 추가 */
if (skb_tailroom(skb) < extra_len) {
    /* tailroom 부족 → 확장 필요 */
    if (pskb_expand_head(skb, 0, extra_len, GFP_ATOMIC))
        goto drop;
}
skb_put_data(skb, extra_data, extra_len);

/* 비선형 여부에 따른 분기 처리 */
if (skb_is_nonlinear(skb)) {
    /* paged fragments가 있음 → skb_copy_bits() 등으로 접근 */
    skb_copy_bits(skb, offset, buf, len);
} else {
    /* 모든 데이터가 linear → 직접 포인터 접근 가능 */
    memcpy(buf, skb->data + offset, len);
}

/* headroom 확인: 터널 캡슐화 전 */
int needed_headroom = sizeof(struct iphdr) + sizeof(struct udphdr) + VXLAN_HLEN;
if (skb_headroom(skb) < needed_headroom) {
    /* skb_cow_head()가 내부적으로 이 검사 + expand를 수행 */
    if (skb_cow_head(skb, needed_headroom))
        goto tx_error;
}

skb_trim 계열 — 끝에서 자르기

skb_trim() 계열 함수는 skb의 전체 길이를 줄입니다. 패킷 끝의 불필요한 데이터 제거, 패딩 제거, IP total length에 맞게 자르기 등에 사용됩니다.

함수프로토타입설명
skb_trimvoid skb_trim(skb, len)전체 길이를 len으로 줄임. linear 전용 — 비선형 skb에서는 BUG 발생
__skb_trimvoid __skb_trim(skb, len)검사 없는 내부 버전. len < skb->len 보장 시 사용
pskb_trimint pskb_trim(skb, len)비선형(paged) skb도 안전하게 trim. 실패 시 -ENOMEM 반환
pskb_trim_rcsumint pskb_trim_rcsum(skb, len)trim + 체크섬 무효화. 수신 경로에서 IP 패킷 길이에 맞게 자를 때 (체크섬 재검증 필요)
skb_trim_rcsum_slowint skb_trim_rcsum_slow(skb, len)pskb_trim_rcsum의 slow path. 체크섬 갱신이 필요한 경우
/* IP 수신 경로: 패킷을 IP total length에 맞게 trim */
unsigned int pkt_len = ntohs(iph->tot_len);
if (pkt_len < skb->len) {
    /* Ethernet padding 등으로 실제보다 길 수 있음 → 잘라내기 */
    if (pskb_trim_rcsum(skb, pkt_len))
        goto drop;
}

/* linear skb에서 단순 trim */
if (!skb_is_nonlinear(skb))
    skb_trim(skb, new_len);  /* tail 포인터 조정 */
else
    pskb_trim(skb, new_len); /* fragments도 처리 */

/* 커스텀 프로토콜에서 트레일러 제거 */
int trailer_len = my_trailer_size(skb);
pskb_trim(skb, skb->len - trailer_len);
⚠️

주의: skb_trim()은 비선형 skb에서 호출하면 BUG를 발생시킵니다. 안전하게 사용하려면 pskb_trim()을 사용하세요. 또한 CHECKSUM_COMPLETE 상태의 skb를 trim할 때는 반드시 pskb_trim_rcsum()을 사용해야 체크섬이 올바르게 갱신됩니다.

skb_pad / skb_padto 계열 — 최소 길이 패딩

Ethernet은 최소 프레임 크기 60바이트(FCS 제외)를 요구합니다. 짧은 패킷을 전송할 때 최소 길이를 맞추기 위해 skb_pad()를 사용합니다.

함수프로토타입설명
skb_padint skb_pad(skb, pad)skb 끝에 pad바이트 0 추가. 실패 시 skb가 해제됨(freed) — 반환값 확인 필수
skb_padtoint skb_padto(skb, len)전체 길이가 len이 되도록 패딩. 실패 시 skb 해제됨
__skb_padint __skb_pad(skb, pad, free_on_error)내부 함수. free_on_error로 에러 시 skb 해제 여부 제어
__skb_put_padtoint __skb_put_padto(skb, len, free_on_error)패딩 + skb->len 갱신까지 수행. eth_skb_pad()가 내부적으로 호출
eth_skb_padint eth_skb_pad(skb)ETH_ZLEN(60바이트)으로 패딩. 드라이버 xmit에서 가장 흔히 사용
/* 네트워크 드라이버 xmit: Ethernet 최소 프레임 크기 보장 */
static netdev_tx_t my_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
    /* ETH_ZLEN = 60. 짧은 패킷을 60바이트까지 0으로 패딩 */
    if (eth_skb_pad(skb))
        return NETDEV_TX_OK;  /* 실패 시 skb는 이미 해제됨 */

    /* ... DMA 매핑 및 전송 ... */
}

/* 특정 길이로 패딩 (예: 46바이트 최소 페이로드) */
if (skb->len < min_len) {
    if (skb_padto(skb, min_len))
        return -ENOMEM;  /* skb는 이미 해제됨 — 이중 해제 금지! */
    skb->len = min_len;  /* __skb_put_padto 사용 시 자동 갱신 */
}
⚠️

주의: skb_pad(), skb_padto()는 실패 시 skb를 자동으로 해제합니다. 에러 경로에서 kfree_skb(skb)를 다시 호출하면 이중 해제(double-free) 버그가 됩니다. 반환값이 에러이면 skb가 이미 사라진 것으로 간주하세요.

TX 경로: 패킷 조립 과정 (프로토콜 레이어별) ETH IP TCP payload headroom / tailroom ① alloc_skb() head / data / tail end (전체 비어있음) skb_reserve(headroom) ② reserve head data = tail end headroom (비어있음) skb_put(payload_len) ③ put head data tail end headroom payload tailroom skb_push(sizeof(tcphdr)) ④ push TCP head data tail end hr TCP payload tailroom skb_push(sizeof(iphdr)) ⑤ push IP head data tail end hr IP TCP payload tailroom skb_push(ETH_HLEN) ⑥ push ETH head / data tail end ETH IP TCP payload tailroom 완성: headroom 모두 소진 → ETH│IP│TCP│payload 순서로 프레임 완성 → NIC 드라이버로 전달 ETH=14B │ IP=20B │ TCP=20B (옵션 미포함) ── 점선: 이전 포인터 위치
TX 경로에서 alloc → reserve → put → push×3 순서로 패킷이 조립되는 과정. 각 push()마다 data 포인터가 head 방향으로 이동하며 프로토콜 헤더가 앞에 추가됨

전형적인 전송/수신 경로 조작 순서

/* ══════════════════════════════════════════════
 * 전송(TX) 경로: reserve → put → push × N
 * ══════════════════════════════════════════════ */
struct sk_buff *skb = alloc_skb(1500 + headroom, GFP_KERNEL);

/* 1. headroom 확보 */
skb_reserve(skb, headroom);   /* data=tail → head+headroom */

/* 2. 페이로드 추가 */
skb_put_data(skb, payload_data, payload_len);  /* tail ↓ */

/* 3. TCP 헤더 추가 */
struct tcphdr *th = skb_push(skb, sizeof(*th));  /* data ↑ */
skb_reset_transport_header(skb);

/* 4. IP 헤더 추가 */
struct iphdr *iph = skb_push(skb, sizeof(*iph));  /* data ↑↑ */
skb_reset_network_header(skb);

/* 5. Ethernet 헤더 추가 */
struct ethhdr *eh = skb_push(skb, ETH_HLEN);  /* data ↑↑↑ */
skb_reset_mac_header(skb);

/* ══════════════════════════════════════════════
 * 수신(RX) 경로: pull × N (위와 반대 순서)
 * ══════════════════════════════════════════════ */
/* 드라이버가 skb를 할당하고 DMA로 데이터 수신 */
skb_put(skb, rx_bytes);  /* 수신된 바이트만큼 len 설정 */

/* L2: eth_type_trans()가 Ethernet 헤더를 파싱하고 pull */
skb->protocol = eth_type_trans(skb, dev);  /* 내부적으로 skb_pull(ETH_HLEN) */

/* L3: IP 수신 핸들러 */
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
    goto drop;
struct iphdr *iph = ip_hdr(skb);
skb_pull(skb, iph->ihl * 4);  /* IP 헤더 건너뛰기 */
skb_reset_transport_header(skb);

/* L4: TCP/UDP 핸들러에서 transport 헤더 접근 */
struct tcphdr *th = tcp_hdr(skb);
/* data는 여전히 TCP 헤더를 가리킴 (TCP가 직접 pull하지 않고 offset 사용) */
RX 경로: 헤더 제거 및 trim 과정 (프로토콜 레이어별) ETH IP TCP payload padding (제거 대상) ① skb_put head / data tail end ETH IP TCP payload pad tailroom eth_type_trans() → 내부 pull(ETH_HLEN) ② pull ETH head data tail end ETH IP TCP payload pad tailroom skb_pull(iph→ihl * 4) ③ pull IP head data tail end headroom TCP payload pad tailroom tcp_hdr(skb) — offset으로 접근 ④ TCP 접근 head data tail end headroom TCP payload pad tailroom TCP는 pull하지 않고 transport_header offset으로 접근 pskb_trim_rcsum(pkt_len) ⑤ trim head data tail end headroom TCP payload 제거됨 tailroom trim 후 TCP│payload만 남음 → 소켓 버퍼로 전달 ── 점선: 이전 포인터 위치
RX 경로에서 수신된 프레임의 헤더가 단계별로 headroom으로 편입되고, padding이 trim으로 제거되는 과정. TCP는 pull 대신 transport_header offset으로 접근

Clone/Copy 메커니즘

여러 곳에서 같은 패킷을 참조해야 할 때 (예: 패킷 미러링, tcpdump 캡처, netfilter bridge), 세 가지 복사 전략을 선택할 수 있습니다:

skb_clone vs pskb_copy vs skb_copy skb_clone sk_buff (원본) sk_buff (clone) 공유 데이터 버퍼 dataref = 2 pskb_copy sk_buff (원본) sk_buff (copy) linear (원본) linear (복사) 공유 paged frags (refcount++) skb_copy sk_buff (원본) sk_buff (copy) 전체 버퍼 (원본) 전체 버퍼 (복사) 비교 요약 skb_clone 메타만 복사 버퍼 100% 공유 가장 빠름 데이터 수정 불가 pskb_copy linear 헤더 복사 paged data 공유 중간 비용 헤더 수정 가능 skb_copy 전체 완전 복사 독립적 버퍼 가장 느림 자유로운 수정
clone은 메타데이터만 복사하고 버퍼를 공유, pskb_copy는 linear 데이터만 복사, skb_copy는 전체 독립 복사
/* clone: sk_buff 메타데이터만 복사, 데이터 버퍼는 공유 (refcount 증가) */
struct sk_buff *clone = skb_clone(skb, GFP_ATOMIC);
/* clone->data == skb->data (같은 버퍼 참조) */
/* skb_shared_info.dataref 증가됨 */

/* pskb_copy: linear 헤더만 복사, paged data는 page refcount 증가 */
struct sk_buff *pcopy = pskb_copy(skb, GFP_ATOMIC);
/* 헤더를 수정해야 하지만 페이로드는 그대로인 경우 최적 */

/* copy: 메타데이터 + linear + paged 데이터 모두 완전 복사 */
struct sk_buff *copy = skb_copy(skb, GFP_ATOMIC);
/* copy->data != skb->data (독립적 버퍼) */

/* skb_share_check: 공유 여부 확인 후 필요 시 clone */
skb = skb_share_check(skb, GFP_ATOMIC);
if (!skb)
    return NET_RX_DROP;
/* 이제 skb를 독점적으로 소유 — 안전하게 메타데이터 수정 가능 */
코드 설명
  • skb_clone()net/core/skbuff.cskb_clone()은 새 sk_buff 메타데이터만 할당하고 데이터 버퍼는 원본과 공유합니다. skb_shinfo(skb)->dataref를 증가시켜 공유를 추적하며, clone된 skb의 데이터를 수정하려면 먼저 pskb_copy()skb_copy()로 분리해야 합니다.
  • pskb_copy()linear 헤더 영역(head~end)만 새로 할당·복사하고, paged fragments는 page refcount 증가로 공유합니다. netfilter NAT처럼 L3/L4 헤더만 수정하고 페이로드는 그대로인 경우 최적의 선택입니다.
  • skb_copy()skb_copy()__alloc_skb()로 새 버퍼를 할당한 뒤 skb_copy_bits()로 linear + paged 데이터를 모두 복사합니다. 결과물은 완전히 독립적이므로 자유롭게 수정 가능하지만, 비용이 가장 큽니다.
  • skb_share_check()skb->users > 1이면(다른 경로에서도 참조 중이면) skb_clone()을 수행하고 원본의 refcount를 감소시킵니다. 프로토콜 핸들러 진입 시 메타데이터 독점 소유를 확보하는 관용 패턴입니다. 실패 시 NULL을 반환하므로 반드시 검사해야 합니다.
💡

선택 기준: 패킷을 읽기만 한다면 skb_clone(), 헤더만 수정해야 한다면 pskb_copy(), 페이로드(Payload)까지 수정해야 한다면 skb_copy()를 사용하세요. Netfilter NAT는 pskb_copy()를 주로 사용합니다.

프래그먼트와 scatter-gather

대용량 패킷은 linear 데이터 외에 paged fragments를 사용합니다. skb_shared_infoend 포인터 바로 뒤에 위치하며, fragment 배열과 GSO 메타데이터를 관리합니다.

/* skb_shared_info: end 포인터 바로 뒤에 위치 */
struct skb_shared_info {
    __u8        nr_frags;           /* fragment 수 */
    __u8        tx_flags;
    unsigned short gso_size;       /* GSO 세그먼트 크기 */
    unsigned short gso_segs;       /* GSO 세그먼트 수 */
    unsigned short gso_type;       /* GSO 타입 */
    struct sk_buff *frag_list;     /* 연결된 skb 리스트 */
    skb_frag_t  frags[MAX_SKB_FRAGS]; /* page fragment 배열 */
    atomic_t    dataref;            /* 데이터 공유 참조 카운트 */
};
코드 설명
  • nr_frags현재 사용 중인 paged fragment 수입니다. frags[] 배열의 유효 범위는 0~nr_frags-1이며, 최대값은 MAX_SKB_FRAGS(PAGE_SIZE=4096 기준 17)입니다.
  • gso_size, gso_segs, gso_typeGSO(Generic Segmentation Offload) 메타데이터입니다. gso_size는 MSS(Maximum Segment Size), gso_segs는 세그먼트 수, gso_type은 프로토콜 유형(SKB_GSO_TCPV4 등)입니다. gso_size > 0이면 이 skb는 아직 분할되지 않은 super-packet입니다.
  • frag_list연결된 skb 리스트로, GRO 병합이나 IP 재조립(Reassembly) 결과물에서 사용됩니다. frags[]가 page fragment를 참조하는 반면, frag_list는 완전한 sk_buff 체인입니다. skb_walk_frags() 매크로로 순회합니다.
  • frags[MAX_SKB_FRAGS]skb_frag_t(=struct bio_vec) 배열로, 각 원소가 {page, offset, size} 튜플입니다. scatter-gather DMA를 위해 물리적으로 분산된 페이지들을 하나의 skb에서 참조할 수 있게 합니다.
  • dataref데이터 버퍼의 공유 참조 카운트입니다. skb_clone() 시 증가하며, skb_shared()dataref > 1인지 확인합니다. 데이터를 수정하려면 dataref == 1이어야 하고, 그렇지 않으면 pskb_copy()로 분리해야 합니다.

프래그먼트 접근 함수

함수프로토타입설명
skb_shinfostruct skb_shared_info *skb_shinfo(skb)skb_shared_info 접근. (struct skb_shared_info *)(skb_end_pointer(skb))
skb_frag_pagestruct page *skb_frag_page(frag)fragment의 페이지(Page) 반환
skb_frag_offunsigned int skb_frag_off(frag)페이지 내 데이터 시작 오프셋
skb_frag_sizeunsigned int skb_frag_size(frag)fragment의 데이터 크기
skb_frag_addressvoid *skb_frag_address(frag)fragment 데이터의 가상 주소. page_address(page) + offset. highmem 페이지에서는 사용 불가
skb_frag_address_safevoid *skb_frag_address_safe(frag)highmem이면 NULL 반환. 안전한 접근 시도
skb_frag_set_pagevoid skb_frag_set_page(skb, i, page)fragment i의 페이지 설정
skb_frag_off_addvoid skb_frag_off_add(frag, delta)fragment 오프셋 증가 (partial consume 시)
skb_frag_size_subvoid skb_frag_size_sub(frag, delta)fragment 크기 감소
skb_frag_size_setvoid skb_frag_size_set(frag, size)fragment 크기 직접 설정
skb_has_frag_listbool skb_has_frag_list(skb)shinfo->frag_list != NULL. GRO/IP fragmentation 결과
skb_walk_fragsskb_walk_frags(skb, iter)frag_list 순회 매크로

프래그먼트 추가 함수

함수프로토타입설명
skb_add_rx_fragvoid skb_add_rx_frag(skb, i, page, off, size, truesize)수신 경로: fragment 추가 + len, data_len, truesize 갱신. 드라이버에서 가장 흔히 사용
skb_fill_page_descvoid skb_fill_page_desc(skb, i, page, off, size)fragment 슬롯 설정 + nr_frags 갱신. len/truesize는 호출자가 직접 갱신
__skb_fill_page_descvoid __skb_fill_page_desc(skb, i, page, off, size)nr_frags 미갱신. 여러 fragment를 일괄 추가 시 마지막에만 nr_frags 설정
skb_coalesce_rx_fragvoid skb_coalesce_rx_frag(skb, i, size, truesize)마지막 fragment 확장 (연속된 페이지). 새 fragment 추가 대신 기존 것 크기 증가
/* fragment 순회 */
struct skb_shared_info *si = skb_shinfo(skb);
for (int i = 0; i < si->nr_frags; i++) {
    skb_frag_t *frag = &si->frags[i];
    struct page *page = skb_frag_page(frag);
    unsigned int offset = skb_frag_off(frag);
    unsigned int size = skb_frag_size(frag);
    /* page + offset에서 size 바이트 데이터 */
}

/* 드라이버 수신: DMA로 받은 페이지를 fragment로 추가 */
struct sk_buff *skb = napi_alloc_skb(napi, 256);  /* 헤더만 linear */
skb_put(skb, header_len);
memcpy(skb->data, dma_buf, header_len);

/* 페이로드는 DMA 페이지를 fragment로 직접 참조 (zero-copy) */
skb_add_rx_frag(skb, 0, rx_page, rx_offset, payload_size, PAGE_SIZE);
/* 내부적으로: skb->len += payload_size,
 *            skb->data_len += payload_size,
 *            skb->truesize += PAGE_SIZE */

/* zero-copy 전송: sendfile에서 파일 페이지를 fragment로 */
skb_fill_page_desc(skb, frag_idx, file_page, page_offset, chunk_size);
skb->len += chunk_size;
skb->data_len += chunk_size;
skb->truesize += chunk_size;  /* 호출자가 직접 갱신 */

/* frag_list 순회 (GRO 병합, IP 재조립 결과) */
struct sk_buff *frag_iter;
skb_walk_frags(skb, frag_iter) {
    /* frag_iter는 frag_list의 각 skb */
    pr_debug("frag_list skb len=%u
", frag_iter->len);
}
코드 설명
  • frags[] 순회skb_shinfo(skb)skb_shared_info에 접근한 뒤, nr_frags만큼 루프를 돌며 각 skb_frag_t의 page/offset/size를 읽습니다. 실제 데이터 접근 시 kmap_local_page(page)로 가상 주소를 얻어야 합니다(highmem 페이지 대응).
  • 드라이버 수신 패턴napi_alloc_skb(napi, 256)으로 헤더만 담을 작은 linear 버퍼를 할당하고, DMA로 받은 페이지는 skb_add_rx_frag()로 fragment에 직접 연결합니다. 이것이 현대 드라이버의 header-split zero-copy 수신 패턴입니다.
  • skb_add_rx_frag()net/core/skbuff.c에서 skb_fill_page_desc() + len/data_len/truesize 갱신을 한 번에 수행합니다. truesize 인자는 데이터 크기가 아닌 실제 할당된 페이지 크기(PAGE_SIZE)를 전달해야 소켓 메모리 추적이 정확합니다.
  • skb_fill_page_desc()fragment 슬롯에 page/offset/size를 설정하고 nr_frags를 증가시키지만, len/data_len/truesize는 갱신하지 않습니다. sendfile의 zero-copy 전송처럼 호출자가 직접 이 값들을 관리해야 하는 경우에 사용합니다.
  • skb_walk_frags()skb_shinfo(skb)->frag_list를 순회하는 매크로입니다. GRO 병합이나 IP 재조립 결과, frag_list에는 각 원본 패킷이 완전한 sk_buff로 연결되어 있습니다. frags[]의 page fragment와는 완전히 다른 구조이므로 별도 처리가 필요합니다.
⚠️

주의: skb_add_rx_frag()의 마지막 인자 truesize실제 할당된 메모리 크기여야 합니다 (보통 PAGE_SIZE). 데이터 크기(size)를 넣으면 소켓 메모리 추적(sk_rmem_alloc)이 부정확해져 OOM이나 조기 드롭이 발생합니다. skb_frag_address()CONFIG_HIGHMEM 환경에서 highmem 페이지에 사용하면 잘못된 주소를 반환합니다 — kmap이 필요합니다.

sk_buff 데이터 분산 저장: linear + paged fragments + frag_list head data tail end hr 헤더 payload (linear) skb_shared_info linear 영역 (len - data_len) frags[] [0] off payload₂ (page frag) [1] off payload₃ (page frag) paged fragments (data_len 일부) frag_list skb A skb B GRO 병합, IP 재조립 결과 (skb 체인) 크기 관계: skb→len = linear + frags[].size 합 + frag_list 체인의 len 합 skb→data_len = frags[].size 합 + frag_list len 합 (비선형 부분만)
sk_buff의 데이터는 linear 영역(head~tail), paged fragments(frags[]), frag_list(skb 체인) 3단계로 분산 저장됨. skb_shared_info는 end 포인터 위치에 존재

고급 데이터 조작

패킷 처리 시 데이터 레이아웃을 변경해야 하는 상황이 자주 발생합니다. 비선형(paged) 데이터 처리, headroom 확장, clone된 skb의 쓰기 권한 확보 등 고급 시나리오를 다루는 함수들입니다.

pskb_may_pull — linear 영역 확보 (★★★ 최중요)

/* include/linux/skbuff.h */
static inline int pskb_may_pull(struct sk_buff *skb, unsigned int len);

pskb_may_pull()프로토콜 헤더 파싱 전에 반드시 호출해야 하는 필수 함수입니다. linear 영역에 최소 len 바이트가 있는지 확인하고, 부족하면 paged fragments에서 linear 영역으로 데이터를 당겨옵니다.

/* 기본 패턴: 프로토콜 헤더 파싱 전 linear 확보 */
static int my_protocol_rcv(struct sk_buff *skb)
{
    struct my_hdr *hdr;

    /* 1단계: 고정 크기 헤더만큼 확보 */
    if (!pskb_may_pull(skb, sizeof(*hdr)))
        goto drop;

    hdr = (struct my_hdr *)skb_transport_header(skb);
    /* 이제 hdr-> 필드에 안전하게 접근 가능 */

    /* 2단계: 가변 길이 헤더 (옵션 등) */
    if (!pskb_may_pull(skb, hdr->hdr_len))
        goto drop;

    hdr = (struct my_hdr *)skb_transport_header(skb); /* ★ 포인터 재취득! */
    /* ... 처리 ... */
}

/* 실제 커널 코드 예: IPv6 옵션 헤더 파싱 */
/* net/ipv6/exthdrs.c */
if (!pskb_may_pull(skb, skb_transport_offset(skb) + 8))
    goto fail;
struct ipv6_opt_hdr *opth = (struct ipv6_opt_hdr *)skb_transport_header(skb);
int optlen = ipv6_optlen(opth);
if (!pskb_may_pull(skb, skb_transport_offset(skb) + optlen))
    goto fail;
opth = (struct ipv6_opt_hdr *)skb_transport_header(skb); /* ★ 재취득 */
⚠️

★ 가장 흔한 커널 네트워크 버그: pskb_may_pull() 호출 후 반드시 헤더 포인터를 재취득해야 합니다. 내부적으로 __pskb_pull_tail()이 새 버퍼를 할당하고 데이터를 복사할 수 있어, 이전 포인터가 해제된 메모리를 가리키게 됩니다. 커널 소스에서 이 패턴의 버그가 수백 건 패치되었습니다.

pskb_may_pull() — paged fragments에서 linear 영역으로 데이터 당기기 linear 버퍼 (head ~ end) paged fragments Before head data tail end hr 헤더(일부) unused 나머지 헤더 payload payload frag[0] frag[1] linear 영역 부족! pskb_may_pull(full_hdr_len) After head data tail end hr 헤더(원본) pulled unused payload payload frag[0] (축소) frag[1] 전체 헤더 linear 확보 완료 주의: pull 시 버퍼 재할당 가능 → 모든 헤더 포인터 재취득 필수! skb_linearize()는 모든 paged data를 linear으로 당기는 극단 버전 (성능 주의)
pskb_may_pull()이 paged fragments에서 linear 영역으로 데이터를 당기는 과정. 같은 색상(주황)이 이동된 데이터를 표시. pull 후 버퍼가 재할당될 수 있어 포인터 재취득 필수

skb_ensure_writable — 쓰기 가능 영역 확보 (6.x+ 권장)

int skb_ensure_writable(struct sk_buff *skb, unsigned int write_len);

skb_ensure_writable()는 패킷 데이터를 수정하기 전에 호출하는 통합 함수입니다. 내부적으로 clone 상태 해제(skb_unclone) + linear 영역 확보(pskb_may_pull)를 모두 수행합니다. skb_make_writable()의 개선 버전으로, netfilter/tc 등에서 패킷 수정 전 표준 패턴입니다.

/* netfilter/tc에서 패킷 내용 수정 전 */
if (skb_ensure_writable(skb, skb_network_header_len(skb) + sizeof(struct tcphdr)))
    goto drop;

/* 이제 IP + TCP 헤더 영역을 안전하게 수정 가능 */
struct iphdr *iph = ip_hdr(skb);   /* ★ 포인터 재취득 */
iph->ttl = 64;
struct tcphdr *th = tcp_hdr(skb);  /* ★ 포인터 재취득 */
th->window = htons(new_window);

/* NAT 구현: 소스 IP 변경 */
if (skb_ensure_writable(skb, skb_network_offset(skb) + sizeof(struct iphdr)))
    return NF_DROP;
iph = ip_hdr(skb);
csum_replace4(&iph->check, iph->saddr, new_saddr);
iph->saddr = new_saddr;
💡

권장: skb_make_writable() 대신 skb_ensure_writable()를 사용하세요. skb_make_writable()write_len에 대해 skb->len 검사를 추가로 수행하는데, 대부분의 경우 불필요합니다. 최신 커널 코드는 skb_ensure_writable()로 통일되는 추세입니다.

skb_linearize — 전체 linearize

static inline int skb_linearize(struct sk_buff *skb);

모든 paged fragments와 frag_list의 데이터를 하나의 연속 linear 버퍼에 합칩니다. 내부적으로 __pskb_pull_tail(skb, skb->data_len)을 호출합니다.

/* scatter-gather를 지원하지 않는 레거시 장치 */
if (skb_is_nonlinear(skb)) {
    if (skb_linearize(skb))
        goto drop;  /* -ENOMEM: 새 버퍼 할당 실패 */
    /* 이제 모든 데이터가 head~tail 사이에 연속 존재 */
}

/* 디버깅/검증: 전체 패킷 덤프 전 */
if (skb_linearize(skb) == 0)
    print_hex_dump(KERN_DEBUG, "pkt: ", DUMP_PREFIX_OFFSET,
                   16, 1, skb->data, skb->len, true);
코드 설명
  • skb_is_nonlinear()skb->data_len != 0을 확인하는 인라인 함수입니다. true이면 데이터가 paged fragments나 frag_list에 분산되어 있으므로, skb->data만으로는 전체 패킷에 접근할 수 없습니다.
  • skb_linearize()include/linux/skbuff.h에서 __pskb_pull_tail(skb, skb->data_len)을 호출하는 인라인 래퍼입니다. net/core/skbuff.c__pskb_pull_tail()이 새 연속 버퍼를 할당하고 frags[] + frag_list 데이터를 모두 복사한 뒤, 비선형 영역을 해제합니다.
  • 레거시 장치 패턴scatter-gather DMA를 지원하지 않는 NIC 드라이버의 ndo_start_xmit()에서 linearize가 필요합니다. 성공하면 data_len == 0이 되어 head~tail 사이에 모든 데이터가 연속으로 존재합니다.
  • 디버깅 패턴print_hex_dump()로 전체 패킷을 덤프하려면 연속 버퍼가 필요하므로 linearize를 먼저 수행합니다. 프로덕션 코드에서는 skb_copy_bits()skb_header_pointer()로 필요한 부분만 읽는 것이 권장됩니다.
⚠️

주의: skb_linearize()성능 킬러입니다. 64KB GSO 패킷을 linearize하면 거대한 메모리 할당 + 전체 복사가 발생합니다. 가능하면 pskb_may_pull()로 필요한 부분만 당기거나, skb_header_pointer()skb_copy_bits()로 비선형 데이터에 접근하세요. skb_linearize()는 최후의 수단으로만 사용합니다.

pskb_expand_head — headroom/tailroom 확장

int pskb_expand_head(struct sk_buff *skb, int nhead, int ntail, gfp_t gfp_mask);

skb의 headroom을 nhead바이트, tailroom을 ntail바이트 추가로 확장합니다. 필요 시 새 버퍼를 할당하고 linear 데이터를 복사합니다. clone 상태의 skb는 자동으로 unclone됩니다.

함수프로토타입설명
pskb_expand_headint pskb_expand_head(skb, nhead, ntail, gfp)headroom/tailroom 확장. 기존 공간이 충분해도 cloned면 재할당
skb_expand_headint skb_expand_head(skb, nhead)headroom만 확장하는 간단 버전 (5.12+). 실패 시 skb 자동 해제
skb_realloc_headroomstruct sk_buff *skb_realloc_headroom(skb, headroom)새 skb를 반환 (원본 skb는 변경 없음). 원본이 필요한 경우에 적합
skb_copy_expandstruct sk_buff *skb_copy_expand(skb, nhead, ntail, gfp)전체 복사 + head/tailroom 확장을 동시에. 완전한 독립 복사본 생성
/* 터널 캡슐화 전: headroom 확장 */
int max_headroom = LL_RESERVED_SPACE(tun_dev) + sizeof(struct iphdr)
                   + sizeof(struct udphdr) + GENEVE_HLEN;

if (skb_headroom(skb) < max_headroom || skb_header_cloned(skb)) {
    if (pskb_expand_head(skb, max_headroom, 0, GFP_ATOMIC))
        goto tx_error;
}

/* skb_expand_head: 더 간단한 API (5.12+) */
if (skb_expand_head(skb, needed_headroom))
    return NETDEV_TX_OK;  /* 실패 시 skb 이미 해제됨! */

/* skb_realloc_headroom: 원본을 보존하며 새 skb 생성 */
struct sk_buff *new_skb = skb_realloc_headroom(skb, needed);
if (!new_skb)
    goto drop;
consume_skb(skb);  /* 원본 해제 */
skb = new_skb;

/* skb_copy_expand: 완전 복사 + 확장 */
struct sk_buff *expanded = skb_copy_expand(skb, extra_head, extra_tail, GFP_ATOMIC);
if (!expanded)
    goto drop;
/* expanded는 완전히 독립된 복사본 + 확장된 headroom/tailroom */
⚠️

주의: pskb_expand_head() 후에는 모든 포인터(data, head, tail, end와 헤더 포인터)가 무효화됩니다. skb_expand_head()는 실패 시 skb를 자동 해제하므로, 에러 경로에서 kfree_skb()를 호출하지 마세요.

pskb_expand_head(nhead, ntail) — 새 버퍼 할당으로 headroom/tailroom 확장 Before (기존 버퍼) head data tail end hr IP TCP payload tr headroom 부족 pskb_expand_head(nhead, ntail, GFP_ATOMIC) After (새 버퍼) head data tail end 확장된 headroom (+nhead) IP TCP payload (복사됨) 확장된 tailroom (+ntail) ↑ 기존 버퍼 범위 핵심: 새 버퍼 할당 + linear 데이터 복사 → 기존 포인터 전부 무효화. clone 상태도 자동 해제됨 skb_cow_head()도 동일 원리: clone 상태면 pskb_expand_head()를 호출하여 독립 버퍼 확보
pskb_expand_head()가 새 버퍼를 할당하여 headroom/tailroom을 확장하는 과정. data 시작 위치(x=240)를 정렬하여 확장 전후 비교. 점선 사각형이 기존 버퍼 범위를 표시

skb_cow 계열 — Copy-On-Write

clone된(공유 버퍼를 가진) skb의 데이터를 수정하려면, 먼저 독점적 쓰기 권한을 확보해야 합니다. skb_cow 계열 함수가 이를 처리합니다.

함수프로토타입설명
skb_cow_headint skb_cow_head(skb, headroom)headroom 확보 + clone 해제를 한 번에. 터널/VLAN 캡슐화의 표준 패턴
skb_cowint skb_cow(skb, headroom)skb_cow_head와 유사하지만 linear 데이터 전체를 COW. 페이로드까지 수정 시
skb_cow_dataint skb_cow_data(skb, tailbits, trailer)paged data를 포함한 전체 데이터를 COW. IPSec 암호화 등 전체 패킷 수정 시
/* skb_cow_head: 터널 encapsulation 전 (★가장 흔한 패턴) */
static int my_tunnel_xmit(struct sk_buff *skb, struct net_device *dev)
{
    int hdr_len = sizeof(struct iphdr) + sizeof(struct gre_hdr);

    /* 클론 상태 해제 + headroom 확보를 동시에 수행 */
    if (skb_cow_head(skb, hdr_len + LL_RESERVED_SPACE(dev))) {
        kfree_skb(skb);
        return NETDEV_TX_OK;
    }

    /* 이제 안전하게 헤더 추가 가능 (독점 소유) */
    skb_push(skb, hdr_len);
    /* ... GRE + IP 헤더 설정 ... */
}

/* skb_cow: 포워딩 경로에서 TTL 수정 */
if (skb_cow(skb, LL_RESERVED_SPACE(rt->dst.dev)))
    goto drop;
ip_hdr(skb)->ttl--;  /* linear 데이터 수정 안전 */
ip_send_check(ip_hdr(skb));

/* skb_cow_data: IPSec ESP 암호화 전 */
struct sk_buff *trailer;
int nfrags = skb_cow_data(skb, esp_trailer_len, &trailer);
if (nfrags < 0)
    goto error;
/* 이제 paged data를 포함한 전체 패킷을 수정 가능 */
/* trailer는 마지막 fragment의 skb를 가리킴 (ESP padding 추가용) */

공유 상태 확인 및 해제 함수

skb가 clone되었거나 여러 곳에서 참조되는지 확인하고, 필요 시 독점 소유를 확보하는 함수들입니다.

함수프로토타입설명
skb_clonedbool skb_cloned(skb)데이터 버퍼가 다른 skb와 공유되는지 (dataref > 1). clone 또는 원본 중 하나
skb_header_clonedbool skb_header_cloned(skb)헤더(linear) 영역만 공유 여부. skb->cloned && dataref != 1
skb_sharedbool skb_shared(skb)sk_buff 메타데이터가 공유되는지 (users > 1). skb_get() 호출 여부
skb_share_checkstruct sk_buff *skb_share_check(skb, gfp)skb_shared()이면 skb_clone(), 아니면 원본 반환. 실패 시 NULL
skb_unsharestruct sk_buff *skb_unshare(skb, gfp)공유 상태이면 완전 복사(skb_copy)하여 독립 skb 반환
skb_uncloneint skb_unclone(skb, gfp)clone 상태면 linear 데이터를 독립 복사. pskb_expand_head(skb, 0, 0, gfp)와 동등
/* 수신 핸들러: 패킷 수정 전 독점 소유 확보 */
static int my_netfilter_hook(struct sk_buff *skb)
{
    /* 1. 메타데이터 공유 해제 (여러 수신자가 같은 skb를 참조할 수 있음) */
    skb = skb_share_check(skb, GFP_ATOMIC);
    if (!skb)
        return NET_RX_DROP;

    /* 2. 데이터 쓰기 권한 확보 (clone된 데이터 버퍼 독립화) */
    if (skb_ensure_writable(skb, skb_network_header_len(skb)))
        goto drop;

    /* 이제 안전하게 패킷 수정 가능 */
    ip_hdr(skb)->tos = new_tos;
    return NET_RX_SUCCESS;
}

/* 브리지에서 패킷을 여러 포트로 복제 전송 */
if (skb_shared(skb)) {
    /* 이미 다른 곳에서 참조 중 → clone으로 분리 */
    struct sk_buff *new_skb = skb_clone(skb, GFP_ATOMIC);
    if (!new_skb)
        return;
    /* new_skb를 해당 포트로 전송 */
}

/* clone 상태 해제 (linear 데이터 독립화) */
if (skb_cloned(skb)) {
    if (skb_unclone(skb, GFP_ATOMIC))
        goto drop;
    /* 이제 linear 데이터를 수정해도 원본에 영향 없음 */
}
ℹ️

shared vs cloned: skb_shared()sk_buff 구조체 자체가 여러 참조자를 가지는지 (users > 1), skb_cloned()데이터 버퍼가 여러 sk_buff에 의해 공유되는지 (dataref > 1)를 확인합니다. skb_clone()은 새 sk_buff를 만들지만 버퍼를 공유하므로, clone 후에는 원본과 clone 모두 skb_cloned() == true가 됩니다.

skb_make_writable — 레거시 호환

int skb_make_writable(struct sk_buff *skb, unsigned int writable_len);

writable_len 바이트까지 쓰기 가능하게 만듭니다. skb_ensure_writable()와 거의 동일하지만, writable_len > skb->len이면 실패를 반환하는 추가 검사가 있습니다. 새 코드에서는 skb_ensure_writable()를 사용하세요.

헤더 포인터 조작 함수

sk_buff는 L2/L3/L4 각 프로토콜 레이어의 헤더 위치를 mac_header, network_header, transport_header 세 개의 오프셋(Offset)으로 추적합니다. 이 오프셋을 설정/조회하는 함수들은 패킷 처리의 기반입니다.

헤더 포인터 접근 함수

함수반환형설명
skb_mac_header(skb)unsigned char *L2(MAC) 헤더 시작 주소. eth_hdr(skb)가 내부적으로 호출
skb_network_header(skb)unsigned char *L3(IP) 헤더 시작 주소. ip_hdr(skb), ipv6_hdr(skb)가 내부적으로 호출
skb_transport_header(skb)unsigned char *L4(TCP/UDP) 헤더 시작 주소. tcp_hdr(skb), udp_hdr(skb)가 내부적으로 호출
skb_inner_network_header(skb)unsigned char *터널 캡슐화 시 내부(inner) L3 헤더. GRE/VXLAN/Geneve 등
skb_inner_transport_header(skb)unsigned char *터널 캡슐화 시 내부(inner) L4 헤더
skb_inner_mac_header(skb)unsigned char *터널 캡슐화 시 내부(inner) L2 헤더

헤더 포인터 설정/리셋 함수

함수동작사용 시점
skb_reset_mac_header(skb)mac_header = data - headL2 헤더 위치를 현재 data로 설정. 드라이버 수신, 캡슐화 후
skb_reset_network_header(skb)network_header = data - headL3 헤더 위치를 현재 data로 설정. eth_type_trans() 이후, IP push 후
skb_reset_transport_header(skb)transport_header = data - headL4 헤더 위치를 현재 data로 설정. IP pull 이후, TCP/UDP push 후
skb_set_mac_header(skb, offset)mac_header = data + offset - headdata 기준 offset 위치에 L2 헤더 설정
skb_set_network_header(skb, offset)network_header = data + offset - headdata 기준 offset 위치에 L3 헤더 설정
skb_set_transport_header(skb, offset)transport_header = data + offset - headdata 기준 offset 위치에 L4 헤더 설정
skb_set_inner_network_header(skb, offset)inner L3 오프셋 설정터널 캡슐화 시 inner 헤더 위치 기록
skb_set_inner_transport_header(skb, offset)inner L4 오프셋 설정터널 캡슐화 시 inner 헤더 위치 기록

헤더 오프셋/길이 조회 함수

함수반환값설명
skb_transport_offset(skb)transport_header - datadata부터 L4 헤더까지의 오프셋. IP 옵션 포함 L3 + L2 길이
skb_network_offset(skb)network_header - datadata부터 L3 헤더까지의 오프셋. L2 pull 전에는 0
skb_network_header_len(skb)transport_header - network_headerL3 헤더 길이 (IP 옵션 포함). netfilter에서 자주 사용
skb_inner_network_offset(skb)inner L3 오프셋터널 내부 L3 헤더까지의 오프셋
skb_mac_header_was_set(skb)boolmac_header가 설정된 적 있는지. 포워딩/캡슐화 후 유효성 확인용
skb_mac_header_rebuild(skb)voidmac_header를 network_header 바로 앞으로 재정렬. 포워딩 후 L2 재구성
/* 프로토콜별 편의 매크로 (헤더 포인터 + 캐스팅) */
struct ethhdr  *eth = eth_hdr(skb);      /* (struct ethhdr *)skb_mac_header(skb) */
struct iphdr   *iph = ip_hdr(skb);       /* (struct iphdr *)skb_network_header(skb) */
struct ipv6hdr *ip6h = ipv6_hdr(skb);   /* (struct ipv6hdr *)skb_network_header(skb) */
struct tcphdr  *th = tcp_hdr(skb);       /* (struct tcphdr *)skb_transport_header(skb) */
struct udphdr  *uh = udp_hdr(skb);       /* (struct udphdr *)skb_transport_header(skb) */
struct icmphdr *icmph = icmp_hdr(skb);   /* (struct icmphdr *)skb_transport_header(skb) */

/* 터널 캡슐화 시 inner/outer 헤더 구분 */
struct iphdr *outer_iph = ip_hdr(skb);
skb_set_inner_network_header(skb, skb_network_offset(skb));
skb_set_inner_transport_header(skb, skb_transport_offset(skb));
/* outer 헤더 추가 후 */
skb_push(skb, outer_hdr_len);
skb_reset_network_header(skb);
/* 이제 ip_hdr(skb)는 outer, skb_inner_network_header(skb)는 inner */

/* 포워딩 후 L2 헤더 재구성 */
if (skb_mac_header_was_set(skb))
    skb_mac_header_rebuild(skb);  /* mac_header를 network_header 앞으로 이동 */
⚠️

주의: skb_push()/skb_pull() 후에는 반드시 skb_reset_*_header()로 해당 레이어의 헤더 포인터를 갱신하세요. 갱신하지 않으면 ip_hdr(skb), tcp_hdr(skb) 등이 잘못된 주소를 반환합니다. 특히 터널 캡슐화/역캡슐화 후에는 inner와 outer 헤더 포인터를 모두 올바르게 설정해야 합니다.

데이터 접근 및 복사 함수

비선형(paged) skb에서 데이터를 읽거나 쓰려면, linear 영역 너머의 fragments까지 처리하는 전용 함수가 필요합니다. 직접 포인터 접근(skb->data + offset)은 linear 영역만 가능합니다.

skb_header_pointer — 비선형 안전 헤더 접근 (★★ 매우 중요)

static inline void *skb_header_pointer(
    const struct sk_buff *skb,
    int offset, int len,
    void *buffer);

skb->data + offset에서 len 바이트를 안전하게 접근합니다. 데이터가 linear 영역에 있으면 직접 포인터를 반환하고, paged 영역에 걸쳐 있으면 buffer에 복사 후 buffer 포인터를 반환합니다. 실패 시 NULL을 반환합니다.

/* 전형적 사용법: netfilter에서 TCP 포트 확인 */
struct tcphdr _tcph;  /* 스택에 임시 버퍼 */
const struct tcphdr *th;

th = skb_header_pointer(skb,
    skb_network_header_len(skb),   /* IP 헤더 뒤 */
    sizeof(_tcph),                 /* TCP 헤더 크기 */
    &_tcph);                       /* 임시 복사 버퍼 */
if (!th)
    return NF_DROP;  /* 데이터 부족 */
/* th가 linear 데이터를 직접 가리키거나, _tcph의 복사본을 가리킴 */
pr_info("src port: %u
", ntohs(th->source));

/* 읽기 전용 접근에 최적: 복사 불필요 시 zero-copy */
/* 쓰기가 필요하면 skb_ensure_writable() + 직접 접근이 더 적합 */

/* 중첩된 프로토콜 헤더 접근 */
struct gre_base_hdr _greh;
const struct gre_base_hdr *greh;
int gre_offset = skb_transport_offset(skb) + sizeof(struct udphdr);
greh = skb_header_pointer(skb, gre_offset, sizeof(_greh), &_greh);
💡

skb_header_pointer vs pskb_may_pull: 읽기 전용이면 skb_header_pointer()가 더 효율적입니다 — skb를 수정하지 않고 필요 시 스택 버퍼에만 복사합니다. 데이터 수정이 필요하면 pskb_may_pull()이나 skb_ensure_writable()를 사용하세요.

skb_copy_bits / skb_store_bits — 비선형 데이터 읽기/쓰기

함수프로토타입설명
skb_copy_bitsint skb_copy_bits(skb, offset, to, len)skb의 offset부터 len바이트를 to 버퍼로 복사. 비선형 안전 (frags + frag_list 순회)
skb_store_bitsint skb_store_bits(skb, offset, from, len)from 버퍼에서 skb의 offset 위치에 len바이트 쓰기. 비선형 안전
skb_copy_from_linear_datavoid skb_copy_from_linear_data(skb, to, len)memcpy(to, skb->data, len). linear 전용 — 비선형 미지원
skb_copy_from_linear_data_offsetvoid ..._offset(skb, off, to, len)memcpy(to, skb->data + off, len). linear 전용
skb_copy_datagram_iterint skb_copy_datagram_iter(skb, off, to, len)iov_iter로 데이터 복사. 소켓 → 사용자 공간 경로 (recvmsg 등)
skb_copy_datagram_msgint skb_copy_datagram_msg(skb, off, msg, len)msghdr로 데이터 복사. skb_copy_datagram_iter 래퍼
/* 비선형 skb에서 특정 오프셋의 데이터 읽기 */
u8 buf[64];
if (skb_copy_bits(skb, offset, buf, sizeof(buf)) < 0)
    goto drop;  /* offset + len > skb->len */

/* 비선형 skb의 특정 위치에 데이터 쓰기 */
__be16 new_port = htons(8080);
skb_store_bits(skb, transport_offset + offsetof(struct udphdr, dest),
               &new_port, sizeof(new_port));

/* 소켓 수신 경로: 사용자 공간으로 복사 */
int my_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int flags)
{
    struct sk_buff *skb = skb_recv_datagram(sk, flags, &err);
    if (!skb)
        return err;

    /* 비선형 skb에서도 안전하게 사용자 공간에 복사 */
    err = skb_copy_datagram_msg(skb, header_len, msg, copy_len);
    skb_free_datagram(sk, skb);
    return err ? err : copy_len;
}
⚠️

주의: skb_copy_from_linear_data()linear 영역만 접근합니다. 비선형 skb에서 이 함수를 사용하면 paged 데이터를 읽지 못합니다. 비선형 가능성이 있으면 반드시 skb_copy_bits()를 사용하세요. skb_store_bits()는 clone된 skb에서도 동작하지만, 원본의 데이터가 변경될 수 있으므로 skb_ensure_writable()를 먼저 호출하세요.

체크섬 조작 함수

패킷의 체크섬(Checksum)은 데이터 무결성 확인의 핵심입니다. skb 데이터를 수정할 때마다 관련 체크섬을 갱신해야 하며, 하드웨어 오프로드 상태도 고려해야 합니다.

함수프로토타입설명
skb_checksum__wsum skb_checksum(skb, offset, len, csum)비선형 안전 체크섬 계산. frags + frag_list 순회. csum은 초기값
skb_checksum_complete__sum16 skb_checksum_complete(skb)수신 패킷 체크섬 검증 완료. 0이면 유효, 비0이면 손상
skb_checksum_helpint skb_checksum_help(skb)HW 오프로드(CHECKSUM_PARTIAL)를 SW 체크섬으로 변환. GSO 전, netfilter NAT 후
skb_postpull_rcsumvoid skb_postpull_rcsum(skb, start, len)skb_pull() 후 체크섬 갱신. 제거된 데이터의 체크섬을 보정
skb_postpush_rcsumvoid skb_postpush_rcsum(skb, start, len)skb_push() 후 체크섬 갱신. 추가된 헤더의 체크섬을 반영
skb_checksum_init__sum16 skb_checksum_init(skb, proto, ...)pseudo-header 체크섬으로 초기화. UDP/TCP 수신 시
csum_replace2void csum_replace2(sum, old, new)2바이트 필드 변경 시 체크섬 증분 갱신 (포트 변경 등)
csum_replace4void csum_replace4(sum, old, new)4바이트 필드 변경 시 체크섬 증분 갱신 (IP 주소 변경)
inet_proto_csum_replace4void inet_proto_csum_replace4(sum, skb, from, to, pseudohdr)L4 체크섬 증분 갱신 + HW 오프로드 상태 고려. NAT 표준 패턴
/* NAT: 소스 IP + 포트 변경 시 체크섬 갱신 */
static void nat_saddr(struct sk_buff *skb, __be32 new_saddr, __be16 new_sport)
{
    struct iphdr *iph = ip_hdr(skb);
    struct tcphdr *th = tcp_hdr(skb);

    /* IP 체크섬: 소스 IP 변경 반영 */
    csum_replace4(&iph->check, iph->saddr, new_saddr);
    iph->saddr = new_saddr;

    /* TCP 체크섬: pseudo-header의 소스 IP 변경 + 소스 포트 변경 */
    inet_proto_csum_replace4(&th->check, skb,
                             iph->saddr, new_saddr, true);  /* pseudohdr=true */
    inet_proto_csum_replace2(&th->check, skb,
                             th->source, new_sport, false);
    th->source = new_sport;
}

/* 수신 경로: 체크섬 검증 */
if (skb->ip_summed == CHECKSUM_COMPLETE) {
    /* HW가 전체 체크섬 제공 → 빠른 검증 */
    if (skb_checksum_complete(skb))
        goto csum_error;
} else if (skb->ip_summed != CHECKSUM_UNNECESSARY) {
    /* SW 체크섬 계산 필요 */
    skb_checksum_init(skb, IPPROTO_TCP, inet_compute_pseudo);
    if (skb_checksum_complete(skb))
        goto csum_error;
}

/* CHECKSUM_PARTIAL → SW 체크섬으로 강제 변환 */
/* (e.g., netfilter REDIRECT 후 loopback으로 재진입 시) */
if (skb_checksum_help(skb))
    goto drop;
⚠️

주의: NAT 등에서 IP 주소나 포트를 변경할 때, inet_proto_csum_replace4()를 사용하면 CHECKSUM_PARTIAL 상태도 올바르게 처리됩니다. csum_replace4()를 직접 사용하면 HW 오프로드 상태를 고려하지 않아 잘못된 체크섬이 전송될 수 있습니다.

skb 분할/병합/변형 함수

GSO/TSO, TCP 재전송, 패킷 포워딩 등에서 skb를 분할하거나 변형해야 하는 경우에 사용되는 함수들입니다.

함수프로토타입설명
skb_splitvoid skb_split(skb, skb1, len)skb를 len 위치에서 둘로 분할. 원본은 앞부분, skb1에 뒷부분. TCP 세그먼트 분할
skb_segmentstruct sk_buff *skb_segment(skb, features)GSO skb를 MSS 크기의 개별 세그먼트 리스트로 분할. SW GSO 경로의 핵심
skb_gso_segmentstruct sk_buff *skb_gso_segment(skb, features)skb_segment의 wrapper. 프로토콜별 GSO 콜백 호출
skb_morphstruct sk_buff *skb_morph(dst, src)dst skb를 src의 복제로 변형 (dst 내용물 교체). TCP fastopen, fclone 최적화
skb_condensevoid skb_condense(skb)linear 영역의 slack(유휴) 공간 축소. 메모리 절약 (소켓 큐에 장기 보관 시)
skb_scrub_packetvoid skb_scrub_packet(skb, xnet)포워딩/라우팅 전 민감 메타데이터 제거. mark, priority, 분류 결과 초기화
skb_orphanvoid skb_orphan(skb)skb↔소켓 연결 해제. destructor 호출 후 sk=NULL. TC/qdisc에서 소켓 메모리 즉시 반환
skb_shiftint skb_shift(to, from, shiftlen)from의 paged data를 to로 이동. TCP coalescing (연속 세그먼트 병합)
/* skb_split: TCP 세그먼트 분할 (재전송 시) */
struct sk_buff *buff = alloc_skb(nsize, GFP_ATOMIC);
if (!buff)
    return -ENOMEM;
skb_split(skb, buff, len);
/* skb: 0 ~ len-1 바이트 (재전송할 부분) */
/* buff: len ~ 끝 (나중에 전송할 부분) */
skb_queue_after(&sk->sk_write_queue, skb, buff);

/* skb_segment: GSO → 개별 MSS 세그먼트로 분할 */
/* NIC가 TSO를 지원하지 않는 경로에서 SW GSO 수행 */
struct sk_buff *segs = skb_gso_segment(skb, dev->features);
if (IS_ERR(segs)) {
    kfree_skb(skb);
    return PTR_ERR(segs);
}
/* segs는 연결된 skb 리스트 (각각 MSS 크기) */
consume_skb(skb);
skb_list_walk_safe(segs, seg, next) {
    skb_mark_not_on_list(seg);
    dev_queue_xmit(seg);
}

/* skb_orphan: qdisc에서 소켓 메모리 즉시 해제 */
/* fq_codel 등에서 큐잉 시간이 길어질 수 있으므로 */
skb_orphan(skb);
/* skb->sk = NULL, destructor가 호출되어 sk_wmem_alloc 감소 */
/* → 소켓이 새 데이터를 전송할 수 있게 됨 */

/* skb_scrub_packet: 네임스페이스 간 포워딩 시 */
skb_scrub_packet(skb, true);  /* xnet=true: 네트워크 네임스페이스 경계 */
/* mark, priority, pkt_type, hash 등 초기화됨 */

/* skb_condense: 소켓 수신 큐 메모리 최적화 */
/* GRO 후 linear 영역에 slack이 클 수 있음 */
skb_condense(skb);
/* data_len이 0이 아니고 truesize > SKB_TRUESIZE(skb_end_offset(skb))이면 */
/* paged data를 가능한 한 linear으로 옮기고 slack 해제 */
⚠️

주의: skb_split()은 원본 skb와 새 skb가 paged fragments를 공유할 수 있습니다. 분할 후 한쪽의 fragment를 수정하면 다른 쪽에 영향을 줄 수 있습니다. skb_segment()에서 반환된 리스트의 각 세그먼트는 next 포인터로 연결되며, 반드시 모든 세그먼트를 전송하거나 해제해야 합니다.

참조 카운트 및 소유권 함수

skb의 수명주기를 관리하는 참조 카운트와 소유권 관련 함수들입니다.

함수프로토타입설명
skb_getstruct sk_buff *skb_get(skb)users 참조 카운트 증가. 여러 곳에서 같은 skb 메타데이터를 참조할 때
skb_unrefbool skb_unref(skb)users 감소. 0이 되면 true 반환 (호출자가 해제 책임). kfree_skb 내부에서 사용
skb_orphanvoid skb_orphan(skb)소켓 연결 해제. destructor 호출 → sk = NULL. 소켓 메모리 즉시 반환
skb_dststruct dst_entry *skb_dst(skb)skb에 연결된 라우팅 결정(dst_entry) 조회
skb_dst_setvoid skb_dst_set(skb, dst)라우팅 결정을 skb에 연결. dst의 참조 카운트는 호출자가 이미 획득
skb_dst_dropvoid skb_dst_drop(skb)dst 참조 해제 + skb->_skb_refdst = 0
skb_dst_forcevoid skb_dst_force(skb)noref dst를 ref dst로 승격 (장기 보관 전). RCU 보호 해제 시 필수
skb_set_owner_rvoid skb_set_owner_r(skb, sk)수신 방향 소켓 소유권 설정. sk_rmem_alloc += truesize
skb_set_owner_wvoid skb_set_owner_w(skb, sk)전송 방향 소켓 소유권 설정. sk_wmem_alloc += truesize
/* skb_get: 패킷 미러링 — 원본을 유지하면서 다른 경로로도 전달 */
struct sk_buff *mirror = skb_get(skb);
/* mirror와 skb는 동일한 sk_buff 구조체 (users == 2) */
/* 주의: 데이터 수정 시 skb_clone() + skb_ensure_writable() 사용 */
netif_rx(mirror);  /* 미러 포트로 전달 */

/* skb_dst_force: qdisc 큐잉 전 dst 참조 고정 */
/* RCU read-side에서 noref로 설정된 dst는 RCU 밖에서 무효화될 수 있음 */
skb_dst_force(skb);
__skb_queue_tail(&qdisc->q, skb);

/* skb_orphan: 소켓 메모리 즉시 반환이 필요한 경우 */
/* 예: TCP 전송 경로에서 qdisc 큐에 오래 대기하면 */
/*     sk_wmem_alloc이 sk_sndbuf에 도달하여 전송 차단 */
skb_orphan(skb);
/* → destructor(sock_wfree) 호출 → sk_wmem_alloc 감소 */
/* → 소켓이 새 데이터를 write 할 수 있게 됨 */
⚠️

주의: skb_get()skb_clone()과 다릅니다. skb_get()같은 sk_buff 구조체의 참조 카운트만 올리므로, 메타데이터(mark, priority 등)를 변경하면 양쪽 모두 영향받습니다. 독립적인 메타데이터가 필요하면 skb_clone()을 사용하세요. 또한 skb_dst_set() 후에는 반드시 대응하는 skb_dst_drop()이 호출되어야 dst 참조 누수가 발생하지 않습니다.

해시/메타데이터 조작 함수

skb에 연결된 패킷 해시, 큐 매핑, 리다이렉트 플래그 등 메타데이터를 조작하는 함수들입니다. RSS/RPS 스티어링, TC 분류, 멀티큐 NIC 큐 선택 등에 사용됩니다.

함수프로토타입설명
skb_get_hashu32 skb_get_hash(skb)패킷의 flow hash 반환. 캐시된 값이 있으면 재사용, 없으면 flow dissector로 계산
skb_set_hashvoid skb_set_hash(skb, hash, type)해시 값과 타입(PKT_HASH_TYPE_L3, PKT_HASH_TYPE_L4) 설정. 드라이버에서 RSS 해시 전달
skb_clear_hashvoid skb_clear_hash(skb)캐시된 해시 무효화. 패킷 내용이 변경되어 재계산이 필요할 때
skb_get_queue_mappingu16 skb_get_queue_mapping(skb)멀티큐 NIC의 TX 큐 인덱스 조회
skb_set_queue_mappingvoid skb_set_queue_mapping(skb, mapping)TX 큐 인덱스 설정. TC mqprio/XPS에서 사용
skb_record_rx_queuevoid skb_record_rx_queue(skb, rx_queue)수신 큐 인덱스 기록. RPS 등에서 활용
skb_set_redirectedvoid skb_set_redirected(skb, from_ingress)TC/XDP 리다이렉트 표시. 패킷이 리다이렉트된 것임을 마킹
skb_clear_redirectedvoid skb_clear_redirected(skb)리다이렉트 표시 해제
/* 드라이버에서 HW RSS 해시 설정 */
static void my_rx_handler(struct sk_buff *skb, u32 hw_hash)
{
    skb_set_hash(skb, hw_hash, PKT_HASH_TYPE_L4);
    skb_record_rx_queue(skb, rx_ring->queue_idx);
    napi_gro_receive(&rx_ring->napi, skb);
}

/* NAT 후 해시 무효화 (IP/포트가 변경됨) */
skb_clear_hash(skb);
/* 다음 skb_get_hash() 호출 시 flow dissector가 재계산 */

/* TC에서 특정 TX 큐로 스티어링 */
skb_set_queue_mapping(skb, target_queue);

/* XDP 리다이렉트 후 마킹 */
skb_set_redirected(skb, true);  /* from_ingress = true */

sk_buff 리스트/큐 관리

struct sk_buff_head는 spinlock을 내장한 이중 연결 리스트로, 소켓 수신/전송 큐, qdisc 큐, TCP 재전송 큐 등에서 skb를 관리하는 데 사용됩니다. lock이 필요한 함수(spinlock 기반)와 lock 없는 함수(__ 접두사)가 쌍으로 존재합니다.

/* sk_buff_head: 이중 연결 리스트의 head (sentinel) */
struct sk_buff_head {
    struct sk_buff *next;
    struct sk_buff *prev;
    __u32           qlen;   /* 큐 내 skb 수 */
    spinlock_t      lock;   /* 동시성 보호 */
};

초기화 및 추가/제거

함수lock설명
skb_queue_head_init(list)큐 초기화 (next=prev=head, qlen=0, spinlock 초기화)
skb_queue_head(list, skb)O에 추가 (LIFO). lock 획득/해제 포함
__skb_queue_head(list, skb)X큐 앞에 추가. 호출자가 lock을 이미 보유해야 함
skb_queue_tail(list, skb)O에 추가 (FIFO). 가장 일반적 패턴
__skb_queue_tail(list, skb)X큐 끝에 추가. lock 없이
skb_dequeue(list)O큐 앞에서 skb 제거 및 반환. 빈 큐면 NULL
__skb_dequeue(list)Xlock 없이 큐 앞에서 제거
skb_dequeue_tail(list)O에서 skb 제거 및 반환
__skb_dequeue_tail(list)Xlock 없이 큐 뒤에서 제거
skb_append(old, new, list)Oold 뒤에 new 삽입
skb_insert(new, old, list)Oold 앞에 new 삽입
skb_unlink(skb, list)O큐에서 특정 skb 제거 (위치 무관)
__skb_unlink(skb, list)Xlock 없이 큐에서 제거

조회 및 순회

함수/매크로설명
skb_queue_empty(list)큐가 비어있는지. list->next == (struct sk_buff *)list
skb_queue_len(list)큐의 skb 수 (list->qlen)
skb_peek(list)큐 첫 번째 skb 반환 (제거 안 함). NULL이면 빈 큐
skb_peek_tail(list)큐 마지막 skb 반환 (제거 안 함)
skb_queue_is_last(list, skb)skb가 큐의 마지막인지
skb_queue_is_first(list, skb)skb가 큐의 첫 번째인지
skb_queue_next(list, skb)다음 skb. is_last이면 BUG
skb_queue_prev(list, skb)이전 skb. is_first이면 BUG
skb_queue_walk(list, skb)큐 전체 순회 매크로. 순회 중 제거 불가
skb_queue_walk_safe(list, skb, tmp)큐 순회 + 안전한 제거 가능 (tmp에 next 저장)
skb_queue_walk_from(list, skb)특정 skb부터 끝까지 순회
skb_queue_walk_from_safe(list, skb, tmp)특정 위치부터 안전한 순회

일괄 처리 (splice/purge)

함수lock설명
skb_queue_purge(list)O큐의 모든 skb를 kfree_skb()로 해제. 큐 비우기
__skb_queue_purge(list)Xlock 없이 큐 비우기
skb_queue_splice(from, to)Xfrom 큐의 모든 skb를 to 큐 앞에 합침. from은 비워짐
skb_queue_splice_tail(from, to)Xfrom 큐를 to 큐 에 합침
skb_queue_splice_init(from, to)Xsplice + from 큐 재초기화
skb_queue_splice_tail_init(from, to)Xsplice_tail + from 큐 재초기화
/* 초기화 */
struct sk_buff_head my_queue;
skb_queue_head_init(&my_queue);

/* 기본 enqueue/dequeue (FIFO) */
skb_queue_tail(&my_queue, skb);
struct sk_buff *s = skb_dequeue(&my_queue);

/* 큐 전체 순회 + 조건부 제거 */
struct sk_buff *skb, *tmp;
spin_lock(&my_queue.lock);
skb_queue_walk_safe(&my_queue, skb, tmp) {
    if (should_drop(skb)) {
        __skb_unlink(skb, &my_queue);
        kfree_skb(skb);
    }
}
spin_unlock(&my_queue.lock);

/* splice: NAPI poll에서 per-CPU 큐를 소켓 큐로 이동 */
struct sk_buff_head process_queue;
skb_queue_head_init(&process_queue);
spin_lock(&napi_queue.lock);
skb_queue_splice_tail_init(&napi_queue, &process_queue);
spin_unlock(&napi_queue.lock);
/* 이제 lock 없이 process_queue를 처리 */
while ((skb = __skb_dequeue(&process_queue)) != NULL)
    netif_receive_skb(skb);

/* 큐 비우기 (모듈 언로드, 소켓 닫기 시) */
skb_queue_purge(&my_queue);
⚠️

주의: __ 접두사 함수(lockless)는 호출자가 이미 lock을 보유하거나, 동시성이 보장되는 컨텍스트(예: NAPI poll 단독 실행)에서만 사용하세요. skb_queue_walk()로 순회 중에 skb를 제거하면 리스트가 손상됩니다 — 반드시 skb_queue_walk_safe()를 사용하세요. skb_queue_purge()는 모든 skb에 kfree_skb()를 호출하므로 드롭 tracepoint가 발생합니다. 정상 정리에서는 개별 consume_skb()를 고려하세요.

sk_buff_head: 이중 연결 원형 리스트 큐 sk_buff_head qlen = 3 spinlock_t lock (sentinel node) skb₁ next / prev skb₂ next / prev skb₃ next / prev next prev next (실선) prev (점선) — enqueue: tail에 추가 (FIFO) / dequeue: head에서 제거
sk_buff_head는 sentinel 노드를 가진 이중 연결 원형 리스트. next는 head→tail 방향, prev는 역방향. qlen이 큐 길이를 추적하며, spinlock으로 동시성을 보호

소켓과 sk_buff의 관계

sk_buff의 sk 필드는 해당 패킷이 소속된 소켓(struct sock)을 가리킵니다. 이 연결은 소켓 메모리 계산, 소켓 옵션 적용, 프로토콜별 처리의 기반이 됩니다.

struct sock 계층 구조

커널 소켓은 3단계 계층으로 구성됩니다:

소켓 구조체 계층과 sk_buff의 관계 struct socket BSD 소켓 인터페이스 file, ops, sk 포인터 struct sock (sk) 프로토콜 무관 공통 계층 sk_receive_queue, sk_write_queue sk_rmem_alloc, sk_wmem_alloc struct tcp_sock / udp_sock 프로토콜별 확장 inet_sock ⊃ sock 내장 sk struct sk_buff skb->sk → sock skb->destructor skb->truesize skb->sk sk_receive_queue sk_receive_queue (RX) 수신 skb 대기열 sk_write_queue (TX) 전송 skb 대기열 sk_backlog (overflow) 소켓 lock 중 수신 대기 sk_error_queue ICMP 에러, MSG_ERRQUEUE
struct socket → struct sock → 프로토콜별 sock. sk_buff는 sk->sk_receive_queue 등에 연결되며 skb->sk로 소켓을 역참조(Dereference)
/* 소켓 구조체 계층 (간략) */
struct socket {              /* BSD 소켓 (사용자 공간 인터페이스) */
    socket_state             state;    /* SS_UNCONNECTED, SS_CONNECTED 등 */
    struct file             *file;     /* VFS file (fd와 연결) */
    struct sock             *sk;       /* 네트워크 레이어 소켓 */
    const struct proto_ops  *ops;      /* connect, sendmsg 등 */
};

struct sock {                /* 프로토콜 무관 공통 소켓 */
    struct sk_buff_head  sk_receive_queue; /* 수신 skb 큐 */
    struct sk_buff_head  sk_write_queue;   /* 전송 skb 큐 */
    struct sk_buff_head  sk_error_queue;   /* 에러 큐 (ICMP 등) */
    struct {
        struct sk_buff *head, *tail;
    }                    sk_backlog;       /* backlog 큐 (lock 중 수신) */

    atomic_t             sk_rmem_alloc;    /* 수신 큐 메모리 사용량 */
    atomic_t             sk_wmem_alloc;    /* 전송 큐 메모리 사용량 */
    int                  sk_rcvbuf;        /* SO_RCVBUF 값 */
    int                  sk_sndbuf;        /* SO_SNDBUF 값 */
    unsigned long        sk_flags;         /* 소켓 플래그 */
    struct proto        *sk_prot;          /* 프로토콜 핸들러 */
    void                (*sk_data_ready)(struct sock *sk);  /* 수신 알림 */
    /* ... */
};

/* 프로토콜별 확장 (임베디드 패턴) */
struct inet_sock {           /* IPv4/IPv6 공통 */
    struct sock      sk;           /* 공통 sock 내장 */
    __be32           inet_saddr;   /* 소스 IP */
    __be32           inet_daddr;   /* 목적지 IP */
    __be16           inet_sport;   /* 소스 포트 */
    __be16           inet_dport;   /* 목적지 포트 */
    __u8             tos;          /* IP_TOS 옵션 */
    __u8             min_ttl;      /* IP_MINTTL 옵션 */
    __s16            uc_ttl;       /* IP_TTL 옵션 (-1 = 기본값) */
    struct ip_options_rcu *inet_opt; /* IP 옵션 */
    /* ... */
};

struct tcp_sock {
    struct inet_connection_sock inet_conn; /* inet_sock ⊃ sock 내장 */
    u32  snd_una;      /* 가장 오래된 미확인 시퀀스 */
    u32  snd_nxt;      /* 다음 전송 시퀀스 */
    u32  rcv_nxt;      /* 다음 수신 기대 시퀀스 */
    u32  mss_cache;    /* MSS (최대 세그먼트 크기) */
    /* ... 수십 개 TCP 전용 필드 ... */
};

struct udp_sock {
    struct inet_sock  inet;
    int               pending;      /* cork 상태 */
    __u8              encap_type;   /* UDP encap (VXLAN 등) */
    /* ... */
};

/* 캐스팅 매크로 */
#define inet_sk(sk)  ((struct inet_sock *)(sk))
#define tcp_sk(sk)   ((struct tcp_sock *)(sk))
#define udp_sk(sk)   ((struct udp_sock *)(sk))

skb↔sk 바인딩과 소켓 메모리 관리(Memory Management)

skb->sk가 설정되면 해당 skb의 메모리 사용량이 소켓에 과금됩니다. 이 메커니즘이 SO_RCVBUF/SO_SNDBUF 제한을 실현합니다:

/* skb를 소켓에 연결 — 메모리 과금 시작 */
static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
    skb->sk = sk;
    skb->destructor = sock_rfree;  /* 해제 시 콜백 */
    atomic_add(skb->truesize, &sk->sk_rmem_alloc);
    /* → 수신 큐 메모리 사용량에 truesize만큼 추가 */
}

static inline void skb_set_owner_w(struct sk_buff *skb, struct sock *sk)
{
    skb->sk = sk;
    skb->destructor = sock_wfree;  /* 해제 시 콜백 */
    refcount_add(skb->truesize, &sk->sk_wmem_alloc);
    /* → 전송 큐 메모리 사용량에 truesize만큼 추가 */
}

/* skb 해제 시: destructor 콜백이 메모리 차감 */
void sock_rfree(struct sk_buff *skb)
{
    struct sock *sk = skb->sk;
    atomic_sub(skb->truesize, &sk->sk_rmem_alloc);
    /* → 수신 큐 메모리 사용량에서 차감 */
}

/* 수신 큐 과부하 확인 — 소켓 버퍼 제한 */
static inline bool sk_rmem_schedule(struct sock *sk, struct sk_buff *skb, int size)
{
    /* sk_rmem_alloc + size > sk_rcvbuf 이면 false → 패킷 드롭 */
    return __sk_mem_schedule(sk, size, SK_MEM_RECV);
}
필드/콜백(Callback)방향역할
sk_rmem_allocRX수신 큐에 쌓인 skb의 총 truesize (SO_RCVBUF와 비교)
sk_wmem_allocTX전송 중인 skb의 총 truesize (SO_SNDBUF와 비교)
sock_rfreeRXskb 해제 시 sk_rmem_alloc 차감
sock_wfreeTXskb 해제 시 sk_wmem_alloc 차감, 전송 대기 프로세스 wakeup
skb_orphan(skb)양방향skb↔sk 연결 해제 (destructor 호출 후 sk=NULL)
ℹ️

skb->truesize는 skb 구조체 크기 + 할당된 데이터 버퍼 크기를 합산한 값입니다. 소켓의 메모리 추적은 이 값 기반이므로, truesize가 실제와 어긋나면 SO_RCVBUF 제한이 오작동합니다. 너무 작으면 메모리 과다 사용, 너무 크면 조기 패킷 드롭이 발생합니다.

sk 참조 카운트(Reference Count)와 수명주기

skb->sk가 가리키는 소켓이 skb보다 먼저 해제되면 Use-After-Free가 발생합니다. 이를 방지하기 위해 커널은 참조 카운트 기반 수명주기 관리를 합니다. struct socksk_refcnt가 0이 되어야 소켓이 실제로 해제됩니다.

sk 참조 카운트 수명주기 sk_alloc() sk_refcnt = 1 sock_hold(sk) refcount_inc(&sk_refcnt) sock_put(sk) refcount_dec → 0? sk_free(sk) 소켓 해제 참조 카운트 변화 타임라인 sk_alloc 1 skb1 연결 (sock_hold) 2 skb2 연결 (sock_hold) 3 close(fd) (sock_put) 2 skb1 해제 (sock_put) 1 skb2 해제 (sock_put→0) 0 sk_free! 주요 참조 보유자 (Reference Holders) sock_hold() 호출 위치 - skb_set_owner_w/r (skb 연결) - sk_clone_lock (TCP accept) - inet_csk_clone_lock - 타이머 콜백 (keepalive 등) - BPF sk_storage - netfilter conntrack sock_put() 호출 위치 - skb->destructor 콜백 - inet_sock_destruct - close() → sock_release - TCP TIME_WAIT 만료 - sk_free_unlock_clone - 타이머 완료 콜백 주의: refcnt 불일치 시 - sock_put 과다 → Use-After-Free - sock_hold 누락 → 조기 해제 - sock_put 누락 → 메모리 누수 - close() 후 skb가 sk 참조 → destructor에서 sock_put → refcnt=0 → sk_free 실행
sk_refcnt는 소켓의 모든 참조자가 해제될 때까지 소켓을 유지. close(fd) 이후에도 전송 중인 skb가 sk를 참조하면 소켓은 살아있음
/* include/net/sock.h — sk 참조 카운트 API */

/* 소켓 할당: sk_refcnt = 1로 시작 */
struct sock *sk_alloc(struct net *net, int family,
                      gfp_t priority, struct proto *prot, int kern)
{
    struct sock *sk = sk_prot_alloc(prot, priority, family);
    if (sk) {
        refcount_set(&sk->sk_refcnt, 1);
        /* net 참조, 프로토콜 핸들러 설정 등 */
        sock_net_set(sk, get_net(net));
        sk->sk_prot = prot;
    }
    return sk;
}

/* 참조 카운트 증가 — sk를 장기 보유할 때 호출 */
static inline void sock_hold(struct sock *sk)
{
    refcount_inc(&sk->sk_refcnt);
}

/* 참조 카운트 감소 — 0이면 sk_free() 호출 */
static inline void sock_put(struct sock *sk)
{
    if (refcount_dec_and_test(&sk->sk_refcnt))
        sk_free(sk);
}

/* 소켓 해제 체인 */
void sk_free(struct sock *sk)
{
    /* 1. sk->sk_destruct 콜백 호출 (프로토콜별 정리) */
    if (sk->sk_destruct)
        sk->sk_destruct(sk);

    /* 2. 필터(BPF) 해제 */
    sk_filter_uncharge(sk, sk->sk_filter);

    /* 3. net namespace 참조 해제 */
    put_net(sock_net(sk));

    /* 4. 프로토콜별 slab에서 메모리 반환 */
    sk_prot_free(sk->sk_prot, sk);
}

/* inet_sock_destruct: AF_INET 소켓의 sk_destruct 콜백 */
void inet_sock_destruct(struct sock *sk)
{
    /* 수신/전송/에러 큐의 잔여 skb 전부 해제 */
    __skb_queue_purge(&sk->sk_receive_queue);
    __skb_queue_purge(&sk->sk_write_queue);
    __skb_queue_purge(&sk->sk_error_queue);

    /* 메모리 추적 검증: 모든 skb가 해제된 후 0이어야 함 */
    WARN_ON(atomic_read(&sk->sk_rmem_alloc));
    WARN_ON(refcount_read(&sk->sk_wmem_alloc));
    WARN_ON(sk->sk_forward_alloc);

    /* IP 옵션, cork 등 해제 */
    kfree(rcu_dereference_protected(
        inet_sk(sk)->inet_opt, 1));
    /* dst_cache 해제 */
    dst_release(rcu_dereference_protected(
        sk->sk_dst_cache, 1));
}
⚠️

close(fd) ≠ sk_free: 사용자 공간에서 close(fd)를 호출해도 소켓이 즉시 해제되지 않습니다. TCP의 경우 TIME_WAIT 상태에서 수십 초간 유지되고, 전송 큐에 미전송 skb가 남아있으면 그 skb들의 destructor가 호출되어 sock_put()을 수행할 때까지 소켓이 살아있습니다. /proc/net/sockstat의 "orphan" 카운트가 이런 소켓을 보여줍니다.

skb_orphan과 소유권(Ownership) 전이

skb_orphan()은 skb와 소켓의 연결을 끊어 소유권을 해제합니다. 이 함수는 네트워크 스택의 여러 전환점에서 호출되며, 소켓 메모리 회계와 소켓 수명주기에 직접 영향을 미칩니다.

skb_orphan() 동작과 호출 시점 skb_orphan(skb) 내부 동작 1. skb->destructor(skb) sock_wfree/sock_rfree 호출 2. sk_wmem/rmem_alloc 차감 → 소켓 메모리 예산 회복 3. skb->sk = NULL skb->destructor = NULL 주요 호출 시점 qdisc 인큐(Enqueue) __dev_queue_xmit() 내부 → skb_orphan_first=1 시 → 소켓 메모리 조기 반환 IP 포워딩(Forwarding) ip_forward() → ip_forward_finish() → 로컬 소켓과 무관해지므로 → skb_orphan() 필수 Netfilter / NAT nf_nat_setup_info() → SNAT/DNAT 후 원본 sk 무효 → skb_orphan() 호출 TCP 재전송(Retransmit) tcp_retransmit_skb() → skb clone 후 원본 orphan → clone에 새로운 owner 설정 소켓 close 후 전송 close() → tcp_close() → 잔여 skb orphan 처리 → sk는 orphan socket이 됨 tun/tap, veth 전달 tun_net_xmit() → 가상 디바이스 경계 통과 시 → skb_orphan() 호출 skb_orphan(): 완전 해제 destructor 호출 → sk=NULL → 메모리 즉시 반환 skb_orphan_partial(): 부분 해제 TX: destructor=sock_efree로 교체 → sk 유지, wmem만 차감
skb_orphan()은 skb와 소켓의 연결을 끊어 소켓 메모리를 조기 반환. qdisc, 포워딩, NAT 등 경계 전환점에서 호출됨
/* include/linux/skbuff.h — skb_orphan */
static inline void skb_orphan(struct sk_buff *skb)
{
    /* destructor가 있으면 호출 → 소켓 메모리 회계 정리 */
    if (skb->destructor) {
        skb->destructor(skb);  /* sock_wfree/sock_rfree 등 */
        skb->destructor = NULL;
    }
    skb->sk = NULL;            /* 소켓 참조 해제 */
}

/* skb_orphan_partial: TX 경로에서 wmem만 반환, sk 참조 유지 */
void skb_orphan_partial(struct sk_buff *skb)
{
    if (skb_is_tcp_pure_ack(skb))
        return;  /* pure ACK은 건드리지 않음 */

    if (skb->destructor) {
        /* 현재 destructor 호출 → wmem_alloc 차감 */
        skb->destructor(skb);
        /* sock_efree로 교체 → sk만 유지 (wmem 0) */
        skb->destructor = sock_efree;
        sock_hold(skb->sk);  /* sk 참조 유지 (refcnt++) */
    }
}

/* sock_efree: sk 참조만 해제 (메모리 회계 없음) */
void sock_efree(struct sk_buff *skb)
{
    sock_put(skb->sk);  /* refcnt-- → 0이면 sk_free */
}

/* 실제 사용 예: qdisc enqueue 시 orphan */
static inline int __dev_xmit_skb(struct sk_buff *skb,
                                   struct Qdisc *q, ...)
{
    /* sysctl net.core.skb_orphan_first = 1일 때 */
    if (READ_ONCE(netdev_budget_usecs))
        skb_orphan_partial(skb);
    /* → qdisc에 진입하면서 소켓 메모리 예산 즉시 반환
     *   이후 NIC 전송 완료까지 skb는 sk 없이(또는 efree 참조만으로) 존재 */
    return q->enqueue(skb, q, ...);
}

/* IP 포워딩 시 orphan */
int ip_forward(struct sk_buff *skb)
{
    /* 포워딩 패킷은 로컬 소켓과 무관 */
    skb_orphan(skb);  /* 완전 orphan */
    /* ... TTL 감소, checksum 재계산, 라우팅 ... */
    return ip_forward_finish(net, sk, skb);
}
💡

성능 영향: skb_orphan_partial()이 qdisc 진입 시점에 호출되면, 소켓의 sk_wmem_alloc이 조기에 감소하여 sendmsg()-EAGAIN 없이 더 많은 데이터를 보낼 수 있습니다. 대역폭이 높은 전송 시 처리량이 개선되지만, qdisc 큐가 비정상적으로 커지면 메모리 사용량이 늘 수 있습니다. tc qdisc show로 큐 길이를 모니터링하세요.

skb->destructor 콜백(Callback) 상세

skb->destructor는 skb가 해제될 때 호출되는 콜백 함수 포인터로, 소켓 메모리 회계의 핵심입니다. 커널은 용도별로 다양한 destructor를 사용합니다:

skb->destructor 콜백 종류와 동작 skb->destructor kfree_skb() 시 호출 sock_rfree sk_rmem_alloc -= truesize 설정: skb_set_owner_r() 용도: 수신 경로 (RX) sock_wfree sk_wmem_alloc -= truesize + sk_write_space() wakeup 용도: 일반 전송 (TX) tcp_wfree sk_wmem_alloc -= truesize + TSQ (TCP Small Queue) 제어 용도: TCP 전송 sock_efree sock_put(sk) — refcnt만 메모리 회계 없음 용도: orphan_partial 후 sock_rmem_free sk_rmem_alloc -= truesize + sock_put(sk) 용도: 에러 큐 skb sock_edemux sock_put(sk) GRO/Early Demux에서 사용 용도: 수신 최적화 sctp_wfree SCTP association 메모리 추적 + 번들링 wakeup 용도: SCTP 전송 unix_destruct_scm SCM_RIGHTS fd 전달 정리 fput() 호출 용도: Unix domain socket 핵심: destructor는 kfree_skb() → __kfree_skb() → skb_release_all() → skb_release_head_state() 경로에서 호출됨 destructor가 NULL이면 소켓 메모리 추적 없음 (skb_orphan 후, 또는 드라이버 직접 할당 skb)
skb->destructor는 skb 해제 시 소켓 메모리 회계를 정리하는 콜백. 용도에 따라 다른 함수가 설정됨
/* net/core/sock.c — sock_wfree (전송 경로 destructor) */
void sock_wfree(struct sk_buff *skb)
{
    struct sock *sk = skb->sk;

    /* wmem_alloc에서 truesize 차감 */
    if (refcount_sub_and_test(skb->truesize, &sk->sk_wmem_alloc)) {
        /* wmem_alloc이 0이 되면 → 마지막 전송 skb 해제
         * → 대기 중인 송신자 wakeup 가능 */
    }

    /* 쓰기 공간이 확보되었음을 알림 */
    if (sock_flag(sk, SOCK_USE_WRITE_QUEUE)) {
        /* TCP: sk_write_space → tcp_write_space → epollout 트리거 */
    } else {
        sk->sk_write_space(sk);
    }
    sock_put(sk);  /* sk 참조 카운트 감소 */
}

/* net/ipv4/tcp_output.c — tcp_wfree (TCP 전용 destructor) */
void tcp_wfree(struct sk_buff *skb)
{
    struct sock *sk = skb->sk;
    struct tcp_sock *tp = tcp_sk(sk);
    unsigned long flags, nval, oval;

    /* TSQ (TCP Small Queue) 제어:
     * NIC 전송 완료 → tcp_wfree → 추가 세그먼트 전송 허용
     * → 지연시간 감소 (qdisc 큐에 과도한 패킷 축적 방지) */
    tcp_tsq_write(sk);

    /* wmem_alloc 차감 */
    refcount_sub(skb->truesize - 1, &sk->sk_wmem_alloc);
    sk->sk_tsq_flags |= TSQF_THROTTLED;
    sock_put(sk);
}

/* kfree_skb 호출 체인에서 destructor가 호출되는 위치 */
void __kfree_skb(struct sk_buff *skb)
{
    skb_release_all(skb);
    /* → skb_release_head_state(skb)
     *   → if (skb->destructor)
     *        skb->destructor(skb);   ← 여기서 호출!
     * → skb_release_data(skb)
     * → kfree_skbmem(skb) */
}
destructor설정 함수회계 동작추가 동작
sock_rfreeskb_set_owner_r()sk_rmem_alloc -= truesize없음
sock_wfreeskb_set_owner_w()sk_wmem_alloc -= truesizesk_write_space() wakeup
tcp_wfreetcp_skb_set_owner_w()sk_wmem_alloc -= truesizeTSQ tasklet → 추가 세그먼트 전송
sock_efreeskb_orphan_partial()없음 (회계 제외)sock_put(sk) — refcnt만 관리
sock_edemuxEarly Demux 경로없음sock_put(sk) — GRO/early demux 최적화
sock_rmem_free에러 큐 등sk_rmem_alloc -= truesizesock_put(sk)
unix_destruct_scmUnix SCM_RIGHTSsk_wmem_alloc -= truesizefput() — fd 참조 해제
sctp_wfreeSCTP 전송asoc→sndbuf_used 차감SCTP 전송 wakeup

sk_forward_alloc과 메모리 예산(Memory Budget)

매 skb마다 __sk_mem_schedule()를 호출하면 atomic 연산 비용이 높습니다. 커널은 sk_forward_alloc이라는 선급 예산을 두어 소켓별로 한 번에 큰 단위로 메모리를 할당받고, skb 단위에서는 이 예산에서 차감하는 방식으로 최적화합니다.

sk_forward_alloc 메모리 예산 모델 proto.memory_allocated 전역 프로토콜 메모리 (TCP/UDP 전체 합산) 단위: 페이지(PAGE_SIZE) sk->sk_forward_alloc 소켓별 선급 예산 SK_MEM_QUANTUM 단위 (= PAGE_SIZE) skb->truesize 개별 skb 메모리 사용량 sk_forward_alloc에서 차감 (atomic 연산 불필요) 충전 차감 __sk_mem_schedule() 플로우 sk_forward_alloc >= size? YES → 즉시 차감 NO → 충전 시도 proto.memory_allocated 충전 성공 압력(Pressure)/거부 프로토콜 메모리 임계값 (sysctl tcp_mem) low: 정상 (제한 없음) pressure: 압력 상태 (할당 제한) high: 신규 할당 거부
sk_forward_alloc은 소켓별 선급 예산. 전역 proto.memory_allocated에서 PAGE_SIZE 단위로 충전하고, skb 단위에서는 local 차감만 수행
/* net/core/sock.c — __sk_mem_schedule */
int __sk_mem_schedule(struct sock *sk, int size, int kind)
{
    int amt = sk_mem_pages(size);
    /* size를 페이지 단위로 올림:
     * sk_mem_pages(size) = (size + PAGE_SIZE - 1) >> PAGE_SHIFT */

    sk->sk_forward_alloc += amt * SK_MEM_QUANTUM;
    /* SK_MEM_QUANTUM = PAGE_SIZE = 4096 (일반적)
     * → 한 번 충전에 최소 4KB 예산 확보 */

    /* 전역 프로토콜 메모리에서 차감 */
    atomic_long_add(amt, sk->sk_prot->memory_allocated);

    /* 메모리 압력 상태 확인 */
    if (mem_cgroup_sockets_enabled && sk->sk_memcg) {
        /* cgroup v2: cgroup 단위 메모리 제한도 적용 */
        if (!mem_cgroup_charge_skmem(sk->sk_memcg, amt,
                                     sk->sk_prot->memory_pressure))
            goto suppress;
    }

    /* tcp_mem[0] (low), tcp_mem[1] (pressure), tcp_mem[2] (high) */
    if (atomic_long_read(sk->sk_prot->memory_allocated) >
        sk->sk_prot->sysctl_mem[2]) {
        /* high 초과 → 거부 (새 skb 할당 실패) */
        sk->sk_prot->enter_memory_pressure(sk);
        goto suppress;
    }
    return 1;  /* 성공 */

suppress:
    /* 예산 롤백 */
    sk->sk_forward_alloc -= amt * SK_MEM_QUANTUM;
    atomic_long_sub(amt, sk->sk_prot->memory_allocated);
    return 0;  /* 실패 → 호출자가 패킷 드롭 */
}

/* sk_mem_reclaim: 잔여 예산을 전역 풀에 반환 */
void sk_mem_reclaim(struct sock *sk)
{
    if (sk->sk_forward_alloc >= SK_MEM_QUANTUM) {
        /* 사용하지 않는 예산을 전역에 반환 → 다른 소켓이 사용 */
        __sk_mem_reclaim(sk, sk->sk_forward_alloc - 1);
    }
}

/* 수신 경로에서의 예산 소비 예시 */
static int tcp_try_rmem_schedule(struct sock *sk, struct sk_buff *skb,
                                unsigned int size)
{
    if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf) {
        /* SO_RCVBUF 초과 → OOM 경로 */
        tcp_try_coalesce(sk, skb, ...);  /* 병합 시도 */
        if (...)
            goto drop;
    }
    if (!sk_rmem_schedule(sk, skb, size)) {
        /* sk_forward_alloc 부족 + __sk_mem_schedule 실패
         * → 메모리 압력 상태: pruning (OOO 큐 정리) 시도 */
        tcp_prune_ofo_queue(sk);
        if (!sk_rmem_schedule(sk, skb, size))
            goto drop;
    }
    return 0;
drop:
    /* → TCPRcvQDrop 카운터 증가 → /proc/net/snmp */
    return -1;
}
ℹ️

sk_forward_allocinet_sock_destruct()에서 0인지 검증됩니다. 0이 아니면 WARN_ON이 발생하며, 이는 메모리 예산 계산 버그를 의미합니다. 커스텀 프로토콜 핸들러를 작성할 때 sk_mem_charge()/sk_mem_uncharge() 호출 쌍이 정확히 일치해야 합니다.

소켓 메모리 압력(Memory Pressure) 시스템

커널은 프로토콜별로 전역 메모리 압력 상태를 관리합니다. TCP, UDP 등 각 프로토콜은 memory_allocatedsysctl_mem[] 임계값으로 시스템 전체의 소켓 메모리를 제어합니다.

소켓 메모리 압력(Memory Pressure) 시스템 sysctl tcp_mem (페이지 단위) tcp_mem[0] = low (예: 191232) tcp_mem[1] = pressure (254976) tcp_mem[2] = high (382464) 메모리 압력 상태 머신(State Machine) 정상(Normal) allocated < low → 제한 없이 할당 압력(Pressure) allocated > pressure → 할당 신중, OOO 정리 거부(Reject) allocated > high → 신규 할당 거부 회복 회복 압력 상태의 영향 TCP 압력 반응 - tcp_prune_ofo_queue(): OOO 정리 - tcp_collapse(): skb 병합/압축 - 수신 윈도우 축소 (rwnd) - TSO 세그먼트 크기 제한 - sk_forward_alloc 회수 UDP 압력 반응 - 수신 패킷 드롭 (무조건) - UdpRcvbufErrors++ (SNMP) - rcvbuf/sndbuf 사이즈 축소 - cork 해제 강제 - memcg 초과 시 추가 제한 모니터링 /proc/net/sockstat: TCP: mem = allocated pages /proc/sys/net/ipv4/tcp_mem: low pressure high cat /proc/net/snmp | grep Tcp
프로토콜별 메모리 압력 시스템: tcp_mem 3단계 임계값으로 전역 소켓 메모리를 관리. 압력 시 TCP는 OOO 정리와 skb 병합, UDP는 즉시 드롭
/* include/net/sock.h — 프로토콜별 메모리 관리 구조체 */
struct proto {
    /* ... */
    atomic_long_t   *memory_allocated;   /* 프로토콜 전체 할당 페이지 수 */
    int             *memory_pressure;    /* 압력 상태 플래그 (0 or 1) */
    long            *sysctl_mem;         /* [low, pressure, high] 배열 */
    int             *sysctl_rmem;        /* [min, default, max] 수신 */
    int             *sysctl_wmem;        /* [min, default, max] 전송 */
    /* ... */
};

/* TCP 프로토콜의 메모리 관련 전역 변수 */
atomic_long_t tcp_memory_allocated;  /* TCP 전체 메모리 (페이지 단위) */
int tcp_memory_pressure;              /* TCP 압력 플래그 */
long sysctl_tcp_mem[3];              /* 부팅 시 자동 계산 */

/* tcp_init_mem: tcp_mem 자동 계산 (부팅 시) */
void tcp_init_mem(void)
{
    unsigned long limit = nr_free_buffer_pages() / 8;
    limit = max(limit, 128UL);
    sysctl_tcp_mem[0] = limit * 3 / 4;   /* low: 전체의 3/32 */
    sysctl_tcp_mem[1] = limit;             /* pressure: 전체의 1/8 */
    sysctl_tcp_mem[2] = limit * 3 / 2;   /* high: 전체의 3/16 */
}

/* sk_under_memory_pressure: 프로토콜 압력 확인 */
static inline bool sk_under_memory_pressure(const struct sock *sk)
{
    /* cgroup 메모리 제한도 고려 */
    if (mem_cgroup_sockets_enabled && sk->sk_memcg &&
        mem_cgroup_under_socket_pressure(sk->sk_memcg))
        return true;
    return !!*sk->sk_prot->memory_pressure;
}

/* tcp_enter_memory_pressure: 압력 상태 진입 */
void tcp_enter_memory_pressure(struct sock *sk)
{
    unsigned long val;

    if (READ_ONCE(tcp_memory_pressure))
        return;
    WRITE_ONCE(tcp_memory_pressure, 1);
    /* → /proc/net/sockstat의 "TCP: ... pressure 1"로 확인 가능 */
}

/* tcp_leave_memory_pressure: 압력 상태 해제 */
void tcp_leave_memory_pressure(struct sock *sk)
{
    unsigned long val;

    if (!READ_ONCE(tcp_memory_pressure))
        return;
    if (atomic_long_read(&tcp_memory_allocated) <
        sysctl_tcp_mem[0])
        WRITE_ONCE(tcp_memory_pressure, 0);
}
# 소켓 메모리 압력 상태 모니터링

# TCP 메모리 사용량과 압력 상태 확인
cat /proc/net/sockstat
# sockets: used 1024
# TCP: inuse 80 orphan 2 tw 15 alloc 85 mem 1520   ← mem = 페이지
# UDP: inuse 12 mem 3
# → mem * PAGE_SIZE = 실제 바이트 (1520 * 4096 ≈ 6.2MB)

# TCP 메모리 임계값 확인/조정 (페이지 단위)
cat /proc/sys/net/ipv4/tcp_mem
# 191232  254976  382464   ← low  pressure  high
# → 750MB  1000MB  1500MB (4KB 페이지 기준)

# UDP 메모리 임계값
cat /proc/sys/net/ipv4/udp_mem
# 191232  254976  382464

# 소켓별 메모리 사용량 확인 (ss)
ss -tm
# ESTAB  0  0  192.168.1.1:80  10.0.0.1:12345
#   skmem:(r0,rb1048576,t0,tb2626560,f0,w0,o0,bl0,d0)
#   r=rmem_alloc  rb=rcvbuf  t=wmem_alloc  tb=sndbuf
#   f=forward_alloc  w=wmem_queued  o=opt_mem  bl=backlog  d=drop

# TCP 압력 상태 실시간 추적
watch -n1 'grep "TCP:" /proc/net/sockstat'
⚠️

cgroup v2 메모리 제한: cgroup v2의 memory.max가 소켓 메모리에도 적용됩니다(CONFIG_MEMCG). sk->sk_memcg가 설정되면 __sk_mem_schedule()mem_cgroup_charge_skmem()을 호출하여 cgroup 단위 제한도 검사합니다. 컨테이너 환경에서 tcp_mem이 충분해도 cgroup 제한으로 패킷이 드롭될 수 있습니다. memory.statsock 항목으로 확인하세요.

소켓 잠금(Lock)과 backlog 처리

소켓의 수신 큐에 skb를 넣으려면 소켓 잠금이 필요합니다. 사용자 프로세스가 recvmsg() 중이라 소켓을 잠그고 있을 때, softirq에서 도착한 패킷은 backlog 큐에 임시 저장됩니다. release_sock() 시점에 backlog이 한꺼번에 처리됩니다.

소켓 잠금과 backlog 처리 메커니즘 프로세스 컨텍스트 (recvmsg) softirq 컨텍스트 (NAPI RX) lock_sock(sk) sk->sk_lock.owned=1 skb_recv_datagram() sk_receive_queue에서 skb dequeue release_sock(sk) __release_sock(sk) — backlog 소진 while (skb = sk->sk_backlog.head) { sk->sk_backlog_rcv(sk, skb); // TCP: tcp_v4_do_rcv() } tcp_v4_rcv(skb) sock_owned_by_user(sk)? sk->sk_lock.owned 확인 YES: backlog 추가 __sk_add_backlog(sk, skb) NO: 직접 수신 큐 YES NO backlog 제한 초과 시 sk_rcvqueues_full(sk) → true → skb 드롭 sk_backlog.len > sk_rcvbuf + sk_sndbuf backlog 소진 성능 영향 backlog이 클수록 release_sock() 지연 증가 → recvmsg() 반환 시간 불예측
lock_sock/release_sock 사이에 softirq가 수신한 패킷은 backlog에 쌓이고, release_sock() 시점에 일괄 처리됨
/* include/net/sock.h — 소켓 잠금 관련 구조와 API */
struct sock {
    /* socket_lock_t sk_lock: 소켓 잠금 */
    struct {
        spinlock_t  slock;     /* softirq 보호용 스핀록 */
        int         owned;     /* 1 = 프로세스가 소유 중 */
        wait_queue_head_t wq;  /* lock 대기 큐 */
    } sk_lock;

    /* backlog 큐 */
    struct {
        atomic_t         rmem_alloc;  /* backlog skb 메모리 */
        int              len;         /* backlog 총 길이 */
        struct sk_buff  *head, *tail; /* backlog 리스트 */
    } sk_backlog;

    /* backlog 처리 콜백 (프로토콜별) */
    int (*sk_backlog_rcv)(struct sock *sk, struct sk_buff *skb);
    /* TCP: tcp_v4_do_rcv
     * UDP: __udp_queue_rcv_skb */
};

/* lock_sock: 프로세스 컨텍스트에서 소켓 잠금 */
void lock_sock(struct sock *sk)
{
    lock_sock_nested(sk, 0);
    /* 1. spin_lock_bh(&sk->sk_lock.slock)  — BH 비활성화
     * 2. sk->sk_lock.owned = 1           — 소유 표시
     * 3. spin_unlock_bh(&sk->sk_lock.slock)
     * → 이후 softirq는 owned=1을 보고 backlog 사용 */
}

/* release_sock: 소켓 잠금 해제 + backlog 처리 */
void release_sock(struct sock *sk)
{
    spin_lock_bh(&sk->sk_lock.slock);
    if (sk->sk_backlog.tail) {
        __release_sock(sk);  /* backlog 소진 */
    }
    sk->sk_lock.owned = 0;
    if (waitqueue_active(&sk->sk_lock.wq))
        wake_up(&sk->sk_lock.wq);
    spin_unlock_bh(&sk->sk_lock.slock);
}

/* __release_sock: backlog 큐의 모든 skb 처리 */
static void __release_sock(struct sock *sk)
{
    struct sk_buff *skb, *next;

    while ((skb = sk->sk_backlog.head) != NULL) {
        sk->sk_backlog.head = sk->sk_backlog.tail = NULL;
        spin_unlock_bh(&sk->sk_lock.slock);

        do {
            next = skb->next;
            skb->next = NULL;
            /* 프로토콜별 수신 처리 */
            sk->sk_backlog_rcv(sk, skb);
            /* TCP: tcp_v4_do_rcv() → ACK 처리, 데이터 큐잉 */
            skb = next;
        } while (skb != NULL);

        spin_lock_bh(&sk->sk_lock.slock);
        /* 처리 중 새 backlog이 쌓였을 수 있음 → while 반복 */
    }
}

/* __sk_add_backlog: softirq에서 backlog에 skb 추가 */
static inline int __sk_add_backlog(struct sock *sk, struct sk_buff *skb)
{
    /* backlog 크기 제한 확인 */
    if (sk->sk_backlog.len >= sk_rcvqueues_full(sk))
        return -ENOBUFS;  /* → 패킷 드롭 */

    /* 리스트에 추가 */
    if (!sk->sk_backlog.tail)
        sk->sk_backlog.head = skb;
    else
        sk->sk_backlog.tail->next = skb;
    sk->sk_backlog.tail = skb;
    sk->sk_backlog.len += skb->truesize;
    return 0;
}
💡

bh_lock_sock vs lock_sock: lock_sock()은 프로세스 컨텍스트 전용이고 sleep 가능합니다. bh_lock_sock(sk)은 softirq에서 사용하며 spin_lock(&sk->sk_lock.slock)만 잡고, owned를 확인해 backlog으로 분기합니다. 두 잠금이 다른 것은 프로세스가 오래 소켓을 잡고 있어도 softirq가 블록되지 않게 하기 위한 설계입니다.

TCP 소켓 버퍼 자동 튜닝(Autotuning)

사용자가 SO_RCVBUF를 명시적으로 설정하지 않으면, TCP는 네트워크 조건에 따라 수신 버퍼를 자동으로 조절합니다. 이를 autotuning이라 하며 tcp_moderate_rcvbuf sysctl로 제어합니다.

TCP 수신 버퍼 자동 튜닝 (Autotuning) sysctl 설정 tcp_moderate_rcvbuf = 1 tcp_rmem = [min default max] [4096 131072 6291456] tcp_wmem = [min default max] tcp_rcv_space_adjust() 수신 처리량 측정: - 시간당 수신 바이트 - RTT × BDP 기반 → sk_rcvbuf 동적 조절 결과 sk_rcvbuf: default → max 범위: [4096, 6291456] BDP에 비례하여 증가 과부하 시 축소 setsockopt(SO_RCVBUF) 설정 시 autotuning 비활성화 사용자가 SO_RCVBUF를 명시하면 SOCK_RCVBUF_LOCK 플래그 설정 → 커널의 자동 조절이 중단됨 → 고정 크기 유지 autotuning 흐름: 데이터 수신 → 처리량 측정 → 버퍼 크기 조절 tcp_rcv_established() tcp_rcv_space_adjust() BDP 계산 sk_rcvbuf 갱신 rwnd 광고
TCP autotuning: BDP(Bandwidth-Delay Product)를 측정하여 sk_rcvbuf를 tcp_rmem[default]에서 tcp_rmem[max]까지 동적 조절
/* net/ipv4/tcp_input.c — tcp_rcv_space_adjust */
void tcp_rcv_space_adjust(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    u32 time, space;

    /* SOCK_RCVBUF_LOCK: 사용자가 SO_RCVBUF 설정 → autotuning 중단 */
    if (sock_flag(sk, SOCK_RCVBUF_LOCK))
        return;

    /* 최근 수신 처리량 측정 (bytes/time) */
    time = tcp_jiffies32 - tp->rcvq_space.time;
    if (time < (tp->rcv_rtt_est.rtt_us >> 3) || tp->rcv_rtt_est.rtt_us == 0)
        return;

    /* 수신 공간 필요량 계산:
     * space = 수신 처리량 × 2 (BDP의 2배 여유) */
    space = tp->rcvq_space.space * 2;

    /* sk_rcvbuf 상한: tcp_rmem[2] (기본 6MB) */
    space = min(space, tcp_space_from_win(tp,
                READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_rmem[2])));

    if (space > sk->sk_rcvbuf) {
        /* 수신 버퍼 확장 */
        WRITE_ONCE(sk->sk_rcvbuf, space);
        /* → 더 큰 수신 윈도우를 상대에게 광고
         * → throughput 향상 (특히 고지연 네트워크) */
    }

    tp->rcvq_space.space = tp->copied_seq - tp->rcvq_space.seq;
    tp->rcvq_space.seq = tp->copied_seq;
    tp->rcvq_space.time = tcp_jiffies32;
}

/* 전송 측 autotuning: sk_sndbuf도 유사하게 동적 조절 */
/* tcp_sndbuf_expand() — TCP pacing과 연동 */
void tcp_sndbuf_expand(struct sock *sk)
{
    if (sock_flag(sk, SOCK_SNDBUF_LOCK))
        return;  /* SO_SNDBUF 설정 시 중단 */

    /* sk_pacing_rate 기반 전송 버퍼 크기 계산 */
    int sndmem = SKB_TRUESIZE(tp->mss_cache) *
                max_t(int, tp->snd_cwnd, tp->reordering + 1);
    sndmem *= 2;  /* 여유 계수 */

    if (sndmem > sk->sk_sndbuf)
        WRITE_ONCE(sk->sk_sndbuf,
                   min(sndmem,
                       READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_wmem[2])));
}
# TCP 자동 튜닝 관련 sysctl

# 자동 튜닝 활성화/비활성화 (기본값: 1)
sysctl net.ipv4.tcp_moderate_rcvbuf
# 1 = 자동 튜닝 활성 (권장)
# 0 = 비활성 → tcp_rmem[1](default)로 고정

# TCP 수신 버퍼 범위 (바이트 단위)
sysctl net.ipv4.tcp_rmem
# 4096  131072  6291456
#  ↑min   ↑default  ↑max (autotuning 상한)
# min: 메모리 압력 시 최소 보장
# default: 새 소켓 초기값
# max: autotuning이 확장 가능한 최대값

# TCP 전송 버퍼 범위 (바이트 단위)
sysctl net.ipv4.tcp_wmem
# 4096  16384  4194304

# 특정 소켓의 autotuning 상태 확인
ss -tmi dst 10.0.0.1
# skmem:(r4096,rb1048576,...,f262144,...)
# rb = sk_rcvbuf (autotuning 결과 값)
# f  = sk_forward_alloc (잔여 예산)

# 고대역폭 장거리 네트워크 (10Gbps, 50ms RTT)에서 최적 설정
# BDP = 10Gbps × 50ms = 62.5MB → tcp_rmem max를 이 이상으로
sysctl -w net.ipv4.tcp_rmem="4096 131072 67108864"   # max 64MB
sysctl -w net.ipv4.tcp_wmem="4096 16384 67108864"    # max 64MB
⚠️

SO_RCVBUF 함정: 애플리케이션이 setsockopt(SO_RCVBUF, ...)을 호출하면 SOCK_RCVBUF_LOCK 플래그가 설정되어 autotuning이 영구 비활성화됩니다. 많은 성능 가이드가 SO_RCVBUF를 크게 설정하라고 권장하지만, 실제로는 커널 autotuning을 방해하여 오히려 성능이 저하될 수 있습니다. tcp_rmem의 max 값을 높이고 SO_RCVBUF는 설정하지 않는 것이 일반적으로 최선입니다.

sk_clone_lock — TCP accept 시 소켓 복제(Clone)

TCP 서버가 accept()를 호출하면, 리스닝(listening) 소켓의 설정을 상속받은 새 소켓이 생성됩니다. 커널은 sk_clone_lock()으로 리스닝 소켓을 복제하여 연결별 소켓을 만듭니다.

/* net/core/sock.c — sk_clone_lock */
struct sock *sk_clone_lock(const struct sock *sk, const gfp_t priority)
{
    struct proto *prot = READ_ONCE(sk->sk_prot);
    struct sock *newsk;

    /* 프로토콜 slab에서 새 소켓 할당 */
    newsk = sk_prot_alloc(prot, priority, sk->sk_family);
    if (!newsk)
        return NULL;

    /* 원본 소켓 전체를 메모리 복사 (shallow copy) */
    sock_copy(newsk, sk);
    /* → sk_rcvbuf, sk_sndbuf, sk_priority, sk_mark 등 상속 */

    /* 복제된 소켓의 고유 필드 초기화 */
    refcount_set(&newsk->sk_refcnt, 2);
    /* refcnt=2: 1(소켓 자체) + 1(caller 참조) */

    /* 큐 초기화 (원본의 큐를 공유하면 안 됨) */
    skb_queue_head_init(&newsk->sk_receive_queue);
    skb_queue_head_init(&newsk->sk_write_queue);
    skb_queue_head_init(&newsk->sk_error_queue);

    /* 메모리 회계 초기화 */
    atomic_set(&newsk->sk_rmem_alloc, 0);
    refcount_set(&newsk->sk_wmem_alloc, 1);
    /* wmem_alloc=1: "sentinel" — 0이 되기 전까지 해제 방지
     * → sock_put() 호출 시 __sk_free 대신 대기 */

    newsk->sk_forward_alloc = 0;
    newsk->sk_backlog.head = newsk->sk_backlog.tail = NULL;
    newsk->sk_backlog.len = 0;

    /* BPF 필터 상속 */
    if (sk->sk_filter) {
        sk_filter_charge(newsk, sk->sk_filter);
        /* BPF 프로그램 참조 카운트 증가 */
    }

    /* net namespace 참조 */
    get_net(sock_net(newsk));

    /* dst_cache (라우팅 캐시) 초기화 */
    sk_dst_reset(newsk);

    /* 잠금은 caller가 해제 */
    bh_lock_sock(newsk);
    return newsk;
}

/* TCP accept 경로 요약:
 * inet_csk_accept()
 *   → reqsk_queue_remove() — SYN 큐에서 완성된 연결 가져옴
 *   → newsk = listen_sk→clone(listen_sk)
 *     → inet_csk_clone_lock()
 *       → sk_clone_lock()
 *       → inet_sk_set_state(newsk, TCP_SYN_RECV → TCP_ESTABLISHED)
 *   → 새 소켓의 fd 생성 → 사용자 공간 반환 */

/* TCP 3-way handshake에서 소켓 복제 시점 */
/*
 * [Client]          [Server listen_sk]
 *   SYN →           → tcp_conn_request()
 *                     → request_sock 할당 (경량)
 *   ← SYN+ACK       ← tcp_v4_send_synack()
 *   ACK →           → tcp_check_req()
 *                     → tcp_v4_syn_recv_sock()
 *                       → sk_clone_lock(listen_sk)  ← 여기서 복제!
 *                       → inet_csk_clone_lock()
 *                       → tcp_create_openreq_child()
 *                       → 새 소켓 완성
 *                     → inet_csk_complete_hashdance()
 *   accept() →       → inet_csk_accept()
 *                       → 완성된 소켓 반환
 */
필드listen_sk 값newsk (복제 후)설명
sk_rcvbuf원본 값상속SO_RCVBUF 설정 또는 autotuning 초기값
sk_sndbuf원본 값상속SO_SNDBUF 설정 또는 autotuning 초기값
sk_prioritySO_PRIORITY상속skb→priority에 복사됨
sk_markSO_MARK상속skb→mark에 복사됨 (netfilter/tc)
sk_filterBPF 프로그램참조 공유SO_ATTACH_FILTER/BPF → 동일 필터 적용
sk_bound_dev_ifSO_BINDTODEVICE상속특정 인터페이스 바인딩
sk_receive_queue리스닝 큐빈 큐 (초기화)연결별 독립 수신 큐
sk_rmem_alloc원본 값0 (초기화)새 연결은 메모리 사용량 0에서 시작
sk_wmem_alloc원본 값1 (sentinel)sentinel 값으로 조기 해제 방지
sk_forward_alloc원본 값0 (초기화)새 연결은 예산 없이 시작
sk_refcnt원본 값2소켓 + caller 참조
ℹ️

wmem_alloc=1 sentinel: 복제된 소켓의 sk_wmem_alloc이 1로 설정되는 이유는, 전송 skb의 destructorsock_wfree()에서 refcount_sub_and_test(truesize, &sk_wmem_alloc)을 수행할 때 0에 도달하는 것을 방지하기 위함입니다. 실제 해제는 sk_free()에서 refcount_sub(1, &sk_wmem_alloc)으로 sentinel을 제거한 후 진행됩니다.

소켓 BPF 필터와 skb

사용자 공간에서 SO_ATTACH_FILTER 또는 SO_ATTACH_BPF로 부착한 BPF 프로그램은 skb가 소켓 수신 큐에 들어가기 전에 실행됩니다. 이 필터는 sk->sk_filter에 저장되며, sk_filter_trim_cap()에서 호출됩니다:

/* include/linux/filter.h — sk_filter 구조 */
struct sk_filter {
    refcount_t          refcnt;     /* 참조 카운트 (clone 시 공유) */
    struct rcu_head     rcu;
    struct bpf_prog    *prog;      /* JIT 컴파일된 BPF 프로그램 */
};

/* net/core/filter.c — sk_filter_trim_cap */
int sk_filter_trim_cap(struct sock *sk, struct sk_buff *skb,
                      unsigned int cap)
{
    struct sk_filter *filter;
    int err;

    /* security_sock_rcv_skb: LSM (SELinux 등) 검사 */
    err = security_sock_rcv_skb(sk, skb);
    if (err)
        return err;

    /* BPF 필터 실행 */
    filter = rcu_dereference(sk->sk_filter);
    if (filter) {
        struct sock *save_sk = skb->sk;
        unsigned int pkt_len;

        skb->sk = sk;  /* BPF가 소켓 정보 접근할 수 있게 */
        pkt_len = bpf_prog_run_save_cb(filter->prog, skb);
        skb->sk = save_sk;

        /* pkt_len=0 → 패킷 드롭
         * pkt_len>0 → skb->len을 pkt_len으로 트림(trim) */
        err = pkt_len ? pskb_trim(skb, max(cap, pkt_len)) : -EPERM;
    }
    return err;
}

/* 호출 위치 예시 */
/* TCP: tcp_v4_do_rcv() → tcp_filter() → sk_filter_trim_cap()
 * UDP: __udp_queue_rcv_skb() → sk_filter_trim_cap()
 * Raw: raw_rcv() → raw_rcv_skb() → sk_filter_trim_cap() */

/* 사용자 공간에서 필터 부착 */
struct sock_fprog bpf = {
    .len = ARRAY_SIZE(code),
    .filter = code,
};
setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));
/* → sock_attach_prog() → sk->sk_filter 설정
 * → 이후 모든 수신 skb가 이 필터를 통과해야 소켓 큐 진입 */

소켓 옵션(setsockopt)과 sk_buff

사용자 공간(User Space)의 setsockopt() 호출은 struct sock 필드를 변경하고, 이것이 skb 생성·처리에 직접 반영됩니다:

소켓 옵션레벨sock/skb 영향
SO_RCVBUFSOL_SOCKETsk->sk_rcvbuf 설정 → 수신 큐 skb 총량 제한
SO_SNDBUFSOL_SOCKETsk->sk_sndbuf 설정 → 전송 큐 skb 총량 제한
SO_MARKSOL_SOCKETsk->sk_markskb->mark로 복사 (netfilter/tc/라우팅)
SO_PRIORITYSOL_SOCKETsk->sk_priorityskb->priority로 복사 (QoS)
SO_BINDTODEVICESOL_SOCKETsk->sk_bound_dev_if → skb의 dev 제한
SO_TIMESTAMPSOL_SOCKET수신 skb에 타임스탬프 기록, recvmsg() cmsg로 전달
SO_BUSY_POLLSOL_SOCKETsk->sk_napi_id + skb->napi_id로 busy polling
IP_TOSSOL_IPinet->tos → 전송 skb IP 헤더 TOS 필드
IP_TTLSOL_IPinet->uc_ttl → 전송 skb IP 헤더 TTL 필드
IP_HDRINCLSOL_IPraw socket: 사용자가 IP 헤더를 직접 제공
TCP_NODELAYSOL_TCPNagle 알고리즘 해제 → 소량 데이터 즉시 skb 전송
TCP_CORKSOL_TCPskb 전송 지연 (cork), uncork 시 한번에 전송
UDP_CORKSOL_UDP여러 sendmsg를 하나의 skb로 합쳐 전송
UDP_GROSOL_UDP수신 UDP GRO 활성화 → 여러 패킷이 하나의 큰 skb로
/* 전송 경로에서 sock 옵션 → skb 필드 복사 과정 */
static void ip_copy_addrs(struct iphdr *iph, const struct flowi4 *fl4)
{
    /* flowi4는 routing lookup 입력: sock의 IP/포트에서 구성 */
    iph->saddr = fl4->saddr;
    iph->daddr = fl4->daddr;
}

/* ip_queue_xmit: TCP 전송 시 sock 옵션 적용 */
int __ip_queue_xmit(struct sock *sk, struct sk_buff *skb, ...)
{
    struct inet_sock *inet = inet_sk(sk);

    /* SK 옵션 → skb 필드 전파 */
    skb->priority = sk->sk_priority;    /* SO_PRIORITY */
    skb->mark = sk->sk_mark;            /* SO_MARK */

    /* IP 헤더 필드: inet_sock에서 가져옴 */
    iph->tos = inet->tos;               /* IP_TOS */
    iph->ttl = ip_select_ttl(inet, ...); /* IP_TTL 또는 기본값 */
    /* ... */
}

/* SO_RCVBUF 설정과 수신 큐 제한의 관계 */
/* 사용자 공간 */
int bufsize = 262144;  /* 256KB */
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
/* 커널: sk->sk_rcvbuf = min(bufsize * 2, sysctl_rmem_max)
 *       → 실제 커널 값은 요청값의 2배 (overhead 고려)
 *
 * 수신 시: atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf 이면
 *          → 새 패킷 드롭 (ENOMEM) */
⚠️

SO_RCVBUF/SO_SNDBUF에 설정한 값은 커널 내에서 2배로 증폭됩니다 (sock_setsockopt() 내부). 이는 skb 구조체와 메타데이터 오버헤드(Overhead)를 고려한 것입니다. getsockopt()으로 읽으면 2배된 값이 반환됩니다. 시스템 전역 상한은 /proc/sys/net/core/rmem_max, wmem_max입니다.

Raw Socket과 sk_buff

Raw socket(SOCK_RAW)은 프로토콜 스택의 일부를 우회하여 직접 패킷을 구성하거나 수신합니다. 일반 SOCK_STREAM/SOCK_DGRAM과 달리 커널의 L4 프로토콜 처리를 거치지 않고 skb를 직접 다루므로, 네트워크 도구(ping, traceroute, tcpdump, nmap 등)와 프로토콜 구현의 핵심입니다.

Raw Socket 계층별 접근 범위 사용자 공간 (User Space) Socket Layer — socket(), sendmsg(), recvmsg() L4: TCP/UDP (SOCK_STREAM/DGRAM은 여기서 처리) L3: IP Layer — ip_rcv(), ip_output() L2: Ethernet / Device Driver — dev_queue_xmit(), netif_receive_skb() AF_INET SOCK_RAW AF_PACKET SOCK_RAW

Raw Socket 타입 비교

타입생성접근 계층skb 관계
IP raw socket socket(AF_INET, SOCK_RAW, IPPROTO_XXX) L3 (IP) IP_HDRINCL 없으면 커널이 IP 헤더 생성. 수신 시 IP 헤더 포함
IP raw + IP_HDRINCL setsockopt(IP_HDRINCL, 1) L3 (IP) 사용자가 IP 헤더까지 직접 작성. skb->data가 IP 헤더부터 시작
Packet socket (L2 raw) socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) L2 (Ethernet) Ethernet 헤더 포함 전체 프레임 접근. skb를 MAC 헤더부터 수신/전송
Packet socket (L2 cooked) socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)) L3 (IP) L2 헤더 제거 후 전달. 송신 시 커널이 L2 헤더 생성
AF_PACKET + TPACKET setsockopt(PACKET_VERSION, TPACKET_V3) L2 (Ethernet) mmap 기반 ring buffer → skb 없이 직접 DMA 버퍼 접근 (고성능)
Ping socket socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP) L4 (ICMP) CAP_NET_RAW 불필요. 커널이 ICMP 헤더 id/checksum 관리

Raw Socket 권한 모델

Raw socket 생성에는 CAP_NET_RAW capability가 필요합니다. 커널은 sock_create()inet_create() 경로에서 capability를 검사합니다:

/* net/ipv4/af_inet.c — inet_create() */
static int inet_create(struct net *net, struct socket *sock,
                       int protocol, int kern)
{
    struct inet_protosw *answer;
    struct sock *sk;

    /* SOCK_RAW 사용 시 CAP_NET_RAW 검사 */
    if (sock->type == SOCK_RAW && !kern &&
        !ns_capable(net->user_ns, CAP_NET_RAW))
        return -EPERM;

    /* protocol 번호로 inetsw[] 해시 테이블에서 프로토콜 핸들러 검색 */
    answer = inet_protosw_lookup(sock->type, protocol);
    /* SOCK_RAW → raw_prot (net/ipv4/raw.c)
     * SOCK_STREAM → tcp_prot
     * SOCK_DGRAM → udp_prot */

    sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer->prot, kern);
    /* ... */
}

/* net/packet/af_packet.c — AF_PACKET도 CAP_NET_RAW 필요 */
static int packet_create(struct net *net, struct socket *sock,
                        int protocol, int kern)
{
    if (!kern && !ns_capable(net->user_ns, CAP_NET_RAW))
        return -EPERM;
    /* ... */
}
ℹ️

Ping socket 예외: Linux 3.0+에서 도입된 ping socket(socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP))은 CAP_NET_RAW 없이도 ICMP Echo Request를 보낼 수 있습니다. /proc/sys/net/ipv4/ping_group_range로 허용 GID 범위를 설정합니다. setcap cap_net_raw+ep /usr/bin/ping 대신 이 메커니즘을 사용합니다.

커널 내부 자료구조 — struct raw_sock

/* include/net/raw.h */
struct raw_sock {
    struct inet_sock  inet;       /* inet_sock 상속 (→ sock → sock_common) */
    struct icmp_filter filter;    /* ICMP 타입별 필터 비트맵 */
    u32               ipmr_table; /* 멀티캐스트 라우팅 테이블 ID */
};

/* raw socket 프로토콜 해시 테이블
 * protocol 번호로 해싱하여 수신 시 O(1) 조회 */
struct raw_hashinfo {
    spinlock_t           lock;
    struct hlist_head    ht[RAW_HTABLE_SIZE]; /* 256 버킷 */
};

/* 전역 raw 해시 테이블 — 모든 AF_INET SOCK_RAW 소켓 관리 */
struct raw_hashinfo raw_v4_hashinfo;  /* IPv4 */
struct raw_hashinfo raw_v6_hashinfo;  /* IPv6 */

/* 해시 함수: protocol 번호를 버킷 인덱스로 변환 */
static inline u32 raw_hashfunc(const struct net *net, u32 proto)
{
    return proto & (RAW_HTABLE_SIZE - 1); /* 0~255 */
}

/* raw socket의 프로토콜 연산 테이블 */
struct proto raw_prot = {
    .name       = "RAW",
    .owner      = THIS_MODULE,
    .close      = raw_close,
    .connect    = ip4_datagram_connect,
    .sendmsg    = raw_sendmsg,
    .recvmsg    = raw_recvmsg,
    .bind       = raw_bind,
    .hash       = raw_hash_sk,
    .unhash     = raw_unhash_sk,
    .obj_size   = sizeof(struct raw_sock),
};

AF_INET SOCK_RAW 수신 경로

IP 계층에서 패킷이 로컬로 배달될 때, TCP/UDP 디먹싱 이전에 raw socket으로의 복제가 먼저 수행됩니다. 즉, raw socket은 패킷의 사본을 받으며, 원본 skb는 정상 프로토콜 스택으로 계속 진행합니다:

Raw Socket 수신 경로 (IPv4) ip_local_deliver() ip_local_deliver_finish() 분기 ① Raw Socket 경로 (먼저 실행) raw_local_deliver() raw_v4_hashinfo [protocol] 조회 raw_v4_input() sk_for_each(): 매칭 소켓 순회 skb_clone(skb, GFP_ATOMIC) 데이터 공유 (zero-copy clone) 원본 skb 유지 사본 → raw sock raw_rcv() xfrm4_policy_check() sock_queue_rcv_skb() sk_rmem_alloc 체크 Raw Socket 수신 큐 recvfrom()으로 IP 헤더 포함 수신 ② 프로토콜 핸들러 경로 (이후 실행) ipprot->handler(skb) inet_protos[protocol] tcp_v4_rcv() udp_rcv() icmp_rcv() 정상 소켓 수신 큐 핵심 포인트 • raw socket은 항상 IP 헤더 포함 수신 • skb_clone()은 데이터 공유 (zero-copy) • 원본 skb는 프로토콜 핸들러로 정상 전달 • CAP_NET_RAW 권한 필요 (비특권 차단) 실행 순서: ① raw_local_deliver(skb, protocol) → ② ipprot->handler(skb) (순차 실행, 동시 아님)
/* net/ipv4/ip_input.c — ip_local_deliver_finish()
 * 패킷이 로컬 배달될 때 raw socket에 먼저 전달 */
static int ip_local_deliver_finish(struct net *net, struct sock *sk,
                                  struct sk_buff *skb)
{
    __skb_pull(skb, skb_network_header_len(skb));

    int protocol = ip_hdr(skb)->protocol;
    const struct net_protocol *ipprot;

    /* ① raw socket이 있으면 먼저 skb 사본 전달 */
    raw_local_deliver(skb, protocol);

    /* ② 등록된 프로토콜 핸들러 호출 (tcp_v4_rcv, udp_rcv 등) */
    ipprot = rcu_dereference(inet_protos[protocol]);
    if (ipprot) {
        ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv,
                              skb);
    }
}

/* net/ipv4/raw.c — raw_local_deliver()
 * 해당 프로토콜의 raw socket들을 해시 테이블에서 찾아 전달 */
int raw_local_deliver(struct sk_buff *skb, int protocol)
{
    struct raw_hashinfo *h = &raw_v4_hashinfo;
    struct hlist_head *head;
    int hash;

    hash = raw_hashfunc(dev_net(skb_dst(skb)->dev), protocol);
    head = &h->ht[hash];

    if (!hlist_empty(head)) {
        /* 매칭되는 모든 raw socket에 skb 복제본 전달 */
        raw_v4_input(skb, ip_hdr(skb), hash);
    }
    return 0;
}

/* net/ipv4/raw.c — raw_v4_input()
 * 프로토콜 번호와 목적지 주소가 매칭되는 모든 raw socket에 전달 */
static int raw_v4_input(struct sk_buff *skb, const struct iphdr *iph,
                       int hash)
{
    struct sock *sk;
    struct hlist_head *head = &raw_v4_hashinfo.ht[hash];
    int delivered = 0;

    rcu_read_lock();
    sk_for_each_rcu(sk, head) {
        /* 프로토콜 번호, 목적지 IP, 소스 IP, 네트워크 네임스페이스 매칭 */
        if (raw_v4_match(net, sk, iph->protocol,
                         iph->saddr, iph->daddr,
                         skb->dev->ifindex, sdif)) {
            /* skb를 clone하여 해당 raw socket에 전달 */
            raw_rcv(sk, skb);
            delivered++;
        }
    }
    rcu_read_unlock();
    return delivered;
}

/* net/ipv4/raw.c — raw_rcv()
 * skb clone → IP 헤더 포함한 상태로 수신 큐에 추가 */
int raw_rcv(struct sock *sk, struct sk_buff *skb)
{
    struct raw_sock *rp = raw_sk(sk);

    /* ICMP 필터 적용: 관심 없는 ICMP 타입은 드롭 */
    if (sk->sk_protocol == IPPROTO_ICMP) {
        struct icmphdr *icmph = icmp_hdr(skb);
        if (raw_icmp_type_filtered(rp, icmph->type))
            return 0; /* 필터에 의해 드롭 */
    }

    /* skb 복제: 원본은 프로토콜 스택이 계속 사용 */
    struct sk_buff *clone = skb_clone(skb, GFP_ATOMIC);
    if (!clone)
        return 0;

    /* 핵심: data 포인터를 network_header (IP 헤더) 위치로 복원
     * ip_local_deliver_finish()에서 __skb_pull로 L4까지 당겼으므로
     * raw socket은 IP 헤더부터 보여줘야 함 */
    skb_push(clone, clone->data - skb_network_header(clone));

    /* 수신 큐에 추가 → recvmsg()로 사용자에게 전달 */
    if (sock_queue_rcv_skb(sk, clone) < 0)
        kfree_skb(clone);

    return 0;
}
💡

핵심 포인트: 동일 프로토콜 번호를 사용하는 여러 raw socket이 열려 있으면, 하나의 수신 패킷이 모든 매칭 소켓에 clone되어 전달됩니다. 예: 두 프로세스가 각각 IPPROTO_ICMP raw socket을 열면, ICMP 패킷 수신 시 두 프로세스 모두 사본을 받습니다. 이는 raw_v4_input()sk_for_each_rcu() 루프가 해시(Hash) 버킷의 모든 소켓을 순회하기 때문입니다.

AF_INET SOCK_RAW 전송 경로

Raw socket의 전송 경로는 IP_HDRINCL 옵션에 따라 두 가지로 분기됩니다:

/* net/ipv4/raw.c — raw_sendmsg()
 * 사용자 공간의 sendto()/sendmsg() → raw_sendmsg() */
static int raw_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
{
    struct inet_sock *inet = inet_sk(sk);
    struct flowi4 fl4;
    struct rtable *rt;
    int err;

    /* 목적지 주소 결정 */
    struct sockaddr_in *usin = (struct sockaddr_in *)msg->msg_name;
    __be32 daddr;

    if (usin) {
        daddr = usin->sin_addr.s_addr;
    } else {
        /* connect()로 미리 바인딩된 주소 사용 */
        daddr = inet->inet_daddr;
        if (!daddr)
            return -EDESTADDRREQ;
    }

    /* 라우팅 테이블 조회 */
    flowi4_init_output(&fl4, ...);
    rt = ip_route_output_flow(net, &fl4, sk);

    if (inet->hdrincl) {
        /* ── IP_HDRINCL 모드 ──
         * 사용자가 IP 헤더를 직접 작성
         * 커널은 최소한의 필드만 보정 */
        err = raw_send_hdrinc(sk, &fl4, msg, len, &rt, msg->msg_flags);
    } else {
        /* ── 일반 raw 모드 ──
         * 커널이 IP 헤더를 자동 생성
         * 사용자 데이터는 L4 페이로드로 취급 */
        err = ip_append_data(sk, &fl4, raw_getfrag,
                             msg, len, 0, &ipc, &rt, msg->msg_flags);
        if (!err) {
            err = ip_push_pending_frames(sk, &fl4);
            /* → ip_output() → dev_queue_xmit() */
        }
    }
    ip_rt_put(rt);
    return err;
}

IP_HDRINCL 상세 — 커널의 보정 동작

IP_HDRINCL을 설정하면 사용자가 IP 헤더를 직접 작성하지만, 커널은 안전성과 정확성을 위해 일부 필드를 자동 보정합니다:

/* net/ipv4/raw.c — raw_send_hdrinc()
 * IP_HDRINCL 모드의 실제 전송 처리 */
static int raw_send_hdrinc(struct sock *sk, struct flowi4 *fl4,
                          struct msghdr *msg, unsigned int len,
                          struct rtable **rtp, unsigned int flags)
{
    struct iphdr *iph;
    struct sk_buff *skb;
    unsigned int iphlen;

    /* skb 할당: IP 헤더 + 페이로드 크기 */
    skb = sock_alloc_send_skb(sk,
        len + LL_ALLOCATED_SPACE(rt->dst.dev), /* L2 headroom 확보 */
        flags & MSG_DONTWAIT, &err);
    if (!skb)
        return err;

    /* L2 헤더 공간 예약 */
    skb_reserve(skb, LL_RESERVED_SPACE(rt->dst.dev));
    skb->protocol = htons(ETH_P_IP);

    /* 사용자 데이터를 skb에 복사 (IP 헤더 포함) */
    skb_put(skb, len);
    skb->network_header = skb->data;
    skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, len);

    iph = ip_hdr(skb);

    /* ── 커널이 자동 보정하는 필드 ── */

    /* (1) tot_len: 0이면 커널이 skb->len으로 설정 */
    if (!iph->tot_len)
        iph->tot_len = htons(len);

    /* (2) saddr: 0이면 라우팅 결과의 소스 IP로 채움 */
    if (!iph->saddr)
        iph->saddr = fl4->saddr;

    /* (3) id: 0이면 커널이 고유 ID 할당 */
    if (!iph->id)
        ip_select_ident(net, skb, NULL);

    /* (4) check: 항상 커널이 재계산 (사용자 값 무시) */
    iph->check = 0;
    iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl);

    /* Netfilter OUTPUT 체인 통과 후 전송 */
    err = NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
                  net, sk, skb, NULL, rt->dst.dev,
                  dst_output);
    return err;
}
IP 헤더 필드사용자 제공 시0 또는 미설정 시
version사용자 값 사용사용자가 반드시 4로 설정해야 함
ihl사용자 값 사용사용자가 설정 (보통 5)
tos사용자 값 사용0 (기본 서비스)
tot_len사용자 값 사용커널이 skb->len으로 설정
id사용자 값 사용커널이 ip_select_ident()로 할당
frag_off사용자 값 사용0 (단편화(Fragmentation) 없음)
ttl사용자 값 사용사용자가 반드시 설정해야 함
protocol사용자 값 사용사용자가 반드시 설정해야 함
saddr사용자 값 사용 (스푸핑 가능)커널이 라우팅 테이블(Routing Table)에서 결정
daddr사용자 값 사용사용자가 반드시 설정해야 함
check무시 — 커널이 항상 재계산커널이 ip_fast_csum()으로 계산
⚠️

IP_HDRINCL과 IP Spoofing: IP_HDRINCL을 사용하면 saddr(소스 IP)를 임의로 설정할 수 있어 IP 스푸핑이 가능합니다. 이것이 CAP_NET_RAW가 필요한 주요 보안 이유 중 하나입니다. 네트워크 장비의 BCP 38 (uRPF) 필터링이나 커널의 rp_filter 설정으로 발신 스푸핑 패킷을 차단할 수 있습니다.

raw_recvmsg() — 사용자 공간으로 전달

/* net/ipv4/raw.c — raw_recvmsg()
 * 사용자의 recvfrom()/recvmsg() 처리 */
static int raw_recvmsg(struct sock *sk, struct msghdr *msg,
                      size_t len, int flags, int *addr_len)
{
    struct sk_buff *skb;
    struct sockaddr_in *sin = (struct sockaddr_in *)msg->msg_name;
    int err, copied;

    /* 수신 큐에서 skb 꺼내기 (대기 가능) */
    skb = skb_recv_datagram(sk, flags, &err);
    if (!skb)
        return err;

    /* skb에서 사용자 버퍼로 데이터 복사
     * → IP 헤더부터 전체 패킷이 사용자에게 전달됨 */
    copied = skb->len;
    if (len < copied) {
        msg->msg_flags |= MSG_TRUNC; /* 잘림 알림 */
        copied = len;
    }
    skb_copy_datagram_msg(skb, 0, msg, copied);

    /* 소스 주소 정보 채우기 */
    if (sin) {
        sin->sin_family = AF_INET;
        sin->sin_addr.s_addr = ip_hdr(skb)->saddr;
        sin->sin_port = 0; /* raw socket은 포트 개념 없음 */
    }

    /* IP_PKTINFO, IP_TTL 등 ancillary data (cmsg) 전달 */
    if (inet_cmsg_flags(inet))
        ip_cmsg_recv(msg, skb);

    skb_free_datagram(sk, skb);
    return copied;
}

ICMP 필터 (ICMP_FILTER)

IPPROTO_ICMP raw socket에서 관심 있는 ICMP 타입만 수신하도록 비트맵(Bitmap) 필터를 설정할 수 있습니다:

/* include/uapi/linux/icmp.h */
struct icmp_filter {
    __u32 data;  /* 비트맵: bit N이 1이면 ICMP type N을 필터링(드롭) */
};

/* 사용자 공간 예: Echo Reply(type 0)만 수신, 나머지 필터링 */
struct icmp_filter filt;
filt.data = ~(1 << ICMP_ECHOREPLY);  /* type 0만 통과 */
setsockopt(fd, SOL_RAW, ICMP_FILTER, &filt, sizeof(filt));

/* 커널 내부: raw_rcv()에서 필터 검사 */
static inline bool raw_icmp_type_filtered(const struct raw_sock *rp,
                                           u8 type)
{
    /* type에 해당하는 비트가 1이면 필터링(드롭) */
    return (rp->filter.data >> type) & 1;
}

IPv6 Raw Socket (AF_INET6 SOCK_RAW)

IPv6 raw socket은 IPv4와 유사하지만 중요한 차이점이 있습니다:

특성IPv4 (AF_INET)IPv6 (AF_INET6)
IP 헤더 접근 IP_HDRINCL로 IP 헤더 포함 가능 IPv6 헤더는 항상 커널이 생성 (IPV6_HDRINCL 미지원)
확장 헤더 IP 옵션을 IP_OPTIONS로 설정 IPV6_RTHDR, IPV6_HOPOPTS 등 ancillary data(cmsg)로 설정
ICMPv6 체크섬 사용자가 직접 계산 커널이 자동 계산 (RFC 3542 요구사항)
체크섬 오프셋 해당 없음 IPV6_CHECKSUM — 페이로드 내 체크섬 위치 지정, 커널이 계산
필터 ICMP_FILTER ICMPV6_FILTER — 256비트 비트맵 (struct icmp6_filter)
커널 소스 net/ipv4/raw.c net/ipv6/raw.c
/* IPv6 raw socket에서 ICMPv6 체크섬은 커널이 자동 계산 */
/* net/ipv6/raw.c — rawv6_send_hdrinc() 내부 */

/* IPV6_CHECKSUM 소켓 옵션: 체크섬 계산 위치 지정 */
int offset = 2;  /* 페이로드 시작부터 체크섬 필드의 바이트 오프셋 */
setsockopt(fd, IPPROTO_IPV6, IPV6_CHECKSUM, &offset, sizeof(offset));
/* 커널이 IPv6 pseudo-header 포함 체크섬을 해당 오프셋에 기록 */

/* ICMPv6 raw socket (protocol = IPPROTO_ICMPV6)은
 * IPV6_CHECKSUM이 자동으로 offset=2에 설정됨
 * → ICMPv6 체크섬 필드 위치가 헤더 시작+2바이트 */

/* ICMPv6 필터 예: Neighbor Solicitation만 수신 */
struct icmp6_filter filt;
ICMP6_FILTER_SETBLOCKALL(&filt);
ICMP6_FILTER_SETPASS(ND_NEIGHBOR_SOLICIT, &filt);
setsockopt(fd, IPPROTO_ICMPV6, ICMPV6_FILTER, &filt, sizeof(filt));

AF_PACKET — L2 프레임 접근

AF_PACKET 소켓은 Ethernet 프레임 수준에서 패킷을 캡처/전송합니다. tcpdump, wireshark, dhclient, arping 등이 사용합니다.

소켓 타입수신 시 포함 헤더전송 시 필요 헤더사용 사례
AF_PACKET, SOCK_RAW Ethernet + IP + L4 + 페이로드 사용자가 Ethernet 헤더 포함 전체 작성 tcpdump, 패킷 injection
AF_PACKET, SOCK_DGRAM IP + L4 + 페이로드 (Ethernet 제거) 커널이 Ethernet 헤더 생성 dhclient, 프로토콜 분석
/* net/packet/af_packet.c — packet_type 등록
 * AF_PACKET 소켓 생성 시 packet_type을 등록하여
 * NIC 드라이버의 수신 경로에 후킹 */
static int packet_create(struct net *net, struct socket *sock,
                        int protocol, int kern)
{
    struct packet_sock *po;
    struct sock *sk;

    sk = sk_alloc(net, PF_PACKET, GFP_KERNEL, &packet_proto, kern);
    po = pkt_sk(sk);

    /* packet_type 구조체 설정 */
    po->prot_hook.func = packet_rcv;       /* 수신 콜백 */
    po->prot_hook.af_packet_priv = sk;    /* 소켓 포인터 */

    if (protocol) {
        po->prot_hook.type = protocol;    /* ETH_P_ALL, ETH_P_IP 등 */
        __register_prot_hook(sk);
        /* → dev_add_pack() → ptype_all 또는 ptype_base[] 리스트에 등록
         * → netif_receive_skb() 경로에서 모든 수신 패킷에 대해 콜백 */
    }
}

/* 수신 콜백: netif_receive_skb() → deliver_skb() → packet_rcv() */
static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
                      struct packet_type *pt, struct net_device *orig_dev)
{
    struct sock *sk = pt->af_packet_priv;
    struct sk_buff *copy;
    unsigned int snaplen, res;

    /* BPF 필터 적용 (setsockopt SO_ATTACH_FILTER) */
    res = run_filter(skb, sk, snaplen);
    if (!res)
        goto drop;  /* BPF 필터에 의해 드롭 */

    /* ETH_P_ALL인 경우 모든 패킷에 대해 호출됨 */
    copy = skb_clone(skb, GFP_ATOMIC);
    if (!copy)
        goto drop;

    /* SOCK_RAW: MAC 헤더부터 전체 프레임 노출 */
    if (sk->sk_type == SOCK_RAW)
        skb_push(copy, skb_mac_header_len(skb));

    /* sockaddr_ll에 수신 메타데이터 기록 */
    struct sockaddr_ll *sll = &PACKET_SKB_CB(copy)->sa.ll;
    sll->sll_ifindex = orig_dev->ifindex;
    sll->sll_hatype = dev->type;
    sll->sll_pkttype = skb->pkt_type;  /* PACKET_HOST, PACKET_BROADCAST 등 */

    sock_queue_rcv_skb(sk, copy);
    return 0;
drop:
    kfree_skb(skb);
    return 0;
}

/* AF_PACKET 전송: 사용자 → dev_queue_xmit() 직접 전달 */
static int packet_sendmsg(struct socket *sock, struct msghdr *msg,
                         size_t len)
{
    /* SOCK_RAW: 사용자가 Ethernet 헤더 포함 전체 프레임 작성 */
    /* SOCK_DGRAM: sockaddr_ll에서 목적지 MAC, 커널이 Ethernet 헤더 생성 */

    struct sk_buff *skb = packet_alloc_skb(sk, ...);

    /* 사용자 데이터 복사 */
    skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, len);

    /* IP 스택을 완전히 우회하여 직접 디바이스 큐로 전송 */
    err = dev_queue_xmit(skb);
    /* → qdisc → NIC 드라이버 → 물리 전송 */
}

TPACKET — mmap 기반 고성능 캡처

TPACKET(PACKET_MMAP)은 커널-사용자 간 mmap된 공유 ring buffer를 사용하여 recvmsg()/sendmsg() 시스템콜 오버헤드 없이 패킷을 교환합니다. tcpdump, libpcap, suricata 등 고성능 캡처 도구의 핵심입니다.

버전커널특징제한/이슈
TPACKET_V1 2.4+ 기본 ring buffer, 고정 크기 프레임 32비트 타임스탬프, 큰 패킷 지원 불가
TPACKET_V2 2.6.27+ VLAN 태그 보존, 64비트 타임스탬프 여전히 고정 크기 프레임
TPACKET_V3 3.2+ 가변 크기 블록, 타임아웃 기반 블록 해제, 배치 처리 TX ring 미지원 (V2 사용), 구현 복잡
TPACKET_V3 Ring Buffer 구조 커널 공간 Block 0 TP_STATUS_KERNEL pkt 1 pkt 2 ← 커널이 쓰는 중 Block 1 TP_STATUS_USER pkt 3 pkt 4 → 사용자가 읽는 중 Block 2 (빈 블록) TP_STATUS_KERNEL Block N-1 TP_STATUS_KERNEL 사용자 공간 (mmap) mmap()으로 매핑된 동일 물리 메모리 → 복사 없이 직접 접근 (zero-copy 수신) poll()/ppoll()로 TP_STATUS_USER 블록 대기 → 시스콜 없이 블록 순회하며 패킷 읽기 처리 완료 후 TP_STATUS_KERNEL로 반환 → 커널이 다시 사용 가능 mmap
/* TPACKET_V3 ring buffer 설정 예 (사용자 공간) */
struct tpacket_req3 req = {
    .tp_block_size  = 1 << 22,     /* 4MB 블록 */
    .tp_block_nr    = 64,          /* 64개 블록 = 256MB */
    .tp_frame_size  = TPACKET_ALIGNMENT << 7, /* 프레임 정렬 */
    .tp_frame_nr    = (1 << 22) * 64 / (TPACKET_ALIGNMENT << 7),
    .tp_retire_blk_tov = 60,      /* 블록 타임아웃 60ms */
    .tp_feature_req_word = TP_FT_REQ_FILL_RXHASH,
};

/* TPACKET 버전 설정 */
int ver = TPACKET_V3;
setsockopt(fd, SOL_PACKET, PACKET_VERSION, &ver, sizeof(ver));

/* RX ring buffer 설정 */
setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &req, sizeof(req));

/* 커널-사용자 공유 메모리 매핑 */
void *ring = mmap(NULL, req.tp_block_size * req.tp_block_nr,
                  PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED,
                  fd, 0);

/* 패킷 수신 루프 (V3 블록 기반) */
while (1) {
    struct tpacket_block_desc *pbd = block_descs[current_block];

    /* 블록이 준비될 때까지 대기 */
    while (!(pbd->hdr.bh1.block_status & TP_STATUS_USER))
        poll(&pfd, 1, -1);

    /* 블록 내 모든 패킷 순회 */
    int num_pkts = pbd->hdr.bh1.num_pkts;
    struct tpacket3_hdr *ppd = (struct tpacket3_hdr *)
        ((uint8_t *)pbd + pbd->hdr.bh1.offset_to_first_pkt);

    for (int i = 0; i < num_pkts; i++) {
        uint8_t *pkt_data = (uint8_t *)ppd + ppd->tp_mac;
        uint32_t pkt_len = ppd->tp_snaplen;

        process_packet(pkt_data, pkt_len);  /* 패킷 처리 */

        ppd = (struct tpacket3_hdr *)
              ((uint8_t *)ppd + ppd->tp_next_offset);
    }

    /* 블록을 커널에 반환 */
    pbd->hdr.bh1.block_status = TP_STATUS_KERNEL;
    current_block = (current_block + 1) % req.tp_block_nr;
}

/* 커널 내부: TPACKET_V3 수신 처리
 * net/packet/af_packet.c — tpacket_rcv() */
static int tpacket_rcv(struct sk_buff *skb, struct net_device *dev,
                      struct packet_type *pt,
                      struct net_device *orig_dev)
{
    /* V3: 현재 블록에 패킷 추가 (가변 크기)
     * → skb 데이터를 mmap 버퍼에 직접 복사
     * → 블록이 가득 차거나 타임아웃 시 TP_STATUS_USER로 전환
     * → 사용자는 poll()로 알림 받고 mmap 메모리에서 직접 읽음
     * → recvmsg() 시스콜 불필요 = zero-copy 수신 */

    /* BPF 필터 먼저 실행 */
    res = run_filter(skb, sk, snaplen);
    if (!res)
        goto drop;

    /* ring buffer의 현재 블록에 패킷 데이터 복사 */
    h.raw = packet_current_rx_frame(po, skb, ...);
    skb_copy_bits(skb, 0, h.raw + macoff, snaplen);

    /* 패킷 메타데이터 기록: 타임스탬프, 길이, VLAN 등 */
    h.h3->tp_sec = ts.tv_sec;
    h.h3->tp_nsec = ts.tv_nsec;
    h.h3->tp_snaplen = snaplen;
    h.h3->tp_len = skb->len;
}

PACKET_FANOUT — 멀티코어 패킷 분산

여러 AF_PACKET 소켓이 동일 인터페이스에서 패킷을 분산 처리할 수 있습니다. suricata, PF_RING 대안으로 사용됩니다:

/* PACKET_FANOUT 모드 */
#define PACKET_FANOUT_HASH         0  /* 흐름 해시 기반 분배 (기본) */
#define PACKET_FANOUT_LB           1  /* 라운드 로빈 */
#define PACKET_FANOUT_CPU          2  /* CPU ID 기반 (RSS 활용) */
#define PACKET_FANOUT_ROLLOVER     3  /* 큐 가득 차면 다음 소켓으로 */
#define PACKET_FANOUT_RND          4  /* 랜덤 분배 */
#define PACKET_FANOUT_QM           5  /* skb 큐 매핑 기반 */
#define PACKET_FANOUT_CBPF         6  /* cBPF 프로그램으로 분배 결정 */
#define PACKET_FANOUT_EBPF         7  /* eBPF 프로그램으로 분배 결정 */

/* 사용 예: 4개 워커 스레드가 흐름 해시 기반으로 패킷 분산 */
int fanout_arg = (PACKET_FANOUT_HASH | PACKET_FANOUT_FLAG_DEFRAG)
                 | (group_id << 16);
setsockopt(fd, SOL_PACKET, PACKET_FANOUT, &fanout_arg,
           sizeof(fanout_arg));

/* 커널 내부: fanout_demux() — 소켓 선택 */
static struct sock *fanout_demux_hash(
    struct packet_fanout *f, struct sk_buff *skb, unsigned int num)
{
    /* skb 흐름 해시를 소켓 수로 나눠 분배 */
    return f->arr[reciprocal_scale(
        __skb_get_hash_symmetric(skb), num)];
}

/* PACKET_FANOUT_FLAG 옵션 */
#define PACKET_FANOUT_FLAG_ROLLOVER  0x1000 /* 소켓 백로그 시 롤오버 */
#define PACKET_FANOUT_FLAG_UNIQUEID  0x2000 /* 고유 그룹 ID 자동 할당 */
#define PACKET_FANOUT_FLAG_DEFRAG    0x8000 /* IP 단편화 재조합 후 분배 */

Raw Socket과 Netfilter 관계

Raw socket으로 전송하는 패킷도 Netfilter 체인을 통과합니다. 수신은 프로토콜 핸들러(Handler) 이전(raw_local_deliver)에 처리되므로 INPUT 체인보다 먼저 clone이 발생합니다:

방향소켓 타입Netfilter 통과 여부설명
TX AF_INET SOCK_RAW OUTPUT 체인 통과 raw_send_hdrinc()NF_HOOK(NF_INET_LOCAL_OUT)
TX AF_PACKET SOCK_RAW Netfilter 우회 dev_queue_xmit() 직접 호출 (L3 스택 미통과)
RX AF_INET SOCK_RAW PREROUTING 이후, INPUT 이전 NF_INET_PRE_ROUTING 통과 후 raw_local_deliver()
RX AF_PACKET SOCK_RAW Netfilter 이전에 수신 netif_receive_skb()에서 ptype 콜백 (L3 이전)
💡

tcpdump가 DROP된 패킷도 보이는 이유: AF_PACKET 소켓은 netif_receive_skb()ptype_all 리스트에 등록되어 Netfilter 이전에 skb 사본을 받습니다. 따라서 iptables/nftables에서 DROP된 패킷도 tcpdump에서 관찰됩니다. 전송 방향도 마찬가지로, AF_PACKET TX는 dev_queue_xmit()을 직접 호출하여 Netfilter OUTPUT 체인을 우회합니다.

Raw Socket의 bind()와 connect()

/* AF_INET SOCK_RAW에서 bind()와 connect()의 역할 */

/* bind() — 수신 필터링: 특정 로컬 IP로의 패킷만 수신 */
struct sockaddr_in addr = {
    .sin_family = AF_INET,
    .sin_addr.s_addr = inet_addr("192.168.1.10"),
};
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
/* → raw_v4_match()에서 daddr 매칭에 사용
 * → 해당 IP가 목적지인 패킷만 수신 큐에 전달 */

/* connect() — 기본 목적지 설정 + 수신 필터링 */
struct sockaddr_in dest = {
    .sin_family = AF_INET,
    .sin_addr.s_addr = inet_addr("10.0.0.1"),
};
connect(fd, (struct sockaddr *)&dest, sizeof(dest));
/* → send()에서 목적지 주소 생략 가능 (sendto 대신 send 사용)
 * → 수신 시 해당 소스 IP에서 온 패킷만 수신 (소스 필터) */

/* AF_PACKET에서 bind() — 특정 인터페이스에 바인딩 */
struct sockaddr_ll sll = {
    .sll_family   = AF_PACKET,
    .sll_protocol = htons(ETH_P_ALL),
    .sll_ifindex  = if_nametoindex("eth0"),
};
bind(fd, (struct sockaddr *)&sll, sizeof(sll));
/* → 해당 인터페이스의 패킷만 수신
 * → 바인딩 없으면 모든 인터페이스의 패킷 수신 */

실용 예제

/* 예제 1: ICMP Echo Request 전송 (ping 구현) */
int fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
/* CAP_NET_RAW 필요 */

struct {
    struct icmphdr hdr;
    char data[56];   /* 페이로드 (타임스탬프 등) */
} pkt;

pkt.hdr.type = ICMP_ECHO;
pkt.hdr.code = 0;
pkt.hdr.un.echo.id = htons(getpid());
pkt.hdr.un.echo.sequence = htons(seq++);
pkt.hdr.checksum = 0;
pkt.hdr.checksum = icmp_checksum(&pkt, sizeof(pkt));

struct sockaddr_in dest = { .sin_family = AF_INET,
    .sin_addr.s_addr = inet_addr("8.8.8.8") };

sendto(fd, &pkt, sizeof(pkt), 0,
       (struct sockaddr *)&dest, sizeof(dest));
/* 커널이 IP 헤더를 자동 생성 (IP_HDRINCL 미설정이므로)
 * → skb 할당 → ICMP 페이로드 복사 → IP 헤더 추가
 * → raw_sendmsg() → ip_append_data() → ip_push_pending_frames()
 * → Netfilter OUTPUT → ip_output() → dev_queue_xmit() */

/* 수신: raw socket은 모든 ICMP 패킷을 받으므로 필터 설정 */
struct icmp_filter filt;
filt.data = ~(1 << ICMP_ECHOREPLY);
setsockopt(fd, SOL_RAW, ICMP_FILTER, &filt, sizeof(filt));

char buf[1500];
struct sockaddr_in from;
socklen_t fromlen = sizeof(from);
int n = recvfrom(fd, buf, sizeof(buf), 0,
                 (struct sockaddr *)&from, &fromlen);
/* buf[0..19] = IP 헤더 (raw socket은 항상 IP 헤더 포함 수신)
 * buf[20..]  = ICMP 헤더 + 페이로드 */
/* 예제 2: ARP Request 전송 (AF_PACKET SOCK_RAW) */
int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ARP));

/* 전체 Ethernet 프레임을 직접 구성 */
struct {
    struct ethhdr  eth;    /* Ethernet 헤더 (14 bytes) */
    struct arphdr  arp;    /* ARP 헤더 */
    uint8_t ar_sha[6];     /* 송신자 MAC */
    uint8_t ar_sip[4];     /* 송신자 IP */
    uint8_t ar_tha[6];     /* 대상 MAC (ARP Request에서는 0) */
    uint8_t ar_tip[4];     /* 대상 IP */
} frame;

/* Ethernet 헤더: 브로드캐스트 */
memset(frame.eth.h_dest, 0xff, ETH_ALEN); /* FF:FF:FF:FF:FF:FF */
memcpy(frame.eth.h_source, my_mac, ETH_ALEN);
frame.eth.h_proto = htons(ETH_P_ARP);

/* ARP 헤더: ARP Request */
frame.arp.ar_hrd = htons(ARPHRD_ETHER);
frame.arp.ar_pro = htons(ETH_P_IP);
frame.arp.ar_hln = 6;
frame.arp.ar_pln = 4;
frame.arp.ar_op  = htons(ARPOP_REQUEST);

/* sockaddr_ll로 출력 인터페이스 지정 */
struct sockaddr_ll sll = {
    .sll_ifindex = if_nametoindex("eth0"),
    .sll_halen   = ETH_ALEN,
};
memset(sll.sll_addr, 0xff, ETH_ALEN);

sendto(fd, &frame, sizeof(frame), 0,
       (struct sockaddr *)&sll, sizeof(sll));
/* → packet_sendmsg() → dev_queue_xmit()
 * → IP 스택, Netfilter 완전 우회
 * → 직접 NIC 드라이버로 전달 */
/* 예제 3: IP_HDRINCL로 커스텀 IP 패킷 전송 */
int fd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
/* IPPROTO_RAW (255)는 자동으로 IP_HDRINCL 활성화 */

struct {
    struct iphdr  ip;
    struct udphdr udp;
    char payload[64];
} pkt;

/* IP 헤더 구성 */
pkt.ip.version  = 4;
pkt.ip.ihl      = 5;
pkt.ip.tos      = 0;
pkt.ip.tot_len  = htons(sizeof(pkt));
pkt.ip.id       = 0;     /* 커널이 자동 할당 */
pkt.ip.frag_off = htons(IP_DF);
pkt.ip.ttl      = 64;
pkt.ip.protocol = IPPROTO_UDP;
pkt.ip.check    = 0;     /* 커널이 자동 계산 */
pkt.ip.saddr    = inet_addr("10.0.0.1");
pkt.ip.daddr    = inet_addr("10.0.0.2");

/* UDP 헤더 구성 */
pkt.udp.source = htons(12345);
pkt.udp.dest   = htons(53);
pkt.udp.len    = htons(sizeof(pkt.udp) + sizeof(pkt.payload));
pkt.udp.check  = 0;     /* UDP 체크섬은 사용자가 계산해야 함 */

sendto(fd, &pkt, sizeof(pkt), 0, ...);
/* → raw_sendmsg() → inet->hdrincl=1이므로 raw_send_hdrinc()
 * → 커널은 check, tot_len(0이면), id(0이면), saddr(0이면)만 보정
 * → NF_HOOK(NF_INET_LOCAL_OUT) → dst_output() → dev_queue_xmit() */

보안 고려사항

위협관련 소켓 타입방어 메커니즘
IP 스푸핑 AF_INET + IP_HDRINCL rp_filter (Reverse Path Filtering), BCP 38 (uRPF)
ARP 스푸핑 AF_PACKET SOCK_RAW DAI (Dynamic ARP Inspection), 정적 ARP 엔트리
패킷 스니핑 AF_PACKET (ETH_P_ALL) CAP_NET_RAW 제한, 네트워크 네임스페이스(Namespace) 격리(Isolation)
프로토콜 스택 DoS SOCK_RAW 대량 전송 net.core.rmem_max, sk->sk_sndbuf 제한
컨테이너(Container) 탈출 AF_PACKET TPACKET CAP_NET_RAW 제거, seccomp 필터
# CAP_NET_RAW 관련 보안 설정

# 특정 바이너리에만 CAP_NET_RAW 부여 (setuid 대체)
setcap cap_net_raw+ep /usr/bin/ping

# ping socket 허용 범위 설정 (CAP_NET_RAW 불필요)
# GID 0~2147483647 범위의 사용자가 ICMP ping 가능
sysctl -w net.ipv4.ping_group_range="0 2147483647"

# Reverse Path Filtering (IP 스푸핑 방지)
sysctl -w net.ipv4.conf.all.rp_filter=1       # strict mode
sysctl -w net.ipv4.conf.all.rp_filter=2       # loose mode

# 컨테이너에서 CAP_NET_RAW 제거 (Docker)
docker run --cap-drop=NET_RAW ...

# seccomp으로 raw socket 시스콜 차단
# socket(AF_PACKET, ...) 또는 socket(AF_INET, SOCK_RAW, ...) 블록

# 열린 raw socket 확인
ss -w -a          # RAW 소켓 목록
cat /proc/net/raw # IPv4 raw socket 상세 정보
cat /proc/net/raw6 # IPv6 raw socket 상세 정보
cat /proc/net/packet # AF_PACKET 소켓 목록
⚠️

IPPROTO_RAW (255) 특수 동작: socket(AF_INET, SOCK_RAW, IPPROTO_RAW)전송 전용 raw socket을 생성합니다. IP_HDRINCL이 자동 활성화되며, 이 소켓으로는 수신이 불가합니다 (recvmsg()가 영원히 블록). 수신하려면 IPPROTO_RAW 대신 구체적인 프로토콜 번호(예: IPPROTO_UDP)를 지정하거나 별도의 수신용 raw socket을 생성해야 합니다.

소켓 디먹싱과 skb 전달

수신된 skb가 올바른 소켓을 찾아가는 과정 (디먹싱):

/* TCP 수신 디먹싱: 4-tuple 해시 → 소켓 lookup */
/* tcp_v4_rcv() 내부 */
struct sock *sk = __inet_lookup_skb(
    &tcp_hashinfo,  /* TCP 소켓 해시 테이블 */
    skb,
    __tcp_hdrlen(th),
    th->source,      /* 소스 포트 */
    th->dest,        /* 목적지 포트 */
    iph->saddr,      /* 소스 IP */
    iph->daddr,      /* 목적지 IP */
    sdif);
/* 반환: established 소켓 또는 listen 소켓 */

/* UDP 수신 디먹싱 */
struct sock *sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest,
                                         udptable);

/* 소켓을 찾은 후 skb를 수신 큐에 전달 */
if (!sock_owned_by_user(sk)) {
    /* 소켓이 lock 상태가 아니면 직접 수신 큐에 추가 */
    __skb_queue_tail(&sk->sk_receive_queue, skb);
    sk->sk_data_ready(sk);  /* epoll/poll/select wakeup */
} else {
    /* 소켓이 lock 중이면 backlog에 임시 저장 */
    __sk_add_backlog(sk, skb);
    /* → release_sock() 시 backlog 처리 */
}
💡

성능 팁: SO_REUSEPORTBPF_PROG_TYPE_SK_REUSEPORT를 조합하면, 동일 포트를 여러 소켓이 공유하면서 BPF 프로그램으로 skb를 특정 소켓에 스티어링할 수 있습니다. 이는 nginx, envoy 등의 고성능 프록시에서 활용됩니다.

Zero-copy 전송

대용량 데이터를 전송할 때, 사용자 공간 버퍼를 커널로 복사하지 않고 직접 NIC에 전달하여 CPU 사용량과 지연을 줄일 수 있습니다:

메커니즘시스템콜동작 방식
sendfile()sendfile(out_fd, in_fd, ...)파일 → 소켓 직접 전송 (페이지 캐시 → skb frag)
splice()splice(fd_in, ..., fd_out, ...)파이프 기반 zero-copy, 파일 ↔ 소켓 모두 가능
MSG_ZEROCOPYsend(fd, buf, len, MSG_ZEROCOPY)사용자 버퍼 → skb frag (완료 통지 필요, 4.14+)
/* 커널 내부: sendfile의 skb 구성 */
/* 파일 페이지를 skb fragment로 직접 참조 */
skb_fill_page_desc(skb, frag_idx, page, offset, size);
/* page refcount 증가, 복사 없음 */

/* MSG_ZEROCOPY: 사용자 버퍼 페이지를 pin */
/* skb->destructor = sock_zerocopy_callback;
 * 전송 완료 시 사용자 공간에 completion notification 전달
 * (errqueue에서 SO_EE_ORIGIN_ZEROCOPY 메시지 수신) */

/* 드라이버: skb_page_frag_refill로 페이지 풀 활용 */
struct page_frag_cache *nc = &this_cpu_ptr(&nf_skb_cache)->pf_cache;
if (!skb_page_frag_refill(size, nc, GFP_ATOMIC))
    return -ENOMEM;
/* nc->va + nc->offset 에서 size 바이트 사용 가능 */
💡

MSG_ZEROCOPY는 10Gbps 이상 고속 네트워크에서 효과적입니다. 그러나 작은 패킷(~수KB 이하)에서는 페이지 pinning과 completion 통지 오버헤드가 복사 비용보다 클 수 있습니다. Google의 벤치마크에 따르면 5~8% CPU 절감이 일반적입니다.

수신/전송 경로에서의 skb 변형

sk_buff 생애주기 (Lifecycle)

sk_buff는 네트워크 패킷의 전체 생명주기 동안 커널 메모리를 차지하며, 할당부터 해제까지 다양한 변형을 거칩니다. 다음 다이어그램은 수신(RX)과 송신(TX) 경로에서 sk_buff의 생애주기를 보여줍니다.

sk_buff 생애주기 개요 할당/초기화 RX/TX 경로 처리 큐잉/해제 `alloc_skb()`, 헤더 포인터 설정 L2/L3/L4, Netfilter, qdisc 소켓 전달 또는 NIC 전송 완료 상세 단계는 아래 상세 다이어그램 참고
먼저 개요 흐름을 보고, 아래 상세 다이어그램에서 함수 단위로 추적하면 이해가 빠릅니다.
sk_buff 생애주기 — 수신/송신 경로 수신 경로 (RX) 1. skb 할당 netdev_alloc_skb() / napi_alloc_skb() 2. DMA 데이터 복사 NIC → skb->data (ring buffer) 3. L2 프로토콜 처리 eth_type_trans(), mac_header 설정 4. L3 프로토콜 처리 ip_rcv(), network_header 설정 5. Netfilter 훅 6. L4 프로토콜 처리 tcp_v4_rcv(), transport_header 설정 7. 소켓 수신 큐 skb_queue_tail(&sock->sk_receive_queue) 8. 유저스페이스 복사 recvmsg() → copy_to_user() 9. skb 해제 kfree_skb() / consume_skb() 송신 경로 (TX) 1. 유저 데이터 복사 sendmsg() → copy_from_user() 2. skb 할당 sock_alloc_send_skb() / alloc_skb() 3. L4 헤더 추가 skb_push(TCP/UDP 헤더) 4. L3 헤더 추가 skb_push(IP 헤더), 라우팅 결정 5. Netfilter 훅 6. L2 헤더 추가 skb_push(Ethernet 헤더) 7. TC/Qdisc (QoS) 트래픽 제어, 우선순위 큐 8. 드라이버 전송 dev_queue_xmit() → DMA 매핑 9. 전송 완료 & 해제 TX 완료 인터럽트 → dev_kfree_skb() skb 포인터 이동 RX: data, tail 이동 (pull) TX: data 이동 (push) 각 계층에서 헤더 참조: mac_header, network_header, transport_header
sk_buff 생애주기: 할당 → 프로토콜 스택 통과 → 소켓 큐 → 유저스페이스 전달 → 해제

참조 카운트와 메모리 관리

/* sk_buff의 참조 카운트는 users 필드로 관리 */
struct sk_buff {
    atomic_t users;  /* 참조 카운트 (skb_get/skb_put으로 증감) */
    /* ... */
};

/* 참조 카운트 증가 — 소유권 공유 */
static inline struct sk_buff *skb_get(struct sk_buff *skb)
{
    refcount_inc(&skb->users);
    return skb;
}

/* 참조 카운트 감소 — 0이 되면 해제 */
static inline void kfree_skb(struct sk_buff *skb)
{
    if (!skb) return;
    if (refcount_dec_and_test(&skb->users))
        __kfree_skb(skb);  /* 실제 해제 */
}

/* 정상 소비 (드롭 아님) — 통계 구분 */
static inline void consume_skb(struct sk_buff *skb)
{
    if (!skb) return;
    if (refcount_dec_and_test(&skb->users))
        __kfree_skb(skb);
}

/* 사용 예: 소켓 큐에서 꺼낸 후 해제 */
struct sk_buff *skb = skb_dequeue(&sk->sk_receive_queue);
if (skb) {
    process_packet(skb);
    consume_skb(skb);  /* 정상 소비 */
}

/* 드롭 시 kfree_skb 사용 (디버깅 추적 가능) */
if (!pskb_may_pull(skb, sizeof(struct iphdr))) {
    kfree_skb(skb);  /* 드롭: perf/dropwatch로 추적됨 */
    return -EINVAL;
}

수신/송신 경로에서의 데이터 영역 변화

수신 경로: 각 계층에서 헤더를 제거하며 data 포인터가 앞으로 이동합니다.

단계 대표 함수 버퍼 레이아웃 data 포인터 위치
1. DMA 복사 직후 NIC RX headroom | ETH | IP | TCP | DATA | tail ETH 시작점
2. L2 처리 eth_type_trans() headroom | ETH | IP | TCP | DATA | tail skb_pull(ETH_HLEN) 후 IP 시작점
3. L3 처리 ip_rcv() headroom | ETH | IP | TCP | DATA | tail skb_pull(ip_hdr_len) 후 TCP 시작점
4. L4 처리 tcp_rcv() headroom | ETH | IP | TCP | DATA | tail payload 시작점 (헤더 제거 완료)

송신 경로: 각 계층에서 헤더를 추가하며 data 포인터가 뒤로 이동합니다.

단계 대표 함수 버퍼 레이아웃 data 포인터 이동
1. payload 준비 소켓 송신 준비 headroom | PAYLOAD | tail payload 시작점
2. L4 헤더 추가 tcp_transmit_skb() headroom | TCP | PAYLOAD | tail skb_push(tcp_hdr_len)
3. L3 헤더 추가 ip_queue_xmit() headroom | IP | TCP | PAYLOAD skb_push(ip_hdr_len)
4. L2 헤더 추가 dev_hard_start_xmit() headroom | ETH | IP | TCP | PAYLOAD skb_push(ETH_HLEN) 후 NIC DMA 전송
headroom 부족 문제: 송신 경로에서 헤더를 추가할 때 headroom이 부족하면 skb_realloc_headroom()이 호출되어 새로운 버퍼를 할당합니다. 이는 성능 저하를 유발하므로, 초기 할당 시 충분한 headroom을 확보하는 것이 중요합니다 (NET_SKB_PAD + NET_IP_ALIGN + 예상 헤더 크기).

eth_type_trans() 호출 전후 skb 필드 변화

NIC 드라이버가 eth_type_trans(skb, dev)를 호출하면 다음 필드들이 갱신됩니다:

필드호출 전 (DMA 직후)호출 후
skb->mac_header 미설정 L2(Ethernet) 헤더 시작 오프셋으로 설정 (skb_reset_mac_header() 수행)
skb->protocol 미설정 EtherType 값 (예: ETH_P_IP, ETH_P_IPV6) — ntohs() 변환 포함
skb->data L2(ETH) 헤더 시작 L3 헤더 시작 (skb_pull(ETH_HLEN) 또는 VLAN 포함 크기만큼 당겨짐)
skb->len L2 프레임 전체 크기 ETH 헤더 제거 후 L3 이상 크기
skb->network_header 미설정 미설정 — ip_rcv() 진입 후 skb_reset_network_header()로 설정됨
skb->mac_len 미설정 미설정 — L3 처리 진입 시 skb->network_header - skb->mac_header로 초기화됨
포인터 복원: mac_header가 설정된 뒤에는 eth_hdr(skb)로 Ethernet 헤더 포인터를 얻을 수 있고, skb_mac_header(skb)skb->head + skb->mac_header에 해당하는 포인터를 얻습니다. skb->data는 이미 L3 시작으로 옮겨졌으므로 혼동하지 않도록 주의하십시오.

수신/전송 경로 요약

수신 경로 (NIC → 앱):

  1. NIC 드라이버: netdev_alloc_skb()로 skb 할당, DMA 데이터 복사
  2. L2 처리: skb_pull(ETH_HLEN)으로 Ethernet 헤더 제거
  3. L3 처리: skb_pull(ip_hdr_len)으로 IP 헤더 제거, transport_header 설정
  4. L4 처리: TCP/UDP 디먹싱, 소켓 수신 큐에 skb_queue_tail()
  5. 앱: recvmsg()에서 데이터를 사용자 공간에 복사

전송 경로 (앱 → NIC):

  1. 앱: sendmsg()에서 사용자 데이터를 skb에 복사
  2. L4: skb_push()로 TCP/UDP 헤더 추가
  3. L3: skb_push()로 IP 헤더 추가, 라우팅
  4. L2: skb_push()로 Ethernet 헤더 추가
  5. NIC 드라이버: dev_queue_xmit() → DMA 전송

커널 내 실제 사용 사례

sk_buff는 네트워크 스택의 모든 서브시스템에서 핵심적으로 사용됩니다:

서브시스템 주요 skb 활용 핵심 함수/패턴
TCP 전송 큐, 재전송(Retransmission) 큐, OOO 큐에 skb 관리 tcp_write_xmit(), tcp_retransmit_skb(), TCP_SKB_CB()로 cb[] 활용
UDP 소켓 수신 큐에 skb 대기열 udp_rcv(), skb_consume_udp(), MSG_PEEK 처리
Netfilter 패킷 필터링/수정/NAT skb_make_writable() 후 헤더 수정, nf_ct_get(skb)로 conntrack
Bridge L2 포워딩, VLAN 처리 skb_clone()으로 멀티캐스트 복제, skb_vlan_push/pop()
Tunnel (GRE, VXLAN) encapsulation/decapsulation skb_cow_head()로 headroom 확보, skb_push()로 외부 헤더 추가
TC (Traffic Control) QoS, 큐잉, 셰이핑 skb->priority, skb->mark, skb_get_queue_mapping()
BPF/XDP 프로그래밍 가능 패킷 처리 TC-BPF: __skb_buff 컨텍스트, XDP: skb 이전 단계 (xdp_buffbuild_skb)
SCTP 멀티스트리밍, 멀티호밍 skb_queue_head_init()으로 청크별 큐 관리
/* TCP: cb[]를 tcp_skb_cb로 활용하는 패턴 */
struct tcp_skb_cb {
    __u32 seq;        /* 시작 시퀀스 번호 */
    __u32 end_seq;    /* 끝 시퀀스 번호 */
    __u32 ack_seq;    /* ACK 번호 */
    __u8  tcp_flags;  /* TCP 플래그 */
    __u8  sacked;     /* SACK 상태 */
    /* ... */
};

#define TCP_SKB_CB(__skb) \
    ((struct tcp_skb_cb *)&((__skb)->cb[0]))

/* 사용 예: TCP 재전송 판단 */
if (after(TCP_SKB_CB(skb)->end_seq, tp->snd_una))
    /* 아직 ACK되지 않은 데이터 — 재전송 대상 */

/* Netfilter: 패킷 수정 전 쓰기 가능 확보 */
static unsigned int my_nf_hook(void *priv,
    struct sk_buff *skb,
    const struct nf_hook_state *state)
{
    struct iphdr *iph;
    if (skb_ensure_writable(skb, sizeof(*iph)))
        return NF_DROP;
    iph = ip_hdr(skb);
    iph->ttl--;   /* 안전하게 수정 가능 */
    ip_send_check(iph);
    return NF_ACCEPT;
}

성능 튜닝 경험적 팁

커널 네트워크 스택과 NIC 드라이버를 다루면서 얻는 경험적 성능 최적화 팁들입니다:

할당 최적화

Clone vs Copy 선택

큐 및 스케줄링

Zero-Copy 경로

하드웨어 offload 활용

# NIC 오프로드 상태 확인
$ ethtool -k eth0 | grep -E "checksum|gso|gro|rss"
tcp-segmentation-offload: on
udp-fragmentation-offload: [fixed]
generic-segmentation-offload: on
generic-receive-offload: on
tcp6-segmentation-offload: on
rx-checksumming: on
tx-checksumming: on

# RSS 설정 확인 및 변경
$ ethtool -l eth0          # 큐 개수 확인
$ ethtool -L eth0 combined 4  # 4개 combined 큐로 설정

# interrupt coalescing 조절 (지연 vs 처리량)
$ ethtool -C eth0 rx-usecs 100 tx-usecs 100  # moderate coalescing
$ ethtool -C eth0 rx-usecs 0 tx-usecs 0      # 낮은 지연 (latency)
실제 서비스 경험: 제가 운영하는 10Gbps DDoS 완화 장비에서 GRO offGRO on으로 변경 시 CPU 사용량이 약 40% 감소했습니다. 하지만 특정 레거시 애플리케이션에서는 GRO로 인한 packet reordering이 문제를 일으킬 수 있어, 프로덕션 변경 전 반드시 테스트 환경에서 검증하세요.

주의사항과 함정 (Common Mistakes)

1. skb leak (메모리 누수)

/* 잘못된 코드: 에러 경로에서 skb 해제 누락 */
static int my_rx(struct sk_buff *skb)
{
    struct my_hdr *hdr = (struct my_hdr *)skb->data;
    if (hdr->version != MY_VERSION)
        return -EINVAL;  /* BUG! skb가 해제되지 않음 */
    /* ... */
}

/* 올바른 코드 */
static int my_rx(struct sk_buff *skb)
{
    struct my_hdr *hdr = (struct my_hdr *)skb->data;
    if (hdr->version != MY_VERSION) {
        kfree_skb(skb);  /* 에러 경로 → kfree_skb (드롭) */
        return -EINVAL;
    }
    /* ... 정상 처리 후 ... */
    consume_skb(skb);  /* 정상 경로 → consume_skb */
    return 0;
}

2. pskb_may_pull 후 포인터 미갱신

/* 잘못된 코드: pull 후 이전 포인터 사용 */
struct iphdr *iph = ip_hdr(skb);
if (!pskb_may_pull(skb, iph->ihl * 4))
    goto drop;
/* BUG! pskb_may_pull이 버퍼를 재할당했을 수 있음 → iph는 dangling pointer */
pr_info("saddr: %pI4\
", &iph->saddr);

/* 올바른 코드: 포인터 재취득 */
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
    goto drop;
struct iphdr *iph = ip_hdr(skb);  /* pull 후 재취득 */
if (!pskb_may_pull(skb, iph->ihl * 4))
    goto drop;
iph = ip_hdr(skb);  /* 두 번째 pull 후에도 재취득! */

3. 공유 skb 데이터 수정

/* 잘못된 코드: clone된 skb의 데이터를 바로 수정 */
struct iphdr *iph = ip_hdr(skb);
iph->ttl = 64;  /* BUG! skb가 clone 상태면 원본도 수정됨 */

/* 올바른 코드: 쓰기 전 독점 소유 확보 */
if (skb_ensure_writable(skb, skb_network_header_len(skb)))
    goto drop;
struct iphdr *iph = ip_hdr(skb);  /* 포인터 재취득 */
iph->ttl = 64;  /* 이제 안전 */

4. truesize 불일치

/* 잘못된 코드: skb에 페이지를 추가하면서 truesize 미갱신 */
skb_add_rx_frag(skb, idx, page, offset, size, size);
/* 마지막 인자(truesize)가 실제 할당 크기보다 작으면
 * → 소켓 메모리 추적(sk_rmem_alloc)이 실제보다 작게 계산됨
 * → 소켓이 제한 없이 메모리를 소비 → OOM 가능
 */

/* 올바른 코드: truesize는 실제 할당된 메모리 크기 */
skb_add_rx_frag(skb, idx, page, offset, size, PAGE_SIZE);
/* PAGE_SIZE = 실제 할당 단위 (page order 0 기준) */

5. refcount 이중 해제(Double Free)

/* 잘못된 코드: netif_rx 후 skb를 다시 해제 */
netif_rx(skb);         /* 네트워크 스택에 소유권 이전 */
kfree_skb(skb);        /* BUG! 이중 해제 → use-after-free */

/* 올바른 패턴: 전달 후 skb를 사용하지 않음 */
netif_rx(skb);         /* 소유권 이전, skb는 더 이상 사용하지 않음 */
/* netif_rx(), netif_receive_skb(), napi_gro_receive() 등은
 * skb의 소유권을 가져감 — 이후 skb 접근 금지 */

6. headroom 부족으로 인한 skb_under_panic

/* 잘못된 코드: headroom 확인 없이 헤더 추가 */
skb_push(skb, sizeof(struct my_encap_hdr));
/* headroom이 부족하면 skb_under_panic → 커널 panic */

/* 올바른 코드: headroom 확보 후 추가 */
int needed = sizeof(struct my_encap_hdr) + LL_RESERVED_SPACE(dev);
if (skb_cow_head(skb, needed)) {
    kfree_skb(skb);
    return NETDEV_TX_OK;
}
skb_push(skb, sizeof(struct my_encap_hdr));  /* 이제 안전 */

7. 레이어 경계 처리 실수

RX → 포워딩 → TX 경로를 거치는 패킷은 mac_header, network_header 포인터의 유효성이 경계마다 달라집니다. 이를 무시하면 잘못된 헤더 접근이나 커널 BUG를 유발합니다.

RX → 포워딩: mac_header 갭 문제

/* 수신 후 포워딩 경로에서 mac_header와 network_header 사이에
 * "갭(hole)"이 생길 수 있음:
 *   eth_type_trans()  → data를 L3 시작으로 당김
 *   ip_rcv()          → network_header = data 위치로 재설정
 *   → mac_header는 그대로이므로 mac_header < network_header
 *      (갭 = Ethernet 헤더 크기)
 *
 * 포워딩 후 재전송 시 L2 헤더 재구성이 필요하면:
 */
skb_mac_header_rebuild(skb);  /* mac_header를 network_header 바로 앞으로 재정렬 */

포워딩 → TX: mac_header 무효화

/* IPSec/GRE 등 캡슐화 후 원래 mac_header가 무효화될 수 있음.
 * 헤더 재구성 전에 반드시 유효성 확인: */
if (skb_mac_header_was_set(skb)) {
    /* mac_header가 유효할 때만 eth_hdr(skb) 접근 */
    struct ethhdr *eth = eth_hdr(skb);
    /* ... */
}

/* 캡슐화로 mac_header가 갱신되지 않은 경우 직접 재설정 */
skb_reset_mac_header(skb);  /* mac_header = data - head */

IPSec: headroom 확보 및 network_header 재설정

/* IPSec 암호화(outbound) 전: ESP/AH 헤더를 위한 headroom 확보 */
int head_delta = skb_cow_head(skb, esp_hdr_len + LL_RESERVED_SPACE(dst->dev));
if (head_delta)
    goto error;
skb_push(skb, esp_hdr_len);  /* ESP 헤더 공간 확보 */
skb_reset_network_header(skb);  /* IP 헤더 위치 재설정 */

/* IPSec 복호화(inbound) 후: network_header가 바뀌었으므로 재설정 필수 */
skb_pull(skb, esp_hdr_len);  /* ESP 헤더 제거 */
skb_reset_network_header(skb);  /* 복호화된 IP 헤더로 포인터 재설정 */
레이어 경계 핵심 원칙: 캡슐화(Encapsulation)/역캡슐화(Decapsulation) 후에는 network_header, transport_header, mac_header를 반드시 재설정하십시오. 포인터를 재설정하지 않으면 ip_hdr(skb), tcp_hdr(skb) 등이 잘못된 주소를 반환하여 조용한 메모리 손상이 발생합니다.

실제 트러블슈팅 사례

네트워크 문제를 분석하면서 자주 마주하는 skb 관련 실제 케이스들입니다:

사례 1: 메모리 누수가 의심될 때

증상: ss -s/proc/net/sockstat에서 사용 중인 소켓 수가 비정상적으로 많거나, 시스템 메모리가 점진적으로 감소합니다.

# 현재 소켓 상태 확인
$ ss -s
$ cat /proc/net/sockstat

# orphan(소멸된) 소켓 수 — TIME_WAIT 소켓이 정리되지 않으면 증가
$ cat /proc/net/sockstat | grep TCP

# 드롭된 패킷 수 확인
$ nstat -az TcpExt.ListenOverflows
$ cat /proc/net/netstat | grep -E "Tcp|Ext" | column -t

원인: 에러 경로에서 kfree_skb() 호출 누락, 또는 consume_skb() 대신 kfree_skb()를 사용해서 메모리 참조가 해제되지 않음.

사례 2: 체크섬 검증 실패

증상: 특정 NIC에서만 TCP/UDP 체크섬 오류가 발생하거나, 애플리케이션에서 "bad checksum" 로그가 반복됩니다.

# NIC 드라이버와 체크섬 오프로드 상태 확인
$ ethtool -k eth0 | grep checksum
rx-checksumming: on
tx-checksumming: on

# 드라이버 메시지 확인 (dmesg)
$ dmesg | grep -i "eth0\|ixgbe\|mlx5"
[12345.678] ixgbe 0000:01:00.0: ixgbe_check_bad_counter: Detected bad TCP checksum, 
           but feature turned on — actual problem may exist

# 테스트: 체크섬 offload 비활성화
$ ethtool -K eth0 rx-checksumming off tx-checksumming off

원인: 일부 저가형 또는 legacy NIC에서 HW 체크섬 계산이 부정확한 경우 (false positive). 커널 버그로 인해 특정 드라이버에서만 발생.

사례 3: GRO로 인한 TCP 재전송 증가

증상: GRO 활성화 후 TCP 재전송이 증가하거나, 특정 애플리케이션에서 패킷 순서 오류 발생.

# GRO 상태 확인
$ ethtool -k eth0 | grep generic-receive-offload

# TCP 재전송 통계 확인
$ nstat -az TcpRetransSegs
$ cat /proc/net/snmp | grep -E "Retrans|OutSegs"

# 문제 구간 확인 — 서버-클라이언트 양쪽에서 GRO 상태 맞춰야 함
$ ethtool -K eth0 gro off  # 테스트를 위해 off
$ iperf3 -c 10.0.0.1 -P 4  # 대역폭 재테스트

원인: GRO가 다른 흐름의 패킷을 잘못 병합하거나, NIC HW GRO의 구현 버그. 특히 가상화(Virtualization) 환경(virtio, VM에서) 자주 발생.

사례 4: 프래그먼트된 대용량 패킷 처리 지연

증상: 대용량 파일 전송 시 예상보다 낮은 throughput, 또는 특정 크기(예: 64KB 근처)에서 throughput 급격 감소.

# skb_linearize 빈도 확인 — linearization은 비용이 큼
$ cat /proc/net/netstat | grep SkbConcatenate
TcpSmbConcatenate: 12345

# GSO/TSO 상태 확인
$ ethtool -k eth0 | grep -E "segmentation|offload"

# 수신측 gro_flush_timeout 확인 (지연 병합)
$ sysctl net.core.gro_flush_timeout
net.core.gro_flush_timeout = 2000

원인: NIC이 TSO를 지원하지 않으면 커널에서 software GSO가 linearize를 유발하거나, 수신 측 GRO가 타임아웃까지 대기를 위해 지연 발생.

사례 5: NAPI 기아(Starvation) 상태 (starvation)

증상: 고대역폭 트래픽에서 일부 CPU만 max softirq time에 도달하고, 다른 CPU는 유휴 상태(Idle State). 드롭이 특정 CPU에서 집중됨.

# CPU별 softirq 처리량 확인
$ cat /proc/net/softnet_stat | awk '{print $1, $2, $3, $4}' | head -20
# 컬럼: cpu_id, processed, dropped, time_squeeze

# IRQ affinity 확인
$ cat /proc/interrupts | grep eth0
$ cat /proc/irq/<irq_num>/smp_affinity

# NAPI 가중치 확인
$ ls /sys/class/net/eth0/napi
$ cat /sys/class/net/eth0/napi/<napi_id>/poll_time

원인: IRQ가 단일 CPU에 집중되거나, NAPI weight가 너무 작아서 time slice 내에 처리를 못 함. RSS 설정과 IRQ balancing 문제.

트러블슈팅 핵심 원칙: 네트워크 문제는 대부분 상호작용하는 여러 요소(RSS, GRO, IRQ affinity, 드라이버 버그)가 복합적으로 작용합니다. 단일 변수만 바꾸고 측정하는 체계적인 접근이 필요합니다. 예를 들어 "GRO만 끄고 latency 측정" → "IRQ affinity만 바꾸고 측정" 식으로요.

디버깅 기법

tracepoint 활용

# skb 드롭 추적 (kfree_skb 호출 위치와 원인)
$ perf trace -e skb:kfree_skb --call-graph dwarf -a sleep 5

# skb 드롭 실시간 모니터링
$ cat /sys/kernel/debug/tracing/events/skb/kfree_skb/format
$ echo 1 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enable
$ cat /sys/kernel/debug/tracing/trace_pipe

# dropwatch (커널 6.x+: kfree_skb_reason으로 드롭 원인 표시)
$ dropwatch -l kas
> start

perf probe로 동적 추적

# 특정 함수에서 skb->len 값 추적
$ perf probe --add 'tcp_v4_rcv skb->len skb->data_len'
$ perf record -e probe:tcp_v4_rcv -a sleep 10
$ perf script

# skb 할당 빈도 측정
$ perf stat -e 'skb:*' -a sleep 10

/proc/net 진단

# 소켓 메모리 사용량 확인 (skb leak 의심 시)
$ cat /proc/net/sockstat
sockets: used 1234
TCP: inuse 56 orphan 2 tw 128 alloc 60 mem 1024
UDP: inuse 12 mem 256

# mem: 페이지 단위 (mem * PAGE_SIZE = 실제 바이트)
# orphan: 소속 프로세스 없는 TCP 소켓 (skb leak 원인 가능)
# tw: TIME_WAIT 상태 (정상적이지만 과다하면 문제)

# 네트워크 스택 통계 (드롭/에러 확인)
$ nstat -az | grep -i drop
$ cat /proc/net/softnet_stat

디버깅 커널 옵션

옵션기능
CONFIG_DEBUG_KMEMLEAKskb를 포함한 커널 메모리 누수 탐지
CONFIG_KASANuse-after-free, out-of-bounds 접근 탐지
CONFIG_NET_DROP_MONITOR네트워크 패킷 드롭 위치 추적
CONFIG_DEBUG_NET네트워크 스택 디버깅 assertion 활성화
CONFIG_SKB_EXTENSIONSskb extension (conntrack, bridge 등) 디버깅

커널 버전별 sk_buff 변경 이력

sk_buff는 Linux 커널 네트워크 스택의 핵심 자료구조로서 수십 년에 걸쳐 지속적으로 변화해 왔습니다. 아래 이력은 각 커널 시리즈에서 sk_buff에 영향을 준 주요 변경사항을 정리합니다.

2.6.x 시대: 구조 간소화와 헤더 분리

Linux 2.6 시리즈는 sk_buff의 내부 구조를 대대적으로 간소화한 시기입니다. 2.6.14 이전에는 sk_buff 구조체 자체에 callback 함수 포인터 배열(destructor_list)이 포함되어 있었으나, 이를 소켓 수준으로 이동시켜 sk_buff 크기를 줄였습니다.

2.6.22에서는 skb_shared_info를 sk_buff 끝 부분(end 포인터 위치)에 배치하는 방식으로 고정하여 캐시 지역성(Cache Locality)을 개선하였습니다. 기존에는 별도 할당으로 포인터를 통해 접근했으나, 같은 캐시 라인에 위치하도록 변경되어 GSO/TSO 처리 성능이 향상되었습니다.

2.6.24에서는 mac_header, network_header, transport_header 필드가 절대 포인터에서 head 기준 16비트 오프셋(offset)으로 변경되었습니다. 이 변경으로 sk_buff 구조체 크기가 줄고, pskb_expand_head()로 버퍼를 재할당할 때 헤더 포인터를 일일이 갱신하지 않아도 되게 되었습니다.

2.6.31에서는 skb_dst_set() / skb_dst() 접근자 함수가 도입되어 dst 포인터에 참조 카운트 정보를 태그 비트(tagged pointer)로 인코딩하는 방식으로 최적화되었습니다. 이를 통해 빠른 경로(fast path)에서 참조 카운트 연산을 생략할 수 있게 되었습니다.

버전변경 내용영향
2.6.14destructor_list 제거, 소켓 레벨 destructor로 통합sk_buff 크기 감소, 메모리 효율 향상
2.6.18ip_summed CHECKSUM_HW → CHECKSUM_PARTIAL / CHECKSUM_COMPLETE 분리체크섬 오프로드 API 명확화
2.6.22skb_shared_info를 skb 말단(end)에 인접 배치GSO/TSO 처리 캐시 지역성 향상
2.6.24mac/network/transport_header를 절대 포인터 → 16비트 오프셋으로 변경sk_buff 구조체 축소, pskb_expand_head 후 포인터 재획득 불필요
2.6.26skb_clone_writable() 도입, 클론 쓰기 가능 여부 판별Netfilter COW 패턴 안정화
2.6.29GRO (Generic Receive Offload) 도입 (Ben Hutchings)소프트웨어 LRO 대체, 프로토콜 중립적 병합
2.6.31skb_dst 태그드 포인터 최적화fast path에서 dst 참조 카운트 비용 절감
2.6.39skb_frag_t의 page+offset 구조 도입 (bio_vec 정렬 준비)비선형 단편 일관성 향상

3.x: bio_vec 정렬과 BUILD_BUG_ON 안전성 강화

3.x 시리즈에서는 skb_frag_tstruct bio_vec와 동일한 레이아웃으로 통일하려는 시도가 이루어졌습니다. 3.2 이전에는 skb_frag_t가 struct page *page + __u32 page_offset + __u32 size였으나, 3.2 이후 struct page *bv_page + unsigned int bv_offset + unsigned int bv_len으로 bio_vec와 호환되도록 정렬되었습니다.

3.13에서는 sk_buff 구조체 레이아웃 변경에 대한 안전망으로 BUILD_BUG_ON 검사가 강화되었습니다. 컴파일 타임에 중요 오프셋과 크기를 검증해 아키텍처별 패딩 차이로 인한 잠재적 버그를 사전에 차단합니다.

버전변경 내용영향
3.2skb_frag_t → bio_vec 정렬, skb_frag_page/offset/size 접근자 추가블록 레이어와 네트워크 레이어 단편 표현 통일
3.3sk_buff에서 ip_summed/remcsum_offload 비트 필드 도입체크섬 상태 정밀 표현
3.10skb_ext 사전 연구: conntrack 슬롯 구조 재편5.x skb_ext 기반 작업
3.13BUILD_BUG_ON으로 sk_buff 크기·오프셋 컴파일 타임 검증아키텍처별 레이아웃 안전성 강화
3.15skb_zerocopy() 도입 (페이지 단편 참조 이전)tun, vhost zero-copy 포워딩 가능
3.16GSO_BY_FRAGS 도입 (단편 수 기반 세그먼트 크기 결정)UDP cork 패킷 처리 효율화
3.18XDP 기초 연구 시작, skb ← → xdp_buff 변환 개념 정립4.x XDP 도입 기반

4.x: MSG_ZEROCOPY, UDP GSO, skb_put_data/zero

4.x 시리즈는 성능 최적화에 초점을 맞춘 변경이 많습니다. 가장 중요한 변화는 4.14에서 도입된 MSG_ZEROCOPY로, send() 시스템 콜에서 사용자 공간 메모리를 커널로 복사하지 않고 페이지 참조만으로 전송하는 기능입니다. 이를 위해 sk_buff에 skb_zcopy 비트와 ubuf_info 포인터가 추가되었습니다.

4.18에서는 UDP GSO가 도입되었습니다. 기존 TSO(TCP Segmentation Offload)와 달리 UDP GSO는 단일 sendmsg() 호출로 전달한 대형 UDP 버퍼를 NIC가 분할하도록 하여 UDP 스트리밍 성능이 크게 향상되었습니다.

4.20에서는 skb_put_data()skb_put_zero()가 공식 API로 추가되어 기존 skb_put()memcpy()/memset() 패턴을 단일 함수로 표현할 수 있게 되었습니다.

버전변경 내용영향
4.8XDP(eXpress Data Path) 도입, xdp_buff ↔ sk_buff 변환 경로 확립드라이버에서 skb 할당 이전 패킷 처리 가능
4.9sk_buff에 tc_index, tc_verd 통합 (TC 메타데이터)tc(1) BPF 프로그램과 sk_buff 연동 강화
4.14MSG_ZEROCOPY 도입: ubuf_info, skb_zcopy 비트 추가TCP 전송 경로에서 사용자 메모리 복사 제거
4.15skb_ext 프로토타입: 가변 크기 확장 메타데이터 개념 연구5.x 정식 도입 기반
4.17skb_condense() 도입: 단편이 없을 때 헤드 버퍼 축소소켓 버퍼 메모리 효율 향상
4.18UDP GSO 도입 (Willem de Bruijn): NETIF_F_GSO_UDP_L4UDP 스트리밍 처리량 대폭 향상
4.20skb_put_data(), skb_put_zero() 공식 API 추가패킷 생성 코드 간소화

5.x: skb_ext 정식 도입과 skb_ensure_writable

5.x는 sk_buff 확장성(Extensibility)과 안전성 측면에서 큰 발전이 있었습니다. 5.1에서 skb_ext 시스템이 정식 도입되어 conntrack, IPsec secpath, bridge NF, MPTCP 서브플로 등 다양한 프로토콜이 sk_buff에 가변 크기 메타데이터를 동적으로 연결할 수 있게 되었습니다. 기존에는 각 기능이 sk_buff 구조체에 직접 포인터 필드를 추가했으나, skb_ext 덕분에 sk_buff 구조체 자체의 비대화를 막을 수 있게 되었습니다.

5.3에서는 skb_ensure_writable()이 도입되어 기존에 혼용되던 skb_make_writable()skb_cow_head()를 단일 인터페이스로 통합하였습니다. BPF TC 프로그램이 sk_buff 데이터를 수정할 때도 이 함수를 통해 안전한 COW를 보장받습니다.

버전변경 내용영향
5.1skb_ext 시스템 정식 도입 (Florian Westphal): SKB_EXT_SEC_PATH, SKB_EXT_BRIDGE_NF 등sk_buff 구조체 비대화 방지, 확장 메타데이터 COW 지원
5.2page_pool API 안정화: page_pool_alloc_pages, page_pool_recycle 표준화드라이버 RX 경로 DMA 캐시 재활용 표준화
5.3skb_ensure_writable() 도입 (Netfilter, BPF 공용)skb 쓰기 보장 API 통합, 기존 skb_make_writable 대체
5.4BPF skb 접근 개선: bpf_skb_load_bytes_relative 헬퍼 추가BPF 프로그램에서 mac/net 헤더 상대 오프셋 안전 접근
5.7sk_buff 내 tc_at_ingress 플래그 추가 (TC ingress/egress 경로 구분)TC 훅 방향 판별 명확화
5.10MPTCP skb_ext 슬롯(SKB_EXT_MPTCP) 추가MPTCP 서브플로 메타데이터를 skb_ext로 관리
5.13skb_csum_hwoffload_help() 도입 (드라이버 TX 체크섬 오프로드 헬퍼)드라이버 TX 체크섬 처리 코드 중복 제거
5.17skb_orphan_frags_rx() 추가 (RX 경로 단편 orphan)소켓 버퍼 해제 경로 안전성 강화

6.x: kfree_skb_reason, page_pool 심화, netmem, devmem TCP

6.x 시리즈는 관찰 가능성(Observability)과 zero-copy 수신의 혁신이 핵심입니다. 6.0에서 도입된 kfree_skb_reason()은 패킷 드롭의 원인을 SKB_DROP_REASON_* 열거형으로 기록하여 perf, bpftrace에서 드롭 원인을 정확히 추적할 수 있게 되었습니다. 기존 kfree_skb()는 드롭 원인을 알 수 없었으나, 이 변경으로 NOT_SPECIFIED부터 TCP_INVALID_SEQUENCE, NETFILTER_DROP 등 수십 가지 세분화된 이유를 기록합니다.

6.1에서는 page_pool이 sk_buff 수명주기와 더욱 깊이 통합되었습니다. page_pool_return_skb_page()를 통해 RX 경로에서 사용한 페이지를 DMA 매핑을 유지한 채 page_pool로 즉시 반환할 수 있어, 고성능 드라이버의 할당/해제 오버헤드가 크게 줄었습니다.

6.6에서는 netmem이라는 새로운 추상 메모리 타입이 도입되었습니다. netmem은 struct page, hugetlb, dma-buf 등 다양한 메모리 유형을 통일된 인터페이스로 다루기 위한 것으로, 향후 devmem TCP(Device Memory TCP)의 기반이 됩니다.

6.9에서는 devmem TCP가 도입되어 NIC에서 수신한 데이터를 CPU 메모리를 거치지 않고 GPU/FPGA 등의 디바이스 메모리에 직접 전달하는 zero-copy 수신이 가능해졌습니다. 이는 SO_DEVMEM_DONTNEED 소켓 옵션과 새로운 recvmsg 메시지 헤더를 통해 사용자 공간에 노출됩니다.

버전변경 내용영향
6.0kfree_skb_reason() 도입: SKB_DROP_REASON_* 열거형 40여 종perf/bpftrace 드롭 원인 정밀 추적 가능
6.1page_pool과 sk_buff 통합 심화: page_pool_return_skb_page()RX 페이지 재활용 효율 극대화, DMA 매핑 캐시 유지
6.2skb_drop_reason 확장: TCP 시퀀스, 윈도우 오류 등 세분화TCP 수신 경로 드롭 디버깅 상세화
6.3AF_XDP 경로 sk_buff 제로 할당 최적화: xsk_buff_alloc 통합AF_XDP zero-copy 모드 지연 시간 감소
6.4skb_frag_t 내 netmem 지원 준비 (bio_vec 하위 호환 유지)다중 메모리 유형 단편 지원 기반
6.6netmem 추상 메모리 타입 도입 (Mina Almasry): net_iov 지원GPU/디바이스 메모리 직접 수신 기반 마련
6.7kfree_skb_reason 이벤트 tracepoint 강화: 드롭 이유 → bpf_map 집계운영 환경 드롭 분석 자동화
6.8page_pool 메모리 제공자(Memory Provider) 인터페이스 도입devmem TCP를 위한 NIC ↔ 디바이스 메모리 DMA 추상화
6.9devmem TCP 도입 (SO_DEVMEM_DONTNEED, MSG_SOCK_DEVMEM): NIC → GPU 직접 전달AI/ML 워크로드에서 CPU 메모리 복사 완전 제거
6.10sk_buff napi_id 활용 강화: busy polling 정밀도 향상저지연 네트워킹(DPDK 대체) 성능 개선
6.11skb_drop_reason 열거형 서브시스템 분리 (SUBSYS 비트 필드)드라이버별 커스텀 드롭 이유 정의 가능
6.12netmem/net_iov 기반 zero-copy TX 실험적 지원TX 경로도 디바이스 메모리 직접 전송 방향 진행 중
버전 이력 활용 팁: 특정 커널 버전에서 sk_buff 관련 문제가 발생했을 때 이 이력을 참조해 해당 버전 인근에서 도입된 변경이 있는지 확인하세요. 특히 드롭 추적은 kfree_skb_reason() 도입 이전(6.0 이전)과 이후의 접근 방식이 크게 다르며, 체크섬 오프로드 문제는 2.6.18 이전 CHECKSUM_HW 시대 드라이버를 다룰 때 주의가 필요합니다.
2.6.x 3.x 4.x 5.x 6.x 2.6.18 2.6.24 3.2 3.15 4.8 4.14 4.18 5.1 5.3 5.10 6.0 6.1 6.6 6.9 체크섬 PARTIAL/COMPLETE 헤더 16비트 오프셋 전환 skb_frag_t → bio_vec 정렬 XDP 도입 MSG_ZEROCOPY ubuf_info 추가 skb_ext 정식 도입 kfree_skb _reason() skb_zerocopy() 페이지 이전 UDP GSO GSO_UDP_L4 skb_ensure _writable() netmem 추상화 도입 devmem TCP NIC→GPU 직접 sk_buff 주요 변화 타임라인 (2.6 ~ 6.x)

skb 확장 (skb_ext)

커널 5.x부터 sk_buff에 가변 메타데이터를 동적으로 연결하는 skb_ext 메커니즘이 도입되었습니다. 이전에는 conntrack 포인터, IPsec secpath 등이 skb 내부에 직접 포함되어 항상 메모리를 차지했으나, skb_ext를 통해 필요할 때만 할당되어 메모리 효율이 크게 개선되었습니다.

skb_ext 확장 아키텍처 struct sk_buff extensions (skb_ext *) active_extensions (u8) len, data, protocol... refcount, users struct skb_ext refcnt (refcount_t) chunks (u8) — 할당 청크 수 data[] — 가변 확장 데이터 extensions SKB_EXT_SEC_PATH IPsec xfrm_state 참조 SKB_EXT_BRIDGE_NF br_netfilter 상태 SKB_EXT_TC TC cls_act 메타데이터 SKB_EXT_MPTCP MPTCP 옵션 (6.x+) skb_clone 시 skb_ext 동작 원본 skb clone skb skb_ext 공유 (refcnt++) COW: 수정 시 skb_ext_cow() → 독립 복사본 생성
skb_ext는 필요할 때만 할당되며, clone 시 refcount 공유 + COW(Copy-on-Write) 방식으로 동작
/* include/linux/skbuff.h — skb_ext 구조체 */
struct skb_ext {
    refcount_t  refcnt;     /* 참조 카운트 (clone 시 공유) */
    u8          offset[SKB_EXT_NUM]; /* 각 확장의 data[] 내 오프셋 */
    u8          chunks;     /* 할당된 청크 수 (64B 단위) */
    char        data[];     /* 가변 길이 확장 데이터 */
};

/* 확장 타입 열거형 */
enum skb_ext_id {
    SKB_EXT_BRIDGE_NF,    /* br_netfilter 상태 (struct nf_bridge_info) */
    SKB_EXT_SEC_PATH,     /* IPsec 보안 경로 (struct sec_path) */
    SKB_EXT_MPTCP,        /* MPTCP 옵션 (struct mptcp_ext) */
    TC_SKB_EXT,           /* TC cls_act 메타데이터 (struct tc_skb_ext) */
    SKB_EXT_NUM           /* 총 확장 타입 수 */
};

/* skb_ext 추가 — 해당 타입의 확장 공간을 할당하고 포인터 반환 */
void *skb_ext_add(struct sk_buff *skb, enum skb_ext_id id)
{
    struct skb_ext *new;
    /* active_extensions 비트맵에 id 설정 */
    skb->active_extensions |= 1 << id;
    /* 확장 공간 할당 또는 기존 공간에서 오프셋 반환 */
    return skb->extensions->data + skb->extensions->offset[id];
}

/* skb_ext 조회 — 해당 확장이 있으면 포인터, 없으면 NULL */
static inline void *skb_ext_find(const struct sk_buff *skb,
                                  enum skb_ext_id id)
{
    if (skb->active_extensions & (1 << id))
        return skb->extensions->data + skb->extensions->offset[id];
    return NULL;
}

/* Netfilter conntrack 연결: nf_ct_get()으로 conntrack 참조 */
static inline struct nf_conn *nf_ct_get(
    const struct sk_buff *skb,
    enum ip_conntrack_info *ctinfo)
{
    /* skb->_nfct에서 conntrack 포인터와 상태 정보 추출 */
    unsigned long nfct = skb->_nfct;
    *ctinfo = nfct & NFCT_INFOMASK;
    return (struct nf_conn *)(nfct & NFCT_PTRMASK);
}

/* TC 확장: cls_act에서 skb에 메타데이터 연결 */
struct tc_skb_ext {
    __u32   chain;       /* TC 체인 번호 */
    __u16   mru;         /* TC Maximum Receive Unit */
    __u16   zone;        /* conntrack zone */
    u8      post_ct:1;   /* CT action 이후 여부 */
    u8      post_ct_snat:1;
    u8      post_ct_dnat:1;
};

/* TC에서 skb_ext 사용 예 */
struct tc_skb_ext *ext = skb_ext_add(skb, TC_SKB_EXT);
if (ext)
    ext->chain = chain_index;
확장 타입구조체크기 (약)사용 서브시스템
SKB_EXT_SEC_PATHsec_path~40BIPsec/xfrm — SA 참조 배열
SKB_EXT_BRIDGE_NFnf_bridge_info~48Bbr_netfilter — 원본 포트/MAC 보존
TC_SKB_EXTtc_skb_ext~12BTC cls_act — 체인/zone/CT 메타
SKB_EXT_MPTCPmptcp_ext~24BMPTCP — DSS/DSN 매핑
💡

성능 영향: skb_ext 도입 전, struct sec_pathstruct nf_bridge_info는 skb 내에 항상 포인터를 차지했습니다 (각 8바이트). IPsec이나 bridge를 사용하지 않는 대다수 패킷에서 이 공간이 낭비되었습니다. skb_ext 전환 후 sizeof(struct sk_buff)가 약 16바이트 줄어들었고, 이는 수백만 동시 패킷을 처리하는 환경에서 상당한 메모리 절감입니다.

page_pool 기반 고성능 할당

커널 5.17+에서 도입된 page_pool은 네트워크 드라이버의 skb 데이터 버퍼 할당을 혁신적으로 개선합니다. DMA 매핑을 캐시하고, 해제된 페이지를 재활용하며, bulk 할당으로 lock contention을 최소화합니다. mlx5, ice, i40e, bnxt 등 주요 고성능 드라이버가 page_pool을 사용합니다.

page_pool 재활용 아키텍처 Buddy Allocator (초기 할당/부족 시) page_pool alloc.cache[] (128) ring.queue (1024) DMA 매핑 캐시 + per-CPU 접근 bulk NIC 드라이버 RX page → DMA → skb alloc struct sk_buff frags[] → page_pool page 네트워크 스택 처리 L2 → L3 → L4 → 소켓 skb 해제 page_pool_put_page() 재활용! DMA unmap 생략 Slow path 해제 dma_unmap + put_page() ring full 성능 비교 (10Gbps NIC, 64B 패킷) 기존: alloc_page + dma_map 매번 → ~150ns/pkt page_pool: 캐시 히트 + DMA skip → ~30ns/pkt
page_pool은 DMA 매핑을 캐시하고 해제된 페이지를 재활용하여 할당/해제 비용을 5배 이상 절감
/* include/net/page_pool/types.h — page_pool 생성 파라미터 */
struct page_pool_params {
    unsigned int  flags;       /* PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV */
    unsigned int  order;       /* page order (0 = 4KB, 1 = 8KB) */
    unsigned int  pool_size;   /* ring 크기 (기본 1024) */
    int           nid;         /* NUMA 노드 (-1 = 현재 노드) */
    struct device *dev;         /* DMA 매핑 대상 디바이스 */
    enum dma_data_direction dma_dir; /* DMA_FROM_DEVICE (RX) */
    unsigned int  max_len;     /* 최대 데이터 길이 */
    unsigned int  offset;      /* 데이터 시작 오프셋 */
};

/* 드라이버 초기화: page_pool 생성 */
struct page_pool_params pp_params = {
    .flags     = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV,
    .order     = 0,           /* 4KB 페이지 */
    .pool_size = 1024,
    .nid       = dev_to_node(dev),
    .dev       = &pdev->dev,
    .dma_dir   = DMA_FROM_DEVICE,
    .max_len   = PAGE_SIZE,
    .offset    = XDP_PACKET_HEADROOM,
};
struct page_pool *pool = page_pool_create(&pp_params);

/* 수신 경로: page_pool에서 페이지 할당 */
struct page *page = page_pool_dev_alloc_pages(pool);
/* → alloc.cache[]에서 O(1) 반환 (캐시 히트)
 * → 캐시 비면 ring.queue에서 bulk refill
 * → ring도 비면 buddy allocator + DMA 매핑 */

dma_addr_t dma = page_pool_get_dma_addr(page);
/* DMA 주소가 이미 캐시됨 — dma_map_page() 호출 불필요! */

/* skb 생성 후 page_pool 페이지를 frag로 추가 */
struct sk_buff *skb = napi_alloc_skb(napi, 256);
skb_add_rx_frag(skb, 0, page, offset, len, PAGE_SIZE);
skb_mark_for_recycle(skb); /* 해제 시 page_pool로 반환 */

/* skb 해제 시: page_pool_put_page()로 자동 재활용 */
/* consume_skb(skb) → skb_free_frag() → page_pool_put_page()
 * → alloc.cache[]에 반환 (fast path)
 * → 또는 ring.queue에 반환 (다른 CPU에서 해제 시) */
코드 설명
  • page_pool_paramsinclude/net/page_pool/types.h에 정의된 생성 파라미터입니다. PP_FLAG_DMA_MAP은 할당 시 자동 DMA 매핑을, PP_FLAG_DMA_SYNC_DEV는 재활용 시 DMA sync를 수행합니다. offsetXDP_PACKET_HEADROOM(256바이트)을 설정하면 XDP 프로그램이 headroom에서 헤더를 조작할 수 있습니다.
  • page_pool_create()net/core/page_pool.c에서 per-CPU alloc cache(배열)와 ptr_ring 기반 반환 큐를 초기화합니다. 드라이버 probe()에서 RX 큐당 하나의 pool을 생성하는 것이 일반적입니다.
  • page_pool_dev_alloc_pages()할당 경로는 3단계 폴백입니다: (1) per-CPU alloc.cache[]에서 O(1) 반환 → (2) 캐시가 비면 ring.queue에서 bulk refill → (3) ring도 비면 buddy allocator + dma_map_page(). 대부분의 경우 단계 1에서 완료되어 할당 비용이 극히 낮습니다.
  • page_pool_get_dma_addr()page의 struct page 메타데이터에 캐시된 DMA 주소를 반환합니다. 기존 방식은 매 패킷마다 dma_map_page()를 호출했지만, page_pool에서는 페이지 재활용 시 DMA 매핑이 유지되므로 IOMMU 변환 비용을 완전히 제거합니다.
  • skb_mark_for_recycle()skb 해제 시 page를 put_page() 대신 page_pool_put_page()로 반환하도록 표시합니다. consume_skb()skb_release_data()napi_pp_put_page() 경로로 page_pool의 alloc cache나 ring에 반환되어 재활용됩니다.
비교 항목기존 (alloc_page + dma_map)page_pool
페이지 할당매번 buddy allocator 호출per-CPU 캐시에서 O(1)
DMA 매핑매번 dma_map_page()캐시된 DMA 주소 재사용
해제dma_unmap + put_page()캐시에 반환 (unmap 생략)
NUMA 인식수동 관리 필요nid 파라미터로 자동
XDP 호환직접 구현 필요내장 XDP headroom 지원
bulk 할당지원 안 함page_pool_alloc_pages_batch()
ℹ️

page_pool 통계 확인: /sys/kernel/debug/page_pool/에서 각 풀의 할당/재활용/실패 통계를 확인할 수 있습니다. ethtool -S eth0 | grep page_pool로 드라이버별 통계도 확인 가능합니다. 재활용율이 90% 이하면 ring 크기 증가 또는 NUMA 문제를 점검하세요.

XDP와 sk_buff 인터페이스

XDP(eXpress Data Path)는 sk_buff 할당 이전 단계에서 패킷을 처리하는 고성능 프레임워크입니다. NIC 드라이버 내부에서 xdp_buff라는 경량 구조체로 패킷을 표현하며, XDP 프로그램의 판정(verdict)에 따라 패킷을 드롭/포워딩하거나 일반 네트워크 스택으로 전달합니다.

XDP ↔ sk_buff 변환 흐름 NIC RX (DMA) ring buffer → page struct xdp_buff data, data_hard_start data_end, data_meta XDP BPF 프로그램 bpf_xdp_adjust_head() bpf_redirect_map() XDP_DROP XDP_TX XDP_REDIRECT XDP_PASS xdp_buff → sk_buff 변환 build_skb() 또는 __xdp_build_skb_from_frame() struct sk_buff 일반 네트워크 스택 진입 netif_receive_skb() → L2/L3/L4 XDP Generic (SKB 모드) sk_buff가 이미 존재 __skb_buff로 래핑 성능 이점 감소
XDP는 sk_buff 할당 이전에 패킷을 처리. XDP_PASS 시에만 build_skb()로 sk_buff 변환
/* include/net/xdp.h — xdp_buff 구조체 (sk_buff보다 훨씬 경량) */
struct xdp_buff {
    void              *data;           /* 패킷 데이터 시작 (L2) */
    void              *data_end;       /* 패킷 데이터 끝 */
    void              *data_meta;      /* 메타데이터 시작 (data 앞) */
    void              *data_hard_start;/* 버퍼 절대 시작 */
    struct xdp_rxq_info *rxq;         /* RX 큐 정보 */
    struct xdp_txq_info *txq;         /* TX 큐 정보 */
    u32               frame_sz;        /* 전체 프레임 크기 */
    u32               flags;           /* XDP_FLAGS_* */
};

/* xdp_buff → sk_buff 변환 (XDP_PASS 시) */
struct sk_buff *xdp_build_skb_from_buff(struct xdp_buff *xdp)
{
    unsigned int headroom = xdp->data - xdp->data_hard_start;
    unsigned int data_len = xdp->data_end - xdp->data;
    struct sk_buff *skb;

    /* build_skb: 기존 버퍼에 sk_buff 메타데이터만 생성 */
    skb = build_skb(xdp->data_hard_start, xdp->frame_sz);
    if (!skb)
        return NULL;

    skb_reserve(skb, headroom);
    __skb_put(skb, data_len);

    /* XDP 메타데이터가 있으면 skb에 전달 */
    if (xdp->data_meta != xdp->data) {
        int metasize = xdp->data - xdp->data_meta;
        skb_metadata_set(skb, metasize);
    }

    return skb;
}

/* XDP BPF 프로그램 예: 특정 포트 패킷만 PASS, 나머지 DROP */
SEC("xdp")
int xdp_filter(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    struct ethhdr *eth = data;

    if (data + sizeof(*eth) > data_end)
        return XDP_DROP;

    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;  /* IP 아니면 일반 스택으로 */

    struct iphdr *ip = data + sizeof(*eth);
    if ((void *)(ip + 1) > data_end)
        return XDP_DROP;

    if (ip->protocol == IPPROTO_TCP) {
        struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);
        if ((void *)(tcp + 1) > data_end)
            return XDP_DROP;
        if (tcp->dest == bpf_htons(80))
            return XDP_PASS;  /* HTTP → sk_buff 생성 → 스택 진입 */
    }
    return XDP_DROP;  /* sk_buff 할당 없이 즉시 드롭 */
}

/* XDP 메타데이터: xdp_buff → sk_buff 전달
 * BPF 프로그램이 data_meta 영역에 커스텀 메타데이터 기록 가능 */
SEC("xdp")
int xdp_with_meta(struct xdp_md *ctx)
{
    /* 메타데이터 영역 확보 (data 앞으로 4바이트) */
    if (bpf_xdp_adjust_meta(ctx, -4))
        return XDP_PASS;

    __u32 *meta = (void *)(long)ctx->data_meta;
    if ((void *)(meta + 1) > (void *)(long)ctx->data)
        return XDP_PASS;

    *meta = 0xCAFE;  /* 커스텀 마크 */
    return XDP_PASS;
    /* → sk_buff 변환 후 skb_metadata_len(skb) == 4
     * → TC BPF에서 __sk_buff->data_meta로 접근 가능 */
}
XDP 액션sk_buff 할당동작성능
XDP_DROP안 함패킷 즉시 드롭, 페이지 반환~24Mpps (64B)
XDP_TX안 함같은 NIC으로 즉시 재전송~14Mpps
XDP_REDIRECT안 함다른 NIC/CPU/AF_XDP로 전달~12Mpps
XDP_PASS생성build_skb() → 일반 스택일반 스택 수준
XDP_ABORTED안 함에러 발생, tracepoint 기록
⚠️

XDP Generic vs Native: XDP_FLAGS_SKB_MODE(Generic)는 sk_buff가 이미 할당된 후 XDP 프로그램을 실행합니다. 따라서 XDP_DROP해도 skb 할당 비용이 발생하며, 성능 이점이 크게 감소합니다. 진정한 고성능을 위해서는 드라이버가 네이티브 XDP를 지원해야 합니다 (XDP_FLAGS_DRV_MODE). ethtool -i eth0으로 드라이버를 확인하고, ip link set dev eth0 xdp obj prog.o으로 로드합니다.

패킷 타임스탬핑 (SO_TIMESTAMPING)

정밀한 네트워크 지연 측정, PTP(Precision Time Protocol) 동기화, 금융 거래 시스템 등에서 패킷의 정확한 송수신 시각이 필요합니다. Linux는 SO_TIMESTAMPING 소켓 옵션으로 하드웨어 타임스탬프(NIC PHY 수준)부터 소프트웨어 타임스탬프(커널 네트워크 스택)까지 다양한 수준의 타임스탬핑을 지원하며, 이 정보는 sk_buff를 통해 전달됩니다.

패킷 타임스탬프 삽입 지점 전송 경로 (TX) sendmsg() SW TX ① SOF_TIMESTAMPING_TX_SOFTWARE SCHED TX ② SOF_TIMESTAMPING_TX_SCHED dev_queue_xmit() HW TX ③ SOF_TIMESTAMPING_TX_HARDWARE (NIC PHY, ns 정밀도) 수신 경로 (RX) HW RX ① SOF_TIMESTAMPING_RX_HARDWARE (NIC PHY, ns 정밀도) netif_receive_skb() SW RX ② SOF_TIMESTAMPING_RX_SOFTWARE (ktime_get_real(), μs 정밀도) recvmsg() + cmsg 전달 skb 내부 타임스탬프 저장 skb_hwtstamps(skb)->hwtstamp HW 타임스탬프 (ktime_t, ns) skb->tstamp (= skb_mstamp_ns) SW 타임스탬프 (ktime_t, ns)
HW 타임스탬프는 NIC PHY 수준(ns 정밀도), SW 타임스탬프는 커널 softirq 수준(μs 정밀도)
/* include/linux/skbuff.h — 타임스탬프 관련 구조체 */
struct skb_shared_hwtstamps {
    union {
        ktime_t hwtstamp;   /* HW 타임스탬프 (NIC PHY) */
        void   *netdev_data; /* 드라이버별 데이터 */
    };
};

/* skb에서 HW 타임스탬프 접근 */
static inline struct skb_shared_hwtstamps *skb_hwtstamps(
    struct sk_buff *skb)
{
    return &skb_shinfo(skb)->hwtstamps;
}

/* NIC 드라이버: 수신 시 HW 타임스탬프 기록 */
static void my_nic_rx_hwtstamp(struct sk_buff *skb,
                                u64 hw_ns)
{
    struct skb_shared_hwtstamps *hwts = skb_hwtstamps(skb);
    hwts->hwtstamp = ns_to_ktime(hw_ns);
    /* → recvmsg()에서 SOF_TIMESTAMPING_RAW_HARDWARE cmsg로 전달 */
}

/* 사용자 공간: SO_TIMESTAMPING 설정 */
int flags = SOF_TIMESTAMPING_RX_HARDWARE    /* 수신 HW 타임스탬프 */
          | SOF_TIMESTAMPING_TX_HARDWARE    /* 전송 HW 타임스탬프 */
          | SOF_TIMESTAMPING_RAW_HARDWARE   /* 원시 HW 시각 (PTP 클럭) */
          | SOF_TIMESTAMPING_SOFTWARE       /* SW 타임스탬프 */
          | SOF_TIMESTAMPING_OPT_TSONLY;    /* 타임스탬프만 (페이로드 생략) */
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags));

/* recvmsg()로 타임스탬프 수신 */
struct msghdr msg = { .msg_control = cbuf, .msg_controllen = sizeof(cbuf) };
recvmsg(fd, &msg, 0);

/* cmsg에서 타임스탬프 추출 */
struct cmsghdr *cm;
for (cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) {
    if (cm->cmsg_level == SOL_SOCKET &&
        cm->cmsg_type == SO_TIMESTAMPING) {
        struct timespec *ts = (struct timespec *)CMSG_DATA(cm);
        /* ts[0] = SW 타임스탬프 (SOF_TIMESTAMPING_SOFTWARE)
         * ts[1] = 예약 (사용 안 함)
         * ts[2] = HW 타임스탬프 (SOF_TIMESTAMPING_RAW_HARDWARE) */
        printf("HW: %ld.%09ld
", ts[2].tv_sec, ts[2].tv_nsec);
    }
}

/* TX 타임스탬프: 전송 완료 시 errqueue에서 수신 */
recvmsg(fd, &msg, MSG_ERRQUEUE);
/* → SOF_TIMESTAMPING_TX_HARDWARE cmsg에 전송 시각 포함
 * → NIC 드라이버가 TX 완료 인터럽트에서 HW 타임스탬프 기록 */
타임스탬프 종류정밀도지연 소스용도
HW 타임스탬프 (PHY)~1nsNIC PTP 클럭PTP 동기화, 금융 트레이딩
SW 타임스탬프 (커널)~1μsktime_get_real() (softirq)일반 지연 측정, tcpdump
TX SCHED~1μsqdisc 진입 시점큐잉 지연 측정
TX ACK (TCP)~1μsACK 수신 시점RTT 측정
💡

PTP 하드웨어 지원 확인: ethtool -T eth0으로 NIC의 HW 타임스탬핑 지원 여부를 확인합니다. hardware-transmit/hardware-receive/hardware-raw-clock이 표시되면 HW 타임스탬프를 사용할 수 있습니다. Intel i210/i225, Mellanox ConnectX-4+, Broadcom BCM57416 등이 대표적인 PTP 지원 NIC입니다.

NAPI와 GRO 상세 흐름

NAPI(New API)는 인터럽트(Interrupt)와 폴링을 결합하여 고속 패킷 수신을 효율적으로 처리합니다. GRO(Generic Receive Offload)는 NAPI poll 내부에서 동일 플로우의 패킷들을 하나의 큰 sk_buff로 병합하여 프로토콜 스택 처리 오버헤드를 줄입니다. 이 두 메커니즘은 현대 Linux 네트워크 성능의 핵심 축입니다.

NAPI poll → GRO → 프로토콜 전달 흐름 NIC RX IRQ napi_schedule() IRQ 비활성화 softirq 스케줄 (NET_RX) napi_poll() 최대 weight(64)개 패킷 처리 budget 소진 → 계속 poll GRO 병합 엔진 gro_list[] (해시 버킷) napi_gro_receive(skb) 각 패킷 GRO_MERGED 기존 skb에 병합 (frag 추가) GRO_HELD gro_list에 대기 (더 병합 기대) GRO_NORMAL 병합 불가 → 즉시 스택 전달 gro_normal_list → netif_receive_skb_list() 병합된 super-skb를 일반 스택에 배치 전달 flush/timeout L3/L4 프로토콜 스택 (ip_rcv → tcp_v4_rcv) napi_complete_done() IRQ 재활성화 budget 남음
NAPI poll에서 GRO가 동일 플로우 패킷을 병합 → 하나의 super-skb로 프로토콜 스택 전달
/* include/linux/netdevice.h — NAPI 구조체 */
struct napi_struct {
    struct list_head  poll_list;    /* softirq 폴링 리스트 */
    unsigned long     state;        /* NAPI_STATE_SCHED 등 */
    int               weight;       /* 한 번 poll에서 처리할 최대 패킷 수 (기본 64) */
    int               defer_hard_irqs_count;
    unsigned long     gro_bitmask;  /* GRO 활성 프로토콜 비트맵 */
    int               (*poll)(struct napi_struct *, int); /* 드라이버 poll 함수 */
    struct list_head  rx_list;      /* GRO 병합 완료 skb 리스트 */
    int               rx_count;     /* rx_list 내 skb 수 */
    struct gro_list   gro_hash[GRO_HASH_BUCKETS]; /* GRO 해시 테이블 */
};

/* 드라이버 poll 함수 패턴 */
static int my_driver_poll(struct napi_struct *napi, int budget)
{
    int work_done = 0;

    while (work_done < budget) {
        struct sk_buff *skb = my_rx_one(napi);
        if (!skb)
            break;

        /* GRO에 전달: 동일 플로우 병합 시도 */
        napi_gro_receive(napi, skb);
        work_done++;
    }

    if (work_done < budget) {
        /* budget을 다 쓰지 않음 → 패킷이 없음 → IRQ 재활성화 */
        napi_complete_done(napi, work_done);
        /* → gro_list flush → IRQ unmask */
    }
    /* budget 소진 → softirq가 다시 poll 호출 예정 */

    return work_done;
}

/* GRO 내부: 병합 판단 로직 (net/core/gro.c) */
static enum gro_result dev_gro_receive(struct napi_struct *napi,
                                      struct sk_buff *skb)
{
    /* 1. 프로토콜별 GRO 콜백 호출 (inet_gro_receive 등) */
    /* 2. gro_hash[]에서 동일 플로우 검색 (5-tuple 매칭) */
    /* 3. 매칭 결과에 따라: */

    if (same_flow && !flush) {
        /* GRO_MERGED: 기존 skb에 새 패킷을 frag로 추가
         * → skb_shinfo(p)->frag_list에 연결
         * → p->len += skb->len (super-packet 크기 증가)
         * → NAPI_GRO_CB(p)->count++ */
        return GRO_MERGED;
    }

    if (same_flow && flush) {
        /* 플로우는 같지만 병합 불가 (PSH 플래그, 순서 불일치 등)
         * → 기존 skb를 flush하고 새 skb를 gro_hash에 등록 */
        napi_gro_complete(napi, pp);
    }

    /* GRO_HELD: 새 플로우 → gro_hash에 등록하고 대기
     * → 같은 플로우의 후속 패킷이 올 때까지 보류 */
    list_add(&skb->list, &napi->gro_hash[hash].list);
    return GRO_HELD;
}

/* GRO 병합 완료 → 일반 스택으로 전달 */
static void napi_gro_complete(struct napi_struct *napi,
                              struct sk_buff *skb)
{
    /* 프로토콜별 GRO complete 콜백 */
    /* → TCP: tcp_gro_complete() — 헤더 보정, 체크섬 설정
     * → skb->ip_summed = CHECKSUM_UNNECESSARY (병합된 패킷) */

    /* gro_normal_one(): rx_list에 추가 */
    gro_normal_one(napi, skb, NAPI_GRO_CB(skb)->count);
    /* → rx_count >= gro_normal_batch(8) 이면 배치 전달:
     *   gro_normal_list() → netif_receive_skb_list()
     *   → 한 번의 함수 호출로 여러 skb를 스택에 전달 */
}
코드 설명
  • napi_structinclude/linux/netdevice.h에 정의된 NAPI 구조체입니다. weight(기본 64)는 한 번의 poll에서 처리할 최대 패킷 수이며, gro_hash[]는 GRO 병합 대기 중인 skb를 플로우별로 관리하는 해시 테이블입니다.
  • 드라이버 poll 루프budget만큼 반복하면서 napi_gro_receive()로 각 skb를 GRO에 전달합니다. budget을 소진하지 않으면(work_done < budget) 패킷이 더 없다는 뜻이므로 napi_complete_done()으로 인터럽트를 재활성화합니다.
  • dev_gro_receive()net/core/gro.c에서 GRO 병합을 결정하는 핵심 함수입니다. 프로토콜별 콜백(inet_gro_receive()tcp4_gro_receive())이 5-tuple 매칭과 병합 가능 여부를 판단합니다.
  • GRO_MERGED같은 플로우의 기존 skb에 새 패킷을 frag_list로 연결하고 len을 합산합니다. 최대 64KB까지 병합하여 프로토콜 스택 진입 횟수를 대폭 줄입니다. NAPI_GRO_CB(p)->count로 병합된 패킷 수를 추적합니다.
  • napi_gro_complete()병합 완료된 super-skb를 일반 네트워크 스택으로 전달합니다. gro_normal_one()rx_list에 추가하고, gro_normal_batch(기본 8) 이상 누적되면 netif_receive_skb_list()로 배치 전달하여 함수 호출 오버헤드를 최소화합니다.
GRO 파라미터기본값조절 방법영향
NAPI weight64netif_napi_add(dev, napi, poll, weight)poll당 처리 패킷 수. 높으면 throughput↑, latency↑
gro_flush_timeout0 (즉시)sysctl net.core.gro_flush_timeout0이 아니면 타이머(Timer)로 flush → 병합 기회 증가
gro_normal_batch8sysctl net.core.gro_normal_batch배치 전달 크기. 높으면 처리량↑, 지연↑
netdev_budget300sysctl net.core.netdev_budgetsoftirq당 전체 NAPI 처리 패킷 상한
busy_poll0 (off)sysctl net.core.busy_poll소켓별 busy polling 시간 (μs)
# GRO 병합 효과 확인
$ ethtool -S eth0 | grep gro
     rx_gro_packets: 1234567        # GRO 처리된 패킷 수
     rx_gro_bytes: 987654321        # GRO 처리된 바이트

# NAPI 통계 확인
$ cat /proc/net/softnet_stat
# 컬럼: processed, dropped, time_squeeze, ..., cpu_collision, received_rps, flow_limit_count
# time_squeeze > 0: budget/time 부족으로 처리 중단 → netdev_budget 증가 고려

# Busy polling 활성화 (저지연 용도)
$ sysctl -w net.core.busy_poll=50       # 50μs 폴링
$ sysctl -w net.core.busy_read=50       # 읽기 시 50μs 폴링

# GRO flush 타임아웃 설정 (병합 기회 증가)
$ sysctl -w net.core.gro_flush_timeout=20000  # 20μs

# per-NAPI 설정 (커널 6.x+)
$ echo 100 > /sys/class/net/eth0/napi/0/gro_flush_timeout
$ echo 16 > /sys/class/net/eth0/napi/0/defer_hard_irqs
⚠️

GRO와 Netfilter 상호작용: GRO로 병합된 super-packet은 skb->len이 64KB에 달할 수 있습니다. 이 상태로 Netfilter를 통과하면 conntrack 등이 정상 동작하지만, iptables -m length 같은 패킷 길이 기반 규칙은 예상과 다르게 동작할 수 있습니다. 필요시 ethtool -K eth0 gro off로 비활성화하거나, nftables의 @th 표현식으로 개별 세그먼트 길이를 확인하세요.

GSO/TSO 분할 메커니즘 상세

GSO(Generic Segmentation Offload)는 커널이 대용량 sk_buff를 NIC의 MTU에 맞는 작은 세그먼트로 분할하는 메커니즘입니다. NIC이 TSO(TCP Segmentation Offload)를 지원하면 하드웨어가 분할하고, 미지원 시 커널의 skb_segment()가 소프트웨어로 처리합니다. 이 과정에서 skb_shared_info의 GSO 필드가 핵심 역할을 합니다.

GSO/TSO 분할 흐름 Super sk_buff (최대 64KB) skb->len = 65536 gso_size=1448, gso_segs=45 gso_type=SKB_GSO_TCPV4 전송 HW TSO? Yes NIC 하드웨어 분할 Super-packet 그대로 DMA → NIC이 분할 CPU 비용 0, 최고 성능 No skb_segment(skb, features) 소프트웨어 GSO: gso_size 기준 분할 분할된 개별 sk_buff 체인 (frag_list 연결) seg 1 (1448B) IP+TCP+payload seg 2 (1448B) IP+TCP+payload ... seg 45 (나머지) IP+TCP+payload skb_segment() 내부 동작 1. gso_size 기준 페이로드 분할 2. 각 세그먼트에 IP+TCP 헤더 복사 3. IP.id 순차 증가, TCP.seq 순차 증가 4. 마지막 세그먼트에 PSH 플래그 설정 분할 후 각 세그먼트의 gso_size=0, gso_segs=0 (더 이상 GSO 아님)
HW TSO 지원 NIC은 super-packet을 그대로 전송, 미지원 시 skb_segment()가 SW로 분할
/* skb_shared_info의 GSO 관련 필드 */
struct skb_shared_info {
    unsigned short gso_size;  /* 세그먼트 크기 (MSS) */
    unsigned short gso_segs;  /* 세그먼트 수 */
    unsigned short gso_type;  /* GSO 타입 비트맵 */
    /* ... */
};

/* GSO 타입 상수 */
#define SKB_GSO_TCPV4    (1 << 0)  /* TCP/IPv4 분할 */
#define SKB_GSO_TCPV6    (1 << 4)  /* TCP/IPv6 분할 */
#define SKB_GSO_UDP_L4   (1 << 17) /* UDP L4 분할 (4.18+) */
#define SKB_GSO_GRE      (1 << 6)  /* GRE 터널 내부 분할 */
#define SKB_GSO_GRE_CSUM (1 << 7)  /* GRE+체크섬 */
#define SKB_GSO_UDP_TUNNEL     (1 << 9)  /* VXLAN/Geneve */
#define SKB_GSO_UDP_TUNNEL_CSUM (1 << 10) /* 터널+체크섬 */
#define SKB_GSO_PARTIAL  (1 << 13) /* 부분 GSO (외부 헤더만 HW) */

/* skb_segment(): SW GSO 분할 핵심 함수 (net/core/skbuff.c) */
struct sk_buff *skb_segment(struct sk_buff *head_skb,
                           netdev_features_t features)
{
    struct sk_buff *segs = NULL;
    unsigned int mss = skb_shinfo(head_skb)->gso_size;
    unsigned int doffset = head_skb->data - skb_mac_header(head_skb);
    unsigned int offset = doffset;
    unsigned int tnl_hlen, headroom;
    unsigned int len, nfrags;

    /* 각 세그먼트에 대해: */
    do {
        struct sk_buff *nskb;
        int hsize, size;

        /* 1. 새 skb 할당 */
        nskb = alloc_skb(hsize + doffset + headroom, GFP_ATOMIC);

        /* 2. L2+L3+L4 헤더 복사 (공통 헤더) */
        skb_copy_from_linear_data(head_skb, skb_put(nskb, doffset),
                                  doffset);

        /* 3. 페이로드를 mss 크기만큼 복사/참조 */
        if (!sg && !nskb->remcsum_offload) {
            /* linear 복사 */
            skb_copy_from_linear_data_offset(head_skb, offset,
                skb_put(nskb, size), size);
        } else {
            /* SG: page fragment 참조 (zero-copy) */
            skb_fill_page_desc(nskb, i, frag->bv_page,
                               frag->bv_offset, frag_size);
        }

        /* 4. 각 세그먼트 고유 필드 설정 */
        skb_shinfo(nskb)->gso_size = 0;  /* 더 이상 GSO 아님 */
        skb_shinfo(nskb)->gso_segs = 0;
        skb_shinfo(nskb)->gso_type = 0;

        /* 5. IP 헤더: tot_len 갱신, id 순차 증가 */
        /* 6. TCP 헤더: seq 순차 증가, 마지막 seg에만 PSH */

        offset += size;
    } while (offset < head_skb->len);

    return segs;  /* 분할된 skb 체인 (next 포인터 연결) */
}

/* GSO 분할 트리거 지점: dev_queue_xmit() → validate_xmit_skb() */
static struct sk_buff *validate_xmit_skb(struct sk_buff *skb,
                                        struct net_device *dev, bool *again)
{
    netdev_features_t features = dev->features;

    if (skb_is_gso(skb)) {
        /* NIC이 해당 GSO 타입을 지원하는지 확인 */
        if (skb_gso_ok(skb, features))
            return skb;  /* HW TSO: 그대로 전달 */

        /* SW GSO: 커널에서 분할 */
        struct sk_buff *segs = skb_gso_segment(skb, features);
        consume_skb(skb);  /* 원본 super-packet 해제 */
        return segs;
    }
    return skb;
}

/* GSO 관련 유틸리티 함수 */
static inline bool skb_is_gso(const struct sk_buff *skb) {
    return skb_shinfo(skb)->gso_size;
}
static inline unsigned int skb_gso_network_seglen(const struct sk_buff *skb) {
    /* gso_size + L4 헤더 + L3 헤더 = 실제 세그먼트의 IP 총 길이 */
    return skb_shinfo(skb)->gso_size +
           skb_network_header_len(skb) + skb_transport_header_len(skb);
}
GSO 타입프로토콜커널 버전특이사항
SKB_GSO_TCPV4TCP/IPv42.6+가장 기본적인 TSO. 대부분 NIC이 HW 지원
SKB_GSO_TCPV6TCP/IPv62.6+IPv6 확장 헤더가 있으면 SW fallback 가능
SKB_GSO_UDP_L4UDP4.18+UDP GSO (USO). QUIC 등에서 활용. HW 지원 희소
SKB_GSO_GREGRE 터널(Tunnel)3.10+외부 GRE 헤더 + 내부 TCP 분할
SKB_GSO_UDP_TUNNELVXLAN/Geneve3.12+외부 UDP + 내부 TCP 분할
SKB_GSO_PARTIAL다양4.7+외부 헤더만 HW, 내부는 SW. tunnel GSO 최적화
SKB_GSO_SCTPSCTP4.15+SCTP 청크 기반 분할
💡

GSO vs TSO 성능 비교: HW TSO는 CPU 비용이 0에 가깝습니다(DMA 한 번으로 64KB 전송). SW GSO는 skb_segment()에서 세그먼트 수만큼 메모리 할당+헤더 복사가 필요하지만, 그래도 사용자 공간에서 sendmsg()를 45번 호출하는 것보다 훨씬 효율적입니다. 시스콜 오버헤드를 한 번으로 줄이는 것이 GSO의 핵심 이점입니다. ethtool -k eth0 | grep segmentation으로 HW 지원 여부를 확인하세요.

skb_shared_info: frags[] vs frag_list 상세

sk_buff의 비선형 데이터는 두 가지 방식으로 표현됩니다: frags[](page fragment 배열)과 frag_list(skb 체인). 이 두 구조는 목적과 사용 상황이 완전히 다르며, 혼동하면 심각한 버그가 발생합니다.

frags[] vs frag_list 구조 비교 frags[] (Scatter-Gather) sk_buff head → linear data len=4096, data_len=3072 skb_shared_info nr_frags = 3 frag_list = NULL frags[0]: page A, 1024B frags[1]: page B, 1024B frags[2]: page C, 1024B 물리 페이지 (struct page) page A page B page C MAX_SKB_FRAGS = 17 (보통) DMA SG 전송에 최적화 NIC scatter-gather 직접 지원 skb_add_rx_frag()로 추가 frag_list (skb 체인) sk_buff (head) linear: IP+TCP 헤더 len=전체, data_len=하위합 skb_shared_info nr_frags = 0 frag_list → skb2 sk_buff (skb2) payload part 1 sk_buff (skb3) payload part 2 next sk_buff (skb4) payload part 3 크기 제한 없음 (skb 체인) GRO 병합, IP 재조합에 사용 각 skb가 독립적 메타데이터 SG DMA에 직접 사용 불가 전송 전 linearize 필요할 수 있음
frags[]는 page 배열로 SG DMA에 최적화, frag_list는 skb 체인으로 GRO/IP 재조합에 사용
특성frags[] (page fragments)frag_list (skb chain)
저장 형태skb_frag_t 배열 (page+offset+size)struct sk_buff 연결 리스트(Linked List)
최대 개수MAX_SKB_FRAGS (보통 17)제한 없음
DMA SG직접 SG 매핑 가능불가 — linearize 또는 변환 필요
오버헤드frag당 16바이트 (page+offset+size)skb당 ~240바이트 (전체 sk_buff)
주요 사용처NIC RX (skb_add_rx_frag), sendfile, spliceGRO 병합, IP defrag, GSO 분할 결과
데이터 접근skb_frag_page(), skb_frag_off()skb_walk_frags(skb, frag_skb)
len/data_lendata_len = frags 총합data_len = frag_list skb들의 len 총합
/* frags[] 접근 패턴 */
struct skb_shared_info *si = skb_shinfo(skb);
for (int i = 0; i < si->nr_frags; i++) {
    skb_frag_t *f = &si->frags[i];
    struct page *page = skb_frag_page(f);
    unsigned int off = skb_frag_off(f);
    unsigned int sz = skb_frag_size(f);
    /* kmap_local_page(page) + off 로 데이터 접근 */
}

/* frag_list 순회 패턴 */
struct sk_buff *frag_iter;
skb_walk_frags(skb, frag_iter) {
    /* frag_iter는 frag_list의 각 skb */
    process_fragment(frag_iter->data, frag_iter->len);
}

/* 전체 skb 데이터를 순차 복사하는 범용 함수 */
/* skb_copy_bits(): linear + frags[] + frag_list 모두 처리 */
int skb_copy_bits(const struct sk_buff *skb, int offset,
                  void *to, int len)
{
    /* 1. linear 영역에서 복사 */
    /* 2. frags[]에서 복사 */
    /* 3. frag_list의 각 skb에서 재귀적으로 복사 */
}

/* MAX_SKB_FRAGS 계산 */
#define MAX_SKB_FRAGS (65536 / PAGE_SIZE + 1)
/* PAGE_SIZE=4096 → MAX_SKB_FRAGS=17
 * 64KB 데이터를 frags로 표현하는 데 필요한 최대 페이지 수
 * +1은 페이지 경계 걸침 고려 */
⚠️

frags[]와 frag_list 혼용 주의: 하나의 skb에 frags[]와 frag_list가 동시에 존재할 수 있습니다. skb->data_len은 두 영역의 합산입니다. skb_linearize()는 모든 비선형 데이터(frags[] + frag_list)를 linear 영역으로 합치므로, 대용량 패킷에서 호출하면 거대한 연속 메모리 할당이 필요해 실패할 수 있습니다. GRO로 병합된 64KB super-packet에 skb_linearize()를 호출하는 것은 안티패턴입니다.

VLAN 태그 처리와 sk_buff

Linux 커널은 VLAN 태그를 두 가지 방식으로 처리합니다: 하드웨어 가속(HW VLAN acceleration)소프트웨어 처리. NIC이 VLAN 태그를 추출/삽입하는 HW 가속 방식이 더 효율적이며, 대부분의 현대 NIC이 지원합니다.

VLAN 태그 RX 처리: HW 가속 vs SW HW VLAN Acceleration (대부분의 NIC) NIC: VLAN 태그 추출 RX descriptor에 기록 __vlan_hwaccel_put_tag(skb) skb->vlan_tci = tag, vlan_present=1 skb->data → IP 헤더 시작 VLAN 태그는 skb 메타에만 존재 빠름! SW VLAN 처리 (HW 미지원 또는 QinQ) NIC: 원시 프레임 전달 VLAN 태그 inline __vlan_get_tag(skb, &tag) Ethernet 프레임 내부에서 파싱 skb_vlan_untag(skb) 4B VLAN 태그 제거 + skb 메타 설정 sk_buff VLAN 관련 필드 skb->vlan_proto ETH_P_8021Q (0x8100) 또는 ETH_P_8021AD (QinQ) skb->vlan_tci PCP(3bit) | DEI(1bit) | VID(12bit) skb_vlan_tag_get(skb) → VID 추출
HW 가속: NIC이 VLAN 태그를 RX descriptor로 추출하여 skb 메타데이터에 저장. 패킷 데이터에서 4바이트 절약
/* sk_buff의 VLAN 필드 */
struct sk_buff {
    __be16  vlan_proto;   /* VLAN 프로토콜 (0x8100 또는 0x88a8) */
    __u16   vlan_tci;     /* TCI: PCP(3) + DEI(1) + VID(12) */
    /* vlan_present는 6.x에서 vlan_all로 통합 */
};

/* NIC 드라이버: HW VLAN 가속 — 수신 시 */
static void my_nic_rx(struct napi_struct *napi, u16 rx_vlan)
{
    struct sk_buff *skb = napi_alloc_skb(napi, 256);
    /* ... DMA 데이터 복사 (VLAN 태그 없는 프레임) ... */

    if (rx_vlan) {
        /* NIC이 추출한 VLAN 태그를 skb 메타에 저장 */
        __vlan_hwaccel_put_tag(skb, htons(ETH_P_8021Q), rx_vlan);
        /* → skb->vlan_proto = ETH_P_8021Q
         * → skb->vlan_tci = rx_vlan
         * → 패킷 데이터에는 VLAN 태그 없음 */
    }
    napi_gro_receive(napi, skb);
}

/* VLAN 태그 확인/추출 */
if (skb_vlan_tag_present(skb)) {
    u16 vid = skb_vlan_tag_get_id(skb);    /* VID (0~4095) */
    u16 prio = skb_vlan_tag_get_prio(skb);  /* PCP (0~7) */
}

/* VLAN 태그 추가/제거 (소프트웨어) */
skb_vlan_push(skb, htons(ETH_P_8021Q), vid | (prio << 13));
/* → 패킷 데이터에 4B VLAN 태그 삽입, headroom 필요 */

skb_vlan_pop(skb);
/* → 패킷에서 VLAN 태그 제거, skb 메타로 이동 */

/* QinQ (802.1ad): 이중 VLAN 태그 */
/* 외부 VLAN: skb->vlan_proto = ETH_P_8021AD, skb->vlan_tci = outer */
/* 내부 VLAN: 패킷 데이터 내 ETH_P_8021Q 태그로 존재 */
skb_vlan_push(skb, htons(ETH_P_8021AD), outer_vid);
/* → 외부 S-tag + 내부 C-tag 이중 태그 구성 */

TCP의 sk_buff 분할/병합/재전송

TCP는 sk_buff를 가장 정교하게 활용하는 프로토콜입니다. 전송 큐의 skb를 MSS 단위로 분할하고, 수신 경로에서 인접 세그먼트를 병합하며, 재전송 시 skb를 재활용합니다. 이 과정에서 TCP_SKB_CB()를 통한 cb[] 활용이 핵심입니다.

/* tcp_fragment(): 하나의 skb를 두 개로 분할
 * 용도: MSS 변경, SACK 기반 부분 재전송, cwnd 축소 시
 * net/ipv4/tcp_output.c */
int tcp_fragment(struct sock *sk, enum tcp_queue tcp_queue,
                struct sk_buff *skb, u32 len, unsigned int mss_now,
                gfp_t gfp)
{
    struct sk_buff *buff;
    int old_factor;

    /* 새 skb 할당 (뒷부분 데이터용) */
    buff = sk_stream_alloc_skb(sk, 0, gfp, 0);

    /* 페이로드 분할: skb의 len 이후 데이터를 buff로 이동 */
    skb_split(skb, buff, len);

    /* TCP_SKB_CB 갱신: 시퀀스 번호 분할 */
    TCP_SKB_CB(buff)->seq = TCP_SKB_CB(skb)->seq + len;
    TCP_SKB_CB(buff)->end_seq = TCP_SKB_CB(skb)->end_seq;
    TCP_SKB_CB(skb)->end_seq = TCP_SKB_CB(buff)->seq;

    /* GSO 세그먼트 수 재계산 */
    tcp_set_skb_tso_segs(skb, mss_now);
    tcp_set_skb_tso_segs(buff, mss_now);

    /* 전송 큐에서 skb 뒤에 buff 삽입 */
    skb_append(skb, buff, &sk->sk_write_queue);
    return 0;
}

/* tcp_try_coalesce(): 인접 수신 skb를 하나로 병합
 * 용도: RX 경로에서 연속 세그먼트 병합 → 소켓 큐 skb 수 감소
 * net/ipv4/tcp_input.c */
static bool tcp_try_coalesce(struct sock *sk,
                            struct sk_buff *to,
                            struct sk_buff *from,
                            bool *fragstolen)
{
    /* from의 데이터를 to에 병합 가능한지 확인 */
    if (TCP_SKB_CB(from)->seq != TCP_SKB_CB(to)->end_seq)
        return false;  /* 연속이 아님 */

    if (!skb_try_coalesce(to, from, fragstolen, &delta))
        return false;  /* 메모리/frag 제한 초과 */

    /* skb_try_coalesce: from의 frags를 to의 frags[]에 추가
     * → from은 해제 가능, to->len 증가 */

    TCP_SKB_CB(to)->end_seq = TCP_SKB_CB(from)->end_seq;
    TCP_SKB_CB(to)->ack_seq = TCP_SKB_CB(from)->ack_seq;
    return true;
}

/* tcp_collapse(): OOO(Out-of-Order) 큐에서 중복/겹침 제거
 * 용도: OOO 큐의 skb가 과도하게 쌓일 때 메모리 절약
 * net/ipv4/tcp_input.c */
static void tcp_collapse(struct sock *sk,
                        struct sk_buff_head *list,
                        struct rb_root *root,
                        struct sk_buff *head,
                        struct sk_buff *tail,
                        u32 start, u32 end)
{
    /* start~end 범위의 skb들을 하나의 skb로 합침
     * → 겹치는 시퀀스 번호는 제거
     * → OOO 큐의 메모리 사용량 감소
     * → tcp_prune_ofo_queue()에서 메모리 압박 시 호출 */
}

/* TCP 재전송: 기존 skb 재활용 */
int __tcp_retransmit_skb(struct sock *sk, struct sk_buff *skb, int segs)
{
    /* 1. skb가 clone 상태면 pskb_copy()로 헤더 독립화 */
    if (skb_cloned(skb)) {
        struct sk_buff *nskb = pskb_copy(skb, GFP_ATOMIC);
        /* 원본을 큐에서 교체 */
    }

    /* 2. TCP 헤더 재구성 (seq, ack, window, timestamp) */
    tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC);
    /* clone_it=1: 재전송 큐에 남기면서 clone 전송 */
}
TCP skb 연산함수트리거 조건skb 변화
분할tcp_fragment()MSS 축소, SACK partial retx1개 skb → 2개 (seq 분할)
수신 병합tcp_try_coalesce()연속 세그먼트 수신2개 skb → 1개 (frags 합체)
OOO 압축tcp_collapse()OOO 큐 메모리 압박N개 skb → 1개 (데이터 병합)
재전송__tcp_retransmit_skb()RTO, SACK, TLP기존 skb clone 후 재전송
GSO 생성tcp_write_xmit()cwnd 허용, TSQ 미달여러 MSS를 하나의 GSO skb로

BPF/TC의 __sk_buff 컨텍스트

eBPF 프로그램(TC classifier, socket filter)은 커널의 struct sk_buff에 직접 접근하지 않고, 안전한 래퍼인 struct __sk_buff를 통해 접근합니다. BPF 검증기(verifier)가 이 구조체의 필드 접근을 커널 내부 sk_buff 필드로 변환합니다.

/* include/uapi/linux/bpf.h — BPF 프로그램이 보는 skb 뷰 */
struct __sk_buff {
    __u32 len;              /* skb->len */
    __u32 pkt_type;         /* skb->pkt_type */
    __u32 mark;             /* skb->mark (읽기/쓰기) */
    __u32 queue_mapping;    /* skb->queue_mapping */
    __u32 protocol;         /* skb->protocol */
    __u32 vlan_present;     /* skb_vlan_tag_present(skb) */
    __u32 vlan_tci;         /* skb->vlan_tci */
    __u32 vlan_proto;       /* skb->vlan_proto */
    __u32 priority;         /* skb->priority (읽기/쓰기) */
    __u32 ingress_ifindex;  /* skb->skb_iif */
    __u32 ifindex;          /* skb->dev->ifindex */
    __u32 tc_index;         /* skb->tc_index */
    __u32 cb[5];            /* skb->cb[] (TC에서 사용) */
    __u32 hash;             /* skb->hash */
    __u32 tc_classid;       /* skb->tc_classid (쓰기) */
    __u32 data;             /* skb->data 포인터 (패킷 시작) */
    __u32 data_end;         /* skb->data + skb_headlen(skb) */
    __u32 napi_id;          /* skb->napi_id */
    __u32 family;           /* sk->sk_family */
    __u32 data_meta;        /* skb->data - skb_metadata_len */
    __u32 flow_keys;        /* flow dissector 결과 */
    __u64 tstamp;           /* skb->tstamp (읽기/쓰기) */
    __u32 wire_len;         /* 원래 와이어 길이 (GSO 이전) */
    __u32 gso_segs;         /* skb_shinfo(skb)->gso_segs */
    __u64 hwtstamp;         /* skb_hwtstamps(skb)->hwtstamp */
};

/* BPF 검증기: __sk_buff 필드 접근 → 실제 skb 오프셋 변환
 * net/core/filter.c — bpf_convert_ctx_access() */
static u32 bpf_convert_ctx_access(...)
{
    switch (si->off) {
    case offsetof(struct __sk_buff, len):
        /* __sk_buff.len → skb->len 직접 매핑 */
        *insn++ = BPF_LDX_MEM(BPF_W, si->dst_reg, si->src_reg,
                               offsetof(struct sk_buff, len));
        break;
    case offsetof(struct __sk_buff, data):
        /* __sk_buff.data → skb->data 포인터 로드 */
        *insn++ = BPF_LDX_MEM(BPF_FIELD_SIZEOF(struct sk_buff, data),
                               si->dst_reg, si->src_reg,
                               offsetof(struct sk_buff, data));
        break;
    }
}

/* TC-BPF 프로그램에서 skb 패킷 데이터 직접 접근 */
SEC("tc")
int tc_filter(struct __sk_buff *skb)
{
    void *data = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;

    /* 패킷 데이터 직접 접근 (bounds check 필수!) */
    struct ethhdr *eth = data;
    if (data + sizeof(*eth) > data_end)
        return TC_ACT_OK;

    /* skb 필드 수정 */
    skb->mark = 42;        /* → skb->mark = 42 (netfilter/tc 마킹) */
    skb->priority = 7;    /* → skb->priority = 7 (QoS) */

    /* 패킷 데이터 수정: bpf_skb_store_bytes() 헬퍼 사용 */
    __u8 new_ttl = 64;
    bpf_skb_store_bytes(skb, ETH_HLEN + offsetof(struct iphdr, ttl),
                        &new_ttl, sizeof(new_ttl), 0);

    /* 헤더 축소/확장: encap/decap */
    bpf_skb_adjust_room(skb, -14, BPF_ADJ_ROOM_MAC, 0);
    /* → skb_pull(14) 효과: L2 헤더 제거 */

    return TC_ACT_OK;
}

/* 주요 BPF skb 헬퍼 함수 */
/* bpf_skb_load_bytes()      — 오프셋에서 N바이트 로드 (비선형 안전) */
/* bpf_skb_store_bytes()     — 오프셋에 N바이트 저장 */
/* bpf_skb_pull_data()       — pskb_may_pull() 래퍼 */
/* bpf_skb_change_head()     — headroom 변경 (encap) */
/* bpf_skb_change_tail()     — tailroom 변경 */
/* bpf_skb_adjust_room()     — MAC/NET 레벨 크기 조정 */
/* bpf_skb_vlan_push/pop()   — VLAN 태그 추가/제거 */
/* bpf_skb_change_proto()    — L3 프로토콜 변경 (IPv4↔IPv6) */
/* bpf_skb_cgroup_id()       — cgroup ID 조회 */
/* bpf_skb_get_tunnel_key()  — 터널 메타데이터 조회 */
/* bpf_redirect()            — 다른 인터페이스로 리다이렉트 */
/* bpf_clone_redirect()      — clone 후 리다이렉트 */
ℹ️

direct packet access vs 헬퍼: data/data_end를 통한 직접 접근은 linear 영역만 가능합니다. 비선형 데이터(frags/frag_list)에 접근하려면 bpf_skb_pull_data(skb, offset)로 먼저 linearize하거나, bpf_skb_load_bytes() 헬퍼를 사용해야 합니다. 직접 접근이 더 빠르지만, GRO로 병합된 대용량 패킷은 헤더만 linear이므로 페이로드 파싱 시 헬퍼가 필요합니다.

Flow Dissector와 RSS/RPS 해시

Flow dissector는 skb에서 프로토콜 헤더를 파싱하여 플로우 키(5-tuple 등)를 추출하는 커널 프레임워크입니다. 추출된 키는 skb->hash에 저장되어 RSS(Receive Side Scaling), RPS(Receive Packet Steering), GRO 병합, 소켓 lookup 등 다양한 곳에서 플로우 기반 분배에 사용됩니다.

Flow Dissector → skb->hash 활용 경로 수신 패킷 ETH+IP+TCP/UDP __skb_flow_dissect() L3: saddr, daddr, protocol L4: sport, dport → flow_keys 구조체 생성 __skb_get_hash() flow_keys → jhash() → skb->hash = result RSS (NIC HW) HW 해시 → RX 큐 선택 RPS (SW) hash → CPU 선택 GRO 병합 hash → gro_hash[] 버킷 SO_REUSEPORT hash → 소켓 선택 skb->hash 해시 타입 (skb->l4_hash, skb->sw_hash) HW hash (NIC RSS) l4_hash=1, sw_hash=0 NIC의 Toeplitz 해시 사용 SW hash (커널 계산) l4_hash=0/1, sw_hash=1 flow dissector + jhash
Flow dissector가 패킷 헤더에서 5-tuple을 추출하고 해시를 계산 → RSS/RPS/GRO/소켓 분배에 사용
/* include/net/flow_dissector.h — 플로우 키 구조체 */
struct flow_keys {
    struct flow_dissector_key_control control;
    struct flow_dissector_key_basic basic;  /* n_proto, ip_proto */
    struct flow_dissector_key_addrs addrs;  /* saddr, daddr */
    struct flow_dissector_key_ports ports;  /* sport, dport */
    /* VLAN, GRE, MPLS 키도 포함 가능 */
};

/* skb->hash 계산 (lazy — 처음 접근 시 계산) */
static inline __u32 skb_get_hash(struct sk_buff *skb)
{
    if (!skb->l4_hash && !skb->sw_hash)
        __skb_get_hash(skb);  /* flow dissector 실행 */
    return skb->hash;
}

/* RPS: SW 기반 CPU 분배 (net/core/dev.c) */
static int get_rps_cpu(struct net_device *dev,
                      struct sk_buff *skb,
                      struct rps_dev_flow **rflowp)
{
    u32 hash = skb_get_hash(skb);
    /* hash를 CPU 수로 나눠 대상 CPU 결정 */
    u32 cpu = reciprocal_scale(hash, cpumask_weight(rps_mask));
    return cpu;
}
해시 소스설정 방법성능커스터마이즈
NIC RSS (HW)ethtool -X eth0 hkey/hfunc최고 (HW 처리)해시 키, 해시 함수, indirection table
RPS (SW)/sys/class/net/eth0/queues/rx-0/rps_cpus양호 (softirq)CPU 비트맵
RFS (Flow Steering)/proc/sys/net/core/rps_sock_flow_entries양호앱이 실행 중인 CPU로 스티어링
XPS (TX)/sys/class/net/eth0/queues/tx-0/xps_cpusTX 큐 선택CPU→TX 큐 매핑

Encapsulation/Tunnel과 sk_buff

터널 프로토콜(GRE, VXLAN, Geneve, WireGuard)은 sk_buff에 외부 헤더를 추가(encap)하거나 제거(decap)합니다. 이 과정에서 headroom 관리, 헤더 포인터 재설정, inner/outer 프로토콜 구분이 핵심입니다.

VXLAN Encapsulation 시 skb 변화 Encapsulation 전 (원본 패킷) headroom Inner ETH 14B Inner IP 20B Inner TCP 20B Payload data Encapsulation 후 (VXLAN 캡슐화) Outer ETH 14B Outer IP 20B Outer UDP 8B VXLAN 8B Inner ETH Inner IP Inner TCP Payload data (새 위치) skb_push(50B) = Outer ETH(14) + Outer IP(20) + Outer UDP(8) + VXLAN(8)
VXLAN encap: 50바이트 외부 헤더 추가. headroom 부족 시 skb_cow_head()로 재할당 필요
/* VXLAN encapsulation 흐름 (drivers/net/vxlan/vxlan_core.c) */
static void vxlan_xmit_one(struct sk_buff *skb, ...)
{
    int headroom = sizeof(struct iphdr)     /* 20B outer IP */
                 + sizeof(struct udphdr)    /* 8B outer UDP */
                 + sizeof(struct vxlanhdr)  /* 8B VXLAN */
                 + LL_RESERVED_SPACE(dst->dev); /* outer L2 */

    /* 1. headroom 확보 (clone이면 독립화) */
    if (skb_cow_head(skb, headroom)) {
        kfree_skb(skb);
        return;
    }

    /* 2. skb->inner_* 헤더 포인터 저장 (decap 시 복원용) */
    skb_set_inner_protocol(skb, skb->protocol);
    skb_set_inner_network_header(skb, skb_network_offset(skb));
    skb_set_inner_transport_header(skb, skb_transport_offset(skb));

    /* 3. encapsulation 플래그 설정 */
    skb->encapsulation = 1;

    /* 4. VXLAN 헤더 추가 */
    struct vxlanhdr *vxh = (struct vxlanhdr *)__skb_push(skb, sizeof(*vxh));
    vxh->vx_flags = htonl(VXLAN_HF_VNI);
    vxh->vx_vni = vxlan_vni_field(vni);

    /* 5. 외부 UDP 헤더 */
    udp_set_csum(skb, ...);

    /* 6. 외부 IP 헤더 → ip_tunnel_xmit() */
    iptunnel_xmit(..., skb, ...);
    /* → skb_push(IP 헤더) → skb_reset_network_header()
     * → ip_local_out() → Netfilter OUTPUT → dev_queue_xmit() */
}

/* Decapsulation: 외부 헤더 제거 후 inner 헤더 복원 */
static int vxlan_rcv(struct sock *sk, struct sk_buff *skb)
{
    /* 1. VXLAN 헤더 파싱 및 VNI 추출 */
    struct vxlanhdr *vxh = vxlan_hdr(skb);

    /* 2. 외부 헤더 제거 */
    __skb_pull(skb, sizeof(struct vxlanhdr));
    skb_reset_network_header(skb);  /* inner IP로 재설정 */

    /* 3. inner 패킷으로 프로토콜 재설정 */
    skb->protocol = eth_type_trans(skb, vxlan->dev);
    skb->encapsulation = 0;

    /* 4. 일반 스택으로 재진입 */
    netif_rx(skb);
}

/* sk_buff의 inner 헤더 포인터 */
struct sk_buff {
    sk_buff_data_t  inner_transport_header; /* 내부 L4 */
    sk_buff_data_t  inner_network_header;   /* 내부 L3 */
    sk_buff_data_t  inner_mac_header;       /* 내부 L2 */
    __be16          inner_protocol;         /* 내부 프로토콜 */
    __u8            encapsulation:1;       /* 캡슐화 여부 */
};
⚠️

터널과 GSO 상호작용: skb->encapsulation = 1이면 GSO/체크섬 오프로드가 inner 패킷 기준으로 동작합니다. NIC이 NETIF_F_GSO_UDP_TUNNEL을 지원하면 HW가 외부 UDP + 내부 TCP를 한 번에 분할합니다. 미지원 NIC에서는 SKB_GSO_PARTIAL을 사용하여 외부 헤더만 SW로, 내부 분할은 HW로 처리하는 하이브리드 방식이 가능합니다(4.7+).

sk_buff 할당 내부 (kmem_cache)

sk_buff의 메모리 할당은 일반 kmalloc()이 아닌 전용 SLAB 캐시(skbuff_head_cache)를 사용합니다. 이는 빈번한 할당/해제에 최적화되어 있으며, fclone(fast clone) 메커니즘으로 clone 비용을 더 줄입니다.

/* net/core/skbuff.c — sk_buff SLAB 캐시 초기화 */
static struct kmem_cache *skbuff_head_cache;
static struct kmem_cache *skbuff_fclone_cache;

void __init skb_init(void)
{
    /* 일반 sk_buff 캐시 */
    skbuff_head_cache = kmem_cache_create(
        "skbuff_head_cache",
        sizeof(struct sk_buff),  /* ~240바이트 */
        0,                         /* 정렬 */
        SLAB_HWCACHE_ALIGN | SLAB_PANIC,
        NULL);

    /* fclone 캐시: sk_buff 2개 + fclone_ref를 하나의 슬랩 객체로 */
    skbuff_fclone_cache = kmem_cache_create(
        "skbuff_fclone_cache",
        sizeof(struct sk_buff_fclones),  /* sk_buff*2 + ref */
        0,
        SLAB_HWCACHE_ALIGN | SLAB_PANIC,
        NULL);
}

/* fclone 구조체: clone 전용 최적화 */
struct sk_buff_fclones {
    struct sk_buff skb1;      /* 원본 sk_buff */
    struct sk_buff skb2;      /* 사전 할당된 clone sk_buff */
    refcount_t     fclone_ref; /* 공유 참조 카운트 */
};

/* __alloc_skb: 내부 할당 로직 */
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
                           int flags, int node)
{
    struct sk_buff *skb;
    u8 *data;

    if (flags & SKB_ALLOC_FCLONE) {
        /* fclone 모드: 2개의 sk_buff를 한 번에 할당
         * TCP 전송 경로에서 사용: 재전송 시 clone 필요 예상 */
        struct sk_buff_fclones *fclones;
        fclones = kmem_cache_alloc_node(skbuff_fclone_cache,
                                         gfp_mask, node);
        skb = &fclones->skb1;
        skb->fclone = SKB_FCLONE_ORIG;  /* 원본 표시 */
        fclones->skb2.fclone = SKB_FCLONE_CLONE; /* clone 슬롯 */
    } else {
        /* 일반 모드: sk_buff 1개만 할당 */
        skb = kmem_cache_alloc_node(skbuff_head_cache,
                                     gfp_mask, node);
        skb->fclone = SKB_FCLONE_UNAVAILABLE;
    }

    /* 데이터 버퍼 할당 (별도) */
    size = SKB_DATA_ALIGN(size);
    data = kmalloc_reserve(size + sizeof(struct skb_shared_info),
                           gfp_mask, node, &pfmemalloc);

    skb->head = data;
    skb->data = data;
    skb->truesize = SKB_TRUESIZE(size);
    refcount_set(&skb->users, 1);
    return skb;
}

/* fclone으로 빠른 clone (별도 할당 불필요) */
struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask)
{
    struct sk_buff *n;

    if (skb->fclone == SKB_FCLONE_ORIG) {
        /* fclone 슬롯이 사용 가능하면 할당 없이 즉시 clone */
        struct sk_buff_fclones *fclones =
            container_of(skb, struct sk_buff_fclones, skb1);
        n = &fclones->skb2;
        if (refcount_inc_not_zero(&fclones->fclone_ref)) {
            /* 할당 없이 clone 완료! → kmem_cache_alloc 비용 절약 */
            goto do_clone;
        }
    }

    /* fclone 불가: 일반 할당 */
    n = kmem_cache_alloc(skbuff_head_cache, gfp_mask);
do_clone:
    /* sk_buff 메타데이터 복사 (데이터 버퍼 공유) */
    __copy_skb_header(n, skb);
    n->cloned = 1;
    skb->cloned = 1;
    atomic_inc(&skb_shinfo(skb)->dataref);
    return n;
}

/* NAPI per-CPU 캐시 (napi_alloc_skb 최적화) */
/* NAPI 수신 경로에서는 skbuff_head_cache 대신
 * per-CPU page fragment cache를 사용하여 allocation lock 경합을 회피
 * → napi_alloc_cache (struct page_frag_cache)
 * → 같은 page에서 연속 skb의 data 버퍼를 할당
 * → TLB miss, cache miss 최소화 */
할당 방식캐시크기사용처
일반 skbskbuff_head_cache~240B대부분의 skb 할당
fclone skbskbuff_fclone_cache~490BTCP TX (clone 예상 시)
데이터 버퍼kmalloc slab가변linear 데이터 영역
NAPI 수신per-CPU page fragPAGE_SIZENAPI poll 내 고속 할당
page_poolper-pool 캐시PAGE_SIZE고성능 NIC 드라이버 (6.x+)
💡

fclone 효과: TCP 전송 경로에서 sk_stream_alloc_skb()SKB_ALLOC_FCLONE 플래그로 skb를 할당합니다. 이는 재전송 시 skb_clone()별도 메모리 할당 없이 사전 할당된 슬롯을 사용하게 합니다. 고부하 TCP 서버에서 재전송율이 높을 때 kmem_cache_alloc() 호출 수를 크게 줄여 성능을 개선합니다. slabinfo -s | grep skbuff로 캐시 사용 통계를 확인할 수 있습니다.

sk_buff 할당: 일반 vs fclone 메모리 레이아웃 일반 할당 (skbuff_head_cache) sk_buff (~240B) head → 데이터 버퍼 + skb_shared_info 대부분의 skb 할당 (RX, UDP TX 등) fclone 할당 (skbuff_fclone_cache) sk_buff_fclones (~490B, 하나의 SLAB 객체) skb₁ (ORIG) skb₂ (CLONE) ref 공유 데이터 버퍼 + skb_shared_info (skb₁, skb₂ 공유) skb_clone(skb₁) → skb₂를 즉시 사용 (kmem_cache_alloc 불필요!)
일반 할당은 sk_buff 1개만 SLAB에서 할당. fclone은 sk_buff 2개 + 참조 카운트를 하나의 SLAB 객체로 사전 할당하여, skb_clone() 시 메모리 할당 없이 skb₂ 슬롯을 즉시 사용

커널 소스 분석: __alloc_skb() 호출 체인

__alloc_skb()는 sk_buff 할당의 핵심 경로입니다. kmem_cache_alloc()로 sk_buff 헤더를 가져오고, __netdev_alloc_frag() 또는 kmalloc_reserve()로 데이터 버퍼를 확보합니다. 아래 다이어그램은 경로별 분기와 내부 호출 관계를 보여줍니다.

__alloc_skb() 호출 체인 alloc_skb() 일반 경로 래퍼 netdev_alloc_skb() RX 드라이버 래퍼 napi_alloc_skb() NAPI 전용 경로 sk_stream_alloc_skb() TCP TX (fclone) __alloc_skb(size, gfp, flags, node) net/core/skbuff.c — 내부 할당 함수 FCLONE? flags & SKB_ALLOC_FCLONE Yes kmem_cache_alloc_node() skbuff_fclone_cache (2×skb+ref) No kmem_cache_alloc_node() skbuff_head_cache (1×skb) 데이터 버퍼 할당 kmalloc_reserve() size + sizeof(skb_shared_info) NAPI 경로 __netdev_alloc_frag() per-CPU 페이지 캐시 skb 초기화: head/data/tail/end 설정 truesize = SKB_TRUESIZE(size), users = 1
__alloc_skb() 내부 분기: fclone 여부에 따라 다른 SLAB 캐시 사용, 데이터 버퍼는 별도 kmalloc_reserve() 호출

__alloc_skb() 소스 코드 상세

/* net/core/skbuff.c */
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
                           int flags, int node)
{
    struct kmem_cache *cache;
    struct sk_buff *skb;
    u8 *data;
    bool pfmemalloc;

    cache = (flags & SKB_ALLOC_FCLONE)
        ? skbuff_fclone_cache : skbuff_head_cache;

    skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);
    if (unlikely(!skb))
        return NULL;
    prefetchw(skb);

    /* skb_shared_info를 포함할 크기로 정렬 */
    size = SKB_DATA_ALIGN(size);
    size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
    data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);
    if (unlikely(!data))
        goto nodata;

    /* skb 포인터 초기화 */
    memset(skb, 0, offsetof(struct sk_buff, tail));
    skb->truesize = SKB_TRUESIZE(size);
    skb->pfmemalloc = pfmemalloc;
    refcount_set(&skb->users, 1);
    skb->head = data;
    skb->data = data;
    skb_reset_tail_pointer(skb);
    skb->end = skb->tail + size - SKB_DATA_ALIGN(sizeof(struct skb_shared_info));

    /* skb_shared_info 초기화: end 포인터 직후에 위치 */
    memset(skb_shinfo(skb), 0, sizeof(struct skb_shared_info));
    atomic_set(&skb_shinfo(skb)->dataref, 1);

    if (flags & SKB_ALLOC_FCLONE) {
        struct sk_buff_fclones *fclones;
        fclones = container_of(skb, struct sk_buff_fclones, skb1);
        skb->fclone = SKB_FCLONE_ORIG;
        refcount_set(&fclones->fclone_ref, 1);
        fclones->skb2.fclone = SKB_FCLONE_CLONE;
    }
    return skb;

nodata:
    kmem_cache_free(cache, skb);
    return NULL;
}
코드 설명
  • 3행size, gfp_mask, flags, node 4개 매개변수 수신. node는 NUMA 노드 번호로 로컬 메모리 할당에 사용됩니다.
  • 9-10행SKB_ALLOC_FCLONE 플래그에 따라 fclone 전용 캐시 또는 일반 캐시 선택. TCP TX 경로는 fclone 캐시를 사용해 재전송 시 별도 할당을 피합니다.
  • 12-14행kmem_cache_alloc_node()로 sk_buff 헤더 객체만 먼저 할당. __GFP_DMA 플래그는 제거해 DMA 전용 영역 낭비를 막습니다.
  • 18-19행SKB_DATA_ALIGN()로 size를 캐시라인 경계에 정렬. 그 뒤에 skb_shared_info 크기도 추가하여 한 번의 kmalloc으로 데이터 버퍼와 shinfo를 연속 배치합니다.
  • 20-23행kmalloc_reserve()는 pfmemalloc(메모리 압박 상황용 예약 풀) 여부를 out-param으로 반환합니다. 할당 실패 시 nodata 레이블로 점프해 이미 할당된 sk_buff 헤더를 반환합니다.
  • 26-31행tail까지의 범위만 memset 0으로 초기화해 불필요한 쓰기를 줄입니다. truesize에는 SKB_TRUESIZE(size)를 설정해 소켓 버퍼 카운팅에 반영합니다.
  • 32-35행head/data는 데이터 버퍼 시작점, tail은 head+sizeof(head)로 리셋, end는 tail + 정렬 크기 - shinfo 크기로 설정됩니다. 이 4개 포인터가 sk_buff 메모리 레이아웃의 핵심입니다.
  • 38-39행skb_shinfo(skb)는 end 포인터가 가리키는 위치로, 데이터 버퍼 끝에 연속 배치된 skb_shared_info를 초기화합니다. dataref=1로 설정해 현재 참조 수 1개임을 표시합니다.
  • 41-47행fclone 모드면 container_of로 sk_buff_fclones 전체 구조체 포인터를 구하고, skb2의 fclone 타입을 SKB_FCLONE_CLONE으로 설정해 빠른 clone 슬롯으로 준비합니다.

__netdev_alloc_frag() — per-CPU 페이지 캐시

NAPI 수신 경로(napi_alloc_skb())는 데이터 버퍼를 kmalloc 대신 per-CPU 페이지 프래그먼트(Fragment) 캐시에서 가져옵니다. 이를 통해 SMP 환경에서의 SLAB 잠금(Lock) 경합을 완전히 제거합니다.

/* net/core/skbuff.c — per-CPU 페이지 캐시에서 데이터 버퍼 할당 */
static void *__netdev_alloc_frag_align(unsigned int fragsz,
                                       unsigned int align_mask)
{
    struct page_frag_cache *nc;
    void *data;

    /* per-CPU 캐시: CPU별로 독립적 → lock-free */
    nc = this_cpu_ptr(&netdev_alloc_cache);
    data = page_frag_alloc_align(nc, fragsz, GFP_ATOMIC, align_mask);
    local_bh_enable();
    return data;
}

/* page_frag_cache: 하나의 물리 페이지에서 연속 할당 */
struct page_frag_cache {
    void *va;           /* 현재 페이지의 가상 주소 */
    __u16 offset;      /* 다음 할당 위치 오프셋 */
    __u16 size;        /* 현재 페이지 크기 (PAGE_SIZE 또는 2×PAGE_SIZE) */
    unsigned int pagecnt_bias; /* page refcount 편향값 */
    bool pfmemalloc;   /* 예약 메모리 풀 사용 여부 */
};
코드 설명
  • 8-9행this_cpu_ptr()로 현재 CPU의 netdev_alloc_cache를 가져옵니다. 각 CPU가 독립된 캐시를 사용하므로 스핀락 없이 동작합니다. 이는 고속 패킷 수신 경로에서 메모리 할당 오버헤드를 획기적으로 줄입니다.
  • 10행page_frag_alloc_align()은 현재 페이지 캐시에 공간이 남아 있으면 offset만 증가시켜 반환합니다. 페이지가 소진되면 새 물리 페이지를 할당합니다. 연속된 skb 데이터가 같은 페이지에 배치되어 TLB/캐시 효율이 높아집니다.
  • 16행va 필드는 현재 사용 중인 페이지의 가상 주소. 할당은 va+offset 위치부터 시작합니다.
  • 17행offset은 현재 페이지 내 다음 할당 시작 위치. 새 데이터를 할당할 때마다 fragsz만큼 증가합니다.
  • 19행pagecnt_bias는 page_ref_count의 편향값으로, page를 개별 put_page() 없이 한 번에 해제할 수 있게 해주는 최적화 기법입니다.

핵심 구조체 필드별 주석: sk_buff와 skb_shared_info

커널 소스 기준 주요 필드 전체를 한국어 주석과 함께 제시합니다. 실제 커널(include/linux/skbuff.h)의 배치 순서에 맞춰 그룹별로 정리했습니다.

/* include/linux/skbuff.h — sk_buff 전체 구조 (6.x 기준, 주요 필드) */
struct sk_buff {
    /* === 큐 연결 (Queue Linkage) === */
    union {
        struct {
            struct sk_buff *next;    /* 이중 연결 리스트: 다음 skb */
            struct sk_buff *prev;    /* 이중 연결 리스트: 이전 skb */
        };
        struct rb_node  rbnode;     /* TCP OFO/retransmit 큐용 레드블랙 트리 노드 */
        struct list_head list;      /* generic list_head 사용 시 (RX 배치 처리) */
        struct llist_node ll_node;  /* lock-free 단방향 리스트 (GRO flush 경로) */
    };

    /* === 소유권 (Ownership) === */
    struct sock        *sk;         /* 이 skb를 소유한 소켓. RX/TX 메모리 할당량 추적 */
    union {
        struct net_device *dev;     /* 수신/전송에 사용된 네트워크 디바이스 */
        unsigned long    dev_scratch;/* dev 필드를 임시 정수로 재사용 (Tx 완료 콜백) */
    };

    /* === 라우팅/전달 메타데이터 === */
    struct  dst_entry *_skb_refdst; /* 라우팅 결정 결과 (dst_hold/dst_release 관리) */
    void            (*destructor)(struct sk_buff *skb);
                                    /* skb 해제 시 호출되는 소멸자. sock_rfree 등 등록 */

    /* === 데이터 길이 === */
    unsigned int    len;            /* linear + paged 전체 데이터 길이 */
    unsigned int    data_len;       /* paged(비선형) 부분의 길이. 0이면 순수 linear */
    __u16           mac_len;        /* MAC(L2) 헤더 길이 */
    __u16           hdr_len;        /* clone 시 writable 복사 헤더 길이 (pskb_copy) */
    __u16           queue_mapping;  /* 멀티큐 NIC의 TX 큐 인덱스 선택 */

    /* === 프로토콜 식별 === */
    __be16          protocol;       /* EtherType: ETH_P_IP(0x0800), ETH_P_IPV6(0x86DD) 등 */

    /* === 비트필드 플래그들 === */
    __u8            cloned:1;        /* 이 skb가 clone인지 여부 (데이터 버퍼 공유 중) */
    __u8            nohdr:1;         /* 페이로드 참조 전용, 헤더 영역 없음 */
    __u8            peeked:1;        /* MSG_PEEK으로 이미 확인된 skb */
    __u8            ip_summed:2;     /* 체크섬 오프로드 상태 (NONE/UNNECESSARY/COMPLETE/PARTIAL) */
    __u8            ooo_okay:1;      /* TCP Out-Of-Order 허용 여부 */

    /* === 체크섬 오프로드 === */
    __wsum          csum;           /* 체크섬 값 (CHECKSUM_COMPLETE 시 HW가 채움) */
    __u16           csum_start;     /* CHECKSUM_PARTIAL: 체크섬 계산 시작 오프셋 (head 기준) */
    __u16           csum_offset;    /* CHECKSUM_PARTIAL: 체크섬 필드 위치 (csum_start 기준) */

    /* === 헤더 오프셋 (sk_buff_data_t는 u16 또는 포인터) === */
    sk_buff_data_t  transport_header; /* L4(TCP/UDP/ICMP) 헤더 시작 오프셋 */
    sk_buff_data_t  network_header;   /* L3(IP) 헤더 시작 오프셋 */
    sk_buff_data_t  mac_header;       /* L2(Ethernet) 헤더 시작 오프셋 */
    sk_buff_data_t  inner_transport_header; /* 터널 내부 L4 오프셋 */
    sk_buff_data_t  inner_network_header;   /* 터널 내부 L3 오프셋 */
    sk_buff_data_t  inner_mac_header;       /* 터널 내부 L2 오프셋 */

    /* === 버퍼 포인터 (메모리 레이아웃의 핵심) === */
    sk_buff_data_t  tail;           /* 데이터 유효 영역 끝 (skb_put으로 증가) */
    sk_buff_data_t  end;            /* 할당된 버퍼의 절대 끝 (skb_shared_info 직전) */
    unsigned char  *head;          /* 할당된 버퍼의 절대 시작 */
    unsigned char  *data;          /* 현재 데이터 유효 영역 시작 (skb_push/pull로 변경) */

    /* === 메모리 회계 === */
    unsigned int    truesize;       /* 실제 점유 메모리 (소켓 버퍼 한계 sk_wmem_queued 등에 반영) */
    refcount_t      users;          /* 참조 카운트. 0이 되면 해제 */

    /* === 프로토콜 제어 블록 === */
    char            cb[48] __aligned(8);
                                    /* 프로토콜 전용 임시 저장소. TCP: tcp_skb_cb, IP: inet_skb_parm */
    __u32           hash;           /* 패킷 플로우 해시 (RSS 큐 선택, RPS, SO_INCOMING_CPU) */
    __u32           priority;       /* QoS 우선순위 (SO_PRIORITY, IP DSCP 반영) */
    __u32           mark;           /* netfilter/tc 마크 (iptables -j MARK, tc filter) */
    __u32           napi_id;        /* SO_BUSY_POLL: 어느 NAPI 인스턴스가 수신했는지 */
    union {
        ktime_t     tstamp;         /* 패킷 수신/전송 타임스탬프 (SO_TIMESTAMPING) */
        u64         skb_mstamp_ns;  /* TCP RTT 측정용 고해상도 타임스탬프 (CLOCK_TAI) */
    };
};
코드 설명
  • 큐 연결 unionnext/prev는 일반 sk_buff_head 큐에 사용되고, rbnode는 TCP 재전송/OFO 큐처럼 정렬이 필요한 경우 레드블랙 트리로 사용됩니다. 같은 메모리를 용도에 따라 재사용해 구조체 크기를 최소화합니다.
  • sk/devsk는 소속 소켓 포인터로, skb의 메모리가 소켓의 sk_rmem_alloc/sk_wmem_queued 카운터에 반영됩니다. dev는 수신/전송 디바이스로 네트워크 네임스페이스 결정에도 사용됩니다.
  • destructorkfree_skb() 호출 시 destructor가 설정돼 있으면 해제 전에 호출합니다. sock_rfree()가 등록되면 소켓의 수신 메모리 카운터를 자동으로 감소시킵니다.
  • len/data_lenlen은 선형+페이지 전체 길이, data_len은 페이지 부분만의 길입니다. len - data_len = linear 크기. skb_headlen(skb)은 이 값을 반환합니다.
  • ip_summedCHECKSUM_UNNECESSARY면 프로토콜 스택이 체크섬 검증을 생략합니다. 고성능 NIC은 수신 시 이 값을 설정해 CPU 절약에 기여합니다.
  • 헤더 오프셋 6개일반 패킷에는 transport/network/mac_header 3개, 터널 패킷에는 inner_ 접두사 3개가 추가됩니다. skb_transport_header(skb) 등 인라인 함수로 head + 오프셋 주소를 구합니다.
  • head/data/tail/end할당 직후: head == data == tail, end는 버퍼 끝. skb_put(n)으로 tail이 n 증가, skb_push(n)으로 data가 n 감소, skb_pull(n)으로 data가 n 증가합니다.
  • truesizesizeof(sk_buff) + 실제 데이터 버퍼 크기. 소켓 버퍼 한계(sk->sk_rcvbuf, sk->sk_sndbuf)와 비교되며, 초과 시 패킷이 드롭되거나 전송이 블록됩니다.
  • cb[48]각 프로토콜 계층이 자신만의 구조체로 캐스팅해 임시 데이터를 저장합니다. 다음 계층에 전달하면 덮어쓰이므로 계층 간 데이터 공유 용도로는 사용하면 안 됩니다.

skb_shared_info 필드별 주석

/* include/linux/skbuff.h — skb_shared_info (end 포인터 직후에 연속 배치) */
struct skb_shared_info {
    /* === 참조 카운트 === */
    __u8            flags;          /* 내부 플래그 (TX_SHARED_SKB 등) */
    __u8            meta_len;       /* XDP 메타데이터 길이 (XDP redirect 경로) */
    __u8            nr_frags;       /* frags[] 배열에 등록된 페이지 프래그먼트 수 (0~MAX_SKB_FRAGS) */
    __u8            tx_flags;       /* TX 완료 알림 플래그 (SKBTX_HW_TSTAMP 등) */
    atomic_t        dataref;        /* 데이터 버퍼 참조 카운트. clone 시 증가, 해제 시 감소 */

    /* === GSO 메타데이터 === */
    unsigned short  gso_size;       /* GSO 세그먼트 크기(MSS). 0이면 GSO 아님 */
    unsigned short  gso_segs;       /* 예상 세그먼트 수 (IP len / gso_size 기반) */
    unsigned int    gso_type;       /* GSO 타입 비트맵: SKB_GSO_TCPV4, SKB_GSO_UDP_L4 등 */
    __u32           tskey;          /* SO_TIMESTAMPING: TX 타임스탬프 식별 키 */

    /* === Scatter-Gather 프래그먼트 배열 === */
    skb_frag_t      frags[MAX_SKB_FRAGS]; /* 물리 페이지 참조 배열. NIC SG DMA에 직접 전달 가능 */

    /* === frag_list: skb 체인 (GRO/UDP 단편화 등) === */
    struct sk_buff  *frag_list;     /* 하위 skb 체인 포인터. GRO 병합, UDP 재조립에 사용 */

    /* === Ubuf/Zero-copy 메타데이터 === */
    struct ubuf_info *uarg;         /* MSG_ZEROCOPY: 사용자 공간 버퍼 참조 추적용 */
};
코드 설명
  • dataref데이터 버퍼를 참조하는 sk_buff 개수. __alloc_skb()에서 1로 초기화, skb_clone() 시 atomic_inc()로 증가, 마지막 참조자가 해제될 때 kfree_skb_partial()이 실제 데이터 버퍼를 해제합니다.
  • nr_fragsfrags[] 배열의 사용 개수. 0이면 순수 linear skb. skb_add_rx_frag()나 skb_fill_page_desc()로 증가. MAX_SKB_FRAGS(보통 17)를 초과하면 skb_linearize() 필요.
  • gso_size/gso_segs/gso_typeTCP가 tcp_sendmsg()에서 super-packet을 만들 때 설정합니다. NIC이 TSO를 지원하면 그대로 DMA되고, 아니면 validate_xmit_skb()에서 skb_segment()가 소프트웨어로 분할합니다.
  • frags[]각 원소는 skb_frag_t로 page 포인터, offset, length를 담습니다. skb_frag_page(), skb_frag_off(), skb_frag_size() 인라인 함수로 접근. NIC 드라이버는 이 배열을 DMA descriptor에 직접 매핑해 zero-copy 전송합니다.
  • frag_listfrags[]와 달리 sk_buff 포인터 체인입니다. GRO(Generic Receive Offload)에서 병합된 패킷들, UDP 재조립 과정에서 사용됩니다. skb_walk_frags() 매크로로 순회합니다.
  • uargMSG_ZEROCOPY(sendmsg 플래그)로 전송 시, 사용자 버퍼가 NIC으로 DMA되는 동안 해제되지 않도록 참조를 유지합니다. 전송 완료 시 sock_zerocopy_callback()이 호출돼 사용자 공간에 완료를 알립니다.

커널 소스 분석: skb_clone() vs skb_copy()

두 함수는 목적이 완전히 다릅니다. skb_clone()은 데이터 버퍼를 공유하므로 매우 빠르지만 데이터를 수정할 수 없고, skb_copy()는 독립 복사본을 만들어 자유로운 수정이 가능합니다.

/* net/core/skbuff.c — skb_clone(): 메타데이터만 복사, 버퍼 공유 */
struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask)
{
    struct sk_buff_fclones *fclones;
    struct sk_buff *n;

    fclones = container_of(skb, struct sk_buff_fclones, skb1);

    /* 1단계: fclone 슬롯 재활용 시도 (추가 할당 없음) */
    if (skb->fclone == SKB_FCLONE_ORIG &&
        refcount_read(&fclones->fclone_ref) == 1) {
        n = &fclones->skb2;
        refcount_set(&fclones->fclone_ref, 2);
        n->fclone = SKB_FCLONE_CLONE;
        goto do_clone;
    }

    /* 2단계: fclone 불가 → 일반 kmem_cache 할당 */
    n = kmem_cache_alloc(skbuff_head_cache, gfp_mask);
    if (!n)
        return NULL;
    n->fclone = SKB_FCLONE_UNAVAILABLE;

do_clone:
    /* 3단계: sk_buff 헤더 복사 (데이터 버퍼 포인터 포함) */
    __copy_skb_header(n, skb);

    /* 4단계: clone 플래그 설정 및 dataref 증가 */
    n->cloned = 1;
    skb->cloned = 1;
    n->nohdr = 0;
    atomic_inc(&skb_shinfo(skb)->dataref);

    /* 5단계: page fragment refcount 증가 */
    skb_clone_fraglist(n);
    skb_clone_sk(n);  /* sk가 있으면 소켓 메모리 카운터 참조 */
    return n;
}

/* net/core/skbuff.c — skb_copy(): 완전한 독립 복사본 생성 */
struct sk_buff *skb_copy(const struct sk_buff *skb, gfp_t gfp_mask)
{
    int headerlen = skb_headroom(skb);
    unsigned int size;
    struct sk_buff *n;

    /* linear 크기 + headroom + skb_shared_info */
    size = skb_end_offset(skb) + skb->data_len;
    n = __alloc_skb(size, gfp_mask, SKB_ALLOC_RX, NUMA_NO_NODE);
    if (!n)
        return NULL;

    /* headroom 확보 */
    skb_reserve(n, headerlen);

    /* linear 데이터 전체 복사 */
    if (skb_copy_bits(skb, -headerlen, n->head, headerlen + skb->len))
        BUG();
    skb_put(n, skb->len);

    /* 메타데이터/헤더 오프셋 복사 */
    __copy_skb_header(n, skb);
    skb_headers_offset_update(n, nhead);
    return n;
}
코드 설명
  • skb_clone 7행container_of로 skb1을 포함한 sk_buff_fclones 구조체 전체 포인터를 구합니다. 이를 통해 skb2(clone 슬롯)에 직접 접근할 수 있습니다.
  • skb_clone 10-14행fclone_ref == 1이면 skb2 슬롯이 현재 미사용 중임을 의미합니다. refcount를 2로 설정하고 skb2를 clone으로 사용하면 kmem_cache_alloc() 호출을 완전히 생략할 수 있습니다. TCP 재전송이 빈번한 환경에서 큰 성능 이득을 줍니다.
  • skb_clone 18-21행fclone 슬롯을 쓸 수 없을 때 일반 skbuff_head_cache에서 새 sk_buff를 할당합니다. 이 경우 추가 메모리 할당이 발생합니다.
  • skb_clone 25-30행__copy_skb_header()는 cb[]를 제외한 헤더 포인터(transport/network/mac), 프로토콜, 플래그 등을 복사합니다. head/data/tail/end 포인터도 그대로 복사되므로 두 sk_buff가 같은 데이터 버퍼를 참조하게 됩니다.
  • skb_clone 32행atomic_inc로 dataref를 증가시킵니다. kfree_skb()는 dataref가 1 이하일 때만 실제 데이터 버퍼를 해제합니다. 이 카운터가 clone 수명 관리의 핵심입니다.
  • skb_copy 46-48행__alloc_skb()를 SKB_ALLOC_RX 플래그로 호출해 완전히 새로운 skb와 데이터 버퍼를 할당합니다. size에 data_len을 더해 paged 데이터까지 선형화할 공간을 확보합니다.
  • skb_copy 53-55행skb_copy_bits()는 선형 및 paged 데이터를 모두 선형 버퍼로 복사합니다. 헤더 영역(-headerlen 오프셋)부터 전체 len을 복사하므로 완전한 독립 복사본이 됩니다.

커널 소스 분석: skb_segment() 상세

skb_segment()는 GSO super-packet을 MTU 크기 세그먼트로 분할하는 소프트웨어 GSO 엔진입니다. 헤더 복사, 페이로드 참조(또는 복사), IP/TCP 헤더 필드 갱신을 세그먼트마다 수행합니다.

skb_segment() — 메모리 구조 변환 원본 super-skb 헤더 (L2+L3+L4): doffset 바이트 페이로드: N × MSS 바이트 gso_size=MSS, gso_segs=N skb_segment() 분할된 세그먼트 체인 헤더 복사 (doffset) 페이로드[0..MSS-1] seg 1 (IP.id=0, seq=N) gso_size=0 헤더 복사 페이로드[MSS..] seg 2 (IP.id=1) gso_size=0 → ··· 헤더 복사 페이로드[나머지] seg N (+PSH 플래그) gso_size=0 skb_segment() 반복 루프 내부 동작 (세그먼트마다) ① 새 skb 할당 alloc_skb(hsize+doffset +headroom, GFP_ATOMIC) ② 헤더 복사 skb_copy_from_linear_data (head_skb, dst, doffset) ③ 페이로드 처리 SG: page_frag 참조 비SG: linear 복사 ④ 헤더 필드 갱신 IP: tot_len, id++, 체크섬 재계산 TCP: seq += prev_mss, PSH(마지막) SG(Scatter-Gather) 지원 여부에 따른 차이 NIC이 SG DMA를 지원하면 페이로드를 page fragment 참조로 처리(zero-copy). 미지원 시 linear 복사 발생. 분할 완료: 각 세그먼트의 gso_size=0, gso_segs=0, gso_type=0 → NIC에 일반 패킷으로 전달 원본 super-skb는 consume_skb()로 해제 → 세그먼트 체인만 남음
skb_segment() 루프: 세그먼트마다 새 skb 할당 → 헤더 복사 → 페이로드 처리(SG 참조 또는 복사) → IP/TCP 필드 갱신
/* net/core/skbuff.c — skb_segment() 핵심 루프 (단순화) */
struct sk_buff *skb_segment(struct sk_buff *head_skb,
                           netdev_features_t features)
{
    struct sk_buff *segs = NULL, **tail = &segs;
    unsigned int mss   = skb_shinfo(head_skb)->gso_size;
    unsigned int doffset = head_skb->data - skb_mac_header(head_skb);
    bool sg = !!(features & NETIF_F_SG);
    unsigned int offset = doffset;  /* 현재 페이로드 처리 위치 */
    int err = -ENOMEM;

    do {
        struct sk_buff *nskb;
        int hsize = min_t(int, doffset + mss, head_skb->len) - doffset;

        /* ① 새 세그먼트용 sk_buff 할당 */
        nskb = alloc_skb(hsize + doffset + headroom, GFP_ATOMIC);
        if (unlikely(!nskb)) {
            err = -ENOMEM;
            goto err;
        }

        /* ② 세그먼트 체인에 연결 */
        *tail = nskb;
        tail = &nskb->next;

        /* ③ headroom 확보 후 L2/L3/L4 헤더 복사 */
        skb_reserve(nskb, headroom);
        skb_put(nskb, doffset);
        skb_copy_from_linear_data(head_skb, nskb->data, doffset);

        /* ④ 페이로드: SG 지원 시 page frag 참조, 아니면 memcpy */
        if (sg) {
            skb_fill_page_desc(nskb, 0,
                frag->bv_page, frag->bv_offset + offset, hsize);
        } else {
            skb_copy_from_linear_data_offset(head_skb, offset,
                skb_put(nskb, hsize), hsize);
        }

        /* ⑤ 세그먼트 GSO 필드 초기화 (더 이상 GSO 아님) */
        skb_shinfo(nskb)->gso_size = 0;
        skb_shinfo(nskb)->gso_segs = 0;
        skb_shinfo(nskb)->gso_type = 0;

        offset += hsize;
    } while (offset < head_skb->len - doffset);

    return segs;
err:
    kfree_skb_list(segs);
    return ERR_PTR(err);
}
코드 설명
  • 4행tail 이중 포인터로 segs 체인을 효율적으로 구성합니다. 첫 세그먼트는 segs에 연결하고, 이후 세그먼트는 이전 nskb->next에 연결됩니다.
  • 5행gso_size가 MSS(Maximum Segment Size). TCP에서는 협상된 MSS에서 TCP/IP 헤더 크기를 뺀 값입니다.
  • 6행doffset은 MAC 헤더부터 데이터 시작까지의 바이트 수 즉 L2+L3+L4 헤더 전체 길입니다. 각 세그먼트에 이 헤더를 복사합니다.
  • 7행NETIF_F_SG 비트로 NIC의 Scatter-Gather DMA 지원 여부를 확인합니다. SG를 지원하면 페이로드를 zero-copy로 참조할 수 있습니다.
  • 15행GFP_ATOMIC으로 할당하는 이유는 이 함수가 softirq 컨텍스트(dev_queue_xmit())에서 호출될 수 있기 때문입니다. 슬립이 허용되지 않습니다.
  • 29행skb_copy_from_linear_data()는 원본 skb의 MAC 헤더부터 doffset 바이트를 새 세그먼트에 복사합니다. IP.tot_len, TCP.seq 등은 이후 별도 갱신됩니다.
  • 32-38행SG가 지원되면 skb_fill_page_desc()로 원본 페이지를 참조만 하므로 메모리 복사가 없습니다. 미지원 시 skb_copy_from_linear_data_offset()으로 데이터를 실제로 복사합니다.
  • 41-43행세그먼트의 gso_size를 0으로 설정해 이 세그먼트가 더 이상 GSO 처리가 필요하지 않음을 표시합니다. NIC 드라이버는 이 값이 0인 일반 패킷으로 취급해 DMA 처리합니다.
  • 48-49행실패 시 kfree_skb_list()로 지금까지 생성된 모든 세그먼트를 한 번에 해제합니다. ERR_PTR()로 오류 코드를 포인터에 인코딩해 반환합니다.

네트워크 네임스페이스와 sk_buff

Linux 네트워크 네임스페이스는 독립된 네트워크 스택(인터페이스, 라우팅, iptables, 소켓)을 제공합니다. sk_buff는 skb->dev를 통해 네임스페이스에 소속되며, veth, bridge 등을 통해 네임스페이스를 넘나들 때 sk_buff의 처리가 변화합니다.

/* sk_buff가 속한 네트워크 네임스페이스 확인 */
static inline struct net *dev_net(const struct net_device *dev)
{
    return read_pnet(&dev->nd_net);
}

/* skb->dev를 통해 네임스페이스 참조 */
struct net *net = dev_net(skb->dev);
/* → net->ipv4.ip_forward (포워딩 설정)
 * → net->ct.nf_conntrack_hash (conntrack 해시)
 * → net->loopback_dev (lo 인터페이스)
 * → 모두 네임스페이스별 독립 */

/* veth: 네임스페이스 간 패킷 전달 */
/* drivers/net/veth.c — veth_xmit() */
static netdev_tx_t veth_xmit(struct sk_buff *skb,
                              struct net_device *dev)
{
    struct veth_priv *priv = netdev_priv(dev);
    struct net_device *rcv = rcu_dereference(priv->peer);
    /* rcv는 다른 네임스페이스의 veth peer 디바이스 */

    /* skb->dev를 peer 디바이스로 교체 → 네임스페이스 전환 */
    skb->dev = rcv;

    /* L2 헤더 재처리 */
    skb->protocol = eth_type_trans(skb, rcv);
    /* → 이제 skb는 rcv가 속한 네임스페이스에서 처리됨
     * → rcv 네임스페이스의 Netfilter, 라우팅, 소켓 lookup 적용 */

    if (likely(veth_forward_skb(rcv, skb, priv, rq, rcv_xdp) == NET_RX_SUCCESS))
        return NETDEV_TX_OK;
    /* veth_forward_skb → netif_rx() 또는 napi_gro_receive()
     * → rcv 네임스페이스의 네트워크 스택에 진입 */
}

/* 네임스페이스 경계에서 주의할 skb 처리 */
/* 1. conntrack: 네임스페이스별 독립 → skb->_nfct 초기화 필요할 수 있음 */
/* 2. skb->mark: 네임스페이스 간 보존됨 → 의도하지 않은 정책 적용 주의 */
/* 3. skb->sk: NULL이 아니면 소켓 네임스페이스와 dev 네임스페이스 불일치 가능 */
ℹ️

컨테이너 네트워킹과 skb: Docker/Kubernetes의 Pod 네트워킹은 veth 쌍을 통해 구현됩니다. 호스트 네임스페이스의 veth에서 dev_queue_xmit(skb)를 호출하면 skb->dev가 컨테이너 네임스페이스의 peer veth로 교체되어 netif_rx()로 재진입합니다. 이 과정에서 XDP는 veth 드라이버에서 실행되어 컨테이너로 진입하기 전에 패킷을 필터링/리다이렉트할 수 있습니다 (Cilium의 veth XDP 모드).

IP 프래그먼테이션(Fragmentation)과 skb 재조립

IP 프래그먼테이션(IP Fragmentation)은 MTU(Maximum Transmission Unit)를 초과하는 패킷을 여러 조각으로 분할하고, 수신 측에서 재조립(Reassembly)하는 메커니즘입니다. Linux 커널은 이 과정을 모두 sk_buff 체인으로 처리하며, 메모리 제한·보안·성능 측면에서 정교한 관리가 필요합니다.

ip_fragment() / ip_do_fragment() 분할 경로

전송 경로에서 패킷이 경로 MTU를 초과하면 ip_fragment()가 호출됩니다. 이 함수는 원본 sk_buff를 MTU 크기에 맞는 여러 sk_buff로 분할합니다.

/* net/ipv4/ip_output.c */
int ip_fragment(struct net *net, struct sock *sk,
               struct sk_buff *skb, unsigned int mtu,
               int (*output)(struct net *, struct sock *, struct sk_buff *))
{
    struct iphdr *iph = ip_hdr(skb);

    /* DF 비트가 설정되어 있으면 ICMP Fragmentation Needed 전송 후 폐기 */
    if (unlikely((iph->frag_off & htons(IP_DF)) &&
                 !(skb->ignore_df))) {
        IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
        icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
                  htonl(mtu));
        kfree_skb(skb);
        return -EMSGSIZE;
    }

    return ip_do_fragment(net, sk, skb, output);
}

int ip_do_fragment(struct net *net, struct sock *sk,
                   struct sk_buff *skb,
                   int (*output)(struct net *, struct sock *, struct sk_buff *))
{
    struct iphdr *iph;
    int ptr;
    struct sk_buff *skb2;
    unsigned int mtu, hlen, left, len, ll_rs;
    int offset;

    iph = ip_hdr(skb);
    mtu = ip_skb_dst_mtu(sk, skb);

    /* 이미 프래그먼트된 패킷 (frag_list 보유) 처리 — 빠른 경로 */
    if (skb_has_frag_list(skb)) {
        return ip_fragment_fast(net, sk, skb, output, mtu);
    }

    hlen = iph->ihl * 4;
    mtu = mtu - hlen;           /* 페이로드(Payload) 기준 MTU */
    /* 8바이트 정렬 필수 (fragment offset 필드는 8바이트 단위) */
    mtu &= ~7;

    left = skb->len - hlen;     /* 분할 대상 페이로드 길이 */
    ptr  = hlen;                /* 현재 읽기 오프셋 */
    offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;

    while (left > 0) {
        len = left;
        if (len > mtu)
            len = mtu;

        /* 마지막 조각이 아닌 경우 8바이트 정렬 */
        if (left > len)
            len &= ~7;

        /* 새 sk_buff 할당: IP 헤더 + 페이로드 len바이트 */
        skb2 = alloc_skb(len + hlen + ll_rs, GFP_ATOMIC);
        if (!skb2) {
            IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
            return -ENOMEM;
        }

        /* 메타데이터 복사 (priority, mark, dst 등) */
        ip_copy_metadata(skb2, skb);
        skb_reserve(skb2, ll_rs);
        skb_put(skb2, len + hlen);
        memcpy(skb2->data, skb->data, hlen); /* IP 헤더 복사 */

        /* 페이로드 복사 (비선형 skb도 처리) */
        if (skb_copy_bits(skb, ptr, skb2->data + hlen, len))
            BUG();

        /* fragment offset 필드 설정 (8바이트 단위) */
        iph = (struct iphdr *)skb2->data;
        iph->frag_off = htons((offset >> 3) |
                         (left > len ? IP_MF : 0) |
                         (ntohs(ip_hdr(skb)->frag_off) & IP_DF));
        iph->tot_len = htons(len + hlen);
        ip_send_check(iph);         /* IP 체크섬 재계산 */

        err = output(net, sk, skb2);
        if (err)
            goto fail;

        offset += len;
        left   -= len;
        ptr    += len;
        IP_INC_STATS(net, IPSTATS_MIB_FRAGCREATES);
    }

    consume_skb(skb);
    IP_INC_STATS(net, IPSTATS_MIB_FRAGOKS);
    return 0;
}
ip_do_fragment() 핵심 포인트
  • 8바이트 정렬 필수: IPv4 fragment offset 필드는 8바이트 단위 값을 저장합니다. mtu &= ~7len &= ~7로 정렬을 보장합니다.
  • GFP_ATOMIC 할당: 전송 경로는 소프트 인터럽트 컨텍스트일 수 있으므로 절대 블로킹 불가. GFP_ATOMIC이 필수입니다.
  • MF 비트: 마지막 조각을 제외한 모든 조각에 IP_MF(More Fragments) 플래그를 설정합니다. 수신 측은 이 비트로 마지막 조각을 인식합니다.
  • ip_copy_metadata(): priority, mark, dst(routing table 항목), skb_dst()를 복사하여 모든 조각이 동일한 라우팅 컨텍스트를 갖게 합니다.
  • IPSTATS 카운터: FRAGCREATES(생성 조각 수), FRAGOKS(성공), FRAGFAILS(실패)로 /proc/net/snmp에서 모니터링할 수 있습니다.

ip_defrag()와 frag_list 재조립

수신 측에서는 ip_defrag()가 분산된 조각들을 모아 원본 패킷을 재조립합니다. 재조립된 패킷은 첫 번째 sk_buff의 skb_shinfo()->frag_list에 나머지 조각을 연결한 형태로 표현됩니다.

/* net/ipv4/ip_fragment.c */

/* inet_frag_queue: 동일 (src, dst, id, proto) 4-tuple의 조각 집합 */
struct inet_frag_queue {
    struct rhash_head    node;       /* rhashtable 해시 노드 */
    union {
        struct frag_v4_compare_key v4; /* IPv4 키: src,dst,id,proto */
        struct frag_v6_compare_key v6; /* IPv6 키: src,dst,id       */
    } key;
    struct timer_list   timer;      /* 만료 타이머 (ipfrag_time) */
    spinlock_t          lock;
    refcount_t          refcnt;
    struct rb_root      rb_fragments; /* 오프셋 순 red-black tree  */
    struct sk_buff      *fragments_tail;
    ktime_t             stamp;
    int                 len;        /* 현재까지 수신된 총 바이트 */
    int                 meat;       /* 중복/갭 제외 유효 바이트   */
    u8                  flags;
    u16                 max_size;   /* 가장 큰 조각 크기           */
    struct netns_frags  *net;
    struct rcu_head     rcu;
};

int ip_defrag(struct net *net, struct sk_buff *skb, u32 user)
{
    struct net_device *dev = skb->dev;
    struct ipq *qp;
    int ret;

    __IP_INC_STATS(net, IPSTATS_MIB_REASMREQDS);
    skb_orphan(skb);           /* 소켓 참조 제거 */

    /* 메모리 임계값 초과 시 오래된 큐 축출(evict) */
    ip_evictor(net);

    /* 4-tuple로 기존 조각 큐 탐색 또는 신규 생성 */
    qp = ip_find(net, ip_hdr(skb), user, skb->dev->ifindex);
    if (qp) {
        ret = ip_frag_queue(qp, skb);  /* 큐에 조각 삽입 */

        if (ret == 1) {
            /* 모든 조각 수신 완료 → 재조립 */
            skb = ip_frag_reasm(qp, &dev);
            ipq_put(qp);
            return net_xmit_eval(ip_local_deliver(skb));
        }
        ipq_put(qp);
        return -EINPROGRESS; /* 아직 더 기다려야 함 */
    }
    return -ENOMEM;
}

/* ip_frag_reasm(): frag_list 구성 */
static struct sk_buff *ip_frag_reasm(struct ipq *qp,
                                        struct net_device **devp)
{
    struct sk_buff *fp, *head = qp->q.fragments;
    struct skb_shared_info *shinfo;

    /* head = 첫 번째 조각 (offset==0) */
    shinfo = skb_shinfo(head);
    shinfo->frag_list = head->next;
    head->next = NULL;

    head->len = head->data_len = qp->q.len - (head->len - head->data_len);

    struct iphdr *iph = ip_hdr(head);
    iph->frag_off = 0;
    iph->tot_len  = htons(qp->q.len);
    __IP_INC_STATS(qp->q.net, IPSTATS_MIB_REASMOKS);

    qp->q.fragments = NULL;
    return head;
}
재조립 메커니즘 상세 설명
  • rb_fragments red-black tree: 조각들을 fragment offset 기준으로 정렬 유지합니다. O(log n) 삽입과 겹침(overlap) 탐지가 가능합니다.
  • frag_list 구성: ip_frag_reasm()은 head sk_buff의 skb_shinfo()->frag_list에 나머지 조각들을 연결합니다. 상위 레이어는 이 비선형 skb를 단일 패킷으로 처리합니다.
  • -EINPROGRESS 반환: 아직 모든 조각이 도착하지 않은 경우 반환하며, skb는 큐에 보관됩니다. NF_STOLEN과 유사하게 호출자는 해당 skb를 더 이상 건드리면 안 됩니다.
  • skb_orphan(): 수신된 조각을 큐에 넣기 전에 소켓 참조를 제거합니다. 조각들은 소켓이 아닌 inet_frag_queue가 소유합니다.
  • ip_evictor(): 메모리 사용량이 high_thresh를 초과하면 가장 오래된 큐를 삭제합니다. ICMP Time Exceeded를 보내지 않으므로 조용히 드롭됩니다.

메모리 제한: ipfrag_high_thresh / ipfrag_low_thresh

재조립 대기 중인 조각들은 시스템 메모리를 사용합니다. Linux는 두 임계값으로 이를 제한합니다.

/* /proc/sys/net/ipv4/ipfrag_high_thresh (기본 4MB) */
/* /proc/sys/net/ipv4/ipfrag_low_thresh  (기본 3MB) */
/* /proc/sys/net/ipv4/ipfrag_time        (기본 30초) */

struct netns_frags {
    long        high_thresh;  /* 축출 시작 임계값 */
    long        low_thresh;   /* 축출 중단 임계값 */
    int         timeout;      /* 큐 만료 시간(초) */
    struct rhashtable rhashtable; /* 조각 큐 해시 테이블 */
    atomic_long_t mem;           /* 현재 사용 중인 메모리 */
};

/* 현재 메모리 사용량 확인: $ cat /proc/net/stat/ip_frag */

/* sysctl 튜닝 예시 — 고성능 서버: 256MB로 확장 */
/* $ sysctl -w net.ipv4.ipfrag_high_thresh=268435456 */
/* $ sysctl -w net.ipv4.ipfrag_low_thresh=201326592  */
/* $ sysctl -w net.ipv4.ipfrag_time=10               */

메모리 제한 초과 시 동작: ipfrag_high_thresh를 초과하면 ip_evictor()가 가장 오래된 조각 큐를 삭제합니다. 삭제된 큐의 첫 번째 조각에 대해 ICMP Time Exceeded를 전송하고 나머지는 조용히 드롭됩니다. 이로 인해 TCP 재전송이 발생할 수 있으므로 DDoS 공격 환경에서는 임계값을 적절히 낮게 설정하는 것이 중요합니다.

프래그먼트 공격 방어

프래그먼트 공격(Fragment Attack)은 고의적으로 비정상적인 조각을 전송해 재조립 버퍼를 소진하거나, 방화벽을 우회하는 기법입니다.

/* 겹침 조각(Overlapping Fragment) 처리 */
static int ip_frag_queue(struct ipq *qp, struct sk_buff *skb)
{
    int ihl, end, flags;
    struct sk_buff *prev_tail;

    /* 이미 타임아웃된 큐에 조각 도착 시 즉시 드롭 */
    if (qp->q.flags & INET_FRAG_COMPLETE)
        goto err;

    ihl = ip_hdrlen(skb);
    end = offset + skb->len - ihl;
    flags = ntohs(ip_hdr(skb)->frag_off);

    /* 최소 8바이트: 마지막 조각 아닌데 MF=0이면 공격으로 간주 */
    if ((flags & IP_MF) == 0 && end < qp->q.len) {
        err = -EINVAL;
        goto err;
    }

    /* 겹침 감지 */
    prev_tail = qp->q.fragments_tail;
    if (prev_tail &&
        offset < prev_tail->ip_defrag_offset + prev_tail->len - ihl) {
        /* 겹침 발생 → 큐 폐기 */
        __IP_INC_STATS(qp->q.net, IPSTATS_MIB_REASMFAILS);
        goto err;
    }

    /* 총 크기가 65535 초과 불가 (IP 최대 패킷 크기) */
    if (end > 65535) {
        __IP_INC_STATS(qp->q.net, IPSTATS_MIB_REASMFAILS);
        goto err;
    }
    return 0;
}

RFC 5722 vs IPv4 겹침 처리 차이: IPv6는 RFC 5722에 따라 겹치는 조각이 하나라도 있으면 전체 재조립 큐를 폐기하고 ICMP Parameter Problem을 전송합니다. IPv4는 역사적으로 겹침을 허용적으로 처리했으나 Linux 4.x 이후 겹침 조각 큐를 즉시 폐기하는 방향으로 강화되었습니다.

프래그먼테이션 → 재조립 SVG 다이어그램

전송 측 (ip_do_fragment) 원본 sk_buff len=4096 mtu=1500 ip_do_fragment() 조각 #1 off=0, MF=1, 1480B 조각 #2 off=1480, MF=1, 1480B 조각 #3 (마지막) off=2960, MF=0, 1136B 네트워크 전송 수신 측: inet_frag_queue inet_frag_queue key: (src,dst,id,proto) rb_fragments (red-black tree) 오프셋 순 정렬 보장 skb#1 off=0 skb#2 off=1480 skb#3 mem: truesize 누적 timer: ipfrag_time(30s) meat == len → 완성 high_thresh 초과 → evict ip_frag_reasm() 재조립 결과 (frag_list) head sk_buff data=조각#1 페이로드 frag_list → skb#2 skb#2 (frag_list next) off=1480, next → skb#3 skb#3 (frag_list tail) off=2960, next=NULL ip_local_deliver() 상위 프로토콜(TCP/UDP) IPv6 프래그먼테이션 차이 • 라우터 중간 분할 금지 (RFC 8200) • Fragment Extension Header 사용 • 겹침 즉시 큐 폐기 (RFC 5722) • ip6_frag_queue() / ip6_frag_reasm() 프래그먼트 공격 방어 • Teardrop: 겹침 조각 → 즉시 드롭 • Tiny Fragment: 8B 최소 크기 검사 • 크기 65535 초과 → REASMFAILS++ • ipfrag_time으로 stale 큐 만료 성능 고려사항 • 프래그먼테이션 자체가 CPU 비용 높음 • PMTUD로 프래그먼테이션 회피 권장 • GSO: 커널 내부 분할 (오프로드 가능) • sysctl net.ipv4.ip_no_pmtu_disc=0 • 터널 환경: PMTUD 차단 빈번 → MTU 수동 설정

IPv6 프래그먼테이션 차이

IPv6는 IPv4와 달리 중간 라우터가 분할을 수행하지 않습니다. 오직 송신 단말만 분할할 수 있으며, Fragment Extension Header를 사용합니다.

/* net/ipv6/ip6_output.c */
/* IPv6 Fragment Extension Header (8바이트) 삽입 */
/* next_header | reserved | fragment_offset | M | identification */
fh->nexthdr      = nexthdr;
fh->reserved     = 0;
fh->frag_off     = htons(offset);   /* 하위 1비트: M 플래그 */
fh->identification = frag_id;      /* 32비트 (IPv4보다 큼) */

/* IPv6 최소 MTU: 1280 바이트 (RFC 8200) */
/* 라우터가 분할 불가하므로 ICMP Packet Too Big 반환 */

/* net/ipv6/reassembly.c: 겹침 즉시 전체 큐 폐기 (RFC 5722) */
if (ip6_frag_too_far(fq) || ip6_frag_overlap(fq, skb)) {
    inet_frag_kill(&fq->q);
    icmpv6_send(skb, ICMPV6_PARAMPROB, ICMPV6_HDR_FIELD, 0);
    goto discard;
}
IPv4 vs IPv6 프래그먼테이션 비교
항목IPv4IPv6
중간 라우터 분할허용금지 (RFC 8200)
Fragment ID16비트32비트
최소 MTU68바이트1280바이트
겹침 처리허용적 (잘라냄)즉시 큐 폐기 (RFC 5722)
분할 헤더IP 헤더 필드Fragment Extension Header
재조립 코드ip_defrag()ipv6_frag_rcv()
sysctl 네임스페이스net.ipv4.ipfrag_*net.ipv6.ip6frag_*

성능 영향과 튜닝

/* 프래그먼테이션 통계 확인 */
/* $ cat /proc/net/snmp | grep -i frag */
/* Ip: ReasmReqds ReasmOKs ReasmFails FragOKs FragFails FragCreates */

/* PMTU Discovery 활성화로 프래그먼테이션 회피 */
/* $ sysctl -w net.ipv4.ip_no_pmtu_disc=0    # 기본값 */

/* 재조립 메모리 모니터링: $ cat /proc/net/stat/ip_frag */
/* 열: timeout  queue  mem(KB)  hash  backlog  oom */

/* GRO는 IP 프래그먼트를 병합하지 않음 (오직 TCP/UDP 세그먼트만) */
/* → IP 프래그먼트는 항상 소프트웨어 재조립 경로 통과 */

/* BIG TCP (Linux 5.19+): jumbo frame 없이 64KB 초과 세그먼트 */
/* $ ip link set eth0 gso_max_size 65536 */

실전 네트워크 드라이버의 skb 사용 패턴

네트워크 드라이버는 sk_buff를 할당·구성·전달·해제하는 주체입니다. 드라이버의 skb 처리 방식은 성능에 직접적인 영향을 미치며, 커널 버전과 하드웨어 기능에 따라 다양한 패턴이 사용됩니다.

패턴 1: 기본 수신 — 전통적 copybreak

copybreak 패턴은 소형 패킷은 복사(copy)하고, 대형 패킷은 페이지를 직접 참조합니다. 이를 통해 DMA 버퍼 재활용과 메모리 낭비 감소를 동시에 달성합니다.

/* 전통적 copybreak 수신 패턴 (e.g., igb, e1000e 계열) */
#define COPYBREAK_DEFAULT 256  /* 256바이트 이하: 복사 */

static void driver_clean_rx_irq(struct driver_ring *rx_ring, int budget)
{
    struct sk_buff *skb;
    unsigned int size;

    while (budget--) {
        size = le16_to_cpu(rx_desc->wb.upper.length);

        if (size <= rx_ring->rx_copybreak) {
            /* 소형 패킷: 새 skb 할당 후 복사 */
            skb = napi_alloc_skb(&rx_ring->napi, size);
            if (unlikely(!skb))
                goto drop;

            dma_sync_single_for_cpu(rx_ring->dev,
                rx_buffer->dma, size, DMA_FROM_DEVICE);
            skb_put_data(skb,
                page_address(rx_buffer->page) + rx_buffer->page_offset,
                size);
            /* DMA 버퍼는 그대로 재활용 가능 */
        } else {
            /* 대형 패킷: build_skb로 DMA 버퍼를 skb에 직접 연결 */
            dma_unmap_single(rx_ring->dev, rx_buffer->dma,
                             rx_ring->rx_buf_len, DMA_FROM_DEVICE);
            skb = build_skb(rx_buffer->page_address,
                             rx_ring->rx_buf_len + SKB_DATA_ALIGN(
                             sizeof(struct skb_shared_info)));
            if (unlikely(!skb))
                goto drop;
            skb_put(skb, size);
            rx_buffer->page = NULL; /* 페이지 소유권 이전 */
        }

        napi_gro_receive(&rx_ring->napi, skb);
    }
}
copybreak 패턴 설명
  • copybreak 임계값: 일반적으로 256~1024바이트로 설정합니다. 소형 패킷은 복사 비용이 작고, DMA 버퍼를 계속 재사용할 수 있어 이득입니다.
  • napi_alloc_skb(): NAPI 컨텍스트 전용 할당자. napi_alloc_cache per-CPU 캐시를 사용해 일반 alloc_skb()보다 빠릅니다.
  • build_skb(): 이미 할당된 메모리(DMA 버퍼)를 sk_buff의 데이터 영역으로 래핑합니다. 추가 메모리 복사 없이 zero-copy 수신이 가능합니다.
  • dma_sync_single_for_cpu(): 복사하는 경우에만 CPU 가시성을 보장합니다. build_skb 경로에서는 dma_unmap이 필요합니다.

패턴 2: Header Split — 헤더/페이로드 분리 수신

최신 NIC는 헤더를 선형 영역에, 페이로드를 page fragment에 별도 저장하는 Header Split을 지원합니다. 이를 통해 헤더 파싱 성능과 페이로드 zero-copy를 동시에 달성합니다.

/* Header Split 패턴 (mlx5, ice, bnxt 계열) */
static struct sk_buff *driver_build_skb_header_split(
    struct driver_rx_ring *ring,
    struct driver_rx_desc *desc)
{
    struct sk_buff *skb;
    void *hdr_buf = ring->hdr_buf[desc->hdr_buf_idx];
    u16 hdr_len  = desc->hdr_len;   /* NIC가 파싱한 헤더 길이 */
    u16 data_len = desc->data_len;  /* 페이로드 길이 */

    /* 헤더: 선형(linear) 영역 — napi_alloc_skb */
    skb = napi_alloc_skb(&ring->napi, hdr_len);
    if (unlikely(!skb))
        return NULL;

    skb_put_data(skb, hdr_buf, hdr_len);

    if (data_len) {
        /* 페이로드: page fragment로 추가 — 복사 없음 */
        struct page *page = ring->rx_pages[desc->data_buf_idx];
        skb_add_rx_frag(skb, skb_shinfo(skb)->nr_frags,
                        page, desc->data_buf_offset, data_len,
                        ring->truesize);
        get_page(page);
    }

    /* skb->data_len > 0 → 비선형 패킷 */
    /* 상위 스택은 pskb_may_pull()로 필요 시 선형화 */
    return skb;
}

/* skb_add_rx_frag() 내부 */
static inline void skb_add_rx_frag(struct sk_buff *skb, int i,
                                     struct page *page, int off,
                                     int size, unsigned int truesize)
{
    skb_fill_page_desc(skb, i, page, off, size); /* frags[i] 설정 */
    skb->len      += size;
    skb->data_len += size;
    skb->truesize += truesize; /* 소켓 버퍼 메모리 추적 */
}
Header Split 패턴의 장단점
  • 장점: TCP payload가 page fragment에 위치하므로 sendfile/splice로 zero-copy 전달 가능. NIC가 헤더를 별도 버퍼에 DMA하므로 CPU 캐시 친화적입니다.
  • 단점: 비선형 skb이므로 단순 포인터 접근 불가. pskb_may_pull(), skb_copy_bits() 사용이 필요합니다.
  • truesize 중요성: page fragment의 truesize는 실제 페이지 크기(보통 4096B)로 설정해야 소켓 메모리 accounting이 정확합니다.

패턴 3: page_pool 기반 수신 (Linux 6.x 현대 드라이버)

page_pool(페이지 풀)은 DMA 매핑 캐싱과 페이지 재활용을 통합하는 Linux 5.x+ 표준 API입니다. 현대 고성능 드라이버의 핵심 할당 메커니즘입니다.

/* page_pool 초기화 */
struct page_pool_params pp_params = {
    .order      = 0,              /* 4KB 단일 페이지 */
    .pool_size  = 512,            /* 풀 크기 */
    .nid        = dev_to_node(dev), /* NUMA 노드 */
    .dev        = ring->dev,       /* DMA 디바이스 */
    .dma_dir    = DMA_FROM_DEVICE,
    .offset     = NET_SKB_PAD,    /* headroom */
    .max_len    = PAGE_SIZE - NET_SKB_PAD,
    .flags      = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV,
};
ring->page_pool = page_pool_create(&pp_params);

/* 수신 처리: napi_build_skb + page_pool 반환 */
static struct sk_buff *driver_rx_page_pool(
    struct driver_ring *ring, struct page *page, u32 size)
{
    struct sk_buff *skb;

    /* DMA sync: CPU가 데이터를 읽기 전 캐시 일관성 보장 */
    page_pool_dma_sync_for_cpu(ring->page_pool, page,
                               ring->pp_params.offset, size);

    /* napi_build_skb: page_pool 인식 — 해제 시 자동 pool 반환 */
    skb = napi_build_skb(page_address(page), PAGE_SIZE);
    if (unlikely(!skb)) {
        page_pool_recycle_direct(ring->page_pool, page);
        return NULL;
    }

    skb_reserve(skb, ring->pp_params.offset);
    skb_put(skb, size);
    /* skb가 해제될 때 page_pool에 자동 반환됨 */
    return skb;
}
page_pool의 핵심 장점
  • DMA 매핑 캐시: page_pool은 페이지를 해제하지 않고 풀로 반환합니다. DMA 매핑이 유지되므로 다음 수신 시 dma_map_single() 비용이 없습니다.
  • napi_build_skb(): build_skb()와 달리 page_pool 메타데이터를 skb에 기록합니다. skb 해제 시 자동으로 page_pool_recycle_direct()가 호출됩니다.
  • PP_FLAG_DMA_SYNC_DEV: 이 플래그를 설정하면 page_pool이 자동으로 dma_sync_for_device()를 호출하므로 드라이버에서 직접 호출할 필요가 없습니다.
  • NUMA 친화적: nid 파라미터로 NIC와 같은 NUMA 노드의 메모리를 사용해 PCIe DMA 지연을 최소화합니다.

패턴 4: XDP → build_skb 변환

XDP_PASS를 반환한 패킷은 xdp_buff에서 sk_buff로 변환되어 일반 네트워크 스택에 진입합니다.

/* XDP → skb 변환 경로 */
static struct sk_buff *driver_xdp_build_skb(
    struct driver_ring *ring, struct xdp_buff *xdp)
{
    struct sk_buff *skb;
    unsigned int metasize = xdp->data - xdp->data_meta;
    unsigned int headroom = xdp->data - xdp->data_hard_start;
    unsigned int datasize = xdp->data_end - xdp->data;

    /* xdp_buff 메모리를 그대로 재사용: 추가 할당 없음 */
    skb = build_skb(xdp->data_hard_start,
                    xdp_data_hard_end(xdp) - xdp->data_hard_start +
                    SKB_DATA_ALIGN(sizeof(struct skb_shared_info)));
    if (unlikely(!skb))
        return NULL;

    skb_reserve(skb, headroom);
    __skb_put(skb, datasize);

    if (metasize)
        skb_metadata_set(skb, metasize); /* XDP metadata → skb cb */

    /* XDP multi-buffer 처리 (jumbo frame, Linux 5.18+) */
    if (xdp_buff_has_frags(xdp)) {
        struct skb_shared_info *sinfo = xdp_get_shared_info_from_buff(xdp);
        int i;
        skb_shinfo(skb)->nr_frags = sinfo->nr_frags;
        for (i = 0; i < sinfo->nr_frags; i++) {
            skb_shinfo(skb)->frags[i] = sinfo->frags[i];
            skb_frag_ref(skb, i);
        }
        skb->data_len = sinfo->xdp_frags_size;
        skb->len     += skb->data_len;
    }
    return skb;
}

패턴 5: TX 완료 처리와 BQL

전송 완료(TX completion) 처리는 DMA 매핑 해제와 sk_buff 해제를 수행합니다. BQL(Byte Queue Limits)은 전송 큐 깊이를 동적으로 조정해 지연을 최소화합니다.

/* TX 완료 처리 (NAPI poll 내에서 호출) */
static void driver_clean_tx(struct driver_tx_ring *tx_ring)
{
    struct sk_buff *skb;
    unsigned int total_bytes = 0, total_pkts = 0;

    while (driver_tx_desc_done(tx_ring)) {
        skb = tx_ring->tx_buffer[tx_ring->next_to_clean].skb;

        if (!skb)
            goto next_desc;

        /* DMA 매핑 해제 */
        dma_unmap_single(tx_ring->dev,
                         dma_unmap_addr(&tx_ring->tx_buffer[tx_ring->next_to_clean], dma),
                         dma_unmap_len(&tx_ring->tx_buffer[tx_ring->next_to_clean], len),
                         DMA_TO_DEVICE);

        total_bytes += skb->len;
        total_pkts++;

        napi_consume_skb(skb, budget); /* consume_skb + NAPI 힌트 */
        tx_ring->tx_buffer[tx_ring->next_to_clean].skb = NULL;

next_desc:
        tx_ring->next_to_clean = driver_ring_next(tx_ring->next_to_clean,
                                                    tx_ring->count);
    }

    /* BQL 완료 보고: 큐 깊이 동적 조정 */
    netdev_tx_completed_queue(txring_txq(tx_ring), total_pkts, total_bytes);

    if (netif_tx_queue_stopped(txring_txq(tx_ring)) &&
        driver_tx_has_space(tx_ring))
        netif_tx_wake_queue(txring_txq(tx_ring));
}

/* BQL 전송 시작 시 보고 */
static netdev_tx_t driver_xmit_frame(struct sk_buff *skb,
                                      struct net_device *netdev)
{
    if (driver_tx_ring_full(tx_ring)) {
        netif_tx_stop_queue(txring_txq(tx_ring));
        return NETDEV_TX_BUSY;
    }
    driver_tx_map_and_queue(tx_ring, skb);
    netdev_tx_sent_queue(txring_txq(tx_ring), skb->len); /* BQL 보고 */
    driver_tx_kick(tx_ring);
    return NETDEV_TX_OK;
}
BQL(Byte Queue Limits) 메커니즘
  • 목적: BQL은 네트워크 인터페이스 TX 큐에서 버퍼링되는 바이트 수를 동적으로 제한합니다. 큐가 너무 깊으면 latency가 증가하고, 너무 얕으면 throughput이 감소합니다.
  • netdev_tx_sent_queue(): 전송을 시작하기 전에 호출합니다. 큐에 들어간 바이트를 추적합니다.
  • netdev_tx_completed_queue(): TX 완료 인터럽트에서 호출합니다. 완료된 바이트를 반영하고 큐 제한을 동적으로 조정합니다.
  • napi_consume_skb(): consume_skb()에 NAPI poll 컨텍스트 힌트를 추가합니다. NAPI가 활성화된 경우 defer-free를 활용해 성능을 향상시킵니다.

패턴 6: Scatter-Gather TX와 DMA 매핑

/* Scatter-Gather TX: 비선형 skb의 모든 세그먼트 DMA 매핑 */
static int driver_tx_map_skb(struct driver_tx_ring *tx_ring,
                               struct sk_buff *skb)
{
    dma_addr_t dma;
    u16 i, count = 0;

    /* 1. 선형(head) 데이터 매핑 */
    dma = dma_map_single(tx_ring->dev, skb->data,
                         skb_headlen(skb), DMA_TO_DEVICE);
    if (dma_mapping_error(tx_ring->dev, dma))
        goto dma_error;

    driver_set_tx_desc(tx_ring, dma, skb_headlen(skb), TX_DESC_FIRST);
    count++;

    /* 2. frags[] 각 페이지 매핑 */
    for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
        const struct skb_frag_struct *frag = &skb_shinfo(skb)->frags[i];
        dma = skb_frag_dma_map(tx_ring->dev, frag, 0,
                               skb_frag_size(frag), DMA_TO_DEVICE);
        if (dma_mapping_error(tx_ring->dev, dma))
            goto dma_error;
        driver_set_tx_desc(tx_ring, dma, skb_frag_size(frag),
                           i + 1 == skb_shinfo(skb)->nr_frags ?
                           TX_DESC_LAST : TX_DESC_MIDDLE);
        count++;
    }

    /* 3. frag_list 처리 */
    if (skb_has_frag_list(skb)) {
        struct sk_buff *fskb;
        skb_walk_frags(skb, fskb)
            count += driver_tx_map_skb_frag(tx_ring, fskb);
    }
    return count;
}

패턴 7: NAPI poll 내부 skb 배치 처리

/* NAPI poll: 배치로 skb 처리하여 인터럽트 오버헤드 감소 */
static int driver_napi_poll(struct napi_struct *napi, int budget)
{
    struct driver_ring *rx_ring =
        container_of(napi, struct driver_ring, napi);
    int cleaned_count = 0;

    /* TX 완료 처리: RX보다 먼저 수행하여 메모리 조기 해제 */
    driver_clean_tx(driver_tx_ring(rx_ring));

    while (cleaned_count < budget) {
        struct sk_buff *skb = driver_fetch_rx_skb(rx_ring);
        if (!skb)
            break;

        /* GRO 처리: 동일 5-tuple 패킷 병합 */
        if (napi_gro_receive(napi, skb) == GRO_DROP)
            rx_ring->rx_stats.drop++;

        cleaned_count++;

        /* 주기적으로 RX 링 버퍼 보충 */
        if (cleaned_count % 16 == 0)
            driver_alloc_rx_buffers(rx_ring);
    }

    /* budget 미달: 모두 처리 완료 → 인터럽트 재활성화 */
    if (cleaned_count < budget) {
        napi_complete_done(napi, cleaned_count);
        driver_enable_irq(rx_ring);
    }

    driver_alloc_rx_buffers(rx_ring);
    return cleaned_count;
}

링 버퍼 ↔ skb 관계 SVG 다이어그램

RX 디스크립터 링 (DMA Ring) desc[0] addr: DMA len: 4096 desc[1] addr: DMA len: 4096 desc[2]←NTU STATUS:DD 완료 ... desc[N-1] addr: DMA ready NTC: next_to_clean NTU: next_to_use (driver 보충 위치) DD 비트: NIC 완료 표시 → NAPI가 확인 page_pool (DMA 매핑 캐시) page DMA매핑 유지됨 page DMA매핑 유지됨 page in-use (skb 소유) ... allocate: GFP_ATOMIC recycle: 자동 (skb 해제 시) miss: kmalloc fallback DMA 재매핑 없음 = 빠름 DMA addr sk_buff 메모리 레이아웃 (수신 완료 후) sk_buff (메타데이터) kmem_cache 할당 head/data/tail/end skb_shared_info nr_frags, frags[] dataref, destructor 선형 데이터 (head~tail) 헤더 영역 (copybreak 이하) frags[] (page_pool 페이지) zero-copy 페이로드 frag_list (IP 재조립/GSO) 일반적인 드라이버 버그 1. DMA 언매핑 누락 → IOMMU 리소스 고갈 2. skb 이중 해제 → use-after-free 크래시 3. truesize 과소 설정 → 소켓 메모리 accounting 오류 4. headroom 부족 → skb_push() BUG_ON 5. GFP_KERNEL in BH ctx → might_sleep() 경고 6. skb 소유권 불명확 → skb_orphan() 누락 7. budget 무시 → NAPI 독점 = softirq 기아 NAPI poll → 네트워크 스택 흐름 HW 인터럽트 napi_schedule() NET_RX_SOFTIRQ driver_napi_poll(budget=64) clean_tx() napi_gro_receive() alloc_rx_buf() netif_receive_skb() → TCP/IP 스택

드라이버 일반 버그와 예방

/* 버그 1: DMA 언매핑 경로 누락 → IOMMU 리소스 고갈 */
/* 잘못된 코드 */
if (driver_build_skb(ring, page, size) == NULL) {
    page_pool_recycle_direct(ring->pool, page); /* DMA 매핑 해제 없음! */
    return;
}
/* 올바른 코드 */
if (driver_build_skb(ring, page, size) == NULL) {
    page_pool_put_full_page(ring->pool, page, false); /* DMA 포함 반환 */
    return;
}

/* 버그 2: NAPI context에서 GFP_KERNEL 사용 */
skb = alloc_skb(size, GFP_KERNEL); /* NAPI poll = 소프트 인터럽트 → 금지! */
skb = napi_alloc_skb(&ring->napi, size); /* 올바른 코드 */

/* 버그 3: budget 초과 처리 → softirq 독점 */
static int good_napi_poll(struct napi_struct *napi, int budget)
{
    int work = 0;
    while (work < budget && driver_rx_pending()) { /* budget 준수 필수 */
        process_skb();
        work++;
    }
    if (work < budget)
        napi_complete_done(napi, work);
    return work;
}

/* 버그 4: skb headroom 부족 → L3/L4 헤더 추가 실패 */
if (skb_cow_head(skb, needed_headroom)) {
    kfree_skb(skb);  /* cow_head 실패 = ENOMEM → 드롭 */
    return -ENOMEM;
}
/* napi_alloc_skb(napi, len) 사용 시 자동으로 NET_SKB_PAD 확보 */

고급 디버깅: bpftrace와 perf를 활용한 skb 분석

sk_buff 관련 문제는 패킷 드롭, 메모리 누수, 성능 병목 등 다양한 형태로 나타납니다. Linux 커널은 bpftrace, perf, ftrace, kmemleak 등 강력한 도구를 제공하며, 이를 조합해 운영 환경에서도 안전하게 문제를 진단할 수 있습니다.

bpftrace one-liner 모음

### 1. kfree_skb 드롭 원인 히스토그램 (Linux 5.17+ reason 파라미터)
bpftrace -e 'tracepoint:skb:kfree_skb {
    @reason[args->reason] = count();
}
interval:s:5 { print(@reason); clear(@reason); }'

### 2. skb 수명 측정 (alloc → free 지연)
bpftrace -e 'kretprobe:__alloc_skb { @birth[retval] = nsecs; }
kprobe:kfree_skb / @birth[arg0] / {
    @latency_us = hist((nsecs - @birth[arg0]) / 1000);
    delete(@birth[arg0]);
}'

### 3. 드롭 핫스팟 (커널 스택 기준)
bpftrace -e 'tracepoint:skb:kfree_skb { @[kstack(5)] = count(); }
END { print(@); }'

### 4. 특정 포트 드롭 모니터링 (TCP 443만)
bpftrace -e 'tracepoint:skb:kfree_skb {
    $skb = (struct sk_buff *)args->skbaddr;
    $th = (struct tcphdr *)($skb->head + $skb->transport_header);
    if ($th->dest == 443 || $th->dest == bswap((uint16)443))
        @drops_port443[kstack(3)] = count();
}'

### 5. skb clone/copy 비율 (zero-copy 효율 측정)
bpftrace -e 'kprobe:skb_clone { @clone = count(); }
kprobe:skb_copy  { @copy  = count(); }
interval:s:5 { printf("clone=%d copy=%d
", @clone, @copy);
               clear(@clone); clear(@copy); }'

### 6. GRO 병합 효율 측정
bpftrace -e 'kretprobe:napi_gro_receive {
    @gro_result[retval] = count();
    /* GRO_MERGED=1, GRO_HELD=3, GRO_NORMAL=4, GRO_DROP=5 */
}
interval:s:5 { print(@gro_result); clear(@gro_result); }'

### 7. TCP 소켓 버퍼 초과 드롭 추적
bpftrace -e 'kprobe:tcp_add_backlog { @backlog_drops[comm] = count(); }'

### 8. skb->truesize 분포 (메모리 사용 패턴)
bpftrace -e 'kprobe:__alloc_skb { @truesize = hist(arg0); }
END { print(@truesize); }'

### 9. TX 큐 중단(queue stop) 빈도
bpftrace -e 'kprobe:netif_tx_stop_queue {
    $q = (struct netdev_queue *)arg0;
    @[str($q->dev->name)] = count();
}
interval:s:1 { print(@); clear(@); }'

### 10. page_pool recycle 히트율
bpftrace -e 'kprobe:page_pool_alloc_pages { @alloc = count(); }
kprobe:page_pool_return_skb_page { @recycle = count(); }
interval:s:5 {
    printf("alloc=%d recycle=%d hit=%d%%
",
           @alloc, @recycle,
           @alloc > 0 ? @recycle * 100 / @alloc : 0);
    clear(@alloc); clear(@recycle); }'

### 11. skb_ext 사용 패턴 (conntrack, ipsec 등)
bpftrace -e 'kprobe:skb_ext_add { @ext_type[arg1] = count(); }
END { print(@ext_type); }'

### 12. 수신 경로 함수 지연 분포
bpftrace -e 'kprobe:netif_receive_skb { @start[tid] = nsecs; }
kretprobe:netif_receive_skb {
    @rx_lat_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
END { print(@rx_lat_us); }'

perf probe를 활용한 skb 추적

# kfree_skb 발생 위치와 호출 스택 분석
sudo perf probe -a 'kfree_skb skb:x64 location:x64'
sudo perf record -e probe:kfree_skb -g --call-graph dwarf -- sleep 10
sudo perf report --call-graph --stdio | head -100

# 특정 함수의 인수 추출
sudo perf probe -a 'ip_drop_skb_reason skb reason'
sudo perf record -e probe:ip_drop_skb_reason -a -- sleep 5
sudo perf script | awk '{print $NF}' | sort | uniq -c | sort -rn

# TCP 수신 경로 성능 분석
sudo perf record -e cycles:pp -g -a --call-graph fp -- sleep 10
sudo perf report --sort=symbol --no-children | grep -E 'tcp|skb|napi'

# skb 처리 CPU별 분포
sudo perf stat -e skb:consume_skb,skb:kfree_skb -a --per-cpu -- sleep 5

# netif_receive_skb 지점 동적 프로브
sudo perf probe --add='netif_receive_skb skb->len:u32'
sudo perf record -e probe:netif_receive_skb -a -- sleep 5
sudo perf script | awk '{print $NF}' | \
  awk -F= '{sum+=$2; cnt++} END {print "avg_skb_len:", sum/cnt}'

ftrace function_graph로 skb 호출 체인 시각화

# TCP 수신 경로 호출 체인 추적
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 'tcp_v4_rcv' > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 1
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace | head -80
# 출력 예시:
#  0)               |  tcp_v4_rcv() {
#  0)   0.312 us    |    __inet_lookup_skb();
#  0)               |    tcp_rcv_established() {
#  0)   0.128 us    |      tcp_queue_rcv();
#  0)   }

# kfree_skb 호출 체인 추적
echo function > /sys/kernel/debug/tracing/current_tracer
echo kfree_skb > /sys/kernel/debug/tracing/set_ftrace_filter
echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 2
echo 0 > /sys/kernel/debug/tracing/tracing_on
grep -A 20 'kfree_skb' /sys/kernel/debug/tracing/trace | head -60

kfree_skb_reason 히스토그램 분석

/* Linux 5.17+: kfree_skb에 reason 파라미터 추가 */
/* include/net/dropreason-core.h */
enum skb_drop_reason {
    SKB_DROP_REASON_NOT_SPECIFIED   = 1,
    SKB_DROP_REASON_NO_SOCKET,       /* 소켓 없음 */
    SKB_DROP_REASON_PKT_TOO_SMALL,   /* 패킷 크기 부족 */
    SKB_DROP_REASON_TCP_CSUM,         /* TCP 체크섬 오류 */
    SKB_DROP_REASON_SOCKET_FILTER,    /* BPF 소켓 필터 드롭 */
    SKB_DROP_REASON_UDP_CSUM,         /* UDP 체크섬 오류 */
    SKB_DROP_REASON_NETFILTER_DROP,   /* netfilter 규칙 드롭 */
    SKB_DROP_REASON_IP_INHDR,         /* IP 헤더 오류 */
    SKB_DROP_REASON_IP_RPFILTER,      /* Reverse path filter */
    SKB_DROP_REASON_FLOW_LIMIT,       /* RPS flow limit 초과 */
    SKB_DROP_REASON_MAX,
};

# dropwatch로 실시간 드롭 위치 확인
# sudo dropwatch -l kas
# 출력: Drops at: ip_rcv_finish+0x4c ... 등 심볼 기반 위치

# /proc/net/stat/nf_conntrack 에서 conntrack 관련 드롭 확인
# cat /proc/net/stat/nf_conntrack | awk '{print "drop:", $3, "insert_failed:", $4}'
주요 drop reason 해석 가이드
reason원인확인 방법
NO_SOCKET소켓이 없음 (UDP 미수신 포트)ss -u로 포트 확인
TCP_CSUMTCP 체크섬 오류NIC 오프로드 설정 확인
NETFILTER_DROPiptables/nftables 규칙iptables -L -v -n
IP_RPFILTER역방향 경로 필터sysctl net.ipv4.conf.*.rp_filter
SOCKET_FILTERBPF 소켓 필터 드롭tcpdump 중단 후 재확인
FLOW_LIMITRPS 플로우 제한sysctl net.core.flow_limit_table_len
NOT_SPECIFIED구버전 코드 경로커널 업그레이드 또는 소스 추적

skb 수명 추적: alloc → free 지연 측정

### skb 수명 전체 추적
bpftrace -e 'kretprobe:__alloc_skb / retval != 0 / { @birth[retval] = nsecs; }
kprobe:kfree_skb / @birth[arg0] / {
    $us = (nsecs - @birth[arg0]) / 1000;
    @skb_lifetime_us = hist($us);
    if ($us > 1000000)
        printf("WARN: long-lived skb %p: %d us
", arg0, $us);
    delete(@birth[arg0]);
}
END { print(@skb_lifetime_us); }'

### 클론 vs 원본 skb 수명 비교
bpftrace -e 'kretprobe:skb_clone / retval != 0 / { @is_clone[retval] = 1; @birth[retval] = nsecs; }
kretprobe:__alloc_skb / retval != 0 / { @birth[retval] = nsecs; }
kprobe:kfree_skb / @birth[arg0] / {
    $us = (nsecs - @birth[arg0]) / 1000;
    if (@is_clone[arg0]) { @clone_life = hist($us); }
    else                 { @orig_life  = hist($us); }
    delete(@birth[arg0]); delete(@is_clone[arg0]);
}
END { print(@orig_life); print(@clone_life); }'

kmemleak을 활용한 skb 메모리 누수 탐지

# kmemleak 활성화 (CONFIG_DEBUG_KMEMLEAK=y 필요)
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak | grep -A 5 'sk_buff\|alloc_skb'

# 드라이버 모듈 특정 누수 확인
echo clear > /sys/kernel/debug/kmemleak
modprobe my_driver
iperf3 -c TARGET -t 60 -P 8  # 부하 유발
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak

/* 드라이버 코드 누수 패턴: 에러 경로에서 skb 해제 누락 */
static netdev_tx_t good_start_xmit(struct sk_buff *skb, ...)
{
    if (driver_hw_error()) {
        dev_kfree_skb_any(skb); /* 에러 → skb 해제 필수 */
        return NETDEV_TX_OK;
    }
    if (driver_tx_full())
        return NETDEV_TX_BUSY; /* BUSY: 상위가 재시도 → skb 반환 불필요 */
    driver_queue_skb(skb);
    return NETDEV_TX_OK;
}

crash tool을 활용한 사후 분석

# vmcore에서 sk_buff 체인 분석
# crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux vmcore

# crash 세션 내 sk_buff 구조체 출력
# crash> struct sk_buff 0xffff888012345000
# crash> list sk_buff.next -s sk_buff.len,data_len \
#      -H ((struct softnet_data *)&softnet_data)->input_queue_head

# TCP 소켓 수신 큐 skb 목록
# crash> list sk_buff.next -s sk_buff.len \
#      -H 0xffff888056789000+0x280  # sk_receive_queue 오프셋

# skb_shinfo 접근
# crash> struct skb_shared_info \
#      (skb->end - sizeof(struct skb_shared_info))

eBPF TC 프로그램으로 skb 검사

/* TC BPF 프로그램: skb 내용 검사 및 통계 수집 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/pkt_cls.h>

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key,   __u32); /* src IP */
    __type(value, __u64); /* 패킷 수 */
} pkt_count SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 1 << 20);
} events SEC(".maps");

SEC("tc")
int tc_skb_inspector(struct __sk_buff *skb)
{
    void *data     = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;
    struct iphdr  *iph = data + 14; /* ETH_HLEN */
    struct tcphdr *tcph;

    if ((void *)(iph + 1) > data_end || iph->protocol != IPPROTO_TCP)
        return TC_ACT_OK;

    tcph = (void *)iph + iph->ihl * 4;
    if ((void *)(tcph + 1) > data_end)
        return TC_ACT_OK;

    __u32 src = iph->saddr;
    __u64 *cnt = bpf_map_lookup_elem(&pkt_count, &src);
    if (cnt) __sync_fetch_and_add(cnt, 1);
    else { __u64 one = 1; bpf_map_update_elem(&pkt_count, &src, &one, BPF_ANY); }

    return TC_ACT_OK;
}

char LICENSE[] SEC("license") = "GPL";
# TC BPF 프로그램 로드 및 연결
clang -O2 -target bpf -c tc_skb_inspector.c -o tc_skb.o
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf direct-action obj tc_skb.o sec tc
bpftool map dump id 5  # pkt_count 맵 내용 확인
# 정리
tc filter del dev eth0 ingress
tc qdisc del dev eth0 clsact

/proc/net/softnet_stat 상세 해석

# /proc/net/softnet_stat: 각 행 = CPU N의 통계 (공백 구분 16진수)
cat /proc/net/softnet_stat

/* 열 의미 (커널 5.15+ 기준) */
/* 열 00: total      — 처리된 총 패킷 수 */
/* 열 01: dropped    — backlog 초과 드롭 */
/* 열 02: time_squeeze — budget/time 소진 횟수 */
/* 열 09: received_rps  — RPS로 넘어온 패킷 수 */
/* 열 10: flow_limit_count — flow limit 드롭 수 */

# 파싱 스크립트
awk 'NR==1 { printf "%-4s %12s %10s %10s
", "CPU","total","dropped","squeeze" }
     { printf "%-4s %12d %10d %10d
", NR-1,
       strtonum("0x"$1), strtonum("0x"$2), strtonum("0x"$3) }' \
  /proc/net/softnet_stat

# time_squeeze 높은 경우: netdev_budget 증가
sysctl -w net.core.netdev_budget=600
sysctl -w net.core.netdev_budget_usecs=8000

# dropped 높은 경우: backlog 큐 크기 증가
sysctl -w net.core.netdev_max_backlog=10000

nstat / ss 출력 해석

# nstat: 커널 MIB 카운터 변화량 모니터링
nstat -z | grep -E 'IpReasmFails|IpFragFails|TcpRetrans|UdpRcvbufErrors'
# IpReasmFails: IP 재조립 실패 (조각 타임아웃, 메모리 부족)
# IpFragFails:  DF 설정 패킷의 MTU 초과 전송 실패
# UdpRcvbufErrors: UDP 소켓 수신 버퍼 초과 드롭

# ss: 소켓 상태와 skb 버퍼 정보
ss -tnip  # Recv-Q/Send-Q 확인
# Recv-Q: 소켓 수신 버퍼에 대기 중인 바이트
# Send-Q: 전송 버퍼에서 ACK 미수신 바이트

ss -tnp | awk '$3 > 10000 {print "HIGH_RECV_Q:", $0}'

# 전체 시스템 TCP 수신 드롭 원인
nstat | grep -E 'TcpExtTCPBacklogDrop|TcpExtListenDrops|TcpExtTCPMinTTLDrop'
# TCPBacklogDrop: accept 큐 초과 드롭
# ListenDrops:    SYN 큐(반연결 큐) 포화 드롭

실전 디버깅 세션 예시

다음은 "특정 서버에서 패킷 드롭이 급증한다"는 신고를 받았을 때의 실전 디버깅 절차입니다.

### Step 1: 드롭 규모 파악
watch -n 1 'nstat | grep -E "IpInDiscards|UdpInErrors|TcpExtTCPBacklogDrop"'

### Step 2: softnet_stat으로 CPU별 드롭 확인
awk '{printf "CPU%d: dropped=%d squeeze=%d
", NR-1,
       strtonum("0x"$2), strtonum("0x"$3)}' /proc/net/softnet_stat
# → CPU 3의 dropped 값이 급증 → RPS 불균형 또는 backlog 포화 의심

### Step 3: kfree_skb reason으로 드롭 원인 식별
bpftrace -e 'tracepoint:skb:kfree_skb { @[args->reason, kstack(3)] = count(); }
interval:s:10 { print(@); exit(); }'
# → SKB_DROP_REASON_TCP_CSUM → NIC 오프로드 비활성화 또는 중간 장비 문제

### Step 4: NIC 오프로드 상태 확인
ethtool -k eth0 | grep -E 'checksum|scatter|gso|gro'
# tx-checksumming: off  ← 오프로드 꺼짐 → 체크섬 오류 발생 가능

### Step 5: 체크섬 오프로드 재활성화
ethtool -K eth0 tx-checksumming on rx-checksumming on

### Step 6: 효과 확인
nstat -az | grep TcpExtTCPAbortOnTimeout
# 카운터 증가 멈춤 확인 → 문제 해결

### 대안 시나리오: conntrack 테이블 포화
sysctl net.netfilter.nf_conntrack_count  # 현재 수
sysctl net.netfilter.nf_conntrack_max    # 최대값
# 90% 이상이면 위험 → 증가 필요
sysctl -w net.netfilter.nf_conntrack_max=1048576
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=300
디버깅 도구 요약 비교
도구용도오버헤드운영 환경 적합도
bpftrace동적 추적, 히스토그램, 스택낮음~중간적합 (복잡한 스크립트 주의)
perf probe정적/동적 프로브, 성능 분석낮음적합
ftrace함수 호출 체인 시각화중간단시간 사용 권장
dropwatch드롭 위치 심볼화낮음적합
nstatMIB 카운터 변화량거의 없음항상 적합
ss/netstat소켓 상태, 큐 크기거의 없음항상 적합
kmemleak메모리 누수 탐지높음개발/스테이징 전용
crashvmcore 사후 분석없음 (오프라인)장애 후 분석 전용

운영 환경 디버깅 원칙: bpftrace와 perf는 런타임 오버헤드가 낮아 운영 환경에서도 사용 가능하지만, 복잡한 kprobe 스크립트는 특정 커널 경로에서 성능 영향을 줄 수 있습니다. ftrace의 function_graph는 추적 대상 함수 수에 비례해 오버헤드가 증가하므로 운영 환경에서는 필요한 함수만 최소한으로 지정하는 것이 중요합니다. kmemleak은 메모리 할당마다 추적 데이터를 유지하므로 운영 환경에는 권장하지 않습니다.

실습 커널 모듈: sk_buff 조작

이 절에서는 sk_buff를 직접 다루는 세 가지 완전한 커널 모듈 예제를 제공합니다. 각 예제는 실제로 빌드·적재·테스트가 가능하며, 핵심 패턴에 대한 상세한 설명을 포함합니다.

예제 1: Netfilter 패킷 인스펙터 (pkt_inspector.c)

목적: NF_INET_PRE_ROUTING 훅에서 인입 패킷의 L2/L3/L4 헤더를 안전하게 파싱하고 로그로 출력합니다. pskb_may_pull()을 사용해 비선형(Non-linear) sk_buff에서도 안전하게 헤더를 읽는 패턴을 보여줍니다.

/* pkt_inspector.c — Netfilter 기반 패킷 인스펙터 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <linux/skbuff.h>
#include <linux/netdevice.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("skb-lab");
MODULE_DESCRIPTION("Netfilter packet inspector");

static unsigned int pkt_inspect_hook(void *priv,
    struct sk_buff *skb,
    const struct nf_hook_state *state)
{
    const struct iphdr  *iph;
    const struct tcphdr *tcph;
    const struct udphdr *udph;
    u32 saddr, daddr;

    /* 1단계: skb가 NULL이거나 IP 패킷이 아니면 통과 */
    if (!skb)
        return NF_ACCEPT;

    /* 2단계: pskb_may_pull — IPv4 헤더 최소 크기만큼 선형 영역 확보
     * 비선형 skb(jumbo frame, 분산 DMA 등)에서 헤더가 frag에 있을 수 있음.
     * pskb_may_pull 이후 skb->data, head 등이 재배치될 수 있으므로
     * 반드시 포인터를 재획득해야 합니다. */
    if (!pskb_may_pull(skb, sizeof(struct iphdr)))
        return NF_ACCEPT;

    /* 3단계: 포인터 재획득 (pskb_may_pull 후 필수) */
    iph = (struct iphdr *)skb_network_header(skb);

    /* IPv4만 처리, 헤더 길이 검증 */
    if (iph->version != 4)
        return NF_ACCEPT;

    saddr = ntohl(iph->saddr);
    daddr = ntohl(iph->daddr);

    /* 4단계: 전송 계층 헤더까지 pull
     * iph->ihl은 4바이트 단위이므로 *4 필요 */
    if (!pskb_may_pull(skb, iph->ihl * 4 + sizeof(struct tcphdr)))
        goto log_l3;

    /* pull 후 iph 포인터 재획득 */
    iph = (struct iphdr *)skb_network_header(skb);

    if (iph->protocol == IPPROTO_TCP) {
        tcph = (struct tcphdr *)skb_transport_header(skb);
        pr_info("[pkt_inspector] TCP %pI4:%u -> %pI4:%u flags=%c%c%c
",
            &iph->saddr, ntohs(tcph->source),
            &iph->daddr, ntohs(tcph->dest),
            tcph->syn ? 'S' : '-',
            tcph->ack ? 'A' : '-',
            tcph->fin ? 'F' : '-');
    } else if (iph->protocol == IPPROTO_UDP) {
        udph = (struct udphdr *)skb_transport_header(skb);
        pr_info("[pkt_inspector] UDP %pI4:%u -> %pI4:%u len=%u
",
            &iph->saddr, ntohs(udph->source),
            &iph->daddr, ntohs(udph->dest),
            ntohs(udph->len));
    }
    return NF_ACCEPT;

log_l3:
    pr_info("[pkt_inspector] IP %pI4 -> %pI4 proto=%u
",
        &iph->saddr, &iph->daddr, iph->protocol);
    return NF_ACCEPT;
}

static struct nf_hook_ops pkt_inspect_ops = {
    .hook      = pkt_inspect_hook,
    .pf        = NFPROTO_IPV4,
    .hooknum   = NF_INET_PRE_ROUTING,
    .priority  = NF_IP_PRI_FIRST,
};

static int __init pkt_inspector_init(void)
{
    return nf_register_net_hook(&init_net, &pkt_inspect_ops);
}

static void __exit pkt_inspector_exit(void)
{
    nf_unregister_net_hook(&init_net, &pkt_inspect_ops);
}

module_init(pkt_inspector_init);
module_exit(pkt_inspector_exit);
코드 설명: pskb_may_pull() 패턴의 핵심
  • pskb_may_pull(skb, len): skb의 선형 영역(linear area, skb->data ~ skb->tail)에 최소 len 바이트가 있음을 보장합니다. 데이터가 비선형(frag/frag_list)에 있으면 선형 영역으로 당겨오며, 이 과정에서 skb->head가 재할당(pskb_expand_head)될 수 있습니다.
  • 포인터 재획득 필수: pskb_may_pull() 호출 후에는 이전에 저장한 헤더 포인터(iph, tcph 등)가 무효화될 수 있으므로 반드시 skb_network_header(), skb_transport_header() 등으로 재획득해야 합니다. 이는 커널 내 모든 네트워크 코드에서 공통으로 지켜야 하는 규칙입니다.
  • skb_network_header() vs (struct iphdr *)skb->data: skb_network_header()skb->head + skb->network_header를 반환합니다. L2 헤더가 있는 경우 skb->data는 이더넷 헤더를 가리키므로, IP 헤더 접근에는 반드시 skb_network_header()를 사용해야 합니다.
  • NF_ACCEPT vs NF_DROP: 인스펙터는 패킷을 수정하지 않으므로 항상 NF_ACCEPT를 반환합니다. pskb_may_pull() 실패 시 패킷을 드롭하지 않고 통과시키는 이유는, 메모리 부족 등의 일시적 오류로 정상 패킷을 차단하지 않기 위함입니다.

Makefile (pkt_inspector)

obj-m += pkt_inspector.o

KDIR ?= /lib/modules/$(shell uname -r)/build

all:
	make -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean

사용법 및 예상 출력

# 빌드 및 적재
make
sudo insmod pkt_inspector.ko

# 패킷 발생 (다른 터미널에서)
curl http://1.1.1.1 &>/dev/null
ping -c 3 8.8.8.8

# 커널 로그 확인
sudo dmesg | grep pkt_inspector
예상 로그 예시
[pkt_inspector] TCP 192.168.1.100:52341 -> 1.1.1.1:80 flags=S--
[pkt_inspector] TCP 1.1.1.1:80 -> 192.168.1.100:52341 flags=-A-
[pkt_inspector] UDP 192.168.1.100:54321 -> 8.8.8.8:53 len=45
# 모듈 제거
sudo rmmod pkt_inspector

예제 2: TTL 수정 모듈 (ttl_modifier.c)

목적: NF_INET_LOCAL_OUT 훅에서 발신 패킷의 IP TTL을 수정합니다. 클론된(shared) sk_buff를 안전하게 수정하는 skb_ensure_writable() COW(Copy-On-Write) 패턴을 보여줍니다.

/* ttl_modifier.c — Netfilter TTL 수정 모듈 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
#include <linux/skbuff.h>

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("TTL modifier via Netfilter");

static int target_ttl = 64;
module_param(target_ttl, int, 0644);
MODULE_PARM_DESC(target_ttl, "Target TTL value (default: 64)");

static unsigned int ttl_mod_hook(void *priv,
    struct sk_buff *skb,
    const struct nf_hook_state *state)
{
    struct iphdr *iph;

    if (!skb)
        return NF_ACCEPT;

    /* 1단계: 선형 영역에 IP 헤더가 있음을 보장 */
    if (!pskb_may_pull(skb, sizeof(struct iphdr)))
        return NF_ACCEPT;

    iph = (struct iphdr *)skb_network_header(skb);
    if (iph->version != 4)
        return NF_ACCEPT;

    /* 2단계: 쓰기 가능 여부 확인 및 COW 수행
     * skb가 다른 곳에서 공유(clone/get)된 경우, 헤더 부분만 복사해
     * 독립적인 쓰기 가능 영역을 확보합니다.
     * 커널 5.x+ 권장 API: skb_ensure_writable()
     * 구 API: skb_make_writable() (레거시, 사용 자제) */
    if (skb_ensure_writable(skb, sizeof(struct iphdr)))
        return NF_DROP;  /* COW 실패 시 드롭 */

    /* 3단계: ensure_writable 후 포인터 재획득 */
    iph = (struct iphdr *)skb_network_header(skb);

    /* 4단계: TTL 수정 */
    iph->ttl = (u8)target_ttl;

    /* 5단계: IP 헤더 체크섬 재계산
     * TTL 변경은 IP 헤더를 수정하므로 체크섬을 갱신해야 합니다.
     * ip_send_check()는 iph->check를 0으로 초기화하고
     * ip_fast_csum()으로 재계산합니다. */
    ip_send_check(iph);

    return NF_ACCEPT;
}

static struct nf_hook_ops ttl_mod_ops = {
    .hook     = ttl_mod_hook,
    .pf       = NFPROTO_IPV4,
    .hooknum  = NF_INET_LOCAL_OUT,
    .priority = NF_IP_PRI_MANGLE,
};

static int __init ttl_modifier_init(void)
{
    pr_info("ttl_modifier: loaded, target_ttl=%d
", target_ttl);
    return nf_register_net_hook(&init_net, &ttl_mod_ops);
}

static void __exit ttl_modifier_exit(void)
{
    nf_unregister_net_hook(&init_net, &ttl_mod_ops);
    pr_info("ttl_modifier: unloaded
");
}

module_init(ttl_modifier_init);
module_exit(ttl_modifier_exit);
코드 설명: skb_ensure_writable() COW 패턴
  • 왜 COW가 필요한가? skb_clone()으로 생성된 sk_buff는 원본과 데이터 영역을 공유합니다(skb_shared() 또는 skb_cloned()). 공유 상태에서 헤더를 직접 수정하면 원본 sk_buff를 사용하는 다른 코드(예: TCP 재전송 큐)가 의도치 않게 수정된 데이터를 보게 됩니다.
  • skb_ensure_writable(skb, len): len 바이트 범위가 쓰기 가능(비공유, 선형)한지 확인하고, 필요하면 해당 부분만 복사(COW)합니다. 반환값이 0이 아니면 실패이므로 드롭합니다. 5.x 이전에는 skb_make_writable()을 사용했으나 현재는 deprecated 추세입니다.
  • ip_send_check(iph): 내부적으로 iph->check = 0; iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl);를 수행합니다. TTL뿐 아니라 IP 헤더의 어떤 필드를 수정해도 이 함수를 반드시 호출해야 합니다.
  • 포인터 재획득 패턴의 일반화: pskb_may_pull()skb_ensure_writable() 모두 내부에서 pskb_expand_head()를 호출할 수 있으며, 이 경우 skb->head가 바뀝니다. 따라서 두 함수 호출 후에는 항상 헤더 포인터를 재획득해야 합니다. 이를 잊으면 UAF(Use-After-Free) 버그로 이어집니다.

사용법 (ttl_modifier)

# 기본 TTL(64)로 적재
sudo insmod ttl_modifier.ko

# TTL 128로 적재
sudo insmod ttl_modifier.ko target_ttl=128

# 확인: 발신 패킷의 TTL 변경 여부
sudo tcpdump -i any -n "ip" -c 10

# 런타임 파라미터 변경 (module_param 0644 권한)
echo 32 | sudo tee /sys/module/ttl_modifier/parameters/target_ttl

예제 3: 커스텀 패킷 생성 모듈 (pkt_gen.c)

목적: alloc_skb()skb_reserve()skb_put()skb_push()dev_queue_xmit() 흐름을 완전히 구현하는 UDP 패킷 생성 모듈입니다. 커스텀 프로토콜 구현이나 패킷 생성기 개발 시 필요한 모든 단계를 포함합니다.

/* pkt_gen.c — 커스텀 UDP 패킷 생성 모듈 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/skbuff.h>
#include <linux/netdevice.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/etherdevice.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/timer.h>

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Custom packet generator using sk_buff APIs");

#define PAYLOAD_DATA  "Hello from kernel pkt_gen!"
#define PAYLOAD_LEN   26
#define DST_IP        htonl(0xC0A80101)  /* 192.168.1.1 */
#define SRC_IP        htonl(0xC0A801FE)  /* 192.168.1.254 */
#define DST_PORT      htons(9999)
#define SRC_PORT      htons(12345)

static char *ifname = "eth0";
module_param(ifname, charp, 0444);
MODULE_PARM_DESC(ifname, "Output network interface (default: eth0)");

static struct timer_list gen_timer;

static int send_custom_packet(void)
{
    struct net_device *dev;
    struct sk_buff    *skb;
    struct ethhdr     *eth;
    struct iphdr      *iph;
    struct udphdr     *udph;
    unsigned char     *payload;
    int                total_len;
    int                ret;

    /* 1단계: 출력 인터페이스 조회 */
    dev = dev_get_by_name(&init_net, ifname);
    if (!dev) {
        pr_err("pkt_gen: interface %s not found
", ifname);
        return -ENODEV;
    }

    /* 2단계: 전체 패킷 크기 계산
     * LL_RESERVED_SPACE = dev->hard_header_len + dev->needed_headroom
     * 이더넷: 14바이트 헤더 + 드라이버 headroom */
    total_len = ETH_HLEN + sizeof(struct iphdr) +
                sizeof(struct udphdr) + PAYLOAD_LEN;

    /* 3단계: sk_buff 할당
     * alloc_skb(size, gfp): headroom+data+tailroom 포함 버퍼 할당
     * GFP_ATOMIC: softirq/tasklet 컨텍스트에서 sleep 불가 상황
     * LL_RESERVED_SPACE: L2 헤더를 위한 추가 headroom 공간 예약분 */
    skb = alloc_skb(total_len + LL_RESERVED_SPACE(dev), GFP_ATOMIC);
    if (!skb) {
        dev_put(dev);
        return -ENOMEM;
    }

    /* 4단계: skb_reserve — headroom 확보
     * data 포인터를 앞으로 당겨 headroom을 확보합니다.
     * 이후 skb_push()로 헤더를 앞에 추가할 공간을 만듭니다.
     * 이 시점: head == data == tail, headroom=LL_RESERVED_SPACE */
    skb_reserve(skb, LL_RESERVED_SPACE(dev));

    /* 5단계: skb_put — 페이로드 공간 확보 (tail 확장)
     * tail 포인터를 뒤로 밀어 데이터 공간을 만들고 포인터 반환
     * 패킷을 뒤에서 앞으로(payload → UDP → IP → ETH 순) 구성 */
    payload = (unsigned char *)skb_put(skb, PAYLOAD_LEN);
    memcpy(payload, PAYLOAD_DATA, PAYLOAD_LEN);

    /* 6단계: skb_push — UDP 헤더 추가 (data 포인터를 앞으로)
     * 반환값: 새로 추가된 헤더의 시작 포인터 */
    udph = (struct udphdr *)skb_push(skb, sizeof(struct udphdr));
    udph->source = SRC_PORT;
    udph->dest   = DST_PORT;
    udph->len    = htons(sizeof(struct udphdr) + PAYLOAD_LEN);
    udph->check  = 0;  /* UDP 체크섬 선택 사항, 0=미사용 */

    /* 7단계: skb_push — IP 헤더 추가 */
    iph = (struct iphdr *)skb_push(skb, sizeof(struct iphdr));
    iph->version  = 4;
    iph->ihl      = sizeof(struct iphdr) / 4;
    iph->tos      = 0;
    iph->tot_len  = htons(sizeof(struct iphdr) +
                          sizeof(struct udphdr) + PAYLOAD_LEN);
    iph->id       = htons(0x1234);
    iph->frag_off = 0;
    iph->ttl      = 64;
    iph->protocol = IPPROTO_UDP;
    iph->check    = 0;
    iph->saddr    = SRC_IP;
    iph->daddr    = DST_IP;
    ip_send_check(iph);

    /* 8단계: skb_push — 이더넷 헤더 추가 */
    eth = (struct ethhdr *)skb_push(skb, ETH_HLEN);
    memset(eth->h_dest,   0xff, ETH_ALEN);  /* 브로드캐스트 */
    memcpy(eth->h_source, dev->dev_addr, ETH_ALEN);
    eth->h_proto = htons(ETH_P_IP);

    /* 9단계: 헤더 오프셋 설정
     * skb_set_*_header는 head 기준 오프셋을 저장합니다. */
    skb_set_mac_header(skb, 0);
    skb_set_network_header(skb, ETH_HLEN);
    skb_set_transport_header(skb, ETH_HLEN + sizeof(struct iphdr));

    /* 10단계: skb 메타데이터 설정 */
    skb->dev      = dev;
    skb->protocol = htons(ETH_P_IP);
    skb->priority = 0;

    /* 11단계: dev_queue_xmit — 전송 큐에 제출
     * 이 시점부터 skb의 소유권이 네트워크 스택으로 이전됩니다.
     * dev_queue_xmit() 호출 후에는 절대로 skb에 접근하면 안 됩니다.
     * (성공·실패 모두 마찬가지) */
    ret = dev_queue_xmit(skb);
    if (ret != NET_XMIT_SUCCESS)
        pr_warn("pkt_gen: xmit returned %d
", ret);

    /* dev 참조 카운트 해제 */
    dev_put(dev);
    return 0;
}

static void gen_timer_cb(struct timer_list *t)
{
    send_custom_packet();
    mod_timer(&gen_timer, jiffies + msecs_to_jiffies(1000));
}

static int __init pkt_gen_init(void)
{
    pr_info("pkt_gen: loaded, iface=%s
", ifname);
    timer_setup(&gen_timer, gen_timer_cb, 0);
    mod_timer(&gen_timer, jiffies + msecs_to_jiffies(1000));
    return 0;
}

static void __exit pkt_gen_exit(void)
{
    del_timer_sync(&gen_timer);
    pr_info("pkt_gen: unloaded
");
}

module_init(pkt_gen_init);
module_exit(pkt_gen_exit);
코드 설명: alloc_skb / skb_reserve / skb_put / skb_push 패턴
  • 패킷 구성 순서 (역순): 헤더를 앞에 추가하는 방식 특성상, 실제 데이터를 먼저 skb_put()으로 뒤에 채운 뒤 skb_push()로 헤더를 차례로 앞에 붙입니다. 즉 페이로드 → L4 → L3 → L2 순서입니다.
  • skb_reserve(skb, len): datatail 모두 len만큼 전진시켜 headroom을 확보합니다. 아직 데이터가 없는 초기 상태에서만 호출해야 합니다. LL_RESERVED_SPACE(dev)는 드라이버가 요구하는 headroom + L2 헤더 크기의 합계입니다.
  • skb_put(skb, len): taillen만큼 뒤로 밀고 이전 tail을 반환합니다. 선형 데이터 영역의 끝에 공간을 추가합니다. len > skb_tailroom(skb)이면 BUG()가 발생하므로, alloc_skb 시 충분한 크기를 지정해야 합니다.
  • skb_push(skb, len): datalen만큼 앞으로 당기고 새 data를 반환합니다. headroom이 부족하면 BUG()가 발생합니다. skb_reserve로 미리 확보한 headroom에서 소비됩니다.
  • dev_queue_xmit() 후 skb 접근 금지: dev_queue_xmit()는 skb 소유권을 커널 네트워크 스택에 넘깁니다. 성공·실패와 무관하게 이 함수 호출 후 skb 포인터로 접근하면 UAF 버그가 발생합니다. dev_put() 등 별도로 처리해야 할 것들은 skb와 분리하여 관리하십시오.
  • skb_set_*_header() 호출 이유: L2/L3/L4 헤더의 위치(head 기준 오프셋)를 skb 구조체에 기록합니다. 이 값이 없으면 이후 단계에서 skb_network_header() 등이 잘못된 위치를 반환합니다.

사용법 (pkt_gen)

# eth0으로 브로드캐스트 UDP 패킷 1초 간격 전송
sudo insmod pkt_gen.ko

# 다른 인터페이스 사용
sudo insmod pkt_gen.ko ifname=ens3

# 수신 측에서 확인
sudo tcpdump -i any udp port 9999 -A

# 모듈 언로드 (타이머 자동 정지)
sudo rmmod pkt_gen
ℹ️

실습 주의사항: 위 예제 모듈은 학습 목적으로 단순화되었습니다. 실제 드라이버나 넷필터 모듈에서는 네임스페이스 처리, 멀티큐 NIC 지원, NAPI 버짓 관리, TSO/GSO 오프로드 여부 확인 등을 추가로 고려해야 합니다. 또한 커널 버전별 API 변화(특히 nf_register_net_hook vs nf_register_hooks)에 주의하십시오.

sk_buff 전체 API 퀵 레퍼런스

아래 표는 sk_buff 관련 핵심 함수를 카테고리별로 정리한 퀵 레퍼런스입니다. 각 함수의 시그니처(Signature), 반환값, 주요 용도를 한눈에 파악할 수 있도록 구성하였습니다.

1. 할당 및 해제

함수시그니처 (핵심 인자)반환값설명
alloc_skb(size, gfp)struct sk_buff *기본 sk_buff 할당. head~end 연속 버퍼.
alloc_skb_with_frags(header_len, data_len, max_page_order, errcode, gfp)struct sk_buff *헤더+페이지 분리 할당. 대용량 패킷.
netdev_alloc_skb(dev, length)struct sk_buff *NIC 수신용 할당. NET_SKB_PAD headroom 자동.
napi_alloc_skb(napi, length)struct sk_buff *NAPI 컨텍스트 최적화 할당. 현재 CPU 캐시 활용.
build_skb(data, frag_size)struct sk_buff *기존 버퍼로 skb 구성. XDP/zero-copy 수신.
build_skb_around(skb, data, frag_size)struct sk_buff *기존 skb 구조체를 재사용해 data 연결.
__alloc_skb(size, gfp, flags, node)struct sk_buff *내부 저수준 할당. fclone/node 제어.
kfree_skb(skb)void참조 카운트 감소. 0이 되면 실제 해제.
kfree_skb_reason(skb, reason)void드롭 이유 추적 포함 해제 (6.0+).
consume_skb(skb)void정상 소비(드롭 아님) 시 사용. tracepoint 구분.
dev_kfree_skb(skb)void드라이버 TX 완료 시 해제. consume_skb 래퍼.
dev_kfree_skb_irq(skb)voidIRQ 컨텍스트에서 TX 완료 해제. defer 큐 사용.
dev_kfree_skb_any(skb)voidIRQ/비IRQ 모두 안전한 TX 해제.
__kfree_skb(skb)void참조 카운트 확인 없이 즉시 해제 (내부용).
skb_free_datagram(sk, skb)voidUDP/raw 소켓 수신 큐에서 데이터그램 해제.

2. 데이터 조작

함수시그니처 (핵심 인자)반환값설명
skb_reserve(skb, len)voidheadroom 확보. 빈 skb에서만 사용.
skb_put(skb, len)void *tail 확장. 선형 데이터 끝에 공간 추가.
skb_put_zero(skb, len)void *skb_put + memset(0).
skb_put_data(skb, data, len)void *skb_put + memcpy.
skb_push(skb, len)void *data 앞으로 이동. 헤더 추가.
skb_pull(skb, len)void *data 뒤로 이동. 헤더 제거. NULL=실패.
skb_pull_inline(skb, len)void *skb_pull 인라인 버전. 빠른 경로 최적화.
__skb_pull(skb, len)void *skb_pull 무조건 버전 (길이 검사 없음).
skb_trim(skb, len)void패킷 끝부터 자르기. 선형 전용.
__skb_trim(skb, len)voidskb_trim 저수준 버전.
skb_pad(skb, pad)int최소 길이 패딩. 패킷을 복사할 수 있음.
skb_padto(skb, len)int패킷을 len 바이트로 패딩.
pskb_expand_head(skb, nhead, ntail, gfp)inthead 영역 재할당. headroom/tailroom 확장.
skb_ensure_writable(skb, write_len)intCOW 수행, 쓰기 가능 보장 (5.x+).
skb_linearize(skb)intfrag_list/frags를 선형 영역으로 통합.
__pskb_pull_tail(skb, delta)unsigned char *frag에서 선형 영역으로 delta 바이트 당겨오기.
pskb_may_pull(skb, len)bool선형 영역 확보 보장. 비선형 skb 안전 접근.
skb_cow_data(skb, tailbits, trailer)intIPsec 등에서 데이터 COW + tailroom 확보.
skb_prepare_seq_read(skb, from, to, st)void비선형 skb 순차 읽기 상태 초기화.
skb_seq_read(consumed, data, datalen, st)unsigned int비선형 skb 순차 읽기.

3. 헤더 포인터 조작

함수/매크로반환값설명
skb_mac_header(skb)unsigned char *L2 헤더 포인터 (head + mac_header).
skb_network_header(skb)unsigned char *L3 헤더 포인터 (head + network_header).
skb_transport_header(skb)unsigned char *L4 헤더 포인터 (head + transport_header).
skb_inner_mac_header(skb)unsigned char *터널 내부 L2 헤더 포인터.
skb_inner_network_header(skb)unsigned char *터널 내부 L3 헤더 포인터.
skb_inner_transport_header(skb)unsigned char *터널 내부 L4 헤더 포인터.
skb_set_mac_header(skb, offset)voidmac_header = data + offset.
skb_set_network_header(skb, offset)voidnetwork_header = data + offset.
skb_set_transport_header(skb, offset)voidtransport_header = data + offset.
skb_reset_mac_header(skb)voidmac_header = data (현재 위치로 리셋).
skb_reset_network_header(skb)voidnetwork_header = data.
skb_reset_transport_header(skb)voidtransport_header = data.
skb_mac_header_len(skb)intnetwork_header - mac_header.
skb_network_header_len(skb)inttransport_header - network_header.
skb_network_offset(skb)intnetwork_header - (data - head).
skb_transport_offset(skb)inttransport_header - (data - head).
skb_header_pointer(skb, offset, len, buf)void *비선형 안전 헤더 접근. frag 경계 처리.

4. 비선형 데이터 (frags/frag_list)

함수/매크로설명
skb_shinfo(skb)skb_shared_info 포인터 반환 (end 위치).
skb_shinfo(skb)->nr_fragsfrags[] 배열의 유효 항목 수.
skb_shinfo(skb)->frags[i]i번째 페이지 frag (skb_frag_t).
skb_frag_page(&frag)frag의 struct page * 반환.
skb_frag_address(&frag)frag의 가상 주소 반환 (kmap 필요 시 불가).
skb_frag_size(&frag)frag의 데이터 크기 반환.
skb_frag_off(&frag)frag 내 데이터 시작 오프셋.
skb_add_rx_frag(skb, i, page, off, size, truesize)수신 frag 추가. nr_frags++, len/truesize 갱신.
skb_fill_page_desc(skb, i, page, off, size)frags[i]에 페이지 정보 채우기. truesize 미갱신.
skb_coalesce_rx_frag마지막 frag가 같은 페이지면 병합, 아니면 추가.
skb_copy_bits(skb, offset, to, len)비선형 skb에서 연속 버퍼로 복사.
skb_store_bits(skb, offset, from, len)연속 버퍼에서 비선형 skb로 쓰기.
skb_copy_datagram_iter(skb, offset, iter, len)iov_iter로 데이터 복사 (user-space 포함).
skb_walk_frags(skb, iter_skb)frag_list 순회 매크로.
skb_has_frag_list(skb)skb_shinfo(skb)->frag_list != NULL 여부.
skb_frag_ref(skb, f)frags[f] 페이지 참조 카운트 증가.
skb_frag_unref(skb, f)frags[f] 페이지 참조 카운트 감소.

5. Clone / Copy

함수시그니처설명
skb_clone(skb, gfp)헤더 복사, 데이터 공유. 빠름. 읽기 전용 데이터 공유 시.
skb_copy(skb, gfp)헤더+데이터 모두 복사. 독립적 수정 필요 시.
pskb_copy(skb, headroom, gfp)선형 영역만 복사, frag 공유. 선택적 headroom 지정.
pskb_copy_for_clone(skb, headroom, gfp)pskb_copy + 클론 표시.
skb_copy_expand(skb, newheadroom, newtailroom, gfp)headroom/tailroom 조정하며 전체 복사.
skb_get(skb)참조 카운트 증가. 다중 큐 보관 시.
skb_clone_sk(skb)소켓 연결 유지 클론.
skb_orphan(skb)소켓과 연결 끊기. destructor 호출.
skb_unshare(skb, gfp)공유 상태면 복사해 독립 skb 반환.
skb_cow(skb, headroom)공유 또는 headroom 부족 시 복사.

6. 큐 관리

함수설명
skb_queue_head_init(list)sk_buff_head 초기화 (next=prev=self, lock 초기화).
skb_queue_head(list, skb)큐 앞에 skb 삽입 (락 획득).
skb_queue_tail(list, skb)큐 끝에 skb 삽입 (락 획득).
skb_dequeue(list)큐 앞에서 skb 제거·반환 (락 획득).
skb_dequeue_tail(list)큐 끝에서 skb 제거·반환.
__skb_queue_head(list, skb)락 없이 큐 앞 삽입 (이미 락 보유 시).
__skb_queue_tail(list, skb)락 없이 큐 끝 삽입.
__skb_dequeue(list)락 없이 큐 앞 제거.
skb_queue_len(list)큐 길이 반환 (qlen 필드).
skb_queue_empty(list)큐가 비어있으면 true.
skb_queue_empty_lockless(list)락 없이 큐 비어있음 확인 (READ_ONCE 사용).
skb_peek(list)첫 번째 skb 반환 (제거 안 함).
skb_peek_tail(list)마지막 skb 반환 (제거 안 함).
skb_queue_purge(list)큐의 모든 skb를 kfree_skb로 제거.
skb_queue_splice(list, head)list를 head 앞에 이어 붙임.
skb_queue_splice_tail(list, head)list를 head 끝에 이어 붙임.
skb_queue_walk(list, skb)큐 순회 매크로 (제거 시 _safe 버전 사용).
skb_queue_walk_safe(list, skb, tmp)순회 중 제거 안전 버전.

7. 체크섬

함수설명
ip_send_check(iph)IPv4 헤더 체크섬 재계산 (iph->check 갱신).
skb_checksum(skb, offset, len, csum)비선형 skb의 체크섬 계산.
skb_checksum_none_assert(skb)ip_summed=CHECKSUM_NONE 단언 (디버그).
skb_csum_unnecessary(skb)하드웨어가 체크섬 검증 완료 여부.
skb_postpull_rcsum(skb, start, len)skb_pull 후 csum 갱신.
skb_pull_rcsum(skb, len)skb_pull + rcsum 자동 갱신.
csum_block_add(csum, csum2, offset)부분 체크섬 누적.
inet_proto_csum_replace4(sum, skb, from, to, pseudohdr)L4 체크섬에서 4바이트 필드 교체 갱신.
inet_proto_csum_replace2(sum, skb, from, to, pseudohdr)L4 체크섬에서 2바이트 필드 교체 갱신.
skb_checksum_setup(skb, recalculate)가상화 환경에서 체크섬 설정.

8. GSO / GRO

함수/필드설명
skb_shinfo(skb)->gso_sizeGSO 세그먼트 크기 (MSS).
skb_shinfo(skb)->gso_segsGSO 세그먼트 수.
skb_shinfo(skb)->gso_typeGSO 유형 (SKB_GSO_TCPV4 등).
skb_gso_segment(skb, features)GSO skb를 세그먼트 리스트로 분할.
skb_gso_validate_network_len(skb, mtu)GSO skb가 MTU를 초과하지 않는지 확인.
skb_gro_header(skb, hlen, off)GRO 헤더 접근 헬퍼.
skb_gro_offset(skb)GRO 처리 시작 오프셋.
skb_gro_len(skb)GRO 처리할 데이터 길이.
napi_gro_receive(napi, skb)GRO로 패킷 수신 처리.
napi_gro_flush(napi, flush_old)GRO로 누적된 패킷 즉시 전달.
dev_gro_receive(napi, skb)GRO 프로토콜 훅 호출 (프로토콜별 병합).

9. 유틸리티 / 기타

함수/매크로설명
skb_is_nonlinear(skb)nr_frags > 0 또는 frag_list 있으면 true.
skb_headlen(skb)선형 데이터 길이 (len - data_len).
skb_tailroom(skb)tail ~ end 여유 공간.
skb_headroom(skb)head ~ data 여유 공간.
skb_shared(skb)users > 1 이면 true (참조 공유).
skb_cloned(skb)클론된 sk_buff 여부 (dataref & SKB_DATAREF_MASK > 1).
skb_pfmemalloc(skb)PFMEMALLOC(메모리 압박) 플래그 확인.
skb_get_hash(skb)L3/L4 기반 플로우 해시 (캐시됨).
skb_set_hash(skb, hash, type)하드웨어 제공 해시 값 설정.
skb_record_rx_queue(skb, rx_queue)수신 큐 번호 기록 (다중 큐 NIC).
skb_get_rx_queue(skb)기록된 수신 큐 번호 반환.
skb_mark_not_on_list(skb)next 포인터 NULL 설정 (큐에서 제거 후).
skb_list_del_init(skb)list_head 기반 skb 리스트에서 제거 및 초기화.
skb_metadata_len(skb)메타데이터 영역 크기 (XDP metadata 등).
skb_ext_add(skb, id)skb extension 추가 (5.1+).
skb_ext_find(skb, id)skb extension 조회.
skb_dump(pr_level, skb, full_pkt)skb 전체 구조 덤프 출력 (디버그).

커널 버전별 sk_buff 변경 이력 확장

sk_buff는 Linux 커널 네트워크 스택의 핵심 자료구조로서 30년 이상에 걸쳐 지속적으로 발전해 왔습니다. 이 절에서는 각 커널 시리즈에서 sk_buff 구조와 API에 영향을 준 주요 변경사항을 연대기 순으로 정리합니다.

2.6.x 시대 (2003–2011): 구조 간소화와 오프로드 기반 확립

Linux 2.6 시리즈는 sk_buff의 내부 구조를 대대적으로 정비한 시기입니다. skb 크기 최소화, 헤더 분리, 오프로드 API 명확화가 이 시기의 핵심 주제였습니다.

버전변경 내용영향
2.6.9skb_shared_info에 GSO 필드 추가 (gso_size, gso_segs)소프트웨어 GSO 기반 확립
2.6.14destructor_list 제거, 소켓 레벨 destructor로 통합sk_buff 크기 감소, 메모리 효율 향상
2.6.18ip_summed: CHECKSUM_HW → CHECKSUM_PARTIAL/CHECKSUM_COMPLETE 분리체크섬 오프로드 API 명확화, 드라이버 인터페이스 안정화
2.6.19skb_shared_info에 tx_flags 추가 (TX 타임스탬핑 기반)하드웨어 TX 타임스탬프 지원 시작
2.6.22skb_shared_info를 skb 말단(end)에 인접 배치GSO/TSO 처리 캐시 지역성 향상
2.6.24mac/network/transport_header: 절대 포인터 → 16비트 오프셋sk_buff 구조체 축소, pskb_expand_head 후 포인터 갱신 불필요
2.6.24net_device 분리: 인터페이스별 통계를 percpu로 이동skb->dev 접근 패턴 안정화
2.6.26skb_clone_writable() 도입Netfilter COW 패턴 안정화
2.6.29GRO(Generic Receive Offload) 도입 (Ben Hutchings)소프트웨어 LRO 대체, 프로토콜 중립적 병합
2.6.31skb_dst 태그드 포인터(tagged pointer) 최적화fast path에서 참조 카운트 연산 절감
2.6.32RPS(Receive Packet Steering): skb->rxhash 도입멀티코어 수신 부하 분산 기반
2.6.36skb_frag_t: page + offset + size 구조 확정DMA frag API 안정화
2.6.37skb_tx_hash() → skb_get_tx_queue() 분리멀티큐 TX 선택 로직 명확화
2.6.39sk_buff에 vlan_tci 필드 추가 (하드웨어 VLAN 오프로드)VLAN 태그 처리 일원화

3.x 시대 (2011–2015): XPS/RFS와 네임스페이스 지원

3.x 시리즈에서는 멀티큐 NIC의 보편화에 맞춰 TX 스케줄링 및 수신 흐름 제어 기능이 sk_buff에 반영되었으며, 네트워크 네임스페이스와의 통합이 강화되었습니다.

버전변경 내용영향
3.0skb_frag_t: struct skb_frag_struct → 정식 typedef, API 함수화frag 접근 코드 일관성 향상
3.1sk_buff에 rxhash를 hash로 통합, l4_rxhash 비트 추가L4 해시 여부 구분 가능
3.3skb_shared_info에 tx_flags 확장 (SKBTX_* 비트플래그)TX 타임스탬핑 옵션 세분화
3.6skb_shared_info에 hwtstamps(hw_timestamps) 필드 추가하드웨어 타임스탬프 직접 저장
3.7BQL(Byte Queue Limits) 통합: netdev_tx_sent_queue()가 skb->len 기반으로 동작TX 큐 지연 최소화, 버퍼 팽창(bufferbloat) 개선
3.10skb->rxhash 필드 제거, skb->hash로 완전 통합해시 필드 단일화, RPS/RFS 인터페이스 정리
3.11TCP Fast Open: skb에 TFO 쿠키 처리 경로 추가연결 설정 지연 감소
3.14skb_shared_info: dataref를 atomic_t → refcount_t 전환 시작참조 카운트 오버플로 감지 향상
3.18eBPF 소켓 필터: sk_buff를 BPF 프로그램에 읽기 전용 노출BPF 기반 패킷 필터링 확장성 확보
3.19XDP 프로토타입: netmap-like raw 버퍼 접근 논의 시작4.x XDP 도입의 전초

4.x 시대 (2015–2019): XDP, 터널링, skb Extension 기반

4.x 시리즈는 XDP의 도입, 터널 프로토콜 지원 강화, 그리고 sk_buff 확장 메커니즘의 기반이 마련된 시기입니다.

버전변경 내용영향
4.1inner_*_header 오프셋 추가 (터널 내부 헤더 지원)VXLAN/GRE/GENEVE 등 터널 처리 표준화
4.2sock_efastpath: 소켓 직접 전송 경로 최적화UDP sendmsg 지연 감소
4.4skb_vlan_push/pop 표준화: __vlan_hwaccel_put_tag 대체VLAN 조작 API 정리
4.8XDP(eXpress Data Path) 드라이버 레벨 훅 도입 (mlx4/ixgbe)sk_buff 없이 패킷 처리 가능, 극저지연 경로
4.10skb_shared_info: gso_type에 SKB_GSO_UDP_TUNNEL_CSUM 추가터널 UDP 오프로드 개선
4.13skb->tstamp: ktime_t에서 u64 nanoseconds로 타입 변경타임스탬프 처리 통일, SO_TXTIME 기반
4.15skb_ext 시스템 초안 (Eric Dumazet): 선택적 확장 필드 관리sk_buff 크기 증가 없이 기능 추가 가능
4.17skb->slow_gro 비트 추가 (GRO 슬로우 경로 표시)GRO 프로토콜 훅 선택적 건너뛰기
4.18UDP GSO 지원: SKB_GSO_UDP_L4 타입 추가UDP 대용량 전송 오프로드 (sendmsg + GSO)
4.20skb_shared_info에 xdp_frags 필드 추가 (멀티버퍼 XDP 기반)XDP 멀티버퍼 지원 준비

5.x 시대 (2019–2022): skb_ext 정식화, page_pool, io_uring 통합

5.x 시리즈는 skb_ext 시스템의 정식 도입, page_pool을 통한 NIC 버퍼 관리 개선, 그리고 io_uring 및 zero-copy 전송 지원의 성숙 단계입니다.

버전변경 내용영향
5.1skb_ext 시스템 정식 도입: skb_ext_add/find/del, SKB_EXT_* ID보안(SEC_PATH), 서비스 품질 등 옵션 필드 동적 관리
5.1page_pool API 안정화: pp_recycle 비트가 skb에 추가됨NIC별 페이지 재활용 표준화, 드라이버 메모리 관리 개선
5.2skb_ensure_writable() 도입 (skb_make_writable 대체)COW API 명확화, 성능 개선
5.4MSG_ZEROCOPY: sock_zerocopy_put() 경로 성숙, uarg 레퍼런스 정리zero-copy 전송 안정화
5.7XDP 멀티버퍼 프레임워크: skb_shared_info->xdp_frags 활용 본격화점보 프레임 XDP 처리 가능
5.9skb->active_extensions 비트마스크로 skb_ext 관리 최적화확장 필드 조회 오버헤드 감소
5.10kfree_skb_reason() 도입: SKB_DROP_REASON_* 열거형패킷 드롭 원인 추적, 디버깅 개선
5.11skb_mark_for_recycle(): page_pool 재활용 마킹 단순화드라이버 코드 간소화
5.14XDP 멀티버퍼 플래그: skb_shared_info에 nr_frags 기반 멀티버퍼 공식 지원XDP_FLAGS_HW_MODE에서 점보 패킷 처리
5.18skb_frag_t에 netmem 필드 추가 예비 작업 (devmem TCP 전구체)비메모리(장치 메모리) frag 표현 기반
5.19GRO list 기반 처리 개선: napi_gro_receive에서 list_head 사용GRO 경로 잠금 경합 감소

6.x 시대 (2022–현재): netmem, devmem TCP, page_pool 완성

6.x 시리즈는 sk_buff 아키텍처의 근본적인 변화가 진행 중인 시기입니다. netmem 추상화와 devmem TCP로 대표되는 장치 메모리 직접 통합이 핵심 주제입니다.

버전변경 내용영향
6.0kfree_skb_reason() 전면 채택: net/core 전체에 drop reason 추가패킷 드롭 원인 가시성 대폭 향상
6.1page_pool: 통계 인프라 추가 (page_pool_get_stats)NIC 버퍼 사용량 모니터링 가능
6.2skb_frag_t: union {struct page *, netmem_ref} 구조로 변환 시작non-page 메모리(DMABUF 등) frag 표현 준비
6.4af_packet: skb_segment() 경로 개선, GSO_BY_FRAGS 처리 최적화AF_PACKET 대용량 전송 성능 향상
6.6netmem 추상화 정식 도입: netmem_ref 타입, net_iov 구조체장치 메모리(DMABUF/P2PDMA)를 skb frag에 직접 연결 가능
6.6page_pool: netmem_ref 기반 할당 지원page_pool이 DMABUF 백엔드 지원
6.7skb_frag_t: skb_frag_netmem() API 추가netmem frag 접근 표준화
6.8MSG_ZEROCOPY: io_uring 통합 개선io_uring 기반 zero-copy 전송 지연 감소
6.9devmem TCP 초기 구현: TCP 수신 경로에서 net_iov frag 직접 user-space 매핑NIC DMA 버퍼를 애플리케이션에 직접 노출 (copy 없는 수신)
6.10devmem TCP: sendmsg 경로에서 DMABUF 직접 전송 지원완전한 zero-copy TX/RX 경로 구현
6.12skb_frag_t API 정리: 하위 호환 유지하며 netmem_ref 기반으로 전환 완료드라이버 maigration 경로 확립

버전별 변화 타임라인

2.6.x 3.x 4.x 5.x 6.x 2.6.18 CHECKSUM 분리 2.6.24 오프셋 전환 2.6.29 GRO 도입 3.7 BQL 통합 3.18 eBPF 소켓 4.8 XDP 도입 4.18 UDP GSO 5.1 skb_ext + page_pool 5.2 skb_ensure_writable 5.10 drop_reason 6.0 drop_reason 전면화 6.6 netmem 도입 6.9 devmem TCP 6.12 netmem_ref 완성

버전 간 API 마이그레이션 가이드

커널 버전 업그레이드 시 sk_buff 관련 코드에서 자주 필요한 마이그레이션 사항을 정리합니다.

구 API (제거/deprecated)신 API전환 시점비고
skb_make_writable()skb_ensure_writable()5.2+Netfilter 수정 패턴
CHECKSUM_HWCHECKSUM_PARTIAL / CHECKSUM_COMPLETE2.6.18+체크섬 오프로드 타입
skb->rxhashskb->hash3.10+L4 해시 통합
nf_register_hooks()nf_register_net_hook()4.13+네임스페이스 지원
kfree_skb() (드롭 시)kfree_skb_reason()5.10+드롭 이유 추적 권장
skb_frag_page() 직접 접근skb_frag_netmem() + netmem API6.7+DMABUF 백엔드 지원 시
skb->tstamp (ktime_t)skb->tstamp (u64 ns)4.13+SO_TXTIME/ETF 스케줄러
netif_rx() (대부분)napi_gro_receive()2.6.29+GRO 이점 활용

sk_buff와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.