GSO/GRO와 네트워크 오프로드
Linux 커널 네트워크 오프로드 메커니즘: 체크섬 오프로드 플래그, GSO(Generic Segmentation Offload)/TSO/UFO, GRO(Generic Receive Offload) 병합 알고리즘, VXLAN/GRE 터널 GSO, HW-GRO, 성능 튜닝 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.
핵심 요약
- 체크섬 오프로드 — CHECKSUM_NONE/UNNECESSARY/PARTIAL/COMPLETE 플래그로 NIC에 체크섬 계산을 위임.
- GSO — 커널이 큰 세그먼트를 유지하다 NIC 직전에 분할. TSO 지원 NIC면 NIC가 분할.
- GRO — napi_gro_receive()에서 연속 패킷을 병합해 프로토콜 스택 호출 횟수 감소.
- 터널 처리 — VXLAN/GRE 등 터널에서 내부 헤더까지 고려한 GSO 분할 필요.
- HW-GRO — NIC가 하드웨어에서 GRO 수행, 드라이버가 gro_list로 전달.
단계별 이해
- 체크섬 오프로드 이해
sk_buff의 ip_summed 필드와 4가지 체크섬 플래그 의미를 먼저 파악합니다. - GSO 전송 흐름
dev_queue_xmit() → validate_xmit_skb() → gso_segment() 경로를 추적합니다. - GRO 수신 흐름
napi_gro_receive() → napi_skb_finish() → skb merge 경로를 확인합니다. - 성능 측정
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 설정 필요 |
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로 스택을 한 번만 통과합니다.
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_TCPV4 | 1 << 0 | IPv4 TCP | 가장 일반적인 TSO/GSO. MSS 단위로 TCP 세그먼트 분할 |
SKB_GSO_TCPV6 | 1 << 5 | IPv6 TCP | IPv6 환경의 TSO/GSO |
SKB_GSO_UDP | 1 << 1 | UDP (IP frag) | IP 단편화(fragmentation) 기반 UDP GSO. UFO(UDP Fragmentation Offload) |
SKB_GSO_UDP_L4 | 1 << 11 | UDP (L4 분할) | UDP 세그먼트 단위 분할 (커널 4.18+). QUIC, WireGuard 등에 사용 |
SKB_GSO_DODGY | 1 << 2 | 모두 | 신뢰할 수 없는 GSO skb (VM에서 전달 등). 재검증 필요 |
SKB_GSO_TCP_ECN | 1 << 3 | TCP + ECN | ECN(Explicit Congestion Notification) 플래그 있는 TCP GSO |
SKB_GSO_TCP_FIXEDID | 1 << 9 | TCP | 모든 세그먼트가 동일 IP ID 사용 (드문 경우) |
SKB_GSO_GRE | 1 << 6 | GRE 터널 | GRE 캡슐화 안의 내부 패킷 GSO |
SKB_GSO_GRE_CSUM | 1 << 7 | GRE + 체크섬 | GRE 체크섬이 활성화된 터널 GSO |
SKB_GSO_UDP_TUNNEL | 1 << 8 | VXLAN/Geneve | UDP 기반 터널 내부 패킷 GSO |
SKB_GSO_UDP_TUNNEL_CSUM | 1 << 10 | VXLAN + csum | 외부 UDP 체크섬이 활성화된 터널 GSO |
SKB_GSO_PARTIAL | 1 << 13 | 터널/복합 | 부분 GSO — 외부 헤더만 NIC가 처리, 내부는 소프트웨어 분할 |
SKB_GSO_TUNNEL_REMCSUM | 1 << 12 | 터널 | 터널 원격 체크섬 오프로드 |
SKB_GSO_SCTP | 1 << 14 | SCTP | SCTP 청크 단위 GSO |
SKB_GSO_ESP | 1 << 15 | IPsec ESP | ESP(Encapsulating Security Payload) GSO |
SKB_GSO_FRAGLIST | 1 << 17 | UDP/IP | frag_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 사용법 (사용자 공간 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 × syscall | 44회 | ~3.5 Gbps |
| sendmmsg() 사용 | 1회 (배치) | 1 × syscall | 44회 | ~5.2 Gbps |
| UDP GSO (64KB) | 1회 | 1 × syscall | 1회 | ~9.2 Gbps |
| UDP GSO + HW USO | 1회 | 1 × syscall | 1회 | ~9.8 Gbps (CPU 최소) |
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 설정과 구조
/* 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 생성
*/
- 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 해시 테이블 구조
/* 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에 전적으로 의존합니다.
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 (커널 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" 파이프라인이 네트워크 장비(라우터, 브리지, 로드밸런서)의 성능을 결정합니다:
/* 포워딩 시 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 주의사항
- 패킷 캡처(tcpdump) — GRO가 병합한 대형 패킷이 캡처되어 MTU 초과로 보일 수 있음. 디버깅 시
ethtool -K eth0 gro off로 비활성화하거나tcpdump가 자동 처리 - Netfilter conntrack — GRO skb가 conntrack에서 분할 없이 하나로 추적되므로, 패킷 카운터가 실제보다 적게 표시됨
- TC(Traffic Control) — GSO skb는 분할 전이므로 큐 깊이가 실제보다 적게 보임.
tc -s qdisc출력 해석 시 주의 - MTU 변경 — GSO/GRO 활성 상태에서 MTU 변경 시 기존 skb의 gso_size와 불일치 가능. 트래픽 중단 후 변경 권장
- IPsec — ESP 암호화 후 GSO 분할 필요.
NETIF_F_GSO_ESP미지원 NIC에서 성능 저하. xfrm offload 확인 필요 - Bridge/OVS — 포워딩 경로에서 GRO로 병합된 skb가 재분할 없이 브리지되면, 수신 측에서 예상치 못한 대형 프레임을 받을 수 있음
- 성능 문제 발생 시
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 성능 벤치마크 절차 === */
/* 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 분할)
시나리오별 최적 설정
| 환경 | GSO | GRO | TSO | HW-GRO | gro_flush_timeout | napi_defer_hard_irqs | 비고 |
|---|---|---|---|---|---|---|---|
| 웹 서버 (HTTP/HTTPS) | ON | ON | ON | ON | 0 | 0 | 기본 설정으로 충분 |
| 10Gbps+ 고처리량 | ON | ON | ON | ON | 20000 | 2 | GRO 병합 극대화 |
| 100Gbps IPv6 | ON | ON | ON | ON | 20000 | 2 | BIG TCP 185KB+ |
| 라우터/방화벽 | ON | ON | ON | ON | 0 | 0 | 포워딩 효율 핵심 |
| 지연 민감 (금융) | ON | ON | ON | OFF | 0 | 0 | busy_poll=50 추가 |
| 극저지연 (HFT) | OFF | OFF | OFF | OFF | 0 | 0 | 커널 우회(DPDK) 권장 |
| 가상화 (KVM) | ON | ON | ON | ON | 0 | 0 | virtio MRG_RXBUF 필수 |
| 컨테이너 (K8s) | ON | ON | ON | ON | 0 | 0 | veth GSO 자동 활성 |
| IPsec VPN | ON | ON | ON | ON | 0 | 0 | xfrm offload + ESP GSO |
| 패킷 캡처/분석 | ON | OFF | ON | OFF | 0 | 0 | GRO 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 타입/Feature | 주요 저자 | 핵심 변경 |
|---|---|---|---|---|
| 2.6.18 | GSO 프레임워크 | SKB_GSO_TCPV4 | Herbert Xu | tcp_gso_segment(), validate_xmit_skb() |
| 2.6.18 | UFO | SKB_GSO_UDP | Herbert Xu | UDP Fragmentation Offload (IP 단편화) |
| 2.6.29 | GRO 프레임워크 | NETIF_F_GRO | Herbert Xu | napi_gro_receive(), 프로토콜 콜백 체인 |
| 3.7 | 터널 GSO | SKB_GSO_GRE/UDP_TUNNEL | Pravin Shelar | VXLAN/GRE 내부 패킷 GSO 지원 |
| 3.14 | TCP Autocorking | - | Eric Dumazet | 자동 데이터 취합으로 GSO 효율 향상 |
| 3.18 | GSO_PARTIAL | SKB_GSO_PARTIAL | Alexander Duyck | 터널 부분 오프로드 (외부 HW + 내부 SW) |
| 4.13 | ESP GSO | SKB_GSO_ESP | Steffen Klassert | IPsec xfrm offload, inline crypto |
| 4.14 | SCTP GSO | SKB_GSO_SCTP | Marcelo R. Leitner | SCTP 청크 단위 GSO |
| 4.18 | UDP GSO | SKB_GSO_UDP_L4 | Willem de Bruijn | UDP L4 세그먼트 분할, UDP_SEGMENT cmsg |
| 4.18 | UDP GRO | UDP_GRO sockopt | Eric Dumazet | UDP 수신 병합 (QUIC 지원) |
| 5.1 | GRO 해시 테이블 | gro_hash[8] | Li RongQing | gro_list를 8-bucket 해시로 교체 → O(1) 검색 |
| 5.18 | XDP multi-buffer | - | Lorenzo Bianconi | XDP에서 비선형 패킷 부분 지원 |
| 5.19 | HW-GRO | NETIF_F_GRO_HW | Eric Dumazet | NIC 하드웨어 GRO (헤더 보존) |
| 6.3 | BIG TCP | gso/gro_max_size 확장 | Eric Dumazet | IPv6 64KB→512KB, Jumbogram 활용 |
| 6.3 | gso_ipv4_max_size | net_device 필드 | Eric Dumazet | IPv4/IPv6 GSO 크기 독립 제어 |
커널 소스 맵
GSO/GRO 관련 핵심 커널 소스 파일 위치입니다. 커널 소스 탐색 시 참고하세요:
| 소스 파일 | 핵심 함수/구조체 | 역할 |
|---|---|---|
include/linux/skbuff.h | skb_shared_info, skb_is_gso(), skb_gso_* | GSO/GRO 관련 skb 헤더 정의 |
include/linux/netdevice.h | napi_struct, gro_list, NETIF_F_* | NAPI GRO 구조체, NIC feature 플래그 |
include/net/gro.h | napi_gro_cb, GRO_HASH_BUCKETS | GRO 제어 블록, 해시 상수 |
net/core/gro.c | dev_gro_receive(), napi_gro_receive(), skb_gro_receive() | GRO 핵심 엔진 |
net/core/skbuff.c | skb_segment(), __skb_gso_segment() | GSO 분할 핵심 |
net/core/dev.c | validate_xmit_skb(), netif_skb_features() | GSO 분할 결정 지점 |
net/ipv4/tcp_offload.c | tcp4_gso_segment(), tcp4_gro_receive() | TCP GSO/GRO 콜백 |
net/ipv6/tcpv6_offload.c | tcp6_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.c | inet_gro_receive(), inet_gro_complete() | IPv4 GRO L3 콜백 |
net/ipv4/ip_output.c | ip_queue_xmit(), ip_finish_output_gso() | IP 출력 경로 GSO 처리 |
net/ipv4/tcp.c | tcp_sendmsg_locked(), tcp_should_autocork() | TCP 전송 + autocorking |
net/ipv4/tcp_output.c | tcp_write_xmit(), tcp_set_skb_tso_segs() | TCP 출력 + GSO 설정 |
net/xfrm/xfrm_device.c | xfrm_dev_state_add() | IPsec xfrm HW offload |
net/ipv4/esp4_offload.c | esp4_gso_segment() | ESP GSO 분할 |
net/sctp/offload.c | sctp_gso_segment() | SCTP GSO 분할 |
drivers/net/veth.c | veth_xmit(), veth_poll() | veth GSO 전달 |
drivers/net/virtio_net.c | virtnet_poll(), free_old_xmit() | virtio-net GSO/GRO |
drivers/net/vxlan/vxlan_core.c | vxlan_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와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.