AF_XDP (XDP Sockets)
AF_XDP는 XDP와 결합하여 커널 우회(kernel bypass) 없이 userspace로 초고속 패킷 전달을 제공하는 소켓 패밀리입니다. Zero-copy 모드에서 수백만 pps의 처리 성능을 달성합니다. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.
핵심 요약
- UMEM — 사용자 공간 공유 패킷 메모리
- RX/TX/FILL/COMP Ring — lockless 큐 기반 데이터 흐름
- Zero-copy — 드라이버 지원 시 최고 성능 경로
- XDP redirect — 패킷 선별/전달 엔트리 포인트
- 큐 핀ning — CPU/NIC queue affinity가 핵심 튜닝 요소
단계별 이해
- UMEM 준비
frame size와 ring depth를 워크로드 특성에 맞춰 결정합니다. - XDP 연결
프로그램에서 대상 패킷을 AF_XDP 소켓으로 redirect합니다. - 모드 선택
copy/zero-copy 성능 차이와 드라이버 제약을 검증합니다. - 운영 튜닝
IRQ affinity, NAPI budget, batching 전략을 조정합니다.
개요
AF_XDP는 XDP 프로그램과 협력하여 선택된 패킷을 userspace 애플리케이션으로 직접 전달합니다. DPDK처럼 커널을 완전히 우회하지 않고도 높은 성능을 얻을 수 있습니다.
AF_XDP 패킷 생애주기
주요 특징
- Zero-Copy — 드라이버가 지원하면 메모리 복사 없이 패킷 전달
- Kernel Integration — 일반 네트워크 스택과 공존 가능
- Per-Queue Sockets — 각 NIC RX/TX 큐마다 독립적인 소켓
- Low Latency — 수십 마이크로초 레이턴시
- Flexible Filtering — XDP 프로그램으로 패킷 선택
아키텍처
UMEM (User Memory)
UMEM은 패킷 데이터를 저장하는 userspace 메모리 영역입니다. 커널과 userspace가 공유합니다.
UMEM 구조
#include <linux/if_xdp.h>
#include <bpf/xsk.h>
struct xsk_umem_config {
__u32 fill_size; /* Fill ring 크기 */
__u32 comp_size; /* Completion ring 크기 */
__u32 frame_size; /* 각 프레임 크기 (2048 or 4096) */
__u32 frame_headroom; /* 프레임 헤드룸 (256) */
__u32 flags; /* XDP_UMEM_UNALIGNED_CHUNK_FLAG */
};
/* UMEM 할당 및 등록 */
void *umem_area;
size_t umem_size = NUM_FRAMES * FRAME_SIZE;
/* Hugepage 사용 권장 (성능 향상) */
umem_area = mmap(NULL, umem_size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);
struct xsk_umem_config umem_config = {
.fill_size = XSK_RING_PROD__DEFAULT_NUM_DESCS,
.comp_size = XSK_RING_CONS__DEFAULT_NUM_DESCS,
.frame_size = FRAME_SIZE,
.frame_headroom = XSK_UMEM__DEFAULT_FRAME_HEADROOM,
.flags = 0,
};
struct xsk_umem *umem;
int ret = xsk_umem__create(&umem, umem_area, umem_size,
&fill_ring, &comp_ring, &umem_config);
UMEM 프레임 레이아웃
AF_XDP Rings
AF_XDP는 4개의 링(Ring)을 사용하여 패킷을 주고받습니다.
Ring 종류
| Ring | 방향 | 역할 |
|---|---|---|
| RX Ring | Kernel → User | 수신 패킷 전달 (CONSUMER) |
| TX Ring | User → Kernel | 송신 패킷 제출 (PRODUCER) |
| Fill Ring | User → Kernel | 빈 프레임 제공 (RX용) |
| Completion Ring | Kernel → User | 송신 완료 프레임 반환 |
Ring 동작 흐름
/* RX 경로 */
1. User: Fill Ring에 빈 프레임 주소 추가
2. Kernel: 패킷 수신 시 Fill Ring에서 프레임 가져옴
3. Kernel: 패킷 복사 후 RX Ring에 프레임 주소 추가
4. User: RX Ring에서 패킷 처리
/* TX 경로 */
1. User: TX Ring에 패킷 프레임 주소 추가
2. Kernel: TX Ring에서 프레임 가져와 송신
3. Kernel: 송신 완료 후 Completion Ring에 프레임 주소 추가
4. User: Completion Ring에서 프레임 재사용
수신(RX) 데이터 흐름 상세
송신(TX) 데이터 흐름 상세
AF_XDP 소켓 생성
xsk_socket 생성 (libbpf)
#include <bpf/xsk.h>
struct xsk_socket_config {
__u32 rx_size; /* RX ring 크기 */
__u32 tx_size; /* TX ring 크기 */
__u32 libbpf_flags; /* XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD */
__u32 xdp_flags; /* XDP_FLAGS_* */
__u16 bind_flags; /* XDP_COPY, XDP_ZEROCOPY, XDP_USE_NEED_WAKEUP */
};
/* AF_XDP 소켓 생성 */
struct xsk_socket *xsk;
struct xsk_socket_config cfg = {
.rx_size = XSK_RING_CONS__DEFAULT_NUM_DESCS,
.tx_size = XSK_RING_PROD__DEFAULT_NUM_DESCS,
.libbpf_flags = 0,
.xdp_flags = XDP_FLAGS_UPDATE_IF_NOEXIST,
.bind_flags = XDP_ZEROCOPY, /* or XDP_COPY */
};
int ret = xsk_socket__create(&xsk, ifname, queue_id, umem,
&rx_ring, &tx_ring, &cfg);
if (ret) {
fprintf(stderr, "Failed to create XSK socket: %d\\n", ret);
return ret;
}
/* 소켓 파일 디스크립터 획득 */
int xsk_fd = xsk_socket__fd(xsk);
Bind Flags
| 플래그 | 설명 |
|---|---|
XDP_COPY |
Copy 모드 (모든 드라이버 지원, 낮은 성능) |
XDP_ZEROCOPY |
Zero-copy 모드 (드라이버 지원 필요, 고성능) |
XDP_USE_NEED_WAKEUP |
Wakeup 플래그 사용 (CPU 사용률 감소) |
XDP_SHARED_UMEM |
여러 소켓이 동일 UMEM 공유 |
패킷 수신
RX 루프
/* Fill Ring에 빈 프레임 추가 */
void xsk_populate_fill_ring(struct xsk_ring_prod *fill, __u64 *frame_addr)
{
__u32 idx;
if (xsk_ring_prod__reserve(fill, BATCH_SIZE, &idx) == BATCH_SIZE) {
for (int i = 0; i < BATCH_SIZE; i++) {
*xsk_ring_prod__fill_addr(fill, idx++) = frame_addr[i];
}
xsk_ring_prod__submit(fill, BATCH_SIZE);
}
}
/* RX Ring에서 패킷 수신 */
void xsk_receive_packets(struct xsk_socket *xsk, struct xsk_ring_cons *rx)
{
__u32 idx_rx = 0;
unsigned int rcvd = xsk_ring_cons__peek(rx, BATCH_SIZE, &idx_rx);
for (unsigned int i = 0; i < rcvd; i++) {
const struct xdp_desc *desc = xsk_ring_cons__rx_desc(rx, idx_rx++);
__u64 addr = desc->addr;
__u32 len = desc->len;
void *pkt = xsk_umem__get_data(umem_area, addr);
/* 패킷 처리 */
process_packet(pkt, len);
/* 프레임을 Fill Ring에 반환 (재사용) */
xsk_populate_fill_ring(&fill_ring, &addr);
}
xsk_ring_cons__release(rx, rcvd);
}
poll()과 통합
#include <poll.h>
/* AF_XDP 소켓은 poll() 가능 */
struct pollfd fds = {
.fd = xsk_socket__fd(xsk),
.events = POLLIN,
};
while (1) {
int ret = poll(&fds, 1, -1);
if (ret > 0 && fds.revents & POLLIN) {
xsk_receive_packets(xsk, &rx_ring);
}
}
패킷 송신
TX 루프
/* 패킷 송신 */
void xsk_send_packet(struct xsk_socket *xsk, struct xsk_ring_prod *tx,
void *pkt_data, size_t pkt_len, __u64 frame_addr)
{
__u32 idx;
if (xsk_ring_prod__reserve(tx, 1, &idx) == 1) {
struct xdp_desc *desc = xsk_ring_prod__tx_desc(tx, idx);
/* 패킷 데이터 복사 */
void *frame = xsk_umem__get_data(umem_area, frame_addr);
memcpy(frame, pkt_data, pkt_len);
/* Descriptor 설정 */
desc->addr = frame_addr;
desc->len = pkt_len;
xsk_ring_prod__submit(tx, 1);
/* Kernel에 송신 시작 알림 (XDP_USE_NEED_WAKEUP 사용 시) */
if (xsk_ring_prod__needs_wakeup(tx))
sendto(xsk_socket__fd(xsk), NULL, 0, MSG_DONTWAIT, NULL, 0);
}
}
/* Completion Ring에서 송신 완료 프레임 회수 */
void xsk_complete_tx(struct xsk_ring_cons *comp)
{
__u32 idx;
unsigned int completed = xsk_ring_cons__peek(comp, BATCH_SIZE, &idx);
if (completed > 0) {
xsk_ring_cons__release(comp, completed);
/* 회수된 프레임 재사용 */
}
}
XDP 프로그램 연동
XDP_REDIRECT to AF_XDP
/* XDP 프로그램: 특정 패킷을 AF_XDP 소켓으로 리다이렉트 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_XSKMAP);
__uint(key_size, sizeof(int));
__uint(value_size, sizeof(int));
__uint(max_entries, 64);
} xsks_map SEC(".maps");
SEC("xdp")
int xdp_sock_prog(struct xdp_md *ctx)
{
int index = ctx->rx_queue_index;
/* 패킷 필터링 로직 */
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;
/* UDP 포트 12345 패킷만 AF_XDP로 */
if (eth->h_proto == htons(ETH_P_IP)) {
struct iphdr *ip = (struct iphdr *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
if (ip->protocol == IPPROTO_UDP) {
struct udphdr *udp = (struct udphdr *)(ip + 1);
if ((void *)(udp + 1) > data_end)
return XDP_PASS;
if (ntohs(udp->dest) == 12345) {
/* AF_XDP 소켓으로 리다이렉트 */
return bpf_redirect_map(&xsks_map, index, 0);
}
}
}
/* 나머지 패킷은 일반 네트워크 스택으로 */
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
XSKMAP 업데이트
/* Userspace: XSKMAP에 AF_XDP 소켓 등록 */
int queue_id = 0;
int xsk_fd = xsk_socket__fd(xsk);
int map_fd; /* BPF_MAP_TYPE_XSKMAP의 fd */
bpf_map_update_elem(map_fd, &queue_id, &xsk_fd, 0);
Zero-Copy vs Copy 모드
모드 비교
| 특성 | Copy 모드 | Zero-Copy 모드 |
|---|---|---|
| 메모리 복사 | 커널 버퍼 → UMEM 복사 | 복사 없음 (직접 UMEM 사용) |
| 드라이버 지원 | 모든 드라이버 | i40e, ice, ixgbe, mlx5, bnxt, igc, veth, virtio_net, stmmac 등 |
| 성능 | ~수백만 pps | ~천만 pps 이상 |
| CPU 사용률 | 높음 | 낮음 |
| 레이턴시 | ~수십 μs | ~수 μs |
Zero-Copy 요구사항
/* Zero-copy 지원 드라이버 확인 */
$ ethtool -i eth0 | grep driver
driver: i40e
/* Zero-copy 모드 시도 */
struct xsk_socket_config cfg = {
.bind_flags = XDP_ZEROCOPY,
};
int ret = xsk_socket__create(&xsk, "eth0", 0, umem, &rx, &tx, &cfg);
if (ret == -EOPNOTSUPP) {
printf("Zero-copy not supported, falling back to copy mode\\n");
cfg.bind_flags = XDP_COPY;
ret = xsk_socket__create(&xsk, "eth0", 0, umem, &rx, &tx, &cfg);
}
Shared UMEM
여러 AF_XDP 소켓이 동일한 UMEM을 공유하여 메모리 효율을 높입니다. XDP_SHARED_UMEM 플래그를 사용하면 N개 큐에 대해 하나의 UMEM만 할당하면 되므로, 멀티큐 환경에서 메모리 사용량이 1/N로 줄어듭니다.
Shared UMEM 코드
/* 첫 번째 소켓: UMEM 생성 (기본 모드) */
struct xsk_socket *xsk1;
struct xsk_socket_config cfg = {
.rx_size = 4096,
.tx_size = 4096,
.bind_flags = XDP_ZEROCOPY, /* Shared UMEM 없이 */
};
xsk_socket__create(&xsk1, "eth0", 0, umem, &rx1, &tx1, &cfg);
/* 두 번째 소켓: 동일한 UMEM 공유 */
struct xsk_socket *xsk2;
cfg.bind_flags = XDP_ZEROCOPY | XDP_SHARED_UMEM;
xsk_socket__create(&xsk2, "eth0", 1, umem, &rx2, &tx2, &cfg);
/* 세 번째 소켓: 역시 동일 UMEM 공유 */
struct xsk_socket *xsk3;
xsk_socket__create(&xsk3, "eth0", 2, umem, &rx3, &tx3, &cfg);
/* 주의: 프레임 주소 관리를 프로그래머가 직접 해야 함
* 같은 프레임을 두 소켓에서 동시에 사용하면 데이터 오염! */
- 프레임 소유권 관리가 프로그래머 책임 — 동일 프레임을 두 소켓이 동시에 사용하면 안 됨
- Fill Ring에 반환하는 프레임 주소가 현재 다른 소켓에서 사용 중이 아닌지 확인 필요
- XDP 프로그램에서
bpf_redirect_map의 대상이 올바른 소켓인지 queue_index로 결정
성능 최적화
Busy Polling
/* XDP_USE_NEED_WAKEUP 비활성화로 busy polling */
cfg.bind_flags = XDP_ZEROCOPY; /* XDP_USE_NEED_WAKEUP 생략 */
while (1) {
/* poll() 없이 바로 수신 (CPU 100% 사용) */
xsk_receive_packets(xsk, &rx_ring);
xsk_complete_tx(&comp_ring);
}
Batching
/* Batch 크기 조정으로 성능 향상 */
#define BATCH_SIZE 64 /* 32, 64, 128 등 */
__u32 idx;
unsigned int rcvd = xsk_ring_cons__peek(rx, BATCH_SIZE, &idx);
/* 한 번에 최대 64개 패킷 처리 */
CPU Affinity
/* 특정 CPU에 바인딩 */
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(2, &cpuset); /* CPU 2 */
pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);
/* NIC IRQ도 동일 CPU로 설정 */
# echo 4 > /proc/irq/123/smp_affinity # CPU 2 (bitmask 0x4)
Hugepage 메모리
# Hugepage 설정 (2MB 페이지 1024개 = 2GB)
$ sudo sysctl -w vm.nr_hugepages=1024
$ cat /proc/meminfo | grep Huge
HugePages_Total: 1024
HugePages_Free: 1024
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
/* Hugepage UMEM 할당 — TLB miss 감소로 성능 향상 */
void *umem_area = mmap(NULL, umem_size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);
if (umem_area == MAP_FAILED) {
/* Hugepage 불가 시 일반 페이지로 fallback */
umem_area = mmap(NULL, umem_size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
}
/* mlock으로 페이지 스왑 방지 (선택적이나 권장) */
mlock(umem_area, umem_size);
SO_PREFER_BUSY_POLL (커널 5.11+)
/* 소켓 레벨 busy polling — NAPI 재스케줄 없이 직접 polling */
int xsk_fd = xsk_socket__fd(xsk);
/* busy poll 활성화 */
int optval = 1;
setsockopt(xsk_fd, SOL_SOCKET, SO_PREFER_BUSY_POLL, &optval, sizeof(optval));
/* busy poll 예산 (패킷 수) */
optval = 64;
setsockopt(xsk_fd, SOL_SOCKET, SO_BUSY_POLL_BUDGET, &optval, sizeof(optval));
/* busy poll 타임아웃 (마이크로초) */
optval = 20;
setsockopt(xsk_fd, SOL_SOCKET, SO_BUSY_POLL, &optval, sizeof(optval));
sysctl 튜닝
# NAPI 관련 튜닝
$ sudo sysctl -w net.core.busy_poll=50 # busy poll 타임아웃 (μs)
$ sudo sysctl -w net.core.busy_read=50 # busy read 타임아웃 (μs)
$ sudo sysctl -w net.core.netdev_budget=300 # NAPI budget
$ sudo sysctl -w net.core.netdev_budget_usecs=2000
# 메모리 관련
$ sudo sysctl -w net.core.optmem_max=65536
$ sudo sysctl -w vm.nr_hugepages=1024
# CPU 전력 관리 비활성화 (최저 레이턴시)
# 부트 파라미터에 추가:
# intel_idle.max_cstate=1 processor.max_cstate=1 idle=poll
성능 벤치마크
xdpsock 샘플 프로그램
# 커널 샘플 프로그램 빌드
$ cd linux/samples/bpf
$ make xdpsock
# RX 벤치마크 (Zero-copy, queue 0)
$ sudo ./xdpsock -i eth0 -q 0 -z -r
# TX 벤치마크
$ sudo ./xdpsock -i eth0 -q 0 -z -t
# L2FWD (Layer 2 Forwarding)
$ sudo ./xdpsock -i eth0 -q 0 -z -l
# 결과 예시
RX: 10,234,567 pps 5,239 Mb/s
TX: 9,876,543 pps 5,059 Mb/s
디버깅
통계 확인
/* AF_XDP 통계 */
struct xdp_statistics stats;
socklen_t len = sizeof(stats);
getsockopt(xsk_socket__fd(xsk), SOL_XDP, XDP_STATISTICS, &stats, &len);
printf("rx_dropped: %llu\\n", stats.rx_dropped);
printf("rx_invalid_descs: %llu\\n", stats.rx_invalid_descs);
printf("tx_invalid_descs: %llu\\n", stats.tx_invalid_descs);
printf("rx_ring_full: %llu\\n", stats.rx_ring_full);
printf("rx_fill_ring_empty_descs: %llu\\n", stats.rx_fill_ring_empty_descs);
printf("tx_ring_empty_descs: %llu\\n", stats.tx_ring_empty_descs);
bpftool로 XSKMAP 확인
# XSKMAP 내용 확인
$ sudo bpftool map dump id 123
key: 00 00 00 00 value: 0a 00 00 00 # queue 0 → socket fd 10
커널 추적 (tracing)
# ftrace로 AF_XDP 함수 추적
$ sudo trace-cmd record -p function_graph -g xsk_rcv -g xsk_sendmsg
$ sudo trace-cmd report | head -50
# bpftrace로 AF_XDP 드롭 원인 분석
$ sudo bpftrace -e '
kprobe:xsk_rcv {
@calls = count();
}
kretprobe:xsk_rcv /retval != 0/ {
@errors[retval] = count();
printf("xsk_rcv error: %d\n", retval);
}
'
# XDP redirect 성공/실패 추적
$ sudo bpftrace -e '
tracepoint:xdp:xdp_redirect_map_err {
@redirect_err[args->act, args->map_id] = count();
}
tracepoint:xdp:xdp_redirect_map {
@redirect_ok = count();
}
'
# perf로 AF_XDP 핫스팟 분석
$ sudo perf record -g -p $(pgrep af_xdp_app) -- sleep 10
$ sudo perf report --stdio
ethtool XDP/XSK 통계
# 드라이버별 AF_XDP 통계 확인 (ice 예시)
$ ethtool -S eth0 | grep -E 'xdp|xsk'
rx_xdp_aborted: 0
rx_xdp_drop: 0
rx_xdp_pass: 12345
rx_xdp_tx: 0
rx_xdp_invalid: 0
rx_xdp_redirect: 9876543
tx-0.tx_xsk_xmit: 5432100
tx-0.tx_xsk_mpwqe: 0
rx-0.rx_xsk_packets: 9876543
rx-0.rx_xsk_bytes: 631458752
rx-0.rx_xsk_drops: 0
rx-0.rx_xsk_cqe_err: 0
rx-0.rx_xsk_congst_umr: 0
커널 설정
# AF_XDP 필수 설정
CONFIG_XDP_SOCKETS=y # AF_XDP 소켓 지원
CONFIG_XDP_SOCKETS_DIAG=y # AF_XDP 진단 (ss --xdp)
# XDP 기본 지원
CONFIG_BPF=y # BPF 서브시스템
CONFIG_BPF_SYSCALL=y # bpf() 시스템 콜
CONFIG_BPF_JIT=y # BPF JIT 컴파일러
CONFIG_HAVE_EBPF_JIT=y # 아키텍처 JIT 지원
CONFIG_NET_XDP=y # XDP 코어
# 성능 최적화 관련
CONFIG_HUGETLBFS=y # Hugepage 지원
CONFIG_HUGETLB_PAGE=y
CONFIG_TRANSPARENT_HUGEPAGE=y
# 디버깅 (개발 시에만)
CONFIG_BPF_EVENTS=y # BPF 이벤트 추적
CONFIG_DEBUG_INFO_BTF=y # BTF (bpftool 사용 시 필요)
# NIC 드라이버 (Zero-copy 지원 드라이버 예시)
CONFIG_ICE=m # Intel E810 (100GbE)
CONFIG_I40E=m # Intel X710/XL710
CONFIG_IXGBE=m # Intel 82599/X520
CONFIG_MLX5_CORE=m # Mellanox ConnectX-5/6/7
설정 확인 스크립트
#!/bin/bash
# af_xdp_config_check.sh — AF_XDP 필수 커널 설정 확인
configs=(
"CONFIG_XDP_SOCKETS"
"CONFIG_XDP_SOCKETS_DIAG"
"CONFIG_BPF"
"CONFIG_BPF_SYSCALL"
"CONFIG_BPF_JIT"
"CONFIG_NET_XDP"
)
for cfg in "${configs[@]}"; do
val=$(zgrep "$cfg=" /proc/config.gz 2>/dev/null || \
grep "$cfg=" /boot/config-$(uname -r) 2>/dev/null)
if [ -z "$val" ]; then
echo "MISSING: $cfg"
elif echo "$val" | grep -q "=y\|=m"; then
echo "OK: $val"
else
echo "DISABLED: $val"
fi
done
# BPF JIT 활성화 확인
echo ""
echo "BPF JIT: $(cat /proc/sys/net/core/bpf_jit_enable)"
echo "Hugepages: $(cat /proc/meminfo | grep HugePages_Total)"
멀티큐 아키텍처와 RSS 연동
현대 NIC은 여러 RX/TX 큐를 제공하며, RSS(Receive Side Scaling)를 통해 패킷을 큐별로 분산합니다. AF_XDP는 큐 단위로 소켓을 바인딩하므로, 멀티큐 환경에서 자연스러운 병렬 처리가 가능합니다.
멀티큐 설정 절차
# 1. NIC 큐 수 확인 및 설정
$ ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
Combined: 64
Current hardware settings:
Combined: 4
# 큐 수를 CPU 코어 수에 맞춤
$ sudo ethtool -L eth0 combined 4
# 2. RSS 해시 키/인디렉션 테이블 확인
$ ethtool -x eth0
RX flow hash indirection table for eth0 with 4 RX ring(s):
0: 0 1 2 3 0 1 2 3
...
# 3. IRQ affinity를 큐별 CPU에 고정
$ sudo sh -c 'echo 1 > /proc/irq/50/smp_affinity' # Queue 0 → CPU 0
$ sudo sh -c 'echo 2 > /proc/irq/51/smp_affinity' # Queue 1 → CPU 1
$ sudo sh -c 'echo 4 > /proc/irq/52/smp_affinity' # Queue 2 → CPU 2
$ sudo sh -c 'echo 8 > /proc/irq/53/smp_affinity' # Queue 3 → CPU 3
# 또는 irqbalance 비활성화 후 스크립트로 설정
$ sudo systemctl stop irqbalance
$ sudo set_irq_affinity.sh eth0
멀티큐 AF_XDP 코드 구조
#include <pthread.h>
#include <bpf/xsk.h>
#define NUM_QUEUES 4
struct xsk_per_queue {
struct xsk_socket *xsk;
struct xsk_ring_cons rx;
struct xsk_ring_prod tx;
struct xsk_ring_prod fill;
struct xsk_ring_cons comp;
int queue_id;
};
static void *rx_thread(void *arg)
{
struct xsk_per_queue *q = arg;
/* 이 스레드를 queue_id와 동일한 CPU에 고정 */
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(q->queue_id, &cpuset);
pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);
while (running) {
__u32 idx = 0;
unsigned int rcvd = xsk_ring_cons__peek(&q->rx, BATCH_SIZE, &idx);
if (!rcvd) {
if (xsk_ring_prod__needs_wakeup(&q->fill))
recvmsg(xsk_socket__fd(q->xsk), NULL, MSG_DONTWAIT);
continue;
}
/* 패킷 처리 ... */
xsk_ring_cons__release(&q->rx, rcvd);
refill_fill_ring(&q->fill, rcvd);
}
return NULL;
}
int main(void)
{
struct xsk_umem *umem;
struct xsk_per_queue queues[NUM_QUEUES];
pthread_t threads[NUM_QUEUES];
/* 공유 UMEM 생성 (한 번만) */
xsk_umem__create(&umem, umem_area, umem_size,
&queues[0].fill, &queues[0].comp, &umem_cfg);
/* 큐별 AF_XDP 소켓 생성 */
for (int i = 0; i < NUM_QUEUES; i++) {
queues[i].queue_id = i;
struct xsk_socket_config cfg = {
.rx_size = 4096,
.tx_size = 4096,
.bind_flags = XDP_ZEROCOPY | (i > 0 ? XDP_SHARED_UMEM : 0),
};
xsk_socket__create(&queues[i].xsk, "eth0", i, umem,
&queues[i].rx, &queues[i].tx, &cfg);
/* XSKMAP에 등록 */
int fd = xsk_socket__fd(queues[i].xsk);
bpf_map_update_elem(xsks_map_fd, &i, &fd, 0);
}
/* 큐별 스레드 시작 */
for (int i = 0; i < NUM_QUEUES; i++)
pthread_create(&threads[i], NULL, rx_thread, &queues[i]);
for (int i = 0; i < NUM_QUEUES; i++)
pthread_join(threads[i], NULL);
}
irqbalance를 비활성화하고 수동으로 IRQ affinity를 설정하세요.
Flow Steering을 통한 트래픽 분리
# ethtool ntuple을 이용해 특정 트래픽을 특정 큐로 유도
# UDP 포트 53 (DNS) 트래픽 → Queue 0
$ sudo ethtool -N eth0 flow-type udp4 dst-port 53 action 0
# UDP 포트 4789 (VXLAN) 트래픽 → Queue 1
$ sudo ethtool -N eth0 flow-type udp4 dst-port 4789 action 1
# 설정된 규칙 확인
$ ethtool -n eth0
4 RX rings available
Total 2 rules
# 규칙 삭제
$ sudo ethtool -N eth0 delete 0
NEED_WAKEUP 메커니즘
XDP_USE_NEED_WAKEUP 플래그는 AF_XDP의 CPU 효율을 대폭 개선하는 핵심 메커니즘입니다. NAPI 컨텍스트에서 커널이 직접 큐를 소비할 수 있을 때는 wakeup이 불필요하고, NAPI가 완료된 후에만 userspace가 커널을 깨워야 합니다.
NEED_WAKEUP RX 코드 패턴
/* 권장: NEED_WAKEUP을 활용한 효율적 RX 루프 */
struct xsk_socket_config cfg = {
.rx_size = 4096,
.bind_flags = XDP_ZEROCOPY | XDP_USE_NEED_WAKEUP,
};
while (running) {
__u32 idx = 0;
unsigned int rcvd = xsk_ring_cons__peek(&rx, BATCH_SIZE, &idx);
if (!rcvd) {
/* Fill Ring wakeup 필요 여부 확인 */
if (xsk_ring_prod__needs_wakeup(&fill)) {
/* NAPI가 비활성 → syscall로 깨움 */
struct pollfd pfd = {
.fd = xsk_socket__fd(xsk),
.events = POLLIN,
};
poll(&pfd, 1, 1000); /* 1초 타임아웃 */
}
continue;
}
/* 패킷 처리 */
process_batch(&rx, idx, rcvd);
xsk_ring_cons__release(&rx, rcvd);
refill_fill_ring(&fill, rcvd);
}
NEED_WAKEUP TX 코드 패턴
/* TX에서의 NEED_WAKEUP 활용 */
xsk_ring_prod__submit(&tx, batch_size);
/* TX Ring wakeup 필요 여부 확인 */
if (xsk_ring_prod__needs_wakeup(&tx)) {
/* 커널에 송신 시작 알림 */
sendto(xsk_socket__fd(xsk), NULL, 0, MSG_DONTWAIT, NULL, 0);
}
/* NEED_WAKEUP 없이는 매번 sendto() 호출 필요 → syscall 오버헤드 */
XDP_USE_NEED_WAKEUP를 사용하면 NAPI가 활성 상태일 때 불필요한 poll()/recvmsg() syscall을 건너뛰어 10~30%의 성능 향상을 얻을 수 있습니다. 특히 낮은 트래픽에서 CPU 사용률이 크게 감소합니다.
XDP 메타데이터 전달
XDP 프로그램에서 AF_XDP 소켓으로 패킷과 함께 커스텀 메타데이터를 전달할 수 있습니다. 이를 통해 패킷 분류 결과, 타임스탬프, 해시 값 등을 userspace로 효율적으로 전달합니다.
메타데이터 레이아웃
XDP 프로그램에서 메타데이터 작성
/* XDP 프로그램: 패킷에 커스텀 메타데이터 추가 */
struct xdp_meta {
__u64 timestamp; /* 패킷 수신 타임스탬프 */
__u32 hash; /* RSS 해시 값 */
__u32 mark; /* 분류 마크 */
};
SEC("xdp")
int xdp_with_meta(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
/* 메타데이터 공간 확보 (data 앞에 16바이트) */
int ret = bpf_xdp_adjust_meta(ctx, -(int)sizeof(struct xdp_meta));
if (ret < 0)
return XDP_PASS;
/* 포인터 재계산 (bpf_xdp_adjust_meta 후 필수) */
data = (void *)(long)ctx->data;
void *data_meta = (void *)(long)ctx->data_meta;
if (data_meta + sizeof(struct xdp_meta) > data)
return XDP_PASS;
struct xdp_meta *meta = data_meta;
meta->timestamp = bpf_ktime_get_ns();
meta->hash = ctx->rx_queue_index;
meta->mark = 0xCAFE;
return bpf_redirect_map(&xsks_map, ctx->rx_queue_index, 0);
}
Userspace에서 메타데이터 읽기
/* Userspace: RX 패킷에서 메타데이터 추출 */
struct xdp_meta {
__u64 timestamp;
__u32 hash;
__u32 mark;
};
void process_with_meta(const struct xdp_desc *desc, void *umem_area)
{
void *pkt = xsk_umem__get_data(umem_area, desc->addr);
/* 메타데이터는 패킷 데이터 바로 앞에 위치 */
struct xdp_meta *meta = pkt - sizeof(struct xdp_meta);
printf("Timestamp: %llu ns, Hash: %u, Mark: 0x%x\n",
meta->timestamp, meta->hash, meta->mark);
/* 실제 패킷 데이터 처리 */
process_packet(pkt, desc->len);
}
frame_headroom이 메타데이터 크기보다 커야 합니다. 기본 headroom은 0이므로, 메타데이터 사용 시 반드시 umem_config.frame_headroom = 256 등으로 설정하세요. 또한 XDP 프로그램에서 bpf_xdp_adjust_meta()를 호출하지 않으면 메타데이터가 존재하지 않습니다.
Unaligned 청크 모드
리눅스 5.7+에서 도입된 XDP_UMEM_UNALIGNED_CHUNK_FLAG는 UMEM 프레임을 정렬 제약 없이 가변 크기로 사용할 수 있게 합니다. 이는 MTU보다 작은 패킷이 대부분인 워크로드에서 메모리 효율을 높입니다.
Aligned vs Unaligned 비교
| 특성 | Aligned (기본) | Unaligned |
|---|---|---|
| 프레임 주소 | frame_size 경계 정렬 필수 | 임의 주소 허용 |
| 주소 형식 | addr = n * frame_size |
addr = base + offset (하위 16비트 = offset) |
| 메모리 낭비 | 작은 패킷에서 낭비 큼 | 실제 사용량만큼만 할당 |
| 프레임 관리 | 단순 (인덱스 기반) | 복잡 (free list 필요) |
| Zero-copy | 지원 | 드라이버에 따라 다름 |
| 커널 버전 | 4.18+ | 5.7+ |
Unaligned 모드 설정
/* Unaligned 청크 모드 UMEM 설정 */
struct xsk_umem_config umem_cfg = {
.fill_size = 4096,
.comp_size = 4096,
.frame_size = 2048, /* 여전히 최대 프레임 크기 지정 */
.frame_headroom = 0,
.flags = XDP_UMEM_UNALIGNED_CHUNK_FLAG, /* 핵심 플래그 */
};
/* 주소 인코딩: 상위 비트 = 오프셋, 하위 비트 = 청크 내 위치 */
/* Unaligned 모드에서 desc->addr의 의미가 달라짐 */
#define XSK_UNALIGNED_BUF_OFFSET_SHIFT 48
#define XSK_UNALIGNED_BUF_ADDR_MASK \
((1ULL << XSK_UNALIGNED_BUF_OFFSET_SHIFT) - 1)
/* 실제 주소 추출 */
static inline __u64 xsk_umem__extract_addr(__u64 addr)
{
return addr & XSK_UNALIGNED_BUF_ADDR_MASK;
}
/* 오프셋 추출 */
static inline __u64 xsk_umem__extract_offset(__u64 addr)
{
return addr >> XSK_UNALIGNED_BUF_OFFSET_SHIFT;
}
/* 조합된 주소 생성 */
static inline __u64 xsk_umem__add_offset_to_addr(__u64 addr)
{
return xsk_umem__extract_addr(addr) + xsk_umem__extract_offset(addr);
}
커널 내부 자료구조
AF_XDP의 커널 측 구현은 net/xdp/ 디렉토리에 있으며, 핵심 자료구조들의 관계를 이해하면 성능 튜닝과 디버깅에 큰 도움이 됩니다.
xdp_sock 구조체 상세
/* include/net/xdp_sock.h — AF_XDP 소켓의 커널 표현 */
struct xdp_sock {
struct sock sk; /* 일반 소켓 임베딩 */
struct xsk_queue *rx; /* RX Ring (kernel → user) */
struct xsk_queue *tx; /* TX Ring (user → kernel) */
struct list_head map_list; /* XSKMAP 등록 목록 */
struct spinlock map_list_lock;
struct xsk_buff_pool *pool; /* UMEM 버퍼 풀 */
u16 queue_id; /* 바인딩된 NIC 큐 번호 */
enum {
XSK_READY = 0,
XSK_BOUND,
XSK_UNBOUND,
} state;
struct xdp_umem *umem; /* UMEM 메모리 영역 */
struct net_device *dev; /* 바인딩된 네트워크 디바이스 */
bool zc; /* zero-copy 활성 여부 */
u64 rx_dropped; /* 드롭 통계 */
u64 rx_queue_full; /* RX ring full 통계 */
struct list_head flush_node; /* flush 대기열 */
};
xsk_buff_pool 구조체
/* net/xdp/xsk_buff_pool.c — 버퍼 풀 관리의 핵심 */
struct xsk_buff_pool {
struct device *dev; /* DMA 디바이스 */
struct net_device *netdev; /* 네트워크 디바이스 */
struct list_head free_list; /* DMA 매핑 해제 대기 */
struct xdp_buff **heads; /* 모든 xdp_buff 헤더 배열 */
u64 *addrs; /* UMEM 주소 배열 */
u32 heads_cnt; /* xdp_buff 수 */
u32 free_heads_cnt; /* 사용 가능한 xdp_buff 수 */
struct xsk_queue *fq; /* Fill Queue (user → kernel) */
struct xsk_queue *cq; /* Completion Queue (kernel → user) */
dma_addr_t *dma_pages; /* DMA 매핑된 페이지 배열 */
struct xdp_buff_xsk *free_heads[]; /* 가용 버퍼 스택 */
u32 chunk_size; /* 프레임 크기 */
u32 chunk_mask;
u32 frame_len; /* chunk_size - headroom */
u8 cached_need_wakeup; /* NEED_WAKEUP 캐시 */
bool uses_need_wakeup;
bool unaligned; /* unaligned chunk 모드 */
};
xsk_queue Ring 구현
/* net/xdp/xsk_queue.h — SPSC(Single Producer Single Consumer) Ring */
struct xsk_queue {
u32 ring_mask; /* nentries - 1 (2의 거듭제곱) */
u32 nentries; /* 엔트리 수 */
u32 cached_prod; /* 로컬 캐시된 producer 인덱스 */
u32 cached_cons; /* 로컬 캐시된 consumer 인덱스 */
struct xdp_ring *ring; /* mmap된 공유 메모리 */
u64 invalid_descs; /* 유효하지 않은 descriptor 수 */
u64 queue_empty_descs; /* 큐 빈 상태 카운트 */
};
/* UAPI: User/Kernel 공유 링 구조 */
struct xdp_ring {
__u32 producer __attribute__((aligned(CACHELINE_SIZE)));
__u32 pad1 __attribute__((aligned(CACHELINE_SIZE)));
__u32 consumer __attribute__((aligned(CACHELINE_SIZE)));
__u32 pad2 __attribute__((aligned(CACHELINE_SIZE)));
__u32 flags; /* XDP_RING_NEED_WAKEUP */
__u32 pad3 __attribute__((aligned(CACHELINE_SIZE)));
};
/*
* producer/consumer 인덱스는 각각 별도의 cache line에 위치
* → false sharing 방지 (성능 핵심)
* → User가 producer를 쓰고, Kernel이 consumer를 쓰는 SPSC 패턴
*/
AF_XDP 시스템 콜 경로
| 시스템 콜 | 커널 함수 | 동작 |
|---|---|---|
socket(AF_XDP, ...) |
xsk_create() |
xdp_sock 할당, 초기화 |
setsockopt(SOL_XDP, XDP_UMEM_REG) |
xsk_setsockopt() |
UMEM 등록, 페이지 pin |
setsockopt(SOL_XDP, XDP_UMEM_FILL_RING) |
xsk_setsockopt() |
Fill Ring 생성/mmap |
setsockopt(SOL_XDP, XDP_UMEM_COMPLETION_RING) |
xsk_setsockopt() |
Completion Ring 생성/mmap |
setsockopt(SOL_XDP, XDP_RX_RING) |
xsk_setsockopt() |
RX Ring 생성/mmap |
setsockopt(SOL_XDP, XDP_TX_RING) |
xsk_setsockopt() |
TX Ring 생성/mmap |
bind() |
xsk_bind() |
NIC 큐 바인딩, ZC 설정, pool 활성화 |
mmap() |
xsk_mmap() |
Ring/UMEM 메모리를 user 매핑 |
sendto() |
xsk_sendmsg() |
TX kick, ndo_xsk_wakeup 호출 |
recvmsg() |
xsk_recvmsg() |
RX wakeup, NAPI 스케줄 |
poll() |
xsk_poll() |
이벤트 대기, NAPI 스케줄 |
getsockopt(XDP_STATISTICS) |
xsk_getsockopt() |
드롭/에러 통계 조회 |
bind() 내부 흐름
/* net/xdp/xsk.c: xsk_bind() 핵심 흐름 (간략화) */
static int xsk_bind(struct socket *sock, struct sockaddr *addr, int addr_len)
{
struct xdp_sock *xs = xdp_sk(sock->sk);
struct sockaddr_xdp *sxdp = (struct sockaddr_xdp *)addr;
/* 1. 네트워크 디바이스 찾기 */
dev = dev_get_by_index(net, sxdp->sxdp_ifindex);
/* 2. queue_id 유효성 검사 */
if (sxdp->sxdp_queue_id >= dev->real_num_rx_queues)
return -EINVAL;
/* 3. Zero-copy 모드 판단 */
if (sxdp->sxdp_flags & XDP_ZEROCOPY) {
/* 드라이버가 ndo_bpf + XDP_SETUP_XSK_POOL 지원 확인 */
err = xsk_check_common(xs, dev);
if (!err)
xs->zc = true;
}
/* 4. xsk_buff_pool 생성 및 DMA 매핑 */
pool = xp_create_and_assign_umem(xs, xs->umem);
xp_dma_map(pool, dev, sxdp->sxdp_queue_id);
/* 5. 드라이버에 pool 등록 (ZC 시) */
if (xs->zc) {
bpf.command = XDP_SETUP_XSK_POOL;
bpf.xsk.pool = pool;
bpf.xsk.queue_id = sxdp->sxdp_queue_id;
dev->netdev_ops->ndo_bpf(dev, &bpf);
}
/* 6. 상태 전이 */
xs->state = XSK_BOUND;
xs->dev = dev;
xs->queue_id = sxdp->sxdp_queue_id;
}
Copy vs Zero-Copy 내부 데이터 경로
AF_XDP의 두 가지 모드는 패킷이 NIC에서 userspace까지 도달하는 경로가 근본적으로 다릅니다. 이 차이가 성능 격차의 원인입니다.
Copy 모드 커널 경로
/* net/xdp/xsk.c: Copy 모드 수신 경로 (간략화) */
static int xsk_rcv(struct xdp_sock *xs, struct xdp_buff *xdp)
{
u32 len = xdp->data_end - xdp->data;
u32 metalen = xdp->data - xdp->data_meta;
/* 1. Fill Ring에서 빈 프레임 주소 가져오기 */
u64 addr;
if (!xskq_cons_peek_addr_unchecked(xs->pool->fq, &addr))
return -ENOBUFS; /* Fill Ring 비었으면 드롭! */
/* 2. UMEM 프레임으로 패킷 데이터 복사 (병목) */
void *buffer = xsk_buff_raw_get_data(xs->pool, addr);
memcpy(buffer, xdp->data_meta, len + metalen);
/* 3. RX Ring에 descriptor 추가 */
struct xdp_desc desc = {
.addr = addr + metalen,
.len = len,
};
xskq_prod_submit_addr(xs->rx, &desc);
/* 4. Fill Ring에서 소비 완료 */
xskq_cons_release(xs->pool->fq);
return 0;
}
Zero-Copy 모드 커널 경로
/* Zero-copy: 드라이버가 직접 xsk_buff_pool에서 버퍼 할당 */
/* 1. 드라이버 NAPI poll에서: Fill Ring → DMA 버퍼 할당 */
struct xdp_buff *xsk_buff_alloc(struct xsk_buff_pool *pool)
{
/* Fill Ring에서 프레임 주소를 가져와 xdp_buff 구성 */
struct xdp_buff_xsk *xskb = pool->free_heads[--pool->free_heads_cnt];
xskb->xdp.data = xskb->orig_addr + pool->headroom;
xskb->xdp.data_end = xskb->xdp.data;
return &xskb->xdp;
}
/* 2. NIC DMA가 UMEM 프레임에 직접 수신 */
/* 3. XDP 프로그램 실행 후 XDP_REDIRECT */
/* 4. xdp_do_redirect에서 AF_XDP 소켓으로 전달 */
int xsk_rcv_zc(struct xdp_sock *xs, struct xdp_buff *xdp)
{
/* memcpy 없이 주소만 RX Ring에 게시 */
struct xdp_buff_xsk *xskb = container_of(xdp, ...);
struct xdp_desc desc = {
.addr = xp_get_handle(xskb),
.len = xdp->data_end - xdp->data,
};
return xskq_prod_reserve_desc(xs->rx, &desc);
}
Zero-Copy 지원 드라이버 상세
| 드라이버 | NIC | ZC 도입 커널 | 비고 |
|---|---|---|---|
i40e |
Intel X710, XL710, XXV710 | 4.18 | 최초 ZC 지원, 가장 성숙 |
ixgbe |
Intel 82599, X520, X540 | 4.18 | 10GbE 레거시, 안정적 |
ice |
Intel E810 (100GbE) | 5.5 | 100GbE, 최고 성능 |
mlx5 |
Mellanox ConnectX-5/6/7 | 5.3 | 25/40/100GbE, multi-buffer 지원 |
bnxt_en |
Broadcom NetXtreme-E | 5.13 | 25/50/100GbE |
igc |
Intel I225/I226 (2.5GbE) | 5.14 | 데스크탑/임베디드 |
stmmac |
Synopsys DWC Ethernet | 5.13 | 임베디드/ARM SoC |
veth |
가상 이더넷 쌍 | 5.9 | 컨테이너 네트워킹 |
virtio_net |
QEMU/KVM VirtIO | 6.4 | 가상화 환경 |
실전 활용 사례
고성능 DNS 서버
DNS는 작은 UDP 패킷으로 구성되어 AF_XDP의 이점을 극대화할 수 있는 대표적인 워크로드입니다.
/* AF_XDP 기반 DNS 서버 핵심 루프 (개념 코드) */
while (running) {
__u32 idx = 0;
unsigned int rcvd = xsk_ring_cons__peek(&rx, BATCH_SIZE, &idx);
for (unsigned int i = 0; i < rcvd; i++) {
const struct xdp_desc *desc = xsk_ring_cons__rx_desc(&rx, idx++);
void *pkt = xsk_umem__get_data(umem_area, desc->addr);
/* L2/L3/L4 파싱 */
struct ethhdr *eth = pkt;
struct iphdr *ip = pkt + sizeof(*eth);
struct udphdr *udp = (void *)ip + ip->ihl * 4;
void *dns_payload = (void *)udp + sizeof(*udp);
/* DNS 쿼리 처리 */
int resp_len = process_dns_query(dns_payload, dns_response);
/* 응답을 같은 프레임에 구성 (in-place 수정) */
swap_mac_addrs(eth);
swap_ip_addrs(ip);
swap_udp_ports(udp);
memcpy(dns_payload, dns_response, resp_len);
/* TX Ring으로 전송 */
xsk_send_frame(&tx, desc->addr, total_len);
}
xsk_ring_cons__release(&rx, rcvd);
}
고속 패킷 캡처
/* AF_XDP 기반 패킷 캡처: pcapng 형식 저장 */
#define RING_SIZE 16384
#define WRITE_BATCH 256
struct capture_ctx {
FILE *pcap_file;
__u64 captured;
__u64 dropped;
};
void capture_loop(struct xsk_socket *xsk, struct capture_ctx *ctx)
{
__u32 idx = 0;
unsigned int rcvd = xsk_ring_cons__peek(&rx, WRITE_BATCH, &idx);
for (unsigned int i = 0; i < rcvd; i++) {
const struct xdp_desc *desc = xsk_ring_cons__rx_desc(&rx, idx++);
void *pkt = xsk_umem__get_data(umem_area, desc->addr);
__u64 ts = get_timestamp_ns();
/* pcapng Enhanced Packet Block 작성 */
write_pcapng_epb(ctx->pcap_file, pkt, desc->len, ts);
ctx->captured++;
}
xsk_ring_cons__release(&rx, rcvd);
refill_fill_ring(&fill, rcvd);
}
L2 포워딩 / 브릿지
/* AF_XDP L2 포워딩: 두 인터페이스 간 패킷 전달 */
struct xsk_socket *xsk_rx; /* eth0 수신 */
struct xsk_socket *xsk_tx; /* eth1 송신 */
void l2fwd_loop(void)
{
while (running) {
__u32 rx_idx = 0, tx_idx = 0;
unsigned int rcvd = xsk_ring_cons__peek(&rx_ring, BATCH_SIZE, &rx_idx);
if (!rcvd)
continue;
/* TX Ring에 공간 확보 */
if (xsk_ring_prod__reserve(&tx_ring, rcvd, &tx_idx) != rcvd) {
/* Completion Ring 회수 후 재시도 */
complete_tx(&comp_ring);
xsk_ring_prod__reserve(&tx_ring, rcvd, &tx_idx);
}
for (unsigned int i = 0; i < rcvd; i++) {
const struct xdp_desc *rx_desc = xsk_ring_cons__rx_desc(&rx_ring, rx_idx++);
struct xdp_desc *tx_desc = xsk_ring_prod__tx_desc(&tx_ring, tx_idx++);
/* 프레임을 TX로 직접 전달 (같은 UMEM 사용 시 zero-copy) */
tx_desc->addr = rx_desc->addr;
tx_desc->len = rx_desc->len;
}
xsk_ring_cons__release(&rx_ring, rcvd);
xsk_ring_prod__submit(&tx_ring, rcvd);
if (xsk_ring_prod__needs_wakeup(&tx_ring))
sendto(xsk_socket__fd(xsk_tx), NULL, 0, MSG_DONTWAIT, NULL, 0);
}
}
IDS/IPS 패킷 검사
/* XDP: 의심 패킷만 AF_XDP로 전달하는 1차 필터 */
SEC("xdp")
int ids_filter(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 == htons(ETH_P_IP)) {
struct iphdr *ip = (struct iphdr *)(eth + 1);
if ((void *)(ip + 1) > data_end) return XDP_PASS;
/* 알려진 악성 IP → 즉시 DROP (XDP에서 처리) */
if (bpf_map_lookup_elem(&blocklist, &ip->saddr))
return XDP_DROP;
/* TCP SYN, 비정상 플래그 → AF_XDP로 정밀 검사 */
if (ip->protocol == IPPROTO_TCP) {
struct tcphdr *tcp = (struct tcphdr *)(((void *)ip) + ip->ihl * 4);
if ((void *)(tcp + 1) > data_end) return XDP_PASS;
if (tcp->syn || tcp->fin || tcp->rst)
return bpf_redirect_map(&xsks_map, ctx->rx_queue_index, 0);
}
}
return XDP_PASS; /* 나머지 → 일반 스택 */
}
veth / 컨테이너 환경에서의 AF_XDP
리눅스 5.9+에서 veth 드라이버가 AF_XDP zero-copy를 지원하면서, 컨테이너 네트워킹에서도 고성능 패킷 처리가 가능해졌습니다.
veth AF_XDP 설정
# 1. veth pair 생성
$ sudo ip link add veth_host type veth peer name veth_cont
# 2. 한 쪽을 컨테이너 네임스페이스로 이동
$ sudo ip link set veth_cont netns container_ns
# 3. 양쪽 인터페이스 활성화
$ sudo ip link set veth_host up
$ sudo ip netns exec container_ns ip link set veth_cont up
# 4. XDP native 모드 확인 (veth는 native XDP 지원)
$ sudo ip link set dev veth_host xdp obj xdp_prog.o sec xdp
# 5. AF_XDP 소켓을 veth_host에 바인딩 (호스트 측)
# → 코드에서 xsk_socket__create(..., "veth_host", 0, ...) 사용
libxdp API
libxdp는 libbpf 위에 구축된 XDP 전용 라이브러리로, 멀티 프로그램 디스패치와 AF_XDP 설정을 단순화합니다.
libxdp 멀티 프로그램
#include <xdp/libxdp.h>
/* 여러 XDP 프로그램을 체인으로 연결 */
struct xdp_program *prog1, *prog2;
/* 프로그램 로드 */
prog1 = xdp_program__open_file("stats.o", "xdp", NULL);
prog2 = xdp_program__open_file("redirect.o", "xdp", NULL);
/* 우선순위 설정 */
xdp_program__set_run_order(prog1, 10);
xdp_program__set_run_order(prog2, 20);
/* 인터페이스에 attach (multiprog 자동 관리) */
xdp_program__attach(prog1, ifindex, XDP_MODE_NATIVE, 0);
xdp_program__attach(prog2, ifindex, XDP_MODE_NATIVE, 0);
/* libxdp이 freplace BPF 프로그램으로 디스패처를 자동 생성 */
libxdp AF_XDP 헬퍼
# xdp-tools 설치 (libxdp 포함)
$ git clone https://github.com/xdp-project/xdp-tools.git
$ cd xdp-tools && ./configure && make
$ sudo make install
# xdp-loader: XDP 프로그램 관리
$ sudo xdp-loader load -m native eth0 xdp_prog.o
$ sudo xdp-loader status eth0
$ sudo xdp-loader unload eth0 --all
# xdp-filter: 내장 패킷 필터
$ sudo xdp-filter load eth0
$ sudo xdp-filter port 12345 # 포트 12345 차단
$ sudo xdp-filter ip 10.0.0.1 # IP 차단
완전한 예제: AF_XDP RX/TX 애플리케이션
시작부터 끝까지 동작하는 최소한의 완전한 AF_XDP 애플리케이션 구조입니다.
전체 초기화 및 실행 흐름
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <poll.h>
#include <sys/mman.h>
#include <bpf/bpf.h>
#include <bpf/xsk.h>
#define NUM_FRAMES 4096
#define FRAME_SIZE XSK_UMEM__DEFAULT_FRAME_SIZE /* 4096 */
#define BATCH_SIZE 64
#define INVALID_UMEM_FRAME UINT64_MAX
static bool running = true;
struct xsk_app {
struct xsk_umem *umem;
struct xsk_socket *xsk;
struct xsk_ring_cons rx;
struct xsk_ring_prod tx;
struct xsk_ring_prod fill;
struct xsk_ring_cons comp;
void *umem_area;
/* 프레임 풀 (간단한 스택 기반) */
__u64 frame_stack[NUM_FRAMES];
__u32 frame_stack_top;
/* 통계 */
__u64 rx_packets;
__u64 tx_packets;
};
/* --- 프레임 풀 관리 --- */
static __u64 frame_alloc(struct xsk_app *app)
{
if (app->frame_stack_top == 0)
return INVALID_UMEM_FRAME;
return app->frame_stack[--app->frame_stack_top];
}
static void frame_free(struct xsk_app *app, __u64 addr)
{
app->frame_stack[app->frame_stack_top++] = addr;
}
/* --- UMEM 초기화 --- */
static int setup_umem(struct xsk_app *app)
{
size_t size = NUM_FRAMES * FRAME_SIZE;
app->umem_area = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (app->umem_area == MAP_FAILED)
return -1;
struct xsk_umem_config cfg = {
.fill_size = NUM_FRAMES,
.comp_size = NUM_FRAMES,
.frame_size = FRAME_SIZE,
.frame_headroom = XSK_UMEM__DEFAULT_FRAME_HEADROOM,
.flags = 0,
};
int ret = xsk_umem__create(&app->umem, app->umem_area, size,
&app->fill, &app->comp, &cfg);
if (ret)
return ret;
/* 프레임 스택 초기화 */
for (__u32 i = 0; i < NUM_FRAMES; i++)
app->frame_stack[i] = i * FRAME_SIZE;
app->frame_stack_top = NUM_FRAMES;
return 0;
}
/* --- AF_XDP 소켓 초기화 --- */
static int setup_socket(struct xsk_app *app, const char *ifname, int queue_id)
{
struct xsk_socket_config cfg = {
.rx_size = NUM_FRAMES,
.tx_size = NUM_FRAMES,
.libbpf_flags = XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD,
.xdp_flags = 0,
.bind_flags = XDP_USE_NEED_WAKEUP | XDP_COPY,
};
int ret = xsk_socket__create(&app->xsk, ifname, queue_id,
app->umem, &app->rx, &app->tx, &cfg);
if (ret)
return ret;
/* Fill Ring에 초기 프레임 제공 */
__u32 idx;
ret = xsk_ring_prod__reserve(&app->fill, NUM_FRAMES / 2, &idx);
for (__u32 i = 0; i < NUM_FRAMES / 2; i++)
*xsk_ring_prod__fill_addr(&app->fill, idx++) = frame_alloc(app);
xsk_ring_prod__submit(&app->fill, NUM_FRAMES / 2);
return 0;
}
/* --- 메인 루프 --- */
static void run(struct xsk_app *app)
{
struct pollfd pfd = {
.fd = xsk_socket__fd(app->xsk),
.events = POLLIN,
};
while (running) {
/* NEED_WAKEUP 활용 */
if (xsk_ring_prod__needs_wakeup(&app->fill))
poll(&pfd, 1, 100);
/* Completion Ring 회수 */
__u32 comp_idx;
unsigned int completed = xsk_ring_cons__peek(&app->comp, BATCH_SIZE, &comp_idx);
for (unsigned int i = 0; i < completed; i++) {
frame_free(app, *xsk_ring_cons__comp_addr(&app->comp, comp_idx++));
}
if (completed)
xsk_ring_cons__release(&app->comp, completed);
/* RX 처리 */
__u32 rx_idx = 0;
unsigned int rcvd = xsk_ring_cons__peek(&app->rx, BATCH_SIZE, &rx_idx);
for (unsigned int i = 0; i < rcvd; i++) {
const struct xdp_desc *desc =
xsk_ring_cons__rx_desc(&app->rx, rx_idx++);
void *pkt = xsk_umem__get_data(app->umem_area, desc->addr);
printf("RX: %u bytes\n", desc->len);
/* 프레임을 Fill Ring에 반환 */
frame_free(app, desc->addr);
app->rx_packets++;
}
if (rcvd) {
xsk_ring_cons__release(&app->rx, rcvd);
/* Fill Ring 보충 */
__u32 fill_idx;
unsigned int reserved = xsk_ring_prod__reserve(&app->fill, rcvd, &fill_idx);
for (unsigned int i = 0; i < reserved; i++)
*xsk_ring_prod__fill_addr(&app->fill, fill_idx++) = frame_alloc(app);
xsk_ring_prod__submit(&app->fill, reserved);
}
}
}
static void sigint_handler(int sig) { running = false; }
int main(int argc, char **argv)
{
if (argc < 2) { fprintf(stderr, "Usage: %s <ifname>\n", argv[0]); return 1; }
signal(SIGINT, sigint_handler);
struct xsk_app app = {};
if (setup_umem(&app) < 0) { perror("UMEM"); return 1; }
if (setup_socket(&app, argv[1], 0) < 0) { perror("socket"); return 1; }
printf("AF_XDP ready on %s queue 0. Ctrl+C to stop.\n", argv[1]);
run(&app);
printf("RX: %llu packets, TX: %llu packets\n", app.rx_packets, app.tx_packets);
xsk_socket__delete(app.xsk);
xsk_umem__delete(app.umem);
munmap(app.umem_area, NUM_FRAMES * FRAME_SIZE);
return 0;
}
빌드 방법
# 의존성 설치 (Ubuntu/Debian)
$ sudo apt install libbpf-dev libxdp-dev clang llvm pkg-config
# XDP BPF 프로그램 컴파일
$ clang -O2 -g -target bpf -c xdp_prog.c -o xdp_prog.o
# Userspace 애플리케이션 컴파일
$ gcc -O2 -o af_xdp_app af_xdp_app.c \
$(pkg-config --cflags --libs libbpf) -lxdp -lpthread
# 실행 (root 권한 필요)
$ sudo ./af_xdp_app eth0
흔한 실수와 트러블슈팅
자주 발생하는 문제
| 증상 | 원인 | 해결 |
|---|---|---|
ENOBUFS 에러 |
Fill Ring이 비어있음 | Fill Ring에 프레임을 충분히 보충 |
EOPNOTSUPP (bind) |
드라이버가 ZC 미지원 | XDP_COPY로 fallback |
EINVAL (bind) |
잘못된 queue_id | ethtool -l로 큐 수 확인 |
EBUSY (bind) |
큐에 다른 XSK가 이미 바인딩됨 | 다른 queue_id 사용 또는 기존 소켓 해제 |
| rx_dropped 증가 | RX Ring이 가득 차 있음 | RX ring size 증가, 처리 속도 개선 |
| rx_fill_ring_empty_descs 증가 | Fill Ring 보충이 너무 느림 | Fill Ring 크기 증가, batch 보충 |
| tx_invalid_descs 증가 | 유효하지 않은 UMEM 주소 | addr가 UMEM 범위 내인지 확인 |
| 패킷이 전혀 수신 안 됨 | XDP 프로그램 미로드 또는 XSKMAP 미등록 | bpftool prog list, bpftool map dump 확인 |
| 성능이 기대보다 낮음 | IRQ/CPU affinity 불일치 | NIC queue, IRQ, 스레드 CPU 1:1 매핑 |
| NEED_WAKEUP 후 hang | poll() timeout이 너무 큼 | 적절한 timeout 설정 (100~1000ms) |
디버깅 명령어 모음
# 1. XDP 프로그램 상태 확인
$ sudo bpftool prog list
$ sudo bpftool prog show id <ID>
# 2. XSKMAP 확인
$ sudo bpftool map list
$ sudo bpftool map dump id <MAP_ID>
# 3. AF_XDP 소켓 상태 (ss 유틸리티)
$ sudo ss -ax | grep xdp
$ sudo ss --xdp -a
# 4. NIC 큐 통계
$ ethtool -S eth0 | grep -E 'xdp|xsk'
rx_xdp_redirect: 12345678
rx_xdp_drop: 0
rx_xdp_tx: 0
rx_xsk_packets: 12345678
# 5. 커널 로그 확인
$ dmesg | grep -i xdp
$ dmesg | grep -i xsk
# 6. perf로 AF_XDP 핫스팟 프로파일링
$ sudo perf top -g -p $(pgrep af_xdp_app)
# 7. bpftrace로 AF_XDP 경로 추적
$ sudo bpftrace -e 'kprobe:xsk_rcv { @[kstack] = count(); }'
# 8. XDP 통계 (ip link)
$ ip -d link show eth0 | grep xdp
prog/xdp id 42 tag abc123...
# 9. NAPI 상태 확인
$ cat /sys/class/net/eth0/queues/rx-0/rps_cpus
$ cat /proc/softirqs | grep NET_RX
성능 최적화 체크리스트
- Zero-copy 모드 활성화 여부 —
ethtool -S eth0 | grep xsk에 ZC 통계가 나오는지 확인 - IRQ affinity —
cat /proc/interrupts로 NIC IRQ가 의도한 CPU에 고정되었는지 확인 - NUMA 노드 — NIC과 동일한 NUMA 노드의 CPU/메모리 사용.
cat /sys/class/net/eth0/device/numa_node - Hugepage — UMEM에 hugepage 사용.
MAP_HUGETLB플래그 또는hugetlbfs - Batch size — 32~128 범위에서 최적값 탐색 (워크로드에 따라 다름)
- Ring size — 최소 4096 권장.
rx_fill_ring_empty_descs가 0이어야 정상 - C-states 비활성화 —
idle=poll또는processor.max_cstate=1부트 파라미터 - Turbo Boost 고정 — 일관된 클럭 주파수 유지
NUMA 고려사항
# NIC의 NUMA 노드 확인
$ cat /sys/class/net/eth0/device/numa_node
0
# NUMA 노드 0의 CPU 목록
$ numactl --hardware | grep "node 0 cpus"
node 0 cpus: 0 1 2 3 4 5
# NUMA-aware로 AF_XDP 앱 실행
$ sudo numactl --cpunodebind=0 --membind=0 ./af_xdp_app eth0
/* NUMA-aware UMEM 할당 */
#include <numa.h>
#include <numaif.h>
int numa_node = 0; /* NIC의 NUMA 노드 */
/* 1. UMEM 메모리를 특정 NUMA 노드에 할당 */
void *umem_area = mmap(NULL, umem_size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);
/* NUMA 바인딩 */
unsigned long nodemask = 1UL << numa_node;
mbind(umem_area, umem_size, MPOL_BIND, &nodemask,
sizeof(nodemask) * 8, MPOL_MF_MOVE | MPOL_MF_STRICT);
/* 2. 스레드도 동일 NUMA 노드에 고정 */
numa_run_on_node(numa_node);
커널 버전별 AF_XDP 변천사
| 커널 버전 | 변경사항 |
|---|---|
| 4.18 (2018) | AF_XDP 최초 도입. Copy 모드, i40e/ixgbe Zero-copy |
| 5.0 | AF_XDP 통계(XDP_STATISTICS), XDP_USE_NEED_WAKEUP 지원 |
| 5.3 | mlx5 ZC 지원, XDP_SHARED_UMEM (여러 소켓 UMEM 공유) |
| 5.4 | xsk_buff_pool 도입 — 기존 UMEM 관리 대폭 리팩토링 |
| 5.5 | ice 드라이버 ZC 지원 (Intel E810 100GbE) |
| 5.7 | XDP_UMEM_UNALIGNED_CHUNK_FLAG — Unaligned 청크 모드 |
| 5.9 | veth ZC 지원 — 컨테이너 네트워킹 가능 |
| 5.11 | AF_XDP busy-poll (SO_PREFER_BUSY_POLL) 지원 |
| 5.13 | bnxt_en, stmmac ZC 지원. Multi-buffer XDP 기초 |
| 5.14 | igc ZC 지원. XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD |
| 5.18 | AF_XDP multi-buffer (Jumbo frame 지원). tx_metadata_len |
| 6.0 | libxdp AF_XDP API 안정화. netlink 기반 XDP 관리 |
| 6.4 | virtio_net ZC 지원. AF_XDP TX metadata |
| 6.6 | TX 타임스탬프, TX 체크섬 오프로드 메타데이터 |
| 6.8+ | AF_XDP netdev generic queue API. XDP hints 확장 |
AF_XDP vs 다른 기술
| 특성 | AF_PACKET | AF_XDP (Copy) | AF_XDP (ZC) | DPDK | io_uring |
|---|---|---|---|---|---|
| 성능 (64B pps) | ~100K | ~1-3M | ~10-14M | ~20-24M | ~5-8M |
| 커널 통합 | 완전 | 완전 | 완전 | 없음 (우회) | 완전 |
| 일반 소켓 공존 | 가능 | 가능 | 가능 | 불가 | 가능 |
| 드라이버 지원 | 모든 NIC | XDP 지원 NIC | 일부 NIC | PMD 필요 | 모든 NIC |
| CPU 전용화 | 불필요 | 선택 | 권장 | 필수 | 선택 |
| 레이턴시 | ~100μs | ~20μs | ~2-5μs | ~1-2μs | ~10-20μs |
| 개발 복잡도 | 낮음 | 중간 | 중간 | 높음 | 중간 |
| 보안/격리 | 커널 보호 | 커널 보호 | 커널 보호 | 약함 | 커널 보호 |
| 최소 커널 버전 | 2.6 | 4.18 | 4.18 | N/A | 5.19+ |
- AF_PACKET — 단순 캡처/모니터링, tcpdump/Wireshark 수준
- AF_XDP — 커널 통합이 필요한 고성능 처리 (DNS, 로드밸런서, IDS)
- DPDK — 최대 성능이 최우선, NIC을 전용으로 사용 가능한 환경
- io_uring — 기존 소켓 API와 호환하면서 성능 개선이 필요한 경우
참고자료
공식 문서
- AF_XDP — Linux Kernel Documentation — 공식 커널 문서
- AF_XDP — eBPF Docs — 개발자 중심 AF_XDP 해설
- BPF Documentation Hub — BPF 커널 문서 최상위
- libbpf Overview (커널 문서)
튜토리얼 및 프로젝트
- XDP Tutorial — AF_XDP 실습
- XDP Tools — xdp-loader, xdp-filter 등
- libbpf (GitHub) — xsk.h AF_XDP API 포함
- libbpf API Documentation
주요 참고 글
- AF_XDP — zero-copy networking (LWN) — AF_XDP 초기 설계
- AF_XDP buffer allocation (LWN) — xsk_buff_pool API
- Cilium BPF & XDP Reference Guide
- Linux eBPF Tracing Tools (Brendan Gregg)
커널 소스 경로
| 파일 | 역할 |
|---|---|
net/xdp/xsk.c |
AF_XDP 소켓 구현 (bind, sendmsg, poll, recvmsg) |
net/xdp/xsk_buff_pool.c |
xsk_buff_pool — DMA 매핑, 버퍼 할당/해제 |
net/xdp/xsk_queue.h |
Fill/RX/TX/Completion SPSC Ring 구현 |
net/xdp/xsk_diag.c |
AF_XDP 소켓 진단 (ss --xdp) |
include/net/xdp_sock.h |
xdp_sock, xdp_umem 내부 구조체 |
include/net/xdp_sock_drv.h |
드라이버용 ZC API (xsk_buff_alloc 등) |
include/uapi/linux/if_xdp.h |
UAPI — xdp_desc, xdp_statistics, sockaddr_xdp, 플래그 |
include/net/xdp.h |
xdp_buff, xdp_frame, xdp_rxq_info 구조체 |
tools/lib/bpf/xsk.c |
libbpf AF_XDP userspace API 구현 |
tools/lib/bpf/xsk.h |
xsk_socket, xsk_umem, xsk_ring API 헤더 |
samples/bpf/xdpsock_user.c |
AF_XDP RX/TX/L2FWD 벤치마크 샘플 |
tools/testing/selftests/bpf/xdp* |
AF_XDP 커널 셀프테스트 |
drivers/net/ethernet/intel/ice/ice_xsk.c |
ice 드라이버 ZC 구현 (참조용) |
주요 발표/영상
- Björn Töpel, Magnus Karlsson — "AF_XDP — scalable high-performance packet processing" (Netdev 0x12, 2018) — AF_XDP 최초 발표
- Magnus Karlsson — "AF_XDP: scalable user-space networking" (Linux Plumbers Conference, 2019)
- Toke Høiland-Jørgensen — "XDP — challenges and future work" (Netdev 0x14, 2020) — libxdp 소개
- Maciej Fijalkowski — "AF_XDP multi-buffer support" (Netdev 0x16, 2022) — Jumbo frame 지원
- BPF/XDP — XDP 프로그래밍
- DPDK/SmartNIC — 고성능 패킷 처리
- 네트워크 스택 — 패킷 경로
관련 문서
- VPP (FD.io) 심화 — 고성능 유저스페이스 패킷 처리 — FD.io VPP 벡터 패킷 처리, 그래프 노드 아키텍처, DPDK 통합, 플러그인, 커널
- NAPI (New API) - 네트워크 패킷 처리 — NAPI 인터럽트 완화 메커니즘 심화: napi_struct, 폴링 버짓, GRO, 멀티큐
- 네트워크 스택 고급 — NAPI 심화, GRO, RSS/RPS/RFS, XPS, aRFS, 멀티코어 네트워크 분산
- NFQUEUE & DPI 엔진 통합 — nfnetlink_queue 내부 구조, libnetfilter_queue API, Sur
- TCP 프로토콜 심화 — Linux TCP 프로토콜 심화: tcp_sock, 상태 머신, 혼잡 제어(CUBIC/BB