AF_XDP (XDP Sockets)
AF_XDP는 XDP와 결합하여 커널 우회(kernel bypass) 없이 userspace로 초고속 패킷 전달을 제공하는 소켓 패밀리입니다. Zero-copy 모드에서 수백만 pps의 처리 성능을 달성합니다. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.
전제 조건: 네트워크 스택과 네트워크 디바이스 드라이버 문서를 먼저 읽으세요.
고성능 패킷 경로는 큐 구조, 메모리 배치, 드롭 위치가 성능을 좌우하므로 드라이버 경계를 먼저 이해하는 것이 중요합니다.
일상 비유: 이 주제는 고속 톨게이트 차선 분리와 비슷합니다.
일반 차선(커널 스택)과 하이패스 차선(XDP/DPDK)을 구분해 보면 왜 지연과 처리량이 달라지는지 명확해집니다.
핵심 요약
- 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처럼 커널을 완전히 우회하지 않고도 높은 성능을 얻을 수 있습니다.
주요 특징
- 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을 공유하여 메모리 효율을 높입니다.
/* 첫 번째 소켓: UMEM 생성 */
struct xsk_socket *xsk1;
xsk_socket__create(&xsk1, "eth0", 0, umem, &rx1, &tx1, &cfg);
/* 두 번째 소켓: 동일한 UMEM 공유 */
struct xsk_socket *xsk2;
cfg.bind_flags |= XDP_SHARED_UMEM;
xsk_socket__create(&xsk2, "eth0", 1, umem, &rx2, &tx2, &cfg);
/* xsk1, xsk2는 동일한 UMEM 공유 */
성능 최적화
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)
성능 벤치마크
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
커널 설정
CONFIG_XDP_SOCKETS=y # AF_XDP 지원
CONFIG_XDP_SOCKETS_DIAG=y # AF_XDP 진단
AF_XDP vs 다른 기술
| 기술 | 성능 | 커널 통합 | 드라이버 지원 | 사용 난이도 |
|---|---|---|---|---|
| AF_PACKET | ~100K pps (기본 모드) | 완전 통합 | 모든 NIC | 낮음 |
| AF_XDP (Copy) | ~1M pps | 완전 통합 | 모든 XDP NIC | 중간 |
| AF_XDP (Zero-copy) | ~10M+ pps | 완전 통합 | 일부 NIC | 중간 |
| DPDK | ~20M+ pps | 커널 우회 | DPDK PMD 필요 | 높음 |
참고자료
공식 문서
- 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)net/xdp/xsk_buff_pool.c— xsk 버퍼 풀 관리net/xdp/xsk_queue.h— Fill/RX/TX/Completion 링 구현include/net/xdp_sock.h— AF_XDP 내부 구조체include/uapi/linux/if_xdp.h— UAPI (xdp_desc, xdp_statistics, 플래그)tools/lib/bpf/xsk.c— libbpf AF_XDP 유저스페이스 APItools/lib/bpf/xsk.h— xsk_socket, xsk_umem, xsk_ring API 헤더samples/bpf/xdpsock_user.c— AF_XDP 벤치마크 샘플 프로그램
다음 학습:
- 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