AF_XDP (XDP Sockets)

AF_XDP는 XDP와 결합하여 커널 우회(kernel bypass) 없이 userspace로 초고속 패킷(Packet) 전달을 제공하는 소켓(Socket) 패밀리입니다. Zero-copy 모드에서 수백만 pps의 처리 성능을 달성합니다. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅(Debugging) 절차까지 실무 관점으로 다룹니다.

전제 조건: 네트워크 스택(Network Stack)네트워크 디바이스 드라이버 문서를 먼저 읽으세요. 고성능 패킷 경로는 큐 구조, 메모리 배치, 드롭 위치가 성능을 좌우하므로 드라이버 경계를 먼저 이해하는 것이 중요합니다.
일상 비유: 이 주제는 고속 톨게이트 차선 분리와 비슷합니다. 일반 차선(커널 스택)과 하이패스 차선(XDP/DPDK)을 구분해 보면 왜 지연(Latency)과 처리량(Throughput)이 달라지는지 명확해집니다.

핵심 요약

  • UMEM — 사용자 공간(User Space) 공유 패킷 메모리
  • 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 (패킷 버퍼(Buffer))

개요

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

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

AF_XDP 패킷 생애주기

AF_XDP 패킷 생애주기 (RX → 처리 → TX) NIC RX Wire → DMA → UMEM frame XDP 프로그램 필터/분류 XDP_REDIRECT RX Ring 커널 → User desc 게시 Userspace 처리 peek → 파싱 → 비즈니스 로직 → 응답 생성 TX Ring User → 커널 sendto() kick NIC TX DMA 전송 → Wire Completion Ring → Fill Ring (프레임 재순환) 전체 RTT (RX → 처리 → TX): Zero-copy ~2-5μs | Copy ~20-50μs 일반 소켓 경로 대비 10~100배 낮은 레이턴시

주요 특징

아키텍처

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);
코드 설명
  • 4-7행 xsk_umem_configfill_size/comp_size는 Fill Ring과 Completion Ring의 엔트리 수입니다. 커널은 이 값을 2의 거듭제곱으로 올림하며, ring_mask = nentries - 1로 비트마스크 인덱싱에 활용합니다. frame_size는 2048(최소) 또는 4096이 일반적이며, PAGE_SIZE보다 크면 커널이 거부합니다.
  • 8행 frame_headroom은 각 프레임의 데이터 시작 앞에 예약되는 공간입니다. XDP 프로그램이 bpf_xdp_adjust_head()로 헤더를 확장하거나, 메타데이터를 저장하는 데 사용됩니다. 0으로 설정하면 프레임 전체를 패킷 데이터에 사용할 수 있습니다.
  • 16-18행 MAP_HUGETLB로 hugepage 기반 mmap을 수행하면 TLB 미스가 줄어 고속 패킷 처리에서 성능이 향상됩니다. UMEM은 연속된 가상 주소 공간이어야 하므로, 충분한 hugepage가 미리 할당되어 있어야 합니다(/sys/kernel/mm/hugepages/).
  • 29-30행 xsk_umem__create()은 내부적으로 setsockopt(XDP_UMEM_REG)setsockopt(XDP_UMEM_FILL_RING/COMPLETION_RING)을 호출하여 커널에 UMEM 영역을 등록합니다. 커널은 이 메모리를 pin_user_pages()로 고정하여 DMA 중 페이지가 스왑아웃되지 않도록 보장합니다.

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을 공유하여 메모리 효율을 높입니다. XDP_SHARED_UMEM 플래그를 사용하면 N개 큐에 대해 하나의 UMEM만 할당하면 되므로, 멀티큐 환경에서 메모리 사용량이 1/N로 줄어듭니다.

Shared UMEM: 하나의 UMEM을 여러 소켓이 공유 공유 UMEM (Single Allocation) Frame 0 | Frame 1 | Frame 2 | ... | Frame N-1 Socket 0 (Queue 0) Fill Ring + Comp Ring Socket 1 (Queue 1) Fill Ring + Comp Ring Socket 2 (Queue 2) Fill Ring + Comp Ring Shared UMEM 핵심 규칙 1. 첫 번째 소켓: UMEM 생성 (XDP_SHARED_UMEM 없이) 2. 이후 소켓: XDP_SHARED_UMEM 플래그로 동일 UMEM에 바인딩 3. 각 소켓은 자신만의 Fill/Completion Ring을 가짐 (RX/TX Ring은 소켓별 독립)

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

/* 주의: 프레임 주소 관리를 프로그래머가 직접 해야 함
 * 같은 프레임을 두 소켓에서 동시에 사용하면 데이터 오염! */
Shared UMEM 주의사항:
  • 프레임 소유권 관리가 프로그래머 책임 — 동일 프레임을 두 소켓이 동시에 사용하면 안 됨
  • 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);
성능 차이: Hugepage 사용 시 TLB(Translation Lookaside Buffer) miss가 크게 줄어 10~20% 성능 향상이 가능합니다. 특히 UMEM 크기가 수십 MB 이상일 때 효과가 뚜렷합니다.

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는 큐 단위로 소켓을 바인딩하므로, 멀티큐 환경에서 자연스러운 병렬 처리가 가능합니다.

AF_XDP 멀티큐 + RSS 아키텍처 NIC (Physical / RSS Engine) RX Queue 0 RX Queue 1 RX Queue 2 RX Queue 3 XDP Program: bpf_redirect_map(&xsks_map, ctx->rx_queue_index, 0) AF_XDP Socket 0 Queue 0 바인딩 CPU 0 affinity AF_XDP Socket 1 Queue 1 바인딩 CPU 1 affinity AF_XDP Socket 2 Queue 2 바인딩 CPU 2 affinity AF_XDP Socket 3 Queue 3 바인딩 CPU 3 affinity Shared UMEM (XDP_SHARED_UMEM) Frame Pool: Pkt0 | Pkt1 | ... | PktN — 모든 소켓이 동일 메모리 영역 사용 Userspace Application (Thread per Queue — pthread_setaffinity_np)

멀티큐 설정 절차

# 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);
}
성능 팁: 멀티큐 AF_XDP에서 최고 성능을 위해서는 NIC Queue ↔ IRQ ↔ CPU ↔ AF_XDP Socket Thread를 1:1:1:1로 고정해야 합니다. 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 상태 전이 다이어그램 NAPI 활성 상태 need_wakeup = false 커널이 직접 Fill Ring에서 프레임을 가져감 → wakeup 불필요 NAPI 비활성 상태 need_wakeup = true 커널이 polling 중단 User가 poll()/recvmsg()로 깨워야 함 NAPI budget 소진 / 타임아웃 poll() / recvmsg() → 인터럽트 재활성화 User 행동 (NEED_WAKEUP 활성) 1. xsk_ring_prod__needs_wakeup(&fq) 검사 2. true → recvmsg() / poll() 호출 3. false → syscall 생략 (CPU 절약) Busy-poll 모드 (비교) NEED_WAKEUP 미사용 항상 spin-loop → CPU 100% 최저 레이턴시 but 최고 전력 소비

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 소켓으로 패킷과 함께 커스텀 메타데이터를 전달할 수 있습니다. 이를 통해 패킷 분류 결과, 타임스탬프, 해시(Hash) 값 등을 userspace로 효율적으로 전달합니다.

메타데이터 레이아웃

UMEM 프레임 메타데이터 레이아웃 0 meta_off headroom data_end frame_size Unused XDP Metadata hash, tstamp, mark Packet Data L2 + L3 + L4 + Payload Unused desc->addr은 Packet Data 시작 위치를 가리킴 메타데이터는 desc->addr - (ctx->data - ctx->data_meta) 위치 frame_headroom 내에 메타데이터 공간 확보 필요

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);
}
주의: 메타데이터를 사용하려면 UMEM의 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);
}
코드 설명
  • 2-6행 기본(aligned) 모드에서 프레임 주소는 frame_size의 배수여야 합니다. Unaligned 모드(XDP_UMEM_UNALIGNED_CHUNK_FLAG)는 이 제약을 제거하여 가변 크기 패킷에 맞게 프레임을 할당할 수 있습니다. 다만 커널의 주소 유효성 검사가 복잡해져 약간의 오버헤드가 발생합니다.
  • 9-12행 Unaligned 모드에서 descriptor의 addr은 48비트 주소 + 16비트 오프셋으로 인코딩됩니다. 상위 16비트(OFFSET_SHIFT = 48)는 프레임 내 오프셋, 하위 48비트는 UMEM 내 베이스 주소입니다. 이 인코딩 방식은 단일 64비트 값으로 주소와 오프셋을 모두 전달합니다.
  • 14-18행 xsk_umem__extract_addr()은 비트마스크로 하위 48비트를 추출하고, xsk_umem__extract_offset()은 상위 16비트를 시프트로 추출합니다. 커널은 이 함수들로 descriptor 주소를 디코딩하여 UMEM 프레임의 실제 위치를 결정합니다.
  • 전체 Unaligned 모드의 주요 이점은 메모리 효율성입니다. Aligned 모드에서 64바이트 패킷도 4096바이트 프레임 전체를 소비하지만, Unaligned 모드에서는 실제 패킷 크기에 가까운 프레임만 할당할 수 있습니다. 반면 Fill Ring에 반환할 주소 계산이 복잡해지는 단점이 있습니다.

커널 내부 자료구조

AF_XDP의 커널 측 구현은 net/xdp/ 디렉토리에 있으며, 핵심 자료구조들의 관계를 이해하면 성능 튜닝과 디버깅에 큰 도움이 됩니다.

AF_XDP 커널 내부 자료구조 관계도 struct xdp_sock (net/xdp/xsk.c) struct sock sk; struct xsk_queue *rx; struct xsk_queue *tx; struct xsk_buff_pool *pool; struct net_device *dev; u16 queue_id; /* zc, state, umem 등 */ struct xsk_buff_pool struct xsk_queue *fq; (Fill) struct xsk_queue *cq; (Comp) struct xdp_buff **free_heads; dma_addr_t *dma_pages; u32 chunk_size, headroom; u32 free_heads_cnt; /* addrs, umem, netdev 등 */ struct xsk_queue u32 ring_mask; u32 nentries; u32 cached_prod / cached_cons; struct xdp_ring *ring; /* mmap'd shared memory */ struct xdp_ring (UAPI) u32 producer; /* User 갱신 */ u32 consumer; /* Kernel 갱신 */ u32 flags; /* NEED_WAKEUP */ struct xdp_desc desc[]; /* lockless SPSC ring */ xdp_desc __u64 addr; __u32 len; __u32 options; net_device ndo_xsk_wakeup ndo_bpf struct xdp_buff void *data, *data_end, *data_meta; struct xsk_buff_pool *pool; pool rx/tx queue fq/cq queue ring desc[] dev xdp_buff per packet

xdp_sock 구조체(Struct) 상세

/* 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 대기열 */
};
코드 설명
  • 3행 struct sock sk는 일반 소켓 구조체의 임베딩(embedding) 패턴입니다. xdp_sk() 매크로가 container_of()로 sock에서 xdp_sock을 역참조하며, 이를 통해 기존 소켓 인프라(poll, epoll, select)를 그대로 활용할 수 있습니다.
  • 4-5행 rx/txstruct xsk_queue 포인터로, 커널↔사용자 공간 간 패킷 descriptor를 교환하는 SPSC Ring입니다. 소켓 생성 시 setsockopt(XDP_RX_RING/TX_RING)으로 할당되며, mmap으로 사용자 공간에 매핑됩니다.
  • 6행 map_list는 이 소켓이 등록된 XSKMAP 맵 목록입니다. XDP 프로그램에서 bpf_redirect_map(&xsks_map, queue_idx, 0) 호출 시 커널이 이 목록을 통해 대상 소켓을 찾습니다.
  • 9행 pool은 UMEM 버퍼 풀의 커널 측 관리 구조체입니다. bind() 호출 시 생성되며, Fill/Completion Ring과 DMA 매핑을 관리합니다. Shared UMEM 모드에서는 여러 소켓이 동일한 pool을 공유합니다.
  • 21행 zc 플래그는 bind()XDP_ZEROCOPY 요청과 드라이버 지원 여부에 따라 결정됩니다. true이면 NIC이 UMEM 메모리에 직접 DMA하고, false이면 커널이 드라이버 버퍼에서 UMEM으로 memcpy합니다.

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 모드 */
};
코드 설명
  • 3-4행 dev는 DMA API 호출에 필요한 디바이스 구조체이고, netdev는 바인딩된 네트워크 인터페이스입니다. Zero-copy 모드에서 커널은 dma_map_page(dev, ...)로 UMEM 페이지를 NIC의 DMA 주소 공간에 매핑합니다.
  • 7-8행 heads 배열은 각 UMEM 프레임에 대응하는 xdp_buff 헤더입니다. free_heads[]스택(LIFO) 구조의 가용 버퍼 목록으로, xsk_buff_alloc()--free_heads_cnt로 O(1) 할당을 수행합니다. LIFO는 최근 사용된 버퍼를 우선 재할당하여 CPU 캐시 히트율을 높입니다.
  • 12-13행 fq(Fill Queue)는 사용자→커널 방향으로 빈 프레임 주소를 공급하고, cq(Completion Queue)는 커널→사용자 방향으로 TX 완료된 프레임 주소를 반환합니다. Fill Ring이 비면 수신 불능(ENOBUFS), Completion Ring이 비면 TX 프레임 재활용 불가로 전송이 멈춥니다.
  • 15행 dma_pages는 UMEM 영역을 PAGE_SIZE 단위로 DMA 매핑한 주소 배열입니다. Zero-copy 드라이버는 이 주소를 NIC의 RX/TX descriptor에 직접 프로그래밍하여 memcpy 없는 패킷 수신/송신을 구현합니다.
  • 20행 cached_need_wakeup은 NAPI가 idle 상태인지를 캐시합니다. 사용자 공간이 Fill Ring에 프레임을 채운 후 poll()이나 sendto()로 커널을 깨울지 여부를 이 값으로 판단하여, 불필요한 syscall을 회피합니다.

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 패턴
 */
코드 설명
  • 2-8행 (xsk_queue) ring_masknentries - 1로 설정되며, 인덱스 계산 시 idx & ring_mask로 모듈로 연산을 비트마스크로 대체합니다. cached_prod/cached_cons는 공유 메모리의 producer/consumer 인덱스를 로컬에 캐싱하여 cache line 바운싱을 줄입니다.
  • 12-19행 (xdp_ring) producerconsumer는 각각 별도의 64바이트 cache line에 배치됩니다(aligned(CACHELINE_SIZE)). User가 producer를 갱신하고 Kernel이 consumer를 갱신하는 SPSC 패턴에서, 이 분리가 없으면 두 CPU 코어가 동일한 cache line을 경합하여 성능이 절반 이하로 떨어집니다.
  • 17행 flagsXDP_RING_NEED_WAKEUP 비트는 커널 NAPI가 idle 상태에 진입할 때 설정됩니다. 사용자 공간은 이 플래그를 읽어 poll() syscall이 필요한지 판단합니다. NAPI가 활발히 polling 중이면 syscall 없이 데이터가 자동으로 처리됩니다.
SPSC Ring: Producer/Consumer 인덱스 동작 Ring Buffer (nentries = 8, ring_mask = 7) desc[0] desc[1] desc[2] desc[3] desc[4] desc[5] desc[6] desc[7] consumer = 3 (다음 소비할 위치) producer = 5 (다음 생산할 위치) 소비 완료 (반환 가능) 데이터 보유 (소비 대기) 비어있음 사용 가능 슬롯 = producer - consumer = 5 - 3 = 2개 인덱스 접근: ring->desc[idx & ring_mask] → wrap-around 자동 처리

AF_XDP 시스템 콜(System Call) 경로

시스템 콜 커널 함수 동작
socket(AF_XDP, ...) xsk_create() xdp_sock 할당, 초기화
setsockopt(SOL_XDP, XDP_UMEM_REG) xsk_setsockopt() UMEM 등록, 페이지(Page) 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 매핑(Mapping)
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;
}
코드 설명
  • 7행 dev_get_by_index()sxdp_ifindex에 해당하는 net_device를 찾습니다. AF_XDP는 물리 NIC뿐 아니라 veth 같은 가상 인터페이스에도 바인딩할 수 있지만, zero-copy는 드라이버가 ndo_bpf를 구현한 물리 NIC에서만 동작합니다.
  • 10-11행 queue_id는 NIC의 RX 큐 번호입니다. 멀티큐 NIC에서 각 큐에 별도의 AF_XDP 소켓을 바인딩하면 락 경합 없이 병렬 수신이 가능합니다. real_num_rx_queues를 초과하면 EINVAL로 거부됩니다.
  • 14-18행 XDP_ZEROCOPY 플래그가 설정되면 xsk_check_common()이 드라이버의 ndo_bpf 콜백과 XDP_SETUP_XSK_POOL 명령 지원 여부를 확인합니다. 지원하지 않으면 zc가 false로 유지되어 자동으로 copy 모드로 폴백합니다.
  • 21-22행 xp_create_and_assign_umem()xsk_buff_pool을 할당하고 UMEM의 각 프레임에 대해 xdp_buff 헤더를 생성합니다. xp_dma_map()은 UMEM 전체 영역을 dma_map_page()로 매핑하여 NIC이 직접 접근할 수 있는 DMA 주소를 확보합니다.
  • 25-30행 Zero-copy 시 드라이버의 ndo_bpf(XDP_SETUP_XSK_POOL) 콜백이 호출되어 해당 RX 큐의 버퍼 할당자를 기존 page allocator에서 xsk_buff_pool로 교체합니다. 이후 NIC의 RX descriptor에는 UMEM 프레임의 DMA 주소가 직접 프로그래밍됩니다.

Copy vs Zero-Copy 내부 데이터 경로

AF_XDP의 두 가지 모드는 패킷이 NIC에서 userspace까지 도달하는 경로가 근본적으로 다릅니다. 이 차이가 성능 격차의 원인입니다.

Copy 모드 vs Zero-Copy 모드 데이터 경로 Copy 모드 Zero-Copy 모드 1. NIC DMA → 드라이버의 page(sk_buff) 2. XDP 프로그램 실행 → XDP_REDIRECT 반환 3. memcpy: 드라이버 page → UMEM 프레임 xsk_copy_xdp() — 성능 병목! 4. RX Ring에 descriptor 게시 5. User: peek → 패킷 처리 1. NIC DMA → UMEM 프레임에 직접 기록 2. XDP 프로그램 실행 → XDP_REDIRECT 반환 3. memcpy 없음! 주소만 전달 xsk_buff_free() — 제로카피 핵심 4. RX Ring에 descriptor 게시 5. User: peek → 패킷 처리 ~2-5M pps (memcpy 비용 포함) ~10-24M pps (memcpy 제거)

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;
}
코드 설명
  • 3-4행 data_end - data가 패킷 길이, data - data_meta가 XDP 메타데이터 길이입니다. Copy 모드에서는 메타데이터까지 포함하여 memcpy하므로, XDP 프로그램이 설정한 분류 정보가 사용자 공간까지 전달됩니다.
  • 6-8행 xskq_cons_peek_addr_unchecked()는 Fill Ring의 consumer 인덱스에서 다음 빈 프레임 주소를 읽습니다. Fill Ring이 비었으면 ENOBUFS를 반환하고 패킷이 드롭됩니다. 이것이 AF_XDP에서 가장 흔한 패킷 손실 원인이므로, 사용자 공간은 Fill Ring을 항상 충분히 채워야 합니다.
  • 10-11행 xsk_buff_raw_get_data()는 UMEM 프레임 주소를 가상 주소로 변환합니다. memcpy()가 Copy 모드의 성능 병목으로, 1500바이트 패킷에서 약 200ns가 소요됩니다. 이 단일 memcpy가 Copy 모드를 2-5 Mpps로 제한하는 원인입니다.
  • 14-18행 RX Ring에 게시되는 xdp_descaddr은 UMEM 내 프레임 오프셋이고 len은 패킷 길이입니다. 사용자 공간은 xsk_ring_cons__peek()로 이 descriptor를 읽고, addr로 UMEM 프레임에서 직접 패킷 데이터에 접근합니다.

Zero-Copy 모드 커널 경로

/* net/xdp/xsk_buff_pool.c + net/xdp/xsk.c — Zero-copy 경로 간략화 */

/* 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);
}
코드 설명
  • 3-8행 xsk_buff_alloc()free_heads 스택에서 사전 할당된 xdp_buff_xsk를 O(1)로 꺼냅니다. 이 버퍼의 DMA 주소는 이미 xp_dma_map()에서 매핑되어 있으므로, 드라이버는 추가 DMA 매핑 없이 NIC의 RX descriptor에 직접 프로그래밍할 수 있습니다.
  • 5-7행 orig_addr + headroom이 패킷 데이터의 시작점입니다. data_end = data로 초기화하고, NIC이 DMA로 데이터를 기록한 후 드라이버가 data_end를 실제 패킷 길이만큼 전진시킵니다.
  • 14행 xsk_rcv_zc()는 Copy 모드의 xsk_rcv()와 달리 memcpy를 수행하지 않습니다. NIC이 이미 UMEM 프레임에 직접 DMA했으므로, xp_get_handle()이 반환하는 UMEM 오프셋 주소만 RX Ring에 게시합니다. 이것이 10-24 Mpps를 달성하는 핵심입니다.
  • 15-18행 container_of()xdp_buff에서 xdp_buff_xsk를 역참조하고, xp_get_handle()이 UMEM 내 오프셋 주소를 계산합니다. 이 주소가 사용자 공간에서 UMEM_area + addr로 직접 패킷에 접근하는 데 사용됩니다.

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 컨테이너(Container) 네트워킹
virtio_net QEMU/KVM VirtIO 6.4 가상화(Virtualization) 환경

실전 활용 사례

고성능 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 기반 DNS 서버는 일반 소켓 기반 대비 5~10배 높은 QPS(Queries per Second)를 달성할 수 있습니다. Meta의 Katran 로드밸런서, Cloudflare의 DNS 파이프라인(Pipeline) 등이 유사한 아키텍처를 사용합니다.

고속 패킷 캡처

/* 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 하이브리드 패턴: XDP 프로그램에서 빠른 1차 필터링(IP 블랙리스트, 레이트 리밋)을 수행하고, 정밀 검사가 필요한 패킷만 AF_XDP로 전달하면 CPU 효율이 극대화됩니다. Suricata IDS가 이 패턴을 AF_XDP 백엔드로 지원합니다(7.0+).
/* 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를 지원하면서, 컨테이너 네트워킹에서도 고성능 패킷 처리가 가능해졌습니다.

컨테이너 환경 AF_XDP (veth ZC) Host Namespace Container Namespace Physical NIC (eth0) + XDP veth_host (veth pair 호스트 측) AF_XDP Socket (호스트) veth_host 바인딩 + ZC 호스트 애플리케이션 / 프록시 veth_container (veth pair 컨테이너 측) AF_XDP Socket (컨테이너) veth_container 바인딩 + ZC 컨테이너 애플리케이션 veth pair CAP_NET_RAW + CAP_BPF 필요 또는 unprivileged BPF 허용

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, ...) 사용
제약사항: veth AF_XDP는 물리 NIC 대비 성능이 낮습니다 (veth 자체의 오버헤드(Overhead)). 그러나 일반 소켓 대비 3~5배 빠르며, 컨테이너 사이드카 프록시(Envoy, Cilium 등)에서 유용합니다.

libxdp API

libxdp는 libbpf 위에 구축된 XDP 전용 라이브러리로, 멀티 프로그램 디스패치(Dispatch)와 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, 스레드(Thread) 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

성능 최적화 체크리스트

성능이 기대보다 낮을 때 순서대로 확인:
  1. Zero-copy 모드 활성화 여부ethtool -S eth0 | grep xsk에 ZC 통계가 나오는지 확인
  2. IRQ affinitycat /proc/interrupts로 NIC IRQ가 의도한 CPU에 고정되었는지 확인
  3. NUMA 노드 — NIC과 동일한 NUMA 노드의 CPU/메모리 사용. cat /sys/class/net/eth0/device/numa_node
  4. Hugepage — UMEM에 hugepage 사용. MAP_HUGETLB 플래그 또는 hugetlbfs
  5. Batch size — 32~128 범위에서 최적값 탐색 (워크로드에 따라 다름)
  6. Ring size — 최소 4096 권장. rx_fill_ring_empty_descs가 0이어야 정상
  7. C-states 비활성화idle=poll 또는 processor.max_cstate=1 부트 파라미터
  8. 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 체크섬(Checksum) 오프로드 메타데이터
6.8+ AF_XDP netdev generic queue API. XDP hints 확장

AF_XDP vs 다른 기술

패킷 처리 기술 성능 비교 (64B 패킷, 단일 큐) Mpps (백만 pps) 25 20 15 10 5 0 ~0.1M AF_PACKET ~1M AF_PKT (MMAP) ~3M AF_XDP (Copy) ~14M AF_XDP (Zero-copy) ~24M DPDK (PMD) ~8M io_uring (zerocopy) ※ 수치는 단일 코어, 64B 패킷, 10GbE NIC 기준 근사값 (환경에 따라 달라짐)
특성 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
개발 복잡도 낮음 중간 중간 높음 중간
보안/격리(Isolation) 커널 보호 커널 보호 커널 보호 약함 커널 보호
최소 커널 버전 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 멀티스레드/멀티큐 설계 패턴

AF_XDP의 실전 배포에서는 NIC의 RSS(Receive Side Scaling) 큐를 여러 AF_XDP 소켓과 매핑하여 병렬 처리를 달성합니다. 설계 패턴은 크게 poll 기반, busy-poll 기반, NAPI 연동의 세 가지로 나뉩니다.

AF_XDP 멀티큐/멀티스레드 아키텍처 NIC (RSS 큐 분배) RX Queue 0 RX Queue 1 RX Queue 2 RX Queue 3 ... XSK Socket 0 UMEM 0 / Ring 0 XSK Socket 1 UMEM 1 / Ring 1 XSK Socket 2 Shared UMEM XSK Socket 3 Shared UMEM Worker Thread 0 CPU 0 pinned Worker Thread 1 CPU 1 pinned Worker Thread 2 CPU 2 pinned Worker Thread 3 CPU 3 pinned XDP_SHARED_UMEM 핵심: IRQ affinity = CPU affinity = Thread affinity (1:1:1 매핑) 캐시 라인 경합 최소화 → 선형 스케일링 (N코어 ≈ N배 성능)

poll vs busy-poll 패턴

poll() 기반은 패킷이 없을 때 CPU를 양보(Yield)하므로 에너지 효율적이지만, 웨이크업 지연이 발생합니다. busy-poll은 소켓 옵션 SO_PREFER_BUSY_POLLSO_BUSY_POLL_BUDGET을 사용하여 NAPI 컨텍스트에서 직접 폴링(Polling)합니다.

/* busy-poll 설정 예제 */
int enable = 1;
int budget = 64;
int timeout_us = 20;  /* busy-poll 타임아웃 (마이크로초) */

setsockopt(xsk_fd, SOL_SOCKET, SO_PREFER_BUSY_POLL, &enable, sizeof(enable));
setsockopt(xsk_fd, SOL_SOCKET, SO_BUSY_POLL, &timeout_us, sizeof(timeout_us));
setsockopt(xsk_fd, SOL_SOCKET, SO_BUSY_POLL_BUDGET, &budget, sizeof(budget));
성능 선택 기준:
  • poll() — 트래픽 변동이 크고 CPU 절약이 중요한 환경 (서버 통합)
  • busy-poll — 일정한 트래픽에서 지연시간이 중요한 환경 (2-5μs 감소)
  • 순수 spin loop — 전용 코어를 사용하는 최대 성능 환경 (DPDK 스타일)

NAPI 연동 패턴

커널 6.4+에서는 XDP_USE_NEED_WAKEUP 플래그와 NAPI의 busy-poll이 결합되어 불필요한 시스템 콜을 제거합니다. NAPI 폴링 루프 내에서 AF_XDP RX ring을 직접 채우므로 컨텍스트 스위치 없이 패킷을 수신합니다.

/* NAPI busy-poll + NEED_WAKEUP 결합 패턴 */
static void rx_loop(struct xsk_socket_info *xsk)
{
    struct pollfd fds = { .fd = xsk_socket__fd(xsk->xsk), .events = POLLIN };

    while (running) {
        /* NEED_WAKEUP 플래그 확인 — 커널이 wakeup 필요 시에만 poll 호출 */
        if (xsk_ring_prod__needs_wakeup(&xsk->fq)) {
            poll(&fds, 1, 0);  /* timeout=0: 즉시 반환 */
        }

        unsigned int rcvd = xsk_ring_cons__peek(&xsk->rx, BATCH_SIZE, &idx_rx);
        if (rcvd == 0)
            continue;

        /* 배치 처리 */
        for (int i = 0; i < rcvd; i++) {
            const struct xdp_desc *desc = xsk_ring_cons__rx_desc(&xsk->rx, idx_rx + i);
            uint8_t *pkt = xsk_umem__get_data(xsk->umem_area, desc->addr);
            process_packet(pkt, desc->len);
        }
        xsk_ring_cons__release(&xsk->rx, rcvd);
        refill_fill_ring(xsk, rcvd);
    }
}

Thread-per-Queue 구현

가장 일반적인 멀티스레드 패턴은 NIC 큐 당 하나의 전용 스레드를 배정하는 것입니다. 각 스레드는 자신의 XSK 소켓만 담당하므로 락 경합(Contention)이 발생하지 않습니다.

struct worker_ctx {
    int queue_id;
    int cpu_id;
    struct xsk_socket_info *xsk;
    pthread_t thread;
};

static void *worker_fn(void *arg)
{
    struct worker_ctx *ctx = arg;
    cpu_set_t cpuset;

    /* CPU pinning — IRQ affinity와 동일한 CPU에 고정 */
    CPU_ZERO(&cpuset);
    CPU_SET(ctx->cpu_id, &cpuset);
    pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);

    while (running) {
        unsigned int rcvd, idx_rx;
        rcvd = xsk_ring_cons__peek(&ctx->xsk->rx, BATCH_SIZE, &idx_rx);
        if (rcvd == 0) {
            /* NEED_WAKEUP 확인 후 poll */
            if (xsk_ring_prod__needs_wakeup(&ctx->xsk->fq))
                poll(&(struct pollfd){.fd = xsk_socket__fd(ctx->xsk->xsk), .events = POLLIN}, 1, 100);
            continue;
        }
        process_batch(ctx->xsk, idx_rx, rcvd);
        xsk_ring_cons__release(&ctx->xsk->rx, rcvd);
        refill_fill_ring(ctx->xsk, rcvd);
    }
    return NULL;
}

/* 메인: N개 큐에 대해 N개 스레드 생성 */
for (int i = 0; i < num_queues; i++) {
    workers[i].queue_id = i;
    workers[i].cpu_id = i;  /* ethtool -L로 큐 수 맞춤 */
    setup_xsk_socket(&workers[i]);
    pthread_create(&workers[i].thread, NULL, worker_fn, &workers[i]);
}
주의: IRQ affinity, 스레드 CPU affinity, XSK 큐 바인딩이 일치하지 않으면 크로스-코어 캐시(Cache) 바운싱이 발생하여 성능이 50% 이상 저하될 수 있습니다. ethtool -L/proc/irq/N/smp_affinity를 반드시 확인하세요.

AF_XDP + DPDK PMD 백엔드 연동

DPDK 20.11+에서는 net_af_xdp PMD를 제공하여 DPDK 애플리케이션이 AF_XDP를 백엔드로 사용할 수 있습니다. 커널 네트워크 스택과 공존하면서도 DPDK의 고성능 프레임워크를 활용할 수 있는 하이브리드 접근법입니다.

AF_XDP + DPDK PMD 연동 구조 Userspace DPDK Application rte_eth_rx/tx_burst() net_af_xdp PMD (af_xdp_pmd) libxdp / libbpf xsk API Kernel XDP 프로그램 AF_XDP Socket 커널 네트워크 스택 NIC (Zero-Copy DMA) RSS → XDP_REDIRECT → AF_XDP | XDP_PASS → 커널 스택

DPDK PMD 설정

# DPDK EAL 파라미터로 AF_XDP PMD 사용
# --vdev: 가상 디바이스로 AF_XDP PMD 생성
dpdk-testpmd \
  --vdev="net_af_xdp0,iface=eth0,start_queue=0,queue_count=4" \
  --no-pci \
  -- -i --nb-cores=4 --rxq=4 --txq=4

# DPDK에서 AF_XDP 장점:
#  1. 커널 네트워크 스택과 공존 (다른 트래픽은 커널로)
#  2. NIC 전용 바인딩 불필요 (vfio-pci 없음)
#  3. 기존 DPDK 앱 코드 변경 최소화
/* DPDK 코드에서 AF_XDP PMD 사용 — 기존 DPDK 코드와 동일 API */
#include <rte_ethdev.h>

/* PMD가 내부적으로 AF_XDP 소켓 생성/관리 */
uint16_t nb_rx = rte_eth_rx_burst(port_id, queue_id, pkts, BURST_SIZE);
for (uint16_t i = 0; i < nb_rx; i++) {
    struct rte_mbuf *m = pkts[i];
    /* rte_pktmbuf_mtod()로 패킷 데이터 접근 — UMEM에 매핑됨 */
    uint8_t *data = rte_pktmbuf_mtod(m, uint8_t *);
    process_dpdk_packet(data, m->data_len);
}
/* TX 경로도 동일 */
uint16_t nb_tx = rte_eth_tx_burst(port_id, queue_id, tx_pkts, nb_tx_pkts);
DPDK PMD 제약사항:
  • Zero-copy 모드는 드라이버 지원 필요 (intel/ice, mlx5 등)
  • PMD 내부에서 XDP 프로그램을 자동 로드하므로 기존 XDP 프로그램과 충돌 가능
  • need_wakeup 기능을 사용하려면 커널 5.11+ 필요
  • rte_mbuf ↔ UMEM frame 변환 오버헤드가 있어 순수 AF_XDP 대비 ~5-10% 성능 감소

AF_XDP 기반 고성능 패킷 캡처

AF_XDP를 활용하면 tcpdump/AF_PACKET 대비 10배 이상의 패킷 캡처 성능을 달성할 수 있습니다. 특히 10G/25G 이상의 고속 링크에서 패킷 손실 없는 전수 캡처가 가능합니다.

AF_XDP 고성능 패킷 캡처 파이프라인 NIC RX Wire-speed 수신 RSS 큐 분배 XDP BPF 필터 포트/프로토콜 사전 필터링 AF_XDP Socket Zero-copy 수신 배치 처리 Lock-free Ring 타임스탬프 추가 메타데이터 기록 파일 Writer pcapng / 커스텀 io_uring 비동기 I/O 성능 비교 (10G NIC, 64-byte 패킷) tcpdump: ~1.5 Mpps | AF_PACKET MMAP: ~5 Mpps | AF_XDP ZC: ~14.8 Mpps 캡처 스레드 (Producer) - XSK RX ring peek + 배치 복사 - HW timestamp (xdp_metadata) - 큐 당 1 스레드, CPU pinned 기록 스레드 (Consumer) - Lock-free ring에서 배치 읽기 - io_uring으로 비동기 파일 쓰기 - 파일 로테이션/압축

캡처 구현 핵심

/* AF_XDP 기반 고성능 패킷 캡처 핵심 루프 */
#include <linux/if_xdp.h>
#include <bpf/xsk.h>
#include <time.h>

struct capture_record {
    uint64_t timestamp_ns;   /* CLOCK_REALTIME 나노초 */
    uint32_t caplen;         /* 캡처된 길이 */
    uint32_t origlen;        /* 원본 패킷 길이 */
    uint32_t queue_id;       /* 수신 큐 번호 */
    /* 뒤에 패킷 데이터 */
};

static void capture_loop(struct xsk_socket_info *xsk, struct ring_buffer *rb)
{
    unsigned int rcvd, idx_rx;
    struct timespec ts;

    while (running) {
        rcvd = xsk_ring_cons__peek(&xsk->rx, BATCH_SIZE, &idx_rx);
        if (rcvd == 0) {
            if (xsk_ring_prod__needs_wakeup(&xsk->fq))
                recvmsg(xsk_socket__fd(xsk->xsk), NULL, MSG_DONTWAIT);
            continue;
        }

        clock_gettime(CLOCK_REALTIME, &ts);
        uint64_t now_ns = ts.tv_sec * 1000000000ULL + ts.tv_nsec;

        for (unsigned int i = 0; i < rcvd; i++) {
            const struct xdp_desc *desc = xsk_ring_cons__rx_desc(&xsk->rx, idx_rx + i);
            uint8_t *pkt = xsk_umem__get_data(xsk->umem_area, desc->addr);

            /* lock-free ring에 기록 */
            struct capture_record *rec = ring_buffer_reserve(rb,
                sizeof(*rec) + desc->len);
            if (rec) {
                rec->timestamp_ns = now_ns;
                rec->caplen = desc->len;
                rec->origlen = desc->len;
                memcpy(rec + 1, pkt, desc->len);
                ring_buffer_commit(rb, rec);
            }
        }
        xsk_ring_cons__release(&xsk->rx, rcvd);
        refill_fill_ring(xsk, rcvd);
    }
}
HW 타임스탬프: 커널 6.3+에서는 bpf_xdp_metadata_rx_timestamp() kfunc를 사용하여 NIC 하드웨어 타임스탬프를 XDP 메타데이터로 전달받을 수 있습니다. PTP 동기화된 NIC에서 나노초 정밀도의 타임스탬프가 가능합니다.

커널 6.x AF_XDP 최신 변경

커널 6.x 시리즈에서는 AF_XDP의 기능과 성능이 대폭 개선되었습니다. multi-buffer 지원, TX 메타데이터, netdev netlink 통합 등 주요 변경 사항을 다룹니다.

AF_XDP Multi-Buffer 처리 흐름 (커널 6.6+) Jumbo Frame (9000 bytes) → 분할 수신 Fragment 0 (4096B) Fragment 1 (4096B) Fragment 2 (808B) XDP_PKT_CONTD flag UMEM Frames (4096B 단위) 각 fragment가 별도 UMEM frame에 매핑 — RX desc에 XDP_PKT_CONTD 비트 설정 Userspace 재조립 1. RX desc에서 XDP_PKT_CONTD 확인 2. 연속 desc를 이어붙여 완전한 패킷 재구성 (scatter-gather) 6.x 주요 변경 6.3: XDP metadata kfunc 6.4: busy-poll + wakeup 6.6: multi-buffer XSK 6.7: TX metadata launch 6.8: netdev netlink NAPI 6.9: TX timestamp 6.11: queue mgmt netlink multi-buffer 사용 시: XDP_USE_SG 플래그 + 드라이버 지원 필수

Multi-Buffer 지원 (커널 6.6+)

커널 6.6에서 도입된 multi-buffer 지원은 AF_XDP가 MTU 9000(jumbo frame)을 처리할 수 있게 합니다. 기존에는 UMEM frame 크기(보통 4096)를 초과하는 패킷을 수신할 수 없었습니다.

/* Multi-buffer AF_XDP 소켓 생성 */
struct xsk_socket_config cfg = {
    .rx_size = 4096,
    .tx_size = 4096,
    .bind_flags = XDP_USE_NEED_WAKEUP | XDP_USE_SG,  /* XDP_USE_SG: multi-buffer 활성화 */
};

/* RX 수신 시 multi-buffer 처리 */
static void handle_multi_buffer_rx(struct xsk_socket_info *xsk)
{
    unsigned int rcvd, idx;
    rcvd = xsk_ring_cons__peek(&xsk->rx, BATCH_SIZE, &idx);

    unsigned int i = 0;
    while (i < rcvd) {
        /* 패킷 시작 — 프래그먼트 수집 */
        uint32_t total_len = 0;
        unsigned int frag_start = i;

        do {
            const struct xdp_desc *desc = xsk_ring_cons__rx_desc(&xsk->rx, idx + i);
            uint8_t *frag = xsk_umem__get_data(xsk->umem_area, desc->addr);

            memcpy(reassembly_buf + total_len, frag, desc->len);
            total_len += desc->len;
            i++;
        } while ((i < rcvd) &&
                 (xsk_ring_cons__rx_desc(&xsk->rx, idx + i - 1)->options & XDP_PKT_CONTD));

        /* 완전한 패킷 처리 */
        process_complete_packet(reassembly_buf, total_len);
    }
    xsk_ring_cons__release(&xsk->rx, rcvd);
}
코드 설명
  • 4행 XDP_USE_SG(Scatter-Gather) 플래그는 커널 6.6+에서 AF_XDP의 멀티버퍼 모드를 활성화합니다. 이 플래그 없이는 UMEM frame_size(보통 4096)를 초과하는 패킷이 드롭되어, 9000 MTU 점보 프레임을 처리할 수 없었습니다.
  • 14-21행 멀티버퍼 패킷은 여러 연속된 RX descriptor로 표현됩니다. 마지막 프래그먼트를 제외한 모든 descriptor의 optionsXDP_PKT_CONTD 플래그가 설정됩니다. 사용자 공간은 이 플래그가 없는 descriptor를 만날 때까지 프래그먼트를 수집하여 패킷을 재조립합니다.
  • 16행 xsk_umem__get_data()umem_area + desc->addr로 프래그먼트 데이터 포인터를 계산합니다. 각 프래그먼트는 별도의 UMEM 프레임에 저장되므로, 재조립 시 연속 버퍼로 memcpy()해야 합니다.

TX Metadata (커널 6.7+)

TX 메타데이터를 통해 userspace에서 TX 경로의 하드웨어 오프로드를 요청할 수 있습니다. 체크섬 오프로드, TX 타임스탬프 요청 등이 가능합니다.

/* TX metadata로 체크섬 오프로드 요청 */
struct xsk_tx_metadata {
    __u64 flags;
    union {
        struct {
            __u16 csum_start;
            __u16 csum_offset;
        } request;
        struct {
            __u64 tx_timestamp;   /* 6.9+: TX 완료 타임스탬프 */
        } completion;
    };
};

/* TX desc에 메타데이터 연결 */
struct xdp_desc *desc = xsk_ring_prod__tx_desc(&xsk->tx, idx);
desc->addr = frame_addr;
desc->len = pkt_len;
desc->options = XDP_TX_METADATA;

/* 메타데이터는 패킷 데이터 앞에 배치 */
struct xsk_tx_metadata *meta = (void *)(xsk->umem_area + frame_addr - sizeof(*meta));
meta->flags = XDP_TXMD_FLAGS_CHECKSUM;
meta->request.csum_start = 14 + 20;  /* L4 시작 오프셋 */
meta->request.csum_offset = 6;       /* UDP 체크섬 필드 오프셋 */
코드 설명
  • 2-12행 xsk_tx_metadata는 커널 6.7+에서 도입된 TX 경로의 하드웨어 오프로드 인터페이스입니다. request 유니온은 전송 시 체크섬 오프로드 파라미터를, completion 유니온은 TX 완료 시 하드웨어 타임스탬프를 담습니다. 드라이버는 xsk_tx_metadata_request 콜백으로 이 요청을 NIC descriptor에 반영합니다.
  • 15행 desc->options = XDP_TX_METADATA는 이 descriptor에 메타데이터가 첨부되었음을 커널에 알립니다. 커널은 이 플래그를 확인한 후 패킷 데이터 앞에서 xsk_tx_metadata 구조체를 읽어 하드웨어 오프로드를 설정합니다.
  • 18-21행 메타데이터는 패킷 데이터 앞(frame_addr - sizeof(*meta))에 배치됩니다. csum_start는 체크섬 계산 시작 오프셋(L4 헤더 시작), csum_offset은 체크섬 값이 기록될 필드 위치입니다. 이를 통해 CPU 대신 NIC 하드웨어가 체크섬을 계산하여 TX 성능이 향상됩니다.

커널 6.8에서 도입된 netdev generic netlink를 통해 AF_XDP 큐 상태를 실시간(Real-time)으로 모니터링할 수 있습니다.

# AF_XDP 소켓 상태 조회 (커널 6.8+)
# netdev netlink — NAPI 인스턴스 및 큐 매핑 확인
python3 tools/net/ynl/cli.py --spec Documentation/netlink/specs/netdev.yaml \
  --dump queue-get --json '{"ifindex": 2}'

# 출력 예시:
# [{"id": 0, "type": "rx", "napi-id": 513, "ifindex": 2},
#  {"id": 0, "type": "tx", "napi-id": 513, "ifindex": 2},
#  {"id": 1, "type": "rx", "napi-id": 514, "ifindex": 2}, ...]

# NAPI 상태 조회
python3 tools/net/ynl/cli.py --spec Documentation/netlink/specs/netdev.yaml \
  --dump napi-get --json '{"ifindex": 2}'

# 큐 단위 AF_XDP 통계 (6.11+)
ethtool -S eth0 | grep xsk
#   rx_queue_0_xsk_packets: 1523847
#   rx_queue_0_xsk_bytes: 97526208
#   rx_queue_0_xsk_drops: 0
커널 버전기능설명
6.3XDP metadata kfuncbpf_xdp_metadata_rx_timestamp(), bpf_xdp_metadata_rx_hash()
6.4busy-poll 개선SO_PREFER_BUSY_POLL + NEED_WAKEUP 결합 최적화
6.6Multi-buffer XSKXDP_USE_SG 플래그, jumbo frame 지원
6.7TX metadataXDP_TX_METADATA, 체크섬 오프로드
6.8Netdev netlinkNAPI/큐 상태 조회 API
6.9TX timestampTX 완료 시 HW 타임스탬프 반환
6.11큐 관리 netlink큐 생성/삭제/바인딩 API 통합

AF_XDP 성능 벤치마크 및 튜닝 가이드

AF_XDP의 성능은 하드웨어, 커널 버전, 드라이버, 그리고 tuning 파라미터에 크게 영향을 받습니다. 체계적인 벤치마크와 단계별 튜닝 가이드를 제공합니다.

AF_XDP 성능 튜닝 계층 H/W NIC 선택 (ZC 지원) NUMA 배치 PCIe 대역폭 RSS 큐 수 영향도: ★★★★★ 커널 ZC 드라이버 활성화 NEED_WAKEUP NAPI budget IRQ coalescing 영향도: ★★★★ 배치 크기 (32-64) CPU pinning busy-poll UMEM 크기 영향도: ★★★ OS hugepages (UMEM) isolcpus irqbalance off C-state 제한 영향도: ★★

벤치마크 방법론

# 1. 기본 벤치마크: xdpsock 샘플 사용
cd linux/samples/bpf
make xdpsock

# RX 성능 테스트 (Zero-copy)
sudo ./xdpsock -i eth0 -r -z -q 0

# TX 성능 테스트
sudo ./xdpsock -i eth0 -t -z -q 0

# L2 포워딩 (RX→TX)
sudo ./xdpsock -i eth0 -l -z -q 0

# 2. 멀티큐 벤치마크
for q in 0 1 2 3; do
    sudo taskset -c $q ./xdpsock -i eth0 -r -z -q $q &
done

# 3. 트래픽 생성 (상대측에서)
# T-Rex / MoonGen / pktgen-dpdk 사용
sudo pktgen-dpdk -l 0-3 -n 4 -- -P -m "[1:2].0"

튜닝 체크리스트

카테고리튜닝 항목명령어/설정예상 효과
NIC RSS 큐 수 = CPU 수 ethtool -L eth0 combined 4 선형 확장
NIC IRQ coalescing ethtool -C eth0 rx-usecs 0 rx-frames 64 레이턴시 감소
NIC Ring buffer 크기 ethtool -G eth0 rx 4096 tx 4096 버스(Bus)트 흡수
CPU IRQ affinity echo N > /proc/irq/IRQ_NUM/smp_affinity_list 캐시 미스 감소
CPU CPU isolation isolcpus=2-7 nohz_full=2-7 간섭 제거
메모리 Hugepages (UMEM) echo 512 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages TLB 미스 감소
메모리 NUMA-local 할당 numactl --membind=0 ./xdp_app 메모리 지연 감소
전력 C-state 제한 processor.max_cstate=1 intel_idle.max_cstate=0 웨이크업 지연 제거
배치 크기 코드: BATCH_SIZE = 64 시스콜 오버헤드 감소
FILL ring 프리필 코드: ring depth의 75% 선채움 기아(Starvation) 방지
성능 기준점 (Intel E810 25G, 64-byte 패킷, 단일 큐):
  • Copy 모드: ~5 Mpps (RX), ~4 Mpps (TX)
  • Zero-copy: ~24 Mpps (RX), ~24 Mpps (TX)
  • Zero-copy + busy-poll: ~26 Mpps (RX)
  • 4큐 병렬: ~90 Mpps (RX, 거의 선형 확장)

실습: AF_XDP 소켓 프로그래밍 기초

이 실습에서는 AF_XDP 소켓을 생성하고 패킷을 수신하는 완전한 프로그램을 단계별로 작성합니다. 가상 환경(veth 페어)을 사용하므로 물리 NIC 없이도 테스트할 수 있습니다.

환경 준비

# 필수 패키지 설치
sudo apt-get install -y clang llvm libbpf-dev libxdp-dev linux-headers-$(uname -r)

# 또는 Fedora/RHEL:
sudo dnf install -y clang llvm libbpf-devel libxdp-devel kernel-headers

# veth 페어 생성 (테스트용)
sudo ip netns add ns_xdp
sudo ip link add veth0 type veth peer name veth1
sudo ip link set veth1 netns ns_xdp
sudo ip addr add 10.0.0.1/24 dev veth0
sudo ip netns exec ns_xdp ip addr add 10.0.0.2/24 dev veth1
sudo ip link set veth0 up
sudo ip netns exec ns_xdp ip link set veth1 up

# veth에서 XDP 사용을 위해 큐 확인
ethtool -l veth0

XDP 프로그램 작성

/* xdp_redirect.bpf.c — AF_XDP로 패킷 리다이렉트하는 XDP 프로그램 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

/* XSKMAP: AF_XDP 소켓 매핑 */
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_redirect(struct xdp_md *ctx)
{
    int queue_id = ctx->rx_queue_index;

    /* XSKMAP에 등록된 소켓이 있으면 리다이렉트 */
    if (bpf_map_lookup_elem(&xsks_map, &queue_id))
        return bpf_redirect_map(&xsks_map, queue_id, XDP_PASS);

    /* 매핑 없으면 커널 스택으로 전달 */
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

Userspace 수신 프로그램

/* af_xdp_rx.c — 기본 AF_XDP 수신 프로그램 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <poll.h>
#include <net/if.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <arpa/inet.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <xdp/xsk.h>

#define NUM_FRAMES    4096
#define FRAME_SIZE    XSK_UMEM__DEFAULT_FRAME_SIZE  /* 4096 */
#define BATCH_SIZE    64
#define FILL_RING_SZ  XSK_RING_PROD__DEFAULT_NUM_DESCS
#define COMP_RING_SZ  XSK_RING_CONS__DEFAULT_NUM_DESCS
#define RX_RING_SZ    XSK_RING_CONS__DEFAULT_NUM_DESCS
#define TX_RING_SZ    XSK_RING_PROD__DEFAULT_NUM_DESCS

static volatile int running = 1;
static void sigint_handler(int sig) { running = 0; }

struct xsk_info {
    struct xsk_ring_prod fq;
    struct xsk_ring_cons rx;
    struct xsk_ring_cons cq;
    struct xsk_ring_prod tx;
    struct xsk_umem *umem;
    struct xsk_socket *xsk;
    void *umem_area;
};

static int setup_umem(struct xsk_info *xi)
{
    size_t umem_size = NUM_FRAMES * FRAME_SIZE;
    struct xsk_umem_config cfg = {
        .fill_size = FILL_RING_SZ,
        .comp_size = COMP_RING_SZ,
        .frame_size = FRAME_SIZE,
        .frame_headroom = XSK_UMEM__DEFAULT_FRAME_HEADROOM,
        .flags = 0,
    };

    xi->umem_area = aligned_alloc(getpagesize(), umem_size);
    if (!xi->umem_area) return -ENOMEM;
    memset(xi->umem_area, 0, umem_size);

    return xsk_umem__create(&xi->umem, xi->umem_area, umem_size,
                            &xi->fq, &xi->cq, &cfg);
}

static int setup_socket(struct xsk_info *xi, const char *ifname, int queue_id)
{
    struct xsk_socket_config cfg = {
        .rx_size = RX_RING_SZ,
        .tx_size = TX_RING_SZ,
        .bind_flags = XDP_USE_NEED_WAKEUP,
        .xdp_flags = XDP_FLAGS_DRV_MODE,  /* 또는 XDP_FLAGS_SKB_MODE */
        .libxdp_flags = 0,
    };

    int ret = xsk_socket__create(&xi->xsk, ifname, queue_id,
                                  xi->umem, &xi->rx, &xi->tx, &cfg);
    if (ret) return ret;

    /* FILL ring 초기화: 프레임 주소를 미리 등록 */
    unsigned int idx;
    int n = xsk_ring_prod__reserve(&xi->fq, FILL_RING_SZ, &idx);
    for (int i = 0; i < n; i++)
        *xsk_ring_prod__fill_addr(&xi->fq, idx + i) = i * FRAME_SIZE;
    xsk_ring_prod__submit(&xi->fq, n);

    return 0;
}

static void print_pkt(const uint8_t *pkt, uint32_t len)
{
    if (len < sizeof(struct ethhdr)) return;
    const struct ethhdr *eth = (const struct ethhdr *)pkt;
    printf("  ETH: %02x:%02x:%02x:%02x:%02x:%02x -> %02x:%02x:%02x:%02x:%02x:%02x",
           eth->h_source[0], eth->h_source[1], eth->h_source[2],
           eth->h_source[3], eth->h_source[4], eth->h_source[5],
           eth->h_dest[0], eth->h_dest[1], eth->h_dest[2],
           eth->h_dest[3], eth->h_dest[4], eth->h_dest[5]);
    printf(" proto=0x%04x len=%u\n", ntohs(eth->h_proto), len);

    if (ntohs(eth->h_proto) == ETH_P_IP && len >= sizeof(struct ethhdr) + sizeof(struct iphdr)) {
        const struct iphdr *ip = (const struct iphdr *)(pkt + sizeof(struct ethhdr));
        char src[16], dst[16];
        inet_ntop(AF_INET, &ip->saddr, src, sizeof(src));
        inet_ntop(AF_INET, &ip->daddr, dst, sizeof(dst));
        printf("    IP: %s -> %s proto=%u\n", src, dst, ip->protocol);
    }
}

static void rx_loop(struct xsk_info *xi)
{
    struct pollfd fds = { .fd = xsk_socket__fd(xi->xsk), .events = POLLIN };
    unsigned long total_pkts = 0;

    printf("AF_XDP 수신 대기중 (Ctrl+C로 종료)...\n");

    while (running) {
        unsigned int rcvd, idx;

        if (xsk_ring_prod__needs_wakeup(&xi->fq))
            poll(&fds, 1, 100);

        rcvd = xsk_ring_cons__peek(&xi->rx, BATCH_SIZE, &idx);
        if (rcvd == 0)
            continue;

        for (unsigned int i = 0; i < rcvd; i++) {
            const struct xdp_desc *desc = xsk_ring_cons__rx_desc(&xi->rx, idx + i);
            uint8_t *pkt = xsk_umem__get_data(xi->umem_area, desc->addr);
            print_pkt(pkt, desc->len);
        }
        xsk_ring_cons__release(&xi->rx, rcvd);

        /* FILL ring 보충 */
        unsigned int fq_idx;
        int reserved = xsk_ring_prod__reserve(&xi->fq, rcvd, &fq_idx);
        for (int i = 0; i < reserved; i++) {
            const struct xdp_desc *desc = xsk_ring_cons__rx_desc(&xi->rx, idx + i);
            *xsk_ring_prod__fill_addr(&xi->fq, fq_idx + i) = desc->addr;
        }
        xsk_ring_prod__submit(&xi->fq, reserved);

        total_pkts += rcvd;
    }
    printf("\n총 수신 패킷: %lu\n", total_pkts);
}

int main(int argc, char **argv)
{
    const char *ifname = argc > 1 ? argv[1] : "veth0";
    int queue_id = argc > 2 ? atoi(argv[2]) : 0;

    signal(SIGINT, sigint_handler);

    struct xsk_info xi = {};
    if (setup_umem(&xi) != 0) { perror("UMEM 생성 실패"); return 1; }
    if (setup_socket(&xi, ifname, queue_id) != 0) { perror("소켓 생성 실패"); return 1; }

    printf("AF_XDP 소켓 생성 완료: %s queue=%d\n", ifname, queue_id);
    rx_loop(&xi);

    xsk_socket__delete(xi.xsk);
    xsk_umem__delete(xi.umem);
    free(xi.umem_area);
    return 0;
}

빌드 및 테스트

# XDP 프로그램 컴파일 (eBPF)
clang -O2 -target bpf -g -c xdp_redirect.bpf.c -o xdp_redirect.bpf.o

# Userspace 프로그램 컴파일
gcc -O2 -o af_xdp_rx af_xdp_rx.c -lbpf -lxdp -lpthread

# XDP 프로그램 로드 (별도 터미널)
sudo ip link set dev veth0 xdp obj xdp_redirect.bpf.o sec xdp

# AF_XDP 수신 프로그램 실행
sudo ./af_xdp_rx veth0 0

# 상대측에서 패킷 전송 (별도 터미널)
sudo ip netns exec ns_xdp ping 10.0.0.1

# 또는 대량 전송:
sudo ip netns exec ns_xdp iperf3 -c 10.0.0.1 -u -b 1G

# 정리
sudo ip link set dev veth0 xdp off
sudo ip link del veth0
sudo ip netns del ns_xdp

실습: libxdp 멀티큐 패킷 처리

이 실습에서는 libxdp를 사용하여 여러 NIC 큐에 AF_XDP 소켓을 바인딩하고 멀티스레드로 패킷을 처리하는 완전한 프로그램을 구현합니다.

libxdp 기반 XDP 프로그램

/* xdp_multiqueue.bpf.c — 멀티큐 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");

/* 통계 수집용 per-CPU 맵 */
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(__u64));
    __uint(max_entries, 64);
} stats_map SEC(".maps");

SEC("xdp")
int xdp_mq_redirect(struct xdp_md *ctx)
{
    int qid = ctx->rx_queue_index;

    /* 통계 업데이트 */
    __u64 *cnt = bpf_map_lookup_elem(&stats_map, &qid);
    if (cnt) __sync_fetch_and_add(cnt, 1);

    /* 큐 번호로 해당 AF_XDP 소켓에 리다이렉트 */
    return bpf_redirect_map(&xsks_map, qid, XDP_PASS);
}

char _license[] SEC("license") = "GPL";

멀티큐 Userspace 프로그램

/* af_xdp_multiqueue.c — libxdp 멀티큐 AF_XDP 처리 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <net/if.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <xdp/xsk.h>
#include <xdp/libxdp.h>

#define MAX_QUEUES   16
#define NUM_FRAMES   4096
#define FRAME_SIZE   4096
#define BATCH_SIZE   64

static volatile int running = 1;
static void sigint_handler(int sig) { running = 0; }

struct queue_ctx {
    int queue_id;
    int cpu_id;
    struct xsk_ring_prod fq;
    struct xsk_ring_cons rx;
    struct xsk_ring_cons cq;
    struct xsk_ring_prod tx;
    struct xsk_umem *umem;
    struct xsk_socket *xsk;
    void *umem_area;
    pthread_t thread;
    unsigned long rx_packets;
    unsigned long rx_bytes;
};

static int create_queue(struct queue_ctx *qctx, const char *ifname, int qid)
{
    size_t umem_size = NUM_FRAMES * FRAME_SIZE;
    struct xsk_umem_config ucfg = {
        .fill_size = 2048, .comp_size = 2048,
        .frame_size = FRAME_SIZE, .frame_headroom = 0,
    };
    struct xsk_socket_config scfg = {
        .rx_size = 2048, .tx_size = 2048,
        .bind_flags = XDP_USE_NEED_WAKEUP,
        .xdp_flags = 0,
        .libxdp_flags = XSK_LIBXDP_FLAGS__INHIBIT_PROG_LOAD,  /* XDP 프로그램 자동 로드 억제 */
    };

    qctx->queue_id = qid;
    qctx->umem_area = aligned_alloc(getpagesize(), umem_size);
    if (!qctx->umem_area) return -ENOMEM;

    int ret = xsk_umem__create(&qctx->umem, qctx->umem_area, umem_size,
                                &qctx->fq, &qctx->cq, &ucfg);
    if (ret) return ret;

    ret = xsk_socket__create(&qctx->xsk, ifname, qid,
                              qctx->umem, &qctx->rx, &qctx->tx, &scfg);
    if (ret) return ret;

    /* FILL ring 초기화 */
    unsigned int idx;
    int n = xsk_ring_prod__reserve(&qctx->fq, NUM_FRAMES, &idx);
    for (int i = 0; i < n; i++)
        *xsk_ring_prod__fill_addr(&qctx->fq, idx + i) = i * FRAME_SIZE;
    xsk_ring_prod__submit(&qctx->fq, n);

    return 0;
}

static void *worker_thread(void *arg)
{
    struct queue_ctx *qctx = arg;
    struct pollfd fds = { .fd = xsk_socket__fd(qctx->xsk), .events = POLLIN };

    /* CPU pinning */
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(qctx->cpu_id, &cpuset);
    pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);

    printf("[Q%d] Worker 시작 (CPU %d)\n", qctx->queue_id, qctx->cpu_id);

    while (running) {
        if (xsk_ring_prod__needs_wakeup(&qctx->fq))
            poll(&fds, 1, 100);

        unsigned int rcvd, idx;
        rcvd = xsk_ring_cons__peek(&qctx->rx, BATCH_SIZE, &idx);
        if (rcvd == 0) continue;

        for (unsigned int i = 0; i < rcvd; i++) {
            const struct xdp_desc *d = xsk_ring_cons__rx_desc(&qctx->rx, idx + i);
            qctx->rx_bytes += d->len;
            /* 여기서 패킷 처리 로직 구현 */
        }
        xsk_ring_cons__release(&qctx->rx, rcvd);
        qctx->rx_packets += rcvd;

        /* FILL ring 보충 */
        unsigned int fq_idx;
        int reserved = xsk_ring_prod__reserve(&qctx->fq, rcvd, &fq_idx);
        for (int i = 0; i < reserved; i++) {
            const struct xdp_desc *d = xsk_ring_cons__rx_desc(&qctx->rx, idx + i);
            *xsk_ring_prod__fill_addr(&qctx->fq, fq_idx + i) = d->addr;
        }
        xsk_ring_prod__submit(&qctx->fq, reserved);
    }

    printf("[Q%d] 종료: %lu pkts, %lu bytes\n",
           qctx->queue_id, qctx->rx_packets, qctx->rx_bytes);
    return NULL;
}

/* 통계 출력 스레드 */
static void *stats_thread(void *arg)
{
    struct queue_ctx *queues = arg;
    int num_queues = *(int *)((char *)arg - sizeof(int));

    while (running) {
        sleep(1);
        unsigned long total_pps = 0;
        for (int i = 0; i < MAX_QUEUES && queues[i].xsk; i++) {
            unsigned long prev = queues[i].rx_packets;
            sleep(0);  /* barrier */
            total_pps += queues[i].rx_packets - prev;
        }
    }
    return NULL;
}

int main(int argc, char **argv)
{
    if (argc < 3) {
        fprintf(stderr, "사용법: %s <ifname> <num_queues>\n", argv[0]);
        return 1;
    }

    const char *ifname = argv[1];
    int num_queues = atoi(argv[2]);
    if (num_queues > MAX_QUEUES) num_queues = MAX_QUEUES;

    signal(SIGINT, sigint_handler);

    /* libxdp로 XDP 프로그램 로드 */
    struct xdp_program *xdp_prog = xdp_program__open_file("xdp_multiqueue.bpf.o", "xdp", NULL);
    if (!xdp_prog) { perror("XDP 프로그램 로드 실패"); return 1; }

    int ifindex = if_nametoindex(ifname);
    int ret = xdp_program__attach(xdp_prog, ifindex, XDP_MODE_NATIVE, 0);
    if (ret) {
        fprintf(stderr, "XDP attach 실패 (err=%d), SKB 모드 시도...\n", ret);
        ret = xdp_program__attach(xdp_prog, ifindex, XDP_MODE_SKB, 0);
        if (ret) { perror("XDP attach 실패"); return 1; }
    }

    /* 각 큐에 대해 소켓 생성 및 스레드 시작 */
    struct queue_ctx queues[MAX_QUEUES] = {};
    for (int i = 0; i < num_queues; i++) {
        queues[i].cpu_id = i;
        ret = create_queue(&queues[i], ifname, i);
        if (ret) {
            fprintf(stderr, "Queue %d 생성 실패: %d\n", i, ret);
            goto cleanup;
        }
        pthread_create(&queues[i].thread, NULL, worker_thread, &queues[i]);
    }

    printf("AF_XDP 멀티큐 (%d큐) 수신 시작: %s\n", num_queues, ifname);
    printf("Ctrl+C로 종료\n\n");

    /* 1초마다 통계 출력 */
    unsigned long prev_total[MAX_QUEUES] = {};
    while (running) {
        sleep(1);
        unsigned long total_pps = 0, total_bps = 0;
        for (int i = 0; i < num_queues; i++) {
            unsigned long pps = queues[i].rx_packets - prev_total[i];
            prev_total[i] = queues[i].rx_packets;
            total_pps += pps;
            total_bps += queues[i].rx_bytes;
        }
        printf("\r[합계] %lu pps, %lu Mbps   ",
               total_pps, (total_bps * 8) / 1000000);
        fflush(stdout);
        for (int i = 0; i < num_queues; i++)
            queues[i].rx_bytes = 0;
    }

cleanup:
    printf("\n정리 중...\n");
    for (int i = 0; i < num_queues; i++) {
        if (queues[i].thread) pthread_join(queues[i].thread, NULL);
        if (queues[i].xsk) xsk_socket__delete(queues[i].xsk);
        if (queues[i].umem) xsk_umem__delete(queues[i].umem);
        free(queues[i].umem_area);
    }
    xdp_program__detach(xdp_prog, ifindex, XDP_MODE_NATIVE, 0);
    xdp_program__close(xdp_prog);
    return 0;
}

빌드 및 실행

# eBPF 프로그램 컴파일
clang -O2 -target bpf -g -c xdp_multiqueue.bpf.c -o xdp_multiqueue.bpf.o

# 멀티큐 프로그램 컴파일
gcc -O2 -o af_xdp_mq af_xdp_multiqueue.c -lbpf -lxdp -lpthread

# NIC 큐 수 설정 (물리 NIC에서)
sudo ethtool -L eth0 combined 4

# IRQ affinity 설정
for i in $(seq 0 3); do
    irq=$(grep "eth0-TxRx-$i" /proc/interrupts | awk '{print $1}' | tr -d ':')
    [ -n "$irq" ] && echo $i > /proc/irq/$irq/smp_affinity_list
done

# 실행 (4큐)
sudo ./af_xdp_mq eth0 4

# veth 환경에서 테스트 (큐 1개만 가능):
sudo ./af_xdp_mq veth0 1

# 성능 모니터링
watch -n 1 'ethtool -S eth0 | grep xsk'
실습 시 주의사항:
  • veth 인터페이스는 보통 큐 1개만 지원하므로, 멀티큐 테스트는 물리 NIC 권장
  • XDP SKB 모드는 Zero-copy를 지원하지 않음 — 성능 테스트 시 DRV 모드 사용
  • CAP_NET_ADMIN + CAP_BPF 권한 필요 (또는 root)
  • XDP 프로그램이 이미 로드된 인터페이스에 다시 로드하면 교체됨 — 정리 후 재시작(Reboot)
다음 단계:
  • L2 포워딩 추가: RX 후 MAC 주소 변환(Address Translation) → TX ring으로 전송
  • XDP 메타데이터 활용: HW timestamp, RSS hash 읽기
  • Shared UMEM 적용: 메모리 사용량 절반 감소
  • io_uring과 결합: AF_XDP + 디스크 I/O 비동기 파이프라인

참고자료

공식 문서

튜토리얼 및 프로젝트

주요 참고 글

커널 소스 경로

파일 역할
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 구현 (참조용)

주요 발표/영상

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