GSO/GRO와 네트워크 오프로드

Linux 커널 네트워크 오프로드 메커니즘: 체크섬 오프로드 플래그, GSO(Generic Segmentation Offload)/TSO/UFO, GRO(Generic Receive Offload) 병합 알고리즘, VXLAN/GRE 터널 GSO, HW-GRO, 성능 튜닝 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.

전제 조건: sk_buff 자료구조네트워킹 심화를 먼저 읽으세요. GSO/GRO는 sk_buff 내부 구조와 NIC 드라이버 인터페이스 이해가 필요합니다.
일상 비유: GSO는 대량 화물을 창고에서 작게 분할하는 것, GRO는 여러 소포를 하나로 묶어 처리하는 것과 같습니다. CPU 대신 NIC가 분할/병합을 담당해 처리 효율을 높입니다.

핵심 요약

  • 체크섬 오프로드 — CHECKSUM_NONE/UNNECESSARY/PARTIAL/COMPLETE 플래그로 NIC에 체크섬 계산을 위임.
  • GSO — 커널이 큰 세그먼트를 유지하다 NIC 직전에 분할. TSO 지원 NIC면 NIC가 분할.
  • GRO — napi_gro_receive()에서 연속 패킷을 병합해 프로토콜 스택 호출 횟수 감소.
  • 터널 처리 — VXLAN/GRE 등 터널에서 내부 헤더까지 고려한 GSO 분할 필요.
  • HW-GRO — NIC가 하드웨어에서 GRO 수행, 드라이버가 gro_list로 전달.

단계별 이해

  1. 체크섬 오프로드 이해
    sk_buff의 ip_summed 필드와 4가지 체크섬 플래그 의미를 먼저 파악합니다.
  2. GSO 전송 흐름
    dev_queue_xmit() → validate_xmit_skb() → gso_segment() 경로를 추적합니다.
  3. GRO 수신 흐름
    napi_gro_receive() → napi_skb_finish() → skb merge 경로를 확인합니다.
  4. 성능 측정
    ethtool -k로 NIC 오프로드 기능 확인, ethtool -K로 켜고 끄며 효과 측정합니다.

체크섬 오프로드 (Checksum Offload)

ip_summed 필드는 sk_buff의 체크섬 처리 상태를 나타냅니다. NIC 하드웨어 체크섬 오프로드를 제어하는 핵심 필드입니다:

모드수신 (RX)전송 (TX)
CHECKSUM_NONE HW 체크섬 미지원. 소프트웨어가 검증해야 함 소프트웨어가 체크섬을 이미 계산 완료
CHECKSUM_UNNECESSARY HW가 체크섬 검증 완료, 유효함 체크섬 불필요 (loopback 등)
CHECKSUM_COMPLETE HW가 전체 패킷 체크섬을 skb->csum에 제공 사용하지 않음
CHECKSUM_PARTIAL 사용하지 않음 HW에게 체크섬 계산 위임. csum_start/csum_offset 설정 필요
RX 체크섬 경로 NIC 수신 (DMA) NIC HW 체크섬 엔진 패킷 전체 1's complement 합산 HW 미지원 HW 전체 합산 HW L4 검증 완료 CHECKSUM_NONE SW가 직접 검증 skb_checksum() CHECKSUM_COMPLETE skb->csum에 합산값 SW가 pseudo-hdr 추가 CSUM_UNNECESSARY 검증 완료, 유효 검증 생략 → 빠름 __skb_checksum_validate() pseudo-header csum 추가: csum_tcpudp_magic(saddr, daddr, len, proto, skb->csum) 결과 == 0 ? 유효 → UNNECESSARY 승격 성능 영향 (10Gbps 기준) CHECKSUM_NONE: ~2.5 Gbps (SW 계산 병목) CHECKSUM_COMPLETE: ~7 Gbps (부분 HW 지원) CHECKSUM_UNNECESSARY: ~9.8 Gbps (전체 HW) TX 체크섬 경로 소켓 → 프로토콜 스택 TCP/UDP 체크섬 설정 ip_summed = CHECKSUM_PARTIAL CHECKSUM_PARTIAL 필드 설정 csum_start: 체크섬 시작 오프셋 (TCP hdr 시작) csum_offset: 체크섬 필드 오프셋 (TCP hdr + 16) pseudo-header csum은 SW가 미리 삽입 validate_xmit_skb() NIC feature 확인 NETIF_F_HW_CSUM ✓ HW CSUM ✗ HW 체크섬 오프로드 NIC가 csum_start부터 계산 후 csum_offset에 삽입 SW Fallback skb_checksum_help() → CPU에서 직접 계산 NIC 체크섬 Feature 비트 NETIF_F_IP_CSUM: IPv4 TCP/UDP L4 체크섬 NETIF_F_IPV6_CSUM: IPv6 TCP/UDP L4 체크섬 NETIF_F_HW_CSUM: 프로토콜 무관 범용 체크섬

CHECKSUM_COMPLETE 상세 동작

CHECKSUM_COMPLETE는 NIC가 L2 페이로드 전체의 raw 1's complement 합산값을 skb->csum에 제공하는 모드입니다. 프로토콜 스택은 이 값에 pseudo-header만 추가하면 됩니다:

/* CHECKSUM_COMPLETE 처리 흐름 상세 */

/* 1. NIC 드라이버: raw 체크섬을 skb->csum에 저장 */
static void driver_set_csum_complete(struct sk_buff *skb,
                                      __wsum hw_csum)
{
    skb->ip_summed = CHECKSUM_COMPLETE;
    skb->csum = hw_csum;  /* NIC가 계산한 L2 페이로드 전체 합산값
                            * = sum(IP header + TCP/UDP header + payload)
                            * pseudo-header는 포함하지 않음 */
}

/* 2. IP 계층: __skb_checksum_validate_complete()
 *    NIC의 raw csum에 pseudo-header 체크섬을 추가하여 검증 */
static inline __sum16 __skb_checksum_validate_complete(
    struct sk_buff *skb, bool complete, __wsum psum)
{
    if (skb->ip_summed == CHECKSUM_COMPLETE) {
        /* skb->csum (NIC raw) + psum (pseudo-header) == 0이면 유효 */
        if (!csum_fold(csum_add(psum, skb->csum))) {
            /* 검증 성공 → UNNECESSARY로 승격 (이후 재검증 생략) */
            skb->ip_summed = CHECKSUM_UNNECESSARY;
            skb->csum_valid = 1;
            return 0;
        }
    }
    /* 실패 → 소프트웨어 전체 재검증 */
    return __skb_checksum_complete(skb);
}

/* 3. TCP 수신 경로에서의 사용 예 */
/* tcp_v4_rcv() → tcp_checksum_complete() */
static inline bool tcp_checksum_complete(struct sk_buff *skb)
{
    /* CHECKSUM_UNNECESSARY → 즉시 통과 (0 반환)
     * CHECKSUM_COMPLETE → pseudo-header 추가 후 검증
     * CHECKSUM_NONE → 전체 SW 재계산 */
    return !skb_csum_unnecessary(skb) &&
           __skb_checksum_complete(skb);
}

/* CHECKSUM_COMPLETE의 장점:
 * - NIC가 프로토콜을 이해할 필요 없음 (raw 합산만)
 * - L4 프로토콜에 무관하게 동작 (TCP, UDP, SCTP, DCCP 등)
 * - CHECKSUM_UNNECESSARY보다 범용적
 * - 단점: pseudo-header 추가 연산이 필요 (미미한 오버헤드) */

CHECKSUM_PARTIAL TX 내부 동작

/* CHECKSUM_PARTIAL: 프로토콜 스택이 pseudo-header를 미리 삽입하고
 * NIC가 나머지 실제 체크섬을 완성하는 협력 모델 */

/* TCP 전송 시 체크섬 설정 흐름 */
static void tcp_v4_send_check(struct sock *sk, struct sk_buff *skb)
{
    struct tcphdr *th = tcp_hdr(skb);
    struct inet_sock *inet = inet_sk(sk);

    if (skb->ip_summed == CHECKSUM_PARTIAL) {
        /* pseudo-header 체크섬만 TCP 체크섬 필드에 삽입
         * NIC가 이 위에 실제 데이터 합산값을 더해 최종 체크섬 완성 */
        th->check = ~tcp_v4_check(skb_tail_pointer(skb) - (u8 *)th,
                                   inet->inet_saddr, inet->inet_daddr, 0);

        /* csum_start/csum_offset 설정 — NIC에게 알려주는 좌표 */
        skb->csum_start = skb_transport_header(skb) - skb->head;
        skb->csum_offset = offsetof(struct tcphdr, check);
        /*
         * csum_start: 체크섬 계산 시작점 (TCP 헤더 시작)
         * csum_offset: 체크섬을 기록할 위치 (TCP 헤더 내 check 필드)
         *
         * NIC의 동작:
         * 1. skb->data + csum_start 부터 끝까지 1's complement 합산
         * 2. 결과를 csum_start + csum_offset 위치에 기록
         * 3. 이미 삽입된 pseudo-header csum과 자동 합산됨
         */
    }
}

/* NIC feature별 체크섬 오프로드 능력 비교 */
/*
 * NETIF_F_IP_CSUM:
 *   - IPv4 TCP/UDP만 지원
 *   - NIC가 IPv4 pseudo-header 구조를 알고 있음
 *   - 구형 NIC에서 주로 사용
 *
 * NETIF_F_IPV6_CSUM:
 *   - IPv6 TCP/UDP 지원
 *   - NETIF_F_IP_CSUM과 보통 함께 지원
 *
 * NETIF_F_HW_CSUM:
 *   - 프로토콜 무관 범용 체크섬
 *   - csum_start/csum_offset으로 임의 위치 체크섬 계산
 *   - 터널, SCTP, 새로운 프로토콜 등 모두 지원
 *   - 최신 NIC(Intel E810, Mellanox CX-5+)에서 지원
 *   - NETIF_F_IP_CSUM/IPV6_CSUM을 완전히 대체
 *
 * 확인:
 *   # ethtool -k eth0 | grep csum
 *   tx-checksum-ipv4: on
 *   tx-checksum-ipv6: on
 *   tx-checksum-ip-generic: on [NETIF_F_HW_CSUM]
 */

Scatter-Gather와 체크섬 계산

대부분의 고성능 NIC는 Scatter-Gather DMA를 사용합니다. 비선형(nonlinear) skb의 체크섬 계산은 frags[] 배열을 순회해야 합니다:

/* 비선형 skb의 체크섬 계산 — skb_checksum() 내부 */
__wsum skb_checksum(const struct sk_buff *skb,
                    int offset, int len, __wsum csum)
{
    /* 1단계: 선형(linear) 영역의 체크섬 */
    int copy = min(len, skb_headlen(skb) - offset);
    if (copy > 0) {
        csum = csum_partial(skb->data + offset, copy, csum);
        len -= copy;
        offset += copy;
    }

    /* 2단계: frags[] 배열의 각 페이지 체크섬 */
    for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
        struct skb_frag_struct *frag = &skb_shinfo(skb)->frags[i];
        int frag_size = skb_frag_size(frag);

        if (copy < frag_size) {
            u8 *vaddr = kmap_local_page(skb_frag_page(frag));
            csum = csum_partial(vaddr + skb_frag_off(frag) + offset,
                                 min(len, frag_size - offset), csum);
            kunmap_local(vaddr);
        }
    }

    /* 3단계: frag_list 체인 (있으면) */
    skb_walk_frags(skb, frag_iter) {
        csum = skb_checksum(frag_iter, offset, len, csum); /* 재귀 */
    }

    return csum;
}

/* 성능 영향:
 * - CHECKSUM_NONE + 비선형 skb = 최악의 조합
 *   → 모든 frag 페이지를 kmap하면서 체크섬 계산
 *   → 10Gbps 환경에서 CPU 50%+ 소비 가능
 * - CHECKSUM_UNNECESSARY → 이 과정 전체 생략
 * - GSO skb의 경우: 체크섬은 분할 시 한꺼번에 계산
 *   → skb_gso_segment() 내부에서 각 세그먼트별 계산
 */

터널/캡슐화 체크섬 처리

VXLAN, GRE 등 터널 환경에서는 외부(outer)와 내부(inner) 두 겹의 체크섬을 처리해야 합니다:

/* 터널 체크섬 이중 처리 구조 */
/*
 * [Outer Eth][Outer IP][Outer UDP csum][VXLAN][Inner Eth][Inner IP][Inner TCP csum][Payload]
 *                       ↑ 외부 체크섬                                  ↑ 내부 체크섬
 *
 * NETIF_F_HW_CSUM 지원 NIC:
 *   → 내부 체크섬: CHECKSUM_PARTIAL로 NIC에 위임
 *   → 외부 체크섬: 비활성화(0) 또는 NIC가 별도 처리
 *
 * 터널 체크섬 관련 NIC feature:
 *   NETIF_F_GSO_UDP_TUNNEL_CSUM: 외부 UDP csum + 내부 GSO
 *   → NIC가 내부 세그먼트 분할 후 각각의 외부 UDP csum도 계산
 */

/* VXLAN TX에서의 체크섬 설정 */
static void vxlan_xmit_skb(struct sk_buff *skb, ...)
{
    /* 내부 패킷 체크섬은 이미 CHECKSUM_PARTIAL로 설정됨 */

    /* 외부 UDP 체크섬 설정 */
    if (!udp_sum) {
        /* 외부 UDP csum 비활성화 (0으로 설정) — 성능 최적화
         * RFC 7348: VXLAN에서 외부 UDP csum은 선택사항 */
        uh->check = 0;
    } else {
        /* 외부 UDP csum 활성화 — 무결성 강화 */
        udp_set_csum(skb->ip_summed != CHECKSUM_PARTIAL,
                     skb, saddr, daddr, skb->len);
    }

    /* 터널 encap 후 체크섬 정보 갱신 */
    skb_set_inner_protocol(skb, htons(ETH_P_TEB));
    skb_set_inner_network_header(skb, inner_nhoff);
    skb_set_inner_transport_header(skb, inner_thoff);
    /* 이 inner offset 정보가 NIC의 터널 체크섬 오프로드에 사용됨 */
}

/* 수신 측: 터널 디캡슐화 시 체크섬 검증 */
/*
 * 1. 외부 UDP csum 검증 (0이면 생략)
 * 2. VXLAN 헤더 파싱
 * 3. 내부 패킷의 ip_summed 복원:
 *    - 외부가 CHECKSUM_UNNECESSARY → 내부도 UNNECESSARY 가능
 *    - skb_checksum_simple_validate()로 내부 csum 검증 최적화
 * 4. remcsum 오프로드: VXLAN GPE 등에서 원격 체크섬 보조
 *    → TUNNEL_REMCSUM 플래그로 NIC가 내부 csum을 계산
 */
/* 수신 경로: 드라이버에서 체크섬 상태 설정 */
static void my_rx_handler(struct sk_buff *skb, bool hw_csum_ok)
{
    if (hw_csum_ok) {
        /* NIC가 체크섬 검증 완료 → 소프트웨어 재검증 생략 */
        skb->ip_summed = CHECKSUM_UNNECESSARY;
    } else {
        /* HW 미지원 → 프로토콜 스택이 직접 검증 */
        skb->ip_summed = CHECKSUM_NONE;
    }
}

/* 전송 경로: 소프트웨어 체크섬 fallback */
if (skb->ip_summed == CHECKSUM_PARTIAL) {
    /* NIC가 NETIF_F_HW_CSUM을 지원하지 않으면 */
    if (skb_checksum_help(skb))  /* SW로 체크섬 계산 */
        goto drop;
}

/* 체크섬 관련 유틸리티 */
skb_checksum(skb, offset, len, 0);       /* skb 데이터의 체크섬 계산 */
csum_tcpudp_magic(saddr, daddr, len, proto, csum); /* pseudo-header 포함 */
skb_postpull_rcsum(skb, hdr, hdr_len);   /* pull 후 csum 보정 */

GSO/GRO와 sk_buff

GSO (Generic Segmentation Offload)GRO (Generic Receive Offload)는 대량 데이터 전송/수신 시 성능을 극대화하는 핵심 메커니즘입니다. 기본 원리는 단순합니다: 네트워크 스택을 통과하는 패킷 수를 줄여 per-packet 오버헤드(헤더 파싱, 룩업, lock 경합, cache miss)를 최소화합니다.

ℹ️

핵심 개념: GSO는 전송(TX) 방향에서 대형 skb를 마지막 순간에 분할하고, GRO는 수신(RX) 방향에서 작은 패킷들을 하나의 대형 skb로 병합합니다. 둘 다 네트워크 스택 통과를 한 번으로 줄여 성능을 극대화합니다. MTU=1500 기준 64KB 데이터 처리 시, ~43개 패킷을 개별 처리하는 대신 1개의 대형 skb로 스택을 한 번만 통과합니다.

TX 경로 — GSO (Generic Segmentation Offload) tcp_sendmsg() tcp_write_xmit() 64KB super-skb 생성 __dev_queue_xmit() qdisc enqueue validate_xmit_skb() GSO 분할 결정 NIC HW TSO 하드웨어가 분할 → 1개 skb 전달 SW GSO skb_gso_segment() → N개 skb 리스트 dev→features NETIF_F_TSO ✓ NETIF_F_TSO ✗ RX 경로 — GRO (Generic Receive Offload) NIC → NAPI poll() napi_gro_receive() GRO 병합 시도 dev_gro_receive() 프로토콜별 GRO 콜백 inet_gro_receive() → tcp4_gro_receive() GRO_MERGED 기존 skb에 병합 → 대형 super-skb GRO_NORMAL 병합 불가 → 일반 경로
GRO는 NAPI 폴링 루프 내에서 동작합니다. NAPI의 napi_gro_receive() 호출 경로, GRO flush 타이머, busy polling과의 연동 등 NAPI 관점의 GRO 통합 아키텍처는 NAPI 심화 — GRO 통합 문서를 참고하세요.

GSO (전송 오프로드) 심화

GSO(Generic Segmentation Offload)는 TSO(TCP Segmentation Offload)의 소프트웨어 일반화입니다. 핵심 아이디어: 가능한 한 늦게(late) 세그먼트를 분할하여 네트워크 스택의 중간 계층(Netfilter, TC, qdisc)에서 처리하는 패킷 수를 최소화합니다.

TSO에서 GSO로의 발전

설명 요약:
  • === 일반 전송 (세그먼트 오프로드 없음) ===
  • tcp_sendmsg()가 MSS 단위로 skb를 생성:
  • write(fd, buf, 64000)
  • → 44개 skb 생성 (각 1460 바이트)
  • → 44번 TCP 헤더 생성 + 체크섬 계산
  • → 44번 IP 계층 통과 + Netfilter/TC 처리
  • → 44번 qdisc enqueue/dequeue
  • → 44번 NIC DMA → 매우 높은 CPU 부하
  • === TSO (하드웨어 오프로드) ===
  • 커널이 최대 64KB의 대형 skb를 생성하여 NIC에 직접 전달:
  • → 1개 skb (64KB) → NIC 하드웨어가 MSS 단위 분할
  • → NIC가 각 세그먼트에 TCP/IP 헤더 복사 + 시퀀스 번호 증가 + 체크섬 계산
  • → 장점: CPU 부하 최소, 10Gbps+ 환경에서 매우 중요
  • → 단점: NIC가 TSO를 지원해야 함, 터널/암호화 등에선 미지원 가능
  • 확인: ethtool -k eth0 | grep tcp-segmentation
  • === GSO (소프트웨어 일반화, 커널 2.6.18+) ===
  • TSO와 동일한 "대형 skb" 전략을 소프트웨어로 구현:
  • → 커널이 대형 skb를 유지하며 네트워크 스택을 통과
  • → validate_xmit_skb()에서 NIC feature 확인:
  • NIC가 TSO 지원 → 그대로 NIC에 전달 (HW offload)
  • NIC가 TSO 미지원 → skb_gso_segment()로 소프트웨어 분할
  • → 중간 계층(Netfilter, TC, qdisc)은 1개 skb만 처리 → 효율 극대화
  • GSO의 핵심 이점:
  • TSO 미지원 NIC에서도 성능 향상 (SW 분할이라도 늦은 분할이 유리)
  • 터널, 가상화 등 복잡한 경로에서도 동작
  • UDP, SCTP 등 TCP 외 프로토콜도 지원

GSO 타입 전체 목록

skb_shared_info→gso_type 필드에 설정되는 비트마스크입니다. 여러 타입이 OR로 조합될 수 있습니다:

GSO 타입 플래그대상 프로토콜설명
SKB_GSO_TCPV41 << 0IPv4 TCP가장 일반적인 TSO/GSO. MSS 단위로 TCP 세그먼트 분할
SKB_GSO_TCPV61 << 5IPv6 TCPIPv6 환경의 TSO/GSO
SKB_GSO_UDP1 << 1UDP (IP frag)IP 단편화(fragmentation) 기반 UDP GSO. UFO(UDP Fragmentation Offload)
SKB_GSO_UDP_L41 << 11UDP (L4 분할)UDP 세그먼트 단위 분할 (커널 4.18+). QUIC, WireGuard 등에 사용
SKB_GSO_DODGY1 << 2모두신뢰할 수 없는 GSO skb (VM에서 전달 등). 재검증 필요
SKB_GSO_TCP_ECN1 << 3TCP + ECNECN(Explicit Congestion Notification) 플래그 있는 TCP GSO
SKB_GSO_TCP_FIXEDID1 << 9TCP모든 세그먼트가 동일 IP ID 사용 (드문 경우)
SKB_GSO_GRE1 << 6GRE 터널GRE 캡슐화 안의 내부 패킷 GSO
SKB_GSO_GRE_CSUM1 << 7GRE + 체크섬GRE 체크섬이 활성화된 터널 GSO
SKB_GSO_UDP_TUNNEL1 << 8VXLAN/GeneveUDP 기반 터널 내부 패킷 GSO
SKB_GSO_UDP_TUNNEL_CSUM1 << 10VXLAN + csum외부 UDP 체크섬이 활성화된 터널 GSO
SKB_GSO_PARTIAL1 << 13터널/복합부분 GSO — 외부 헤더만 NIC가 처리, 내부는 소프트웨어 분할
SKB_GSO_TUNNEL_REMCSUM1 << 12터널터널 원격 체크섬 오프로드
SKB_GSO_SCTP1 << 14SCTPSCTP 청크 단위 GSO
SKB_GSO_ESP1 << 15IPsec ESPESP(Encapsulating Security Payload) GSO
SKB_GSO_FRAGLIST1 << 17UDP/IPfrag_list 기반 GSO (GRO에서 병합된 skb 재전송 시)

GSO 관련 skb_shared_info 필드

/* include/linux/skbuff.h — skb_shared_info의 GSO 관련 필드 */
struct skb_shared_info {
    ...
    unsigned short gso_size;   /* 분할 단위 크기 (MSS 또는 세그먼트 크기)
                                   * TCP: MSS (예: 1460)
                                   * UDP GSO: 각 데이터그램 크기 (예: 1472)
                                   * 0이면 GSO 미사용 */
    unsigned short gso_segs;   /* 예상 분할 세그먼트 수 (힌트 값)
                                   * DIV_ROUND_UP(skb->len - hdr_len, gso_size)
                                   * NIC의 BQL(Byte Queue Limit) 계산에 활용 */
    unsigned int   gso_type;   /* SKB_GSO_* 비트마스크 (위 표 참조)
                                   * 여러 플래그 OR 조합 가능
                                   * 예: SKB_GSO_TCPV4 | SKB_GSO_TCP_ECN */
    ...
};

/* GSO skb인지 확인하는 헬퍼 함수들 */
static inline bool skb_is_gso(const struct sk_buff *skb)
{
    return skb_shinfo(skb)->gso_size;  /* gso_size != 0이면 GSO skb */
}

static inline bool skb_is_gso_v6(const struct sk_buff *skb)
{
    return skb_shinfo(skb)->gso_type & SKB_GSO_TCPV6;
}

/* GSO 세그먼트의 실제 와이어(wire) 크기 계산 */
static inline unsigned int skb_gso_network_seglen(const struct sk_buff *skb)
{
    unsigned int hdr_len = skb_transport_header(skb) - skb_network_header(skb);
    return hdr_len + skb_shinfo(skb)->gso_size;
}

GSO 전송 경로 상세

TCP 전송을 예로 들어 GSO skb가 생성되고 분할되는 전체 경로를 추적합니다:

/* 1단계: tcp_sendmsg() — 사용자 데이터를 skb에 적재 */
/*
 * tcp_sendmsg_locked()에서 sk_stream_alloc_skb()로 skb 할당
 * → tcp_send_mss()로 현재 MSS 결정 (경로 MTU - 헤더)
 * → size_goal: GSO 활성 시 MSS * max_segs (최대 64KB)
 * → 하나의 skb에 size_goal만큼 데이터를 적재
 */
int mss_now = tcp_send_mss(sk, &size_goal, flags);
/* size_goal 예시:
 *   MSS=1460, sk->sk_gso_max_segs=44 → size_goal = 1460 * 44 = 64240
 *   → 하나의 skb에 최대 64KB 데이터 적재
 */

/* 2단계: tcp_write_xmit() — skb에 TCP 헤더 부착 및 GSO 설정 */
/*
 * tcp_init_tso_segs()에서 GSO 관련 필드 설정:
 */
static void tcp_set_skb_tso_segs(struct sk_buff *skb, unsigned int mss_now)
{
    struct skb_shared_info *shinfo = skb_shinfo(skb);

    if (skb->len <= mss_now) {
        /* MSS 이하 → 분할 불필요, GSO 미사용 */
        shinfo->gso_segs = 1;
        shinfo->gso_size = 0;
        shinfo->gso_type = 0;
    } else {
        /* MSS 초과 → GSO skb로 설정 */
        shinfo->gso_segs = DIV_ROUND_UP(skb->len, mss_now);
        shinfo->gso_size = mss_now;
        shinfo->gso_type = sk->sk_gso_type;  /* SKB_GSO_TCPV4 등 */
    }
}

/* 3단계: ip_queue_xmit() → __dev_queue_xmit() → qdisc */
/*
 * IP 계층에서 라우팅 조회, TTL 설정 등을 한 번만 수행
 * qdisc에서도 1개의 대형 skb만 큐잉
 * → 여기까지 N개 패킷이 아닌 1개의 대형 skb로 처리
 */

/* 4단계: validate_xmit_skb() — GSO 분할 결정의 핵심 */
static struct sk_buff *validate_xmit_skb(struct sk_buff *skb,
                                           struct net_device *dev, bool *again)
{
    netdev_features_t features;

    /* NIC가 지원하는 feature 확인 */
    features = netif_skb_features(skb);

    if (skb_is_gso(skb)) {
        /* GSO skb이고 NIC가 해당 오프로드를 지원하면 → HW 처리 (분할 안 함) */
        /* NIC가 미지원이면 → __skb_gso_segment()로 소프트웨어 분할 */
        struct sk_buff *segs = __skb_gso_segment(skb, features, true);
        if (IS_ERR_OR_NULL(segs)) {
            if (!segs)
                return skb;  /* NIC가 HW 처리 가능 → 원본 skb 그대로 */
            kfree_skb(skb);
            return NULL;
        }
        /* SW 분할 완료: segs는 분할된 skb 리스트 */
        consume_skb(skb);
        return segs;
    }
    ...
}

skb_gso_segment() 내부 동작

/* net/core/skbuff.c — 소프트웨어 GSO 분할의 핵심 함수 */
struct sk_buff *skb_gso_segment(struct sk_buff *skb, netdev_features_t features)
{
    /* 프로토콜별 GSO 콜백 호출 */
    /* TCP: tcp4_gso_segment() 또는 tcp6_gso_segment()
     * UDP: udp4_ufo_fragment() 또는 __udp_gso_segment()
     * 터널: skb_udp_tunnel_segment() 등 */

    return skb_mac_gso_segment(skb, features);
}

/* net/ipv4/tcp_offload.c — TCP GSO 분할 */
struct sk_buff *tcp4_gso_segment(struct sk_buff *skb,
                                 netdev_features_t features)
{
    /* tcp_gso_segment()가 실제 분할 수행:
     *
     * 1. 원본 skb의 데이터를 gso_size(MSS) 단위로 분할
     * 2. 각 세그먼트에 새 TCP 헤더 복사:
     *    - seq 번호 증가 (prev_seq += gso_size)
     *    - PSH 플래그는 마지막 세그먼트에만 설정
     *    - FIN 플래그는 마지막 세그먼트에만 설정
     *    - CWR 플래그는 첫 세그먼트에만 설정
     * 3. 각 세그먼트의 IP 헤더 업데이트:
     *    - total_length 재계산
     *    - IP ID 증가 (SKB_GSO_TCP_FIXEDID 아닌 경우)
     * 4. 체크섬 재계산 (CHECKSUM_PARTIAL이면 pseudo-header만)
     * 5. 분할된 skb들을 linked list로 반환 (skb->next)
     */

    if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
        return ERR_PTR(-EINVAL);

    return tcp_gso_segment(skb, features);
}

/* 분할 결과 사용 예시 */
struct sk_buff *segs = skb_gso_segment(skb, features);
struct sk_buff *seg, *tmp;

skb_list_walk_safe(segs, seg, tmp) {
    skb_mark_not_on_list(seg);
    /* 각 세그먼트는 독립적인 skb:
     * - 자체 TCP/IP 헤더 보유
     * - 정확한 시퀀스 번호
     * - 올바른 체크섬
     * - skb->len == gso_size + hdr_len (마지막은 작을 수 있음)
     */
    dev_queue_xmit(seg);
}

GSO_PARTIAL — 터널 환경의 부분 오프로드

SKB_GSO_PARTIAL은 NIC가 외부(outer) 헤더는 처리할 수 있지만 내부(inner) 패킷의 GSO는 지원하지 않는 경우를 위한 메커니즘입니다:

/* VXLAN 터널에서의 GSO_PARTIAL 동작:
 *
 * [Outer Eth][Outer IP][Outer UDP][VXLAN Hdr][Inner Eth][Inner IP][Inner TCP][Payload]
 *
 * NIC가 NETIF_F_GSO_UDP_TUNNEL은 지원하지만
 * NETIF_F_GSO_UDP_TUNNEL_CSUM은 미지원하는 경우:
 *
 * 1. 커널이 내부(inner) TCP 세그먼트를 소프트웨어로 분할
 * 2. 각 분할된 세그먼트에 외부(outer) 헤더를 붙임
 * 3. 외부 헤더의 처리(체크섬 등)는 NIC에 위임
 *
 * 관련 NIC feature 확인:
 */
/* # ethtool -k eth0 | grep tunnel
 *   tx-udp_tnl-segmentation: on        (외부 UDP 터널 GSO)
 *   tx-udp_tnl-csum-segmentation: off   (외부 csum 미지원 → PARTIAL 필요)
 */

/* net_device features 비트 매핑 */
NETIF_F_TSO                  /* TCP Segmentation Offload (IPv4) */
NETIF_F_TSO6                 /* TCP Segmentation Offload (IPv6) */
NETIF_F_GSO_GRE              /* GRE 터널 내부 GSO */
NETIF_F_GSO_GRE_CSUM         /* GRE 체크섬 + 내부 GSO */
NETIF_F_GSO_UDP_TUNNEL       /* UDP 터널 (VXLAN/Geneve) 내부 GSO */
NETIF_F_GSO_UDP_TUNNEL_CSUM  /* UDP 터널 + 외부 UDP 체크섬 GSO */
NETIF_F_GSO_PARTIAL          /* 부분 GSO: 외부는 HW, 내부는 SW */
NETIF_F_GSO_UDP_L4           /* UDP L4 세그먼트 오프로드 (4.18+) */
NETIF_F_GSO_ESP              /* IPsec ESP GSO */
NETIF_F_GSO_SCTP             /* SCTP GSO */

GSO 최대 크기 제어

/* net_device의 GSO 크기 제한 */
struct net_device {
    unsigned int gso_max_size;    /* GSO skb 최대 크기 (기본 65536)
                                    * ip link set dev eth0 gso_max_size 32768
                                    * 으로 조절 가능 */
    u16          gso_max_segs;    /* 최대 세그먼트 수 (기본 65535)
                                    * NIC 하드웨어 제한에 따라 설정 */
    unsigned int gso_ipv4_max_size; /* IPv4 전용 GSO 최대 크기 (6.3+) */
};

/* 소켓 레벨에서도 GSO 크기 제어 가능 */
struct sock {
    int          sk_gso_max_size;  /* 소켓별 GSO 최대 크기 */
    u16          sk_gso_max_segs;  /* 소켓별 최대 세그먼트 수 */
    int          sk_gso_type;      /* 지원하는 GSO 타입 비트마스크 */
};

/* BIG TCP (커널 6.3+): IPv6에서 64KB 넘는 GSO 허용 */
/* ip link set dev eth0 gso_max_size 185000
 * → IPv6 jumbogram 활용, 단일 skb에 ~185KB 적재 가능
 * → GRO에서도 gro_max_size로 대응
 *
 * 주의: IPv4는 IP 헤더의 total_length가 16비트이므로 64KB가 한계
 *       IPv6 jumbogram 확장 헤더로 이 제한을 우회
 */

GRO (수신 오프로드) 심화

GRO(Generic Receive Offload)는 수신된 작은 패킷들을 하나의 큰 skb로 합치는 메커니즘입니다. LRO(Large Receive Offload)의 소프트웨어 일반화로, LRO와 달리 원본 헤더 정보를 보존하여 라우팅/포워딩 환경에서도 안전하게 동작합니다.

LRO vs GRO 비교

특성LRO (Large Receive Offload)GRO (Generic Receive Offload)
구현 위치NIC 드라이버 또는 하드웨어커널 네트워크 스택 (NAPI 레벨)
헤더 보존병합 시 원본 헤더 정보 손실 가능원본 헤더 정보 완전 보존
라우팅/포워딩패킷 재분할 시 헤더 복구 불가 → 부적합안전하게 재분할 가능 → 적합
프로토콜 지원TCP만TCP, UDP, GRE, VXLAN 등 확장 가능
병합 기준느슨함 (IP/TCP 4-tuple만)엄격함 (헤더 완전 일치 검증)
현재 상태deprecated (대부분 GRO로 대체)표준 수신 오프로드

GRO 수신 파이프라인

/* === GRO 수신 경로 전체 흐름 ===
 *
 * NIC IRQ
 *  └→ napi_schedule()
 *      └→ NAPI poll 함수 (드라이버)
 *          └→ napi_gro_receive(napi, skb)           ← 진입점
 *              └→ dev_gro_receive(napi, skb)
 *                  ├→ gro_list에서 동일 flow 검색
 *                  └→ 프로토콜별 GRO 콜백 체인:
 *                      └→ inet_gro_receive()         (L3: IP)
 *                          └→ tcp4_gro_receive()     (L4: TCP)
 *                              └→ GRO 결과 반환
 *                                  ├→ GRO_MERGED:      기존 skb에 병합 완료
 *                                  ├→ GRO_MERGED_FREE: 병합 + 현재 skb 해제
 *                                  ├→ GRO_HELD:        gro_list에 보관 (다음 패킷 대기)
 *                                  ├→ GRO_NORMAL:      병합 불가 → 일반 경로
 *                                  └→ GRO_CONSUMED:    콜백이 직접 처리 완료
 */

/* net/core/gro.c — GRO 핵심 진입점 */
gro_result_t napi_gro_receive(struct napi_struct *napi,
                              struct sk_buff *skb)
{
    /* skb 전처리: VLAN 태그 처리, 해시 설정 등 */
    skb_gro_reset_offset(skb);

    return napi_skb_finish(napi, skb, dev_gro_receive(napi, skb));
}

static enum gro_result dev_gro_receive(struct napi_struct *napi,
                                        struct sk_buff *skb)
{
    struct list_head *gro_head = &napi->gro_hash[bucket].list;
    int count = napi->gro_hash[bucket].count;

    /* gro_list에서 병합 가능한 기존 skb 검색 */
    list_for_each_entry(pp, gro_head, list) {
        /* same_flow: 동일 flow인지 빠른 비교 (rxhash 기반) */
        NAPI_GRO_CB(pp)->same_flow = 1;
    }

    /* 프로토콜별 GRO 콜백 호출 (inet_gro_receive 등) */
    pp = call_gro_receive(ptype->callbacks.gro_receive, gro_head, skb);

    if (pp == skb) return GRO_NORMAL;     /* 병합 불가 */
    if (pp)        return GRO_MERGED;     /* 병합 성공 → flush 대상 */
    if (NAPI_GRO_CB(skb)->flush)
        return GRO_NORMAL;                /* 병합 불가 플래그 */

    /* 새 flow → gro_list에 추가 (다음 패킷 대기) */
    if (count < MAX_GRO_SKBS)            /* MAX_GRO_SKBS = 8 */
        return GRO_HELD;
    else
        return GRO_NORMAL;                /* 버킷 가득 참 → 일반 경로 */
}

GRO 병합 기준

패킷이 기존 flow에 병합되려면 다음 조건을 모두 만족해야 합니다:

/* === TCP GRO 병합 조건 (tcp4_gro_receive) ===
 *
 * 1. L2 레벨: 동일 수신 해시 (rxhash) — 빠른 사전 필터링
 *
 * 2. L3 레벨 (inet_gro_receive):
 *    - 동일 IP 프로토콜 (IPPROTO_TCP)
 *    - 동일 src/dst IP 주소
 *    - 동일 TOS (Type of Service)
 *    - 동일 TTL (합치면 안 되는 경우 방지)
 *    - IP 옵션 없음 (옵션이 있으면 병합 거부)
 *    - IP ID가 순차적으로 증가 (또는 DF 비트 설정 시 무관)
 *
 * 3. L4 레벨 (tcp_gro_receive):
 *    - 동일 src/dst 포트
 *    - TCP 시퀀스 번호가 연속 (이전 패킷의 끝 + 1)
 *    - TCP 윈도우 크기가 동일
 *    - ACK 플래그만 설정 (SYN, FIN, RST, URG, ECE, CWR → 병합 거부)
 *    - TCP 타임스탬프 옵션이 있으면 값이 동일하거나 증가
 *    - 이전에 병합된 패킷과 옵션 길이가 동일
 *
 * 4. 크기 제한:
 *    - 병합된 skb 크기가 gro_max_size를 초과하지 않아야 함
 *    - 기본 gro_max_size = 65536 (BIG TCP 시 더 큼)
 */

/* net/ipv4/tcp_offload.c — TCP GRO 병합 핵심 검증 */
struct sk_buff *tcp_gro_receive(struct list_head *head,
                                struct sk_buff *skb)
{
    struct tcphdr *th = tcp_hdr(skb);

    list_for_each_entry(p, head, list) {
        if (!NAPI_GRO_CB(p)->same_flow)
            continue;

        th2 = tcp_hdr(p);

        /* 포트 비교 */
        if (*((u32 *)&th->source) ^ *((u32 *)&th2->source)) {
            NAPI_GRO_CB(p)->same_flow = 0;
            continue;
        }

        /* 시퀀스 번호 연속 확인 */
        if (ntohl(th2->seq) + skb_gro_len(p) != ntohl(th->seq)) {
            NAPI_GRO_CB(p)->flush = 1;  /* 비연속 → flush */
            continue;
        }

        /* ACK 외 플래그 확인: SYN/FIN/RST → flush */
        if (th->fin || th->syn || th->rst || th->urg)
            flush = 1;

        /* 윈도우 크기 일치 확인 */
        if (th->window ^ th2->window)
            flush = 1;

        /* 타임스탬프 옵션 검증 */
        if (pcount > 1 || tcp_flag_word(th2) & TCP_FLAG_CWR)
            flush = 1;

        /* 병합 수행: skb 데이터를 기존 p에 append */
        if (!flush)
            skb_gro_receive(p, skb);  /* → 아래 설명 */
    }
    ...
}

GRO 데이터 병합 방식

GRO는 두 가지 방식으로 수신 데이터를 병합합니다:

병합 방식조건데이터 구조장단점
frag 기반 skb가 선형(linear) 데이터일 때 skb_shared_info→frags[] 배열에 페이지 추가 메모리 효율적, scatter-gather DMA에 적합. MAX_SKB_FRAGS(17) 제한
frag_list 기반 frag 공간 부족 또는 skb 자체가 이미 비선형일 때 skb_shared_info→frag_list에 skb 체인 연결 구조 단순, 제한 없음. 하지만 GSO 재분할 시 오버헤드
/* net/core/gro.c — skb_gro_receive() 핵심 로직 */
int skb_gro_receive(struct sk_buff *p, struct sk_buff *skb)
{
    unsigned int new_len = p->len + skb->len;

    /* gro_max_size 초과 검사 */
    if (new_len > gro_max_size(p))
        return -E2BIG;

    if (skb_headlen(skb) <= offset) {
        /* === frag 기반 병합 ===
         * 새 skb의 페이지를 기존 skb의 frags[]에 추가 */
        struct skb_shared_info *pinfo = skb_shinfo(p);

        if (pinfo->nr_frags + skb_shinfo(skb)->nr_frags > MAX_SKB_FRAGS)
            goto merge_frag_list;

        /* frags[] 배열에 새 페이지들을 복사 */
        for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
            pinfo->frags[pinfo->nr_frags++] = skb_shinfo(skb)->frags[i];
        }
    } else {
merge_frag_list:
        /* === frag_list 기반 병합 ===
         * 새 skb를 기존 skb의 frag_list 체인에 연결 */
        if (!skb_shinfo(p)->frag_list)
            skb_shinfo(p)->frag_list = skb;
        else
            NAPI_GRO_CB(p)->last->next = skb;

        NAPI_GRO_CB(p)->last = skb;
    }

    /* 병합된 skb의 총 길이 업데이트 */
    p->len += skb->len;
    p->data_len += skb->len;
    p->truesize += skb->truesize;
    NAPI_GRO_CB(p)->count++;      /* 병합된 패킷 수 */

    return 0;
}

GRO Flush 메커니즘

gro_list에 보관 중인 병합된 skb는 다음 조건에서 상위 스택으로 전달(flush)됩니다:

/* GRO flush 발생 조건과 동작 */

/* 1. napi_complete_done() 호출 시 — 가장 일반적
 *    NAPI poll에서 budget 미만 처리 → 인터럽트 재활성화 전 flush */
bool napi_complete_done(struct napi_struct *napi, int work_done)
{
    /* gro_list의 모든 보류 skb를 flush */
    gro_normal_list(napi);  /* → netif_receive_skb_list_internal() */
    ...
}

/* 2. MAX_GRO_SKBS(8) 초과 시 — 버킷당 보관 제한
 *    동일 해시 버킷에 8개 이상 skb가 쌓이면 가장 오래된 것 flush */

/* 3. 비연속 패킷 수신 시 (flush 플래그)
 *    시퀀스 번호 불연속, 다른 플래그 등 → 현재 보관 중인 skb flush */

/* 4. gro_flush_timeout 타이머 만료 (busy polling 관련) */
/* sysctl: net.core.gro_flush_timeout
 *   기본값: 0 (즉시 flush, 타이머 미사용)
 *   설정 시: napi_complete에서 바로 flush하지 않고 타이머 대기
 *   → 더 많은 패킷을 병합할 기회를 제공하지만 지연 증가
 *
 * 관련 sysctl:
 *   net.core.napi_defer_hard_irqs = N
 *   → N번의 빈 poll 후에야 IRQ 재활성화
 *   → gro_flush_timeout과 함께 사용하면 GRO 효율 극대화
 */

/* 5. 명시적 flush 호출 */
napi_gro_flush(napi, false);  /* 모든 보류 skb를 즉시 flush */

/* flush된 skb는 netif_receive_skb_list_internal()로 전달되어
 * 일반 수신 경로(IP → TCP → 소켓)를 탐니다.
 * 이때 skb->len은 원래 여러 패킷의 합이므로
 * TCP 수신 측에서 효율적으로 처리됩니다. */

GRO API 변형

/* 드라이버에서 사용하는 GRO 진입점들 */

/* 1. napi_gro_receive() — 가장 일반적
 *    완전한 skb를 GRO 처리 */
gro_result_t napi_gro_receive(struct napi_struct *napi,
                              struct sk_buff *skb);

/* 2. napi_gro_frags() — 헤더/데이터 분리 수신 시
 *    NIC가 헤더를 선형 영역에, 페이로드를 페이지에 배치한 경우
 *    napi->skb에 미리 설정된 skb 사용 */
gro_result_t napi_gro_frags(struct napi_struct *napi);
/*
 * 사용 패턴 (고성능 드라이버):
 *   napi->skb = netdev_alloc_skb(...);
 *   skb_put(napi->skb, hdr_len);        // 헤더를 선형 영역에
 *   skb_fill_page_desc(napi->skb, ...);  // 페이로드를 frag으로
 *   napi_gro_frags(napi);
 *   // napi->skb는 내부에서 소비/해제됨
 */

/* 3. napi_gro_complete() — 내부 함수
 *    GRO 완료 시 호출, 프로토콜별 gro_complete 콜백 실행 후 상위 전달 */

/* GRO 결과 타입 */
enum gro_result {
    GRO_MERGED,       /* 기존 skb에 병합 성공 — flush 대상으로 표시 */
    GRO_MERGED_FREE,  /* 병합 성공 + 현재 skb 해제 가능 */
    GRO_HELD,         /* gro_list에 보관 (새 flow, 다음 패킷 대기) */
    GRO_NORMAL,       /* 병합 불가 — 일반 수신 경로로 */
    GRO_CONSUMED,     /* 콜백이 직접 처리 완료 */
};

하드웨어 GRO (HW-GRO)

커널 5.19+에서 도입된 HW-GRO는 NIC 하드웨어가 GRO를 수행하되, LRO와 달리 헤더 정보를 보존합니다:

/* HW-GRO vs SW-GRO vs LRO 비교
 *
 * LRO (deprecated):
 *   - NIC가 패킷 병합, 헤더 정보 손실
 *   - 포워딩 시 문제 → ip_summed 등 불일치
 *   - ethtool -K eth0 lro on/off
 *
 * SW-GRO (기본):
 *   - 커널 NAPI 레벨에서 병합
 *   - 헤더 완전 보존, 모든 프로토콜 지원
 *   - CPU 오버헤드 있음
 *   - ethtool -K eth0 gro on/off
 *
 * HW-GRO (5.19+):
 *   - NIC 하드웨어가 병합하되 개별 헤더를 보존
 *   - NIC가 "header split" 또는 RSC(Receive Side Coalescing) 활용
 *   - CPU 오버헤드 최소화 + 헤더 보존의 장점
 *   - ethtool -K eth0 rx-gro-hw on/off
 *
 * 확인:
 *   # ethtool -k eth0 | grep gro
 *   generic-receive-offload: on
 *   rx-gro-hw: on [requested on]
 */

/* NIC feature 플래그 */
NETIF_F_GRO     /* 소프트웨어 GRO 지원 (기본 on) */
NETIF_F_GRO_HW  /* 하드웨어 GRO 지원 (5.19+) */

/* 드라이버에서 HW-GRO 결과를 커널에 전달하는 방법 */
/* NIC가 병합한 패킷을 수신하면, 드라이버는:
 * 1. skb->len에 병합된 총 크기 설정
 * 2. skb_shinfo(skb)->gso_size에 원래 MSS 설정
 * 3. skb_shinfo(skb)->gso_type에 SKB_GSO_TCPV4 등 설정
 * 4. napi_gro_receive()가 아닌 일반 경로로 전달 가능
 *    (이미 병합됨, 커널 GRO 불필요)
 */

프로토콜별 GRO 콜백

/* GRO 콜백 등록 구조 */
struct net_offload {
    struct offload_callbacks callbacks;
};

struct offload_callbacks {
    struct sk_buff *(*gro_receive)(struct list_head *head,
                                    struct sk_buff *skb);
    int            (*gro_complete)(struct sk_buff *skb, int nhoff);
};

/* 프로토콜별 GRO 콜백 체인 (L2 → L3 → L4):
 *
 * Ethernet:
 *   → eth_gro_receive()
 *     → inet_gro_receive() (IPv4) 또는 ipv6_gro_receive()
 *       → tcp4_gro_receive() 또는 udp4_gro_receive()
 *
 * VXLAN 터널:
 *   → eth_gro_receive()
 *     → inet_gro_receive()
 *       → udp4_gro_receive()
 *         → vxlan_gro_receive()      ← 터널 디캡슐화
 *           → eth_gro_receive()      ← 내부 패킷 재귀 처리
 *             → inet_gro_receive()
 *               → tcp4_gro_receive()
 */

/* UDP GRO 콜백 (4.18+ UDP GSO와 짝) */
static struct sk_buff *udp4_gro_receive(struct list_head *head,
                                         struct sk_buff *skb)
{
    /* UDP는 TCP와 달리 시퀀스 번호가 없으므로
     * 병합 기준이 다름:
     *   - 동일 src/dst IP + port
     *   - 동일 데이터그램 크기 (마지막 제외)
     *   - 소켓이 UDP_GRO 옵션을 설정했어야 함
     */

    /* 터널 프로토콜 콜백이 등록되어 있으면 터널 GRO */
    struct udp_sock *up = udp_sk(sk);
    if (up->encap_type && up->gro_receive)
        return call_gro_receive(up->gro_receive, head, skb);

    /* 일반 UDP GRO */
    return udp_gro_receive(head, skb);
}

드라이버에서의 GRO 사용 패턴

/* 전형적인 NAPI poll 함수에서의 GRO 사용 */
static int my_napi_poll(struct napi_struct *napi, int budget)
{
    struct my_adapter *adapter = container_of(napi, struct my_adapter, napi);
    int work_done = 0;

    while (work_done < budget) {
        struct my_rx_desc *desc = my_get_rx_desc(adapter);
        struct sk_buff *skb;

        if (!desc)
            break;

        skb = my_build_skb(adapter, desc);
        if (!skb) {
            adapter->stats.alloc_fail++;
            break;
        }

        /* 필수 skb 메타데이터 설정 — GRO 정확도에 영향 */
        skb->protocol = eth_type_trans(skb, adapter->netdev);

        /* 체크섬 오프로드 상태: GRO 효율에 직접 영향
         * CHECKSUM_UNNECESSARY → GRO가 체크섬 재검증 생략 → 빠른 병합 */
        if (desc->rx_status & RX_CSUM_OK)
            skb->ip_summed = CHECKSUM_UNNECESSARY;

        /* RX 해시: GRO가 동일 flow를 빠르게 찾는 데 사용 */
        skb_set_hash(skb, desc->rss_hash, PKT_HASH_TYPE_L4);

        /* VLAN 태그 (있으면) */
        if (desc->rx_status & RX_VLAN_STRIPPED)
            __vlan_hwaccel_put_tag(skb, htons(ETH_P_8021Q), desc->vlan_tci);

        /* GRO 진입 — 여기서 패킷 병합 시도 */
        napi_gro_receive(napi, skb);
        work_done++;
    }

    if (work_done < budget) {
        /* 모든 수신 패킷 처리 완료 → IRQ 재활성화 */
        if (napi_complete_done(napi, work_done))
            my_enable_irq(adapter);
        /* napi_complete_done() 내부에서 gro_list flush 수행 */
    }

    return work_done;
}

UDP GSO (USO — UDP Segmentation Offload)

커널 4.18에서 도입된 UDP GSO(흔히 USO라 부름)는 TCP GSO와 동일한 "지연 분할" 전략을 UDP에 적용합니다. QUIC, WireGuard, DNS-over-HTTPS, 게임 서버 등 대량 UDP 전송에서 획기적인 성능 향상을 제공합니다.

UDP GSO (USO) 분할 과정 sendmsg() + UDP_SEGMENT cmsg: gso_size=1472 (64KB 버퍼) udp_sendmsg() 1개 super-skb 생성 (64KB) skb_shared_info 설정 gso_size = 1472 (사용자 지정 세그먼트 크기) gso_type = SKB_GSO_UDP_L4 gso_segs = DIV_ROUND_UP(64000, 1472) = 44 IP → Netfilter → TC → qdisc 1개 skb로 스택 통과 (44배 절약) validate_xmit_skb() NETIF_F_GSO_UDP_L4 ✓ NETIF_F_GSO_UDP_L4 ✗ HW USO (NIC가 분할) NIC가 각 세그먼트에 UDP 헤더 복사 → 각 데이터그램: [IP][UDP hdr][1472B payload] SW GSO (__udp_gso_segment) 커널이 44개 독립 UDP 데이터그램으로 분할 각각 자체 UDP 헤더 + IP 헤더 + 체크섬 Wire: 44 × [IP][UDP][1472B] 개별 전송

UDP GSO 사용법 (사용자 공간 API)

/* 사용자 공간에서 UDP GSO 사용 — sendmsg() + UDP_SEGMENT ancillary data */

#include <netinet/udp.h>

int send_udp_gso(int fd, const void *buf, size_t len, uint16_t gso_size)
{
    struct msghdr msg = {};
    struct iovec iov = { .iov_base = (void *)buf, .iov_len = len };
    char control[CMSG_SPACE(sizeof(uint16_t))];

    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = control;
    msg.msg_controllen = sizeof(control);

    /* UDP_SEGMENT cmsg로 세그먼트 크기 지정 */
    struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
    cm->cmsg_level = SOL_UDP;
    cm->cmsg_type = UDP_SEGMENT;        /* 커널 4.18+ */
    cm->cmsg_len = CMSG_LEN(sizeof(uint16_t));
    *((uint16_t *)CMSG_DATA(cm)) = gso_size;

    /* 64KB 버퍼를 한 번의 sendmsg()로 전송
     * → 커널이 내부적으로 gso_size 단위 분할
     * → sendmsg() 시스템콜 1회로 44개 UDP 데이터그램 전송 */
    return sendmsg(fd, &msg, 0);
}

/* 사용 예시 */
char buf[65536];
send_udp_gso(sockfd, buf, sizeof(buf), 1472);
/* → 커널이 65536 / 1472 = 44+1개 UDP 데이터그램으로 분할 전송
 *   마지막 세그먼트: 65536 - 1472*44 = 768 바이트 (짧을 수 있음)
 */

/* 수신 측: UDP GRO로 병합된 패킷을 효율적으로 수신 */
int val = 1;
setsockopt(sockfd, SOL_UDP, UDP_GRO, &val, sizeof(val));
/* → 커널이 GRO로 병합된 대형 UDP 메시지를 한 번에 전달
 * → recvmsg()의 GRO_UDP ancillary data로 원본 세그먼트 크기 확인 가능
 */

UDP GSO 커널 내부

/* net/ipv4/udp_offload.c — __udp_gso_segment() 핵심 로직 */
struct sk_buff *__udp_gso_segment(struct sk_buff *gso_skb,
                                   netdev_features_t features,
                                   bool is_ipv6)
{
    unsigned int mss = skb_shinfo(gso_skb)->gso_size;
    struct sk_buff *segs, *seg;
    struct udphdr *uh;

    /* skb_segment()로 mss 단위 분할 수행 */
    segs = skb_segment(gso_skb, features & ~NETIF_F_SG, false);
    if (IS_ERR(segs))
        return segs;

    /* 각 세그먼트에 독립 UDP 헤더 설정 */
    seg = segs;
    do {
        uh = udp_hdr(seg);
        /* UDP length = 세그먼트 데이터 크기 + UDP 헤더(8) */
        uh->len = htons(sizeof(*uh) + seg->len - skb_transport_offset(seg)
                        - sizeof(*uh));

        /* 마지막 세그먼트가 아니면 동일 UDP src/dst 포트 유지
         * (TCP와 달리 시퀀스 번호 증가 없음 — 각 세그먼트가 독립 데이터그램) */

        /* 체크섬 재계산 */
        if (seg->ip_summed == CHECKSUM_PARTIAL)
            udp_set_csum(!0, seg, ...);

    } while ((seg = seg->next));

    return segs;
}

/* UDP GSO vs TCP GSO 핵심 차이 */
/*
 * TCP GSO:
 *   - 시퀀스 번호 증가: seq += gso_size (각 세그먼트)
 *   - PSH/FIN 마지막에만, CWR 첫 세그먼트에만
 *   - 수신 측 TCP가 자동 재조립 (시퀀스 기반)
 *
 * UDP GSO:
 *   - 시퀀스 번호 없음 → 각 세그먼트가 완전 독립 데이터그램
 *   - IP ID는 증가 (IPv4)
 *   - 수신 측 재조립은 애플리케이션 책임
 *   - UDP_GRO 소켓 옵션으로 GRO 병합 시 원본 크기 보존
 *
 * UDP GSO가 유용한 워크로드:
 *   - QUIC (자체 시퀀스/신뢰성)
 *   - WireGuard (고정 크기 패킷 대량 전송)
 *   - DNS-over-HTTPS 서버 (다수 응답 일괄 전송)
 *   - 미디어 스트리밍 (RTP/UDP 대량 전송)
 *   - 게임 서버 (다수 클라이언트에 상태 업데이트)
 */

UDP GSO 성능 영향

시나리오sendmsg() 호출 수시스템콜 오버헤드스택 통과 횟수처리량 (10Gbps NIC)
개별 UDP 전송 (1472B × 44)44회44 × syscall44회~3.5 Gbps
sendmmsg() 사용1회 (배치)1 × syscall44회~5.2 Gbps
UDP GSO (64KB)1회1 × syscall1회~9.2 Gbps
UDP GSO + HW USO1회1 × syscall1회~9.8 Gbps (CPU 최소)
QUIC과 UDP GSO: QUIC 프로토콜(HTTP/3)은 UDP 기반이므로 UDP GSO의 최대 수혜자입니다. Google의 QUICHE, Cloudflare의 quiche, Meta의 mvfst 등 주요 QUIC 구현체는 모두 UDP GSO를 활용합니다. gso_size를 QUIC 패킷 크기(보통 1200~1350B)로 설정하면, 단일 sendmsg()로 수십 개의 QUIC 패킷을 일괄 전송할 수 있습니다.

BIG TCP (대형 세그먼트, 커널 6.3+)

전통적으로 GSO/GRO의 최대 크기는 64KB(IP 헤더의 Total Length 필드가 16비트)로 제한되었습니다. BIG TCP는 IPv6 Jumbogram 확장 헤더를 활용하여 이 제한을 우회, 최대 512KB까지의 super-skb를 허용합니다.

BIG TCP: 64KB → 512KB 세그먼트 기존: 최대 64KB (IPv4/IPv6) 1 super-skb = 64KB (MSS×44, MTU=1500 기준) IPv4: Total Length = 16bit → 최대 65535B IPv6: Payload Length = 16bit → 최대 65535B → 100Gbps 환경에서 per-skb 오버헤드 여전히 병목 BIG TCP: 최대 512KB (IPv6 only) 1 super-skb = 185~512KB (MSS×350, MTU=1500) IPv6 Jumbogram (RFC 2675): Payload = 32bit Hop-by-Hop Options → Jumbo Payload 확장 헤더 → 100Gbps에서 per-skb 오버헤드 8배 감소 100Gbps NIC 포워딩 성능 비교 GSO/GRO OFF: ~15 Gbps GSO/GRO 64KB: ~62 Gbps BIG TCP 185KB: ~88 Gbps BIG TCP 512KB: ~97 Gbps

BIG TCP 설정과 구조

/* BIG TCP 활성화 (커널 6.3+, IPv6 전용) */

# GSO/GRO 최대 크기를 64KB 이상으로 확장
# ip link set dev eth0 gso_max_size 185000
# ip link set dev eth0 gro_max_size 185000
#
# 더 큰 값도 가능 (NIC 지원 시):
# ip link set dev eth0 gso_max_size 524280
# ip link set dev eth0 gro_max_size 524280
#
# 확인:
# ip -d link show eth0 | grep -E 'gso_max|gro_max'
#   gso_max_size 185000 gso_max_segs 65535
#   gro_max_size 185000

/* BIG TCP 조건과 제약사항 */
/*
 * 1. IPv6 전용: IPv4는 IP 헤더 total_length가 16비트라 64KB 제한 불변
 *    → IPv6 Jumbogram 확장 헤더로 32비트 payload length 사용
 *
 * 2. NIC 지원 필요: TSO/GRO 하드웨어가 64KB 이상을 처리할 수 있어야 함
 *    → Intel E810, Mellanox CX-5/6/7, Broadcom BCM5750X 등 지원
 *    → ethtool -k eth0에서 TSO max size 확인
 *
 * 3. Jumbogram은 로컬 스택에서만 사용:
 *    → 와이어(wire)에는 여전히 MSS 단위 세그먼트가 전송됨
 *    → Jumbogram 헤더는 GRO 병합/GSO 분할 내부에서만 존재
 *    → 네트워크 장비(라우터/스위치)에 영향 없음
 *
 * 4. 커널 내부 변경점:
 *    → skb->len이 u32 (4GB까지) → 문제 없음
 *    → skb_shared_info->gso_size는 u16 (65535) → 문제 없음 (MSS 단위)
 *    → net_device->gso_max_size가 unsigned int로 확장 (6.3+)
 */

/* 커널 내부: BIG TCP skb 생성 */
static int tcp_sendmsg_locked(struct sock *sk, ...)
{
    /* size_goal 계산에서 BIG TCP 반영 */
    int mss_now = tcp_send_mss(sk, &size_goal, flags);
    /* BIG TCP 활성 시:
     *   sk->sk_gso_max_size = 185000 (또는 더 큰 값)
     *   size_goal = min(sk->sk_gso_max_size, 65535 * mss_now / mss_now)
     *            = 185000 (IPv6)
     *   → 하나의 skb에 185KB 데이터 적재
     *   → gso_segs = 185000 / 1460 = 126개
     */
}

/* IPv6 Jumbogram 처리 */
/* tcp_gso_segment()에서 BIG TCP skb 분할 시:
 * - payload_length > 65535 감지
 * - Hop-by-Hop Jumbo Payload 옵션 헤더 추가
 * - 각 세그먼트는 일반 크기 (MSS)이므로 Jumbogram 불필요
 * - 수신 GRO 측: 일반 패킷을 병합하면 자동으로 BIG TCP skb 생성
 */
BIG TCP 주의사항:
  • IPv4 미지원: IPv4는 total_length 16비트 한계로 BIG TCP 불가. IPv6 전용.
  • tcpdump: GRO 시 185KB+ 패킷이 캡처될 수 있음. MTU 초과처럼 보이지만 정상.
  • iptables/nftables: conntrack이 대형 skb를 처리하므로 패킷 카운터가 크게 다를 수 있음.
  • MTU 경로: BIG TCP는 로컬 스택 내부만 영향. 와이어에는 MSS 단위 전송.

GRO 내부 자료구조 심화

GRO의 핵심은 napi_struct 내부의 해시 테이블과 NAPI_GRO_CB 제어 블록입니다. 이 자료구조가 O(1)에 가까운 flow 매칭을 가능하게 합니다.

GRO 해시 테이블 (napi->gro_hash) napi_struct gro_hash[8] (버킷 8개) gro_bitmask (활성 비트맵) gro_count (전체 skb 수) gro_flush_timeout [0] count=2 skb: flow A (TCP) merged=12 pkts, 17KB skb: flow B (TCP) merged=8 pkts, 11KB [1] count=0 (빈 버킷) [2] count=1 skb: flow C (UDP) merged=3 pkts, 4KB [3]~[7] ... NAPI_GRO_CB(skb) — skb->cb[] 내 GRO 제어 블록 napi_gro_cb 구조체 frag0: 첫 frag 포인터 (fast path) frag0_len: 첫 frag 길이 data_offset: L4 페이로드 시작 flush: 병합 불가 플래그 flush_id: IP ID 기반 flush count: 병합된 패킷 수 same_flow: 동일 flow 매칭 last: frag_list 마지막 포인터 GRO 매칭 알고리즘 1. rxhash → 버킷 인덱스 계산 2. 버킷의 skb 리스트 순회 3. same_flow=1 초기 설정 4. L3 콜백: IP 주소/TOS/TTL 비교 5. L4 콜백: 포트/seq/윈도우 비교 6. same_flow 유지 → 병합 7. 모두 실패 → GRO_HELD/NORMAL GRO 성능 특성 버킷 수: 8 (GRO_HASH_BUCKETS) 버킷당 최대: 8 (MAX_GRO_SKBS) 전체 최대: 64 활성 flow 해시 충돌: 같은 버킷 선형 검색 평균 검색: O(1)~O(8) 캐시 친화적: skb->cb 활용 (별도 할당 없음)

GRO 해시 테이블 구조

/* include/linux/netdevice.h — GRO 해시 테이블 */
#define GRO_HASH_BUCKETS  8    /* 해시 버킷 수 */

struct gro_list {
    struct list_head list;    /* 이 버킷의 skb 리스트 */
    int              count;   /* 리스트의 skb 수 (최대 MAX_GRO_SKBS=8) */
};

struct napi_struct {
    ...
    struct gro_list gro_hash[GRO_HASH_BUCKETS];
    unsigned long  gro_bitmask;  /* 비어있지 않은 버킷의 비트맵
                                    * → 빈 버킷은 건너뛰어 flush 최적화 */
    struct list_head rx_list;    /* GRO 완료 후 상위 전달 대기 리스트 */
    int            rx_count;    /* rx_list의 skb 수 */
    ...
};

/* 해시 버킷 선택: skb의 rxhash를 버킷 수로 모듈러 */
static inline unsigned int gro_hash_bucket(struct sk_buff *skb)
{
    /* skb->hash: NIC의 RSS(Receive Side Scaling) 또는 SW 해시
     * → 동일 flow의 패킷은 동일 해시 → 동일 버킷
     * → 해시 품질이 GRO 효율에 직결 */
    return skb_get_hash_raw(skb) & (GRO_HASH_BUCKETS - 1);
}

/* NAPI_GRO_CB: skb->cb[] 공간을 GRO 제어 블록으로 활용 */
struct napi_gro_cb {
    /* frag0 최적화: 첫 번째 frag의 가상 주소를 캐시
     * → 비선형 skb에서 헤더 접근 시 kmap 비용 절감 */
    void     *frag0;
    unsigned int frag0_len;

    /* 데이터 오프셋: L4 페이로드 시작 위치
     * → 프로토콜 콜백이 이전 계층의 헤더 길이를 전달 */
    int      data_offset;

    /* 병합 제어 플래그 */
    u16      flush;         /* !0 → 이 skb를 즉시 flush */
    u16      flush_id;      /* IP ID 불연속 시 flush 카운터 */
    u16      count;         /* 이 skb에 병합된 패킷 수 */

    /* flow 매칭 */
    u8       same_flow:1;   /* 1: 동일 flow로 판정 (프로토콜 콜백이 설정) */
    u8       encap_mark:1;  /* 터널 캡슐화 표시 */
    u8       csum_valid:1;  /* 체크섬 검증 완료 */
    u8       csum_cnt;      /* 체크섬 불필요 카운트 (터널 중첩) */
    u8       is_flist;      /* frag_list 기반 GRO */

    /* frag_list 병합용 */
    struct sk_buff *last;  /* frag_list의 마지막 skb */

    /* 재귀적 GRO (터널) */
    int      recursion_counter;  /* 터널 중첩 깊이 (최대 3) */
    int      network_offset;    /* 네트워크 헤더 오프셋 */
    int      inner_network_offset; /* 내부 네트워크 헤더 오프셋 */
};

/* sizeof(napi_gro_cb) ≤ 48 (skb->cb[] = 48 바이트)
 * → 별도 메모리 할당 없이 skb 내부 공간 활용
 * → 캐시 라인 효율 극대화 */

GRO Flush 상세 경로

/* GRO flush: gro_list의 병합된 skb를 상위 스택으로 전달 */

/* 1. napi_gro_flush() — 전체 버킷 flush */
void napi_gro_flush(struct napi_struct *napi, bool flush_old)
{
    unsigned long bitmask = napi->gro_bitmask;
    unsigned int i;

    /* bitmask로 비어있지 않은 버킷만 순회 — 불필요한 탐색 제거 */
    while ((i = __ffs(bitmask)) < GRO_HASH_BUCKETS) {
        struct list_head *head = &napi->gro_hash[i].list;
        struct sk_buff *skb, *p;

        list_for_each_entry_safe(skb, p, head, list) {
            if (flush_old && NAPI_GRO_CB(skb)->age == jiffies)
                continue;  /* 이번 NAPI 사이클에서 추가된 건 유지 */

            /* flush: 프로토콜별 gro_complete 콜백 호출 후 상위 전달 */
            gro_flush_oldest(napi, head);
        }

        bitmask &= ~(1UL << i);
    }
}

/* 2. gro_normal_list() — rx_list 일괄 전달 */
static void gro_normal_list(struct napi_struct *napi)
{
    if (!napi->rx_count)
        return;

    /* rx_list의 모든 skb를 한꺼번에 상위 스택으로 전달
     * → netif_receive_skb_list_internal() — 리스트 기반 최적화
     * → 개별 netif_receive_skb() 대비 RCU/lock 오버헤드 감소 */
    netif_receive_skb_list_internal(&napi->rx_list);

    INIT_LIST_HEAD(&napi->rx_list);
    napi->rx_count = 0;
}

/* 3. gro_complete 콜백 체인 */
/*
 * flush 시 호출되는 프로토콜별 gro_complete:
 *
 * tcp4_gro_complete():
 *   - TCP 헤더의 ACK number 최종 설정
 *   - TCP 윈도우 업데이트
 *   - skb_shinfo->gso_type = SKB_GSO_TCPV4 설정
 *   - skb_shinfo->gso_segs = 병합된 패킷 수
 *   → GSO로 다시 분할할 때 필요한 정보 보존
 *
 * inet_gro_complete():
 *   - IP 헤더의 total_length 업데이트 (병합 후 총 크기)
 *   - IP ID 범위 기록
 *
 * 최종적으로 netif_receive_skb()로 전달:
 *   → ip_rcv() → tcp_v4_rcv() → sk_receive_queue
 *   → 애플리케이션의 recv() 한 번으로 64KB+ 수신 가능
 */

가상화 환경의 GSO/GRO

가상화, 컨테이너, SDN 환경에서 GSO/GRO는 가상 인터페이스 간 패킷 전달 최적화의 핵심입니다. virtio-net, veth, macvtap, TAP 등 가상 디바이스는 하드웨어 오프로드가 없으므로 소프트웨어 GSO/GRO에 전적으로 의존합니다.

가상화 환경 GSO/GRO 경로 VM / Container (Guest) App: send(64KB) Guest TCP Stack GSO skb 생성 (64KB) virtio-net TX vring Container: send(64KB) GSO skb (64KB) veth TX GSO skb 그대로 전달 (분할 없이!) Host Kernel vhost-net (KVM) 또는 QEMU virtio-net TAP device TUN_VNET_HDR 지원 GSO skb 수신 (분할 안 됨!) VIRTIO_NET_F_MRG_RXBUF: 큰 skb 그대로 전달 Linux Bridge / OVS / tc 1개 대형 skb로 처리 (매우 효율적) veth peer RX GRO 병합 Physical NIC TX HW TSO 또는 SW GSO XDP와 GSO/GRO 상호작용 XDP_PASS: GRO 이전 동작 → GRO 적용됨 XDP_TX/REDIRECT: GRO 우회 → 개별 패킷 xdp_frame은 GSO 미지원 → 분할된 상태로 처리

virtio-net GSO/GRO 연동

/* virtio-net의 GSO 오프로드 핵심: vnet_hdr */

/* Guest → Host: GSO skb를 분할하지 않고 그대로 전달 */
struct virtio_net_hdr_v1 {
    __u8  flags;           /* VIRTIO_NET_HDR_F_NEEDS_CSUM 등 */
    __u8  gso_type;         /* VIRTIO_NET_HDR_GSO_TCPV4/V6/UDP */
    __le16 hdr_len;         /* 모든 헤더의 총 길이 */
    __le16 gso_size;        /* MSS (분할 단위) — skb_shinfo->gso_size에 매핑 */
    __le16 csum_start;      /* 체크섬 시작 오프셋 */
    __le16 csum_offset;     /* 체크섬 필드 오프셋 */
    __le16 num_buffers;     /* MRG_RXBUF: 사용된 버퍼 수 */
};

/* Guest TX: GSO skb → vnet_hdr 변환 */
/*
 * 1. Guest의 TCP 스택이 64KB GSO skb 생성
 * 2. virtio-net 드라이버가 vnet_hdr에 GSO 정보 기록
 * 3. skb 데이터를 분할하지 않고 vring으로 전달
 * 4. Host의 vhost-net 또는 QEMU가 수신:
 *    a. vnet_hdr의 gso_type/gso_size를 읽어
 *    b. Host skb의 skb_shinfo에 GSO 정보 복원
 *    c. Host 네트워크 스택은 이 skb를 GSO skb로 인식
 *    d. Host → 물리 NIC 전달 시: HW TSO 또는 SW GSO로 분할
 *
 * → Guest-Host 간 패킷이 분할 없이 전달 → 매우 효율적
 */

/* VIRTIO_NET_F_MRG_RXBUF (Mergeable Receive Buffers) */
/*
 * Host → Guest 방향에서 GRO 효율을 극대화:
 * - 여러 vring 버퍼를 하나의 큰 패킷으로 병합
 * - num_buffers 필드로 사용된 버퍼 수 표시
 * - Guest가 64KB skb를 받을 때:
 *   → 여러 4KB vring 버퍼가 하나의 skb로 조립
 *   → skb_shinfo->frags[]에 각 버퍼 페이지 매핑
 *   → GRO가 병합한 대형 패킷을 Guest가 효율적으로 수신
 *
 * MRG_RXBUF 없으면:
 *   → 각 vring 버퍼가 별도 skb → 64KB 패킷을 위해 44개 skb
 *   → Guest의 수신 성능 크게 저하
 */

/* VIRTIO_NET_F_GUEST_TSO4/TSO6/UFO */
/*
 * Host가 Guest에 GSO skb를 직접 전달할 수 있음:
 * - Host의 GRO로 병합된 패킷 → 분할 없이 Guest에 전달
 * - Guest의 네트워크 스택이 GSO skb로 인식
 * - Guest가 다른 NIC로 포워딩 시: Guest의 GSO가 분할
 *
 * 이 feature 없으면:
 * - Host가 반드시 세그먼트 단위로 분할 후 전달
 * - 성능 대폭 저하 (특히 포워딩 시)
 */

veth (Container) GSO/GRO

/* veth: 컨테이너 네트워킹의 핵심 가상 인터페이스 */

/* veth의 GSO 처리가 특별한 이유:
 * - veth는 peer 디바이스와 1:1 연결
 * - TX 측의 GSO skb를 분할하지 않고 RX peer로 직접 전달
 * - peer의 GRO에서 재병합할 필요 없음 (이미 대형 skb)
 *
 * drivers/net/veth.c */
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);

    /* GSO skb를 분할하지 않고 peer로 직접 전달!
     * → 64KB skb가 그대로 peer의 수신 경로에 진입
     * → peer 측에서 netif_rx() 또는 napi_gro_receive() */

    if (likely(veth_forward_skb(rcv, skb, priv, rq, false) == NET_RX_SUCCESS))
        return NETDEV_TX_OK;

    ...
}

/* veth features: 사실상 모든 오프로드를 "지원"
 * → 실제 HW가 아니므로 SW GSO fallback이지만,
 *   peer 전달 시 분할이 발생하지 않으므로 무관 */
/*
 * veth가 광고하는 features:
 *   NETIF_F_SG            → Scatter-Gather (항상)
 *   NETIF_F_HW_CSUM       → 체크섬 (소프트웨어로 처리)
 *   NETIF_F_TSO            → TCP GSO
 *   NETIF_F_TSO6           → TCP GSO IPv6
 *   NETIF_F_GSO_UDP_L4     → UDP GSO
 *   NETIF_F_GSO_UDP_TUNNEL → 터널 GSO
 *   ...
 *
 * 이 features 덕분에:
 *   Container → veth TX: GSO skb 그대로 전달 (분할 안 함)
 *   veth peer RX → Bridge/OVS: 대형 skb로 전달
 *   Bridge → Physical NIC TX: NIC의 실제 feature에 맞춰 분할
 *
 * 성능 영향:
 *   veth + GSO ON:  ~40 Gbps (Container 간)
 *   veth + GSO OFF: ~8 Gbps (44배 더 많은 skb 처리)
 */

XDP와 GSO/GRO 상호작용

/* XDP (eXpress Data Path)와 GSO/GRO의 관계 */

/* XDP는 GRO 이전에 동작:
 *
 * NIC IRQ → NAPI poll
 *   → [XDP 프로그램 실행] ← GRO보다 먼저!
 *     → XDP_PASS: GRO로 진입 (napi_gro_receive)
 *     → XDP_DROP: 즉시 드롭 (GRO 도달 안 함)
 *     → XDP_TX: 같은 NIC로 반송 (GRO 우회)
 *     → XDP_REDIRECT: 다른 NIC/AF_XDP로 전달 (GRO 우회)
 *
 * 핵심: XDP_TX/REDIRECT는 개별 패킷 단위로 동작
 * → GRO 병합 이전이므로 각 패킷이 독립적
 * → 대량 트래픽에서 per-packet XDP는 CPU 집약적
 */

/* XDP에서 GSO skb를 처리할 수 없는 이유 */
/*
 * xdp_frame/xdp_buff는 단일 선형 버퍼만 지원:
 *   - frags[] 미지원 (XDP multi-buffer는 5.18+에서 부분 지원)
 *   - skb_shared_info의 gso_size/gso_type 없음
 *   - 따라서 GSO super-skb를 XDP로 처리 불가
 *
 * GRO → XDP 경로 시도 시:
 *   - GRO가 병합한 대형 skb는 XDP_PASS 후 상위 스택으로
 *   - XDP generic 모드: skb 기반이므로 GSO skb 처리 가능하나
 *     → 성능 이점이 사라짐 (native XDP 대비 10배+ 느림)
 *
 * 해결책: XDP에서 GRO와 유사한 기능이 필요하면
 *   → AF_XDP + 사용자 공간 병합 (DPDK 스타일)
 *   → eBPF TC (GRO 이후 동작, GSO skb 처리 가능)
 */

/* eBPF TC classifier와 GSO/GRO */
/*
 * TC eBPF는 GRO 이후, GSO 이전에 동작:
 *   GRO → IP → [TC ingress eBPF] → routing → [TC egress eBPF] → GSO → NIC
 *
 * 따라서 TC eBPF에서:
 *   - 수신: GRO로 병합된 대형 skb를 처리 (효율적)
 *   - 송신: GSO 분할 전 대형 skb를 처리 (효율적)
 *   - skb->len이 64KB+일 수 있음 → 주의
 *   - bpf_skb_change_tail() 등 크기 변경 시 GSO 무효화 가능
 */

IPsec ESP GSO

IPsec ESP(Encapsulating Security Payload) 환경에서 GSO는 특별한 도전 과제를 제기합니다. 암호화는 패킷 단위로 수행해야 하지만, GSO는 분할을 최대한 지연시키기 때문입니다.

IPsec ESP GSO 처리 흐름 ESP 오프로드 없음 (기존 방식) GSO skb 64KB GSO 분할 (먼저!) → 44개 개별 skb ESP 암호화 × 44회 AES-GCM 44번 호출 44개 패킷 전송 느림 ESP GSO 오프로드 (NETIF_F_GSO_ESP, 커널 4.13+) GSO skb 64KB xfrm output (ESP) 1회 ESP 헤더 추가 GSO 분할 (늦게) ESP 포함 상태로 분할 NIC HW ESP + TSO NIC가 암호화+분할 빠름 NIC 지원 시: 암호화+분할을 하드웨어에서 일괄 처리 지원 NIC: Intel E810, Mellanox CX-6+, Marvell LiquidIO 확인: ethtool -k eth0 | grep esp
/* IPsec ESP GSO (커널 4.13+, xfrm offload) */

/* ESP GSO의 핵심 문제:
 * - ESP는 패킷 단위로 암호화/인증 (IV, SN, ICV가 패킷별 고유)
 * - GSO는 분할을 최대한 지연
 * → 충돌: 대형 skb 상태에서 어떻게 패킷별 ESP 처리?
 *
 * 해결책 1: SW ESP + GSO 분할 선행 (기존 방식)
 *   → GSO 먼저 분할 → 44개 패킷에 각각 ESP 적용
 *   → 스택 효율 이점 상실
 *
 * 해결책 2: ESP GSO offload (NETIF_F_GSO_ESP)
 *   → 대형 skb에 ESP 메타데이터만 설정
 *   → validate_xmit_skb()에서 분할 + ESP 동시 처리
 *   → 또는 NIC HW가 암호화+분할 일괄 처리 (inline crypto)
 */

/* xfrm offload 설정 확인 */
/*
 * # ip xfrm state list
 *   ...
 *   offload dev eth0 dir out   ← NIC 오프로드 활성
 *
 * # ethtool -k eth0 | grep esp
 *   esp-hw-offload: on
 *   tx-esp-segmentation: on     ← ESP GSO 지원
 *
 * 설정:
 * # ip xfrm state add ... offload dev eth0 dir out
 */

/* ESP GSO 분할 시 각 세그먼트 처리 */
/*
 * esp4_gso_segment() / esp6_gso_segment():
 * 1. 원본 대형 skb를 MSS 단위로 분할
 * 2. 각 세그먼트에:
 *    - 고유 ESP SPI 유지 (동일 SA)
 *    - 고유 Sequence Number 할당 (증가)
 *    - 고유 IV (Initialization Vector) 생성
 *    - ESP 트레일러 (Padding + Next Header + ICV) 추가
 *    - AES-GCM/CBC 등으로 암호화
 *    - ICV(Integrity Check Value) 계산
 * 3. 각 세그먼트가 독립적인 ESP 패킷으로 완성
 *
 * HW offload 시:
 *   → 위 과정을 NIC 하드웨어가 수행
 *   → CPU는 SA 메타데이터만 DMA 디스크립터에 기록
 *   → 100Gbps IPsec에서 필수
 */

TCP Autocorking과 GSO

TCP autocorking은 GSO 효율을 극대화하는 보조 메커니즘입니다. 작은 write()가 연속될 때 자동으로 데이터를 모아 하나의 큰 GSO skb를 만듭니다.

/* TCP Autocorking (커널 3.14+, 기본 활성) */

/* 문제 상황:
 * 애플리케이션이 작은 write()를 반복:
 *   write(fd, buf1, 100);   // 100B
 *   write(fd, buf2, 200);   // 200B
 *   write(fd, buf3, 150);   // 150B
 *
 * Autocorking 없으면:
 *   → 각 write()마다 별도 skb → 3개 작은 패킷 전송
 *   → GSO 효과 없음, Nagle과 다름 (Nagle은 ACK 대기)
 *
 * Autocorking 있으면:
 *   → 이미 전송 큐에 미전송 skb가 있고, 아직 ACK 안 왔으면
 *   → 새 데이터를 기존 skb에 append
 *   → 최종적으로 하나의 큰 skb로 GSO 전송
 */

/* net/ipv4/tcp.c — tcp_sendmsg_locked()에서의 autocorking 판단 */
static bool tcp_should_autocork(struct sock *sk,
                                struct sk_buff *skb,
                                int size_goal)
{
    /* autocorking 조건:
     * 1. net.ipv4.tcp_autocorking = 1 (sysctl, 기본 on)
     * 2. 전송 큐에 이미 미전송 데이터가 있음 (sk->sk_wmem_queued > 0)
     * 3. 마지막 전송 후 아직 ACK를 받지 못함
     * 4. 현재 skb에 충분한 공간이 남아있음 (< size_goal)
     */
    return sysctl_tcp_autocorking &&
           skb != tcp_send_head(sk) &&
           refcount_read(&sk->sk_wmem_alloc) > SKB_TRUESIZE(1) &&
           tcp_packets_in_flight(tcp_sk(sk)) >= tcp_sk(sk)->snd_cwnd;
}

/* Autocorking ↔ GSO 시너지 */
/*
 * Autocorking이 데이터를 모으는 동안:
 *   → 하나의 skb에 여러 write() 데이터가 축적
 *   → skb->len이 size_goal(= MSS × max_segs)에 도달하면 전송
 *   → 결과: 최대 64KB GSO skb로 전송
 *
 * Autocorking이 없으면:
 *   → 각 write()가 즉시 tcp_push() → 작은 skb 다수
 *   → GSO 효과 미미 (skb당 데이터가 적음)
 *
 * sysctl 제어:
 *   # sysctl net.ipv4.tcp_autocorking
 *   net.ipv4.tcp_autocorking = 1       (기본)
 *
 * TCP_CORK 소켓 옵션과의 차이:
 *   TCP_CORK: 명시적 — setsockopt()로 수동 제어
 *   Autocorking: 자동 — 전송 상태를 보고 커널이 결정
 *   → 대부분의 경우 autocorking으로 충분
 *   → 정밀 제어가 필요하면 TCP_CORK 또는 TCP_NODELAY
 *
 * 모니터링:
 *   # nstat -az | grep AutoCorking
 *   TcpExtTCPAutoCorking   123456    # autocorking 발동 횟수
 */

SCTP GSO

SCTP(Stream Control Transmission Protocol)는 TCP와 달리 청크(chunk) 기반 프로토콜이므로 GSO 분할 방식이 다릅니다:

/* SCTP GSO (커널 4.14+, SKB_GSO_SCTP) */

/* SCTP vs TCP 분할 차이:
 *
 * TCP GSO:
 *   [IP][TCP hdr, seq=1000][Payload 64KB]
 *   → [IP][TCP, seq=1000][1460B] + [IP][TCP, seq=2460][1460B] + ...
 *   시퀀스 번호 기반 분할
 *
 * SCTP GSO:
 *   [IP][SCTP Common Hdr][DATA chunk 1][DATA chunk 2]...[DATA chunk N]
 *   → [IP][SCTP Hdr][DATA chunk 1] + [IP][SCTP Hdr][DATA chunk 2] + ...
 *   청크 단위 분할 (각 청크가 자체 TSN 보유)
 */

/* net/sctp/offload.c — SCTP GSO 분할 */
static struct sk_buff *sctp_gso_segment(struct sk_buff *skb,
                                        netdev_features_t features)
{
    /* SCTP 분할 특이점:
     * 1. 각 DATA 청크가 이미 독립적 (자체 TSN, Stream ID)
     * 2. SCTP 공통 헤더의 CRC32c 체크섬을 각 세그먼트에서 재계산
     * 3. IP 헤더의 total_length 업데이트
     *
     * gso_size = 하나의 DATA 청크 크기
     * → skb_segment()로 청크 경계에서 분할
     */

    struct sk_buff *segs = skb_segment(skb, features, false);
    struct sk_buff *seg;
    struct sctphdr *sh;

    /* 각 세그먼트의 SCTP CRC32c 재계산 */
    skb_list_walk_safe(segs, seg, tmp) {
        sh = sctp_hdr(seg);
        sh->checksum = sctp_compute_cksum(seg, skb_transport_offset(seg));
    }

    return segs;
}

/* SCTP GSO의 체크섬 특이점:
 * - SCTP는 TCP/UDP와 달리 CRC32c 체크섬 사용
 * - pseudo-header 없음 (IP 주소 변경에 무관)
 * - CHECKSUM_PARTIAL로 NIC에 위임 불가 (대부분 NIC가 CRC32c 미지원)
 * - 따라서 항상 소프트웨어에서 CRC32c 계산
 * - NETIF_F_SCTP_CRC: CRC32c HW 오프로드 (일부 NIC, Intel X710+)
 *
 * # ethtool -k eth0 | grep sctp
 * tx-sctp-segmentation: on        # SCTP GSO
 * tx-checksum-sctp: on            # SCTP CRC32c HW offload
 */

GSO ↔ GRO 상호작용

포워딩 환경에서 GRO로 병합된 skb가 다시 GSO로 분할되는 경우가 빈번합니다. 이 "GRO → forward → GSO" 파이프라인이 네트워크 장비(라우터, 브리지, 로드밸런서)의 성능을 결정합니다:

NIC RX 43 × 1500B pkts GRO → 1 × 64KB skb IP Forward / Netfilter / TC 1개 skb만 처리 (per-pkt 비용 1/43) conntrack, NAT 등 1회만 수행 GSO → 43개 세그먼트 NIC TX 43 × 1500B wire 포워딩 경로: 스택 처리 비용 1/43로 감소 conntrack 1회, routing lookup 1회, Netfilter 규칙 매칭 1회 → 10Gbps+ 라우터/방화벽에서 필수 최적화
/* 포워딩 시 GRO → GSO 동작 */
/*
 * 1. 수신 NIC에서 GRO가 43개 패킷을 1개 대형 skb로 병합
 * 2. ip_forward()에서 라우팅 결정 (1회)
 * 3. Netfilter (conntrack, NAT 등) 통과 (1회)
 * 4. 송신 NIC로 전달:
 *    - 송신 NIC가 TSO 지원 → 대형 skb 그대로 전달
 *    - 송신 NIC가 TSO 미지원 → GSO가 소프트웨어 분할
 *
 * 포워딩 효율:
 *   GRO/GSO OFF: 43 × {routing + conntrack + NAT + filter} = 43 × CPU cycles
 *   GRO/GSO ON:  1 × {routing + conntrack + NAT + filter} = 1 × CPU cycles
 *   → 약 43배 효율 향상 (64KB / 1500 MTU 기준)
 */

/* GRO로 병합된 skb가 다시 분할될 때의 GSO 타입 */
/* GRO 병합 시 gso_size와 gso_type이 자동 설정되므로
 * 포워딩 경로의 GSO가 원본 패킷과 동일하게 분할 가능 */
if (NAPI_GRO_CB(p)->count > 1) {
    skb_shinfo(p)->gso_size = skb_gro_len(skb);  /* 원본 세그먼트 크기 */
    skb_shinfo(p)->gso_type |= SKB_GSO_TCPV4;
}

성능 튜닝과 모니터링

ethtool 오프로드 제어

/* GSO/GRO 관련 ethtool 명령어 */

/* 현재 오프로드 상태 확인 */
# ethtool -k eth0 | grep -E 'offload|gso|gro|tso'
tcp-segmentation-offload: on          # TSO (HW)
generic-segmentation-offload: on      # GSO (SW fallback)
generic-receive-offload: on           # GRO (SW)
rx-gro-hw: on [requested on]          # HW-GRO (5.19+)
udp-segmentation-offload: off         # USO (4.18+)
tx-udp_tnl-segmentation: on           # 터널 TSO
tx-udp_tnl-csum-segmentation: on      # 터널 TSO + csum
large-receive-offload: off [requested off]  # LRO (deprecated)

/* 개별 오프로드 제어 */
# ethtool -K eth0 gso on|off        # GSO 전환
# ethtool -K eth0 gro on|off        # GRO 전환
# ethtool -K eth0 tso on|off        # TSO 전환
# ethtool -K eth0 rx-gro-hw on|off  # HW-GRO 전환
# ethtool -K eth0 lro on|off        # LRO 전환 (비권장)

/* GSO/GRO 최대 크기 확인 및 설정 */
# ip -d link show eth0 | grep gso
#   gso_max_size 65536 gso_max_segs 65535
#   gro_max_size 65536
#
# BIG TCP 활성화 (IPv6, 6.3+):
# ip link set dev eth0 gso_max_size 185000
# ip link set dev eth0 gro_max_size 185000

GRO 관련 sysctl 튜닝

/* GRO 성능에 영향을 주는 sysctl 파라미터 */

/* 1. gro_flush_timeout — GRO 패킷 보관 타임아웃 (나노초)
 *    기본값: 0 (napi_complete 시 즉시 flush)
 *    설정 시: 타이머 만료까지 더 많은 패킷 병합 시도
 *    → 처리량 증가, 하지만 지연 시간도 증가 */
# sysctl -w net.core.gro_flush_timeout=20000     # 20μs

/* 2. napi_defer_hard_irqs — 빈 poll 허용 횟수
 *    기본값: 0 (즉시 IRQ 재활성화)
 *    설정 시: N번 빈 poll 후에야 IRQ 재활성화
 *    → gro_flush_timeout과 함께 사용하면 busy-poll 모드 */
# sysctl -w net.core.napi_defer_hard_irqs=2       # 2번 빈 poll 허용

/* 3. 권장 조합 (10Gbps+ 고처리량 환경):
 *    gro_flush_timeout=20000 + napi_defer_hard_irqs=2
 *    → IRQ 없이 busy-poll로 패킷 수신 → GRO 병합 극대화
 *    → 단, CPU 사용률 약간 증가 (IRQ coalescence와 트레이드오프)
 *
 * 4. 지연 민감 환경 (금융, 게임):
 *    gro_flush_timeout=0 + napi_defer_hard_irqs=0
 *    → 최소 지연, 하지만 GRO 병합 기회 감소
 *    → 극단적 경우 GRO 자체를 비활성화 고려 */

/* 5. busy_poll / busy_read — 소켓 레벨 busy polling
 *    → epoll/poll에서 커널이 먼저 NAPI poll 시도 (IRQ 대기 없이)
 *    → GRO와 결합하면 ultra-low latency + 높은 처리량 */
# sysctl -w net.core.busy_poll=50           # 50μs busy poll
# sysctl -w net.core.busy_read=50           # 50μs busy read

모니터링과 통계

/* GSO/GRO 동작 상태 확인 */

/* 1. NIC 통계 — GRO 병합 횟수 */
# ethtool -S eth0 | grep -i gro
#   rx_gro_packets: 1234567        # GRO로 병합된 패킷 수
#   rx_gro_bytes: 98765432000      # GRO로 병합된 바이트 수
#   rx_gro_dropped: 0              # GRO 중 드롭

/* 2. /proc/net/dev — 일반 인터페이스 통계 */
# cat /proc/net/dev
# → GRO 활성 시 RX packets가 줄고 bytes는 동일
# → 패킷당 평균 크기가 크면 GRO가 잘 동작하는 것

/* 3. nstat — 프로토콜별 통계 */
# nstat -az | grep -i gro
#   TcpExtTCPAutoCorking    123456  # TCP autocorking (GSO 관련)

/* 4. perf로 GSO/GRO 함수 프로파일링 */
# perf top -g -e cycles -- -K
#   → skb_gso_segment, tcp_gso_segment, dev_gro_receive 등의 CPU 비중 확인
#   → GRO/GSO 오버헤드가 높으면 HW 오프로드 확인

/* 5. ftrace로 GSO 분할 추적 */
# echo 1 > /sys/kernel/debug/tracing/events/net/net_dev_xmit/enable
# cat /sys/kernel/debug/tracing/trace_pipe
#   → skb->len 변화로 GSO 분할 여부 확인

/* 6. GRO 효율 지표 계산 */
/* GRO ratio = (NIC rx_packets) / (netif_receive_skb 호출 수)
 * → 비율이 높을수록 GRO가 효과적으로 동작
 * → TCP 워크로드에서 일반적으로 40~60:1 (64KB / 1500 MTU)
 */

GSO/GRO 주의사항

GSO/GRO가 문제를 일으키는 경우:
  1. 패킷 캡처(tcpdump) — GRO가 병합한 대형 패킷이 캡처되어 MTU 초과로 보일 수 있음. 디버깅 시 ethtool -K eth0 gro off로 비활성화하거나 tcpdump가 자동 처리
  2. Netfilter conntrack — GRO skb가 conntrack에서 분할 없이 하나로 추적되므로, 패킷 카운터가 실제보다 적게 표시됨
  3. TC(Traffic Control) — GSO skb는 분할 전이므로 큐 깊이가 실제보다 적게 보임. tc -s qdisc 출력 해석 시 주의
  4. MTU 변경 — GSO/GRO 활성 상태에서 MTU 변경 시 기존 skb의 gso_size와 불일치 가능. 트래픽 중단 후 변경 권장
  5. IPsec — ESP 암호화 후 GSO 분할 필요. NETIF_F_GSO_ESP 미지원 NIC에서 성능 저하. xfrm offload 확인 필요
  6. Bridge/OVS — 포워딩 경로에서 GRO로 병합된 skb가 재분할 없이 브리지되면, 수신 측에서 예상치 못한 대형 프레임을 받을 수 있음
GSO/GRO 디버깅 팁:
  • 성능 문제 발생 시 ethtool -K eth0 gro off; ethtool -K eth0 tso off로 오프로드를 순차 비활성화하여 원인 분리
  • ss -ti로 TCP 소켓별 MSS, cwnd를 확인하여 GSO 세그먼트 크기 추정
  • ip -s link show eth0에서 TX/RX 패킷 수 대비 바이트 수 비율로 GSO/GRO 효과 확인
  • 가상화 환경에서는 virtio-net의 VIRTIO_NET_F_MRG_RXBUF 플래그가 GRO에 직접 영향

실전 벤치마크와 성능 분석

GSO/GRO의 효과를 정량적으로 확인하는 벤치마크 방법과 결과입니다. 환경에 따라 수치가 달라지므로 자체 측정이 필수입니다.

GSO/GRO 벤치마크: TCP 단일 스트림 (10Gbps NIC) 10 8 6 4 2 0 Gbps 2.8 9.7 TCP 처리량 4.5 9.4 IP 포워딩 6.5 9.5 UDP GSO 82% 28% CPU 사용률 815K 19K PPS (낮을수록) GSO/GRO OFF GSO/GRO ON (PPS: 스택 통과 패킷 수 — 낮을수록 GSO/GRO가 효과적으로 병합)

벤치마크 측정 방법

/* === GSO/GRO 성능 벤치마크 절차 === */

/* 1. 기준점 설정: 오프로드 비활성화 */
# ethtool -K eth0 gso off gro off tso off
# ethtool -K eth0 rx-gro-hw off 2>/dev/null

/* 2. TCP 처리량 측정 (iperf3) */
# 서버: iperf3 -s
# 클라이언트: iperf3 -c SERVER_IP -t 30 -P 1
#   → Bandwidth: 2.8 Gbps (GSO/GRO OFF)
#
# 오프로드 활성화 후 재측정:
# ethtool -K eth0 gso on gro on tso on
# iperf3 -c SERVER_IP -t 30 -P 1
#   → Bandwidth: 9.7 Gbps (GSO/GRO ON)

/* 3. CPU 사용률 동시 측정 */
# mpstat -P ALL 1 30
#   GSO/GRO OFF: CPU 82% (softirq + ksoftirqd 병목)
#   GSO/GRO ON:  CPU 28% (대부분 유휴)

/* 4. 포워딩 성능 (netperf) */
# Host A → Router → Host B
# Router에서:
# sysctl net.ipv4.ip_forward=1
# netperf -H HOST_B -t TCP_STREAM -l 30 -- -m 65536
#   GRO/GSO OFF: 4.5 Gbps (conntrack per-packet 병목)
#   GRO/GSO ON:  9.4 Gbps (conntrack 1/44 실행)

/* 5. UDP GSO 벤치마크 */
# 서버: iperf3 -s
# 클라이언트: iperf3 -c SERVER_IP -u -b 10G -l 64000 --udp-gso
#   UDP 개별: 6.5 Gbps
#   UDP GSO:  9.5 Gbps

/* 6. GRO 효율 확인 */
# 전송 중 패킷 통계 비교:
# watch -n1 'cat /proc/net/dev | grep eth0'
#   GRO OFF: RX packets ~815,000/sec (815K PPS)
#   GRO ON:  RX packets ~19,000/sec (19K PPS, 각 ~64KB)
#   → GRO 비율: 815K / 19K ≈ 43:1 (거의 이론치)

/* 7. perf로 핫스팟 분석 */
# perf record -g -a -- sleep 10
# perf report
#   GSO/GRO OFF 핫스팟:
#     15.2%  tcp_v4_rcv
#     12.8%  ip_rcv
#      8.5%  nf_conntrack_in
#      7.2%  __netif_receive_skb_core
#   GSO/GRO ON 핫스팟:
#     22.1%  copy_to_user (데이터 복사가 주 병목)
#      8.3%  tcp_recvmsg
#      5.1%  skb_gso_segment (GSO 분할)

시나리오별 최적 설정

환경GSOGROTSOHW-GROgro_flush_timeoutnapi_defer_hard_irqs비고
웹 서버 (HTTP/HTTPS)ONONONON00기본 설정으로 충분
10Gbps+ 고처리량ONONONON200002GRO 병합 극대화
100Gbps IPv6ONONONON200002BIG TCP 185KB+
라우터/방화벽ONONONON00포워딩 효율 핵심
지연 민감 (금융)ONONONOFF00busy_poll=50 추가
극저지연 (HFT)OFFOFFOFFOFF00커널 우회(DPDK) 권장
가상화 (KVM)ONONONON00virtio MRG_RXBUF 필수
컨테이너 (K8s)ONONONON00veth GSO 자동 활성
IPsec VPNONONONON00xfrm offload + ESP GSO
패킷 캡처/분석ONOFFONOFF00GRO OFF로 원본 패킷 확인

장애 사례와 디버깅

실전 트러블슈팅 사례

/* === 사례 1: tcpdump에서 MTU 초과 패킷이 보이는 문제 ===
 *
 * 증상:
 *   # tcpdump -i eth0 -n
 *   10.0.0.1 > 10.0.0.2: TCP, length 65160  ← MTU(1500)보다 훨씬 큼!
 *
 * 원인:
 *   tcpdump는 GRO 이후의 skb를 캡처
 *   → GRO가 병합한 대형 skb가 그대로 pcap에 기록
 *   → 정상 동작 (문제 아님)
 *
 * 확인:
 *   # ethtool -K eth0 gro off
 *   # tcpdump -i eth0 -n
 *   → 이제 MSS 단위 패킷만 보임 (1460B 등)
 *   # ethtool -K eth0 gro on   ← 측정 후 복원
 *
 * 또는 AF_PACKET 레벨에서 제어:
 *   tcpdump --immediate-mode   ← 일부 환경에서 도움
 */

/* === 사례 2: GRO 활성 상태에서 패킷 드롭 ===
 *
 * 증상:
 *   # netstat -s | grep "packets dropped"
 *   큰 수의 드롭, GRO OFF 시 감소
 *
 * 원인:
 *   GRO가 64KB skb를 생성 → 소켓 수신 버퍼(sk_rcvbuf) 빠르게 소진
 *   → sk_rmem_alloc이 sk_rcvbuf 초과 → 드롭
 *
 * 해결:
 *   # sysctl -w net.core.rmem_max=16777216
 *   # sysctl -w net.core.rmem_default=8388608
 *   또는 애플리케이션에서:
 *   setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
 */

/* === 사례 3: 터널(VXLAN) 환경에서 성능 저하 ===
 *
 * 증상:
 *   VXLAN 터널 throughput이 물리 NIC의 50% 수준
 *
 * 원인:
 *   NIC가 터널 GSO를 지원하지 않아 모든 패킷이 SW GSO 분할
 *   + 외부 UDP 체크섬 SW 계산
 *
 * 진단:
 *   # ethtool -k eth0 | grep tunnel
 *   tx-udp_tnl-segmentation: off [requested on]  ← HW 미지원!
 *   tx-udp_tnl-csum-segmentation: off
 *
 * 해결:
 *   a. 터널 NIC 교체 (Intel E810, Mellanox CX-5+ 등)
 *   b. 외부 UDP csum 비활성화:
 *      # ip link set vxlan0 type vxlan udpcsum off
 *      → RFC 7348에서 허용, 약 10~20% 성능 개선
 *   c. GSO_PARTIAL 활용:
 *      → NIC가 외부 헤더만 처리, 내부는 SW 분할
 */

/* === 사례 4: 가상화 환경에서 Guest 네트워크 느림 ===
 *
 * 증상:
 *   KVM Guest에서 iperf3 결과가 2Gbps 미만
 *
 * 진단:
 *   # (Guest) ethtool -k eth0
 *   generic-segmentation-offload: off   ← GSO 비활성!
 *   generic-receive-offload: off
 *
 * 원인:
 *   virtio-net feature negotiation 실패
 *   또는 QEMU 옵션에서 오프로드 비활성
 *
 * 해결:
 *   1. QEMU 옵션 확인:
 *      -device virtio-net-pci,host_tso4=on,guest_tso4=on,
 *              host_ufo=on,guest_ufo=on,mrg_rxbuf=on
 *   2. Guest 내 활성화:
 *      # ethtool -K eth0 gso on gro on tso on
 *   3. vhost-net 사용 확인 (QEMU 프로세스 내 처리보다 빠름)
 */

/* === 사례 5: GSO skb가 Netfilter에서 문제 ===
 *
 * 증상:
 *   iptables -m length --length 0:1500 매칭이 동작 안 함
 *   (GSO skb는 64KB이므로 매칭 실패)
 *
 * 원인:
 *   GSO skb가 분할 전에 Netfilter를 통과
 *   → skb->len이 64KB → length 매칭이 MSS 단위가 아님
 *
 * 해결:
 *   a. -m connbytes 사용 (바이트 기반)
 *   b. 또는 NFQUEUE + GSO 분할 후 처리
 *   c. nftables에서는 meta l4proto + payload 매칭 권장
 */

/* === 사례 6: MTU 변경 후 연결 끊김 ===
 *
 * 증상:
 *   ip link set eth0 mtu 9000 후 기존 TCP 연결 끊김
 *
 * 원인:
 *   기존 연결의 MSS가 이전 MTU 기준
 *   → GSO gso_size가 이전 MSS 기준으로 설정됨
 *   → MTU 변경 후 gso_size와 실제 MTU 불일치
 *
 * 해결:
 *   a. MTU 변경 전 기존 연결 종료
 *   b. 또는 TCP_MAXSEG 소켓 옵션으로 MSS 수동 조정
 *   c. 점진적 MTU 변경: 서비스 재시작 배포와 함께
 */

GSO/GRO 디버깅 도구 모음

/* === GSO/GRO 디버깅 도구 총정리 === */

/* 1. ethtool — 오프로드 상태 및 통계 */
# ethtool -k eth0                 # 오프로드 플래그
# ethtool -S eth0 | grep -i gro   # GRO 통계
# ethtool -S eth0 | grep -i tso   # TSO 통계
# ethtool --show-features eth0    # 전체 feature 목록

/* 2. ip 명령 — GSO/GRO 최대 크기 */
# ip -d link show eth0 | grep -E 'gso|gro'
# ip -s link show eth0            # TX/RX 통계

/* 3. ss — TCP 소켓 상태 */
# ss -ti state established
#   → mss:1460 cwnd:44 → GSO 가능 세그먼트 수 추정
#   → delivery_rate 102Mbps → 실제 전송률

/* 4. /proc/net/snmp — 프로토콜 카운터 */
# cat /proc/net/snmp | grep Tcp
#   InSegs: GRO 병합 후 카운트 (적을수록 GRO 효과적)
#   OutSegs: GSO 분할 전 카운트

/* 5. nstat — 세부 통계 */
# nstat -az | grep -i -E 'gro|cork|seg'
#   TcpExtTCPAutoCorking: autocorking 발동 횟수
#   TcpExtTCPOrigDataSent: 원본 데이터 세그먼트 수

/* 6. ftrace — GSO/GRO 함수 추적 */
# echo 'skb_gso_segment' > /sys/kernel/debug/tracing/set_ftrace_filter
# echo function > /sys/kernel/debug/tracing/current_tracer
# echo 1 > /sys/kernel/debug/tracing/tracing_on
# cat /sys/kernel/debug/tracing/trace_pipe
#   → GSO 분할 호출 빈도와 타이밍 확인

/* 7. perf probe — 동적 트레이스포인트 */
# perf probe --add 'dev_gro_receive skb->len'
# perf record -e probe:dev_gro_receive -a -- sleep 5
# perf script
#   → GRO 진입 시 skb 크기 분포 확인

/* 8. bpftrace — eBPF 기반 실시간 분석 */
# bpftrace -e '
  kprobe:napi_gro_receive {
    @gro_count = count();
    @gro_size = hist(((struct sk_buff *)arg1)->len);
  }
  interval:s:5 { print(@gro_size); clear(@gro_size); }
'
#   → 5초마다 GRO로 들어오는 skb 크기 히스토그램

/* 9. dropwatch — 패킷 드롭 추적 */
# dropwatch -l kas
#   → GSO/GRO 관련 드롭 지점 식별
#   → skb_gso_validate 실패, gro_max_size 초과 등

커널 버전별 GSO/GRO 진화

GSO/GRO 커널 진화 타임라인 2.6.18 (2006) GSO (Generic Segmentation Offload) 도입 — Herbert Xu TSO의 소프트웨어 일반화. tcp_gso_segment() 구현 2.6.29 (2009) GRO (Generic Receive Offload) 도입 — Herbert Xu LRO 대체. napi_gro_receive(), 프로토콜별 콜백 체인 3.14 (2014) TCP Autocorking 도입 — Eric Dumazet 작은 write()를 자동으로 모아 GSO 효율 극대화 4.13 (2017) IPsec ESP GSO (SKB_GSO_ESP) — Steffen Klassert xfrm offload + NIC inline crypto. IPsec 성능 혁신 4.14 (2017) SCTP GSO (SKB_GSO_SCTP) 도입 4.18 (2018) UDP GSO (SKB_GSO_UDP_L4) — Willem de Bruijn QUIC/WireGuard 성능 혁신. UDP_SEGMENT cmsg, UDP_GRO sockopt 5.19 (2022) HW-GRO (NETIF_F_GRO_HW) — NIC 하드웨어 GRO LRO 대비 헤더 보존. rx-gro-hw ethtool 플래그 6.3 (2023) BIG TCP — Eric Dumazet IPv6 64KB→512KB 세그먼트. 100Gbps+ 환경 필수. gso/gro_max_size 확장
커널 버전기능GSO 타입/Feature주요 저자핵심 변경
2.6.18GSO 프레임워크SKB_GSO_TCPV4Herbert Xutcp_gso_segment(), validate_xmit_skb()
2.6.18UFOSKB_GSO_UDPHerbert XuUDP Fragmentation Offload (IP 단편화)
2.6.29GRO 프레임워크NETIF_F_GROHerbert Xunapi_gro_receive(), 프로토콜 콜백 체인
3.7터널 GSOSKB_GSO_GRE/UDP_TUNNELPravin ShelarVXLAN/GRE 내부 패킷 GSO 지원
3.14TCP Autocorking-Eric Dumazet자동 데이터 취합으로 GSO 효율 향상
3.18GSO_PARTIALSKB_GSO_PARTIALAlexander Duyck터널 부분 오프로드 (외부 HW + 내부 SW)
4.13ESP GSOSKB_GSO_ESPSteffen KlassertIPsec xfrm offload, inline crypto
4.14SCTP GSOSKB_GSO_SCTPMarcelo R. LeitnerSCTP 청크 단위 GSO
4.18UDP GSOSKB_GSO_UDP_L4Willem de BruijnUDP L4 세그먼트 분할, UDP_SEGMENT cmsg
4.18UDP GROUDP_GRO sockoptEric DumazetUDP 수신 병합 (QUIC 지원)
5.1GRO 해시 테이블gro_hash[8]Li RongQinggro_list를 8-bucket 해시로 교체 → O(1) 검색
5.18XDP multi-buffer-Lorenzo BianconiXDP에서 비선형 패킷 부분 지원
5.19HW-GRONETIF_F_GRO_HWEric DumazetNIC 하드웨어 GRO (헤더 보존)
6.3BIG TCPgso/gro_max_size 확장Eric DumazetIPv6 64KB→512KB, Jumbogram 활용
6.3gso_ipv4_max_sizenet_device 필드Eric DumazetIPv4/IPv6 GSO 크기 독립 제어

커널 소스 맵

GSO/GRO 관련 핵심 커널 소스 파일 위치입니다. 커널 소스 탐색 시 참고하세요:

소스 파일핵심 함수/구조체역할
include/linux/skbuff.hskb_shared_info, skb_is_gso(), skb_gso_*GSO/GRO 관련 skb 헤더 정의
include/linux/netdevice.hnapi_struct, gro_list, NETIF_F_*NAPI GRO 구조체, NIC feature 플래그
include/net/gro.hnapi_gro_cb, GRO_HASH_BUCKETSGRO 제어 블록, 해시 상수
net/core/gro.cdev_gro_receive(), napi_gro_receive(), skb_gro_receive()GRO 핵심 엔진
net/core/skbuff.cskb_segment(), __skb_gso_segment()GSO 분할 핵심
net/core/dev.cvalidate_xmit_skb(), netif_skb_features()GSO 분할 결정 지점
net/ipv4/tcp_offload.ctcp4_gso_segment(), tcp4_gro_receive()TCP GSO/GRO 콜백
net/ipv6/tcpv6_offload.ctcp6_gso_segment(), tcp6_gro_receive()IPv6 TCP GSO/GRO
net/ipv4/udp_offload.c__udp_gso_segment(), udp4_gro_receive()UDP GSO/GRO 콜백
net/ipv4/af_inet.cinet_gro_receive(), inet_gro_complete()IPv4 GRO L3 콜백
net/ipv4/ip_output.cip_queue_xmit(), ip_finish_output_gso()IP 출력 경로 GSO 처리
net/ipv4/tcp.ctcp_sendmsg_locked(), tcp_should_autocork()TCP 전송 + autocorking
net/ipv4/tcp_output.ctcp_write_xmit(), tcp_set_skb_tso_segs()TCP 출력 + GSO 설정
net/xfrm/xfrm_device.cxfrm_dev_state_add()IPsec xfrm HW offload
net/ipv4/esp4_offload.cesp4_gso_segment()ESP GSO 분할
net/sctp/offload.csctp_gso_segment()SCTP GSO 분할
drivers/net/veth.cveth_xmit(), veth_poll()veth GSO 전달
drivers/net/virtio_net.cvirtnet_poll(), free_old_xmit()virtio-net GSO/GRO
drivers/net/vxlan/vxlan_core.cvxlan_xmit_skb(), vxlan_gro_receive()VXLAN 터널 GSO/GRO
소스 탐색 팁:
  • grep -rn 'gso_segment' net/ — 프로토콜별 GSO 분할 함수 찾기
  • grep -rn 'gro_receive' net/ — 프로토콜별 GRO 병합 함수 찾기
  • grep -rn 'NETIF_F_GSO' include/linux/netdev_features.h — GSO feature 비트 정의
  • git log --oneline --all -- net/core/gro.c — GRO 변경 이력 추적

GSO/GRO와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.