VPP API 계층 레퍼런스
clib · vlib · vnet · VCL · TLS · offload로 이어지는 VPP API 계층 구조와 각 계층의 책임을 한 장의 지도로 정리합니다.
VPP의 소스 트리에는 기능이 비슷해 보이는 접두사(clib_, vlib_, vnet_, session_, vppcom_, tls_, vnet_crypto_)가 여러 개 섞여 있어서 처음 접하면 어느 계층의 API를 써야 할지 판단하기 어렵습니다. 이 절은 각 접두사가 어느 계층의 책임을 지고, 어떤 핵심 자료형(Type)과 함수가 있으며, 플러그인과 애플리케이션이 각각 어디에서 시작해야 하는지를 정리합니다. 아래 SVG는 전체 계층을 단 한 장에 펼쳐 본 지도(Map)입니다.
이 지도를 사용하는 기본 원칙은 단순합니다. 아래 계층은 위 계층의 언어를 모른다는 것입니다. 예를 들어 clib_bihash는 자기가 FIB를 저장하는지 ACL을 저장하는지 알지 못하고, vlib는 패킷이 TLS인지 IPsec인지 구분하지 않으며, vnet은 애플리케이션 세션이 어떤 소켓 API를 통해 접근하는지 신경 쓰지 않습니다. 이 단방향 의존성 덕분에 VPP는 플러그인으로 기능을 갈아 끼워도 하부가 흔들리지 않습니다.
각 계층의 책임과 대표 API
| 계층 | 주요 디렉터리 | 대표 자료형 | 대표 API | 언제 직접 호출하는가 |
|---|---|---|---|---|
| ⑤ clib/vppinfra | src/vppinfra/ |
u8 * 벡터, pool_t, clib_bihash_*_t |
clib_mem_alloc, vec_add1, pool_get, clib_bihash_add_del, clib_atomic_fetch_add, clib_spinlock_lock |
플러그인 내부 자료구조, 집계 카운터, 해시 기반 세션/플로우 테이블을 손수 만들 때 |
| ④ vlib | src/vlib/ |
vlib_main_t, vlib_buffer_t, vlib_node_registration_t, vlib_frame_t |
VLIB_REGISTER_NODE, vlib_get_buffer, vlib_get_next_frame, vlib_validate_buffer_enqueue_*, vlib_process_wait_for_event, vlib_worker_thread_barrier_sync |
새 그래프 노드를 작성하거나 CLI/프로세스 노드를 등록할 때 — 플러그인 개발의 기본 언어 |
| ③ vnet | src/vnet/ |
vnet_main_t, vnet_hw_interface_t, ip4_fib_t, ipsec_sa_t, vnet_crypto_op_t |
vnet_sw_interface_set_flags, fib_table_entry_path_add, ipsec_sa_add_and_lock, vnet_feature_enable_disable, vnet_crypto_process_ops |
인터페이스, 라우팅, ACL, IPsec, 암호화(Encryption)처럼 "네트워크 장비다운" 기능을 붙일 때 |
| ② session / transport | src/vnet/session/, tcp/, tls/, quic/ |
session_t, app_worker_t, tls_ctx_t, svm_fifo_t |
session_alloc_for_connection, tcp_connection_alloc, tls_init_ctx, svm_fifo_enqueue |
TCP/TLS/QUIC 엔진을 확장하거나 새 트랜스포트를 플러그인으로 추가할 때. 일반 플러그인은 거의 직접 만지지 않음 |
| ① 애플리케이션 / VCL | src/vcl/ |
vppcom_endpt_t, vls_handle_t, vppcom_cert_key_pair_t |
vppcom_app_create, vppcom_session_create, vppcom_session_connect, vppcom_epoll_wait, vppcom_add_cert_key_pair |
유저 프로세스가 VPP 호스트 스택을 "소켓처럼" 사용하고자 할 때 (대부분의 애플리케이션 개발 경로) |
| ⑥ 오프로드 | DPDK · cryptodev · ipsecmb | rte_mbuf, rte_crypto_op, rte_cryptodev_* |
rte_eth_rx_burst, rte_cryptodev_enqueue_burst, rte_cryptodev_dequeue_burst, IPsec-MB의 IMB_AES_GCM_ENC_* |
드라이버 또는 vnet_crypto 백엔드를 직접 작성할 때 — 일반적으로 플러그인은 vnet_crypto_*를 통해 간접 사용 |
하나의 요청이 계층을 어떻게 관통하는가
추상 개념을 구체화하기 위해, HTTPS 요청이 들어와서 IPsec 터널로 재전송되는 극단적인 경로를 따라가 봅니다. 이 한 줄의 플로우가 위 여섯 계층을 모두 건드립니다.
- ⑥ DPDK PMD가 NIC 링에서
rte_mbuf를 가져와 VPP의vlib_buffer_t로 감쌉니다. - ④ vlib의
dpdk-input노드가 프레임을 만들고, 벡터 단위로 다음 노드에 넘깁니다. - ③ vnet의
ethernet-input → ip4-input → ip4-lookup이ip4_fib_t와adj_*를 사용해 FIB 룩업을 수행합니다. - ② session의
tcp-established가 TCP 상태 머신을 돌리고, TLS 세션이면tls_* → svm_fifo_*경로로 복호화(Decryption)된 평문을 RX FIFO에 적재합니다. - ① VCL 애플리케이션이
vppcom_session_read로 평문을 읽고, 검사/변형 후vppcom_session_write로 다른 세션에 씁니다. - 반대 방향으로 내려오는 패킷은 다시 ③ vnet의
esp-encrypt가 ⑤ clib의clib_atomic_fetch_add로ipsec_sa_t.seq를 증가시키고,vnet_crypto_process_ops를 호출합니다. - ⑥ cryptodev(QAT)가 실제 AES-GCM 연산을 수행하고, 완료된 버퍼가 ④ vlib의 TX 경로로 돌아옵니다.
접두사만 보고 계층을 판별하는 법
실전에서는 헤더 파일명과 함수 접두사만 보고도 어느 계층에서 호출해야 하는지 거의 판단할 수 있습니다.
clib_*,vec_,pool_,hash_,bihash_→ ⑤ clib. 자료구조, 원자 연산, 락, 시간. 어디서든 자유롭게 호출 가능.vlib_*,VLIB_*매크로 → ④ vlib. 반드시vlib_main_t *vm컨텍스트가 있어야 하며, 워커 스레드 안에서 또는 배리어 아래서만 호출해야 하는 경우가 있음.vnet_*,fib_*,ip4_*,ipsec_*,classify_*→ ③ vnet. "네트워크 장비 의미"를 바꾸는 API. 대부분 메인 스레드 배리어 아래에서 호출 필요.session_*,tcp_*,udp_*,tls_*,quic_*,svm_fifo_*→ ② 세션/전송. 호스트 스택 엔진 내부. 일반 플러그인은 VCL 또는 session API 상위만 사용하는 편이 안전.vppcom_*,vls_*→ ① VCL. 유저스페이스 애플리케이션 전용. VPP 프로세스 내부(플러그인)에서 호출 금지.rte_*,IMB_*,qat_*→ ⑥ 오프로드. DPDK/ipsecmb/QAT 라이브러리의 직접 API. 일반 코드는vnet_crypto_*로 우회해 호출.
clib_*와 vlib_*는 이름이 비슷해 보이지만, clib_*는 컨텍스트가 없는 순수 유틸리티이고 vlib_*는 반드시 런타임 컨텍스트(vm)를 요구합니다. 예를 들어 clib_mem_alloc은 어디서든 부를 수 있지만 vlib_get_buffer(vm, bi)는 vm 없이 호출되면 세그폴트합니다. 이 차이가 플러그인 초기화 함수에서 가장 자주 틀리는 지점입니다.
계층 경계 교차 — 실제 코드 예제
플러그인이 하나의 작업을 수행하면서 여러 계층을 함께 쓰는 전형적인 패턴입니다. ACL-match 후 FIB 우선순위를 갱신하는 간단한 예제를 통해 각 계층 API의 역할 분담을 보여줍니다.
/* ① PROCESS 노드 (vlib 계층) — CLI 핸들러가 이벤트를 보내면 깨어남 */
static uword
myplugin_process (vlib_main_t *vm, vlib_node_runtime_t *rt, vlib_frame_t *f)
{
uword event_type, *event_data = 0;
while (1) {
vlib_process_wait_for_event(vm); /* ④ vlib — 이벤트 대기 */
event_type = vlib_process_get_events(vm, &event_data);
if (event_type == MY_EVENT_UPDATE_ROUTE) {
/* ③ vnet — FIB 엔트리 갱신 (배리어 불필요: 이미 메인 루프 내) */
fib_prefix_t pfx = { .fp_len = 32, .fp_proto = FIB_PROTOCOL_IP4 };
pfx.fp_addr.ip4 = *(ip4_address_t *) event_data[0];
fib_table_entry_path_add(
0, /* table-id 0 */
&pfx,
FIB_SOURCE_PLUGIN_LOW, FIB_ENTRY_FLAG_NONE,
DPO_PROTO_IP4, &nh_addr, sw_if_index,
~0, 1, NULL, FIB_ROUTE_PATH_FLAG_NONE);
}
vec_reset_length(event_data);
}
return 0;
}
/* ② 데이터 경로 노드 (vlib 계층) — 패킷이 올 때마다 호출 */
VLIB_NODE_FN(myplugin_node) (vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame)
{
u32 *from = vlib_frame_vector_args(frame), n = frame->n_vectors;
while (n--) {
vlib_buffer_t *b = vlib_get_buffer(vm, from[0]); /* ④ vlib */
ip4_header_t *ip = vlib_buffer_get_current(b);
/* ⑤ clib — bihash 조회 (락 없는 reader 경로) */
clib_bihash_kv_8_8_t kv = { .key = ip->dst_address.as_u32 };
if (BV(clib_bihash_search)(&myplugin_main.flow_table, &kv, &kv) == 0) {
/* hit — 프로세스 노드에 갱신 요청 */
vlib_process_signal_event(vm, /* ④ vlib */
my_process_node.index,
MY_EVENT_UPDATE_ROUTE,
(uword) &ip->dst_address);
}
from++; /* 다음 버퍼 인덱스 */
}
vlib_buffer_enqueue_to_single_next(vm, node, vlib_frame_vector_args(frame),
MY_NEXT_LOOKUP, frame->n_vectors);
return frame->n_vectors;
}
계층 경계 교차 비용 분석
플러그인이 자신이 속한 계층을 벗어나 다른 계층의 API를 직접 호출하면, 기능 오작동뿐만 아니라 측정 가능한 성능 비용이 발생합니다. 아래는 대표적인 세 가지 교차 시나리오와 그 비용입니다.
- vnet FIB 갱신을 데이터 경로 노드에서 직접 호출:
fib_table_entry_path_add()같은 vnet 제어 평면 API는 내부적으로vlib_worker_thread_barrier_sync()를 통해 배리어를 획득해야 합니다. 데이터 경로 노드에서 이 함수를 호출하면 배리어 획득에 ~500ns가 소요되고, 다른 모든 워커 스레드가 정지(stall)되어 벡터 처리가 중단됩니다. 패킷 처리량이 많을수록 워커 stall의 파급 효과는 크게 증폭됩니다. - 플러그인 내부에서
vppcom_*호출: 해당 없음(N/A).vppcom_*는 유저스페이스 독립 프로세스용 라이브러리이며, VPP 데몬 내부(플러그인 코드)에서 링크되거나 호출될 수 없습니다. VCL은 공유 메모리 채널을 통해 VPP 세션 레이어와 통신하는 외부 클라이언트이므로, 플러그인이 같은 주소 공간에서 직접 호출하면 이중 세션 레이어 혼동이 발생합니다. 플러그인에서는 반드시session_*/app_*API를 사용해야 합니다. - 일반 플러그인에서
rte_*를 직접 호출: DPDK의rte_*API는 VPP의 버퍼 수명 주기(buffer lifecycle)를 완전히 우회합니다. 워커 루프에서 직접 호출하면 VPP의vlib_buffer_t참조 카운팅, 사전 할당 풀, 트레이스 훅을 모두 건너뛰어 메모리 누수·더블 프리·undefined behavior를 유발합니다.rte_*의 직접 호출은 초기화(init) 및 정리(cleanup) 단계에서만 허용됩니다. 런타임 데이터 경로에서는 반드시vnet_crypto_*또는VNET_DEVICE_CLASS드라이버 인터페이스를 통해 간접 사용해야 합니다.
잘못된 호출 패턴 요약
| 잘못된 호출 패턴 | 실제 증상 | 올바른 대안 |
|---|---|---|
데이터 경로 node_fn에서 fib_table_entry_path_add() 직접 호출 |
워커 배리어 경합 → 전체 워커 stall → 지연(latency) 급증 및 크래시 가능 | PROCESS 노드에 이벤트 전송 후 PROCESS 노드에서 FIB 갱신 수행 |
VLIB_INIT_FUNCTION 내에서 vlib_buffer_alloc() 호출 |
버퍼 풀 미초기화 상태 → NULL 역참조 → 즉시 크래시 | VLIB_MAIN_LOOP_ENTER_FUNCTION에서 첫 번째 루프 진입 시 할당 |
워커 스레드에서 rte_eth_rx_burst() 직접 사용 |
VPP 버퍼 수명 주기 우회 → 메모리 누수·더블 프리 · PMD 경합 | VNET_DEVICE_CLASS + VLIB_REGISTER_NODE로 드라이버 플러그인 등록 |
커널 플러그인 내부에서 vppcom_session_create() 호출 |
VCL은 외부 프로세스용 라이브러리 — 동일 주소 공간 호출 시 이중 세션 레이어 혼동 | 플러그인에서는 session_* / app_* API 사용 |
올바른 이벤트 전달 패턴
아래 예제는 VLIB_NODE_FN 데이터 경로에서 제어 평면 작업이 필요할 때 PROCESS 노드에 이벤트를 전달하는 올바른 패턴을 보여줍니다. 이 패턴을 사용하면 데이터 경로 노드는 배리어 없이 즉시 복귀하고, 제어 평면 갱신은 PROCESS 노드에서 안전하게 실행됩니다.
/*
* 올바른 패턴: VLIB_NODE_FN에서 제어 평면 작업이 필요할 때
* 직접 FIB/vnet API를 호출하지 않고 PROCESS 노드에 이벤트를 전달한다.
*/
/* 이벤트 타입 정의 */
#define MY_CTRL_EVENT_FIB_UPDATE 1
/* 데이터 경로 노드 — 패킷 벡터마다 호출 */
VLIB_NODE_FN (my_worker_node)
(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame)
{
u32 *from = vlib_frame_vector_args (frame);
u32 n = frame->n_vectors;
while (n--)
{
vlib_buffer_t *b = vlib_get_buffer (vm, from[0]); /* ④ vlib */
ip4_header_t *ip = vlib_buffer_get_current (b);
/* ⑤ clib bihash — 락 없는 reader, 데이터 경로 안전 */
clib_bihash_kv_8_8_t kv = { .key = ip->dst_address.as_u32 };
if (clib_bihash_search_8_8 (&my_main.flow_tbl, &kv, &kv) == 0)
{
/*
* 제어 평면 갱신 필요: FIB를 직접 건드리지 않고
* vlib_process_signal_event()로 PROCESS 노드에 이벤트 전송.
* 이 호출은 lock-free 큐에 쓰기만 하므로 배리어 없이 복귀한다.
*/
vlib_process_signal_event (vm,
my_ctrl_process_node_index,
MY_CTRL_EVENT_FIB_UPDATE,
(uword) ip->dst_address.as_u32);
}
from++;
}
vlib_buffer_enqueue_to_single_next (vm, node,
vlib_frame_vector_args (frame),
MY_NEXT_IP4_LOOKUP, frame->n_vectors);
return frame->n_vectors;
}
/* PROCESS 노드 — 메인 루프에서 깨어나 제어 평면 갱신 수행 */
static uword
my_ctrl_process (vlib_main_t *vm, vlib_node_runtime_t *rt, vlib_frame_t *f)
{
uword event_type;
uword *event_data = 0;
while (1)
{
vlib_process_wait_for_event (vm);
event_type = vlib_process_get_events (vm, &event_data);
switch (event_type)
{
case MY_CTRL_EVENT_FIB_UPDATE:
{
for (int i = 0; i < vec_len (event_data); i++)
{
ip4_address_t dst;
dst.as_u32 = (u32) event_data[i];
fib_prefix_t pfx = {
.fp_len = 32,
.fp_proto = FIB_PROTOCOL_IP4,
.fp_addr = { .ip4 = dst },
};
/* ③ vnet FIB 갱신 — PROCESS 노드 컨텍스트이므로 안전 */
fib_table_entry_path_add (
0, &pfx,
FIB_SOURCE_PLUGIN_LOW, FIB_ENTRY_FLAG_NONE,
DPO_PROTO_IP4, &my_main.nh_addr,
my_main.sw_if_index, ~0, 1, NULL,
FIB_ROUTE_PATH_FLAG_NONE);
}
}
break;
default:
break;
}
vec_reset_length (event_data);
}
return 0;
}
잘못된 계층 호출의 결과
| 상황 | 잘못된 코드 | 결과 | 올바른 방법 |
|---|---|---|---|
| 데이터 경로 노드에서 FIB 갱신 | fib_table_entry_path_add()를 노드 함수에서 직접 호출 |
배리어 없이 FIB 트라이 수정 → 다른 워커와 경합 → 크래시 또는 메모리 손상 | PROCESS 노드에 이벤트를 보내고 거기서 FIB 갱신 |
| 플러그인 init에서 버퍼 할당 | vlib_get_buffer(vm, bi)를 VLIB_INIT_FUNCTION에서 호출 |
버퍼 풀이 아직 초기화되지 않음 → NULL 역참조 | VLIB_MAIN_LOOP_ENTER_FUNCTION에서 최초 실행 시 할당 |
| VCL API를 플러그인 내부에서 호출 | vppcom_session_create()를 플러그인 코드에서 직접 호출 |
VCL은 독립된 사용자 공간 라이브러리 — VPP 프로세스 내에서 호출 시 이중 세션 레이어 혼동 | 플러그인에서는 session_* / app_* API 사용 |
rte_*를 일반 플러그인에서 직접 호출 |
rte_eth_rx_burst()를 입력 노드에서 직접 사용 |
DPDK 초기화 순서 의존성 위반, 다른 PMD와 충돌 가능 | VNET_DEVICE_CLASS + VLIB_REGISTER_NODE로 드라이버 플러그인 등록 |
잘못된 계층 호출 증상 및 디버깅
잘못된 계층 호출은 즉시 크래시를 일으키지 않는 경우도 있어서 원인 파악이 어렵습니다. 아래는 실수 유형별 증상과 GDB 중단점(breakpoint) 설정 방법입니다.
| 증상 메시지 | 원인 | GDB 중단점 |
|---|---|---|
SIGSEGV at vlib_get_buffer |
초기화 컨텍스트(VLIB_INIT_FUNCTION)에서 vlib API 호출 — 버퍼 풀 미초기화 상태 |
|
assert failed at vlib_buffer_alloc |
워커 컨텍스트 바깥에서 버퍼 할당 시도 — 풀 메타데이터 불일치 | |
worker thread barrier timeout |
데이터 경로 노드에서 FIB 갱신 호출 → 워커 배리어 대기 시간 초과 | |
show runtime 일시 정지 카운터 급등: VPP CLI에서 show runtime을 주기적으로 실행할 때 특정 PROCESS 노드의 suspends 카운터가 비정상적으로 높다면 PROCESS 노드에 이벤트가 폭주하고 있다는 신호입니다. 데이터 경로 노드가 과도하게 이벤트를 시그널하거나, PROCESS 노드의 처리 루프가 느린 제어 평면 API를 반복 호출하고 있을 때 나타납니다.
# PROCESS 노드 오버로드 확인 — suspends 카운터 확인
vpp# show runtime
이벤트 로그로 배리어 대기 패턴 추적: VPP의 내장 이벤트 로거를 활성화하면 배리어 획득·해제 타임스탬프를 기록합니다. show event-log로 출력된 로그에서 barrier_sync와 barrier_release 사이의 간격이 길면 제어 평면 API가 데이터 경로를 방해하고 있음을 의미합니다.
# 이벤트 로거 활성화 및 배리어 대기 패턴 확인
vpp# event-logger on
vpp# # ... 재현 트래픽 발생 ...
vpp# show event-log
25.10 / 26.02 주요 API 변경
아래 변경사항은 기존 플러그인이나 바이너리 API 클라이언트에 영향을 줄 수 있습니다. 각 계층별 상세 내용은 해당 페이지를 참조하시기 바랍니다.
| 계층 | 변경 종류 | 대상 | 버전 | 조치 |
|---|---|---|---|---|
| ⑥ 디바이스 | 🗑️ 제거 | avf_create / avf_delete (AVF 플러그인 전체) |
26.02 | dev_infra API 사용 권장 (src/dev_infra/dev.api — 공식 마이그레이션 가이드 참조) |
| ③ vnet | 🗑️ Deprecated | tap_create_v2 / tap_create_v2_reply |
26.02 | tap_create_v3로 마이그레이션 (num_tx_queues u16 필드 추가) |
| ③ vnet | ⚠️ 코드 변경 | 레거시 virtio pre-1.0 지원 제거 | 26.02 | 커널 4.1+ 에서는 영향 없음 (deprecated API 목록 미등재) |
| ⑤ clib | 🗑️ 제거 | vppmem_preload 라이브러리 |
26.02 | 빌트인 clib_mem_main_t.alloc_free_intercept 사용 |
| ④ vlib | 🆕 신규 | vlib_get_next_frame_p (포인터 기반 next frame) |
26.02 | 기존 카운터 기반 매크로와 병용 가능 |
| ① VCL | 🆕 신규 | vppcom_app_create_with_config(vppcom_cfg_t *) |
26.02 | 환경 변수 없이 프로그래밍 방식으로 VCL 초기화 가능 |
| ② session | 🆕 신규 | APP_OPTIONS_MAX_FIFO_MEMORY |
25.10 | 애플리케이션별 FIFO 메모리 상한 설정 |
| ③ vnet | 🆕 신규 | gre_tunnel_add_del_v2 (GRE 키 지원) |
25.10 | 기존 gre_tunnel_add_del은 유지 |