DPDK는 Data Plane Development Kit의 약자로, Intel 아키텍처 기반에서 고성능 패킷 처리를 위해 설계된 시스템 소프트웨어이다. 일반적인 리눅스 환경에서는 ice와 같은 커널 NIC 드라이버가 NIC를 제어하고, 패킷은 커널 네트워크 스택을 거쳐 처리된다.
하지만 DPDK는 일반 커널 NIC 드라이버를 통해 패킷을 송수신하는 것이 아니라,NIC를 커널로부터 분리한 뒤 User Space에서 직접 제어한다. 이를 위해 DPDK는UIO또는VFIO와 같은PCI 장치 접근용 커널 드라이버를 사용한다.
DPDK Driver
DPDK가 커널 네트워크 스택을 사용하지 않는다고 해서 커널이 완전히 필요 없는 것은 아니다. User Space 의 애플리케이션이 PCI 장치인 NIC를 직접 제어하려면, 최소한 다음 기능은 필요하다.
NIC의 PCI BAR 레지스터 영역을 User Space에 매핑
장치의 인터럽트 전달
DMA에 사용할 메모리 접근 제어
이 역할을 해 주는 것이 바로 UIO 와 VFIO 이다.
※ PCI Bar
PCI 장치가 가지고 있는 메모리 공간이 시스템 메모리 주소에 매핑되는 기준 주소를 저장하는 레지스터
UIO ( igb_uio )
UIO는 Userspace I/O의 약자로, User Space 프로그램이 장치에 직접 접근할 수 있도록 도와주는 단순한 드라이버이다.
UIO를 사용하면 PCI 장치의 BAR 영역을 User Space에 mmap() 하여, 애플리케이션이 장치 레지스터를 직접 읽고 쓸 수 있다. 각 UIO 장치는 보통 /dev/uio0, /dev/uio1 과 같은 디바이스 파일로 노출되며, 관련 정보는 sysfs를 통해 확인할 수 있다.
또한 인터럽트는 디바이스 파일에 대해 read() 를 수행하는 방식으로 전달된다. 애플리케이션이 read() 상태로 대기하고 있다가 인터럽트가 발생하면 깨어나며, 이때 누적 인터럽트 횟수를 확인할 수 있다.
다만 UIO는 구조가 단순한 대신 DMA 접근 보호 기능이 부족하다는 한계가 있다.
※ IOMMU (Input-Output Memory Management Unit)
• NIC → (DMA) → 메인 메모리
NIC는 패킷을 처리할 때 CPU를 거치지 않고 DMA(Direct Memory Access) 를 사용해 메모리에 직접 접근한다. 이 방식은 성능 면에서는 매우 효율적이지만, 반대로 잘못된 메모리 주소에 접근하면 다른 프로세스 메모리나 커널 메모리 영역을 침범할 위험이 있다. 이 문제를 막아주는 하드웨어가 IOMMU 이다.
IOMMU는 DMA 요청을 물리 주소로 변환하여 허용된 메모리 영역인지 확인 후 사용한다.
VFIO ( vfio-pci )
VFIO는 UIO와 마찬가지로 DPDK가 커널 네트워크 스택을 거치지 않고 NIC에 접근할 수 있도록 해주는 커널 모듈이다. 하지만 VFIO는 IOMMU를 기반으로 DMA 격리와 메모리 보호 기능을 제공한다는 점에서 UIO보다 훨씬 안전하다.
그래서 DPDK 환경에서는 일반적으로 VFIO 사용이 권장된다.
PMD ( Polling Mode Drivers )
앞서 설명한 VFIO, UIO 드라이버는 패킷을 직접 처리하는 드라이버가 아니다. 이들의 역할은 DPDK 애플리케이션이 User Space에서 NIC 하드웨어에 접근할 수 있도록 연결해 주는 것에 가깝다.
실제 패킷 송수신 처리는 PMD가 담당한다.
PMD는 DPDK에서 NIC 하드웨어를 User Space에서 직접 제어하기 위한 드라이버 계층이다. PMD는 NIC의 Rx/Tx Descriptor Ring 에 직접 접근하여 패킷 입출력을 처리하며, DMA를 통해 패킷을 송수신한다.
NIC는 제조사와 세대마다 내부 구조와 동작 방식이 다르다. 이 차이를 애플리케이션이 직접 다루면 코드가 복잡해지고 이식성이 떨어진다. 그래서 DPDK는 공통 인터페이스 계층인 ethdev API를 제공한다.
ethdev = 인터페이스 계층
PMD = 실제 구현 계층
애플리케이션은 rte_eth_dev 라는 포트 객체를 기준으로 rte_eth_rx_burst(), rte_eth_tx_burst() 와 같은 ethdev API를 호출한다. 그러면 실제로는 rte_eth_dev 에 연결된 PMD 함수가 실행되어, 해당 NIC 하드웨어를 직접 제어하게 된다.
즉, 애플리케이션은 모든 NIC를 공통된 방식으로 다룰 수 있고, NIC별 상세한 하드웨어 제어는 PMD가 담당한다. 이러한 초기화 과정이 완료되면 이후 실제 패킷 송수신이 가능해진다.
PMD는 실제 NIC 패킷 처리를 위해 다음과 같은 역할을 수행한다.
레지스터/포트 설정
RX/TX 큐(=descriptor ring) 설정/운영
DMA 버퍼(mbuf)와 링 연결
burst 기반 송수신 수행(rx/tx)
PMD는 NIC의 Rx/Tx Descriptor Ring을 지속적으로 polling 하면서 패킷을 처리한다.
수신 시에는 완료 상태로 표시된 descriptor가 있는지 계속 확인하고, 완료된 descriptor가 있으면 그에 연결된mbuf를 즉시 가져와 처리한다. 패킷이 없는 경우에도 PMD는 sleep 상태로 들어가지 않고 계속 descriptor 상태를 확인하는데, 이 방식을 Busy Polling 이라고 한다.
이렇게 PMD가 패킷을 가져올 때, 일반적인 리눅스 커널에서는 패킷을 sk_buff 구조체로 관리하지만, DPDK에서는 mbuf 라는 구조를 사용한다.
mbuf
mbuf 는 고성능 패킷 처리를 위해 메모리 구조와 접근 방식을 최적화한 데이터 구조체이다.
일반적인 메모리 할당 방식처럼 운영체제로부터 매번 버퍼를 새로 할당받는 것이 아니라, 미리 준비된 mempool 에서 고정 크기 버퍼를 가져와 사용한 뒤 다시 반납하는 방식으로 동작한다. 이를 통해 반복적인 할당과 해제로 인한 오버헤드를 줄일 수 있다.
rte_mbuf 의 메모리 구조는 크게 다음 세 부분으로 구성된다.
rte_mbuf 구조체 : 패킷 상태와 제어 정보를 담는 메타데이터 영역
private 영역 : 애플리케이션이나 드라이버가 선택적으로 사용할 수 있는 추가 공간
data buffer : 실제 패킷 데이터가 저장되는 버퍼 영역
이 중 data buffer는 다시 headroom, 실제 패킷 데이터 영역, tailroom 으로 나뉜다.
headroom / tailroom은 payload 앞/뒷쪽에 남겨둔 여유 공간으로, 패킷 데이터를 수정할 때 복사 없이 처리하기 위해 필요하다. 패킷을 처리하다 보면 다음과 같은 작업이 발생한다.
• L2 헤더 추가 • VLAN 태그 삽입 • 터널링 헤더 추가
그래서 DPDK는 headroom과 tailroom을 두고, 필요 시 data_off 같은 오프셋만 조정하여 복사 없이 헤더를 붙일 수 있도록 설계되어 있다. 기본 headroom 크기는 RTE_PKTMBUF_HEADROOM 매크로로 정의되며, 일반적으로 128바이트이다.
rte_mbuf(struct)
버퍼 위치 정보
buf_addr, buf_len, data_off
패킷 길이 정보
data_len, pkt_len
프로토콜 정보
packet_type, l2/l3/l4_len
오프로딩 정보
ol_flags, tx_offload
멀티세그 정보
next, nb_segs
메타데이터 영역인 rte_mbuf 구조체에는 패킷 길이, 버퍼 크기, 데이터 시작 위치, 오프로딩 정보 등 CPU가 패킷 처리 시 반복적으로 참조하는 정보가 저장된다. 반면 실제 패킷 바이트 자체는 별도의 data buffer에 위치한다.
mbuf 사용 이유
1. Zero Copy
패킷의 payload를 매번 새로 복사하지 않고, mbuf 포인터를 전달하는 방식으로 패킷을 처리할 수 있다.
예를 들어 rte_eth_rx_burst() 는 NIC로부터 패킷을 받아올 때, 패킷 데이터를 새로 복사해서 반환하는 것이 아니라 수신된 패킷이 담긴 mbuf 포인터 배열을 채워 반환한다. 이 덕분에 불필요한 데이터 복사를 줄이고 고속 처리가 가능하다.
2. LowAllocation Overhead
일반적인 방식처럼 매번 alloc() / free() 를 호출하는 대신, 미리 준비된 mempool 에서 mbuf 를 꺼내 사용한 뒤 다시 반납한다. 이를 통해 반복적인 메모리 할당과 해제로 인한 비용을 줄일 수 있다.
3. Cache 효율
rte_mbuf 구조체는 패킷 처리 과정에서 자주 참조하는 필드들이 앞쪽 cache line에 위치하도록 구성되어 있어, CPU가 반복적으로 접근할 때 캐시 효율을 높일 수 있다. 이러한 구조는 대량 패킷 처리 환경에서 성능 향상에 도움이 된다.
single segment
mbuf 는 하나의 데이터 버퍼만 사용하는 single segment 형태와, 여러 개의 mbuf 를 체인처럼 연결하여 하나의 패킷을 표현하는 multi segment 형태를 지원한다.
Single segment는 하나의 mbuf 안에 하나의 패킷 전체가 들어가는 형태이다.
패킷 전체가 하나의 data buffer에 저장된다.
next 포인터는 NULL 이다.
data_len 과 pkt_len 값이 동일하다.
일반적인 이더넷 프레임 크기의 패킷은 대부분 single segment로 처리된다. 이 경우 구조가 단순하고, next 포인터도 사용하지 않으므로 처리 비용이 낮다.
multi segment
일반적인 작은 패킷은single segment로 처리되지만,큰 패킷이나 분할된 데이터는multi segment로 처리한다.
multi segment는 하나의 패킷 데이터를 여러 개의 mbuf에 나누어 저장하고, 이를 next 포인터로 연결하는 형태이다.
즉, 첫 번째 mbuf가 패킷의 시작을 나타내고, 뒤에 연결된 mbuf들이 나머지 payload를 이어서 담는 구조이다. 이렇게 여러 mbuf가 연결된 형태를 흔히 mbuf chain 이라고 부른다.
mempool
DPDK는 고속 패킷 처리 과정에서 malloc() / free() 를 반복 호출하지 않기 위해 mempool 을 사용하여 mbuf 를 관리한다. mempool은 동일한 크기의 객체들을 미리 여러 개 생성해 두고, 필요할 때 빠르게 꺼내 쓰고 다시 반납하는 객체 풀이다.
DPDK에서는 패킷 버퍼로 사용할 mbuf 를 이 mempool 안에 미리 준비해 둔다. 즉, 각 객체 슬롯마다 mbuf 1개가 대응된다고 이해하면 된다.
다음 pktmbuf_pool_create()는 DPDK에서 mempool을 생성하는 대표적인 API 이다.
이 함수를 호출하면, DPDK는 지정한 개수만큼의 mbuf 객체를 담을 수 있는 mempool을 생성한다. 먼저 큰 메모리 영역을 확보한 뒤, 그 안에 고정 크기의 mbuf 객체들을 미리 여러 개 배치해 둔다. 이후 애플리케이션은 패킷을 처리할 때 이 "mbuf_pool"에서 mbuf 를 하나 꺼내 사용하고, 처리가 끝나면 다시 rte_pktmbuf_free()로 pool에 반납하여 재사용한다.