DPDK

DPDK(Data Plane Development Kit) 고성능 패킷(Packet) 처리 구조를 심층 분석합니다. EAL 초기화와 hugepage/NUMA 메모리 모델, IOVA(VA/PA), PMD poll-mode 루프, rte_mbuf/ring/graph/mempool 데이터 경로, VFIO/UIO 및 bifurcated PMD, AF_XDP와 representor/rte_flow/hairpin, dmadev, OVS-DPDK·VPP·testpmd 운영 시나리오, trace·metrics·power 관리와 하드닝·트러블슈팅까지 실무 중심으로 다룹니다.

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

핵심 요약

  • EAL (Environment Abstraction Layer) — DPDK의 초기화 계층으로, CPU 코어 바인딩, 휴즈페이지(Hugepage) 메모리 할당, PCI 디바이스 탐색 등 플랫폼 추상화를 담당합니다.
  • PMD (Poll Mode Driver) — 인터럽트 없이 폴링(Polling) 방식으로 NIC 큐를 직접 읽어 패킷을 수신합니다. 커널 드라이버를 완전히 우회하는 DPDK의 핵심 메커니즘입니다.
  • rte_mbuf — DPDK의 패킷 버퍼 구조체로, 커널의 sk_buff를 대체합니다. 메모리 풀에서 사전 할당되어 할당/해제 오버헤드를 최소화합니다.
  • Ring 라이브러리 — 락-프리(Lock-free) FIFO 큐로, 코어 간 패킷 전달에 사용됩니다. SPSC/MPMC 등 다양한 생산자-소비자 모드를 지원합니다.
  • Mempoolrte_mbuf 객체를 사전 할당하는 메모리 풀입니다. NUMA 노드별 할당과 코어별 로컬 캐시로 메모리 접근 지연을 줄입니다.
  • VFIO/UIO — NIC를 커널 드라이버에서 분리하여 유저 공간 PMD에 바인딩하는 디바이스 바인딩 메커니즘입니다.
  • rte_flow — NIC 하드웨어 수준에서 플로우 분류(Flow Classification)와 오프로드 규칙을 정의하는 API입니다.
  • OVS-DPDK — Open vSwitch의 DPDK 백엔드로, 가상화 환경에서 vhost-user를 통해 VM 간 고속 패킷 스위칭을 수행합니다.

단계별 이해

  1. EAL 환경 구성 — 휴즈페이지와 CPU 코어를 준비합니다.

    DPDK는 커널 메모리 할당자를 사용하지 않으므로, 휴즈페이지(2MB/1GB)를 먼저 예약하고 rte_eal_init()으로 코어·메모리·PCI 디바이스를 초기화합니다.

  2. NIC 바인딩과 PMD 이해 — 커널 드라이버를 분리하고 유저 공간에서 NIC를 직접 제어합니다.

    dpdk-devbind.py로 NIC를 VFIO 또는 UIO에 바인딩한 뒤, PMD가 폴링 루프에서 rte_eth_rx_burst()/rte_eth_tx_burst()로 패킷을 주고받는 구조를 학습합니다.

  3. mbuf·Mempool·Ring 파이프라인 구성 — 패킷 버퍼 할당과 코어 간 전달 경로를 설계합니다.

    Mempool에서 mbuf를 할당받아 수신하고, Ring 큐로 워커(Worker) 코어에 전달한 뒤 송신하는 기본 파이프라인을 구현합니다.

  4. rte_flow로 하드웨어 오프로드 적용 — NIC 수준에서 플로우 분류와 RSS(Receive Side Scaling) 규칙을 설정합니다.

    소프트웨어 분류 전에 NIC가 먼저 패킷을 분류하도록 rte_flow 규칙을 작성하여 CPU 부하를 줄입니다.

  5. 성능 측정과 병목 분석 — PPS, 지연, 캐시 미스를 계측하여 최적화합니다.

    dpdk-testpmd로 기본 성능을 측정하고, dpdk-proc-info와 perf로 코어별 패킷 처리량과 LLC 캐시 미스를 추적합니다.

하드웨어 가속: SmartNIC/DPU에서 하드웨어 기반 네트워크 가속 기술을 확인하세요.
관련 표준: DPDK API Documentation, PCIe Base Specification — 고성능 패킷 처리 프레임워크 및 하드웨어 표준입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

DPDK는 Intel이 주도하여 개발한 고성능 패킷 처리 프레임워크로, 커널 네트워크 스택을 완전히 바이패스하여 유저 공간에서 NIC를 직접 제어합니다. 표준 커널 드라이버가 인터럽트(Interrupt) 기반으로 패킷을 처리하는 반면, DPDK는 폴링(polling) 기반 PMD(Poll Mode Driver)를 사용하여 컨텍스트 스위칭(Context Switching)과 인터럽트 오버헤드(Overhead)를 제거합니다. 10~400GbE 환경에서 수십~수백 Mpps(백만 패킷/초)의 처리량을 단일 서버에서 달성할 수 있으며, 통신사(NFV), 클라우드(가상 스위칭), 금융(초저지연 트레이딩), CDN, 보안 장비 등에서 핵심 기술로 사용됩니다.

DPDK와 커널의 관계: DPDK는 커널 모듈(Kernel Module)이 아니라 유저 공간 라이브러리입니다. 그러나 커널의 VFIO, UIO, hugepages, IOMMU 등 여러 서브시스템에 의존하며, 최근에는 커널의 AF_XDP 소켓(Socket)을 PMD 백엔드로 사용하는 하이브리드 접근도 지원합니다. 커널 개발자가 DPDK의 동작 원리를 이해하면 드라이버 최적화, VFIO/IOMMU 튜닝, AF_XDP 개발 등에서 큰 도움이 됩니다.

DPDK 아키텍처 개요

User Space (DPDK Application) DPDK Application (l2fwd, OVS-DPDK, VPP ...) mbuf 패킷 버퍼 Ring Lock-free 큐 Mempool 메모리 풀 Hash / LPM 탐색 라이브러리 Cryptodev / Eventdev 가속 프레임워크 EAL (Environment Abstraction Layer) — hugepage, CPU 코어, PCI, 로깅, 타이머 PMD (VFIO) PMD (UIO) PMD (AF_XDP) PMD (virtio) Kernel Space VFIO / IOMMU UIO (igb_uio) AF_XDP socket Hugepages (hugetlbfs) vfio-pci / uio_pci_generic Hardware NIC (10/25/100/400 GbE) DMA Engine PCIe BAR / MMIO 레지스터
계층구성 요소역할
Applicationl2fwd, l3fwd, OVS-DPDK, VPP, Pktgen패킷 처리 로직 구현
Core Librariesmbuf, Ring, Mempool, Hash, LPM, ACL고성능 데이터 구조 및 알고리즘
EALEnvironment Abstraction Layerhugepage, CPU 코어, PCI 디바이스, 타이머(Timer), 로깅 추상화
PMDPoll Mode Driver (ixgbe, i40e, mlx5, virtio, af_xdp...)NIC별 RX/TX 드라이버 (유저 공간)
KernelVFIO, UIO, hugepages, IOMMU디바이스 접근 및 메모리 관리(Memory Management) 지원
HardwareNIC, DMA, PCIe BAR물리적 패킷 송수신, DMA 전송

커널 네트워크 스택 vs DPDK 비교

항목커널 네트워크 스택DPDK
실행 공간커널 공간(Kernel Space)유저 공간
패킷 수신 모델인터럽트 → NAPI 폴링 (하이브리드)100% 폴링 (busy-wait)
패킷 버퍼sk_buff (동적 할당)rte_mbuf (사전 할당 Mempool)
프로토콜 스택완전한 L2~L7 (TCP/IP, Netfilter, conntrack...)없음 (앱이 직접 구현 또는 별도 라이브러리)
CPU 사용필요 시만 사용 (이벤트 기반)전용 코어 상시 100% 사용 (busy-poll)
지연 시간수~수십 μs수백 ns ~ 수 μs
처리량 (64B pkt)~1-5 Mpps/core~14.88 Mpps/core (10GbE wire rate)
메모리 관리SLAB/SLUB + per-CPU 캐시(Cache)Hugepage 기반 Mempool
보안/격리(Isolation)네임스페이스(Namespace), Netfilter, SELinux 등IOMMU/VFIO로 DMA 격리만
도구/디버깅(Debugging)tcpdump, ss, ip, nftables, eBPFdpdk-proc-info, dpdk-pdump, DPDK telemetry
적용 분야범용 서버/데스크탑/IoTNFV, SDN 스위칭, 패킷 브로커, DPI, 로드밸런서
커널 네트워크 스택 NIC (HW interrupt) IRQ → softirq/NAPI sk_buff 할당 + 복사 L2/L3/L4 프로토콜 스택 Netfilter/conntrack socket → User App 6단계: ~5–20 μs DPDK (PMD) NIC (DMA → hugepage) PMD poll (user space) rte_mbuf (zero-copy) Application 직접 처리 3단계: ~0.3–2 μs 커널 바이패스로 4~10× 지연 감소

항목별 상세 비교

위 표의 각 항목이 왜 차이를 만드는지 구체적으로 살펴봅니다.

DPDK 사용 시 트레이드오프: DPDK는 커널 네트워크 스택을 바이패스하므로 iptables, tc, conntrack, TCP/IP 프로토콜 처리 등 커널이 제공하는 모든 기능을 사용할 수 없습니다. 이런 기능이 필요하면 앱에서 직접 구현하거나 VPP/FD.io 같은 유저 공간 네트워크 스택을 사용해야 합니다. 또한 전용 CPU 코어를 상시 폴링에 사용하므로 코어 수가 제한된 환경에서는 비효율적입니다.
하이브리드 접근: 반드시 "전부 커널" 또는 "전부 DPDK"를 선택할 필요는 없습니다. AF_XDP PMD를 사용하면 커널 드라이버를 유지하면서 XDP 경로로 고속 패킷 처리를 할 수 있고, Bifurcated PMD(mlx5)는 커널 netdev와 DPDK가 기능을 분담합니다. 트래픽 유형별로 적합한 경로를 혼합하는 것이 실전에서 흔한 패턴입니다.

EAL (Environment Abstraction Layer)

EAL은 DPDK의 초기화 계층으로, 애플리케이션이 하드웨어와 OS 세부 사항에 독립적으로 동작하도록 추상화합니다. rte_eal_init() 호출 시 수행되는 핵심 작업:

rte_eal_init() 초기화 흐름 (Linux) 1) 커맨드라인 파싱 -l, --socket-mem, --proc-type 등 2) Hugepage 매핑 sysfs 조회, hugetlbfs 확인 mmap + pagemap, NUMA 분배 3) 메모리 서브시스템 준비 malloc_heap, memzone, mempool 4) CPU 코어 관리 lcore 파싱, pthread 생성 affinity 설정, main lcore 선택 5) PCI 버스 스캔 /sys/bus/pci/devices 순회 VFIO/UIO 바인딩 상태 확인 PMD와 vendor:device 매칭 6) 서비스 코어(선택) --service-lcore 7) 공통 런타임 초기화 완료 로깅, 타이머, 인터럽트 핸들러 준비 이후 포트/큐 초기화 및 main loop 진입
/* DPDK 애플리케이션 기본 구조 */
#include <rte_eal.h>
#include <rte_ethdev.h>
#include <rte_mbuf.h>

int main(int argc, char *argv[])
{
    /* EAL 초기화: hugepage 매핑, 코어 설정, PCI 스캔 */
    int ret = rte_eal_init(argc, argv);
    if (ret < 0)
        rte_exit(EXIT_FAILURE, "EAL init failed\\n");

    /* Mempool 생성 (패킷 버퍼 풀) */
    struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create(
        "MBUF_POOL",
        8192,              /* 풀 크기 (mbuf 개수) */
        256,               /* per-core 캐시 크기 */
        0,                 /* priv_size */
        RTE_MBUF_DEFAULT_BUF_SIZE,  /* 데이터룸 크기 */
        rte_socket_id()    /* NUMA 소켓 */
    );

    /* 포트 설정 및 시작 */
    struct rte_eth_conf port_conf = { 0 };
    rte_eth_dev_configure(port_id, nb_rx_q, nb_tx_q, &port_conf);
    rte_eth_rx_queue_setup(port_id, 0, 1024, rte_eth_dev_socket_id(port_id),
                          NULL, mbuf_pool);
    rte_eth_tx_queue_setup(port_id, 0, 1024, rte_eth_dev_socket_id(port_id),
                          NULL);
    rte_eth_dev_start(port_id);

    /* 메인 패킷 처리 루프 (busy-poll) */
    while (!force_quit) {
        struct rte_mbuf *bufs[32];
        uint16_t nb_rx = rte_eth_rx_burst(port_id, 0, bufs, 32);
        if (nb_rx == 0)
            continue;

        /* 패킷 처리 ... */
        process_packets(bufs, nb_rx);

        uint16_t nb_tx = rte_eth_tx_burst(port_id, 0, bufs, nb_rx);
        for (uint16_t i = nb_tx; i < nb_rx; i++)
            rte_pktmbuf_free(bufs[i]);  /* 전송 실패 mbuf 반환 */
    }

    rte_eal_cleanup();
    return 0;
}
EAL 핵심 옵션:
  • -l 0-3 — 사용할 논리 코어 목록. DPDK 워커 스레드(Thread)가 이 코어에 1:1 매핑(Mapping)
  • -n 4 — 메모리 채널 수 (NUMA interleaving 최적화)
  • --socket-mem 1024,1024 — NUMA 노드별 hugepage 할당량 (MB)
  • -a 0000:03:00.0 — 허용할 PCI 디바이스 (allowlist)
  • --file-prefix dpdk1 — 여러 DPDK 프로세스(Process) 공존 시 hugepage 파일 구분
  • --proc-type secondary — 멀티프로세스 모드 (primary/secondary)
  • --no-huge — hugepage 없이 실행 (개발/테스트용, 성능 저하)

rte_eal_init() 내부 구현 분석

rte_eal_init()은 DPDK의 진입점으로, 내부적으로 20개 이상의 서브시스템을 순서대로 초기화합니다. 핵심 단계를 소스 수준에서 분석합니다.

/* lib/eal/linux/eal.c — rte_eal_init() 핵심 흐름 (간략화) */
int
rte_eal_init(int argc, char **argv)
{
    /* ① 커맨드라인 파싱: -l, -n, --socket-mem, -a 등 */
    eal_parse_args(argc, argv);

    /* ② 프로세스 타입 결정: primary vs secondary */
    rte_config.process_type = eal_proc_type_detect();

    /* ③ 플러그인 로딩: lib/ 아래 PMD .so 동적 로드 */
    eal_plugins_init();

    /* ④ Hugepage 메모리 매핑 — 가장 중요한 단계 */
    rte_eal_hugepage_init();
    /*   내부:
     *   - /sys/kernel/mm/hugepages 스캔 → 2MB/1GB 크기별 페이지 수 확인
     *   - /dev/hugepages/에 파일 생성 → mmap(MAP_HUGETLB | MAP_POPULATE)
     *   - /proc/self/pagemap 읽어 물리 주소(PA) 획득 → IOVA PA 모드에 사용
     *   - NUMA 노드별 메모리 분배: --socket-mem 또는 자동 감지
     */

    /* ⑤ 메모리 서브시스템 초기화 */
    rte_eal_memzone_init();   /* 이름 기반 메모리 영역 관리 */
    rte_eal_malloc_heap_init();  /* 힙 관리자: hugepage 위 동적 할당 */

    /* ⑥ IOVA 모드 결정: VA vs PA */
    iova_mode = rte_bus_get_iommu_class();
    /*   IOMMU 있음 → VA 모드 (유저 가상 주소 = IOVA)
     *   IOMMU 없음 → PA 모드 (물리 주소 = IOVA, root 필요) */

    /* ⑦ lcore 스레드 생성 */
    RTE_LCORE_FOREACH_WORKER(i) {
        pthread_create(&lcore_config[i].thread_id,
                       NULL, eal_thread_loop, (void *)(uintptr_t)i);
        /* 각 스레드를 지정된 CPU에 affinity 설정 */
        pthread_setaffinity_np(lcore_config[i].thread_id,
                               sizeof(cpu_set), &lcore_config[i].cpuset);
    }

    /* ⑧ 버스 스캔: PCI, VDEV 등 모든 디바이스 열거 */
    rte_bus_scan();
    /*   PCI 버스: /sys/bus/pci/devices/ 순회
     *   각 디바이스의 vendor:device ID를 등록된 PMD와 매칭
     *   VFIO/UIO 바인딩 상태 확인 */

    /* ⑨ 버스 프로빙: 매칭된 PMD로 디바이스 초기화 */
    rte_bus_probe();
    /*   PMD의 probe() 콜백 호출
     *   PCI BAR mmap, 하드웨어 초기화, 포트 등록 */

    /* ⑩ 서비스 코어, 타이머, 로깅 등 런타임 초기화 */
    rte_service_init();
    rte_timer_subsystem_init();

    return fctret;  /* 파싱된 EAL 인자 수 반환 */
}

코드 분석: rte_eal_init() 내부

  • ④ rte_eal_hugepage_init(): EAL의 가장 핵심 단계입니다. /sys/kernel/mm/hugepages/에서 사용 가능한 hugepage를 확인한 후, /dev/hugepages/에 파일을 만들고 mmap(MAP_HUGETLB | MAP_POPULATE)로 매핑합니다. MAP_POPULATE가 핵심인데, 이 플래그가 없으면 첫 접근 시 페이지 폴트가 발생하여 데이터 경로에서 지연 스파이크를 일으킵니다. PA 모드에서는 추가로 /proc/self/pagemap을 읽어 가상 주소 → 물리 주소 변환 테이블을 구축합니다.
  • ⑥ IOVA 모드: rte_bus_get_iommu_class()는 VFIO가 사용 가능하면 VA 모드, UIO나 no-IOMMU면 PA 모드를 선택합니다. VA 모드에서는 유저 가상 주소를 그대로 IOVA로 사용하므로 /proc/self/pagemap 조회가 불필요하고, IOMMU가 가상→물리 변환을 처리합니다.
  • ⑦ lcore 스레드: -l 0-3으로 지정된 각 CPU에 1개씩 pthread를 생성하고, pthread_setaffinity_np()로 해당 CPU에 고정합니다. 생성된 스레드는 eal_thread_loop()에서 rte_eal_mp_wait_lcore()가 호출될 때까지 대기하다가, rte_eal_remote_launch()로 워커 함수를 할당받으면 실행을 시작합니다.
  • ⑧⑨ 버스 스캔/프로빙: rte_bus_scan()/sys/bus/pci/devices/를 순회하며 모든 PCI 디바이스를 열거합니다. rte_bus_probe()는 각 디바이스의 vendor:device ID를 등록된 PMD의 ID 테이블과 비교하여 매칭되면 PMD의 probe() 콜백을 호출합니다. 이때 PCI BAR이 mmap되고, NIC 하드웨어가 초기화되며, ethdev 포트가 등록됩니다.

Hugepage 매핑 상세 과정

/* lib/eal/linux/eal_hugepage_info.c — hugepage 정보 수집 */
static int
hugepage_info_init(void)
{
    /* /sys/kernel/mm/hugepages/ 아래 디렉토리 순회 */
    /* hugepages-2048kB, hugepages-1048576kB 등 */
    for (dirent = readdir(dir); dirent; dirent = readdir(dir)) {
        /* 크기별 free/total hugepage 수 읽기 */
        get_hugepage_dir(hpi->hugepage_sz, hpi->hugedir,
                          sizeof(hpi->hugedir));

        /* 마운트포인트 확인: /dev/hugepages 또는 /mnt/huge 등 */
        hpi->num_pages[socket] = get_num_hugepages(
            hpi->hugedir, hpi->hugepage_sz, socket);
    }
    return 0;
}

/* lib/eal/linux/eal_memory.c — 실제 hugepage mmap */
static void *
alloc_hugepage(struct hugepage_info *hpi, int socket)
{
    /* hugepage 파일 생성 */
    fd = open(filepath, O_CREAT | O_RDWR, 0600);
    ftruncate(fd, hpi->hugepage_sz);

    /* mmap: MAP_HUGETLB로 hugepage 매핑
     * MAP_POPULATE: 즉시 물리 페이지 할당 (fault 방지)
     * MAP_SHARED: 멀티프로세스 모드에서 secondary와 공유 */
    addr = mmap(NULL, hpi->hugepage_sz,
                 PROT_READ | PROT_WRITE,
                 MAP_SHARED | MAP_POPULATE | MAP_HUGETLB,
                 fd, 0);

    /* mbind()로 NUMA 노드에 바인딩 */
    mbind(addr, hpi->hugepage_sz, MPOL_BIND,
           &nodemask, max_node, MPOL_MF_STRICT);

    /* PA 모드: /proc/self/pagemap에서 물리 주소 획득 */
    if (rte_eal_iova_mode() == RTE_IOVA_PA)
        physaddr = rte_mem_virt2phy(addr);

    return addr;
}

코드 분석: Hugepage 매핑

  • hugepage_info_init(): sysfs를 읽어 시스템에서 사용 가능한 hugepage 크기와 개수를 파악합니다. DPDK는 가능한 가장 큰 hugepage(1GB > 2MB)를 우선 사용합니다. 1GB hugepage 1개 = 2MB hugepage 512개와 동일한 메모리를 TLB 엔트리 1개로 커버합니다.
  • MAP_POPULATE: 이 플래그 없이 mmap하면 처음 접근 시 minor page fault가 발생합니다. DPDK 데이터 경로에서 page fault는 수십 μs의 지연 스파이크를 일으키므로, 초기화 시점에 모든 페이지를 미리 할당합니다.
  • MAP_SHARED: 멀티프로세스 모드에서 primary와 secondary 프로세스가 동일한 hugepage 파일을 mmap하여 메모리를 공유합니다. mempool, ring 등의 데이터 구조가 이 공유 메모리 위에 배치됩니다.
  • mbind(MPOL_BIND): 특정 NUMA 노드에서만 물리 메모리를 할당하도록 강제합니다. NIC이 NUMA 노드 0에 연결되어 있다면, 해당 NIC의 RX/TX 큐에 사용할 mempool도 노드 0에 배치해야 크로스-NUMA 지연(40~100ns)을 방지할 수 있습니다.
  • rte_mem_virt2phy(): /proc/self/pagemap을 읽어 가상 주소를 물리 주소로 변환합니다. PA 모드에서 NIC DMA가 물리 주소로 접근하므로 이 변환이 필수입니다. VA 모드에서는 IOMMU가 변환을 처리하므로 이 단계가 생략됩니다.

PCI 버스 프로빙과 PMD 매칭

/* drivers/bus/pci/linux/pci.c — PCI 디바이스 스캔 */
static int
pci_scan_one(const char *dirname, const struct rte_pci_addr *addr)
{
    struct rte_pci_device *dev;

    dev = malloc(sizeof(*dev));
    dev->addr = *addr;

    /* sysfs에서 디바이스 정보 읽기 */
    snprintf(filename, sizeof(filename),
             "%s/vendor", dirname);
    pci_parse_sysfs_value(filename, &dev->id.vendor_id);

    snprintf(filename, sizeof(filename),
             "%s/device", dirname);
    pci_parse_sysfs_value(filename, &dev->id.device_id);

    /* NUMA 노드 확인 */
    snprintf(filename, sizeof(filename),
             "%s/numa_node", dirname);
    pci_parse_sysfs_value(filename, &dev->device.numa_node);

    /* VFIO/UIO 바인딩 드라이버 확인 */
    snprintf(filename, sizeof(filename),
             "%s/driver", dirname);
    dev->kdrv = pci_get_kernel_driver_by_path(filename);
    /* RTE_PCI_KDRV_VFIO, RTE_PCI_KDRV_UIO_GENERIC 등 */

    /* PCI BAR 리소스 매핑 정보 */
    pci_parse_sysfs_resource(dirname, dev);

    rte_pci_insert_device(dev);
    return 0;
}

/* drivers/bus/pci/pci_common.c — PMD 매칭과 프로빙 */
static int
pci_probe_all_drivers(struct rte_pci_device *dev)
{
    /* 등록된 모든 PMD 드라이버를 순회 */
    TAILQ_FOREACH(dr, &rte_pci_bus.driver_list, next) {
        /* vendor:device ID 매칭 */
        if (rte_pci_match(dr, dev)) {
            /* PMD의 probe() 콜백 호출 */
            ret = dr->probe(dr, dev);
            /* probe() 내부:
             *   1. PCI BAR mmap (VFIO/UIO 경유)
             *   2. NIC 하드웨어 리셋 + 초기화
             *   3. rte_eth_dev 등록 (포트 할당)
             *   4. dev_ops 함수 포인터 테이블 설정
             */
            return ret;
        }
    }
    return -ENODEV;
}

코드 분석: PCI 프로빙

  • pci_scan_one(): /sys/bus/pci/devices/0000:XX:YY.Z/ 아래의 vendor, device, numa_node, driver 등 sysfs 파일을 읽어 PCI 디바이스 정보를 수집합니다. 이 정보는 이후 PMD 매칭에 사용됩니다.
  • kdrv 필드: NIC이 현재 어떤 커널 드라이버에 바인딩되어 있는지 확인합니다. vfio-pci면 VFIO 경로, uio_pci_generic이면 UIO 경로로 BAR 매핑을 수행합니다. 커널 NIC 드라이버(ixgbe, i40e 등)에 바인딩된 상태면 DPDK가 사용할 수 없습니다.
  • rte_pci_match(): PMD가 등록한 pci_id_table[]의 vendor:device:subsystem ID와 스캔된 디바이스의 ID를 비교합니다. 예: ixgbe PMD는 Intel 82599(8086:10FB), i40e PMD는 Intel X710(8086:1572)에 매칭됩니다.
  • probe() 콜백: 매칭된 PMD의 probe()가 호출되면, (1) VFIO를 통해 PCI BAR을 mmap하고, (2) NIC 하드웨어를 리셋/초기화하고, (3) rte_eth_dev 구조체를 등록하여 ethdev 포트를 생성합니다. 이후 애플리케이션은 rte_eth_dev_configure()로 큐를 설정합니다.

PMD (Poll Mode Driver)

PMD는 DPDK의 핵심으로, 커널 드라이버 없이 유저 공간에서 NIC를 직접 제어합니다. NIC의 PCIe BAR를 mmap()하여 하드웨어 레지스터(Register)에 직접 접근하고, DMA 디스크립터 링(descriptor ring)을 유저 공간 메모리에 배치합니다.

PMD가 필요한 이유
전통적인 네트워크 스택은 NIC가 패킷을 수신하면 커널이 Interrupt를 발생시켜 처리합니다. 하지만 고속 패킷 처리에서 Interrupt 기반 모델은:

PMD는 이 문제를 해결하기 위해 폴링(Polling) 방식으로 동작합니다. 애플리케이션이 직접 NIC를 폴링하여 패킷을 수신하므로 오버헤드가 없습니다.

RX (수신) 동작 원리

PMD의 RX 동작은 생산자-소비자(Producer-Consumer) 패턴을 따릅니다:

  1. 초기 상태: 빈 Descriptor
    PMD는 초기화 시에 mbuf 메모리 풀(mempool)에서 Descriptor마다 빈 mbuf를 할당하여 Descriptor Ring에 채워둡니다. 이 mbuf는 패킷 데이터를 저장할 수 있는 버퍼입니다.
  2. NIC의 DMA 동작
    NIC가 네트워크 케이블에서 패킷을 수신하면, Descriptor에 있는 mbuf 주소로 DMA를 통해 패킷 데이터를 직접 메모리에 기록합니다. 이때 Descriptor의 DD(Descriptor Done) 비트를 1로 설정합니다.
  3. PMD의 폴링
    애플리케이션이 rte_eth_rx_burst()를 호출하면, PMD는 Descriptor Ring을 순회하며 각 Descriptor의 DD 비트를 확인합니다.
  4. 패킷 처리
    DD=1인 Descriptor(패킷이 도착한)를 찾으면:
    • Descriptor에서 패킷 메타데이터(길이, RSS hash, VLAN 등)를 추출
    • mbuf에 패킷 데이터가 있으므로, 이를 mbuf 포인터 배열에 추가
  5. Descriptor 재사용
    사용된 Descriptor에 새로운 mbuf를 할당하여 채워 넣고, Tail Register를 갱신하여 NIC에게 "이 Descriptor를 사용 가능"함을 알려줍니다.

이 과정이 매번 반복되며, PMD는 Interrupt 없이도 패킷을 지속적으로 수신할 수 있습니다.

PMD RX (수신) 동작 원리 왼쪽: NIC 영역 | 중간: Descriptor Ring (공유메모리) | 오른쪽: Mempool (mbuf pool) ① 초기화 시 (PMD 포트 초기화) PMD가 Mempool에서 mbuf를 할당 → Descriptor Ring에 채워넣음 각 Descriptor에는 빈 mbuf의 DMA 주소(buf_iova)가 저장됨, DD=0 ② NIC가 패킷 수신 → DMA 기록 NIC가 패킷 수신 (이더넷 케이블) NIC가 Descriptor의 mbuf 주소로 DMA로 패킷 데이터 기록 Descriptor Ring mbuf_addr | DD=1 패킷 데이터 기록 mbuf (버퍼) 패킷 데이터 저장됨 ③ PMD가 폴링 (rte_eth_rx_burst) Application rte_eth_rx_burst() 호출 Descriptor Ring DD=1 검사 → pkt_len, RSS hash 등 추출 mbuf 패킷 데이터 + 메타데이터 (pkt_len, RSS, VLAN...) rx_pkts[] ④ Descriptor 재사용 (새 mbuf 할당) Mempool에서 새 mbuf 할당 rte_mbuf_raw_alloc() Descriptor Ring 새 mbuf 주소 기록 DD=0으로 초기화 Tail Register NIC에게 반환 NIC가 다시 사용 가능 핵심: Descriptor는 "빈 mbuf에 대한 티켓" 역할을 함 | DD=1: NIC가 패킷 기록 완료 | DD=0: 빈 Descriptor (사용 가능)
/* PMD의 RX burst 내부 (간략화) — ixgbe_recv_pkts() 기반 */
static uint16_t
pmd_recv_pkts(void *rx_queue, struct rte_mbuf **rx_pkts, uint16_t nb_pkts)
{
    struct pmd_rx_queue *rxq = rx_queue;
    volatile union rx_desc *rxdp;  /* 하드웨어 descriptor 포인터 */
    uint16_t nb_rx = 0;

    while (nb_rx < nb_pkts) {
        rxdp = &rxq->rx_ring[rxq->rx_tail];

        /* DD 비트 확인: NIC가 DMA를 완료했는가? */
        if (!(rxdp->wb.status & RX_DESC_DD))
            break;  /* 더 이상 수신된 패킷 없음 */

        /* mbuf 회수 및 메타데이터 설정 */
        struct rte_mbuf *mb = rxq->sw_ring[rxq->rx_tail];
        mb->pkt_len = rxdp->wb.pkt_len;
        mb->data_len = rxdp->wb.pkt_len;
        mb->hash.rss = rxdp->wb.rss_hash;
        mb->vlan_tci = rxdp->wb.vlan_tag;

        rx_pkts[nb_rx++] = mb;

        /* 새 mbuf를 할당하여 descriptor에 채움 (NIC에 반환) */
        struct rte_mbuf *nmb = rte_mbuf_raw_alloc(rxq->mb_pool);
        rxdp->read.pkt_addr = rte_mbuf_data_iova(nmb);
        rxq->sw_ring[rxq->rx_tail] = nmb;

        rxq->rx_tail = (rxq->rx_tail + 1) & rxq->rx_tail_mask;
    }

    /* tail 레지스터 갱신 → NIC에 새 descriptor 사용 가능 알림 */
    if (nb_rx)
        rte_write32(rxq->rx_tail, rxq->tail_ptr);

    return nb_rx;
}

/* PMD의 TX burst 내부 (간략화) */
static uint16_t
pmd_xmit_pkts(void *tx_queue, struct rte_mbuf **tx_pkts, uint16_t nb_pkts)
{
    struct pmd_tx_queue *txq = tx_queue;
    uint16_t nb_tx = 0;

    /* 이전에 전송 완료된 descriptor 정리 (mbuf 반환) */
    tx_free_bufs(txq);

    while (nb_tx < nb_pkts) {
        volatile union tx_desc *txdp = &txq->tx_ring[txq->tx_tail];
        struct rte_mbuf *mb = tx_pkts[nb_tx];

        /* TX descriptor에 mbuf의 DMA 주소와 길이 설정 */
        txdp->read.buffer_addr = rte_mbuf_data_iova(mb);
        txdp->read.cmd_type_len = mb->data_len | TX_DESC_EOP | TX_DESC_RS;

        txq->sw_ring[txq->tx_tail] = mb;
        txq->tx_tail = (txq->tx_tail + 1) & txq->tx_tail_mask;
        nb_tx++;
    }

    /* tail 레지스터 갱신 → NIC가 전송 시작 */
    rte_wmb();  /* 메모리 배리어: descriptor 쓰기 완료 보장 */
    rte_write32(txq->tx_tail, txq->tail_ptr);

    return nb_tx;
}

코드 분석: PMD RX/TX 구현 핵심

  • pmd_recv_pkts() — RX 경로:
    • rx_ring[rx_tail]: NIC의 하드웨어 descriptor ring은 hugepage 위의 DMA 가능 메모리에 배치됩니다. volatile 키워드는 NIC가 DMA로 descriptor를 수정하므로 컴파일러 최적화를 방지합니다.
    • DD 비트 검사: rxdp->wb.status & RX_DESC_DD는 NIC가 DMA 완료 후 설정하는 "Descriptor Done" 비트입니다. 이 비트가 0이면 아직 패킷이 도착하지 않았으므로 루프를 종료합니다. 이것이 "폴링"의 실체입니다.
    • sw_ring: 하드웨어 descriptor에는 DMA 물리 주소만 있으므로, 대응하는 rte_mbuf 가상 주소 포인터를 소프트웨어 링(sw_ring[])에 별도로 유지합니다.
    • rte_mbuf_raw_alloc(): 사용된 mbuf를 대체하기 위해 mempool에서 새 mbuf를 즉시 할당합니다. 이 "swap" 패턴이 제로카피(Zero-copy)의 핵심입니다 — 패킷 데이터를 복사하지 않고, mbuf 포인터만 교체합니다.
    • rte_write32(rx_tail): NIC의 MMIO Tail Register에 새 tail 위치를 기록합니다. NIC는 이 레지스터를 폴링하여 사용 가능한 descriptor가 추가되었음을 인지하고, 해당 descriptor의 mbuf 주소로 다음 패킷을 DMA합니다.
  • pmd_xmit_pkts() — TX 경로:
    • tx_free_bufs(): 이전 호출에서 NIC가 전송 완료한 descriptor를 확인(DD 비트)하고, 해당 mbuf를 mempool에 반환합니다. TX에서 mbuf 반환 시점이 RX와 다른 핵심 차이점입니다.
    • TX_DESC_EOP | TX_DESC_RS: EOP(End Of Packet)은 이 descriptor가 패킷의 마지막 세그먼트임을, RS(Report Status)는 NIC가 전송 완료 후 DD 비트를 설정하라는 요청입니다. RS를 매 descriptor마다 설정하면 오버헤드가 커지므로, 보통 N개(예: 32)마다 한 번씩 설정합니다.
    • rte_wmb(): Write Memory Barrier입니다. descriptor 메모리 쓰기가 CPU 파이프라인을 벗어나 실제 메모리에 반영된 후에 Tail Register를 갱신하도록 보장합니다. 이 배리어가 없으면 NIC이 아직 쓰이지 않은 descriptor를 읽어 잘못된 주소로 DMA할 수 있습니다.

TX (송신) 동작 원리

TX 동작은 RX의 역순으로 진행됩니다:

  1. 애플리케이션의 패킷 준비
    애플리케이션이 전송할 패킷이 담긴 mbuf 배열을 준비합니다.
  2. Descriptor 채우기
    PMD는 각 mbuf의 DMA 주소와 길이를 TX Descriptor에 기록합니다. 명령어 필드에 패킷 끝(EOP), Report Status(RS) 플래그를 설정합니다.
  3. Tail Register 갱신
    모든 Descriptor를 채운 후, 메모리 배리어(Memory Barrier)를 수행하고 Tail Register에 새 위치를 기록합니다. 이를 통해 NIC에게 "이제 전송해도 좋다"고 알려줍니다.
  4. NIC의 DMA 및 전송
    NIC가 Descriptor를 읽어 mbuf에서 데이터를 DMA로 읽어 네트워크 케이블로 전송합니다.
  5. Descriptor 회수
    전송이 완료되면 NIC가 Descriptor의 DD 비트를 설정하고, PMD가 다음 번 호출 시 사용된 mbuf를 회수하여 Mempool에 반환합니다.
메모리 배리어(Memory Barrier)의 중요성
rte_wmb()는 Descriptor 쓰기가 실제 메모리에 완료된 후 Tail Register를 갱신하도록 보장합니다. 이를 통해 NIC가 Descriptor를 읽을 때 데이터가 아직 메모리에 쓰여지지 않은 추측적 읽기를 방지합니다.
PMD TX (송신) 동작 원리 왼쪽: 애플리케이션/Descriptor | 오른쪽: mbuf | 아래: NIC ① 애플리케이션 준비 → PMD가 Descriptor 채움 tx_pkts[] (전송할 패킷) Descriptor Ring buffer_addr, len, EOP, RS mbuf 주소 읽음 mbuf (패킷 데이터) ② 메모리 배리어 + Tail Register 갱신 rte_wmb() Tail Register NIC에게 알림 "이제 전송" ③ NIC가 Descriptor 읽고 DMA로 데이터 전송 NIC가 Descriptor 읽음 mbuf 주소, 길이 획득 NIC가 DMA로 패킷 데이터 읽음 mbuf에서 데이터 읽음 → 네트워크로 전송 NIC 패킷 전송 완료 ④ 전송 완료 → Descriptor 회수 (mbuf 반환) NIC가 DD=1 설정 (전송 완료) PMD가 Descriptor 검사 DD=1 → 사용된 mbuf 회수 Mempool에 반환 rte_mbuf_raw_free() 핵심: EOP(End Of Packet) | RS(Report Status): NIC에게 전송 완료 후 DD 비트 설정 요청
PMD 종류백엔드대표 드라이버특징
Physical PMDVFIO / UIOixgbe, i40e, ice, mlx5, bnxt, ena실제 NIC 직접 제어, 최고 성능
Virtual PMDvirtiovirtio-net, vmxnet3, avfVM 내부에서 가상 NIC 접근
AF_XDP PMD커널 AF_XDP 소켓af_xdp커널 기능 유지하면서 고성능, VFIO 불필요
SW PMDlibpcap / TAPpcap, net_tap개발/테스트용, 커널 NIC에 연결
Crypto PMDQAT / AESNI / SWqat, aesni_mb, openssl암호화(Encryption) 가속기 접근
Compress PMDQAT / zlibqat_comp, zlib압축 가속기
Event PMD하드웨어 이벤트 스케줄러(Scheduler)dlb2, sw_event이벤트 기반 패킷 분배

Rx/Tx 오프로드 플래그

DPDK PMD는 NIC 하드웨어의 오프로드 기능을 ol_flagstx_offload 필드로 노출합니다. 올바른 오프로드 설정은 CPU 부하를 NIC으로 이전하여 처리량을 크게 향상시킵니다.

Rx 오프로드플래그동작
IP Checksum 검증RTE_ETH_RX_OFFLOAD_IPV4_CKSUMNIC가 IPv4 헤더 체크섬(Checksum)을 검증, 결과를 ol_flags에 기록
TCP/UDP Checksum 검증RTE_ETH_RX_OFFLOAD_TCP_CKSUMNIC가 L4 체크섬 검증
VLAN 스트리핑RTE_ETH_RX_OFFLOAD_VLAN_STRIPNIC가 VLAN 태그를 제거하고 vlan_tci에 저장
RSSRTE_ETH_RX_OFFLOAD_RSS_HASHNIC가 패킷 해시(Hash) 계산, hash.rss에 저장
Scatter (SG)RTE_ETH_RX_OFFLOAD_SCATTERJumbo Frame을 다중 mbuf에 분산 수신
LRORTE_ETH_RX_OFFLOAD_TCP_LRONIC가 TCP 세그먼트를 병합 (GRO의 하드웨어 버전)
TimestampRTE_ETH_RX_OFFLOAD_TIMESTAMPNIC가 수신 타임스탬프 기록 (PTP/IEEE 1588)
Tx 오프로드플래그동작
IP Checksum 계산RTE_ETH_TX_OFFLOAD_IPV4_CKSUMNIC가 송신 시 IPv4 체크섬 계산 (앱은 0으로 설정)
TCP/UDP ChecksumRTE_ETH_TX_OFFLOAD_TCP_CKSUMNIC가 L4 체크섬 계산
TSO (TCP Segmentation)RTE_ETH_TX_OFFLOAD_TCP_TSONIC가 대형 TCP 세그먼트를 MSS 단위로 분할
VLAN 삽입RTE_ETH_TX_OFFLOAD_VLAN_INSERTNIC가 VLAN 태그 삽입
Multi-segmentRTE_ETH_TX_OFFLOAD_MULTI_SEGSScatter-Gather 송신 (다중 mbuf 체인)
VXLAN/GRE TSORTE_ETH_TX_OFFLOAD_VXLAN_TNL_TSO터널(Tunnel) 내부 TCP에 대한 TSO
/* Tx 오프로드 설정 예시: TSO + Checksum */
struct rte_eth_conf port_conf = {
    .txmode = {
        .offloads = RTE_ETH_TX_OFFLOAD_IPV4_CKSUM
                   | RTE_ETH_TX_OFFLOAD_TCP_CKSUM
                   | RTE_ETH_TX_OFFLOAD_TCP_TSO,
    },
};

/* 패킷 송신 시 mbuf 오프로드 필드 설정 */
pkt->ol_flags = RTE_MBUF_F_TX_IPV4 | RTE_MBUF_F_TX_IP_CKSUM
              | RTE_MBUF_F_TX_TCP_CKSUM | RTE_MBUF_F_TX_TCP_SEG;
pkt->l2_len = sizeof(struct rte_ether_hdr);   /* 14 */
pkt->l3_len = sizeof(struct rte_ipv4_hdr);     /* 20 */
pkt->l4_len = sizeof(struct rte_tcp_hdr);      /* 20 (옵션 없음) */
pkt->tso_segsz = 1460;                         /* MSS */

/* Checksum: pseudo-header checksum을 앱이 계산, 나머지는 NIC */
rte_ipv4_phdr_cksum(ip_hdr, pkt->ol_flags);

/* NIC capability 조회 */
struct rte_eth_dev_info dev_info;
rte_eth_dev_info_get(port_id, &dev_info);
printf("RX offloads: 0x%lx\\n", dev_info.rx_offload_capa);
printf("TX offloads: 0x%lx\\n", dev_info.tx_offload_capa);
오프로드 디버깅: 오프로드 플래그를 잘못 설정하면 패킷이 조용히 손상됩니다. 예: l2_len/l3_len이 틀리면 NIC가 잘못된 위치에 체크섬을 계산합니다. testpmdcsum fwd 모드로 오프로드 조합을 먼저 검증하세요.

벡터화 PMD와 스칼라 PMD

DPDK PMD는 동일 NIC에 대해 여러 코드 경로를 제공합니다. 벡터화(vectorized) PMD는 SSE/AVX2/AVX-512/NEON SIMD 명령어를 사용하여 한 번에 여러 descriptor를 처리하므로 스칼라 경로 대비 1.5~3배의 처리량을 달성합니다.

PMD 경로특징활성화 조건
벡터 (AVX-512)16개 descriptor 동시 처리, 최고 성능AVX-512 CPU + 단순 오프로드 + 단일 세그먼트
벡터 (AVX2)8개 descriptor 동시 처리AVX2 CPU + 제한된 오프로드
벡터 (SSE/NEON)4개 descriptor 동시 처리기본 x86-64 / ARM64
스칼라 (simple)1개씩 처리, 단순 오프로드만벡터 비활성 시 fallback
스칼라 (full)1개씩 처리, 모든 오프로드 지원TSO, Scatter, Tunnel 등 복잡한 오프로드
벡터 경로 비활성 원인:
  • RTE_ETH_RX_OFFLOAD_SCATTER 활성화 (Jumbo Frame)
  • 복잡한 Tx 오프로드 (TSO, 터널 체크섬)
  • VLAN 스트리핑/삽입 (일부 PMD)
  • rte_eth_dev_info_get()rx_burst_mode/tx_burst_mode로 현재 경로 확인 가능
  • testpmd에서 show port 0 rx_offload capabilities로 벡터 호환 오프로드 확인

벡터 PMD 구현 분석 (AVX2 RX 경로)

벡터 PMD가 어떻게 SIMD로 다수 descriptor를 동시 처리하는지 i40e AVX2 RX 경로를 기반으로 분석합니다.

/* drivers/net/i40e/i40e_rxtx_vec_avx2.c — AVX2 벡터 RX (간략화) */
static inline uint16_t
_recv_raw_pkts_vec_avx2(struct i40e_rx_queue *rxq,
                         struct rte_mbuf **rx_pkts,
                         uint16_t nb_pkts)
{
    volatile union i40e_rx_desc *rxdp = rxq->rx_ring + rxq->rx_tail;
    struct rte_mbuf **sw_ring = rxq->sw_ring + rxq->rx_tail;
    uint16_t nb_rx = 0;

    /* 8개 descriptor를 한 번에 256비트 AVX2 레지스터로 로드 */
    while (nb_rx < nb_pkts) {
        /* ① 8개 descriptor의 status 필드를 AVX2로 일괄 로드 */
        __m256i descs_lo = _mm256_loadu_si256(
            (__m256i *)(rxdp));        /* desc[0]~desc[3] */
        __m256i descs_hi = _mm256_loadu_si256(
            (__m256i *)(rxdp + 4));    /* desc[4]~desc[7] */

        /* ② DD 비트 추출: 8개 descriptor 동시 검사 */
        __m256i staterr = _mm256_and_si256(
            _mm256_unpackhi_epi32(descs_lo, descs_hi),
            dd_mask);
        uint8_t dd_bits = (uint8_t)_mm256_movemask_epi8(
            _mm256_cmpeq_epi32(staterr, dd_mask));

        /* DD=0인 첫 번째 descriptor에서 중단 */
        int num_valid = __builtin_ctz(~dd_bits) / 4;
        if (num_valid == 0) break;

        /* ③ 패킷 길이를 AVX2로 일괄 추출 → mbuf.pkt_len 설정 */
        __m256i pktlen = _mm256_srli_epi32(
            _mm256_and_si256(descs_lo, len_mask), 10);
        /* shuffle로 pkt_len 필드 위치에 정렬 */

        /* ④ mbuf 포인터를 rx_pkts[]에 일괄 저장 */
        _mm256_storeu_si256((__m256i *)&rx_pkts[nb_rx],
            _mm256_loadu_si256((__m256i *)&sw_ring[0]));

        /* ⑤ 새 mbuf 할당 + descriptor 리필 (배치) */
        i40e_rxq_rearm(rxq);

        nb_rx += num_valid;
        rxdp += 8;
        sw_ring += 8;
    }
    return nb_rx;
}

코드 분석: AVX2 벡터 RX

  • ① 256비트 일괄 로드: _mm256_loadu_si256()는 256비트(32바이트)를 한 번에 로드합니다. i40e descriptor가 32바이트이므로, 두 번의 로드로 8개 descriptor(256바이트)를 처리합니다. 스칼라 경로에서 8번의 메모리 읽기가 필요한 작업을 2번으로 줄입니다.
  • ② DD 비트 일괄 검사: _mm256_cmpeq_epi32 + _mm256_movemask_epi8 조합으로 8개 descriptor의 DD 비트를 한 번에 비교하여 비트마스크로 변환합니다. __builtin_ctz()로 연속된 유효 descriptor 수를 O(1)에 계산합니다. 스칼라 경로에서 8번의 조건 분기가 필요한 작업을 분기 없이 처리합니다.
  • ③ 패킷 길이 일괄 추출: descriptor의 pkt_len 필드를 비트 시프트와 마스크로 한 번에 추출하여 mbuf 구조체의 해당 위치에 기록합니다. SIMD shuffle 명령어로 바이트 위치를 재배열하는 기법이 핵심입니다.
  • ④ mbuf 포인터 일괄 저장: sw_ring에서 8개 mbuf 포인터를 _mm256_loadu_si256()로 한 번에 읽어 rx_pkts[] 배열에 저장합니다. 8개의 포인터 복사가 단일 SIMD 명령어로 완료됩니다.
  • ⑤ descriptor 리필: i40e_rxq_rearm()은 사용된 descriptor에 새 mbuf를 배치로 할당합니다. mempool에서 8개를 한 번에 가져와(rte_mempool_get_bulk) descriptor에 IOVA 주소를 기록합니다.
  • 성능 효과: 스칼라 경로 대비 (1) 메모리 로드 횟수 8배 감소, (2) 조건 분기 제거로 분기 예측 실패 없음, (3) 포인터 이동이 SIMD 레지스터 단위로 수행. 결과적으로 코어당 처리량이 1.5~2.5배 향상됩니다.

인터럽트 모드 (하이브리드 폴링)

DPDK는 100% busy-poll만 지원하는 것이 아닙니다. Rx 인터럽트 모드를 사용하면 빈 폴링 시 epoll_wait()로 블로킹하여 CPU를 절약하고, 패킷이 도착하면 VFIO eventfd 인터럽트로 깨어나 폴링을 재개합니다.

/* 인터럽트 모드: 빈 폴링 시 epoll 대기 */
while (!force_quit) {
    uint16_t nb_rx = rte_eth_rx_burst(port_id, queue_id, pkts, 32);

    if (nb_rx == 0) {
        /* 패킷 없음 → 인터럽트 활성화 후 대기 */
        rte_eth_dev_rx_intr_enable(port_id, queue_id);
        struct rte_epoll_event ev;
        rte_epoll_wait(epfd, &ev, 1, timeout_ms);
        rte_eth_dev_rx_intr_disable(port_id, queue_id);
        continue;
    }

    /* 패킷 있음 → 일반 폴링 처리 */
    process_packets(pkts, nb_rx);
}
모드CPU 사용지연적합한 경우
Pure polling100% (항상)최소 (~수백 ns)고 트래픽, 지연 민감 (NFV, 금융)
Interrupt + polling부하 비례첫 패킷 ~수 μs (인터럽트 복귀)저 트래픽, 전력 민감, 코어 부족
Empty poll power적응형설정에 따라 가변가변 부하, 전력 관리 필요
examples/l3fwd-power: DPDK 공식 예제 l3fwd-power는 인터럽트 모드와 empty poll 전력 관리를 함께 시연합니다. 이 예제를 분석하면 busy-poll과 인터럽트 하이브리드의 실제 구현 패턴을 이해할 수 있습니다.

rte_mbuf 구조체(Struct)

rte_mbuf는 DPDK의 패킷 버퍼로, 커널의 sk_buff에 대응하지만 설계 철학이 다릅니다. sk_buff는 동적으로 할당되며 메타데이터가 풍부한 반면, rte_mbuf는 Mempool에서 사전 할당되어 고정 크기의 경량 구조체입니다.

/* rte_mbuf 핵심 필드 (lib/mbuf/rte_mbuf_core.h) */
struct rte_mbuf {
    /* 캐시라인 0 (핫 필드) */
    void         *buf_addr;       /* 가상 주소: headroom + 데이터 시작 */
    rte_iova_t    buf_iova;       /* IO 가상 주소 (DMA용, IOVA) */

    union {
        struct {
            uint32_t pkt_len;       /* 전체 패킷 길이 (체인 포함) */
            uint16_t data_len;      /* 이 세그먼트의 데이터 길이 */
            uint16_t vlan_tci;      /* VLAN Tag */
        };
    };
    uint64_t      ol_flags;       /* Offload 플래그 (checksum, TSO, VLAN...) */

    union {
        uint32_t rss;            /* RSS 해시값 */
        struct {
            uint16_t hash;
            uint16_t id;
        } fdir;                     /* Flow Director 필터 ID */
    } hash;

    uint16_t      data_off;       /* buf_addr부터 데이터 시작까지 오프셋 (headroom) */
    uint16_t      refcnt;         /* 참조 카운트 (공유 mbuf) */
    uint16_t      nb_segs;        /* 체인된 세그먼트 수 */
    uint16_t      port;           /* 입력 포트 ID */

    /* 캐시라인 1 (TX offload) */
    uint64_t      tx_offload;     /* L2/L3/L4 len, TSO segsz 등 (비트필드) */
    struct rte_mempool *pool;    /* 소속 Mempool (반환 시 사용) */
    struct rte_mbuf    *next;    /* 다음 세그먼트 (체인) */

    /* 메모리 배치: rte_mbuf 구조체 + headroom + packet data + tailroom */
    /* 자세한 배치는 아래 SVG 참고 */
};
rte_mbuf 메모리 배치 rte_mbuf struct headroom (128B) packet data (variable) tailroom buf_addr data_off 기준 데이터 시작
비교 항목sk_buff (커널)rte_mbuf (DPDK)
크기~240 바이트 (가변)2 캐시라인 (128 바이트) 고정
할당__alloc_skb() → SLABMempool에서 사전 할당 (rte_pktmbuf_alloc)
해제kfree_skb() / consume_skb()rte_pktmbuf_free() → Mempool 반환
DMA 주소dma_map_single() 필요buf_iova 필드에 사전 매핑
메타데이터풍부 (sk, dst, nf_bridge, tc...)최소한 (앱이 필요 시 priv_size로 확장)
체인frag_list, skb_shinfonext 포인터로 세그먼트 체인
복제skb_clone() (데이터 공유)rte_pktmbuf_clone() (refcnt 증가)

mbuf 조작 함수 상세 분석

DPDK의 mbuf 조작 함수들은 성능을 위해 대부분 static inline으로 구현됩니다. 핵심 함수의 내부 동작을 분석합니다.

/* lib/mbuf/rte_mbuf.h — mbuf 할당과 해제 핵심 구현 */

/* ━━━ 할당: rte_pktmbuf_alloc() ━━━ */
static inline struct rte_mbuf *
rte_pktmbuf_alloc(struct rte_mempool *mp)
{
    struct rte_mbuf *m;

    /* mempool에서 mbuf 1개를 꺼냄 */
    if (rte_mempool_get(mp, (void **)&m) < 0)
        return NULL;

    /* mbuf 필드 초기화 (패킷 수신 준비) */
    rte_pktmbuf_reset(m);
    /* rte_pktmbuf_reset() 내부:
     *   m->next = NULL;
     *   m->pkt_len = 0;
     *   m->tx_offload = 0;
     *   m->vlan_tci = 0;
     *   m->nb_segs = 1;
     *   m->port = RTE_MBUF_PORT_INVALID;
     *   m->ol_flags = 0;
     *   m->data_off = RTE_PKTMBUF_HEADROOM;  // 128바이트
     *   m->data_len = 0;
     */
    return m;
}

/* ━━━ 해제: rte_pktmbuf_free() ━━━ */
static inline void
rte_pktmbuf_free(struct rte_mbuf *m)
{
    struct rte_mbuf *m_next;

    while (m != NULL) {
        m_next = m->next;
        /* 참조 카운트 감소: 0이면 실제 해제 */
        if (__rte_mbuf_refcnt_update(m, -1) == 0) {
            m->next = NULL;
            rte_mbuf_raw_free(m);
            /* → rte_mempool_put(m->pool, m)
             *   per-core 캐시에 반환 */
        }
        m = m_next;
    }
}

/* ━━━ 데이터 접근: rte_pktmbuf_mtod() ━━━ */
#define rte_pktmbuf_mtod_offset(m, t, o) \
    ((t)((char *)(m)->buf_addr + (m)->data_off + (o)))

#define rte_pktmbuf_mtod(m, t) \
    rte_pktmbuf_mtod_offset(m, t, 0)

/* 사용 예: 이더넷 헤더 접근 */
struct rte_ether_hdr *eth = rte_pktmbuf_mtod(m,
    struct rte_ether_hdr *);

/* ━━━ 헤더 추가: rte_pktmbuf_prepend() ━━━ */
static inline char *
rte_pktmbuf_prepend(struct rte_mbuf *m, uint16_t len)
{
    /* headroom 공간이 충분한지 확인 */
    if (rte_pktmbuf_headroom(m) < len)
        return NULL;

    /* data_off를 앞으로 이동 → 헤더 공간 확보 */
    m->data_off -= len;
    m->data_len += len;
    m->pkt_len  += len;

    return (char *)m->buf_addr + m->data_off;
}

/* ━━━ 데이터 추가: rte_pktmbuf_append() ━━━ */
static inline char *
rte_pktmbuf_append(struct rte_mbuf *m, uint16_t len)
{
    void *tail;
    struct rte_mbuf *m_last = rte_pktmbuf_lastseg(m);

    /* tailroom 공간이 충분한지 확인 */
    if (rte_pktmbuf_tailroom(m_last) < len)
        return NULL;

    /* 현재 데이터 끝 위치 계산 */
    tail = (char *)m_last->buf_addr + m_last->data_off
           + m_last->data_len;
    m_last->data_len += len;
    m->pkt_len       += len;

    return (char *)tail;
}

/* ━━━ 복제: rte_pktmbuf_clone() ━━━ */
static inline struct rte_mbuf *
rte_pktmbuf_clone(struct rte_mbuf *md,
                   struct rte_mempool *mp)
{
    struct rte_mbuf *mc;

    /* 새 mbuf 할당 (간접 mbuf) */
    mc = rte_pktmbuf_alloc(mp);

    /* 원본의 buf_addr과 data_off를 공유 → 데이터 복사 없음 */
    rte_pktmbuf_attach(mc, md);
    /* attach 내부:
     *   mc->buf_addr = md->buf_addr;
     *   mc->buf_iova = md->buf_iova;
     *   mc->data_off = md->data_off;
     *   mc->data_len = md->data_len;
     *   rte_mbuf_refcnt_update(md, 1);  // 원본 참조 카운트 증가
     *   mc->ol_flags |= IND_ATTACHED_MBUF;
     */

    mc->pkt_len = md->pkt_len;
    mc->nb_segs = md->nb_segs;
    return mc;
}

코드 분석: mbuf 조작 함수

  • rte_pktmbuf_alloc(): rte_mempool_get()은 먼저 per-core 로컬 캐시에서 mbuf를 꺼냅니다(락 없음, O(1)). 캐시가 비면 공용 Ring에서 배치로 가져옵니다. rte_pktmbuf_reset()은 메타데이터를 초기화하고 data_off를 128바이트(headroom)로 설정합니다. headroom은 이후 rte_pktmbuf_prepend()로 터널 헤더나 VLAN 태그를 추가할 공간입니다.
  • rte_pktmbuf_free(): 체인된 모든 세그먼트를 순회하며 각각의 refcnt를 감소시킵니다. refcnt가 0이 되면 rte_mempool_put()으로 per-core 캐시에 반환합니다. rte_pktmbuf_clone()으로 복제된 mbuf는 원본의 refcnt가 2 이상이므로, clone을 free해도 원본 데이터가 유지됩니다.
  • rte_pktmbuf_mtod(): 매크로로 구현되어 함수 호출 오버헤드가 없습니다. buf_addr + data_off가 패킷 데이터의 시작 주소입니다. data_offprepend/adj 연산에 의해 변경될 수 있습니다.
  • rte_pktmbuf_prepend(): headroom을 소비하여 패킷 앞에 헤더를 추가합니다. 데이터 복사 없이 data_off 포인터만 이동하므로 O(1)입니다. 터널 캡슐화(VXLAN, GRE)에서 외부 헤더 추가에 사용됩니다. headroom이 부족하면 NULL을 반환하므로, 여러 단계의 캡슐화가 필요하면 headroom을 넉넉히(256바이트 이상) 설정해야 합니다.
  • rte_pktmbuf_append(): tailroom을 소비하여 패킷 뒤에 데이터를 추가합니다. 패딩, 트레일러, FCS 삽입 등에 사용됩니다. 멀티 세그먼트 mbuf에서는 마지막 세그먼트의 tailroom을 사용합니다.
  • rte_pktmbuf_clone(): 데이터를 복사하지 않고 새 mbuf가 원본의 버퍼를 공유하는 "간접 mbuf"를 생성합니다. 원본의 refcnt를 1 증가시켜 원본이 먼저 해제되지 않도록 보호합니다. 패킷 미러링, 멀티캐스트(여러 포트로 동일 패킷 전송)에 유용합니다.

배치 할당/해제 (Bulk Operations)

/* 배치 할당: 한 번에 여러 mbuf를 mempool에서 꺼냄 */
struct rte_mbuf *mbufs[32];
int ret = rte_pktmbuf_alloc_bulk(pool, mbufs, 32);
/* 내부: rte_mempool_get_bulk()로 32개를 한 번에 가져옴
 * → per-core 캐시에서 32개 pop (CAS 불필요)
 * → 캐시 부족 시 공용 Ring에서 bulk 리필 (CAS 1회)
 * → 개별 alloc 32번 호출 대비 ~3배 빠름 */

/* 배치 해제: 한 번에 여러 mbuf를 mempool에 반환 */
unsigned int i;
for (i = 0; i < nb_rx; i++)
    pkts[i] = bufs[i];  /* 미전송 패킷 수집 */
rte_pktmbuf_free_bulk(pkts, nb_rx);
/* 내부: rte_mempool_put_bulk()로 한 번에 반환
 * → per-core 캐시에 한 번에 push
 * → 캐시 오버플로우 시 공용 Ring에 bulk 반환 */
성능 핵심: 배치 연산 — 개별 rte_pktmbuf_alloc()/rte_pktmbuf_free()를 루프에서 호출하면 매번 캐시 상태를 확인합니다. _bulk() 변형은 단일 캐시 접근으로 여러 객체를 처리하므로 3~5배 빠릅니다. PMD 내부에서 descriptor 리필 시 항상 bulk 할당을 사용하는 이유입니다.

멀티 세그먼트 mbuf (Scatter-Gather)

단일 mbuf의 데이터 영역(기본 2048B)보다 큰 패킷(Jumbo Frame, 최대 9KB)은 여러 mbuf를 next 포인터로 체이닝하여 표현합니다. 이를 Scatter-Gather라고 하며, NIC의 SG DMA 기능과 연동됩니다.

멀티 세그먼트 mbuf 체인 (Jumbo Frame 9000B) mbuf[0] hdr data 2048B pkt_len=9000 next mbuf[1] data 2048B data_len=2048 next mbuf[2] data 2048B data_len=2048 next mbuf[3] 2856B next=NULL head mbuf: pkt_len = 전체 패킷 길이 (9000B), nb_segs = 4, data_len = 이 세그먼트만의 데이터 길이 후속 mbuf: pkt_len = 0, data_len = 이 세그먼트 데이터 길이, next = 다음 세그먼트 (마지막은 NULL) 해제: rte_pktmbuf_free()는 체인을 순회하며 모든 세그먼트를 각각의 mempool에 반환 주의: Scatter RX를 사용하려면 rte_eth_rxmode.offloads에 RTE_ETH_RX_OFFLOAD_SCATTER 설정 필요
/* 멀티 세그먼트 mbuf 순회 */
struct rte_mbuf *seg = pkt;
uint32_t total_len = 0;

while (seg != NULL) {
    uint8_t *data = rte_pktmbuf_mtod(seg, uint8_t *);
    process_segment(data, seg->data_len);
    total_len += seg->data_len;
    seg = seg->next;
}
/* total_len == pkt->pkt_len 이어야 정상 */

/* 멀티 세그먼트를 단일 세그먼트로 선형화 (필요 시) */
if (rte_pktmbuf_linearize(pkt) != 0)
    rte_pktmbuf_free(pkt);  /* 선형화 실패 (headroom 부족) */

PMD Scatter RX 구현 분석

NIC가 수신한 패킷이 단일 descriptor 버퍼(2048B)보다 크면, PMD는 여러 descriptor에 걸쳐 수신하고 mbuf를 체이닝합니다.

/* drivers/net/ixgbe/ixgbe_rxtx.c — Scatter RX 경로 (간략화) */
static uint16_t
ixgbe_recv_pkts_lro_bulk_alloc(void *rx_queue,
    struct rte_mbuf **rx_pkts, uint16_t nb_pkts)
{
    struct ixgbe_rx_queue *rxq = rx_queue;
    struct rte_mbuf *first_seg = NULL;
    struct rte_mbuf *last_seg = NULL;
    uint16_t nb_rx = 0;

    while (nb_rx < nb_pkts) {
        volatile union ixgbe_adv_rx_desc *rxdp;
        rxdp = &rxq->rx_ring[rxq->rx_tail];

        if (!(rxdp->wb.upper.status_error & IXGBE_RXDADV_STAT_DD))
            break;

        struct rte_mbuf *rxm = rxq->sw_ring[rxq->rx_tail];
        rxm->data_len = rxdp->wb.upper.length;

        /* ① 첫 번째 세그먼트인가? */
        if (first_seg == NULL) {
            first_seg = rxm;
            first_seg->pkt_len = 0;
            first_seg->nb_segs = 0;
        }

        /* ② 체인에 현재 세그먼트 연결 */
        first_seg->pkt_len += rxm->data_len;
        first_seg->nb_segs++;
        if (last_seg != NULL)
            last_seg->next = rxm;
        last_seg = rxm;
        rxm->next = NULL;

        /* ③ EOP(End Of Packet) 비트: 패킷 완료? */
        if (rxdp->wb.upper.status_error & IXGBE_RXDADV_STAT_EOP) {
            /* 패킷 완전 수신 — 메타데이터 설정 */
            first_seg->hash.rss = rxdp->wb.lower.hi_dword.rss;
            first_seg->ol_flags = rx_desc_status_to_pkt_flags(
                rxdp->wb.upper.status_error);
            first_seg->port = rxq->port_id;

            rx_pkts[nb_rx++] = first_seg;
            first_seg = NULL;
            last_seg = NULL;
        }

        /* 새 mbuf 할당하여 descriptor 리필 */
        struct rte_mbuf *nmb = rte_mbuf_raw_alloc(rxq->mb_pool);
        rxdp->read.pkt_addr = rte_mbuf_data_iova_default(nmb);
        rxq->sw_ring[rxq->rx_tail] = nmb;
        rxq->rx_tail = (rxq->rx_tail + 1) & rxq->rx_tail_mask;
    }
    return nb_rx;
}

코드 분석: PMD Scatter RX

  • first_seg / last_seg 추적: 패킷이 여러 descriptor에 걸칠 수 있으므로, 첫 번째 세그먼트와 마지막 세그먼트를 별도로 추적합니다. 새 descriptor를 처리할 때마다 last_seg->next = rxm으로 체인을 연장합니다.
  • EOP 비트: NIC는 패킷의 마지막 descriptor에 EOP(End Of Packet) 상태 비트를 설정합니다. EOP가 없는 descriptor는 패킷의 중간 세그먼트이므로, 다음 descriptor를 계속 체이닝합니다. EOP를 만나면 first_segrx_pkts[]에 저장하고 초기화합니다.
  • pkt_len vs data_len: first_seg->pkt_len은 전체 패킷 길이(모든 세그먼트 합), 각 세그먼트의 data_len은 해당 세그먼트만의 데이터 길이입니다. 첫 번째 세그먼트만 pkt_len이 유효하고 후속 세그먼트의 pkt_len은 0입니다.
  • 성능 영향: Scatter RX는 descriptor마다 조건 분기(EOP 체크)와 포인터 연결이 추가되므로 벡터 PMD를 사용할 수 없습니다. 9000B Jumbo Frame은 최소 5개 descriptor(9000/2048=4.4)를 소비하므로, 동일 PPS에서 descriptor 소비율이 ~5배 증가합니다.

rte_pktmbuf_chain()과 rte_pktmbuf_linearize() 구현

/* lib/mbuf/rte_mbuf.h — 두 mbuf를 체이닝 */
static inline int
rte_pktmbuf_chain(struct rte_mbuf *head,
                   struct rte_mbuf *tail)
{
    struct rte_mbuf *cur_tail;

    /* head의 마지막 세그먼트 찾기 */
    cur_tail = rte_pktmbuf_lastseg(head);

    /* tail이 이미 간접 mbuf이거나 refcnt > 1이면 실패 */
    if (tail->ol_flags & IND_ATTACHED_MBUF ||
        rte_mbuf_refcnt_read(tail) != 1)
        return -EOVERFLOW;

    /* 체인 연결 */
    cur_tail->next = tail;
    head->nb_segs += tail->nb_segs;
    head->pkt_len += tail->pkt_len;

    /* tail은 더 이상 독립 패킷이 아님 */
    tail->pkt_len = tail->data_len;
    return 0;
}

/* lib/mbuf/rte_mbuf.c — 멀티 세그먼트를 단일 버퍼로 선형화 */
int
rte_pktmbuf_linearize(struct rte_mbuf *mbuf)
{
    size_t seg_len, copy_len;
    struct rte_mbuf *m, *m_next;
    char *buffer;

    /* 이미 단일 세그먼트면 아무것도 안 함 */
    if (mbuf->nb_segs == 1)
        return 0;

    /* 첫 번째 mbuf의 tailroom에 나머지 데이터가 들어가는지 확인 */
    copy_len = mbuf->pkt_len - mbuf->data_len;
    if (rte_pktmbuf_tailroom(mbuf) < copy_len)
        return -1;  /* 공간 부족 */

    /* 후속 세그먼트 데이터를 첫 번째 mbuf 뒤에 복사 */
    buffer = rte_pktmbuf_mtod_offset(mbuf, char *,
                                       mbuf->data_len);
    m = mbuf->next;
    while (m != NULL) {
        m_next = m->next;
        seg_len = m->data_len;
        rte_memcpy(buffer,
                    rte_pktmbuf_mtod(m, char *),
                    seg_len);
        buffer += seg_len;

        /* 후속 세그먼트를 mempool에 반환 */
        rte_pktmbuf_free_seg(m);
        m = m_next;
    }

    /* 단일 세그먼트로 갱신 */
    mbuf->data_len = mbuf->pkt_len;
    mbuf->nb_segs = 1;
    mbuf->next = NULL;
    return 0;
}

코드 분석: chain()과 linearize()

  • rte_pktmbuf_chain(): 두 개의 독립 mbuf(또는 체인)를 하나의 패킷으로 연결합니다. 터널 캡슐화에서 외부 헤더(새 mbuf)와 내부 패킷(기존 mbuf)을 합칠 때 유용합니다. head->pkt_len에 tail의 길이를 누적하고, head->nb_segs에 tail의 세그먼트 수를 더합니다. 간접 mbuf(refcnt > 1)는 체인할 수 없습니다.
  • rte_pktmbuf_linearize(): 멀티 세그먼트 mbuf를 단일 연속 버퍼로 변환합니다. 후속 세그먼트의 데이터를 첫 번째 mbuf의 tailroom에 rte_memcpy()로 복사하고, 후속 세그먼트를 mempool에 반환합니다. tailroom이 부족하면 실패합니다.
  • linearize 대안: 9000B 패킷을 linearize하려면 첫 번째 mbuf에 9000B tailroom이 필요하지만, 기본 mbuf 데이터 영역은 2048B입니다. 따라서 Jumbo Frame에 linearize를 적용하려면 mempool 생성 시 data_room_size를 9KB+headroom으로 늘려야 합니다. 이는 메모리 낭비가 크므로, 실무에서는 세그먼트를 직접 순회하는 것이 일반적입니다.
  • rte_pktmbuf_free_seg(): 체인 전체가 아닌 단일 세그먼트만 해제합니다. rte_pktmbuf_free()와 달리 next 포인터를 따라가지 않으므로, linearize에서 후속 세그먼트를 개별 해제할 때 사용합니다.
Scatter-Gather 성능 영향: 멀티 세그먼트 mbuf는 벡터화 PMD 경로를 비활성화할 수 있습니다. Jumbo Frame이 필요하지 않다면 MTU를 1500으로 유지하여 단일 세그먼트 fast path를 사용하세요. 필요한 경우에도 rte_pktmbuf_linearize()보다는 세그먼트를 직접 순회하는 것이 더 효율적입니다.

Ring 라이브러리

DPDK rte_ring은 고정 크기, FIFO, lock-free 큐로, 코어 간 패킷 전달, 멀티프로세스 IPC, 태스크(Task) 분배 등에 사용됩니다. CAS(Compare-And-Swap) 연산 기반으로 락 없이 다수 생산자/소비자를 지원합니다.

/* Ring 기본 사용 */
struct rte_ring *ring = rte_ring_create(
    "MY_RING",
    1024,                /* 크기 (2의 거듭제곱이어야 함) */
    rte_socket_id(),     /* NUMA 소켓 */
    RING_F_SP_ENQ | RING_F_SC_DEQ  /* 단일 생산자/소비자 (더 빠름) */
);

/* 생산자: 패킷을 Ring에 삽입 */
struct rte_mbuf *pkts[32];
unsigned nb = rte_eth_rx_burst(port, 0, pkts, 32);
unsigned sent = rte_ring_enqueue_burst(ring, (void **)pkts, nb, NULL);

/* 소비자: Ring에서 패킷 추출 (다른 코어) */
struct rte_mbuf *dequeued[32];
unsigned got = rte_ring_dequeue_burst(ring, (void **)dequeued, 32, NULL);
rte_ring (Lock-free) 개념도 [slot0][slot1]...[slotN-1] / prod.head, prod.tail, cons.head, cons.tail MP/MC: head는 CAS로 예약, tail은 순서 보장 갱신 SP/SC: CAS 생략 가능, 메모리 배리어 중심으로 더 빠름 rte_ring (size = 2^n) prod.head - prod.tail - ... - cons.head - cons.tail [slot0][slot1][slot2]...[slotN-1] 1) CAS로 슬롯 예약 2) 데이터 복사 3) tail 순차 갱신 MP/MC 모드 생산자/소비자 다수, CAS 필수 정합성 확보 대신 오버헤드 증가 SP/SC 모드 단일 생산자/소비자, CAS 생략 메모리 배리어 중심으로 저지연
Ring 모드 선택:
  • RING_F_SP_ENQ | RING_F_SC_DEQ — 1:1 코어 매핑 시 최적 (파이프라인(Pipeline) 모델)
  • RING_F_MP_HTS_ENQ | RING_F_MC_HTS_DEQ — head/tail sync 방식. 기존 MP/MC보다 공정하고 순서 보장(Ordering) 강화
  • Ring 크기는 반드시 2의 거듭제곱이어야 합니다 (마스크 연산 최적화)

MP enqueue CAS 알고리즘 구현 분석

Multi-Producer(MP) enqueue는 여러 코어가 동시에 Ring에 삽입할 수 있도록 lock-free CAS 알고리즘을 사용합니다. 이 구현은 DPDK 성능의 핵심 기반입니다.

/* lib/ring/rte_ring_c11_pvt.h — MP enqueue 핵심 (간략화) */
static inline unsigned int
__rte_ring_move_prod_head(struct rte_ring *r,
    unsigned int is_sp, unsigned int n,
    enum rte_ring_queue_behavior behavior,
    uint32_t *old_head, uint32_t *new_head,
    uint32_t *free_entries)
{
    uint32_t cons_tail;
    int success;

    *old_head = __atomic_load_n(&r->prod.head,
                                 __ATOMIC_RELAXED);
    do {
        /* ① 현재 소비자 tail 읽기 (데이터 의존성으로 acquire) */
        cons_tail = __atomic_load_n(&r->cons.tail,
                                     __ATOMIC_ACQUIRE);

        /* ② 빈 슬롯 수 계산 */
        *free_entries = r->capacity -
            (*old_head - cons_tail);

        /* ③ 삽입할 수 있는 개수 결정 */
        if (n > *free_entries)
            n = (behavior == RTE_RING_QUEUE_FIXED) ?
                0 : *free_entries;
        if (n == 0) return 0;

        *new_head = *old_head + n;

        if (is_sp) {
            /* SP 모드: CAS 불필요, 직접 갱신 */
            r->prod.head = *new_head;
            success = 1;
        } else {
            /* ④ MP 모드: CAS로 head를 원자적으로 이동 */
            success = __atomic_compare_exchange_n(
                &r->prod.head,
                old_head, *new_head,
                0,  /* weak CAS: 실패 시 재시도 */
                __ATOMIC_RELAXED,
                __ATOMIC_RELAXED);
        }
    } while (!success);
    /* CAS 성공: [old_head, new_head) 구간이 이 코어에 예약됨 */

    return n;
}

/* 예약 후 데이터 복사 + tail 갱신 */
static inline unsigned int
__rte_ring_do_enqueue_elem(struct rte_ring *r,
    const void *obj_table, unsigned int esize,
    unsigned int n, ...)
{
    uint32_t old_head, new_head;

    /* ① head 이동 (슬롯 예약) */
    n = __rte_ring_move_prod_head(r, is_sp, n,
            behavior, &old_head, &new_head, &free);

    /* ② 예약된 슬롯에 데이터 복사 */
    __rte_ring_enqueue_elems(r, old_head, obj_table,
                             esize, n);

    /* ③ tail을 순차적으로 갱신
     * 다른 코어가 먼저 CAS에 성공했지만 아직 데이터 복사 중이면,
     * 이 코어는 spin-wait하며 기다림 → 순서 보장 */
    __rte_ring_update_tail(&r->prod, old_head,
                            new_head, is_sp, 1);
    /* update_tail 내부:
     * while (rte_atomic_load(&prod.tail) != old_head)
     *     rte_pause();  // 이전 코어의 복사 완료 대기
     * rte_atomic_store(&prod.tail, new_head, release);
     */

    return n;
}

코드 분석: Ring MP enqueue CAS 알고리즘

  • 3단계 프로토콜: MP enqueue는 (1) CAS로 head를 이동하여 슬롯 예약 → (2) 예약된 슬롯에 데이터 복사 → (3) tail을 순차적으로 갱신하는 3단계입니다. 이 분리가 lock-free의 핵심입니다.
  • CAS(Compare-And-Swap): __atomic_compare_exchange_n()prod.head가 예상값(old_head)과 같으면 new_head로 교체하고, 다르면(다른 코어가 먼저 수정) 실패하여 do-while 루프에서 재시도합니다. 코어 수가 적을수록 CAS 경합이 적어 성능이 좋습니다.
  • SP 모드 최적화: 단일 생산자에서는 CAS가 불필요합니다. 직접 prod.head를 갱신하므로 메모리 배리어 비용만 남습니다. 파이프라인 모델에서 코어 간 1:1 Ring에는 SP/SC를 사용하는 것이 2~3배 빠릅니다.
  • tail 순차 갱신: CAS는 head만 이동하므로, 코어 A가 슬롯 [0,4)를 예약하고 코어 B가 [4,8)을 예약한 상황에서 B가 먼저 데이터 복사를 완료할 수 있습니다. 하지만 tail은 0→4→8 순서로만 갱신되어야 소비자가 올바른 데이터를 읽습니다. 따라서 B는 A의 tail 갱신이 완료될 때까지 rte_pause()로 spin-wait합니다.
  • rte_pause(): x86에서는 PAUSE 명령어로 구현됩니다. spin-wait 루프에서 파이프라인 플러시를 줄이고, Hyper-Threading 환경에서 sibling 코어에 실행 자원을 양보합니다.
  • 마스크 연산: Ring 크기가 2^n이므로 old_head & mask로 인덱스를 계산합니다. 나머지 연산(%)보다 비트 AND(&)가 수십 배 빠릅니다. 이것이 Ring 크기가 반드시 2의 거듭제곱이어야 하는 이유입니다.
  • capacity vs size: 실제 사용 가능 슬롯 수(capacity)는 size - 1입니다. 한 슬롯을 비워두어 "가득 참"과 "빈 상태"를 구분합니다 (head == tail이면 빈 상태).
Core A Core B ① CAS(head, 0→4) 성공 슬롯 [0,1,2,3] 예약 ① CAS(head, 4→8) 성공 슬롯 [4,5,6,7] 예약 ② 슬롯 [0~3]에 데이터 복사 (느림 — 캐시 미스 발생) ② 슬롯 [4~7]에 데이터 복사 (빠르게 완료) ③ spin-wait: tail==0? (A 대기) rte_pause() 반복 ③ tail = 4 (release store) 소비자가 [0~3] 읽기 가능 ③ tail = 8 (release store) 소비자가 [4~7] 읽기 가능 tail==4 감지

Mempool과 메모리 관리

rte_mempool은 고정 크기 객체의 풀로, DPDK에서 mbuf 할당/해제의 핵심입니다. Hugepage 위에 구축되며, per-core 캐시로 코어 간 경합(Contention)을 최소화합니다.

Mempool 메모리 레이아웃 (Hugepage 2MB / 1GB) rte_mempool header (메타데이터: 크기·이름·통계·캐시 포인터) Per-Core Cache (락 없이 할당/반환 — O(1)) Core 0: [mbuf ptr] × 256 Core 1: [mbuf ptr] × 256 Core 2: [mbuf ptr] × 256 ··· 캐시 미스 시 공용 Ring에서 배치(bulk) 로드 → CAS 1회만 발생 Ring (공용 풀) — [mbuf ptr] × N (캐시 부족 시 배치로 가져옴) mbuf Objects (물리적으로 연속된 Hugepage 상에 배치) mbuf[0] headroom data area mbuf[1] headroom data area ··· mbuf[N] headroom data area 각 mbuf = rte_mbuf 구조체(128B) + headroom(128B) + data(2048B) — 캐시라인 정렬 할당 경로 ① per-core 캐시에서 mbuf 포인터 가져옴 (O(1)) ② 캐시 비면 → 공용 Ring에서 bulk 가져옴 (CAS 1회) ③ Ring도 비면 → 할당 실패 반환 반환 경로 ① per-core 캐시에 mbuf 포인터 반환 (O(1)) ② 캐시 가득 차면 → 공용 Ring에 bulk 반환 ③ 전체 과정에서 락 없음 — lock-free CAS만 사용
/* Mempool 생성과 NUMA 인식 */
struct rte_mempool *pool;

/* 기본: 패킷 mbuf 풀 */
pool = rte_pktmbuf_pool_create(
    "PKT_POOL",
    65535,                   /* 총 mbuf 개수 (2^n - 1 권장) */
    512,                     /* per-core 캐시 크기 (0이면 캐시 비활성) */
    0,                       /* priv_size: mbuf당 추가 메타데이터 크기 */
    RTE_MBUF_DEFAULT_BUF_SIZE,  /* 2048 + 128(headroom) = 2176 */
    rte_socket_id()          /* NUMA 노드 */
);

/* 고급: 외부 메모리로 Mempool 생성 (huge 1GB) */
struct rte_pktmbuf_extmem ext_mem = {
    .buf_ptr  = mmap_addr,      /* 사전 매핑된 hugepage 주소 */
    .buf_iova = iova_addr,      /* IOMMU 매핑된 DMA 주소 */
    .buf_len  = 1ULL << 30,    /* 1GB */
    .elt_size = 2176,           /* mbuf + data 크기 */
};
pool = rte_pktmbuf_pool_create_extbuf("EXT_POOL", 65535, 512,
                                        0, 2176, rte_socket_id(),
                                        &ext_mem, 1);

Mempool 내부 구현: per-core 캐시와 할당 경로

/* lib/mempool/rte_mempool.h — per-core 캐시 구조 */
struct rte_mempool_cache {
    uint32_t size;       /* 캐시 최대 크기 (생성 시 지정) */
    uint32_t flushthresh; /* = size × 1.5, 초과 시 Ring으로 반환 */
    uint32_t len;        /* 현재 캐시에 있는 객체 수 */
    void *objs[0];       /* 객체 포인터 배열 (LIFO 스택) */
};

/* lib/mempool/rte_mempool.h — 할당 핵심: rte_mempool_get() */
static inline int
rte_mempool_get_bulk(struct rte_mempool *mp,
                      void **obj_table, unsigned int n)
{
    struct rte_mempool_cache *cache;

    /* ① 현재 코어의 로컬 캐시 가져오기 */
    cache = rte_mempool_default_cache(mp,
                rte_lcore_id());

    if (cache != NULL && cache->len >= n) {
        /* ② 캐시에 충분한 객체 → 캐시에서 직접 pop
         * 락 없음, CAS 없음, O(1) */
        cache->len -= n;
        rte_memcpy(obj_table,
                    &cache->objs[cache->len],
                    sizeof(void *) * n);
        return 0;
    }

    /* ③ 캐시 부족 → 공용 Ring에서 배치 리필 */
    unsigned int req = n + (cache ? cache->size : 0);
    int ret = rte_mempool_ops_dequeue_bulk(mp,
                  cache ? cache->objs : obj_table, req);
    if (ret < 0)
        return ret;  /* Ring도 비어있음 */

    if (cache) {
        cache->len = cache->size;  /* 캐시 리필 완료 */
        /* 나머지 n개를 obj_table에 복사 */
        rte_memcpy(obj_table,
                    &cache->objs[cache->len],
                    sizeof(void *) * n);
        cache->len -= n;
    }
    return 0;
}

/* lib/mempool/rte_mempool.h — 반환 핵심: rte_mempool_put() */
static inline void
rte_mempool_put_bulk(struct rte_mempool *mp,
                      void * const *obj_table,
                      unsigned int n)
{
    struct rte_mempool_cache *cache;

    cache = rte_mempool_default_cache(mp,
                rte_lcore_id());

    if (cache != NULL) {
        /* 캐시에 push (LIFO 스택) */
        rte_memcpy(&cache->objs[cache->len],
                    obj_table,
                    sizeof(void *) * n);
        cache->len += n;

        /* flushthresh 초과 시 넘치는 분량을 Ring에 반환 */
        if (cache->len >= cache->flushthresh) {
            rte_mempool_ops_enqueue_bulk(mp,
                &cache->objs[cache->size],
                cache->len - cache->size);
            cache->len = cache->size;
        }
        return;
    }

    /* 캐시 없음: 공용 Ring에 직접 반환 */
    rte_mempool_ops_enqueue_bulk(mp, obj_table, n);
}

코드 분석: Mempool per-core 캐시

  • 2레벨 구조: Mempool은 "per-core 로컬 캐시 → 공용 Ring" 2레벨로 구성됩니다. 데이터 경로의 99% 이상의 할당/반환은 로컬 캐시에서 완료되어 코어 간 경합이 없습니다.
  • LIFO 스택: 로컬 캐시는 objs[] 배열 + len 카운터의 LIFO 스택입니다. 가장 최근에 반환된 mbuf를 다시 할당하므로 L1/L2 캐시에 hot한 상태로 재사용될 확률이 높습니다.
  • flushthresh: size × 1.5로 설정됩니다. put 시 캐시가 이 임계치를 넘으면 size까지만 유지하고 나머지를 공용 Ring에 반환합니다. 이 히스테리시스가 없으면 "캐시 가득 → Ring 반환 → 바로 캐시 빔 → Ring 로드" 핑퐁이 발생합니다.
  • rte_mempool_ops_dequeue_bulk(): 공용 Ring에서 객체를 꺼내는 연산입니다. 내부적으로 rte_ring_mc_dequeue_bulk()를 호출하며, 이때만 CAS 연산이 발생합니다. 캐시 리필 시 cache->size개를 한 번에 가져오므로 CAS는 수백~수천 패킷당 1회만 발생합니다.
  • rte_lcore_id(): TLS(Thread-Local Storage)에서 현재 lcore ID를 O(1)에 반환합니다. 이 값으로 per-core 캐시 배열에 인덱싱합니다. 따라서 DPDK 스레드가 아닌 외부 스레드에서 mempool에 접근하면 캐시를 사용할 수 없어 성능이 저하됩니다.

rte_pktmbuf_pool_create() 내부 동작

/* lib/mbuf/rte_mbuf.c — Mempool 생성 핵심 (간략화) */
struct rte_mempool *
rte_pktmbuf_pool_create(const char *name,
    unsigned int n, unsigned int cache_size,
    uint16_t priv_size, uint16_t data_room_size,
    int socket_id)
{
    struct rte_mempool *mp;
    unsigned int elt_size;

    /* ① 요소 크기 계산 */
    elt_size = sizeof(struct rte_mbuf)  /* 128B */
             + priv_size               /* 사용자 메타데이터 */
             + data_room_size;         /* headroom + 데이터 */
    /* 기본: 128 + 0 + 2176 = 2304B per mbuf */

    /* ② Mempool 구조체 생성 */
    mp = rte_mempool_create(name, n, elt_size,
                              cache_size, 0,
                              NULL, NULL,
                              rte_pktmbuf_init, NULL,
                              socket_id, 0);
    /*   rte_mempool_create() 내부:
     *   a) memzone 할당: hugepage에서 n × elt_size 연속 메모리 확보
     *   b) Ring 생성: 공용 풀로 사용할 rte_ring 할당
     *   c) per-core 캐시 배열 할당: lcore 수 × sizeof(cache)
     *   d) 각 요소에 대해 rte_pktmbuf_init() 콜백 호출
     */

    return mp;
}

/* lib/mbuf/rte_mbuf.c — mbuf 초기화 콜백 */
void
rte_pktmbuf_init(struct rte_mempool *mp,
                  void *opaque_arg,
                  void *_m, unsigned int i)
{
    struct rte_mbuf *m = _m;
    uint32_t mbuf_size = rte_pktmbuf_priv_size(mp)
                        + mp->elt_size;

    memset(m, 0, mbuf_size);

    /* buf_addr: mbuf 구조체 바로 뒤 */
    m->buf_addr = (char *)m + sizeof(struct rte_mbuf)
                + rte_pktmbuf_priv_size(mp);

    /* buf_iova: DMA용 IO 가상 주소 */
    m->buf_iova = rte_mempool_virt2iova(m)
                + sizeof(struct rte_mbuf)
                + rte_pktmbuf_priv_size(mp);

    m->buf_len = mp->elt_size
               - sizeof(struct rte_mbuf)
               - rte_pktmbuf_priv_size(mp);

    m->data_off = RTE_PKTMBUF_HEADROOM;  /* 128B */
    m->refcnt = 1;
    m->nb_segs = 1;
    m->pool = mp;
}

코드 분석: Mempool 생성과 mbuf 초기화

  • elt_size 계산: 각 mbuf 요소는 rte_mbuf 구조체(128B) + priv_size + data_room_size(2176B)로 구성됩니다. RTE_MBUF_DEFAULT_BUF_SIZERTE_PKTMBUF_HEADROOM(128) + RTE_ETHER_MAX_LEN(1518) + margin = 2176바이트입니다. 모든 요소가 동일 크기이므로 메모리 단편화가 없습니다.
  • memzone 할당: rte_mempool_create() 내부에서 hugepage 위에 n × elt_size 바이트의 연속 메모리를 확보합니다. 이 메모리가 물리적으로 연속(1GB hugepage 내부)하면 NIC DMA가 scatter 없이 접근할 수 있어 효율적입니다.
  • rte_pktmbuf_init() 콜백: 각 mbuf에 대해 한 번씩 호출되어 buf_addr, buf_iova, buf_len 등을 설정합니다. buf_iova는 생성 시점에 계산되어 고정되므로, RX 경로에서 매번 가상→물리 주소 변환을 할 필요가 없습니다. 이 "사전 매핑(pre-mapping)" 최적화가 DPDK의 제로카피 DMA를 가능하게 합니다.
  • n 값 권장: n = 2^k - 1 (예: 65535)을 권장합니다. 내부 Ring 크기가 2^k이고 1슬롯을 가드로 사용하므로, 2^k - 1개의 요소를 저장하면 공간 낭비 없이 Ring을 최대한 활용합니다.
  • cache_size: 0이면 per-core 캐시 비활성(매번 공용 Ring 접근). 256~512가 일반적입니다. burst_size × 1.5 이상을 권장합니다(예: burst=32이면 cache≥48). 너무 크면 mempool의 유효 객체 수가 n - (lcore_count × cache_size)로 줄어들어 부족해질 수 있습니다.
Hugepage가 필수인 이유:
  • TLB 효율 — 64K개 mbuf × 2KB = 128MB. 4KB 페이지(Page)면 32,768 TLB 엔트리 필요하지만, 2MB hugepage면 64개, 1GB hugepage면 1개
  • 연속 물리 메모리(Physical Memory) — NIC DMA가 물리적으로 연속된 영역을 필요로 함. Hugepage는 2MB/1GB 단위로 물리 연속 보장
  • IOVA 변환 단순화 — 큰 페이지 단위로 IOMMU 매핑하므로 IOTLB 미스 최소화
  • 페이지 폴트(Page Fault) 제거 — mmap 시 MAP_POPULATE로 즉시 물리 페이지 할당. 런타임 페이지 폴트 없음

rte_hash — Cuckoo 해시 테이블

rte_hash는 DPDK의 고성능 해시 테이블로, 플로우 테이블, 세션 캐시, ARP 테이블 등 데이터 경로의 키-값 조회에 사용됩니다. 내부적으로 Cuckoo 해싱(Cuckoo Hashing)을 사용하여 최악의 경우에도 O(1) 조회를 보장합니다.

Primary Table (hash1) bucket[0] | sig | key_idx | sig | key_idx | ... | (8 slots) bucket[1] ← key "10.0.0.1:80" 여기에 저장 bucket[2] bucket[N] 각 버킷 = 8 슬롯 (캐시라인 정렬) sig = 키 해시의 상위 16비트 (signature) key_idx = 키 데이터 배열의 인덱스 Secondary Table (hash2) bucket[0] bucket[1] bucket[2] ← primary 충돌 시 여기에 이동 bucket[N] secondary 버킷 = hash1 XOR hash2로 계산 두 테이블 모두 가득 차면 eviction 체인 시도 충돌 시 이동 키 데이터는 별도 배열(key_store)에 저장, 버킷에는 signature + index만 보유 → 캐시 효율 극대화

rte_hash 생성과 조회 구현

/* rte_hash 생성 */
struct rte_hash_parameters params = {
    .name = "flow_table",
    .entries = 65536,        /* 최대 엔트리 수 */
    .key_len = sizeof(struct flow_key),  /* 키 크기 (바이트) */
    .hash_func = rte_hash_crc,  /* CRC32 해시 함수 */
    .hash_func_init_val = 0,
    .socket_id = rte_socket_id(),
    .extra_flag = RTE_HASH_EXTRA_FLAGS_RW_CONCURRENCY_LF
                | RTE_HASH_EXTRA_FLAGS_MULTI_WRITER_ADD,
};
struct rte_hash *h = rte_hash_create(&params);

/* 키-값 삽입 */
struct flow_key key = { .src_ip = ..., .dst_ip = ..., ... };
int32_t pos = rte_hash_add_key(h, &key);
/* pos >= 0이면 성공, pos가 key_store 인덱스 */
flow_data[pos] = my_flow_data;  /* 사용자 데이터 매핑 */

/* 키-값 삽입 (데이터 포인터 포함) */
rte_hash_add_key_data(h, &key, (void *)flow_data_ptr);

/* 조회 */
int32_t ret = rte_hash_lookup(h, &key);
if (ret >= 0)
    result = flow_data[ret];  /* 인덱스로 데이터 접근 */

/* 데이터 포인터로 직접 조회 */
void *data;
ret = rte_hash_lookup_data(h, &key, &data);

/* 배치 조회: 여러 키를 한 번에 조회 (prefetch 최적화) */
const void *keys[32];
int32_t positions[32];
int hits = rte_hash_lookup_bulk(h, keys, 32, positions);

/* 삭제 */
rte_hash_del_key(h, &key);

rte_hash 내부 구현: Cuckoo 해싱 알고리즘

/* lib/hash/rte_cuckoo_hash.c — rte_hash_add_key() 내부 (간략화) */
int32_t
__rte_hash_add_key_with_hash(const struct rte_hash *h,
    const void *key, hash_sig_t sig)
{
    uint32_t prim_bucket_idx, sec_bucket_idx;
    uint16_t short_sig;  /* signature: 해시의 상위 16비트 */

    /* ① 해시값에서 primary/secondary 버킷 인덱스 계산 */
    prim_bucket_idx = sig & h->bucket_bitmask;
    short_sig = get_short_sig(sig);
    sec_bucket_idx = rte_hash_secondary_hash(sig)
                   & h->bucket_bitmask;

    /* ② primary 버킷에서 빈 슬롯 탐색 */
    struct rte_hash_bucket *prim_bkt =
        &h->buckets[prim_bucket_idx];
    for (int i = 0; i < RTE_HASH_BUCKET_ENTRIES; i++) {
        if (prim_bkt->sig_current[i] == NULL_SIGNATURE) {
            /* 빈 슬롯 발견 → 키 저장 */
            prim_bkt->sig_current[i] = short_sig;
            prim_bkt->key_idx[i] = alloc_key_slot(h);
            rte_memcpy(get_key_slot(h, prim_bkt->key_idx[i]),
                        key, h->key_len);
            return prim_bkt->key_idx[i];
        }
    }

    /* ③ primary 가득 참 → secondary 버킷 시도 */
    struct rte_hash_bucket *sec_bkt =
        &h->buckets[sec_bucket_idx];
    for (int i = 0; i < RTE_HASH_BUCKET_ENTRIES; i++) {
        if (sec_bkt->sig_current[i] == NULL_SIGNATURE) {
            sec_bkt->sig_current[i] = short_sig;
            sec_bkt->key_idx[i] = alloc_key_slot(h);
            rte_memcpy(get_key_slot(h, sec_bkt->key_idx[i]),
                        key, h->key_len);
            return sec_bkt->key_idx[i];
        }
    }

    /* ④ 양쪽 모두 가득 → Cuckoo eviction (BFS 탐색) */
    return rte_hash_cuckoo_insert_mw(h, prim_bkt, sec_bkt,
                                      key, sig, short_sig);
}

/* lib/hash/rte_cuckoo_hash.c — rte_hash_lookup() 내부 (간략화) */
int32_t
__rte_hash_lookup_with_hash(const struct rte_hash *h,
    const void *key, hash_sig_t sig)
{
    uint32_t prim_idx = sig & h->bucket_bitmask;
    uint16_t short_sig = get_short_sig(sig);
    struct rte_hash_bucket *bkt;

    /* ① primary 버킷에서 signature 비교 (캐시라인 1개) */
    bkt = &h->buckets[prim_idx];
    for (int i = 0; i < RTE_HASH_BUCKET_ENTRIES; i++) {
        if (bkt->sig_current[i] == short_sig) {
            /* signature 일치 → 키 전체 비교 */
            void *key_slot = get_key_slot(h, bkt->key_idx[i]);
            if (rte_hash_cmp_eq(key, key_slot, h) == 0)
                return bkt->key_idx[i];  /* 찾음 */
        }
    }

    /* ② secondary 버킷에서 재시도 */
    uint32_t sec_idx = rte_hash_secondary_hash(sig)
                     & h->bucket_bitmask;
    bkt = &h->buckets[sec_idx];
    for (int i = 0; i < RTE_HASH_BUCKET_ENTRIES; i++) {
        if (bkt->sig_current[i] == short_sig) {
            void *key_slot = get_key_slot(h, bkt->key_idx[i]);
            if (rte_hash_cmp_eq(key, key_slot, h) == 0)
                return bkt->key_idx[i];
        }
    }

    return -ENOENT;  /* 키 없음 */
}

코드 분석: rte_hash Cuckoo 해싱

  • Cuckoo 해싱 원리: 각 키에 두 개의 후보 버킷(primary, secondary)이 있습니다. 삽입 시 primary에 먼저 시도하고, 가득 차면 secondary를 시도합니다. 양쪽 모두 가득 차면 기존 엔트리를 "쫓아내어(evict)" 그 엔트리의 대안 버킷으로 이동시킵니다. 이 연쇄 이동을 BFS로 탐색합니다.
  • 버킷 구조 (8-way set-associative): 각 버킷에 8개 슬롯이 있으며, 64바이트 캐시라인에 정렬됩니다. signature(16비트)와 key_idx(32비트)만 버킷에 저장하고 실제 키 데이터는 별도 배열(key_store)에 둡니다. 이 구조로 조회 시 캐시라인 1개만 로드하여 8개 signature를 한 번에 비교할 수 있습니다.
  • signature 비교: 해시의 상위 16비트를 signature로 사용합니다. signature 비교로 대부분의 비매칭을 걸러내고, signature가 일치한 경우에만 실제 키 전체를 비교합니다. 이 2단계 필터로 키 비교 횟수를 최소화합니다.
  • rte_hash_secondary_hash(): secondary 버킷 인덱스는 primary_hash XOR short_sig으로 계산합니다. XOR 관계이므로 secondary에서 primary를 역으로 계산할 수 있어, eviction 시 원래 버킷을 찾을 수 있습니다.
  • 조회 보장: 키가 존재하면 반드시 primary 또는 secondary 버킷 2개만 확인하면 됩니다. 최악의 경우에도 O(1)이며, 캐시라인 접근은 최대 2회(+ 키 비교 1회)입니다.
  • RW_CONCURRENCY_LF: lock-free 읽기/쓰기 동시성을 활성화합니다. 읽기 스레드는 잠금 없이 조회하고, 쓰기 스레드는 CAS로 버킷 슬롯을 갱신합니다. 삭제 시에는 QSBR(RCU)로 안전한 메모리 회수가 필요합니다.
  • rte_hash_lookup_bulk(): 여러 키를 배치로 조회합니다. 내부적으로 첫 번째 키의 버킷을 prefetch하면서 이전 키의 비교를 수행하는 소프트웨어 파이프라이닝을 적용합니다. 단일 조회 대비 ~2배 처리량을 달성합니다.

배치 조회 파이프라이닝 기법

/* rte_hash_lookup_bulk() 내부의 소프트웨어 파이프라이닝 (간략화) */

/* 1단계: 모든 키의 해시 계산 + primary 버킷 prefetch */
for (uint32_t i = 0; i < num_keys; i++) {
    hash_vals[i] = rte_hash_hash(h, keys[i]);
    prim_idx[i] = hash_vals[i] & h->bucket_bitmask;
    rte_prefetch0(&h->buckets[prim_idx[i]]);
}

/* 2단계: primary 버킷 signature 비교 + secondary prefetch */
for (uint32_t i = 0; i < num_keys; i++) {
    struct rte_hash_bucket *bkt = &h->buckets[prim_idx[i]];
    int found = search_bucket(bkt, short_sigs[i]);
    if (!found) {
        sec_idx[i] = rte_hash_secondary_hash(hash_vals[i])
                   & h->bucket_bitmask;
        rte_prefetch0(&h->buckets[sec_idx[i]]);
    }
}

/* 3단계: secondary 비교 + 키 전체 비교 */
for (uint32_t i = 0; i < num_keys; i++) {
    if (positions[i] < 0) {
        /* primary 미스 → secondary에서 재시도 */
        search_bucket(&h->buckets[sec_idx[i]], ...);
    }
    /* signature 일치 엔트리가 있으면 키 전체 비교 */
    rte_prefetch0(get_key_slot(h, key_idx));
}

/* 4단계: 키 데이터 비교 (이미 prefetch 완료) */
for (uint32_t i = 0; i < num_keys; i++) {
    if (rte_hash_cmp_eq(keys[i], key_slot, h) == 0)
        positions[i] = key_idx;
}
rte_hash 크기 결정: entries는 예상 최대 엔트리 수의 1.5~2배로 설정합니다. 점유율(load factor)이 75%를 넘으면 Cuckoo eviction 빈도가 급증하여 삽입 성능이 저하됩니다. 65536 엔트리에 최대 40000개 플로우를 저장하면 점유율 ~61%로 안정적입니다.

RCU QSBR — 안전한 메모리 회수

QSBR(Quiescent State Based Reclamation)은 DPDK의 RCU(Read-Copy-Update) 구현입니다. lock-free 해시 테이블에서 삭제된 엔트리의 메모리를 안전하게 회수하는 데 사용됩니다. 삭제 시점에 다른 코어가 해당 메모리를 읽고 있을 수 있으므로, 모든 읽기 코어가 quiescent state를 보고한 뒤에 메모리를 해제합니다.

시간 → Writer del_key(K) defer_free(K) Reader 0 lookup(K) — 아직 읽는 중 quiescent Reader 1 quiescent Grace Period (모든 reader가 quiescent 보고 대기) 실제 free(K) GP 완료 모든 reader가 quiescent state를 보고해야 GP가 완료되고 메모리가 안전하게 해제됩니다
/* QSBR + rte_hash 통합 사용 패턴 */
#include <rte_hash.h>
#include <rte_rcu_qsbr.h>

/* ━━━ 초기화 ━━━ */
size_t sz = rte_rcu_qsbr_get_memsize(num_threads);
struct rte_rcu_qsbr *qsv = rte_zmalloc("qsbr", sz,
                                        RTE_CACHE_LINE_SIZE);
rte_rcu_qsbr_init(qsv, num_threads);

/* rte_hash에 QSBR 연결 (삭제 시 자동 defer) */
struct rte_hash_rcu_config rcu_cfg = {
    .v = qsv,
    .mode = RTE_HASH_QSBR_MODE_DQ,  /* Deferred Queuing */
    .dq_size = 1024,
    .trigger_reclaim_limit = 0,
    .max_reclaim_size = 128,
};
rte_hash_rcu_qsbr_add(hash_table, &rcu_cfg);

/* ━━━ Reader 스레드 (데이터 경로) ━━━ */
rte_rcu_qsbr_thread_register(qsv, lcore_id);
rte_rcu_qsbr_thread_online(qsv, lcore_id);

while (!force_quit) {
    /* 패킷 수신 + 해시 조회 (lock-free) */
    uint16_t nb_rx = rte_eth_rx_burst(port, queue, pkts, 32);
    for (int i = 0; i < nb_rx; i++) {
        int32_t pos = rte_hash_lookup(hash_table, &key);
        if (pos >= 0)
            process_flow(flow_data[pos], pkts[i]);
    }

    /* 폴링 루프 끝에서 quiescent state 보고 */
    rte_rcu_qsbr_quiescent(qsv, lcore_id);
}

/* ━━━ Writer 스레드 (제어 경로) ━━━ */
/* 삭제: rte_hash가 내부적으로 QSBR defer를 사용 */
rte_hash_del_key(hash_table, &key);
/* → 즉시 키 슬롯 제거, 메모리는 grace period 후 자동 회수
 * rte_hash_rcu_qsbr_add()로 연결했으므로 별도 코드 불필요 */

코드 분석: QSBR(RCU) 메모리 회수

  • Quiescent State: reader가 rte_rcu_qsbr_quiescent()를 호출하면 "현재 공유 데이터에 대한 참조를 보유하고 있지 않습니다"는 신호입니다. DPDK 데이터 경로에서는 폴링 루프의 끝(모든 패킷 처리 완료 후)에 호출합니다.
  • Grace Period: writer가 키를 삭제한 시점부터, 삭제 시점에 활성 상태였던 모든 reader가 quiescent state를 보고할 때까지의 기간입니다. grace period가 완료되면 삭제된 엔트리의 메모리를 안전하게 해제할 수 있습니다.
  • RTE_HASH_QSBR_MODE_DQ: Deferred Queuing 모드입니다. 삭제된 키를 즉시 해제하지 않고 내부 큐에 쌓아두었다가, grace period가 완료되면 배치로 해제합니다. 이 모드에서는 rte_hash_del_key()가 자동으로 defer 처리를 수행하므로 사용자 코드가 단순해집니다.
  • rte_rcu_qsbr_thread_online/offline: reader가 활성 상태인지 비활성 상태인지를 표시합니다. offline인 스레드는 grace period 계산에서 제외됩니다. 스레드가 블로킹 작업(sleep, I/O)을 수행할 때는 offline으로 전환하여 grace period가 무한히 길어지는 것을 방지해야 합니다.
  • 왜 RCU가 필요한가: lock-free 해시 테이블에서 writer가 키를 삭제하더라도, 삭제 시점에 다른 코어의 reader가 해당 엔트리의 키 데이터를 읽고 있을 수 있습니다. RCU 없이 즉시 free하면 use-after-free가 발생합니다. RCU는 모든 reader가 기존 참조를 포기할 때까지 기다려 안전하게 메모리를 해제합니다.
  • 성능 영향: reader 경로의 rte_rcu_qsbr_quiescent()는 단순한 카운터 증가(~1ns)이므로 데이터 경로 성능에 거의 영향이 없습니다. 이것이 rwlock 대비 QSBR의 핵심 장점입니다. rwlock은 reader도 캐시라인 쓰기(lock 획득)가 필요하여 코어 수에 비례하여 성능이 저하됩니다.

Memzone — 이름 기반 메모리 영역 관리

rte_memzone은 hugepage 위에 이름으로 참조 가능한 연속 메모리 영역을 예약하는 저수준 API입니다. Mempool, Ring, 해시 테이블 등 DPDK의 모든 데이터 구조가 내부적으로 memzone을 통해 메모리를 할당합니다. 멀티프로세스 모드에서 primary와 secondary가 동일한 이름으로 memzone을 조회하여 메모리를 공유합니다.

Hugepage 메모리 (예: 1GB × 4 = 4GB) "PKT_POOL" mempool (150MB) "FLOW_TBL" rte_hash (8MB) "RX_RING" ring (32KB) "PORT0_RXQ0" descriptor ring (64KB) (미사용) "STATS_MEM" 사용자 정의 (4KB) 각 memzone = 이름 + 시작 주소(VA/IOVA) + 크기 + NUMA 노드 + 정렬 멀티프로세스: primary가 예약, secondary가 이름으로 조회(rte_memzone_lookup) memzone 메타데이터 테이블 (rte_config → mem_config) name[32] | addr_64 | iova | len | hugepage_sz | socket_id | flags 최대 RTE_MAX_MEMZONE(2560)개 — 초과 시 -ENOSPC

rte_memzone_reserve() 구현 분석

/* lib/eal/common/eal_common_memzone.c — memzone 예약 (간략화) */
const struct rte_memzone *
rte_memzone_reserve_aligned(const char *name,
    size_t len, int socket_id,
    unsigned int flags, unsigned int align)
{
    struct rte_mem_config *mcfg = rte_eal_get_configuration()->mem_config;
    struct rte_memzone *mz;
    void *addr;

    /* ① 이름 중복 검사 */
    rte_rwlock_write_lock(&mcfg->mlock);
    if (memzone_lookup_thread_unsafe(name) != NULL) {
        rte_rwlock_write_unlock(&mcfg->mlock);
        return NULL;  /* 이미 존재 */
    }

    /* ② 빈 memzone 슬롯 할당 */
    mz = get_free_memzone_slot(mcfg);
    if (mz == NULL) {
        rte_rwlock_write_unlock(&mcfg->mlock);
        return NULL;  /* RTE_MAX_MEMZONE 초과 */
    }

    /* ③ malloc_heap에서 hugepage 메모리 할당 */
    addr = malloc_heap_alloc(name, len, socket_id, flags, align);
    if (addr == NULL) {
        rte_rwlock_write_unlock(&mcfg->mlock);
        return NULL;  /* 메모리 부족 */
    }

    /* ④ memzone 메타데이터 기록 */
    strlcpy(mz->name, name, sizeof(mz->name));
    mz->addr = addr;
    mz->iova = rte_malloc_virt2iova(addr);
    mz->len = len;
    mz->hugepage_sz = mcfg->memseg[0].hugepage_sz;
    mz->socket_id = socket_id;
    mz->flags = flags;

    rte_rwlock_write_unlock(&mcfg->mlock);
    return mz;
}

/* memzone 조회 (멀티프로세스에서 secondary가 사용) */
const struct rte_memzone *
rte_memzone_lookup(const char *name)
{
    struct rte_mem_config *mcfg = rte_eal_get_configuration()->mem_config;
    const struct rte_memzone *mz;

    rte_rwlock_read_lock(&mcfg->mlock);
    mz = memzone_lookup_thread_unsafe(name);
    rte_rwlock_read_unlock(&mcfg->mlock);
    return mz;
}

코드 분석: memzone 예약과 조회

  • rte_memzone_reserve(): hugepage 메모리를 이름 기반으로 예약합니다. 내부적으로 malloc_heap_alloc()을 호출하여 EAL이 mmap한 hugepage 영역에서 요청 크기만큼 할당합니다. 할당된 메모리의 가상 주소(VA)와 IOVA가 모두 memzone 메타데이터에 기록됩니다.
  • 이름 기반 식별: memzone 이름은 최대 32자이며, 전역 메타데이터 테이블에서 유일해야 합니다. rte_mempool_create("PKT_POOL", ...)를 호출하면 내부적으로 rte_memzone_reserve("RG_PKT_POOL", ...)로 메모리를 할당합니다.
  • 멀티프로세스 공유: primary 프로세스가 rte_memzone_reserve()로 메모리를 할당하면, secondary 프로세스는 rte_memzone_lookup()으로 동일한 이름의 memzone을 찾아 같은 hugepage 메모리에 접근합니다. 두 프로세스가 같은 hugepage 파일을 mmap하므로 물리적으로 동일한 메모리입니다.
  • RTE_MAX_MEMZONE: 최대 memzone 수는 기본 2560개입니다. 포트 수 × 큐 수 × (RX descriptor ring + TX descriptor ring) + mempool + ring + hash 등을 합산하면 대규모 환경에서 한계에 도달할 수 있습니다. 컴파일 시 RTE_MAX_MEMZONE을 늘려 조정합니다.
  • 정렬(align): rte_memzone_reserve_aligned()의 align 파라미터로 캐시라인(64B), 페이지(4KB), hugepage(2MB) 등 정렬을 지정할 수 있습니다. NIC의 descriptor ring은 보통 128B 또는 4KB 정렬이 필요합니다.
  • IOVA 연속성: RTE_MEMZONE_IOVA_CONTIG 플래그를 지정하면 IOVA 주소가 연속인 메모리만 할당합니다. NIC DMA가 scatter-gather를 지원하지 않는 경우 필요합니다. 1GB hugepage 내부는 항상 IOVA 연속이므로 이 플래그의 성공률이 높습니다.
/* memzone 실전 사용 예시: 공유 통계 메모리 */

/* Primary 프로세스: memzone 예약 */
const struct rte_memzone *mz = rte_memzone_reserve_aligned(
    "APP_STATS",
    sizeof(struct app_stats) * MAX_PORTS,
    rte_socket_id(),
    RTE_MEMZONE_SIZE_HINT_ONLY,  /* 크기 부족 시 다른 NUMA도 허용 */
    RTE_CACHE_LINE_SIZE);        /* 64B 정렬 */
struct app_stats *stats = mz->addr;
memset(stats, 0, mz->len);

/* Secondary 프로세스: 동일 이름으로 조회 */
const struct rte_memzone *mz = rte_memzone_lookup("APP_STATS");
struct app_stats *stats = mz->addr;
/* stats는 primary와 동일한 물리 메모리를 가리킴 */

/* 모든 memzone 목록 출력 (디버깅) */
rte_memzone_dump(stdout);
memzone vs rte_malloc: rte_malloc()은 이름 없는 동적 할당이고, rte_memzone_reserve()는 이름 기반 고정 할당입니다. memzone은 멀티프로세스 공유가 가능하고 IOVA 연속성을 보장할 수 있지만, 개수 제한(RTE_MAX_MEMZONE)이 있고 해제 후 재사용이 제한적입니다. 일반적으로 mempool/ring/hash 같은 장기 데이터 구조는 memzone, 임시 버퍼는 rte_malloc을 사용합니다.

DPDK 커널 인터페이스: VFIO vs UIO

DPDK가 유저 공간에서 NIC를 제어하려면 커널의 도움이 필요합니다. PCIe BAR 메모리를 유저 공간에 매핑하고, DMA를 위한 물리/IO 주소 변환(Address Translation)을 제공하는 두 가지 메커니즘:

항목VFIO (vfio-pci)UIO (igb_uio / uio_pci_generic)
IOMMU필수 (DMA 격리)불필요 (DMA 격리 없음)
보안안전 (IOMMU가 DMA 범위 제한)위험 (디바이스가 모든 물리 메모리에 접근 가능)
인터럽트MSI/MSI-X eventfd (전체 지원)제한적 (INTx만 또는 커널 패치(Patch) 필요)
비특권 실행가능 (/dev/vfio 권한 설정)root 필수
IOMMU 그룹그룹 내 모든 디바이스를 vfio에 바인딩해야 함개별 디바이스만 바인딩
VM 패스스루KVM/QEMU와 통합 지원미지원
커널 모듈vfio-pci (in-tree)igb_uio (DPDK 동봉), uio_pci_generic (in-tree)
권장 여부권장 (기본)레거시 (IOMMU 없는 환경에서만)
# ━━━ VFIO 기반 DPDK 디바이스 바인딩 ━━━

# 1. IOMMU 활성화 확인
dmesg | grep -i iommu
# "DMAR: IOMMU enabled" 또는 "AMD-Vi: IOMMU performance counters supported"

# 2. vfio-pci 모듈 로드
modprobe vfio-pci

# 3. NIC 상태 확인
dpdk-devbind --status

# Network devices using kernel driver
# 0000:03:00.0 'Ethernet Controller X710' if=ens3f0 drv=i40e
# 0000:03:00.1 'Ethernet Controller X710' if=ens3f1 drv=i40e

# 4. 커널 드라이버에서 언바인딩 → VFIO에 바인딩
ip link set ens3f0 down
dpdk-devbind --bind=vfio-pci 0000:03:00.0

# 5. 바인딩 확인
dpdk-devbind --status
# Network devices using DPDK-compatible driver
# 0000:03:00.0 'Ethernet Controller X710' drv=vfio-pci

# ━━━ Hugepage 설정 ━━━

# 2MB hugepage 1024개 = 2GB
echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages

# NUMA 노드별 설정 (듀얼 소켓)
echo 512 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 512 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages

# 1GB hugepage (부팅 커맨드라인에서만 예약 가능)
# GRUB: default_hugepagesz=1G hugepagesz=1G hugepages=4

# hugetlbfs 마운트
mount -t hugetlbfs nodev /dev/hugepages

# ━━━ UIO 기반 (레거시, IOMMU 없는 환경) ━━━
modprobe uio_pci_generic
dpdk-devbind --bind=uio_pci_generic 0000:03:00.0
# ⚠ DMA 격리 없음 — 프로덕션에서 비권장
/* 커널 VFIO-PCI 드라이버가 DPDK에 제공하는 인터페이스 */

/* 1. Container FD — IOMMU 도메인 (DMA 매핑 관리) */
container_fd = open("/dev/vfio/vfio", O_RDWR);
ioctl(container_fd, VFIO_SET_IOMMU, VFIO_TYPE1v2_IOMMU);

/* 2. Group FD — IOMMU 그룹 */
group_fd = open("/dev/vfio/15", O_RDWR);
ioctl(group_fd, VFIO_GROUP_SET_CONTAINER, &container_fd);

/* 3. Device FD — 특정 PCI 디바이스 */
device_fd = ioctl(group_fd, VFIO_GROUP_GET_DEVICE_FD, "0000:03:00.0");

/* 4. BAR 매핑 — NIC 레지스터에 직접 접근 */
struct vfio_region_info reg = { .argsz = sizeof(reg), .index = 0 };
ioctl(device_fd, VFIO_DEVICE_GET_REGION_INFO, &reg);
bar0 = mmap(NULL, reg.size, PROT_READ | PROT_WRITE, MAP_SHARED,
            device_fd, reg.offset);
/* 이제 bar0[offset]으로 NIC 레지스터 직접 read/write 가능 */

/* 5. DMA 매핑 — hugepage를 IOMMU에 등록 */
struct vfio_iommu_type1_dma_map dma = {
    .argsz = sizeof(dma),
    .flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE,
    .vaddr = (uint64_t)hugepage_vaddr,
    .iova  = (uint64_t)hugepage_vaddr,  /* VA=IOVA 모드 */
    .size  = hugepage_size,
};
ioctl(container_fd, VFIO_IOMMU_MAP_DMA, &dma);
/* NIC DMA가 이 hugepage 영역에 접근 가능 */

코드 분석: VFIO API 시퀀스

  • Container FD: /dev/vfio/vfio를 열면 VFIO 컨테이너를 생성합니다. 컨테이너는 IOMMU 도메인을 대표하며, 같은 컨테이너에 속한 디바이스들은 동일한 DMA 주소 공간을 공유합니다. VFIO_SET_IOMMU로 Type1 IOMMU 백엔드를 설정하면 Intel VT-d / AMD-Vi가 활성화됩니다.
  • Group FD: /dev/vfio/<group_id>는 IOMMU 그룹을 나타냅니다. 같은 PCIe bus 세그먼트에 있는 디바이스들이 하나의 그룹에 속하며, 그룹 내 모든 디바이스가 vfio-pci에 바인딩되어야 컨테이너에 추가할 수 있습니다. VFIO_GROUP_SET_CONTAINER로 그룹을 컨테이너에 연결합니다.
  • Device FD: VFIO_GROUP_GET_DEVICE_FD로 특정 PCI 디바이스의 파일 디스크립터를 획득합니다. 이 FD를 통해 BAR 영역 mmap, 인터럽트 설정, 디바이스 리셋 등 모든 디바이스 제어가 가능합니다. DPDK EAL은 이 FD를 PMD에 전달하여 NIC 하드웨어를 초기화합니다.
  • BAR 매핑 (4단계): VFIO_DEVICE_GET_REGION_INFO로 BAR 크기와 오프셋을 조회한 후, mmap()으로 유저 공간에 매핑합니다. 이후 PMD는 이 매핑된 주소에 직접 읽기/쓰기하여 NIC의 Descriptor Ring Head/Tail 레지스터, 통계 레지스터, 설정 레지스터에 접근합니다.
  • DMA 매핑 (5단계): VFIO_IOMMU_MAP_DMA는 hugepage의 가상 주소를 IOMMU에 등록하여 NIC DMA가 접근할 수 있게 합니다. VA 모드에서는 vaddr == iova로 설정하여 유저 공간 가상 주소를 그대로 DMA 주소로 사용합니다. IOMMU 하드웨어가 이 IOVA를 물리 주소로 변환합니다.

IOVA 모드: VA vs PA

현재 DPDK EAL은 Linux에서 버스(Bus)가 요구하는 모드, 물리 주소(Physical Address) 접근 가능 여부, IOMMU 그룹 존재 여부를 함께 보고 IOVA 모드를 고릅니다. 공식 Programmer's Guide는 대부분의 경우 RTE_IOVA_VA를 기본 선호 모드로 설명합니다. VFIO + IOMMU가 있으면 VA와 IOVA를 같게 둘 수 있어 초기화와 DMA 매핑이 단순해지기 때문입니다.

IOVA 모드 비교: VA 우선, PA는 예외적 RTE_IOVA_VA Hugepage VA IOVA = VA VFIO / IOMMU DMA 주소 변환과 격리 장점: rootless 친화, PA 접근 불필요, mempool 부팅 시간 유리 권장: 대부분의 최신 VFIO 환경 RTE_IOVA_PA Hugepage VA Physical Address /proc/self/pagemap CAP_SYS_ADMIN 필요 가능 장점: 레거시 드라이버/특정 PA 요구 경로와 호환 단점: 권한 요구 증가, 초기화 실패 가능성 높음
항목RTE_IOVA_VARTE_IOVA_PA
기본 선호도대부분의 Linux + VFIO 환경에서 권장버스/드라이버가 PA를 요구할 때만 사용
물리 주소 접근불필요필요할 수 있음
권한 요구낮음/proc/self/pagemap 접근 권한이 문제 되기 쉬움
mempool/메모리 부팅 시간유리IOVA 연속 메모리 탐색 비용이 커질 수 있음
대표 사용처VFIO, 멀티프로세스, 일반적인 최신 NIC레거시 UIO, 일부 특수 PMD/플랫폼
실무 포인트: 기본 모드를 믿되, 문제를 재현할 때는 --iova-mode=va 또는 --iova-mode=pa를 명시해 EAL 결정 로직을 고정하는 편이 좋습니다. 특히 cannot open /proc/self/pagemap 계열 오류는 "DPDK 전체 문제"가 아니라 PA 모드 전제와 권한 문제인 경우가 많습니다.

비특권 실행과 EAL 메모리 옵션

최신 Linux GSG는 비특권 DPDK 실행을 공식적으로 다룹니다. 핵심은 hugepage 예약은 root가 먼저 수행하고, 그 뒤에는 hugetlbfs 디렉터리와 /dev/vfio/* 권한을 사용자나 전용 그룹에 넘겨주는 방식입니다. 멀티프로세스가 필요 없다면 --in-memory를 사용해 hugetlbfs 파일 접근 자체를 생략할 수도 있습니다.

# 1. root가 hugepage 예약
sudo dpdk-hugepages.py -p 1G --setup 2G

# 2. 사용자 전용 hugepage 마운트 생성
export HUGEDIR=$HOME/huge-1G
mkdir -p "$HUGEDIR"
sudo dpdk-hugepages.py --mount --directory "$HUGEDIR" \
  --user "$(id -u)" --group "$(id -g)"

# 3. VFIO 디바이스를 일반 사용자에게 넘김
sudo dpdk-devbind --bind=vfio-pci --uid "$(id -u)" --gid "$(id -g)" \
  0000:03:00.0

# 4. 필요 리소스 한도 조정 (/etc/security/limits.d/dpdk.conf)
* soft memlock unlimited
* hard memlock unlimited
* soft nofile 1048576
* hard nofile 1048576

# 5. 비특권 testpmd 실행
dpdk-testpmd -l 2-5 -n 4 \
  --huge-dir "$HUGEDIR" \
  --iova-mode=va \
  -a 0000:03:00.0 \
  -- -i --rxq=2 --txq=2 --forward-mode=io
EAL 옵션의미주의점
--in-memoryhugetlbfs 파일 대신 익명 매핑 사용멀티프로세스에는 부적합
--single-file-segments페이지별 파일 대신 memseg list당 파일 수 축소vhost-user처럼 FD 수가 민감한 환경에 유리
--legacy-mem시작 시 모든 hugepage를 미리 잡아 연속 IOVA 확보메모리 사용량이 커지고 동적 해제가 어려움
--match-allocations할당과 해제를 더 엄격히 대응시킴--legacy-mem, --no-huge와 같이 쓰지 않음
--huge-unlinkhugetlbfs에 남는 backing file 오염을 줄임멀티프로세스를 사실상 포기하는 쪽에 가깝습니다
PA 모드의 추가 권한: 공식 문서는 물리 주소가 필요할 때 실행 파일에 cap_dac_read_search, cap_ipc_lock, cap_sys_admin가 필요할 수 있다고 설명합니다. 따라서 가능하면 vfio-pci + RTE_IOVA_VA 조합으로 문제를 단순화하는 것이 낫습니다.

bifurcated PMD와 Linux control plane

모든 DPDK 포트가 VFIO/UIO로 디바이스 전체를 탈취하는 것은 아닙니다. 최신 DPDK 문서는 일부 NIC가 bifurcated driver 모델을 제공한다고 설명합니다. 이 모델에서는 커널 드라이버가 netdev와 control plane을 유지하고, DPDK PMD는 데이터 경로를 분담합니다. 대표적인 예가 mlx5 계열입니다.

모델커널 netdev장점주의점
Full device ownership없음 또는 비활성가장 단순한 성능 모델, 디바이스를 애플리케이션이 독점커널 도구와 공존이 어렵고 VFIO/IOMMU 구성이 필수
Bifurcated PMD유지Linux control plane, representor, switchdev/TC와 조합 가능드라이버별 제약이 크고 디버깅이 복합적
운영 해석: "DPDK를 씁니다"가 항상 "NIC을 커널에서 떼어낸다"를 의미하지는 않습니다. SmartNIC/eSwitch/representor를 쓰는 환경에서는 커널 netdev + DPDK PMD + 하드웨어 스위치가 동시에 존재하는 모델을 먼저 떠올려야 합니다.

패킷 처리 파이프라인 모델

DPDK 애플리케이션은 두 가지 기본 패킷 처리 모델을 사용합니다:

Run-to-Completion 모델 — 코어당 독립 처리 파이프라인 RSS(Receive Side Scaling)로 NIC가 각 큐로 트래픽을 자동 분산 NIC (RSS 분산) Queue 0 Queue 1 Queue 2 Core 0 ① RX burst (Port0 Q0) ② Process pkt ③ TX burst (Port1 Q0) Core 1 ① RX burst (Port0 Q1) ② Process pkt ③ TX burst (Port1 Q1) Core 2 ① RX burst (Port1 Q0) ② Process pkt ③ TX burst (Port0 Q0) 코어 간 통신 없음 (Ring 불필요) — 각 코어가 완전 독립 처리 — 단순·확장성 높음 — L2/L3 포워딩에 최적
코어단계역할
Core 0RX burst수신 패킷을 가져와 다음 단계로 전달
Core 1DPI/파싱L3~L7 분류 및 메타데이터 생성
Core 2NAT/암호화정책 적용/변환 처리
Core 3TX burst처리 완료 패킷 송신

단계 간 전달은 rte_ring으로 수행합니다. 장점은 단계별 독립 확장, 단점은 Ring 통과 지연과 NUMA 크로싱 비용입니다.

/* Run-to-Completion 모델 구현 예시 (RSS 기반 L2 포워딩) */

/* 포트 설정: 코어 수만큼 RX/TX 큐 생성 */
struct rte_eth_conf port_conf = {
    .rxmode = {
        .mq_mode = RTE_ETH_MQ_RX_RSS,    /* RSS 활성화 */
    },
    .rx_adv_conf = {
        .rss_conf = {
            .rss_hf = RTE_ETH_RSS_IP | RTE_ETH_RSS_TCP | RTE_ETH_RSS_UDP,
        },
    },
};
rte_eth_dev_configure(port_id, nb_cores, nb_cores, &port_conf);

/* 각 코어에 RX/TX 큐 1개씩 할당 */
for (int q = 0; q < nb_cores; q++) {
    rte_eth_rx_queue_setup(port_id, q, 1024, socket_id, NULL, mbuf_pool);
    rte_eth_tx_queue_setup(port_id, q, 1024, socket_id, NULL);
}

/* 워커 함수: 각 코어가 독립적으로 실행 */
static int
worker_main(void *arg)
{
    unsigned core_id = rte_lcore_id();
    unsigned queue_id = core_to_queue[core_id];

    while (!force_quit) {
        struct rte_mbuf *pkts[32];
        uint16_t nb_rx = rte_eth_rx_burst(port_id, queue_id, pkts, 32);

        for (uint16_t i = 0; i < nb_rx; i++) {
            struct rte_ether_hdr *eth = rte_pktmbuf_mtod(pkts[i],
                                        struct rte_ether_hdr *);
            /* MAC 주소 스왑 (L2 포워딩) */
            struct rte_ether_addr tmp = eth->dst_addr;
            eth->dst_addr = eth->src_addr;
            eth->src_addr = tmp;
        }

        uint16_t nb_tx = rte_eth_tx_burst(port_id, queue_id, pkts, nb_rx);
        for (uint16_t i = nb_tx; i < nb_rx; i++)
            rte_pktmbuf_free(pkts[i]);
    }
    return 0;
}

/* 모든 워커 코어에서 실행 시작 */
rte_eal_mp_remote_launch(worker_main, NULL, CALL_MAIN);

코드 분석: Run-to-Completion 파이프라인 구현

  • RTE_ETH_MQ_RX_RSS: NIC 하드웨어의 RSS(Receive Side Scaling)를 활성화합니다. NIC는 패킷의 5-tuple(src/dst IP, src/dst port, protocol) 해시를 계산하여 여러 RX 큐에 분배합니다. rss_hf 필드로 해시에 포함할 헤더를 선택합니다.
  • rte_eth_dev_configure() 내부:
    • PMD의 dev_configure() 콜백을 호출하여 NIC 하드웨어에 큐 수, RSS 설정, 오프로드 플래그를 프로그래밍합니다.
    • RX/TX 큐 수가 NIC의 max_rx_queues/max_tx_queues를 초과하면 에러를 반환합니다.
    • 요청된 오프로드 플래그를 NIC의 rx_offload_capa/tx_offload_capa와 대조하여 지원 여부를 검증합니다.
  • rte_eth_rx_queue_setup() 내부:
    • Descriptor Ring 메모리를 hugepage에서 DMA 가능 영역으로 할당합니다 (rte_eth_dma_zone_reserve()).
    • sw_ring(mbuf 포인터 배열)을 할당하고, mempool에서 mbuf를 꺼내 descriptor에 채웁니다.
    • socket_id 파라미터로 NIC이 연결된 NUMA 노드에 메모리를 배치합니다.
  • rte_eal_mp_remote_launch(): 모든 lcore에서 worker_main()을 동시에 실행합니다. CALL_MAIN 플래그는 main lcore(EAL 초기화를 수행한 코어)도 워커에 포함시킵니다. 각 코어의 pthread는 이미 EAL 초기화 시 생성되어 대기 중이므로, 이 함수는 단순히 함수 포인터를 설정하고 배리어를 해제합니다.
  • core_to_queue 매핑: 각 코어가 전용 RX/TX 큐를 사용하므로 코어 간 경합이 없습니다. RSS가 5-tuple 해시로 패킷을 분배하므로 동일 플로우의 패킷은 항상 같은 코어에서 처리되어 순서가 보장됩니다.

Pipeline 모델: 코어 간 Ring 전달 구현

/* Pipeline 모델 — 단계별 전용 코어 할당 */

/* 코어 간 전달용 Ring 생성 */
struct rte_ring *rx_to_worker = rte_ring_create(
    "RX_WORKER", 1024,
    rte_socket_id(),
    RING_F_SP_ENQ | RING_F_SC_DEQ);  /* 1:1이므로 SP/SC */

struct rte_ring *worker_to_tx = rte_ring_create(
    "WORKER_TX", 1024,
    rte_socket_id(),
    RING_F_SP_ENQ | RING_F_SC_DEQ);

/* RX 코어: NIC → Ring */
static int rx_core(void *arg) {
    while (!force_quit) {
        struct rte_mbuf *pkts[32];
        uint16_t nb = rte_eth_rx_burst(port, 0, pkts, 32);
        if (nb)
            rte_ring_enqueue_burst(rx_to_worker,
                (void **)pkts, nb, NULL);
    }
    return 0;
}

/* Worker 코어: Ring → 처리 → Ring */
static int worker_core(void *arg) {
    while (!force_quit) {
        struct rte_mbuf *pkts[32];
        unsigned nb = rte_ring_dequeue_burst(rx_to_worker,
            (void **)pkts, 32, NULL);
        for (unsigned i = 0; i < nb; i++)
            process_packet(pkts[i]);  /* DPI, NAT, ACL 등 */
        if (nb)
            rte_ring_enqueue_burst(worker_to_tx,
                (void **)pkts, nb, NULL);
    }
    return 0;
}

/* TX 코어: Ring → NIC */
static int tx_core(void *arg) {
    while (!force_quit) {
        struct rte_mbuf *pkts[32];
        unsigned nb = rte_ring_dequeue_burst(worker_to_tx,
            (void **)pkts, 32, NULL);
        if (nb) {
            uint16_t sent = rte_eth_tx_burst(port, 0,
                pkts, nb);
            for (uint16_t i = sent; i < nb; i++)
                rte_pktmbuf_free(pkts[i]);
        }
    }
    return 0;
}

코드 분석: Pipeline 모델 구현

  • SP/SC Ring: RX→Worker, Worker→TX가 각각 1:1이므로 RING_F_SP_ENQ | RING_F_SC_DEQ로 CAS를 생략합니다. SP/SC는 MP/MC 대비 2~3배 빠릅니다.
  • rte_ring_enqueue_burst(): Ring이 가득 차면 삽입할 수 없는 패킷은 버립니다. 반환값이 enqueue된 수이므로, 미삽입 패킷의 mbuf를 free하지 않으면 mempool 누수가 발생합니다. 위 예시에서 RX 코어는 이를 생략했는데, 실무에서는 반드시 처리해야 합니다.
  • NUMA 고려: Ring을 NIC과 같은 NUMA 노드에 배치해야 합니다. 크로스-NUMA Ring 접근은 40~100ns 추가 지연이 발생합니다. Worker 코어도 동일 NUMA 노드에 할당하는 것이 이상적이지만, 코어가 부족하면 Ring만이라도 NIC 노드에 두는 것이 효과적입니다.
  • Pipeline vs RTC 선택: 처리 단계별 CPU 비용이 크게 다를 때(예: RX 10%, DPI 70%, TX 20%) Pipeline이 유리합니다. Worker 코어를 여러 개 두고 RX→Worker Ring을 MP/SC로 설정하면 부하를 분산할 수 있습니다. 처리가 균일하면 RTC가 단순하고 Ring 통과 지연이 없어 더 좋습니다.

Graph 라이브러리 — 노드 기반 패킷 실행기

rte_graph는 DPDK의 패킷 처리 로직을 "노드 그래프"로 표현하는 런타임입니다. 예전의 수동 while (rx_burst → process → tx_burst) 루프를 대체한다기보다, 그 루프를 재사용 가능한 노드 단위로 정규화해 주는 계층이라고 보는 편이 정확합니다. 최신 Programmer's Guide는 Graph가 run-to-completion(RTC)dispatch 두 실행 모델을 제공하고, 통계/멀티프로세스/그래프 export까지 지원한다고 설명합니다.

Graph 라이브러리: RTC와 Dispatch 실행 모델 RTC graph ethdev_rx parser/classify tx 한 lcore가 그래프를 끝까지 순회 기존 RTC 루프를 노드화해 유지보수성 확보 장점: 가장 단순, 예측 가능한 캐시 동작 권장: 고정 기능 L2/L3 포워더, NFV dataplane Dispatch graph Source node dispatcher worker A worker B Source node와 worker node를 분리 작업 종류별 분산이 쉬움 권장: 기능이 많은 서비스 체인, 노드 재사용이 많은 앱 주의: RTC보다 구조가 복잡하고 디버깅 지점이 늘어남 공식 Graph 문서는 x86/ARM64 서버에서 burst 256, ARM64 임베디드에서 64/128이 실용적인 출발점이라고 설명합니다.
/* Graph 생성 흐름의 핵심만 남긴 예시 */
struct rte_graph_param graph_conf = {
    .socket_id = rte_socket_id(),
    .nb_node_patterns = 1,
    .node_patterns = (const char *[]){ "*" },
};

rte_graph_t graph_id = rte_graph_create("graph0", &graph_conf);
struct rte_graph *graph = rte_graph_lookup("graph0");

/* 메인 루프는 rte_graph_walk() 한 줄로 단순화 */
while (!force_quit)
    rte_graph_walk(graph);
항목수동 RTC 루프GraphEventdev
구현 단위직접 작성한 while 루프노드와 에지이벤트 큐와 스케줄러
장점가장 단순, 오버헤드 최소구조화, 재사용, export/통계유연한 워커 스케줄링, 플로우 순서 제어
적합한 경우짧은 포워딩 경로서비스 체인, 공통 노드 재사용부하 편차가 큰 멀티스테이지 파이프라인
주의점코드가 커지면 유지보수 어려움노드 설계와 디버깅 규율 필요가장 복잡, 디바이스 지원 차이 큼

rte_graph_walk() 내부 구현 분석

/* lib/graph/rte_graph_worker.h — rte_graph_walk() 핵심 (간략화) */
static inline void
rte_graph_walk(struct rte_graph *graph)
{
    struct rte_node *node;
    rte_graph_off_t head;

    /* ① 보류 큐(pending list)에서 실행할 노드를 순회 */
    head = graph->head;
    while (head != RTE_GRAPH_OFF_INVALID) {
        node = RTE_PTR_ADD(graph, head);
        head = node->next;  /* 다음 보류 노드로 이동 */

        /* ② 노드의 입력 objs 배열에 패킷이 있으면 process() 호출 */
        if (node->idx > 0) {
            node->process(graph, node,
                          node->objs,   /* rte_mbuf** 배열 */
                          node->idx);   /* 패킷 수 */
            node->idx = 0;  /* 처리 완료, 카운터 리셋 */
        }
    }

    /* ③ 보류 큐 초기화 — 다음 walk()를 위해 비움 */
    graph->head = RTE_GRAPH_OFF_INVALID;
    graph->tail = RTE_GRAPH_OFF_INVALID;
}

/* 노드 process() 콜백 예시: ethdev_rx 소스 노드 */
static uint16_t
ethdev_rx_node_process(struct rte_graph *graph,
                        struct rte_node *node,
                        void **objs, uint16_t cnt)
{
    struct ethdev_rx_node_ctx *ctx = node->ctx;
    uint16_t next_index = 0;  /* 기본 다음 노드 */

    /* NIC에서 패킷 수신 */
    uint16_t nb_rx = rte_eth_rx_burst(
        ctx->port_id, ctx->queue_id,
        (struct rte_mbuf **)objs,
        RTE_GRAPH_BURST_SIZE);

    if (nb_rx == 0)
        return 0;

    /* 수신된 패킷을 다음 노드(예: ip4_lookup)로 전달 */
    rte_node_next_stream_move(graph, node,
                               next_index);
    return nb_rx;
}

/* 노드 process() 예시: ip4_lookup 처리 노드 */
static uint16_t
ip4_lookup_node_process(struct rte_graph *graph,
                         struct rte_node *node,
                         void **objs, uint16_t cnt)
{
    struct rte_mbuf *mbuf;
    uint16_t next;

    for (uint16_t i = 0; i < cnt; i++) {
        mbuf = (struct rte_mbuf *)objs[i];

        /* FIB 조회 → 다음 홉 결정 → 다음 노드 선택 */
        next = lpm_lookup(mbuf);

        /* 패킷을 해당 다음 노드의 스트림에 추가 */
        rte_node_enqueue_x1(graph, node, next, objs[i]);
    }
    return cnt;
}

코드 분석: rte_graph_walk() 내부

  • 보류 큐(Pending List): Graph는 "실행 대기 중인 노드"를 연결 리스트로 관리합니다. 소스 노드(ethdev_rx)가 패킷을 수신하면 자신과 다음 노드를 보류 큐에 추가합니다. rte_graph_walk()는 이 큐를 순회하며 각 노드의 process()를 호출합니다.
  • node->objs[]: 각 노드는 입력 패킷(rte_mbuf 포인터)을 저장하는 배열을 가집니다. 이전 노드가 rte_node_enqueue_x1()으로 패킷을 넣으면 node->idx가 증가하고, process()에서 이 배열을 받아 처리합니다.
  • rte_node_next_stream_move(): 현재 노드의 objs를 다음 노드의 objs로 "이동"합니다. 실제로는 포인터만 교환하므로 mbuf 복사가 없습니다. 소스 노드(ethdev_rx)처럼 모든 패킷이 같은 다음 노드로 가는 경우에 최적입니다.
  • rte_node_enqueue_x1(): 패킷별로 다른 다음 노드를 선택해야 할 때 사용합니다. ip4_lookup처럼 FIB 조회 결과에 따라 rewrite, drop, local 등 여러 다음 노드로 분기할 때 필요합니다.
  • 성능 핵심: Graph의 성능은 "같은 다음 노드로 가는 패킷을 얼마나 배치(batch)할 수 있는가"에 달렸습니다. 256개 패킷이 모두 같은 다음 노드로 가면 rte_node_next_stream_move() 한 번이지만, 패킷마다 다르면 256번의 rte_node_enqueue_x1()이 호출됩니다. 실무에서는 rte_node_enqueue_x4()로 4개씩 배치 삽입합니다.
실무 포인트: Graph는 Eventdev의 대체제가 아니라 코드 구조화 도구에 더 가깝습니다. "스케줄링을 NIC/하드웨어 이벤트 장치에 맡길 것인가"가 핵심이면 Eventdev, "run-to-completion을 유지하되 노드 재사용성을 높일 것인가"가 핵심이면 Graph가 더 맞습니다.

AF_XDP PMD — 커널 통합 고성능 경로

AF_XDP는 DPDK의 완전한 커널 바이패스와 커널 네트워크 스택 사이의 중간 지점입니다. 커널의 XDP 훅에서 패킷을 유저 공간으로 제로카피 전달하므로, VFIO 바인딩 없이 커널 드라이버를 유지하면서 고성능 패킷 처리가 가능합니다.

비교 항목DPDK (VFIO PMD)DPDK (AF_XDP PMD)커널 XDP
커널 바이패스완전 바이패스부분 바이패스 (XDP 훅까지 커널)커널 내 처리
NIC 드라이버DPDK PMD (유저 공간)커널 NIC 드라이버 유지커널 NIC 드라이버
커널 기능사용 불가 (tc, iptables, tcpdump...)부분 사용 가능 (같은 NIC의 다른 큐)모두 사용 가능
성능최고 (~14.88 Mpps/core @10G)높음 (~10 Mpps/core @10G)높음 (~24 Mpps @XDP_TX)
IOMMU필요 (VFIO)불필요불필요
특권CAP_SYS_RAWIOCAP_NET_RAW / CAP_BPFCAP_BPF
NIC 공유불가 (DPDK 전용)가능 (큐별 분리)가능
설정 복잡도VFIO 바인딩, hugepageXDP 프로그램 로드, UMEM 설정XDP 프로그램만
# ━━━ AF_XDP PMD로 DPDK 실행 ━━━

# NIC은 커널 드라이버에 그대로 유지 (VFIO 바인딩 불필요!)
ip link show ens3f0
# ens3f0: ... state UP ... driver: i40e

# AF_XDP PMD로 DPDK 앱 실행
dpdk-l2fwd -l 0-3 -n 4 \
  --vdev net_af_xdp0,iface=ens3f0,start_queue=0,queue_count=4 \
  -- -p 0x1

# 제로카피 모드 (NIC 드라이버가 지원하는 경우)
# i40e, ice, mlx5 등이 AF_XDP 제로카피 지원
dpdk-l2fwd -l 0-3 -n 4 \
  --vdev net_af_xdp0,iface=ens3f0,start_queue=0,queue_count=4 \
  -- -p 0x1
# 제로카피는 NIC 드라이버가 자동 감지하여 활성화
AF_XDP 제로카피 흐름 NIC RX Queue UMEM (shared) AF_XDP Socket DPDK App Fill/Completion/RX/TX Ring으로 버퍼 오프셋 전달, 데이터 복사는 생략
AF_XDP와 DPDK의 최신 경계: 커널 문서는 AF_XDP를 통해 제로카피 고속 패킷 전달을 제공하고, DPDK는 이를 PMD 백엔드로 활용할 수 있습니다. 즉 "커널 기능을 남길 것인가"와 "NIC 전체를 유저 공간이 독점할 것인가"를 분리해 설계할 수 있는 점이 2026년 시점의 중요한 변화입니다.

rte_flow, representor, embedded switch

현대 NIC와 DPU는 단순한 RX/TX 큐 이상의 기능을 제공합니다. PF/VF/SF 앞단의 embedded switch(eSwitch)를 하드웨어가 직접 처리하고, DPDK는 representor 포트와 rte_flow transfer 규칙으로 이 내부 스위치를 제어합니다. 이 개념을 이해해야 OVS hardware offload, SR-IOV, SmartNIC, switchdev 구성이 한 그림으로 연결됩니다.

representor 포트와 eSwitch transfer 규칙 PF / uplink port 외부 물리 네트워크 NIC 내장 eSwitch VF0/SF0 transfer rule VF1/SF1 DPDK 애플리케이션 representor 포트 rte_flow API 핵심: queue 단위 패킷 처리와 별개로, 하드웨어 스위치 도메인에는 representor와 transfer 규칙이 따로 존재합니다.
개념의미운영 포인트
PFPhysical Function. NIC의 기본 함수uplink 제어, VF/SF 관리, representor의 기준점
VFSR-IOV 가상 함수VM/컨테이너(Container)에 직결되기 쉽고 representor와 1:1 대응
SFSubFunction일부 최신 NIC/DPU가 제공하는 더 세밀한 함수 단위
RepresentoreSwitch 내부 포트를 호스트에서 표현한 가상 포트control plane, 통계, 미러링, 정책 투입 지점
transfer 규칙wire ↔ VF/SF ↔ representor 사이 하드웨어 스위치 도메인 규칙일반 ingress/egress 큐 규칙과 구분해서 생각해야 함
Bifurcated PMD커널 netdev와 DPDK가 기능을 분담대표적으로 mlx5 계열에서 중요
# representor 포트를 함께 열어 eSwitch를 제어하는 예시
dpdk-testpmd -l 2-7 -n 4 \
  -a 0000:03:00.0,representor=vf[0-3] \
  -- -i

# rte_flow 관점에서는 attr.transfer = 1 이면
# 큐가 아니라 eSwitch 도메인에 규칙을 넣겠다는 뜻입니다.
/* eSwitch transfer 규칙의 핵심만 남긴 예시 */
struct rte_flow_attr attr = {
    .ingress = 1,
    .transfer = 1,   /* 큐가 아닌 eSwitch 도메인 */
};

/* 패턴/액션은 NIC별 지원 범위가 다르므로
 * 반드시 rte_flow_validate()와 rte_eth_dev_info_get()로 먼저 확인 */
rte_flow_validate(port_id, &attr, pattern, actions, &error);
대표적 오해: representor는 "느린 소프트웨어 포트"가 아니라 하드웨어 내부 포트의 제어 창입니다. 패킷이 항상 representor를 거쳐 복사된다고 생각하면 설계가 어긋납니다. 실제 데이터는 eSwitch에서 직접 전환되고, representor는 정책·가시성·예외 처리의 접점이 됩니다.

rte_flow 패턴·액션 카탈로그

rte_flow는 NIC 하드웨어에 패킷 분류/액션 규칙을 설치하는 통합 API입니다. 패턴(pattern)으로 매칭 조건을 정의하고, 액션(action)으로 매칭된 패킷의 처리를 지정합니다. NIC별 지원 범위가 다르므로 반드시 rte_flow_validate()로 검증해야 합니다.

패턴 항목매칭 대상사용 예
RTE_FLOW_ITEM_TYPE_ETH이더넷 헤더 (src/dst MAC, EtherType)특정 MAC 주소 필터링
RTE_FLOW_ITEM_TYPE_VLANVLAN 태그 (VID, PCP, DEI)VLAN별 큐 분배
RTE_FLOW_ITEM_TYPE_IPV4IPv4 헤더 (src/dst IP, protocol, TTL)서브넷별 라우팅
RTE_FLOW_ITEM_TYPE_IPV6IPv6 헤더 (src/dst IP, next_hdr, flow_label)IPv6 플로우 레이블 분류
RTE_FLOW_ITEM_TYPE_TCPTCP 헤더 (src/dst port, flags)특정 포트 트래픽 분리
RTE_FLOW_ITEM_TYPE_UDPUDP 헤더 (src/dst port)DNS/VXLAN 트래픽 식별
RTE_FLOW_ITEM_TYPE_VXLANVXLAN 헤더 (VNI)오버레이(Overlay) 네트워크 분류
RTE_FLOW_ITEM_TYPE_GREGRE 헤더 (protocol, key)터널 트래픽 분류
RTE_FLOW_ITEM_TYPE_GENEVEGeneve 헤더 (VNI, options)OVN/Geneve 오버레이
RTE_FLOW_ITEM_TYPE_MARK이전 규칙이 설정한 마크다단계 필터링
RTE_FLOW_ITEM_TYPE_META메타데이터 (드라이버 정의)eSwitch/representor 간 상태 전달
RTE_FLOW_ITEM_TYPE_CONNTRACKCT 상태 (established, new, invalid)상태 기반 방화벽(Firewall)
RTE_FLOW_ITEM_TYPE_PORT_REPRESENTORrepresentor 포트eSwitch 규칙에서 소스/대상 지정
액션동작사용 예
RTE_FLOW_ACTION_TYPE_QUEUE특정 RX 큐로 전달플로우별 코어 고정
RTE_FLOW_ACTION_TYPE_RSSRSS로 큐 그룹에 분배서비스별 RSS 분리
RTE_FLOW_ACTION_TYPE_DROP패킷 드롭DDoS 필터링, ACL
RTE_FLOW_ACTION_TYPE_MARKmbuf에 마크 값 설정소프트웨어 후처리 힌트
RTE_FLOW_ACTION_TYPE_COUNT매칭 패킷/바이트 카운터통계, 과금
RTE_FLOW_ACTION_TYPE_SET_MAC_SRC/DSTMAC 주소 변경L2 NAT
RTE_FLOW_ACTION_TYPE_SET_IPV4_SRC/DSTIP 주소 변경NAT, 로드밸런싱
RTE_FLOW_ACTION_TYPE_SET_TP_SRC/DSTTCP/UDP 포트 변경NAPT
RTE_FLOW_ACTION_TYPE_VXLAN_ENCAP/DECAPVXLAN 캡슐화(Encapsulation)/해제오버레이 네트워크
RTE_FLOW_ACTION_TYPE_PORT_ID다른 포트(representor)로 전달eSwitch 내부 포워딩
RTE_FLOW_ACTION_TYPE_JUMP다른 flow 테이블로 이동다단계 파이프라인
RTE_FLOW_ACTION_TYPE_METER트래픽 미터링 (srTCM/trTCM)QoS, 과금
RTE_FLOW_ACTION_TYPE_AGE타임아웃 기반 규칙 만료동적 플로우 관리
RTE_FLOW_ACTION_TYPE_CONNTRACKCT 상태 추적/업데이트하드웨어 상태 방화벽
RTE_FLOW_ACTION_TYPE_SAMPLE패킷 샘플링 (미러링)모니터링, 디버깅
/* rte_flow 규칙 생성 — IPv4 TCP 포트 80을 Queue 2로 전달 */
struct rte_flow_attr attr = { .ingress = 1 };

/* 패턴: ETH / IPv4 / TCP dst_port=80 */
struct rte_flow_item_eth eth_spec = { 0 };
struct rte_flow_item_ipv4 ipv4_spec = { 0 };
struct rte_flow_item_tcp tcp_spec = {
    .hdr.dst_port = rte_cpu_to_be_16(80),
};
struct rte_flow_item_tcp tcp_mask = {
    .hdr.dst_port = 0xFFFF,
};

struct rte_flow_item pattern[] = {
    { .type = RTE_FLOW_ITEM_TYPE_ETH,  .spec = &eth_spec },
    { .type = RTE_FLOW_ITEM_TYPE_IPV4, .spec = &ipv4_spec },
    { .type = RTE_FLOW_ITEM_TYPE_TCP,  .spec = &tcp_spec, .mask = &tcp_mask },
    { .type = RTE_FLOW_ITEM_TYPE_END },
};

/* 액션: Queue 2 + 카운터 */
struct rte_flow_action_queue queue = { .index = 2 };
struct rte_flow_action_count count = { 0 };

struct rte_flow_action actions[] = {
    { .type = RTE_FLOW_ACTION_TYPE_COUNT, .conf = &count },
    { .type = RTE_FLOW_ACTION_TYPE_QUEUE, .conf = &queue },
    { .type = RTE_FLOW_ACTION_TYPE_END },
};

/* 검증 후 생성 */
struct rte_flow_error error;
if (rte_flow_validate(port_id, &attr, pattern, actions, &error) == 0) {
    struct rte_flow *flow = rte_flow_create(port_id, &attr,
                                              pattern, actions, &error);
} else {
    printf("Flow validate failed: %s\\n", error.message);
}

Connection Tracking (CT) 오프로드

DPDK 22.x 이후 rte_flow는 하드웨어 기반 Connection Tracking을 지원합니다. 커널의 conntrack처럼 TCP/UDP 연결 상태(NEW, ESTABLISHED, RELATED, INVALID)를 NIC eSwitch 하드웨어가 추적하고, rte_flow 규칙에서 상태를 기반으로 패킷을 분류/드롭합니다. OVS hardware offload의 stateful firewall 지원에 핵심입니다.

CT 상태의미전형적 액션
NEW새 연결의 첫 패킷 (SYN)CT 테이블에 등록, 정책 검사 후 허용/드롭
ESTABLISHED양방향 패킷이 관찰된 연결빠른 경로로 직접 전달 (하드웨어 오프로드)
RELATED기존 연결과 관련된 패킷 (FTP DATA 등)허용
INVALID추적할 수 없는 패킷드롭
/* CT 오프로드: ESTABLISHED 연결은 하드웨어에서 직접 전달 */
struct rte_flow_item_conntrack ct_item = {
    .flags = RTE_FLOW_CONNTRACK_FLAG_ESTABLISHED,
};

struct rte_flow_item ct_pattern[] = {
    { .type = RTE_FLOW_ITEM_TYPE_ETH },
    { .type = RTE_FLOW_ITEM_TYPE_IPV4 },
    { .type = RTE_FLOW_ITEM_TYPE_TCP },
    { .type = RTE_FLOW_ITEM_TYPE_CONNTRACK, .spec = &ct_item },
    { .type = RTE_FLOW_ITEM_TYPE_END },
};

/* ESTABLISHED → representor로 직접 전달 (CPU 바이패스) */
struct rte_flow_action ct_actions[] = {
    { .type = RTE_FLOW_ACTION_TYPE_PORT_ID, .conf = &repr_port },
    { .type = RTE_FLOW_ACTION_TYPE_END },
};
CT 오프로드와 OVS: OVS 2.17+ 는 tc-ct 오프로드를 통해 커널 conntrack 하드웨어 오프로드를 지원하고, DPDK 측에서는 rte_flow CT 액션이 같은 NIC eSwitch 하드웨어를 사용합니다. 두 경로 모두 SmartNIC(mlx5, bnxt 등)의 eSwitch CT 테이블을 활용하므로, NIC 펌웨어(Firmware)의 CT 테이블 크기가 실질적 한계입니다.

Flow isolated mode

isolated mode는 "명시적으로 설치한 rte_flow 규칙만 포트에 들어오게 하겠습니다"는 선언입니다. 이 모드를 켜면 PMD의 기본 RSS/기본 큐 처리 경로를 차단하고, 포트 전체를 flow rule 기반 제어면으로 바꾸게 됩니다. 하드웨어 오프로드 어플라이언스나 엄격한 서비스 체인에서 특히 유용합니다.

testpmd> flow isolate 0 true
testpmd> flow create 0 ingress pattern eth / ipv4 / tcp / end \
          actions queue index 0 / end
testpmd> flow list 0
장점주의점
기본 포트 동작을 제거해 "무엇이 패킷을 받는지"를 명시적으로 통제드라이버별로 되돌림 가능 여부와 지원 범위가 다릅니다.
오프로드 디버깅 시 소프트웨어 fallback을 숨겨 문제를 더 빨리 드러냄rule 누락 시 패킷이 조용히 사라질 수 있으므로 초기 bring-up 때는 신중해야 합니다.
보안 관점에서 default queue를 닫고 allowlist 규칙만 남기기 쉬움테스트 도구 없는 운영 전환은 위험합니다.
운영 포인트: isolated mode는 디버깅을 쉽게 만들기도 하지만, 반대로 기본 경로라는 안전망을 제거합니다. 따라서 처음에는 비격리 상태에서 rule을 검증하고, 이후 isolated mode로 전환하는 2단계 절차가 안전합니다.

Hairpin queue와 intra-device 포워딩

hairpin queue는 패킷을 호스트 메모리까지 올리지 않고 NIC 내부에서 다른 RX/TX 큐나 포트로 되돌리는 기능입니다. OVS hardware offload, representor 기반 서비스 체인, 고속 TAP 없는 VM 브리징, 장치 내부 loopback 테스트에서 중요합니다. 최신 testpmd 문서도 hairpin 관련 명령과 포트 프록시 조회 기능을 별도로 다룹니다.

Hairpin queue: 호스트 메모리를 거치지 않는 NIC 내부 경로 Uplink / PF ingress wire packet rte_flow match NIC datapath / eSwitch hairpin RX hairpin TX VF / representor / peer port 서비스 체인 다음 홉 호스트 메모리 미경유 핵심: mbuf를 올려서 CPU가 다시 밀어 넣는 대신, NIC 내부 큐 바인딩으로 우회합니다.
/* Hairpin capability 조회 후 큐 구성하는 패턴 */
struct rte_eth_hairpin_cap cap;
rte_eth_dev_hairpin_capability_get(port_id, &cap);

struct rte_eth_hairpin_conf hp = {
    .peer_count = 1,
    .manual_bind = 1,
    .tx_explicit = 1,
};

rte_eth_rx_hairpin_queue_setup(port_id, rxq_id, 1024, &hp);
rte_eth_tx_hairpin_queue_setup(port_id, txq_id, 1024, &hp);
적합한 경우이유
Representor 기반 서비스 체인VM/VF/SF 사이 트래픽을 NIC 내부에서 우회시켜 CPU 부하를 줄입니다.
Port-to-port 브리지(Bridge)동일 디바이스 안의 포트를 초저지연으로 연결할 수 있습니다.
하드웨어 오프로드 검증flow rule이 실제 eSwitch 경로를 타는지 빠르게 확인할 수 있습니다.

Virtio/Vhost-user 아키텍처

DPDK 환경에서 VM과의 패킷 교환은 vhost-user 프로토콜을 통해 이루어집니다. QEMU 게스트의 virtio-net 프론트엔드와 호스트의 DPDK vhost 백엔드가 공유 메모리(hugepage) 위의 virtqueue를 통해 제로카피로 패킷을 전달합니다. 이 구조를 이해해야 OVS-DPDK, Snabbswitch, VPP 등 가상 스위칭의 성능 특성을 설명할 수 있습니다.

vhost-user 공유 메모리 아키텍처 Guest VM (QEMU) Guest 애플리케이션 virtio-net 드라이버 virtqueue (vring) descriptor table avail ring (guest→host) used ring (host→guest) Host (DPDK vhost backend) OVS-DPDK / VPP rte_vhost 라이브러리 PMD thread busy-poll virtqueue rte_vhost_enqueue_burst rte_vhost_dequeue_burst UNIX Socket (제어 채널) 공유 Hugepage 메모리 (guest RAM = host hugepage mmap) virtqueue descriptor/ring 패킷 데이터 버퍼 mbuf 메타데이터 (호스트 측) Guest가 virtio 드라이버로 패킷을 avail ring에 게시 → Host PMD가 직접 hugepage에서 읽음 (복사 없음)
항목vhost-uservhost-net (커널)SR-IOV 패스스루
호스트 백엔드DPDK 유저 공간커널 vhost 모듈VF 직접 할당
스위칭OVS-DPDK / VPP커널 브릿지 / OVSNIC eSwitch
성능 (64B)~8 Mpps/core~2 Mpps/core~14 Mpps (wire)
유연성높음 (소프트웨어 스위칭)중간낮음 (고정 기능)
라이브 마이그레이션지원 (dirty page tracking)지원제한적
멀티큐MQ virtio 지원지원VF 큐 수에 의존
제어 채널UNIX 소켓ioctl없음 (직접 VF)
# ━━━ QEMU vhost-user 설정 ━━━

# 1. vhost-user 소켓 경로 (OVS-DPDK가 생성)
# /tmp/vhost-user0

# 2. QEMU 실행 (vhost-user 백엔드 연결)
qemu-system-x86_64 \
  -m 4G -smp 4 \
  -object memory-backend-file,id=mem,size=4G,mem-path=/dev/hugepages,share=on \
  -numa node,memdev=mem \
  -chardev socket,id=char0,path=/tmp/vhost-user0,server=off \
  -netdev vhost-user,id=net0,chardev=char0,vhostforce=on,queues=4 \
  -device virtio-net-pci,netdev=net0,mq=on,vectors=10 \
  ...

# 핵심 옵션 설명:
# share=on        — hugepage를 호스트와 게스트가 공유 (필수)
# vhostforce=on   — vhost-user 강제 사용
# queues=4        — 멀티큐 virtio (vCPU 수에 맞춤)
# vectors=10      — MSI-X 벡터 수 (2*queues + 2)

rte_vhost_enqueue/dequeue 구현 분석

/* lib/vhost/virtio_net.c — 호스트→게스트 패킷 전달 (간략화) */
uint16_t
rte_vhost_enqueue_burst(int vid, uint16_t queue_id,
    struct rte_mbuf **pkts, uint16_t count)
{
    struct virtio_net *dev = get_device(vid);
    struct vhost_virtqueue *vq = dev->virtqueue[queue_id];
    uint16_t avail_idx, free_entries;

    /* ① 게스트의 avail ring에서 빈 descriptor 확인 */
    avail_idx = __atomic_load_n(&vq->avail->idx,
                                 __ATOMIC_ACQUIRE);
    free_entries = avail_idx - vq->last_avail_idx;
    count = RTE_MIN(count, free_entries);

    for (uint16_t i = 0; i < count; i++) {
        /* ② avail ring에서 descriptor index 가져오기 */
        uint16_t desc_idx = vq->avail->ring[
            (vq->last_avail_idx + i) & (vq->size - 1)];

        struct vring_desc *desc = &vq->desc[desc_idx];

        /* ③ 게스트 메모리에 패킷 데이터 복사
         * (공유 hugepage이므로 호스트에서 직접 접근 가능) */
        void *dst = (void *)(uintptr_t)
            gpa_to_hva(dev, desc->addr);
        rte_memcpy(dst + virtio_hdr_size,
                    rte_pktmbuf_mtod(pkts[i], void *),
                    pkts[i]->pkt_len);

        /* ④ virtio 헤더 설정 (checksum 오프로드 힌트 등) */
        struct virtio_net_hdr *hdr = dst;
        hdr->num_buffers = 1;
        hdr->flags = 0;

        /* ⑤ used ring에 완료 보고 */
        vq->used->ring[used_idx & (vq->size - 1)] =
            (struct vring_used_elem){
                .id = desc_idx,
                .len = pkts[i]->pkt_len + virtio_hdr_size,
            };
    }

    /* ⑥ used ring index 갱신 → 게스트에게 새 패킷 도착 알림 */
    __atomic_store_n(&vq->used->idx,
        vq->last_used_idx + count, __ATOMIC_RELEASE);

    /* ⑦ 게스트에 인터럽트(eventfd kick) 전송 (필요 시) */
    if (!(vq->avail->flags & VRING_AVAIL_F_NO_INTERRUPT))
        eventfd_write(vq->callfd, (eventfd_t)1);

    return count;
}

코드 분석: vhost-user 패킷 전달

  • avail ring / used ring: virtio의 split virtqueue는 3개 영역으로 구성됩니다. descriptor table(버퍼 주소/길이), avail ring(게스트→호스트: "이 descriptor가 비어있음"), used ring(호스트→게스트: "이 descriptor에 데이터를 넣었음"). DPDK vhost는 호스트 측에서 이 링을 직접 조작합니다.
  • gpa_to_hva(): 게스트 물리 주소(GPA)를 호스트 가상 주소(HVA)로 변환합니다. QEMU가 memory-backend-file,share=on으로 게스트 RAM을 hugepage에 매핑했으므로, 호스트의 DPDK 프로세스도 같은 hugepage에 접근할 수 있습니다. 이 주소 변환 테이블은 vhost-user UNIX 소켓의 제어 메시지로 교환됩니다.
  • rte_memcpy(): 패킷 데이터를 게스트의 virtio 버퍼에 복사합니다. vhost-user는 "제로카피"라고 불리지만, 실제로는 DPDK mbuf → 게스트 virtio 버퍼 간 1회 복사가 발생합니다. 진정한 제로카피(복사 0회)는 packed virtqueue + 직접 mbuf 공유 모드에서만 가능하며, 게스트의 virtio 드라이버가 이를 지원해야 합니다.
  • eventfd kick: 게스트가 VRING_AVAIL_F_NO_INTERRUPT 플래그를 설정하지 않았다면, 호스트가 eventfd_write()로 게스트에 인터럽트를 전달합니다. 게스트의 virtio 드라이버가 이 인터럽트를 받아 used ring을 확인합니다. 고성능 환경에서는 게스트도 폴링 모드로 동작하여 인터럽트를 비활성화합니다.
  • dequeue(게스트→호스트): rte_vhost_dequeue_burst()는 반대 방향으로 동작합니다. 게스트가 avail ring에 전송할 패킷의 descriptor를 게시하면, 호스트 PMD가 게스트 메모리에서 데이터를 읽어 DPDK mbuf에 복사합니다.
vhost-user vs vhost-vDPA: vDPA(virtio Data Path Acceleration)는 vhost-user의 데이터 경로를 하드웨어(SmartNIC)로 오프로드합니다. 제어 경로는 여전히 vhost 프로토콜을 사용하지만, 패킷 전달은 NIC가 직접 수행하므로 SR-IOV 수준의 성능과 vhost-user 수준의 유연성을 동시에 달성합니다. DPDK 24.x부터 vDPA 드라이버가 강화되고 있습니다.

OVS-DPDK (Open vSwitch + DPDK)

OVS-DPDK는 Open vSwitch의 데이터플레인을 DPDK로 교체하여 가상 스위칭 성능을 극대화한 구성입니다. 클라우드/NFV 환경에서 VM/컨테이너 간 네트워크 트래픽을 유저 공간에서 처리하여, 기존 커널 OVS 대비 5~10배의 처리량을 달성합니다.

버전 호환성: 2026년 3월 7일 기준 Open vSwitch 공식 최신 DPDK 설치 문서는 DPDK 25.11.0 지원을 명시합니다. OVS-DPDK는 커널 모듈보다 OVS 사용자 공간(User Space) 바이너리와 DPDK ABI 조합에 더 민감하므로, 배포판 기본 패키지와 자체 빌드를 섞기 전에 지원 조합을 먼저 고정해야 합니다.
OVS-DPDK 아키텍처 VM1 Guest OS virtio-net VM2 Guest OS virtio-net VM3 Guest OS virtio-net 물리 NIC 25GbE (VFIO) DPDK PMD vhost-user vhost-user vhost-user OVS-DPDK (vswitchd) DPDK Datapath (PMD threads — busy-poll) EMC Exact Match Cache dpcls 튜플 기반 분류 upcall 미스 → ofproto • EMC: 최빈 플로우 해시 캐시 (O(1)) — 높은 hit rate가 핵심 • dpcls: 튜플-공간 탐색 (5-tuple 분류) — EMC 미스 시 • upcall: 완전 미스 시 ofproto의 OpenFlow 테이블 참조 OpenFlow Pipeline (ofproto) Table 0 Table 1 ··· Actions • output: 포트로 전송 • mod_vlan / drop • NORMAL: L2 학습 PMD threads: 전용 CPU 코어에서 busy-poll로 모든 포트(vhost-user + 물리 NIC) 폴링 vhost-user: UNIX 소켓으로 QEMU와 공유 메모리 설정 → 게스트 virtio 큐에 제로카피 직접 접근 EMC hit rate ↑ → 성능 극대화 (해시 O(1)) | 물리 NIC ↔ VM 간 패킷은 Hugepage 공유 메모리로 전달
# ━━━ OVS-DPDK 설정 예시 ━━━

# 1. OVS에 DPDK 초기화 파라미터 설정
ovs-vsctl --no-wait set Open_vSwitch . \
  other_config:dpdk-init=true \
  other_config:dpdk-socket-mem="1024,1024" \
  other_config:dpdk-lcore-mask=0x3 \
  other_config:dpdk-hugepage-dir="/dev/hugepages"

# 2. OVS 데몬 재시작
systemctl restart openvswitch-switch

# 3. DPDK 브릿지 생성
ovs-vsctl add-br br0 -- set bridge br0 datapath_type=netdev

# 4. DPDK 물리 포트 추가 (VFIO 바인딩된 NIC)
ovs-vsctl add-port br0 dpdk-p0 -- set Interface dpdk-p0 \
  type=dpdk options:dpdk-devargs=0000:03:00.0

# 5. vhost-user 포트 추가 (VM 연결용)
ovs-vsctl add-port br0 vhost-user0 -- set Interface vhost-user0 \
  type=dpdkvhostuserclient \
  options:vhost-server-path="/tmp/vhost-user0"

# 6. PMD 스레드 CPU 할당 (NUMA 인식)
ovs-vsctl set Open_vSwitch . other_config:pmd-cpu-mask=0x3C

# 7. 플로우 설정 (OpenFlow)
ovs-ofctl add-flow br0 "in_port=dpdk-p0,actions=output:vhost-user0"
ovs-ofctl add-flow br0 "in_port=vhost-user0,actions=output:dpdk-p0"

# ━━━ 성능 확인 ━━━
ovs-appctl dpif-netdev/pmd-stats-show   # PMD 스레드 통계
ovs-appctl dpif-netdev/pmd-rxq-show     # RX 큐 매핑
ovs-appctl dpctl/dump-flows             # 데이터패스 플로우
ovs-appctl coverage/show                # 내부 이벤트 카운터

OVS-DPDK 데이터패스 내부 구현 분석

OVS-DPDK의 패킷 처리 핵심은 PMD 스레드가 실행하는 dp_netdev_process_rxq_port() 함수입니다. 이 함수가 NIC/vhost-user 큐에서 패킷을 수신하고, 플로우 캐시를 조회하여 액션을 실행합니다.

/* lib/dpif-netdev.c — OVS-DPDK PMD 스레드 메인 루프 (간략화) */
static void
dp_netdev_process_rxq_port(struct dp_netdev_pmd_thread *pmd,
                            struct dp_netdev_rxq *rxq)
{
    struct dp_packet_batch batch;
    int error;

    /* ① NIC/vhost-user에서 패킷 배치 수신 */
    error = netdev_rxq_recv(rxq->rx, &batch, NULL);
    /* netdev_rxq_recv()는 내부적으로
     * rte_eth_rx_burst() 또는 rte_vhost_dequeue_burst()를 호출 */

    if (error || dp_packet_batch_is_empty(&batch))
        return;

    /* ② 패킷을 플로우 캐시에서 조회하여 분류 */
    dp_netdev_input(pmd, &batch, rxq->port->port_no);
}

/* lib/dpif-netdev.c — 플로우 조회 3단계 캐시 */
static void
dp_netdev_input(struct dp_netdev_pmd_thread *pmd,
                struct dp_packet_batch *packets,
                odp_port_t port_no)
{
    /* ① EMC (Exact Match Cache) — O(1) 해시 조회
     * 최빈 플로우를 해시 테이블에 캐시
     * 히트 시 즉시 액션 실행 → 최고 성능 경로 */
    emc_processing(pmd, packets, keys, &missed_batch);

    if (dp_packet_batch_is_empty(&missed_batch))
        return;  /* 모든 패킷 EMC 히트 → 완료 */

    /* ② dpcls (Datapath Classifier) — 튜플 기반 분류
     * EMC 미스 패킷을 5-tuple 마스크 기반으로 조회
     * EMC보다 느리지만 와일드카드 매칭 지원 */
    dpcls_lookup(pmd->cls, keys, &missed_batch,
                  &upcall_batch);

    if (!dp_packet_batch_is_empty(&upcall_batch)) {
        /* ③ upcall — ofproto로 미스 보고
         * dpcls에도 없는 새 플로우 → OpenFlow 테이블 조회
         * 매칭된 액션을 데이터패스에 캐시 설치 */
        dp_netdev_upcall(pmd, &upcall_batch);
    }

    /* 분류된 모든 패킷에 대해 액션 실행 */
    dp_netdev_execute_actions(pmd, packets, actions);
    /* 액션 예: output(포트 전송), mod_vlan, set_field,
     * ct(conntrack), drop, controller 등 */
}

코드 분석: OVS-DPDK 데이터패스

  • 3단계 플로우 캐시: OVS-DPDK의 성능 핵심은 EMC → dpcls → upcall 3단계 조회입니다. EMC는 최빈 플로우를 해시 O(1)로 매칭하여 ~14 Mpps/core를 달성합니다. EMC 히트율이 90% 이상이면 커널 OVS 대비 10배 이상 성능이 나옵니다.
  • EMC(Exact Match Cache): 8192개 엔트리의 해시 테이블로, 5-tuple(src/dst IP, src/dst port, protocol) 해시로 인덱싱합니다. 히트 시 캐시된 dp_netdev_flow 구조체의 액션을 즉시 실행합니다. 테이블 크기는 other_config:n-dpdk-rxqs로 조정할 수 있습니다.
  • dpcls(Datapath Classifier): 와일드카드 매칭이 가능한 튜플 기반 분류기입니다. EMC가 exact match만 지원하는 반면, dpcls는 서브넷 마스크 등 부분 매칭을 처리합니다. 내부적으로 튜플 공간(Tuple Space Search) 알고리즘을 사용합니다.
  • upcall: EMC와 dpcls 모두 미스인 새 플로우의 첫 패킷은 ofproto(OpenFlow 파이프라인)로 "올려보냅니다(upcall)". ofproto가 OpenFlow 테이블을 순회하여 매칭 액션을 결정하면, 이를 데이터패스에 캐시로 설치합니다. upcall은 느리므로(~1000 pps), 정상 운영에서는 대부분의 패킷이 EMC/dpcls에서 처리되어야 합니다.
  • dp_netdev_execute_actions(): 분류 결과에 따라 패킷에 액션을 실행합니다. output 액션은 해당 포트(vhost-user, DPDK physical)의 netdev_send()를 호출하여 rte_eth_tx_burst() 또는 rte_vhost_enqueue_burst()로 패킷을 전송합니다.
  • PMD 스레드: pmd-cpu-mask로 지정된 코어에서 busy-poll로 실행됩니다. 각 PMD 스레드는 할당된 RX 큐(물리 NIC + vhost-user 포트)를 라운드 로빈으로 폴링합니다. ovs-appctl dpif-netdev/pmd-rxq-show로 큐-스레드 매핑을 확인할 수 있습니다.

DPDK 성능 튜닝

DPDK 성능 튜닝은 시스템 레벨(커널 부트 파라미터, BIOS)에서 시작하여 DPDK 레벨(burst 크기, descriptor ring, mempool), 마지막으로 애플리케이션 레벨(파이프라인 구조, 캐시 정렬)까지 3단계로 진행합니다. 각 항목이 성능에 미치는 영향과 설정 근거를 상세히 설명합니다.

1단계: 시스템 레벨 isolcpus + nohz_full 1GB hugepage 예약 NUMA 정렬 (NIC↔코어↔메모리) C-state/P-state 비활성화 iommu=pt intel_iommu=on irqbalance 중지 효과: 기저 jitter 제거 2단계: DPDK 레벨 burst 크기 32~64 descriptor ring 2048~4096 mempool 캐시 256~512 벡터 PMD 경로 활성화 PCIe Write-Combining HW 오프로드 (RSS, checksum) 효과: 패킷당 CPU 사이클 최소화 3단계: 앱 레벨 캐시라인 정렬 데이터 구조 prefetch(다음 mbuf/테이블) 분기 예측 힌트 (likely/unlikely) 벡터화 SIMD 연산 파이프라인/코어 배치 최적화 rte_flow HW 오프로드 효과: 와이어레이트 달성
튜닝 항목설정효과
CPU 격리 isolcpus=4-15 nohz_full=4-15 rcu_nocbs=4-15 DPDK 코어에서 커널 스케줄러/RCU/타이머 인터럽트 제거. jitter 최소화
1GB Hugepage hugepagesz=1G hugepages=8 2MB 대비 TLB 미스 99% 감소. IOTLB 효율도 향상
NUMA 정렬 NIC과 같은 NUMA 노드의 코어/메모리 사용 크로스-NUMA 접근 시 40~100ns 추가 지연 방지
RX/TX burst 크기 32~64 (2의 거듭제곱) 벡터화 PMD 경로 활성화. prefetch 효율 극대화
Descriptor ring 크기 2048~4096 마이크로버스트 흡수. 너무 크면 캐시 효율 저하
Mempool 캐시 per-core 캐시 256~512 공용 Ring 접근 빈도 감소. 캐시 > burst_size × 1.5 권장
하이퍼스레딩 비활성화 또는 sibling 회피 HT sibling이 같은 L1/L2 캐시를 공유하여 캐시 오염
전력 관리 intel_pstate=disable processor.max_cstate=1 C-state 전환 지연 제거 (C6→C0 복귀에 ~100μs)
IRQ 밸런싱 irqbalance 중지, DPDK 코어에서 IRQ 배제 폴링 코어에 인터럽트 간섭 제거
PCIe MMIO 최적화 Write-Combining 활성화 (RTE_ETH_TX_OFFLOAD_WC) TX tail 레지스터 쓰기를 배치하여 PCIe 트랜잭션(Transaction) 감소
# ━━━ 시스템 레벨 DPDK 성능 튜닝 ━━━

# CPU 격리 (GRUB 커맨드라인)
# GRUB_CMDLINE_LINUX="isolcpus=4-15 nohz_full=4-15 rcu_nocbs=4-15 \
#   intel_pstate=disable processor.max_cstate=1 intel_idle.max_cstate=0 \
#   default_hugepagesz=1G hugepagesz=1G hugepages=8 \
#   iommu=pt intel_iommu=on"

# NUMA 토폴로지 확인 — NIC이 어느 NUMA 노드에 있는지
cat /sys/bus/pci/devices/0000:03:00.0/numa_node
# 0  → NUMA 노드 0의 코어와 메모리를 사용해야 함

# 해당 NUMA 노드의 코어 확인
lscpu | grep "NUMA node0"
# NUMA node0 CPU(s): 0-7,16-23

# irqbalance 비활성화 + DPDK 코어에서 IRQ 배제
systemctl stop irqbalance
# /proc/irq/default_smp_affinity에서 DPDK 코어 제외
echo 000F > /proc/irq/default_smp_affinity  # 코어 0-3만 IRQ 허용

# CPU 주파수 고정 (Turbo Boost는 유지, P-state 전환 최소화)
cpupower -c 4-15 frequency-set -g performance

# 코어별 C-state 확인
turbostat --quiet --show Core,CPU,Busy%,Bzy_MHz,IRQ,C1%,C6% sleep 1

burst 크기와 벡터 PMD의 관계

DPDK PMD는 burst 크기에 따라 서로 다른 코드 경로를 선택합니다. burst 크기가 충분히 크고 2의 거듭제곱이면 벡터화(Vectorized) PMD가 활성화되어 SIMD 명령어(SSE4/AVX2/AVX-512)로 여러 descriptor를 한 번에 처리합니다.

burst 크기PMD 경로성능 특성
1~4스칼라(Scalar) 경로descriptor 하나씩 처리. 기능은 모두 지원하지만 느림
8~16스칼라 + prefetchprefetch로 캐시 미스 감소, 중간 성능
32~64 (권장)벡터(Vector) 경로SIMD로 4~8개 descriptor 동시 처리. 최대 성능
128 이상벡터 경로 (과다)L1/L2 캐시 오염 위험. 성능 하락 가능
벡터 PMD 활성화 조건 (i40e/ice 예시): (1) burst 크기 ≥ 32, (2) scatter 비활성화, (3) LRO 비활성화, (4) VLAN strip 미사용. 이 조건 중 하나라도 충족하지 않으면 스칼라 경로로 폴백합니다. dpdk-testpmdshow port info all에서 "Rx offloads"를 확인하여 벡터 경로가 활성화되었는지 판단할 수 있습니다.

성능 측정과 병목 분석

# ━━━ testpmd로 기본 PPS 측정 ━━━
dpdk-testpmd -l 4-7 -n 4 -- -i --nb-cores=2 --rxq=2 --txq=2

# testpmd 내부 명령
testpmd> set fwd io             # 단순 포워딩 (최대 PPS 측정용)
testpmd> start
testpmd> show port stats all    # RX/TX PPS, 바이트, 에러 확인

# ━━━ perf로 코어별 병목 분석 ━━━
# LLC(Last Level Cache) 미스 확인 — 캐시 효율 측정
perf stat -e LLC-load-misses,LLC-store-misses \
         -e instructions,cycles \
         -C 4-7 sleep 5

# 함수별 CPU 사용 프로파일링
perf top -C 4 -g --no-children

# ━━━ dpdk-proc-info로 큐별 통계 ━━━
dpdk-proc-info -- --stats
dpdk-proc-info -- --xstats      # 확장 통계 (드롭, 에러 상세)

QoS/Traffic Management (rte_sched)

rte_sched는 DPDK의 계층적 QoS 스케줄러로, 통신사 NFV와 엔터프라이즈 네트워크에서 가입자/서비스별 대역폭(Bandwidth) 보장과 트래픽 셰이핑을 유저 공간에서 구현합니다. 커널의 tc/HTB/TBF에 대응하지만, DPDK 데이터 경로 내에서 패킷 단위로 동작하므로 수십 Mpps 수준의 처리량에서도 정밀한 QoS를 유지할 수 있습니다.

rte_sched 계층적 QoS 스케줄러 (5단계 트리) Port (출력 포트) Subport 0 (고객 A) Subport 1 (고객 B) Subport 2 (고객 C) Pipe 0 (가입자 1) Pipe 1 (가입자 2) Pipe N ... TC0 (BE) TC1 (AF) TC2 (EF) TC3~12 Q0 Q1 Q2 Q3 5단계 계층 (Port → Subport → Pipe → TC → Queue) Port: 출력 포트 전체 대역폭 제한 (Token Bucket) Subport: 고객/서비스 그룹별 대역폭 보장 Pipe: 개별 가입자/회선, Token Bucket으로 셰이핑 TC: Traffic Class, Strict Priority 또는 WFQ Queue: TC 내부 큐, WRR(Weighted Round Robin) 스케줄링 WRED (Weighted Random Early Detection) 드롭 정책 지원 최대: 1 port × 64 subport × 4K pipe × 13 TC × 4 queue = 1,327,104개 리프 큐 / 포트
계층스케줄링 알고리즘설정 예
PortToken Bucket (전체 출력 rate)10 Gbps line rate
SubportToken Bucket + TC간 WFQ고객 A: 2 Gbps 보장
PipeToken Bucket (가입자 셰이핑)가입자당 100 Mbps
TCStrict Priority (높은 TC 우선)TC0=BE, TC1=AF, TC2=EF(VoIP)
QueueWRR (가중치 라운드 로빈(Round Robin))Q0: weight=4, Q1: weight=2
/* rte_sched QoS 스케줄러 설정 (간략화) */
struct rte_sched_subport_params subport_params = {
    .tb_rate = 1250000000,       /* 10 Gbps in bytes/s */
    .tb_size = 1000000,           /* 토큰 버킷 크기 */
    .tc_rate = {
        [0] = 500000000,          /* TC0: 4 Gbps (Best Effort) */
        [1] = 375000000,          /* TC1: 3 Gbps (Assured Forwarding) */
        [2] = 250000000,          /* TC2: 2 Gbps (Expedited Forwarding) */
        [3] = 125000000,          /* TC3: 1 Gbps */
    },
    .tc_period = 10,              /* TC 갱신 주기 (ms) */
};

struct rte_sched_pipe_params pipe_profiles[] = {
    {   /* 프로필 0: 일반 가입자 100 Mbps */
        .tb_rate = 12500000,       /* 100 Mbps */
        .tb_size = 100000,
        .tc_rate = { 6250000, 3125000, 3125000, 0 },
        .tc_period = 40,
        .wrr_weights = { 1, 1, 1, 1 },
    },
};

/* 스케줄러 생성 및 패킷 큐잉/디큐잉 */
struct rte_sched_port *sched = rte_sched_port_config(&port_params);
rte_sched_subport_config(sched, 0, &subport_params, 0);
rte_sched_pipe_config(sched, 0, pipe_id, 0);

/* 데이터 경로에서: classify → enqueue → dequeue */
rte_sched_port_pkt_write(sched, pkts, nb_pkts, subport, pipe, tc, queue);
uint16_t nb_enq = rte_sched_port_enqueue(sched, pkts, nb_pkts);
uint16_t nb_deq = rte_sched_port_dequeue(sched, out_pkts, 32);
rte_eth_tx_burst(port_id, 0, out_pkts, nb_deq);

코드 분석: rte_sched 데이터 경로

  • rte_sched_port_pkt_write(): 패킷의 mbuf에 QoS 분류 정보(subport, pipe, TC, queue)를 기록합니다. 내부적으로 mbuf->hash.sched 필드에 4개 값을 packed 형태로 저장합니다. 이 함수를 호출하기 전에 애플리케이션이 패킷을 분류(5-tuple → 가입자 → pipe 매핑)해야 합니다.
  • rte_sched_port_enqueue() 내부:
    • 패킷의 sched 메타데이터를 읽어 해당 subport/pipe/TC/queue를 결정합니다.
    • WRED(Weighted Random Early Detection)로 드롭 여부를 판단합니다. 큐 점유율이 min_th 이하면 무조건 수용, max_th 이상이면 무조건 드롭, 사이에서는 확률적으로 드롭합니다.
    • 드롭되지 않은 패킷을 해당 큐(rte_ring)에 삽입합니다.
  • rte_sched_port_dequeue() 내부 — Token Bucket 알고리즘:
    • 현재 시간(rte_rdtsc())을 기준으로 마지막 갱신 이후 경과 시간에 비례하여 Port, Subport, Pipe의 토큰을 충전합니다.
    • 각 Pipe에 대해 TC를 Strict Priority 순서로 순회합니다. 높은 우선순위 TC(EF)가 토큰이 있으면 먼저 dequeue합니다.
    • 같은 TC 내의 여러 Queue는 WRR(Weighted Round Robin)로 순회합니다. weight가 높은 큐가 더 많은 패킷을 dequeue합니다.
    • dequeue된 패킷의 바이트 수만큼 Pipe/Subport/Port의 토큰을 소비합니다. 토큰이 부족하면 해당 계층의 dequeue가 중단됩니다.
  • Token Bucket 파라미터: tb_rate는 초당 충전되는 토큰 바이트 수(= 대역폭), tb_size는 버킷 최대 크기(= 버스트 허용량)입니다. 예: tb_rate=12500000(100Mbps), tb_size=100000(100KB 버스트 허용).
  • tc_period: TC 토큰 충전 주기입니다. 짧을수록 정밀하지만 CPU 오버헤드가 증가합니다. 10~40ms가 일반적입니다.

WRED 드롭 정책 구현

/* lib/sched/rte_sched.c — WRED 드롭 판단 (간략화) */
static inline int
rte_sched_port_enqueue_qwa(struct rte_sched_port *port,
    uint32_t qindex, struct rte_mbuf *pkt)
{
    struct rte_sched_queue *q = port->queue + qindex;
    uint32_t qlen = q->qw - q->qr;  /* 현재 큐 길이 */

    /* WRED: 색상별 드롭 확률 계산 */
    enum rte_color color = pkt->color;  /* GREEN/YELLOW/RED */
    struct rte_red *red = &q->red_params[color];

    /* EWMA(지수 가중 이동 평균)로 평균 큐 길이 갱신 */
    red->avg = ((1 - red->wq_log2) * red->avg)
             + (red->wq_log2 * qlen);

    if (red->avg < red->min_th) {
        /* 안전 영역: 무조건 수용 */
        return 0;  /* enqueue 허용 */
    } else if (red->avg >= red->max_th) {
        /* 위험 영역: 무조건 드롭 */
        rte_pktmbuf_free(pkt);
        return -1;
    } else {
        /* 중간 영역: 확률적 드롭
         * 드롭 확률 = (avg - min_th) / (max_th - min_th) × maxp */
        uint32_t drop_prob = rte_red_calc_drop_prob(red);
        if (rte_rand() % 100 < drop_prob) {
            rte_pktmbuf_free(pkt);
            return -1;
        }
        return 0;
    }
}

코드 분석: WRED 드롭 정책

  • 패킷 색상(Color): DSCP나 외부 미터(meter)가 패킷에 GREEN/YELLOW/RED 색상을 부여합니다. 색상별로 다른 min_th/max_th를 설정하여 RED 패킷을 먼저 드롭하고 GREEN 패킷을 보호합니다.
  • EWMA 평균: 순간 큐 길이 대신 지수 가중 이동 평균을 사용하여 마이크로버스트에 과민 반응하지 않습니다. wq_log2가 클수록 평균이 느리게 변합니다.
  • 확률적 드롭: 중간 영역에서 큐 길이에 비례하여 드롭 확률이 증가합니다. TCP의 혼잡 제어가 패킷 드롭을 감지하여 전송률을 줄이므로, 큐가 가득 차기 전에 미리 신호를 보내는 효과가 있습니다.
  • Tail Drop과의 차이: Tail Drop(큐 가득 차면 모든 신규 패킷 드롭)은 "전역 동기화(Global Synchronization)" 문제를 일으킵니다. 여러 TCP 플로우가 동시에 드롭되어 동시에 재전송하면 처리량이 급감합니다. WRED는 확률적으로 분산 드롭하여 이를 방지합니다.
커널 tc와의 차이: 커널 tc/HTB는 커널 공간에서 sk_buff를 다루고 Qdisc 프레임워크를 사용합니다. DPDK rte_sched는 유저 공간에서 rte_mbuf를 직접 큐잉하므로 컨텍스트 스위칭 없이 동작하지만, 커널의 nftables/conntrack 기반 QoS 정책과는 통합되지 않습니다.

Eventdev — 이벤트 기반 패킷 스케줄링

rte_eventdev는 DPDK의 이벤트 기반 프로그래밍 모델로, 하드웨어 이벤트 스케줄러(Intel DLB2 등)나 소프트웨어 구현을 통해 패킷을 워커 코어에 동적으로 분배합니다. Run-to-Completion과 Pipeline 모델의 장점을 결합합니다.

RX Adapter NIC RX - event Event Scheduler Atomic Queue Ordered Queue Parallel Queue TX Adapter event - NIC TX Worker 0 Worker 1 Worker 2

큐 타입별 시맨틱 상세

큐 타입플로우 순서동시성락 필요사용 사례
Atomic 동일 flow_id → 동일 워커 보장 서로 다른 플로우만 병렬 불필요 (플로우 단위 직렬화) conntrack, 세션 상태, NAT 테이블 갱신
Ordered 출력 시 원래 순서로 재정렬 모든 워커 병렬 처리 가능 상태 접근 시 필요 IPSec 암호화/복호화 (순서 유지 필수)
Parallel 보장 없음 최대 (제약 없음) 상태 접근 시 필요 무상태 패킷 검사, DPI, 로깅
Atomic 큐의 핵심: 동일한 flow_id(보통 5-tuple 해시)를 가진 이벤트는 이전 이벤트의 처리가 완료(rte_event_enqueue_burst() 또는 릴리스)될 때까지 다른 워커에 분배되지 않습니다. 이 보장 덕분에 플로우별 상태(세션 테이블, 카운터)에 락 없이 접근할 수 있습니다.

Eventdev 워커 루프 예시

/* eventdev 워커 코어 — 이벤트 수신 → 처리 → 전달 */
static int
event_worker_loop(void *arg)
{
    uint8_t dev_id = *(uint8_t *)arg;
    struct rte_event events[BURST_SIZE];

    while (!force_quit) {
        /* 이벤트 스케줄러에서 이벤트 수신 */
        uint16_t nb_rx = rte_event_dequeue_burst(
            dev_id, /* eventdev ID */
            port_id, /* 워커 포트 ID */
            events, BURST_SIZE,
            0       /* timeout: 0이면 non-blocking */
        );

        for (uint16_t i = 0; i < nb_rx; i++) {
            struct rte_mbuf *m = events[i].mbuf;

            /* 패킷 처리 로직 (L3 포워딩, DPI 등) */
            process_packet(m);

            /* 처리 결과를 TX Adapter 큐로 전달 */
            events[i].queue_id = TX_QUEUE_ID;
            events[i].op = RTE_EVENT_OP_FORWARD;
        }

        if (nb_rx > 0)
            rte_event_enqueue_burst(dev_id, port_id,
                                     events, nb_rx);
    }
    return 0;
}

코드 분석

  • 5행: rte_event 구조체는 mbuf 포인터, flow_id, queue_id, 스케줄링 타입, 우선순위를 포함합니다. ethdev의 rte_mbuf를 이벤트로 감싸서 스케줄러에 넣는 구조입니다.
  • 8행: rte_event_dequeue_burst()는 ethdev의 rte_eth_rx_burst()에 해당하는 이벤트 수신 함수입니다. 스케줄러가 큐 타입(Atomic/Ordered/Parallel) 정책에 따라 이벤트를 분배합니다.
  • 21행: RTE_EVENT_OP_FORWARD는 이벤트를 다른 큐로 전달(forward)하겠다는 의미입니다. RTE_EVENT_OP_RELEASE로 설정하면 이벤트를 소비(drop)합니다.
  • 24행: rte_event_enqueue_burst()로 처리된 이벤트를 TX Adapter 큐에 넣으면, TX Adapter가 자동으로 rte_eth_tx_burst()를 호출하여 패킷을 송신합니다.

Eventdev 초기화 코드

/* eventdev + RX/TX Adapter 초기화 골격 */
struct rte_event_dev_config dev_conf = {
    .nb_event_queues = 4,
    .nb_event_ports = 6,        /* 워커 포트 수 */
    .nb_events_limit = 4096,
    .nb_event_queue_flows = 1024,
    .nb_event_port_dequeue_depth = 128,
    .nb_event_port_enqueue_depth = 128,
};
rte_event_dev_configure(dev_id, &dev_conf);

/* Atomic 큐 설정 — 플로우별 직렬화 보장 */
struct rte_event_queue_conf q_conf = {
    .schedule_type = RTE_SCHED_TYPE_ATOMIC,
    .nb_atomic_flows = 1024,
    .nb_atomic_order_sequences = 1024,
};
rte_event_queue_setup(dev_id, queue_id, &q_conf);

/* RX Adapter: ethdev 포트 → eventdev 큐 연결 */
rte_event_eth_rx_adapter_create(adapter_id, dev_id, &port_conf);
rte_event_eth_rx_adapter_queue_add(adapter_id,
    eth_port_id, -1, /* -1 = 모든 RX 큐 */
    &queue_conf);
rte_event_eth_rx_adapter_start(adapter_id);

/* TX Adapter: eventdev → ethdev 포트 연결 */
rte_event_eth_tx_adapter_create(tx_adapter_id, dev_id, &port_conf);
rte_event_eth_tx_adapter_queue_add(tx_adapter_id,
    eth_port_id, -1);
rte_event_eth_tx_adapter_start(tx_adapter_id);

Eventdev vs 수동 Pipeline vs Run-to-Completion

항목Run-to-Completion수동 Pipeline (Ring)Eventdev
코어 할당RX+처리+TX 동일 코어단계별 전용 코어스케줄러가 동적 분배
부하 분산RSS 고정Ring 용량으로 간접 조절스케줄러가 자동 분배
플로우 순서RSS로 보장Ring FIFO로 보장Atomic/Ordered 큐로 보장
코어 추가/제거RSS 재설정 필요새 Ring + 코어 할당 필요포트 추가만으로 완료
HW 가속없음없음Intel DLB2, Marvell SSO 등
복잡도낮음중간높음 (초기 설정)
적합 워크로드균일 트래픽, 단순 처리단계별 처리 비용 차이 클 때가변 트래픽, 다단계 파이프라인
HW Eventdev 선택 기준: 소프트웨어 eventdev(sw)는 CPU 코어 1개를 스케줄러로 소비합니다. 하드웨어 eventdev(Intel DLB2 등)는 전용 ASIC이 스케줄링하므로 CPU 오버헤드가 0이고 지연이 ~1μs 수준입니다. 워커 코어가 8개 이상인 환경에서 가변 트래픽을 처리한다면 HW eventdev의 ROI가 명확합니다.

Cryptodev — 암호화 가속 프레임워크

rte_cryptodev는 DPDK의 암호화 추상화 계층으로, 하드웨어 가속기(Intel QAT, Marvell OCTEON, ARM CE)와 소프트웨어 구현(AESNI, OpenSSL, ARMv8 CE)을 동일한 API로 접근합니다. IPSec VPN, TLS 프록시, MACsec, 디스크 암호화 가속 등에서 DPDK 데이터 경로와 통합되어 패킷 단위 암호 연산을 인라인 또는 lookaside 방식으로 처리합니다.

Cryptodev 아키텍처: Lookaside vs Inline Lookaside 모델 RX burst enqueue_burst dequeue_burst Crypto PMD QAT / AESNI / OpenSSL / SW 패킷이 crypto 장치로 갔다가 돌아오는 비동기 모델 장점: 유연, 다양한 알고리즘, 독립 큐 단점: enqueue/dequeue 추가 지연 Inline 모델 RX burst 패킷 처리 TX burst NIC Inline Crypto NIC 하드웨어가 직접 암복호화 NIC가 RX/TX 경로에서 직접 암복호화 수행 장점: 최소 지연, CPU 부하 없음 단점: NIC별 알고리즘 제한, 설정 복잡
/* Cryptodev 세션 생성 및 암호화 요청 (간략화) */

/* 1. Crypto 디바이스 설정 */
struct rte_cryptodev_config conf = {
    .nb_queue_pairs = 1,
    .socket_id = rte_socket_id(),
};
rte_cryptodev_configure(cdev_id, &conf);

/* 2. 세션 풀 생성 */
struct rte_mempool *sess_pool = rte_cryptodev_sym_session_pool_create(
    "SESS_POOL", 256, 0, 0, 0, rte_socket_id());

/* 3. 대칭 암호화 세션: AES-128-CBC + HMAC-SHA256 */
struct rte_crypto_sym_xform cipher_xform = {
    .type = RTE_CRYPTO_SYM_XFORM_CIPHER,
    .cipher = {
        .op = RTE_CRYPTO_CIPHER_OP_ENCRYPT,
        .algo = RTE_CRYPTO_CIPHER_AES_CBC,
        .key = { .data = key, .length = 16 },
        .iv = { .offset = IV_OFFSET, .length = 16 },
    },
    .next = &auth_xform,  /* 인증 체이닝 */
};

void *session = rte_cryptodev_sym_session_create(cdev_id,
    &cipher_xform, sess_pool);

/* 4. 암호화 요청 (enqueue/dequeue) */
struct rte_crypto_op *ops[32];
rte_crypto_op_bulk_alloc(op_pool, RTE_CRYPTO_OP_TYPE_SYMMETRIC,
                        ops, 32);

for (int i = 0; i < 32; i++) {
    ops[i]->sym->m_src = pkts[i];  /* mbuf 연결 */
    rte_crypto_op_attach_sym_session(ops[i], session);
}

uint16_t enq = rte_cryptodev_enqueue_burst(cdev_id, 0, ops, 32);
/* ... 다른 작업 수행 ... */
uint16_t deq = rte_cryptodev_dequeue_burst(cdev_id, 0, ops, 32);
Crypto PMD백엔드특징
QATIntel QuickAssist하드웨어 가속, 대칭/비대칭/해시, 압축도 지원
AESNI-MBIntel AES-NI 명령어소프트웨어, x86 AES-NI/AVX2/AVX-512 활용
OpenSSLlibcrypto소프트웨어, 가장 넓은 알고리즘 지원
ARMv8 CEARM Crypto ExtensionARM 서버 네이티브 가속
Scheduler멀티 PMD 번들여러 crypto PMD를 묶어 부하 분산(Load Balancing)
Null무연산성능 베이스라인 측정용
모델작동 방식적합한 경우
Lookaside Protocol전체 프로토콜(ESP 등)을 crypto 장치가 처리IPSec 게이트웨이, 하드웨어가 프로토콜 인식
Lookaside None앱이 프로토콜 처리, crypto 장치는 알고리즘만커스텀 프로토콜, 세밀한 제어 필요 시
Inline CryptoNIC RX/TX 경로에서 하드웨어 암복호화와이어 스피드 IPSec, MACsec
Inline ProtocolNIC이 전체 IPSec/MACsec 프로토콜 처리최저 지연 IPSec 어플라이언스
IPSec + DPDK: examples/ipsec-secgw는 DPDK 기반 IPSec 게이트웨이 레퍼런스 구현으로, Lookaside/Inline 모델을 모두 지원합니다. VPP의 ipsec 노드와 함께 프로덕션 IPSec 구현의 두 가지 주요 선택지입니다.

DPDK 멀티프로세스 모드

DPDK는 primary-secondary 프로세스 모델을 지원합니다. Primary 프로세스가 EAL 초기화, 포트 설정, Mempool 생성을 수행하고, Secondary 프로세스가 공유 메모리(hugepage)를 통해 동일한 자원에 접근합니다.

DPDK 멀티프로세스 아키텍처 (Primary / Secondary) Primary Process --proc-type primary rte_eal_init() — Hugepage 매핑 생성 rte_mempool_create() — Mempool 생성 rte_eth_dev_configure() — 포트 설정/시작 rte_ring_create() — Ring 생성 패킷 포워딩 (RX → Process → TX) 모든 물리 NIC 소유 및 제어 Secondary Process(es) --proc-type secondary --file-prefix dpdk0 rte_eal_init() — 동일 Hugepage에 Attach rte_mempool_lookup() — Mempool 공유 참조 rte_eth_dev_get_port() — 포트 룩업(읽기 전용) rte_ring_lookup() — Ring 공유 참조 pdump / 통계 / 텔레메트리 수집 물리 NIC는 primary가 소유 — secondary는 공유 객체만 접근 Shared Hugepage Memory Mempool · Ring · mbuf (물리 주소 동일, VA도 동일 — mmap 동일 주소 보장) rte_config 공유 메모리 (EAL 메타데이터 — 모든 프로세스 공유) 포인터 기반 접근 가능 — VA 동일 보장으로 구조체 포인터 직접 사용 주요 사용 사례 • 패킷 캡처: primary가 포워딩, secondary(dpdk-pdump)가 미러 캡처 → pcap 저장 • 모니터링/텔레메트리: secondary가 통계 수집·분석 (primary 중단 없이) • 무중단 업그레이드: 신규 secondary 시작 후 기존 프로세스 교체 • 다중 앱 분리: primary(포워딩) + secondary(제어/관리) 역할 분리
# 멀티프로세스 실행 예시

# Primary 프로세스 시작
dpdk-l2fwd -l 0-3 -n 4 --proc-type primary -- -p 0x3

# Secondary 프로세스 (패킷 덤프)
dpdk-pdump -l 4 --proc-type secondary -- \
  --pdump 'port=0,queue=*,rx-dev=/tmp/rx.pcap'

# Secondary 프로세스 (통계 모니터링)
dpdk-proc-info --proc-type secondary -- --stats

컨테이너·Kubernetes 통합

DPDK를 컨테이너화하면 배포·확장·롤백(Rollback)이 용이하지만, hugepage 마운트(Mount), VFIO 디바이스 노출, CPU 격리, NUMA 인식 스케줄링 등 추가 설정이 필요합니다. Kubernetes 환경에서는 SR-IOV Device PluginMultus CNI가 DPDK 워크로드에 VF를 동적 할당하는 표준 패턴입니다.

Kubernetes DPDK 배포 아키텍처 Worker Node (baremetal / VM) Kubelet CPU Manager (static policy) Topology Manager (NUMA) hugepage 리소스 관리 SR-IOV Device Plugin VF 풀 관리 VFIO 바인딩 자동화 리소스 어드버타이징 Multus CNI 다중 네트워크 인터페이스 DPDK VF를 Pod에 연결 NetworkAttachmentDefinition NIC (PF) VF0, VF1, ... SR-IOV 활성화 vfio-pci 바인딩 DPDK Pod DPDK 애플리케이션 PMD (vfio-pci VF) /dev/vfio/N 마운트 /dev/hugepages 마운트 resources: hugepages-1Gi: 2Gi, intel.com/sriov_dpdk: 1 Guaranteed QoS + cpuManagerPolicy=static → 전용 CPU 코어 할당 (isolcpus 효과) Pod 스펙 핵심 요소 • securityContext.privileged: false (VFIO는 privileged 불필요) • volumes: hugepages (emptyDir medium: HugePages-1Gi) • resources.limits: hugepages-1Gi: 2Gi (Guaranteed QoS 필수) • resources.limits: intel.com/sriov_dpdk: 1 (VF 할당) • CPU limits = requests (static CPU manager 트리거) • topologyManager: single-numa-node (NUMA 정렬 강제) • annotation: k8s.v1.cni.cncf.io/networks (Multus) • DPDK_DEVARGS 환경변수로 PCI 주소 주입
# Kubernetes DPDK Pod 스펙 예시
apiVersion: v1
kind: Pod
metadata:
  name: dpdk-testpmd
  annotations:
    k8s.v1.cni.cncf.io/networks: sriov-dpdk-net
spec:
  containers:
  - name: testpmd
    image: dpdk-app:24.11
    command: ["dpdk-testpmd"]
    args:
      - "-l"
      - "2-5"
      - "-n"
      - "4"
      - "--iova-mode=va"
      - "--in-memory"
      - "--"
      - "-i"
      - "--rxq=2"
      - "--txq=2"
    resources:
      requests:
        cpu: "4"
        memory: "4Gi"
        hugepages-1Gi: "2Gi"
        intel.com/sriov_dpdk: "1"
      limits:
        cpu: "4"
        memory: "4Gi"
        hugepages-1Gi: "2Gi"
        intel.com/sriov_dpdk: "1"
    securityContext:
      capabilities:
        add: ["IPC_LOCK", "SYS_RAWIO"]
    volumeMounts:
    - name: hugepages
      mountPath: /dev/hugepages
  volumes:
  - name: hugepages
    emptyDir:
      medium: HugePages-1Gi
컨테이너 환경 항목설정이유
HugepageemptyDir.medium: HugePages-1GiDPDK는 hugetlbfs 필수, Pod 내 /dev/hugepages에 마운트
CPU ManagercpuManagerPolicy: staticGuaranteed QoS Pod에 전용 코어 할당 (busy-poll 필수)
Topology Managersingle-numa-nodeNIC VF, CPU, 메모리를 같은 NUMA 노드에 배치
SR-IOV Device PluginConfigMap으로 VF 풀 정의VF를 VFIO 바인딩하고 리소스로 어드버타이징
Multus CNINetworkAttachmentDefinition기본 CNI(Calico 등) + DPDK VF 다중 네트워크
securityContextIPC_LOCK, SYS_RAWIOhugepage mlock과 VFIO 접근에 필요 (privileged 불필요)
Docker 직접 실행--device /dev/vfio/N -v /dev/hugepagesK8s 없는 Docker 환경 최소 설정
# ━━━ Docker 직접 실행 예시 ━━━

# SR-IOV VF 생성
echo 4 > /sys/class/net/ens3f0/device/sriov_numvfs

# VF를 vfio-pci에 바인딩
dpdk-devbind --bind=vfio-pci 0000:03:02.0

# VFIO 그룹 번호 확인
ls -la /sys/bus/pci/devices/0000:03:02.0/iommu_group
# → /sys/kernel/iommu_groups/42

# Docker 실행 (privileged 없이!)
docker run --rm -it \
  --device /dev/vfio/42 \
  --device /dev/vfio/vfio \
  -v /dev/hugepages:/dev/hugepages \
  --ulimit memlock=-1:-1 \
  --cpuset-cpus=4-7 \
  dpdk-app:24.11 \
  dpdk-testpmd -l 4-7 -n 4 -a 0000:03:02.0 -- -i
컨테이너 DPDK 주의점:
  • --privileged 플래그는 가능한 사용하지 말고, 필요한 capability만 부여
  • 멀티프로세스 모드(--proc-type secondary)는 같은 hugepage 네임스페이스를 공유해야 하므로 컨테이너 간 사용이 복잡
  • --in-memory 옵션은 hugetlbfs 파일을 만들지 않아 단일 프로세스 컨테이너에 적합
  • NUMA 토폴로지(Topology)가 호스트와 일치하는지 반드시 확인 (VM 위 컨테이너는 가짜 NUMA일 수 있음)

DPDK 디버깅과 모니터링

도구용도사용법
dpdk-proc-info 포트 통계, 큐 통계, 메모리 사용량 dpdk-proc-info -- --stats --xstats
dpdk-pdump 패킷 캡처 (pcap 형식) dpdk-pdump -- --pdump 'port=0,...'
dpdk-dumpcap Wireshark 친화적 캡처 (pcapng) dpdk-dumpcap -i <iface> -w trace.pcapng
dpdk-telemetry.py JSON 기반 텔레메트리 API UNIX 소켓으로 런타임 통계 조회
dpdk-devbind PCI 디바이스 바인딩 관리 dpdk-devbind --status
dpdk-testpmd 포트 기능 테스트, 성능 벤치마크 dpdk-testpmd -- -i --forward-mode=io
rte_eth_stats_get() 프로그래밍 방식 통계 수집 RX/TX 패킷/바이트/에러 카운터
# ━━━ DPDK 디버깅/모니터링 명령 모음 ━━━

# 포트 통계 (기본 + 확장)
dpdk-proc-info -- --stats
dpdk-proc-info -- --xstats  # NIC별 상세 카운터 (rx_good_bytes, tx_errors, ...)

# 실시간 패킷 캡처 (secondary 프로세스)
dpdk-pdump -l 8 --proc-type secondary -- \
  --pdump 'port=0,queue=*,rx-dev=/tmp/capture.pcap'
# 캡처 파일을 tcpdump/Wireshark로 분석
tcpdump -r /tmp/capture.pcap

# dumpcap 스타일 캡처 (pcapng)
dpdk-dumpcap --list-interfaces
dpdk-dumpcap -i "0000:03:00.0" -c 1000 -w /tmp/trace.pcapng

# 텔레메트리 API (DPDK 20.05+)
# UNIX 소켓 경로: /var/run/dpdk/rte/dpdk_telemetry.vX
dpdk-telemetry.py
# 대화형 쿼리:
# --> /ethdev/stats,0
# --> /ethdev/xstats,0
# --> /eal/params
# --> /mempool/list

# testpmd로 성능 측정
dpdk-testpmd -l 0-3 -n 4 -a 0000:03:00.0 -- \
  -i \
  --forward-mode=io \
  --rxq=4 --txq=4 \
  --nb-cores=3 \
  --burst=32

# testpmd 내부 명령:
# testpmd> start
# testpmd> show port stats all
# testpmd> show fwd stats all
# testpmd> show port xstats 0
# testpmd> stop

# Mempool 상태 확인
dpdk-proc-info -- --mempool=MBUF_POOL

# EAL 로그 레벨 (런타임 조정)
# --log-level=lib.eal:8   (DEBUG)
# --log-level=pmd.net.mlx5:7  (INFO)
/* 프로그래밍 방식 통계 수집 */
struct rte_eth_stats stats;
rte_eth_stats_get(port_id, &stats);

printf("Port %u: RX %lu pkts (%lu bytes) TX %lu pkts (%lu bytes)\\n",
       port_id,
       stats.ipackets, stats.ibytes,
       stats.opackets, stats.obytes);
printf("  RX errors: %lu  TX errors: %lu  RX no-mbuf: %lu\\n",
       stats.ierrors, stats.oerrors, stats.rx_nombuf);

/* 확장 통계 (xstats): NIC별 상세 카운터 */
int len = rte_eth_xstats_get(port_id, NULL, 0);
struct rte_eth_xstat *xstats = malloc(len * sizeof(*xstats));
struct rte_eth_xstat_name *names = malloc(len * sizeof(*names));
rte_eth_xstats_get(port_id, xstats, len);
rte_eth_xstats_get_names(port_id, names, len);

for (int i = 0; i < len; i++)
    printf("  %s: %lu\\n", names[i].name, xstats[i].value);

추가 런타임 서비스: DMA, 전력, 메트릭, 추적

DPDK는 ethdev와 mempool만으로 끝나지 않습니다. 최신 Programmer's Guide는 DMA 엔진 추상화(dmadev), 유휴 폴링 완화용 전력 관리, 메트릭 레지스트리, 저오버헤드 trace 라이브러리를 별도 서브시스템으로 제공합니다. 패킷 처리 코드가 어느 정도 안정되면, 실제 운영 품질은 이 주변 서비스에서 갈리는 경우가 많습니다.

dmadev — 복사/메모리 이동 오프로딩

rte_dmadev는 패킷 그 자체를 처리하는 장치가 아니라 메모리 이동 작업을 오프로딩하는 계층입니다. 공식 문서는 submission/completion ring 기반 API와 ring-less API를 함께 설명합니다. 패킷 payload 복사, 배치 memcpy, 압축/암호화 전 사전 정렬, 소프트웨어 파이프라인의 copy stage 분리에 유용합니다.

CPU 코어 rte_dma_copy() Submission Ring src_iova → dst_iova DMA 엔진(HW) CBDMA / IOAT / IDXD Completion Ring rte_dma_completed() submit → HW가 비동기 복사 CPU는 복사 대기 없이 다른 작업 가능 핵심: CPU memcpy 대신 DMA 엔진이 백그라운드 복사 → 패킷 처리 코어의 메모리 대역폭 절약
/* dmadev 장치 탐색 및 설정 */
int16_t dev_id = rte_dma_next_dev(0);
if (dev_id < 0)
    rte_exit(EXIT_FAILURE, "No DMA device found\n");

struct rte_dma_conf dev_conf = { .nb_vchans = 1 };
rte_dma_configure(dev_id, &dev_conf);

/* vchan(가상 채널) 설정 — 방향, 큐 깊이 */
struct rte_dma_vchan_conf vchan_conf = {
    .direction = RTE_DMA_DIR_MEM_TO_MEM,
    .nb_desc = 2048,   /* submission ring 크기 */
};
rte_dma_vchan_setup(dev_id, 0, &vchan_conf);
rte_dma_start(dev_id);

/* ━━━ 데이터 경로: 비동기 복사 + 완료 수거 ━━━ */
uint16_t vchan = 0;
uint16_t job = rte_dma_copy(dev_id, vchan,
                            src_iova, dst_iova, copy_len,
                            RTE_DMA_OP_FLAG_SUBMIT);
/* RTE_DMA_OP_FLAG_SUBMIT: copy + submit 한 번에 수행 */

/* 다른 패킷 처리 작업을 수행... */

/* 완료 수거 — 배치로 여러 작업의 완료 확인 */
uint16_t last_idx;
bool has_error;
uint16_t done = rte_dma_completed(dev_id, vchan,
                                  32, &last_idx, &has_error);
if (has_error) {
    /* 실패한 작업의 인덱스 확인 */
    rte_dma_completed_status(dev_id, vchan,
                              32, &last_idx, status);
}

코드 분석

  • 1~14행: rte_dma_next_dev()로 사용 가능한 DMA 장치를 탐색합니다. Intel IOAT/CBDMA/IDXD, ARM DPAA2 QDMA 등이 지원됩니다. nb_vchans로 하나의 물리 DMA 엔진을 여러 가상 채널로 나눌 수 있습니다.
  • 18~21행: RTE_DMA_OP_FLAG_SUBMIT 플래그를 사용하면 rte_dma_copy()rte_dma_submit()을 하나로 합쳐 함수 호출 오버헤드를 줄입니다. 여러 작업을 배치로 모아 제출할 때는 플래그 없이 rte_dma_copy()만 반복 후 마지막에 rte_dma_submit()을 한 번 호출합니다.
  • 26~32행: rte_dma_completed()는 완료된 작업 수를 반환합니다. has_error가 true이면 rte_dma_completed_status()로 개별 작업의 실패 원인을 확인할 수 있습니다.
패턴언제 쓰나주의점
ring-based API배치 큐잉, 명확한 completion 수거가 필요할 때큐 깊이와 completion polling 지연을 같이 튜닝해야 합니다.
ring-less API드라이버가 더 직접적인 제출 경로를 제공할 때장치별 지원 범위 차이가 큽니다.
vchan하나의 DMA 장치를 논리 채널로 분리할 때ethdev queue와 일대일 대응을 강제하지 말고 작업 특성에 맞춰 배치해야 합니다.
scatter-gather비연속 메모리 영역을 한 번에 복사할 때rte_dma_copy_sg()는 일부 드라이버만 지원하므로 rte_dma_info_get()으로 사전 확인이 필요합니다.
dmadev vs CPU memcpy: 4KB 이하 소량 복사에서는 CPU memcpy()가 DMA 엔진보다 빠릅니다(제출/완료 오버헤드). 64KB 이상의 대량 복사 또는 패킷 처리 코어의 메모리 대역폭이 병목인 경우에 dmadev가 유리합니다. Intel DSA(IDXD) 벤치마크 기준 256KB 복사에서 CPU 대비 ~30% 대역폭 절약 효과가 있습니다.

유휴 폴링과 전력 관리

DPDK는 본질적으로 busy-poll 프레임워크이지만, 최신 power management 문서는 empty poll 기반 절전, monitor/pause instruction, CPU frequency scaling 연계를 별도로 제공합니다. 즉 "DPDK는 항상 100% CPU를 태운다"는 단순화는 반만 맞습니다. 낮은 부하 구간에서 지연 허용치가 있다면, empty poll 횟수를 기준으로 절전과 지연을 절충할 수 있습니다.

Empty poll 기반 전력 관리 rx_burst() 0 packet empty poll counter 임계치 누적 pause/monitor frequency down 새 패킷 도착 즉시 복귀 지연 최저가 목표면 계속 busy-poll, 전력까지 관리해야 하면 empty poll 임계치와 wake-up 비용을 함께 튜닝합니다.
전략장점대가적합 환경
순수 busy-poll지연 최저, 가장 단순전력 사용량 최고HFT, 금융 거래소, 패킷 브로커
empty poll + pause낮은 부하에서 전력 절감tail latency가 약간 흔들릴 수 있음CDN, DNS 리졸버, 로드밸런서
frequency scaling 연동장시간 저부하 서비스에서 유리복귀 지연이 workload 민감에지 NFV, 야간 운영
monitor/mwait 기반메모리 쓰기 감지 시 즉시 복귀x86 특화, 일부 CPU만 지원지연 민감 + 전력 절감 동시 필요
/* empty poll 기반 전력 관리 적용 예시 */
#include <rte_power_empty_poll.h>

/* 초기화: 임계치와 주파수 단계 설정 */
rte_power_empty_poll_stat_init(&ep_params,
    freq_info, RTE_MAX_LCORE);

/* 폴링 루프 내부 */
uint16_t nb_rx = rte_eth_rx_burst(port, queue, bufs, BURST);
if (nb_rx == 0) {
    /* 빈 폴링 횟수 누적 → 임계치 초과 시 자동으로 주파수 낮춤 */
    rte_power_empty_poll_stat_update(lcore_id);
} else {
    /* 패킷 수신 → 즉시 최대 주파수 복귀 */
    rte_power_poll_stat_update(lcore_id, nb_rx);
}

Metrics와 Trace

Metrics 라이브러리는 이름이 등록된 메트릭 집합에 값을 채워 넣는 모델이고, Trace 라이브러리는 저오버헤드 순환 버퍼에 이벤트를 남긴 뒤 파일로 내보내는 모델입니다. 관측 대상을 "지속적 수치"와 "순간 사건"으로 분리하면 설계가 단순해집니다.

ethdev xstats mempool stats app custom metrics Metrics 라이브러리 지속 수치 레지스트리 key-value 기반 조회 Trace 라이브러리 순환 버퍼 이벤트 기록 CTF 형식 덤프 Telemetry 소켓 UNIX 소켓 JSON API dpdk-telemetry.py Prometheus/Grafana babeltrace2 (CTF)
/* ━━━ Metrics 라이브러리: 커스텀 메트릭 등록과 갱신 ━━━ */
const char * const names[] = {
    "rx_missed_errors",
    "flow_table_hits",
    "flow_table_misses",
};
int key = rte_metrics_reg_names(names, 3);

/* 주기적 갱신 (예: 1초 간격) */
uint64_t values[3] = {
    rx_missed,
    flow_hits,
    flow_misses,
};
rte_metrics_update_values(RTE_METRICS_GLOBAL,
                           key, values, 3);

/* ━━━ Trace 라이브러리: 커스텀 트레이스 포인트 정의 ━━━ */

/* 헤더 파일에서 트레이스 포인트 선언 */
RTE_TRACE_POINT(
    app_trace_pkt_drop,
    RTE_TRACE_POINT_ARGS(uint16_t port_id,
                          uint32_t reason),
    rte_trace_point_emit_u16(port_id);
    rte_trace_point_emit_u32(reason);
)

/* 데이터 경로에서 호출 */
app_trace_pkt_drop(port_id, DROP_REASON_TTL_EXPIRED);

코드 분석

  • 2~6행: rte_metrics_reg_names()는 여러 메트릭 이름을 한 번에 등록합니다. 반환값은 첫 번째 키(key)이며, 나머지는 연속된 정수입니다. 포트별 xstats와 겹치지 않는 "서비스 관점 지표"(플로우 테이블 적중률 등)를 별도로 관리하면 운영 가시성이 높아집니다.
  • 19~27행: RTE_TRACE_POINT 매크로는 CTF(Common Trace Format) 호환 트레이스 포인트를 정의합니다. 데이터 경로에서 호출해도 오버헤드가 ~20ns 수준이므로, 드롭 사유나 상태 전이 같은 희귀 이벤트를 기록하는 데 적합합니다.
  • 30행: 트레이스 덤프는 --trace=.* EAL 옵션이나 rte_trace_save() API로 파일에 기록하며, babeltrace2로 분석합니다.
# ━━━ Telemetry 소켓으로 런타임 모니터링 ━━━

# Telemetry 소켓 경로 확인
ls /var/run/dpdk/rte/dpdk_telemetry.*

# dpdk-telemetry.py로 대화형 조회
dpdk-telemetry.py
--> /ethdev/xstats,0
--> /ethdev/info,0
--> /mempool/list
--> /mempool/info,pktmbuf_pool_0

# Trace 수집 및 분석
# EAL 옵션으로 트레이스 활성화
./dpdk-app --trace="app_trace_*" --trace-dir=/tmp/dpdk-trace

# babeltrace2로 CTF 트레이스 분석
babeltrace2 /tmp/dpdk-trace/rte-*
도구용도오버헤드
Metrics지속적 수치, 외부 모니터링 수집갱신 시 ~10ns (atomic write)포트 xstats와 중복되지 않게 "서비스 관점 지표"를 따로 두면 좋습니다.
Trace희귀 사건, 상태 전이, 에러 경로 분석이벤트당 ~20nsoverwrite/discard 정책을 정하고, 장애 재현 시점에만 활성화하는 편이 안전합니다.
Telemetry런타임 조회 인터페이스조회 시만 (비동기)Metrics/Trace/ethdev/mempool 상태를 한 소켓으로 묶어 조회할 수 있습니다.
Prometheus 연동시계열 대시보드수집 주기에 비례Telemetry → JSON → Prometheus exporter → Grafana 파이프라인이 일반적입니다.

실전 예제: 고성능 L2 포워딩 애플리케이션

DPDK의 가장 기본적인 데이터 경로인 L2(MAC 기반) 포워딩을 실전 수준으로 구현합니다. 단순 예제와 달리 NUMA 배치, 멀티큐 활용, graceful shutdown, xstats 모니터링까지 포함한 프로덕션 패턴을 다룹니다.

L2 Forwarding 데이터 경로 NIC Port 0 RX Queue 0..N rte_eth_rx_burst() burst 32 mbufs MAC Lookup dst_port = port_map[src] Swap MAC src↔dst 교환 rte_eth_tx_burst() NIC Port 1 TX lcore N (NUMA-local) — Run-to-Completion 루프 rte_eal_remote_launch(l2fwd_main_loop, NULL, lcore_id) → 각 lcore가 RX→Lookup→Swap→TX를 독립 실행 멀티큐 매핑 전략 • lcore 0 → Port 0 RXQ 0, Port 1 TXQ 0 • lcore 1 → Port 0 RXQ 1, Port 1 TXQ 1 • NUMA node 일치 → 크로스소켓 지연 제거 성능 핵심 포인트 • burst size 32 — L1 캐시 라인 활용 최적화 • rte_prefetch0() — 다음 mbuf 프리페치 • tx_buffer — 불완전 burst 누적 후 일괄 전송
/* l2fwd 프로덕션 패턴 — 핵심 루프 */
#define MAX_PKT_BURST 32
#define BURST_TX_DRAIN_US 100

static volatile bool force_quit = false;

static void
l2fwd_main_loop(void)
{
    struct rte_mbuf *pkts_burst[MAX_PKT_BURST];
    struct rte_eth_dev_tx_buffer *buffer;
    uint16_t port_id, dst_port;
    unsigned lcore_id = rte_lcore_id();
    uint64_t prev_tsc = 0, cur_tsc, diff_tsc;
    const uint64_t drain_tsc =
        ((uint64_t)BURST_TX_DRAIN_US * rte_get_tsc_hz()) / 1000000;

    /* 이 lcore가 담당하는 포트/큐 정보 조회 */
    struct lcore_queue_conf *qconf = &lcore_queue_conf[lcore_id];
    if (qconf->n_rx_port == 0) return;

    while (!force_quit) {
        cur_tsc = rte_rdtsc();
        diff_tsc = cur_tsc - prev_tsc;

        /* TX drain: 미완성 burst를 주기적으로 flush */
        if (unlikely(diff_tsc > drain_tsc)) {
            for (unsigned i = 0; i < qconf->n_rx_port; i++) {
                port_id = qconf->rx_port_list[i];
                dst_port = l2fwd_dst_ports[port_id];
                buffer = tx_buffer[dst_port];
                rte_eth_tx_buffer_flush(dst_port, 0, buffer);
            }
            prev_tsc = cur_tsc;
        }

        /* RX burst → MAC 교환 → TX buffer */
        for (unsigned i = 0; i < qconf->n_rx_port; i++) {
            port_id = qconf->rx_port_list[i];
            dst_port = l2fwd_dst_ports[port_id];
            buffer = tx_buffer[dst_port];

            uint16_t nb_rx = rte_eth_rx_burst(port_id, 0,
                                              pkts_burst, MAX_PKT_BURST);
            if (nb_rx == 0) continue;

            for (uint16_t j = 0; j < nb_rx; j++) {
                struct rte_mbuf *m = pkts_burst[j];
                rte_prefetch0(rte_pktmbuf_mtod(m, void *));
                l2fwd_simple_forward(m, dst_port, buffer);
            }
        }
    }
}

static void
l2fwd_simple_forward(struct rte_mbuf *m,
                     uint16_t dst_port,
                     struct rte_eth_dev_tx_buffer *buffer)
{
    struct rte_ether_hdr *eth =
        rte_pktmbuf_mtod(m, struct rte_ether_hdr *);

    /* src와 dst MAC 교환 */
    struct rte_ether_addr tmp;
    rte_ether_addr_copy(&eth->src_addr, &tmp);
    rte_ether_addr_copy(&eth->dst_addr, &eth->src_addr);
    rte_ether_addr_copy(&tmp, &eth->dst_addr);

    rte_eth_tx_buffer(dst_port, 0, buffer, m);
}

포트 초기화와 mempool 설정

/* mempool 생성 — NUMA 노드별 */
struct rte_mempool *pktmbuf_pool[RTE_MAX_NUMA_NODES];

for (int i = 0; i < rte_socket_count(); i++) {
    int sid = rte_socket_id_by_idx(i);
    char name[32];
    snprintf(name, sizeof(name), "mbuf_pool_%d", sid);
    pktmbuf_pool[sid] = rte_pktmbuf_pool_create(
        name,
        8192,       /* n: 포트당 4096 + 여유 */
        256,        /* cache_size: lcore 로컬 캐시 */
        0,          /* priv_size */
        RTE_MBUF_DEFAULT_BUF_SIZE,
        sid);
    if (!pktmbuf_pool[sid])
        rte_exit(EXIT_FAILURE, "NUMA %d mempool 생성 실패\n", sid);
}

/* 포트 구성 */
struct rte_eth_conf port_conf = {
    .rxmode = { .mq_mode = RTE_ETH_MQ_RX_RSS },
    .rx_adv_conf.rss_conf = {
        .rss_key = NULL,   /* 기본 Toeplitz 키 사용 */
        .rss_hf = RTE_ETH_RSS_IP | RTE_ETH_RSS_TCP | RTE_ETH_RSS_UDP,
    },
    .txmode = { .mq_mode = RTE_ETH_MQ_TX_NONE },
};

RTE_ETH_FOREACH_DEV(port_id) {
    int socket_id = rte_eth_dev_socket_id(port_id);
    rte_eth_dev_configure(port_id, nb_rxq, nb_txq, &port_conf);

    for (uint16_t q = 0; q < nb_rxq; q++)
        rte_eth_rx_queue_setup(port_id, q, 1024,
                              socket_id, NULL, pktmbuf_pool[socket_id]);
    for (uint16_t q = 0; q < nb_txq; q++)
        rte_eth_tx_queue_setup(port_id, q, 1024,
                              socket_id, NULL);
    rte_eth_dev_start(port_id);
    rte_eth_promiscuous_mode_enable(port_id);
}
성능 포인트:
  • rte_prefetch0()는 다음 mbuf 데이터를 L1 캐시에 미리 로드하여 cache miss를 줄입니다.
  • tx_buffer는 불완전한 burst를 모아 한 번에 전송하므로 PCIe 트랜잭션 수를 줄입니다.
  • mempool을 NUMA 노드별로 분리하면 크로스소켓 메모리 접근 지연을 피할 수 있습니다.
  • RSS를 활성화하면 NIC이 플로우 해시 기반으로 패킷을 여러 RX 큐에 분산합니다.

실전 예제: rte_flow 하드웨어 오프로드

rte_flow API를 사용하면 NIC 하드웨어에 직접 플로우 규칙을 설치하여 소프트웨어 처리 없이 패킷을 분류·드롭·리디렉트할 수 있습니다. 여기서는 실전에서 자주 사용되는 패턴 세 가지 — 특정 플로우를 지정 큐로 분배, 불필요한 트래픽 하드웨어 드롭, hairpin을 이용한 NIC 내부 루프백 — 을 다룹니다.

rte_flow 하드웨어 오프로드 구조 NIC Hardware (Embedded Switch / Flow Engine) Pattern Match ETH / IPV4 / TCP src_ip, dst_port, ... Flow Table Group 0 → Group 1 Priority 기반 매칭 Actions QUEUE / RSS DROP / COUNT MARK / FLAG HAIRPIN / JUMP 소프트웨어 경로 (PMD) RX Queue 0 RX Queue 1 Worker lcore 0 Worker lcore 1 하드웨어 전용 경로 HW DROP (불필요 트래픽) Hairpin (RX→TX) COUNT — 하드웨어 카운터로 통계 수집 (CPU 부하 없음)

패턴 1: 특정 TCP 포트 트래픽을 지정 큐로 분배

/* TCP dst port 80 → RX Queue 1로 분배 */
struct rte_flow_attr attr = {
    .ingress = 1,
    .priority = 0,
};

/* 패턴: ETH / IPv4 / TCP dst_port=80 */
struct rte_flow_item_eth  eth_spec  = { 0 };
struct rte_flow_item_ipv4 ipv4_spec = { 0 };
struct rte_flow_item_tcp  tcp_spec  = {
    .hdr.dst_port = rte_cpu_to_be_16(80),
};
struct rte_flow_item_tcp  tcp_mask  = {
    .hdr.dst_port = 0xFFFF,
};

struct rte_flow_item pattern[] = {
    { .type = RTE_FLOW_ITEM_TYPE_ETH,  .spec = &eth_spec },
    { .type = RTE_FLOW_ITEM_TYPE_IPV4, .spec = &ipv4_spec },
    { .type = RTE_FLOW_ITEM_TYPE_TCP,  .spec = &tcp_spec,
                                       .mask = &tcp_mask },
    { .type = RTE_FLOW_ITEM_TYPE_END },
};

/* 액션: Queue 1로 분배 + 카운터 */
struct rte_flow_action_queue  queue_action  = { .index = 1 };
struct rte_flow_action_count count_action = { 0 };

struct rte_flow_action actions[] = {
    { .type = RTE_FLOW_ACTION_TYPE_COUNT, .conf = &count_action },
    { .type = RTE_FLOW_ACTION_TYPE_QUEUE, .conf = &queue_action },
    { .type = RTE_FLOW_ACTION_TYPE_END },
};

struct rte_flow_error error;
struct rte_flow *flow = rte_flow_create(port_id, &attr,
                                           pattern, actions, &error);
if (!flow)
    rte_exit(EXIT_FAILURE, "Flow 생성 실패: %s\n", error.message);

패턴 2: 하드웨어 수준 트래픽 드롭

/* 특정 src IP로부터의 모든 트래픽을 하드웨어에서 드롭 */
struct rte_flow_item_ipv4 ipv4_spec = {
    .hdr.src_addr = rte_cpu_to_be_32(RTE_IPV4(10,0,0,100)),
};
struct rte_flow_item_ipv4 ipv4_mask = {
    .hdr.src_addr = 0xFFFFFFFF,
};

struct rte_flow_item pattern[] = {
    { .type = RTE_FLOW_ITEM_TYPE_ETH },
    { .type = RTE_FLOW_ITEM_TYPE_IPV4,
      .spec = &ipv4_spec, .mask = &ipv4_mask },
    { .type = RTE_FLOW_ITEM_TYPE_END },
};

struct rte_flow_action actions[] = {
    { .type = RTE_FLOW_ACTION_TYPE_DROP },
    { .type = RTE_FLOW_ACTION_TYPE_END },
};

/* validate → create 2단계 패턴 사용 */
if (rte_flow_validate(port_id, &attr, pattern, actions, &error))
    fprintf(stderr, "Flow 유효성 검사 실패: %s\n", error.message);
else
    flow = rte_flow_create(port_id, &attr, pattern, actions, &error);

패턴 3: Hairpin — NIC 내부 RX→TX 루프백

/* Hairpin 큐 설정: RX Queue 2 → TX Queue 2 (NIC 내부) */
struct rte_eth_hairpin_conf hairpin_conf = {
    .peer_count = 1,
    .peers[0] = { .port = port_id, .queue = 2 },
    .manual_bind = 0,
    .force_memory = 1,
};

rte_eth_rx_hairpin_queue_setup(port_id, 2, 0, &hairpin_conf);
rte_eth_tx_hairpin_queue_setup(port_id, 2, 0, &hairpin_conf);

/* rte_flow로 매칭 트래픽을 hairpin 큐로 보냄 */
struct rte_flow_action_queue hairpin_queue = { .index = 2 };
struct rte_flow_action actions[] = {
    { .type = RTE_FLOW_ACTION_TYPE_QUEUE, .conf = &hairpin_queue },
    { .type = RTE_FLOW_ACTION_TYPE_END },
};
주의: rte_flow 규칙은 NIC 하드웨어 테이블 용량에 제한됩니다. Mellanox ConnectX-5는 수십만 규칙을 지원하지만, Intel E810은 수천 개 수준입니다. rte_flow_validate()를 항상 먼저 호출하여 하드웨어 지원 여부를 확인하세요. Flow aging(RTE_FLOW_ACTION_TYPE_AGE)을 활용하면 오래된 규칙을 자동으로 만료시킬 수 있습니다.

실전 예제: pktgen-dpdk 패킷 생성기 활용

pktgen-dpdk는 DPDK 기반 고성능 패킷 생성·수신 도구로, line rate 트래픽 생성정밀한 성능 측정을 동시에 수행합니다. 10/25/100 GbE 환경에서 실제 PPS, 지연, 드롭률을 측정하는 실전 절차를 다룹니다.

# pktgen-dpdk 빌드 (meson 기반)
git clone https://github.com/pktgen/Pktgen-DPDK.git
cd Pktgen-DPDK
meson build -Denable_lua=false
ninja -C build

# 실행: 4 lcore, 2 포트
sudo ./build/app/pktgen -l 0-3 -n 4 --socket-mem 1024,0 -- \
    -P -m "[1:2].0" -m "[3].1"
# -P: 프로미스큐어스 모드
# -m "[lcore_list].port": lcore와 포트 매핑

성능 측정 시나리오

# pktgen 콘솔에서 실행하는 명령

# 시나리오 1: 64바이트 최소 프레임 — 최대 PPS 측정
set 0 size 64
set 0 rate 100
set 0 count 0        # 무한 전송
start 0

# 시나리오 2: IMIX (Internet Mix) — 실제 트래픽 패턴 시뮬레이션
# 64B:40%, 594B:33%, 1518B:27% (일반적인 인터넷 트래픽 비율)
set 0 size 64
set 0 rate 40
start 0
# 별도 포트/스트림에서 594B, 1518B 생성

# 시나리오 3: 지연 측정 (latency)
enable 0 latency
set 0 rate 50        # 50% rate로 지연 측정
start 0
page latency         # 지연 통계 페이지 전환

# 결과 확인
page main            # 메인 통계 (PPS, Mbps, 오류)
page stats           # 상세 포트 통계

# 결과 CSV 저장
save results.csv
프레임 크기10 GbE 이론 PPS25 GbE 이론 PPS100 GbE 이론 PPS측정 포인트
64 B14.88 Mpps37.20 Mpps148.81 MppsCPU core당 PPS, 멀티큐 확장성
128 B8.45 Mpps21.13 Mpps84.46 Mpps소규모 페이로드(Payload) 패킷 처리 효율
512 B2.35 Mpps5.87 Mpps23.50 Mpps메모리 대역폭 병목(Bottleneck) 확인
1518 B0.81 Mpps2.03 Mpps8.13 MppsMTU 크기 대역폭 포화 테스트
IMIX~3.5 Mpps~8.7 Mpps~34.8 Mpps실제 워크로드 시뮬레이션
측정 팁:
  • 최소 30초 이상 지속 전송하여 안정 상태(steady state) 수치를 확인합니다.
  • page stats에서 oErrors가 0이 아니면 TX 큐 오버플로우를 의심합니다.
  • 지연 측정 시 enable latency는 패킷에 타임스탬프를 삽입하므로 PPS가 약간 감소합니다.
  • 비교 측정 시 반드시 동일 NUMA 노드, 동일 hugepage 구성에서 반복합니다.

흔한 실수와 안티패턴

DPDK는 커널 네트워크 스택 대비 하드웨어에 훨씬 가까운 수준에서 동작하므로, 한 가지 설정 오류가 성능을 10배 이상 저하시키는 경우가 빈번합니다. 아래는 실무에서 반복적으로 발생하는 실수와 그 진단·해결 방법입니다.

1. Hugepage 부족 / NUMA 불일치

증상원인해결
EAL 초기화 실패 또는 mempool 생성 오류 hugepage 총량 부족 dpdk-hugepages.py --setup 4G로 충분한 hugepage 확보
성능이 기대치의 50~70% NIC이 NUMA 0인데 hugepage가 NUMA 1에만 할당 --socket-mem 2048,2048로 노드별 균등 할당
rte_pktmbuf_pool_create() 실패 다른 DPDK 프로세스가 hugepage를 점유 ls /dev/hugepages/로 잔여 파일 확인 후 정리
# NUMA 노드별 hugepage 상태 확인
cat /sys/devices/system/node/node0/hugepages/hugepages-1048576kB/nr_hugepages
cat /sys/devices/system/node/node1/hugepages/hugepages-1048576kB/nr_hugepages

# NIC이 어느 NUMA 노드에 있는지 확인
cat /sys/bus/pci/devices/0000:3b:00.0/numa_node

# 노드별 hugepage 할당 (커널 부팅 파라미터)
# GRUB: hugepagesz=1G hugepages=4 default_hugepagesz=1G
# 또는 런타임:
echo 2 > /sys/devices/system/node/node0/hugepages/hugepages-1048576kB/nr_hugepages
echo 2 > /sys/devices/system/node/node1/hugepages/hugepages-1048576kB/nr_hugepages

2. IOMMU/VFIO 설정 누락

증상원인해결
vfio-pci 바인딩 실패 IOMMU가 비활성 상태 GRUB에 intel_iommu=on iommu=pt 추가 후 리부팅
DMA 에러, 패킷 유실 igb_uio 사용 시 DMA 격리 없음 vfio-pci로 전환 (프로덕션 필수)
IOMMU 그룹 충돌 같은 그룹의 다른 디바이스가 커널 드라이버에 바인딩 그룹 내 모든 디바이스를 vfio-pci에 바인딩 또는 ACS 활성화
igb_uio vs vfio-pci 선택 기준:
  • 프로덕션: 반드시 vfio-pci — DMA 격리, 비특권 실행, Secure Boot 호환
  • 레거시/실험: igb_uio — IOMMU 없는 환경에서만 (root 필수, 보안 취약)
  • 가상 머신: vfio-pci + no-iommu 모드 (격리 없음 인지 필요)

3. lcore 수와 RX/TX 큐 매핑 불일치

/* 흔한 실수: lcore 4개인데 RX 큐가 2개뿐 */
lcore 0 → RXQ 0  (정상)
lcore 1 → RXQ 1  (정상)
lcore 2 → ???     (유휴 — CPU 100% 점유하며 아무 일 안 함)
lcore 3 → ???     (유휴 — CPU 100% 점유하며 아무 일 안 함)

/* 올바른 매핑: lcore 수 = RX 큐 수 */
rte_eth_dev_configure(port_id, 4, 4, &port_conf);  /* 4 RXQ, 4 TXQ */
/* 또는 lcore 수를 NIC 큐 수에 맞춤 */
dpdk-app -l 0-1 ...  /* 2 lcore for 2 queues */

/* NIC 최대 큐 수 확인 */
struct rte_eth_dev_info dev_info;
rte_eth_dev_info_get(port_id, &dev_info);
printf("max_rx_queues=%u max_tx_queues=%u\n",
       dev_info.max_rx_queues, dev_info.max_tx_queues);

4. rte_mbuf 메모리 풀 크기 과소/과대 설정

문제증상권장
과소 설정 rte_eth_rx_burst() 반환값이 0으로 고정, rx_mbuf_alloc_failed xstat 증가 최소 N = nb_ports × nb_rxq × rx_desc + nb_ports × nb_txq × tx_desc + nb_lcores × cache_size + 512
과대 설정 hugepage 고갈, 다른 DPDK 앱/SPDK 실행 불가 실제 in-flight 패킷 수 기반으로 산정 (2~3배 여유면 충분)
/* mempool 크기 산정 공식 */
#define NB_RXD 1024
#define NB_TXD 1024
#define MEMPOOL_CACHE_SIZE 256

unsigned nb_mbufs = RTE_MAX(
    nb_ports * (nb_rxq * NB_RXD + nb_txq * NB_TXD +
                MAX_PKT_BURST + nb_lcores * MEMPOOL_CACHE_SIZE),
    8192U);

/* 실전 권장: 포트 2개, 큐 4개씩이면 */
/* 2 * (4*1024 + 4*1024 + 32 + 4*256) = 18,496 → 2^15 = 32768로 올림 */

5. RSS 해시 키 설정 오류

/* 흔한 실수: rss_hf에 NIC이 지원하지 않는 플래그 포함 */
struct rte_eth_dev_info dev_info;
rte_eth_dev_info_get(port_id, &dev_info);

/* 반드시 NIC 지원 범위와 AND 연산 */
port_conf.rx_adv_conf.rss_conf.rss_hf &=
    dev_info.flow_type_rss_offloads;

/* 흔한 실수 2: 커스텀 해시 키 길이가 NIC과 불일치 */
/* Intel NIC: 40바이트, Mellanox: 40바이트, 일부: 52바이트 */
printf("RSS key len = %u\n", dev_info.hash_key_size);

/* 대칭(Symmetric) RSS가 필요한 경우 — 양방향 동일 큐 */
/* Toeplitz 대칭 키 사용 또는 XOR 기반 해시 함수 선택 */
static uint8_t symmetric_key[40] = {
    0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
    0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
    0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
    0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
    0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
};
port_conf.rx_adv_conf.rss_conf.rss_key = symmetric_key;
port_conf.rx_adv_conf.rss_conf.rss_key_len = 40;

6. 애플리케이션 종료 시 리소스 미해제

/* 시그널 핸들러 */
static void signal_handler(int signum)
{
    if (signum == SIGINT || signum == SIGTERM)
        force_quit = true;
}

/* 올바른 종료 시퀀스 */
static void cleanup(void)
{
    uint16_t port_id;

    /* 1. 모든 lcore의 메인 루프 종료 대기 */
    rte_eal_mp_wait_lcore();

    /* 2. 포트 정지 및 해제 */
    RTE_ETH_FOREACH_DEV(port_id) {
        rte_eth_dev_stop(port_id);
        rte_flow_flush(port_id, NULL);  /* flow rule 정리 */
        rte_eth_dev_close(port_id);
    }

    /* 3. mempool은 rte_eal_cleanup()이 처리 */
    rte_eal_cleanup();
}

/* main()에서: */
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
/* ... 메인 루프 ... */
cleanup();
리소스 누수 체크리스트:
  • rte_flow_flush() 미호출 → NIC에 고아 flow rule 잔존, 재시작(Reboot) 시 예기치 않은 패킷 분류
  • rte_eth_dev_stop() 미호출 → NIC이 계속 DMA 수행, hugepage 해제 불가
  • rte_eal_cleanup() 미호출 → /dev/hugepages/에 파일 잔존, 다음 실행 시 hugepage 부족
  • DPDK 22.11 이후 rte_eth_dev_close()dev_stop()이 포함되지 않음 — 반드시 별도 호출

이벤트 드리븐 vs Poll 모드 비교

DPDK의 기본 모델인 Poll 모드(PMD)는 lcore가 무한 루프로 NIC을 폴링하여 최소 지연을 달성하지만, CPU를 항상 100% 점유합니다. 이벤트 드리븐 모드(rte_event)는 하드웨어 또는 소프트웨어 이벤트 스케줄러를 통해 "일이 있을 때만" 워커를 깨워 전력 효율과 확장성을 높입니다.

Poll Mode vs Event-Driven Mode Poll Mode (PMD) NIC RXQ lcore (poll) TX while(!quit) { rx_burst(); process(); tx_burst(); } 특성 + 최소 지연 (수백 ns) + 예측 가능한 성능 + 구현 단순 - CPU 100% 점유 (전력 낭비) - 1 lcore = 1 큐 고정 매핑 - 트래픽 없어도 busy-wait 적합: NFV, 통신장비, 전용 어플라이언스 Event-Driven (rte_event) RX Adapter (interrupt) Event Scheduler Worker 0 Worker 1 rte_event_dequeue_burst() → process → enqueue() 특성 + 유연한 워커 스케줄링 + Atomic 큐로 락 없는 플로우 처리 + 워커 수 동적 조정 가능 - 이벤트 디스패치 오버헤드 (~μs 추가) - 하드웨어 스케줄러 의존 (DLB2 등) - 디버깅 복잡도 증가 적합: 멀티스테이지 파이프라인, 전력 민감 환경
비교 항목Poll Mode (PMD)Event-Driven (rte_event)하이브리드
CPU 사용100% 항상이벤트 시에만고부하 구간 poll, 저부하 구간 event
지연수백 ns~1-5 μs (스케줄링 포함)구간별 상이
확장성큐 수 = lcore 수 고정워커 동적 할당 가능핫 경로만 poll 고정
플로우 순서RSS 기반 (해시 충돌 시 순서 역전 가능)Atomic/Ordered 큐로 보장경로별 선택
전력 효율매우 낮음양호중간
구현 복잡도낮음높음매우 높음
하드웨어 요구없음DLB2/OCTEON SSO 권장선택적
/* 이벤트 드리븐 모드 기본 루프 */
static void
event_worker_loop(void *arg)
{
    uint8_t event_dev_id = *(uint8_t *)arg;
    struct rte_event events[MAX_PKT_BURST];

    while (!force_quit) {
        uint16_t nb_events = rte_event_dequeue_burst(
            event_dev_id,
            0,        /* port_id */
            events,
            MAX_PKT_BURST,
            0);       /* timeout_ticks: 0 = non-blocking */

        for (uint16_t i = 0; i < nb_events; i++) {
            struct rte_mbuf *m = events[i].mbuf;
            /* 패킷 처리 로직 */
            process_packet(m);

            /* TX Adapter로 전달 */
            events[i].op = RTE_EVENT_OP_FORWARD;
            events[i].queue_id = TX_STAGE_QUEUE;
        }

        if (nb_events > 0)
            rte_event_enqueue_burst(event_dev_id, 0,
                                    events, nb_events);
    }
}
하이브리드 전략: 실무에서는 두 모델을 결합하는 경우가 많습니다. 예를 들어 ingress 경로는 poll 모드로 최소 지연을 유지하고, 암호화·압축 등 CPU 집약적 스테이지는 eventdev로 부하를 분산하는 파이프라인이 효과적입니다. DPDK l2fwd-event 예제가 이 패턴의 참고 구현입니다.

Graph Library: 패킷 처리 파이프라인

rte_graph는 기존 수동 RTC 루프를 재사용 가능한 노드 그래프로 구조화하는 프레임워크입니다. 여기서는 기본 개념을 넘어 커스텀 노드 작성, 노드 간 데이터 전달, 성능 통계 수집, 그리고 dispatch 모드 활용까지 실전 패턴을 다룹니다.

Graph Library: 커스텀 노드 파이프라인 ethdev_rx Source Node classifier 커스텀 분류 노드 ip4_lookup acl_filter ip4_rewrite pkt_drop ethdev_tx 커스텀 노드 구현 구조 노드 등록 RTE_NODE_REGISTER() .process = my_node_fn .nb_edges = 2 (next0, next1) .init = my_node_init process 함수 패턴 objs = rte_node_next_stream_get() for (i=0; i < nb_objs; i++) classify → rte_node_enqueue_x1() rte_node_next_stream_put() 성능 최적화 포인트 • 4개 mbuf 동시 처리 (x4 unroll) • rte_prefetch0() 다음 burst 데이터 • 동일 next로 향하는 패킷 일괄 전달 • ctx에 per-node 상태 저장 (전역 변수 금지)
/* 커스텀 분류 노드 예시 */
static uint16_t
classifier_node_process(struct rte_graph *graph,
                        struct rte_node *node,
                        void **objs, uint16_t nb_objs)
{
    uint16_t n_ip4 = 0, n_drop = 0;
    void **to_ip4, **to_drop;

    /* next edge 0 = ip4_lookup, edge 1 = drop */
    to_ip4 = rte_node_next_stream_get(graph, node, 0, nb_objs);
    to_drop = rte_node_next_stream_get(graph, node, 1, nb_objs);

    for (uint16_t i = 0; i < nb_objs; i++) {
        struct rte_mbuf *m = (struct rte_mbuf *)objs[i];
        struct rte_ether_hdr *eth =
            rte_pktmbuf_mtod(m, struct rte_ether_hdr *);

        if (eth->ether_type == rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4))
            to_ip4[n_ip4++] = m;
        else
            to_drop[n_drop++] = m;
    }

    rte_node_next_stream_put(graph, node, 0, n_ip4);
    rte_node_next_stream_put(graph, node, 1, n_drop);
    return nb_objs;
}

static struct rte_node_register classifier_node = {
    .name = "classifier",
    .process = classifier_node_process,
    .nb_edges = 2,
    .next_nodes = { [0] = "ip4_lookup",
                    [1] = "pkt_drop" },
};
RTE_NODE_REGISTER(classifier_node);

Graph 성능 통계와 디버깅

/* 그래프 통계 수집 활성화 */
struct rte_graph_cluster_stats_param stats_param = {
    .socket_id = SOCKET_ID_ANY,
    .fn = NULL,         /* 기본 콘솔 출력 */
    .nb_graph_patterns = 1,
    .graph_patterns = (const char *[]){ "*" },
};

struct rte_graph_cluster_stats *stats =
    rte_graph_cluster_stats_create(&stats_param);

/* 주기적으로 호출하여 노드별 통계 출력 */
rte_graph_cluster_stats_get(stats, 0);

/* 출력 예시:
 * +------+---+------+----------+----------+---------+
 * | Node |St | Calls|   Objs   |  Realloc |  Cycles |
 * +------+---+------+----------+----------+---------+
 * | rx   | 0 | 1.2M | 38.5M   |    0     |   120   |
 * | cls  | 0 | 1.2M | 38.5M   |   12     |   180   |
 * | tx   | 0 | 1.1M | 37.0M   |    0     |    95   |
 * | drop | 0 | 0.1M |  1.5M   |    0     |    40   |
 * +------+---+------+----------+----------+---------+
 */

/* DOT 그래프 export (Graphviz 시각화용) */
rte_graph_export("graph0", fopen("pipeline.dot", "w"));
Graph vs Eventdev 선택: Graph는 코드 구조화 도구이고 Eventdev는 스케줄링 도구입니다. Graph를 사용하더라도 내부적으로 eventdev를 통합할 수 있습니다. "노드 재사용성과 통계"가 중요하면 Graph, "부하 기반 동적 워커 분배"가 중요하면 Eventdev, 둘 다 필요하면 Graph 노드 안에서 eventdev API를 호출하는 조합이 가능합니다.

DPDK + GPU 연동 (gpudev)

DPDK 21.11에서 도입된 rte_gpudev 라이브러리는 GPU 메모리와 DPDK mbuf 간 zero-copy 데이터 교환을 가능하게 합니다. DPI(Deep Packet Inspection), AI/ML 기반 트래픽 분석, 암호화/압축 가속 등 GPU의 대규모 병렬 연산 능력을 네트워크 데이터 경로에 직접 통합할 수 있습니다.

DPDK + GPU (gpudev) 데이터 흐름 NIC PMD RX/TX CPU (DPDK lcore) rte_eth_rx_burst() 패킷 전처리/분류 GPU comm_list 준비 GPU (CUDA/ROCm) GPU 메모리 (VRAM) DPI 커널 ML 추론 결과 처리 DROP / FORWARD 메모리 공유 메커니즘 CPU 가시 메모리 rte_gpu_mem_cpu_map() → GPU VRAM 매핑 mbuf 외부 버퍼로 GPU 메모리 연결 GPU 가시 메모리 rte_gpu_mem_alloc() → GPU VRAM 할당 GDR (GPUDirect RDMA) NIC→GPU 직접 Communication List rte_gpu_comm_*() API CPU↔GPU 비동기 배치 통신
/* DPDK gpudev 기본 사용 패턴 */
#include <rte_gpudev.h>

/* 1. GPU 디바이스 탐색 */
int16_t gpu_id = rte_gpu_find_next(RTE_GPU_ID_NONE);
struct rte_gpu_info gpu_info;
rte_gpu_info_get(gpu_id, &gpu_info);
printf("GPU: %s, NUMA: %d\n", gpu_info.name, gpu_info.numa_node);

/* 2. GPU 메모리 할당 */
size_t gpu_buf_size = 1024 * 1024;  /* 1 MB */
void *gpu_buf = rte_gpu_mem_alloc(gpu_id, gpu_buf_size, 0);

/* 3. CPU에서 GPU 메모리 접근 가능하게 매핑 */
void *cpu_ptr;
rte_gpu_mem_cpu_map(gpu_id, gpu_buf_size, gpu_buf, &cpu_ptr);

/* 4. Communication List 생성 (비동기 배치 통신) */
struct rte_gpu_comm_list *comm_list =
    rte_gpu_comm_create_list(gpu_id, 64);  /* 64 슬롯 */

/* 5. 패킷 데이터를 comm_list에 등록 */
rte_gpu_comm_populate_list_pkts(comm_list, 0, pkts, nb_rx);

/* 6. GPU 커널 실행 (CUDA 예시) */
/* GPU 측에서 comm_list를 읽어 패킷 처리 */

/* 7. 정리 */
rte_gpu_comm_destroy_list(comm_list, 64);
rte_gpu_mem_cpu_unmap(gpu_id, cpu_ptr);
rte_gpu_mem_free(gpu_id, gpu_buf);
전송 경로방법지연적합 용도
NIC → CPU → GPUmbuf → rte_gpu_mem_cpu_map → GPU 복사~10 μs소량 패킷, CPU 전처리 필요
NIC → GPU (GDR)GPUDirect RDMA, NIC DMA → GPU VRAM 직접~2 μs대량 패킷, 최소 지연 요구
GPU → CPU → NICGPU 결과 → CPU 읽기 → TX burst~10 μsGPU 분석 후 포워딩/드롭 결정
GPU 연동 시 주의사항:
  • NUMA 정렬 필수: NIC, GPU, DPDK lcore가 같은 NUMA 노드에 있어야 PCIe 대역폭을 온전히 활용합니다.
  • 배치 크기: GPU 커널은 수천 개 패킷을 한 번에 처리할 때 효율적입니다. 소규모 burst(<32)에서는 커널 론칭 오버헤드가 지배적입니다.
  • NVIDIA: cuda.pc를 meson 빌드에 포함 (-Dgpu_drivers=cuda). CUDA 11.4 이상 권장.
  • gpudev는 실험적 API: DPDK 24.x 기준 __rte_experimental 태그. ABI 안정성이 보장되지 않습니다.

DPDK 23.x / 24.x 주요 변경사항

DPDK는 연 3회(3월, 7월, 11월) 메이저 릴리스를 발행하며, 각 LTS(Long Term Support)는 2년간 유지됩니다. 최근 릴리스에서 데이터 경로 성능, API 안정성, 하드웨어 지원 범위에 중요한 변경이 있었습니다.

DPDK 23.x 시리즈 (23.03, 23.07, 23.11)

변경사항버전영향
rte_flow 비동기 API 안정화23.03rte_flow_async_create()가 실험적에서 안정 API로 전환. 수만 개 flow rule을 non-blocking으로 삽입/삭제 가능
Graph dispatch 모드 추가23.03기존 RTC(run-to-completion)에 더해 dispatcher 기반 멀티코어 실행 모델 도입
rte_mempool 성능 개선23.07per-lcore 캐시 리필 로직 최적화, 소규모 burst에서 15~20% 처리량 향상
Eventdev DLB2 v2.5 지원23.07Intel DLB2 하드웨어 스케줄러 드라이버 업데이트, COS(Class of Service) 지원
AF_XDP 멀티큐 개선23.11AF_XDP PMD에서 여러 RX/TX 큐를 효율적으로 사용. 커널 6.6+ 요구
Telemetry v223.11JSON 기반 텔레메트리 인터페이스 확장, Prometheus exporter와 직접 연동 가능
Marvell OCTEON 10 PMD23.11ARM64 기반 DPU용 네트워크 드라이버 추가

DPDK 24.x 시리즈 (24.03, 24.07, 24.11)

변경사항버전영향
C11 필수 전환24.03빌드 시 C11 표준 필수. _Atomic 타입 사용 확대, 구형 컴파일러(GCC < 4.9) 미지원
rte_eth_dev 구조 정리24.03비추천 API 대량 제거. rte_eth_dev_info_get() 반환 구조체 변경 — 기존 코드 마이그레이션 필요
MSIX 인터럽트 프레임워크 리팩터링24.07인터럽트 핸들러(Handler) 등록 API 변경. rte_intr_callback_register() 시그니처 수정
gpudev CUDA 12 지원24.07CUDA 12.x 호환, rte_gpu_comm_* API 개선, 비동기 커널 론칭 최적화
NVIDIA BlueField-3 PMD24.07BF3 DPU 네이티브 PMD 추가. ConnectX-7 기반 200 GbE 지원
Flow Quota / Meter 정책 확장24.11rte_flow에 quota 기반 rate-limiting 액션 추가, meter 정책 동적 변경 지원
Power 라이브러리 개편24.11rte_power가 AMD EPYC cppc, Intel HWP를 직접 제어. 유휴 lcore 절전 전략 세분화
24.11 LTS24.112년 LTS. 22.11 LTS → 24.11 LTS 마이그레이션 시 ABI 호환성 주의

마이그레이션 주요 주의사항

/* 22.11 LTS → 24.11 LTS 마이그레이션 체크리스트 */

1. 빌드 환경
   - GCC 10+ 또는 Clang 12+ (C11 필수)
   - meson 0.53+ → 0.56+ 업그레이드 권장
   - pkg-config 파일명 변경 확인: libdpdk.pc

2. API 변경 (컴파일 오류 유발)
   - rte_eth_dev_count() → rte_eth_dev_count_avail()
   - rte_eth_dev_attach() → rte_dev_probe() + rte_eth_dev_get_port_by_name()
   - rte_memcpy() inline 제거 → 표준 memcpy() 사용 권장
   - DEV_RX_OFFLOAD_* → RTE_ETH_RX_OFFLOAD_* 접두사 변경

3. ABI 변경 (재컴파일 필요)
   - rte_eth_dev_info 구조체 크기 변경
   - rte_flow_item 내부 필드 재배치
   - rte_mbuf dynamic field 오프셋 변경 가능

4. 드라이버 지원
   - net/bnxt: 펌웨어 226.x+ 필수
   - net/mlx5: MLNX_OFED 5.9+ 또는 rdma-core 44+ 필요
   - net/ice: DDP 패키지 버전 확인
업그레이드 전략: DPDK LTS 마이그레이션은 "testpmd로 기본 기능 검증 → 단위 테스트 → 통합 테스트" 3단계로 진행하는 것이 안전합니다. 특히 rte_flow 규칙은 NIC 펌웨어와 DPDK 버전 조합에 따라 동작이 달라질 수 있으므로, rte_flow_validate()로 모든 규칙을 재검증해야 합니다.

testpmd 기반 재현 실습

testpmd는 DPDK를 배울 때 가장 중요한 도구입니다. 포트 초기화, 큐 배치, 오프로드 플래그, xstats, flow rule, representor, hairpin을 하나씩 검증할 수 있기 때문입니다. 실무에서는 "내 애플리케이션이 이상하다"라고 보기 전에 먼저 testpmd로 하드웨어와 EAL 조합이 정상인지 확인해야 합니다.

랩 환경 준비

# 1. hugepage 준비
sudo dpdk-hugepages.py -p 1G --setup 2G
dpdk-hugepages.py --show

# 2. 장치 바인딩 상태 확인
dpdk-devbind --status

# 3. VFIO 바인딩
sudo modprobe vfio-pci
sudo ip link set ens3f0 down
sudo dpdk-devbind --bind=vfio-pci 0000:03:00.0

# 4. interactive testpmd 시작
dpdk-testpmd -l 2-5 -n 4 \
  --iova-mode=va \
  -a 0000:03:00.0 \
  -- -i \
  --nb-cores=2 \
  --rxq=2 --txq=2 \
  --burst=32 \
  --port-topology=chained

세션 중 확인 명령

testpmd> show port info all
testpmd> show port stats all
testpmd> show rxq info 0 0
testpmd> show txq info 0 0
testpmd> set fwd macswap
testpmd> set burst 64
testpmd> start
testpmd> stop
testpmd> show fwd stats all
testpmd> show port xstats 0
testpmd> clear port stats all
testpmd> dump mempool
testpmd> quit

패킷 캡처와 관찰 포인트

관찰 대상확인 도구정상 신호
링크/속도/오프로드show port info all링크 업, MTU, RSS/offload capability가 기대치와 일치
큐 배치show rxq info, show txq infoNUMA 노드와 큐 수가 워커 수에 맞음
xstatsshow port xstats 0rx_discards, rx_missed_errors, rx_nombuf가 낮음
패킷 캡처dpdk-dumpcap 또는 dpdk-pdump테스트 프레임이 기대한 queue/port로 보임
dpdk-dumpcap 제약: 공식 도구 문서는 dpdk-dumpcap이 primary 애플리케이션과 같은 DPDK 버전을 써야 하고, primary 쪽에서 패킷 캡처 프레임워크가 준비되어 있어야 한다고 설명합니다. 바로 캡처가 되지 않으면 NIC 문제보다 버전/프레임워크 전제부터 확인하세요.
권장 절차: io 모드로 하드웨어 RX/TX만 먼저 확인하고, 그다음 macswap, flow, representor, hairpin처럼 상태 공간을 넓혀 가는 편이 디버깅 비용이 낮습니다.

VPP (FD.io) — 유저 공간 네트워크 스택

VPP(Vector Packet Processing)는 Cisco가 개발하고 Linux Foundation의 FD.io 프로젝트로 운영되는 고성능 유저 공간 네트워크 스택입니다. DPDK PMD 위에서 동작하며 L2~L4 스위칭/라우팅, NAT, IPSec, ACL 등 커널 네트워크 스택의 기능을 유저 공간에서 구현합니다. "벡터 처리(Vector Processing)"라는 고유한 패킷 처리 모델이 VPP의 핵심 성능 비결입니다.

벡터 처리 모델

전통적인 패킷 처리(OVS, 커널 등)는 한 패킷을 모든 처리 단계(L2→L3→ACL→NAT→TX)를 거치게 한 뒤 다음 패킷으로 넘어갑니다. 이 방식은 단계마다 다른 코드를 실행하므로 I-cache(명령어 캐시)가 매번 교체됩니다. VPP는 반대로 같은 처리 노드를 256개 패킷에 연속 적용한 뒤 다음 노드로 넘어가는 벡터 모델을 사용합니다.

전통적 모델 (패킷 단위) pkt1:L2 pkt1:L3 pkt1:ACL pkt1:TX pkt2:L2 pkt2:L3 pkt2:ACL pkt2:TX pkt3:L2 pkt3:L3 pkt3:ACL pkt3:TX 매 단계마다 I-cache 교체 → 캐시 미스 누적 VPP 벡터 모델 (노드 단위) L2 노드 pkt1 pkt2 pkt3 ... pkt256 → L2 일괄 처리 L3 노드 pkt1 pkt2 pkt3 ... pkt256 → L3 일괄 처리 ACL 노드 pkt1 pkt2 pkt3 ... pkt256 → ACL 일괄 처리 동일 코드를 256회 반복 → I-cache hot 유지 prefetch로 D-cache 미스도 최소화
벡터 크기(Vector Size)의 의미: VPP의 기본 벡터 크기는 256입니다. 한 노드가 256개 패킷을 배치로 처리하면, (1) 노드 함수 코드가 I-cache에 완전히 적재된 상태로 256회 실행되고, (2) 다음 패킷의 데이터를 CLIB_PREFETCH()로 미리 가져오므로 D-cache 미스도 줄어듭니다. 이 두 가지 효과가 합쳐져 코어당 ~15 Mpps를 달성합니다.

VPP 노드 그래프 구조

VPP의 패킷 처리 파이프라인은 노드 그래프(Node Graph)로 구성됩니다. 각 노드는 특정 처리 단계(L2 입력, IPv4 조회, ACL 적용 등)를 담당하며, 패킷의 다음 노드(next-node)를 결정하여 전달합니다.

노드 이름역할다음 노드 예시
dpdk-inputDPDK PMD에서 패킷 수신ethernet-input
ethernet-input이더넷 헤더 파싱, VLAN 처리ip4-input, ip6-input, arp-input
ip4-inputIPv4 헤더 검증, TTL 체크ip4-lookup, ip4-local
ip4-lookupFIB 조회, 다음 홉 결정ip4-rewrite, ip4-arp
ip4-rewriteMAC 재작성, TTL 감소interface-output
acl-plugin-in-ip4-faACL 규칙 매칭다음 feature 노드 또는 drop
nat44-in2outNAT44 내부→외부 변환ip4-lookup
interface-output출력 인터페이스로 패킷 전달dpdk-tx
# ━━━ VPP CLI 기본 조작 ━━━

# VPP 상태 확인
vppctl show version
vppctl show interface

# DPDK 인터페이스 할당 후 활성화
vppctl create interface dpdk GigabitEthernet3/0/0
vppctl set interface state GigabitEthernet3/0/0 up
vppctl set interface ip address GigabitEthernet3/0/0 10.0.0.1/24

# NAT44 설정 예시
vppctl nat44 add interface address GigabitEthernet3/0/1
vppctl set interface nat44 in GigabitEthernet3/0/0 out GigabitEthernet3/0/1

# 노드 그래프 성능 분석
vppctl show runtime                 # 노드별 벡터 크기, 클럭, 호출 횟수
vppctl show node counters           # 노드별 처리 패킷/드롭 카운터
vppctl clear runtime                # 카운터 초기화

# 패킷 트레이싱 (디버깅)
vppctl trace add dpdk-input 100     # dpdk-input 노드에서 100개 패킷 캡처
vppctl show trace                   # 캡처된 패킷의 노드별 처리 경로 출력

OVS-DPDK vs VPP 상세 비교

비교 항목OVS-DPDKVPP/FD.io
주요 용도L2 가상 스위칭 (OpenFlow)L2~L4 라우팅/NAT/IPSec
패킷 처리플로우 테이블 매칭벡터 그래프 (노드 체인)
처리 모델플로우 캐시 + upcall벡터화: 같은 노드를 256패킷 배치로 처리
I-cache 효율보통 (플로우별 분기)높음 (동일 코드를 벡터 크기만큼 반복)
기능L2 스위칭 특화L2~L4, NAT44/NAT64, SRv6, IPSec, MPLS, VXLAN 등
설정OpenFlow / OVNCLI / API (VPP API, NETCONF/YANG)
성능 (64B)~10 Mpps/core~15 Mpps/core (벡터화 효과)
확장 메커니즘OpenFlow 규칙 + OVN플러그인 노드 그래프 (C API, 핫 플러그)
오케스트레이션OpenStack Neutron, OVN 통합 성숙Honeycomb/hc2vpp (NETCONF/YANG), NSM
TCP/IP 스택없음 (L2 전용)HostStack (내장 TCP/TLS), VCL
멀티코어PMD 코어 + 핸들러 코어 분리워커별 독립 노드 그래프 실행
생태계매우 넓음 (OpenStack, K8s, VMware 등)NFV/SD-WAN 중심 (Cisco, Calico/VPP)
선택 기준: L2 가상 스위칭이 주 목적이고 OpenStack/OVN 통합이 필요하면 OVS-DPDK가 적합합니다. L3 라우팅, NAT, IPSec, ACL 등 복잡한 패킷 처리가 필요하고 최대 처리량이 중요하면 VPP가 유리합니다. VPP의 HostStack을 사용하면 TLS 프록시, HTTP 서버 등 TCP 애플리케이션도 유저 공간에서 구현할 수 있습니다.

DPDK 관련 커널 소스 구조

DPDK 자체는 유저 공간 라이브러리이지만, 커널 측에서 DPDK 동작을 지원하는 핵심 구성 요소가 있습니다. DPDK가 NIC를 직접 제어하려면 커널이 디바이스를 유저 공간에 노출하고, DMA를 격리하고, 대용량 페이지를 제공해야 합니다. 여기서는 각 커널 서브시스템이 DPDK 동작에 어떻게 기여하는지 소스 수준에서 분석합니다.

커널 경로역할DPDK 관련성
drivers/vfio/VFIO 프레임워크PCIe 디바이스를 유저 공간에 안전하게 노출
drivers/uio/UIO 프레임워크레거시 디바이스 접근 (IOMMU 없는 환경)
net/xdp/AF_XDP 소켓커널 기반 제로카피 패킷 전달
mm/hugetlb.cHugepage 관리DPDK 메모리 관리의 기반
drivers/iommu/IOMMU (VT-d/AMD-Vi)VFIO DMA 격리, IOVA 매핑
kernel/irq/IRQ 관리MSI/MSI-X eventfd (VFIO 인터럽트)
drivers/net/NIC 커널 드라이버AF_XDP PMD가 커널 드라이버의 XDP 지원에 의존

VFIO 커널 서브시스템 상세

VFIO(Virtual Function I/O)는 DPDK가 NIC를 유저 공간에서 안전하게 제어하는 핵심 메커니즘입니다. IOMMU를 이용하여 DMA 격리를 보장하면서 디바이스의 PCI BAR, 인터럽트, DMA 매핑을 유저 공간에 노출합니다.

/* drivers/vfio/pci/vfio_pci_core.c — PCI BAR 영역을 유저 공간에 노출 */

static int
vfio_pci_core_mmap(struct vfio_device *core_vdev,
                    struct vm_area_struct *vma)
{
    struct vfio_pci_core_device *vdev =
        container_of(core_vdev, struct vfio_pci_core_device, vdev);
    unsigned int index;

    index = vma->vm_pgoff >> (VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT);

    /* BAR 인덱스 유효성 검증 */
    if (index >= VFIO_PCI_NUM_REGIONS ||
        !(vdev->bar_mmap_supported & (1 << index)))
        return -EINVAL;

    /* Write-Combining 또는 Uncacheable 매핑 설정 */
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

    /* PCI BAR 물리 주소를 유저 공간 VMA에 매핑 */
    return remap_pfn_range(vma, vma->vm_start,
                            vdev->bar_mmap[index].pfn,
                            vma->vm_end - vma->vm_start,
                            vma->vm_page_prot);
}

코드 분석: VFIO PCI BAR 매핑

  • vfio_pci_core_mmap(): DPDK가 mmap() 시스템 콜로 NIC의 PCI BAR 영역을 유저 공간에 매핑할 때 호출됩니다. PMD는 이 매핑을 통해 NIC의 하드웨어 레지스터(Descriptor Ring Head/Tail, 통계 등)에 직접 접근합니다.
  • vm_pgoff → index: VFIO는 BAR 번호를 VMA offset 상위 비트로 인코딩합니다. BAR0~BAR5까지 6개 영역을 각각 별도로 매핑할 수 있습니다.
  • pgprot_noncached(): PCI BAR는 메모리 매핑 I/O(MMIO)이므로 CPU 캐시를 우회하는 uncacheable 매핑을 사용합니다. TX tail 레지스터 쓰기 시 Write-Combining이 활성화되면 PCIe 트랜잭션을 합쳐 성능이 향상됩니다.
  • remap_pfn_range(): 물리 PCI BAR 주소를 유저 공간 가상 주소에 매핑하는 커널 함수입니다. 이후 DPDK PMD는 이 주소에 직접 읽기/쓰기하여 NIC를 제어합니다.
/* drivers/vfio/vfio_iommu_type1.c — DMA 매핑 (IOVA → 물리 주소) */

static int
vfio_dma_do_map(struct vfio_iommu *iommu,
                struct vfio_iommu_type1_dma_map *map)
{
    dma_addr_t iova = map->iova;
    unsigned long vaddr = map->vaddr;
    size_t size = map->size;
    int prot = 0;

    if (map->flags & VFIO_DMA_MAP_FLAG_READ)
        prot |= IOMMU_READ;
    if (map->flags & VFIO_DMA_MAP_FLAG_WRITE)
        prot |= IOMMU_WRITE;

    /* 유저 공간 hugepage의 물리 주소를 pin하고
     * IOMMU 페이지 테이블에 IOVA → PA 매핑 설치 */
    return vfio_pin_map_dma(iommu, iova, vaddr, size, prot);
}

코드 분석: VFIO DMA 매핑

  • vfio_dma_do_map(): DPDK EAL이 ioctl(VFIO_IOMMU_MAP_DMA)를 호출하면 실행됩니다. DPDK의 hugepage 메모리를 NIC가 DMA로 접근할 수 있도록 IOMMU 페이지 테이블에 매핑을 설치합니다.
  • IOVA(I/O Virtual Address): NIC가 DMA 시 사용하는 주소입니다. DPDK의 IOVA VA 모드에서는 유저 공간 가상 주소와 동일한 값을 IOVA로 사용하고, PA 모드에서는 물리 주소를 사용합니다.
  • vfio_pin_map_dma(): (1) 유저 공간 가상 주소에 해당하는 물리 페이지를 pin(고정)하여 스왑아웃을 방지하고, (2) IOMMU 하드웨어에 IOVA→물리 주소 매핑을 설치합니다. 이 매핑이 있어야 NIC의 DMA가 올바른 메모리에 도달합니다.
  • prot 플래그: IOMMU_READ는 NIC가 메모리를 읽을 수 있음(TX 버퍼), IOMMU_WRITE는 NIC가 메모리에 쓸 수 있음(RX 버퍼)을 의미합니다. IOMMU는 이 권한을 하드웨어 수준에서 강제하여 DMA 격리를 보장합니다.

UIO 커널 서브시스템

UIO(Userspace I/O)는 VFIO 이전의 레거시 디바이스 접근 방식입니다. IOMMU 보호 없이 디바이스를 유저 공간에 노출하므로 보안상 VFIO를 우선 사용해야 합니다.

/* drivers/uio/uio_pci_generic.c — UIO PCI 디바이스 등록 */

static struct pci_driver uio_pci_driver = {
    .name     = "uio_pci_generic",
    .id_table = NULL,  /* 모든 PCI 디바이스에 바인딩 가능 */
    .probe    = uio_pci_generic_probe,
    .remove   = uio_pci_generic_remove,
};

static int
uio_pci_generic_probe(struct pci_dev *pdev,
                       const struct pci_device_id *id)
{
    struct uio_pci_generic_dev *gdev;

    /* 디바이스가 INTx 인터럽트를 지원하는지 확인 */
    if (!pdev->irq)
        return -ENODEV;

    gdev = devm_kzalloc(&pdev->dev, sizeof(*gdev), GFP_KERNEL);
    gdev->info.name = "uio_pci_generic";
    gdev->info.version = DRIVER_VERSION;
    gdev->info.irqcontrol = uio_pci_generic_irqcontrol;
    gdev->info.irq = pdev->irq;

    /* PCI BAR 영역을 UIO 리소스로 등록 →
     * 유저 공간에서 /dev/uioX를 mmap()하면 BAR에 접근 가능 */
    return devm_uio_register_device(&pdev->dev, &gdev->info);
}

코드 분석: UIO vs VFIO 핵심 차이

  • id_table = NULL: UIO PCI generic 드라이버는 특정 벤더/디바이스 ID 없이 모든 PCI 디바이스에 바인딩할 수 있습니다. dpdk-devbind.py로 NIC를 uio_pci_generic에 바인딩하면 이 probe 함수가 호출됩니다.
  • IOMMU 부재: UIO는 IOMMU 통합이 없으므로, NIC이 DMA로 시스템 메모리 전체에 접근할 수 있습니다. 악의적/결함 NIC이 다른 프로세스의 메모리를 읽거나 덮어쓸 수 있어 프로덕션 환경에서는 VFIO가 필수입니다.
  • 인터럽트: UIO는 INTx 레벨 인터럽트만 지원합니다. VFIO는 MSI/MSI-X를 eventfd로 연결하여 효율적인 인터럽트 처리가 가능합니다. DPDK는 PMD 폴링이 주 경로이지만, 링크 상태 변경 등 관리 인터럽트에는 eventfd가 필요합니다.

AF_XDP 커널 소켓 구현

AF_XDP는 커널 드라이버를 유지하면서 XDP 경로로 패킷을 유저 공간에 제로카피 전달하는 메커니즘입니다. DPDK AF_XDP PMD는 이 커널 소켓을 활용합니다.

/* net/xdp/xsk.c — AF_XDP 소켓의 패킷 수신 핵심 경로 */

int xsk_rcv(struct xdp_sock *xs,
            struct xdp_buff *xdp)
{
    u32 len = xdp->data_end - xdp->data;
    u64 addr;
    int err;

    /* UMEM fill ring에서 빈 프레임 주소를 꺼냄 */
    err = xsk_rcv_peek(xs, &addr);
    if (err)
        return err;

    if (xs->umem->flags & XDP_UMEM_FLAG_UNALIGNED_CHUNKS) {
        /* 제로카피 모드: NIC DMA가 UMEM에 직접 기록 */
        xsk_rcv_zc(xs, xdp, addr, len);
    } else {
        /* 복사 모드: 커널 XDP 버퍼 → UMEM 프레임으로 memcpy */
        memcpy(xsk_buff_raw_get_data(xs->umem, addr),
               xdp->data, len);
    }

    /* RX ring에 완료된 프레임 정보를 넣어 유저 공간에 알림 */
    xsk_rcv_commit(xs, addr, len);
    return 0;
}

코드 분석: AF_XDP 수신 경로

  • xsk_rcv(): NIC 드라이버의 XDP 프로그램이 XDP_REDIRECT로 패킷을 AF_XDP 소켓에 전달하면 호출됩니다. DPDK AF_XDP PMD의 rte_eth_rx_burst()는 이 경로의 유저 공간 측 대응 함수입니다.
  • UMEM: 유저 공간과 커널이 공유하는 메모리 영역으로, DPDK의 mempool/hugepage와 유사한 역할입니다. Fill Ring(유저→커널: 빈 프레임 제공)과 Completion Ring(커널→유저: 완료 알림)으로 구성됩니다.
  • 제로카피 vs 복사: 제로카피 모드에서는 NIC DMA가 UMEM에 직접 기록하므로 memcpy가 없습니다. 복사 모드에서는 커널 XDP 버퍼에서 UMEM으로 한 번 복사합니다. 제로카피는 NIC 드라이버가 AF_XDP 제로카피를 지원해야 합니다(i40e, ice, mlx5 등).
  • DPDK와의 관계: DPDK AF_XDP PMD는 이 소켓을 통해 패킷을 수신하므로, 커널 NIC 드라이버가 유지됩니다. ethtool, tc, 커널 라우팅 테이블 등을 함께 사용할 수 있어 순수 VFIO 바인딩보다 운영 유연성이 높습니다.

Hugepage와 DPDK 메모리 모델

/* mm/hugetlb.c — Hugepage 예약과 할당 핵심 */

long hugetlb_reserve_pages(struct inode *inode,
                            long from, long to,
                            struct vm_area_struct *vma,
                            vm_flags_t vm_flags)
{
    long chg, add = -1;
    struct hstate *h = hstate_inode(inode);

    /* 요청된 범위만큼 hugepage 예약 수 증가 */
    chg = region_chg(&inode->i_mapping->private_list,
                      from, to, NULL);

    /* NUMA 정책에 따라 특정 노드에서 hugepage 확보 */
    if (hugetlb_acct_memory(h, chg))
        return -ENOMEM;

    /* 예약 성공: 이후 fault 시 실제 물리 페이지 할당 */
    region_add(&inode->i_mapping->private_list, from, to,
               NULL, NULL);
    return 0;
}

코드 분석: Hugepage 예약

  • hugetlb_reserve_pages(): DPDK EAL이 /dev/hugepages에 파일을 생성하고 mmap()하면, 커널은 이 함수로 hugepage를 예약합니다. DPDK는 이 hugepage 위에 mempool, ring, mbuf 등 모든 데이터 구조를 배치합니다.
  • hstate: hugepage 크기(2MB 또는 1GB)별 상태를 관리하는 구조체입니다. DPDK는 1GB hugepage를 권장합니다. 2MB hugepage는 TLB 엔트리를 더 많이 소비하고, 대규모 mempool에서 IOTLB 미스가 발생할 수 있습니다.
  • NUMA 노드 할당: hugetlb_acct_memory()는 NUMA 정책(mbind, set_mempolicy)에 따라 특정 노드에서 hugepage를 확보합니다. DPDK는 NIC가 연결된 NUMA 노드와 동일한 노드에서 hugepage를 할당하여 크로스-NUMA 접근 지연(40~100ns)을 방지합니다.
  • 핀닝(Pinning): hugepage는 스왑아웃되지 않으므로 DPDK 메모리가 항상 물리 RAM에 상주합니다. NIC의 DMA가 물리 주소로 직접 접근하므로, 페이지가 스왑아웃되면 데이터 손실이 발생합니다.

커널 설정 항목별 해설

# DPDK 관련 커널 설정 (CONFIG_*)

# ━━━ VFIO (권장: IOMMU 기반 안전한 디바이스 패스스루) ━━━
CONFIG_VFIO=m                   # VFIO 코어 프레임워크
CONFIG_VFIO_PCI=m               # PCI 디바이스용 VFIO 드라이버
CONFIG_VFIO_IOMMU_TYPE1=m       # Type1 IOMMU 백엔드 (DMA 격리)
CONFIG_VFIO_NOIOMMU=y           # no-IOMMU 모드 (테스트용, 보안 위험)

# ━━━ UIO (레거시: IOMMU 없는 환경에서만 사용) ━━━
CONFIG_UIO=m                    # UIO 코어 프레임워크
CONFIG_UIO_PCI_GENERIC=m        # 범용 PCI UIO 드라이버

# ━━━ IOMMU (VFIO의 DMA 격리를 제공하는 핵심) ━━━
CONFIG_IOMMU_SUPPORT=y          # IOMMU 프레임워크 활성화
CONFIG_INTEL_IOMMU=y            # Intel VT-d (Xeon/Core 서버)
CONFIG_AMD_IOMMU=y              # AMD-Vi (EPYC/Ryzen 서버)
CONFIG_IOMMU_DEFAULT_DMA_LAZY=y # IOVA 지연 해제 (TLB 무효화 배치 처리로 성능 향상)

# ━━━ Hugepage (DPDK 메모리 관리의 기반) ━━━
CONFIG_HUGETLBFS=y              # /dev/hugepages 파일시스템 지원
CONFIG_HUGETLB_PAGE=y           # 2MB/1GB hugepage 지원
CONFIG_TRANSPARENT_HUGEPAGE=y   # THP — DPDK는 명시적 hugetlbfs 선호
                                # (THP는 compaction 오버헤드 + 예측 불가)

# ━━━ AF_XDP (커널 드라이버 유지 + XDP 경로 고속 처리) ━━━
CONFIG_XDP_SOCKETS=y            # AF_XDP 소켓 지원
CONFIG_BPF_SYSCALL=y            # BPF 시스템 콜 (XDP 프로그램 로딩)
CONFIG_NET_CLS_BPF=m            # tc BPF classifier (선택)

# ━━━ NUMA (멀티소켓 서버 필수) ━━━
CONFIG_NUMA=y                   # NUMA 토폴로지 인식
CONFIG_NUMA_BALANCING=y         # 자동 NUMA 밸런싱 (커널 용도)
                                # DPDK는 자체 NUMA 바인딩 사용

# ━━━ 부트 커맨드라인 (GRUB) ━━━
# intel_iommu=on iommu=pt     ← VFIO 사용 시 필수
#   iommu=pt: pass-through 모드 (커널 DMA는 IOMMU 미적용,
#             VFIO가 관리하는 디바이스만 IOMMU 적용 → 성능 향상)
# hugepagesz=1G hugepages=8   ← 부팅 시 1GB hugepage 8개 예약
# default_hugepagesz=1G       ← 기본 hugepage 크기를 1GB로 설정
VFIO_NOIOMMU 경고: CONFIG_VFIO_NOIOMMU=y는 IOMMU 없는 환경에서 VFIO API를 사용할 수 있게 하지만, DMA 격리가 없으므로 NIC이 시스템 메모리 전체에 접근할 수 있습니다. 프로덕션 환경에서는 반드시 IOMMU가 활성화된 상태에서 vfio-pci를 사용해야 합니다.

DPDK 빌드 시스템(Build System)과 버전 변천

DPDK는 22.11 LTS 이후 Meson + Ninja를 유일한 공식 빌드 시스템으로 사용합니다. 이전의 make 기반 빌드는 21.11에서 완전히 제거되었습니다. ABI 안정성 정책은 LTS 릴리스 간에 유지되며, 연 1회 LTS와 분기별 안정 릴리스로 운영됩니다.

# ━━━ DPDK Meson 빌드 (권장) ━━━

# 의존성 설치 (Debian/Ubuntu)
apt install build-essential meson ninja-build python3-pyelftools \
    libnuma-dev libpcap-dev pkg-config

# 소스 다운로드 및 빌드
tar xf dpdk-24.11.tar.xz && cd dpdk-24.11
meson setup build
# 또는 옵션 지정:
meson setup build \
  -Dplatform=native \
  -Dexamples=l2fwd,l3fwd,testpmd \
  -Dmax_ethports=32 \
  -Ddisable_drivers=net/bnxt,net/enic \
  -Ddefault_library=shared
ninja -C build
ninja -C build install
ldconfig

# pkg-config로 DPDK 연동 확인
pkg-config --cflags --libs libdpdk

# ━━━ 애플리케이션 빌드 (Meson 연동) ━━━
# meson.build 예시:
# project('my_app', 'c')
# dpdk_dep = dependency('libdpdk')
# executable('my_app', 'main.c', dependencies: dpdk_dep)
Meson 옵션기본값설명
-Dplatformnativenative(현재 CPU 최적화), generic(이식성), x86-64-v3(AVX2) 등
-Dmax_ethports32최대 이더넷 포트 수 (구조체 크기에 영향)
-Dmax_lcores128최대 논리 코어 수
-Ddefault_librarysharedshared/static — 정적 링크는 단일 바이너리 배포에 유리
-Ddisable_drivers없음불필요 PMD 제외 → 빌드 시간·바이너리 크기 감소
-Denable_kmodsfalseigb_uio 등 커널 모듈 빌드 (deprecated)
-Dteststrue유닛 테스트 빌드 (CI 환경)

코어 빌드 옵션 상세

Meson 빌드 시스템에서 -D옵션=값 형태로 지정하는 핵심 빌드 옵션의 전체 목록입니다. meson configure build 명령으로 현재 설정을 확인할 수 있습니다.

옵션기본값허용 값설명
-Dplatformnativenative, generic, x86-64-v2~v4CPU 최적화 수준. native는 빌드 호스트 CPU에 맞춰 최적화
-DmachineautoGCC -marchplatformnative일 때 세부 아키텍처 지정 (예: icelake-server)
-Dcpu_instruction_setautoauto, generic, nativeARM 전용: NEON/SVE/SVE2 명령어 세트 선택
-Dbuildtypereleaserelease, debug, debugoptimized, plainMeson 내장 옵션. debug-O0 -g, release-O3
-Doptimization30~3, sbuildtype 설정을 재정의하는 세부 최적화 수준
-Ddebugtruetrue/false디버그 심볼 포함 여부 (-g 플래그)
-Dwerrorfalsetrue/false경고를 에러로 처리 (-Werror). CI에서 권장
-Ddeveloper_modeautoauto, enabled, disabledGit 트리에서 자동 활성화. 추가 경고·werror·디버그 체크 포함
-Dcheck_includesfalsetrue/false각 공개 헤더의 독립 컴파일 가능성 검증
-Db_ltofalsetrue/falseLTO (Link-Time Optimization). 바이너리 크기 감소·성능 향상
-Db_pgooffoff, generate, usePGO (Profile-Guided Optimization). 2단계 빌드 필요
-Dprefix/usr/local경로설치 경로 접두사
-Dlibdirlib경로라이브러리 설치 디렉터리 (일부 배포판은 lib64)
platform vs machine 우선순위(Priority): platformnative 또는 generic이면 DPDK가 자동으로 적절한 -march 값을 선택합니다. machine을 명시하면 platform의 자동 감지를 재정의합니다. 크로스 컴파일(Cross Compilation) 시에는 machine이 반드시 필요합니다.

드라이버·라이브러리 선택 옵션

DPDK는 200개 이상의 PMD(Poll Mode Driver)를 포함하며, 불필요한 드라이버를 제외하면 빌드 시간과 바이너리 크기를 크게 줄일 수 있습니다.

옵션기본값설명
-Ddisable_drivers없음제외할 드라이버 (쉼표 구분). 와일드카드 지원: net/i*
-Denable_drivers없음 (전체)포함할 드라이버만 지정 (화이트리스트 방식)
-Ddisable_libs없음제외할 라이브러리 (쉼표 구분). 의존성 있는 드라이버도 함께 제외
-Denable_kmodsfalseigb_uio 등 커널 모듈 빌드. DPDK 20.11+에서 deprecated
-Dkernel_dir자동 감지커널 헤더 경로 (kmod 빌드 또는 KNI에 필요)
-Ddisable_apps없음제외할 애플리케이션 (예: test, proc-info)
-Denable_apps없음 (전체)포함할 애플리케이션만 지정
# ━━━ 최소 드라이버 빌드 (mlx5 + virtio만) ━━━
meson setup build \
  -Denable_drivers=net/mlx5,net/virtio,common/mlx5 \
  -Ddisable_libs=pipeline,table,port,fib,rib,reorder \
  -Ddisable_apps=test \
  -Dplatform=generic \
  -Ddefault_library=static \
  -Db_lto=true
enable + disable 동시 사용 규칙: enable_driversdisable_drivers를 동시에 지정하면 enable_drivers가 우선합니다. enable_drivers에 나열된 드라이버만 빌드 후보가 되고, 그중 disable_drivers에 해당하는 것이 제외됩니다. enable_drivers만 사용하는 것이 직관적입니다.

크기 제한 및 리소스 상수

DPDK의 내부 자료 구조 크기를 컴파일 타임에 결정하는 상수입니다. 기본값은 범용 서버를 기준으로 설정되어 있으며, 임베디드 환경이나 대규모 NIC 환경에서는 조정이 필요합니다.

옵션 / 매크로(Macro)기본값영향
-Dmax_ethports32최대 이더넷 포트 수. 포트별 배열 크기 결정
-Dmax_lcores128최대 논리 코어 수. RTE_MAX_LCORE에 매핑
-Dmax_numa_nodes4최대 NUMA 노드 수. 메모리 풀 분할에 영향
RTE_MAX_ETHPORTSmax_ethportsrte_ethdev 배열 크기. SmartNIC VF가 많으면 증가 필요
RTE_MAX_LCOREmax_lcoresper-lcore 변수 배열 크기. 대형 NUMA 서버는 256+ 필요
RTE_MAX_MEMSEG_LISTS64hugepage 메모리 세그먼트 목록 수. 다중 hugepage 크기 시 증가
RTE_MAX_MEM_MB524288최대 hugepage 메모리 (MB). 512GB 기본, TB급 서버는 증가 필요
RTE_MAX_QUEUES_PER_PORT1024포트당 최대 큐 수. RSS 해시 분산 범위에 영향
값 조정 가이드: 상수를 과도하게 높이면 per-lcore 변수와 포트별 배열이 커져 캐시 효율이 저하됩니다. 실제 사용하는 포트·코어 수의 2배 정도로 설정하는 것이 적절합니다. RTE_MAX_MEM_MBrte_eal_init() 시 실제 hugepage 할당량과는 무관한 상한값입니다.

기능 토글 옵션

빌드에 포함할 기능을 세밀하게 제어하는 옵션입니다.

옵션기본값설명
-Dteststrue유닛 테스트 빌드. CI에서는 true, 프로덕션 빌드에서는 false
-Dexamples빈 문자열빌드할 예제 (쉼표 구분). all로 전체 빌드 가능
-Denable_apps전체빌드할 앱 선택 (test-pmd, proc-info, pdump 등)
-Denable_docsfalseAPI 문서(Doxygen) 및 가이드(Sphinx) 빌드
-Denable_trace_fpfalsefast-path trace point 활성화. 성능 오버헤드 미미
-Dmbuf_refcnt_atomictruembuf 참조 카운트(Reference Count) 원자적 연산(Atomic Operation). 단일 스레드면 false로 성능 향상
-Dper_library_versionstrue라이브러리별 개별 SO 버전 관리. false면 단일 ABI 버전
-Denable_driver_sdkfalse드라이버 개발용 내부 헤더 설치. out-of-tree PMD 개발 시 필요
-Dlog_default_levelinfo기본 로그 수준: emergency~debug (8단계)
-Duse_hpetfalseHPET 타이머 사용. TSC가 정확한 현대 CPU에서는 불필요

크로스 컴파일 설정

DPDK는 Meson의 크로스 컴파일 파일(cross-file)을 사용하여 ARM, RISC-V 등 다양한 아키텍처용 빌드를 지원합니다.

x86_64 호스트 크로스 툴체인 cross-file Meson + Ninja 빌드 수행 install ARM64 타겟 libdpdk + PMD 배포 DPU BlueField
크로스파일 항목값 예시설명
[binaries] caarch64-linux-gnu-gcc크로스 C 컴파일러 경로
[binaries] araarch64-linux-gnu-ar크로스 아카이버
[binaries] stripaarch64-linux-gnu-strip심볼 제거 도구
[binaries] pkgconfigaarch64-linux-gnu-pkg-config크로스 pkg-config
[host_machine] cpu_familyaarch64타겟 CPU 패밀리
# ━━━ ARM64 크로스 컴파일 파일 (aarch64_cross.txt) ━━━

[binaries]
c = 'aarch64-linux-gnu-gcc'
cpp = 'aarch64-linux-gnu-g++'
ar = 'aarch64-linux-gnu-ar'
strip = 'aarch64-linux-gnu-strip'
pkgconfig = 'aarch64-linux-gnu-pkg-config'

[host_machine]
system = 'linux'
cpu_family = 'aarch64'
cpu = 'armv8-a'
endian = 'little'

[properties]
platform = 'generic'
cpu_instruction_set = 'generic'
# ━━━ 크로스 컴파일 빌드 명령 ━━━

# ARM64 크로스 컴파일 (BlueField DPU 등)
meson setup build-arm64 \
  --cross-file config/arm/arm64_bluefield_platform \
  -Dplatform=generic \
  -Denable_drivers=net/mlx5,common/mlx5,regex/mlx5,vdpa/mlx5 \
  -Ddefault_library=static

ninja -C build-arm64
DESTDIR=/opt/dpdk-arm64 ninja -C build-arm64 install

# RISC-V 크로스 컴파일 (실험적, DPDK 23.11+)
meson setup build-riscv \
  --cross-file config/riscv/riscv64_linux_gcc \
  -Dplatform=generic

ninja -C build-riscv
의존성 크로스 설치: 크로스 컴파일 시 libnuma, libpcap 등의 타겟 아키텍처용 라이브러리가 필요합니다. Debian 멀티아치 환경에서는 apt install libnuma-dev:arm64 libpcap-dev:arm64로 설치합니다. sysroot를 별도로 구성할 경우 [properties] 섹션에 sys_root를 지정합니다.

make → Meson 전환 매핑

DPDK 21.11에서 make 빌드가 제거되었습니다. 기존 CONFIG_RTE_* 환경 변수와 대응하는 Meson 옵션 매핑입니다.

make (CONFIG_RTE_*)Meson (-D)비고
CONFIG_RTE_MACHINE-DmachineGCC -march
CONFIG_RTE_ARCH자동 감지Meson이 호스트/크로스파일에서 결정
CONFIG_RTE_MAX_ETHPORTS-Dmax_ethports동일 기능
CONFIG_RTE_MAX_LCORE-Dmax_lcores동일 기능
CONFIG_RTE_MAX_NUMA_NODES-Dmax_numa_nodes동일 기능
CONFIG_RTE_LIBRTE_PMD_*-Denable_drivers / -Ddisable_drivers개별 옵션 → 통합 리스트
CONFIG_RTE_BUILD_SHARED_LIB-Ddefault_library=sharedMeson 내장 옵션
CONFIG_RTE_EAL_IGB_UIO-Denable_kmodsdeprecated
CONFIG_RTE_LIBRTE_VHOST자동 (libc 감지)vhost-user 빌드 조건 자동 판단
CONFIG_RTE_APP_TEST-Dtests테스트 빌드 토글
CONFIG_RTE_BUILD_EXAMPLES-Dexamples쉼표 구분 리스트 또는 all
CONFIG_RTE_EXEC_ENV_LINUX자동 감지실행 환경(Linux/FreeBSD) 자동 판단
CONFIG_RTE_ENABLE_STDATOMIC-Denable_stdatomicDPDK 23.11+: C11 stdatomic 사용
CONFIG_RTE_USE_HPET-Duse_hpetHPET 타이머
T= (target)--cross-file크로스 컴파일 대상 지정
O= (output dir)meson setup <dir>빌드 디렉터리 지정
EXTRA_CFLAGS-Dc_args추가 C 플래그 (Meson 내장)
자동 변환 도구 없음: DPDK 공식 프로젝트에서는 make → Meson 자동 변환 스크립트를 제공하지 않습니다. .config 파일의 CONFIG_RTE_* 항목을 수동으로 위 매핑표에 따라 -D 옵션으로 변환해야 합니다. meson_options.txt 파일에서 현재 버전의 모든 옵션을 확인할 수 있습니다.

실전 빌드 레시피

환경별 최적화된 빌드 명령 모음입니다.

# ━━━ 1. 최소 CI 빌드 (빠른 검증) ━━━
meson setup build-ci \
  -Dbuildtype=debugoptimized \
  -Dwerror=true \
  -Dtests=true \
  -Dexamples=l2fwd,l3fwd \
  -Ddisable_drivers=raw/*,crypto/*,compress/*,regex/*,ml/* \
  -Dcheck_includes=true
ninja -C build-ci
meson test -C build-ci --suite fast-tests
# ━━━ 2. 프로덕션 mlx5 빌드 (최대 성능) ━━━
meson setup build-prod \
  -Dbuildtype=release \
  -Dplatform=native \
  -Denable_drivers=net/mlx5,common/mlx5,regex/mlx5 \
  -Ddefault_library=static \
  -Db_lto=true \
  -Dmax_ethports=16 \
  -Dmax_lcores=64 \
  -Dtests=false \
  -Denable_trace_fp=true \
  -Dmbuf_refcnt_atomic=false \
  -Dc_args='-march=icelake-server -mtune=icelake-server'
ninja -C build-prod
sudo ninja -C build-prod install && sudo ldconfig
# ━━━ 3. 디버그 빌드 (문제 추적) ━━━
meson setup build-debug \
  -Dbuildtype=debug \
  -Doptimization=0 \
  -Ddebug=true \
  -Dtests=true \
  -Dexamples=all \
  -Dlog_default_level=debug \
  -Denable_trace_fp=true \
  -Dc_args='-fsanitize=address,undefined' \
  -Dc_link_args='-fsanitize=address,undefined'
ninja -C build-debug
# ━━━ 4. 문서 빌드 (Doxygen + Sphinx) ━━━
apt install doxygen python3-sphinx python3-sphinx-rtd-theme
meson setup build-doc \
  -Denable_docs=true \
  -Dtests=false \
  -Dexamples=
ninja -C build-doc doc
# ━━━ 5. ARM64 DPU 빌드 (NVIDIA BlueField-3) ━━━
meson setup build-bf3 \
  --cross-file config/arm/arm64_bluefield_platform \
  -Dplatform=generic \
  -Denable_drivers=net/mlx5,common/mlx5,regex/mlx5,vdpa/mlx5,compress/mlx5 \
  -Ddefault_library=static \
  -Db_lto=true \
  -Dmax_ethports=8 \
  -Dtests=false \
  -Dprefix=/opt/dpdk
ninja -C build-bf3
DESTDIR=/mnt/bf3-rootfs ninja -C build-bf3 install

DPDK 주요 버전 변천

버전연도주요 변경
1.x~2.x2013~2015Intel 초기 릴리스, make 빌드, ixgbe/i40e PMD, 기본 mbuf/ring/mempool
16.04~16.112016연월 버전 체계 도입, rte_flow API 초안, Eventdev 프레임워크
17.11 LTS2017첫 LTS, Cryptodev 성숙, Flow API 정식화, AF_XDP 실험
18.11 LTS2018ABI 안정성 정책 도입, Meson 빌드 추가, mlx5 bifurcated PMD
19.11 LTS2019AF_XDP PMD 정식, Graph 라이브러리, Telemetry v2, RCU 라이브러리
20.11 LTS2020API 대정비 (rte_eth → rte_ethdev), dmadev 초안, IOAT 드라이버
21.11 LTS2021make 빌드 제거, Meson 전용, dmadev 정식, GPU 디바이스, Trace 라이브러리
22.11 LTS2022ABI 23.x 시리즈, Graph dispatch 모델, flow aging, CT offload 강화
23.11 LTS2023vhost DMA offload, rte_flow async, ethdev 텔레메트리 확장
24.11 LTS2024ABI 25.x 시리즈, ARM SVE2 최적화, GPUdev DMA, power intrinsics 강화

버전별 옵션 변경 이력

DPDK 버전 업그레이드 시 빌드 스크립트를 수정해야 하는 옵션 변경 사항입니다.

버전추가된 옵션변경된 옵션제거/Deprecated
18.11Meson 빌드 도입 (make과 병행)
19.11enable_trace_fpmax_lcores 기본값 128→128 유지
20.11disable_apps, enable_appsdisable_drivers 와일드카드 지원enable_kmods deprecated
21.11platform (기존 machine 대체)machineplatform 하위로 이동make 빌드 완전 제거
22.11enable_driver_sdk, log_default_levelmax_ethports 기본값 32 유지KNI 라이브러리 deprecated
23.11enable_stdatomic, RISC-V 크로스파일cpu_instruction_set ARM 확장igb_uio 소스 제거
24.03x86-64-v2~v4 platform 값developer_mode 동작 세분화
24.11ARM SVE2 cpu_instruction_setmax_numa_nodes 기본값 4→4 유지use_hpet deprecated 예고
릴리스 노트 diff 확인법: 버전 간 옵션 변경을 정확히 파악하려면 diff <(cd dpdk-23.11 && meson configure build) <(cd dpdk-24.11 && meson configure build) 명령 또는 meson_options.txt 파일의 Git diff를 확인합니다. 릴리스 노트의 "Build System" 섹션도 참고하세요.
LTS 정책: DPDK LTS는 출시 후 최소 2년간 백포트 패치가 유지됩니다. OVS-DPDK, VPP 등 상위 프로젝트는 특정 DPDK LTS 버전에만 호환되므로, 버전 조합을 먼저 고정한 뒤 빌드해야 합니다.

타 바이패스 프레임워크 비교

DPDK는 유일한 커널 바이패스 기술이 아닙니다. 각 프레임워크는 서로 다른 설계 철학과 트레이드오프를 가집니다.

커널 바이패스 프레임워크 스펙트럼 커널 통합 ← → 완전 바이패스 XDP 커널 내 eBPF 훅 커널 기능 유지 AF_XDP 커널↔유저 공유 부분 바이패스 io_uring 커널 비동기 I/O 네트워크는 실험적 Netmap 커널 모듈 + 유저 공유 링 버퍼 DPDK 완전 유저 공간 NIC 전용 점유 XDP: 커널 내에서 패킷을 가장 먼저 처리, eBPF 프로그램, tc/iptables 공존 가능 AF_XDP: XDP에서 유저 공간으로 제로카피 전달, DPDK PMD 백엔드로도 사용, NIC 공유 가능 io_uring: 범용 비동기 I/O, 네트워크 제로카피는 6.x 커널에서 실험 중, 패킷 처리보다 I/O 멀티플렉싱에 강점 Netmap: FreeBSD 출신, 커널 모듈로 NIC 링을 유저 공간에 매핑, Linux에서는 주류가 아님 DPDK: 완전 유저 공간 제어, PMD 에코시스템 최대, 커널 기능 사용 불가, 가장 높은 처리량 선택 기준: 커널 기능 필요성, NIC 공유 여부, 에코시스템 성숙도, 운영 복잡성 허용 범위
항목DPDKXDP (eBPF)AF_XDPNetmapio_uring (net)
실행 위치유저 공간커널 (NIC 드라이버 훅)유저 공간 (커널 XDP 경유)유저 공간 (커널 모듈)커널 (비동기 syscall)
NIC 점유전용 (VFIO/UIO)공유 (커널 드라이버)큐별 분리 가능전용/공유 혼합공유 (커널 스택)
처리량 (64B)~14.88 Mpps/core~24 Mpps (XDP_TX)~10 Mpps/core~12 Mpps/core아직 미성숙
지연수백 ns수백 ns~1 μs~1 μs수 μs
커널 기능사용 불가모두 가능부분 가능제한적모두 가능
프로토콜 스택없음 (VPP 등 별도)커널 TCP/IP없음/커널 혼합없음커널 TCP/IP
NIC 지원 범위최대 (100+ PMD)드라이버 XDP 지원 필요AF_XDP 지원 NIC제한적모든 NIC
에코시스템OVS, VPP, SPDK, 상업 앱Cilium, Katran, bpfilterDPDK AF_XDP PMD학술/소규모범용 서버
학습 곡선높음중간 (eBPF)중간~높음중간낮음
적합 분야NFV, 통신, 전용 어플라이언스DDoS 방어, 로드밸런서, 커널 내 필터링커널 기능 + 고성능 혼합연구/교육범용 서버 I/O 최적화
선택 가이드:
  • 커널 기능(iptables, tc, routing)이 필요하다면 → XDP 또는 AF_XDP
  • NIC을 다른 프로세스와 공유해야 하는 경우 → AF_XDP 또는 XDP
  • 최대 처리량, 최소 지연이 절대 우선이라면 → DPDK
  • 이미 VPP/OVS-DPDK 에코시스템을 사용 중이라면 → DPDK 유지
  • L7 애플리케이션(웹 서버, DB) 최적화라면 → io_uring

DPDK 구동 시 NVMe 성능 저하 분석

DPDK와 NVMe 드라이버는 직접 코드 경로를 공유하지 않지만, PCIe 버스·CPU 코어·메모리 계층·커널 인프라 수준에서 다수의 간접 간섭 경로가 존재합니다. DPDK 애플리케이션을 기동한 뒤 NVMe SSD의 IOPS·지연이 갑자기 악화되는 현상은 대부분 아래 17가지 원인 중 하나(또는 복합)에 해당합니다.

핵심 전제: DPDK PMD는 NIC을 유저 공간에서 전용 점유하므로, NIC과 NVMe가 동일 PCIe Root Complex / Switch를 통과하면 하드웨어 수준에서 경합이 발생합니다. 소프트웨어 프로파일링(Profiling)만으로는 원인을 찾기 어려운 경우가 많습니다.

1. PCIe 버스 대역폭 경합

DPDK 10 GbE 라인레이트(~14.88 Mpps, 64B 프레임)는 PCIe 3.0 x8 대역폭의 상당 부분을 소비합니다. NVMe가 같은 Root Complex 하위 Switch에 연결되면 TLP(Transaction Layer Packet) 수준에서 중재 경합이 발생합니다.

PCIe 토폴로지와 NIC/NVMe 경합 경로 CPU + Root Complex PCIe Switch A PCIe Switch B NIC (DPDK PMD) 25/100 GbE NVMe SSD 경합 발생! NVMe SSD 분리 — 권장 ⚡ TLP 경합 해결: NIC과 NVMe를 서로 다른 PCIe Switch (또는 CPU 직결 슬롯)에 배치 확인: lspci -tv 로 토폴로지 확인, perf stat -e uncore_iio 로 포트별 트래픽 측정
원인메커니즘증상확인 방법해결
업스트림 대역폭 포화 NIC과 NVMe가 동일 PCIe Switch 하위 → 업스트림 링크 대역폭을 공유 NVMe 대역폭 30-50% 하락, avgqu-sz 증가 lspci -tv로 토폴로지 확인
perf stat -e uncore_iio
NVMe를 별도 PCIe Switch / CPU 직결 슬롯으로 이동
TLP Credit 고갈 DPDK PMD가 DMA descriptor를 대량 발급 → Posted/Non-Posted Credit 소진 → NVMe Completion 지연 NVMe p99 지연 스파이크 (정상 대비 5~10×) setpci로 Link Status 확인
PCIe AER 로그
NIC tx_burst 크기 제한, PCIe MPS/MRRS 튜닝
PCIe Ordering Rule x86 PCIe는 Posted Write 순서 보장 필요 → NIC DMA와 NVMe DMA 간 Ordering Barrier 발생 단일 Switch에서만 NVMe 쓰기 지연 증가 분리 배치 후 지연 비교 A/B 테스트 Relaxed Ordering 활성화 (setpci DEVCTL2)
Relaxed Ordering: DPDK 20.11+는 mlx5/mlx4 PMD에서 Relaxed Ordering을 지원합니다. devargsmprq_en=1과 함께 설정하면 PCIe Ordering 부하를 줄일 수 있지만, 일부 플랫폼에서는 데이터 무결성(Integrity) 문제가 보고되었으므로 반드시 검증 후 적용하세요.

2. CPU 코어·IRQ Affinity 충돌

DPDK PMD는 전용 코어에서 100% CPU를 소비하는 폴링 루프를 실행합니다. NVMe 인터럽트(IRQ)가 같은 코어에 할당되면, IRQ 처리가 지연되어 NVMe I/O 완료 레이턴시가 급증합니다.

원인메커니즘증상확인 방법해결
DPDK 코어와 NVMe IRQ 동일 코어 PMD 폴링 루프가 코어를 100% 점유 → NVMe MSI-X IRQ의 softirq 처리 기아(starvation) NVMe await 급증, /proc/interrupts에서 특정 코어에 NVMe IRQ 편중 cat /proc/interrupts | grep nvme
DPDK --lcores 설정과 비교
NVMe IRQ를 DPDK 미사용 코어로 이동: echo <mask> > /proc/irq/<N>/smp_affinity
Hyper-Threading 간섭 DPDK가 물리 코어의 한 HT를 점유하면, 형제 HT에서 NVMe IRQ 처리 성능 40~60% 하락 HT 형제 코어에서 NVMe 지연 악화, LLC miss 증가 lscpu -e로 HT 쌍 확인
perf stat -C <core>
HT 형제를 동시에 DPDK에 할당하거나, NVMe IRQ를 물리적으로 다른 코어에 할당
irqbalance 충돌 irqbalance 데몬이 NVMe IRQ를 DPDK 코어로 재배치(Relocation) 운영 중 간헐적 NVMe 지연 스파이크 systemctl status irqbalance
IRQ affinity 변화 모니터링
IRQBALANCE_BANNED_CPUS 환경변수로 DPDK 코어 제외, 또는 irqbalance 중지 후 수동 설정
# NVMe IRQ를 DPDK 미사용 코어(예: 코어 8~15)로 제한
for irq in $(grep nvme /proc/interrupts | awk '{print $1}' | tr -d ':'); do
    echo ff00 > /proc/irq/$irq/smp_affinity    # 코어 8~15 마스크
done

# irqbalance에서 DPDK 코어(0~7) 제외
echo 'IRQBALANCE_BANNED_CPUS="00ff"' >> /etc/default/irqbalance
systemctl restart irqbalance

3. NUMA 토폴로지 불일치

NUMA 시스템에서 NVMe 컨트롤러가 NUMA 노드 0에 연결되었는데, 애플리케이션이 NUMA 노드 1에서 I/O를 발급하면 Cross-NUMA 메모리 접근으로 지연이 30~100% 증가합니다. DPDK가 hugepage를 특정 NUMA 노드에서 대량 할당하면, 반대 노드의 NVMe 드라이버가 사용할 로컬 메모리가 부족해집니다.

원인메커니즘증상확인 방법해결
Cross-NUMA DMA NVMe 컨트롤러와 NIC이 서로 다른 NUMA 노드 → DMA 완료 데이터가 리모트 메모리 통과 NVMe 대역폭 저하, numastat에서 other_node 증가 cat /sys/block/nvme*/device/numa_node
numastat -p <pid>
NIC과 NVMe를 같은 NUMA 노드에 배치, DPDK --socket-mem 조정
Hugepage NUMA 편향 DPDK가 한 노드에서 hugepage를 대량 소비 → 해당 노드의 NVMe DMA 버퍼 할당 실패/폴백 cat /sys/devices/system/node/node*/hugepages/*/free_hugepages에서 편향 numactl --hardware
hugepage 노드별 잔량 확인
노드별 hugepage 균등 할당: echo N > /sys/devices/system/node/nodeX/hugepages/.../nr_hugepages

4. Hugepage 메모리 경합

DPDK는 기동 시 수 GB~수십 GB의 1G/2M hugepage를 예약합니다. 이로 인해 시스템 전체의 가용 메모리가 감소하여 NVMe 드라이버와 파일시스템(Filesystem)에 영향을 줍니다.

원인메커니즘증상확인 방법해결
Page cache 축소 hugepage 예약이 일반 메모리를 압박 → page cache 크기 감소 → NVMe 버퍼 I/O 캐시 히트율 하락 free -m에서 buff/cache 급감, NVMe 읽기 IOPS 하락 free -m, vmstat 1bi/bo 모니터링 DPDK hugepage를 필요한 최소량으로 제한, --socket-mem 명시
THP compaction 스톰 THP(Transparent Hugepage) 활성 상태에서 DPDK hugepage 예약이 메모리 단편화(Fragmentation) 유발 → compaction이 NVMe I/O 경로에서 stall kcompactd CPU 사용 급증, NVMe 지연 간헐적 스파이크 grep compact /proc/vmstat
perf top에서 compact_zone_order
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
OOM 킬러 간접 효과 hugepage로 인한 메모리 부족 → OOM 킬러가 NVMe I/O를 발행하는 프로세스 종료 dmesg에 OOM 메시지, 워크로드 중단 dmesg | grep -i oom 시스템 총 메모리 대비 DPDK hugepage 비율 20% 이하 유지

5. IOMMU/VFIO 오버헤드

DPDK는 vfio-pci를 통해 NIC에 접근하며, 이때 IOMMU가 DMA 주소 변환을 수행합니다. NVMe도 IOMMU를 통과하므로, IOTLB 경합과 IOMMU 그룹 공유 문제가 발생할 수 있습니다.

원인메커니즘증상확인 방법해결
IOTLB miss/flush DPDK가 대량의 DMA 매핑을 생성 → IOTLB 엔트리 갱신 빈번 → NVMe DMA의 IOTLB miss 증가 IOMMU 통과 시 NVMe 지연 증가 (passthrough 대비 5~15%) perf stat -e dTLB-load-misses
dmesg | grep -i iommu
Intel: intel_iommu=on iommu=pt (pass-through 모드)
DPDK: --iova-mode=pa (가능한 경우)
IOMMU 그룹 공유 NIC과 NVMe가 같은 IOMMU 그룹 → VFIO가 그룹 전체를 잠금(Lock), NVMe 드라이버 바인딩 충돌 가능 NVMe 장치 인식 실패 또는 성능 저하 find /sys/kernel/iommu_groups/ -type l ACS(Access Control Services) 지원 스위치 사용, 물리적으로 분리된 슬롯 배치
IOMMU pass-through 모드: iommu=pt는 DPDK가 VFIO를 사용할 때 다른 장치(NVMe 포함)의 DMA를 pass-through로 처리하여 IOMMU 오버헤드를 제거합니다. 단, DMA 격리가 약화되므로 보안이 중요한 환경에서는 주의가 필요합니다.

6. LLC 캐시 오염 / 메모리 대역폭 포화

DPDK PMD는 대량의 패킷 버퍼를 연속적으로 접근하며 LLC(Last Level Cache)를 오염시킵니다. NVMe 드라이버의 Completion Queue 엔트리와 I/O 메타데이터가 캐시에서 밀려나면 성능이 저하됩니다.

CPU 리소스 간섭 계층 DPDK PMD 코어 L1/L2: 패킷 버퍼, 디스크립터 링 NVMe IRQ / IO 워커 코어 L1/L2: CQE, SQE, 페이지 캐시 LLC (Last Level Cache) — 공유 · 경합 DPDK mbuf prefetch가 NVMe CQE/메타데이터를 evict 메모리 대역폭 (DDR4/DDR5 채널) DMA 읽기/쓰기 + CPU 접근이 채널 대역폭 포화 해결: Intel CAT/MBA (RDT)로 LLC 파티셔닝, DPDK 코어를 별도 COS에 격리, pcm-memory.x로 대역폭 모니터링
원인메커니즘증상확인 방법해결
LLC 오염 DPDK mbuf 대량 prefetch → LLC way를 점유 → NVMe CQE·SQE 캐시 miss NVMe 지연 증가, perf stat에서 LLC-load-misses 증가 perf stat -e LLC-load-misses -C <nvme_core>
Intel RDT: pqos -m all:0-15
Intel CAT(Cache Allocation Technology): DPDK 코어의 LLC way 제한
pqos -e "llc:1=0x00f;llc:2=0xff0"
메모리 대역폭 포화 DPDK DMA + NVMe DMA + CPU 접근이 메모리 채널 대역폭을 초과 양쪽 모두 성능 저하, pcm-memory.x에서 채널 사용률 80%+ pcm-memory.x 또는 perf stat -e uncore_imc Intel MBA(Memory Bandwidth Allocation)로 DPDK 대역폭 상한 설정

7. 전원 관리(Power Management)·스케줄러·RCU 간섭

DPDK PMD의 busy-poll 루프는 커널의 전원 관리, 스케줄러, RCU 콜백(Callback) 처리와 충돌할 수 있습니다.

원인메커니즘증상확인 방법해결
C-state / P-state 전환 DPDK 미사용 코어가 deep C-state → NVMe IRQ 도착 시 웨이크업 지연 (수십 μs) NVMe p99 지연에 간헐적 수십 μs 스파이크 turbostat으로 C-state 분포 확인 NVMe 코어에 processor.max_cstate=1 또는
cpupower idle-set -D 0
커널 스케줄러 간섭 DPDK 코어가 isolcpus로 격리되지 않으면 커널 스레드(kworker 등)가 NVMe 처리를 방해 DPDK 코어에서 간헐적 패킷 드롭, NVMe 코어에서 스케줄링 지연 ps -eo pid,psr,comm | grep kworker 커널 부트 파라미터: isolcpus=0-7 nohz_full=0-7 rcu_nocbs=0-7
RCU 콜백 지연 DPDK 코어가 RCU grace period를 block → 커널 메모리 해제 지연 → NVMe 관련 slab 압박 /sys/kernel/debug/rcu/rcu_preempt/rcudata에서 콜백 누적 cat /sys/kernel/debug/rcu/*/rcudata rcu_nocbs=<dpdk_cores>로 RCU 콜백을 다른 코어에 위임
# DPDK 코어 0~7 격리 + RCU 오프로드 (GRUB 커널 파라미터)
GRUB_CMDLINE_LINUX="isolcpus=0-7 nohz_full=0-7 rcu_nocbs=0-7 \
    intel_idle.max_cstate=1 processor.max_cstate=1 \
    intel_iommu=on iommu=pt"

8. blk-mq 큐 매핑·MSI-X·Thermal

NVMe 드라이버의 blk-mq 큐 할당, MSI-X 벡터 분배, 열 관리(Thermal Management) 등도 DPDK와의 상호작용에서 영향을 받습니다.

원인메커니즘증상확인 방법해결
blk-mq 큐 매핑 왜곡 isolcpus로 코어를 격리하면 blk-mq가 NVMe 큐를 남은 코어에만 매핑 → 큐 불균형 특정 NVMe 큐에 I/O 집중, /sys/block/nvme0n1/mq/*/cpu_list 편향 cat /sys/block/nvme0n1/mq/*/cpu_list managed_irq 파라미터 활용, blk_mq_update_nr_hw_queues() 재조정
MSI-X 벡터 경합 NIC과 NVMe의 MSI-X 벡터가 플랫폼 한계에 도달 → 인터럽트 공유/코얼레싱 인터럽트 공유로 NVMe 지연 증가 cat /proc/interrupts에서 벡터 분포 확인 BIOS에서 MSI-X 벡터 풀 확대, 불필요한 장치 비활성화
Thermal throttling DPDK 코어의 100% 사용률 → CPU 패키지 온도 상승 → Thermal throttling이 NVMe 코어 클럭도 제한 turbostat에서 PkgWatt 급증, 코어 클럭 하락 turbostat --show PkgWatt,CoreTmp 적절한 냉각, DPDK power_pmd_mgmt API로 적응형 폴링
NVMe poll 모드 경합 커널 NVMe 드라이버가 io_poll 모드(io_uring) 사용 시, DPDK PMD와 CPU 사이클 경합 양쪽 모두 처리량 하락 cat /sys/block/nvme*/queue/io_poll NVMe poll 모드 비활성화 또는 전용 코어 분리
SPDK와 DPDK 동시 사용 SPDK(유저 공간 NVMe 드라이버)와 DPDK가 hugepage·코어·VFIO를 공유 → 리소스 경합 두 프레임워크 모두 성능 저하 SPDK + DPDK 동시 기동 여부 확인 hugepage·코어·IOMMU 그룹을 명시적으로 분리 할당

9. 종합 진단 체크리스트와 스크립트

아래 체크리스트를 순서대로 확인하면, DPDK–NVMe 간섭의 대부분을 빠르게 진단할 수 있습니다.

#점검 항목명령 / 확인 방법정상 기준
1PCIe 토폴로지 분리lspci -tvNIC과 NVMe가 다른 Switch 하위
2NUMA 노드 일치cat /sys/class/net/*/device/numa_node
cat /sys/block/nvme*/device/numa_node
NIC과 NVMe가 같은 NUMA 노드
3NVMe IRQ affinitycat /proc/irq/*/smp_affinity_list (nvme)DPDK 코어와 겹치지 않음
4HT 형제 분리lscpu -eDPDK 코어의 HT 형제에 NVMe IRQ 없음
5irqbalance 제외grep BANNED /etc/default/irqbalanceDPDK 코어가 BANNED 목록에 포함
6Hugepage 잔량cat /sys/devices/system/node/node*/hugepages/*/free_hugepages각 노드에 충분한 여유 hugepage
7일반 메모리 여유free -mbuff/cache가 총 메모리의 20%+
8THP 설정cat /sys/kernel/mm/transparent_hugepage/enabledmadvise 또는 never
9IOMMU 모드dmesg | grep -i iommuiommu=pt 활성
10IOMMU 그룹find /sys/kernel/iommu_groups/ -type lNIC과 NVMe가 다른 그룹
11LLC miss 비율perf stat -e LLC-load-misses -C <nvme_core>DPDK 기동 전후 차이 <10%
12메모리 대역폭pcm-memory.x 또는 perf stat -e uncore_imc채널 사용률 <70%
13C-stateturbostat --show Avg_MHz,Busy%,Bzy_MHz,PkgWattNVMe 코어가 deep C-state에 빠지지 않음
14Thermalturbostat --show CoreTmp,PkgTmp패키지 온도 <85°C
15blk-mq 큐 분포cat /sys/block/nvme0n1/mq/*/cpu_list큐가 NVMe NUMA 코어에 고르게 분포
16RCU 콜백cat /sys/kernel/debug/rcu/*/rcudataDPDK 코어에서 콜백 누적 없음
#!/bin/bash
# dpdk-nvme-diag.sh — DPDK/NVMe 간섭 종합 진단 스크립트
set -euo pipefail

echo "=== 1. PCIe 토폴로지 ==="
lspci -tv 2>/dev/null | head -40

echo -e "\n=== 2. NUMA 노드 확인 ==="
for dev in /sys/class/net/*/device/numa_node; do
    [ -f "$dev" ] && echo "$(dirname $(dirname $dev) | xargs basename): $(cat $dev)"
done
for dev in /sys/block/nvme*/device/numa_node; do
    [ -f "$dev" ] && echo "$(echo $dev | grep -o 'nvme[0-9]*'): $(cat $dev)"
done

echo -e "\n=== 3. NVMe IRQ Affinity ==="
for irq in $(grep nvme /proc/interrupts 2>/dev/null | awk '{print $1}' | tr -d ':'); do
    echo "IRQ $irq: $(cat /proc/irq/$irq/smp_affinity_list 2>/dev/null)"
done

echo -e "\n=== 4. Hugepage 상태 (노드별) ==="
for node in /sys/devices/system/node/node*; do
    for hp in "$node"/hugepages/hugepages-*; do
        [ -d "$hp" ] && echo "$(basename $node) $(basename $hp): \
free=$(cat $hp/free_hugepages)/total=$(cat $hp/nr_hugepages)"
    done
done

echo -e "\n=== 5. 메모리 상태 ==="
free -m

echo -e "\n=== 6. THP 설정 ==="
cat /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null

echo -e "\n=== 7. IOMMU 모드 ==="
dmesg 2>/dev/null | grep -i "iommu.*mode\|iommu.*pass" | tail -3

echo -e "\n=== 8. IOMMU 그룹 (NIC/NVMe) ==="
for dev in $(lspci -D | grep -iE 'nvme|ethernet' | awk '{print $1}'); do
    grp=$(find /sys/kernel/iommu_groups/ -name "$dev" 2>/dev/null | grep -o 'iommu_groups/[0-9]*')
    echo "$dev → $grp"
done

echo -e "\n=== 9. C-state (turbostat) ==="
command -v turbostat &>/dev/null && turbostat --show Avg_MHz,Busy%,CoreTmp -n 1 2>/dev/null | head -10 \
    || echo "turbostat not available"

echo -e "\n=== 10. blk-mq 큐 매핑 ==="
for mq in /sys/block/nvme*/mq/*/cpu_list; do
    [ -f "$mq" ] && echo "$(echo $mq | grep -o 'nvme[^/]*/mq/[0-9]*'): $(cat $mq)"
done

echo -e "\n=== 진단 완료 ==="
우선순위별 해결 순서: 간섭 원인을 찾았다면 아래 순서로 해결하는 것이 가장 효과적입니다.
  1. PCIe 토폴로지 분리 — 하드웨어 수준 경합 제거 (가장 큰 효과)
  2. NUMA 정렬 — NIC·NVMe·DPDK 코어를 같은 NUMA 노드에 배치
  3. CPU 코어/IRQ 분리isolcpus + IRQ affinity 설정
  4. Hugepage 최적화 — 노드별 균등 할당, 최소량 사용
  5. IOMMU 튜닝iommu=pt, IOMMU 그룹 분리
  6. LLC/메모리 대역폭 격리 — Intel CAT/MBA (RDT) 적용
  7. 전원/스케줄러 튜닝 — C-state 제한, nohz_full, rcu_nocbs
  8. blk-mq/MSI-X/Thermal — 큐 재매핑, 냉각 확인

운영 하드닝과 트러블슈팅

DPDK 장애는 대개 "패킷 처리 코드"보다 권한, IOMMU 그룹, hugepage 배치, NUMA 불일치, 보안 부팅에서 먼저 발생합니다. 따라서 운영 체크리스트를 코드 리뷰와 별도로 가져가는 편이 안전합니다.

하드닝 체크리스트

항목권장 설정이유
장치 접근vfio-pci + IOMMU 기본DMA 격리와 비특권 실행 경로를 동시에 확보합니다.
UIO 사용 범위랩/레거시 전용공식 문서는 UEFI Secure Boot가 UIO 모듈 로드를 막을 수 있다고 경고합니다.
코어 선택코어 0은 OS용으로 남기고 sibling 충돌 회피Linux GSG도 코어 0 비사용을 권장합니다.
Hugepage 권한전용 디렉터리, 최소 권한, 충분한 memlockrootless 실행과 실패 원인 분리를 쉽게 합니다.
NUMA 정렬NIC, 워커 코어, mempool을 같은 노드에 둠크로스소켓 지연과 IOTLB 비용을 줄입니다.
호환성 고정OVS/DPDK/펌웨어 조합을 하나의 단위로 관리ABI와 PMD capability 차이로 인한 현장 편차를 줄입니다.

자주 보는 증상과 원인

증상원인조치
cannot open /proc/self/pagemapPA 모드 전제 또는 물리 주소 접근 권한 부족--iova-mode=va로 강제하거나 공식 문서가 요구하는 capability를 부여
VFIO group is not viableIOMMU 그룹 안에 다른 함수/브리지 디바이스가 남아 있음그룹 전체를 확인하고 같은 그룹의 불필요 디바이스를 커널에서 떼거나 ACS 토폴로지를 재검토
VFIO_IOMMU_MAP_DMA failedmemlock 부족 또는 dma_entry_limit 한계리소스 한도를 올리고 hugepage 세그먼트 수를 줄이며, 필요 시 커널 dma_entry_limit를 확인
rx_nombuf 증가mempool 고갈, 잘못된 NUMA 배치, per-core 캐시 과소mempool 크기·캐시·NUMA를 다시 맞추고 show port xstats로 추적
AF_XDP PMD가 zero-copy로 붙지 않음드라이버 미지원 또는 XDP/queue 조합 불일치커널 AF_XDP 드라이버 capability와 queue 구성을 먼저 확인
dpdk_initialized=false (OVS)hugepage 디렉터리, socket-mem, VFIO 바인딩, ABI 조합 중 하나가 불일치ovs-vsctl get Open_vSwitch . dpdk_initialized, dpdk-devbind --status, OVS 로그를 같이 확인
Secure Boot 환경에서 UIO 모듈 로드 실패서명되지 않은 UIO 계열 모듈이 차단됨가능하면 VFIO로 전환하고, 꼭 필요하면 MOK/모듈 서명 절차를 밟음
no-IOMMU 모드: 커널 문서와 DPDK 문서는 모두 no-IOMMU 경로를 unsafe로 취급합니다. 테스트 랩에서만 쓰고, 운영 환경에서는 디바이스가 호스트 메모리 전체를 DMA로 건드릴 수 있는 전제를 항상 기억해야 합니다.

보안 공격 표면과 위협 모델

DPDK는 커널 네트워크 스택을 우회하므로 커널이 제공하는 보안 계층(Netfilter, SELinux, seccomp, 네임스페이스)을 사용할 수 없습니다. 따라서 DPDK 애플리케이션 자체가 신뢰 경계(trust boundary)가 되며, 공격 표면을 명시적으로 인식하고 완화해야 합니다.

DPDK 보안 공격 표면 외부 네트워크 악의적 패킷 주입 NIC (DMA 엔진) DMA 범위 공격 DPDK 애플리케이션 파싱 취약점, 메모리 오염 mbuf 오버플로우 공유 메모리 멀티프로세스 격리 주요 위협 벡터 ① DMA 공격 (IOMMU 미사용 시) UIO/no-IOMMU: 악성 NIC 펌웨어가 호스트 메모리 전체에 DMA 가능 → 커널 메모리 변조, 권한 상승 ② 악성 패킷 파싱 취약점 Netfilter/conntrack 없이 raw 패킷을 직접 파싱 → 버퍼 오버플로우, 정수 오버플로우, 포맷 스트링 공격 ③ Hugepage 정보 유출 hugepage 파일 권한 미설정 → 다른 프로세스가 패킷 데이터 열람 가능, /proc/self/pagemap 물리 주소 노출 ④ Telemetry/UNIX 소켓 노출 DPDK telemetry 소켓 권한 미설정 → 비인가 사용자가 런타임 상태 조회·조작 가능 ⑤ 멀티프로세스 공유 메모리 ⑥ Side-channel (Spectre/Meltdown) busy-poll + 유저 공간 NIC 접근: 캐시 타이밍 공격에 노출, KPTI/Retpoline 미적용 ⑦ Supply chain (PMD/드라이버) 서드파티 PMD, NIC 펌웨어 업데이트: 신뢰할 수 없는 바이너리가 DMA 경로에 직접 접근 ⑧ DoS (리소스 고갈) mempool 고갈, descriptor ring 가득 참, 과도한 flow rule → 정상 트래픽 처리 불가 ⑨ VFIO 그룹 탈취 같은 IOMMU 그룹 내 다른 디바이스를 통해 격리 우회 가능성
위협완화 방법운영 체크
DMA 공격VFIO + IOMMU 필수 사용, UIO/no-IOMMU 금지dmesg | grep -i iommu "enabled" 확인
패킷 파싱모든 수신 패킷 길이/헤더 검증, fuzzing 테스트AFL/libFuzzer로 패킷 파서 정기 테스트
Hugepage 노출전용 디렉터리 + 최소 권한, --in-memory 사용ls -la /dev/hugepages/ 권한 확인
Telemetry 소켓소켓 파일 권한 제한 (0600), 전용 그룹ls -la /var/run/dpdk/
공유 메모리secondary 프로세스를 신뢰된 바이너리로 제한hugetlbfs 파일 소유권과 접근 로그
Side-channelDPDK 코어에서 타 워크로드 격리, CPU 취약점(Vulnerability) 패치 적용lscpu vulnerability 항목 확인
펌웨어 공급망NIC 펌웨어 서명 검증(Signature Verification), 공식 소스만 사용ethtool -i 펌웨어 버전 추적
DoSmempool 여유 모니터링, flow rule 수 제한, rate-limitrx_nombuf xstat 감시
IOMMU 그룹ACS 지원 확인, 불필요 디바이스 분리ls /sys/kernel/iommu_groups/*/devices/
DPDK CVE 사례: 과거 DPDK vhost-user 구현에서 가상 큐 크기 검증 누락으로 인한 힙 오버플로우(CVE-2020-10722~10726), rte_flow 규칙 처리 중 OOB 읽기 등의 취약점이 보고되었습니다. 유저 공간 라이브러리라고 해서 보안 감사에서 제외하면 안 됩니다.
DPDK 학습 순서 권장:
  • 1단계dpdk-testpmd로 기본 포트 동작 이해 (io/mac/macswap 모드)
  • 2단계examples/l2fwd 소스 분석 (가장 단순한 DPDK 앱, ~300줄)
  • 3단계examples/l3fwd 분석 (LPM/EM 라우팅, RSS 활용)
  • 4단계rte_ring, rte_mempool 내부 구현 분석
  • 5단계 — PMD 소스 분석 (drivers/net/ixgbe/ 또는 drivers/net/i40e/)
  • 6단계 — Eventdev, Cryptodev 등 고급 프레임워크
관련 문서:

참고자료

공식 문서

성능 튜닝

드라이버 및 디바이스

프레임워크 및 통합

주요 참고 글

커널 소스 경로 (DPDK 연동)

필수 관련 문서: 참고 문서: