| 검색 | ?

Difference between r1.79 and the current

@@ -1,4 +1,4 @@
#keywords minzkn,hwport,programming,home,development,linux,software,skbuff,sk_buff,skb,skb_shared_info,frags,kernel,network,inline,macro,head,tail,mac_header,network_header,header,offset
#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에 대하여]
----
@@ -12,7 +12,8 @@
[[TableOfContents]]

최근 정리 내용은 [[HTML(<a href="/linuxkernel" target="_top" title="Linux Kernel Docs - Linux 커널 개발자를 위한 종합 한글 레퍼런스"><b>리눅스 커널 정리</b> (Linux 커널 개발자를 위한 종합 한글 레퍼런스)</a>)]] 을 참고하세요.
=== 개요 ===
== 개요 ==
|Linux packet journey,napi, hardware queue,skb| 참고 영상 ||
|| [[Play(https://youtu.be/6Fl1rsxk4JQ)]] ||

@@ -23,582 +24,3777 @@
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)에는 크게 두 가지로 나뉩니다. (위 그림상에서는 좌측 상단 구름)
* 내 장비가 처리해야 하는 Local-In 목적의 패킷 (위 그림 상에서는 좌측 하단 부)
* 즉, 목적지 MAC 주소가 나의 MAC 주소인 PACKET_HOST 상황 및 목적지 IP 주소가 나의 IP 주소인 상태를 의미
* 다른 장비에게 넘겨주어야 하는 Forward 목적의 패킷으로 나뉘어집니다. (위 그림상에서는 가운데 상단부)
* 출력(Output)에도 크게 두 가지로 나뉩니다. (위 그림상에서는 우측 하단 구름)
* 다른 장비에서 인입(Input)되어 포워드(Forward)를 거쳐서 넘어온 패킷을 출력(Output)하는 Forward 목적의 패킷 (위 그림에서는 좌측 상단에서 우측 하단으로 흐르는 패킷)
* 내 장비에서 발생(생성)하여 출력(Output)하려는 Local-Out 목적의 패킷 (위 그림에서는 우측 상단에서 우측 하단으로 흐르는 패킷)
* IPSec/AH/IPCOMP/... 등의 변환(XFRM, Transform) 흐름도 있습니다. 이 경우 Local-In 과정에서 다시 회기하거나 Forward 과정에서 경로가 바뀌는 등의 요소들이 존재합니다.
* Local-In 흐름으로 암호문이 인입되어 복호화하고 복호화된 평문은 다시 회기하는 흐름
* Forward 되어 dst_output 을 거쳐서 Local-Out(POSTROUTING) 흐름에서 평문을 암호화하고 암호화된 패킷의 라우팅을 다시 확인 후 dst_output 을 거쳐서 Local-Out 으로 암호문이 전송되는 흐름
* Filter/Mangle/Nat/... 등의 흐름도 있습니다. (위 그림에서 충분히 이것들은 모두 담아서 그리지는 못했으니 참고)
* Filter의 경우 PREROUTING/FORWARD/POSTROUTING/LOCALOUT 등 어느 위치에서 어떤 상태의 패킷 처리 하는지 이해 필요
* NAT의 경우 SNAT, DNAT, FNAT, XNAT 등이 어느 위치에서 처리 되는지 이해 필요
* Mangle의 경우 MSS조정, TProxy, ... 등이 어느 위치에서 처리 되는지 이해 필요
* IPSec VPN의 경우 XFRM 흐름을 이해 필요
* TTL 감소는 어느 위치에서 처리 되는지 이해 필요
* 라우팅(Routing) 은 크게 PREROUTING과 POSTROUTING 시점으로 구분하기도 하고 dst_input() / dst_output() 을 호출하는 시점전에 skb->dst를 채우는 위치를 통해서 구분할 수도 있습니다. 라우팅은 각기 흐름 요소 중간중간 패킷의 변화가 있을 때 갱신될 수 있습니다.
* Forward 를 결정짓는 것은 Routing 또는 Policy (XFRM policy) 라고 할 수 있겠죠.
* 위 그림은 L3 Layer 흐름 기준이라고 볼 수 있으나 bridge등의 L2 layer 흐름, L4 layer 등의 관점에서 skb->data와 skb->len이 어디를 얼만큼 지정해야 하는지 이해할 수 있는지가 가장 중요한 부분이라고 생각됩니다.
* 각 Layer에서 checksum 은 어떻게 계산되고 완성되는지 이해 필요 (skb->ip_summed 이해 필요, Checksum Offload 기능에는 어떤 것이 있고 어떻게 처리되는지 이해 필요)
* 참고: [wiki:rfc1071checksum Computing the Internet Checksum (RFC1071)]
* skb를 통하여 packet data를 접근할 때 포인팅관점을 정확히 이해해야 하며 shared data 구조도 고려해야 합니다.
* Scatter-Gather 또한 이해하면 좋습니다.
* skbuff 의 설계 관점이 어디에 있는 가를 이해하면 왜 이런 구조가 나와야 할까에 대한 의문을 해소하는데 도움이 됩니다.
* skb 하나가 인입된 후에 조작 / 추가 / 합치는 등이 있어도 skb 포인터의 변화를 최소화 하는 관점
* 복사 및 할당에 대한 overhead를 줄이는 방법을 제공하는 관점
* headroom / tailroom / shared / scatter&gather / page
* Layer초점이 어디에 있는 가를 포인팅하는 관점
* 성능과 메모리 사용 효율의 균형 관점
* skb mempool / queue
* driver에서의 split 구현 (인입된 크기와 관계 없이 더 작게 쪼갤수록 queue 사용률에 대한 효율은 증가)
=== skbuff 의 주요 개괄적 특성 및 주의 사항 요점 정리 ===
== sk_buff 자료구조 == 
[[attachment:sk_buff-structure-0-20220812.png]]
* 'struct sk_buff'는 크게 그 자신의 구조체 sk_buff 와 linear-data buffer 부분 (위 그림에서 'data size' 에 해당하는 부분. alloc_skb 함수에 주어지는 size 인자는 이 것을 의미합니다.) 그리고 frag/paged data부분 ('struct skb_shared_info')를 가지고 있습니다
* 보통 'struct sk_buff' 와 data buffer 를 각각 할당하고 data buffer 는 실제 data를 저장하는 부분과 단편화(frag/paged) 된 다른 data를 가르키는 'struct skb_shared_info'를 갖습니다. (이것은 할당의 효율과 최소한의 복사구조를 갖기 위한 구조라고 생각하고 보시면 이해가 용이합니다.)
* packet flow 상에서 항상 head <= data <= tail <= end 조건을 유지하도록 구현합니다. (일단, 이것을 위반하면 동작이 폭주할 수 있습니다.)
* 또한 특정 layer 처리 시점에서 포인팅이 무효화될 수 있습니다.
* {{{head [<= headroom] <= data( [mac_header <=] network_header [<= transport_header] ) <= tail [<= tailroom] <= end (end - head <= truesize - sizeof(struct sk_buff) - sizeof(struct skb_shared_info))}}} 조건정도로 정리할 수 있어보입니다.
* 이 때 mac_header는 이 조건을 지킬 수 없으면 (Local-Out 또는 RX 시점 이후에 발생할 수 있음) 무효상태로 표시됩니다. 무효상태가 되면 skb_mac_header_was_set(skb) 호출로 확인해볼 때 0을 반환하게 됩니다.
* data와 len은 head에서 tail 사이를 Layer처리 단계에 따라서 포인팅을 이동하면서 처리합니다.
* data + len은 tail을 초과할 수 있습니다. 하지만 data + len - data_len은 tail을 넘지 않아야 합니다. (skb_shared_info 에 기인함.)
* 즉, tail은 data + len - data_len 이 되어야 할 겁니다. 하지만 tail 은 data + len이 되는 경우는 data_len은 0이라는 조건이 필요합니다.
* skb->data 로 직접 접근할 수 있는 크기는 skb->len - skb->data_len 이며 skb->data_len이 0보다 큰 경우는 Non-Linear model 입니다. 여기서 skb->len - skb->data_len은 skb_headlen(skb) 로 확인할 수 있습니다.
* Device driver에 따라서 할당된 skb의 headroom공간은 다를 수 있습니다. (L2의 종류에 따라서 skb_reserve 로 예약되는 크기가 다르게 할 수 있다는 관점)
* head 는 실제 skb의 저장 공간의 첫 시작을 의미합니다.
* truesize 는 skb 에 할당된 실제 크기로 'struct sk_buffer' 및 'struct skb_shared_info' 를 포함한 총 할당 크기를 의미합니다.
* data 는 현재 Layer 가 다루어야 하는 위치를 의미합니다.
* data는 head 보다 앞 설수 없습니다. (즉, 항상 head <= data <= tail <= end 조건을 유지)
* skb_push(skb), skb_pull(skb) 함수가 이러한 Layer 전환 기점에 호출됩니다.
* skb_push는 data를 앞쪽 (head방향)으로 이동하고 len을 그만큼 증가 시킵니다. (즉, 현재의 data 위치를 Layer가 낮은 쪽으로 이동)
* 즉, data 가 L3 layer (network_header) 를 가르키고 있다가 L2 layer (mac_header) 로 이동하는 경우등에 사용합니다.
* skb_pull는 data를 뒤쪽 (tail방향)으로 이동하고 len을 그만큼 감소 시킵니다. (즉, 현재의 data 위치를 Layer가 높은 쪽으로 이동)
* 즉, data 가 L2 layer (mac_header) 를 가르키고 있다가 L3 layer (network_header) 로 이동하는 경우등에 사용합니다.
* skb_put은 지정한 길이만큼 tail을 end쪽으로 이동 시키고 len도 그만큼 증가시킵니다. 반환값은 tail을 이동하기 이전의 포인터 위치를 반환합니다.
* 즉, 현재 Layer에 추가 data를 덧붙이기 위한 tail 쪽 공간 확보를 할 때 사용합니다.
* skb->len : 현재 Layer 단계에서의 skb->data를 기점으로 길이 (즉, skb->data가 L4 layer 를 가르킨다면 skb->len 은 L4 크기를 의미함)
* 즉, L2 layer 시점에서는 L2의 길이를 의미하지만 L3 layer handler로 올라가면 더이상 L2 길이가 아닌 L3 길이를 나타냅니다.
* 보통은 Kernel flow 에서 L4 layer 까지 올라가는 경우가 적지만 전혀 없는 것은 아니며 이 경우도 skb->len은 L4 길이를 나타내어야 합니다. L4 layer 조작을 마치면 다시 L3 layer로 환원하는 식
* skb->data_len 은 skb->data 가 아닌 skb skb_shared_info가 가지는 크기를 의미합니다.
* skb->data + x 를 통해서 접근하는 구현을 하려고 한다면 x는 skb->len - skb->data_len을 초과하게 되면 잘못된 구현이라는 것.
* skb_shared_info가 사용되는 구조를 Non-Linear model 이라고들 합니다. 이 경우 skb->data 포인팅에서 직접적으로 접근을 보장하는 것은 L3 Header 까지만입니다. 그 이상은 skb_copy_bits 를 통해서 접근하거나 직접 skb_shared_info 로 접근해서 frags접근을 해야 합니다.
* skb->data_len이 0이면 Linear model 이며 0보다 큰 값을 가진다면 Non-linear model 입니다.
* Non-linear model 에서 shared 가 있는 경우 패킷을 조작하면 의도하지 않은 동작이 있을 수 있습니다.
* shared된 것을 독립적으로 패킷 조작하려면 skb_share_check 함수 사용을 검토해야 할 수 있습니다.
* skb를 읽기만 하는 경우는 shared 를 유지해도 되지만 변경/조작/갱신을 하려는 목적의 접근시 사전에 shared 를 해제하는 것을 고려해야 할 수 있습니다.
* tail 은 현재 skb에 유효 저장된 마지막 위치를 의미합니다.
* skb가 할당된 직후 시점에는 data 와 위치가 같은 상태에서 시작합니다. 이후 L2, L3, L4가 채워지면서 이 부분이 뒤로 늘어납니다.
* tail은 결코 end를 넘을 수 없습니다. (즉, 항상 head <= data <= tail <= end 조건을 유지)
* tail을 조정하는 것은 skb_put이 대표적입니다. (skb_put 함수는 tail과 len을 지정한 길이만큼 함께 조정합니다.)
* skb_reserve 함수도 이를 조정합니다. (skb의 data 와 함께 조정함)
* skb의 headroom 은 data 와 head 간의 격차를 headroom 으로 정의함.
* headroom과 tailroom을 확보(추가 할당)하려면 pskb_expand_head 또는 skb_copy_expand 또는 skb_cow 함수를 사용할 수 있습니다. (최하위 기본 함수는 pskb_expand_head 입니다.)
* 보통 xfrm 등에서 IPSec Encryption 할 때 UDP,ESP 등의 헤더를 추가적으로 붙이게 되는데 이 때 이러한 할당으로 확보하는 방식이 호출될 수 있습니다.
* alloc_skb 직후 일정량의 headroom 을 미리 건너뛰는 것으로도 headroom을 확보(추가 할당이 아닌 skb->data를 밀어서 확보)하는 방식의 skb_reserve 함수도 있습니다. (보통 driver에서 L2 이하 헤더를 위해서 이를 통하여 미리 예약을 지정합니다.)
* skb_reserve 함수는 보통 alloc_skb 직후의 비어있는 상태에서만 사용하는게 의미가 있습니다. (headroom을 확보하고 tailroom은 감소하는 형태의 기능)
* end 는 skb의 저장공간의 맨 마지막을 의미합니다.
* end 위치 바로 뒤에 'struct skb_shared_info' 가 위치합니다.
* mac_header 는 L2 layer 의 entry 의 위치를 의미합니다.
* skb_mac_header_was_set(skb) 함수로 체크하여 0인 경우는 L2 header 를 다루지 않는 상태의 skb임을 확인해야 함. Rx ~ forward 구간은 거의 1이며 Local-Out 등은 0인 경우가 존재
* skb->mac_len : L2 header 의 크기를 의미
* 여기서 중요한 것은 mac_header 와 network_header 사이에는 L2 header 만 꽉 채워진 것이 아니고 의미없는 공간 (Hole) 이 존재할 수 있다는 점입니다. TX시점에서 mac_header 가 network_header 바로 앞에 붙여지는 동작을 하게 됩니다.
* 이러한 Hole 을 강제로 없애기 위해서 skb_mac_header_rebuild 함수를 사용할 수 있기도 합니다. (mac header를 network header 앞으로 memmove 로 이동하는 구현)
* network_header 는 L3 layer 의 entry 의 위치를 의미합니다.
* IPSec 에서는
* Decrypt 직전에는 New IP header를 가르키지만 Decrypt 이후에는 암호문의 IV를 건너뛴 Original IP Header 를 가르킵니다.
* Decrypt 후에는 network_header 가 이동하지만 mac_header는 그대로이므로 mac_header + mac_len 부터 network_header 까지 사이에 New IP 및 ESP header 그리고 IV까지영역이 skb 유효공간의 의미로는 의미없는 Hole 상태가 됩니다.
* 즉, Decrypt 후에도 암호문내에 있는 L3+L4 평문의 위치는 변화하지 않으며 network_header 포인팅만 바뀝니다. (이 사항은 실제와 약간은 다른 설명이므로 유의!)
* transport_header 는 L4 layer 의 entry 의 위치를 의미합니다.
* headroom 은 data의 앞부분을 일컫는 용어이며 skb alloc 시 일정량 약간의 여유공간을 의미하기도 합니다. (실제로 이 양은 driver {{{[net_device]}}} 에 따라 다를 수 있음)
* tailroom 은 tail 뒷부분에 skb alloc 시 일정량 약간의 여유공간을 의미 (실제로 이 양은 driver에 따라 다를 수 있음)
* Layer 가 올라가면 (처리 handler의 계층이 올라가면) 하위 Layer 의 저장된 값은 항상이라고 볼 수 없으나 더이상 볼일이 없어야 원칙적으로 맞습니다. (IPIP/IPSec/GRE/L2TP/... 등 흐름에서 위치이동이 아닌 Hole을 두어 성능을 유지하는 것을 위해서... 어찌 보면 skb 자료구조의 포인팅 개념의 목적 중 하나)
* 그리고 Forward를 거쳐서 Layer 가 내려가면 각 하위 Layer 는 새롭게 채워지거나 shift (Layer간에 생긴 빈 공간을 정리 또는 skb 분할등) 하는게 맞습니다.
* skb_mac_header_rebuild 함수가 L2 header와 L3 header 간의 hole 을 제거하기 위한 가장 좋은 예가 되는 함수입니다.
* 여기서 head, data, tail, end 등이 주요 기점이라는 것입니다. mac_header, network_header, transport_header는 L2, L3, L4 layer 처리포인터를 의미합니다.
* mac_header와 network_header 와는 실제 RX에서 Forward 까지의 경우 network header 가 뒤로 후퇴하는 경우가 존재합니다. (Decrypt, Decapsulation 등 IPsec/GRE/Tun/IPIP/L2TP 의 Decap 동작에 의해서) 그리고 Forward 에서 TX 사이의 경우 mac_header 는 마지막에 확정되며 확정되기 전까지 mac_header는 유효하지 않거나 network_header 가 침범할 수 있습니다. (Encrypt/Encap 등의 동작에 의해서. network_header 가 mac_header를 앞서는 경우 mac_header는 invalid 상태로 처리할 수 있어야 합니다.)
* head는 저장공간 기준 첫 시작. tail은 저장유효공간의 끝을 가르키므로 tail - head 가 실제 저장된 유효공간이라고 볼 수 있습니다.
* skb->len 은 skb->data 와 관련있습니다. 이것은 RX 에서 TX 사이에 Layer handler 에 따라서 계속하여 해당 Layer 에 맞는 위치와 길이를 갖도록 MUST 구현되어야 합니다.
* skb의 위치(포인팅 의미를 갖는)를 의미하는 모든 멤버변수는 직접 접근하는 것을 하면 호환(32bits와 64bits 아키텍쳐를 기준으로 다른 기준으로 구현되기 때문) 에 문제가 생기므로 가급적 관련 inline macro를 찾아서 사용해야 합니다.
* IPSec VPN 의 경의 skb 할당시 New IP + ESP/AH + Trailer 를 고려한 skb의 headroom 및 tailroom 이 확보되는게 좋습니다.
* {{{skb->cb[]}}} : 각 Layer 처리부에서 임시로 처리할 데이터를 저장하는 용도로 사용됩니다. 이 임시공간은 정확히 skb flow를 완전히 이해해야 그 생명주기를 파악할 수 있습니다. (보통은 마지막 Layer 처리부에서 사용합니다.)
* IP defragment 하는 과정에서 현재 패킷을 잠시 모아두게 되는데 이 때도 {{{skb->cb[]}}} 에 defragment 과정에서만 사용할 정보를 담는데 이용합니다.
* 이 작은 공간이 유용할 때가 대부분은 skb 가 STOLEN 되어 잠시 작업을 보류할 때나 패킷을 암/복호화를 수행시 비동기 H/W 로 수행될 때 그 정보를 잠깐 저장할 목적으로 많이 사용합니다.
* 다소 공간이 작기 때문에 좀더 큰 임시 공간을 필요로 하는 경우는 tailroom 에서 일부를 사용하기도 합니다.
* sk_buffer 가 shared 상태(skb->users 가 1보다 큰 경우) 이면 sk_buffer 의 포인팅 및 data는 읽기만 할 수 있고 쓰기 동작을 해서는 안됩니다.
* 즉, 패킷의 변화를 만들려면 shared 상태가 아니어야 하며 clone 등을 통해서 새로운 skb를 가지고 포인팅 변화를 진행할 수 있으며 data 쓰기도 가능하려면 unclone 해야 합니다.
* skb_shared 및 skb_cloned 함수로 이러한 공유 상태를 확인 할 수 있습니다.
=== skbuff (skb) 를 지정한 NIC로 Tx 하는 방법 ===
* Direct xmit : 직접 전송하는 방법
{{{#!enscript c
struct sk_buff *skb; /* 주어진 skb 로 가정 */
struct net_device *odev; /* Tx 할 NIC의 net_device (또는 이 부분을 skb->dst 가 있는 경우 여기서 가르키는 net_device일수도 있음) */
__u16 queue_map; /* Tx 할 NIC가 Multi-queue 를 지원하는 경우 이에 대한 몇번째 txq를 선택할지 결정할 번호 (이 값의 범위는 0 부터 odev->real_num_tx_queues 사이에서 유효함) */
struct netdev_queue *txq; /* dev 의 queue_mapping 에 의해서 선택된 txq (여기에는 qdisc도 포함되어 있음) */
int ret; /* Tx 결과 상태 NETDEV_TX_XXX , NET_XMIT_XXX 등이 OR 형태로 설정 */

/* 이것으로 skb->queue_mapping 설정됨. (비슷하지만 관점이 조금 다른 함수로 skb_record_rx_queue 함수가 있음) */ 
skb_set_queue_mapping(skb, queue_map /* 값이 적절히 분배하는 알고리즘에 의해서 설정되어야 함. 특별한 알고리즘이 없다면 smp_processor_id() 대체 가능 */);
Linux 커널 네트워크 스택의 핵심 자료구조 sk_buff: 메모리 레이아웃, 데이터 조작 함수, clone/copy, 소켓(struct sock) 연동, 소켓 옵션, raw socket, 체크섬 오프로드, GSO/GRO, zero-copy 전송까지 종합 분석합니다. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.

...
'''주의'''

/* link down 상태이면 전송할없으므로 DROP */
if(!netif_running(odev) || !netif_carrier_ok(odev)) {
return(NET_XMIT_DROP);
전제 조건: 네트워크 스택 (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 네트워크 코드를 읽을없습니다.
 
* 헤더 파일: {{{<linux/skbuff.h>}}}
* 주요 소스: {{{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 검증 필요 */
}
}

/* skb->queue_mapping 에 따른 odev 의 txq를 선택 */
#if 1L
queue_map = skb_get_queue_mapping(skb);
txq = netdev_get_tx_queue(odev, queue_map);
#else
txq = skb_get_tx_queue(odev, skb);
#endif
/* 전송: CHECKSUM_PARTIAL 설정 */
skb->ip_summed = CHECKSUM_PARTIAL;
skb->csum_start = skb_transport_header(skb) - skb->head;
skb->csum_offset = offsetof(struct udphdr, check);
}}}

/* Tx lock 을 활성화 (단, LLTX 장치는 lock을 활성화 하지 않을수 있음) */
local_bh_disable();
HARD_TX_LOCK(odev, txq, smp_processor_id() /* owner cpu */);
'''팁'''

#if 1L
if(netif_tx_queue_stopped(txq)) { /* txq->state __QUEUE_STATE_DRV_XOFF bit 상태를 갖는 경우를 의미하며 전송이 가능하여도 상대방 수신측이 받기 어려운 상태등을 의미 */
/* NETDEV_TX_BUSY 간주 */
ret = NETDEV_TX_BUSY;
goto unlock;
경험적 팁: 실제 네트워크 드라이버 개발 시, {{{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;
}
#elif 1L /* txq->state __QUEUE_STATE_DRV_XOFF 또는 __QUEUE_STATE_FROZEN bit 상태를 갖는 경우를 의미하며 __QUEUE_STATE_FROZEN 상태조건은 옛날 커널에서는 BQL이 활성화되는 경우 다소 문제가 있음 */
if(netif_xmit_frozen_or_drv_stopped(txq)) {
/* NETDEV_TX_BUSY 간주 */
ret = NETDEV_TX_BUSY;
goto unlock;
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);
}
#endif
}}}

/* NIC Driver ndo_start_xmit 통하여 전송개시 (실제 구현에서는 이렇게 netdev_ops 직접 접근하기 보다는 netdev_start_xmit 같은 함수를 사용하는게 바람직) */
ret = odev->netdev_ops->ndo_start_xmit(skb, odev);
if(ret == NETDEV_TX_OK) { /* ret 값이 NETDEV_TX_OK{0} 경우는 성공이며 나머지는 전송하지 못한 경우를 의미 */
txq_trans_update(txq);
=== 고급 데이터 조작 ===
 
패킷 처리 시 데이터 레이아웃을 변경해야 하는 상황이 자주 발생합니다. 아래 함수들은 각각 다른 상황에서 사용됩니다:
 
||함수||동작||사용 시나리오||
||{{{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); /* 포인터 재취득! */
/* ... 처리 ... */
}
else {
...
 
/* 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 헤더 설정 ... */
}

/* Tx lock 해제 */
unlock:
HARD_TX_UNLOCK(odev, txq);
local_bh_enable();
/* skb_linearize: fragment가 있는 skb를 하나의 연속 버퍼로 */
if (skb_is_nonlinear(skb)) {
if (skb_linearize(skb))
goto drop; /* 메모리 부족 */
/* 이제 모든 데이터가 head~tail 사이에 연속으로 존재 */
}
}}}
* Queued xmit : queue (qdisc등...) 을 경유하여 전송하는 방법
{{{#!enscript c
struct sk_buff *skb; /* 주어진 skb 로 가정 */
struct net_device *odev; /* Tx 할 NIC의 net_device (또는 이 부분을 skb->dst 가 있는 경우 여기서 가르키는 net_device일수도 있음) */
__u16 queue_map; /* Tx 할 NIC가 Multi-queue 를 지원하는 경우 이에 대한 몇번째 txq를 선택할지 결정할 번호 (이 값의 범위는 0 부터 odev->real_num_tx_queues 사이에서 유효함) */
struct netdev_queue *txq; /* dev 의 queue_mapping 에 의해서 선택된 txq (여기에는 qdisc도 포함되어 있음) */
int ret; /* Tx 결과 상태 NETDEV_TX_XXX , NET_XMIT_XXX 등이 OR 형태로 설정 */

/* 이것으로 skb->queue_mapping 이 설정됨. (비슷하지만 관점이 조금 다른 함수로 skb_set_queue_mapping 함수가 있음) */
skb_record_rx_queue(skb, queue_map /* 이 값이 적절히 분배하는 알고리즘에 의해서 설정되어야 함. 특별한 알고리즘이 없다면 smp_processor_id() 로 대체 가능 */);
'''주의'''

...
{{{pskb_may_pull()}}} 호출 후에는 반드시 헤더 포인터를 재취득해야 합니다. 내부적으로 버퍼 재할당이 일어날 수 있어 이전 포인터가 무효화됩니다. 이 실수는 커널 네트워크 코드에서 가장 흔한 버그 패턴 중 하나입니다.

/* link down 상태이면 전송할없으므로 DROP */
if(!netif_running(odev) || !netif_carrier_ok(odev)) {
return(NET_XMIT_DROP);
=== 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만큼 추가 */
}

skb->dev = odev;
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만큼 추가 */
}

local_bh_disable();
/* skb 해제 시: destructor 콜백이 메모리 차감 */
void sock_rfree(struct sk_buff *skb)
{
struct sock *sk = skb->sk;
atomic_sub(skb->truesize, &sk->sk_rmem_alloc); 
/* → 수신 큐 메모리 사용량에서 차감 */
}

ret = dev_queue_xmit(skb);
if(ret == NETDEV_TX_OK) { /* ret 값이 NETDEV_TX_OK{0} 경우는 성공이며 나머지는 전송하지 못한 경우를 의미 */
...
/* 수신 큐 과부하 확인 — 소켓 버퍼 제한 */
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);
}
else {
...
}}}
 
||필드/콜백||방향||역할||
||{{{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 또는 기본값 */
/* ... */
}

local_bh_enable();
/* 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) */
}}}
=== "include/linux/skbuff.h" ===
==== CHECKSUM_XXX ====
{{{#!enscript c
#define CHECKSUM_NONE 0
#define CHECKSUM_UNNECESSARY 1
#define CHECKSUM_COMPLETE 2
#define CHECKSUM_PARTIAL 3

#define SKB_MAX_CSUM_LEVEL 3
'''주의'''
 
{{{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
}}}
[wiki:rfc1071checksum Computing the Internet Checksum (RFC1071)] 참고
==== "struct sk_buff" 자료형 ====
{{{#!folding 펼쳐보기 (일부 최신 커널 버젼과 다를 수 있음. 일부 생략)
{{{#!enscript c
struct sk_buff_head {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;

__u32 qlen;
spinlock_t lock;
===== 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 */
};

#if 0 // 예전 버젼
typedef struct skb_frag_struct skb_frag_t;
struct skb_frag_struct {
struct {
struct page *p;
} page;
#if (BITS_PER_LONG > 32) || (PAGE_SIZE >= 65536)
__u32 page_offset;
__u32 size; 
#else
__u16 page_offset;
__u16 size;
#endif
/* raw socket 프로토콜 해시 테이블
* protocol 번호로 해싱하여 수신 O(1) 조회 */
struct raw_hashinfo {
spinlock_t lock;
struct hlist_head ht[RAW_HTABLE_SIZE]; /* 256 버킷 */
};
#else // 최근 버젼
typedef struct bio_vec skb_frag_t;
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
 
/* 전역 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),
};
#endif
}}}

struct skb_shared_info {
__u8 __unused;
__u8 meta_len;
__u8 nr_frags;
__u8 tx_flags;
unsigned short gso_size;
/* Warning: this field is not always filled in (UFO)! */
unsigned short gso_segs;
struct sk_buff *frag_list;
struct skb_shared_hwtstamps hwtstamps;
unsigned int gso_type;
u32 tskey;
===== AF_INET SOCK_RAW 수신 경로 =====

/*
* Warning : all fields before dataref are cleared in __alloc_skb()
*/
atomic_t dataref;
IP 계층에서 패킷이 로컬로 배달될 때, TCP/UDP 디먹싱 이전에 raw socket으로의 복제가 먼저 수행됩니다. 즉, raw socket은 패킷의 사본을 받으며, 원본 skb는 정상 프로토콜 스택으로 계속 진행합니다:

/* Intermediate layers must ensure that destructor_arg
* remains valid until skb destructor */
void * destructor_arg;
{{{#!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 타입만 수신하도록 비트맵 필터를 설정할 수 있습니다:

/* must be last field, see pskb_expand_head() */ 
skb_frag_t frags[MAX_SKB_FRAGS];
{{{#!plain
[코드: C]
/* include/uapi/linux/icmp.h */
struct icmp_filter {
__u32 data; /* 비트맵: bit N이 1이면 ICMP type N을 필터링(드롭) */
};

#ifdef NET_SKBUFF_DATA_USES_OFFSET
typedef unsigned int sk_buff_data_t; 
#else
typedef unsigned char *sk_buff_data_t; 
#endif
/* 사용자 공간 예: 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 {
union {
struct {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
atomic_t users; /* 참조 카운트 (skb_get/skb_put으로 증감) */
/* ... */
};

union {
struct net_device *dev;
/* Some protocols might use this space to store information,
* while device pointer would be NULL.
* UDP receive path is one user.
*/
unsigned long dev_scratch;
};
};
struct rb_node rbnode; /* used in netem, ip4 defrag, and tcp stack */
struct list_head list;
};
/* 참조 카운트 증가 소유권 공유 */
static inline struct sk_buff *skb_get(struct sk_buff *skb)
{
refcount_inc(&skb->users);
return skb; 
}

union {
struct sock *sk;
int ip_defrag_offset;
};
/* 참조 카운트 감소 — 0이 되면 해제 */
static inline void kfree_skb(struct sk_buff *skb)
{
if (!skb) return;
if (refcount_dec_and_test(&skb->users))
__kfree_skb(skb); /* 실제 해제 */
}

union {
ktime_t tstamp;
u64 skb_mstamp_ns; /* earliest departure time */
};
/*
* This is the control buffer. It is free to use for every
* layer. Please put your private variables there. If you
* want to keep them across layers you have to do a skb_clone()
* first. This is owned by whoever has the skb queued ATM.
*/
char cb[48] __aligned(8);
/* 정상 소비 (드롭 아님) 통계 구분 */
static inline void consume_skb(struct sk_buff *skb)
{
if (!skb) return;
if (refcount_dec_and_test(&skb->users))
__kfree_skb(skb); 
}

union {
struct {
unsigned long _skb_refdst;
void (*destructor)(struct sk_buff *skb);
};
struct list_head tcp_tsorted_anchor;
};
/* 사용 예: 소켓 큐에서 꺼낸 후 해제 */
struct sk_buff *skb = skb_dequeue(&sk->sk_receive_queue);
if (skb) {
process_packet(skb);
consume_skb(skb); /* 정상 소비 */
}

#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE) 
unsigned long _nfct; 
#endif
unsigned int len,
data_len;
__u16 mac_len,
hdr_len;
/* 드롭 시 kfree_skb 사용 (디버깅 추적 가능) */
if (!pskb_may_pull(skb, sizeof(struct iphdr))) {
kfree_skb(skb); /* 드롭: perf/dropwatch로 추적됨 */
return -EINVAL;
}
}}}

/* Following fields are _not_ copied in __copy_skb_header()
* Note that queue_mapping is here mostly to fill a hole.
*/
__u16 queue_mapping;
===== 수신/송신 경로에서의 데이터 영역 변화 =====

/* if you move cloned around you also must adapt those constants */
#ifdef __BIG_ENDIAN_BITFIELD
#define CLONED_MASK (1 << 7)
#else
#define CLONED_MASK 1
#endif
#define CLONED_OFFSET() offsetof(struct sk_buff, __cloned_offset)
수신 경로: 계층에서 헤더를 제거하며 {{{data}}} 포인터가 앞으로 이동합니다.

/* private: */
__u8 __cloned_offset[0];
/* public: */
__u8 cloned:1,
nohdr:1,
fclone:2,
peeked:1,
head_frag:1,
pfmemalloc:1;
#ifdef CONFIG_SKB_EXTENSIONS
__u8 active_extensions;
#endif
/* fields enclosed in headers_start/headers_end are copied
* using a single memcpy() in __copy_skb_header()
*/
/* private: */
__u32 headers_start[0];
/* public: */
||단계||대표 함수||버퍼 레이아웃||{{{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 시작점 (헤더 제거 완료)||

/* if you move pkt_type around you also must adapt those constants */
#ifdef __BIG_ENDIAN_BITFIELD
#define PKT_TYPE_MAX (7 << 5)
#else
#define PKT_TYPE_MAX 7
#endif
#define PKT_TYPE_OFFSET() offsetof(struct sk_buff, __pkt_type_offset)
송신 경로: 계층에서 헤더를 추가하며 {{{data}}} 포인터가 뒤로 이동합니다.

/* private: */
__u8 __pkt_type_offset[0];
/* public: */
__u8 pkt_type:3;
__u8 ignore_df:1;
__u8 nf_trace:1;
__u8 ip_summed:2;
__u8 ooo_okay:1;
||단계||대표 함수||버퍼 레이아웃||{{{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 전송||

__u8 l4_hash:1;
__u8 sw_hash:1;
__u8 wifi_acked_valid:1;
__u8 wifi_acked:1;
__u8 no_fcs:1;
/* Indicates the inner headers are valid in the skbuff. */
__u8 encapsulation:1;
__u8 encap_hdr_csum:1;
__u8 csum_valid:1;
'''주의'''

#ifdef __BIG_ENDIAN_BITFIELD
#define PKT_VLAN_PRESENT_BIT 7
#else
#define PKT_VLAN_PRESENT_BIT 0
#endif
#define PKT_VLAN_PRESENT_OFFSET() offsetof(struct sk_buff, __pkt_vlan_present_offset)
/* private: */
__u8 __pkt_vlan_present_offset[0];
/* public: */
__u8 vlan_present:1;
__u8 csum_complete_sw:1;
__u8 csum_level:2;
__u8 csum_not_inet:1;
__u8 dst_pending_confirm:1;
#ifdef CONFIG_IPV6_NDISC_NODETYPE
__u8 ndisc_nodetype:2;
#endif
headroom 부족 문제: 송신 경로에서 헤더를 추가할 때 headroom이 부족하면 {{{skb_realloc_headroom()}}}이 호출되어 새로운 버퍼를 할당합니다. 이는 성능 저하를 유발하므로, 초기 할당 시 충분한 headroom을 확보하는 것이 중요합니다 ({{{NET_SKB_PAD}}} + {{{NET_IP_ALIGN}}} + 예상 헤더 크기).

__u8 ipvs_property:1;
__u8 inner_protocol_type:1;
__u8 remcsum_offload:1;
#ifdef CONFIG_NET_SWITCHDEV
__u8 offload_fwd_mark:1;
__u8 offload_l3_fwd_mark:1;
#endif
#ifdef CONFIG_NET_CLS_ACT
__u8 tc_skip_classify:1;
__u8 tc_at_ingress:1;
#endif
#ifdef CONFIG_NET_REDIRECT
__u8 redirected:1;
__u8 from_ingress:1;
#endif
#ifdef CONFIG_TLS_DEVICE
__u8 decrypted:1;
#endif
===== eth_type_trans() 호출 전후 skb 필드 변화 =====

#ifdef CONFIG_NET_SCHED
__u16 tc_index; /* traffic control index */
#endif
NIC 드라이버가 {{{eth_type_trans(skb, dev)}}}를 호출하면 다음 필드들이 갱신됩니다:

union {
__wsum csum;
struct { 
__u16 csum_start;
__u16 csum_offset;
};
};
__u32 priority;
int skb_iif;
__u32 hash;
__be16 vlan_proto;
__u16 vlan_tci;
#if defined(CONFIG_NET_RX_BUSY_POLL) || defined(CONFIG_XPS) 
union {
unsigned int napi_id;
unsigned int sender_cpu;
};
#endif
#ifdef CONFIG_NETWORK_SECMARK
__u32 secmark;
#endif
||필드||호출 전 (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}}}로 초기화됨||

union {
__u32 mark;
__u32 reserved_tailroom;
};
'''팁'''

union { 
__be16 inner_protocol;
__u8 inner_ipproto;
};
포인터 복원: {{{mac_header}}}가 설정된 뒤에는 {{{eth_hdr(skb)}}}로 Ethernet 헤더 포인터를 얻을 수 있고, {{{skb_mac_header(skb)}}}로 {{{skb->head + skb->mac_header}}}에 해당하는 포인터를 얻습니다. {{{skb->data}}}는 이미 L3 시작으로 옮겨졌으므로 혼동하지 않도록 주의하십시오.

__u16 inner_transport_header;
__u16 inner_network_header;
__u16 inner_mac_header;
==== 수신/전송 경로 요약 ====

__be16 protocol;
__u16 transport_header;
__u16 network_header;
__u16 mac_header;
수신 경로 (NIC → 앱):

/* private: */
__u32 headers_end[0];
/* public: */
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()}}}에서 데이터를 사용자 공간에 복사

/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,
*data;
unsigned int truesize;
refcount_t users;
전송 경로 (앱 NIC):

#ifdef CONFIG_SKB_EXTENSIONS
/* only useable after checking ->active_extensions != 0 */
struct skb_ext *extensions;
#endif
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)
}}}
==== SKB_DATA_ALIGN(X) ====
{{{#!enscript c
#define SKB_DATA_ALIGN(X) ALIGN(X, SMP_CACHE_BYTES)
 
'''팁'''
 
실제 서비스 경험: 제가 운영하는 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;
}
}}}
==== SKB_WITH_OVERHEAD(X) ====
{{{#!enscript c
#define SKB_WITH_OVERHEAD(X) \
((X) - SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))
 
==== 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 후에도 재취득! */
}}}
==== SKB_MAX_ORDER(X, ORDER) ====
{{{#!enscript c
#define SKB_MAX_ORDER(X, ORDER) \
SKB_WITH_OVERHEAD((PAGE_SIZE <<; (ORDER)) - (X))
 
==== 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; /* 이제 안전 */
}}}
==== SKB_MAX_HEAD(X) ====
{{{#!enscript c
#define SKB_MAX_HEAD(X) (SKB_MAX_ORDER((X), 0))
 
==== 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 기준) */
}}}
==== SKB_MAX_ALLOC ====
{{{#!enscript c
#define SKB_MAX_ALLOC (SKB_MAX_ORDER(0, 2))
 
==== 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 접근 금지 */
}}}
==== SKB_TRUESIZE(X) ====
{{{#!enscript c
/* return minimum truesize of one skb containing X bytes of data */
#define SKB_TRUESIZE(X) ((X) + \
SKB_DATA_ALIGN(sizeof(struct sk_buff)) + \
SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))
 
==== 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)); /* 이제 안전 */
}}}
* sk_buff 할당할 크기를 주어진 data크기와 함께 합산. (skb 포인터의 실제 할당 크기)
==== MAX_SKB_FRAGS ====
{{{#!enscript c
#if (65536/PAGE_SIZE + 1) < 16
#define MAX_SKB_FRAGS 16UL
#else
#define MAX_SKB_FRAGS (65536/PAGE_SIZE + 1)
#endif
 
==== 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 바로 앞으로 재정렬 */
}}}
* skb->frags 의 최대 배열요소 개수를 이 정의로 제한합니다. (이 이상의 frags 배열로 쪼개지 않는다는 것. 4K 이상의 page 기준 16개)
=== NAPI driver 요건 ===
요즘 작성되는 대부분의 NIC driver는 NAPI구조에 맞춰 작성됩니다. 이 절은 이러한 NAPI driver가 충족해야 하는 조건들을 정리합니다.
* NIC driver에서 Kernel stack으로 넘어가는 기점
* napi_gro_receive(napi /* napi context */, skb /* Kernel stack으로 넘기는 sk_buff *) 통해서 NAPI driver는 Kernel stack으로 패킷을 넘겨줍니다.
* napi_gro_receive 호출 직전 sk_buff의 포인팅 요건
* eth_type_trans 호출 (Ethernet NIC인 경우는 eth_type_trans 함수가 이러한 조정을 합니다) 직전에 skb->data는 L2 header위치를 가르키고 있어야 하며 skb->len은 L2 frame size (FCS제외) 으로 설정되어 있어야 합니다.
* eth_type_trans 호출하면서 다음과 같이 포인팅이 설정됩니다.
* skb->mac_header는 L2 header 를 가르키고 있어야 합니다.
* skb->protocol은 L2 header 의 EtherType값으로 설정되어야 합니다.
* skb->data는 L2 header 다음의 위치를 가르키고 있어야 합니다.
* skb->len은 L2를 제외한 frame size이어야 합니다. (이것은 L3 packet size를 의미하지 않습니다. skb->len은 L3 packet size보다 같거가 클 수 있습니다. Kernel stack 초입부에서 이것을 trim 하는 부분이 있을 수 있습니다.)
* Kernel stack 초입부에서는...
* skb->data가 L2 header를 건너뛴 부분에 위치하므로 skb->data 위치를 skb->network_header로 설정합니다.
* skb->mac_header와 skb->network_header의 간격을 skb->mac_len으로 설정합니다. (즉, skb->mac_len은 L2 Header 크기로 설정됩니다.) 
* 이후 적절한 gro_receive handler로 분기하며 포인팅이 조정됩니다.
=== sk_buff 주요 함수들 ===
==== skb_frag_size(frag) ====
{{{#!enscript c
static inline unsigned int skb_frag_size(const skb_frag_t *frag);
===== 포워딩 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 */
}}}
* 주어진 skb_frag_t 에서 크기 정보를 반환합니다. "frag->bv_len"을 의미합니다. (예전 커널에서는 "frag->size"를 의미)
==== skb_frag_size_set(frag, size) ====
{{{#!enscript c
static inline void skb_frag_size_set(skb_frag_t *frag, unsigned int size)
 
===== 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 헤더로 포인터 재설정 */
}}}
* 주어진 skb_frag_t 에서 크기를 설정합니다.
=== 참고자료 ===
* [[HTML(<a href="/linuxkernel" target="_top" title="Linux Kernel Docs - Linux 커널 개발자를 위한 종합 한글 레퍼런스"><b>리눅스 커널 정리</b> (Linux 커널 개발자를 위한 종합 한글 레퍼런스)</a>)]]
* '''[wiki:LinuxKernelPortingGuide 리눅스 커널 포팅 가이드]'''
* [wiki:OSI_7LayerModel OSI 7 계층모델]
* [wiki:Ethernet 이더넷 (Ethernet)]
* [wiki:ICMP ICMP(Internet Control Message Protocol)]
* [wiki:IPv4 IPv4]
* [wiki:IPv6 IPv6]
* [wiki:NAT NAT(Network Address Translation)]
* [wiki:rfc1071checksum Computing the Internet Checksum (RFC1071)] : IP, UDP, TCP protocol 에서 사용하는 checksum 알고리즘
* [wiki:AboutLinuxKernel 리눅스 커널에 대하여]
* [wiki:SlabAllocator 슬랩할당자 (Slab Allocator)]
* [wiki:XDP XDP(eXpress Data Path)]
* [wiki:HowToiptables iptables 사용법]
* [^https://www.kernel.org/ The Linux Kernel Archives]
* [^https://www.kernel.org/doc/htmldocs/networking/ch01s02.html] (Socket Buffer Functions)
* [^http://www.skbuff.net/ sk_buff{} documents and resources]
* [^https://people.cs.clemson.edu/~westall/853/notes/skbuff.pdf Management of sk_buffs]
* [^http://www.chiark.greenend.org.uk/doc/linux-doc-2.6.32/html/networking/index.html Linux Networking and Network Devices APIs] or [^http://einon.net/DocBook/networking/index.html] or [^http://docs.huihoo.com/linux/kernel/2.6.26/networking/index.html]
* [^http://www.chiark.greenend.org.uk/doc/linux-doc-2.6.32/html/networking/ch01s02.html Socket Buffer Functions]
* [^https://pr0gr4m.github.io/linux/kernel/sk_buff/ Linux Kernel SKB]
* [^http://amsekharkernel.blogspot.com/2014/08/what-is-skb-in-linux-kernel-what-are.html What is SKB in Linux kernel? What are SKB operations? Memory Representation of SKB? How to send packet out using skb operations?]
* [^https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=sysganda&logNo=30169551168]
* [^http://www.embeddedlinux.org.cn/linux_net/0596002556/understandlni-CHP-21-SECT-1.html]
* [^https://stackoverflow.com/questions/3869028/how-to-use-cryptoapi-in-the-linux-kernel-2-6]

'''주의'''
 
레이어 경계 핵심 원칙: 캡슐화/역캡슐화 후에는 {{{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/<irq_num>/smp_affinity
 
# NAPI 가중치 확인
$ ls /sys/class/net/eth0/napi
$ cat /sys/class/net/eth0/napi/<napi_id>/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) — 고급 네트워크 최적화와 디버깅




대문 / 프로그래밍 / Linux Kernel의 skbuff에 대하여

Linux Kernel의 skbuff(Socket buffer descriptors)에 대하여

Contents

1. Linux Kernel의 skbuff(Socket buffer descriptors)에 대하여
2. 개요
3. sk_buff 자료구조
3.1. 핵심 요약
3.2. 단계별 이해
3.3. 개요
3.4. struct sk_buff 주요 필드
3.4.1. 자주 사용되는 추가 필드
3.4.2. 체크섬 오프로드와 ip_summed
3.5. skb 할당과 해제
3.6. 메모리 레이아웃
3.6.1. 메모리 관련 주요 매크로
3.7. 데이터 조작 함수
3.8. Clone/Copy 메커니즘
3.9. 프래그먼트와 scatter-gather
3.10. 고급 데이터 조작
3.11. sk_buff 리스트 관리
3.12. 소켓과 sk_buff의 관계
3.12.1. struct sock 계층 구조
3.12.2. skb↔sk 바인딩과 소켓 메모리 관리
3.12.3. 소켓 옵션(setsockopt)과 sk_buff
3.12.4. Raw Socket과 sk_buff
3.12.4.1. Raw Socket 타입 비교
3.12.4.2. Raw Socket 권한 모델
3.12.4.3. 커널 내부 자료구조 — struct raw_sock
3.12.4.4. AF_INET SOCK_RAW 수신 경로
3.12.4.5. AF_INET SOCK_RAW 전송 경로
3.12.4.6. IP_HDRINCL 상세 — 커널의 보정 동작
3.12.4.7. raw_recvmsg() — 사용자 공간으로 전달
3.12.4.8. ICMP 필터 (ICMP_FILTER)
3.12.4.9. IPv6 Raw Socket (AF_INET6 SOCK_RAW)
3.12.4.10. AF_PACKET 심화 — L2 프레임 접근
3.12.4.11. TPACKET — mmap 기반 고성능 캡처
3.12.4.12. PACKET_FANOUT — 멀티코어 패킷 분산
3.12.4.13. Raw Socket과 Netfilter 관계
3.12.4.14. Raw Socket의 bind()와 connect()
3.12.4.15. 실용 예제
3.12.4.16. 보안 고려사항
3.12.5. 소켓 디먹싱과 skb 전달
3.13. Zero-copy 전송
3.14. 수신/전송 경로에서의 skb 변형
3.14.1. sk_buff 생애주기 (Lifecycle)
3.14.1.1. 참조 카운트와 메모리 관리
3.14.1.2. 수신/송신 경로에서의 데이터 영역 변화
3.14.1.3. eth_type_trans() 호출 전후 skb 필드 변화
3.14.2. 수신/전송 경로 요약
3.15. 커널 내 실제 사용 사례
3.16. 성능 튜닝 경험적 팁
3.16.1. 할당 최적화
3.16.2. Clone vs Copy 선택
3.16.3. 큐 및 스케줄링
3.16.4. Zero-Copy 경로
3.16.5. 하드웨어 offload 활용
3.17. 주의사항과 함정 (Common Mistakes)
3.17.1. 1. skb leak (메모리 누수)
3.17.2. 2. pskb_may_pull 후 포인터 미갱신
3.17.3. 3. 공유 skb 데이터 수정
3.17.4. 4. truesize 불일치
3.17.5. 5. refcount 이중 해제
3.17.6. 6. headroom 부족으로 인한 skb_under_panic
3.17.7. 7. 레이어 경계 처리 실수
3.17.7.1. RX → 포워딩: mac_header 갭 문제
3.17.7.2. 포워딩 → TX: mac_header 무효화
3.17.7.3. IPSec: headroom 확보 및 network_header 재설정
3.18. 실제 트러블슈팅 사례
3.18.1. 사례 1: 메모리 누수가 의심될 때
3.18.2. 사례 2: 체크섬 검증 실패
3.18.3. 사례 3: GRO로 인한 TCP 재전송 증가
3.18.4. 사례 4: 프래그먼트된 대용량 패킷 처리 지연
3.18.5. 사례 5: NAPI 기아 상태 (starvation)
3.19. 디버깅 기법
3.19.1. tracepoint 활용
3.19.2. perf probe로 동적 추적
3.19.3. /proc/net 진단
3.19.4. 디버깅 커널 옵션
3.20. 커널 버전별 변경사항
3.21. skb 확장 (skb_ext)
3.22. page_pool 기반 고성능 할당
3.23. XDP와 sk_buff 인터페이스
3.24. 패킷 타임스탬핑 (SO_TIMESTAMPING)
3.25. NAPI와 GRO 상세 흐름
3.26. GSO/TSO 분할 메커니즘 상세
3.27. skb_shared_info: frags[] vs frag_list 상세
3.28. VLAN 태그 처리와 sk_buff
3.29. TCP의 sk_buff 분할/병합/재전송
3.30. BPF/TC의 __sk_buff 컨텍스트
3.31. Flow Dissector와 RSS/RPS 해시
3.32. Encapsulation/Tunnel과 sk_buff
3.33. sk_buff 할당 내부 (kmem_cache)
3.34. 네트워크 네임스페이스와 sk_buff
3.35. 관련 문서

2. 개요

Linux packet journey,napi, hardware queue,skb
참고 영상

Linux kernel 에서 sk_buff (skb) 자료형은 network packet 을 처리하는데 중요한 부분입니다. 이 문서는 이를 설명하기 위해서 작성되었습니다.

이 문서는 저 혼자 모든 것을 이해하고 작성한 것이 절대로 아니며 수 많은 검색과 분석을 통하여 먼저 선두에서 정보를 공유해주신 수많은 이름 모를 선배님들의 발자취에 의해서 작성한 것입니다. 개인 적인 관점에서의 해석이 틀릴 수 있으며 이러한 부분을 알려주시면 내용을 갱신하겠습니다.

sk_buff (skb) 는 대략 다음과 같은 흐름내에서 데이터를 다루는게 목적입니다. 여기서 패킷의 인입(Input)/포워딩(Forward)/출력(Output) 이 어디서 일어나는지를 중심으로 파악되고 있어야 할 필요가 있습니다. (아래 그림은 IPSec packet 관점에서 그린것이므로 IPSec 용어만 빼고 일반 네트워크 패킷으로 가정하여 보시면 됩니다.)
An_IPsec_Packet_flow_2017.07.18_v0.4_(Linux_kernel_v3.8_vanilla_source_기준).png
[PNG image (557.04 KB)]
  • 인입(Input)에는 크게 두 가지로 나뉩니다. (위 그림상에서는 좌측 상단 구름)

3. sk_buff 자료구조


sk_buff-structure-0-20220812.png
[PNG image (177.51 KB)]

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)을 구분해 보면 왜 지연과 처리량이 달라지는지 명확해집니다.

3.1. 핵심 요약


  • 메모리 레이아웃 — head, data, tail, end 4개 포인터로 버퍼 관리. skb_push/pull/put로 데이터 영역 조작.
  • 참조 모델 — clone은 메타데이터만 복사하고 버퍼 공유, copy는 완전 복사. 참조 카운트 관리가 핵심.
  • 소켓 메모리 — sk_rmem_alloc/sk_wmem_alloctruesize 기반 소켓 버퍼 제한 구현.
  • 헤더 포인터 — 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로 변환되어 일반 스택 진입.

3.2. 단계별 이해


  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) 섹션을 참고하세요.

3.3. 개요


struct sk_buff(흔히 skb라 부름)는 Linux 네트워크 스택에서 모든 패킷을 표현하는 자료구조입니다. 수신 패킷이든 전송 패킷이든, L2 프레임이든 L4 세그먼트든, 커널 내에서 네트워크 데이터가 이동할 때는 항상 sk_buff가 사용됩니다.

O'Reilly의 "Understanding Linux Network Internals"에서 Christian Benvenuti는 sk_buff를 "네트워크 스택의 왕(king of networking)"이라 표현했습니다. 이 자료구조를 이해하지 않고는 Linux 네트워크 코드를 읽을 수 없습니다.

  • 헤더 파일: <linux/skbuff.h>
  • 주요 소스: net/core/skbuff.c
  • 구조체 크기: x86_64에서 약 232바이트 (커널 6.x 기준)

3.4. struct sk_buff 주요 필드


[코드: 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;          /* 참조 카운트 */
};

3.4.1. 자주 사용되는 추가 필드


위의 핵심 필드 외에도, sk_buff에는 프로토콜 처리와 성능 최적화에 쓰이는 중요한 필드들이 있습니다:

[코드: 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)
hash32비트수신 패킷의 flow hash (RSS, RPS에 활용)skb_get_hash(skb)
pkt_type3비트패킷 수신 유형: HOST, BROADCAST, MULTICAST, OTHERHOST직접 접근
ip_summed2비트체크섬 오프로드 모드 (NONE/UNNECESSARY/COMPLETE/PARTIAL)직접 접근
mark32비트netfilter, tc, 라우팅 결정에 사용되는 패킷 마크직접 접근
queue_mapping16비트멀티큐 NIC에서 TX/RX 큐 선택skb_get_queue_mapping(skb)
napi_id32비트NAPI 인스턴스 식별 (SO_BUSY_POLL 연동)직접 접근

정보

cb[48]은 각 프로토콜 레이어가 자신만의 구조체로 캐스팅하여 사용합니다. 예를 들어 TCP는 TCP_SKB_CB(skb)struct tcp_skb_cb에 시퀀스 번호, 타임스탬프 등을 저장합니다. 한 레이어가 cb를 사용하면 다음 레이어에서 덮어쓰므로, 레이어 간 데이터 전달에는 사용하지 마세요.

3.4.2. 체크섬 오프로드와 ip_summed


ip_summed 필드는 NIC 하드웨어의 체크섬 오프로드 상태를 나타내는 핵심 플래그입니다:

의미 (RX)의미 (TX)
CHECKSUM_NONEHW 미지원, SW 검증 필요SW가 체크섬 계산 완료
CHECKSUM_UNNECESSARYHW 검증 완료, 유효함체크섬 불필요 (loopback 등)
CHECKSUM_COMPLETEHW가 전체 체크섬 제공사용 안 함
CHECKSUM_PARTIAL사용 안 함HW에 체크섬 계산 위임

[코드: 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로 비활성화하고 테스트하세요.

3.5. 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)내부 APISKB_ALLOC_FCLONE, SKB_ALLOC_RX 등 플래그 지정

[코드: 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()를 사용하면 드롭 모니터링 도구가 오탐을 보고합니다. 반드시 의미에 맞는 함수를 사용하세요.

3.6. 메모리 레이아웃


sk_buff의 데이터 영역은 4개의 핵심 포인터로 관리됩니다:

[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을 트리거합니다.

3.6.1. 메모리 관련 주요 매크로


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)로 정확히 넘겨야 합니다.

3.7. 데이터 조작 함수


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)

[코드: 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);

3.8. Clone/Copy 메커니즘


여러 곳에서 같은 패킷을 참조해야 할 때 (예: 패킷 미러링, tcpdump 캡처, netfilter bridge), 세 가지 복사 전략을 선택할 수 있습니다:

[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 공유
- 중간 비용
- 헤더 수정 가능
- 전체 완전 복사
- 독립적 버퍼
- 가장 느림
- 자유로운 수정

[코드: 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()를 주로 사용합니다.

3.9. 프래그먼트와 scatter-gather


대용량 패킷은 linear 데이터 외에 paged fragments를 사용합니다:

[코드: 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);
}

3.10. 고급 데이터 조작


패킷 처리 시 데이터 레이아웃을 변경해야 하는 상황이 자주 발생합니다. 아래 함수들은 각각 다른 상황에서 사용됩니다:

함수동작사용 시나리오
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에서 패킷 내용 수정 전

[코드: 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() 호출 후에는 반드시 헤더 포인터를 재취득해야 합니다. 내부적으로 버퍼 재할당이 일어날 수 있어 이전 포인터가 무효화됩니다. 이 실수는 커널 네트워크 코드에서 가장 흔한 버그 패턴 중 하나입니다.

3.11. sk_buff 리스트 관리


[코드: 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);            /* 전체 비우기 */

3.12. 소켓과 sk_buff의 관계


sk_buff의 sk 필드는 해당 패킷이 소속된 소켓(struct sock)을 가리킵니다. 이 연결은 소켓 메모리 계산, 소켓 옵션 적용, 프로토콜별 처리의 기반이 됩니다.

3.12.1. struct sock 계층 구조


커널 소켓은 3단계 계층으로 구성됩니다:

[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

[코드: 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))

3.12.2. skb↔sk 바인딩과 소켓 메모리 관리


skb->sk가 설정되면 해당 skb의 메모리 사용량이 소켓에 과금됩니다. 이 메커니즘이 SO_RCVBUF/SO_SNDBUF 제한을 실현합니다:

[코드: 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_allocRX수신 큐에 쌓인 skb의 총 truesize (SO_RCVBUF와 비교)
sk_wmem_allocTX전송 중인 skb의 총 truesize (SO_SNDBUF와 비교)
sock_rfreeRXskb 해제 시 sk_rmem_alloc 차감
sock_wfreeTXskb 해제 시 sk_wmem_alloc 차감, 전송 대기 프로세스 wakeup
skb_orphan(skb)양방향skb↔sk 연결 해제 (destructor 호출 후 sk=NULL)

정보

skb->truesize는 skb 구조체 크기 + 할당된 데이터 버퍼 크기를 합산한 값입니다. 소켓의 메모리 추적은 이 값 기반이므로, truesize가 실제와 어긋나면 SO_RCVBUF 제한이 오작동합니다. 너무 작으면 메모리 과다 사용, 너무 크면 조기 패킷 드롭이 발생합니다.

3.12.3. 소켓 옵션(setsockopt)과 sk_buff


사용자 공간의 setsockopt() 호출은 struct sock 필드를 변경하고, 이것이 skb 생성·처리에 직접 반영됩니다:

소켓 옵션레벨sock/skb 영향
SO_RCVBUFSOL_SOCKETsk->sk_rcvbuf 설정 → 수신 큐 skb 총량 제한
SO_SNDBUFSOL_SOCKETsk->sk_sndbuf 설정 → 전송 큐 skb 총량 제한
SO_MARKSOL_SOCKETsk->sk_markskb->mark로 복사 (netfilter/tc/라우팅)
SO_PRIORITYSOL_SOCKETsk->sk_priorityskb->priority로 복사 (QoS)
SO_BINDTODEVICESOL_SOCKETsk->sk_bound_dev_if → skb의 dev 제한
SO_TIMESTAMPSOL_SOCKET수신 skb에 타임스탬프 기록, recvmsg() cmsg로 전달
SO_BUSY_POLLSOL_SOCKETsk->sk_napi_id + skb->napi_id로 busy polling
IP_TOSSOL_IPinet->tos → 전송 skb IP 헤더 TOS 필드
IP_TTLSOL_IPinet->uc_ttl → 전송 skb IP 헤더 TTL 필드
IP_HDRINCLSOL_IPraw socket: 사용자가 IP 헤더를 직접 제공
TCP_NODELAYSOL_TCPNagle 알고리즘 해제 → 소량 데이터 즉시 skb 전송
TCP_CORKSOL_TCPskb 전송 지연 (cork), uncork 시 한번에 전송
UDP_CORKSOL_UDP여러 sendmsg를 하나의 skb로 합쳐 전송
UDP_GROSOL_UDP수신 UDP GRO 활성화 → 여러 패킷이 하나의 큰 skb로

[코드: 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입니다.

3.12.4. Raw Socket과 sk_buff


Raw socket(SOCK_RAW)은 프로토콜 스택의 일부를 우회하여 직접 패킷을 구성하거나 수신합니다. 일반 SOCK_STREAM/SOCK_DGRAM과 달리 커널의 L4 프로토콜 처리를 거치지 않고 skb를 직접 다루므로, 네트워크 도구(ping, traceroute, tcpdump, nmap 등)와 프로토콜 구현의 핵심입니다.

[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

3.12.4.1. Raw Socket 타입 비교

타입생성접근 계층skb 관계
IP raw socketsocket(AF_INET, SOCK_RAW, IPPROTO_XXX)L3 (IP)IP_HDRINCL 없으면 커널이 IP 헤더 생성. 수신 시 IP 헤더 포함
IP raw + IP_HDRINCLsetsockopt(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 + TPACKETsetsockopt(PACKET_VERSION, TPACKET_V3)L2 (Ethernet)mmap 기반 ring buffer → skb 없이 직접 DMA 버퍼 접근 (고성능)
Ping socketsocket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)L4 (ICMP)CAP_NET_RAW 불필요. 커널이 ICMP 헤더 id/checksum 관리

3.12.4.2. Raw Socket 권한 모델

Raw socket 생성에는 CAP_NET_RAW capability가 필요합니다. 커널은 sock_create()inet_create() 경로에서 capability를 검사합니다:

[코드: 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 대신 이 메커니즘을 사용합니다.

3.12.4.3. 커널 내부 자료구조 — struct raw_sock

[코드: 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),
};

3.12.4.4. AF_INET SOCK_RAW 수신 경로

IP 계층에서 패킷이 로컬로 배달될 때, TCP/UDP 디먹싱 이전에 raw socket으로의 복제가 먼저 수행됩니다. 즉, raw socket은 패킷의 사본을 받으며, 원본 skb는 정상 프로토콜 스택으로 계속 진행합니다:

[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) (순차 실행, 동시 아님)

[코드: 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() 루프가 해시 버킷의 모든 소켓을 순회하기 때문입니다.

3.12.4.5. AF_INET SOCK_RAW 전송 경로

Raw socket의 전송 경로는 IP_HDRINCL 옵션에 따라 두 가지로 분기됩니다:

[코드: 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;
}

3.12.4.6. IP_HDRINCL 상세 — 커널의 보정 동작

IP_HDRINCL을 설정하면 사용자가 IP 헤더를 직접 작성하지만, 커널은 안전성과 정확성을 위해 일부 필드를 자동 보정합니다:

[코드: 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 설정으로 발신 스푸핑 패킷을 차단할 수 있습니다.

3.12.4.7. raw_recvmsg() — 사용자 공간으로 전달

[코드: 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;
}

3.12.4.8. ICMP 필터 (ICMP_FILTER)

IPPROTO_ICMP raw socket에서 관심 있는 ICMP 타입만 수신하도록 비트맵 필터를 설정할 수 있습니다:

[코드: 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;
}

3.12.4.9. 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_FILTERICMPV6_FILTER — 256비트 비트맵 (struct icmp6_filter)
커널 소스net/ipv4/raw.cnet/ipv6/raw.c

[코드: 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));

3.12.4.10. AF_PACKET 심화 — L2 프레임 접근

AF_PACKET 소켓은 Ethernet 프레임 수준에서 패킷을 캡처/전송합니다. tcpdump, wireshark, dhclient, arping 등이 사용합니다.

소켓 타입수신 시 포함 헤더전송 시 필요 헤더사용 사례
AF_PACKET, SOCK_RAWEthernet + IP + L4 + 페이로드사용자가 Ethernet 헤더 포함 전체 작성tcpdump, 패킷 injection
AF_PACKET, SOCK_DGRAMIP + L4 + 페이로드 (Ethernet 제거)커널이 Ethernet 헤더 생성dhclient, 프로토콜 분석

[코드: 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 드라이버 → 물리 전송 */
}

3.12.4.11. TPACKET — mmap 기반 고성능 캡처

TPACKET(PACKET_MMAP)은 커널-사용자 간 mmap된 공유 ring buffer를 사용하여 recvmsg()/sendmsg() 시스템콜 오버헤드 없이 패킷을 교환합니다. tcpdump, libpcap, suricata 등 고성능 캡처 도구의 핵심입니다.

버전커널특징제한/이슈
TPACKET_V12.4+기본 ring buffer, 고정 크기 프레임32비트 타임스탬프, 큰 패킷 지원 불가
TPACKET_V22.6.27+VLAN 태그 보존, 64비트 타임스탬프여전히 고정 크기 프레임
TPACKET_V33.2+가변 크기 블록, 타임아웃 기반 블록 해제, 배치 처리TX ring 미지원 (V2 사용), 구현 복잡

[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

[코드: 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;
}

3.12.4.12. PACKET_FANOUT — 멀티코어 패킷 분산

여러 AF_PACKET 소켓이 동일 인터페이스에서 패킷을 분산 처리할 수 있습니다. suricata, PF_RING 대안으로 사용됩니다:

[코드: 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 단편화 재조합 후 분배 */

3.12.4.13. Raw Socket과 Netfilter 관계

Raw socket으로 전송하는 패킷도 Netfilter 체인을 통과합니다. 수신은 프로토콜 핸들러 이전(raw_local_deliver)에 처리되므로 INPUT 체인보다 먼저 clone이 발생합니다:

방향소켓 타입Netfilter 통과 여부설명
TXAF_INET SOCK_RAWOUTPUT 체인 통과raw_send_hdrinc()NF_HOOK(NF_INET_LOCAL_OUT)
TXAF_PACKET SOCK_RAWNetfilter 우회dev_queue_xmit() 직접 호출 (L3 스택 미통과)
RXAF_INET SOCK_RAWPREROUTING 이후, INPUT 이전NF_INET_PRE_ROUTING 통과 후 raw_local_deliver()
RXAF_PACKET SOCK_RAWNetfilter 이전에 수신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 체인을 우회합니다.

3.12.4.14. Raw Socket의 bind()와 connect()

[코드: 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));
/* → 해당 인터페이스의 패킷만 수신
 * → 바인딩 없으면 모든 인터페이스의 패킷 수신 */

3.12.4.15. 실용 예제

[코드: 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 헤더 + 페이로드 */

[코드: 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 드라이버로 전달 */

[코드: 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() */

3.12.4.16. 보안 고려사항

위협관련 소켓 타입방어 메커니즘
IP 스푸핑AF_INET + IP_HDRINCLrp_filter (Reverse Path Filtering), BCP 38 (uRPF)
ARP 스푸핑AF_PACKET SOCK_RAWDAI (Dynamic ARP Inspection), 정적 ARP 엔트리
패킷 스니핑AF_PACKET (ETH_P_ALL)CAP_NET_RAW 제한, 네트워크 네임스페이스 격리
프로토콜 스택 DoSSOCK_RAW 대량 전송net.core.rmem_max, sk->sk_sndbuf 제한
컨테이너 탈출AF_PACKET TPACKETCAP_NET_RAW 제거, seccomp 필터

[코드: 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을 생성해야 합니다.

3.12.5. 소켓 디먹싱과 skb 전달


수신된 skb가 올바른 소켓을 찾아가는 과정 (디먹싱):

[코드: 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_REUSEPORTBPF_PROG_TYPE_SK_REUSEPORT를 조합하면, 동일 포트를 여러 소켓이 공유하면서 BPF 프로그램으로 skb를 특정 소켓에 스티어링할 수 있습니다. 이는 nginx, envoy 등의 고성능 프록시에서 활용됩니다.

3.13. Zero-copy 전송


대용량 데이터를 전송할 때, 사용자 공간 버퍼를 커널로 복사하지 않고 직접 NIC에 전달하여 CPU 사용량과 지연을 줄일 수 있습니다:

메커니즘시스템콜동작 방식
sendfile()sendfile(out_fd, in_fd, ...)파일 → 소켓 직접 전송 (페이지 캐시 → skb frag)
splice()splice(fd_in, ..., fd_out, ...)파이프 기반 zero-copy, 파일 ↔ 소켓 모두 가능
MSG_ZEROCOPYsend(fd, buf, len, MSG_ZEROCOPY)사용자 버퍼 → skb frag (완료 통지 필요, 4.14+)

[코드: 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 절감이 일반적입니다.

3.14. 수신/전송 경로에서의 skb 변형


3.14.1. sk_buff 생애주기 (Lifecycle)


sk_buff는 네트워크 패킷의 전체 생명주기 동안 커널 메모리를 차지하며, 할당부터 해제까지 다양한 변형을 거칩니다. 다음 다이어그램은 수신(RX)과 송신(TX) 경로에서 sk_buff의 생애주기를 보여줍니다.

[SVG 텍스트 변환: sk_buff 생애주기 개요]
캡션: 먼저 개요 흐름을 보고, 아래 상세 다이어그램에서 함수 단위로 추적하면 이해가 빠릅니다.
텍스트 요소:
- 할당/초기화
- RX/TX 경로 처리
- 큐잉/해제
- `alloc_skb()`, 헤더 포인터 설정
- L2/L3/L4, Netfilter, qdisc
- 소켓 전달 또는 NIC 전송 완료
- 상세 단계는 아래 상세 다이어그램 참고

[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

3.14.1.1. 참조 카운트와 메모리 관리

[코드: 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;
}

3.14.1.2. 수신/송신 경로에서의 데이터 영역 변화

수신 경로: 각 계층에서 헤더를 제거하며 data 포인터가 앞으로 이동합니다.

단계대표 함수버퍼 레이아웃data 포인터 위치
1. DMA 복사 직후NIC RXheadroomETHIPTCPDATAtailETH 시작점
2. L2 처리eth_type_trans()headroomETHIPTCPDATAtailskb_pull(ETH_HLEN) 후 IP 시작점
3. L3 처리ip_rcv()headroomETHIPTCPDATAtailskb_pull(ip_hdr_len) 후 TCP 시작점
4. L4 처리tcp_rcv()headroomETHIPTCPDATAtailpayload 시작점 (헤더 제거 완료)

송신 경로: 각 계층에서 헤더를 추가하며 data 포인터가 뒤로 이동합니다.

단계대표 함수버퍼 레이아웃data 포인터 이동
1. payload 준비소켓 송신 준비headroomPAYLOADtailpayload 시작점
2. L4 헤더 추가tcp_transmit_skb()headroomTCPPAYLOADtailskb_push(tcp_hdr_len)
3. L3 헤더 추가ip_queue_xmit()headroomIPTCPPAYLOADskb_push(ip_hdr_len)
4. L2 헤더 추가dev_hard_start_xmit()headroomETHIPTCPPAYLOADskb_push(ETH_HLEN) 후 NIC DMA 전송

주의

headroom 부족 문제: 송신 경로에서 헤더를 추가할 때 headroom이 부족하면 skb_realloc_headroom()이 호출되어 새로운 버퍼를 할당합니다. 이는 성능 저하를 유발하므로, 초기 할당 시 충분한 headroom을 확보하는 것이 중요합니다 (NET_SKB_PAD + NET_IP_ALIGN + 예상 헤더 크기).

3.14.1.3. 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->dataL2(ETH) 헤더 시작L3 헤더 시작 (skb_pull(ETH_HLEN) 또는 VLAN 포함 크기만큼 당겨짐)
skb->lenL2 프레임 전체 크기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 시작으로 옮겨졌으므로 혼동하지 않도록 주의하십시오.

3.14.2. 수신/전송 경로 요약


수신 경로 (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 전송

3.15. 커널 내 실제 사용 사례


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패킷 필터링/수정/NATskb_make_writable() 후 헤더 수정, nf_ct_get(skb)로 conntrack
BridgeL2 포워딩, VLAN 처리skb_clone()으로 멀티캐스트 복제, skb_vlan_push/pop()
Tunnel (GRE, VXLAN)encapsulation/decapsulationskb_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_buffbuild_skb)
SCTP멀티스트리밍, 멀티호밍skb_queue_head_init()으로 청크별 큐 관리

[코드: 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;
}

3.16. 성능 튜닝 경험적 팁


커널 네트워크 스택과 NIC 드라이버를 다루면서 얻는 경험적 성능 최적화 팁들입니다:

3.16.1. 할당 최적화


  • 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 호출은 심각한 성능 저하 유발.

3.16.2. Clone vs Copy 선택


  • 읽기 전용 경로: skb_clone 사용. 데이터 버퍼 공유하므로 memcpy 1회 절약. Netfilter의 MIRROR 타겟, tcpdump가 이 패턴.
  • 헤더만 수정: pskb_copy 사용. Linear 영역만 복사하고 프래그먼트는 refcount 공유. NAT, 라우팅 변경에서 주로 사용.
  • Payload 수정 필요: skb_copy 사용. 완전 복사이므로 가장 느리지만 안전.
  • 경험적 판단: "이 패킷을 두 곳에서 동시에 수정하는가?" → 아니면 clone, 그 외면 copy.

3.16.3. 큐 및 스케줄링


  • 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_addnapi->weight(기본 64) 값을 조절. 높은 대역폭 지연이 허용되면 값을 크게, 저지연이 중요하면 작게 설정.

3.16.4. Zero-Copy 경로


  • sendfile(): 파일 전송 시 가장 효율적. 페이지 캐시 → NIC 직접 경로로 복사 최소화. HTTP 서버 정적 파일 전송에 적합.
  • MSG_ZEROCOPY: 대용량 UDP 전송에서 효과적. 10Gbps 이상에서 CPU 절약이 크게 향상됨. 단, TX 완료 대기로 추가 지연이 발생함.
  • TPACKET (mbuf): 고성능 캡처에서 필수. mmap으로 커널-사용자 공간 복사를 완전히 제거함. suricata, tcpdump -i any 참조.

3.16.5. 하드웨어 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 사용량 크게 감소.

[코드: 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 offGRO on으로 변경 시 CPU 사용량이 약 40% 감소했습니다. 하지만 특정 레거시 애플리케이션에서는 GRO로 인한 packet reordering이 문제를 일으킬 수 있어, 프로덕션 변경 전 반드시 테스트 환경에서 검증하세요.

3.17. 주의사항과 함정 (Common Mistakes)


3.17.1. 1. skb leak (메모리 누수)


[코드: 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;
}

3.17.2. 2. pskb_may_pull 후 포인터 미갱신


[코드: 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.17.3. 3. 공유 skb 데이터 수정


[코드: 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;  /* 이제 안전 */

3.17.4. 4. truesize 불일치


[코드: 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 기준) */

3.17.5. 5. refcount 이중 해제


[코드: 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 접근 금지 */

3.17.6. 6. headroom 부족으로 인한 skb_under_panic


[코드: 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));  /* 이제 안전 */

3.17.7. 7. 레이어 경계 처리 실수


RX → 포워딩 → TX 경로를 거치는 패킷은 mac_header, network_header 포인터의 유효성이 경계마다 달라집니다. 이를 무시하면 잘못된 헤더 접근이나 커널 BUG를 유발합니다.

3.17.7.1. RX → 포워딩: mac_header 갭 문제

[코드: 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 바로 앞으로 재정렬 */

3.17.7.2. 포워딩 → TX: mac_header 무효화

[코드: 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 */

3.17.7.3. IPSec: headroom 확보 및 network_header 재설정

[코드: 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) 등이 잘못된 주소를 반환하여 조용한 메모리 손상이 발생합니다.

3.18. 실제 트러블슈팅 사례


네트워크 문제를 분석하면서 자주 마주하는 skb 관련 실제 케이스들입니다:

3.18.1. 사례 1: 메모리 누수가 의심될 때


증상: ss -s/proc/net/sockstat에서 사용 중인 소켓 수가 비정상적으로 많거나, 시스템 메모리가 점진적으로 감소합니다.

[코드: 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()를 사용해서 메모리 참조가 해제되지 않음.

3.18.2. 사례 2: 체크섬 검증 실패


증상: 특정 NIC에서만 TCP/UDP 체크섬 오류가 발생하거나, 애플리케이션에서 "bad checksum" 로그가 반복됩니다.

[코드: 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.18.3. 사례 3: GRO로 인한 TCP 재전송 증가


증상: GRO 활성화 후 TCP 재전송이 증가하거나, 특정 애플리케이션에서 패킷 순서 오류 발생.

[코드: 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에서) 자주 발생.

3.18.4. 사례 4: 프래그먼트된 대용량 패킷 처리 지연


증상: 대용량 파일 전송 시 예상보다 낮은 throughput, 또는 특정 크기(예: 64KB 근처)에서 throughput 급격 감소.

[코드: 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가 타임아웃까지 대기를 위해 지연 발생.

3.18.5. 사례 5: NAPI 기아 상태 (starvation)


증상: 고대역폭 트래픽에서 일부 CPU만 max softirq time에 도달하고, 다른 CPU는 유휴 상태. 드롭이 특정 CPU에서 집중됨.

[코드: 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/<irq_num>/smp_affinity

# NAPI 가중치 확인
$ ls /sys/class/net/eth0/napi
$ cat /sys/class/net/eth0/napi/<napi_id>/poll_time

원인: IRQ가 단일 CPU에 집중되거나, NAPI weight가 너무 작아서 time slice 내에 처리를 못 함. RSS 설정과 IRQ balancing 문제.

주의

트러블슈팅 핵심 원칙: 네트워크 문제는 غالب히 상호작용하는 여러 요소(RSS, GRO, IRQ affinity, 드라이버 버그)가 복합적으로 작용합니다. 단일 변수만 바꾸고 측정하는 체계적인 접근이 필요합니다. 예를 들어 "GRO만 끄고 latency 측정" → "IRQ affinity만 바꾸고 측정" 식으로요.

3.19. 디버깅 기법


3.19.1. tracepoint 활용


[코드: 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

3.19.2. perf probe로 동적 추적


[코드: 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

3.19.3. /proc/net 진단


[코드: 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

3.19.4. 디버깅 커널 옵션


옵션기능
CONFIG_DEBUG_KMEMLEAKskb를 포함한 커널 메모리 누수 탐지
CONFIG_KASANuse-after-free, out-of-bounds 접근 탐지
CONFIG_NET_DROP_MONITOR네트워크 패킷 드롭 위치 추적
CONFIG_DEBUG_NET네트워크 스택 디버깅 assertion 활성화
CONFIG_SKB_EXTENSIONSskb extension (conntrack, bridge 등) 디버깅

3.20. 커널 버전별 변경사항


버전변경 내용
3.18skb_frag_off() 접근자 도입 (직접 필드 접근 대체)
4.14MSG_ZEROCOPY 소켓 옵션 도입
4.18UDP GSO (SKB_GSO_UDP_L4) 지원
5.0XDP에서 skb 모드 (XDP_FLAGS_SKB_MODE) 공식 지원
5.3skb_ensure_writable() 도입 (skb_make_writable 대체)
5.17page_pool 기반 skb 할당 최적화
6.0kfree_skb_reason() 도입 — 드롭 원인 추적 개선
6.2skb->csum_level 필드로 중첩 체크섬 오프로드 지원
6.8netmem 기반 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

3.21. skb 확장 (skb_ext)


커널 5.x부터 sk_buff에 가변 메타데이터를 동적으로 연결하는 skb_ext 메커니즘이 도입되었습니다. 이전에는 conntrack 포인터, IPsec secpath 등이 skb 내부에 직접 포함되어 항상 메모리를 차지했으나, skb_ext를 통해 필요할 때만 할당되어 메모리 효율이 크게 개선되었습니다.

[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() →
- 독립 복사본 생성

[코드: 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_PATHsec_path~40BIPsec/xfrm — SA 참조 배열
SKB_EXT_BRIDGE_NFnf_bridge_info~48Bbr_netfilter — 원본 포트/MAC 보존
TC_SKB_EXTtc_skb_ext~12BTC cls_act — 체인/zone/CT 메타
SKB_EXT_MPTCPmptcp_ext~24BMPTCP — DSS/DSN 매핑


성능 영향: skb_ext 도입 전, struct sec_pathstruct nf_bridge_info는 skb 내에 항상 포인터를 차지했습니다 (각 8바이트). IPsec이나 bridge를 사용하지 않는 대다수 패킷에서 이 공간이 낭비되었습니다. skb_ext 전환 후 sizeof(struct sk_buff)가 약 16바이트 줄어들었고, 이는 수백만 동시 패킷을 처리하는 환경에서 상당한 메모리 절감입니다.

3.22. page_pool 기반 고성능 할당


커널 5.17+에서 도입된 page_pool은 네트워크 드라이버의 skb 데이터 버퍼 할당을 혁신적으로 개선합니다. DMA 매핑을 캐시하고, 해제된 페이지를 재활용하며, bulk 할당으로 lock contention을 최소화합니다. mlx5, ice, i40e, bnxt 등 주요 고성능 드라이버가 page_pool을 사용합니다.

[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

[코드: 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 문제를 점검하세요.

3.23. XDP와 sk_buff 인터페이스


XDP(eXpress Data Path)는 sk_buff 할당 이전 단계에서 패킷을 처리하는 고성능 프레임워크입니다. NIC 드라이버 내부에서 xdp_buff라는 경량 구조체로 패킷을 표현하며, XDP 프로그램의 판정(verdict)에 따라 패킷을 드롭/포워딩하거나 일반 네트워크 스택으로 전달합니다.

[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로 래핑
- 성능 이점 감소

[코드: 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으로 로드합니다.

3.24. 패킷 타임스탬핑 (SO_TIMESTAMPING)


정밀한 네트워크 지연 측정, PTP(Precision Time Protocol) 동기화, 금융 거래 시스템 등에서 패킷의 정확한 송수신 시각이 필요합니다. Linux는 SO_TIMESTAMPING 소켓 옵션으로 하드웨어 타임스탬프(NIC PHY 수준)부터 소프트웨어 타임스탬프(커널 네트워크 스택)까지 다양한 수준의 타임스탬핑을 지원하며, 이 정보는 sk_buff를 통해 전달됩니다.

[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)

[코드: 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)~1nsNIC PTP 클럭PTP 동기화, 금융 트레이딩
SW 타임스탬프 (커널)~1μsktime_get_real() (softirq)일반 지연 측정, tcpdump
TX SCHED~1μsqdisc 진입 시점큐잉 지연 측정
TX ACK (TCP)~1μsACK 수신 시점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입니다.

3.25. NAPI와 GRO 상세 흐름


NAPI(New API)는 인터럽트와 폴링을 결합하여 고속 패킷 수신을 효율적으로 처리합니다. GRO(Generic Receive Offload)는 NAPI poll 내부에서 동일 플로우의 패킷들을 하나의 큰 sk_buff로 병합하여 프로토콜 스택 처리 오버헤드를 줄입니다. 이 두 메커니즘은 현대 Linux 네트워크 성능의 핵심 축입니다.

[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 남음

[코드: 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 weight64netif_napi_add(dev, napi, poll, weight)poll당 처리 패킷 수. 높으면 throughput↑, latency↑
gro_flush_timeout0 (즉시)sysctl net.core.gro_flush_timeout0이 아니면 타이머로 flush → 병합 기회 증가
gro_normal_batch8sysctl net.core.gro_normal_batch배치 전달 크기. 높으면 처리량↑, 지연↑
netdev_budget300sysctl net.core.netdev_budgetsoftirq당 전체 NAPI 처리 패킷 상한
busy_poll0 (off)sysctl net.core.busy_poll소켓별 busy polling 시간 (μs)

[코드: 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 표현식으로 개별 세그먼트 길이를 확인하세요.

3.26. GSO/TSO 분할 메커니즘 상세


GSO(Generic Segmentation Offload)는 커널이 대용량 sk_buff를 NIC의 MTU에 맞는 작은 세그먼트로 분할하는 메커니즘입니다. NIC이 TSO(TCP Segmentation Offload)를 지원하면 하드웨어가 분할하고, 미지원 시 커널의 skb_segment()가 소프트웨어로 처리합니다. 이 과정에서 skb_shared_info의 GSO 필드가 핵심 역할을 합니다.

[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 아님)

[코드: 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_TCPV4TCP/IPv42.6+가장 기본적인 TSO. 대부분 NIC이 HW 지원
SKB_GSO_TCPV6TCP/IPv62.6+IPv6 확장 헤더가 있으면 SW fallback 가능
SKB_GSO_UDP_L4UDP4.18+UDP GSO (USO). QUIC 등에서 활용. HW 지원 희소
SKB_GSO_GREGRE 터널3.10+외부 GRE 헤더 + 내부 TCP 분할
SKB_GSO_UDP_TUNNELVXLAN/Geneve3.12+외부 UDP + 내부 TCP 분할
SKB_GSO_PARTIAL다양4.7+외부 헤더만 HW, 내부는 SW. tunnel GSO 최적화
SKB_GSO_SCTPSCTP4.15+SCTP 청크 기반 분할


GSO vs TSO 성능 비교: HW TSO는 CPU 비용이 0에 가깝습니다(DMA 한 번으로 64KB 전송). SW GSO는 skb_segment()에서 세그먼트 수만큼 메모리 할당+헤더 복사가 필요하지만, 그래도 사용자 공간에서 sendmsg()를 45번 호출하는 것보다 훨씬 효율적입니다. 시스콜 오버헤드를 한 번으로 줄이는 것이 GSO의 핵심 이점입니다. ethtool -k eth0 | grep segmentation으로 HW 지원 여부를 확인하세요.

3.27. skb_shared_info: frags[] vs frag_list 상세


sk_buff의 비선형 데이터는 두 가지 방식으로 표현됩니다: frags[](page fragment 배열)과 frag_list(skb 체인). 이 두 구조는 목적과 사용 상황이 완전히 다르며, 혼동하면 심각한 버그가 발생합니다.

[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, spliceGRO 병합, IP defrag, GSO 분할 결과
데이터 접근skb_frag_page(), skb_frag_off()skb_walk_frags(skb, frag_skb)
len/data_lendata_len = frags 총합data_len = frag_list skb들의 len 총합

[코드: 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()를 호출하는 것은 안티패턴입니다.

3.28. VLAN 태그 처리와 sk_buff


Linux 커널은 VLAN 태그를 두 가지 방식으로 처리합니다: 하드웨어 가속(HW VLAN acceleration)과 소프트웨어 처리. NIC이 VLAN 태그를 추출/삽입하는 HW 가속 방식이 더 효율적이며, 대부분의 현대 NIC이 지원합니다.

[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 추출

[코드: 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 이중 태그 구성 */

3.29. TCP의 sk_buff 분할/병합/재전송


TCP는 sk_buff를 가장 정교하게 활용하는 프로토콜입니다. 전송 큐의 skb를 MSS 단위로 분할하고, 수신 경로에서 인접 세그먼트를 병합하며, 재전송 시 skb를 재활용합니다. 이 과정에서 TCP_SKB_CB()를 통한 cb[] 활용이 핵심입니다.

[코드: 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 retx1개 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로

3.30. BPF/TC의 __sk_buff 컨텍스트


eBPF 프로그램(TC classifier, socket filter)은 커널의 struct sk_buff에 직접 접근하지 않고, 안전한 래퍼인 struct __sk_buff를 통해 접근합니다. BPF 검증기(verifier)가 이 구조체의 필드 접근을 커널 내부 sk_buff 필드로 변환합니다.

[코드: 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이므로 페이로드 파싱 시 헬퍼가 필요합니다.

3.31. Flow Dissector와 RSS/RPS 해시


Flow dissector는 skb에서 프로토콜 헤더를 파싱하여 플로우 키(5-tuple 등)를 추출하는 커널 프레임워크입니다. 추출된 키는 skb->hash에 저장되어 RSS(Receive Side Scaling), RPS(Receive Packet Steering), GRO 병합, 소켓 lookup 등 다양한 곳에서 플로우 기반 분배에 사용됩니다.

[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

[코드: 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_cpusTX 큐 선택CPU→TX 큐 매핑

3.32. Encapsulation/Tunnel과 sk_buff


터널 프로토콜(GRE, VXLAN, Geneve, WireGuard)은 sk_buff에 외부 헤더를 추가(encap)하거나 제거(decap)합니다. 이 과정에서 headroom 관리, 헤더 포인터 재설정, inner/outer 프로토콜 구분이 핵심입니다.

[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)

[코드: 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+).

3.33. sk_buff 할당 내부 (kmem_cache)


sk_buff의 메모리 할당은 일반 kmalloc()이 아닌 전용 SLAB 캐시(skbuff_head_cache)를 사용합니다. 이는 빈번한 할당/해제에 최적화되어 있으며, fclone(fast clone) 메커니즘으로 clone 비용을 더 줄입니다.

[코드: 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 최소화 */

할당 방식캐시크기사용처
일반 skbskbuff_head_cache~240B대부분의 skb 할당
fclone skbskbuff_fclone_cache~490BTCP TX (clone 예상 시)
데이터 버퍼kmalloc slab가변linear 데이터 영역
NAPI 수신per-CPU page fragPAGE_SIZENAPI poll 내 고속 할당
page_poolper-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로 캐시 사용 통계를 확인할 수 있습니다.

3.34. 네트워크 네임스페이스와 sk_buff


Linux 네트워크 네임스페이스는 독립된 네트워크 스택(인터페이스, 라우팅, iptables, 소켓)을 제공합니다. sk_buff는 skb->dev를 통해 네임스페이스에 소속되며, veth, bridge 등을 통해 네임스페이스를 넘나들 때 sk_buff의 처리가 변화합니다.

[코드: 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 모드).

3.35. 관련 문서


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



Copyright ⓒ MINZKN.COM
All Rights Reserved.