네트워크 스택(Network Stack) 개요 (Network Stack Overview)
Linux 커널 네트워크 스택의 전체 처리 경로를 RX 인터럽트(Interrupt)부터 사용자 공간(User Space) 소켓(Socket)까지 단계별로 해설합니다. NAPI 폴링(Polling)과 softirq, sk_buff 수명주기, IPv4/IPv6·TCP/UDP 처리, 라우팅(Routing)/FIB 조회, Netfilter 훅, qdisc/TC 송신 경로를 연결해 설명하고, 실제 운영에서 자주 마주치는 지연(Latency)·드롭·재전송(Retransmission) 문제를 계측하고 튜닝하는 실무 절차까지 포함합니다.
핵심 요약
- 패킷 수명주기 — ingress, 처리, egress 경로를 연결합니다.
- 큐/버퍼 모델 — sk_buff와 큐 지점의 역할을 분리합니다.
- 정책/데이터 분리 — 제어 평면과 데이터 평면을 구분합니다.
- 성능 지표 — PPS, 지연, 드롭 원인을 함께 분석합니다.
- 오프로딩(Offloading) 경계 — NIC/XDP/DPDK 경계를 명확히 유지합니다.
단계별 이해
- 경로 고정
문제가 발생한 ingress/egress 지점을 먼저 특정합니다. - 큐 관찰
백로그와 드롭 위치를 계측합니다. - 정책 반영 확인
라우팅/필터 변경이 데이터 경로에 반영됐는지 봅니다. - 부하 검증
실제 트래픽 패턴에서 재현성을 확인합니다.
전체 네트워크 스택 계층도
Linux 커널의 네트워크 스택은 유저 공간 애플리케이션에서 물리 NIC까지 7개 주요 계층으로 구성됩니다. 아래 대형 다이어그램은 각 계층의 핵심 모듈과 데이터 흐름을 한눈에 보여줍니다. 수신(RX) 경로는 아래에서 위로, 송신(TX) 경로는 위에서 아래로 진행합니다.
위 다이어그램에서 주목할 점은 다음과 같습니다:
- Netfilter와 TC는 네트워크 계층에 병렬로 삽입됩니다 — 두 프레임워크 모두 패킷 경로의 훅 포인트에 콜백(Callback)을 등록하는 구조입니다. Netfilter는 L3 계층 전후에, TC는 디바이스 출력 직전에 위치합니다.
- XDP는 드라이버 계층에서 동작합니다 — sk_buff 할당 전에 eBPF 프로그램을 실행하므로 최소 오버헤드(Overhead)로 패킷을 처리할 수 있습니다.
- GRO/GSO는 디바이스 계층의 최적화입니다 — 수신 시 GRO가 패킷을 병합하고, 송신 시 GSO가 세그먼트 분할을 지연합니다.
- AF_PACKET은 소켓 계층에서 직접 디바이스 계층에 접근합니다 — tcpdump 같은 패킷 캡처 도구가 사용하며, 전송/네트워크 계층을 우회합니다.
커널 내부 함수 호출 흐름 상세
아래 다이어그램들은 Linux 커널 네트워크 스택 내부의 함수 호출 흐름을 상세히 보여줍니다. 수신(RX)부터 송신(TX), IPSec 처리, 브리지 흐름까지 5개의 다이어그램으로 구성됩니다. sk_buff 구조체 상세는 sk_buff 문서를 참고하세요.
① RX 수신 경로: NIC 드라이버 → XDP → NAPI → GRO → 프로토콜 디스패치
패킷이 NIC 하드웨어에 도착하면 DMA로 메모리에 복사된 뒤, XDP 필터를 거치고 NAPI 폴링 루프에서 GRO 병합을 수행합니다. 이후 netif_receive_skb()를 통해 프로토콜 타입별 핸들러로 디스패치됩니다.
② IP 라우팅 & 분기: PRE_ROUTING → 라우팅 결정 → Local-In / Forward / Local-Out
ip_rcv()로 진입한 패킷은 Netfilter NF_INET_PRE_ROUTING 훅을 거친 뒤 FIB 테이블을 조회하여 세 가지 경로(로컬 수신, 포워딩, 로컬 출력)로 분기합니다.
③ xfrm/IPSec 처리: 복호화(RX) & 암호화(TX)
IPSec은 커널의 xfrm 서브시스템이 담당합니다. 수신 시 SAD를 조회해 ESP/AH를 복호화하고, 송신 시 SPD에 따라 암호화한 뒤 NF_INET_LOCAL_OUT으로 재주입합니다.
④ TX 출력 경로: dst_output → ip_output → Neighbour → NIC
라우팅/Local-Out/Forward에서 결정된 패킷은 dst_output()을 시작으로 Netfilter POST_ROUTING, GSO/단편화 판단, Neighbour 서브시스템의 L2 헤더 추가, TC/qdisc 스케줄링을 거쳐 NIC 드라이버로 전달됩니다.
⑤ Bridge 처리 흐름: br_handle_frame → NF_BR 훅 → FDB 분기
브리지 디바이스는 rx_handler로 등록된 br_handle_frame()에서 패킷을 가로채고, FDB 조회 결과에 따라 유니캐스트 포워딩, 플러딩, 멀티캐스트 전달, 로컬 수신으로 분기합니다.
네트워크 스택 개요
Linux 네트워크 스택은 OSI 7계층 모델에 대응하는 계층적 구조로 설계되어 있습니다. 패킷은 sk_buff(소켓 버퍼) 구조체(Struct)로 표현되며, 각 계층을 통과하면서 헤더가 추가/제거됩니다. 커널 소스에서 네트워크 코드는 net/ 디렉터리에 위치하며, 프로토콜별(net/ipv4/, net/ipv6/, net/core/)로 분리되어 있습니다.
네트워크 스택의 주요 설계 원칙은 다음과 같습니다:
- 프로토콜 독립성 —
struct proto와struct net_protocol을 통해 전송/네트워크 계층 프로토콜을 플러그인 방식으로 등록합니다. TCP, UDP, SCTP 등이 동일한 인터페이스를 구현합니다. - 제로카피 경로 —
sendfile(),splice(),MSG_ZEROCOPY를 통해 유저 공간 버퍼에서 NIC까지 복사를 최소화합니다. - 네트워크 네임스페이스(Namespace) —
struct net으로 네트워크 스택 전체를 격리(Isolation)합니다. 각 네임스페이스는 독립적인 라우팅 테이블(Routing Table), Netfilter 규칙, 인터페이스를 가집니다. - 훅 기반 확장 — Netfilter, TC, XDP 등 데이터 경로 곳곳에 훅 포인트를 두어 패킷 검사/수정/리다이렉트를 가능하게 합니다.
- 멀티큐 병렬화 — NIC의 여러 RX/TX 큐를 별도 CPU에 바인딩하여 패킷 처리를 병렬화합니다. RSS, RPS, XPS가 이를 지원합니다.
- 지연 처리(Deferred Processing) — 하드 IRQ에서 최소 작업만 수행하고, softirq/NAPI 폴링으로 배치 처리하여 인터럽트 오버헤드를 줄입니다.
커널 소스 디렉토리 구조
Linux 커널의 네트워크 코드는 크게 세 디렉토리 트리에 분산되어 있습니다: 프로토콜 구현(net/), NIC 드라이버(drivers/net/), 헤더 파일(include/net/, include/linux/). 아래 표는 각 하위 디렉토리의 역할과 주요 소스 파일을 상세하게 정리합니다.
net/ 디렉토리 상세 맵
| 디렉토리 | 역할 | 주요 파일 | 줄 수 (대략) |
|---|---|---|---|
net/core/ | 프로토콜 독립 코어 인프라 | dev.c(디바이스 관리), skbuff.c(sk_buff), sock.c(소켓 공통), filter.c(BPF), flow_dissector.c | ~70,000 |
net/ipv4/ | IPv4 프로토콜 스택 | ip_input.c, ip_output.c, tcp.c, tcp_input.c, tcp_output.c, udp.c, route.c, fib_trie.c | ~120,000 |
net/ipv6/ | IPv6 프로토콜 스택 | ip6_input.c, ip6_output.c, tcp_ipv6.c, udp_ipv6.c, route.c, ndisc.c | ~60,000 |
net/netfilter/ | Netfilter 프레임워크 | nf_conntrack_core.c, nf_tables_api.c, nf_nat_core.c, nf_flow_table_core.c | ~80,000 |
net/sched/ | TC(Traffic Control) | sch_generic.c, cls_api.c, sch_fq_codel.c, sch_htb.c, act_api.c | ~40,000 |
net/xdp/ | XDP 프레임워크 | xdp_umem.c, xsk.c, xsk_buff_pool.c | ~5,000 |
net/bridge/ | Linux 브릿지 | br_input.c, br_forward.c, br_fdb.c, br_vlan.c | ~15,000 |
net/unix/ | Unix 도메인 소켓 | af_unix.c, garbage.c | ~4,000 |
net/packet/ | AF_PACKET (raw 패킷) | af_packet.c | ~5,000 |
net/netlink/ | Netlink 소켓 | af_netlink.c, genetlink.c | ~4,000 |
net/sctp/ | SCTP 프로토콜 | sm_statefuns.c, associola.c, output.c | ~30,000 |
net/openvswitch/ | Open vSwitch 커널 모듈(Kernel Module) | datapath.c, flow.c, actions.c | ~10,000 |
net/tls/ | 커널 TLS (kTLS) | tls_main.c, tls_sw.c, tls_device.c | ~6,000 |
drivers/net/ 디렉토리 맵
| 디렉토리 | 역할 | 주요 드라이버 |
|---|---|---|
drivers/net/ethernet/intel/ | Intel NIC 드라이버 | ixgbe/(10G), i40e/(40G), ice/(100G), e1000e/(1G), igc/(2.5G) |
drivers/net/ethernet/mellanox/ | NVIDIA/Mellanox NIC | mlx5/(ConnectX-5/6/7), mlx4/(ConnectX-3) |
drivers/net/ethernet/broadcom/ | Broadcom NIC | bnxt/(NetXtreme-E), tg3.c |
drivers/net/ethernet/realtek/ | Realtek NIC | r8169.c(RTL8111/8168) |
drivers/net/virtio_net.c | 가상화(Virtualization) NIC | virtio-net 드라이버 (KVM/QEMU) |
drivers/net/veth.c | 가상 이더넷 쌍 | 네트워크 네임스페이스 연결 |
drivers/net/bonding/ | 본딩(Bonding) 드라이버 | NIC 결합 (active-backup, 802.3ad 등) |
drivers/net/macvlan.c | MACVLAN 드라이버 | MAC 기반 가상 NIC |
drivers/net/tun.c | TUN/TAP 드라이버 | 유저 공간 네트워크 터널(Tunnel) |
include/ 헤더 파일 맵
| 디렉토리 | 역할 | 핵심 헤더 |
|---|---|---|
include/linux/ | 공통 커널 네트워크 헤더 | skbuff.h, netdevice.h, socket.h, tcp.h, if_ether.h |
include/net/ | 네트워크 서브시스템 내부 헤더 | sock.h, tcp.h, ip.h, dst.h, netfilter.h, flow.h, xdp.h |
include/uapi/linux/ | UAPI (유저 공간 API) | tcp.h, in.h, socket.h, bpf.h, if_link.h |
커널 네트워크 코드 규모
Linux 커널의 네트워크 코드는 커널 전체 코드의 약 10-15%를 차지하는 대규모 서브시스템입니다. 아래 표는 주요 디렉토리별 대략적인 코드 규모를 보여줍니다:
| 디렉토리 | 파일 수 | 코드 줄 수 (대략) | 비율 |
|---|---|---|---|
net/ (전체) | ~2,500 | ~800,000 | 가장 큰 서브시스템 |
drivers/net/ (전체) | ~4,000 | ~1,200,000 | NIC 드라이버 |
include/net/ + include/linux/ (네트워크) | ~300 | ~100,000 | 헤더 파일 |
| 합계 | ~6,800 | ~2,100,000 | 전체 커널의 약 12% |
git grep -n 'EXPORT_SYMBOL.*함수명' net/ 또는 Bootlin Elixir 온라인 소스 브라우저를 활용하세요. net/core/dev.c의 __netif_receive_skb()부터 추적하면 수신 경로의 핵심 분기점을 파악할 수 있습니다.
핵심 소스 파일과 진입점(Entry Point)
네트워크 스택을 처음 분석할 때 아래 소스 파일부터 읽으면 전체 구조를 빠르게 파악할 수 있습니다:
| 파일 | 핵심 함수 | 역할 |
|---|---|---|
net/core/dev.c | __netif_receive_skb(), __dev_queue_xmit() | 수신/송신 경로의 중앙 허브 |
net/core/skbuff.c | alloc_skb(), skb_clone(), kfree_skb() | sk_buff 할당/해제/조작 |
net/core/sock.c | sock_sendmsg(), sock_recvmsg() | 소켓 공통 인터페이스 |
net/ipv4/ip_input.c | ip_rcv(), ip_local_deliver() | IPv4 수신 경로 |
net/ipv4/ip_output.c | ip_output(), ip_queue_xmit() | IPv4 송신 경로 |
net/ipv4/tcp.c | tcp_sendmsg(), tcp_recvmsg() | TCP 소켓 인터페이스 |
net/ipv4/tcp_input.c | tcp_v4_rcv(), tcp_rcv_state_process() | TCP 수신/상태 머신 |
net/ipv4/tcp_output.c | tcp_write_xmit(), tcp_transmit_skb() | TCP 송신/세그먼트 생성 |
net/ipv4/route.c | ip_route_input_noref(), ip_route_output_key() | IPv4 라우팅 캐시(Cache) |
net/ipv4/fib_trie.c | fib_table_lookup() | FIB LC-trie 조회 |
net/sched/sch_generic.c | qdisc_run(), dev_hard_start_xmit() | TC/qdisc 프레임워크 |
패킷 수신 경로 (RX Path)
패킷이 NIC에 도달한 순간부터 유저 공간 recv()가 데이터를 받을 때까지의 전체 경로를 단계별로 추적합니다. 이 경로를 정확히 이해하면 지연 원인 분석과 드롭 포인트 추적이 훨씬 용이해집니다.
단계별 상세 설명
1단계 - NIC DMA 전송: NIC가 패킷을 수신하면 DMA(Direct Memory Access)를 통해 미리 할당된 RX Ring Buffer의 빈 슬롯에 패킷 데이터를 기록합니다. RX Ring Buffer는 드라이버 초기화 시 napi_alloc_skb() 또는 page_pool을 사용하여 할당한 메모리 영역입니다. DMA 완료 후 NIC는 하드웨어 인터럽트(IRQ)를 발생시킵니다.
2단계 - Hard IRQ 처리: 드라이버의 인터럽트 핸들러(Handler)가 실행됩니다. 이 핸들러는 최소한의 작업만 수행합니다: napi_schedule()를 호출하여 NAPI 폴링을 예약하고, 해당 큐의 인터럽트를 비활성화합니다. 이렇게 함으로써 고부하 시 인터럽트 폭풍(interrupt storm)을 방지합니다.
/* 드라이버 인터럽트 핸들러 전형적 패턴 */
static irqreturn_t driver_irq_handler(int irq, void *data)
{
struct driver_priv *priv = data;
/* 인터럽트 비활성화 (폴링 모드 전환) */
driver_disable_irq(priv);
/* NAPI 폴링 예약 */
napi_schedule(&priv->napi);
return IRQ_HANDLED;
}
3단계 - SoftIRQ / NAPI 폴링: NET_RX_SOFTIRQ가 트리거되면 net_rx_action()이 실행되어 예약된 NAPI 구조체의 poll() 콜백을 호출합니다. 이 콜백에서 드라이버는 RX Ring에서 수신된 패킷을 budget(기본 64) 개수만큼 배치로 처리합니다. budget 내에서 모든 패킷을 처리하면 napi_complete_done()으로 인터럽트를 재활성화합니다.
4단계 - GRO/XDP 처리: NAPI poll 내부에서 napi_gro_receive()가 호출되면 GRO(Generic Receive Offload)가 동일 플로우의 연속 패킷을 병합하여 상위 계층의 처리 횟수를 줄입니다. XDP 프로그램이 연결되어 있다면 bpf_prog_run_xdp()에서 XDP_DROP, XDP_TX, XDP_REDIRECT, XDP_PASS 중 하나를 결정합니다.
5단계 - L2 디멀티플렉싱: __netif_receive_skb()에서 skb->protocol 필드(이더넷의 EtherType)를 기반으로 적절한 L3 프로토콜 핸들러를 찾습니다. IPv4(ETH_P_IP), IPv6(ETH_P_IPV6), ARP(ETH_P_ARP) 등을 구분합니다. AF_PACKET 소켓(tcpdump 등)이 있다면 이 시점에 패킷 복사본이 전달됩니다.
6단계 - IP 계층 처리: ip_rcv()에서 IP 헤더 유효성을 검증하고 Netfilter NF_INET_PRE_ROUTING 훅을 통과시킵니다. ip_rcv_finish()에서 FIB(Forwarding Information Base) 조회를 수행하여 패킷의 목적지가 로컬인지, 포워딩 대상인지 결정합니다.
7단계 - L4 전달: 로컬 대상 패킷은 ip_local_deliver()를 거쳐 Netfilter NF_INET_LOCAL_IN 훅을 통과한 후, IP 프로토콜 번호에 따라 tcp_v4_rcv()(TCP) 또는 udp_rcv()(UDP) 등 전송 계층 핸들러로 전달됩니다.
8단계 - 소켓 수신 큐: 전송 계층이 패킷을 해당 소켓의 sk_receive_queue에 삽입하고 sk->sk_data_ready() 콜백으로 대기 중인 프로세스(Process)를 깨웁니다. 유저 공간의 recv()/recvmsg()가 큐에서 데이터를 꺼내 사용자 버퍼로 복사합니다.
netdev_budget 초과(NAPI), netdev_max_backlog 초과(per-CPU backlog), Netfilter DROP, 소켓 버퍼 초과(sk_rcvbuf) 등이 대표적입니다. /proc/net/softnet_stat과 ethtool -S로 드롭 위치를 특정할 수 있습니다.
net_rx_action() 내부 구현
NET_RX_SOFTIRQ가 트리거되면 net_rx_action()이 호출됩니다. 이 함수는 per-CPU softnet_data의 poll_list에 등록된 NAPI 구조체를 순회하며, 각각의 poll() 콜백을 호출합니다. 전체 budget(기본 300)과 시간 제한(2 jiffies)이라는 두 가지 제약 조건을 적용하여, softirq가 CPU를 과도하게 점유하지 않도록 합니다.
/* net/core/dev.c — net_rx_action() 핵심 로직 (간략화) */
static void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
unsigned long time_limit = jiffies + 2; /* 최대 2 jiffies */
int budget = READ_ONCE(netdev_budget); /* 기본 300 */
LIST_HEAD(list);
LIST_HEAD(repoll);
local_irq_disable();
list_splice_init(&sd->poll_list, &list);
local_irq_enable();
for (;;) {
struct napi_struct *n;
if (list_empty(&list)) {
if (!sd_has_rps_ipi_waiting(sd) &&
list_empty(&repoll))
return;
break;
}
n = list_first_entry(&list, struct napi_struct, poll_list);
budget -= napi_poll(n, &repoll);
/* budget 소진 또는 시간 초과 시 중단 */
if (unlikely(budget <= 0 ||
time_after_eq(jiffies, time_limit))) {
sd->time_squeeze++; /* softnet_stat 두 번째 열 */
break;
}
}
/* 미처리 NAPI를 poll_list에 복원, softirq 재스케줄 */
local_irq_disable();
list_splice_tail_init(&sd->poll_list, &list);
list_splice_tail(&repoll, &list);
if (!list_empty(&list))
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
local_irq_enable();
}
코드 설명
- 3-5행per-CPU
softnet_data에서 시간 제한(2 jiffies)과 budget(기본 300)을 설정합니다. budget은/proc/sys/net/core/netdev_budget으로 조정할 수 있습니다. - 9-10행인터럽트를 비활성화한 상태에서 poll_list를 로컬 list로 이동합니다. 인터럽트 핸들러가 동시에 poll_list를 수정하는 것을 방지합니다.
- 19행
napi_poll()을 호출하여 해당 NAPI 구조체의poll()콜백을 실행합니다. 반환값은 처리한 패킷 수입니다. - 22-25행budget이 0 이하이거나 시간이 초과되면 루프를 중단합니다.
time_squeeze카운터를 증가시켜 이 상황을 추적할 수 있습니다. - 28-32행미처리 NAPI가 남아 있으면 poll_list에 복원하고
NET_RX_SOFTIRQ를 재발생시킵니다. 이를 통해 남은 패킷이 다음 softirq 사이클에서 처리됩니다.
NAPI 폴링 루프 상세
드라이버의 poll() 콜백은 RX Ring Buffer에서 패킷을 꺼내 상위 계층으로 전달하는 핵심 루프입니다. budget 범위 내에서 가능한 많은 패킷을 처리하며, 모든 패킷을 소진하면 인터럽트를 재활성화합니다. 아래는 전형적인 드라이버 poll 함수의 구조입니다.
/* 전형적인 NIC 드라이버 NAPI poll 함수 */
static int driver_poll(struct napi_struct *napi, int budget)
{
struct driver_ring *ring = container_of(napi, struct driver_ring, napi);
int work_done = 0;
while (work_done < budget) {
struct sk_buff *skb;
struct rx_desc *desc = &ring->desc[ring->next_to_clean];
/* 디스크립터 소유권 확인 (DD 비트) */
if (!(le32_to_cpu(desc->status) & RX_DESC_DONE))
break;
/* DMA 언맵 및 sk_buff 구성 */
dma_sync_single_for_cpu(ring->dev, desc->dma_addr,
ring->buf_len, DMA_FROM_DEVICE);
skb = ring->skb_array[ring->next_to_clean];
skb_put(skb, desc->length);
/* 프로토콜, 체크섬 오프로드 결과 설정 */
skb->protocol = eth_type_trans(skb, ring->netdev);
if (desc->status & RX_CSUM_VALID)
skb->ip_summed = CHECKSUM_UNNECESSARY;
/* GRO를 통해 상위 계층 전달 */
napi_gro_receive(napi, skb);
ring->next_to_clean = (ring->next_to_clean + 1) % ring->count;
work_done++;
}
/* 새 버퍼 할당하여 RX Ring 보충 */
driver_alloc_rx_buffers(ring);
/* 모든 패킷 처리 완료 시 인터럽트 재활성화 */
if (work_done < budget) {
if (napi_complete_done(napi, work_done))
driver_enable_irq(ring);
}
return work_done;
}
코드 설명
- 7-13행budget 한도 내에서 RX 디스크립터의 DD(Descriptor Done) 비트를 확인하여 NIC가 패킷을 완료했는지 검사합니다. DD 비트가 없으면 더 이상 처리할 패킷이 없으므로 루프를 종료합니다.
- 16-20행DMA 동기화 후 sk_buff에 실제 패킷 길이를 설정합니다.
skb_put()으로 tail 포인터를 이동하여 유효 데이터 영역을 확정합니다. - 22-24행
eth_type_trans()가 이더넷 헤더를 파싱하여 EtherType을 설정하고, NIC 체크섬 오프로드 결과를ip_summed에 반영합니다.CHECKSUM_UNNECESSARY는 커널이 체크섬 재검증을 생략해도 됨을 의미합니다. - 27행
napi_gro_receive()로 GRO 병합을 시도한 후__netif_receive_skb()로 전달합니다. - 37-39행work_done이 budget보다 작으면 모든 패킷을 처리한 것이므로,
napi_complete_done()으로 NAPI 상태를 해제하고 인터럽트를 재활성화합니다. busy polling이 활성화된 경우napi_complete_done()이 false를 반환하여 인터럽트를 재활성화하지 않을 수 있습니다.
GRO 병합 메커니즘
GRO(Generic Receive Offload)는 동일 플로우의 연속 패킷을 하나의 큰 sk_buff로 병합하여 상위 스택의 처리 횟수를 줄이는 메커니즘입니다. napi_gro_receive()는 내부적으로 dev_gro_receive()를 호출하여 프로토콜별 GRO 콜백을 실행합니다.
/* net/core/gro.c — GRO 수신 처리 핵심 흐름 (간략화) */
gro_result_t dev_gro_receive(struct napi_struct *napi,
struct sk_buff *skb)
{
struct list_head *gro_head = &napi->gro_hash[bucket].list;
struct sk_buff *pp = NULL;
/* 1. L2 프로토콜별 gro_receive 콜백 호출 */
/* IPv4 → inet_gro_receive() → tcp4_gro_receive() */
/* IPv6 → ipv6_gro_receive() → tcp6_gro_receive() */
pp = call_gro_receive(ptype->callbacks.gro_receive, gro_head, skb);
switch (ret) {
case GRO_MERGED:
/* skb가 기존 GRO 패킷에 병합됨 — skb 해제 */
break;
case GRO_MERGED_FREE:
/* 병합 후 원본 skb 즉시 해제 */
break;
case GRO_HELD:
/* skb를 GRO 리스트에 보관 (향후 병합 대기) */
list_add(&skb->list, gro_head);
napi->gro_hash[bucket].count++;
break;
case GRO_NORMAL:
/* GRO 불가 — 일반 수신 경로로 전달 */
gro_normal_one(napi, skb, 1);
break;
case GRO_CONSUMED:
/* 프로토콜이 skb 소유권을 가져감 */
break;
}
return ret;
}
코드 설명
- 5행NAPI 구조체에 GRO 해시 테이블이 존재하며, 플로우의 해시값에 따라 버킷이 결정됩니다. 동일 플로우의 패킷들이 같은 버킷에 모여 병합 후보를 빠르게 찾을 수 있습니다.
- 8-11행EtherType에 등록된 프로토콜별
gro_receive콜백을 호출합니다. IPv4 TCP의 경우inet_gro_receive()→tcp4_gro_receive()체인으로 처리됩니다. - 13-15행
GRO_MERGED는 패킷이 기존 GRO 버퍼에 성공적으로 병합되었음을 의미합니다. TCP의 경우 시퀀스 번호가 연속이고 헤더가 호환되는 경우 병합됩니다. - 20-23행
GRO_HELD는 병합 대상이 아직 없어 GRO 리스트에 보관하는 경우입니다. 다음 패킷이 도착하면 이 패킷과 병합을 시도합니다.gro_hash[].count가MAX_GRO_SKBS(8)에 도달하면 가장 오래된 패킷을 플러시합니다. - 24-27행
GRO_NORMAL은 GRO 병합이 불가능한 패킷(UDP 비-GRO, 단편 패킷 등)으로, 일반 수신 경로(__netif_receive_skb())로 전달됩니다.
GRO 병합이 성공하면 하나의 64KB sk_buff로 수십 개의 세그먼트를 전달할 수 있어, TCP 수신 경로의 함수 호출 횟수가 대폭 감소합니다. GRO 병합 조건은 다음과 같습니다:
| 조건 | 설명 | 불만족 시 |
|---|---|---|
| 동일 플로우 | src/dst IP, src/dst port, protocol이 동일 | GRO_NORMAL |
| 연속 시퀀스 | TCP 시퀀스 번호가 이전 세그먼트에 연속 | GRO_NORMAL |
| 호환 헤더 | IP/TCP 헤더 옵션이 동일 (timestamp 제외) | GRO_NORMAL |
| 동일 MAC | VLAN 태그, MAC 헤더가 동일 | GRO_NORMAL |
| PSH 플래그 | PSH가 설정된 세그먼트는 병합 후 즉시 플러시 | flush 트리거 |
| 최대 크기 | 병합 후 총 크기가 64KB 이하 | flush 후 새 GRO 시작 |
Busy Polling (SO_BUSY_POLL)
Busy Polling은 유저 공간의 poll()/epoll_wait()/select()가 반환 전에 직접 NAPI poll을 호출하는 메커니즘입니다. softirq 스케줄링 지연을 제거하여 수 마이크로초 수준의 레이턴시 개선을 달성할 수 있습니다. 금융 트레이딩, HPC 메시지 패싱 등 초저지연 환경에서 유용합니다.
/* include/net/busy_poll.h — sk_busy_loop() 핵심 로직 (간략화) */
static inline void sk_busy_loop(struct sock *sk, int nonblock)
{
unsigned int napi_id = READ_ONCE(sk->sk_napi_id);
unsigned long end_time = busy_loop_end_time(sk);
int (*busy_poll)(struct napi_struct *napi, int budget);
if (!napi_id)
return;
do {
local_bh_disable();
/* 소켓에 연결된 NAPI를 직접 poll */
napi_busy_loop(napi_id, nonblock ? NULL : busy_loop_current_should_spin,
sk->sk_prefer_busy_poll ? busy_poll : NULL,
BUSY_POLL_BUDGET);
local_bh_enable();
} while (!nonblock && !sk_has_rx_data(sk) &&
!busy_loop_timeout(end_time));
}
코드 설명
- 4행
sk_napi_id는 이 소켓의 마지막 수신 패킷을 처리한 NAPI 인스턴스의 ID입니다. RX 경로에서 자동으로 설정됩니다. - 5행
busy_loop_end_time()은SO_BUSY_POLL소켓 옵션에 설정된 시간(마이크로초)을 기반으로 종료 시점을 계산합니다. - 13-16행
napi_busy_loop()가 해당 NAPI의 poll 콜백을BUSY_POLL_BUDGET(8)만큼 직접 호출합니다. softirq 컨텍스트가 아닌 프로세스 컨텍스트에서 실행되므로, 해당 CPU의 BH를 비활성화합니다. - 18-19행소켓에 수신 데이터가 도착하거나 타임아웃이 만료될 때까지 루프를 반복합니다.
/* Busy Polling 활성화: 유저 공간 설정 */
int busy_poll_usec = 50; /* 50μs 동안 busy poll */
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL,
&busy_poll_usec, sizeof(busy_poll_usec));
/* SO_PREFER_BUSY_POLL: 드라이버 전용 busy poll 콜백 사용 */
int prefer = 1;
setsockopt(fd, SOL_SOCKET, SO_PREFER_BUSY_POLL,
&prefer, sizeof(prefer));
코드 설명
- 2-3행
SO_BUSY_POLL에 마이크로초 단위 값을 설정합니다. 0이면 비활성화, 양수이면 해당 시간 동안 busy poll을 수행합니다. - 7-9행
SO_PREFER_BUSY_POLL은 드라이버가 전용 busy poll 콜백(ndo_busy_poll)을 제공할 때 이를 우선 사용하도록 합니다.
# 시스템 전역 busy poll 설정 (sysctl)
sysctl -w net.core.busy_poll=50 # poll/select/epoll 기본 busy poll 시간(μs)
sysctl -w net.core.busy_read=50 # read/recv 기본 busy poll 시간(μs)
# NAPI ID 기반 소켓 바인딩 확인
cat /proc/net/tcp | awk '{print $13}' # napi_id 열 확인
RX 경로 성능 카운터 심층 분석
/proc/net/softnet_stat은 per-CPU 네트워크 소프트 인터럽트 통계를 16진수로 출력합니다. 각 행이 하나의 CPU에 대응하며, 패킷 드롭과 softirq 스케줄링 문제를 진단하는 핵심 도구입니다.
/* net/core/net-procfs.c — softnet_stat 출력 형식 */
/* include/linux/netdevice.h — softnet_data 구조체 */
struct softnet_data {
struct list_head poll_list; /* 폴링 대기 NAPI 리스트 */
struct sk_buff_head input_pkt_queue; /* non-NAPI 또는 RPS 입력 큐 */
struct sk_buff_head process_queue; /* 처리 중인 패킷 큐 */
unsigned int processed; /* 열 1: 총 처리 패킷 수 */
unsigned int time_squeeze; /* 열 2: budget/시간 초과 횟수 */
unsigned int received_rps; /* 열 9: RPS 수신 횟수 */
unsigned int dropped; /* input_pkt_queue 오버플로우 */
unsigned int flow_limit_count; /* 열 10: flow limit 드롭 */
unsigned int cpu_collision; /* 열 8: TX CPU 충돌 */
unsigned int backlog_len; /* 현재 backlog 길이 */
};
코드 설명
- 8행
processed:/proc/net/softnet_stat의 첫 번째 열입니다. 해당 CPU에서 처리된 총 패킷 수를 나타냅니다. - 9행
time_squeeze: 두 번째 열입니다.net_rx_action()이 budget 또는 시간 제한으로 중단된 횟수입니다. 이 값이 증가하면netdev_budget또는netdev_budget_usecs를 늘려야 합니다. - 11행
dropped:input_pkt_queue가netdev_max_backlog(기본 1000)을 초과하여 드롭된 패킷 수입니다. non-NAPI 드라이버 또는 RPS가 활성화된 경우에 발생합니다. - 13행
cpu_collision: TX 경로에서 다른 CPU가 이미 qdisc 락을 보유하여 전송에 실패한 횟수입니다. 멀티큐 NIC에서 XPS를 설정하면 줄일 수 있습니다.
| 열 번호 | 필드 | 의미 | 대응 조치 |
|---|---|---|---|
| 1 | processed | 처리된 총 패킷 수 | CPU 간 불균형 시 RPS/RSS 조정 |
| 2 | time_squeeze | budget/시간 초과로 중단된 횟수 | netdev_budget 증가 (300→600) |
| 3 | (0) | 미사용 (구 backlog 필드) | — |
| 4-7 | (0) | 미사용 | — |
| 8 | cpu_collision | TX qdisc 락 충돌 | 멀티큐 + XPS 설정 |
| 9 | received_rps | RPS IPI로 수신된 패킷 | RPS 정상 동작 확인 |
| 10 | flow_limit_count | Flow Limit으로 드롭 | flow_limit_table_len 조정 |
| 11 | softnet_backlog_len | 현재 backlog 큐 길이 | 실시간 큐 상태 확인 |
| 12 | index | CPU 번호 | — |
| 13 | dropped | backlog 오버플로우 드롭 | netdev_max_backlog 증가 |
# softnet_stat 실시간 모니터링
watch -n1 'cat /proc/net/softnet_stat'
# 특정 CPU의 time_squeeze 변화 추적
while true; do
awk 'NR==1 {printf "CPU0 processed=%d time_squeeze=%d dropped=%d\n", \
strtonum("0x"$1), strtonum("0x"$2), strtonum("0x"$3)}' \
/proc/net/softnet_stat
sleep 1
done
# budget 관련 sysctl 튜닝
sysctl -w net.core.netdev_budget=600 # 기본 300
sysctl -w net.core.netdev_budget_usecs=4000 # 기본 2000 (2ms)
sysctl -w net.core.netdev_max_backlog=2000 # 기본 1000
RPS/RFS/XPS 패킷 스티어링
멀티큐 NIC에서도 모든 큐가 효율적으로 사용되지 않거나, 단일 큐 NIC에서 멀티코어를 활용하기 위해 소프트웨어 패킷 스티어링이 필요합니다. RPS(Receive Packet Steering), RFS(Receive Flow Steering), XPS(Transmit Packet Steering)는 패킷 처리를 여러 CPU에 분산합니다.
/* net/core/dev.c — RPS 처리 (get_rps_cpu) 핵심 로직 (간략화) */
static int get_rps_cpu(struct net_device *dev,
struct sk_buff *skb,
struct rps_dev_flow **rflowp)
{
const struct rps_sock_flow_table *sock_flow_table;
struct netdev_rx_queue *rxqueue = dev->_rx;
u32 flow_hash;
int cpu = -1;
/* 1. 패킷의 플로우 해시 계산 (5-tuple) */
flow_hash = skb_get_hash(skb);
if (!flow_hash)
goto done;
/* 2. RFS: 소켓이 마지막으로 실행된 CPU로 스티어링 */
sock_flow_table = rcu_dereference(rps_sock_flow_table);
if (sock_flow_table) {
u32 ident = sock_flow_table->ents[flow_hash &
sock_flow_table->mask];
cpu = ident & ~0x80000000U; /* CPU 번호 추출 */
}
/* 3. RPS fallback: 해시 기반 CPU 선택 */
if (cpu < 0) {
u32 map_len = map->len;
cpu = map->cpus[reciprocal_scale(flow_hash, map_len)];
}
return cpu;
}
코드 설명
- 12행
skb_get_hash()는 패킷의 5-tuple(소스/목적지 IP:포트, 프로토콜)에서 해시를 계산합니다. 동일 플로우의 패킷은 동일 해시 → 동일 CPU로 전달되어 캐시 효율성을 유지합니다. - 17-22행RFS가 활성화되어 있으면
rps_sock_flow_table에서 해당 플로우의 소켓이 마지막으로 실행된 CPU를 찾습니다. 이를 통해 수신 패킷이 소켓을 처리하는 CPU와 같은 CPU에서 처리되어 캐시 미스를 줄입니다. - 25-28행RFS 정보가 없으면 RPS fallback으로, CPU 맵에서 해시 기반으로 CPU를 선택합니다. CPU 맵은
/sys/class/net/<dev>/queues/rx-<N>/rps_cpus에서 설정합니다.
| 기능 | 방향 | 수준 | 설정 방법 | 동작 |
|---|---|---|---|---|
| RSS | RX | 하드웨어 | ethtool -X | NIC 해시 → 하드웨어 큐 분배 |
| RPS | RX | 소프트웨어 | rps_cpus sysfs | 해시 → CPU IPI 전달 |
| RFS | RX | 소프트웨어 | rps_sock_flow_entries | 소켓 CPU 친화성 기반 |
| XPS | TX | 소프트웨어 | xps_cpus sysfs | CPU → TX 큐 매핑 |
| aRFS | RX | 하드웨어 | ethtool -K ntuple on | NIC 하드웨어 플로우 스티어링 |
# RPS 설정 (CPU 0-3에 분배)
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus
# RFS 활성화 (전역 플로우 테이블 크기)
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
# XPS 설정 (CPU 0 → TX 큐 0, CPU 1 → TX 큐 1)
echo 1 > /sys/class/net/eth0/queues/tx-0/xps_cpus
echo 2 > /sys/class/net/eth0/queues/tx-1/xps_cpus
# RSS 인다이렉션 테이블 확인
ethtool -x eth0
# RSS 해시 키 설정
ethtool -X eth0 equal 4 # 4개 큐에 균등 분배
패킷 송신 경로 (TX Path)
유저 공간의 send()/sendmsg() 호출부터 NIC가 패킷을 와이어에 전송할 때까지의 경로입니다. 수신 경로의 역순이지만, qdisc(큐잉 규칙)와 GSO(Generic Segmentation Offload)가 개입하는 점이 핵심 차이입니다.
TX 경로의 핵심 처리 흐름
소켓 계층에서 전송 계층까지: 유저 공간의 send() 시스템 콜(System Call)은 sock_sendmsg()를 거쳐 프로토콜별 sendmsg() 구현으로 전달됩니다. TCP의 경우 tcp_sendmsg()가 유저 데이터를 소켓의 sk_write_queue에 sk_buff 형태로 복사하고, Nagle 알고리즘과 혼잡 제어(Congestion Control)를 거쳐 tcp_write_xmit()에서 실제 전송을 결정합니다. UDP는 udp_sendmsg()에서 즉시 IP 계층으로 전달합니다.
IP 계층 처리: ip_queue_xmit()에서 라우팅 결과(dst_entry)를 참조하여 소스 IP, 출력 인터페이스를 결정하고 IP 헤더를 추가합니다. Netfilter NF_INET_LOCAL_OUT과 NF_INET_POST_ROUTING 훅을 통과합니다. IP 단편화(Fragmentation)가 필요하면 ip_fragment()에서 MTU에 맞게 분할합니다.
TC/qdisc 계층: __dev_queue_xmit()에서 출력 디바이스의 qdisc에 패킷을 enqueue합니다. 기본 qdisc는 pfifo_fast 또는 fq_codel이며, htb, tbf 등으로 대역폭(Bandwidth) 제한과 트래픽 셰이핑을 수행할 수 있습니다. dequeue된 패킷은 dev_hard_start_xmit()으로 드라이버에 전달됩니다.
드라이버 전송: ndo_start_xmit() 콜백에서 DMA 매핑(Mapping) 후 TX Ring Buffer에 디스크립터를 기록하고, NIC의 doorbell 레지스터(Register)에 쓰기를 수행하여 전송을 시작합니다. TX 완료 인터럽트로 sk_buff를 해제합니다.
ethtool -k로 오프로드 상태를 확인할 수 있습니다.
__dev_queue_xmit() 내부 구현
__dev_queue_xmit()은 IP 계층에서 내려온 sk_buff를 출력 디바이스의 qdisc에 전달하는 핵심 함수입니다. qdisc가 없거나(noqueue) 조건을 만족하면 qdisc를 우회하여 직접 전송하는 경로도 있습니다.
/* net/core/dev.c — __dev_queue_xmit() 핵심 로직 (간략화) */
static int __dev_queue_xmit(struct sk_buff *skb,
struct net_device *sb_dev)
{
struct net_device *dev = skb->dev;
struct netdev_queue *txq;
struct Qdisc *q;
/* 1. TC egress 처리 (tc filter/action) */
skb = sch_handle_egress(skb, &rc, dev);
if (!skb)
goto out;
/* 2. TX 큐 선택 (XPS 또는 해시 기반) */
txq = netdev_core_pick_tx(dev, skb, sb_dev);
q = rcu_dereference_bh(txq->qdisc);
if (q->enqueue) {
/* 3a. qdisc 경로: enqueue 후 dequeue/전송 */
rc = __dev_xmit_skb(skb, q, dev, txq);
} else {
/* 3b. qdisc 우회 (noqueue): 직접 전송 */
if (dev_xmit_recursion()) {
net_crit_ratelimited("Recursion! dev=%s\n", dev->name);
goto drop;
}
/* BQL 체크 후 직접 전송 */
skb_get_tx_queue(skb, txq);
PRANDOM_ADD_NOISE(skb, dev, txq, jiffies);
rc = dev_hard_start_xmit(skb, dev, txq, NULL);
}
return rc;
}
코드 설명
- 10행
sch_handle_egress()는 TC egress 훅을 처리합니다.tc filter로 설정된 규칙이 있으면 이 시점에서 패킷을 분류/수정/드롭할 수 있습니다. - 15행
netdev_core_pick_tx()는 멀티큐 NIC에서 전송할 TX 큐를 선택합니다. XPS(Transmit Packet Steering)가 설정되어 있으면 CPU-큐 매핑을 사용하고, 그렇지 않으면 skb 해시를 기반으로 선택합니다. - 18-20행qdisc에
enqueue함수가 있으면 일반 qdisc 경로로 진입합니다.__dev_xmit_skb()는 qdisc 락을 획득하고, enqueue → dequeue →dev_hard_start_xmit()순서로 처리합니다. - 21-31행loopback, veth 등 qdisc가
noqueue인 디바이스는 qdisc 처리를 건너뛰고dev_hard_start_xmit()을 직접 호출합니다. 재귀 방지 검사가 포함되어 있습니다.
qdisc 처리 흐름
qdisc에 패킷이 enqueue되면, __qdisc_run()이 dequeue 루프를 실행하여 패킷을 드라이버로 전달합니다. 이 과정은 qdisc 락을 보유한 상태에서 수행되므로, 다른 CPU와의 경합이 발생할 수 있습니다.
/* net/sched/sch_generic.c — qdisc 실행 흐름 (간략화) */
void __qdisc_run(struct Qdisc *q)
{
int quota = READ_ONCE(dev_tx_weight); /* 기본 64 */
int packets;
while (qdisc_restart(q, &packets)) {
quota -= packets;
if (quota <= 0) {
/* quota 소진 시 softirq로 연기 */
__netif_schedule(q);
break;
}
}
}
/* qdisc_restart() 내부 */
static inline bool qdisc_restart(struct Qdisc *q, int *packets)
{
struct sk_buff *skb = dequeue_skb(q, packets);
if (unlikely(!skb))
return false;
/* GSO 세그먼테이션이 필요하면 이 시점에서 수행 */
if (sch_direct_xmit(skb, q, dev, txq, NULL, true))
return true; /* dequeue 계속 */
return false; /* 드라이버 큐 full — 중단 */
}
코드 설명
- 4행
dev_tx_weight는 한 번의__qdisc_run()호출에서 처리할 최대 패킷 수입니다. 기본값은 64이며,/proc/sys/net/core/dev_weight로 조정할 수 있습니다. - 7행
qdisc_restart()는 qdisc에서 패킷을 dequeue하고 드라이버로 전송을 시도합니다. 성공하면 true를 반환하여 루프가 계속됩니다. - 10-12행quota가 소진되면
__netif_schedule()로NET_TX_SOFTIRQ를 발생시켜 나머지 패킷을 softirq 컨텍스트에서 처리합니다. - 20행
dequeue_skb()는 qdisc의dequeue콜백을 호출합니다. fq_codel의 경우 CoDel 알고리즘으로 큐 지연을 측정하고, 지연이target(5ms)을 초과하면 패킷을 드롭합니다. - 24-25행
sch_direct_xmit()은dev_hard_start_xmit()을 호출하여 실제 전송을 수행합니다. 드라이버의 TX 큐가 가득 차면(NETDEV_TX_BUSY) false를 반환하고, qdisc에 패킷을 다시 requeue합니다.
GSO 세그먼테이션 내부
GSO(Generic Segmentation Offload)는 TCP/UDP 대형 패킷을 MSS 크기의 세그먼트로 분할하는 소프트웨어 메커니즘입니다. dev_hard_start_xmit()에서 NIC가 TSO를 지원하지 않으면 __skb_gso_segment()를 호출하여 분할합니다.
/* net/core/skbuff.c — GSO 세그먼테이션 핵심 경로 (간략화) */
struct sk_buff *__skb_gso_segment(struct sk_buff *skb,
netdev_features_t features, bool tx_path)
{
struct sk_buff *segs;
/* skb_shared_info→gso_type에 따라 콜백 선택 */
/* SKB_GSO_TCPV4 → tcp4_gso_segment() */
/* SKB_GSO_TCPV6 → tcp6_gso_segment() */
/* SKB_GSO_UDP_L4 → __udp_gso_segment() */
segs = skb_mac_gso_segment(skb, features);
return segs; /* 세그먼트 sk_buff 연결 리스트 */
}
/* TCP GSO 세그먼테이션 (net/ipv4/tcp_offload.c) */
struct sk_buff *tcp4_gso_segment(struct sk_buff *skb,
netdev_features_t features)
{
/* gso_size = MSS, gso_segs = 원본 길이 / MSS */
unsigned int mss = skb_shinfo(skb)->gso_size;
/* skb_segment()로 실제 분할 수행 */
/* 각 세그먼트에 TCP/IP 헤더 복사, 시퀀스 번호 조정 */
/* 마지막 세그먼트에만 PSH 플래그 설정 */
segs = tcp_gso_segment(skb, features);
/* 각 세그먼트의 체크섬 재계산 */
return segs;
}
코드 설명
- 7-10행
skb_shared_info의gso_type필드에 따라 프로토콜별 세그먼테이션 콜백이 선택됩니다. TCP, UDP, SCTP 등 각 프로토콜이 고유한 GSO 콜백을 등록합니다. - 11행
skb_mac_gso_segment()는 MAC 계층부터 세그먼테이션을 시작하여, 각 계층의 GSO 콜백을 체인으로 호출합니다. - 21행
gso_size는 MSS(Maximum Segment Size)에 해당하며, 각 세그먼트의 페이로드 크기를 결정합니다. - 26행
tcp_gso_segment()가 실제 분할을 수행합니다. 각 세그먼트에 TCP 헤더를 복사하고, 시퀀스 번호를mss씩 증가시키며, 마지막 세그먼트에만 PSH 플래그를 설정합니다.
| GSO 타입 플래그 | 프로토콜 | 콜백 | 비고 |
|---|---|---|---|
SKB_GSO_TCPV4 | TCP over IPv4 | tcp4_gso_segment() | 가장 일반적 |
SKB_GSO_TCPV6 | TCP over IPv6 | tcp6_gso_segment() | IPv6 확장 헤더 처리 |
SKB_GSO_UDP_L4 | UDP (GSO) | __udp_gso_segment() | UDP GSO (커널 4.18+) |
SKB_GSO_PARTIAL | 부분 GSO | 드라이버별 | NIC가 외부 헤더만 처리 |
SKB_GSO_GRE | GRE 터널 | gre_gso_segment() | 터널 내부 세그먼테이션 |
SKB_GSO_UDP_TUNNEL | VXLAN/Geneve | 터널별 | 캡슐화 후 세그먼테이션 |
TX Completion과 BQL
TX Completion은 NIC가 패킷 전송을 완료한 후 DMA 매핑을 해제하고 sk_buff를 반환하는 과정입니다. BQL(Byte Queue Limits)은 드라이버의 TX 큐에 과도한 바이트가 적체되는 것을 방지하여 qdisc 계층의 지연을 최소화합니다.
/* TX Completion 처리 (NAPI TX clean) — 드라이버 패턴 */
static int driver_tx_clean(struct driver_ring *ring, int budget)
{
unsigned int total_bytes = 0, total_packets = 0;
while (total_packets < budget) {
struct tx_desc *desc = &ring->desc[ring->next_to_clean];
if (!(desc->status & TX_DESC_DONE))
break;
/* DMA 매핑 해제 */
dma_unmap_single(ring->dev, desc->dma_addr,
desc->length, DMA_TO_DEVICE);
/* sk_buff 해제 — 정상 전송이므로 consume_skb 사용 */
total_bytes += ring->tx_buf[ring->next_to_clean].bytecount;
dev_consume_skb_any(ring->tx_buf[ring->next_to_clean].skb);
total_packets++;
ring->next_to_clean = (ring->next_to_clean + 1) % ring->count;
}
/* BQL 완료 통지 — 큐에서 제거된 바이트 수 보고 */
netdev_tx_completed_queue(ring->txq, total_packets, total_bytes);
/* TX 큐가 멈췄으면 재개 */
if (unlikely(netif_tx_queue_stopped(ring->txq)) &&
driver_desc_unused(ring) >= TX_WAKE_THRESHOLD)
netif_tx_wake_queue(ring->txq);
return total_packets;
}
코드 설명
- 9-10행TX 디스크립터의 DD(Descriptor Done) 비트를 확인합니다. NIC가 전송을 완료하면 이 비트를 설정합니다.
- 13-14행
dma_unmap_single()로 DMA 매핑을 해제합니다. 이 시점 이후에야 sk_buff의 데이터 영역을 안전하게 해제할 수 있습니다. - 18행
dev_consume_skb_any()는consume_skb()의 변형으로, 정상적인 패킷 소비를 나타냅니다.kfree_skb()와 달리 drop_monitor에 기록되지 않습니다. - 25행
netdev_tx_completed_queue()는 BQL에 전송 완료된 바이트 수를 보고합니다. BQL은 이 정보를 사용하여 TX 큐의 최대 허용 바이트 수를 동적으로 조정합니다. - 28-30행TX 큐가 이전에
netif_tx_stop_queue()로 중단되었고, 충분한 디스크립터가 확보되면 큐를 재개합니다.
netdev_tx_sent_queue()(전송 시)와 netdev_tx_completed_queue()(완료 시)를 호출하여 in-flight 바이트 수를 추적합니다. in-flight 바이트가 동적으로 계산된 limit를 초과하면 qdisc에 netif_tx_stop_queue()를 통지하여 추가 enqueue를 중단합니다. 이를 통해 드라이버의 TX 버퍼가 과도하게 차는 것(bufferbloat)을 방지하고, qdisc(fq_codel 등)가 효과적으로 작동할 수 있게 합니다. BQL 상태는 /sys/class/net/<dev>/queues/tx-<N>/byte_queue_limits/에서 확인할 수 있습니다.
패킷 수명주기 End-to-End 추적
실제 운영 환경에서 TCP 연결의 패킷 하나가 생성되고 소멸되기까지의 전체 여정을 추적합니다. 아래는 클라이언트가 send()로 데이터를 보내고 서버가 recv()로 받는 과정에서 커널 내부에서 발생하는 모든 주요 이벤트를 시간순으로 정리한 것입니다.
패킷별 커널 내부 타임스탬프 추적
패킷의 지연을 정밀하게 분석하려면 각 단계의 타임스탬프를 기록해야 합니다. Linux 커널은 SO_TIMESTAMPING 소켓 옵션을 통해 소프트웨어/하드웨어 타임스탬프를 제공합니다.
/* SO_TIMESTAMPING으로 패킷별 정밀 타임스탬프 수집 */
int flags = SOF_TIMESTAMPING_TX_SOFTWARE |
SOF_TIMESTAMPING_RX_SOFTWARE |
SOF_TIMESTAMPING_SOFTWARE |
SOF_TIMESTAMPING_TX_HARDWARE |
SOF_TIMESTAMPING_RX_HARDWARE |
SOF_TIMESTAMPING_RAW_HARDWARE;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags));
주요 추적 지점과 tracepoint
| 추적 지점 | tracepoint / kprobe | 측정 내용 |
|---|---|---|
| sk_buff 할당 | skb:skb_copy_datagram_iovec | 유저 버퍼 복사 시점 |
| qdisc enqueue | qdisc:qdisc_enqueue | TC 큐잉 지연 |
| 드라이버 전송 | net:net_dev_start_xmit | 드라이버 진입 시점 |
| NIC 전송 완료 | net:net_dev_xmit | HW 전송 완료 |
| 패킷 수신 | net:netif_receive_skb | 소프트웨어 수신 시점 |
| 패킷 드롭 | skb:kfree_skb | 드롭 위치와 원인 |
| TCP 재전송 | tcp:tcp_retransmit_skb | 재전송 발생 시점 |
| TCP 상태 전이 | sock:inet_sock_set_state | 소켓 상태 변경 |
# ftrace로 패킷 경로 추적
echo 1 > /sys/kernel/debug/tracing/events/net/netif_receive_skb/enable
echo 1 > /sys/kernel/debug/tracing/events/net/net_dev_start_xmit/enable
echo 1 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enable
cat /sys/kernel/debug/tracing/trace_pipe
# bpftrace로 TCP 재전송 추적
bpftrace -e 'tracepoint:tcp:tcp_retransmit_skb {
printf("retransmit: sport=%d dport=%d state=%d\n",
args->sport, args->dport, args->state);
}'
# perf로 패킷 드롭 위치 추적
perf record -g -e skb:kfree_skb -- sleep 10
perf script | head -50
sk_buff 구조체
struct sk_buff는 네트워크 패킷을 표현하는 핵심 자료구조입니다. head/data/tail/end 4개 포인터로 버퍼를 관리하며, 각 프로토콜 계층이 skb_push()/skb_pull()로 헤더를 추가/제거합니다. 소속 소켓(sk), 네트워크 디바이스(dev), 프로토콜(protocol), 계층별 헤더 오프셋(Offset) 등 메타데이터를 포함합니다.
주요 sk_buff 조작 함수
| 함수 | 동작 | 사용 시점 |
|---|---|---|
alloc_skb(size, gfp) | sk_buff + 데이터 버퍼 할당 | 패킷 생성 시 |
skb_reserve(skb, len) | data/tail을 len만큼 전진 (headroom 확보) | 드라이버 초기화, 헤더 공간 예약 |
skb_put(skb, len) | tail을 len만큼 전진 (데이터 추가) | 패킷 데이터 기록 |
skb_push(skb, len) | data를 len만큼 후퇴 (헤더 추가) | L3/L2 헤더 추가 (송신) |
skb_pull(skb, len) | data를 len만큼 전진 (헤더 제거) | L2/L3 헤더 파싱 (수신) |
skb_clone(skb, gfp) | 메타데이터만 복제, 데이터 공유 | tcpdump, Netfilter 복제 |
skb_copy(skb, gfp) | 메타데이터 + 데이터 전체 복제 | 데이터 수정이 필요한 경우 |
kfree_skb(skb) | 참조 카운트(Reference Count) 감소, 0이면 해제 | 드롭, 처리 완료 |
consume_skb(skb) | 정상 소비로 해제 (drop_monitor 무시) | 정상 처리 완료 |
sk_buff 할당 내부 구현
alloc_skb()는 네트워크 스택에서 가장 빈번하게 호출되는 할당 함수입니다. sk_buff 메타데이터 구조체와 데이터 버퍼를 별도로 할당하며, 캐시 효율성을 위해 SLAB/SLUB 할당자의 전용 캐시(skbuff_head_cache)를 사용합니다.
/* net/core/skbuff.c — alloc_skb() 내부 구현 (간략화) */
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
int flags, int node)
{
struct sk_buff *skb;
u8 *data;
unsigned int osize;
/* 1. sk_buff 메타데이터 할당 (전용 kmem_cache) */
skb = kmem_cache_alloc_node(skbuff_head_cache,
gfp_mask & ~GFP_DMA, node);
if (unlikely(!skb))
return NULL;
/* 2. 데이터 버퍼 할당 (size + skb_shared_info 크기) */
size = SKB_DATA_ALIGN(size);
size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
data = kmalloc_reserve(size, gfp_mask, node, &osize);
if (unlikely(!data))
goto nodata;
size = osize - SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
/* 3. sk_buff 필드 초기화 */
skb->head = data;
skb->data = data;
skb_reset_tail_pointer(skb);
skb->end = skb->tail + size;
skb->mac_header = (typeof(skb->mac_header))~0U;
skb->transport_header = (typeof(skb->transport_header))~0U;
/* 4. skb_shared_info 초기화 (end 포인터 직후) */
struct skb_shared_info *shinfo = skb_shinfo(skb);
memset(shinfo, 0, offsetof(struct skb_shared_info, dataref));
atomic_set(&shinfo->dataref, 1);
skb_set_kcov_handle(skb, kcov_common_handle());
return skb;
}
코드 설명
- 10-11행
skbuff_head_cache는struct sk_buff전용 SLAB 캐시입니다. 네트워크 스택에서 가장 빈번한 할당 대상이므로, 전용 캐시를 사용하여 할당/해제 속도를 최적화합니다. - 16-18행데이터 버퍼 크기는 요청 크기에
skb_shared_info크기를 더한 값으로 정렬합니다.skb_shared_info는 데이터 버퍼 끝에 위치하며, 프래그먼트 정보, GSO 메타데이터 등을 저장합니다. - 25-28행
head와data를 버퍼 시작으로 설정하고,tail도 시작 위치로 리셋합니다.end는 데이터 영역의 끝(skb_shared_info 직전)을 가리킵니다. - 33-35행
skb_shinfo(skb)매크로는skb->end위치의skb_shared_info를 반환합니다.dataref를 1로 초기화하여 참조 카운트를 설정합니다.
Paged Data와 skb_shared_info
skb_shared_info는 sk_buff의 데이터 버퍼 끝(end 포인터 위치)에 배치되는 메타데이터 구조체입니다. 선형 데이터 영역 외에 페이지 기반 데이터(paged data), GSO 정보, frag_list를 관리합니다.
/* include/linux/skbuff.h — skb_shared_info 구조체 */
struct skb_shared_info {
__u8 flags; /* SKBFL_* 플래그 */
__u8 meta_len; /* 메타데이터 길이 */
__u8 nr_frags; /* frags[] 사용 수 (최대 17) */
__u8 tx_flags; /* SKBTX_* 플래그 */
unsigned short gso_size; /* GSO 세그먼트 크기 (MSS) */
unsigned short gso_segs; /* GSO 세그먼트 수 */
unsigned int gso_type; /* SKB_GSO_* 플래그 */
struct sk_buff *frag_list; /* IP 단편/GRO 리스트 */
struct skb_shared_hwtstamps hwtstamps; /* HW 타임스탬프 */
atomic_t dataref; /* 데이터 참조 카운트 */
unsigned int xdp_frags_size; /* XDP 프래그먼트 총 크기 */
/* Paged data — 각 프래그먼트는 page + offset + size */
skb_frag_t frags[MAX_SKB_FRAGS]; /* 최대 17개 */
};
/* skb_frag_t — 페이지 프래그먼트 */
typedef struct skb_frag {
struct {
struct page *p; /* 물리 페이지 */
} bv_page;
__u32 bv_offset; /* 페이지 내 오프셋 */
__u32 bv_len; /* 데이터 길이 */
} skb_frag_t;
코드 설명
- 5행
nr_frags는 현재 사용 중인 페이지 프래그먼트 수입니다.MAX_SKB_FRAGS는 기본 17로, 하나의 sk_buff가 최대 17개의 페이지 프래그먼트를 가질 수 있습니다. - 7-9행GSO 관련 필드들입니다.
gso_size는 세그먼트당 페이로드 크기(MSS),gso_segs는 세그먼트 수,gso_type은 프로토콜별 GSO 타입입니다. - 10행
frag_list는 IP 단편화로 생성된 프래그먼트 체인이나 GRO로 병합된 sk_buff 리스트를 가리킵니다. - 12행
dataref는 데이터 버퍼의 참조 카운트입니다.skb_clone()시 이 값이 증가하여 여러 sk_buff가 동일 데이터를 공유할 수 있습니다. - 16행
frags[]배열의 각 요소는 하나의 페이지 프래그먼트를 나타냅니다. DMA scatter-gather 전송에서 각 프래그먼트가 별도의 DMA 디스크립터에 매핑됩니다.
sk_buff 참조 카운트와 수명 관리
sk_buff에는 두 가지 참조 카운트가 존재합니다: skb->users(sk_buff 구조체 자체)와 skb_shared_info->dataref(데이터 버퍼)입니다. 이 이중 참조 모델은 메타데이터와 데이터의 독립적인 수명 관리를 가능하게 합니다.
/* sk_buff 참조 카운트 관리 함수들 */
/* skb_get() — sk_buff 참조 카운트 증가 */
static inline struct sk_buff *skb_get(struct sk_buff *skb)
{
refcount_inc(&skb->users);
return skb;
}
/* kfree_skb() — 드롭으로 인한 해제 (drop_monitor 추적) */
void kfree_skb_reason(struct sk_buff *skb,
enum skb_drop_reason reason)
{
if (!skb_unref(skb))
return;
/* drop_reason 추적 포인트 발생 */
trace_kfree_skb(skb, __builtin_return_address(0), reason);
__kfree_skb(skb);
}
/* consume_skb() — 정상 소비로 해제 (drop_monitor 무시) */
void consume_skb(struct sk_buff *skb)
{
if (!skb_unref(skb))
return;
/* consume 추적 포인트 발생 (drop으로 기록하지 않음) */
trace_consume_skb(skb);
__kfree_skb(skb);
}
코드 설명
- 4-8행
skb_get()은 sk_buff의users참조 카운트를 증가시킵니다. tcpdump(AF_PACKET)에서 패킷 복사본을 유지할 때, Netfilter에서 패킷을 검사할 때 등에 사용됩니다. - 11-20행
kfree_skb_reason()은 패킷이 드롭된 경우에 사용됩니다.trace_kfree_skb추적 포인트를 발생시켜drop_monitor도구나perf로 드롭 원인과 위치를 추적할 수 있습니다. - 23-31행
consume_skb()는 패킷이 정상적으로 처리 완료된 경우에 사용됩니다.trace_consume_skb를 발생시키며 drop_monitor에 기록되지 않습니다. TX 완료, recv() 복사 후 해제 등에서 사용합니다.
enum skb_drop_reason이 도입되어 드롭 원인을 세분화합니다. SKB_DROP_REASON_NOT_SPECIFIED, SKB_DROP_REASON_NO_SOCKET, SKB_DROP_REASON_TCP_OLD_ACK 등 수십 가지 원인을 구분할 수 있습니다. perf trace --event skb:kfree_skb로 추적하면 드롭 위치와 원인을 함께 확인할 수 있습니다.
Page Pool 프레임워크
Page Pool은 NIC 드라이버의 RX 경로에서 페이지 할당/해제 오버헤드를 줄이기 위한 페이지 재활용 프레임워크입니다. 페이지를 DMA 매핑 상태로 캐싱하여, 매 패킷마다 alloc_page()/dma_map_page()를 호출하는 비용을 제거합니다.
/* include/net/page_pool/types.h — Page Pool 생성 */
struct page_pool_params {
unsigned int flags; /* PP_FLAG_* */
unsigned int order; /* 페이지 order (보통 0) */
unsigned int pool_size; /* 풀 크기 (Ring Buffer 크기) */
int nid; /* NUMA 노드 */
struct device *dev; /* DMA 매핑용 디바이스 */
enum dma_data_direction dma_dir; /* DMA_FROM_DEVICE */
unsigned int max_len; /* 최대 버퍼 길이 */
unsigned int offset; /* headroom 오프셋 */
};
/* 드라이버 초기화 시 Page Pool 생성 */
struct page_pool *pool = page_pool_create(¶ms);
/* NAPI poll에서 페이지 할당 (DMA 매핑 포함) */
struct page *page = page_pool_dev_alloc_pages(pool);
/* 패킷 처리 완료 후 페이지 반환 (재활용) */
page_pool_put_full_page(pool, page, false);
/* sk_buff에서 Page Pool 페이지 사용 시 */
skb_mark_for_recycle(skb); /* consume_skb 시 자동 재활용 */
코드 설명
- 3행
PP_FLAG_DMA_MAP은 페이지 할당 시 자동으로 DMA 매핑을 수행합니다.PP_FLAG_DMA_SYNC_DEV는 재활용 시 DMA 동기화도 자동 처리합니다. - 14행
page_pool_create()는 per-CPU 캐시와 공유 풀을 초기화합니다. per-CPU 캐시는 락 없이 빠르게 할당/반환할 수 있습니다. - 17행
page_pool_dev_alloc_pages()는 먼저 per-CPU 캐시에서 페이지를 찾고, 없으면 공유 풀, 최후에는 페이지 할당자(alloc_pages_node())를 사용합니다. - 20행드라이버가 직접 페이지를 반환하면 재활용 경로로 진입합니다. DMA 매핑이 유지되므로 다음 RX에서 재매핑이 불필요합니다.
- 23행
skb_mark_for_recycle()은 sk_buff가consume_skb()로 해제될 때 내부 페이지를 Page Pool로 자동 반환하도록 표시합니다.
소켓 API와 커널 내부 매핑
유저 공간의 BSD 소켓 API 호출이 커널 내부에서 어떤 함수 체인으로 변환되는지를 정리합니다. 이 매핑을 알면 네트워크 문제 디버깅(Debugging) 시 커널 소스의 어디를 확인해야 하는지 바로 파악할 수 있습니다.
소켓 계층 (Socket Layer)
소켓은 유저스페이스 프로세스와 커널 네트워크 스택을 연결하는 인터페이스입니다. struct socket(VFS 인터페이스)과 struct sock(프로토콜 계층)의 이중 구조로 설계되어, 유저 공간 시스템 콜과 프로토콜 구현을 깔끔하게 분리합니다.
struct socket { /* VFS/유저 인터페이스 */
socket_state state; /* SS_CONNECTED 등 */
short type; /* SOCK_STREAM, SOCK_DGRAM */
struct file *file; /* VFS file 연결 */
struct sock *sk; /* 프로토콜 소켓 */
const struct proto_ops *ops; /* sendmsg/recvmsg 등 */
};
struct sock { /* 프로토콜 계층 (TCP/UDP) */
struct sock_common __sk_common;
struct sk_buff_head sk_receive_queue; /* 수신 큐 */
struct sk_buff_head sk_write_queue; /* 송신 큐 */
atomic_t sk_wmem_alloc; /* 송신 버퍼 사용량 */
atomic_t sk_rmem_alloc; /* 수신 버퍼 사용량 */
int sk_sndbuf; /* SO_SNDBUF */
int sk_rcvbuf; /* SO_RCVBUF */
/* ... */
};
소켓 생성 과정
유저 공간에서 socket(AF_INET, SOCK_STREAM, 0)을 호출하면 커널 내부에서 다음 과정이 진행됩니다:
sys_socket()→__sock_create():struct socket할당inet_create(): AF_INET 패밀리에 등록된 프로토콜(TCP)에서struct sock할당sock_map_fd(): VFS 파일 디스크립터(File Descriptor)에 매핑하여read()/write()/poll()가능하게 함- 프로토콜별 초기화:
tcp_v4_init_sock()에서 TCP 관련 필드(혼잡 제어, 재전송 타이머(Timer) 등) 초기화
/* net/socket.c — __sock_create() 핵심 로직 (간략화) */
int __sock_create(struct net *net, int family, int type,
int protocol, struct socket **res, int kern)
{
struct socket *sock;
const struct net_proto_family *pf;
/* 1. socket 구조체 할당 (inode 포함) */
sock = sock_alloc();
if (!sock)
return -ENFILE;
sock->type = type;
/* 2. 주소 패밀리별 핸들러 조회 */
pf = rcu_dereference(net_families[family]);
/* AF_INET → inet_family_ops */
/* AF_UNIX → unix_family_ops */
/* 3. 패밀리별 create 호출 */
err = pf->create(net, sock, protocol, kern);
/* inet_create(): struct sock 할당, 프로토콜 ops 연결 */
/* 4. BPF cgroup 소켓 생성 훅 */
err = security_socket_post_create(sock, family, type, protocol, kern);
*res = sock;
return 0;
}
코드 설명
- 9행
sock_alloc()은 sockfs에 inode를 생성하고,struct socket을 할당합니다. 소켓은 VFS에서 특수 파일로 취급되어read()/write()/poll()이 가능합니다. - 16행
net_families[]배열에서 주소 패밀리(AF_INET, AF_UNIX 등)에 해당하는 핸들러를 찾습니다. 각 패밀리는sock_register()로 등록됩니다. - 21행AF_INET의 경우
inet_create()가 호출되어struct sock을 할당하고, 소켓 타입(SOCK_STREAM, SOCK_DGRAM)에 따라 TCP/UDP의proto_ops를 연결합니다. - 24행LSM(Linux Security Module)과 BPF cgroup 소켓 프로그램이 소켓 생성을 검사합니다. 정책에 위반되면 소켓 생성이 거부됩니다.
소켓-VFS 연결 구조
소켓은 VFS의 파일 디스크립터를 통해 유저 공간에 노출됩니다. socket() 시스템 콜은 내부적으로 sock_alloc()으로 inode를 생성하고, sock_map_fd()로 파일 디스크립터에 매핑합니다. 이 구조 덕분에 select()/poll()/epoll()로 소켓을 다른 파일과 함께 멀티플렉싱할 수 있습니다.
/* VFS → 소켓 연결 체인 */
/* fd → struct file → file->private_data → struct socket */
/* struct socket → socket->sk → struct sock */
/* struct sock → (struct tcp_sock 또는 struct udp_sock) */
/* 소켓 파일 연산: 소켓도 파일처럼 동작합니다 */
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek, /* seek 미지원 */
.read_iter = sock_read_iter, /* read() → recvmsg() */
.write_iter = sock_write_iter, /* write() → sendmsg() */
.poll = sock_poll, /* poll/epoll 지원 */
.unlocked_ioctl = sock_ioctl, /* ioctl 지원 */
.mmap = sock_mmap, /* mmap (AF_PACKET 등) */
.release = sock_close, /* close() → 소켓 정리 */
.fasync = sock_fasync, /* SIGIO 비동기 알림 */
.sendpage = sock_sendpage, /* sendfile/splice 지원 */
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
};
코드 설명
- 1-4행유저 공간의 fd는 VFS
struct file을 가리키고,private_data로struct socket에 접근합니다.socket->sk로 프로토콜별struct sock(TCP의 경우struct tcp_sock)에 접근합니다. - 10-11행
read()/write()시스템 콜이 소켓에 사용되면 내부적으로recvmsg()/sendmsg()로 변환됩니다. 이 덕분에 소켓을 파이프나 파일과 동일한 API로 다룰 수 있습니다. - 12행
sock_poll()은 소켓의 읽기/쓰기 가능 상태를 확인합니다. epoll에서EPOLLIN/EPOLLOUT이벤트를 감시할 때 이 함수가 호출됩니다. - 17행
sock_sendpage()는sendfile()시스템 콜에서 사용됩니다. 파일의 페이지를 소켓의 sk_buff에 직접 매핑하여 제로카피 전송을 수행합니다.
주요 소켓 주소 패밀리
| 주소 패밀리 | 용도 | 소켓 타입 | 핵심 소스 |
|---|---|---|---|
AF_INET | IPv4 네트워킹 | STREAM, DGRAM, RAW | net/ipv4/af_inet.c |
AF_INET6 | IPv6 네트워킹 | STREAM, DGRAM, RAW | net/ipv6/af_inet6.c |
AF_UNIX | 로컬 IPC | STREAM, DGRAM, SEQPACKET | net/unix/af_unix.c |
AF_PACKET | 원시 패킷 캡처 | RAW, DGRAM | net/packet/af_packet.c |
AF_NETLINK | 커널-유저 통신 | RAW, DGRAM | net/netlink/af_netlink.c |
AF_XDP | 고성능 패킷 처리 | RAW | net/xdp/xsk.c |
소켓 메모리 관리
소켓은 송신/수신 버퍼 크기를 sk_sndbuf/sk_rcvbuf로 제한하며, 실제 사용량을 sk_wmem_alloc/sk_rmem_alloc으로 추적합니다. TCP의 경우 전역 메모리 압력(memory pressure) 메커니즘이 모든 TCP 소켓의 메모리 할당을 조절합니다.
/* net/core/sock.c — 소켓 메모리 회계 핵심 로직 */
/* 수신 방향: sk_buff를 수신 큐에 추가할 때 */
int __sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
unsigned long flags;
int err = 0;
/* sk_rmem_alloc + skb->truesize가 sk_rcvbuf 초과 시 드롭 */
if (atomic_read(&sk->sk_rmem_alloc) + skb->truesize >=
(unsigned int)READ_ONCE(sk->sk_rcvbuf)) {
err = -ENOMEM;
goto out;
}
/* skb->truesize만큼 수신 메모리 사용량 증가 */
skb_set_owner_r(skb, sk);
__skb_queue_tail(&sk->sk_receive_queue, skb);
sk->sk_data_ready(sk); /* 대기 프로세스 깨움 */
out:
return err;
}
/* TCP 전역 메모리 압력 (sysctl tcp_mem) */
/* tcp_mem[0]: 정상 — 모든 소켓 자유롭게 할당 */
/* tcp_mem[1]: 압력 — 새 할당 제한 시작 */
/* tcp_mem[2]: 한계 — 새 할당 거부 (OOM) */
long sysctl_tcp_mem[3]; /* 페이지 단위 */
코드 설명
- 10-11행현재 수신 메모리 사용량(
sk_rmem_alloc)에 새 sk_buff의truesize(메타데이터 + 데이터 포함 실제 크기)를 더한 값이 수신 버퍼 한도(sk_rcvbuf)를 초과하면 패킷을 드롭합니다. - 17행
skb_set_owner_r()은 sk_buff의 소유권을 소켓에 할당하고,sk_rmem_alloc을skb->truesize만큼 증가시킵니다. - 19행
sk_data_ready()콜백은 기본적으로sock_def_readable()로,sk_wq에 대기 중인 프로세스를 깨웁니다. epoll 환경에서는ep_poll_callback()이 호출됩니다. - 24-27행TCP 전역 메모리 한도는
sysctl net.ipv4.tcp_mem으로 설정합니다. 시스템의 총 TCP 소켓 메모리 사용량이 이 임계값을 초과하면 memory pressure 상태에 진입합니다.
# 소켓 메모리 관련 sysctl 확인
sysctl net.ipv4.tcp_mem # 전역 TCP 메모리 한도 (페이지)
sysctl net.ipv4.tcp_rmem # 소켓당 수신 버퍼 [min default max]
sysctl net.ipv4.tcp_wmem # 소켓당 송신 버퍼 [min default max]
sysctl net.core.rmem_max # SO_RCVBUF 최대값
sysctl net.core.wmem_max # SO_SNDBUF 최대값
# 현재 TCP 메모리 사용량 확인
cat /proc/net/sockstat | grep TCP # TCP: inuse, orphan, tw, alloc, mem
TCP 자동 버퍼 튜닝 (Autotuning)
Linux TCP는 수신/송신 버퍼 크기를 연결의 RTT와 대역폭에 따라 자동으로 조정합니다. tcp_rmem/tcp_wmem의 min/default/max 값이 자동 튜닝의 범위를 결정합니다.
/* net/ipv4/tcp_input.c — TCP 자동 수신 버퍼 조정 (간략화) */
void tcp_rcv_space_adjust(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
int time, space;
/* RTT 동안 수신된 데이터량으로 필요 버퍼 추정 */
time = tcp_stamp_us_delta(tp->tcp_mstamp, tp->rcvq_space.time);
if (time < (tp->rcv_rtt_est.rtt_us >> 3))
return;
/* BDP (Bandwidth-Delay Product) 계산 */
space = 2 * (tp->copied_seq - tp->rcvq_space.seq);
space = max(tp->rcvq_space.space, space);
/* sk_rcvbuf 조정 (tcp_rmem[2]가 상한) */
if (space > READ_ONCE(sk->sk_rcvbuf)) {
int new_rcvbuf = min(space, READ_ONCE(
sock_net(sk)->ipv4.sysctl_tcp_rmem[2]));
WRITE_ONCE(sk->sk_rcvbuf, new_rcvbuf);
}
/* 수신 윈도우 스케일링도 같이 조정 */
tp->rcvq_space.space = space;
tp->rcvq_space.seq = tp->copied_seq;
tp->rcvq_space.time = tp->tcp_mstamp;
}
코드 설명
- 8-10행RTT(Round Trip Time)의 1/8 시간이 경과해야 조정을 시도합니다. 너무 자주 조정하면 오버헤드가 커집니다.
- 13행BDP(Bandwidth-Delay Product)를 추정합니다. RTT 동안 수신된 데이터량의 2배를 필요 버퍼 크기로 계산합니다. 이를 통해 대역폭이 높거나 RTT가 긴 연결에서 자동으로 버퍼를 확대합니다.
- 17-20행
sk_rcvbuf를 추정 BDP까지 확대하되,tcp_rmem[2](max) 값을 초과하지 않습니다. 전역 메모리 압력(memory pressure) 상태에서는 확대가 제한됩니다.
memory.max으로 컨테이너의 전체 메모리를 제한할 때 소켓 버퍼 메모리도 포함됩니다. memory.stat에서 sock 필드가 소켓 메모리 사용량을 보여줍니다. TCP 자동 튜닝은 cgroup 메모리 한도를 인식하여, 한도에 가까우면 버퍼 확대를 자제합니다. memory.events의 sock_max 이벤트로 소켓 메모리 한도 도달을 모니터링할 수 있습니다.
# TCP 자동 튜닝 확인
sysctl net.ipv4.tcp_moderate_rcvbuf # 1 = 자동 튜닝 활성 (기본)
# 특정 연결의 현재 버퍼 크기 확인
ss -tim dst 10.0.0.1:443
# 출력: skmem:(r0,rb6291456,t0,tb87380,...)
# rb = 현재 수신 버퍼 크기 (자동 조정됨)
# tb = 현재 송신 버퍼 크기
# 자동 튜닝 비활성화 (특정 소켓에서)
# setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
# SO_RCVBUF를 명시적으로 설정하면 해당 소켓의 자동 튜닝이 비활성화됩니다
# cgroup v2 소켓 메모리 확인
cat /sys/fs/cgroup/system.slice/docker-<ID>.scope/memory.stat | grep sock
# sock 12345678 (소켓 버퍼 사용량, 바이트)
SO_REUSEPORT와 eBPF 소켓 선택
SO_REUSEPORT는 여러 소켓이 동일한 주소:포트에 바인드할 수 있게 하여, 수신 연결을 여러 프로세스/스레드에 분산합니다. 기본적으로 해시 기반으로 소켓을 선택하며, BPF 프로그램을 부착하여 커스텀 선택 로직을 구현할 수 있습니다.
/* SO_REUSEPORT 기본 사용법 */
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
bind(fd, addr, addrlen);
listen(fd, backlog);
/* BPF 소켓 선택 프로그램 부착 */
/* net/core/sock_reuseport.c — reuseport_select_sock() */
struct sock *reuseport_select_sock(struct sock *sk,
u32 hash, struct sk_buff *skb, int hdr_len)
{
struct sock_reuseport *reuse = rcu_dereference(sk->sk_reuseport_cb);
struct bpf_prog *prog;
prog = rcu_dereference(reuse->prog);
if (prog) {
/* BPF 프로그램이 소켓 인덱스를 반환 */
u32 index = bpf_prog_run_save_cb(prog, skb);
sk2 = reuse->socks[index];
} else {
/* 기본: 해시 기반 소켓 선택 */
sk2 = reuse->socks[reciprocal_scale(hash, reuse->num_socks)];
}
return sk2;
}
코드 설명
- 1-5행여러 프로세스가 동일한 주소:포트에
SO_REUSEPORT를 설정하고 바인드하면, 커널이 수신 패킷을 자동으로 분산합니다. - 15-19행BPF 프로그램이 부착되어 있으면, 프로그램이 패킷 내용(소스 IP, 쿠키 등)을 검사하여 소켓 인덱스를 반환합니다. 이를 통해 특정 클라이언트의 연결을 특정 워커에 고정(session affinity)할 수 있습니다.
- 22행BPF 프로그램이 없으면 패킷의 4-tuple 해시를 사용하여 소켓을 선택합니다.
reciprocal_scale()은 나눗셈 없이 해시를 소켓 수 범위로 스케일합니다.
epoll과 소켓 통합
epoll은 소켓의 이벤트(수신 데이터, 연결 완료, 쓰기 가능 등)를 효율적으로 감시하는 I/O 멀티플렉싱 메커니즘입니다. 소켓의 sk_data_ready, sk_write_space 등의 콜백이 epoll의 대기 큐와 연결됩니다.
/* fs/eventpoll.c + net/core/sock.c — epoll-소켓 연동 경로 */
/* 1. epoll_ctl(EPOLL_CTL_ADD) 시 */
/* → ep_insert() → ep_item_poll() → sock_poll() */
/* → init_waitqueue_func_entry(&ep_entry->wait, ep_poll_callback) */
/* → add_wait_queue(sk_sleep(sk), &ep_entry->wait) */
/* 2. 패킷 수신 시 콜백 체인 */
/* tcp_v4_rcv() → tcp_data_queue() */
/* → sk->sk_data_ready(sk) */
/* → sock_def_readable() */
/* → wake_up_interruptible_sync_poll(sk_wq, EPOLLIN) */
/* → ep_poll_callback() */
/* → ep를 rdllist에 추가 → epoll_wait() 반환 */
/* 3. Edge-Triggered vs Level-Triggered */
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; /* Edge-Triggered */
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
/* ET 모드에서는 데이터를 모두 읽을 때까지 반복해야 합니다 */
/* 그렇지 않으면 새 데이터 도착까지 알림을 받지 못합니다 */
코드 설명
- 3-6행
epoll_ctl(EPOLL_CTL_ADD)는 소켓의 대기 큐에ep_poll_callback을 등록합니다. 이후 소켓 상태 변경 시 이 콜백이 호출됩니다. - 8-14행패킷이 TCP 수신 큐에 삽입되면
sk_data_ready→sock_def_readable()→ep_poll_callback()체인으로 epoll에 통지됩니다.ep_poll_callback()은 해당 epoll 항목을 ready list에 추가하고,epoll_wait()에서 대기 중인 프로세스를 깨웁니다. - 17-19행
EPOLLET(Edge-Triggered) 모드에서는 상태가 변경될 때만 알림을 받습니다. Level-Triggered(기본)에서는 조건이 충족되는 동안 계속 알림을 받습니다.
소켓 해시 테이블 조회
수신 패킷이 전송 계층에 도달하면, 4-tuple(소스 IP/포트, 목적지 IP/포트) 해시를 기반으로 대상 소켓을 찾습니다. TCP는 inet_hashinfo 해시 테이블에서, UDP는 udp_table에서 조회합니다.
/* net/ipv4/inet_hashtables.c — TCP 소켓 조회 (간략화) */
struct sock *__inet_lookup_established(
struct net *net,
struct inet_hashinfo *hashinfo,
const __be32 saddr, const __be16 sport,
const __be32 daddr, const u16 hnum,
const int dif, const int sdif)
{
/* 1. 4-tuple 해시 계산 */
unsigned int hash = inet_ehashfn(net, daddr, hnum,
saddr, sport);
unsigned int slot = hash & hashinfo->ehash_mask;
/* 2. 해시 버킷에서 ESTABLISHED 소켓 검색 */
struct inet_ehash_bucket *head = &hashinfo->ehash[slot];
sk_for_each_rcu(sk, &head->chain) {
if (sk->sk_hash != hash)
continue;
if (inet_match(net, sk, saddr, daddr, sport, hnum, dif, sdif))
return sk;
}
/* 3. 없으면 LISTEN 해시에서 검색 */
return __inet_lookup_listener(net, hashinfo, skb,
doff, saddr, sport, daddr, hnum, dif, sdif);
}
코드 설명
- 10-12행
inet_ehashfn()은 4-tuple을 해시합니다. Jenkins 해시 또는 SipHash를 사용하여 해시 충돌 공격을 방지합니다. 해시 시드는 부팅 시 랜덤으로 생성됩니다. - 15-21행ESTABLISHED 해시 테이블(ehash)에서 4-tuple이 정확히 일치하는 소켓을 찾습니다. RCU 보호 하에 조회하므로 락 없이 빠르게 검색할 수 있습니다.
- 24-26행ESTABLISHED 소켓이 없으면 LISTEN 해시 테이블에서 목적지 포트로 리스닝 소켓을 찾습니다.
SO_REUSEPORT가 설정된 경우 여러 리스닝 소켓 중 하나를 선택합니다.
# TCP 해시 테이블 크기 확인
dmesg | grep "TCP established hash"
# TCP established hash table entries: 524288 (order: 10, 4194304 bytes)
# 현재 소켓 통계
ss -s # 전체 소켓 요약
cat /proc/net/sockstat # TCP/UDP 소켓 상세 통계
cat /proc/net/sockstat6 # IPv6 소켓 통계
# LISTEN 소켓의 accept queue 상태
ss -lnt | awk '{print $2, $3, $4}' # Recv-Q Send-Q Local Address
# Recv-Q = 현재 accept queue 대기 수
# Send-Q = accept queue 최대 크기 (backlog)
네트워크 계층 (IP)
네트워크 계층은 패킷의 소스/목적지 주소 관리, 라우팅, 단편화/재조립을 담당합니다. Linux 커널은 IPv4(net/ipv4/)와 IPv6(net/ipv6/)를 독립적으로 구현하며, 듀얼 스택으로 동시 운영합니다.
IPv4 수신 처리 흐름
/* net/ipv4/ip_input.c — IPv4 수신 진입점 */
int ip_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev)
{
struct net *net = dev_net(dev);
/* 1. IP 헤더 길이, 버전, 체크섬 검증 */
/* 2. pskb_may_pull()로 헤더 접근 보장 */
/* 3. Netfilter NF_INET_PRE_ROUTING 훅 통과 */
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
}
/* ip_rcv_finish → ip_route_input_noref()로 FIB 조회 */
/* 결과에 따라: */
/* 로컬 대상 → ip_local_deliver() → L4 전달 */
/* 포워딩 → ip_forward() → ip_output() */
/* 멀티캐스트 → ip_mr_input() */
IP 단편화와 재조립
MTU(Maximum Transmission Unit)보다 큰 패킷은 단편화가 필요합니다. 송신 측에서 ip_fragment()가 수행하며, 수신 측에서 ip_defrag()가 단편을 재조립합니다. DF(Don't Fragment) 비트가 설정된 경우 단편화 대신 ICMP "Fragmentation Needed" 메시지를 반환합니다. Path MTU Discovery는 이 메커니즘을 활용합니다.
| 항목 | IPv4 | IPv6 |
|---|---|---|
| 주소 길이 | 32비트 (4바이트) | 128비트 (16바이트) |
| 헤더 크기 | 가변 (20~60바이트) | 고정 40바이트 + 확장 헤더 |
| 단편화 | 라우터/호스트 모두 가능 | 소스 호스트만 가능 |
| 체크섬(Checksum) | 헤더 체크섬 포함 | 헤더 체크섬 없음 (L2/L4 의존) |
| 브로드캐스트 | 지원 | 미지원 (멀티캐스트로 대체) |
| 이웃 탐색 | ARP | NDP (ICMPv6 기반) |
| 기본 MTU | 576바이트 (권장 최소) | 1280바이트 (필수 최소) |
| 커널 소스 | net/ipv4/ | net/ipv6/ |
이웃 서브시스템 (Neighbour Subsystem)
이웃 서브시스템은 L3 주소(IP)를 L2 주소(MAC)로 변환하는 프레임워크입니다. IPv4에서는 ARP, IPv6에서는 NDP(Neighbor Discovery Protocol)를 사용합니다. struct neighbour는 이웃 항목을 나타내며, 상태 머신을 통해 주소 해석의 진행 상황을 관리합니다.
/* include/net/neighbour.h — struct neighbour 핵심 필드 */
struct neighbour {
struct neighbour *next; /* 해시 체인 */
struct neigh_table *tbl; /* ARP 또는 NDP 테이블 */
struct neigh_parms *parms; /* 인터페이스별 파라미터 */
unsigned long updated; /* 마지막 업데이트 시각 */
rwlock_t lock;
refcount_t refcnt;
unsigned int arp_queue_len_bytes;
struct sk_buff_head arp_queue; /* 해석 대기 패킷 큐 */
struct timer_list timer; /* 상태 전이 타이머 */
unsigned long used; /* 마지막 사용 시각 */
atomic_t probes; /* 보낸 프로브 수 */
u8 nud_state; /* NUD_* 상태 */
u8 type;
u8 dead;
u8 protocol;
u8 ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))];
/* 하드웨어 주소 (MAC) */
struct net_device *dev; /* 출력 디바이스 */
const struct neigh_ops *ops; /* 출력 콜백 */
u8 primary_key[]; /* L3 주소 (IP) */
};
코드 설명
- 10행
arp_queue는 MAC 주소가 아직 해석되지 않은 상태에서 전송을 시도하는 패킷을 임시로 저장하는 큐입니다. 주소 해석이 완료되면 대기 중인 패킷을 모두 전송합니다. - 14행
nud_state는 NUD(Neighbor Unreachability Detection) 상태입니다. NUD_INCOMPLETE, NUD_REACHABLE, NUD_STALE, NUD_DELAY, NUD_PROBE, NUD_FAILED 등의 값을 가집니다. - 18-19행
ha는 해석된 하드웨어 주소(MAC 주소)입니다. ARP 응답이나 NDP Neighbor Advertisement를 수신하면 이 필드가 설정됩니다.
/* net/ipv4/arp.c — arp_process() 핵심 로직 (간략화) */
static int arp_process(struct net *net, struct sock *sk,
struct sk_buff *skb)
{
struct arphdr *arp = arp_hdr(skb);
unsigned char *sha, *tha; /* 소스/타깃 하드웨어 주소 */
__be32 sip, tip; /* 소스/타깃 IP 주소 */
/* ARP 헤더에서 주소 추출 */
arp_ptr = (unsigned char *)(arp + 1);
sha = arp_ptr; /* Sender Hardware Address */
memcpy(&sip, arp_ptr + dev->addr_len, 4); /* Sender IP */
tha = arp_ptr + dev->addr_len + 4; /* Target HA */
memcpy(&tip, arp_ptr + 2 * dev->addr_len + 4, 4);
if (arp->ar_op == htons(ARPOP_REQUEST)) {
/* ARP Request: 대상 IP가 로컬이면 ARP Reply 전송 */
if (inet_addr_type(net, tip) == RTN_LOCAL) {
arp_send_dst(ARPOP_REPLY, ETH_P_ARP, sip, dev,
tip, sha, dev->dev_addr, sha, reply_dst);
}
/* Sender의 MAC-IP 매핑을 이웃 캐시에 갱신 */
neigh_event_ns(&arp_tbl, sha, &sip, dev);
} else if (arp->ar_op == htons(ARPOP_REPLY)) {
/* ARP Reply: Sender의 MAC-IP 매핑 갱신 */
n = __neigh_lookup(&arp_tbl, &sip, dev, 0);
if (n) {
neigh_update(n, sha, NUD_REACHABLE, ...);
/* arp_queue의 대기 패킷 전송 */
}
}
}
코드 설명
- 10-14행ARP 패킷에서 Sender/Target의 하드웨어 주소(MAC)와 프로토콜 주소(IP)를 추출합니다. ARP 헤더 직후에 이 주소들이 순서대로 배치됩니다.
- 16-21행ARP Request를 수신하면, 대상 IP가 로컬 인터페이스에 할당된 IP인지 확인하고, 맞으면 ARP Reply를 전송합니다. 프록시 ARP가 활성화된 경우 다른 서브넷의 IP에도 응답합니다.
- 23행
neigh_event_ns()는 ARP Request의 Sender 정보로 이웃 캐시를 갱신합니다. 이를 통해 요청자의 MAC 주소를 학습합니다. - 26-29행ARP Reply를 수신하면 이웃 항목을
NUD_REACHABLE상태로 업데이트합니다.arp_queue에 대기 중인 패킷들이 있으면 이 시점에 모두 전송됩니다.
/* net/core/neighbour.c — neigh_resolve_output() TX 경로 */
/* IP → 이웃 → dev_queue_xmit 출력 흐름 */
int neigh_resolve_output(struct neighbour *neigh,
struct sk_buff *skb)
{
if (!neigh_event_send(neigh, skb)) {
/* MAC 주소 해석 완료 → L2 헤더 추가 후 전송 */
rc = dev_hard_header(skb, dev, ntohs(skb->protocol),
neigh->ha, NULL, skb->len);
if (rc >= 0)
rc = dev_queue_xmit(skb);
}
/* neigh_event_send()가 1을 반환하면: */
/* ARP/NDP 요청 전송 후 skb를 arp_queue에 대기 */
return rc;
}
코드 설명
- 6행
neigh_event_send()는 이웃 항목의 상태를 확인합니다. MAC 주소가 해석된 상태(REACHABLE/STALE)이면 0을 반환하고, 미해석 상태이면 ARP/NDP 요청을 전송하고 1을 반환합니다. - 8-9행
dev_hard_header()는neigh->ha(해석된 MAC 주소)를 사용하여 이더넷 헤더를 추가합니다. - 13-14행MAC 주소가 아직 없으면 패킷은
arp_queue에 대기합니다. ARP Reply가 도착하면 대기 중인 패킷이 모두 전송됩니다.
# ARP/이웃 캐시 관리 명령어
ip neigh show # ARP 캐시 전체 표시
ip neigh show dev eth0 # eth0의 이웃 항목만
ip neigh add 192.168.1.1 lladdr aa:bb:cc:dd:ee:ff dev eth0 # 정적 항목 추가
ip neigh flush dev eth0 # eth0의 캐시 초기화
# 이웃 파라미터 조정
sysctl -w net.ipv4.neigh.eth0.base_reachable_time_ms=30000 # REACHABLE 타임아웃
sysctl -w net.ipv4.neigh.eth0.gc_stale_time=60 # STALE GC 시간
sysctl -w net.ipv4.neigh.default.gc_thresh3=4096 # 최대 이웃 항목 수
# IPv6 이웃 탐색
ip -6 neigh show # NDP 캐시
ip_rcv_finish() 내부 상세
ip_rcv_finish()는 Netfilter PRE_ROUTING 훅을 통과한 패킷의 라우팅 결정과 목적지 캐시 설정을 담당합니다. Early Demux 최적화를 통해 TCP/UDP 소켓이 이미 결정된 경우 FIB 조회를 생략할 수 있습니다.
/* net/ipv4/ip_input.c — ip_rcv_finish() 핵심 로직 (간략화) */
static int ip_rcv_finish(struct net *net, struct sock *sk,
struct sk_buff *skb)
{
struct net_device *dev = skb->dev;
int err;
/* 1. Early Demux — 소켓 캐시에서 dst_entry 획득 */
if (READ_ONCE(net->ipv4.sysctl_ip_early_demux) &&
!skb_dst(skb) && !skb->sk) {
int protocol = ip_hdr(skb)->protocol;
/* TCP/UDP의 early_demux 콜백 호출 */
/* 4-tuple로 소켓을 미리 찾아 dst_entry 캐시 사용 */
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot && ipprot->early_demux)
ipprot->early_demux(skb);
}
/* 2. dst_entry가 없으면 FIB 조회 수행 */
if (!skb_valid_dst(skb)) {
err = ip_route_input_noref(skb, ip_hdr(skb)->daddr,
ip_hdr(skb)->saddr,
ip_hdr(skb)->tos, dev);
if (unlikely(err))
goto drop;
}
/* 3. dst_entry의 input 함수 호출 */
/* 로컬: ip_local_deliver() */
/* 포워딩: ip_forward() */
return dst_input(skb);
}
코드 설명
- 9-17행Early Demux는 IP 처리 초기에 TCP/UDP 소켓을 미리 찾아서, 해당 소켓의
dst_entry캐시를 재사용하는 최적화입니다.sysctl_ip_early_demux가 활성화(기본)되어 있으면 FIB 조회를 생략하여 패킷당 수백 ns를 절약할 수 있습니다. - 21-26행Early Demux가 실패하거나 비활성화된 경우
ip_route_input_noref()로 FIB(Forwarding Information Base) 조회를 수행합니다. FIB 조회 결과에 따라dst_entry의input함수가ip_local_deliver(로컬) 또는ip_forward(포워딩)로 설정됩니다. - 32행
dst_input(skb)는skb_dst(skb)->input(skb)를 호출하여 패킷을 다음 처리 단계로 전달합니다.
IP 단편화/재조립 내부
IP 단편화는 패킷이 출력 인터페이스의 MTU를 초과할 때 ip_do_fragment()에서 수행됩니다. 재조립은 수신 측에서 ip_defrag()가 단편을 수집하여 원본 패킷을 복원합니다.
/* net/ipv4/ip_output.c — ip_do_fragment() 핵심 로직 (간략화) */
int ip_do_fragment(struct net *net, struct sock *sk,
struct sk_buff *skb,
int (*output)(struct net *, struct sock *,
struct sk_buff *))
{
unsigned int mtu = ip_skb_dst_mtu(sk, skb);
unsigned int hlen = ip_hdrlen(skb);
unsigned int left = skb->len - hlen; /* 페이로드 길이 */
unsigned int len = (mtu - hlen) & ~7; /* 8바이트 정렬 */
unsigned int offset = (ntohs(ip_hdr(skb)->frag_off) &
IP_OFFSET) << 3;
/* DF 비트 설정 시 ICMP "Fragmentation Needed" 반환 */
if (unlikely((ip_hdr(skb)->frag_off & htons(IP_DF)) &&
!skb->ignore_df)) {
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
htonl(mtu));
goto fail;
}
/* 각 단편에 대해: */
while (left > 0) {
/* 단편 sk_buff 생성 */
/* IP 헤더 복사, frag_off 설정, MF 비트 설정 */
/* output() 콜백으로 전송 */
left -= len;
offset += len;
}
}
코드 설명
- 7-10행MTU에서 IP 헤더 길이를 빼고 8바이트 경계로 정렬하여 각 단편의 최대 페이로드 크기를 결정합니다. IP 단편의 오프셋은 8바이트 단위이므로 페이로드 크기도 8의 배수여야 합니다.
- 15-20행DF(Don't Fragment) 비트가 설정된 경우 단편화 대신 ICMP "Fragmentation Needed" 메시지를 소스 호스트에 반환합니다. Path MTU Discovery는 이 메커니즘을 활용하여 경로의 최소 MTU를 탐색합니다.
- 23-29행각 단편에 원본 IP 헤더를 복사하고,
frag_off필드에 오프셋과 MF(More Fragments) 비트를 설정합니다. 마지막 단편에는 MF 비트가 설정되지 않습니다.
/* net/ipv4/ip_fragment.c — ip_defrag() 재조립 핵심 (간략화) */
int ip_defrag(struct net *net, struct sk_buff *skb, u32 user)
{
struct ipq *qp;
/* IP ID + 소스/목적지 IP로 재조립 큐 검색/생성 */
qp = ip_find(net, ip_hdr(skb), user, vif);
if (!qp)
goto drop;
/* 단편을 큐에 삽입 (오프셋 순서 유지) */
ip_frag_queue(qp, skb);
/* 모든 단편이 도착했는지 확인 */
if (ip_frag_reasm(qp, skb, prev_tail, net)) {
/* 재조립 완료 — 원본 패킷 복원 */
return 0;
}
return -EINPROGRESS; /* 아직 단편 대기 중 */
}
코드 설명
- 7행
ip_find()는 IP Identification, 소스/목적지 IP, 프로토콜을 키로 해시 테이블에서 재조립 큐를 찾거나 새로 생성합니다. - 12행
ip_frag_queue()는 단편을 오프셋 순서대로 큐에 삽입합니다. 겹치는 단편은 탐지하여 처리하며, 보안을 위해 과도한 단편 수에 대한 제한이 있습니다. - 15행
ip_frag_reasm()은 모든 단편이 도착했는지(첫 단편의 오프셋 0 + 마지막 단편의 MF=0 + 전체 길이 일치) 확인하고, 성공하면 하나의 sk_buff로 재조립합니다.
net.ipv4.ipfrag_max_dist(기본 64)로 비순차 단편 수를 제한하고, ipfrag_time(기본 30초)으로 타임아웃을 설정하며, ipfrag_high_thresh/ipfrag_low_thresh로 전체 메모리 사용량을 제한합니다. 메모리 임계값 초과 시 ip_evictor()가 가장 오래된 큐를 강제로 제거합니다.
# IP 단편화 관련 sysctl 설정
sysctl net.ipv4.ipfrag_time # 재조립 타임아웃 (기본 30초)
sysctl net.ipv4.ipfrag_high_thresh # 메모리 상한 (기본 4MB)
sysctl net.ipv4.ipfrag_low_thresh # 메모리 하한 (기본 3MB)
sysctl net.ipv4.ipfrag_max_dist # 비순차 단편 최대 수 (기본 64)
# 단편화 통계 확인
cat /proc/net/snmp | grep -A1 Ip
# ReasmTimeout ReasmReqds ReasmOKs ReasmFails
# FragOKs FragFails FragCreates
nstat -az | grep -i frag
# IpReasmReqds — 재조립 요청 수
# IpReasmOKs — 재조립 성공 수
# IpReasmFails — 재조립 실패 수
# IpFragOKs — 단편화 성공 수
# IpFragFails — 단편화 실패 수 (DF 비트 등)
# PMTU Discovery 관련
sysctl net.ipv4.ip_no_pmtu_disc # 0: PMTUD 활성 (기본)
sysctl net.ipv4.tcp_mtu_probing # 0: 비활성, 1: ICMP 차단 시만, 2: 항상
# 경로 MTU 캐시 확인
ip route get 10.0.0.1 # mtu 필드 확인
ip route show cache # PMTU 캐시된 경로
IP 옵션과 확장 헤더
IPv4 옵션과 IPv6 확장 헤더는 패킷에 추가 정보를 전달하는 메커니즘입니다. 보안과 성능에 영향을 미치므로, 커널은 각 옵션을 엄격하게 검증합니다.
| IPv4 옵션 | 타입 | 용도 | 보안 고려 |
|---|---|---|---|
| Record Route | 7 | 경로 기록 | 정보 유출 위험, 일반적으로 차단 |
| Timestamp | 68 | 홉별 시간 기록 | 정보 유출 위험 |
| Loose Source Route | 131 | 경유 라우터 지정 | 보안 위험 — rp_filter로 차단 권장 |
| Strict Source Route | 137 | 정확한 경로 지정 | 보안 위험 — 차단 권장 |
| Router Alert | 148 | 라우터 특별 처리 요청 | IGMP/RSVP에서 사용 |
| IPv6 확장 헤더 | Next Header 값 | 용도 | 처리 |
|---|---|---|---|
| Hop-by-Hop | 0 | 모든 홉에서 처리 | Router Alert, Jumbo Payload |
| Routing | 43 | SRv6 세그먼트 라우팅 | SRv6 인프라에서 사용 |
| Fragment | 44 | 단편화 정보 | 소스만 단편화 가능 |
| Destination | 60 | 목적지 옵션 | MIPv6 등 |
| Authentication (AH) | 51 | 무결성 검증 | IPsec |
| ESP | 50 | 암호화 | IPsec |
# Source Routing 차단 (보안 강화)
sysctl -w net.ipv4.conf.all.accept_source_route=0
sysctl -w net.ipv6.conf.all.accept_source_route=0
# Reverse Path Filtering (스푸핑 방어)
sysctl -w net.ipv4.conf.all.rp_filter=1 # strict mode
sysctl -w net.ipv4.conf.all.rp_filter=2 # loose mode (멀티호밍 시)
# ICMP 리다이렉트 차단
sysctl -w net.ipv4.conf.all.accept_redirects=0
sysctl -w net.ipv4.conf.all.send_redirects=0
sysctl -w net.ipv6.conf.all.accept_redirects=0
전송 계층 (Transport Layer)
전송 계층은 종단 간 데이터 전달의 신뢰성, 흐름 제어(Flow Control), 멀티플렉싱을 담당합니다. Linux 커널은 struct proto 인터페이스를 통해 다양한 전송 프로토콜을 플러그인 방식으로 지원합니다.
TCP 개요
Linux TCP 구현은 RFC 793 상태 머신(CLOSED, LISTEN, SYN_SENT, SYN_RECEIVED, ESTABLISHED, FIN_WAIT_1, FIN_WAIT_2, CLOSE_WAIT, CLOSING, LAST_ACK, TIME_WAIT)을 정밀하게 구현합니다. 플러그인 방식의 혼잡 제어(CUBIC 기본, BBR, Reno, DCTCP 등), kTLS, Zero-Copy sendfile, RACK/TLP 재전송을 지원합니다.
/* 혼잡 제어 알고리즘 등록 인터페이스 */
struct tcp_congestion_ops {
struct list_head list;
u32 key;
u32 flags;
/* 혼잡 이벤트 콜백 */
void (*init)(struct sock *sk);
void (*cong_avoid)(struct sock *sk, u32 ack, u32 acked);
u32 (*ssthresh)(struct sock *sk);
void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);
void (*pkts_acked)(struct sock *sk, const struct ack_sample *sample);
/* ... */
char name[TCP_CA_NAME_MAX];
struct module *owner;
};
| 혼잡 제어 | 감지 방식 | 특징 | 사용 시나리오 |
|---|---|---|---|
| CUBIC | 손실 기반 | 3차 함수 cwnd 증가, 빠른 수렴 | 기본값, 범용 환경 |
| BBR | 대역폭/RTT 모델 | BtlBw x RTprop 기반, 버퍼 최소화 | 고지연, 패킷 손실 환경 |
| BBRv2 | 모델 기반 + ECN | 공정성(Fairness) 개선, 큐잉 감소 | BBR 후속, 범용 |
| DCTCP | ECN 기반 | ECN 비율에 비례하여 cwnd 조절 | 데이터센터 내부 |
| Reno | 손실 기반 | AIMD(가산 증가/승산 감소) | 레거시, 교육용 |
| Vegas | 지연 기반 | RTT 변화 감지 | 저지연 요구 환경 |
# TCP 혼잡 제어 알고리즘 관리
sysctl net.ipv4.tcp_congestion_control # 현재 기본 알고리즘
sysctl net.ipv4.tcp_available_congestion_control # 사용 가능한 알고리즘
sysctl net.ipv4.tcp_allowed_congestion_control # 비root 사용 허용
# BBR 활성화
sysctl -w net.core.default_qdisc=fq # BBR은 fq qdisc 필요
sysctl -w net.ipv4.tcp_congestion_control=bbr
# 소켓별 혼잡 제어 변경 (프로그래밍)
# setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, "bbr", 3);
# TCP 상태별 소켓 수 확인
ss -s | grep TCP
cat /proc/net/sockstat | grep TCP
# 특정 연결의 TCP 내부 상태 확인
ss -ti dst 10.0.0.1:443
# 출력: cubic wscale:7,7 rto:204 rtt:1.234/0.567
# cwnd:10 ssthresh:7 bytes_sent:12345 retrans:0/0
TCP 윈도우 관리
TCP는 수신 윈도우(rwnd)와 혼잡 윈도우(cwnd)의 최솟값으로 전송 속도를 결정합니다. 수신 윈도우는 수신자의 버퍼 가용 공간을 나타내고, 혼잡 윈도우는 네트워크의 가용 용량을 추정합니다.
/* TCP 전송 가능량 결정 */
/* effective_window = min(cwnd, rwnd) - in_flight */
/* net/ipv4/tcp_output.c — tcp_write_xmit() 핵심 로직 (간략화) */
static bool tcp_write_xmit(struct sock *sk,
unsigned int mss_now, int nonagle,
int push_one, gfp_t gfp)
{
struct tcp_sock *tp = tcp_sk(sk);
unsigned int cwnd_quota;
while ((skb = tcp_send_head(sk))) {
cwnd_quota = tcp_cwnd_test(tp, skb);
if (!cwnd_quota) {
/* cwnd 초과 — 전송 중단 */
tcp_chrono_start(sk, TCP_CHRONO_RWND_LIMITED);
break;
}
/* Nagle 알고리즘 확인 */
if (unlikely(!tcp_nagle_test(tp, skb, mss_now, nonagle)))
break;
/* 수신 윈도우 확인 */
if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))
break;
/* TSO/GSO 세그먼트 수 결정 */
tso_segs = tcp_init_tso_segs(skb, mss_now);
/* 전송 */
tcp_transmit_skb(sk, skb, 1, gfp);
tcp_event_new_data_sent(sk, skb);
}
return !tp->packets_out;
}
코드 설명
- 12-17행
tcp_cwnd_test()는tp->snd_cwnd에서 현재 in-flight 패킷 수를 빼서 전송 가능한 세그먼트 수를 계산합니다. cwnd를 초과하면 전송을 중단합니다. - 20-21행Nagle 알고리즘은 작은 세그먼트의 불필요한 전송을 방지합니다. MSS 미만의 데이터가 있고 ACK 미확인 데이터가 존재하면 전송을 지연합니다.
TCP_NODELAY옵션으로 비활성화할 수 있습니다. - 24-25행
tcp_snd_wnd_test()는 수신자가 광고한 수신 윈도우(rwnd) 범위 내인지 확인합니다. 수신자의 버퍼가 가득 차면(rwnd=0) Zero Window Probe를 시작합니다. - 31행
tcp_transmit_skb()가 TCP/IP 헤더를 추가하고 IP 계층으로 전달합니다. 재전송 큐에 sk_buff 복사본을 유지합니다.
UDP 개요
UDP는 비연결형 프로토콜로, 상태 머신이나 혼잡 제어 없이 패킷을 전달합니다. udp_sendmsg()에서 IP 계층으로 즉시 전달하고, udp_rcv()에서 포트 번호 기반으로 소켓에 전달합니다. UDP-GRO를 지원하여 수신 성능을 개선하며, QUIC(UDP 기반 HTTP/3) 프로토콜의 기반이 됩니다.
kTLS (Kernel TLS)
kTLS는 TLS 레코드 계층의 암호화/복호화를 커널 공간에서 수행하는 기능입니다. 유저 공간의 TLS 라이브러리(OpenSSL 등)가 핸드셰이크를 완료한 후, 세션 키를 커널에 전달하면 이후의 데이터 전송은 커널 TCP 계층에서 직접 암호화됩니다. sendfile()과 결합하면 TLS 정적 파일 서빙에서 유저 공간 복사를 완전히 제거할 수 있습니다.
/* kTLS 설정 — 유저 공간 (OpenSSL 핸드셰이크 후) */
/* 1. ULP(Upper Layer Protocol)로 TLS 설정 */
setsockopt(fd, SOL_TCP, TCP_ULP, "tls", sizeof("tls"));
/* 2. TX 방향 암호화 키 설정 */
struct tls12_crypto_info_aes_gcm_128 crypto_info;
crypto_info.info.version = TLS_1_2_VERSION;
crypto_info.info.cipher_type = TLS_CIPHER_AES_GCM_128;
memcpy(crypto_info.iv, iv, TLS_CIPHER_AES_GCM_128_IV_SIZE);
memcpy(crypto_info.key, key, TLS_CIPHER_AES_GCM_128_KEY_SIZE);
memcpy(crypto_info.salt, salt, TLS_CIPHER_AES_GCM_128_SALT_SIZE);
memcpy(crypto_info.rec_seq, rec_seq, TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE);
setsockopt(fd, SOL_TLS, TLS_TX, &crypto_info, sizeof(crypto_info));
/* 3. RX 방향 복호화 키 설정 (선택적) */
setsockopt(fd, SOL_TLS, TLS_RX, &crypto_info_rx, sizeof(crypto_info_rx));
/* 이후 send()/sendfile()은 커널에서 자동 암호화 */
sendfile(fd, file_fd, NULL, file_size);
/* 파일 → 페이지 캐시 → 커널 TLS 암호화 → NIC */
/* 유저 공간 복사 0회! */
코드 설명
- 4행
TCP_ULP로 TLS를 설정하면, TCP 소켓에 kTLS 계층이 삽입됩니다.struct tls_context가 생성되어 암호화 컨텍스트를 관리합니다. - 7-14행TLS 핸드셰이크에서 협상된 암호화 키, IV(Initialization Vector), salt, 레코드 시퀀스 번호를 커널에 전달합니다. 커널은 이 정보로 AES-GCM 암호화를 수행합니다.
- 16행
TLS_TX를 설정하면 이후의 모든send()/sendfile()데이터가 커널 내부에서 TLS 레코드로 암호화됩니다. - 21-23행kTLS +
sendfile()조합은 파일 데이터를 유저 공간으로 복사하지 않고 커널에서 직접 암호화하여 전송합니다. Nginx 등의 웹 서버에서 정적 파일 서빙 시 CPU 사용률을 30~50% 절감할 수 있습니다.
| kTLS 모드 | 암호화 위치 | 성능 | 지원 환경 |
|---|---|---|---|
| SW kTLS | 커널 CPU | 유저 복사 제거 | 모든 NIC |
| HW kTLS | NIC 하드웨어 | CPU 암호화 비용 제거 | Mellanox CX-6+, Intel E810 |
# kTLS 활성화 확인
grep TLS /boot/config-$(uname -r) # CONFIG_TLS=m
modprobe tls # 모듈 로드
# kTLS 통계 확인
cat /proc/net/tls_stat
# TlsCurrTxSw TlsCurrRxSw TlsCurrTxDevice TlsCurrRxDevice
# TlsTxSw TlsRxSw TlsTxDevice TlsRxDevice
# NIC kTLS 하드웨어 오프로드 확인
ethtool -k eth0 | grep tls
# tls-hw-tx-offload: on
# tls-hw-rx-offload: on
SCTP 개요
SCTP(Stream Control Transmission Protocol)는 멀티스트리밍과 멀티호밍을 지원하는 전송 프로토콜입니다. 하나의 연결(association) 내에 여러 스트림을 가질 수 있어 Head-of-Line Blocking을 방지하고, 여러 IP 주소를 동시에 사용하여 경로 장애에 대한 복원력을 제공합니다.
TCP 수신 경로 내부 (tcp_v4_rcv)
tcp_v4_rcv()는 IP 계층에서 TCP 패킷을 수신하는 진입점입니다. 소켓 조회, 시퀀스 번호 검증, ACK 처리, 데이터 큐잉을 수행합니다. 성능을 위해 Fast Path(헤더 예측)와 Slow Path를 구분합니다.
/* net/ipv4/tcp_ipv4.c — tcp_v4_rcv() 핵심 로직 (간략화) */
int tcp_v4_rcv(struct sk_buff *skb)
{
const struct tcphdr *th = tcp_hdr(skb);
struct sock *sk;
/* 1. TCP 체크섬 검증 */
if (skb_checksum_init(skb, IPPROTO_TCP, inet_compute_pseudo))
goto csum_error;
/* 2. 소켓 조회 (4-tuple 해시) */
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th),
th->source, th->dest, sdif, &refcounted);
if (!sk)
goto no_tcp_socket;
/* 3. TIME_WAIT 상태 처리 */
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;
/* 4. 소켓 락 획득 시도 */
if (!sock_owned_by_user(sk)) {
/* Fast path: 소켓 락 미보유 → 즉시 처리 */
ret = tcp_v4_do_rcv(sk, skb);
} else {
/* Slow path: 유저 프로세스가 락 보유 중 */
/* backlog 큐에 추가, release_sock() 시 처리 */
if (!tcp_add_backlog(sk, skb))
goto discard_and_relse;
}
return ret;
}
코드 설명
- 8-9행TCP 체크섬을 검증합니다. NIC가 체크섬 오프로드를 수행한 경우(
CHECKSUM_UNNECESSARY) 이 검증은 생략됩니다. - 12-15행
__inet_lookup_skb()로 4-tuple(소스/목적지 IP:포트)에 해당하는 소켓을 찾습니다. ESTABLISHED 해시 → LISTEN 해시 순서로 검색합니다. - 22-24행소켓 락이 비어 있으면
tcp_v4_do_rcv()로 즉시 처리합니다. 이 경로에서 헤더 예측(header prediction)이 활성화되어 있으면 Fast Path로 ACK 처리와 데이터 큐잉을 최적화합니다. - 25-29행유저 프로세스가 소켓 락을 보유 중이면(예:
recv()실행 중) backlog 큐에 패킷을 추가합니다. 유저 프로세스가release_sock()을 호출하면 backlog 큐의 패킷이 일괄 처리됩니다.
/* net/ipv4/tcp_input.c — TCP Fast Path (Header Prediction) */
int tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
/* 헤더 예측: 예상 시퀀스, 예상 ACK, 단순 데이터 패킷 */
if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
/* ★ Fast Path — 99%+ 패킷이 이 경로 */
/* 순수 ACK (데이터 없음) */
if (skb->len == tcp_header_len) {
tcp_ack(sk, skb, 0);
__kfree_skb(skb);
return;
}
/* 데이터 패킷 — 소켓 수신 큐에 직접 추가 */
eaten = tcp_queue_rcv(sk, skb, &fragstolen);
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
tcp_event_data_recv(sk, skb);
__tcp_ack_snd_check(sk, 0); /* 지연 ACK 확인 */
return;
}
/* ★ Slow Path — OOO, 재전송, FIN 등 */
tcp_data_queue(sk, skb);
}
코드 설명
- 7-8행Header Prediction은 TCP 플래그와 시퀀스 번호가 예상값과 일치하는지 검사합니다. 대부분의 데이터 전송에서 패킷은 순서대로 도착하므로, 이 조건을 만족하는 비율이 99% 이상입니다.
- 13-16행데이터가 없는 순수 ACK 패킷은
tcp_ack()으로 혼잡 창 업데이트와 재전송 타이머 관리만 수행합니다. - 20-24행순서가 맞는 데이터 패킷은
tcp_queue_rcv()로 소켓 수신 큐에 직접 추가합니다. 지연 ACK 로직을 확인하여 즉시 ACK 또는 타이머 기반 지연 ACK를 결정합니다. - 28행Fast Path 조건이 불만족하면 Slow Path(
tcp_data_queue())로 진입합니다. Out-of-Order 패킷 관리, SACK 처리, 재전송 탐지 등 복잡한 로직이 여기에 있습니다.
TCP 타이머 시스템
TCP는 신뢰성 있는 전송을 위해 여러 타이머를 운영합니다. 각 타이머는 특정 상태에서 활성화되며, 만료 시 재전송, 연결 종료, 또는 keepalive 프로브를 수행합니다.
| 타이머 | 용도 | 기본 설정 | 관련 sysctl |
|---|---|---|---|
| 재전송 타이머 | ACK 미수신 세그먼트 재전송 | RTO 기반 (200ms~120s) | tcp_retries1, tcp_retries2 |
| 지연 ACK 타이머 | ACK 전송 지연 (피기백 기회) | 40~200ms | tcp_delack_min |
| Keepalive 타이머 | 유휴 연결 생존 확인 | 7200초 (2시간) | tcp_keepalive_time, _intvl, _probes |
| TIME_WAIT 타이머 | 이전 연결 패킷 소멸 대기 | 60초 (2*MSL) | tcp_tw_timeout |
| FIN_WAIT2 타이머 | orphan FIN_WAIT2 소켓 정리 | 60초 | tcp_fin_timeout |
| TLP 타이머 | Tail Loss Probe 전송 | 2*SRTT 또는 최소 10ms | — |
| Zero Window 프로브 | 수신 창 0 시 프로빙 | 지수 백오프 | — |
/* net/ipv4/tcp_timer.c — TCP 타이머 콜백 등록 */
void tcp_init_xmit_timers(struct sock *sk)
{
/* 재전송/TLP 타이머 */
inet_csk_init_xmit_timers(sk,
&tcp_write_timer, /* 재전송 타이머 만료 시 */
&tcp_delack_timer, /* 지연 ACK 타이머 만료 시 */
&tcp_keepalive_timer); /* Keepalive 타이머 만료 시 */
}
/* tcp_write_timer() → tcp_retransmit_timer() */
void tcp_retransmit_timer(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
/* RTO 만료: 가장 오래된 미확인 세그먼트 재전송 */
if (tcp_retransmit_skb(sk, tcp_rtx_queue_head(sk), 1))
return;
/* RTO 지수 백오프 (최대 120초) */
inet_csk(sk)->icsk_rto = min(inet_csk(sk)->icsk_rto << 1,
TCP_RTO_MAX);
/* 최대 재시도 횟수 초과 시 연결 중단 */
if (tcp_out_of_resources(sk, true))
return;
tcp_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
}
코드 설명
- 5-8행소켓 초기화 시 세 가지 핵심 타이머의 만료 콜백을 등록합니다. 각 타이머는 독립적으로 동작하며,
inet_csk(INET connection socket) 프레임워크가 관리합니다. - 17행
tcp_retransmit_skb()가 재전송 큐(tcp_rtx_queue)의 첫 번째(가장 오래된) 미확인 세그먼트를 재전송합니다. - 21-22행RTO(Retransmission Timeout)를 2배로 증가시킵니다(지수 백오프). 최대값은
TCP_RTO_MAX(120초)입니다. 초기 RTO는 1초이며, RTT 측정 후 SRTT 기반으로 동적 계산됩니다. - 25-26행재시도 횟수가
tcp_retries2(기본 15)를 초과하면 연결을 포기하고 소켓을 닫습니다.
TCP 제로카피 메커니즘
TCP에서 제로카피(Zero-Copy)는 유저 공간 버퍼의 데이터를 커널 버퍼로 복사하지 않고 직접 NIC에 전달하는 기법입니다. MSG_ZEROCOPY(송신), sendfile()(파일→소켓), TCP receive zero-copy(수신) 등 여러 방식이 있습니다.
/* MSG_ZEROCOPY 송신 — 유저 공간 */
setsockopt(fd, SOL_SOCKET, SO_ZEROCOPY, &one, sizeof(one));
/* 제로카피 전송 */
send(fd, buf, len, MSG_ZEROCOPY);
/* 완료 통지 수신 (errqueue에서) */
struct msghdr msg = {};
struct sock_extended_err *serr;
char cmsg_buf[64];
msg.msg_control = cmsg_buf;
msg.msg_controllen = sizeof(cmsg_buf);
/* MSG_ERRQUEUE에서 완료 통지 읽기 */
recvmsg(fd, &msg, MSG_ERRQUEUE);
/* serr->ee_origin == SO_EE_ORIGIN_ZEROCOPY */
/* serr->ee_data = 완료된 전송의 카운터 */
/* 이 시점 이후 buf를 안전하게 재사용할 수 있습니다 */
코드 설명
- 2행
SO_ZEROCOPY를 설정하면 소켓이 제로카피 전송을 지원합니다. 내부적으로sk->sk_socket->file->f_op에SOCK_ZEROCOPY플래그가 설정됩니다. - 5행
MSG_ZEROCOPY플래그로 전송하면, 커널이 유저 버퍼를get_user_pages()로 고정(pin)하여 DMA 전송에 직접 사용합니다. 데이터 복사가 발생하지 않습니다. - 16행NIC가 DMA 전송을 완료하면 error queue(
MSG_ERRQUEUE)를 통해 완료 통지를 보냅니다. 유저 공간은 이 통지를 받은 후에야 버퍼를 수정하거나 해제할 수 있습니다.
/* sendfile() — 커널 내부 경로 */
/* sys_sendfile() → do_sendfile() → do_splice_direct() */
/* → splice_direct_to_actor() → pipe + tcp_splice_read/write */
/* sendfile(out_fd, in_fd, offset, count) */
/* 파일 → 파이프 → 소켓 전송 (유저 공간 복사 없음) */
/* 페이지 캐시의 페이지를 sk_buff의 frags[]에 직접 매핑 */
/* 커널 내부: tcp_sendpage() 또는 tcp_sendmsg_locked() */
/* skb_fill_page_desc()로 파일 페이지를 skb에 추가 */
skb_fill_page_desc(skb, i, page, offset, len);
/* → NIC DMA scatter-gather로 직접 전송 */
코드 설명
- 2-3행
sendfile()은 파일 시스템의 페이지 캐시에서 페이지를 직접 소켓의 sk_buff에 매핑합니다. 유저 공간으로의 데이터 복사가 완전히 제거됩니다. - 11행
skb_fill_page_desc()는 파일의 페이지를 sk_buff의frags[]배열에 추가합니다. NIC가 scatter-gather DMA를 지원하면 여러 페이지를 하나의 전송으로 처리할 수 있습니다.
TCP 연결 관리 내부
TCP 연결 수립은 SYN Queue(half-open)와 Accept Queue(established)의 이중 큐 구조로 관리됩니다. SYN Flood 공격 방어를 위한 SYN Cookie 메커니즘과, 지연을 줄이기 위한 TCP Fast Open도 구현되어 있습니다.
/* TCP 연결 수립 큐 구조 */
/* SYN Queue (half-open 연결): request_sock */
/* 클라이언트 SYN → request_sock 생성 → SYN+ACK 전송 */
/* 최대 크기: sysctl tcp_max_syn_backlog (기본 256) */
/* Accept Queue (완전 연결): */
/* 클라이언트 ACK → full sock 생성 → accept queue 삽입 */
/* 최대 크기: listen(fd, backlog)의 backlog 인수 */
/* 또는 sysctl net.core.somaxconn (기본 4096) */
/* SYN Cookie 메커니즘 */
/* SYN Queue 오버플로우 시 활성화 (tcp_syncookies=1) */
u32 __cookie_v4_init_sequence(const struct iphdr *iph,
const struct tcphdr *th,
u16 *mssp)
{
/* ISN에 MSS, 타임스탬프, 비밀 키를 인코딩 */
/* SYN+ACK의 시퀀스 번호 자체에 연결 정보 저장 */
/* → request_sock 할당 불필요 → 메모리 고갈 방지 */
return secure_tcp_syn_cookie(saddr, daddr, sport, dport,
sseq, data);
}
/* TCP Fast Open (TFO) */
/* 첫 연결: 쿠키 요청 (TCP option) */
/* 이후 연결: SYN + 쿠키 + 데이터 → 0-RTT 핸드셰이크 */
/* sysctl: net.ipv4.tcp_fastopen = 3 (클라이언트+서버) */
코드 설명
- 3-5행SYN Queue는 SYN을 수신했지만 아직 3-way 핸드셰이크가 완료되지 않은 half-open 연결을 저장합니다.
request_sock구조체로 표현되며,tcp_max_syn_backlog로 크기를 제한합니다. - 7-10행Accept Queue는 핸드셰이크가 완료된 연결을
accept()가 가져갈 때까지 보관합니다. 크기는listen()의 backlog 인수와somaxconn중 작은 값으로 제한됩니다. - 12-22행SYN Cookie는 SYN Queue가 가득 찼을 때 활성화됩니다.
request_sock을 할당하지 않고, SYN+ACK의 ISN(Initial Sequence Number)에 연결 정보(MSS, 타임스탬프)를 암호학적으로 인코딩합니다. 클라이언트의 ACK에서 이 정보를 복원하여 연결을 생성합니다. - 25-28행TCP Fast Open은 이전에 발급받은 쿠키를 SYN 패킷에 포함하여, 서버가 SYN+ACK를 보내기 전에 이미 데이터를 처리할 수 있게 합니다. HTTP 요청 등에서 1-RTT를 절약합니다.
UDP 내부 상세
UDP는 TCP에 비해 단순하지만, 고성능 처리를 위한 다양한 최적화가 구현되어 있습니다. VXLAN, WireGuard 등의 터널링 프로토콜이 UDP 캡슐화를 사용하므로, UDP 수신 경로의 성능은 네트워크 가상화에 직접적인 영향을 미칩니다.
/* net/ipv4/udp.c — __udp4_lib_rcv() 핵심 로직 (간략화) */
int __udp4_lib_rcv(struct sk_buff *skb,
struct udp_table *udptable, int proto)
{
struct sock *sk;
const struct udphdr *uh = udp_hdr(skb);
/* 1. UDP 체크섬 검증 */
if (udp4_csum_init(skb, uh, proto))
goto csum_error;
/* 2. 소켓 조회 (dest port 해시) */
sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
if (sk) {
/* 3a. UDP 캡슐화 (VXLAN, WireGuard 등) */
if (udp_sk(sk)->encap_type) {
int ret = udp_sk(sk)->encap_rcv(sk, skb);
if (ret <= 0)
return -ret;
/* ret > 0: 캡슐화 처리 안 함, 일반 UDP 계속 */
}
/* 3b. 일반 UDP 수신 */
return udp_unicast_rcv_skb(sk, skb, uh);
}
/* 4. 소켓 없음 → ICMP Port Unreachable */
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
goto drop;
}
코드 설명
- 13행UDP 소켓 조회는 목적지 포트를 기반으로
udp_table해시 테이블에서 수행합니다. 연결 개념이 없으므로 포트 번호만으로 소켓을 찾습니다. - 17-21행UDP 캡슐화가 설정된 소켓(VXLAN, WireGuard, GTP 등)은
encap_rcv콜백으로 내부 패킷을 디캡슐화합니다. 이 콜백이 0을 반환하면 패킷 처리가 완료된 것이며, 양수를 반환하면 일반 UDP 수신으로 계속합니다. - 29행매칭되는 소켓이 없으면 ICMP "Port Unreachable" 메시지를 전송합니다. UDP 스캔 공격 시 이 ICMP 응답이 대량 생성될 수 있으므로,
icmp_ratelimitsysctl로 속도를 제한합니다.
UDP-GRO와 UDP GSO
UDP-GRO는 NIC 또는 GRO 계층에서 동일 플로우의 연속 UDP 패킷을 하나의 큰 sk_buff로 병합합니다. UDP GSO는 반대로 대형 UDP 데이터그램을 세그먼트로 분할하여 전송합니다. 두 기능 모두 QUIC, WireGuard 등 UDP 기반 프로토콜의 성능을 크게 향상시킵니다.
/* UDP GSO 송신 — 유저 공간 설정 */
int gso_size = 1472; /* 세그먼트 크기 (MTU - IP/UDP 헤더) */
setsockopt(fd, IPPROTO_UDP, UDP_SEGMENT,
&gso_size, sizeof(gso_size));
/* 대형 데이터그램 전송 (최대 64KB) */
/* 커널이 자동으로 gso_size 단위로 분할 */
send(fd, large_buf, 65507, 0);
/* 또는 sendmsg로 세그먼트 크기 지정 */
struct msghdr msg = {};
char cmsg_buf[CMSG_SPACE(sizeof(uint16_t))];
msg.msg_control = cmsg_buf;
msg.msg_controllen = sizeof(cmsg_buf);
struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
cm->cmsg_level = SOL_UDP;
cm->cmsg_type = UDP_SEGMENT;
cm->cmsg_len = CMSG_LEN(sizeof(uint16_t));
*(uint16_t *)CMSG_DATA(cm) = gso_size;
sendmsg(fd, &msg, 0);
코드 설명
- 2-4행
UDP_SEGMENT소켓 옵션으로 GSO 세그먼트 크기를 설정합니다. 일반적으로 MTU에서 IP(20) + UDP(8) 헤더를 뺀 1472바이트를 사용합니다. - 8행64KB 근처의 대형 데이터를 한 번에 전송하면, 커널이
__udp_gso_segment()에서 자동으로 분할합니다. 시스템 콜 횟수가 1/44로 줄어들어 PPS가 크게 향상됩니다. - 16-21행
sendmsg()의 ancillary data로 세그먼트 크기를 패킷별로 다르게 지정할 수 있습니다. QUIC에서 패킷 크기가 가변인 경우 유용합니다.
/* UDP-GRO 수신 — 유저 공간 설정 */
int val = 1;
setsockopt(fd, IPPROTO_UDP, UDP_GRO, &val, sizeof(val));
/* recvmsg()에서 GRO된 대형 데이터그램 수신 */
/* cmsg로 각 세그먼트의 크기 정보 전달 */
struct cmsghdr *cm;
for (cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) {
if (cm->cmsg_type == UDP_GRO) {
uint16_t gro_size = *(uint16_t *)CMSG_DATA(cm);
/* gro_size 단위로 수신 데이터를 분리 처리 */
}
}
코드 설명
- 2-3행
UDP_GRO소켓 옵션을 활성화하면 GRO로 병합된 대형 UDP 데이터그램을 한 번의recvmsg()로 수신할 수 있습니다. - 9-12행GRO된 데이터의 원래 세그먼트 크기가 ancillary data(cmsg)로 전달됩니다. 유저 공간은 이 크기를 기반으로 개별 데이터그램 경계를 파악합니다.
UDP 송신 코크 모드 (Cork Mode)
UDP 코크 모드(UDP_CORK)는 여러 send() 호출의 데이터를 하나의 UDP 데이터그램으로 결합하여 전송합니다. 애플리케이션이 여러 조각의 데이터를 순차적으로 기록한 후 하나의 패킷으로 전송해야 할 때 유용합니다.
/* UDP Cork 모드 사용 */
int cork = 1;
setsockopt(fd, IPPROTO_UDP, UDP_CORK, &cork, sizeof(cork));
/* 여러 write()가 하나의 UDP 데이터그램에 축적 */
send(fd, header, header_len, 0); /* 아직 전송 안 됨 */
send(fd, payload, payload_len, 0); /* 아직 전송 안 됨 */
send(fd, trailer, trailer_len, 0); /* 아직 전송 안 됨 */
/* Cork 해제 시 축적된 데이터를 하나의 데이터그램으로 전송 */
cork = 0;
setsockopt(fd, IPPROTO_UDP, UDP_CORK, &cork, sizeof(cork));
/* 이 시점에 header+payload+trailer가 하나의 UDP 패킷으로 전송 */
코드 설명
- 2-3행
UDP_CORK를 1로 설정하면 이후의send()데이터가 소켓의 pending 버퍼에 축적됩니다. - 6-8행여러 번의
send()호출에도 불구하고, 데이터가 즉시 전송되지 않고sk_write_queue에 축적됩니다. - 11-12행
UDP_CORK를 0으로 해제하면udp_push_pending_frames()가 호출되어 축적된 모든 데이터가 하나의 UDP 데이터그램으로 전송됩니다.
프로토콜 등록 메커니즘
Linux 커널의 네트워크 스택은 프로토콜 독립적인 프레임워크로 설계되어, 새로운 네트워크/전송 프로토콜을 모듈로 등록할 수 있습니다. 이 구조 덕분에 IPv4, IPv6, TCP, UDP, SCTP 등이 동일한 인터페이스를 통해 동작합니다.
L3 프로토콜 등록: packet_type
L2(이더넷 등) 위에서 동작하는 L3 프로토콜은 struct packet_type을 dev_add_pack()으로 등록합니다. 패킷이 __netif_receive_skb()에 도달하면 skb->protocol(EtherType)에 따라 등록된 핸들러가 호출됩니다.
/* net/ipv4/af_inet.c — IPv4 프로토콜 등록 */
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP), /* EtherType 0x0800 */
.func = ip_rcv, /* 수신 핸들러 */
};
/* 초기화 시 등록 */
dev_add_pack(&ip_packet_type);
/* IPv6의 경우: ETH_P_IPV6 (0x86DD) → ipv6_rcv() */
/* ARP의 경우: ETH_P_ARP (0x0806) → arp_rcv() */
L4 프로토콜 등록: net_protocol
IP 계층 위에서 동작하는 L4 프로토콜은 struct net_protocol을 inet_add_protocol()로 등록합니다. IP 헤더의 프로토콜 번호에 따라 해당 핸들러가 호출됩니다.
/* net/ipv4/af_inet.c — TCP/UDP/ICMP 프로토콜 등록 */
static const struct net_protocol tcp_protocol = {
.handler = tcp_v4_rcv, /* TCP 수신 핸들러 */
.err_handler = tcp_v4_err, /* ICMP 에러 핸들러 */
.no_policy = 1,
};
static const struct net_protocol udp_protocol = {
.handler = udp_rcv, /* UDP 수신 핸들러 */
.err_handler = udp_err,
.no_policy = 1,
};
/* IP 프로토콜 번호로 등록 */
inet_add_protocol(&tcp_protocol, IPPROTO_TCP); /* 프로토콜 6 */
inet_add_protocol(&udp_protocol, IPPROTO_UDP); /* 프로토콜 17 */
inet_add_protocol(&icmp_protocol, IPPROTO_ICMP); /* 프로토콜 1 */
소켓 프로토콜 등록: struct proto
소켓 계층에서 사용할 프로토콜은 struct proto를 proto_register()로 등록합니다. 이 구조체는 connect(), sendmsg(), recvmsg() 등의 프로토콜별 구현을 담습니다.
/* net/ipv4/tcp_ipv4.c — TCP 프로토콜 오퍼레이션 */
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.sendmsg = tcp_sendmsg,
.recvmsg = tcp_recvmsg,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.hash = inet_hash,
.unhash = inet_unhash,
.obj_size = sizeof(struct tcp_sock),
/* ... */
};
EXPORT_SYMBOL(tcp_prot);
주소 패밀리 등록: inet_protosw
소켓 타입(SOCK_STREAM, SOCK_DGRAM 등)과 프로토콜의 매핑은 struct inet_protosw 배열로 정의됩니다. socket(AF_INET, SOCK_STREAM, 0) 호출 시 이 배열에서 적절한 프로토콜을 찾습니다.
/* net/ipv4/af_inet.c — AF_INET 프로토콜 스위치 */
static struct inet_protosw inetsw_array[] = {
{
.type = SOCK_STREAM, /* 스트림 소켓 */
.protocol = IPPROTO_TCP, /* → TCP */
.prot = &tcp_prot, /* struct proto */
.ops = &inet_stream_ops, /* struct proto_ops */
},
{
.type = SOCK_DGRAM, /* 데이터그램 소켓 */
.protocol = IPPROTO_UDP, /* → UDP */
.prot = &udp_prot,
.ops = &inet_dgram_ops,
},
{
.type = SOCK_RAW, /* RAW 소켓 */
.protocol = IPPROTO_IP, /* → RAW */
.prot = &raw_prot,
.ops = &inet_sockraw_ops,
},
};
packet_type) → L3 디멀티플렉싱(net_protocol) → 소켓 프로토콜(struct proto) → 주소 패밀리 스위치(inet_protosw). 이 4단계 구조 덕분에 새로운 프로토콜을 기존 코드 수정 없이 모듈로 추가할 수 있습니다.
Netfilter 프레임워크
Netfilter는 커널의 패킷 필터링 프레임워크입니다. 네트워크 스택의 5개 훅 포인트에 콜백을 등록하여 패킷을 검사, 수정, 차단합니다. iptables/nftables, conntrack, NAT의 백엔드입니다.
Netfilter 훅별 역할
| 훅 | 위치 | 주요 용도 | nftables 체인 |
|---|---|---|---|
NF_INET_PRE_ROUTING | IP 수신 직후, 라우팅 전 | DNAT, conntrack 생성 | prerouting |
NF_INET_LOCAL_IN | 라우팅 후, 로컬 대상 | 입력 필터링, Rate Limiting | input |
NF_INET_FORWARD | 라우팅 후, 포워딩 대상 | 포워딩 필터링 | forward |
NF_INET_LOCAL_OUT | 로컬 송신, 라우팅 전 | 출력 필터링, 로컬 DNAT | output |
NF_INET_POST_ROUTING | 라우팅 후, 인터페이스 출력 전 | SNAT/Masquerade | postrouting |
conntrack (연결 추적(Connection Tracking))
Netfilter의 연결 추적 시스템(nf_conntrack)은 모든 연결의 상태를 추적하여 stateful 방화벽(Firewall)과 NAT를 가능하게 합니다. 각 연결은 struct nf_conn으로 표현되며, 해시 테이블(Hash Table)에 저장됩니다. 연결 상태는 NEW, ESTABLISHED, RELATED, INVALID로 분류됩니다.
# conntrack 상태 확인
conntrack -L # 전체 연결 목록
conntrack -C # 현재 추적 중인 연결 수
cat /proc/sys/net/netfilter/nf_conntrack_max # 최대 추적 수
cat /proc/sys/net/netfilter/nf_conntrack_count # 현재 추적 수
nf_conntrack_max 도달 시 새 연결이 거부되고, 해시 충돌이 많으면 지연이 증가합니다. Netfilter Flowtable(nf_flow_table)을 사용하면 ESTABLISHED 연결의 패킷을 빠른 경로로 처리하여 conntrack 오버헤드를 줄일 수 있습니다.
Netfilter 훅 등록 내부
Netfilter 훅 함수는 nf_register_net_hook()으로 등록하며, 각 훅 포인트에 우선순위(priority) 기반으로 정렬됩니다. 패킷이 훅 포인트를 통과할 때 nf_hook_slow()가 등록된 함수들을 순서대로 호출합니다.
/* include/linux/netfilter.h — 훅 등록 구조체 */
struct nf_hook_ops {
nf_hookfn *hook; /* 훅 콜백 함수 */
struct net_device *dev; /* 디바이스 필터 (선택) */
int hooknum; /* NF_INET_* 훅 포인트 */
int priority; /* 실행 우선순위 */
};
/* net/netfilter/core.c — nf_hook_slow() 처리 루프 (간략화) */
int nf_hook_slow(struct sk_buff *skb,
struct nf_hook_state *state,
const struct nf_hook_entries *e,
unsigned int s)
{
unsigned int verdict;
for (; s < e->num_hook_entries; s++) {
verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);
switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
break; /* 다음 훅으로 계속 */
case NF_DROP:
kfree_skb(skb);
return NF_DROP; /* 패킷 폐기 */
case NF_STOLEN:
return NF_STOLEN; /* 모듈이 소유권 가져감 */
case NF_QUEUE:
/* NFQUEUE로 유저 공간 전달 */
return nf_queue(skb, state, s, verdict);
case NF_REPEAT:
s--; /* 현재 훅 재실행 */
break;
}
}
return 1; /* 모든 훅 통과 */
}
코드 설명
- 3행
hook콜백 함수는NF_ACCEPT,NF_DROP,NF_STOLEN,NF_QUEUE,NF_REPEAT중 하나를 반환합니다. - 6행
priority는 동일 훅 포인트에 등록된 여러 훅 함수의 실행 순서를 결정합니다. conntrack은NF_IP_PRI_CONNTRACK(-200)으로 대부분의 필터보다 먼저 실행됩니다. - 17-18행등록된 훅 엔트리를 순서대로 순회하며 콜백을 호출합니다.
NF_ACCEPT이면 다음 훅으로 진행하고,NF_DROP이면 즉시 패킷을 폐기합니다. - 27-29행
NF_QUEUE는 패킷을nfnetlink_queue를 통해 유저 공간(예: Suricata IPS)으로 전달합니다. 유저 공간이 판정(verdict)을 내리면 패킷 처리가 재개됩니다.
nf_conntrack 내부 구현
연결 추적은 패킷의 5-tuple(프로토콜, 소스/목적지 IP:포트)을 기반으로 해시 테이블에서 연결 항목(struct nf_conn)을 조회합니다. NAT 변환 시 원본 방향(original)과 응답 방향(reply)의 tuple 쌍이 저장됩니다.
/* net/netfilter/nf_conntrack_core.c — 연결 추적 핵심 (간략화) */
unsigned int nf_conntrack_in(struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct nf_conn *ct;
enum ip_conntrack_info ctinfo;
struct nf_conntrack_tuple tuple;
/* 1. 패킷에서 tuple 추출 (프로토콜, IP, 포트) */
if (!nf_ct_get_tuple(skb, &tuple))
return NF_ACCEPT;
/* 2. 해시 테이블에서 기존 연결 조회 */
u32 hash = hash_conntrack_raw(&tuple, zone);
ct = __nf_conntrack_find_get(net, zone, &tuple, hash);
if (!ct) {
/* 3. 새 연결: nf_conn 할당, NEW 상태 */
ct = init_conntrack(net, tmpl, &tuple, skb, dataoff, hash);
if (!ct || IS_ERR(ct))
return NF_DROP;
}
/* 4. 프로토콜별 추적 업데이트 */
/* TCP: 상태 머신 전이 (SYN_SENT→ESTABLISHED 등) */
/* UDP: 타임아웃 갱신 */
ret = nf_conntrack_handle_packet(ct, skb, dataoff, ctinfo, state);
/* 5. skb에 conntrack 정보 연결 */
nf_ct_set(skb, ct, ctinfo);
return ret;
}
코드 설명
- 10-11행
nf_ct_get_tuple()은 패킷에서 프로토콜, 소스/목적지 IP, 포트를 추출하여nf_conntrack_tuple을 구성합니다. - 14-15행tuple의 해시로 conntrack 해시 테이블을 조회합니다. 해시 테이블 크기는
nf_conntrack_buckets로 설정하며, 기본값은nf_conntrack_max/4입니다. - 19행기존 연결이 없으면
init_conntrack()으로 새nf_conn을 할당합니다. 전체 연결 수가nf_conntrack_max에 도달하면early_drop()으로 가장 오래된 연결을 제거합니다. - 30행
nf_ct_set()으로 sk_buff에 conntrack 정보를 연결합니다. 이후 Netfilter 규칙에서ct state established등의 조건 매칭에 사용됩니다.
# conntrack 튜닝 명령어
# 최대 추적 수 증가 (기본 65536)
sysctl -w net.netfilter.nf_conntrack_max=262144
# 해시 버킷 수 조정 (모듈 로드 시)
# modprobe nf_conntrack hashsize=65536
echo 65536 > /sys/module/nf_conntrack/parameters/hashsize
# 타임아웃 최적화
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=86400
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
sysctl -w net.netfilter.nf_conntrack_udp_timeout=30
sysctl -w net.netfilter.nf_conntrack_udp_timeout_stream=120
# conntrack 이벤트 모니터링
conntrack -E # 실시간 이벤트 감시
conntrack -E -e NEW,DESTROY # 연결 생성/삭제만
# conntrack 통계
cat /proc/net/stat/nf_conntrack # per-CPU 통계
# 열: searched found new invalid ignore delete ...
# drop early_drop error search_restart
nftables vs iptables 아키텍처
nftables는 iptables의 후속으로, 바이트코드 VM 기반의 유연한 패킷 분류 엔진을 제공합니다. iptables의 테이블/체인/규칙 구조를 유지하면서도, set 인프라(해시/RB-tree/bitmap)와 verdict map으로 성능과 표현력을 개선합니다.
| 특성 | iptables | nftables |
|---|---|---|
| 커널 모듈 | x_tables + match/target 모듈 | nf_tables 단일 모듈 |
| 규칙 평가 | 선형 순차 매칭 | 바이트코드 VM + set 조회 |
| 규칙 업데이트 | 전체 체인 원자적 교체 | 개별 규칙 원자적 추가/삭제 |
| 프로토콜 지원 | IPv4/IPv6/ARP 별도 | inet 패밀리로 통합 가능 |
| Set 타입 | ipset (별도 모듈) | 내장 (hash, rbtree, bitmap) |
| Verdict Map | 미지원 | Set 요소에 verdict 연결 |
| 커널 인터페이스 | setsockopt/getsockopt | netlink (nfnetlink) |
| 성능 (대규모 규칙) | O(n) 선형 | O(1) 해시 set 사용 시 |
nftables 바이트코드 VM 명령어 예시
nft add rule inet filter input tcp dport 80 accept 의 내부 표현:
명령어 1: meta load l4proto → reg1 [ meta load l4proto => reg 1 ] 명령어 2: cmp eq reg1 0x06 (TCP) [ cmp eq reg 1 0x00000006 ] 명령어 3: payload load 2b @ transport header + 2 → reg1 [ payload load 2b @ transport header + 2 => reg 1 ] 명령어 4: cmp eq reg1 0x0050 (포트 80) [ cmp eq reg 1 0x00005000 ] 명령어 5: immediate verdict accept [ immediate reg 0 accept ]
Set을 사용한 O(1) 조회 예시:
nft add set inet filter allowed_ports { type inet_service; }
nft add element inet filter allowed_ports { 80, 443, 8080 }
nft add rule inet filter input tcp dport @allowed_ports accept
→ lookup 명령이 해시 테이블에서 O(1) 조회
코드 설명
- 4-13행nftables VM은 레지스터 기반 바이트코드를 실행합니다. 각 명령이 패킷 필드를 레지스터에 로드하고, 비교하고, verdict를 결정합니다. iptables의 match/target 함수 호출보다 오버헤드가 적습니다.
- 16-19행Set을 사용하면 수만 개의 IP/포트를 하나의 규칙으로 처리할 수 있습니다. 내부적으로 해시 테이블, RB-tree, 또는 비트맵을 사용하여 O(1) 또는 O(log n) 조회를 수행합니다. iptables에서는 규칙 수만큼 O(n) 순차 매칭이 필요했습니다.
Flowtable 가속 경로
Netfilter Flowtable은 ESTABLISHED 연결의 패킷을 Netfilter 훅을 우회하여 빠르게 전달하는 가속 메커니즘입니다. conntrack이 연결을 추적한 후, Flowtable에 등록된 플로우는 PRE_ROUTING 단계에서 바로 출력 인터페이스로 전달됩니다.
/* net/netfilter/nf_flow_table_core.c — Flowtable 동작 원리 */
/* 1. nftables에서 flowtable 정의 */
/* nft add flowtable inet filter ft { */
/* hook ingress priority 0; */
/* devices = { eth0, eth1 }; */
/* } */
/* 2. forward 체인에서 flow offload 규칙 추가 */
/* nft add rule inet filter forward */
/* ct state established flow add @ft */
/* 3. 커널 내부: flow 엔트리 조회 및 fast path */
unsigned int nf_flow_offload_ip_hook(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct flow_offload_tuple_rhash *tuplehash;
struct nf_flowtable *flow_table = priv;
/* flow 테이블에서 조회 (rhashtable) */
tuplehash = flow_offload_lookup(flow_table, &tuple);
if (!tuplehash)
return NF_ACCEPT; /* 미등록: 일반 경로 */
/* Fast path: IP TTL 감소, MAC 헤더 교체 */
ip_decrease_ttl(ip_hdr(skb));
nf_flow_nat_ip(flow, skb, thoff, dir); /* NAT 적용 */
skb->dev = outdev;
neigh_xmit(NEIGH_ARP_TABLE, outdev, &nexthop, skb);
return NF_STOLEN; /* Netfilter 훅 체인 우회 */
}
코드 설명
- 3-7행nftables에서 flowtable을 정의할 때, 가속을 적용할 디바이스 목록을 지정합니다. ingress 훅에 등록되어 PRE_ROUTING 이전에 실행됩니다.
- 22행
flow_offload_lookup()은 rhashtable(resizable hash table)에서 플로우를 O(1)으로 조회합니다. - 27-30행Fast path에서는 IP 라우팅, Netfilter 훅 체인, conntrack 업데이트를 모두 건너뛰고, TTL 감소와 NAT 변환만 수행한 후
neigh_xmit()으로 직접 출력합니다. - 31행
NF_STOLEN을 반환하여 이후의 Netfilter 훅 처리를 완전히 우회합니다. 이를 통해 포워딩 성능을 3~5배 향상시킬 수 있습니다.
NF_FLOW_HW_OFFLOAD 플래그로 NIC 하드웨어까지 오프로드할 수 있습니다. tc flower 규칙으로 변환되어 NIC의 TCAM/eSwitch에 설치됩니다. Mellanox ConnectX-5+, Marvell Prestera 등이 지원하며, nft add flowtable ... flags offload로 활성화합니다.
네트워크 디바이스와 NAPI
struct net_device는 네트워크 인터페이스(eth0, wlan0 등)를 나타냅니다. net_device_ops로 드라이버 콜백을 등록하고, NAPI를 통해 고속 패킷 처리를 수행합니다.
net_device 핵심 필드
struct net_device {
char name[IFNAMSIZ]; /* 인터페이스 이름 */
unsigned int mtu; /* 최대 전송 단위 */
unsigned int flags; /* IFF_UP, IFF_PROMISC 등 */
unsigned char dev_addr[MAX_ADDR_LEN]; /* MAC 주소 */
const struct net_device_ops *netdev_ops; /* 드라이버 콜백 */
const struct ethtool_ops *ethtool_ops; /* ethtool 콜백 */
struct netdev_rx_queue *_rx; /* RX 큐 배열 */
unsigned int num_rx_queues; /* RX 큐 수 */
struct netdev_queue *_tx; /* TX 큐 배열 */
unsigned int num_tx_queues; /* TX 큐 수 */
struct Qdisc *qdisc; /* 기본 qdisc */
/* ... */
};
NAPI 동작 흐름
NAPI(New API)는 인터럽트와 폴링을 혼합하여 고부하 네트워크에서 인터럽트 폭풍을 방지하는 수신 경로 프레임워크입니다. 핵심 동작은 다음과 같습니다:
- 패킷 도착 / IRQ 발생 — NIC가 패킷을 수신하면 하드웨어 인터럽트를 발생시킵니다.
- 폴링 모드 전환 — 드라이버 IRQ 핸들러가
napi_schedule()를 호출하고 인터럽트를 비활성화합니다. - 배치 처리 —
NET_RX_SOFTIRQ에서poll()콜백이 budget(기본 64)만큼 패킷을 배치로 처리합니다. - 인터럽트 복귀 — 큐가 비면
napi_complete_done()으로 인터럽트를 재활성화합니다.
| 링크 속도 | 최소 프레임(64B) PPS | 인터럽트 방식 | NAPI 배치 |
|---|---|---|---|
| 1 Gbps | 약 1,488,000 | CPU 포화 위험 | 안정적 처리 |
| 10 Gbps | 약 14,880,000 | 불가 (인터럽트만으로 CPU 전부 소모) | 멀티큐 + NAPI 필수 |
| 25 Gbps | 약 37,200,000 | 불가 | NAPI + XDP 권장 |
| 100 Gbps | 약 148,800,000 | 불가 | NAPI + 멀티큐 + XDP 필수 |
net_device 등록 과정
register_netdev()는 네트워크 디바이스를 커널에 등록하는 함수입니다. RTNL(Routing Netlink) 락을 획득한 상태에서 register_netdevice()를 호출하며, 디바이스 이름 할당, sysfs 등록, 알림(notification) 발송 등을 수행합니다.
/* net/core/dev.c — register_netdevice() 핵심 단계 (간략화) */
int register_netdevice(struct net_device *dev)
{
struct net *net = dev_net(dev);
/* 1. ndo_init() 콜백 호출 (드라이버 초기화) */
if (dev->netdev_ops->ndo_init) {
ret = dev->netdev_ops->ndo_init(dev);
if (ret)
goto err;
}
/* 2. 디바이스 이름이 %d 패턴이면 자동 할당 */
if (strchr(dev->name, '%'))
dev_alloc_name(dev, dev->name);
/* 3. 네트워크 네임스페이스의 디바이스 리스트에 추가 */
list_netdevice(dev);
/* 4. sysfs/procfs 등록 */
netdev_register_kobject(dev);
/* 5. NETDEV_REGISTER 알림 발송 */
call_netdevice_notifiers(NETDEV_REGISTER, dev);
/* 6. default qdisc 할당 */
dev_init_scheduler(dev);
/* 7. 디바이스 상태를 REGISTERED로 변경 */
dev->reg_state = NETREG_REGISTERED;
return 0;
}
코드 설명
- 7-10행
ndo_init()콜백은 드라이버가 하드웨어 초기화를 수행하는 시점입니다. DMA 할당, Ring Buffer 설정, NAPI 등록 등이 이루어집니다. - 18행
list_netdevice()는 디바이스를 네트워크 네임스페이스의 디바이스 리스트에 추가합니다.ip link show로 확인할 수 있는 인터페이스 목록이 이 리스트입니다. - 24행
NETDEV_REGISTER알림은 notifier chain을 통해 라우팅, Netfilter, bonding 등 관심 서브시스템에 새 디바이스 등록을 통지합니다. - 27행
dev_init_scheduler()는 기본 qdisc(보통pfifo_fast또는fq_codel)를 할당합니다.sysctl net.core.default_qdisc에 의해 결정됩니다.
register_netdev()를 포함한 대부분의 네트워크 설정 작업은 RTNL(Routing Netlink) 뮤텍스를 보유한 상태에서 수행됩니다. 이 락은 전역적이며, ip link, ifconfig, 디바이스 등록/해제, 라우팅 테이블 변경 등 모든 네트워크 설정 변경이 직렬화됩니다. 따라서 대량의 네트워크 디바이스(컨테이너 수천 개)를 동시에 생성/삭제하면 RTNL 경합이 병목이 될 수 있습니다. 커널 5.13+에서는 RTNL 락을 세분화하는 작업이 진행 중입니다.
# 네트워크 디바이스 정보 확인
ip -d link show eth0 # 상세 디바이스 정보
ethtool -i eth0 # 드라이버/펌웨어 정보
ethtool -l eth0 # RX/TX 큐 수
ethtool -g eth0 # Ring Buffer 크기
ethtool -k eth0 # 오프로드 기능 목록
# 인터럽트 상태 확인
cat /proc/interrupts | grep eth0 # per-CPU 인터럽트 분배
cat /proc/irq/<IRQ>/smp_affinity # IRQ CPU 친화성
# NAPI 상태 확인
cat /sys/class/net/eth0/napi_defer_hard_irqs # IRQ 지연 횟수
cat /sys/class/net/eth0/gro_flush_timeout # GRO 플러시 타임아웃
인터럽트 코얼레싱 (Interrupt Coalescing)
인터럽트 코얼레싱은 여러 패킷을 모아서 하나의 인터럽트로 처리하여 CPU 오버헤드를 줄이는 NIC 하드웨어 기능입니다. ethtool -C로 설정하며, 지연(latency)과 처리량(throughput)의 트레이드오프를 조절합니다.
# 인터럽트 코얼레싱 현재 설정 확인
ethtool -c eth0
# 설정 변경
# rx-usecs: 인터럽트 발생까지 최대 대기 시간 (μs)
# rx-frames: 인터럽트 발생까지 최대 패킷 수
ethtool -C eth0 rx-usecs 50 rx-frames 64
ethtool -C eth0 tx-usecs 50 tx-frames 64
# 적응형 코얼레싱 (트래픽 패턴에 따라 자동 조정)
ethtool -C eth0 adaptive-rx on adaptive-tx on
# 초저지연 설정 (금융 트레이딩 등)
ethtool -C eth0 rx-usecs 0 rx-frames 1
ethtool -C eth0 tx-usecs 0 tx-frames 1
| 설정 | 지연 | 처리량 | CPU 사용률 | 사용 시나리오 |
|---|---|---|---|---|
| rx-usecs=0, rx-frames=1 | 최소 (~1μs) | 낮음 | 매우 높음 | 초저지연 (HFT) |
| rx-usecs=50, rx-frames=64 | 보통 (~50μs) | 높음 | 보통 | 범용 서버 |
| rx-usecs=250, rx-frames=256 | 높음 (~250μs) | 매우 높음 | 낮음 | 대량 전송 (백업 등) |
| adaptive-rx on | 동적 | 동적 | 동적 | 혼합 워크로드 |
드라이버 모델: ndo_* 콜백
struct net_device_ops는 드라이버가 구현해야 하는 콜백 함수 집합입니다. 패킷 전송, 인터페이스 설정, 멀티캐스트 필터, TC 오프로드 등 모든 디바이스 동작이 이 인터페이스를 통해 추상화됩니다.
/* include/linux/netdevice.h — net_device_ops 핵심 콜백 */
struct net_device_ops {
/* 인터페이스 관리 */
int (*ndo_open)(struct net_device *dev);
int (*ndo_stop)(struct net_device *dev);
/* 패킷 전송 (TX 핵심 경로) */
netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb,
struct net_device *dev);
/* TX 큐 선택 (멀티큐) */
u16 (*ndo_select_queue)(struct net_device *dev,
struct sk_buff *skb,
struct net_device *sb_dev);
/* MAC 주소/멀티캐스트 필터 */
void (*ndo_set_rx_mode)(struct net_device *dev);
int (*ndo_set_mac_address)(struct net_device *dev, void *addr);
/* MTU 변경 */
int (*ndo_change_mtu)(struct net_device *dev, int new_mtu);
/* TC 하드웨어 오프로드 */
int (*ndo_setup_tc)(struct net_device *dev,
enum tc_setup_type type,
void *type_data);
/* XDP 프로그램 부착 */
int (*ndo_bpf)(struct net_device *dev,
struct netdev_bpf *bpf);
/* VLAN 필터 */
int (*ndo_vlan_rx_add_vid)(struct net_device *dev,
__be16 proto, u16 vid);
/* ... 50개 이상의 콜백 */
};
코드 설명
- 4-5행
ndo_open은ip link set dev eth0 up시 호출됩니다. Ring Buffer 할당, NAPI 활성화, 인터럽트 등록, PHY 링크 시작 등을 수행합니다.ndo_stop은 인터페이스 다운 시 역순으로 정리합니다. - 8-9행
ndo_start_xmit은 TX 경로의 최종 단계로, 드라이버가 DMA 디스크립터를 설정하고 NIC에 전송을 지시합니다.NETDEV_TX_OK또는NETDEV_TX_BUSY를 반환합니다. - 17행
ndo_set_rx_mode는 유니캐스트/멀티캐스트 주소 필터와 promiscuous 모드를 NIC 하드웨어에 설정합니다.ip maddr이나 tcpdump 시작 시 호출됩니다. - 24-26행
ndo_setup_tc는 TC 규칙의 하드웨어 오프로드를 처리합니다.tc flower필터,mqprioqdisc 등을 NIC의 eSwitch/TCAM에 설치합니다. - 29-30행
ndo_bpf는 XDP 프로그램의 부착/분리를 처리합니다.XDP_SETUP_PROG명령으로 프로그램을 설치하고, 드라이버가 RX 경로에 XDP 실행 지점을 설정합니다.
주요 자료구조 관계도
Linux 네트워크 스택의 핵심 자료구조들은 서로 긴밀하게 연결되어 있습니다. 아래 다이어그램은 패킷 처리에 관여하는 주요 구조체 간의 관계를 보여줍니다.
struct sk_buff는 약 232바이트(64비트 시스템), struct sock은 약 760바이트, struct tcp_sock은 약 2,400바이트입니다. 고성능 서버에서 수십만 연결을 처리할 때 이 구조체들의 메모리 사용량이 상당할 수 있습니다. slabtop이나 /proc/slabinfo에서 TCPv6, sock_inode_cache, skbuff_head_cache 등으로 확인할 수 있습니다.
주요 자료구조 메모리 사용량 추적
# SLAB 캐시에서 네트워크 관련 객체 확인
slabtop -s c | head -20
# 개별 SLAB 캐시 상세 확인
cat /proc/slabinfo | grep -E "^(sk_buff|tcp_sock|sock_inode|request_sock)"
# 소켓 메모리 사용량 요약
cat /proc/net/sockstat
# sockets: used 1234
# TCP: inuse 567 orphan 0 tw 89 alloc 600 mem 123
# UDP: inuse 12 mem 3
# TCP 메모리 압력 상태 확인
cat /proc/sys/net/ipv4/tcp_mem
# 세 값: low(정상) pressure(경고) high(제한) — 페이지 단위
# 네트워크 버퍼 메모리 사용량 (바이트)
cat /proc/sys/net/core/optmem_max
cat /proc/sys/net/core/rmem_default
cat /proc/sys/net/core/wmem_default
자료구조 상속 계층
네트워크 자료구조는 C 언어의 구조체 내포(embedding)를 통해 상속 패턴을 구현합니다:
| 구조체 | 상위 구조체 | 추가 필드 | 크기 (대략) |
|---|---|---|---|
sock_common | (기반) | 해시, 바인드 포트, 패밀리 | ~120B |
sock | sock_common 내포 | 큐, 버퍼, 콜백, 타이머 | ~760B |
inet_sock | sock 내포 | IP 주소, 포트, TTL, TOS | ~900B |
inet_connection_sock | inet_sock 내포 | accept 큐, 재전송 타이머, PMTU | ~1,200B |
tcp_sock | inet_connection_sock 내포 | cwnd, srtt, SACK, 혼잡 제어 | ~2,400B |
udp_sock | inet_sock 내포 | pending, corkflag, encap | ~950B |
이 패턴 덕분에 struct sock *sk 포인터를 tcp_sk(sk), inet_sk(sk) 등의 매크로(Macro)로 캐스팅하여 프로토콜별 필드에 접근할 수 있습니다. 커널의 네트워크 코드에서 가장 빈번하게 사용되는 패턴입니다.
/* 소켓 캐스팅 매크로 */
struct tcp_sock *tp = tcp_sk(sk); /* sock → tcp_sock */
struct inet_sock *inet = inet_sk(sk); /* sock → inet_sock */
struct inet_connection_sock *icsk = inet_csk(sk);
/* TCP 혼잡 윈도우 접근 */
u32 cwnd = tp->snd_cwnd;
u32 srtt = tp->srtt_us >> 3; /* 마이크로초 단위 */
/* IP 주소 접근 */
__be32 saddr = inet->inet_saddr;
__be32 daddr = inet->inet_daddr;
__be16 sport = inet->inet_sport;
라우팅 서브시스템
Linux 라우팅은 FIB(Forwarding Information Base)를 기반으로 Longest Prefix Match(LPM) 조회를 수행합니다. FIB는 내부적으로 LC-trie(Level-Compressed trie) 자료구조를 사용하여 대규모 라우팅 테이블에서도 효율적인 조회를 보장합니다.
라우팅 조회 과정
- FIB 조회 —
fib_lookup()에서 목적지 IP에 대한 LPM(Longest Prefix Match) 조회를 수행합니다. - dst_entry 생성 — 조회 결과로
struct dst_entry를 생성하여sk_buff에 연결합니다. 이 구조체는 출력 인터페이스, 다음 홉, MTU 정보를 포함합니다. - 이웃 조회 — 다음 홉의 L2 주소(MAC)를 ARP/NDP 캐시에서 조회합니다. 캐시 미스 시 ARP 요청을 보내고 패킷을
neigh->arp_queue에 대기시킵니다.
라우팅 테이블 구조
| 테이블 | ID | 용도 | 조회 순서 |
|---|---|---|---|
local | 255 | 로컬 주소, 브로드캐스트 | 1순위 (항상 먼저 조회) |
main | 254 | 일반 라우팅 엔트리 | 2순위 |
default | 253 | 기본 규칙용 | 3순위 |
| 사용자 정의 | 1~252 | Policy Routing | ip rule로 조회 순서 지정 |
# 라우팅 테이블 확인
ip route show table main # 기본 라우팅 테이블
ip route show table local # 로컬 주소 테이블
ip rule list # Policy Routing 규칙
# 특정 목적지에 대한 라우팅 결정 확인
ip route get 8.8.8.8 # FIB 조회 결과
ip route get 8.8.8.8 from 10.0.0.1 iif eth0 # 소스/인터페이스 지정
FIB 조회 내부 구현
fib_lookup()은 Policy Routing 규칙(ip rule)을 순회하며, 매칭되는 규칙의 라우팅 테이블에서 LPM 조회를 수행합니다. 내부적으로 LC-trie(Level-Compressed Trie) 자료구조를 사용하여 대규모 BGP 라우팅 테이블(수십만 엔트리)에서도 효율적으로 조회합니다.
/* net/ipv4/fib_rules.c — fib_lookup() 핵심 로직 (간략화) */
int fib_lookup(struct net *net,
const struct flowi4 *flp,
struct fib_result *res, unsigned int flags)
{
/* Policy Routing 비활성 시 (대부분의 서버) */
/* main + local 테이블만 직접 조회 (빠른 경로) */
if (!net->ipv4.fib_has_custom_rules) {
res->tclassid = 0;
/* 1. local 테이블 조회 (로컬 주소, 브로드캐스트) */
err = fib_table_lookup(local_table, flp, res, flags);
if (err != -EAGAIN)
return err;
/* 2. main 테이블 조회 */
err = fib_table_lookup(main_table, flp, res, flags);
if (err != -EAGAIN)
return err;
return -ENETUNREACH;
}
/* Policy Routing 활성 시: 규칙 순회 */
return __fib_lookup(net, flp, res, flags);
}
/* net/ipv4/fib_trie.c — fib_table_lookup() LC-trie 조회 */
int fib_table_lookup(struct fib_table *tb,
const struct flowi4 *flp,
struct fib_result *res, int fib_flags)
{
struct trie *t = (struct trie *)tb->tb_data;
t_key key = ntohl(flp->daddr); /* 목적지 IP */
struct key_vector *n, *pn;
t_key cindex;
/* Trie 루트에서 시작하여 목적지 IP 비트를 따라 탐색 */
n = get_child_rcu(t->kv, key);
while (n) {
/* Leaf 노드: 프리픽스 매칭 확인 */
/* Internal 노드: 다음 비트로 자식 탐색 */
/* Longest Prefix Match를 위해 백트래킹 */
/* ... */
}
/* fib_result에 nexthop, 출력 디바이스, 경로 타입 설정 */
return err;
}
코드 설명
- 8행Policy Routing이 비활성(기본)인 경우 최적화된 경로로 진입합니다.
fib_has_custom_rules는 사용자 정의 ip rule이 추가되었을 때만 true가 됩니다. - 12행local 테이블을 먼저 조회합니다. 로컬에 할당된 IP 주소나 브로드캐스트 주소에 대한 매칭이 여기서 이루어집니다.
- 17행local 테이블에서 매칭이 없으면 main 테이블에서 실제 라우팅 조회를 수행합니다.
- 34행목적지 IP를 호스트 바이트 순서로 변환하여 Trie 키로 사용합니다. Trie는 IP 주소의 비트를 상위부터 탐색합니다.
- 39행LC-trie는 일반 binary trie보다 레벨을 압축하여 조회 깊이를 줄입니다. 인터넷 전체 라우팅 테이블(약 90만 엔트리)에서도 평균 4-6단계 조회로 완료됩니다.
dst_entry 캐시와 라우팅 캐시
dst_entry는 라우팅 결정의 결과를 캐싱하는 구조체입니다. 커널 3.6부터 전역 라우팅 캐시(rt_hash_table)가 제거되고, 소켓별/FIB 엔트리별 캐시로 대체되었습니다. Early Demux에서 소켓의 dst_entry를 재사용하는 것이 현재의 주된 캐싱 전략입니다.
/* include/net/dst.h — dst_entry 핵심 필드 */
struct dst_entry {
struct net_device *dev; /* 출력 디바이스 */
struct dst_ops *ops; /* IPv4/IPv6 ops */
unsigned long expires; /* 만료 시간 */
int (*input)(struct sk_buff *skb);
int (*output)(struct net *net,
struct sock *sk,
struct sk_buff *skb);
unsigned int flags; /* DST_* 플래그 */
int error;
struct neighbour *_neighbour; /* 다음 홉 이웃 */
/* ... */
};
/* 수신 패킷: input 콜백 사용 */
/* 로컬 대상: input = ip_local_deliver */
/* 포워딩: input = ip_forward */
/* 송신 패킷: output 콜백 사용 */
/* 유니캐스트: output = ip_output */
/* 멀티캐스트: output = ip_mc_output */
코드 설명
- 3행
dev는 패킷의 출력 네트워크 인터페이스입니다. FIB 조회 결과로 결정됩니다. - 6-9행
input과output함수 포인터는 패킷의 다음 처리 단계를 결정합니다. 수신 패킷은dst_input(skb)으로, 송신 패킷은dst_output(skb)으로 호출됩니다. - 12행
_neighbour는 다음 홉의 이웃 항목(ARP 캐시 엔트리)을 가리킵니다. L2 헤더 추가 시 이 이웃의 MAC 주소를 사용합니다.
# 라우팅 캐시/FIB 통계 확인
cat /proc/net/fib_trie # FIB trie 구조 (상세)
cat /proc/net/fib_triestat # FIB trie 통계
# Aver depth: 2.52
# Max depth: 7
# Leaves: 834
# Internal nodes: 312
# rt_cache 통계 (레거시, 참고용)
cat /proc/net/stat/rt_cache
# ECMP (Equal-Cost Multi-Path) 설정
ip route add 10.0.0.0/8 \
nexthop via 192.168.1.1 weight 1 \
nexthop via 192.168.1.2 weight 1
# ECMP 해시 기반 선택 (L3 또는 L3+L4)
sysctl net.ipv4.fib_multipath_hash_policy # 0=L3, 1=L3+L4, 2=L3+L4+src
Policy Routing과 ip rule
Policy Routing은 소스 IP, 마킹(fwmark), 인터페이스 등 다양한 조건에 따라 서로 다른 라우팅 테이블을 적용하는 기능입니다. ip rule 명령으로 규칙을 관리하며, 규칙은 우선순위(priority) 순서로 평가됩니다.
# Policy Routing 기본 규칙 확인
ip rule list
# 0: from all lookup local → 로컬 주소 매칭
# 32766: from all lookup main → 기본 라우팅
# 32767: from all lookup default → 기본값
# 소스 IP 기반 라우팅 (멀티호밍)
ip rule add from 10.0.1.0/24 table 100 prio 100
ip route add default via 192.168.1.1 table 100
# fwmark 기반 라우팅 (Netfilter 연동)
iptables -t mangle -A OUTPUT -p tcp --dport 80 -j MARK --set-mark 1
ip rule add fwmark 1 table 200 prio 200
ip route add default via 10.0.0.1 table 200
# 인터페이스 기반 라우팅
ip rule add iif eth1 table 300 prio 300
# VRF (Virtual Routing and Forwarding) 설정
ip link add vrf-red type vrf table 10
ip link set dev vrf-red up
ip link set dev eth1 master vrf-red
# eth1의 모든 트래픽이 table 10으로 라우팅됩니다
# 라우팅 결정 디버깅
ip route get 8.8.8.8 from 10.0.1.5 # 소스 IP 지정 조회
ip route get 8.8.8.8 mark 1 # fwmark 지정 조회
ip route get fibmatch 10.0.0.0/24 # 정확한 FIB 매칭 조회
IPv6 라우팅 특이사항
IPv6 라우팅은 IPv4와 유사하지만 몇 가지 중요한 차이가 있습니다. IPv6는 확장 헤더를 사용하며, NDP(Neighbor Discovery Protocol)로 ARP를 대체하고, Router Advertisement(RA)를 통한 자동 주소 설정(SLAAC)을 지원합니다.
| 항목 | IPv4 | IPv6 |
|---|---|---|
| 라우팅 테이블 | fib_table (LC-trie) | fib6_table (radix tree) |
| 이웃 탐색 | ARP (이더넷 브로드캐스트) | NDP (ICMPv6 멀티캐스트) |
| 주소 자동 설정 | DHCP | SLAAC + DHCPv6 |
| 단편화 | 라우터/호스트 | 소스 호스트만 (PMTUD 필수) |
| FIB 조회 함수 | fib_lookup() | fib6_lookup() |
| 라우팅 캐시 | per-socket dst_entry | per-socket + exception table |
| 멀티패스 | nexthop weight | nexthop weight + source hash |
# IPv6 라우팅 확인
ip -6 route show # IPv6 라우팅 테이블
ip -6 route get 2001:db8::1 # IPv6 경로 조회
ip -6 neigh show # NDP 이웃 캐시
# Router Advertisement 확인
radvdump # RA 패킷 덤프
# IPv6 관련 sysctl
sysctl net.ipv6.conf.all.forwarding # IPv6 포워딩 (라우터 모드)
sysctl net.ipv6.conf.all.accept_ra # RA 수락 (SLAAC)
sysctl net.ipv6.conf.all.use_tempaddr # Privacy Extensions
TC (Traffic Control)와 qdisc
패킷 스케줄링과 트래픽 셰이핑을 담당합니다. 각 네트워크 디바이스에 qdisc(큐잉 규칙)가 연결됩니다. TC는 3개 구성 요소로 이루어집니다: qdisc(큐잉 규칙), class(계층적 분류), filter(패킷 분류기).
주요 qdisc 유형
| qdisc | 유형 | 동작 | 용도 |
|---|---|---|---|
pfifo_fast | Classless | 3-band 우선순위(Priority) FIFO | 레거시 기본값 |
fq_codel | Classless | Fair Queuing + CoDel AQM | 현재 기본값, 버퍼블로트 방지 |
htb | Classful | Hierarchical Token Bucket | 대역폭 보장/제한 |
tbf | Classless | Token Bucket Filter | 단순 속도 제한 |
netem | Classless | 네트워크 에뮬레이션 | 지연/손실/중복 테스트 |
mqprio | Classful | 멀티큐 우선순위 매핑 | 하드웨어 큐 연동 |
clsact | Classful | ingress/egress eBPF 연결점 | TC BPF 프로그램 부착 |
ingress | 특수 | 수신 경로 필터링 | 수신 패킷 분류/리다이렉트 |
# TC 기본 사용 예제
tc qdisc show dev eth0 # 현재 qdisc 확인
tc qdisc add dev eth0 root fq_codel # fq_codel 설정
tc qdisc add dev eth0 root netem delay 100ms # 100ms 지연 추가
# HTB로 대역폭 제한
tc qdisc add dev eth0 root handle 1: htb default 10
tc class add dev eth0 parent 1: classid 1:10 htb rate 100mbit ceil 200mbit
tc class add dev eth0 parent 1: classid 1:20 htb rate 50mbit ceil 100mbit
# TC BPF 프로그램 부착
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj prog.o sec classifier
fq_codel 내부 동작
fq_codel은 Fair Queuing과 CoDel(Controlled Delay) AQM(Active Queue Management)을 결합한 qdisc로, 현재 Linux의 기본 qdisc입니다. 각 플로우에 별도의 큐를 할당하여 공정성을 보장하고, CoDel 알고리즘으로 큐잉 지연이 target(기본 5ms)을 초과하면 패킷을 드롭하여 버퍼블로트(Bufferbloat)를 방지합니다.
/* net/sched/sch_fq_codel.c — fq_codel 핵심 구조 (간략화) */
struct fq_codel_sched_data {
u32 flows_cnt; /* 플로우 큐 수 (기본 1024) */
u32 quantum; /* DRR 양자 (기본 1514) */
u32 drop_batch_size; /* 한번에 드롭할 패킷 수 */
struct codel_params cparams; /* CoDel 파라미터 */
struct fq_codel_flow *flows; /* 플로우 큐 배열 */
struct list_head new_flows; /* 새 플로우 리스트 */
struct list_head old_flows; /* 기존 플로우 리스트 */
};
/* CoDel 파라미터 */
/* target: 허용 큐잉 지연 (기본 5ms) */
/* interval: 판단 주기 (기본 100ms) */
/* 큐잉 지연이 target을 interval 동안 */
/* 지속적으로 초과하면 드롭 시작 */
/* 패킷 분류: skb_get_hash()로 플로우 해시 계산 */
/* → hash % flows_cnt로 플로우 큐 결정 */
/* Deficit Round Robin으로 플로우 간 공정 dequeue */
코드 설명
- 3행
flows_cnt는 플로우 큐의 수입니다. 기본 1024개의 큐를 사용하여 최대 1024개의 플로우를 독립적으로 관리합니다.tc qdisc ... fq_codel flows 4096으로 변경할 수 있습니다. - 4행
quantum은 Deficit Round Robin에서 각 플로우가 한 라운드에 전송할 수 있는 바이트 수입니다. MTU + 이더넷 헤더 크기로 설정됩니다. - 8-9행
new_flows와old_flows는 DRR 스케줄러의 두 리스트입니다. 새 플로우는new_flows에서 우선 처리되어 짧은 플로우(HTTP 요청 등)의 지연을 최소화합니다. - 13-16행CoDel은 큐잉 지연을 측정하여 버퍼블로트를 감지합니다. 패킷이 큐에 머문 시간이 target(5ms)을 interval(100ms) 동안 지속적으로 초과하면 드롭 빈도를 점진적으로 증가시킵니다.
# fq_codel 상세 통계 확인
tc -s qdisc show dev eth0
# 전형적인 출력:
# qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024
# quantum 1514 target 5ms interval 100ms memory_limit 32Mb
# ecn_mark 0 ce_threshold 0us
# Sent 123456789 bytes 987654 pkt (dropped 12, overlimits 34 requeues 5)
# backlog 0b 0p requeues 5
# maxpacket 1514 drop_overlimit 0 new_flow_count 456
# ecn_mark 0 new_flows_len 0 old_flows_len 2
# fq_codel 파라미터 조정
tc qdisc replace dev eth0 root fq_codel \
target 1ms \ # 저지연 환경: 1ms 타깃
interval 50ms \ # 판단 주기 단축
flows 4096 \ # 플로우 큐 수 증가
quantum 1514 # DRR 양자
멀티코어 패킷 분산
고속 네트워크에서 단일 CPU로는 패킷 처리량(Throughput)이 부족합니다. Linux 커널은 여러 메커니즘으로 패킷 처리를 여러 CPU에 분산합니다.
| 메커니즘 | 계층 | 동작 | 설정 방법 |
|---|---|---|---|
| RSS (Receive Side Scaling) | NIC 하드웨어 | NIC가 플로우 해시로 RX 큐를 선택, 각 큐에 별도 IRQ/CPU 할당 | ethtool -L, ethtool -X |
| RPS (Receive Packet Steering) | 커널 소프트웨어 | 소프트웨어 플로우 해시로 타겟 CPU 선택 (RSS 미지원 NIC용) | /sys/class/net/*/queues/rx-*/rps_cpus |
| RFS (Receive Flow Steering) | 커널 소프트웨어 | 패킷을 소켓 소유 CPU로 전달 (캐시 지역성 최적화) | /proc/sys/net/core/rps_sock_flow_entries |
| XPS (Transmit Packet Steering) | 커널 소프트웨어 | 송신 CPU와 TX 큐의 매핑 최적화 | /sys/class/net/*/queues/tx-*/xps_cpus |
| aRFS (Accelerated RFS) | NIC 하드웨어 | NIC 하드웨어에서 RFS 규칙 적용 | ethtool -K rx-flow-hash |
enqueue_to_backlog()로 해당 CPU의 softnet_data.input_pkt_queue에 패킷을 삽입하고 IPI(Inter-Processor Interrupt)로 타겟 CPU를 깨웁니다. 이 큐의 크기는 /proc/sys/net/core/netdev_max_backlog(기본 1000)으로 제한되며, 초과 시 패킷이 드롭됩니다.
# RSS 설정: 큐 수 확인/변경
ethtool -l eth0 # 채널(큐) 수 확인
ethtool -L eth0 combined 8 # 8개 결합 큐로 설정
# IRQ-CPU 매핑 확인
cat /proc/interrupts | grep eth0 # NIC 인터럽트 분포
# RPS 활성화 (예: 8코어 시스템에서 CPU 0-7)
echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus
# softnet 통계 확인 (드롭 모니터링)
cat /proc/net/softnet_stat
Flow Dissector
Flow Dissector는 패킷의 L3/L4 헤더를 파싱하여 플로우 키(5-tuple)를 추출하는 커널 프레임워크입니다. RSS, RPS, fq_codel, TC 등 플로우 기반 처리를 수행하는 모든 서브시스템이 이 프레임워크를 사용합니다.
/* net/core/flow_dissector.c — Flow Dissector 핵심 구조 */
struct flow_keys {
struct flow_dissector_key_control control;
struct flow_dissector_key_basic basic; /* n_proto, ip_proto */
struct flow_dissector_key_ipv4_addrs addrs; /* src, dst IP */
struct flow_dissector_key_ports ports; /* src, dst port */
struct flow_dissector_key_tags tags; /* VLAN ID 등 */
struct flow_dissector_key_vlan vlan; /* VLAN 정보 */
struct flow_dissector_key_keyid keyid; /* 터널 키 */
};
/* 패킷에서 플로우 키 추출 */
u32 skb_get_hash(struct sk_buff *skb)
{
if (!skb->l4_hash && !skb->sw_hash) {
/* NIC가 RSS 해시를 제공하지 않으면 소프트웨어 해시 계산 */
__skb_get_hash(skb);
/* → skb_flow_dissect_flow_keys() */
/* → __skb_flow_dissect() — 프로토콜별 파싱 */
/* → flow_hash_from_keys() — jhash2 기반 해시 */
}
return skb->hash;
}
코드 설명
- 2-10행
flow_keys구조체는 패킷의 플로우 식별 정보를 포함합니다. L3 주소(IPv4/IPv6), L4 포트, VLAN 태그, 터널 키 등을 추출합니다. - 16-21행NIC가 RSS 해시를 제공하면(
l4_hash또는 하드웨어 해시) 이를 사용하고, 제공하지 않으면 소프트웨어에서 패킷 헤더를 파싱하여 해시를 계산합니다.
IRQ 친화성(Affinity) 최적화
고성능 네트워크에서는 NIC 인터럽트를 적절한 CPU에 분배하는 것이 중요합니다. 각 RX/TX 큐의 IRQ를 특정 CPU에 고정하고, NAPI 처리와 유저 프로세스가 같은 CPU에서 실행되도록 구성합니다.
# irqbalance 중지 (수동 IRQ 관리 시)
systemctl stop irqbalance
# NIC IRQ 번호 확인
grep eth0 /proc/interrupts | awk '{print $1, $NF}'
# IRQ별 CPU 친화성 설정
# eth0-rx-0 → CPU 0, eth0-rx-1 → CPU 1, ...
echo 1 > /proc/irq/<IRQ0>/smp_affinity # CPU 0
echo 2 > /proc/irq/<IRQ1>/smp_affinity # CPU 1
echo 4 > /proc/irq/<IRQ2>/smp_affinity # CPU 2
echo 8 > /proc/irq/<IRQ3>/smp_affinity # CPU 3
# 자동화 스크립트 (set_irq_affinity 도구 사용)
# Intel 드라이버 패키지에 포함된 스크립트:
/usr/local/bin/set_irq_affinity.sh eth0
# NUMA 노드 확인
cat /sys/class/net/eth0/device/numa_node
# NIC가 연결된 NUMA 노드의 CPU에 IRQ를 할당하면
# 메모리 접근 지연이 줄어 성능이 향상됩니다
# 처리 확인
watch -n1 'cat /proc/interrupts | grep eth0'
# 각 CPU의 인터럽트 카운트가 균등한지 확인
numactl --hardware로 NUMA 토폴로지를 확인하고, NIC의 NUMA 노드는 cat /sys/class/net/eth0/device/numa_node로 확인합니다. 잘못된 NUMA 노드에 IRQ를 할당하면 원격 메모리 접근(remote memory access)으로 인해 PPS가 크게 저하될 수 있습니다.
네트워크 오프로드 기술
CPU 부하를 줄이기 위해 네트워크 처리의 일부를 NIC 하드웨어 또는 커널 소프트웨어 계층에서 최적화합니다. 오프로드 기능은 ethtool -k로 확인하고 ethtool -K로 제어합니다.
| 오프로드 | 방향 | 동작 | 성능 영향 |
|---|---|---|---|
| TSO (TCP Segmentation Offload) | TX | NIC 하드웨어에서 TCP 세그먼트 분할 | CPU 사용률 크게 감소 |
| GSO (Generic Segmentation Offload) | TX | 커널에서 대형 세그먼트를 지연 분할 | qdisc까지 단일 skb 전달 |
| GRO (Generic Receive Offload) | RX | 동일 플로우 패킷을 병합하여 상위 계층 호출 횟수 감소 | CPU 사용률 20-30% 감소 |
| LRO (Large Receive Offload) | RX | NIC 하드웨어에서 패킷 병합 | 라우터/포워딩에서 문제 가능 |
| 체크섬 오프로드 | TX/RX | NIC에서 L3/L4 체크섬 계산/검증 | CPU 부하 감소 |
| Scatter/Gather | TX | 비연속 메모리를 DMA로 직접 전송 | 메모리 복사 제거 |
# 오프로드 상태 확인
ethtool -k eth0 | grep -E "tcp-segmentation|generic-segmentation|generic-receive|checksum"
# 오프로드 제어
ethtool -K eth0 tso on # TSO 활성화
ethtool -K eth0 gro on # GRO 활성화
ethtool -K eth0 gso on # GSO 활성화
ethtool -K eth0 tx-checksum-ipv4 on # TX 체크섬 오프로드
체크섬 오프로드 내부 동작
체크섬 오프로드는 NIC 하드웨어에서 IP/TCP/UDP 체크섬을 계산(TX) 또는 검증(RX)하여 CPU 부하를 줄입니다. skb->ip_summed 필드가 체크섬 상태를 나타냅니다.
| ip_summed 값 | 방향 | 의미 | 커널 동작 |
|---|---|---|---|
CHECKSUM_NONE | RX | 체크섬 미검증 | 커널이 소프트웨어로 검증 |
CHECKSUM_UNNECESSARY | RX | NIC가 체크섬 검증 완료 | 커널이 검증 생략 |
CHECKSUM_COMPLETE | RX | NIC가 부분 체크섬 제공 | 커널이 나머지 검증 |
CHECKSUM_PARTIAL | TX | 커널이 부분 계산, NIC가 완성 | NIC가 최종 체크섬 삽입 |
TX 체크섬 오프로드 — 커널 내부
TCP/UDP 송신 시 의사 헤더 체크섬만 계산하며, ip_summed = CHECKSUM_PARTIAL을 설정합니다.
csum_start: 체크섬 계산 시작 오프셋csum_offset: 체크섬 삽입 위치 (TCP: 16, UDP: 6)
NIC 드라이버가 TX 디스크립터에 체크섬 오프로드 플래그를 설정하면, NIC 하드웨어가 csum_start부터 패킷 끝까지 체크섬을 계산하고 csum_offset 위치에 결과를 삽입합니다.
NIC가 체크섬 오프로드를 지원하지 않으면: dev_hard_start_xmit() → skb_csum_hwoffload_help() → skb_checksum_help()로 소프트웨어 체크섬을 계산합니다.
코드 설명
- 3-6행TX 체크섬 오프로드에서는 커널이 의사 헤더(pseudo header) 체크섬만 계산하고, 나머지는 NIC에 위임합니다.
csum_start와csum_offset으로 NIC에 계산 범위와 삽입 위치를 알려줍니다. - 12-14행NIC가 체크섬 오프로드를 지원하지 않으면
skb_checksum_help()가 소프트웨어에서 체크섬을 완전히 계산합니다. 이는 가상 NIC(veth, bridge)에서 자주 발생합니다.
XDP와 네트워크 스택 우회
XDP(eXpress Data Path)는 드라이버 레벨에서 eBPF 프로그램을 실행하여 sk_buff 할당 전에 패킷을 처리합니다. DDoS 방어, 패킷 필터링, 로드 밸런싱 등에서 10Mpps 이상의 성능을 달성할 수 있습니다.
/* XDP 프로그램의 반환값 (verdict) */
enum xdp_action {
XDP_ABORTED = 0, /* 오류 발생 — 패킷 드롭 + 추적 */
XDP_DROP, /* 패킷 드롭 (가장 빠름) */
XDP_PASS, /* 일반 네트워크 스택으로 전달 */
XDP_TX, /* 수신 인터페이스로 재전송 */
XDP_REDIRECT, /* 다른 인터페이스/AF_XDP로 리다이렉트 */
};
/* XDP 프로그램 예시: 특정 IP 드롭 (DDoS 방어) */
SEC("xdp")
int xdp_firewall(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = data + sizeof(*eth);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
/* BPF 맵에서 차단 IP 조회 */
if (bpf_map_lookup_elem(&blocked_ips, &ip->saddr))
return XDP_DROP; /* sk_buff 할당 전 드롭 */
return XDP_PASS;
}
코드 설명
- 2-8행XDP verdict는 패킷의 처리 방향을 결정합니다.
XDP_DROP은 sk_buff를 할당하지 않고 즉시 드롭하므로 가장 빠르며, DDoS 방어에 이상적입니다. - 14-15행XDP 프로그램은
xdp_md구조체로 패킷 데이터에 접근합니다.data와data_end포인터 사이가 패킷 데이터이며, 경계 검사가 필수입니다. - 29-30행BPF 맵(해시 테이블)에서 소스 IP를 조회하여 차단 여부를 결정합니다. 맵 조회는 O(1)이므로 수백만 개의 IP를 차단해도 성능 저하가 없습니다.
# XDP 프로그램 부착
ip link set dev eth0 xdpgeneric obj prog.o sec xdp # 소프트웨어 XDP
ip link set dev eth0 xdp obj prog.o sec xdp # 드라이버 XDP (네이티브)
# XDP 프로그램 상태 확인
ip link show eth0 | grep xdp
# XDP 프로그램 제거
ip link set dev eth0 xdp off
# XDP 통계 확인
bpftool prog show
bpftool map dump id <MAP_ID>
네트워크 네임스페이스와 가상 디바이스
네트워크 네임스페이스(struct net)는 네트워크 스택 전체를 격리하는 커널 기능입니다. 각 네임스페이스는 독립적인 네트워크 인터페이스, 라우팅 테이블, Netfilter 규칙, 소켓 공간을 가집니다. 컨테이너(Docker, Kubernetes)의 네트워크 격리 기반이 됩니다.
네트워크 네임스페이스 내부 구조
커널 내부에서 모든 네트워크 관련 함수는 struct net *net 매개변수를 통해 현재 네임스페이스를 전달받습니다. 예를 들어 라우팅 조회 시 fib_lookup(net, ...)으로 해당 네임스페이스의 라우팅 테이블만 조회합니다.
/* include/net/net_namespace.h — 네트워크 네임스페이스 핵심 필드 */
struct net {
struct list_head list; /* 전역 네임스페이스 리스트 */
struct ns_common ns; /* 공통 네임스페이스 정보 */
struct net_device *loopback_dev; /* lo 인터페이스 */
struct hlist_head *dev_name_head; /* 이름별 디바이스 해시 */
struct hlist_head *dev_index_head;/* 인덱스별 디바이스 해시 */
struct netns_ipv4 ipv4; /* IPv4 전용 데이터 */
struct netns_ipv6 ipv6; /* IPv6 전용 데이터 */
struct netns_nf nf; /* Netfilter 데이터 */
struct netns_ct ct; /* conntrack 데이터 */
struct sock *rtnl; /* rtnetlink 소켓 */
/* ... */
};
# 네트워크 네임스페이스 생성 및 관리
ip netns add ns1 # 네임스페이스 생성
ip netns list # 네임스페이스 목록
ip netns exec ns1 ip addr show # 네임스페이스 내에서 명령 실행
# veth 페어로 네임스페이스 연결
ip link add veth0 type veth peer name veth1
ip link set veth1 netns ns1
ip addr add 10.0.0.1/24 dev veth0
ip netns exec ns1 ip addr add 10.0.0.2/24 dev veth1
ip link set veth0 up
ip netns exec ns1 ip link set veth1 up
가상 네트워크 디바이스 유형
Linux 커널은 다양한 가상 네트워크 디바이스를 제공합니다. 각 유형은 특정 네트워크 아키텍처 패턴에 맞게 설계되었습니다.
| 디바이스 유형 | 용도 | 동작 원리 | 주요 사용 사례 |
|---|---|---|---|
| veth | 네임스페이스 연결 | 두 끝이 서로 연결된 가상 이더넷 쌍 | Docker 컨테이너, K8s Pod |
| bridge | L2 스위칭 | 소프트웨어 이더넷 브릿지, MAC 학습 | VM 네트워킹, 컨테이너 브릿지 |
| TUN | L3 터널 | 유저 공간과 IP 패킷 교환 | VPN (OpenVPN, WireGuard) |
| TAP | L2 터널 | 유저 공간과 이더넷 프레임 교환 | QEMU/KVM 네트워킹 |
| MACVLAN | MAC 기반 가상화 | 하나의 물리 NIC에 여러 MAC 주소 | 컨테이너 직접 네트워킹 |
| IPVLAN | IP 기반 가상화 | 동일 MAC, 다른 IP로 L3 격리 | MAC 주소 제한 환경 |
| VXLAN | 오버레이(Overlay) 네트워크 | UDP 캡슐화(Encapsulation)로 L2 over L3 터널 | K8s Flannel, 멀티테넌트 |
| GENEVE | 범용 오버레이 | 확장 가능한 UDP 캡슐화 | OVN, 차세대 오버레이 |
| bond | NIC 결합 | 여러 물리 NIC를 하나로 결합 | 고가용성, 대역폭 집계 |
| dummy | 가상 인터페이스 | 항상 UP 상태인 가상 NIC | loopback 대체, 라우팅 앵커 |
| VRF | L3 도메인 격리 | 라우팅 테이블 격리 (L3 master) | 멀티테넌트 라우팅 |
가상 디바이스 생성 명령 예시
# veth 쌍 생성
ip link add veth0 type veth peer name veth1
# Linux Bridge 생성 및 포트 추가
ip link add br0 type bridge
ip link set eth0 master br0
ip link set veth0 master br0
ip link set br0 up
# MACVLAN 생성 (bridge 모드)
ip link add macvlan0 link eth0 type macvlan mode bridge
ip addr add 10.0.0.100/24 dev macvlan0
ip link set macvlan0 up
# IPVLAN 생성 (L3 모드)
ip link add ipvlan0 link eth0 type ipvlan mode l3
ip addr add 10.0.0.101/24 dev ipvlan0
ip link set ipvlan0 up
# VXLAN 생성 (멀티캐스트 기반)
ip link add vxlan100 type vxlan id 100 \
group 239.1.1.1 dev eth0 dstport 4789
ip link set vxlan100 up
# GENEVE 생성
ip link add geneve100 type geneve id 100 \
remote 10.0.0.2 dstport 6081
ip link set geneve100 up
# TUN 디바이스 생성
ip tuntap add name tun0 mode tun
ip addr add 10.1.0.1/24 dev tun0
ip link set tun0 up
# Bond 생성 (802.3ad LACP)
ip link add bond0 type bond mode 802.3ad
ip link set eth0 master bond0
ip link set eth1 master bond0
ip link set bond0 up
# VRF 생성
ip link add vrf-red type vrf table 100
ip link set vrf-red up
ip link set eth2 master vrf-red
veth 쌍 + bridge(docker0) 구조입니다. 컨테이너의 eth0은 veth의 한쪽 끝이고, 다른 쪽은 호스트의 docker0 브릿지에 연결됩니다. Kubernetes는 CNI 플러그인에 따라 다양한 가상 디바이스 조합을 사용합니다(Calico: IPIP/VXLAN, Cilium: veth + eBPF, Flannel: VXLAN).
컨테이너 네트워킹 아키텍처 비교
| CNI 플러그인 | 데이터 경로 | 오버레이 | 정책 엔진(Policy Engine) | 성능 특성 |
|---|---|---|---|---|
| Docker Bridge | veth + bridge | 없음 (호스트 로컬) | iptables | 기준선 |
| Flannel | veth + bridge + VXLAN | VXLAN | 외부 의존 | 오버레이 오버헤드 |
| Calico | veth + 라우팅 | IPIP/VXLAN (선택) | iptables/eBPF | L3 직접 라우팅으로 높은 성능 |
| Cilium | veth + eBPF | VXLAN/GENEVE (선택) | eBPF | kube-proxy 대체, 최고 성능 |
| Antrea | OVS | GENEVE | OVS + eBPF | OVS 기반 유연한 정책 |
| SR-IOV | VF 직접 할당 | 없음 | 하드웨어 | 최소 지연, 최대 대역폭 |
네트워크 네임스페이스 내부 구현
네트워크 네임스페이스(struct net)의 내부 구현을 이해하면 컨테이너 네트워킹의 성능 특성과 제약을 파악할 수 있습니다. 이 섹션에서는 네임스페이스 생성/삭제 과정과 프로토콜 스택 격리 메커니즘을 설명합니다.
네임스페이스 생성 과정
clone(CLONE_NEWNET) 또는 unshare(CLONE_NEWNET) 시스템 콜로 새 네트워크 네임스페이스를 생성하면 커널 내부에서 다음 과정이 진행됩니다:
- struct net 할당 —
copy_net_ns()가 새struct net을 할당합니다. 각 네임스페이스 서브시스템(IPv4, IPv6, Netfilter 등)의pernet_operations가 초기화됩니다. - loopback 디바이스 생성 —
loopback_net_init()이 새 네임스페이스 전용lo인터페이스를 생성합니다. 이것이 네임스페이스의 최초 네트워크 인터페이스입니다. - 프로토콜별 초기화 — IPv4 라우팅 테이블(
fib_net_init()), Netfilter 상태(nf_conntrack_pernet_init()), proc 파일시스템(Filesystem) 엔트리 등이 네임스페이스별로 초기화됩니다. - sysctl 복사 — 네트워크 관련 sysctl 변수가 네임스페이스별로 독립 복사됩니다.
/proc/sys/net/아래의 모든 항목이 네임스페이스별로 격리됩니다.
/* net/core/net_namespace.c — 네임스페이스 생성 핵심 */
static struct net *setup_net(struct net *net, struct user_namespace *user_ns)
{
/* 참조 카운트 초기화 */
refcount_set(&net->ns.count, 1);
/* 모든 pernet_operations의 init 콜백 호출 */
list_for_each_entry(ops, &pernet_list, list) {
if (ops->init) {
error = ops->init(net);
if (error < 0)
goto out_undo;
}
}
/* ... */
}
pernet_operations 등록 메커니즘
각 네트워크 서브시스템은 struct pernet_operations를 등록하여 네임스페이스 생성/삭제 시 호출되는 콜백을 정의합니다. 이 메커니즘 덕분에 새로운 네트워크 기능을 추가해도 네임스페이스 격리가 자동으로 적용됩니다.
/* 서브시스템별 pernet_operations 등록 예시 */
static struct pernet_operations ipv4_net_ops = {
.init = ipv4_net_init,
.exit = ipv4_net_exit,
};
/* 초기화 시 등록 */
register_pernet_subsys(&ipv4_net_ops);
/* 네임스페이스 생성 시 ipv4_net_init() 자동 호출 */
/* 네임스페이스 삭제 시 ipv4_net_exit() 자동 호출 */
네임스페이스 간 디바이스 이동
ip link set dev eth0 netns ns1 명령은 dev_change_net_namespace() 함수를 호출합니다. 이 과정에서 디바이스의 nd_net 포인터가 대상 네임스페이스로 변경되고, 기존 네임스페이스의 디바이스 리스트에서 제거된 후 대상 네임스페이스의 리스트에 삽입됩니다. 디바이스의 모든 qdisc, 소켓 연결, Netfilter 상태가 재초기화됩니다.
네임스페이스에서 격리되는 자원
| 자원 | 격리 수준 | 공유 여부 | 커널 구현 |
|---|---|---|---|
| 네트워크 인터페이스 | 완전 격리 | 네임스페이스 전용 | dev_net(dev) |
| 라우팅 테이블 | 완전 격리 | 독립 FIB | net->ipv4.fib_table_hash |
| Netfilter 규칙 | 완전 격리 | 독립 nftables/iptables | net->nf |
| conntrack 테이블 | 완전 격리 | 독립 해시 테이블 | net->ct |
| 소켓 공간 | 완전 격리 | 포트 충돌 없음 | sock_net(sk) |
| sysctl 변수 | 완전 격리 | 독립 값 | netns_ipv4 등 |
| /proc/net | 완전 격리 | 네임스페이스별 통계 | proc_net_init() |
| ARP/NDP 캐시 | 완전 격리 | 독립 이웃 테이블 | neigh_table |
가상 네트워크 디바이스 유형
Linux는 다양한 가상 네트워크 디바이스를 제공하여 네트워크 토폴로지를 소프트웨어로 구성할 수 있습니다. 각 디바이스 유형은 고유한 성능 특성과 사용 시나리오를 가집니다.
| 디바이스 유형 | 용도 | 패킷 경로 | 성능 특성 | 사용 예 |
|---|---|---|---|---|
| veth | 네임스페이스 간 연결 | 한쪽 전송 → 다른 쪽 수신 | 추가 softirq, sk_buff 복사 | Docker 기본 네트워킹 |
| bridge | L2 스위칭 | FDB 조회 → 포트 포워딩 | 멀티 포트 시 해시 조회 | VM/컨테이너 브릿지 |
| macvlan | MAC 기반 가상 인터페이스 | MAC 주소로 직접 분배 | veth보다 낮은 오버헤드 | 경량 컨테이너 |
| ipvlan | IP 기반 가상 인터페이스 | IP 주소로 직접 분배 | MAC 학습 불필요 | 동일 MAC 공유 시 |
| vxlan | L2 over UDP 터널 | UDP 캡슐화/디캡슐화 | 캡슐화 오버헤드 | 오버레이 네트워크 |
| geneve | 범용 터널 (VXLAN 후속) | UDP 캡슐화, 가변 옵션 | 캡슐화 오버헤드 | OVN, Cilium |
| wireguard | VPN 터널 | 암호화 + UDP 캡슐화 | 암호화 CPU 비용 | 사이트간 VPN |
| tun/tap | 유저 공간 네트워킹 | 유저 ↔ 커널 패킷 전달 | 유저/커널 전환 비용 | OpenVPN, QEMU |
| bond | 링크 집합 | 해시/라운드로빈 분배 | 분배 오버헤드 최소 | 고가용성, 대역폭 합산 |
| team | 링크 집합 (bond 대체) | Netlink 기반 설정 | bond과 유사 | 유연한 정책 설정 |
/* drivers/net/veth.c — veth 패킷 전달 핵심 (간략화) */
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);
if (unlikely(!rcv)) {
kfree_skb(skb);
return NETDEV_TX_OK;
}
/* 피어 디바이스로 패킷 전달 */
skb->dev = rcv; /* 수신 디바이스 변경 */
skb->protocol = eth_type_trans(skb, rcv);
skb->pkt_type = PACKET_HOST;
/* XDP 프로그램이 있으면 실행 */
if (likely(veth_xdp_rx(rq, skb)))
goto xmit;
/* NAPI를 통해 피어 네임스페이스에서 수신 처리 */
if (netif_receive_skb(skb) == NET_RX_SUCCESS)
return NETDEV_TX_OK;
return NETDEV_TX_OK;
}
코드 설명
- 6행veth의 각 끝은 peer 포인터로 상대방을 가리킵니다. 한쪽에서 전송하면 상대방의 수신 경로로 패킷이 전달됩니다.
- 14-16행sk_buff의
dev를 피어 디바이스로 변경하고,eth_type_trans()로 프로토콜을 설정합니다. 패킷이 마치 피어 디바이스에서 수신된 것처럼 처리됩니다. - 22행
netif_receive_skb()로 피어 네임스페이스의 네트워크 스택에 패킷을 주입합니다. 이 호출은 softirq 컨텍스트에서 실행되므로 추가적인 CPU 비용이 발생합니다.
# 가상 디바이스 생성 명령어 모음
# veth 페어 생성
ip link add veth0 type veth peer name veth1
ip link set veth1 netns ns1
# Linux 브릿지 생성
ip link add br0 type bridge
ip link set eth0 master br0
ip link set veth0 master br0
ip link set br0 up
# MACVLAN 생성
ip link add macvlan0 link eth0 type macvlan mode bridge
ip link set macvlan0 netns ns1
# IPVLAN 생성
ip link add ipvlan0 link eth0 type ipvlan mode l3
# VXLAN 터널 생성
ip link add vxlan100 type vxlan id 100 \
local 192.168.1.1 remote 192.168.1.2 \
dstport 4789 dev eth0
# WireGuard 인터페이스 생성
ip link add wg0 type wireguard
wg set wg0 private-key /path/to/private.key \
listen-port 51820 \
peer PEER_PUBLIC_KEY= endpoint 10.0.0.2:51820 \
allowed-ips 10.10.10.0/24
# TUN 디바이스 생성 (유저 공간 프로그램에서)
ip tuntap add dev tun0 mode tun
제로카피 네트워킹 (Zero-Copy Networking)
제로카피(Zero-Copy)는 유저 공간과 커널 공간 사이의 데이터 복사를 제거하여 CPU 사용률과 메모리 대역폭을 절약하는 기법입니다. Linux 네트워크 스택은 송신(sendfile, MSG_ZEROCOPY), 수신(tcp_mmap), 그리고 io_uring을 통한 비동기 소켓 I/O까지 다양한 제로카피 메커니즘을 제공합니다.
io_uring 소켓 통합
io_uring은 커널 5.6부터 소켓 I/O를 지원하며, IORING_OP_SEND, IORING_OP_RECV, IORING_OP_ACCEPT 등의 비동기 소켓 연산을 제공합니다. Submission Queue(SQ)와 Completion Queue(CQ)를 유저-커널 간 공유하여 시스템 콜 오버헤드를 최소화합니다.
/* io_uring 제로카피 소켓 전송 (IORING_OP_SEND_ZC) */
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
/* 제로카피 전송 설정 */
io_uring_prep_send_zc(sqe, sockfd, buf, len, 0, 0);
sqe->flags |= IOSQE_CQE_SKIP_SUCCESS;
/* 제출 — 시스템 콜 없이 SQ에 직접 기록 */
io_uring_submit(ring);
/* 완료 확인 — CQ에서 결과 수집 */
struct io_uring_cqe *cqe;
io_uring_wait_cqe(ring, &cqe);
/* IORING_CQE_F_NOTIF: 제로카피 완료 통지 */
/* 이 CQE 이후 buf를 재사용할 수 있습니다 */
if (cqe->flags & IORING_CQE_F_NOTIF)
io_uring_cqe_seen(ring, cqe);
코드 설명
- 5행
io_uring_prep_send_zc()는IORING_OP_SEND_ZC연산을 준비합니다. 내부적으로MSG_ZEROCOPY와 동일한 메커니즘(페이지 핀)을 사용합니다. - 9행
io_uring_submit()은 SQ에 기록된 요청을 커널에 제출합니다.IORING_SETUP_SQPOLL모드에서는 커널 스레드가 SQ를 폴링하므로 시스템 콜이 필요 없습니다. - 17행
IORING_CQE_F_NOTIF플래그가 설정된 CQE는 DMA 전송이 완료되어 유저 버퍼를 재사용할 수 있음을 알립니다.
| 메커니즘 | 방향 | 데이터 소스 | 커널 버전 | 특징 |
|---|---|---|---|---|
sendfile() | TX | 파일 → 소켓 | 2.2+ | 페이지 캐시 직접 참조, 가장 성숙 |
splice() | TX/RX | 파이프 기반 | 2.6.17+ | sendfile보다 유연, 파이프 필요 |
MSG_ZEROCOPY | TX | 유저 버퍼 → 소켓 | 4.14+ | 범용, 완료 통지 필요, 10KB+ 효율적 |
tcp_mmap() | RX | 소켓 → 유저 매핑 | 4.18+ | TCP 수신 제로카피, 실험적 |
io_uring SEND_ZC | TX | 유저 버퍼 | 6.0+ | 비동기 + 제로카피, 최고 성능 |
AF_XDP | TX/RX | UMEM | 4.18+ | 커널 스택 우회, 최고 PPS |
splice와 vmsplice
splice()는 파이프를 중간 매개체로 사용하여 두 파일 디스크립터(File Descriptor) 간에 제로카피 데이터 전송을 수행합니다. vmsplice()는 유저 공간 메모리를 파이프에 제로카피로 주입합니다.
/* splice 기반 소켓 전송 — 유저 공간 */
int pipefd[2];
pipe(pipefd);
/* 1. 파일 → 파이프 (splice) */
splice(file_fd, &off, pipefd[1], NULL, len, SPLICE_F_MOVE);
/* 2. 파이프 → 소켓 (splice) */
splice(pipefd[0], NULL, sock_fd, NULL, len, SPLICE_F_MOVE);
/* vmsplice: 유저 메모리 → 파이프 (제로카피) */
struct iovec iov = { .iov_base = buf, .iov_len = len };
vmsplice(pipefd[1], &iov, 1, SPLICE_F_GIFT);
/* SPLICE_F_GIFT: 페이지 소유권을 파이프에 양도 */
/* → 이후 buf 수정 금지, 파이프가 페이지 해제 */
코드 설명
- 6행
splice()는 파일의 페이지 캐시에서 파이프 버퍼로 페이지 참조를 이동합니다. 실제 데이터 복사 없이 페이지 참조만 전달됩니다. - 9행파이프에서 소켓으로의
splice()는 파이프 버퍼의 페이지를 sk_buff의frags[]에 매핑합니다.sendfile()과 동일한 효과이지만, 파이프를 통해 더 유연한 데이터 흐름이 가능합니다. - 13행
vmsplice()에SPLICE_F_GIFT를 사용하면 유저 페이지의 소유권을 파이프에 양도합니다. 이 경우 유저 공간에서 해당 메모리를 수정하면 안 됩니다.
성능 비교와 선택 가이드
| 사용 시나리오 | 권장 메커니즘 | 근거 |
|---|---|---|
| 정적 파일 서빙 (Nginx, Apache) | sendfile() | 가장 성숙, 페이지 캐시 활용 |
| 프록시 (리버스 프록시, LB) | splice() | 소켓→소켓 전달, 중간 수정 가능 |
| 애플리케이션 데이터 전송 (DB 복제) | MSG_ZEROCOPY | 유저 버퍼 직접 전송, 10KB+ 효율 |
| 고성능 비동기 I/O | io_uring SEND_ZC | syscall 오버헤드 최소화 |
| 패킷 처리 (DPI, IDS) | AF_XDP | 커널 스택 완전 우회 |
| 소형 패킷 (<4KB) | 일반 send() | 제로카피 오버헤드가 복사보다 큼 |
MSG_ZEROCOPY의 경우 get_user_pages()의 오버헤드(TLB flush, 페이지 테이블 조작)로 인해 약 10KB 이하의 전송에서는 일반 send()보다 느릴 수 있습니다. 또한 완료 통지를 위한 recvmsg(MSG_ERRQUEUE) 호출이 추가로 필요합니다. 벤치마크로 실제 워크로드에서의 효과를 반드시 확인해야 합니다.
네트워크 초기화 시퀀스 (Network Initialization)
Linux 커널의 네트워크 스택은 부팅 시 특정 순서로 초기화됩니다. 각 서브시스템은 의존성을 고려하여 올바른 순서로 로드되어야 하며, core_initcall, subsys_initcall, fs_initcall 등의 initcall 매크로로 순서를 지정합니다.
/* net/socket.c — 네트워크 초기화 진입점 */
static int __init sock_init(void)
{
/* 1. sk_buff SLAB 캐시 초기화 */
skb_init();
/* 2. VFS 소켓 파일시스템 등록 */
init_inodecache();
register_filesystem(&sock_fs_type);
sock_mnt = kern_mount(&sock_fs_type);
/* 3. Netfilter 프레임워크 초기화 */
netfilter_init();
return 0;
}
core_initcall(sock_init); /* 최우선 초기화 */
/* net/ipv4/af_inet.c — inet_init() IPv4 스택 초기화 */
static int __init inet_init(void)
{
/* 1. 프로토콜 구조체 등록 */
proto_register(&tcp_prot, 1);
proto_register(&udp_prot, 1);
proto_register(&raw_prot, 1);
proto_register(&ping_prot, 1);
/* 2. AF_INET 소켓 패밀리 등록 */
sock_register(&inet_family_ops);
/* 3. ARP 초기화 */
arp_init();
/* 4. IP 계층 초기화 */
ip_init();
/* 5. TCP/UDP L4 프로토콜 등록 */
inet_add_protocol(&icmp_protocol, IPPROTO_ICMP);
inet_add_protocol(&udp_protocol, IPPROTO_UDP);
inet_add_protocol(&tcp_protocol, IPPROTO_TCP);
/* 6. TCP 서브시스템 초기화 */
tcp_init(); /* 해시 테이블, 메모리, 타이머 */
udp_init(); /* UDP 해시 테이블 */
udplite_init(); /* UDP-Lite */
/* 7. ICMP/IGMP 초기화 */
icmp_init();
igmp_mc_init();
/* 8. IP L3 패킷 핸들러 등록 (EtherType 0x0800) */
dev_add_pack(&ip_packet_type);
/* 9. IP 단편화, FIB, 이웃 테이블 등 */
ip_mr_init();
inet_initpeers();
return 0;
}
fs_initcall(inet_init); /* sock_init 이후 실행 */
코드 설명
- 2-17행
sock_init()은core_initcall로 등록되어 부팅 초기에 실행됩니다. sk_buff 캐시, 소켓 VFS, Netfilter 프레임워크를 초기화합니다. 모든 프로토콜의 기반이 됩니다. - 23-26행TCP, UDP, RAW, PING 프로토콜의
struct proto를 등록합니다. 이 단계에서 SLAB 캐시(tcp_sock, udp_sock 등)가 생성됩니다. - 30행
sock_register()로 AF_INET 패밀리를 등록합니다. 이후socket(AF_INET, ...)호출 시inet_create()가 호출됩니다. - 38-40행IP 프로토콜 번호(ICMP=1, TCP=6, UDP=17)에 L4 핸들러를 등록합니다.
ip_local_deliver_finish()에서 이 핸들러가 호출됩니다. - 43행
tcp_init()은 ESTABLISHED/LISTEN 해시 테이블, TCP 메모리 한도(tcp_mem), 혼잡 제어 모듈 등을 초기화합니다. - 52행
dev_add_pack()으로 EtherType 0x0800(IPv4)에ip_rcv()핸들러를 등록합니다. 이 시점부터 IPv4 패킷 수신이 가능해집니다. - 59행
fs_initcall은core_initcall이후에 실행되므로,sock_init()이 완료된 후inet_init()이 실행됨이 보장됩니다.
초기화 순서와 의존성
| initcall 레벨 | 함수 | 초기화 내용 | 의존성 |
|---|---|---|---|
core_initcall | sock_init() | sk_buff, 소켓 VFS, Netfilter 기반 | 메모리 관리자 |
core_initcall | net_dev_init() | softnet_data, NET_RX/TX_SOFTIRQ | softirq |
subsys_initcall | net_ns_init() | init_net 네임스페이스 | sock_init |
subsys_initcall | netdev_kobject_init() | sysfs 네트워크 클래스 | sysfs |
fs_initcall | inet_init() | IPv4 전체 (TCP/UDP/ICMP/ARP) | sock_init, net_dev_init |
module_init | inet6_init() | IPv6 전체 | inet_init |
module_init | nf_conntrack_init() | 연결 추적 (모듈) | netfilter_init |
late_initcall | tcp_congestion_default() | 기본 혼잡 제어 설정 | tcp_init |
/* net/core/dev.c — net_dev_init() per-CPU 구조체 초기화 */
static int __init net_dev_init(void)
{
int i;
/* per-CPU softnet_data 초기화 */
for_each_possible_cpu(i) {
struct softnet_data *sd = &per_cpu(softnet_data, i);
INIT_LIST_HEAD(&sd->poll_list);
skb_queue_head_init(&sd->input_pkt_queue);
skb_queue_head_init(&sd->process_queue);
sd->backlog.poll = process_backlog;
sd->backlog.weight = 64; /* NAPI weight */
}
/* NET_RX_SOFTIRQ / NET_TX_SOFTIRQ 등록 */
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
return 0;
}
subsys_initcall(net_dev_init);
코드 설명
- 7-15행각 CPU마다
softnet_data구조체를 초기화합니다.poll_list(NAPI 폴링 대기 큐),input_pkt_queue(RPS 입력 큐),process_queue(처리 중 큐)를 설정합니다. - 13-14행per-CPU backlog NAPI 구조체를 초기화합니다. non-NAPI 드라이버나 RPS가 사용하는 기본 NAPI 인스턴스로,
process_backlog()함수가 poll 콜백으로 등록됩니다. - 18-19행
open_softirq()로 네트워크 softirq를 등록합니다.net_rx_action()은 앞서 설명한 NAPI 폴링 루프를 실행하고,net_tx_action()은 지연된 TX 완료 처리를 수행합니다.
modprobe 시점에 의존성이 해결됩니다. 예를 들어 nf_conntrack 모듈은 nf_defrag_ipv4에 의존하므로, modprobe nf_conntrack 시 자동으로 의존 모듈이 먼저 로드됩니다. lsmod | grep nf_로 로드된 Netfilter 모듈과 의존 관계를 확인할 수 있습니다.
부팅 로그에서 네트워크 초기화 확인
# 네트워크 초기화 관련 부팅 메시지 확인
dmesg | grep -iE "(tcp|udp|ipv4|ipv6|netfilter|napi|conntrack)" | head -30
# 전형적인 출력 예시:
# [ 0.123456] TCP established hash table entries: 524288
# [ 0.123789] TCP bind hash table entries: 65536
# [ 0.124012] TCP: Hash tables configured (established 524288 bind 65536)
# [ 0.124234] UDP hash table entries: 32768
# [ 0.124567] UDP-Lite hash table entries: 32768
# [ 0.456789] NET: Registered PF_INET protocol family
# [ 0.567890] NET: Registered PF_INET6 protocol family
# [ 1.234567] nf_conntrack version 0.5.0 (131072 buckets, 524288 max)
# initcall 타이밍 분석 (커널 파라미터 initcall_debug 필요)
dmesg | grep "initcall.*returned" | grep -i net
# 프로토콜 패밀리 등록 상태
cat /proc/net/protocols # 등록된 프로토콜 목록
# protocol size sockets memory press maxhdr slab module
# TCPv6 2416 123 234 no 320 yes kernel
# UDPv6 1216 45 12 no 0 yes kernel
# TCP 2080 567 890 no 320 yes kernel
# UDP 960 234 56 no 0 yes kernel
네트워크 스택 추적(Tracing) 포인트
커널은 네트워크 스택의 주요 경로에 tracepoint를 제공합니다. 이를 통해 패킷의 전체 경로를 추적하고, 성능 병목과 드롭 원인을 정밀하게 분석할 수 있습니다.
# 사용 가능한 네트워크 tracepoint 확인
ls /sys/kernel/debug/tracing/events/net/
ls /sys/kernel/debug/tracing/events/skb/
ls /sys/kernel/debug/tracing/events/napi/
ls /sys/kernel/debug/tracing/events/tcp/
ls /sys/kernel/debug/tracing/events/sock/
# 패킷 드롭 추적 (kfree_skb)
perf trace --no-syscalls -e skb:kfree_skb -a -- sleep 5
# 특정 인터페이스의 NAPI 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/napi/napi_poll/enable
cat /sys/kernel/debug/tracing/trace_pipe | head -20
# TCP 재전송 추적
perf trace --no-syscalls -e tcp:tcp_retransmit_skb -a -- sleep 10
# TCP 상태 전이 추적
perf trace --no-syscalls -e sock:inet_sock_set_state -a -- sleep 5
# bpftrace를 이용한 고급 추적
bpftrace -e 'tracepoint:skb:kfree_skb {
printf("drop reason=%d location=%s\n",
args->reason, ksym(args->location));
}'
# 네트워크 계층별 지연 측정 (bpftrace)
bpftrace -e 'kprobe:ip_rcv { @start[tid] = nsecs; }
kretprobe:ip_rcv /@start[tid]/ {
@latency_ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}'
| Tracepoint 카테고리 | 주요 이벤트 | 용도 |
|---|---|---|
skb:kfree_skb | 패킷 드롭 | 드롭 원인과 위치 추적 |
skb:consume_skb | 패킷 정상 소비 | 처리 완료 확인 |
napi:napi_poll | NAPI 폴링 실행 | CPU 사용률, budget 소비 분석 |
net:netif_receive_skb | RX 수신 진입 | RX 경로 타이밍 |
net:net_dev_xmit | TX 전송 완료 | TX 경로 타이밍, 전송 실패 |
tcp:tcp_retransmit_skb | TCP 재전송 | 네트워크 문제 탐지 |
tcp:tcp_probe | TCP 상태 샘플링 | cwnd, srtt, ssthresh 추적 |
sock:inet_sock_set_state | 소켓 상태 전이 | 연결 수립/종료 모니터링 |
fib:fib_table_lookup | FIB 라우팅 조회 | 라우팅 성능 분석 |
qdisc:qdisc_dequeue | qdisc dequeue | qdisc 지연 측정 |
커널 빌드 옵션 (Kconfig)
네트워크 스택의 각 기능은 커널 빌드 시 Kconfig 옵션으로 활성화/비활성화합니다. 프로덕션 환경에서 필요한 기능을 정확히 활성화하면 커널 크기를 줄이고 보안 공격 면을 최소화할 수 있습니다.
핵심 네트워크 Kconfig 옵션
| 옵션 | 기본값 | 역할 | 비활성화 시 영향 |
|---|---|---|---|
CONFIG_NET | y | 네트워킹 지원 | 네트워크 기능 전체 비활성화 |
CONFIG_INET | y | TCP/IP 네트워킹 | IPv4/IPv6 스택 비활성화 |
CONFIG_IPV6 | m/y | IPv6 프로토콜 지원 | IPv6 연결 불가 |
CONFIG_NETFILTER | y | Netfilter 프레임워크 | 방화벽/NAT 불가 |
CONFIG_NF_CONNTRACK | m/y | 연결 추적 | stateful 방화벽/NAT 불가 |
CONFIG_NET_SCHED | y | TC/qdisc | 트래픽 셰이핑 불가 |
CONFIG_BPF_SYSCALL | y | eBPF 시스템 콜 | XDP/TC BPF 프로그램 불가 |
CONFIG_XDP_SOCKETS | m/y | AF_XDP 소켓 | AF_XDP 기반 고성능 처리 불가 |
CONFIG_BRIDGE | m | 이더넷 브릿지 | L2 브릿지 기능 불가 |
CONFIG_VLAN_8021Q | m | 802.1Q VLAN | VLAN 태깅 불가 |
CONFIG_NET_CLS_BPF | m | TC BPF classifier | TC BPF 프로그램 부착 불가 |
CONFIG_TLS | m | 커널 TLS (kTLS) | TLS 오프로드 불가 |
CONFIG_NET_NS | y | 네트워크 네임스페이스 | 컨테이너 네트워크 격리 불가 |
NIC 드라이버 Kconfig
| 옵션 | NIC | 비고 |
|---|---|---|
CONFIG_IXGBE | Intel 10G (ixgbe) | 82599, X520, X540 |
CONFIG_I40E | Intel 25/40G (i40e) | XL710, XXV710 |
CONFIG_ICE | Intel 100G (ice) | E810 |
CONFIG_MLX5_CORE | Mellanox ConnectX-5/6/7 | mlx5 드라이버 |
CONFIG_BNXT | Broadcom NetXtreme-E | bnxt_en 드라이버 |
CONFIG_VIRTIO_NET | virtio-net | KVM/QEMU 가상 NIC |
CONFIG_R8169 | Realtek 8168/8111 | 일반 데스크톱 NIC |
# 현재 커널의 네트워크 관련 Kconfig 확인
zcat /proc/config.gz | grep -E "^CONFIG_(NET|INET|IPV6|NETFILTER|BPF|XDP)" 2>/dev/null || \
grep -E "^CONFIG_(NET|INET|IPV6|NETFILTER|BPF|XDP)" /boot/config-$(uname -r)
# 특정 드라이버 모듈 로드 상태 확인
lsmod | grep -E "ixgbe|i40e|ice|mlx5|bnxt|virtio_net"
# 커널 빌드 시 네트워크 설정 메뉴
make menuconfig # Networking support → Networking options
Kconfig 의존성 관계
네트워크 Kconfig 옵션들은 복잡한 의존성 관계를 가집니다. 아래는 주요 의존성 체인입니다:
| 기능 | 필수 의존성 | 권장 옵션 |
|---|---|---|
| TC BPF classifier | CONFIG_NET_SCHED + CONFIG_BPF_SYSCALL | CONFIG_NET_CLS_ACT |
| XDP | CONFIG_BPF_SYSCALL + NIC 드라이버 | CONFIG_BPF_JIT |
| AF_XDP | CONFIG_XDP_SOCKETS + CONFIG_BPF_SYSCALL | CONFIG_BPF_JIT |
| NAT | CONFIG_NETFILTER + CONFIG_NF_CONNTRACK | CONFIG_NF_NAT |
| nftables | CONFIG_NETFILTER + CONFIG_NF_TABLES | CONFIG_NF_TABLES_INET |
| VXLAN | CONFIG_VXLAN + CONFIG_INET | CONFIG_NET_UDP_TUNNEL |
| kTLS | CONFIG_TLS + CONFIG_INET | CONFIG_TLS_DEVICE |
| WireGuard | CONFIG_WIREGUARD + CONFIG_INET | CONFIG_CRYPTO_CHACHA20POLY1305 |
| Bonding | CONFIG_BONDING | CONFIG_NET_TEAM (대안) |
| OVS | CONFIG_OPENVSWITCH + CONFIG_BRIDGE | CONFIG_OPENVSWITCH_VXLAN |
프로덕션 환경별 권장 Kconfig
일반 서버 (웹/API):
CONFIG_NET=y
CONFIG_INET=y
CONFIG_IPV6=y
CONFIG_NETFILTER=y
CONFIG_NF_CONNTRACK=y
CONFIG_NF_NAT=y
CONFIG_NF_TABLES=y
CONFIG_NET_SCHED=y
CONFIG_NET_SCH_FQ_CODEL=y
CONFIG_TCP_CONG_BBR=m
CONFIG_TLS=m
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y
컨테이너 호스트 (Docker/K8s):
CONFIG_NET_NS=y # 필수: 네트워크 네임스페이스
CONFIG_BRIDGE=m # Docker 기본 네트워킹
CONFIG_VXLAN=m # Flannel/Calico VXLAN 모드
CONFIG_GENEVE=m # OVN GENEVE 오버레이
CONFIG_MACVLAN=m # MACVLAN 네트워킹
CONFIG_IPVLAN=m # IPVLAN 네트워킹
CONFIG_VETH=y # 필수: veth 쌍
CONFIG_NETFILTER=y # iptables/nftables
CONFIG_NF_CONNTRACK=y # conntrack
CONFIG_OPENVSWITCH=m # OVS 사용 시
CONFIG_NET_CLS_BPF=m # TC BPF (Cilium 등)
고성능 네트워크 장비 (방화벽/로드밸런서):
CONFIG_BPF_SYSCALL=y # eBPF 필수
CONFIG_BPF_JIT=y # JIT 컴파일
CONFIG_XDP_SOCKETS=y # AF_XDP
CONFIG_NET_CLS_BPF=y # TC BPF
CONFIG_NETFILTER=y # Netfilter
CONFIG_NF_FLOW_TABLE=m # Flowtable 가속
CONFIG_NET_SCH_FQ=y # Fair Queueing (BBR용)
CONFIG_TCP_CONG_BBR=y # BBR 혼잡 제어
CONFIG_NET_ACT_CT=m # TC conntrack action
CONFIG_NET_ACT_POLICE=m # TC 폴리싱
네트워크 성능 튜닝 가이드
네트워크 성능 문제를 체계적으로 분석하고 튜닝하기 위한 핵심 매개변수와 관찰 도구를 정리합니다.
핵심 sysctl 매개변수
| 매개변수 | 기본값 | 설명 | 권장 조정 |
|---|---|---|---|
net.core.rmem_max | 212992 | 소켓 수신 버퍼 최대 | 고대역폭: 16MB+ |
net.core.wmem_max | 212992 | 소켓 송신 버퍼 최대 | 고대역폭: 16MB+ |
net.ipv4.tcp_rmem | 4096 131072 6291456 | TCP 수신 버퍼 (min/default/max) | 고대역폭: max 증가 |
net.ipv4.tcp_wmem | 4096 16384 4194304 | TCP 송신 버퍼 (min/default/max) | 고대역폭: max 증가 |
net.core.netdev_max_backlog | 1000 | per-CPU 수신 백로그 큐 크기 | 고PPS: 5000~30000 |
net.core.netdev_budget | 300 | NAPI poll 총 budget | 고PPS: 600~1200 |
net.core.somaxconn | 4096 | listen() backlog 최대 | 고연결: 65535 |
net.ipv4.tcp_max_syn_backlog | 2048 | SYN 큐 최대 크기 | 고연결: 8192~65535 |
net.ipv4.tcp_tw_reuse | 2 | TIME_WAIT 소켓 재사용 | 고연결: 1 (활성화) |
net.ipv4.tcp_congestion_control | cubic | 혼잡 제어 알고리즘 | 환경별: bbr, cubic |
net.nf_conntrack_max | 262144 | conntrack 최대 항목 수 | 고연결: 1048576+ |
성능 관찰 도구
# 인터페이스 통계 (패킷 수, 바이트, 에러, 드롭)
ip -s link show eth0
ethtool -S eth0 | grep -E "rx_|tx_|drop|error"
# softirq 처리 통계 (드롭, 시간 초과, CPU 분포)
cat /proc/net/softnet_stat
# TCP 연결 상태 분포
ss -s # 요약 통계
ss -tnp # TCP 연결 목록
ss -ti # TCP 내부 정보 (cwnd, RTT, retrans)
# 소켓 메모리 사용량
cat /proc/net/sockstat
# conntrack 상태
conntrack -C # 현재 추적 수
conntrack -S # 통계 (drop, insert_failed 등)
# 패킷 드롭 추적 (perf/dropwatch)
perf record -g -e skb:kfree_skb # kfree_skb tracepoint
dropwatch -l kas # 드롭 위치 추적
일반적인 병목 포인트
| 증상 | 확인 방법 | 원인 | 해결 |
|---|---|---|---|
| RX 드롭 증가 | ethtool -S: rx_dropped | RX Ring Buffer 부족 | ethtool -G eth0 rx 4096 |
| softnet backlog 드롭 | /proc/net/softnet_stat 2번째 열 | netdev_max_backlog 부족 | sysctl 증가 또는 RPS 설정 |
| TCP 재전송 증가 | ss -ti: retrans | 네트워크 혼잡/패킷 손실 | 혼잡 제어 변경(BBR), MTU 조정 |
| conntrack 풀 | conntrack -C = max | 연결 추적 테이블 소진 | nf_conntrack_max 증가 |
| 소켓 버퍼 부족 | /proc/net/sockstat: mem | 수신/송신 버퍼 부족 | tcp_rmem/tcp_wmem max 증가 |
| 단일 CPU 과부하 | mpstat -P ALL | 인터럽트/softirq 집중 | RSS/RPS 설정, IRQ 분산 |
시나리오별 sysctl 튜닝 프로파일
웹 서버 (고연결 수):
# 고연결 웹 서버 최적화
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_fin_timeout=15
sysctl -w net.ipv4.tcp_keepalive_time=600
sysctl -w net.ipv4.tcp_keepalive_intvl=30
sysctl -w net.ipv4.tcp_keepalive_probes=5
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
고대역폭 파일 전송 서버:
# 대용량 파일 전송 최적화 (10Gbps+)
sysctl -w net.core.rmem_max=67108864
sysctl -w net.core.wmem_max=67108864
sysctl -w net.ipv4.tcp_rmem="4096 87380 67108864"
sysctl -w net.ipv4.tcp_wmem="4096 65536 67108864"
sysctl -w net.ipv4.tcp_congestion_control=bbr
sysctl -w net.ipv4.tcp_mtu_probing=1
sysctl -w net.core.default_qdisc=fq
고PPS 패킷 처리 (방화벽/로드밸런서):
# 고PPS 환경 최적화
sysctl -w net.core.netdev_max_backlog=30000
sysctl -w net.core.netdev_budget=600
sysctl -w net.core.netdev_budget_usecs=8000
sysctl -w net.nf_conntrack_max=2097152
sysctl -w net.netfilter.nf_conntrack_buckets=524288
sysctl -w net.ipv4.tcp_max_orphans=262144
네트워크 성능 벤치마크 도구
| 도구 | 측정 항목 | 용도 | 사용 예 |
|---|---|---|---|
iperf3 | TCP/UDP 대역폭 | 기본 대역폭 측정 | iperf3 -s / iperf3 -c host |
netperf | TCP/UDP 대역폭, 지연 | 다양한 네트워크 벤치마크 | netperf -H host -t TCP_RR |
nuttcp | TCP/UDP 대역폭 | UDP 멀티캐스트, 양방향 테스트 | nuttcp -S / nuttcp host |
pktgen | PPS (패킷/초) | 커널 내장 패킷 생성기, 최대 PPS 측정 | modprobe pktgen |
wrk | HTTP RPS, 지연 | HTTP 벤치마크 | wrk -t4 -c100 -d30s url |
sockperf | 소켓 지연 | 마이크로초 단위 지연 측정 | sockperf sr --tcp -p 12345 |
xdp-bench | XDP PPS | XDP 드롭/리다이렉트 성능 | xdp-bench drop eth0 |
# iperf3 TCP 대역폭 측정
iperf3 -s # 서버
iperf3 -c 10.0.0.1 -t 30 -P 4 # 클라이언트: 30초, 4 스트림
# netperf TCP Request/Response 지연 측정
netperf -H 10.0.0.1 -t TCP_RR -l 30 -- -o min_latency,max_latency,mean_latency
# 커널 pktgen으로 최대 PPS 측정
echo "rem_device_all" > /proc/net/pktgen/kpktgend_0
echo "add_device eth0" > /proc/net/pktgen/kpktgend_0
echo "pkt_size 64" > /proc/net/pktgen/eth0
echo "count 10000000" > /proc/net/pktgen/eth0
echo "dst 10.0.0.2" > /proc/net/pktgen/eth0
echo "dst_mac aa:bb:cc:dd:ee:ff" > /proc/net/pktgen/eth0
echo "start" > /proc/net/pktgen/pgctrl
cat /proc/net/pktgen/eth0
실무 트러블슈팅 체크리스트
네트워크 문제를 체계적으로 진단하기 위한 단계별 체크리스트입니다:
- 물리 계층 확인 —
ethtool eth0으로 링크 상태, 속도, 듀플렉스 확인.ethtool -S eth0으로 CRC 에러, alignment 에러 확인. - L2 확인 —
ip link show로 인터페이스 상태, MTU, MAC 주소 확인.bridge fdb show로 브릿지 FDB 확인.ip neigh show로 ARP/NDP 캐시 확인. - L3 확인 —
ip route get [대상IP]로 라우팅 결정 확인.traceroute또는mtr로 경로 추적.ping으로 연결성 확인. - L4 확인 —
ss -tnp으로 TCP 연결 상태 확인.ss -ti로 cwnd, RTT, 재전송 확인.nstat으로 TCP/IP 통계 확인. - 방화벽 확인 —
nft list ruleset또는iptables -L -n -v로 규칙 확인.conntrack -L로 연결 추적 상태 확인. - 드롭 추적 —
/proc/net/softnet_stat으로 softirq 드롭 확인.ethtool -S으로 NIC 드롭 확인.perf record -e skb:kfree_skb으로 커널 드롭 위치 추적. - 성능 프로파일링(Profiling) —
perf top으로 CPU 핫스팟 확인.mpstat -P ALL 1로 CPU별 부하 분포 확인.sar -n DEV 1로 인터페이스별 처리량 모니터링.
ethtool -S), (2) 커널 드롭 — backlog 초과, Netfilter DROP, 소켓 버퍼 초과 (/proc/net/softnet_stat, nstat), (3) 애플리케이션 드롭 — 소켓 버퍼에서 데이터를 빠르게 소비하지 못함 (ss -tnp의 Recv-Q). 각 드롭 유형에 따라 해결 방법이 다르므로 정확한 분류가 중요합니다.
nstat과 SNMP 카운터
nstat은 커널 네트워크 SNMP 카운터의 변화량을 표시하는 도구로, TCP 재전송, IP 단편화, ICMP 에러 등의 통계를 효율적으로 모니터링할 수 있습니다.
# nstat — 변화량 표시 (가장 유용)
nstat -a # 모든 카운터
nstat -az # 0인 카운터도 표시
nstat -s # 1회성 스냅샷
nstat TcpRetransSegs # 특정 카운터만
# 주요 TCP 관찰 카운터
# TcpRetransSegs — 재전송된 세그먼트 수
# TcpInErrs — 오류 수신 세그먼트
# TcpExtTCPLostRetransmit — SACK로 감지된 재전송 손실
# TcpExtTCPTimeouts — RTO 타임아웃 발생 횟수
# TcpExtListenDrops — accept queue 오버플로우
# TcpExtListenOverflows — SYN 큐 오버플로우
# TcpExtTCPBacklogDrop — backlog 큐 드롭
# 주요 IP 관찰 카운터
# IpInReceives — 수신 패킷 총수
# IpOutRequests — 송신 패킷 총수
# IpReasmFails — 재조립 실패
# IpFragFails — 단편화 실패 (DF 비트 등)
# 변화 추적 (1초 간격)
watch -n1 'nstat -s TcpRetransSegs TcpExtListenDrops TcpExtTCPTimeouts'
ss 명령어 고급 활용
# TCP 내부 정보 (cwnd, rtt, mss, 재전송 등)
ss -ti state established
# 전형적인 출력 해석:
# cubic wscale:7,7 rto:204 rtt:1.234/0.567 ato:40 mss:1448
# pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10 ssthresh:7
# bytes_sent:12345 bytes_acked:12345 bytes_received:67890
# segs_out:100 segs_in:200 data_segs_out:80 data_segs_in:150
# send 93.8Mbps lastsnd:42 lastrcv:42 lastack:42
# pacing_rate 187Mbps delivery_rate 93.8Mbps
# busy:5ms retrans:0/0 dsack_dups:0 rcv_rtt:1
# 특정 상태의 소켓만 필터링
ss -tnp state time-wait # TIME_WAIT 소켓
ss -tnp state close-wait # CLOSE_WAIT 소켓 (서버 문제 시)
ss -tnp state syn-recv # SYN_RECV (SYN flood 시 증가)
# 소켓 메모리 사용량 표시
ss -tnm state established
# skmem:(r0,rb131072,t0,tb46080,f0,w0,o0,bl0,d0)
# r=수신큐, rb=수신버퍼, t=송신큐, tb=송신버퍼
# f=forward_alloc, w=wmem_queued, o=opt_mem
# 특정 포트/프로세스 필터
ss -tnp 'sport == :80' # 소스 포트 80
ss -tnp 'dport == :443' # 목적지 포트 443
ss -tnp '( sport == :80 or sport == :443 )'
perf와 eBPF 기반 네트워크 프로파일링
# 네트워크 스택 CPU 프로파일링
perf top --sort comm,dso,symbol # 실시간 핫스팟 확인
# 네트워크 관련 함수 프로파일링
perf record -g -p $(pgrep nginx) -- sleep 30
perf report --sort comm,symbol | grep -E "tcp_|ip_|napi_|netif_"
# flamegraph 생성 (네트워크 스택 시각화)
perf script | stackcollapse-perf.pl | flamegraph.pl > net-flame.svg
# bpftrace: TCP 연결 지연 히스토그램
bpftrace -e 'kprobe:tcp_v4_connect { @start[tid] = nsecs; }
kretprobe:tcp_v4_connect /@start[tid]/ {
@connect_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# bpftrace: 소켓 수신 큐 깊이 모니터링
bpftrace -e 'kprobe:tcp_recvmsg {
$sk = (struct sock *)arg0;
@recv_q = hist($sk->sk_receive_queue.qlen);
}'
# BCC 도구 (bcc-tools 패키지)
tcplife -D # TCP 연결 수명/바이트 추적
tcpretrans # TCP 재전송 실시간 추적
tcpconnect # TCP 연결 시도 추적
tcpaccept # TCP accept 추적
tcpdrop # TCP 드롭 추적 (드롭 원인 포함)
tcpsynbl # SYN backlog 크기 추적
sar -n DEV 1으로 인터페이스 처리량 확인, (2) mpstat -P ALL 1로 CPU별 softirq/user 비율 확인, (3) /proc/net/softnet_stat으로 NAPI budget 초과/드롭 확인, (4) ethtool -S으로 NIC 에러 확인, (5) ss -ti로 TCP 재전송/cwnd 확인, (6) 필요시 perf/bpftrace로 커널 내부 프로파일링 수행. 이 순서로 진행하면 대부분의 네트워크 성능 문제를 체계적으로 분석할 수 있습니다.
고성능 패킷 처리와 주제
기본 네트워크 스택 성능이 요구 사항을 충족하지 못할 때 사용할 수 있는 고성능 기술과 주제를 정리합니다.
XDP (eXpress Data Path)
XDP는 NIC 드라이버 단에서 eBPF 프로그램을 실행하여 sk_buff 할당 전에 패킷을 처리합니다. XDP_DROP(드롭), XDP_TX(송신 반사), XDP_REDIRECT(다른 인터페이스/AF_XDP 소켓 전달), XDP_PASS(정상 스택 진행) 동작을 지원합니다. DDoS 방어, 로드밸런싱에서 수백만 PPS 처리가 가능합니다.
AF_XDP
AF_XDP 소켓은 XDP와 유저 공간을 공유 메모리(UMEM)로 연결하여 커널 바이패스에 가까운 성능을 제공합니다. DPDK와 유사한 성능이지만 커널 보안 모델을 유지합니다. AF_XDP의 핵심 구성 요소는 4개의 링 버퍼(Ring Buffer)입니다:
- FILL Ring — 유저 공간이 커널에 빈 UMEM 프레임 주소를 전달하여 수신 버퍼로 사용하도록 합니다.
- COMPLETION Ring — 커널이 송신 완료된 UMEM 프레임 주소를 유저 공간에 반환합니다.
- RX Ring — 커널이 수신된 패킷의 UMEM 프레임 주소와 길이를 유저 공간에 전달합니다.
- TX Ring — 유저 공간이 송신할 패킷의 UMEM 프레임 주소와 길이를 커널에 전달합니다.
/* AF_XDP 소켓 기본 설정 패턴 */
struct xsk_socket_config cfg = {
.rx_size = 4096,
.tx_size = 4096,
.libbpf_flags = XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD,
};
/* UMEM 설정 */
struct xsk_umem_config umem_cfg = {
.fill_size = 4096,
.comp_size = 4096,
.frame_size = 4096,
.frame_headroom = 0,
};
/* UMEM 생성 (4096 프레임 x 4096 바이트 = 16MB) */
xsk_umem__create(&umem, buffer, FRAME_NUM * FRAME_SIZE,
&fq, &cq, &umem_cfg);
/* AF_XDP 소켓 생성 */
xsk_socket__create(&xsk, "eth0", 0, umem,
&rx, &tx, &cfg);
AF_XDP 성능 최적화
/* AF_XDP RX 루프 — 고성능 패턴 */
while (1) {
unsigned int rcvd, i;
u32 idx_rx = 0, idx_fq = 0;
/* 1. RX 링에서 수신 패킷 확인 */
rcvd = xsk_ring_cons__peek(&xsk->rx, BATCH_SIZE, &idx_rx);
if (!rcvd) {
/* busy poll 또는 poll()/epoll() 대기 */
poll(fds, 1, 1000);
continue;
}
/* 2. FILL 링에 빈 프레임 보충 */
xsk_ring_prod__reserve(&xsk->umem->fq, rcvd, &idx_fq);
for (i = 0; i < rcvd; i++) {
u64 addr = xsk_ring_cons__rx_desc(&xsk->rx, idx_rx)->addr;
u32 len = xsk_ring_cons__rx_desc(&xsk->rx, idx_rx)->len;
/* 3. 패킷 데이터 접근 (UMEM + addr) */
void *pkt = xsk_umem__get_data(xsk->umem->buffer, addr);
process_packet(pkt, len);
/* FILL 링에 프레임 반환 */
*xsk_ring_prod__fill_addr(&xsk->umem->fq, idx_fq++) = addr;
idx_rx++;
}
xsk_ring_prod__submit(&xsk->umem->fq, rcvd);
xsk_ring_cons__release(&xsk->rx, rcvd);
}
코드 설명
- 7행
xsk_ring_cons__peek()는 RX 링에서 사용 가능한 패킷 수를 확인합니다. 배치 처리를 위해 최대BATCH_SIZE만큼 한 번에 가져옵니다. - 15행FILL 링에 빈 UMEM 프레임을 보충합니다. 커널은 FILL 링에서 프레임을 가져와 다음 수신 패킷에 사용합니다.
- 21행
xsk_umem__get_data()는 UMEM 베이스 주소와 프레임 오프셋으로 패킷 데이터 포인터를 계산합니다. 커널-유저 간 데이터 복사가 없습니다. - 28-29행처리 완료 후 FILL 링과 RX 링의 인덱스를 업데이트합니다. 커널은 이 인덱스를 확인하여 프레임 재활용과 새 패킷 수신을 진행합니다.
# AF_XDP 성능 측정
# xdp-bench를 이용한 AF_XDP 수신 성능 테스트
xdp-bench rxdrop -d eth0 -M af_xdp
# AF_XDP 소켓 상태 확인
bpftool map show # XSK_MAP 확인
# AF_XDP 최적화 설정
# 1. NIC 큐와 XSK 1:1 바인딩 (전용 모드)
# 2. XDP_FLAGS_DRV_MODE (네이티브 XDP)
# 3. zero-copy 모드 (XDP_ZEROCOPY)
# 4. NUMA 친화 메모리 할당
# 5. CPU 고정 (taskset/isolcpus)
DPDK (Data Plane Development Kit)
DPDK는 커널 네트워크 스택을 완전히 우회하는 유저 공간 패킷 처리 프레임워크입니다. PMD(Poll Mode Driver)가 전용 CPU 코어에서 NIC 큐를 직접 폴링하여 인터럽트, 컨텍스트 스위칭(Context Switching), 커널 오버헤드를 완전히 제거합니다. 통신사, 금융 거래, NFV(Network Functions Virtualization) 환경에서 사용됩니다.
| 특성 | AF_XDP | DPDK |
|---|---|---|
| 커널 관여 | 최소 (XDP 연동) | 없음 (완전 바이패스) |
| 보안 모델 | 커널이 UMEM 관리 | 커널 보안 미적용 |
| NIC 지원 | XDP 지원 NIC | DPDK PMD 지원 NIC |
| 배포 복잡도 | 낮음 (커널 내장) | 높음 (별도 라이브러리/드라이버) |
| Hugepage | 선택적 | 필수 |
| CPU 전용 할당 | 선택적 | 권장 (isolcpus) |
| 성능 (64B PPS) | ~25Mpps/core | ~35Mpps/core |
| 생태계 | libbpf, 커널 도구 | VPP, OVS-DPDK, SPDK |
커널 바이패스 기술 비교
| 기술 | 커널 관여도 | 프로그래밍 모델 | 성능 | 보안 모델 |
|---|---|---|---|---|
| 일반 소켓 | 전체 스택 | BSD 소켓 API | 기준선 | 커널 보안 완전 적용 |
| XDP | 드라이버 단 eBPF | eBPF 프로그램 | 10-20x | eBPF 검증기 |
| AF_XDP | 최소 (공유 메모리) | UMEM ring | 50-100x | 커널 관리 UMEM |
| DPDK | 없음 (완전 바이패스) | PMD 폴링 | 최대 | 커널 보안 미적용 |
기술 선택 가이드
네트워크 요구사항에 따라 적절한 기술을 선택하는 것이 중요합니다. 아래는 일반적인 시나리오별 권장 기술 스택입니다:
| 시나리오 | PPS 요구 | 지연 요구 | 권장 기술 | 대안 |
|---|---|---|---|---|
| 일반 웹 서버 | < 100K | < 1ms | 기본 스택 + BBR | epoll + 소켓 튜닝 |
| 리버스 프록시 | < 1M | < 100us | NAPI 튜닝 + RPS | SO_REUSEPORT |
| 소프트웨어 방화벽 | 1-10M | < 50us | nftables + Flowtable | XDP + eBPF |
| L4 로드밸런서 | 1-50M | < 10us | XDP + eBPF | IPVS + Maglev |
| DDoS 완화 | > 10M | < 1us | XDP_DROP | NIC flow director |
| 패킷 캡처/분석 | 1-100M | 비실시간 | AF_XDP + UMEM | PF_RING, AF_PACKET v3 |
| 통신사 NFV | > 50M | < 5us | DPDK + VPP | AF_XDP |
| 금융 거래 | < 1M | < 1us | DPDK/RDMA kernel bypass | Solarflare OpenOnload |
eBPF 네트워킹 프로그램 유형
eBPF는 네트워크 스택의 다양한 지점에 프로그램을 부착할 수 있으며, 각 유형은 고유한 입력 컨텍스트와 동작을 가집니다.
| 프로그램 유형 | 부착 지점 | 입력 컨텍스트 | 성능 수준 | 사용 예 |
|---|---|---|---|---|
BPF_PROG_TYPE_XDP | NIC 드라이버 | xdp_md (raw 패킷) | 최고 (sk_buff 전) | DDoS 방어, LB |
BPF_PROG_TYPE_SCHED_CLS | TC ingress/egress | __sk_buff | 높음 | 패킷 분류/수정 |
BPF_PROG_TYPE_SOCK_OPS | TCP 이벤트 | bpf_sock_ops | 보통 | TCP 튜닝 |
BPF_PROG_TYPE_SK_SKB | 소켓 데이터 | __sk_buff | 보통 | sockmap 리다이렉트 |
BPF_PROG_TYPE_CGROUP_SKB | cgroup 소켓 | __sk_buff | 보통 | 컨테이너 정책 |
BPF_PROG_TYPE_SK_REUSEPORT | SO_REUSEPORT | sk_reuseport_md | 높음 | 커스텀 LB |
BPF_PROG_TYPE_FLOW_DISSECTOR | 플로우 파싱 | __sk_buff | 높음 | 커스텀 해시 |
BPF_PROG_TYPE_LWT_* | 라우팅 (LWT) | __sk_buff | 보통 | 패킷 캡슐화 |
/* TC BPF 프로그램 예시: 패킷 리다이렉트 */
SEC("tc")
int tc_redirect(struct __sk_buff *skb)
{
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
/* MAC 주소 교환 */
unsigned char tmp[ETH_ALEN];
__builtin_memcpy(tmp, eth->h_source, ETH_ALEN);
__builtin_memcpy(eth->h_source, eth->h_dest, ETH_ALEN);
__builtin_memcpy(eth->h_dest, tmp, ETH_ALEN);
/* 다른 인터페이스로 리다이렉트 */
return bpf_redirect(ifindex_peer, 0);
}
/* SOCK_OPS BPF: TCP 옵션 튜닝 */
SEC("sockops")
int bpf_sockops(struct bpf_sock_ops *skops)
{
switch (skops->op) {
case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB:
/* 연결 수립 시 혼잡 제어 변경 */
bpf_setsockopt(skops, SOL_TCP, TCP_CONGESTION,
"bbr", 4);
break;
case BPF_SOCK_OPS_RTO_CB:
/* RTO 이벤트 로깅 */
break;
}
return 1;
}
코드 설명
- 2-20행TC BPF 프로그램은
__sk_buff컨텍스트로 패킷에 접근합니다. XDP보다 상위 계층에서 동작하므로 L3/L4 헤더 파싱이 완료된 상태이며,bpf_redirect()로 다른 인터페이스로 패킷을 전달할 수 있습니다. - 23-36행SOCK_OPS 프로그램은 TCP 이벤트(연결 수립, RTO, ECN 등)에 반응합니다.
bpf_setsockopt()로 소켓 옵션을 동적으로 변경할 수 있어, 연결 대상에 따라 혼잡 제어를 자동으로 선택하는 등의 정책을 구현할 수 있습니다.
sockmap과 소켓 리다이렉트
sockmap은 eBPF 맵을 이용하여 소켓 간 데이터를 커널 내부에서 직접 전달하는 메커니즘입니다. 프록시 서버에서 유저 공간으로의 데이터 복사를 제거하여 처리량과 지연을 크게 개선합니다.
/* sockmap 기반 L7 프록시 가속 */
/* BPF 맵: 소켓 쌍 저장 */
struct {
__uint(type, BPF_MAP_TYPE_SOCKHASH);
__uint(max_entries, 65536);
__type(key, struct sock_key);
__type(value, int);
} sock_map SEC(".maps");
/* SK_SKB 프로그램: 소켓 데이터 리다이렉트 */
SEC("sk_skb/stream_verdict")
int bpf_prog_verdict(struct __sk_buff *skb)
{
struct sock_key key = {};
key.sip = skb->remote_ip4;
key.dport = skb->remote_port;
/* 대상 소켓을 sockmap에서 찾아 리다이렉트 */
return bpf_sk_redirect_hash(skb, &sock_map, &key, BPF_F_INGRESS);
/* 유저 공간 복사 없이 소켓→소켓 직접 전달 */
/* 프록시 throughput 3-5배 향상 */
}
코드 설명
- 4-9행
BPF_MAP_TYPE_SOCKHASH는 소켓 참조를 해시 맵에 저장합니다. 프록시가 클라이언트-업스트림 소켓 쌍을 이 맵에 등록합니다. - 20행
bpf_sk_redirect_hash()는 수신된 데이터를 sockmap에서 찾은 대상 소켓으로 직접 전달합니다. 유저 공간의recv()+send()루프를 완전히 제거합니다.
bpf_sk_msg_verdict 프로그램으로 트래픽 정책도 동시에 적용할 수 있습니다.
추가 주제:
- 멀티코어 패킷 분산 — RSS/RPS/RFS/XPS, per-CPU backlog, flow dissector: 네트워크 패킷 흐름 & 디버깅
- 패킷 드롭 디버깅 — 큐 적체, reorder, conntrack 오버헤드 분석: 네트워크 패킷 흐름 & 디버깅
- 네트워크 공격 방어 — SYN/UDP/ICMP Flood, syncookie, rate limiting: 네트워크 공격 방어
- BPF/eBPF/XDP — 프로그래머블 패킷 처리: BPF/eBPF/XDP
- AF_XDP — 유저 공간 고성능 패킷 처리: AF_XDP
- DPDK — 완전 커널 바이패스, PMD 폴링: DPDK
- SmartNIC/DPU — NIC 하드웨어 오프로드: SmartNIC/DPU
- InfiniBand/RDMA — 저지연 네트워킹, 원격 메모리 접근: InfiniBand/RDMA
커널 버전별 주요 네트워킹 변경 사항
| 커널 버전 | 주요 변경 | 영향 |
|---|---|---|
| 4.8+ | XDP 초기 지원 | 고성능 패킷 처리의 새 시대 |
| 4.18+ | AF_XDP 소켓 | 유저 공간 고성능 패킷 I/O |
| 5.0+ | BPF 트램펄린 | BPF-to-BPF 호출, 확장된 프로그래밍 |
| 5.1+ | Netfilter Flowtable | conntrack 가속, ESTABLISHED 빠른 경로 |
| 5.4+ | MPTCP v0, BBRv2 (실험) | 멀티패스 TCP, 개선된 혼잡 제어 |
| 5.6+ | WireGuard 메인라인 | 현대적 VPN 프로토콜 기본 지원 |
| 5.8+ | devlink 포트 분리/합병 | SmartNIC eSwitch 세밀 제어 |
| 5.10+ | MPTCP 안정화 | 프로덕션 멀티패스 TCP |
| 5.15+ | page_pool 성능 개선 | RX 경로 메모리 할당 최적화 |
| 6.0+ | BIG TCP (IPv6 GRO) | 512KB+ 이상 GRO 패킷 지원 |
| 6.1+ | 네트워크 다중 큐 개선 | NAPI 스레딩, 더 나은 멀티코어 스케일링 |
| 6.6+ | netdev GenL API | ethtool/devlink 통합 Netlink API |
핵심 함수 빠른 참조
네트워크 스택의 주요 함수를 경로별로 정리합니다. 커널 소스 분석이나 ftrace/kprobe 설정 시 참고할 수 있습니다.
RX 경로 (NIC → 유저 공간)
| 단계 | 함수 | 소스 파일 | 역할 |
|---|---|---|---|
| IRQ | napi_schedule() | include/linux/netdevice.h | NAPI 폴링 예약 |
| SoftIRQ | net_rx_action() | net/core/dev.c | NAPI poll 루프 실행 |
| NAPI | napi_gro_receive() | net/core/gro.c | GRO 병합 시도 |
| L2 | __netif_receive_skb() | net/core/dev.c | 프로토콜 디멀티플렉싱 |
| L3 | ip_rcv() | net/ipv4/ip_input.c | IP 헤더 검증 |
| L3 | ip_rcv_finish() | net/ipv4/ip_input.c | FIB 조회, early demux |
| L3 | ip_local_deliver() | net/ipv4/ip_input.c | 로컬 패킷 L4 전달 |
| L4 (TCP) | tcp_v4_rcv() | net/ipv4/tcp_ipv4.c | TCP 수신 진입점 |
| L4 (TCP) | tcp_rcv_established() | net/ipv4/tcp_input.c | Fast/Slow path 분기 |
| L4 (UDP) | udp_rcv() | net/ipv4/udp.c | UDP 수신 진입점 |
| 소켓 | sock_queue_rcv_skb() | net/core/sock.c | 소켓 수신 큐 삽입 |
| 소켓 | sk->sk_data_ready() | net/core/sock.c | 대기 프로세스 깨움 |
TX 경로 (유저 공간 → NIC)
| 단계 | 함수 | 소스 파일 | 역할 |
|---|---|---|---|
| 소켓 | sock_sendmsg() | net/socket.c | 소켓 계층 진입 |
| L4 (TCP) | tcp_sendmsg() | net/ipv4/tcp.c | TCP 데이터 큐잉 |
| L4 (TCP) | tcp_write_xmit() | net/ipv4/tcp_output.c | cwnd/Nagle 확인, 전송 결정 |
| L4 (UDP) | udp_sendmsg() | net/ipv4/udp.c | UDP 즉시 전달 |
| L3 | ip_queue_xmit() | net/ipv4/ip_output.c | IP 헤더, 라우팅 |
| L3 | ip_output() | net/ipv4/ip_output.c | Netfilter POSTROUTING |
| L2.5 | __dev_queue_xmit() | net/core/dev.c | qdisc enqueue/dequeue |
| L2 | dev_hard_start_xmit() | net/core/dev.c | GSO 처리, 드라이버 호출 |
| 드라이버 | ndo_start_xmit() | 드라이버별 | DMA 매핑, TX Ring |
Netfilter 훅 경로
| 훅 | 호출 시점 | 핵심 함수 | 주요 용도 |
|---|---|---|---|
| PRE_ROUTING | ip_rcv() 내부 | NF_HOOK(NF_INET_PRE_ROUTING) | DNAT, conntrack 생성 |
| LOCAL_IN | ip_local_deliver() | NF_HOOK(NF_INET_LOCAL_IN) | 입력 필터링 |
| FORWARD | ip_forward() | NF_HOOK(NF_INET_FORWARD) | 포워딩 필터링 |
| LOCAL_OUT | __ip_local_out() | NF_HOOK(NF_INET_LOCAL_OUT) | 출력 필터링 |
| POST_ROUTING | ip_output() | NF_HOOK(NF_INET_POST_ROUTING) | SNAT/Masquerade |
핵심 소스 파일 맵
| 경로 | 내용 | 주요 함수 |
|---|---|---|
net/core/dev.c | 네트워크 디바이스 핵심 | netif_receive_skb, __dev_queue_xmit |
net/core/skbuff.c | sk_buff 관리 | alloc_skb, kfree_skb, skb_clone |
net/core/sock.c | 소켓 핵심 | sock_sendmsg, sock_recvmsg |
net/core/gro.c | GRO 프레임워크 | dev_gro_receive, napi_gro_receive |
net/ipv4/ip_input.c | IPv4 수신 | ip_rcv, ip_rcv_finish |
net/ipv4/ip_output.c | IPv4 송신 | ip_queue_xmit, ip_output, ip_fragment |
net/ipv4/tcp_input.c | TCP 수신 | tcp_rcv_established, tcp_data_queue |
net/ipv4/tcp_output.c | TCP 송신 | tcp_write_xmit, tcp_transmit_skb |
net/ipv4/tcp_ipv4.c | TCP IPv4 연동 | tcp_v4_rcv, tcp_v4_connect |
net/ipv4/udp.c | UDP 구현 | udp_sendmsg, udp_rcv |
net/ipv4/route.c | IPv4 라우팅 | ip_route_input, ip_route_output |
net/ipv4/fib_trie.c | FIB LC-trie | fib_table_lookup |
net/netfilter/core.c | Netfilter 핵심 | nf_hook_slow, nf_register_net_hook |
net/netfilter/nf_conntrack_core.c | 연결 추적 | nf_conntrack_in, resolve_normal_ct |
net/sched/sch_generic.c | qdisc 프레임워크 | __qdisc_run, qdisc_restart |
용어 정리
| 약어/용어 | 전체 이름 | 설명 |
|---|---|---|
| NAPI | New API | 인터럽트/폴링 하이브리드 수신 프레임워크 |
| GRO | Generic Receive Offload | 수신 패킷 병합 (소프트웨어) |
| GSO | Generic Segmentation Offload | 대형 패킷 세그먼테이션 (소프트웨어) |
| TSO | TCP Segmentation Offload | NIC 하드웨어 세그먼테이션 |
| BQL | Byte Queue Limits | TX 큐 바이트 제한 (bufferbloat 방지) |
| RSS | Receive Side Scaling | NIC 하드웨어 RX 큐 분배 |
| RPS | Receive Packet Steering | 소프트웨어 RX CPU 분배 |
| RFS | Receive Flow Steering | 소켓 CPU 친화성 기반 분배 |
| XPS | Transmit Packet Steering | CPU-TX 큐 매핑 |
| FIB | Forwarding Information Base | 라우팅 테이블 (LC-trie) |
| LPM | Longest Prefix Match | 라우팅 조회 알고리즘 |
| NUD | Neighbor Unreachability Detection | ARP/NDP 이웃 상태 머신 |
| cwnd | Congestion Window | TCP 혼잡 윈도우 |
| rwnd | Receive Window | TCP 수신 윈도우 |
| RTT | Round Trip Time | 패킷 왕복 시간 |
| RTO | Retransmission Timeout | 재전송 타임아웃 |
| SRTT | Smoothed Round Trip Time | 평활화된 RTT |
| BDP | Bandwidth-Delay Product | 대역폭 × 지연 (필요 버퍼 크기) |
| MSS | Maximum Segment Size | TCP 최대 세그먼트 크기 |
| MTU | Maximum Transmission Unit | 인터페이스 최대 전송 단위 |
| PPS | Packets Per Second | 초당 패킷 처리 수 |
| PMTUD | Path MTU Discovery | 경로 MTU 탐색 |
| TFO | TCP Fast Open | 0-RTT TCP 핸드셰이크 |
| TLP | Tail Loss Probe | 꼬리 손실 프로브 (빠른 손실 감지) |
| RACK | Recent ACKnowledgment | 시간 기반 손실 감지 |
| ECN | Explicit Congestion Notification | 명시적 혼잡 알림 |
| SACK | Selective ACKnowledgment | 선택적 확인 응답 |
| kTLS | Kernel TLS | 커널 내 TLS 레코드 암호화 |
| ULP | Upper Layer Protocol | TCP 상위 계층 프로토콜 플러그인 |
| XDP | eXpress Data Path | 드라이버 단 eBPF 패킷 처리 |
| UMEM | User Memory | AF_XDP 공유 메모리 영역 |
| PMD | Poll Mode Driver | DPDK 폴링 기반 드라이버 |
| VRF | Virtual Routing and Forwarding | 가상 라우팅 도메인 |
하위 문서 네비게이션 가이드
이 페이지(Page)는 네트워크 스택의 전체 구조를 조감하는 게이트웨이 문서입니다. 아래 가이드는 학습 목적과 역할에 따라 어떤 하위 문서를 읽어야 하는지 안내합니다.
문서 활용 팁
이 사이트의 네트워크 관련 문서는 약 35개 페이지로 구성되어 있습니다. 효율적으로 활용하려면 다음을 참고하세요:
- 처음 접하는 개념 — 이 개요 문서를 전체적으로 읽고, 관심 분야의 기술 문서로 이동하세요.
- 특정 문제 디버깅 — 패킷 흐름 & 디버깅 문서에서 증상별 진단 방법을 찾으세요.
- 성능 최적화 — 이 문서의 성능 튜닝 섹션에서 시작하여, 해당 계층의 기술 문서를 참조하세요.
- 커널 소스 분석 — 소스 디렉토리 구조 섹션의 핵심 파일 목록에서 시작하세요.
- 고성능 요구사항 — 고급 주제 섹션에서 기술 비교표를 확인한 후 해당 문서로 이동하세요.
역할별 권장 학습 순서
| 역할 | 1순위 | 2순위 | 3순위 |
|---|---|---|---|
| NIC 드라이버 개발자 | sk_buff, NAPI, net_device 드라이버 | GSO/GRO, ethtool | XDP, AF_XDP |
| 프로토콜 개발자 | IP, TCP, UDP | 소켓 계층, sk_buff | SCTP, kTLS |
| 네트워크 관리자 | 라우팅, Netfilter | TC, 네트워크 네임스페이스 | 패킷 흐름/디버깅, ethtool |
| 보안 엔지니어 | Netfilter, conntrack | 네트워크 공격 방어, IPSec/xfrm | eBPF 보안 정책 |
| 성능 엔지니어 | 패킷 흐름/디버깅, ethtool | BPF/XDP, AF_XDP | DPDK, SmartNIC/DPU |
| 컨테이너/클라우드 | 네트워크 네임스페이스, VXLAN/GENEVE | Bridge, Bonding/MACVLAN | OVS, eBPF, VRF |
관련 문서
이 페이지는 Linux 네트워크 스택의 전체 아키텍처를 조감하는 게이트웨이 문서입니다. 각 주제에 대해 독립적인 기술 문서가 준비되어 있으며, 해당 문서에서 커널 소스 코드 수준의 상세한 설명, 성능 벤치마크, 실전 트러블슈팅 사례를 다룹니다. 학습 순서는 위의 하위 문서 네비게이션 가이드를 참고하세요.
핵심 자료구조 & 수신 경로
L2 계층
L3/L4 프로토콜
인프라 & 제어 평면
가상 네트워크 & 오버레이
고성능 네트워킹
네트워크 보안
Documentation/networking/ 디렉토리에 있습니다.
특히 scaling.txt(RSS/RPS/RFS/XPS), ip-sysctl.txt(sysctl 매개변수),
af_xdp.rst(AF_XDP API)는 필수 참고 문서입니다.
커뮤니티 리소스로는 kernel.org 네트워킹 문서,
netdev 메일링 리스트,
Brendan Gregg의 네트워크 분석 자료가 유용합니다.