sk_buff 자료구조 (Socket Buffer)
Linux 커널 네트워크 스택의 핵심 자료구조 sk_buff: 메모리 레이아웃, 데이터 조작 함수, clone/copy, 소켓(struct sock) 연동, 소켓 옵션, raw socket, 체크섬 오프로드, GSO/GRO, zero-copy 전송까지 종합 분석합니다.
개요
struct sk_buff(흔히 skb라 부름)는 Linux 네트워크 스택에서 모든 패킷을 표현하는 자료구조입니다. 수신 패킷이든 전송 패킷이든, L2 프레임이든 L4 세그먼트든, 커널 내에서 네트워크 데이터가 이동할 때는 항상 sk_buff가 사용됩니다.
O'Reilly의 "Understanding Linux Network Internals"에서 Christian Benvenuti는 sk_buff를 "네트워크 스택의 왕(king of networking)"이라 표현했습니다. 이 자료구조를 이해하지 않고는 Linux 네트워크 코드를 읽을 수 없습니다.
- 헤더 파일:
<linux/skbuff.h> - 주요 소스:
net/core/skbuff.c - 구조체 크기: x86_64에서 약 232바이트 (커널 6.x 기준)
struct sk_buff 주요 필드
/* include/linux/skbuff.h (주요 필드만 발췌) */
struct sk_buff {
union {
struct {
struct sk_buff *next; /* 리스트 내 다음 skb */
struct sk_buff *prev; /* 리스트 내 이전 skb */
};
struct rb_node rbnode; /* TCP retransmit queue용 */
};
struct sock *sk; /* 소속 소켓 */
struct net_device *dev; /* 수신/전송 네트워크 디바이스 */
unsigned int len; /* 전체 데이터 길이 (linear + frags) */
unsigned int data_len; /* 비선형(paged) 데이터 길이 */
__u16 mac_len; /* MAC 헤더 길이 */
__u16 hdr_len; /* 클론 시 writable 헤더 길이 */
__be16 protocol; /* 패킷 프로토콜 (ETH_P_IP 등) */
__u32 priority; /* QoS 우선순위 */
sk_buff_data_t transport_header; /* L4 헤더 오프셋 */
sk_buff_data_t network_header; /* L3 헤더 오프셋 */
sk_buff_data_t mac_header; /* L2 헤더 오프셋 */
sk_buff_data_t tail; /* 데이터 끝 */
sk_buff_data_t end; /* 할당된 버퍼 끝 */
unsigned char *head; /* 할당된 버퍼 시작 */
unsigned char *data; /* 실제 데이터 시작 */
unsigned int truesize; /* 실제 메모리 사용량 */
refcount_t users; /* 참조 카운트 */
};
자주 사용되는 추가 필드
위의 핵심 필드 외에도, sk_buff에는 프로토콜 처리와 성능 최적화에 쓰이는 중요한 필드들이 있습니다:
struct sk_buff {
/* ... 위의 핵심 필드들 ... */
char cb[48]; /* 프로토콜별 제어 블록 (Control Buffer) */
__u32 hash; /* 패킷 해시 (RSS, flow steering) */
__u8 pkt_type:3; /* PACKET_HOST, PACKET_BROADCAST 등 */
__u8 ip_summed:2; /* 체크섬 오프로드 상태 */
__u32 mark; /* netfilter/tc 마킹 (iptables -j MARK) */
__u16 queue_mapping; /* 멀티큐 NIC 큐 인덱스 */
unsigned int napi_id; /* NAPI 구조체 ID (busy polling) */
union {
__u32 tstamp; /* 수신 타임스탬프 */
u64 skb_mstamp_ns; /* 고해상도 타임스탬프 */
};
__u8 cloned:1; /* clone 여부 */
__u8 nohdr:1; /* 페이로드 참조만 (헤더 없음) */
__u8 peeked:1; /* MSG_PEEK으로 이미 확인됨 */
};
| 필드 | 크기 | 용도 | 접근 방법 |
|---|---|---|---|
cb[48] | 48바이트 | 프로토콜 레이어가 임시 데이터 저장 (TCP: tcp_skb_cb) | TCP_SKB_CB(skb), IPCB(skb) |
hash | 32비트 | 수신 패킷의 flow hash (RSS, RPS에 활용) | skb_get_hash(skb) |
pkt_type | 3비트 | 패킷 수신 유형: HOST, BROADCAST, MULTICAST, OTHERHOST | 직접 접근 |
ip_summed | 2비트 | 체크섬 오프로드 모드 (NONE/UNNECESSARY/COMPLETE/PARTIAL) | 직접 접근 |
mark | 32비트 | netfilter, tc, 라우팅 결정에 사용되는 패킷 마크 | 직접 접근 |
queue_mapping | 16비트 | 멀티큐 NIC에서 TX/RX 큐 선택 | skb_get_queue_mapping(skb) |
napi_id | 32비트 | NAPI 인스턴스 식별 (SO_BUSY_POLL 연동) | 직접 접근 |
cb[48]은 각 프로토콜 레이어가 자신만의 구조체로 캐스팅하여 사용합니다. 예를 들어 TCP는 TCP_SKB_CB(skb)로 struct tcp_skb_cb에 시퀀스 번호, 타임스탬프 등을 저장합니다. 한 레이어가 cb를 사용하면 다음 레이어에서 덮어쓰므로, 레이어 간 데이터 전달에는 사용하지 마세요.
skb 할당과 해제
sk_buff 할당 함수는 사용 상황에 따라 여러 변형이 있습니다:
| 함수 | 컨텍스트 | 특징 |
|---|---|---|
alloc_skb(size, gfp) | 일반 (프로세스/softirq) | 기본 할당 함수. kmalloc으로 linear 버퍼 할당 |
netdev_alloc_skb(dev, len) | NAPI/irq 수신 경로 | NET_SKB_PAD headroom 자동 확보, per-CPU 캐시 활용 |
napi_alloc_skb(napi, len) | NAPI poll 내부 | NAPI 전용 per-CPU 페이지 캐시, 최적 성능 |
build_skb(data, frag_size) | 사전 할당 버퍼 | 이미 할당된 버퍼에 skb 메타데이터만 생성 |
__alloc_skb(size, gfp, flags) | 내부 API | SKB_ALLOC_FCLONE, SKB_ALLOC_RX 등 플래그 지정 |
/* 일반적인 전송 경로 할당 */
struct sk_buff *skb = alloc_skb(MAX_HEADER + payload_len, GFP_KERNEL);
if (!skb)
return -ENOMEM;
skb_reserve(skb, MAX_HEADER); /* L2/L3/L4 헤더용 headroom */
/* NAPI 수신 경로 할당 (드라이버 내) */
struct sk_buff *skb = napi_alloc_skb(napi, 256); /* 헤더만 linear */
/* 페이로드는 page fragment로 추가 */
skb_add_rx_frag(skb, 0, page, offset, size, truesize);
/* build_skb: XDP, 고성능 드라이버에서 사용 */
void *buf = page_address(page);
struct sk_buff *skb = build_skb(buf, PAGE_SIZE);
if (!skb) {
put_page(page);
return;
}
skb_reserve(skb, headroom);
해제 함수도 상황에 따라 구분됩니다:
| 함수 | 용도 | tracepoint |
|---|---|---|
kfree_skb(skb) | 패킷 드롭 (에러/필터링) | skb:kfree_skb 발생 (원인 추적 가능) |
consume_skb(skb) | 정상적 소비 완료 | skb:consume_skb 발생 |
dev_kfree_skb_any(skb) | 드라이버 (irq/process 모두) | 컨텍스트에 따라 지연 해제 가능 |
dev_consume_skb_any(skb) | 드라이버 정상 소비 | irq-safe한 consume_skb |
kfree_skb_reason(skb, reason) | 드롭 원인 명시 (6.x+) | 드롭 원인을 enum으로 기록 |
kfree_skb()와 consume_skb()의 차이는 기능적으로 동일하지만, tracepoint가 다릅니다. 정상 경로에서 kfree_skb()를 사용하면 드롭 모니터링 도구가 오탐을 보고합니다. 반드시 의미에 맞는 함수를 사용하세요.
메모리 레이아웃
sk_buff의 데이터 영역은 4개의 핵심 포인터로 관리됩니다:
데이터 조작 함수
sk_buff의 데이터 영역을 조작하는 4대 함수:
| 함수 | 동작 | 용도 |
|---|---|---|
skb_reserve(skb, len) | data와 tail을 len만큼 뒤로 | 할당 직후 headroom 확보 |
skb_put(skb, len) | tail을 len만큼 뒤로 | 데이터 끝에 추가 (전송 시) |
skb_push(skb, len) | data를 len만큼 앞으로 | 헤더 추가 (L4→L3→L2) |
skb_pull(skb, len) | data를 len만큼 뒤로 | 헤더 제거 (수신 시 L2→L3→L4) |
/* 전형적인 전송 경로에서의 skb 조작 순서 */
struct sk_buff *skb = alloc_skb(1500 + headroom, GFP_KERNEL);
/* 1. headroom 확보 */
skb_reserve(skb, headroom); /* data, tail 이동 → headroom 공간 */
/* 2. 페이로드 추가 */
unsigned char *p = skb_put(skb, payload_len); /* tail 이동 */
memcpy(p, payload_data, payload_len);
/* 3. TCP 헤더 추가 */
struct tcphdr *th = skb_push(skb, sizeof(*th)); /* data 앞으로 이동 */
skb_reset_transport_header(skb);
/* 4. IP 헤더 추가 */
struct iphdr *ih = skb_push(skb, sizeof(*ih)); /* data 더 앞으로 */
skb_reset_network_header(skb);
/* 5. Ethernet 헤더 추가 */
struct ethhdr *eh = skb_push(skb, ETH_HLEN);
skb_reset_mac_header(skb);
Clone/Copy 메커니즘
여러 곳에서 같은 패킷을 참조해야 할 때 (예: 패킷 미러링, tcpdump 캡처, netfilter bridge), 세 가지 복사 전략을 선택할 수 있습니다:
/* clone: sk_buff 메타데이터만 복사, 데이터 버퍼는 공유 (refcount 증가) */
struct sk_buff *clone = skb_clone(skb, GFP_ATOMIC);
/* clone->data == skb->data (같은 버퍼 참조) */
/* skb_shared_info.dataref 증가됨 */
/* pskb_copy: linear 헤더만 복사, paged data는 page refcount 증가 */
struct sk_buff *pcopy = pskb_copy(skb, GFP_ATOMIC);
/* 헤더를 수정해야 하지만 페이로드는 그대로인 경우 최적 */
/* copy: 메타데이터 + linear + paged 데이터 모두 완전 복사 */
struct sk_buff *copy = skb_copy(skb, GFP_ATOMIC);
/* copy->data != skb->data (독립적 버퍼) */
/* skb_share_check: 공유 여부 확인 후 필요 시 clone */
skb = skb_share_check(skb, GFP_ATOMIC);
if (!skb)
return NET_RX_DROP;
/* 이제 skb를 독점적으로 소유 — 안전하게 메타데이터 수정 가능 */
선택 기준: 패킷을 읽기만 한다면 skb_clone(), 헤더만 수정해야 한다면 pskb_copy(), 페이로드까지 수정해야 한다면 skb_copy()를 사용하세요. Netfilter NAT는 pskb_copy()를 주로 사용합니다.
프래그먼트와 scatter-gather
대용량 패킷은 linear 데이터 외에 paged fragments를 사용합니다:
/* skb_shared_info: end 포인터 바로 뒤에 위치 */
struct skb_shared_info {
__u8 nr_frags; /* fragment 수 */
__u8 tx_flags;
unsigned short gso_size; /* GSO 세그먼트 크기 */
unsigned short gso_segs; /* GSO 세그먼트 수 */
unsigned short gso_type; /* GSO 타입 */
struct sk_buff *frag_list; /* 연결된 skb 리스트 */
skb_frag_t frags[MAX_SKB_FRAGS]; /* page fragment 배열 */
atomic_t dataref; /* 데이터 공유 참조 카운트 */
};
/* fragment 접근 */
struct skb_shared_info *si = skb_shinfo(skb);
for (int i = 0; i < si->nr_frags; i++) {
skb_frag_t *frag = &si->frags[i];
struct page *page = skb_frag_page(frag);
unsigned int offset = skb_frag_off(frag);
unsigned int size = skb_frag_size(frag);
}
고급 데이터 조작
패킷 처리 시 데이터 레이아웃을 변경해야 하는 상황이 자주 발생합니다. 아래 함수들은 각각 다른 상황에서 사용됩니다:
| 함수 | 동작 | 사용 시나리오 |
|---|---|---|
skb_linearize(skb) | 모든 paged fragment를 linear 영역으로 합침 | 레거시 드라이버, fragment 미지원 코드 |
pskb_may_pull(skb, len) | len 바이트까지 linear 영역에 확보 | 프로토콜 헤더 파싱 전 (필수 패턴) |
pskb_expand_head(skb, nhead, ntail, gfp) | headroom/tailroom 확장 (필요 시 버퍼 재할당) | encapsulation 헤더 추가 (tunnel, VLAN) |
skb_cow_head(skb, headroom) | 공유 skb의 헤더를 안전하게 쓰기 가능하게 | clone된 skb의 헤더 수정 전 |
skb_make_writable(skb, len) | len 바이트까지 쓰기 가능하게 (clone 해제+linearize) | netfilter에서 패킷 내용 수정 전 |
/* pskb_may_pull: 프로토콜 헤더 파싱의 필수 패턴 */
static int my_protocol_rcv(struct sk_buff *skb)
{
struct my_hdr *hdr;
/* linear 영역에 최소 헤더 크기만큼 확보 */
if (!pskb_may_pull(skb, sizeof(*hdr)))
goto drop;
hdr = (struct my_hdr *)skb_transport_header(skb);
/* 이제 hdr-> 필드에 안전하게 접근 가능 */
/* 가변 길이 헤더라면 두 번째 pull */
if (!pskb_may_pull(skb, hdr->hdr_len))
goto drop;
hdr = (struct my_hdr *)skb_transport_header(skb); /* 포인터 재취득! */
/* ... 처리 ... */
}
/* skb_cow_head: 터널 encapsulation 전 headroom 확보 */
static int my_tunnel_xmit(struct sk_buff *skb, struct net_device *dev)
{
int hdr_len = sizeof(struct iphdr) + sizeof(struct gre_hdr);
/* headroom이 부족하거나 skb가 공유 상태이면 재할당 */
if (skb_cow_head(skb, hdr_len + LL_RESERVED_SPACE(dev))) {
kfree_skb(skb);
return NETDEV_TX_OK;
}
/* 이제 안전하게 헤더 추가 가능 */
skb_push(skb, hdr_len);
/* ... GRE + IP 헤더 설정 ... */
}
/* skb_linearize: fragment가 있는 skb를 하나의 연속 버퍼로 */
if (skb_is_nonlinear(skb)) {
if (skb_linearize(skb))
goto drop; /* 메모리 부족 */
/* 이제 모든 데이터가 head~tail 사이에 연속으로 존재 */
}
pskb_may_pull() 호출 후에는 반드시 헤더 포인터를 재취득해야 합니다. 내부적으로 버퍼 재할당이 일어날 수 있어 이전 포인터가 무효화됩니다. 이 실수는 커널 네트워크 코드에서 가장 흔한 버그 패턴 중 하나입니다.
sk_buff 리스트 관리
/* sk_buff_head: 이중 연결 리스트의 head (sentinel) */
struct sk_buff_head {
struct sk_buff *next;
struct sk_buff *prev;
__u32 qlen; /* 큐 내 skb 수 */
spinlock_t lock; /* 동시성 보호 */
};
/* 초기화 */
struct sk_buff_head my_queue;
skb_queue_head_init(&my_queue);
/* 큐 조작 */
skb_queue_tail(&my_queue, skb); /* 큐 끝에 추가 */
skb_queue_head(&my_queue, skb); /* 큐 앞에 추가 */
struct sk_buff *s = skb_dequeue(&my_queue); /* 큐 앞에서 제거 */
skb_queue_purge(&my_queue); /* 전체 비우기 */
소켓과 sk_buff의 관계
sk_buff의 sk 필드는 해당 패킷이 소속된 소켓(struct sock)을 가리킵니다. 이 연결은 소켓 메모리 계산, 소켓 옵션 적용, 프로토콜별 처리의 기반이 됩니다.
struct sock 계층 구조
커널 소켓은 3단계 계층으로 구성됩니다:
/* 소켓 구조체 계층 (간략) */
struct socket { /* BSD 소켓 (사용자 공간 인터페이스) */
socket_state state; /* SS_UNCONNECTED, SS_CONNECTED 등 */
struct file *file; /* VFS file (fd와 연결) */
struct sock *sk; /* 네트워크 레이어 소켓 */
const struct proto_ops *ops; /* connect, sendmsg 등 */
};
struct sock { /* 프로토콜 무관 공통 소켓 */
struct sk_buff_head sk_receive_queue; /* 수신 skb 큐 */
struct sk_buff_head sk_write_queue; /* 전송 skb 큐 */
struct sk_buff_head sk_error_queue; /* 에러 큐 (ICMP 등) */
struct {
struct sk_buff *head, *tail;
} sk_backlog; /* backlog 큐 (lock 중 수신) */
atomic_t sk_rmem_alloc; /* 수신 큐 메모리 사용량 */
atomic_t sk_wmem_alloc; /* 전송 큐 메모리 사용량 */
int sk_rcvbuf; /* SO_RCVBUF 값 */
int sk_sndbuf; /* SO_SNDBUF 값 */
unsigned long sk_flags; /* 소켓 플래그 */
struct proto *sk_prot; /* 프로토콜 핸들러 */
void (*sk_data_ready)(struct sock *sk); /* 수신 알림 */
/* ... */
};
/* 프로토콜별 확장 (임베디드 패턴) */
struct inet_sock { /* IPv4/IPv6 공통 */
struct sock sk; /* 공통 sock 내장 */
__be32 inet_saddr; /* 소스 IP */
__be32 inet_daddr; /* 목적지 IP */
__be16 inet_sport; /* 소스 포트 */
__be16 inet_dport; /* 목적지 포트 */
__u8 tos; /* IP_TOS 옵션 */
__u8 min_ttl; /* IP_MINTTL 옵션 */
__s16 uc_ttl; /* IP_TTL 옵션 (-1 = 기본값) */
struct ip_options_rcu *inet_opt; /* IP 옵션 */
/* ... */
};
struct tcp_sock {
struct inet_connection_sock inet_conn; /* inet_sock ⊃ sock 내장 */
u32 snd_una; /* 가장 오래된 미확인 시퀀스 */
u32 snd_nxt; /* 다음 전송 시퀀스 */
u32 rcv_nxt; /* 다음 수신 기대 시퀀스 */
u32 mss_cache; /* MSS (최대 세그먼트 크기) */
/* ... 수십 개 TCP 전용 필드 ... */
};
struct udp_sock {
struct inet_sock inet;
int pending; /* cork 상태 */
__u8 encap_type; /* UDP encap (VXLAN 등) */
/* ... */
};
/* 캐스팅 매크로 */
#define inet_sk(sk) ((struct inet_sock *)(sk))
#define tcp_sk(sk) ((struct tcp_sock *)(sk))
#define udp_sk(sk) ((struct udp_sock *)(sk))
skb↔sk 바인딩과 소켓 메모리 관리
skb->sk가 설정되면 해당 skb의 메모리 사용량이 소켓에 과금됩니다. 이 메커니즘이 SO_RCVBUF/SO_SNDBUF 제한을 실현합니다:
/* skb를 소켓에 연결 — 메모리 과금 시작 */
static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
skb->sk = sk;
skb->destructor = sock_rfree; /* 해제 시 콜백 */
atomic_add(skb->truesize, &sk->sk_rmem_alloc);
/* → 수신 큐 메모리 사용량에 truesize만큼 추가 */
}
static inline void skb_set_owner_w(struct sk_buff *skb, struct sock *sk)
{
skb->sk = sk;
skb->destructor = sock_wfree; /* 해제 시 콜백 */
refcount_add(skb->truesize, &sk->sk_wmem_alloc);
/* → 전송 큐 메모리 사용량에 truesize만큼 추가 */
}
/* skb 해제 시: destructor 콜백이 메모리 차감 */
void sock_rfree(struct sk_buff *skb)
{
struct sock *sk = skb->sk;
atomic_sub(skb->truesize, &sk->sk_rmem_alloc);
/* → 수신 큐 메모리 사용량에서 차감 */
}
/* 수신 큐 과부하 확인 — 소켓 버퍼 제한 */
static inline bool sk_rmem_schedule(struct sock *sk, struct sk_buff *skb, int size)
{
/* sk_rmem_alloc + size > sk_rcvbuf 이면 false → 패킷 드롭 */
return __sk_mem_schedule(sk, size, SK_MEM_RECV);
}
| 필드/콜백 | 방향 | 역할 |
|---|---|---|
sk_rmem_alloc | RX | 수신 큐에 쌓인 skb의 총 truesize (SO_RCVBUF와 비교) |
sk_wmem_alloc | TX | 전송 중인 skb의 총 truesize (SO_SNDBUF와 비교) |
sock_rfree | RX | skb 해제 시 sk_rmem_alloc 차감 |
sock_wfree | TX | skb 해제 시 sk_wmem_alloc 차감, 전송 대기 프로세스 wakeup |
skb_orphan(skb) | 양방향 | skb↔sk 연결 해제 (destructor 호출 후 sk=NULL) |
skb->truesize는 skb 구조체 크기 + 할당된 데이터 버퍼 크기를 합산한 값입니다. 소켓의 메모리 추적은 이 값 기반이므로, truesize가 실제와 어긋나면 SO_RCVBUF 제한이 오작동합니다. 너무 작으면 메모리 과다 사용, 너무 크면 조기 패킷 드롭이 발생합니다.
소켓 옵션(setsockopt)과 sk_buff
사용자 공간의 setsockopt() 호출은 struct sock 필드를 변경하고, 이것이 skb 생성·처리에 직접 반영됩니다:
| 소켓 옵션 | 레벨 | sock/skb 영향 |
|---|---|---|
SO_RCVBUF | SOL_SOCKET | sk->sk_rcvbuf 설정 → 수신 큐 skb 총량 제한 |
SO_SNDBUF | SOL_SOCKET | sk->sk_sndbuf 설정 → 전송 큐 skb 총량 제한 |
SO_MARK | SOL_SOCKET | sk->sk_mark → skb->mark로 복사 (netfilter/tc/라우팅) |
SO_PRIORITY | SOL_SOCKET | sk->sk_priority → skb->priority로 복사 (QoS) |
SO_BINDTODEVICE | SOL_SOCKET | sk->sk_bound_dev_if → skb의 dev 제한 |
SO_TIMESTAMP | SOL_SOCKET | 수신 skb에 타임스탬프 기록, recvmsg() cmsg로 전달 |
SO_BUSY_POLL | SOL_SOCKET | sk->sk_napi_id + skb->napi_id로 busy polling |
IP_TOS | SOL_IP | inet->tos → 전송 skb IP 헤더 TOS 필드 |
IP_TTL | SOL_IP | inet->uc_ttl → 전송 skb IP 헤더 TTL 필드 |
IP_HDRINCL | SOL_IP | raw socket: 사용자가 IP 헤더를 직접 제공 |
TCP_NODELAY | SOL_TCP | Nagle 알고리즘 해제 → 소량 데이터 즉시 skb 전송 |
TCP_CORK | SOL_TCP | skb 전송 지연 (cork), uncork 시 한번에 전송 |
UDP_CORK | SOL_UDP | 여러 sendmsg를 하나의 skb로 합쳐 전송 |
UDP_GRO | SOL_UDP | 수신 UDP GRO 활성화 → 여러 패킷이 하나의 큰 skb로 |
/* 전송 경로에서 sock 옵션 → skb 필드 복사 과정 */
static void ip_copy_addrs(struct iphdr *iph, const struct flowi4 *fl4)
{
/* flowi4는 routing lookup 입력: sock의 IP/포트에서 구성 */
iph->saddr = fl4->saddr;
iph->daddr = fl4->daddr;
}
/* ip_queue_xmit: TCP 전송 시 sock 옵션 적용 */
int __ip_queue_xmit(struct sock *sk, struct sk_buff *skb, ...)
{
struct inet_sock *inet = inet_sk(sk);
/* SK 옵션 → skb 필드 전파 */
skb->priority = sk->sk_priority; /* SO_PRIORITY */
skb->mark = sk->sk_mark; /* SO_MARK */
/* IP 헤더 필드: inet_sock에서 가져옴 */
iph->tos = inet->tos; /* IP_TOS */
iph->ttl = ip_select_ttl(inet, ...); /* IP_TTL 또는 기본값 */
/* ... */
}
/* SO_RCVBUF 설정과 수신 큐 제한의 관계 */
/* 사용자 공간 */
int bufsize = 262144; /* 256KB */
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
/* 커널: sk->sk_rcvbuf = min(bufsize * 2, sysctl_rmem_max)
* → 실제 커널 값은 요청값의 2배 (overhead 고려)
*
* 수신 시: atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf 이면
* → 새 패킷 드롭 (ENOMEM) */
SO_RCVBUF/SO_SNDBUF에 설정한 값은 커널 내에서 2배로 증폭됩니다 (sock_setsockopt() 내부). 이는 skb 구조체와 메타데이터 오버헤드를 고려한 것입니다. getsockopt()으로 읽으면 2배된 값이 반환됩니다. 시스템 전역 상한은 /proc/sys/net/core/rmem_max, wmem_max입니다.
Raw Socket과 sk_buff
Raw socket(SOCK_RAW)은 프로토콜 스택의 일부를 우회하여 직접 패킷을 구성하거나 수신합니다. 일반 SOCK_STREAM/SOCK_DGRAM과 달리 커널의 L4 프로토콜 처리를 거치지 않고 skb를 직접 다루므로, 네트워크 도구(ping, traceroute, tcpdump, nmap 등)와 프로토콜 구현의 핵심입니다.
Raw Socket 타입 비교
| 타입 | 생성 | 접근 계층 | skb 관계 |
|---|---|---|---|
| IP raw socket | socket(AF_INET, SOCK_RAW, IPPROTO_XXX) |
L3 (IP) | IP_HDRINCL 없으면 커널이 IP 헤더 생성. 수신 시 IP 헤더 포함 |
| IP raw + IP_HDRINCL | setsockopt(IP_HDRINCL, 1) |
L3 (IP) | 사용자가 IP 헤더까지 직접 작성. skb->data가 IP 헤더부터 시작 |
| Packet socket (L2 raw) | socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) |
L2 (Ethernet) | Ethernet 헤더 포함 전체 프레임 접근. skb를 MAC 헤더부터 수신/전송 |
| Packet socket (L2 cooked) | socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)) |
L3 (IP) | L2 헤더 제거 후 전달. 송신 시 커널이 L2 헤더 생성 |
| AF_PACKET + TPACKET | setsockopt(PACKET_VERSION, TPACKET_V3) |
L2 (Ethernet) | mmap 기반 ring buffer → skb 없이 직접 DMA 버퍼 접근 (고성능) |
| Ping socket | socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP) |
L4 (ICMP) | CAP_NET_RAW 불필요. 커널이 ICMP 헤더 id/checksum 관리 |
Raw Socket 권한 모델
Raw socket 생성에는 CAP_NET_RAW capability가 필요합니다. 커널은 sock_create() → inet_create() 경로에서 capability를 검사합니다:
/* net/ipv4/af_inet.c — inet_create() */
static int inet_create(struct net *net, struct socket *sock,
int protocol, int kern)
{
struct inet_protosw *answer;
struct sock *sk;
/* SOCK_RAW 사용 시 CAP_NET_RAW 검사 */
if (sock->type == SOCK_RAW && !kern &&
!ns_capable(net->user_ns, CAP_NET_RAW))
return -EPERM;
/* protocol 번호로 inetsw[] 해시 테이블에서 프로토콜 핸들러 검색 */
answer = inet_protosw_lookup(sock->type, protocol);
/* SOCK_RAW → raw_prot (net/ipv4/raw.c)
* SOCK_STREAM → tcp_prot
* SOCK_DGRAM → udp_prot */
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer->prot, kern);
/* ... */
}
/* net/packet/af_packet.c — AF_PACKET도 CAP_NET_RAW 필요 */
static int packet_create(struct net *net, struct socket *sock,
int protocol, int kern)
{
if (!kern && !ns_capable(net->user_ns, CAP_NET_RAW))
return -EPERM;
/* ... */
}
Ping socket 예외: Linux 3.0+에서 도입된 ping socket(socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP))은 CAP_NET_RAW 없이도 ICMP Echo Request를 보낼 수 있습니다. /proc/sys/net/ipv4/ping_group_range로 허용 GID 범위를 설정합니다. setcap cap_net_raw+ep /usr/bin/ping 대신 이 메커니즘을 사용합니다.
커널 내부 자료구조 — struct raw_sock
/* include/net/raw.h */
struct raw_sock {
struct inet_sock inet; /* inet_sock 상속 (→ sock → sock_common) */
struct icmp_filter filter; /* ICMP 타입별 필터 비트맵 */
u32 ipmr_table; /* 멀티캐스트 라우팅 테이블 ID */
};
/* raw socket 프로토콜 해시 테이블
* protocol 번호로 해싱하여 수신 시 O(1) 조회 */
struct raw_hashinfo {
spinlock_t lock;
struct hlist_head ht[RAW_HTABLE_SIZE]; /* 256 버킷 */
};
/* 전역 raw 해시 테이블 — 모든 AF_INET SOCK_RAW 소켓 관리 */
struct raw_hashinfo raw_v4_hashinfo; /* IPv4 */
struct raw_hashinfo raw_v6_hashinfo; /* IPv6 */
/* 해시 함수: protocol 번호를 버킷 인덱스로 변환 */
static inline u32 raw_hashfunc(const struct net *net, u32 proto)
{
return proto & (RAW_HTABLE_SIZE - 1); /* 0~255 */
}
/* raw socket의 프로토콜 연산 테이블 */
struct proto raw_prot = {
.name = "RAW",
.owner = THIS_MODULE,
.close = raw_close,
.connect = ip4_datagram_connect,
.sendmsg = raw_sendmsg,
.recvmsg = raw_recvmsg,
.bind = raw_bind,
.hash = raw_hash_sk,
.unhash = raw_unhash_sk,
.obj_size = sizeof(struct raw_sock),
};
AF_INET SOCK_RAW 수신 경로
IP 계층에서 패킷이 로컬로 배달될 때, TCP/UDP 디먹싱 이전에 raw socket으로의 복제가 먼저 수행됩니다. 즉, raw socket은 패킷의 사본을 받으며, 원본 skb는 정상 프로토콜 스택으로 계속 진행합니다:
/* net/ipv4/ip_input.c — ip_local_deliver_finish()
* 패킷이 로컬 배달될 때 raw socket에 먼저 전달 */
static int ip_local_deliver_finish(struct net *net, struct sock *sk,
struct sk_buff *skb)
{
__skb_pull(skb, skb_network_header_len(skb));
int protocol = ip_hdr(skb)->protocol;
const struct net_protocol *ipprot;
/* ① raw socket이 있으면 먼저 skb 사본 전달 */
raw_local_deliver(skb, protocol);
/* ② 등록된 프로토콜 핸들러 호출 (tcp_v4_rcv, udp_rcv 등) */
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot) {
ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv,
skb);
}
}
/* net/ipv4/raw.c — raw_local_deliver()
* 해당 프로토콜의 raw socket들을 해시 테이블에서 찾아 전달 */
int raw_local_deliver(struct sk_buff *skb, int protocol)
{
struct raw_hashinfo *h = &raw_v4_hashinfo;
struct hlist_head *head;
int hash;
hash = raw_hashfunc(dev_net(skb_dst(skb)->dev), protocol);
head = &h->ht[hash];
if (!hlist_empty(head)) {
/* 매칭되는 모든 raw socket에 skb 복제본 전달 */
raw_v4_input(skb, ip_hdr(skb), hash);
}
return 0;
}
/* net/ipv4/raw.c — raw_v4_input()
* 프로토콜 번호와 목적지 주소가 매칭되는 모든 raw socket에 전달 */
static int raw_v4_input(struct sk_buff *skb, const struct iphdr *iph,
int hash)
{
struct sock *sk;
struct hlist_head *head = &raw_v4_hashinfo.ht[hash];
int delivered = 0;
rcu_read_lock();
sk_for_each_rcu(sk, head) {
/* 프로토콜 번호, 목적지 IP, 소스 IP, 네트워크 네임스페이스 매칭 */
if (raw_v4_match(net, sk, iph->protocol,
iph->saddr, iph->daddr,
skb->dev->ifindex, sdif)) {
/* skb를 clone하여 해당 raw socket에 전달 */
raw_rcv(sk, skb);
delivered++;
}
}
rcu_read_unlock();
return delivered;
}
/* net/ipv4/raw.c — raw_rcv()
* skb clone → IP 헤더 포함한 상태로 수신 큐에 추가 */
int raw_rcv(struct sock *sk, struct sk_buff *skb)
{
struct raw_sock *rp = raw_sk(sk);
/* ICMP 필터 적용: 관심 없는 ICMP 타입은 드롭 */
if (sk->sk_protocol == IPPROTO_ICMP) {
struct icmphdr *icmph = icmp_hdr(skb);
if (raw_icmp_type_filtered(rp, icmph->type))
return 0; /* 필터에 의해 드롭 */
}
/* skb 복제: 원본은 프로토콜 스택이 계속 사용 */
struct sk_buff *clone = skb_clone(skb, GFP_ATOMIC);
if (!clone)
return 0;
/* 핵심: data 포인터를 network_header (IP 헤더) 위치로 복원
* ip_local_deliver_finish()에서 __skb_pull로 L4까지 당겼으므로
* raw socket은 IP 헤더부터 보여줘야 함 */
skb_push(clone, clone->data - skb_network_header(clone));
/* 수신 큐에 추가 → recvmsg()로 사용자에게 전달 */
if (sock_queue_rcv_skb(sk, clone) < 0)
kfree_skb(clone);
return 0;
}
핵심 포인트: 동일 프로토콜 번호를 사용하는 여러 raw socket이 열려 있으면, 하나의 수신 패킷이 모든 매칭 소켓에 clone되어 전달됩니다. 예: 두 프로세스가 각각 IPPROTO_ICMP raw socket을 열면, ICMP 패킷 수신 시 두 프로세스 모두 사본을 받습니다. 이는 raw_v4_input()의 sk_for_each_rcu() 루프가 해시 버킷의 모든 소켓을 순회하기 때문입니다.
AF_INET SOCK_RAW 전송 경로
Raw socket의 전송 경로는 IP_HDRINCL 옵션에 따라 두 가지로 분기됩니다:
/* net/ipv4/raw.c — raw_sendmsg()
* 사용자 공간의 sendto()/sendmsg() → raw_sendmsg() */
static int raw_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
{
struct inet_sock *inet = inet_sk(sk);
struct flowi4 fl4;
struct rtable *rt;
int err;
/* 목적지 주소 결정 */
struct sockaddr_in *usin = (struct sockaddr_in *)msg->msg_name;
__be32 daddr;
if (usin) {
daddr = usin->sin_addr.s_addr;
} else {
/* connect()로 미리 바인딩된 주소 사용 */
daddr = inet->inet_daddr;
if (!daddr)
return -EDESTADDRREQ;
}
/* 라우팅 테이블 조회 */
flowi4_init_output(&fl4, ...);
rt = ip_route_output_flow(net, &fl4, sk);
if (inet->hdrincl) {
/* ── IP_HDRINCL 모드 ──
* 사용자가 IP 헤더를 직접 작성
* 커널은 최소한의 필드만 보정 */
err = raw_send_hdrinc(sk, &fl4, msg, len, &rt, msg->msg_flags);
} else {
/* ── 일반 raw 모드 ──
* 커널이 IP 헤더를 자동 생성
* 사용자 데이터는 L4 페이로드로 취급 */
err = ip_append_data(sk, &fl4, raw_getfrag,
msg, len, 0, &ipc, &rt, msg->msg_flags);
if (!err) {
err = ip_push_pending_frames(sk, &fl4);
/* → ip_output() → dev_queue_xmit() */
}
}
ip_rt_put(rt);
return err;
}
IP_HDRINCL 상세 — 커널의 보정 동작
IP_HDRINCL을 설정하면 사용자가 IP 헤더를 직접 작성하지만, 커널은 안전성과 정확성을 위해 일부 필드를 자동 보정합니다:
/* net/ipv4/raw.c — raw_send_hdrinc()
* IP_HDRINCL 모드의 실제 전송 처리 */
static int raw_send_hdrinc(struct sock *sk, struct flowi4 *fl4,
struct msghdr *msg, unsigned int len,
struct rtable **rtp, unsigned int flags)
{
struct iphdr *iph;
struct sk_buff *skb;
unsigned int iphlen;
/* skb 할당: IP 헤더 + 페이로드 크기 */
skb = sock_alloc_send_skb(sk,
len + LL_ALLOCATED_SPACE(rt->dst.dev), /* L2 headroom 확보 */
flags & MSG_DONTWAIT, &err);
if (!skb)
return err;
/* L2 헤더 공간 예약 */
skb_reserve(skb, LL_RESERVED_SPACE(rt->dst.dev));
skb->protocol = htons(ETH_P_IP);
/* 사용자 데이터를 skb에 복사 (IP 헤더 포함) */
skb_put(skb, len);
skb->network_header = skb->data;
skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, len);
iph = ip_hdr(skb);
/* ── 커널이 자동 보정하는 필드 ── */
/* (1) tot_len: 0이면 커널이 skb->len으로 설정 */
if (!iph->tot_len)
iph->tot_len = htons(len);
/* (2) saddr: 0이면 라우팅 결과의 소스 IP로 채움 */
if (!iph->saddr)
iph->saddr = fl4->saddr;
/* (3) id: 0이면 커널이 고유 ID 할당 */
if (!iph->id)
ip_select_ident(net, skb, NULL);
/* (4) check: 항상 커널이 재계산 (사용자 값 무시) */
iph->check = 0;
iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl);
/* Netfilter OUTPUT 체인 통과 후 전송 */
err = NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
net, sk, skb, NULL, rt->dst.dev,
dst_output);
return err;
}
| IP 헤더 필드 | 사용자 제공 시 | 0 또는 미설정 시 |
|---|---|---|
version | 사용자 값 사용 | 사용자가 반드시 4로 설정해야 함 |
ihl | 사용자 값 사용 | 사용자가 설정 (보통 5) |
tos | 사용자 값 사용 | 0 (기본 서비스) |
tot_len | 사용자 값 사용 | 커널이 skb->len으로 설정 |
id | 사용자 값 사용 | 커널이 ip_select_ident()로 할당 |
frag_off | 사용자 값 사용 | 0 (단편화 없음) |
ttl | 사용자 값 사용 | 사용자가 반드시 설정해야 함 |
protocol | 사용자 값 사용 | 사용자가 반드시 설정해야 함 |
saddr | 사용자 값 사용 (스푸핑 가능) | 커널이 라우팅 테이블에서 결정 |
daddr | 사용자 값 사용 | 사용자가 반드시 설정해야 함 |
check | 무시 — 커널이 항상 재계산 | 커널이 ip_fast_csum()으로 계산 |
IP_HDRINCL과 IP Spoofing: IP_HDRINCL을 사용하면 saddr(소스 IP)를 임의로 설정할 수 있어 IP 스푸핑이 가능합니다. 이것이 CAP_NET_RAW가 필요한 주요 보안 이유 중 하나입니다. 네트워크 장비의 BCP 38 (uRPF) 필터링이나 커널의 rp_filter 설정으로 발신 스푸핑 패킷을 차단할 수 있습니다.
raw_recvmsg() — 사용자 공간으로 전달
/* net/ipv4/raw.c — raw_recvmsg()
* 사용자의 recvfrom()/recvmsg() 처리 */
static int raw_recvmsg(struct sock *sk, struct msghdr *msg,
size_t len, int flags, int *addr_len)
{
struct sk_buff *skb;
struct sockaddr_in *sin = (struct sockaddr_in *)msg->msg_name;
int err, copied;
/* 수신 큐에서 skb 꺼내기 (대기 가능) */
skb = skb_recv_datagram(sk, flags, &err);
if (!skb)
return err;
/* skb에서 사용자 버퍼로 데이터 복사
* → IP 헤더부터 전체 패킷이 사용자에게 전달됨 */
copied = skb->len;
if (len < copied) {
msg->msg_flags |= MSG_TRUNC; /* 잘림 알림 */
copied = len;
}
skb_copy_datagram_msg(skb, 0, msg, copied);
/* 소스 주소 정보 채우기 */
if (sin) {
sin->sin_family = AF_INET;
sin->sin_addr.s_addr = ip_hdr(skb)->saddr;
sin->sin_port = 0; /* raw socket은 포트 개념 없음 */
}
/* IP_PKTINFO, IP_TTL 등 ancillary data (cmsg) 전달 */
if (inet_cmsg_flags(inet))
ip_cmsg_recv(msg, skb);
skb_free_datagram(sk, skb);
return copied;
}
ICMP 필터 (ICMP_FILTER)
IPPROTO_ICMP raw socket에서 관심 있는 ICMP 타입만 수신하도록 비트맵 필터를 설정할 수 있습니다:
/* include/uapi/linux/icmp.h */
struct icmp_filter {
__u32 data; /* 비트맵: bit N이 1이면 ICMP type N을 필터링(드롭) */
};
/* 사용자 공간 예: Echo Reply(type 0)만 수신, 나머지 필터링 */
struct icmp_filter filt;
filt.data = ~(1 << ICMP_ECHOREPLY); /* type 0만 통과 */
setsockopt(fd, SOL_RAW, ICMP_FILTER, &filt, sizeof(filt));
/* 커널 내부: raw_rcv()에서 필터 검사 */
static inline bool raw_icmp_type_filtered(const struct raw_sock *rp,
u8 type)
{
/* type에 해당하는 비트가 1이면 필터링(드롭) */
return (rp->filter.data >> type) & 1;
}
IPv6 Raw Socket (AF_INET6 SOCK_RAW)
IPv6 raw socket은 IPv4와 유사하지만 중요한 차이점이 있습니다:
| 특성 | IPv4 (AF_INET) | IPv6 (AF_INET6) |
|---|---|---|
| IP 헤더 접근 | IP_HDRINCL로 IP 헤더 포함 가능 |
IPv6 헤더는 항상 커널이 생성 (IPV6_HDRINCL 미지원) |
| 확장 헤더 | IP 옵션을 IP_OPTIONS로 설정 |
IPV6_RTHDR, IPV6_HOPOPTS 등 ancillary data(cmsg)로 설정 |
| ICMPv6 체크섬 | 사용자가 직접 계산 | 커널이 자동 계산 (RFC 3542 요구사항) |
| 체크섬 오프셋 | 해당 없음 | IPV6_CHECKSUM — 페이로드 내 체크섬 위치 지정, 커널이 계산 |
| 필터 | ICMP_FILTER |
ICMPV6_FILTER — 256비트 비트맵 (struct icmp6_filter) |
| 커널 소스 | net/ipv4/raw.c |
net/ipv6/raw.c |
/* IPv6 raw socket에서 ICMPv6 체크섬은 커널이 자동 계산 */
/* net/ipv6/raw.c — rawv6_send_hdrinc() 내부 */
/* IPV6_CHECKSUM 소켓 옵션: 체크섬 계산 위치 지정 */
int offset = 2; /* 페이로드 시작부터 체크섬 필드의 바이트 오프셋 */
setsockopt(fd, IPPROTO_IPV6, IPV6_CHECKSUM, &offset, sizeof(offset));
/* 커널이 IPv6 pseudo-header 포함 체크섬을 해당 오프셋에 기록 */
/* ICMPv6 raw socket (protocol = IPPROTO_ICMPV6)은
* IPV6_CHECKSUM이 자동으로 offset=2에 설정됨
* → ICMPv6 체크섬 필드 위치가 헤더 시작+2바이트 */
/* ICMPv6 필터 예: Neighbor Solicitation만 수신 */
struct icmp6_filter filt;
ICMP6_FILTER_SETBLOCKALL(&filt);
ICMP6_FILTER_SETPASS(ND_NEIGHBOR_SOLICIT, &filt);
setsockopt(fd, IPPROTO_ICMPV6, ICMPV6_FILTER, &filt, sizeof(filt));
AF_PACKET 심화 — L2 프레임 접근
AF_PACKET 소켓은 Ethernet 프레임 수준에서 패킷을 캡처/전송합니다. tcpdump, wireshark, dhclient, arping 등이 사용합니다.
| 소켓 타입 | 수신 시 포함 헤더 | 전송 시 필요 헤더 | 사용 사례 |
|---|---|---|---|
AF_PACKET, SOCK_RAW |
Ethernet + IP + L4 + 페이로드 | 사용자가 Ethernet 헤더 포함 전체 작성 | tcpdump, 패킷 injection |
AF_PACKET, SOCK_DGRAM |
IP + L4 + 페이로드 (Ethernet 제거) | 커널이 Ethernet 헤더 생성 | dhclient, 프로토콜 분석 |
/* net/packet/af_packet.c — packet_type 등록
* AF_PACKET 소켓 생성 시 packet_type을 등록하여
* NIC 드라이버의 수신 경로에 후킹 */
static int packet_create(struct net *net, struct socket *sock,
int protocol, int kern)
{
struct packet_sock *po;
struct sock *sk;
sk = sk_alloc(net, PF_PACKET, GFP_KERNEL, &packet_proto, kern);
po = pkt_sk(sk);
/* packet_type 구조체 설정 */
po->prot_hook.func = packet_rcv; /* 수신 콜백 */
po->prot_hook.af_packet_priv = sk; /* 소켓 포인터 */
if (protocol) {
po->prot_hook.type = protocol; /* ETH_P_ALL, ETH_P_IP 등 */
__register_prot_hook(sk);
/* → dev_add_pack() → ptype_all 또는 ptype_base[] 리스트에 등록
* → netif_receive_skb() 경로에서 모든 수신 패킷에 대해 콜백 */
}
}
/* 수신 콜백: netif_receive_skb() → deliver_skb() → packet_rcv() */
static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev)
{
struct sock *sk = pt->af_packet_priv;
struct sk_buff *copy;
unsigned int snaplen, res;
/* BPF 필터 적용 (setsockopt SO_ATTACH_FILTER) */
res = run_filter(skb, sk, snaplen);
if (!res)
goto drop; /* BPF 필터에 의해 드롭 */
/* ETH_P_ALL인 경우 모든 패킷에 대해 호출됨 */
copy = skb_clone(skb, GFP_ATOMIC);
if (!copy)
goto drop;
/* SOCK_RAW: MAC 헤더부터 전체 프레임 노출 */
if (sk->sk_type == SOCK_RAW)
skb_push(copy, skb_mac_header_len(skb));
/* sockaddr_ll에 수신 메타데이터 기록 */
struct sockaddr_ll *sll = &PACKET_SKB_CB(copy)->sa.ll;
sll->sll_ifindex = orig_dev->ifindex;
sll->sll_hatype = dev->type;
sll->sll_pkttype = skb->pkt_type; /* PACKET_HOST, PACKET_BROADCAST 등 */
sock_queue_rcv_skb(sk, copy);
return 0;
drop:
kfree_skb(skb);
return 0;
}
/* AF_PACKET 전송: 사용자 → dev_queue_xmit() 직접 전달 */
static int packet_sendmsg(struct socket *sock, struct msghdr *msg,
size_t len)
{
/* SOCK_RAW: 사용자가 Ethernet 헤더 포함 전체 프레임 작성 */
/* SOCK_DGRAM: sockaddr_ll에서 목적지 MAC, 커널이 Ethernet 헤더 생성 */
struct sk_buff *skb = packet_alloc_skb(sk, ...);
/* 사용자 데이터 복사 */
skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, len);
/* IP 스택을 완전히 우회하여 직접 디바이스 큐로 전송 */
err = dev_queue_xmit(skb);
/* → qdisc → NIC 드라이버 → 물리 전송 */
}
TPACKET — mmap 기반 고성능 캡처
TPACKET(PACKET_MMAP)은 커널-사용자 간 mmap된 공유 ring buffer를 사용하여 recvmsg()/sendmsg() 시스템콜 오버헤드 없이 패킷을 교환합니다. tcpdump, libpcap, suricata 등 고성능 캡처 도구의 핵심입니다.
| 버전 | 커널 | 특징 | 제한/이슈 |
|---|---|---|---|
TPACKET_V1 |
2.4+ | 기본 ring buffer, 고정 크기 프레임 | 32비트 타임스탬프, 큰 패킷 지원 불가 |
TPACKET_V2 |
2.6.27+ | VLAN 태그 보존, 64비트 타임스탬프 | 여전히 고정 크기 프레임 |
TPACKET_V3 |
3.2+ | 가변 크기 블록, 타임아웃 기반 블록 해제, 배치 처리 | TX ring 미지원 (V2 사용), 구현 복잡 |
/* TPACKET_V3 ring buffer 설정 예 (사용자 공간) */
struct tpacket_req3 req = {
.tp_block_size = 1 << 22, /* 4MB 블록 */
.tp_block_nr = 64, /* 64개 블록 = 256MB */
.tp_frame_size = TPACKET_ALIGNMENT << 7, /* 프레임 정렬 */
.tp_frame_nr = (1 << 22) * 64 / (TPACKET_ALIGNMENT << 7),
.tp_retire_blk_tov = 60, /* 블록 타임아웃 60ms */
.tp_feature_req_word = TP_FT_REQ_FILL_RXHASH,
};
/* TPACKET 버전 설정 */
int ver = TPACKET_V3;
setsockopt(fd, SOL_PACKET, PACKET_VERSION, &ver, sizeof(ver));
/* RX ring buffer 설정 */
setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &req, sizeof(req));
/* 커널-사용자 공유 메모리 매핑 */
void *ring = mmap(NULL, req.tp_block_size * req.tp_block_nr,
PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED,
fd, 0);
/* 패킷 수신 루프 (V3 블록 기반) */
while (1) {
struct tpacket_block_desc *pbd = block_descs[current_block];
/* 블록이 준비될 때까지 대기 */
while (!(pbd->hdr.bh1.block_status & TP_STATUS_USER))
poll(&pfd, 1, -1);
/* 블록 내 모든 패킷 순회 */
int num_pkts = pbd->hdr.bh1.num_pkts;
struct tpacket3_hdr *ppd = (struct tpacket3_hdr *)
((uint8_t *)pbd + pbd->hdr.bh1.offset_to_first_pkt);
for (int i = 0; i < num_pkts; i++) {
uint8_t *pkt_data = (uint8_t *)ppd + ppd->tp_mac;
uint32_t pkt_len = ppd->tp_snaplen;
process_packet(pkt_data, pkt_len); /* 패킷 처리 */
ppd = (struct tpacket3_hdr *)
((uint8_t *)ppd + ppd->tp_next_offset);
}
/* 블록을 커널에 반환 */
pbd->hdr.bh1.block_status = TP_STATUS_KERNEL;
current_block = (current_block + 1) % req.tp_block_nr;
}
/* 커널 내부: TPACKET_V3 수신 처리
* net/packet/af_packet.c — tpacket_rcv() */
static int tpacket_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt,
struct net_device *orig_dev)
{
/* V3: 현재 블록에 패킷 추가 (가변 크기)
* → skb 데이터를 mmap 버퍼에 직접 복사
* → 블록이 가득 차거나 타임아웃 시 TP_STATUS_USER로 전환
* → 사용자는 poll()로 알림 받고 mmap 메모리에서 직접 읽음
* → recvmsg() 시스콜 불필요 = zero-copy 수신 */
/* BPF 필터 먼저 실행 */
res = run_filter(skb, sk, snaplen);
if (!res)
goto drop;
/* ring buffer의 현재 블록에 패킷 데이터 복사 */
h.raw = packet_current_rx_frame(po, skb, ...);
skb_copy_bits(skb, 0, h.raw + macoff, snaplen);
/* 패킷 메타데이터 기록: 타임스탬프, 길이, VLAN 등 */
h.h3->tp_sec = ts.tv_sec;
h.h3->tp_nsec = ts.tv_nsec;
h.h3->tp_snaplen = snaplen;
h.h3->tp_len = skb->len;
}
PACKET_FANOUT — 멀티코어 패킷 분산
여러 AF_PACKET 소켓이 동일 인터페이스에서 패킷을 분산 처리할 수 있습니다. suricata, PF_RING 대안으로 사용됩니다:
/* PACKET_FANOUT 모드 */
#define PACKET_FANOUT_HASH 0 /* 흐름 해시 기반 분배 (기본) */
#define PACKET_FANOUT_LB 1 /* 라운드 로빈 */
#define PACKET_FANOUT_CPU 2 /* CPU ID 기반 (RSS 활용) */
#define PACKET_FANOUT_ROLLOVER 3 /* 큐 가득 차면 다음 소켓으로 */
#define PACKET_FANOUT_RND 4 /* 랜덤 분배 */
#define PACKET_FANOUT_QM 5 /* skb 큐 매핑 기반 */
#define PACKET_FANOUT_CBPF 6 /* cBPF 프로그램으로 분배 결정 */
#define PACKET_FANOUT_EBPF 7 /* eBPF 프로그램으로 분배 결정 */
/* 사용 예: 4개 워커 스레드가 흐름 해시 기반으로 패킷 분산 */
int fanout_arg = (PACKET_FANOUT_HASH | PACKET_FANOUT_FLAG_DEFRAG)
| (group_id << 16);
setsockopt(fd, SOL_PACKET, PACKET_FANOUT, &fanout_arg,
sizeof(fanout_arg));
/* 커널 내부: fanout_demux() — 소켓 선택 */
static struct sock *fanout_demux_hash(
struct packet_fanout *f, struct sk_buff *skb, unsigned int num)
{
/* skb 흐름 해시를 소켓 수로 나눠 분배 */
return f->arr[reciprocal_scale(
__skb_get_hash_symmetric(skb), num)];
}
/* PACKET_FANOUT_FLAG 옵션 */
#define PACKET_FANOUT_FLAG_ROLLOVER 0x1000 /* 소켓 백로그 시 롤오버 */
#define PACKET_FANOUT_FLAG_UNIQUEID 0x2000 /* 고유 그룹 ID 자동 할당 */
#define PACKET_FANOUT_FLAG_DEFRAG 0x8000 /* IP 단편화 재조합 후 분배 */
Raw Socket과 Netfilter 관계
Raw socket으로 전송하는 패킷도 Netfilter 체인을 통과합니다. 수신은 프로토콜 핸들러 이전(raw_local_deliver)에 처리되므로 INPUT 체인보다 먼저 clone이 발생합니다:
| 방향 | 소켓 타입 | Netfilter 통과 여부 | 설명 |
|---|---|---|---|
| TX | AF_INET SOCK_RAW | OUTPUT 체인 통과 | raw_send_hdrinc() → NF_HOOK(NF_INET_LOCAL_OUT) |
| TX | AF_PACKET SOCK_RAW | Netfilter 우회 | dev_queue_xmit() 직접 호출 (L3 스택 미통과) |
| RX | AF_INET SOCK_RAW | PREROUTING 이후, INPUT 이전 | NF_INET_PRE_ROUTING 통과 후 raw_local_deliver() |
| RX | AF_PACKET SOCK_RAW | Netfilter 이전에 수신 | netif_receive_skb()에서 ptype 콜백 (L3 이전) |
tcpdump가 DROP된 패킷도 보이는 이유: AF_PACKET 소켓은 netif_receive_skb()의 ptype_all 리스트에 등록되어 Netfilter 이전에 skb 사본을 받습니다. 따라서 iptables/nftables에서 DROP된 패킷도 tcpdump에서 관찰됩니다. 전송 방향도 마찬가지로, AF_PACKET TX는 dev_queue_xmit()을 직접 호출하여 Netfilter OUTPUT 체인을 우회합니다.
Raw Socket의 bind()와 connect()
/* AF_INET SOCK_RAW에서 bind()와 connect()의 역할 */
/* bind() — 수신 필터링: 특정 로컬 IP로의 패킷만 수신 */
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = inet_addr("192.168.1.10"),
};
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
/* → raw_v4_match()에서 daddr 매칭에 사용
* → 해당 IP가 목적지인 패킷만 수신 큐에 전달 */
/* connect() — 기본 목적지 설정 + 수신 필터링 */
struct sockaddr_in dest = {
.sin_family = AF_INET,
.sin_addr.s_addr = inet_addr("10.0.0.1"),
};
connect(fd, (struct sockaddr *)&dest, sizeof(dest));
/* → send()에서 목적지 주소 생략 가능 (sendto 대신 send 사용)
* → 수신 시 해당 소스 IP에서 온 패킷만 수신 (소스 필터) */
/* AF_PACKET에서 bind() — 특정 인터페이스에 바인딩 */
struct sockaddr_ll sll = {
.sll_family = AF_PACKET,
.sll_protocol = htons(ETH_P_ALL),
.sll_ifindex = if_nametoindex("eth0"),
};
bind(fd, (struct sockaddr *)&sll, sizeof(sll));
/* → 해당 인터페이스의 패킷만 수신
* → 바인딩 없으면 모든 인터페이스의 패킷 수신 */
실용 예제
/* 예제 1: ICMP Echo Request 전송 (ping 구현) */
int fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
/* CAP_NET_RAW 필요 */
struct {
struct icmphdr hdr;
char data[56]; /* 페이로드 (타임스탬프 등) */
} pkt;
pkt.hdr.type = ICMP_ECHO;
pkt.hdr.code = 0;
pkt.hdr.un.echo.id = htons(getpid());
pkt.hdr.un.echo.sequence = htons(seq++);
pkt.hdr.checksum = 0;
pkt.hdr.checksum = icmp_checksum(&pkt, sizeof(pkt));
struct sockaddr_in dest = { .sin_family = AF_INET,
.sin_addr.s_addr = inet_addr("8.8.8.8") };
sendto(fd, &pkt, sizeof(pkt), 0,
(struct sockaddr *)&dest, sizeof(dest));
/* 커널이 IP 헤더를 자동 생성 (IP_HDRINCL 미설정이므로)
* → skb 할당 → ICMP 페이로드 복사 → IP 헤더 추가
* → raw_sendmsg() → ip_append_data() → ip_push_pending_frames()
* → Netfilter OUTPUT → ip_output() → dev_queue_xmit() */
/* 수신: raw socket은 모든 ICMP 패킷을 받으므로 필터 설정 */
struct icmp_filter filt;
filt.data = ~(1 << ICMP_ECHOREPLY);
setsockopt(fd, SOL_RAW, ICMP_FILTER, &filt, sizeof(filt));
char buf[1500];
struct sockaddr_in from;
socklen_t fromlen = sizeof(from);
int n = recvfrom(fd, buf, sizeof(buf), 0,
(struct sockaddr *)&from, &fromlen);
/* buf[0..19] = IP 헤더 (raw socket은 항상 IP 헤더 포함 수신)
* buf[20..] = ICMP 헤더 + 페이로드 */
/* 예제 2: ARP Request 전송 (AF_PACKET SOCK_RAW) */
int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ARP));
/* 전체 Ethernet 프레임을 직접 구성 */
struct {
struct ethhdr eth; /* Ethernet 헤더 (14 bytes) */
struct arphdr arp; /* ARP 헤더 */
uint8_t ar_sha[6]; /* 송신자 MAC */
uint8_t ar_sip[4]; /* 송신자 IP */
uint8_t ar_tha[6]; /* 대상 MAC (ARP Request에서는 0) */
uint8_t ar_tip[4]; /* 대상 IP */
} frame;
/* Ethernet 헤더: 브로드캐스트 */
memset(frame.eth.h_dest, 0xff, ETH_ALEN); /* FF:FF:FF:FF:FF:FF */
memcpy(frame.eth.h_source, my_mac, ETH_ALEN);
frame.eth.h_proto = htons(ETH_P_ARP);
/* ARP 헤더: ARP Request */
frame.arp.ar_hrd = htons(ARPHRD_ETHER);
frame.arp.ar_pro = htons(ETH_P_IP);
frame.arp.ar_hln = 6;
frame.arp.ar_pln = 4;
frame.arp.ar_op = htons(ARPOP_REQUEST);
/* sockaddr_ll로 출력 인터페이스 지정 */
struct sockaddr_ll sll = {
.sll_ifindex = if_nametoindex("eth0"),
.sll_halen = ETH_ALEN,
};
memset(sll.sll_addr, 0xff, ETH_ALEN);
sendto(fd, &frame, sizeof(frame), 0,
(struct sockaddr *)&sll, sizeof(sll));
/* → packet_sendmsg() → dev_queue_xmit()
* → IP 스택, Netfilter 완전 우회
* → 직접 NIC 드라이버로 전달 */
/* 예제 3: IP_HDRINCL로 커스텀 IP 패킷 전송 */
int fd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
/* IPPROTO_RAW (255)는 자동으로 IP_HDRINCL 활성화 */
struct {
struct iphdr ip;
struct udphdr udp;
char payload[64];
} pkt;
/* IP 헤더 구성 */
pkt.ip.version = 4;
pkt.ip.ihl = 5;
pkt.ip.tos = 0;
pkt.ip.tot_len = htons(sizeof(pkt));
pkt.ip.id = 0; /* 커널이 자동 할당 */
pkt.ip.frag_off = htons(IP_DF);
pkt.ip.ttl = 64;
pkt.ip.protocol = IPPROTO_UDP;
pkt.ip.check = 0; /* 커널이 자동 계산 */
pkt.ip.saddr = inet_addr("10.0.0.1");
pkt.ip.daddr = inet_addr("10.0.0.2");
/* UDP 헤더 구성 */
pkt.udp.source = htons(12345);
pkt.udp.dest = htons(53);
pkt.udp.len = htons(sizeof(pkt.udp) + sizeof(pkt.payload));
pkt.udp.check = 0; /* UDP 체크섬은 사용자가 계산해야 함 */
sendto(fd, &pkt, sizeof(pkt), 0, ...);
/* → raw_sendmsg() → inet->hdrincl=1이므로 raw_send_hdrinc()
* → 커널은 check, tot_len(0이면), id(0이면), saddr(0이면)만 보정
* → NF_HOOK(NF_INET_LOCAL_OUT) → dst_output() → dev_queue_xmit() */
보안 고려사항
| 위협 | 관련 소켓 타입 | 방어 메커니즘 |
|---|---|---|
| IP 스푸핑 | AF_INET + IP_HDRINCL | rp_filter (Reverse Path Filtering), BCP 38 (uRPF) |
| ARP 스푸핑 | AF_PACKET SOCK_RAW | DAI (Dynamic ARP Inspection), 정적 ARP 엔트리 |
| 패킷 스니핑 | AF_PACKET (ETH_P_ALL) | CAP_NET_RAW 제한, 네트워크 네임스페이스 격리 |
| 프로토콜 스택 DoS | SOCK_RAW 대량 전송 | net.core.rmem_max, sk->sk_sndbuf 제한 |
| 컨테이너 탈출 | AF_PACKET TPACKET | CAP_NET_RAW 제거, seccomp 필터 |
# CAP_NET_RAW 관련 보안 설정
# 특정 바이너리에만 CAP_NET_RAW 부여 (setuid 대체)
setcap cap_net_raw+ep /usr/bin/ping
# ping socket 허용 범위 설정 (CAP_NET_RAW 불필요)
# GID 0~2147483647 범위의 사용자가 ICMP ping 가능
sysctl -w net.ipv4.ping_group_range="0 2147483647"
# Reverse Path Filtering (IP 스푸핑 방지)
sysctl -w net.ipv4.conf.all.rp_filter=1 # strict mode
sysctl -w net.ipv4.conf.all.rp_filter=2 # loose mode
# 컨테이너에서 CAP_NET_RAW 제거 (Docker)
docker run --cap-drop=NET_RAW ...
# seccomp으로 raw socket 시스콜 차단
# socket(AF_PACKET, ...) 또는 socket(AF_INET, SOCK_RAW, ...) 블록
# 열린 raw socket 확인
ss -w -a # RAW 소켓 목록
cat /proc/net/raw # IPv4 raw socket 상세 정보
cat /proc/net/raw6 # IPv6 raw socket 상세 정보
cat /proc/net/packet # AF_PACKET 소켓 목록
IPPROTO_RAW (255) 특수 동작: socket(AF_INET, SOCK_RAW, IPPROTO_RAW)는 전송 전용 raw socket을 생성합니다. IP_HDRINCL이 자동 활성화되며, 이 소켓으로는 수신이 불가합니다 (recvmsg()가 영원히 블록). 수신하려면 IPPROTO_RAW 대신 구체적인 프로토콜 번호(예: IPPROTO_UDP)를 지정하거나 별도의 수신용 raw socket을 생성해야 합니다.
소켓 디먹싱과 skb 전달
수신된 skb가 올바른 소켓을 찾아가는 과정 (디먹싱):
/* TCP 수신 디먹싱: 4-tuple 해시 → 소켓 lookup */
/* tcp_v4_rcv() 내부 */
struct sock *sk = __inet_lookup_skb(
&tcp_hashinfo, /* TCP 소켓 해시 테이블 */
skb,
__tcp_hdrlen(th),
th->source, /* 소스 포트 */
th->dest, /* 목적지 포트 */
iph->saddr, /* 소스 IP */
iph->daddr, /* 목적지 IP */
sdif);
/* 반환: established 소켓 또는 listen 소켓 */
/* UDP 수신 디먹싱 */
struct sock *sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest,
udptable);
/* 소켓을 찾은 후 skb를 수신 큐에 전달 */
if (!sock_owned_by_user(sk)) {
/* 소켓이 lock 상태가 아니면 직접 수신 큐에 추가 */
__skb_queue_tail(&sk->sk_receive_queue, skb);
sk->sk_data_ready(sk); /* epoll/poll/select wakeup */
} else {
/* 소켓이 lock 중이면 backlog에 임시 저장 */
__sk_add_backlog(sk, skb);
/* → release_sock() 시 backlog 처리 */
}
성능 팁: SO_REUSEPORT와 BPF_PROG_TYPE_SK_REUSEPORT를 조합하면, 동일 포트를 여러 소켓이 공유하면서 BPF 프로그램으로 skb를 특정 소켓에 스티어링할 수 있습니다. 이는 nginx, envoy 등의 고성능 프록시에서 활용됩니다.
체크섬 오프로드 (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로 스택을 한 번만 통과합니다.
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의 핵심 이점:
* 1. TSO 미지원 NIC에서도 성능 향상 (SW 분할이라도 늦은 분할이 유리)
* 2. 터널, 가상화 등 복잡한 경로에서도 동작
* 3. 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;
}
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에 직접 영향
Zero-copy 전송
대용량 데이터를 전송할 때, 사용자 공간 버퍼를 커널로 복사하지 않고 직접 NIC에 전달하여 CPU 사용량과 지연을 줄일 수 있습니다:
| 메커니즘 | 시스템콜 | 동작 방식 |
|---|---|---|
sendfile() | sendfile(out_fd, in_fd, ...) | 파일 → 소켓 직접 전송 (페이지 캐시 → skb frag) |
splice() | splice(fd_in, ..., fd_out, ...) | 파이프 기반 zero-copy, 파일 ↔ 소켓 모두 가능 |
MSG_ZEROCOPY | send(fd, buf, len, MSG_ZEROCOPY) | 사용자 버퍼 → skb frag (완료 통지 필요, 4.14+) |
/* 커널 내부: sendfile의 skb 구성 */
/* 파일 페이지를 skb fragment로 직접 참조 */
skb_fill_page_desc(skb, frag_idx, page, offset, size);
/* page refcount 증가, 복사 없음 */
/* MSG_ZEROCOPY: 사용자 버퍼 페이지를 pin */
/* skb->destructor = sock_zerocopy_callback;
* 전송 완료 시 사용자 공간에 completion notification 전달
* (errqueue에서 SO_EE_ORIGIN_ZEROCOPY 메시지 수신) */
/* 드라이버: skb_page_frag_refill로 페이지 풀 활용 */
struct page_frag_cache *nc = &this_cpu_ptr(&nf_skb_cache)->pf_cache;
if (!skb_page_frag_refill(size, nc, GFP_ATOMIC))
return -ENOMEM;
/* nc->va + nc->offset 에서 size 바이트 사용 가능 */
MSG_ZEROCOPY는 10Gbps 이상 고속 네트워크에서 효과적입니다. 그러나 작은 패킷(~수KB 이하)에서는 페이지 pinning과 completion 통지 오버헤드가 복사 비용보다 클 수 있습니다. Google의 벤치마크에 따르면 5~8% CPU 절감이 일반적입니다.
수신/전송 경로에서의 skb 변형
수신 경로 (NIC → 앱):
- NIC 드라이버:
netdev_alloc_skb()로 skb 할당, DMA 데이터 복사 - L2 처리:
skb_pull(ETH_HLEN)으로 Ethernet 헤더 제거 - L3 처리:
skb_pull(ip_hdr_len)으로 IP 헤더 제거, transport_header 설정 - L4 처리: TCP/UDP 디먹싱, 소켓 수신 큐에
skb_queue_tail() - 앱:
recvmsg()에서 데이터를 사용자 공간에 복사
전송 경로 (앱 → NIC):
- 앱:
sendmsg()에서 사용자 데이터를 skb에 복사 - L4:
skb_push()로 TCP/UDP 헤더 추가 - L3:
skb_push()로 IP 헤더 추가, 라우팅 - L2:
skb_push()로 Ethernet 헤더 추가 - NIC 드라이버:
dev_queue_xmit()→ DMA 전송
커널 내 실제 사용 사례
sk_buff는 네트워크 스택의 모든 서브시스템에서 핵심적으로 사용됩니다:
| 서브시스템 | 주요 skb 활용 | 핵심 함수/패턴 |
|---|---|---|
| TCP | 전송 큐, 재전송 큐, OOO 큐에 skb 관리 | tcp_write_xmit(), tcp_retransmit_skb(), TCP_SKB_CB()로 cb[] 활용 |
| UDP | 소켓 수신 큐에 skb 대기열 | udp_rcv(), skb_consume_udp(), MSG_PEEK 처리 |
| Netfilter | 패킷 필터링/수정/NAT | skb_make_writable() 후 헤더 수정, nf_ct_get(skb)로 conntrack |
| Bridge | L2 포워딩, VLAN 처리 | skb_clone()으로 멀티캐스트 복제, skb_vlan_push/pop() |
| Tunnel (GRE, VXLAN) | encapsulation/decapsulation | skb_cow_head()로 headroom 확보, skb_push()로 외부 헤더 추가 |
| TC (Traffic Control) | QoS, 큐잉, 셰이핑 | skb->priority, skb->mark, skb_get_queue_mapping() |
| BPF/XDP | 프로그래밍 가능 패킷 처리 | TC-BPF: __skb_buff 컨텍스트, XDP: skb 이전 단계 (xdp_buff → build_skb) |
| SCTP | 멀티스트리밍, 멀티호밍 | skb_queue_head_init()으로 청크별 큐 관리 |
/* TCP: cb[]를 tcp_skb_cb로 활용하는 패턴 */
struct tcp_skb_cb {
__u32 seq; /* 시작 시퀀스 번호 */
__u32 end_seq; /* 끝 시퀀스 번호 */
__u32 ack_seq; /* ACK 번호 */
__u8 tcp_flags; /* TCP 플래그 */
__u8 sacked; /* SACK 상태 */
/* ... */
};
#define TCP_SKB_CB(__skb) \
((struct tcp_skb_cb *)&((__skb)->cb[0]))
/* 사용 예: TCP 재전송 판단 */
if (after(TCP_SKB_CB(skb)->end_seq, tp->snd_una))
/* 아직 ACK되지 않은 데이터 — 재전송 대상 */
/* Netfilter: 패킷 수정 전 쓰기 가능 확보 */
static unsigned int my_nf_hook(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct iphdr *iph;
if (skb_ensure_writable(skb, sizeof(*iph)))
return NF_DROP;
iph = ip_hdr(skb);
iph->ttl--; /* 안전하게 수정 가능 */
ip_send_check(iph);
return NF_ACCEPT;
}
주의사항과 함정 (Common Mistakes)
1. skb leak (메모리 누수)
/* 잘못된 코드: 에러 경로에서 skb 해제 누락 */
static int my_rx(struct sk_buff *skb)
{
struct my_hdr *hdr = (struct my_hdr *)skb->data;
if (hdr->version != MY_VERSION)
return -EINVAL; /* BUG! skb가 해제되지 않음 */
/* ... */
}
/* 올바른 코드 */
static int my_rx(struct sk_buff *skb)
{
struct my_hdr *hdr = (struct my_hdr *)skb->data;
if (hdr->version != MY_VERSION) {
kfree_skb(skb); /* 에러 경로 → kfree_skb (드롭) */
return -EINVAL;
}
/* ... 정상 처리 후 ... */
consume_skb(skb); /* 정상 경로 → consume_skb */
return 0;
}
2. pskb_may_pull 후 포인터 미갱신
/* 잘못된 코드: pull 후 이전 포인터 사용 */
struct iphdr *iph = ip_hdr(skb);
if (!pskb_may_pull(skb, iph->ihl * 4))
goto drop;
/* BUG! pskb_may_pull이 버퍼를 재할당했을 수 있음 → iph는 dangling pointer */
pr_info("saddr: %pI4\n", &iph->saddr);
/* 올바른 코드: 포인터 재취득 */
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
goto drop;
struct iphdr *iph = ip_hdr(skb); /* pull 후 재취득 */
if (!pskb_may_pull(skb, iph->ihl * 4))
goto drop;
iph = ip_hdr(skb); /* 두 번째 pull 후에도 재취득! */
3. 공유 skb 데이터 수정
/* 잘못된 코드: clone된 skb의 데이터를 바로 수정 */
struct iphdr *iph = ip_hdr(skb);
iph->ttl = 64; /* BUG! skb가 clone 상태면 원본도 수정됨 */
/* 올바른 코드: 쓰기 전 독점 소유 확보 */
if (skb_ensure_writable(skb, skb_network_header_len(skb)))
goto drop;
struct iphdr *iph = ip_hdr(skb); /* 포인터 재취득 */
iph->ttl = 64; /* 이제 안전 */
4. truesize 불일치
/* 잘못된 코드: skb에 페이지를 추가하면서 truesize 미갱신 */
skb_add_rx_frag(skb, idx, page, offset, size, size);
/* 마지막 인자(truesize)가 실제 할당 크기보다 작으면
* → 소켓 메모리 추적(sk_rmem_alloc)이 실제보다 작게 계산됨
* → 소켓이 제한 없이 메모리를 소비 → OOM 가능
*/
/* 올바른 코드: truesize는 실제 할당된 메모리 크기 */
skb_add_rx_frag(skb, idx, page, offset, size, PAGE_SIZE);
/* PAGE_SIZE = 실제 할당 단위 (page order 0 기준) */
5. refcount 이중 해제
/* 잘못된 코드: netif_rx 후 skb를 다시 해제 */
netif_rx(skb); /* 네트워크 스택에 소유권 이전 */
kfree_skb(skb); /* BUG! 이중 해제 → use-after-free */
/* 올바른 패턴: 전달 후 skb를 사용하지 않음 */
netif_rx(skb); /* 소유권 이전, skb는 더 이상 사용하지 않음 */
/* netif_rx(), netif_receive_skb(), napi_gro_receive() 등은
* skb의 소유권을 가져감 — 이후 skb 접근 금지 */
6. headroom 부족으로 인한 skb_under_panic
/* 잘못된 코드: headroom 확인 없이 헤더 추가 */
skb_push(skb, sizeof(struct my_encap_hdr));
/* headroom이 부족하면 skb_under_panic → 커널 panic */
/* 올바른 코드: headroom 확보 후 추가 */
int needed = sizeof(struct my_encap_hdr) + LL_RESERVED_SPACE(dev);
if (skb_cow_head(skb, needed)) {
kfree_skb(skb);
return NETDEV_TX_OK;
}
skb_push(skb, sizeof(struct my_encap_hdr)); /* 이제 안전 */
디버깅 기법
tracepoint 활용
# skb 드롭 추적 (kfree_skb 호출 위치와 원인)
$ perf trace -e skb:kfree_skb --call-graph dwarf -a sleep 5
# skb 드롭 실시간 모니터링
$ cat /sys/kernel/debug/tracing/events/skb/kfree_skb/format
$ echo 1 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enable
$ cat /sys/kernel/debug/tracing/trace_pipe
# dropwatch (커널 6.x+: kfree_skb_reason으로 드롭 원인 표시)
$ dropwatch -l kas
> start
perf probe로 동적 추적
# 특정 함수에서 skb->len 값 추적
$ perf probe --add 'tcp_v4_rcv skb->len skb->data_len'
$ perf record -e probe:tcp_v4_rcv -a sleep 10
$ perf script
# skb 할당 빈도 측정
$ perf stat -e 'skb:*' -a sleep 10
/proc/net 진단
# 소켓 메모리 사용량 확인 (skb leak 의심 시)
$ cat /proc/net/sockstat
sockets: used 1234
TCP: inuse 56 orphan 2 tw 128 alloc 60 mem 1024
UDP: inuse 12 mem 256
# mem: 페이지 단위 (mem * PAGE_SIZE = 실제 바이트)
# orphan: 소속 프로세스 없는 TCP 소켓 (skb leak 원인 가능)
# tw: TIME_WAIT 상태 (정상적이지만 과다하면 문제)
# 네트워크 스택 통계 (드롭/에러 확인)
$ nstat -az | grep -i drop
$ cat /proc/net/softnet_stat
디버깅 커널 옵션
| 옵션 | 기능 |
|---|---|
CONFIG_DEBUG_KMEMLEAK | skb를 포함한 커널 메모리 누수 탐지 |
CONFIG_KASAN | use-after-free, out-of-bounds 접근 탐지 |
CONFIG_NET_DROP_MONITOR | 네트워크 패킷 드롭 위치 추적 |
CONFIG_DEBUG_NET | 네트워크 스택 디버깅 assertion 활성화 |
CONFIG_SKB_EXTENSIONS | skb extension (conntrack, bridge 등) 디버깅 |
커널 버전별 변경사항
| 버전 | 변경 내용 |
|---|---|
| 3.18 | skb_frag_off() 접근자 도입 (직접 필드 접근 대체) |
| 4.14 | MSG_ZEROCOPY 소켓 옵션 도입 |
| 4.18 | UDP GSO (SKB_GSO_UDP_L4) 지원 |
| 5.0 | XDP에서 skb 모드 (XDP_FLAGS_SKB_MODE) 공식 지원 |
| 5.3 | skb_ensure_writable() 도입 (skb_make_writable 대체) |
| 5.17 | page_pool 기반 skb 할당 최적화 |
| 6.0 | kfree_skb_reason() 도입 — 드롭 원인 추적 개선 |
| 6.2 | skb->csum_level 필드로 중첩 체크섬 오프로드 지원 |
| 6.8 | netmem 기반 skb frag 관리 (page → netmem 추상화) |
참고 자료: skbuff.h (Bootlin), O'Reilly "Understanding Linux Network Internals" Ch. 2, LWN의 skb lifecycle 시리즈, Documentation/networking/skbuff.rst