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가 핵심 튜닝 요소

단계별 이해

  1. UMEM 준비
    frame size와 ring depth를 워크로드 특성에 맞춰 결정합니다.
  2. XDP 연결
    프로그램에서 대상 패킷을 AF_XDP 소켓으로 redirect합니다.
  3. 모드 선택
    copy/zero-copy 성능 차이와 드라이버 제약을 검증합니다.
  4. 운영 튜닝
    IRQ affinity, NAPI budget, batching 전략을 조정합니다.
관련 문서: BPF/XDP (XDP 프로그램), 네트워크 스택 (패킷 처리), DPDK/SmartNIC (성능 비교), sk_buff (패킷 버퍼)

개요

AF_XDP는 XDP 프로그램과 협력하여 선택된 패킷을 userspace 애플리케이션으로 직접 전달합니다. DPDK처럼 커널을 완전히 우회하지 않고도 높은 성능을 얻을 수 있습니다.

NIC RX Queue XDP_REDIRECT AF_XDP Socket RX/FILL Ring Userspace Poll Loop UMEM 소비 TX/COMPLETION Ring 재전송/회수

주요 특징

아키텍처

Userspace Application UMEM (User Memory) Pkt0 | Pkt1 | Pkt2 | ... | PktN Kernel/User Boundary AF_XDP Socket RX Ring (Consumer) TX Ring (Producer) XDP Program: if (selected) return XDP_REDIRECT; NIC Driver (Zero-Copy Mode) - RX Queue 0 | RX Queue 1 | RX Queue 2 Physical NIC

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 프레임 레이아웃

UMEM 프레임 구조 (예: 2048 바이트) 0 256 2048 (byte) Headroom 256 bytes Packet Data 최대 1792 bytes — 실제 패킷 Unused (여유) XDP 메타데이터/헤더 추가용 rx_batch로 채움, sendmsg()로 전송 Total: 2048 bytes (0x800) — chunk_size 정렬 단위 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) 데이터 흐름 상세

AF_XDP 수신(RX) 데이터 흐름 User / Kernel Boundary 1. Fill Queue 채우기 User: UMEM 빈 프레임 주소 제출 2. Fill → DMA Kernel: Fill에서 프레임 가져옴 3. NIC DMA 수신 패킷 → UMEM 프레임에 기록 4. RX Queue 게시 Kernel: RX Ring에 desc 추가 5. RX 패킷 소비 User: peek → 패킷 처리 프레임 재순환 User(Fill 채움) → Kernel(DMA 수신) → Kernel(RX 게시) → User(패킷 소비) → 프레임 재사용 순환

송신(TX) 데이터 흐름 상세

AF_XDP 송신(TX) 데이터 흐름 User / Kernel Boundary 1. TX Queue 채우기 User: 패킷 작성 + desc 제출 2. sendto() / kick User: 커널에 송신 알림 3. TX Ring 소비 Kernel: TX desc 가져옴 4. NIC DMA 송신 UMEM 프레임 → NIC 전송 5. Completion Queue Kernel: 완료 프레임 주소 반환 6. UMEM 프레임 회수 User: Comp peek → 재사용 프레임 재순환 User(TX 채움 + sendto) → Kernel(TX 소비 → DMA 전송) → Kernel(Completion) → User(프레임 회수) 순환

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 필요 높음

참고자료

공식 문서

튜토리얼 및 프로젝트

주요 참고 글

커널 소스 경로

다음 학습:
필수 관련 문서: 참고 문서: