#keywords sk_buff, skb, socket, buffer, skb_push, skb_pull, skb_put, frag_list, GSO, TSO, skb_segment, VLAN, BPF, __sk_buff, flow_dissector, RSS, RPS, encapsulation, tunnel, kmem_cache, netns
#title Linux Kernel의 skbuff(Socket buffer descriptors)에 대하여
[wiki:Home 대문] / [wiki:CategoryProgramming 프로그래밍] / [wiki:skbuff Linux Kernel의 skbuff에 대하여]
----
== [wiki:skbuff Linux Kernel의 skbuff(Socket buffer descriptors)에 대하여] ==
* 작성자
조재혁([mailto:minzkn@minzkn.com])
* 고친과정
2020년 12월 07일 : 처음씀
[[TableOfContents]]
최근 정리 내용은 [[HTML(리눅스 커널 정리 (Linux 커널 개발자를 위한 종합 한글 레퍼런스))]] 을 참고하세요.
== 개요 ==
|Linux packet journey,napi, hardware queue,skb| 참고 영상 ||
|| [[Play(https://youtu.be/6Fl1rsxk4JQ)]] ||
Linux kernel 에서 sk_buff (skb) 자료형은 network packet 을 처리하는데 중요한 부분입니다. 이 문서는 이를 설명하기 위해서 작성되었습니다.
이 문서는 저 혼자 모든 것을 이해하고 작성한 것이 절대로 아니며 수 많은 검색과 분석을 통하여 먼저 선두에서 정보를 공유해주신 수많은 이름 모를 선배님들의 발자취에 의해서 작성한 것입니다. 개인 적인 관점에서의 해석이 틀릴 수 있으며 이러한 부분을 알려주시면 내용을 갱신하겠습니다.
sk_buff (skb) 는 대략 다음과 같은 흐름내에서 데이터를 다루는게 목적입니다. 여기서 패킷의 인입(Input)/포워딩(Forward)/출력(Output) 이 어디서 일어나는지를 중심으로 파악되고 있어야 할 필요가 있습니다. (아래 그림은 IPSec packet 관점에서 그린것이므로 IPSec 용어만 빼고 일반 네트워크 패킷으로 가정하여 보시면 됩니다.)
[attachment:VirtualPrivateNetwork/An_IPsec_Packet_flow_2017.07.18_v0.4_(Linux_kernel_v3.8_vanilla_source_기준).png]
* 인입(Input)에는 크게 두 가지로 나뉩니다. (위 그림상에서는 좌측 상단 구름)
== sk_buff 자료구조 ==
[[attachment:sk_buff-structure-0-20220812.png]]
Linux 커널 네트워크 스택의 핵심 자료구조 sk_buff: 메모리 레이아웃, 데이터 조작 함수, clone/copy, 소켓(struct sock) 연동, 소켓 옵션, raw socket, 체크섬 오프로드, GSO/GRO, zero-copy 전송까지 종합 분석합니다. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.
'''주의'''
전제 조건: 네트워크 스택 (https://minzkn.com/linuxkernel/pages/networking-overview.html)과 네트워크 디바이스 드라이버 (https://minzkn.com/linuxkernel/pages/net-device-driver.html) 문서를 먼저 읽으세요. 고성능 패킷 경로는 큐 구조, 메모리 배치, 드롭 위치가 성능을 좌우하므로 드라이버 경계를 먼저 이해하는 것이 중요합니다.
'''팁'''
일상 비유: 이 주제는 고속 톨게이트 차선 분리와 비슷합니다. 일반 차선(커널 스택)과 하이패스 차선(XDP/DPDK)을 구분해 보면 왜 지연과 처리량이 달라지는지 명확해집니다.
=== 핵심 요약 ===
* 메모리 레이아웃 — {{{head}}}, {{{data}}}, {{{tail}}}, {{{end}}} 4개 포인터로 버퍼 관리. {{{skb_push/pull/put}}}로 데이터 영역 조작.
* 참조 모델 — {{{clone}}}은 메타데이터만 복사하고 버퍼 공유, {{{copy}}}는 완전 복사. 참조 카운트 관리가 핵심.
* 소켓 메모리 — {{{sk_rmem_alloc}}}/{{{sk_wmem_alloc}}}이 {{{truesize}}} 기반 소켓 버퍼 제한 구현.
* 헤더 포인터 — {{{mac_header}}}, {{{network_header}}}, {{{transport_header}}}로 L2/L3/L4 헤더 오프셋 추적.
* 수명주기 — 할당 → 프로토콜 처리 → 소켓 전달 → 사용자 복사 → 해제. 각 단계에서 다른 함수와 상태 변화.
* skb 확장 — {{{skb_ext}}}로 conntrack, IPsec secpath, bridge NF 등 가변 메타데이터를 skb에 동적 연결. 5.x+에서 메모리 효율 향상.
* page_pool — 최신 고성능 드라이버(6.x+)는 page_pool로 DMA 매핑 캐시와 페이지 재활용을 구현해 할당/해제 비용 최소화.
* XDP 인터페이스 — {{{xdp_buff}}}는 skb 할당 이전 단계로 동작. XDP_PASS 시 {{{build_skb()}}}를 통해 sk_buff로 변환되어 일반 스택 진입.
=== 단계별 이해 ===
1. 구조체 이해
4개 포인터(head, data, tail, end)와 3개 헤더 포인터(mac, network, transport)의 관계를 먼저 익힙시다. 이게 sk_buff의 핵심입니다.
2. 데이터 조작 함수
{{{skb_push}}}(헤더 추가), {{{skb_pull}}}(헤더 제거), {{{skb_put}}}(데이터 추가)의 동작을 코드로 직접 연습합니다.
3. 할당 함수 선택
{{{alloc_skb}}}(일반), {{{netdev_alloc_skb}}}(드라이버 수신), {{{napi_alloc_skb}}}(NAPI), {{{page_pool_alloc_pages}}}(6.x 고성능) 차이점을 파악합니다.
4. 수명주기 추적
수신 경로(NIC → 드라이버 → L2 → L3 → L4 → 소켓)와 전송 경로의 함수 호출 체인을 따라가며 이해합니다.
5. 확장 시스템 학습
{{{skb_ext}}}, {{{page_pool}}}, XDP ↔ sk_buff 변환 등 현대 커널의 최적화 메커니즘을 파악해 고성능 드라이버 코드를 읽는 눈을 키웁니다.
6. 실전 디버깅 연습
{{{perf trace -e skb:kfree_skb}}}로 드롭 원인을 추적하고, {{{/proc/net/softnet_stat}}}으로 CPU별 처리량을 분석하며, {{{dropwatch}}}로 병목 지점을 찾아봅니다.
'''정보'''
관련 표준: IEEE 802.3 (Ethernet 프레임), RFC 791 (IPv4), RFC 8200 (IPv6) — sk_buff는 이 표준들이 정의하는 패킷 구조를 커널 내부에서 표현합니다. 종합 목록은 참고자료 — 표준 & 규격 (https://minzkn.com/linuxkernel/pages/references.html#std-networking) 섹션을 참고하세요.
=== 개요 ===
{{{struct sk_buff}}}(흔히 skb라 부름)는 Linux 네트워크 스택에서 모든 패킷을 표현하는 자료구조입니다. 수신 패킷이든 전송 패킷이든, L2 프레임이든 L4 세그먼트든, 커널 내에서 네트워크 데이터가 이동할 때는 항상 sk_buff가 사용됩니다.
O'Reilly의 "Understanding Linux Network Internals"에서 Christian Benvenuti는 sk_buff를 "네트워크 스택의 왕(king of networking)"이라 표현했습니다. 이 자료구조를 이해하지 않고는 Linux 네트워크 코드를 읽을 수 없습니다.
* 헤더 파일: {{{}}}
* 주요 소스: {{{net/core/skbuff.c}}}
* 구조체 크기: x86_64에서 약 232바이트 (커널 6.x 기준)
=== struct sk_buff 주요 필드 ===
{{{#!plain
[코드: C]
/* 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에는 프로토콜 처리와 성능 최적화에 쓰이는 중요한 필드들이 있습니다:
{{{#!plain
[코드: C]
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를 사용하면 다음 레이어에서 덮어쓰므로, 레이어 간 데이터 전달에는 사용하지 마세요.
==== 체크섬 오프로드와 ip_summed ====
{{{ip_summed}}} 필드는 NIC 하드웨어의 체크섬 오프로드 상태를 나타내는 핵심 플래그입니다:
||값||의미 (RX)||의미 (TX)||
||{{{CHECKSUM_NONE}}}||HW 미지원, SW 검증 필요||SW가 체크섬 계산 완료||
||{{{CHECKSUM_UNNECESSARY}}}||HW 검증 완료, 유효함||체크섬 불필요 (loopback 등)||
||{{{CHECKSUM_COMPLETE}}}||HW가 전체 체크섬 제공||사용 안 함||
||{{{CHECKSUM_PARTIAL}}}||사용 안 함||HW에 체크섬 계산 위임||
{{{#!plain
[코드: C]
/* 수신: 드라이버에서 체크섬 상태 설정 */
void my_driver_rx(struct sk_buff *skb, bool csum_ok)
{
if (csum_ok) {
skb->ip_summed = CHECKSUM_UNNECESSARY; /* SW 검증 생략 */
} else {
skb->ip_summed = CHECKSUM_NONE; /* SW 검증 필요 */
}
}
/* 전송: CHECKSUM_PARTIAL 설정 예 */
skb->ip_summed = CHECKSUM_PARTIAL;
skb->csum_start = skb_transport_header(skb) - skb->head;
skb->csum_offset = offsetof(struct udphdr, check);
}}}
'''팁'''
경험적 팁: 실제 네트워크 드라이버 개발 시, {{{CHECKSUM_UNNECESSARY}}}를 설정하면 TCP/UDP 프로토콜 스택에서 {{{__skb_checksum_validate_needed()}}}를 건너뛰어 CPU 사이클을 크게 절약합니다. 하지만 일부 buggy NIC에서는 가짜 양수(false positive)가 발생할 수 있어, 문제 발생 시 {{{ethtool -K eth0 rx-checksumming off}}}로 비활성화하고 테스트하세요.
=== skb 할당과 해제 ===
sk_buff 할당 함수는 사용 상황에 따라 여러 변형이 있습니다:
||함수||컨텍스트||특징||
||{{{alloc_skb(size, gfp)}}}||일반 (프로세스/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 등 플래그 지정||
{{{#!plain
[코드: C]
/* 일반적인 전송 경로 할당 */
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개의 핵심 포인터로 관리됩니다:
{{{#!plain
[SVG 텍스트 변환: sk_buff 메모리 레이아웃 다이어그램]
캡션: head~end: 할당된 버퍼 전체, data~tail: 현재 유효 데이터, end 뒤에 skb_shared_info
텍스트 요소:
- sk_buff 메모리 레이아웃
- headroom
- 데이터 영역
- (len - data_len)
- tailroom
- head
- data
- tail
- end
- skb_
- shared_
- info
- (frags[])
}}}
이 레이아웃은 불변식 {{{head ≤ data ≤ tail ≤ end}}}를 항상 유지합니다.
이 조건이 깨지면 커널 패닉 또는 메모리 손상으로 이어집니다. {{{skb_push()}}}/{{{skb_pull()}}}/{{{skb_put()}}} 계열 함수는 호출 전에 이 불변식을 검증하며, 위반 시 {{{skb_over_panic}}} / {{{skb_under_panic}}}을 트리거합니다.
==== 메모리 관련 주요 매크로 ====
sk_buff 버퍼 크기를 계산할 때 자주 사용되는 매크로입니다:
||매크로||정의 (개념)||용도||
||{{{SKB_DATA_ALIGN(X)}}}||{{{ALIGN(X, SMP_CACHE_BYTES)}}}||SMP 캐시 라인 단위(보통 64B)로 올림 정렬. {{{end}}} 포인터 계산에 사용.||
||{{{SKB_WITH_OVERHEAD(X)}}}||{{{X - SKB_DATA_ALIGN(sizeof(struct skb_shared_info))}}}||할당 크기 X에서 {{{skb_shared_info}}}를 뺀 실제 사용 가능한 linear 데이터 크기.||
||{{{SKB_TRUESIZE(X)}}}||{{{SKB_DATA_ALIGN(X + sizeof(struct sk_buff)) + SKB_DATA_ALIGN(sizeof(struct skb_shared_info))}}}||X바이트 데이터를 담는 sk_buff를 할당할 때 실제로 필요한 총 메모리 크기. {{{skb->truesize}}} 초기값으로 사용.||
||{{{SKB_MAX_HEAD(X)}}}||{{{SKB_WITH_OVERHEAD(PAGE_SIZE - X)}}}||헤더용 headroom X를 예약한 뒤 한 페이지 내에서 사용할 수 있는 linear 데이터 최대 크기.||
'''팁'''
truesize 사용 예: {{{alloc_skb(size, gfp)}}}는 내부적으로 {{{SKB_TRUESIZE(size)}}}로 {{{skb->truesize}}}를 초기화합니다. 소켓의 수신 버퍼 제한({{{sk_rmem_alloc}}})은 이 값을 누적해 추적하므로, 페이지 프래그먼트를 직접 추가할 때는 {{{skb_add_rx_frag()}}}의 {{{truesize}}} 인자를 실제 할당 크기(예: {{{PAGE_SIZE}}})로 정확히 넘겨야 합니다.
=== 데이터 조작 함수 ===
sk_buff의 데이터 영역을 조작하는 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)||
{{{#!plain
[코드: C]
/* 전형적인 전송 경로에서의 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), 세 가지 복사 전략을 선택할 수 있습니다:
{{{#!plain
[SVG 텍스트 변환: skb_clone vs pskb_copy vs skb_copy 다이어그램]
캡션: clone은 메타데이터만 복사하고 버퍼를 공유, pskb_copy는 linear 데이터만 복사, skb_copy는 전체 독립 복사
텍스트 요소:
- skb_clone vs pskb_copy vs skb_copy
- skb_clone
- sk_buff (원본)
- sk_buff (clone)
- 공유 데이터 버퍼
- dataref = 2
- pskb_copy
- sk_buff (copy)
- linear (원본)
- linear (복사)
- 공유 paged frags (refcount++)
- skb_copy
- 전체 버퍼 (원본)
- 전체 버퍼 (복사)
- 비교 요약
- 메타만 복사
- 버퍼 100% 공유
- 가장 빠름
- 데이터 수정 불가
- linear 헤더 복사
- paged data 공유
- 중간 비용
- 헤더 수정 가능
- 전체 완전 복사
- 독립적 버퍼
- 가장 느림
- 자유로운 수정
}}}
{{{#!plain
[코드: C]
/* 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를 사용합니다:
{{{#!plain
[코드: C]
/* 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에서 패킷 내용 수정 전||
{{{#!plain
[코드: C]
/* 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 리스트 관리 ===
{{{#!plain
[코드: C]
/* 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단계 계층으로 구성됩니다:
{{{#!plain
[SVG 텍스트 변환: 소켓 구조체 계층과 sk_buff의 관계 다이어그램]
캡션: struct socket → struct sock → 프로토콜별 sock. sk_buff는 sk->sk_receive_queue 등에 연결되며 skb->sk로 소켓을 역참조
텍스트 요소:
- 소켓 구조체 계층과 sk_buff의 관계
- struct socket
- BSD 소켓 인터페이스
- file, ops, sk 포인터
- struct sock (sk)
- 프로토콜 무관 공통 계층
- sk_receive_queue, sk_write_queue
- sk_rmem_alloc, sk_wmem_alloc
- struct tcp_sock / udp_sock
- 프로토콜별 확장
- inet_sock ⊃ sock 내장
- sk
- struct sk_buff
- skb->sk → sock
- skb->destructor
- skb->truesize
- skb->sk
- sk_receive_queue
- sk_receive_queue (RX)
- 수신 skb 대기열
- sk_write_queue (TX)
- 전송 skb 대기열
- sk_backlog (overflow)
- 소켓 lock 중 수신 대기
- sk_error_queue
- ICMP 에러, MSG_ERRQUEUE
}}}
{{{#!plain
[코드: C]
/* 소켓 구조체 계층 (간략) */
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}}} 제한을 실현합니다:
{{{#!plain
[코드: C]
/* 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로||
{{{#!plain
[코드: C]
/* 전송 경로에서 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 등)와 프로토콜 구현의 핵심입니다.
{{{#!plain
[SVG 텍스트 변환: Raw Socket 계층별 접근 범위 다이어그램]
텍스트 요소:
- Raw Socket 계층별 접근 범위
- 사용자 공간 (User Space)
- Socket Layer — socket(), sendmsg(), recvmsg()
- L4: TCP/UDP (SOCK_STREAM/DGRAM은 여기서 처리)
- L3: IP Layer — ip_rcv(), ip_output()
- L2: Ethernet / Device Driver — dev_queue_xmit(), netif_receive_skb()
- AF_INET SOCK_RAW
- AF_PACKET SOCK_RAW
}}}
===== Raw Socket 타입 비교 =====
||타입||생성||접근 계층||skb 관계||
||IP raw socket||{{{socket(AF_INET, SOCK_RAW, IPPROTO_XXX)}}}||L3 (IP)||{{{IP_HDRINCL}}} 없으면 커널이 IP 헤더 생성. 수신 시 IP 헤더 포함||
||IP raw + IP_HDRINCL||{{{setsockopt(IP_HDRINCL, 1)}}}||L3 (IP)||사용자가 IP 헤더까지 직접 작성. skb->data가 IP 헤더부터 시작||
||Packet socket (L2 raw)||{{{socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))}}}||L2 (Ethernet)||Ethernet 헤더 포함 전체 프레임 접근. skb를 MAC 헤더부터 수신/전송||
||Packet socket (L2 cooked)||{{{socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))}}}||L3 (IP)||L2 헤더 제거 후 전달. 송신 시 커널이 L2 헤더 생성||
||AF_PACKET + TPACKET||{{{setsockopt(PACKET_VERSION, TPACKET_V3)}}}||L2 (Ethernet)||mmap 기반 ring buffer → skb 없이 직접 DMA 버퍼 접근 (고성능)||
||Ping socket||{{{socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)}}}||L4 (ICMP)||CAP_NET_RAW 불필요. 커널이 ICMP 헤더 id/checksum 관리||
===== Raw Socket 권한 모델 =====
Raw socket 생성에는 {{{CAP_NET_RAW}}} capability가 필요합니다. 커널은 {{{sock_create()}}} → {{{inet_create()}}} 경로에서 capability를 검사합니다:
{{{#!plain
[코드: C]
/* 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 =====
{{{#!plain
[코드: C]
/* 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는 정상 프로토콜 스택으로 계속 진행합니다:
{{{#!plain
[SVG 텍스트 변환: Raw Socket 수신 경로 (IPv4) 다이어그램]
텍스트 요소:
- Raw Socket 수신 경로 (IPv4)
- ip_local_deliver()
- ip_local_deliver_finish()
- 분기
- ① Raw Socket 경로 (먼저 실행)
- raw_local_deliver()
- raw_v4_hashinfo
- [protocol] 조회
- raw_v4_input()
- sk_for_each():
- 매칭 소켓 순회
- skb_clone(skb, GFP_ATOMIC)
- 데이터 공유 (zero-copy clone)
- 원본 skb 유지
- 사본 → raw sock
- raw_rcv()
- xfrm4_policy_check()
- sock_queue_rcv_skb()
- sk_rmem_alloc 체크
- Raw Socket 수신 큐
- recvfrom()으로 IP 헤더 포함 수신
- ② 프로토콜 핸들러 경로 (이후 실행)
- ipprot->handler(skb)
- inet_protos[protocol]
- tcp_v4_rcv()
- udp_rcv()
- icmp_rcv()
- 정상 소켓 수신 큐
- 핵심 포인트
- • raw socket은 항상 IP 헤더 포함 수신
- • skb_clone()은 데이터 공유 (zero-copy)
- • 원본 skb는 프로토콜 핸들러로 정상 전달
- • CAP_NET_RAW 권한 필요 (비특권 차단)
- 실행 순서: ① raw_local_deliver(skb, protocol) → ② ipprot->handler(skb) (순차 실행, 동시 아님)
}}}
{{{#!plain
[코드: C]
/* 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}}} 옵션에 따라 두 가지로 분기됩니다:
{{{#!plain
[코드: C]
/* 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 헤더를 직접 작성하지만, 커널은 안전성과 정확성을 위해 일부 필드를 자동 보정합니다:
{{{#!plain
[코드: C]
/* 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() — 사용자 공간으로 전달 =====
{{{#!plain
[코드: C]
/* 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 타입만 수신하도록 비트맵 필터를 설정할 수 있습니다:
{{{#!plain
[코드: C]
/* 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}}}||
{{{#!plain
[코드: 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, 프로토콜 분석||
{{{#!plain
[코드: C]
/* 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 사용), 구현 복잡||
{{{#!plain
[SVG 텍스트 변환: TPACKET_V3 Ring Buffer 구조 다이어그램]
텍스트 요소:
- TPACKET_V3 Ring Buffer 구조
- 커널 공간
- Block 0
- TP_STATUS_KERNEL
- pkt 1
- pkt 2
- ← 커널이 쓰는 중
- Block 1
- TP_STATUS_USER
- pkt 3
- pkt 4
- → 사용자가 읽는 중
- Block 2 (빈 블록)
- Block N-1
- 사용자 공간 (mmap)
- mmap()으로 매핑된 동일 물리 메모리
- → 복사 없이 직접 접근 (zero-copy 수신)
- poll()/ppoll()로 TP_STATUS_USER 블록 대기
- → 시스콜 없이 블록 순회하며 패킷 읽기
- 처리 완료 후 TP_STATUS_KERNEL로 반환
- → 커널이 다시 사용 가능
- mmap
}}}
{{{#!plain
[코드: C]
/* 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}}} 대안으로 사용됩니다:
{{{#!plain
[코드: C]
/* 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() =====
{{{#!plain
[코드: C]
/* 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));
/* → 해당 인터페이스의 패킷만 수신
* → 바인딩 없으면 모든 인터페이스의 패킷 수신 */
}}}
===== 실용 예제 =====
{{{#!plain
[코드: C]
/* 예제 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 헤더 + 페이로드 */
}}}
{{{#!plain
[코드: C]
/* 예제 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 드라이버로 전달 */
}}}
{{{#!plain
[코드: C]
/* 예제 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 필터||
{{{#!plain
[코드: bash]
# 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가 올바른 소켓을 찾아가는 과정 (디먹싱):
{{{#!plain
[코드: C]
/* 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}}} 등의 고성능 프록시에서 활용됩니다.
=== 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+)||
{{{#!plain
[코드: C]
/* 커널 내부: sendfile의 skb 구성 */
/* 파일 페이지를 skb fragment로 직접 참조 */
skb_fill_page_desc(skb, frag_idx, page, offset, size);
/* page refcount 증가, 복사 없음 */
/* MSG_ZEROCOPY: 사용자 버퍼 페이지를 pin */
/* skb->destructor = sock_zerocopy_callback;
* 전송 완료 시 사용자 공간에 completion notification 전달
* (errqueue에서 SO_EE_ORIGIN_ZEROCOPY 메시지 수신) */
/* 드라이버: skb_page_frag_refill로 페이지 풀 활용 */
struct page_frag_cache *nc = &this_cpu_ptr(&nf_skb_cache)->pf_cache;
if (!skb_page_frag_refill(size, nc, GFP_ATOMIC))
return -ENOMEM;
/* nc->va + nc->offset 에서 size 바이트 사용 가능 */
}}}
'''팁'''
{{{MSG_ZEROCOPY}}}는 10Gbps 이상 고속 네트워크에서 효과적입니다. 그러나 작은 패킷(~수KB 이하)에서는 페이지 pinning과 completion 통지 오버헤드가 복사 비용보다 클 수 있습니다. Google의 벤치마크에 따르면 5~8% CPU 절감이 일반적입니다.
=== 수신/전송 경로에서의 skb 변형 ===
==== sk_buff 생애주기 (Lifecycle) ====
sk_buff는 네트워크 패킷의 전체 생명주기 동안 커널 메모리를 차지하며, 할당부터 해제까지 다양한 변형을 거칩니다. 다음 다이어그램은 수신(RX)과 송신(TX) 경로에서 sk_buff의 생애주기를 보여줍니다.
{{{#!plain
[SVG 텍스트 변환: sk_buff 생애주기 개요]
캡션: 먼저 개요 흐름을 보고, 아래 상세 다이어그램에서 함수 단위로 추적하면 이해가 빠릅니다.
텍스트 요소:
- 할당/초기화
- RX/TX 경로 처리
- 큐잉/해제
- `alloc_skb()`, 헤더 포인터 설정
- L2/L3/L4, Netfilter, qdisc
- 소켓 전달 또는 NIC 전송 완료
- 상세 단계는 아래 상세 다이어그램 참고
}}}
{{{#!plain
[SVG 텍스트 변환: sk_buff 생애주기 상세 흐름]
캡션: sk_buff 생애주기: 할당 → 프로토콜 스택 통과 → 소켓 큐 → 유저스페이스 전달 → 해제
텍스트 요소:
- sk_buff 생애주기 — 수신/송신 경로
- 수신 경로 (RX)
- 1. skb 할당
- netdev_alloc_skb() / napi_alloc_skb()
- 2. DMA 데이터 복사
- NIC → skb->data (ring buffer)
- 3. L2 프로토콜 처리
- eth_type_trans(), mac_header 설정
- 4. L3 프로토콜 처리
- ip_rcv(), network_header 설정
- 5. Netfilter 훅
- 6. L4 프로토콜 처리
- tcp_v4_rcv(), transport_header 설정
- 7. 소켓 수신 큐
- skb_queue_tail(&sock->sk_receive_queue)
- 8. 유저스페이스 복사
- recvmsg() → copy_to_user()
- 9. skb 해제
- kfree_skb() / consume_skb()
- 송신 경로 (TX)
- 1. 유저 데이터 복사
- sendmsg() → copy_from_user()
- 2. skb 할당
- sock_alloc_send_skb() / alloc_skb()
- 3. L4 헤더 추가
- skb_push(TCP/UDP 헤더)
- 4. L3 헤더 추가
- skb_push(IP 헤더), 라우팅 결정
- 6. L2 헤더 추가
- skb_push(Ethernet 헤더)
- 7. TC/Qdisc (QoS)
- 트래픽 제어, 우선순위 큐
- 8. 드라이버 전송
- dev_queue_xmit() → DMA 매핑
- 9. 전송 완료 & 해제
- TX 완료 인터럽트 → dev_kfree_skb()
- skb 포인터 이동
- RX: data, tail 이동 (pull)
- TX: data 이동 (push)
- 각 계층에서 헤더 참조:
- mac_header, network_header,
- transport_header
}}}
===== 참조 카운트와 메모리 관리 =====
{{{#!plain
[코드: C]
/* sk_buff의 참조 카운트는 users 필드로 관리 */
struct sk_buff {
atomic_t users; /* 참조 카운트 (skb_get/skb_put으로 증감) */
/* ... */
};
/* 참조 카운트 증가 — 소유권 공유 */
static inline struct sk_buff *skb_get(struct sk_buff *skb)
{
refcount_inc(&skb->users);
return skb;
}
/* 참조 카운트 감소 — 0이 되면 해제 */
static inline void kfree_skb(struct sk_buff *skb)
{
if (!skb) return;
if (refcount_dec_and_test(&skb->users))
__kfree_skb(skb); /* 실제 해제 */
}
/* 정상 소비 (드롭 아님) — 통계 구분 */
static inline void consume_skb(struct sk_buff *skb)
{
if (!skb) return;
if (refcount_dec_and_test(&skb->users))
__kfree_skb(skb);
}
/* 사용 예: 소켓 큐에서 꺼낸 후 해제 */
struct sk_buff *skb = skb_dequeue(&sk->sk_receive_queue);
if (skb) {
process_packet(skb);
consume_skb(skb); /* 정상 소비 */
}
/* 드롭 시 kfree_skb 사용 (디버깅 추적 가능) */
if (!pskb_may_pull(skb, sizeof(struct iphdr))) {
kfree_skb(skb); /* 드롭: perf/dropwatch로 추적됨 */
return -EINVAL;
}
}}}
===== 수신/송신 경로에서의 데이터 영역 변화 =====
수신 경로: 각 계층에서 헤더를 제거하며 {{{data}}} 포인터가 앞으로 이동합니다.
||단계||대표 함수||버퍼 레이아웃||{{{data}}} 포인터 위치||
||1. DMA 복사 직후||NIC RX||`headroom||ETH||IP||TCP||DATA||tail`||ETH 시작점||
||2. L2 처리||{{{eth_type_trans()}}}||`headroom||ETH||IP||TCP||DATA||tail`||{{{skb_pull(ETH_HLEN)}}} 후 IP 시작점||
||3. L3 처리||{{{ip_rcv()}}}||`headroom||ETH||IP||TCP||DATA||tail`||{{{skb_pull(ip_hdr_len)}}} 후 TCP 시작점||
||4. L4 처리||{{{tcp_rcv()}}}||`headroom||ETH||IP||TCP||DATA||tail`||payload 시작점 (헤더 제거 완료)||
송신 경로: 각 계층에서 헤더를 추가하며 {{{data}}} 포인터가 뒤로 이동합니다.
||단계||대표 함수||버퍼 레이아웃||{{{data}}} 포인터 이동||
||1. payload 준비||소켓 송신 준비||`headroom||PAYLOAD||tail`||payload 시작점||
||2. L4 헤더 추가||{{{tcp_transmit_skb()}}}||`headroom||TCP||PAYLOAD||tail`||{{{skb_push(tcp_hdr_len)}}}||
||3. L3 헤더 추가||{{{ip_queue_xmit()}}}||`headroom||IP||TCP||PAYLOAD`||{{{skb_push(ip_hdr_len)}}}||
||4. L2 헤더 추가||{{{dev_hard_start_xmit()}}}||`headroom||ETH||IP||TCP||PAYLOAD`||{{{skb_push(ETH_HLEN)}}} 후 NIC DMA 전송||
'''주의'''
headroom 부족 문제: 송신 경로에서 헤더를 추가할 때 headroom이 부족하면 {{{skb_realloc_headroom()}}}이 호출되어 새로운 버퍼를 할당합니다. 이는 성능 저하를 유발하므로, 초기 할당 시 충분한 headroom을 확보하는 것이 중요합니다 ({{{NET_SKB_PAD}}} + {{{NET_IP_ALIGN}}} + 예상 헤더 크기).
===== eth_type_trans() 호출 전후 skb 필드 변화 =====
NIC 드라이버가 {{{eth_type_trans(skb, dev)}}}를 호출하면 다음 필드들이 갱신됩니다:
||필드||호출 전 (DMA 직후)||호출 후||
||{{{skb->mac_header}}}||미설정||L2(Ethernet) 헤더 시작 오프셋으로 설정 ({{{skb_reset_mac_header()}}} 수행)||
||{{{skb->protocol}}}||미설정||EtherType 값 (예: {{{ETH_P_IP}}}, {{{ETH_P_IPV6}}}) — {{{ntohs()}}} 변환 포함||
||{{{skb->data}}}||L2(ETH) 헤더 시작||L3 헤더 시작 ({{{skb_pull(ETH_HLEN)}}} 또는 VLAN 포함 크기만큼 당겨짐)||
||{{{skb->len}}}||L2 프레임 전체 크기||ETH 헤더 제거 후 L3 이상 크기||
||{{{skb->network_header}}}||미설정||미설정 — {{{ip_rcv()}}} 진입 후 {{{skb_reset_network_header()}}}로 설정됨||
||{{{skb->mac_len}}}||미설정||미설정 — L3 처리 진입 시 {{{skb->network_header - skb->mac_header}}}로 초기화됨||
'''팁'''
포인터 복원: {{{mac_header}}}가 설정된 뒤에는 {{{eth_hdr(skb)}}}로 Ethernet 헤더 포인터를 얻을 수 있고, {{{skb_mac_header(skb)}}}로 {{{skb->head + skb->mac_header}}}에 해당하는 포인터를 얻습니다. {{{skb->data}}}는 이미 L3 시작으로 옮겨졌으므로 혼동하지 않도록 주의하십시오.
==== 수신/전송 경로 요약 ====
수신 경로 (NIC → 앱):
1. NIC 드라이버: {{{netdev_alloc_skb()}}}로 skb 할당, DMA 데이터 복사
2. L2 처리: {{{skb_pull(ETH_HLEN)}}}으로 Ethernet 헤더 제거
3. L3 처리: {{{skb_pull(ip_hdr_len)}}}으로 IP 헤더 제거, transport_header 설정
4. L4 처리: TCP/UDP 디먹싱, 소켓 수신 큐에 {{{skb_queue_tail()}}}
5. 앱: {{{recvmsg()}}}에서 데이터를 사용자 공간에 복사
전송 경로 (앱 → NIC):
1. 앱: {{{sendmsg()}}}에서 사용자 데이터를 skb에 복사
2. L4: {{{skb_push()}}}로 TCP/UDP 헤더 추가
3. L3: {{{skb_push()}}}로 IP 헤더 추가, 라우팅
4. L2: {{{skb_push()}}}로 Ethernet 헤더 추가
5. NIC 드라이버: {{{dev_queue_xmit()}}} → DMA 전송
=== 커널 내 실제 사용 사례 ===
sk_buff는 네트워크 스택의 모든 서브시스템에서 핵심적으로 사용됩니다:
||서브시스템||주요 skb 활용||핵심 함수/패턴||
||TCP||전송 큐, 재전송 큐, 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()}}}으로 청크별 큐 관리||
{{{#!plain
[코드: C]
/* TCP: cb[]를 tcp_skb_cb로 활용하는 패턴 */
struct tcp_skb_cb {
__u32 seq; /* 시작 시퀀스 번호 */
__u32 end_seq; /* 끝 시퀀스 번호 */
__u32 ack_seq; /* ACK 번호 */
__u8 tcp_flags; /* TCP 플래그 */
__u8 sacked; /* SACK 상태 */
/* ... */
};
#define TCP_SKB_CB(__skb) \
((struct tcp_skb_cb *)&((__skb)->cb[0]))
/* 사용 예: TCP 재전송 판단 */
if (after(TCP_SKB_CB(skb)->end_seq, tp->snd_una))
/* 아직 ACK되지 않은 데이터 — 재전송 대상 */
/* Netfilter: 패킷 수정 전 쓰기 가능 확보 */
static unsigned int my_nf_hook(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct iphdr *iph;
if (skb_ensure_writable(skb, sizeof(*iph)))
return NF_DROP;
iph = ip_hdr(skb);
iph->ttl--; /* 안전하게 수정 가능 */
ip_send_check(iph);
return NF_ACCEPT;
}
}}}
=== 성능 튜닝 경험적 팁 ===
커널 네트워크 스택과 NIC 드라이버를 다루면서 얻는 경험적 성능 최적화 팁들입니다:
==== 할당 최적화 ====
* NAPI 컨텍스트에서는 {{{napi_alloc_skb}}} 사용: 일반 {{{alloc_skb}}}보다 per-CPU 캐시를 활용해Cache hit율 높임. IRQ 컨텍스트에서는 atomic GFP(Get Free Pages) 플래그 필수.
* 페이지 프래그먼트 활용: 수신 시 데이터가 크면 linear 버퍼 대신 {{{skb_add_rx_frag}}}로 페이지을 DMA 버퍼에 직접 추가. memcpy를 피하면 대역폭 활용이 크게 향상됨.
* headroom 충분하게 확보: 초기 할당 시 {{{NET_SKB_PAD}}}(보통 32바이트) + {{{NET_IP_ALIGN}}}(2 또는 0) + 최대 헤더 크기(예: 100바이트) 확보. 나중에 {{{skb_realloc_headroom}}} 호출은 심각한 성능 저하 유발.
==== Clone vs Copy 선택 ====
* 읽기 전용 경로: {{{skb_clone}}} 사용. 데이터 버퍼 공유하므로 memcpy 1회 절약. Netfilter의 MIRROR 타겟, tcpdump가 이 패턴.
* 헤더만 수정: {{{pskb_copy}}} 사용. Linear 영역만 복사하고 프래그먼트는 refcount 공유. NAT, 라우팅 변경에서 주로 사용.
* Payload 수정 필요: {{{skb_copy}}} 사용. 완전 복사이므로 가장 느리지만 안전.
* 경험적 판단: "이 패킷을 두 곳에서 동시에 수정하는가?" → 아니면 clone, 그 외면 copy.
==== 큐 및 스케줄링 ====
* RSS(Receive Side Scaling) 활용: 멀티큐 NIC에서 {{{skb->queue_mapping}}}이 수신 큐 인덱스 저장. {{{irqbalance}}} 또는 수동 IRQ affinity 설정으로 각 큐를 다른 CPU에 분산.
* softirq 튜닝: {{{/proc/net/softnet_stat}}}에서 각 CPU의 처리량 확인. {{{net.core.netdev_budget}}}(기본 300)으로 softirq time slice 조절.
* NAPI 폴링 시간: {{{netif_napi_add}}} 시 {{{napi->weight}}}(기본 64) 값을 조절. 높은 대역폭 지연이 허용되면 값을 크게, 저지연이 중요하면 작게 설정.
==== Zero-Copy 경로 ====
* sendfile(): 파일 전송 시 가장 효율적. 페이지 캐시 → NIC 직접 경로로 복사 최소화. HTTP 서버 정적 파일 전송에 적합.
* MSG_ZEROCOPY: 대용량 UDP 전송에서 효과적. 10Gbps 이상에서 CPU 절약이 크게 향상됨. 단, TX 완료 대기로 추가 지연이 발생함.
* TPACKET (mbuf): 고성능 캡처에서 필수. mmap으로 커널-사용자 공간 복사를 완전히 제거함. {{{suricata}}}, {{{tcpdump -i any}}} 참조.
==== 하드웨어 offload 활용 ====
* 체크섬 offload 활성화: {{{ethtool -K eth0 rx-checksumming on tx-checksumming on}}}. 대부분의 modern NIC에서 기본값.
* TSO/GSO 활성화: {{{ethtool -K eth0 tso on gso on}}}. 대용량 TCP 전송 시 극적인 성능 향상. 64KB super-packet이 NIC에서 자동 분할.
* GRO 활성화: {{{ethtool -K eth0 gro on}}}. 수신측 병합으로 수천 PPS에서 CPU 사용량 크게 감소.
{{{#!plain
[코드: bash]
# NIC 오프로드 상태 확인
$ ethtool -k eth0 | grep -E "checksum|gso|gro|rss"
tcp-segmentation-offload: on
udp-fragmentation-offload: [fixed]
generic-segmentation-offload: on
generic-receive-offload: on
tcp6-segmentation-offload: on
rx-checksumming: on
tx-checksumming: on
# RSS 설정 확인 및 변경
$ ethtool -l eth0 # 큐 개수 확인
$ ethtool -L eth0 combined 4 # 4개 combined 큐로 설정
# interrupt coalescing 조절 (지연 vs 처리량)
$ ethtool -C eth0 rx-usecs 100 tx-usecs 100 # moderate coalescing
$ ethtool -C eth0 rx-usecs 0 tx-usecs 0 # 낮은 지연 (latency)
}}}
'''팁'''
실제 서비스 경험: 제가 운영하는 10Gbps DDoS 완화 장비에서 {{{GRO off}}} → {{{GRO on}}}으로 변경 시 CPU 사용량이 약 40% 감소했습니다. 하지만 특정 레거시 애플리케이션에서는 GRO로 인한 packet reordering이 문제를 일으킬 수 있어, 프로덕션 변경 전 반드시 테스트 환경에서 검증하세요.
=== 주의사항과 함정 (Common Mistakes) ===
==== 1. skb leak (메모리 누수) ====
{{{#!plain
[코드: C]
/* 잘못된 코드: 에러 경로에서 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 후 포인터 미갱신 ====
{{{#!plain
[코드: C]
/* 잘못된 코드: 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 데이터 수정 ====
{{{#!plain
[코드: C]
/* 잘못된 코드: 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 불일치 ====
{{{#!plain
[코드: C]
/* 잘못된 코드: 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 이중 해제 ====
{{{#!plain
[코드: C]
/* 잘못된 코드: 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 ====
{{{#!plain
[코드: C]
/* 잘못된 코드: headroom 확인 없이 헤더 추가 */
skb_push(skb, sizeof(struct my_encap_hdr));
/* headroom이 부족하면 skb_under_panic → 커널 panic */
/* 올바른 코드: headroom 확보 후 추가 */
int needed = sizeof(struct my_encap_hdr) + LL_RESERVED_SPACE(dev);
if (skb_cow_head(skb, needed)) {
kfree_skb(skb);
return NETDEV_TX_OK;
}
skb_push(skb, sizeof(struct my_encap_hdr)); /* 이제 안전 */
}}}
==== 7. 레이어 경계 처리 실수 ====
RX → 포워딩 → TX 경로를 거치는 패킷은 {{{mac_header}}}, {{{network_header}}} 포인터의 유효성이 경계마다 달라집니다. 이를 무시하면 잘못된 헤더 접근이나 커널 BUG를 유발합니다.
===== RX → 포워딩: mac_header 갭 문제 =====
{{{#!plain
[코드: C]
/* 수신 후 포워딩 경로에서 mac_header와 network_header 사이에
* "갭(hole)"이 생길 수 있음:
* eth_type_trans() → data를 L3 시작으로 당김
* ip_rcv() → network_header = data 위치로 재설정
* → mac_header는 그대로이므로 mac_header < network_header
* (갭 = Ethernet 헤더 크기)
*
* 포워딩 후 재전송 시 L2 헤더 재구성이 필요하면:
*/
skb_mac_header_rebuild(skb); /* mac_header를 network_header 바로 앞으로 재정렬 */
}}}
===== 포워딩 → TX: mac_header 무효화 =====
{{{#!plain
[코드: C]
/* IPSec/GRE 등 캡슐화 후 원래 mac_header가 무효화될 수 있음.
* 헤더 재구성 전에 반드시 유효성 확인: */
if (skb_mac_header_was_set(skb)) {
/* mac_header가 유효할 때만 eth_hdr(skb) 접근 */
struct ethhdr *eth = eth_hdr(skb);
/* ... */
}
/* 캡슐화로 mac_header가 갱신되지 않은 경우 직접 재설정 */
skb_reset_mac_header(skb); /* mac_header = data - head */
}}}
===== IPSec: headroom 확보 및 network_header 재설정 =====
{{{#!plain
[코드: C]
/* IPSec 암호화(outbound) 전: ESP/AH 헤더를 위한 headroom 확보 */
int head_delta = skb_cow_head(skb, esp_hdr_len + LL_RESERVED_SPACE(dst->dev));
if (head_delta)
goto error;
skb_push(skb, esp_hdr_len); /* ESP 헤더 공간 확보 */
skb_reset_network_header(skb); /* IP 헤더 위치 재설정 */
/* IPSec 복호화(inbound) 후: network_header가 바뀌었으므로 재설정 필수 */
skb_pull(skb, esp_hdr_len); /* ESP 헤더 제거 */
skb_reset_network_header(skb); /* 복호화된 IP 헤더로 포인터 재설정 */
}}}
'''주의'''
레이어 경계 핵심 원칙: 캡슐화/역캡슐화 후에는 {{{network_header}}}, {{{transport_header}}}, {{{mac_header}}}를 반드시 재설정하십시오. 포인터를 재설정하지 않으면 {{{ip_hdr(skb)}}}, {{{tcp_hdr(skb)}}} 등이 잘못된 주소를 반환하여 조용한 메모리 손상이 발생합니다.
=== 실제 트러블슈팅 사례 ===
네트워크 문제를 분석하면서 자주 마주하는 skb 관련 실제 케이스들입니다:
==== 사례 1: 메모리 누수가 의심될 때 ====
증상: {{{ss -s}}}나 {{{/proc/net/sockstat}}}에서 사용 중인 소켓 수가 비정상적으로 많거나, 시스템 메모리가 점진적으로 감소합니다.
{{{#!plain
[코드: bash]
# 현재 소켓 상태 확인
$ ss -s
$ cat /proc/net/sockstat
# orphan(소멸된) 소켓 수 — TIME_WAIT 소켓이 정리되지 않으면 증가
$ cat /proc/net/sockstat | grep TCP
# 드롭된 패킷 수 확인
$ nstat -az TcpExt.ListenOverflows
$ cat /proc/net/netstat | grep -E "Tcp|Ext" | column -t
}}}
원인: 에러 경로에서 {{{kfree_skb()}}} 호출 누락, 또는 {{{consume_skb()}}} 대신 {{{kfree_skb()}}}를 사용해서 메모리 참조가 해제되지 않음.
==== 사례 2: 체크섬 검증 실패 ====
증상: 특정 NIC에서만 TCP/UDP 체크섬 오류가 발생하거나, 애플리케이션에서 "bad checksum" 로그가 반복됩니다.
{{{#!plain
[코드: bash]
# NIC 드라이버와 체크섬 오프로드 상태 확인
$ ethtool -k eth0 | grep checksum
rx-checksumming: on
tx-checksumming: on
# 드라이버 메시지 확인 (dmesg)
$ dmesg | grep -i "eth0\|ixgbe\|mlx5"
[12345.678] ixgbe 0000:01:00.0: ixgbe_check_bad_counter: Detected bad TCP checksum,
but feature turned on — actual problem may exist
# 테스트: 체크섬 offload 비활성화
$ ethtool -K eth0 rx-checksumming off tx-checksumming off
}}}
원인: 일부 저가형 또는 legacy NIC에서 HW 체크섬 계산이 부정확한 경우 (false positive). 커널 버그로 인해 특정 드라이버에서만 발생.
==== 사례 3: GRO로 인한 TCP 재전송 증가 ====
증상: GRO 활성화 후 TCP 재전송이 증가하거나, 특정 애플리케이션에서 패킷 순서 오류 발생.
{{{#!plain
[코드: bash]
# GRO 상태 확인
$ ethtool -k eth0 | grep generic-receive-offload
# TCP 재전송 통계 확인
$ nstat -az TcpRetransSegs
$ cat /proc/net/snmp | grep -E "Retrans|OutSegs"
# 문제 구간 확인 — 서버-클라이언트 양쪽에서 GRO 상태 맞춰야 함
$ ethtool -K eth0 gro off # 테스트를 위해 off
$ iperf3 -c 10.0.0.1 -P 4 # 대역폭 재테스트
}}}
원인: GRO가 다른 흐름의 패킷을 잘못 병합하거나, NIC HW GRO의 구현 버그. 특히 가상화 환경(virtio, VM에서) 자주 발생.
==== 사례 4: 프래그먼트된 대용량 패킷 처리 지연 ====
증상: 대용량 파일 전송 시 예상보다 낮은 throughput, 또는 특정 크기(예: 64KB 근처)에서 throughput 급격 감소.
{{{#!plain
[코드: bash]
# skb_linearize 빈도 확인 — linearization은 비용이 큼
$ cat /proc/net/netstat | grep SkbConcatenate
TcpSmbConcatenate: 12345
# GSO/TSO 상태 확인
$ ethtool -k eth0 | grep -E "segmentation|offload"
# 수신측 gro_flush_timeout 확인 (지연 병합)
$ sysctl net.core.gro_flush_timeout
net.core.gro_flush_timeout = 2000
}}}
원인: NIC이 TSO를 지원하지 않으면 커널에서 software GSO가 linearize를 유발하거나, 수신 측 GRO가 타임아웃까지 대기를 위해 지연 발생.
==== 사례 5: NAPI 기아 상태 (starvation) ====
증상: 고대역폭 트래픽에서 일부 CPU만 max softirq time에 도달하고, 다른 CPU는 유휴 상태. 드롭이 특정 CPU에서 집중됨.
{{{#!plain
[코드: bash]
# CPU별 softirq 처리량 확인
$ cat /proc/net/softnet_stat | awk '{print $1, $2, $3, $4}' | head -20
# 컬럼: cpu_id, processed, dropped, time_squeeze
# IRQ affinity 확인
$ cat /proc/interrupts | grep eth0
$ cat /proc/irq//smp_affinity
# NAPI 가중치 확인
$ ls /sys/class/net/eth0/napi
$ cat /sys/class/net/eth0/napi//poll_time
}}}
원인: IRQ가 단일 CPU에 집중되거나, NAPI weight가 너무 작아서 time slice 내에 처리를 못 함. RSS 설정과 IRQ balancing 문제.
'''주의'''
트러블슈팅 핵심 원칙: 네트워크 문제는 غالب히 상호작용하는 여러 요소(RSS, GRO, IRQ affinity, 드라이버 버그)가 복합적으로 작용합니다. 단일 변수만 바꾸고 측정하는 체계적인 접근이 필요합니다. 예를 들어 "GRO만 끄고 latency 측정" → "IRQ affinity만 바꾸고 측정" 식으로요.
=== 디버깅 기법 ===
==== tracepoint 활용 ====
{{{#!plain
[코드: bash]
# 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로 동적 추적 ====
{{{#!plain
[코드: bash]
# 특정 함수에서 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 진단 ====
{{{#!plain
[코드: bash]
# 소켓 메모리 사용량 확인 (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) (https://elixir.bootlin.com/linux/latest/source/include/linux/skbuff.h), O'Reilly "Understanding Linux Network Internals" Ch. 2, LWN의 skb lifecycle 시리즈 (https://lwn.net/Articles/615238/), {{{Documentation/networking/skbuff.rst}}}
=== skb 확장 (skb_ext) ===
커널 5.x부터 {{{sk_buff}}}에 가변 메타데이터를 동적으로 연결하는 skb_ext 메커니즘이 도입되었습니다. 이전에는 conntrack 포인터, IPsec secpath 등이 skb 내부에 직접 포함되어 항상 메모리를 차지했으나, skb_ext를 통해 필요할 때만 할당되어 메모리 효율이 크게 개선되었습니다.
{{{#!plain
[SVG 텍스트 변환: skb_ext 확장 아키텍처]
캡션: skb_ext는 필요할 때만 할당되며, clone 시 refcount 공유 + COW(Copy-on-Write) 방식으로 동작
텍스트 요소:
- struct sk_buff
- extensions (skb_ext *)
- active_extensions (u8)
- len, data, protocol...
- refcount, users
- struct skb_ext
- refcnt (refcount_t)
- chunks (u8) — 할당 청크 수
- data[] — 가변 확장 데이터
- extensions
- SKB_EXT_SEC_PATH
- IPsec xfrm_state 참조
- SKB_EXT_BRIDGE_NF
- br_netfilter 상태
- SKB_EXT_TC
- TC cls_act 메타데이터
- SKB_EXT_MPTCP
- MPTCP 옵션 (6.x+)
- skb_clone 시 skb_ext 동작
- 원본 skb
- clone skb
- skb_ext 공유 (refcnt++)
- COW: 수정 시
- skb_ext_cow() →
- 독립 복사본 생성
}}}
{{{#!plain
[코드: C]
/* include/linux/skbuff.h — skb_ext 구조체 */
struct skb_ext {
refcount_t refcnt; /* 참조 카운트 (clone 시 공유) */
u8 offset[SKB_EXT_NUM]; /* 각 확장의 data[] 내 오프셋 */
u8 chunks; /* 할당된 청크 수 (64B 단위) */
char data[]; /* 가변 길이 확장 데이터 */
};
/* 확장 타입 열거형 */
enum skb_ext_id {
SKB_EXT_BRIDGE_NF, /* br_netfilter 상태 (struct nf_bridge_info) */
SKB_EXT_SEC_PATH, /* IPsec 보안 경로 (struct sec_path) */
SKB_EXT_MPTCP, /* MPTCP 옵션 (struct mptcp_ext) */
TC_SKB_EXT, /* TC cls_act 메타데이터 (struct tc_skb_ext) */
SKB_EXT_NUM /* 총 확장 타입 수 */
};
/* skb_ext 추가 — 해당 타입의 확장 공간을 할당하고 포인터 반환 */
void *skb_ext_add(struct sk_buff *skb, enum skb_ext_id id)
{
struct skb_ext *new;
/* active_extensions 비트맵에 id 설정 */
skb->active_extensions |= 1 << id;
/* 확장 공간 할당 또는 기존 공간에서 오프셋 반환 */
return skb->extensions->data + skb->extensions->offset[id];
}
/* skb_ext 조회 — 해당 확장이 있으면 포인터, 없으면 NULL */
static inline void *skb_ext_find(const struct sk_buff *skb,
enum skb_ext_id id)
{
if (skb->active_extensions & (1 << id))
return skb->extensions->data + skb->extensions->offset[id];
return NULL;
}
/* Netfilter conntrack 연결: nf_ct_get()으로 conntrack 참조 */
static inline struct nf_conn *nf_ct_get(
const struct sk_buff *skb,
enum ip_conntrack_info *ctinfo)
{
/* skb->_nfct에서 conntrack 포인터와 상태 정보 추출 */
unsigned long nfct = skb->_nfct;
*ctinfo = nfct & NFCT_INFOMASK;
return (struct nf_conn *)(nfct & NFCT_PTRMASK);
}
/* TC 확장: cls_act에서 skb에 메타데이터 연결 */
struct tc_skb_ext {
__u32 chain; /* TC 체인 번호 */
__u16 mru; /* TC Maximum Receive Unit */
__u16 zone; /* conntrack zone */
u8 post_ct:1; /* CT action 이후 여부 */
u8 post_ct_snat:1;
u8 post_ct_dnat:1;
};
/* TC에서 skb_ext 사용 예 */
struct tc_skb_ext *ext = skb_ext_add(skb, TC_SKB_EXT);
if (ext)
ext->chain = chain_index;
}}}
||확장 타입||구조체||크기 (약)||사용 서브시스템||
||{{{SKB_EXT_SEC_PATH}}}||{{{sec_path}}}||~40B||IPsec/xfrm — SA 참조 배열||
||{{{SKB_EXT_BRIDGE_NF}}}||{{{nf_bridge_info}}}||~48B||br_netfilter — 원본 포트/MAC 보존||
||{{{TC_SKB_EXT}}}||{{{tc_skb_ext}}}||~12B||TC cls_act — 체인/zone/CT 메타||
||{{{SKB_EXT_MPTCP}}}||{{{mptcp_ext}}}||~24B||MPTCP — DSS/DSN 매핑||
'''팁'''
성능 영향: skb_ext 도입 전, {{{struct sec_path}}}와 {{{struct nf_bridge_info}}}는 skb 내에 항상 포인터를 차지했습니다 (각 8바이트). IPsec이나 bridge를 사용하지 않는 대다수 패킷에서 이 공간이 낭비되었습니다. skb_ext 전환 후 {{{sizeof(struct sk_buff)}}}가 약 16바이트 줄어들었고, 이는 수백만 동시 패킷을 처리하는 환경에서 상당한 메모리 절감입니다.
=== page_pool 기반 고성능 할당 ===
커널 5.17+에서 도입된 page_pool은 네트워크 드라이버의 skb 데이터 버퍼 할당을 혁신적으로 개선합니다. DMA 매핑을 캐시하고, 해제된 페이지를 재활용하며, bulk 할당으로 lock contention을 최소화합니다. {{{mlx5}}}, {{{ice}}}, {{{i40e}}}, {{{bnxt}}} 등 주요 고성능 드라이버가 page_pool을 사용합니다.
{{{#!plain
[SVG 텍스트 변환: page_pool 재활용 아키텍처]
캡션: page_pool은 DMA 매핑을 캐시하고 해제된 페이지를 재활용하여 할당/해제 비용을 5배 이상 절감
텍스트 요소:
- Buddy Allocator
- (초기 할당/부족 시)
- page_pool
- alloc.cache[] (128)
- ring.queue (1024)
- DMA 매핑 캐시 + per-CPU 접근
- bulk
- NIC 드라이버 RX
- page → DMA → skb
- alloc
- struct sk_buff
- frags[] → page_pool page
- 네트워크 스택 처리
- L2 → L3 → L4 → 소켓
- skb 해제
- page_pool_put_page()
- 재활용!
- DMA unmap 생략
- Slow path 해제
- dma_unmap + put_page()
- ring full
- 성능 비교 (10Gbps NIC, 64B 패킷)
- 기존: alloc_page + dma_map 매번 → ~150ns/pkt
- page_pool: 캐시 히트 + DMA skip → ~30ns/pkt
}}}
{{{#!plain
[코드: C]
/* include/net/page_pool/types.h — page_pool 생성 파라미터 */
struct page_pool_params {
unsigned int flags; /* PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV */
unsigned int order; /* page order (0 = 4KB, 1 = 8KB) */
unsigned int pool_size; /* ring 크기 (기본 1024) */
int nid; /* NUMA 노드 (-1 = 현재 노드) */
struct device *dev; /* DMA 매핑 대상 디바이스 */
enum dma_data_direction dma_dir; /* DMA_FROM_DEVICE (RX) */
unsigned int max_len; /* 최대 데이터 길이 */
unsigned int offset; /* 데이터 시작 오프셋 */
};
/* 드라이버 초기화: page_pool 생성 */
struct page_pool_params pp_params = {
.flags = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV,
.order = 0, /* 4KB 페이지 */
.pool_size = 1024,
.nid = dev_to_node(dev),
.dev = &pdev->dev,
.dma_dir = DMA_FROM_DEVICE,
.max_len = PAGE_SIZE,
.offset = XDP_PACKET_HEADROOM,
};
struct page_pool *pool = page_pool_create(&pp_params);
/* 수신 경로: page_pool에서 페이지 할당 */
struct page *page = page_pool_dev_alloc_pages(pool);
/* → alloc.cache[]에서 O(1) 반환 (캐시 히트)
* → 캐시 비면 ring.queue에서 bulk refill
* → ring도 비면 buddy allocator + DMA 매핑 */
dma_addr_t dma = page_pool_get_dma_addr(page);
/* DMA 주소가 이미 캐시됨 — dma_map_page() 호출 불필요! */
/* skb 생성 후 page_pool 페이지를 frag로 추가 */
struct sk_buff *skb = napi_alloc_skb(napi, 256);
skb_add_rx_frag(skb, 0, page, offset, len, PAGE_SIZE);
skb_mark_for_recycle(skb); /* 해제 시 page_pool로 반환 */
/* skb 해제 시: page_pool_put_page()로 자동 재활용 */
/* consume_skb(skb) → skb_free_frag() → page_pool_put_page()
* → alloc.cache[]에 반환 (fast path)
* → 또는 ring.queue에 반환 (다른 CPU에서 해제 시) */
}}}
||비교 항목||기존 (alloc_page + dma_map)||page_pool||
||페이지 할당||매번 buddy allocator 호출||per-CPU 캐시에서 O(1)||
||DMA 매핑||매번 {{{dma_map_page()}}}||캐시된 DMA 주소 재사용||
||해제||{{{dma_unmap}}} + {{{put_page()}}}||캐시에 반환 (unmap 생략)||
||NUMA 인식||수동 관리 필요||{{{nid}}} 파라미터로 자동||
||XDP 호환||직접 구현 필요||내장 XDP headroom 지원||
||bulk 할당||지원 안 함||{{{page_pool_alloc_pages_batch()}}}||
'''정보'''
page_pool 통계 확인: {{{/sys/kernel/debug/page_pool/}}}에서 각 풀의 할당/재활용/실패 통계를 확인할 수 있습니다. {{{ethtool -S eth0 | grep page_pool}}}로 드라이버별 통계도 확인 가능합니다. 재활용율이 90% 이하면 ring 크기 증가 또는 NUMA 문제를 점검하세요.
=== XDP와 sk_buff 인터페이스 ===
XDP(eXpress Data Path)는 sk_buff 할당 이전 단계에서 패킷을 처리하는 고성능 프레임워크입니다. NIC 드라이버 내부에서 {{{xdp_buff}}}라는 경량 구조체로 패킷을 표현하며, XDP 프로그램의 판정(verdict)에 따라 패킷을 드롭/포워딩하거나 일반 네트워크 스택으로 전달합니다.
{{{#!plain
[SVG 텍스트 변환: XDP ↔ sk_buff 변환 흐름]
캡션: XDP는 sk_buff 할당 이전에 패킷을 처리. XDP_PASS 시에만 build_skb()로 sk_buff 변환
텍스트 요소:
- NIC RX (DMA)
- ring buffer → page
- struct xdp_buff
- data, data_hard_start
- data_end, data_meta
- XDP BPF 프로그램
- bpf_xdp_adjust_head()
- bpf_redirect_map()
- XDP_DROP
- XDP_TX
- XDP_REDIRECT
- XDP_PASS
- xdp_buff → sk_buff 변환
- build_skb() 또는 __xdp_build_skb_from_frame()
- struct sk_buff
- 일반 네트워크 스택 진입
- netif_receive_skb() → L2/L3/L4
- XDP Generic (SKB 모드)
- sk_buff가 이미 존재
- __skb_buff로 래핑
- 성능 이점 감소
}}}
{{{#!plain
[코드: C]
/* include/net/xdp.h — xdp_buff 구조체 (sk_buff보다 훨씬 경량) */
struct xdp_buff {
void *data; /* 패킷 데이터 시작 (L2) */
void *data_end; /* 패킷 데이터 끝 */
void *data_meta; /* 메타데이터 시작 (data 앞) */
void *data_hard_start;/* 버퍼 절대 시작 */
struct xdp_rxq_info *rxq; /* RX 큐 정보 */
struct xdp_txq_info *txq; /* TX 큐 정보 */
u32 frame_sz; /* 전체 프레임 크기 */
u32 flags; /* XDP_FLAGS_* */
};
/* xdp_buff → sk_buff 변환 (XDP_PASS 시) */
struct sk_buff *xdp_build_skb_from_buff(struct xdp_buff *xdp)
{
unsigned int headroom = xdp->data - xdp->data_hard_start;
unsigned int data_len = xdp->data_end - xdp->data;
struct sk_buff *skb;
/* build_skb: 기존 버퍼에 sk_buff 메타데이터만 생성 */
skb = build_skb(xdp->data_hard_start, xdp->frame_sz);
if (!skb)
return NULL;
skb_reserve(skb, headroom);
__skb_put(skb, data_len);
/* XDP 메타데이터가 있으면 skb에 전달 */
if (xdp->data_meta != xdp->data) {
int metasize = xdp->data - xdp->data_meta;
skb_metadata_set(skb, metasize);
}
return skb;
}
/* XDP BPF 프로그램 예: 특정 포트 패킷만 PASS, 나머지 DROP */
SEC("xdp")
int xdp_filter(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if (data + sizeof(*eth) > data_end)
return XDP_DROP;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS; /* IP 아니면 일반 스택으로 */
struct iphdr *ip = data + sizeof(*eth);
if ((void *)(ip + 1) > data_end)
return XDP_DROP;
if (ip->protocol == IPPROTO_TCP) {
struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);
if ((void *)(tcp + 1) > data_end)
return XDP_DROP;
if (tcp->dest == bpf_htons(80))
return XDP_PASS; /* HTTP → sk_buff 생성 → 스택 진입 */
}
return XDP_DROP; /* sk_buff 할당 없이 즉시 드롭 */
}
/* XDP 메타데이터: xdp_buff → sk_buff 전달
* BPF 프로그램이 data_meta 영역에 커스텀 메타데이터 기록 가능 */
SEC("xdp")
int xdp_with_meta(struct xdp_md *ctx)
{
/* 메타데이터 영역 확보 (data 앞으로 4바이트) */
if (bpf_xdp_adjust_meta(ctx, -4))
return XDP_PASS;
__u32 *meta = (void *)(long)ctx->data_meta;
if ((void *)(meta + 1) > (void *)(long)ctx->data)
return XDP_PASS;
*meta = 0xCAFE; /* 커스텀 마크 */
return XDP_PASS;
/* → sk_buff 변환 후 skb_metadata_len(skb) == 4
* → TC BPF에서 __sk_buff->data_meta로 접근 가능 */
}
}}}
||XDP 액션||sk_buff 할당||동작||성능||
||{{{XDP_DROP}}}||안 함||패킷 즉시 드롭, 페이지 반환||~24Mpps (64B)||
||{{{XDP_TX}}}||안 함||같은 NIC으로 즉시 재전송||~14Mpps||
||{{{XDP_REDIRECT}}}||안 함||다른 NIC/CPU/AF_XDP로 전달||~12Mpps||
||{{{XDP_PASS}}}||생성||{{{build_skb()}}} → 일반 스택||일반 스택 수준||
||{{{XDP_ABORTED}}}||안 함||에러 발생, tracepoint 기록||—||
'''주의'''
XDP Generic vs Native: {{{XDP_FLAGS_SKB_MODE}}}(Generic)는 sk_buff가 이미 할당된 후 XDP 프로그램을 실행합니다. 따라서 XDP_DROP해도 skb 할당 비용이 발생하며, 성능 이점이 크게 감소합니다. 진정한 고성능을 위해서는 드라이버가 네이티브 XDP를 지원해야 합니다 ({{{XDP_FLAGS_DRV_MODE}}}). {{{ethtool -i eth0}}}으로 드라이버를 확인하고, {{{ip link set dev eth0 xdp obj prog.o}}}으로 로드합니다.
=== 패킷 타임스탬핑 (SO_TIMESTAMPING) ===
정밀한 네트워크 지연 측정, PTP(Precision Time Protocol) 동기화, 금융 거래 시스템 등에서 패킷의 정확한 송수신 시각이 필요합니다. Linux는 {{{SO_TIMESTAMPING}}} 소켓 옵션으로 하드웨어 타임스탬프(NIC PHY 수준)부터 소프트웨어 타임스탬프(커널 네트워크 스택)까지 다양한 수준의 타임스탬핑을 지원하며, 이 정보는 sk_buff를 통해 전달됩니다.
{{{#!plain
[SVG 텍스트 변환: 패킷 타임스탬프 삽입 지점]
캡션: HW 타임스탬프는 NIC PHY 수준(ns 정밀도), SW 타임스탬프는 커널 softirq 수준(μs 정밀도)
텍스트 요소:
- 전송 경로 (TX)
- sendmsg()
- SW TX ①
- SOF_TIMESTAMPING_TX_SOFTWARE
- SCHED TX ②
- SOF_TIMESTAMPING_TX_SCHED
- dev_queue_xmit()
- HW TX ③
- SOF_TIMESTAMPING_TX_HARDWARE
- (NIC PHY, ns 정밀도)
- 수신 경로 (RX)
- HW RX ①
- SOF_TIMESTAMPING_RX_HARDWARE
- netif_receive_skb()
- SW RX ②
- SOF_TIMESTAMPING_RX_SOFTWARE
- (ktime_get_real(), μs 정밀도)
- recvmsg() + cmsg 전달
- skb 내부 타임스탬프 저장
- skb_hwtstamps(skb)->hwtstamp
- HW 타임스탬프 (ktime_t, ns)
- skb->tstamp (= skb_mstamp_ns)
- SW 타임스탬프 (ktime_t, ns)
}}}
{{{#!plain
[코드: C]
/* include/linux/skbuff.h — 타임스탬프 관련 구조체 */
struct skb_shared_hwtstamps {
union {
ktime_t hwtstamp; /* HW 타임스탬프 (NIC PHY) */
void *netdev_data; /* 드라이버별 데이터 */
};
};
/* skb에서 HW 타임스탬프 접근 */
static inline struct skb_shared_hwtstamps *skb_hwtstamps(
struct sk_buff *skb)
{
return &skb_shinfo(skb)->hwtstamps;
}
/* NIC 드라이버: 수신 시 HW 타임스탬프 기록 */
static void my_nic_rx_hwtstamp(struct sk_buff *skb,
u64 hw_ns)
{
struct skb_shared_hwtstamps *hwts = skb_hwtstamps(skb);
hwts->hwtstamp = ns_to_ktime(hw_ns);
/* → recvmsg()에서 SOF_TIMESTAMPING_RAW_HARDWARE cmsg로 전달 */
}
/* 사용자 공간: SO_TIMESTAMPING 설정 */
int flags = SOF_TIMESTAMPING_RX_HARDWARE /* 수신 HW 타임스탬프 */
| SOF_TIMESTAMPING_TX_HARDWARE /* 전송 HW 타임스탬프 */
| SOF_TIMESTAMPING_RAW_HARDWARE /* 원시 HW 시각 (PTP 클럭) */
| SOF_TIMESTAMPING_SOFTWARE /* SW 타임스탬프 */
| SOF_TIMESTAMPING_OPT_TSONLY; /* 타임스탬프만 (페이로드 생략) */
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags));
/* recvmsg()로 타임스탬프 수신 */
struct msghdr msg = { .msg_control = cbuf, .msg_controllen = sizeof(cbuf) };
recvmsg(fd, &msg, 0);
/* cmsg에서 타임스탬프 추출 */
struct cmsghdr *cm;
for (cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) {
if (cm->cmsg_level == SOL_SOCKET &&
cm->cmsg_type == SO_TIMESTAMPING) {
struct timespec *ts = (struct timespec *)CMSG_DATA(cm);
/* ts[0] = SW 타임스탬프 (SOF_TIMESTAMPING_SOFTWARE)
* ts[1] = 예약 (사용 안 함)
* ts[2] = HW 타임스탬프 (SOF_TIMESTAMPING_RAW_HARDWARE) */
printf("HW: %ld.%09ld\n", ts[2].tv_sec, ts[2].tv_nsec);
}
}
/* TX 타임스탬프: 전송 완료 시 errqueue에서 수신 */
recvmsg(fd, &msg, MSG_ERRQUEUE);
/* → SOF_TIMESTAMPING_TX_HARDWARE cmsg에 전송 시각 포함
* → NIC 드라이버가 TX 완료 인터럽트에서 HW 타임스탬프 기록 */
}}}
||타임스탬프 종류||정밀도||지연 소스||용도||
||HW 타임스탬프 (PHY)||~1ns||NIC PTP 클럭||PTP 동기화, 금융 트레이딩||
||SW 타임스탬프 (커널)||~1μs||{{{ktime_get_real()}}} (softirq)||일반 지연 측정, tcpdump||
||TX SCHED||~1μs||qdisc 진입 시점||큐잉 지연 측정||
||TX ACK (TCP)||~1μs||ACK 수신 시점||RTT 측정||
'''팁'''
PTP 하드웨어 지원 확인: {{{ethtool -T eth0}}}으로 NIC의 HW 타임스탬핑 지원 여부를 확인합니다. {{{hardware-transmit}}}/{{{hardware-receive}}}/{{{hardware-raw-clock}}}이 표시되면 HW 타임스탬프를 사용할 수 있습니다. Intel i210/i225, Mellanox ConnectX-4+, Broadcom BCM57416 등이 대표적인 PTP 지원 NIC입니다.
=== NAPI와 GRO 상세 흐름 ===
NAPI(New API)는 인터럽트와 폴링을 결합하여 고속 패킷 수신을 효율적으로 처리합니다. GRO(Generic Receive Offload)는 NAPI poll 내부에서 동일 플로우의 패킷들을 하나의 큰 sk_buff로 병합하여 프로토콜 스택 처리 오버헤드를 줄입니다. 이 두 메커니즘은 현대 Linux 네트워크 성능의 핵심 축입니다.
{{{#!plain
[SVG 텍스트 변환: NAPI poll → GRO → 프로토콜 전달 흐름]
캡션: NAPI poll에서 GRO가 동일 플로우 패킷을 병합 → 하나의 super-skb로 프로토콜 스택 전달
텍스트 요소:
- NIC RX IRQ
- napi_schedule()
- IRQ 비활성화
- softirq 스케줄 (NET_RX)
- napi_poll()
- 최대 weight(64)개 패킷 처리
- budget 소진 → 계속 poll
- GRO 병합 엔진
- gro_list[] (해시 버킷)
- napi_gro_receive(skb)
- 각 패킷
- GRO_MERGED
- 기존 skb에 병합 (frag 추가)
- GRO_HELD
- gro_list에 대기 (더 병합 기대)
- GRO_NORMAL
- 병합 불가 → 즉시 스택 전달
- gro_normal_list → netif_receive_skb_list()
- 병합된 super-skb를 일반 스택에 배치 전달
- flush/timeout
- L3/L4 프로토콜 스택 (ip_rcv → tcp_v4_rcv)
- napi_complete_done()
- IRQ 재활성화
- budget 남음
}}}
{{{#!plain
[코드: C]
/* include/linux/netdevice.h — NAPI 구조체 */
struct napi_struct {
struct list_head poll_list; /* softirq 폴링 리스트 */
unsigned long state; /* NAPI_STATE_SCHED 등 */
int weight; /* 한 번 poll에서 처리할 최대 패킷 수 (기본 64) */
int defer_hard_irqs_count;
unsigned long gro_bitmask; /* GRO 활성 프로토콜 비트맵 */
int (*poll)(struct napi_struct *, int); /* 드라이버 poll 함수 */
struct list_head rx_list; /* GRO 병합 완료 skb 리스트 */
int rx_count; /* rx_list 내 skb 수 */
struct gro_list gro_hash[GRO_HASH_BUCKETS]; /* GRO 해시 테이블 */
};
/* 드라이버 poll 함수 패턴 */
static int my_driver_poll(struct napi_struct *napi, int budget)
{
int work_done = 0;
while (work_done < budget) {
struct sk_buff *skb = my_rx_one(napi);
if (!skb)
break;
/* GRO에 전달: 동일 플로우 병합 시도 */
napi_gro_receive(napi, skb);
work_done++;
}
if (work_done < budget) {
/* budget을 다 쓰지 않음 → 패킷이 없음 → IRQ 재활성화 */
napi_complete_done(napi, work_done);
/* → gro_list flush → IRQ unmask */
}
/* budget 소진 → softirq가 다시 poll 호출 예정 */
return work_done;
}
/* GRO 내부: 병합 판단 로직 (net/core/gro.c) */
static enum gro_result dev_gro_receive(struct napi_struct *napi,
struct sk_buff *skb)
{
/* 1. 프로토콜별 GRO 콜백 호출 (inet_gro_receive 등) */
/* 2. gro_hash[]에서 동일 플로우 검색 (5-tuple 매칭) */
/* 3. 매칭 결과에 따라: */
if (same_flow && !flush) {
/* GRO_MERGED: 기존 skb에 새 패킷을 frag로 추가
* → skb_shinfo(p)->frag_list에 연결
* → p->len += skb->len (super-packet 크기 증가)
* → NAPI_GRO_CB(p)->count++ */
return GRO_MERGED;
}
if (same_flow && flush) {
/* 플로우는 같지만 병합 불가 (PSH 플래그, 순서 불일치 등)
* → 기존 skb를 flush하고 새 skb를 gro_hash에 등록 */
napi_gro_complete(napi, pp);
}
/* GRO_HELD: 새 플로우 → gro_hash에 등록하고 대기
* → 같은 플로우의 후속 패킷이 올 때까지 보류 */
list_add(&skb->list, &napi->gro_hash[hash].list);
return GRO_HELD;
}
/* GRO 병합 완료 → 일반 스택으로 전달 */
static void napi_gro_complete(struct napi_struct *napi,
struct sk_buff *skb)
{
/* 프로토콜별 GRO complete 콜백 */
/* → TCP: tcp_gro_complete() — 헤더 보정, 체크섬 설정
* → skb->ip_summed = CHECKSUM_UNNECESSARY (병합된 패킷) */
/* gro_normal_one(): rx_list에 추가 */
gro_normal_one(napi, skb, NAPI_GRO_CB(skb)->count);
/* → rx_count >= gro_normal_batch(8) 이면 배치 전달:
* gro_normal_list() → netif_receive_skb_list()
* → 한 번의 함수 호출로 여러 skb를 스택에 전달 */
}
}}}
||GRO 파라미터||기본값||조절 방법||영향||
||NAPI weight||64||{{{netif_napi_add(dev, napi, poll, weight)}}}||poll당 처리 패킷 수. 높으면 throughput↑, latency↑||
||gro_flush_timeout||0 (즉시)||{{{sysctl net.core.gro_flush_timeout}}}||0이 아니면 타이머로 flush → 병합 기회 증가||
||gro_normal_batch||8||{{{sysctl net.core.gro_normal_batch}}}||배치 전달 크기. 높으면 처리량↑, 지연↑||
||netdev_budget||300||{{{sysctl net.core.netdev_budget}}}||softirq당 전체 NAPI 처리 패킷 상한||
||busy_poll||0 (off)||{{{sysctl net.core.busy_poll}}}||소켓별 busy polling 시간 (μs)||
{{{#!plain
[코드: bash]
# GRO 병합 효과 확인
$ ethtool -S eth0 | grep gro
rx_gro_packets: 1234567 # GRO 처리된 패킷 수
rx_gro_bytes: 987654321 # GRO 처리된 바이트
# NAPI 통계 확인
$ cat /proc/net/softnet_stat
# 컬럼: processed, dropped, time_squeeze, ..., cpu_collision, received_rps, flow_limit_count
# time_squeeze > 0: budget/time 부족으로 처리 중단 → netdev_budget 증가 고려
# Busy polling 활성화 (저지연 용도)
$ sysctl -w net.core.busy_poll=50 # 50μs 폴링
$ sysctl -w net.core.busy_read=50 # 읽기 시 50μs 폴링
# GRO flush 타임아웃 설정 (병합 기회 증가)
$ sysctl -w net.core.gro_flush_timeout=20000 # 20μs
# per-NAPI 설정 (커널 6.x+)
$ echo 100 > /sys/class/net/eth0/napi/0/gro_flush_timeout
$ echo 16 > /sys/class/net/eth0/napi/0/defer_hard_irqs
}}}
'''주의'''
GRO와 Netfilter 상호작용: GRO로 병합된 super-packet은 {{{skb->len}}}이 64KB에 달할 수 있습니다. 이 상태로 Netfilter를 통과하면 conntrack 등이 정상 동작하지만, {{{iptables -m length}}} 같은 패킷 길이 기반 규칙은 예상과 다르게 동작할 수 있습니다. 필요시 {{{ethtool -K eth0 gro off}}}로 비활성화하거나, nftables의 {{{@th}}} 표현식으로 개별 세그먼트 길이를 확인하세요.
=== GSO/TSO 분할 메커니즘 상세 ===
GSO(Generic Segmentation Offload)는 커널이 대용량 sk_buff를 NIC의 MTU에 맞는 작은 세그먼트로 분할하는 메커니즘입니다. NIC이 TSO(TCP Segmentation Offload)를 지원하면 하드웨어가 분할하고, 미지원 시 커널의 {{{skb_segment()}}}가 소프트웨어로 처리합니다. 이 과정에서 {{{skb_shared_info}}}의 GSO 필드가 핵심 역할을 합니다.
{{{#!plain
[SVG 텍스트 변환: GSO/TSO 분할 흐름과 skb_shared_info GSO 필드]
캡션: HW TSO 지원 NIC은 super-packet을 그대로 전송, 미지원 시 skb_segment()가 SW로 분할
텍스트 요소:
- GSO/TSO 분할 흐름
- Super sk_buff (최대 64KB)
- skb->len = 65536
- gso_size=1448, gso_segs=45
- gso_type=SKB_GSO_TCPV4
- 전송
- HW TSO?
- Yes
- NIC 하드웨어 분할
- Super-packet 그대로 DMA → NIC이 분할
- CPU 비용 0, 최고 성능
- No
- skb_segment(skb, features)
- 소프트웨어 GSO: gso_size 기준 분할
- 분할된 개별 sk_buff 체인 (frag_list 연결)
- seg 1 (1448B)
- IP+TCP+payload
- seg 2 (1448B)
- ...
- seg 45 (나머지)
- skb_segment() 내부 동작
- 1. gso_size 기준 페이로드 분할
- 2. 각 세그먼트에 IP+TCP 헤더 복사
- 3. IP.id 순차 증가, TCP.seq 순차 증가
- 4. 마지막 세그먼트에 PSH 플래그 설정
- 분할 후 각 세그먼트의 gso_size=0, gso_segs=0 (더 이상 GSO 아님)
}}}
{{{#!plain
[코드: C]
/* skb_shared_info의 GSO 관련 필드 */
struct skb_shared_info {
unsigned short gso_size; /* 세그먼트 크기 (MSS) */
unsigned short gso_segs; /* 세그먼트 수 */
unsigned short gso_type; /* GSO 타입 비트맵 */
/* ... */
};
/* GSO 타입 상수 */
#define SKB_GSO_TCPV4 (1 << 0) /* TCP/IPv4 분할 */
#define SKB_GSO_TCPV6 (1 << 4) /* TCP/IPv6 분할 */
#define SKB_GSO_UDP_L4 (1 << 17) /* UDP L4 분할 (4.18+) */
#define SKB_GSO_GRE (1 << 6) /* GRE 터널 내부 분할 */
#define SKB_GSO_GRE_CSUM (1 << 7) /* GRE+체크섬 */
#define SKB_GSO_UDP_TUNNEL (1 << 9) /* VXLAN/Geneve */
#define SKB_GSO_UDP_TUNNEL_CSUM (1 << 10) /* 터널+체크섬 */
#define SKB_GSO_PARTIAL (1 << 13) /* 부분 GSO (외부 헤더만 HW) */
/* skb_segment(): SW GSO 분할 핵심 함수 (net/core/skbuff.c) */
struct sk_buff *skb_segment(struct sk_buff *head_skb,
netdev_features_t features)
{
struct sk_buff *segs = NULL;
unsigned int mss = skb_shinfo(head_skb)->gso_size;
unsigned int doffset = head_skb->data - skb_mac_header(head_skb);
unsigned int offset = doffset;
unsigned int tnl_hlen, headroom;
unsigned int len, nfrags;
/* 각 세그먼트에 대해: */
do {
struct sk_buff *nskb;
int hsize, size;
/* 1. 새 skb 할당 */
nskb = alloc_skb(hsize + doffset + headroom, GFP_ATOMIC);
/* 2. L2+L3+L4 헤더 복사 (공통 헤더) */
skb_copy_from_linear_data(head_skb, skb_put(nskb, doffset),
doffset);
/* 3. 페이로드를 mss 크기만큼 복사/참조 */
if (!sg && !nskb->remcsum_offload) {
/* linear 복사 */
skb_copy_from_linear_data_offset(head_skb, offset,
skb_put(nskb, size), size);
} else {
/* SG: page fragment 참조 (zero-copy) */
skb_fill_page_desc(nskb, i, frag->bv_page,
frag->bv_offset, frag_size);
}
/* 4. 각 세그먼트 고유 필드 설정 */
skb_shinfo(nskb)->gso_size = 0; /* 더 이상 GSO 아님 */
skb_shinfo(nskb)->gso_segs = 0;
skb_shinfo(nskb)->gso_type = 0;
/* 5. IP 헤더: tot_len 갱신, id 순차 증가 */
/* 6. TCP 헤더: seq 순차 증가, 마지막 seg에만 PSH */
offset += size;
} while (offset < head_skb->len);
return segs; /* 분할된 skb 체인 (next 포인터 연결) */
}
/* GSO 분할 트리거 지점: dev_queue_xmit() → validate_xmit_skb() */
static struct sk_buff *validate_xmit_skb(struct sk_buff *skb,
struct net_device *dev, bool *again)
{
netdev_features_t features = dev->features;
if (skb_is_gso(skb)) {
/* NIC이 해당 GSO 타입을 지원하는지 확인 */
if (skb_gso_ok(skb, features))
return skb; /* HW TSO: 그대로 전달 */
/* SW GSO: 커널에서 분할 */
struct sk_buff *segs = skb_gso_segment(skb, features);
consume_skb(skb); /* 원본 super-packet 해제 */
return segs;
}
return skb;
}
/* GSO 관련 유틸리티 함수 */
static inline bool skb_is_gso(const struct sk_buff *skb) {
return skb_shinfo(skb)->gso_size;
}
static inline unsigned int skb_gso_network_seglen(const struct sk_buff *skb) {
/* gso_size + L4 헤더 + L3 헤더 = 실제 세그먼트의 IP 총 길이 */
return skb_shinfo(skb)->gso_size +
skb_network_header_len(skb) + skb_transport_header_len(skb);
}
}}}
||GSO 타입||프로토콜||커널 버전||특이사항||
||{{{SKB_GSO_TCPV4}}}||TCP/IPv4||2.6+||가장 기본적인 TSO. 대부분 NIC이 HW 지원||
||{{{SKB_GSO_TCPV6}}}||TCP/IPv6||2.6+||IPv6 확장 헤더가 있으면 SW fallback 가능||
||{{{SKB_GSO_UDP_L4}}}||UDP||4.18+||UDP GSO (USO). QUIC 등에서 활용. HW 지원 희소||
||{{{SKB_GSO_GRE}}}||GRE 터널||3.10+||외부 GRE 헤더 + 내부 TCP 분할||
||{{{SKB_GSO_UDP_TUNNEL}}}||VXLAN/Geneve||3.12+||외부 UDP + 내부 TCP 분할||
||{{{SKB_GSO_PARTIAL}}}||다양||4.7+||외부 헤더만 HW, 내부는 SW. tunnel GSO 최적화||
||{{{SKB_GSO_SCTP}}}||SCTP||4.15+||SCTP 청크 기반 분할||
'''팁'''
GSO vs TSO 성능 비교: HW TSO는 CPU 비용이 0에 가깝습니다(DMA 한 번으로 64KB 전송). SW GSO는 {{{skb_segment()}}}에서 세그먼트 수만큼 메모리 할당+헤더 복사가 필요하지만, 그래도 사용자 공간에서 {{{sendmsg()}}}를 45번 호출하는 것보다 훨씬 효율적입니다. 시스콜 오버헤드를 한 번으로 줄이는 것이 GSO의 핵심 이점입니다. {{{ethtool -k eth0 | grep segmentation}}}으로 HW 지원 여부를 확인하세요.
=== skb_shared_info: frags[] vs frag_list 상세 ===
sk_buff의 비선형 데이터는 두 가지 방식으로 표현됩니다: frags[](page fragment 배열)과 frag_list(skb 체인). 이 두 구조는 목적과 사용 상황이 완전히 다르며, 혼동하면 심각한 버그가 발생합니다.
{{{#!plain
[SVG 텍스트 변환: frags[] vs frag_list 구조 비교]
캡션: frags[]는 page 배열로 SG DMA에 최적화, frag_list는 skb 체인으로 GRO/IP 재조합에 사용
텍스트 요소:
- frags[] (Scatter-Gather)
- sk_buff
- head → linear data
- len=4096, data_len=3072
- skb_shared_info
- nr_frags = 3
- frag_list = NULL
- frags[0]: page A, 1024B
- frags[1]: page B, 1024B
- frags[2]: page C, 1024B
- 물리 페이지 (struct page)
- page A
- page B
- page C
- MAX_SKB_FRAGS = 17 (보통)
- DMA SG 전송에 최적화
- NIC scatter-gather 직접 지원
- skb_add_rx_frag()로 추가
- frag_list (skb 체인)
- sk_buff (head)
- linear: IP+TCP 헤더
- len=전체, data_len=하위합
- nr_frags = 0
- frag_list → skb2
- sk_buff (skb2)
- payload part 1
- sk_buff (skb3)
- payload part 2
- next
- sk_buff (skb4)
- payload part 3
- 크기 제한 없음 (skb 체인)
- GRO 병합, IP 재조합에 사용
- 각 skb가 독립적 메타데이터
- SG DMA에 직접 사용 불가
- 전송 전 linearize 필요할 수 있음
}}}
||특성||frags[] (page fragments)||frag_list (skb chain)||
||저장 형태||{{{skb_frag_t}}} 배열 (page+offset+size)||{{{struct sk_buff}}} 연결 리스트||
||최대 개수||{{{MAX_SKB_FRAGS}}} (보통 17)||제한 없음||
||DMA SG||직접 SG 매핑 가능||불가 — linearize 또는 변환 필요||
||오버헤드||frag당 16바이트 (page+offset+size)||skb당 ~240바이트 (전체 sk_buff)||
||주요 사용처||NIC RX ({{{skb_add_rx_frag}}}), sendfile, splice||GRO 병합, IP defrag, GSO 분할 결과||
||데이터 접근||{{{skb_frag_page()}}}, {{{skb_frag_off()}}}||{{{skb_walk_frags(skb, frag_skb)}}}||
||len/data_len||{{{data_len}}} = frags 총합||{{{data_len}}} = frag_list skb들의 len 총합||
{{{#!plain
[코드: C]
/* frags[] 접근 패턴 */
struct skb_shared_info *si = skb_shinfo(skb);
for (int i = 0; i < si->nr_frags; i++) {
skb_frag_t *f = &si->frags[i];
struct page *page = skb_frag_page(f);
unsigned int off = skb_frag_off(f);
unsigned int sz = skb_frag_size(f);
/* kmap_local_page(page) + off 로 데이터 접근 */
}
/* frag_list 순회 패턴 */
struct sk_buff *frag_iter;
skb_walk_frags(skb, frag_iter) {
/* frag_iter는 frag_list의 각 skb */
process_fragment(frag_iter->data, frag_iter->len);
}
/* 전체 skb 데이터를 순차 복사하는 범용 함수 */
/* skb_copy_bits(): linear + frags[] + frag_list 모두 처리 */
int skb_copy_bits(const struct sk_buff *skb, int offset,
void *to, int len)
{
/* 1. linear 영역에서 복사 */
/* 2. frags[]에서 복사 */
/* 3. frag_list의 각 skb에서 재귀적으로 복사 */
}
/* MAX_SKB_FRAGS 계산 */
#define MAX_SKB_FRAGS (65536 / PAGE_SIZE + 1)
/* PAGE_SIZE=4096 → MAX_SKB_FRAGS=17
* 64KB 데이터를 frags로 표현하는 데 필요한 최대 페이지 수
* +1은 페이지 경계 걸침 고려 */
}}}
'''주의'''
frags[]와 frag_list 혼용 주의: 하나의 skb에 frags[]와 frag_list가 동시에 존재할 수 있습니다. {{{skb->data_len}}}은 두 영역의 합산입니다. {{{skb_linearize()}}}는 모든 비선형 데이터(frags[] + frag_list)를 linear 영역으로 합치므로, 대용량 패킷에서 호출하면 거대한 연속 메모리 할당이 필요해 실패할 수 있습니다. GRO로 병합된 64KB super-packet에 {{{skb_linearize()}}}를 호출하는 것은 안티패턴입니다.
=== VLAN 태그 처리와 sk_buff ===
Linux 커널은 VLAN 태그를 두 가지 방식으로 처리합니다: 하드웨어 가속(HW VLAN acceleration)과 소프트웨어 처리. NIC이 VLAN 태그를 추출/삽입하는 HW 가속 방식이 더 효율적이며, 대부분의 현대 NIC이 지원합니다.
{{{#!plain
[SVG 텍스트 변환: VLAN 태그 HW 가속 vs SW 처리]
캡션: HW 가속: NIC이 VLAN 태그를 RX descriptor로 추출하여 skb 메타데이터에 저장. 패킷 데이터에서 4바이트 절약
텍스트 요소:
- VLAN 태그 RX 처리: HW 가속 vs SW
- HW VLAN Acceleration (대부분의 NIC)
- NIC: VLAN 태그 추출
- RX descriptor에 기록
- __vlan_hwaccel_put_tag(skb)
- skb->vlan_tci = tag, vlan_present=1
- skb->data → IP 헤더 시작
- VLAN 태그는 skb 메타에만 존재
- 빠름!
- SW VLAN 처리 (HW 미지원 또는 QinQ)
- NIC: 원시 프레임 전달
- VLAN 태그 inline
- __vlan_get_tag(skb, &tag)
- Ethernet 프레임 내부에서 파싱
- skb_vlan_untag(skb)
- 4B VLAN 태그 제거 + skb 메타 설정
- sk_buff VLAN 관련 필드
- skb->vlan_proto
- ETH_P_8021Q (0x8100)
- 또는 ETH_P_8021AD (QinQ)
- skb->vlan_tci
- PCP(3bit) | DEI(1bit) | VID(12bit)
- skb_vlan_tag_get(skb) → VID 추출
}}}
{{{#!plain
[코드: C]
/* sk_buff의 VLAN 필드 */
struct sk_buff {
__be16 vlan_proto; /* VLAN 프로토콜 (0x8100 또는 0x88a8) */
__u16 vlan_tci; /* TCI: PCP(3) + DEI(1) + VID(12) */
/* vlan_present는 6.x에서 vlan_all로 통합 */
};
/* NIC 드라이버: HW VLAN 가속 — 수신 시 */
static void my_nic_rx(struct napi_struct *napi, u16 rx_vlan)
{
struct sk_buff *skb = napi_alloc_skb(napi, 256);
/* ... DMA 데이터 복사 (VLAN 태그 없는 프레임) ... */
if (rx_vlan) {
/* NIC이 추출한 VLAN 태그를 skb 메타에 저장 */
__vlan_hwaccel_put_tag(skb, htons(ETH_P_8021Q), rx_vlan);
/* → skb->vlan_proto = ETH_P_8021Q
* → skb->vlan_tci = rx_vlan
* → 패킷 데이터에는 VLAN 태그 없음 */
}
napi_gro_receive(napi, skb);
}
/* VLAN 태그 확인/추출 */
if (skb_vlan_tag_present(skb)) {
u16 vid = skb_vlan_tag_get_id(skb); /* VID (0~4095) */
u16 prio = skb_vlan_tag_get_prio(skb); /* PCP (0~7) */
}
/* VLAN 태그 추가/제거 (소프트웨어) */
skb_vlan_push(skb, htons(ETH_P_8021Q), vid | (prio << 13));
/* → 패킷 데이터에 4B VLAN 태그 삽입, headroom 필요 */
skb_vlan_pop(skb);
/* → 패킷에서 VLAN 태그 제거, skb 메타로 이동 */
/* QinQ (802.1ad): 이중 VLAN 태그 */
/* 외부 VLAN: skb->vlan_proto = ETH_P_8021AD, skb->vlan_tci = outer */
/* 내부 VLAN: 패킷 데이터 내 ETH_P_8021Q 태그로 존재 */
skb_vlan_push(skb, htons(ETH_P_8021AD), outer_vid);
/* → 외부 S-tag + 내부 C-tag 이중 태그 구성 */
}}}
=== TCP의 sk_buff 분할/병합/재전송 ===
TCP는 sk_buff를 가장 정교하게 활용하는 프로토콜입니다. 전송 큐의 skb를 MSS 단위로 분할하고, 수신 경로에서 인접 세그먼트를 병합하며, 재전송 시 skb를 재활용합니다. 이 과정에서 {{{TCP_SKB_CB()}}}를 통한 cb[] 활용이 핵심입니다.
{{{#!plain
[코드: C]
/* tcp_fragment(): 하나의 skb를 두 개로 분할
* 용도: MSS 변경, SACK 기반 부분 재전송, cwnd 축소 시
* net/ipv4/tcp_output.c */
int tcp_fragment(struct sock *sk, enum tcp_queue tcp_queue,
struct sk_buff *skb, u32 len, unsigned int mss_now,
gfp_t gfp)
{
struct sk_buff *buff;
int old_factor;
/* 새 skb 할당 (뒷부분 데이터용) */
buff = sk_stream_alloc_skb(sk, 0, gfp, 0);
/* 페이로드 분할: skb의 len 이후 데이터를 buff로 이동 */
skb_split(skb, buff, len);
/* TCP_SKB_CB 갱신: 시퀀스 번호 분할 */
TCP_SKB_CB(buff)->seq = TCP_SKB_CB(skb)->seq + len;
TCP_SKB_CB(buff)->end_seq = TCP_SKB_CB(skb)->end_seq;
TCP_SKB_CB(skb)->end_seq = TCP_SKB_CB(buff)->seq;
/* GSO 세그먼트 수 재계산 */
tcp_set_skb_tso_segs(skb, mss_now);
tcp_set_skb_tso_segs(buff, mss_now);
/* 전송 큐에서 skb 뒤에 buff 삽입 */
skb_append(skb, buff, &sk->sk_write_queue);
return 0;
}
/* tcp_try_coalesce(): 인접 수신 skb를 하나로 병합
* 용도: RX 경로에서 연속 세그먼트 병합 → 소켓 큐 skb 수 감소
* net/ipv4/tcp_input.c */
static bool tcp_try_coalesce(struct sock *sk,
struct sk_buff *to,
struct sk_buff *from,
bool *fragstolen)
{
/* from의 데이터를 to에 병합 가능한지 확인 */
if (TCP_SKB_CB(from)->seq != TCP_SKB_CB(to)->end_seq)
return false; /* 연속이 아님 */
if (!skb_try_coalesce(to, from, fragstolen, &delta))
return false; /* 메모리/frag 제한 초과 */
/* skb_try_coalesce: from의 frags를 to의 frags[]에 추가
* → from은 해제 가능, to->len 증가 */
TCP_SKB_CB(to)->end_seq = TCP_SKB_CB(from)->end_seq;
TCP_SKB_CB(to)->ack_seq = TCP_SKB_CB(from)->ack_seq;
return true;
}
/* tcp_collapse(): OOO(Out-of-Order) 큐에서 중복/겹침 제거
* 용도: OOO 큐의 skb가 과도하게 쌓일 때 메모리 절약
* net/ipv4/tcp_input.c */
static void tcp_collapse(struct sock *sk,
struct sk_buff_head *list,
struct rb_root *root,
struct sk_buff *head,
struct sk_buff *tail,
u32 start, u32 end)
{
/* start~end 범위의 skb들을 하나의 skb로 합침
* → 겹치는 시퀀스 번호는 제거
* → OOO 큐의 메모리 사용량 감소
* → tcp_prune_ofo_queue()에서 메모리 압박 시 호출 */
}
/* TCP 재전송: 기존 skb 재활용 */
int __tcp_retransmit_skb(struct sock *sk, struct sk_buff *skb, int segs)
{
/* 1. skb가 clone 상태면 pskb_copy()로 헤더 독립화 */
if (skb_cloned(skb)) {
struct sk_buff *nskb = pskb_copy(skb, GFP_ATOMIC);
/* 원본을 큐에서 교체 */
}
/* 2. TCP 헤더 재구성 (seq, ack, window, timestamp) */
tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC);
/* clone_it=1: 재전송 큐에 남기면서 clone 전송 */
}
}}}
||TCP skb 연산||함수||트리거 조건||skb 변화||
||분할||{{{tcp_fragment()}}}||MSS 축소, SACK partial retx||1개 skb → 2개 (seq 분할)||
||수신 병합||{{{tcp_try_coalesce()}}}||연속 세그먼트 수신||2개 skb → 1개 (frags 합체)||
||OOO 압축||{{{tcp_collapse()}}}||OOO 큐 메모리 압박||N개 skb → 1개 (데이터 병합)||
||재전송||{{{__tcp_retransmit_skb()}}}||RTO, SACK, TLP||기존 skb clone 후 재전송||
||GSO 생성||{{{tcp_write_xmit()}}}||cwnd 허용, TSQ 미달||여러 MSS를 하나의 GSO skb로||
=== BPF/TC의 __sk_buff 컨텍스트 ===
eBPF 프로그램(TC classifier, socket filter)은 커널의 {{{struct sk_buff}}}에 직접 접근하지 않고, 안전한 래퍼인 {{{struct __sk_buff}}}를 통해 접근합니다. BPF 검증기(verifier)가 이 구조체의 필드 접근을 커널 내부 sk_buff 필드로 변환합니다.
{{{#!plain
[코드: C]
/* include/uapi/linux/bpf.h — BPF 프로그램이 보는 skb 뷰 */
struct __sk_buff {
__u32 len; /* skb->len */
__u32 pkt_type; /* skb->pkt_type */
__u32 mark; /* skb->mark (읽기/쓰기) */
__u32 queue_mapping; /* skb->queue_mapping */
__u32 protocol; /* skb->protocol */
__u32 vlan_present; /* skb_vlan_tag_present(skb) */
__u32 vlan_tci; /* skb->vlan_tci */
__u32 vlan_proto; /* skb->vlan_proto */
__u32 priority; /* skb->priority (읽기/쓰기) */
__u32 ingress_ifindex; /* skb->skb_iif */
__u32 ifindex; /* skb->dev->ifindex */
__u32 tc_index; /* skb->tc_index */
__u32 cb[5]; /* skb->cb[] (TC에서 사용) */
__u32 hash; /* skb->hash */
__u32 tc_classid; /* skb->tc_classid (쓰기) */
__u32 data; /* skb->data 포인터 (패킷 시작) */
__u32 data_end; /* skb->data + skb_headlen(skb) */
__u32 napi_id; /* skb->napi_id */
__u32 family; /* sk->sk_family */
__u32 data_meta; /* skb->data - skb_metadata_len */
__u32 flow_keys; /* flow dissector 결과 */
__u64 tstamp; /* skb->tstamp (읽기/쓰기) */
__u32 wire_len; /* 원래 와이어 길이 (GSO 이전) */
__u32 gso_segs; /* skb_shinfo(skb)->gso_segs */
__u64 hwtstamp; /* skb_hwtstamps(skb)->hwtstamp */
};
/* BPF 검증기: __sk_buff 필드 접근 → 실제 skb 오프셋 변환
* net/core/filter.c — bpf_convert_ctx_access() */
static u32 bpf_convert_ctx_access(...)
{
switch (si->off) {
case offsetof(struct __sk_buff, len):
/* __sk_buff.len → skb->len 직접 매핑 */
*insn++ = BPF_LDX_MEM(BPF_W, si->dst_reg, si->src_reg,
offsetof(struct sk_buff, len));
break;
case offsetof(struct __sk_buff, data):
/* __sk_buff.data → skb->data 포인터 로드 */
*insn++ = BPF_LDX_MEM(BPF_FIELD_SIZEOF(struct sk_buff, data),
si->dst_reg, si->src_reg,
offsetof(struct sk_buff, data));
break;
}
}
/* TC-BPF 프로그램에서 skb 패킷 데이터 직접 접근 */
SEC("tc")
int tc_filter(struct __sk_buff *skb)
{
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
/* 패킷 데이터 직접 접근 (bounds check 필수!) */
struct ethhdr *eth = data;
if (data + sizeof(*eth) > data_end)
return TC_ACT_OK;
/* skb 필드 수정 */
skb->mark = 42; /* → skb->mark = 42 (netfilter/tc 마킹) */
skb->priority = 7; /* → skb->priority = 7 (QoS) */
/* 패킷 데이터 수정: bpf_skb_store_bytes() 헬퍼 사용 */
__u8 new_ttl = 64;
bpf_skb_store_bytes(skb, ETH_HLEN + offsetof(struct iphdr, ttl),
&new_ttl, sizeof(new_ttl), 0);
/* 헤더 축소/확장: encap/decap */
bpf_skb_adjust_room(skb, -14, BPF_ADJ_ROOM_MAC, 0);
/* → skb_pull(14) 효과: L2 헤더 제거 */
return TC_ACT_OK;
}
/* 주요 BPF skb 헬퍼 함수 */
/* bpf_skb_load_bytes() — 오프셋에서 N바이트 로드 (비선형 안전) */
/* bpf_skb_store_bytes() — 오프셋에 N바이트 저장 */
/* bpf_skb_pull_data() — pskb_may_pull() 래퍼 */
/* bpf_skb_change_head() — headroom 변경 (encap) */
/* bpf_skb_change_tail() — tailroom 변경 */
/* bpf_skb_adjust_room() — MAC/NET 레벨 크기 조정 */
/* bpf_skb_vlan_push/pop() — VLAN 태그 추가/제거 */
/* bpf_skb_change_proto() — L3 프로토콜 변경 (IPv4↔IPv6) */
/* bpf_skb_cgroup_id() — cgroup ID 조회 */
/* bpf_skb_get_tunnel_key() — 터널 메타데이터 조회 */
/* bpf_redirect() — 다른 인터페이스로 리다이렉트 */
/* bpf_clone_redirect() — clone 후 리다이렉트 */
}}}
'''정보'''
direct packet access vs 헬퍼: {{{data}}}/{{{data_end}}}를 통한 직접 접근은 linear 영역만 가능합니다. 비선형 데이터(frags/frag_list)에 접근하려면 {{{bpf_skb_pull_data(skb, offset)}}}로 먼저 linearize하거나, {{{bpf_skb_load_bytes()}}} 헬퍼를 사용해야 합니다. 직접 접근이 더 빠르지만, GRO로 병합된 대용량 패킷은 헤더만 linear이므로 페이로드 파싱 시 헬퍼가 필요합니다.
=== Flow Dissector와 RSS/RPS 해시 ===
Flow dissector는 skb에서 프로토콜 헤더를 파싱하여 플로우 키(5-tuple 등)를 추출하는 커널 프레임워크입니다. 추출된 키는 {{{skb->hash}}}에 저장되어 RSS(Receive Side Scaling), RPS(Receive Packet Steering), GRO 병합, 소켓 lookup 등 다양한 곳에서 플로우 기반 분배에 사용됩니다.
{{{#!plain
[SVG 텍스트 변환: Flow Dissector와 skb->hash 활용 경로]
캡션: Flow dissector가 패킷 헤더에서 5-tuple을 추출하고 해시를 계산 → RSS/RPS/GRO/소켓 분배에 사용
텍스트 요소:
- Flow Dissector → skb->hash 활용 경로
- 수신 패킷
- ETH+IP+TCP/UDP
- __skb_flow_dissect()
- L3: saddr, daddr, protocol
- L4: sport, dport
- → flow_keys 구조체 생성
- __skb_get_hash()
- flow_keys → jhash()
- → skb->hash = result
- RSS (NIC HW)
- HW 해시 → RX 큐 선택
- RPS (SW)
- hash → CPU 선택
- GRO 병합
- hash → gro_hash[] 버킷
- SO_REUSEPORT
- hash → 소켓 선택
- skb->hash 해시 타입 (skb->l4_hash, skb->sw_hash)
- HW hash (NIC RSS)
- l4_hash=1, sw_hash=0
- NIC의 Toeplitz 해시 사용
- SW hash (커널 계산)
- l4_hash=0/1, sw_hash=1
- flow dissector + jhash
}}}
{{{#!plain
[코드: C]
/* include/net/flow_dissector.h — 플로우 키 구조체 */
struct flow_keys {
struct flow_dissector_key_control control;
struct flow_dissector_key_basic basic; /* n_proto, ip_proto */
struct flow_dissector_key_addrs addrs; /* saddr, daddr */
struct flow_dissector_key_ports ports; /* sport, dport */
/* VLAN, GRE, MPLS 키도 포함 가능 */
};
/* skb->hash 계산 (lazy — 처음 접근 시 계산) */
static inline __u32 skb_get_hash(struct sk_buff *skb)
{
if (!skb->l4_hash && !skb->sw_hash)
__skb_get_hash(skb); /* flow dissector 실행 */
return skb->hash;
}
/* RPS: SW 기반 CPU 분배 (net/core/dev.c) */
static int get_rps_cpu(struct net_device *dev,
struct sk_buff *skb,
struct rps_dev_flow **rflowp)
{
u32 hash = skb_get_hash(skb);
/* hash를 CPU 수로 나눠 대상 CPU 결정 */
u32 cpu = reciprocal_scale(hash, cpumask_weight(rps_mask));
return cpu;
}
}}}
||해시 소스||설정 방법||성능||커스터마이즈||
||NIC RSS (HW)||{{{ethtool -X eth0 hkey/hfunc}}}||최고 (HW 처리)||해시 키, 해시 함수, indirection table||
||RPS (SW)||{{{/sys/class/net/eth0/queues/rx-0/rps_cpus}}}||양호 (softirq)||CPU 비트맵||
||RFS (Flow Steering)||{{{/proc/sys/net/core/rps_sock_flow_entries}}}||양호||앱이 실행 중인 CPU로 스티어링||
||XPS (TX)||{{{/sys/class/net/eth0/queues/tx-0/xps_cpus}}}||TX 큐 선택||CPU→TX 큐 매핑||
=== Encapsulation/Tunnel과 sk_buff ===
터널 프로토콜(GRE, VXLAN, Geneve, WireGuard)은 sk_buff에 외부 헤더를 추가(encap)하거나 제거(decap)합니다. 이 과정에서 headroom 관리, 헤더 포인터 재설정, inner/outer 프로토콜 구분이 핵심입니다.
{{{#!plain
[SVG 텍스트 변환: 터널 Encapsulation 시 skb 헤더 변화]
캡션: VXLAN encap: 50바이트 외부 헤더 추가. headroom 부족 시 skb_cow_head()로 재할당 필요
텍스트 요소:
- VXLAN Encapsulation 시 skb 변화
- Encapsulation 전 (원본 패킷)
- headroom
- Inner ETH
- 14B
- Inner IP
- 20B
- Inner TCP
- Payload
- data
- Encapsulation 후 (VXLAN 캡슐화)
- Outer
- ETH 14B
- IP 20B
- UDP 8B
- VXLAN
- 8B
- data (새 위치)
- skb_push(50B) = Outer ETH(14) + Outer IP(20) + Outer UDP(8) + VXLAN(8)
}}}
{{{#!plain
[코드: C]
/* VXLAN encapsulation 흐름 (drivers/net/vxlan/vxlan_core.c) */
static void vxlan_xmit_one(struct sk_buff *skb, ...)
{
int headroom = sizeof(struct iphdr) /* 20B outer IP */
+ sizeof(struct udphdr) /* 8B outer UDP */
+ sizeof(struct vxlanhdr) /* 8B VXLAN */
+ LL_RESERVED_SPACE(dst->dev); /* outer L2 */
/* 1. headroom 확보 (clone이면 독립화) */
if (skb_cow_head(skb, headroom)) {
kfree_skb(skb);
return;
}
/* 2. skb->inner_* 헤더 포인터 저장 (decap 시 복원용) */
skb_set_inner_protocol(skb, skb->protocol);
skb_set_inner_network_header(skb, skb_network_offset(skb));
skb_set_inner_transport_header(skb, skb_transport_offset(skb));
/* 3. encapsulation 플래그 설정 */
skb->encapsulation = 1;
/* 4. VXLAN 헤더 추가 */
struct vxlanhdr *vxh = (struct vxlanhdr *)__skb_push(skb, sizeof(*vxh));
vxh->vx_flags = htonl(VXLAN_HF_VNI);
vxh->vx_vni = vxlan_vni_field(vni);
/* 5. 외부 UDP 헤더 */
udp_set_csum(skb, ...);
/* 6. 외부 IP 헤더 → ip_tunnel_xmit() */
iptunnel_xmit(..., skb, ...);
/* → skb_push(IP 헤더) → skb_reset_network_header()
* → ip_local_out() → Netfilter OUTPUT → dev_queue_xmit() */
}
/* Decapsulation: 외부 헤더 제거 후 inner 헤더 복원 */
static int vxlan_rcv(struct sock *sk, struct sk_buff *skb)
{
/* 1. VXLAN 헤더 파싱 및 VNI 추출 */
struct vxlanhdr *vxh = vxlan_hdr(skb);
/* 2. 외부 헤더 제거 */
__skb_pull(skb, sizeof(struct vxlanhdr));
skb_reset_network_header(skb); /* inner IP로 재설정 */
/* 3. inner 패킷으로 프로토콜 재설정 */
skb->protocol = eth_type_trans(skb, vxlan->dev);
skb->encapsulation = 0;
/* 4. 일반 스택으로 재진입 */
netif_rx(skb);
}
/* sk_buff의 inner 헤더 포인터 */
struct sk_buff {
sk_buff_data_t inner_transport_header; /* 내부 L4 */
sk_buff_data_t inner_network_header; /* 내부 L3 */
sk_buff_data_t inner_mac_header; /* 내부 L2 */
__be16 inner_protocol; /* 내부 프로토콜 */
__u8 encapsulation:1; /* 캡슐화 여부 */
};
}}}
'''주의'''
터널과 GSO 상호작용: {{{skb->encapsulation = 1}}}이면 GSO/체크섬 오프로드가 inner 패킷 기준으로 동작합니다. NIC이 {{{NETIF_F_GSO_UDP_TUNNEL}}}을 지원하면 HW가 외부 UDP + 내부 TCP를 한 번에 분할합니다. 미지원 NIC에서는 {{{SKB_GSO_PARTIAL}}}을 사용하여 외부 헤더만 SW로, 내부 분할은 HW로 처리하는 하이브리드 방식이 가능합니다(4.7+).
=== sk_buff 할당 내부 (kmem_cache) ===
sk_buff의 메모리 할당은 일반 {{{kmalloc()}}}이 아닌 전용 SLAB 캐시({{{skbuff_head_cache}}})를 사용합니다. 이는 빈번한 할당/해제에 최적화되어 있으며, fclone(fast clone) 메커니즘으로 clone 비용을 더 줄입니다.
{{{#!plain
[코드: C]
/* net/core/skbuff.c — sk_buff SLAB 캐시 초기화 */
static struct kmem_cache *skbuff_head_cache;
static struct kmem_cache *skbuff_fclone_cache;
void __init skb_init(void)
{
/* 일반 sk_buff 캐시 */
skbuff_head_cache = kmem_cache_create(
"skbuff_head_cache",
sizeof(struct sk_buff), /* ~240바이트 */
0, /* 정렬 */
SLAB_HWCACHE_ALIGN | SLAB_PANIC,
NULL);
/* fclone 캐시: sk_buff 2개 + fclone_ref를 하나의 슬랩 객체로 */
skbuff_fclone_cache = kmem_cache_create(
"skbuff_fclone_cache",
sizeof(struct sk_buff_fclones), /* sk_buff*2 + ref */
0,
SLAB_HWCACHE_ALIGN | SLAB_PANIC,
NULL);
}
/* fclone 구조체: clone 전용 최적화 */
struct sk_buff_fclones {
struct sk_buff skb1; /* 원본 sk_buff */
struct sk_buff skb2; /* 사전 할당된 clone sk_buff */
refcount_t fclone_ref; /* 공유 참조 카운트 */
};
/* __alloc_skb: 내부 할당 로직 */
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
int flags, int node)
{
struct sk_buff *skb;
u8 *data;
if (flags & SKB_ALLOC_FCLONE) {
/* fclone 모드: 2개의 sk_buff를 한 번에 할당
* TCP 전송 경로에서 사용: 재전송 시 clone 필요 예상 */
struct sk_buff_fclones *fclones;
fclones = kmem_cache_alloc_node(skbuff_fclone_cache,
gfp_mask, node);
skb = &fclones->skb1;
skb->fclone = SKB_FCLONE_ORIG; /* 원본 표시 */
fclones->skb2.fclone = SKB_FCLONE_CLONE; /* clone 슬롯 */
} else {
/* 일반 모드: sk_buff 1개만 할당 */
skb = kmem_cache_alloc_node(skbuff_head_cache,
gfp_mask, node);
skb->fclone = SKB_FCLONE_UNAVAILABLE;
}
/* 데이터 버퍼 할당 (별도) */
size = SKB_DATA_ALIGN(size);
data = kmalloc_reserve(size + sizeof(struct skb_shared_info),
gfp_mask, node, &pfmemalloc);
skb->head = data;
skb->data = data;
skb->truesize = SKB_TRUESIZE(size);
refcount_set(&skb->users, 1);
return skb;
}
/* fclone으로 빠른 clone (별도 할당 불필요) */
struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask)
{
struct sk_buff *n;
if (skb->fclone == SKB_FCLONE_ORIG) {
/* fclone 슬롯이 사용 가능하면 할당 없이 즉시 clone */
struct sk_buff_fclones *fclones =
container_of(skb, struct sk_buff_fclones, skb1);
n = &fclones->skb2;
if (refcount_inc_not_zero(&fclones->fclone_ref)) {
/* 할당 없이 clone 완료! → kmem_cache_alloc 비용 절약 */
goto do_clone;
}
}
/* fclone 불가: 일반 할당 */
n = kmem_cache_alloc(skbuff_head_cache, gfp_mask);
do_clone:
/* sk_buff 메타데이터 복사 (데이터 버퍼 공유) */
__copy_skb_header(n, skb);
n->cloned = 1;
skb->cloned = 1;
atomic_inc(&skb_shinfo(skb)->dataref);
return n;
}
/* NAPI per-CPU 캐시 (napi_alloc_skb 최적화) */
/* NAPI 수신 경로에서는 skbuff_head_cache 대신
* per-CPU page fragment cache를 사용하여 allocation lock 경합을 회피
* → napi_alloc_cache (struct page_frag_cache)
* → 같은 page에서 연속 skb의 data 버퍼를 할당
* → TLB miss, cache miss 최소화 */
}}}
||할당 방식||캐시||크기||사용처||
||일반 skb||{{{skbuff_head_cache}}}||~240B||대부분의 skb 할당||
||fclone skb||{{{skbuff_fclone_cache}}}||~490B||TCP TX (clone 예상 시)||
||데이터 버퍼||{{{kmalloc}}} slab||가변||linear 데이터 영역||
||NAPI 수신||per-CPU page frag||PAGE_SIZE||NAPI poll 내 고속 할당||
||page_pool||per-pool 캐시||PAGE_SIZE||고성능 NIC 드라이버 (6.x+)||
'''팁'''
fclone 효과: TCP 전송 경로에서 {{{sk_stream_alloc_skb()}}}는 {{{SKB_ALLOC_FCLONE}}} 플래그로 skb를 할당합니다. 이는 재전송 시 {{{skb_clone()}}}이 별도 메모리 할당 없이 사전 할당된 슬롯을 사용하게 합니다. 고부하 TCP 서버에서 재전송율이 높을 때 {{{kmem_cache_alloc()}}} 호출 수를 크게 줄여 성능을 개선합니다. {{{slabinfo -s | grep skbuff}}}로 캐시 사용 통계를 확인할 수 있습니다.
=== 네트워크 네임스페이스와 sk_buff ===
Linux 네트워크 네임스페이스는 독립된 네트워크 스택(인터페이스, 라우팅, iptables, 소켓)을 제공합니다. sk_buff는 {{{skb->dev}}}를 통해 네임스페이스에 소속되며, veth, bridge 등을 통해 네임스페이스를 넘나들 때 sk_buff의 처리가 변화합니다.
{{{#!plain
[코드: C]
/* sk_buff가 속한 네트워크 네임스페이스 확인 */
static inline struct net *dev_net(const struct net_device *dev)
{
return read_pnet(&dev->nd_net);
}
/* skb->dev를 통해 네임스페이스 참조 */
struct net *net = dev_net(skb->dev);
/* → net->ipv4.ip_forward (포워딩 설정)
* → net->ct.nf_conntrack_hash (conntrack 해시)
* → net->loopback_dev (lo 인터페이스)
* → 모두 네임스페이스별 독립 */
/* veth: 네임스페이스 간 패킷 전달 */
/* drivers/net/veth.c — veth_xmit() */
static netdev_tx_t veth_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct veth_priv *priv = netdev_priv(dev);
struct net_device *rcv = rcu_dereference(priv->peer);
/* rcv는 다른 네임스페이스의 veth peer 디바이스 */
/* skb->dev를 peer 디바이스로 교체 → 네임스페이스 전환 */
skb->dev = rcv;
/* L2 헤더 재처리 */
skb->protocol = eth_type_trans(skb, rcv);
/* → 이제 skb는 rcv가 속한 네임스페이스에서 처리됨
* → rcv 네임스페이스의 Netfilter, 라우팅, 소켓 lookup 적용 */
if (likely(veth_forward_skb(rcv, skb, priv, rq, rcv_xdp) == NET_RX_SUCCESS))
return NETDEV_TX_OK;
/* veth_forward_skb → netif_rx() 또는 napi_gro_receive()
* → rcv 네임스페이스의 네트워크 스택에 진입 */
}
/* 네임스페이스 경계에서 주의할 skb 처리 */
/* 1. conntrack: 네임스페이스별 독립 → skb->_nfct 초기화 필요할 수 있음 */
/* 2. skb->mark: 네임스페이스 간 보존됨 → 의도하지 않은 정책 적용 주의 */
/* 3. skb->sk: NULL이 아니면 소켓 네임스페이스와 dev 네임스페이스 불일치 가능 */
}}}
'''정보'''
컨테이너 네트워킹과 skb: Docker/Kubernetes의 Pod 네트워킹은 veth 쌍을 통해 구현됩니다. 호스트 네임스페이스의 veth에서 {{{dev_queue_xmit(skb)}}}를 호출하면 skb->dev가 컨테이너 네임스페이스의 peer veth로 교체되어 {{{netif_rx()}}}로 재진입합니다. 이 과정에서 XDP는 veth 드라이버에서 실행되어 컨테이너로 진입하기 전에 패킷을 필터링/리다이렉트할 수 있습니다 (Cilium의 veth XDP 모드).
=== 관련 문서 ===
sk_buff와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.
* GSO/GRO와 네트워크 오프로드 (https://minzkn.com/linuxkernel/pages/gso-gro.html) — 체크섬 오프로드, GSO/TSO, GRO 병합
* 네트워크 스택 (https://minzkn.com/linuxkernel/pages/networking-overview.html) — 네트워크 스택 개요
* TCP (https://minzkn.com/linuxkernel/pages/tcp.html) — TCP에서의 sk_buff 사용
* Netfilter (https://minzkn.com/linuxkernel/pages/netfilter.html) — sk_buff 조작
* DMA (https://minzkn.com/linuxkernel/pages/dma.html) — sk_buff의 DMA 매핑
* 디바이스 드라이버 (https://minzkn.com/linuxkernel/pages/device-drivers.html) — 네트워크 드라이버와 sk_buff
* AF_XDP (https://minzkn.com/linuxkernel/pages/af-xdp.html) — XDP 소켓과 UMEM, zero-copy 수신
* eBPF (https://minzkn.com/linuxkernel/pages/bpf-xdp.html) — BPF 프로그램의 skb 접근과 헬퍼 함수
* 네트워크 심화 (https://minzkn.com/linuxkernel/pages/networking-advanced.html) — 고급 네트워크 최적화와 디버깅