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 설정 필요
/* 수신 경로: 드라이버에서 체크섬 상태 설정 */
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;
}

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와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.