VPP 그래프 노드 내부 구현
VPP 핵심·추가 그래프 노드 내부 구현 코드 리딩 — ethernet-input, ip4-input, ip4-lookup, ip4-rewrite, arp-input, l2 노드군, Process Nodes와 vlib_process_signal_event를 다룹니다.
핵심 그래프 노드 내부 구현
VPP 패킷 그래프의 핵심 노드들은 고성능을 위해 정교하게 최적화된 내부 구현을 가지고 있습니다. 각 노드의 처리 함수는 quad-loop 또는 dual-loop 패턴을 사용하여 IPC(Instructions Per Cycle)를 극대화합니다.
next_index를 어떻게 채우는가) ④ 트레이스/에러 카운터(관측성 훅이 어디에 있는가). 이 네 가지는 어느 핵심 노드에서도 동일한 패턴으로 반복됩니다.
ethernet-input 노드 (node.c)
ethernet-input 노드는 패킷 그래프의 최초 L2 처리 지점으로, 이더넷 프레임의 EtherType을 분류하여 적절한 다음 노드로 디스패치합니다. 핵심 구현은 ethernet_input_inline() 함수에 있으며, sparse vector를 이용한 O(1) EtherType 룩업이 특징입니다.
| EtherType | 값 | 다음 노드 | 설명 |
|---|---|---|---|
ETHERNET_TYPE_IP4 | 0x0800 | ip4-input | IPv4 패킷 처리 |
ETHERNET_TYPE_IP6 | 0x86DD | ip6-input | IPv6 패킷 처리 |
ETHERNET_TYPE_ARP | 0x0806 | arp-input | ARP 요청/응답 |
ETHERNET_TYPE_MPLS | 0x8847 | mpls-input | MPLS 레이블 처리 |
ETHERNET_TYPE_VLAN | 0x8100 | (내부 처리) | Single VLAN 태그 |
ETHERNET_TYPE_DOT1AD | 0x88A8 | (내부 처리) | QinQ 외부 태그 |
VLAN 태그 처리에서는 single tagging(802.1Q)과 double tagging(QinQ/802.1ad)을 모두 지원합니다. VLAN 태그가 감지되면 외부 태그를 파싱하여 VLAN ID와 우선순위(Priority)를 추출하고, 내부 EtherType을 기반으로 최종 디스패치를 수행합니다.
/* ethernet_input_inline() — quad-loop 패턴 (simplified pseudo-code) */
static_always_inline uword
ethernet_input_inline (vlib_main_t *vm,
vlib_node_runtime_t *node,
vlib_frame_t *frame)
{
u32 n_left, *from, *to_next;
u32 next_index;
from = vlib_frame_vector_args (frame);
n_left = frame->n_vectors;
/* Quad-loop: 4개 패킷 동시 처리 */
while (n_left >= 4)
{
vlib_buffer_t *b0, *b1, *b2, *b3;
u16 type0, type1, type2, type3;
/* 프리페치: 다음 4개 패킷 메타데이터 미리 로드 */
vlib_prefetch_buffer_header (b[4], LOAD);
vlib_prefetch_buffer_header (b[5], LOAD);
vlib_prefetch_buffer_header (b[6], LOAD);
vlib_prefetch_buffer_header (b[7], LOAD);
b0 = vlib_get_buffer (vm, from[0]);
b1 = vlib_get_buffer (vm, from[1]);
b2 = vlib_get_buffer (vm, from[2]);
b3 = vlib_get_buffer (vm, from[3]);
/* 이더넷 헤더에서 EtherType 추출 */
ethernet_header_t *e0 = vlib_buffer_get_current (b0);
type0 = clib_net_to_host_u16 (e0->type);
/* VLAN 태그 확인 및 처리 */
if (PREDICT_FALSE (type0 == ETHERNET_TYPE_VLAN
|| type0 == ETHERNET_TYPE_DOT1AD))
{
/* VLAN 헤더 파싱: tag, priority, inner EtherType */
ethernet_vlan_header_t *v0 = (e0 + 1);
u16 inner_type = clib_net_to_host_u16 (v0->type);
/* QinQ: 이중 태그인 경우 한 단계 더 파싱 */
if (inner_type == ETHERNET_TYPE_VLAN)
inner_type = parse_second_vlan_tag (v0);
type0 = inner_type;
vlib_buffer_advance (b0, sizeof(*v0));
}
/* Sparse vector O(1) 룩업으로 다음 노드 결정 */
next0 = sparse_vec_index (em->l3_next.input_next_by_type, type0);
/* ... b1, b2, b3도 동일 패턴 반복 ... */
vlib_validate_buffer_enqueue_x4 (vm, node, next_index,
to_next, n_left_to_next,
bi0, bi1, bi2, bi3,
next0, next1, next2, next3);
n_left -= 4;
from += 4;
}
/* 잔여 패킷 single-loop 처리 */
while (n_left > 0)
{ /* ... 단일 패킷 처리 ... */ }
return frame->n_vectors;
}
코드 설명
-
13~16행
quad-loop 프리페치로 다음 4개 패킷의 버퍼 헤더를 캐시에 미리 로드합니다.
vlib_prefetch_buffer_header()는 메타데이터(첫 번째 캐시라인)만 프리페치합니다. - 24~25행 이더넷 헤더에서 EtherType을 추출하고 네트워크 바이트 순서(Byte Order)를 호스트 순서로 변환합니다. 이 값으로 L3 프로토콜을 결정합니다.
-
28~40행
VLAN 태그 처리는
PREDICT_FALSE로 감싸 분기 예측을 최적화합니다. 대부분 패킷은 태그 없는 일반 이더넷이므로, 이 경로는 거의 실행되지 않습니다. -
43행
sparse_vec_index()는 희소 벡터(sparse vector) 자료구조를 사용하여 EtherType → 다음 노드 인덱스를 O(1)에 매핑합니다. 해시 테이블(Hash Table)보다 캐시 친화적입니다.
ip4-input 노드 (ip4_input.c)
ip4-input 노드는 IPv4 패킷의 유효성을 검증하는 L3 진입점입니다. IP 헤더의 버전, 헤더 길이(IHL), 체크섬(Checksum), TTL 등을 검사하며, 유효하지 않은 패킷은 ip4-drop 노드로 전달합니다. Feature arc가 활성화된 인터페이스에서는 vnet_feature_arc_start()를 호출하여 feature 체인을 시작합니다.
| 검증 항목 | 조건 | 실패 시 동작 |
|---|---|---|
| IP 버전 | version == 4 | ip4-drop (BAD_VERSION) |
| 헤더 길이 | ihl >= 5 (20바이트 이상) | ip4-drop (BAD_LENGTH) |
| 총 길이 | total_length <= buffer_length | ip4-drop (BAD_LENGTH) |
| 체크섬 | ip4_header_checksum == 0 | ip4-drop (BAD_CHECKSUM) |
| TTL | ttl > 0 | icmp4-error (TTL_EXPIRED) |
| 소스 주소 | 멀티캐스트 소스 아님 | ip4-drop |
/* ip4_input_inline() — 헤더 검증 로직 (simplified pseudo-code) */
static_always_inline uword
ip4_input_inline (vlib_main_t *vm,
vlib_node_runtime_t *node,
vlib_frame_t *frame,
int verify_checksum)
{
u32 n_left, *from;
ip4_input_next_t next;
from = vlib_frame_vector_args (frame);
n_left = frame->n_vectors;
while (n_left > 0)
{
vlib_buffer_t *b0 = vlib_get_buffer (vm, from[0]);
ip4_header_t *ip0 = vlib_buffer_get_current (b0);
/* 1단계: IP 버전 및 헤더 길이 검증 */
u8 version0 = (ip0->ip_version_and_header_length >> 4);
u8 ihl0 = (ip0->ip_version_and_header_length & 0xF);
if (PREDICT_FALSE (version0 != 4 || ihl0 < 5))
{
next = IP4_INPUT_NEXT_DROP;
b0->error = node->errors[IP4_ERROR_BAD_VERSION];
goto enqueue;
}
/* 2단계: 체크섬 검증 (하드웨어 오프로드 안 된 경우) */
if (verify_checksum)
{
u16 sum0 = ip4_header_checksum (ip0);
if (PREDICT_FALSE (sum0 != ip0->checksum))
{
next = IP4_INPUT_NEXT_DROP;
b0->error = node->errors[IP4_ERROR_BAD_CHECKSUM];
goto enqueue;
}
}
/* 3단계: TTL 검사 */
if (PREDICT_FALSE (ip0->ttl == 0))
{
next = IP4_INPUT_NEXT_ICMP_ERROR;
b0->error = node->errors[IP4_ERROR_TTL_EXPIRED];
goto enqueue;
}
/* 4단계: Feature arc 시작 (인터페이스에 feature 설정 시) */
if (PREDICT_FALSE (b0->feature_arc_index != ~0))
{
vnet_feature_arc_start (im->feat_arc_index,
b0->sw_if_index[VLIB_RX],
&next, b0);
}
else
next = IP4_INPUT_NEXT_LOOKUP;
enqueue:
vlib_validate_buffer_enqueue_x1 (vm, node, next_index,
to_next, n_left_to_next,
bi0, next);
n_left--;
from++;
}
return frame->n_vectors;
}
코드 설명
-
18~20행
IP 버전과 IHL(Internet Header Length) 필드를 비트 연산으로 추출합니다.
PREDICT_FALSE로 감싸 비정상 패킷 경로의 분기 예측 비용을 최소화합니다. -
28~33행
verify_checksum매개변수로 체크섬 검증을 조건부 실행합니다. NIC 하드웨어 오프로드가 활성화된 경우 이 검증을 건너뛰어 CPU 사이클을 절약합니다. - 41~42행 TTL이 0이면 ICMP Time Exceeded 메시지를 생성하는 경로로 전환합니다. 이 검사는 라우터의 기본 의무이며, rewrite 단계에서 TTL을 감소시키기 전에 수행합니다.
-
48~53행
Feature arc가 설정된 인터페이스에서는
vnet_feature_arc_start()로 feature 체인을 시작합니다. 설정되지 않은 인터페이스는 바로 ip4-lookup으로 진행합니다.
ip4-lookup 노드 (ip4_forward.c)
ip4-lookup 노드는 목적지 IP 주소를 기반으로 FIB(Forwarding Information Base) 검색을 수행합니다. VPP는 mtrie(Multibit Trie) 자료구조를 사용하여 최장 접두사 매칭(LPM)을 수행하며, 검색 결과로 load-balance 객체를 얻습니다. 다중 경로(ECMP)인 경우 5-tuple flow hash를 계산하여 특정 adjacency를 선택합니다.
mtrie 검색은 3단계로 진행됩니다. 먼저 상위 16비트로 첫 번째 플라이(ply)를 검색하고, 결과가 리프가 아니면 다음 8비트로 두 번째 플라이, 마지막 8비트로 세 번째 플라이를 검색합니다. 대부분의 경로는 첫 번째 또는 두 번째 단계에서 리프에 도달하므로 매우 빠릅니다.
/* ip4_lookup_inline() — mtrie 검색 및 ECMP (simplified pseudo-code) */
static_always_inline uword
ip4_lookup_inline (vlib_main_t *vm,
vlib_node_runtime_t *node,
vlib_frame_t *frame)
{
ip4_fib_mtrie_t *mtrie0;
u32 n_left, *from;
from = vlib_frame_vector_args (frame);
n_left = frame->n_vectors;
while (n_left >= 4)
{
vlib_buffer_t *b0, *b1;
ip4_header_t *ip0, *ip1;
ip4_fib_mtrie_leaf_t leaf0, leaf1;
/* 프리페치: 다음 패킷의 IP 헤더를 캐시에 미리 로드 */
{
vlib_buffer_t *p2, *p3;
p2 = vlib_get_buffer (vm, from[2]);
p3 = vlib_get_buffer (vm, from[3]);
vlib_prefetch_buffer_header (p2, LOAD);
vlib_prefetch_buffer_header (p3, LOAD);
clib_prefetch_load (p2->data);
clib_prefetch_load (p3->data);
}
b0 = vlib_get_buffer (vm, from[0]);
b1 = vlib_get_buffer (vm, from[1]);
ip0 = vlib_buffer_get_current (b0);
ip1 = vlib_buffer_get_current (b1);
/* 1단계: FIB 테이블 선택 (VRF 기반) */
u32 fib_index0 = vec_elt (im->fib_index_by_sw_if_index,
b0->sw_if_index[VLIB_RX]);
mtrie0 = &ip4_fib_get(fib_index0)->mtrie;
/* 2단계: mtrie 검색 (최장 접두사 매칭) */
leaf0 = ip4_fib_mtrie_lookup_step_one (mtrie0, &ip0->dst_address);
leaf0 = ip4_fib_mtrie_lookup_step (mtrie0, leaf0,
&ip0->dst_address, 2);
leaf0 = ip4_fib_mtrie_lookup_step (mtrie0, leaf0,
&ip0->dst_address, 3);
/* 3단계: load-balance 객체 획득 */
u32 lbi0 = ip4_fib_mtrie_leaf_get_adj_index (leaf0);
load_balance_t *lb0 = load_balance_get (lbi0);
/* 4단계: ECMP — flow hash로 경로 선택 */
u32 hash0;
if (PREDICT_FALSE (lb0->lb_n_buckets > 1))
{
/* 5-tuple hash: src_ip, dst_ip, proto, src_port, dst_port */
hash0 = ip4_compute_flow_hash (ip0, lb0->lb_hash_config);
/* hash 값으로 버킷 인덱스 계산 */
hash0 = hash0 % lb0->lb_n_buckets;
}
else
hash0 = 0;
/* 5단계: adjacency 선택 → 다음 노드 결정 */
dpo_id_t *dpo0 = load_balance_get_bucket_i (lb0, hash0);
next0 = dpo0->dpoi_next_node;
vnet_buffer(b0)->ip.adj_index[VLIB_TX] = dpo0->dpoi_index;
/* ... b1도 동일 패턴 ... */
vlib_validate_buffer_enqueue_x2 (vm, node, next_index,
to_next, n_left_to_next,
bi0, bi1, next0, next1);
n_left -= 2;
from += 2;
}
return frame->n_vectors;
}
코드 설명
- 20~23행 dual-loop 프리페치 블록에서 다음 패킷의 버퍼 헤더와 데이터를 모두 캐시에 로드합니다. mtrie 검색은 데이터(IP 헤더)에 접근하므로 데이터 프리페치가 필수입니다.
- 33~43행 mtrie 3단계 검색을 수행합니다. 상위 16비트, 중간 8비트, 하위 8비트 순으로 탐색하며, 대부분의 라우팅 테이블(Routing Table)에서는 1~2단계에서 리프에 도달하여 매우 빠릅니다.
- 46~57행 load-balance 객체에서 ECMP 경로를 선택합니다. 5-tuple flow hash를 계산하여 동일 플로우가 항상 같은 경로로 전달되도록 보장하며, 세션 단위 일관성을 유지합니다.
-
60~62행
선택된 DPO(Data-Path Object)에서 다음 노드와 adjacency 인덱스를 추출하고,
vnet_bufferopaque 영역에 저장합니다. ip4-rewrite 노드가 이 인덱스를 사용하여 MAC 헤더를 재작성합니다.
ip4-rewrite 노드 (ip4_forward.c)
ip4-rewrite 노드는 패킷 전달의 마지막 L3 처리 단계로, adjacency에 저장된 rewrite data를 패킷에 적용합니다. MAC 주소 재작성, TTL 감소, IP 체크섬 incremental 업데이트를 수행하며, 완료된 패킷을 interface-output 노드로 전달합니다.
체크섬 업데이트는 전체 재계산 대신 incremental 방식을 사용합니다. TTL이 1 감소하면 체크섬에 0x0100을 더하는 것으로 충분하며, 이는 전체 헤더를 다시 순회하는 것보다 훨씬 빠릅니다. vnet_rewrite_one_header()와 vnet_rewrite_two_headers()는 adjacency의 rewrite string을 이더넷 헤더 위치에 복사하여 목적지/소스 MAC 주소와 EtherType을 한 번에 기록합니다.
/* ip4_rewrite_inline() — dual-loop 패턴 (simplified pseudo-code) */
static_always_inline uword
ip4_rewrite_inline (vlib_main_t *vm,
vlib_node_runtime_t *node,
vlib_frame_t *frame,
int do_counters, int is_midchain, int is_mcast)
{
u32 n_left, *from;
ip_adjacency_t *adj0, *adj1;
from = vlib_frame_vector_args (frame);
n_left = frame->n_vectors;
while (n_left >= 2)
{
vlib_buffer_t *b0, *b1;
ip4_header_t *ip0, *ip1;
u32 adj_index0, adj_index1;
u32 next0, next1;
/* 프리페치: 다음 패킷 및 adjacency 데이터 */
vlib_prefetch_buffer_header (b[2], LOAD);
vlib_prefetch_buffer_header (b[3], LOAD);
b0 = vlib_get_buffer (vm, from[0]);
b1 = vlib_get_buffer (vm, from[1]);
ip0 = vlib_buffer_get_current (b0);
ip1 = vlib_buffer_get_current (b1);
/* 1단계: adjacency 획득 (lookup에서 설정한 인덱스) */
adj_index0 = vnet_buffer(b0)->ip.adj_index[VLIB_TX];
adj0 = adj_get (adj_index0);
/* 2단계: TTL 감소 */
ip0->ttl -= 1;
/* 3단계: 체크섬 incremental 업데이트
* TTL 1 감소 = 체크섬에 0x0100 가산 (one's complement) */
u32 sum0 = ip0->checksum + clib_host_to_net_u16 (0x0100);
sum0 += (sum0 >= 0xFFFF); /* carry 처리 */
ip0->checksum = sum0;
/* 4단계: TTL 만료 확인 → ICMP 생성 */
if (PREDICT_FALSE (ip0->ttl == 0))
{
next0 = IP4_REWRITE_NEXT_ICMP_ERROR;
goto enqueue;
}
/* 5단계: MAC 헤더 재작성 (adjacency rewrite data 복사) */
vnet_rewrite_two_headers (adj0[0], adj1[0], ip0, ip1,
sizeof (ethernet_header_t));
/* rewrite_two_headers는 다음을 수행합니다:
* - 버퍼 포인터를 L2 헤더 시작으로 후퇴
* - adjacency의 rewrite_data를 복사 (dst MAC + src MAC + EtherType)
* - 두 패킷을 동시 처리하여 캐시 활용 극대화 */
/* 6단계: 출력 인터페이스 설정 */
vnet_buffer(b0)->sw_if_index[VLIB_TX] = adj0->rewrite_header.sw_if_index;
next0 = adj0->rewrite_header.next_index; /* → interface-output */
enqueue:
vlib_validate_buffer_enqueue_x2 (vm, node, next_index,
to_next, n_left_to_next,
bi0, bi1, next0, next1);
n_left -= 2;
from += 2;
}
/* 잔여 패킷 single-loop 처리 */
while (n_left > 0)
{ /* ... 단일 패킷 처리 ... */ }
return frame->n_vectors;
}
코드 설명
-
27~28행
lookup 단계에서 저장한 adjacency 인덱스를
vnet_bufferopaque에서 읽습니다. 노드 간 메타데이터 전달에 버퍼 opaque 영역을 사용하는 VPP의 핵심 패턴입니다. - 34~37행 체크섬 incremental 업데이트는 TTL 감소분(0x0100)만 가산합니다. one's complement carry 처리를 한 줄로 수행하며, 전체 헤더 재계산 대비 10배 이상 빠릅니다.
-
46~48행
vnet_rewrite_two_headers()는 2개 패킷의 MAC 헤더를 동시에 재작성합니다. adjacency에 사전 계산된 rewrite 문자열(dst MAC + src MAC + EtherType)을 memcpy로 한 번에 기록합니다. -
55~56행
출력 인터페이스와 다음 노드를 adjacency의 rewrite_header에서 읽습니다. 일반적으로
interface-output노드로 전달되어 최종 NIC 송신이 수행됩니다.
추가 그래프 노드 내부 구현
앞서 살펴본 ethernet-input, ip4-input, ip4-lookup, ip4-rewrite 외에도 VPP 그래프에는 수십 개의 핵심 노드가 존재합니다. 이 절에서는 DPDK 입력, TCP 처리, NAT44, L2 포워딩 노드의 내부 구현을 상세히 분석합니다.
VLIB_REGISTER_NODE의 .type 필드를 먼저 확인하면 흐름을 빠르게 잡을 수 있습니다.
dpdk-input 노드 (dpdk/device/dpdk.h)
dpdk-input은 INPUT 타입 노드로, 그래프의 최상단에서 NIC 수신 링을 폴링하여 패킷 벡터를 생성합니다. VPP의 모든 패킷 처리는 이 노드에서 시작됩니다.
INPUT 노드 폴링 메커니즘은 다음과 같이 동작합니다:
rte_eth_rx_burst()호출: DPDK의 PMD(Poll Mode Driver)를 통해 NIC RX 링에서 최대VLIB_FRAME_SIZE(256)개의 패킷을 배치로 수집합니다. 이 호출은 커널 인터럽트(Interrupt) 없이 직접 하드웨어 디스크립터를 읽습니다.rte_mbuf→vlib_buffer_t변환: DPDK의rte_mbuf메타데이터를 VPP 내부의vlib_buffer_t구조체로 변환합니다. 버퍼 풀에서 사전 할당된 공유 메모리 영역을 사용하므로 복사 없이 포인터 산술로 변환이 완료됩니다.- 벡터 프레임 생성: 변환된 버퍼 인덱스 배열을
vlib_frame_t에 채우고, 다음 노드인ethernet-input으로 전달합니다. 이때 프레임의n_vectors필드가 실제 수집된 패킷 수로 설정됩니다. - Adaptive polling: 연속으로 패킷이 없는 상황이 감지되면 인터럽트 모드로 전환하여 CPU 사용률을 줄입니다. 패킷이 다시 도착하면 즉시 폴링 모드로 복귀합니다. 이 동작은
dpdk { ... }설정의poll-sleep-usec로 조정할 수 있습니다.
/* dpdk_device_input() — DPDK RX 폴링 및 벡터 생성 (simplified pseudo-code) */
static uword
dpdk_device_input (vlib_main_t *vm, dpdk_device_t *xd,
vlib_node_runtime_t *node, u16 queue_id)
{
u32 n_buffers, n_left;
u32 next_index = VNET_DEVICE_INPUT_NEXT_ETHERNET_INPUT;
u32 buffer_indices[VLIB_FRAME_SIZE];
struct rte_mbuf *mbufs[VLIB_FRAME_SIZE];
/* 1단계: DPDK PMD를 통해 NIC RX 링에서 패킷 배치 수집 */
n_buffers = rte_eth_rx_burst (xd->port_id, queue_id,
mbufs, VLIB_FRAME_SIZE);
if (n_buffers == 0)
{
/* adaptive polling: 패킷 없으면 인터럽트 모드 전환 고려 */
if (xd->flags & DPDK_DEVICE_FLAG_INT_SUPPORTED)
dpdk_device_enable_interrupt (xd, queue_id);
return 0;
}
/* 2단계: rte_mbuf → vlib_buffer_t 변환 (제로 카피) */
for (int i = 0; i < n_buffers; i++)
{
vlib_buffer_t *b;
/* mbuf 주소에서 vlib_buffer_t 오프셋 계산 (공유 메모리) */
b = vlib_buffer_from_rte_mbuf (mbufs[i]);
buffer_indices[i] = vlib_get_buffer_index (vm, b);
/* 버퍼 메타데이터 초기화 */
b->current_length = mbufs[i]->pkt_len;
b->current_data = 0;
b->total_length_not_including_first_buffer = 0;
/* 오프로드 플래그 전파: checksum, VLAN 등 */
if (mbufs[i]->ol_flags & RTE_MBUF_F_RX_IP_CKSUM_GOOD)
b->flags |= VNET_BUFFER_F_L4_CHECKSUM_COMPUTED;
}
/* 3단계: 벡터 프레임 생성 및 ethernet-input으로 enqueue */
n_left = n_buffers;
while (n_left > 0)
{
u32 *to_next, n_left_to_next;
vlib_get_next_frame (vm, node, next_index,
to_next, n_left_to_next);
u32 n_copy = clib_min (n_left, n_left_to_next);
clib_memcpy_fast (to_next,
buffer_indices + (n_buffers - n_left),
n_copy * sizeof (u32));
n_left_to_next -= n_copy;
n_left -= n_copy;
vlib_put_next_frame (vm, node, next_index, n_left_to_next);
}
/* 인터페이스 카운터 갱신 */
vlib_increment_combined_counter (
&vnet_main.interface_main.combined_sw_if_counters
[VNET_INTERFACE_COUNTER_RX],
vm->thread_index, xd->sw_if_index,
n_buffers, /* packets */
total_bytes /* bytes */);
return n_buffers;
}
코드 설명
-
11~13행
rte_eth_rx_burst()는 DPDK PMD를 통해 커널 바이패스로 NIC RX 링에서 최대 256개 패킷을 배치 수집합니다. 시스템 콜(System Call)과 인터럽트가 없어 지연가 수백 나노초 수준입니다. - 16~18행 Adaptive polling은 연속 빈 폴링 시 인터럽트 모드로 전환합니다. 유휴 상태에서 CPU 사용률을 0%로 줄이면서, 패킷 도착 시 즉시 폴링 모드로 복귀합니다.
-
22~26행
rte_mbuf에서vlib_buffer_t로의 변환은 포인터 산술만으로 수행됩니다. 두 구조체가 같은 hugepage 공유 메모리에 배치되어 있으므로 데이터 복사가 불필요합니다. -
40~43행
clib_memcpy_fast()로 버퍼 인덱스 배열을 프레임에 복사한 후vlib_put_next_frame()으로 ethernet-input 노드에 전달합니다. 이 시점에서 패킷이 그래프 처리 파이프라인에 진입합니다.
tcp-input 노드 (tcp_input.c)
tcp-input 노드는 VPP의 호스트 스택(Host Stack) 내에서 TCP 세그먼트를 수신하고 상태 머신을 구동하는 핵심 노드입니다. 일반적인 커널 TCP 스택과 달리 VPP는 사용자 공간에서 세션 레이어를 직접 관리합니다.
TCP 상태 머신 처리 흐름은 다음과 같습니다:
tcp_input_inline()함수: 수신 세그먼트의 TCP 헤더를 파싱하고, 4-tuple(src/dst IP, src/dst port) 해시로 기존 연결을 검색합니다. 연결이 존재하면 해당tcp_connection_t의 현재 상태에 따라 처리 함수를 디스패치합니다.- 상태별 디스패치:
ESTABLISHED상태는 fast-path로 처리되어 대부분의 데이터 전송 세그먼트가 최소 분기로 처리됩니다.SYN_RCVD,FIN_WAIT_1,FIN_WAIT_2,CLOSE_WAIT,CLOSING,LAST_ACK,TIME_WAIT상태는 각각 전용 핸들러(Handler)에서 처리됩니다. - ACK 처리:
tcp_rcv_ack()에서 SACK(Selective ACK) 스코어보드를 갱신하고, 확인된 바이트에 대한 congestion window를 조정합니다. Reno, CUBIC, NewReno 등 혼잡 제어(Congestion Control) 알고리즘을 플러그인 방식으로 교체할 수 있습니다. - 재전송(Retransmission) 타이머와 RTO 계산: 각 연결의 SRTT(Smoothed RTT)와 RTTVAR를 갱신하고, RFC 6298 기반으로 RTO(Retransmission Timeout)를 계산합니다. 타이머 만료 시
tcp-timer프로세스 노드가 재전송을 트리거합니다.
/* tcp_input_inline() — ESTABLISHED fast-path (simplified pseudo-code) */
static uword
tcp_input_inline (vlib_main_t *vm, vlib_node_runtime_t *node,
vlib_frame_t *frame, u8 is_ip4)
{
u32 n_left = frame->n_vectors;
u32 *from = vlib_frame_vector_args (frame);
while (n_left > 0)
{
vlib_buffer_t *b = vlib_get_buffer (vm, from[0]);
tcp_header_t *tcp = vlib_buffer_get_current (b);
tcp_connection_t *tc;
/* 4-tuple 해시로 기존 연결 검색 */
tc = tcp_lookup_connection (b, vm->thread_index, is_ip4);
if (PREDICT_FALSE (!tc))
{
/* SYN 패킷이면 tcp-listen으로, 아니면 RST 전송 */
goto next_packet;
}
/* ESTABLISHED 상태 fast-path */
if (PREDICT_TRUE (tc->state == TCP_STATE_ESTABLISHED))
{
/* 시퀀스 번호 검증 */
if (PREDICT_FALSE (
!tcp_segment_in_rcv_wnd (tc, tcp)))
{
tcp_send_ack (tc);
goto next_packet;
}
/* ACK 처리: snd_una 전진, 혼잡 윈도우 갱신 */
if (tcp->flags & TCP_FLAG_ACK)
{
tcp_rcv_ack (tc, tcp, b);
/* SACK 스코어보드 갱신 */
if (tcp_opts_sack_present (&tc->rcv_opts))
tcp_rcv_sacks (tc, tc->snd_una);
}
/* 데이터 수신: 순서대로면 즉시 전달, 아니면 재정렬 큐 */
if (data_len > 0)
{
if (PREDICT_TRUE (
seq == tc->rcv_nxt))
tcp_session_enqueue_data (tc, b, data_len);
else
tcp_session_enqueue_ooo (tc, b, data_len);
/* 지연 ACK 타이머 시작 또는 즉시 ACK */
tcp_maybe_send_ack (tc);
}
/* RTT 샘플 갱신 (RFC 6298) */
if (tc->rtt_ts && tcp_ack_is_newer (tc, tcp))
{
f64 rtt_sample = tcp_time_now () - tc->rtt_ts;
tcp_update_rto (tc, rtt_sample);
}
}
else
{
/* slow-path: 상태별 핸들러 디스패치 */
switch (tc->state)
{
case TCP_STATE_SYN_RCVD:
tcp_rcv_state_syn_rcvd (tc, tcp, b);
break;
case TCP_STATE_FIN_WAIT_1:
tcp_rcv_state_fin_wait_1 (tc, tcp, b);
break;
case TCP_STATE_FIN_WAIT_2:
tcp_rcv_state_fin_wait_2 (tc, tcp, b);
break;
case TCP_STATE_CLOSE_WAIT:
tcp_rcv_state_close_wait (tc, tcp, b);
break;
case TCP_STATE_TIME_WAIT:
tcp_rcv_state_time_wait (tc, tcp, b);
break;
default:
break;
}
}
next_packet:
from++;
n_left--;
}
return frame->n_vectors;
}
코드 설명
-
12~17행
4-tuple 해시로 기존 TCP 연결을 O(1)에 검색합니다.
PREDICT_FALSE로 연결 미발견 경로를 예측 실패로 표시하여, 대부분의 패킷이 기존 연결에 매칭되는 fast-path를 최적화합니다. - 23~28행 ESTABLISHED 상태 fast-path에서 시퀀스 번호를 수신 윈도우와 대조합니다. 윈도우 밖의 패킷은 즉시 ACK를 전송하고 폐기하여 불필요한 처리를 방지합니다.
-
32~38행
순서대로 도착한 데이터는
tcp_session_enqueue_data()로 세션 FIFO에 직접 삽입하고, 순서가 어긋난 데이터는 재정렬 큐에 버퍼링합니다. 대부분 패킷이 순서대로 도착하므로PREDICT_TRUE로 최적화합니다. - 43~47행 RTT 샘플을 측정하여 RTO(Retransmission Timeout)를 RFC 6298에 따라 갱신합니다. 정확한 RTT 추적은 혼잡 제어와 재전송 타이머의 핵심입니다.
- 51~63행 ESTABLISHED 외 상태는 slow-path로 분기하여 상태별 핸들러를 호출합니다. TCP 상태 머신의 복잡한 전이 로직이 여기서 처리됩니다.
nat44-in2out / nat44-out2in 노드
NAT44 ED(Endpoint-Dependent) 모드는 VPP의 고성능 NAT 구현으로, 전체 5-tuple을 기반으로 세션을 관리합니다. nat44-in2out은 내부에서 외부 방향, nat44-out2in은 외부에서 내부 방향의 주소 변환(Address Translation)(NAT)을 수행합니다.
NAT44 Endpoint-Dependent 처리 흐름은 다음과 같습니다:
nat44_ed_in2out_node_fn_inline(): 패킷의 source IP/port, destination IP/port, protocol 5-tuple을 추출하고, 세션 테이블에서 기존 매핑을 검색합니다.- 세션 테이블 룩업:
clib_bihash(bounded-index extensible hash) 기반 해시 테이블에서 5-tuple 키로snat_session_t를 검색합니다. 히트 시 기존 변환 규칙을 즉시 적용합니다. - 세션 미스 시 동적 할당: 주소 풀에서 가용 외부 IP를 선택하고, 포트 범위 내에서 사용 가능한 포트를 할당합니다. 할당된 매핑으로 새
snat_session_t를 생성하고 in2out/out2in 양방향 해시에 삽입합니다. - 패킷 변환: source IP와 source port를 할당된 외부 주소/포트로 교체하고, IP 헤더 체크섬과 L4(TCP/UDP) 체크섬을 incremental update 방식으로 갱신합니다. 전체 재계산 대신 변경된 필드의 차분만 반영하여 성능을 최적화합니다.
/* nat44_ed_in2out_node_fn_inline() — NAT44 ED in2out 처리 (simplified pseudo-code) */
static uword
nat44_ed_in2out_node_fn_inline (vlib_main_t *vm,
vlib_node_runtime_t *node,
vlib_frame_t *frame,
int is_slow_path)
{
snat_main_t *sm = &snat_main;
u32 n_left = frame->n_vectors;
u32 *from = vlib_frame_vector_args (frame);
u32 thread_index = vm->thread_index;
while (n_left > 0)
{
vlib_buffer_t *b = vlib_get_buffer (vm, from[0]);
ip4_header_t *ip = vlib_buffer_get_current (b);
snat_session_t *s;
u32 next = NAT44_ED_IN2OUT_NEXT_LOOKUP;
/* 1단계: 5-tuple 추출 (src/dst IP, src/dst port, protocol) */
nat_ed_ses_key_t kv;
nat44_ed_make_key (&kv, ip->src_address, ip->dst_address,
ip4_get_src_port (ip),
ip4_get_dst_port (ip),
ip->protocol,
vnet_buffer(b)->sw_if_index[VLIB_RX]);
/* 2단계: 세션 테이블 룩업 (clib_bihash) */
if (clib_bihash_search_16_8 (&sm->flow_hash,
&kv, &value) == 0)
{
/* 세션 히트: 기존 매핑 적용 */
s = pool_elt_at_index (
sm->per_thread_data[thread_index].sessions,
value.value);
}
else
{
/* 세션 미스: 새 매핑 생성 (slow-path) */
if (!is_slow_path)
{
next = NAT44_ED_IN2OUT_NEXT_SLOW_PATH;
goto enqueue;
}
/* 주소 풀에서 외부 IP 선택 */
snat_address_t *a;
u16 ext_port;
nat44_ed_alloc_addr_and_port (
sm, thread_index, ip->protocol,
&a, &ext_port);
/* 새 세션 생성 및 양방향 해시 삽입 */
s = nat44_ed_session_alloc (sm, thread_index);
s->in2out.addr = ip->src_address;
s->in2out.port = ip4_get_src_port (ip);
s->out2in.addr = a->addr;
s->out2in.port = ext_port;
s->ext_host_addr = ip->dst_address;
s->ext_host_port = ip4_get_dst_port (ip);
/* in2out 방향 해시 삽입 */
nat44_ed_session_add_to_flow_hash (sm, s, thread_index);
}
/* 3단계: 패킷 변환 — src IP/port 교체 */
ip4_address_t old_addr = ip->src_address;
u16 old_port = ip4_get_src_port (ip);
ip->src_address = s->out2in.addr;
ip4_set_src_port (ip, s->out2in.port);
/* 4단계: 체크섬 incremental update */
ip_csum_update (ip->checksum,
old_addr.as_u32, s->out2in.addr.as_u32,
ip4_header_t,
src_address);
/* L4 체크섬도 incremental update (TCP/UDP) */
tcp_udp_csum_update (ip, old_addr, old_port,
s->out2in.addr, s->out2in.port);
/* 세션 타임스탬프 갱신 (LRU 에이징용) */
s->last_heard = vlib_time_now (vm);
enqueue:
vlib_validate_buffer_enqueue_x1 (vm, node, next_index,
to_next, n_left_to_next,
from[0], next);
from++;
n_left--;
}
return frame->n_vectors;
}
코드 설명
- 14~18행 5-tuple을 추출하여 세션 키를 구성합니다. Endpoint-Dependent 모드는 소스/목적지 IP와 포트를 모두 사용하므로, 같은 내부 호스트의 서로 다른 연결에 각각 다른 외부 포트를 할당합니다.
-
21~28행
clib_bihash_search_16_8으로 128비트 키 해시 테이블에서 기존 세션을 O(1)에 검색합니다. 히트 시pool_elt_at_index()로 세션 객체를 직접 참조합니다. - 33~56행 세션 미스 시 slow-path에서 주소 풀로부터 외부 IP/포트를 할당하고, 양방향(in2out + out2in) 해시 엔트리를 동시에 삽입합니다. 이후 같은 플로우의 패킷은 fast-path로 처리됩니다.
- 59~72행 IP 헤더와 L4 체크섬을 incremental 방식으로 갱신합니다. 변경된 필드의 차이값만 계산하므로 전체 재계산보다 훨씬 빠르며, NAT의 성능 병목(Bottleneck)을 해소합니다.
l2-input / l2-fwd 노드
VPP의 L2 브릿지 도메인(Bridge Domain)은 전통적인 스위치의 포워딩 기능을 사용자 공간에서 구현합니다. l2-input 노드가 L2 처리 파이프라인의 진입점이고, l2-fwd 노드가 MAC 테이블 기반 포워딩을 수행합니다.
L2 브릿지 도메인 처리 흐름은 다음과 같습니다:
l2_input_node_fn(): 인터페이스의sw_if_index에서 소속 bridge-domain을 확인하고, 해당 bridge-domain의 Feature Arc(learning, forwarding, flooding, ARP termination 등)를 설정합니다. Feature bitmap에 따라 활성화된 기능 노드들이 순차 실행됩니다.l2_fwd_node_fn(): destination MAC 주소로 MAC 테이블을 룩업하여 출력 인터페이스를 결정합니다. MAC 테이블은clib_bihash기반으로 구현되어 수백만 엔트리를 효율적으로 검색합니다.- MAC 러닝: source MAC 주소가 테이블에 없으면 현재 입력 인터페이스와 매핑하여 새 엔트리를 삽입합니다. 에이징 타이머(기본 300초)가 만료되면 엔트리가 자동 삭제됩니다.
- BUM 트래픽 플러딩: Broadcast, Unknown unicast, Multicast 트래픽은
l2-flood노드로 전달되어 bridge-domain 내 모든 멤버 인터페이스로 복제됩니다. 입력 인터페이스는 플러딩 대상에서 제외됩니다.
/* l2_fwd_node_fn() — L2 MAC 룩업 포워딩 (simplified pseudo-code) */
static uword
l2_fwd_node_fn (vlib_main_t *vm,
vlib_node_runtime_t *node,
vlib_frame_t *frame)
{
l2fib_main_t *fm = &l2fib_main;
u32 n_left = frame->n_vectors;
u32 *from = vlib_frame_vector_args (frame);
while (n_left > 0)
{
vlib_buffer_t *b = vlib_get_buffer (vm, from[0]);
ethernet_header_t *eth = vlib_buffer_get_current (b);
u32 bd_index = vnet_buffer(b)->l2.bd_index;
u32 next;
/* 1단계: destination MAC으로 L2 FIB 검색 */
l2fib_entry_key_t key;
l2fib_entry_result_t result;
l2fib_make_key (&key, eth->dst_address, bd_index);
if (clib_bihash_search_8_8 (&fm->mac_table,
&key, &result) == 0)
{
/* MAC 히트: 출력 인터페이스 결정 */
u32 sw_if_index = result.fields.sw_if_index;
/* 입력 = 출력이면 드롭 (split-horizon) */
if (PREDICT_FALSE (
sw_if_index == vnet_buffer(b)->sw_if_index[VLIB_RX]))
{
next = L2FWD_NEXT_DROP;
goto enqueue;
}
/* 출력 인터페이스 설정 */
vnet_buffer(b)->sw_if_index[VLIB_TX] = sw_if_index;
next = L2FWD_NEXT_L2_OUTPUT;
/* 필터링 엔트리이면 드롭 */
if (PREDICT_FALSE (result.fields.filter))
next = L2FWD_NEXT_DROP;
/* 정적 엔트리가 아니면 에이징 타임스탬프 갱신 */
if (!result.fields.static_mac)
result.fields.timestamp = l2fib_cur_age (fm);
}
else
{
/* MAC 미스: BUM 트래픽으로 간주, flood 경로 */
next = L2FWD_NEXT_FLOOD;
}
/* 2단계: source MAC 러닝 */
if (PREDICT_TRUE (
vnet_buffer(b)->l2.feature_bitmap & L2INPUT_FEAT_LEARN))
{
l2fib_entry_key_t src_key;
l2fib_entry_result_t src_result;
l2fib_make_key (&src_key, eth->src_address, bd_index);
if (clib_bihash_search_8_8 (&fm->mac_table,
&src_key, &src_result) != 0)
{
/* 새 source MAC: 엔트리 삽입 (러닝) */
src_result.fields.sw_if_index =
vnet_buffer(b)->sw_if_index[VLIB_RX];
src_result.fields.static_mac = 0;
src_result.fields.timestamp = l2fib_cur_age (fm);
clib_bihash_add_del_8_8 (&fm->mac_table,
&src_key, &src_result,
1 /* is_add */);
/* MAC 러닝 이벤트 로그 */
l2fib_learn_event (fm, eth->src_address,
bd_index,
vnet_buffer(b)->sw_if_index[VLIB_RX]);
}
else if (src_result.fields.sw_if_index !=
vnet_buffer(b)->sw_if_index[VLIB_RX])
{
/* MAC 이동: 인터페이스 갱신 */
src_result.fields.sw_if_index =
vnet_buffer(b)->sw_if_index[VLIB_RX];
clib_bihash_add_del_8_8 (&fm->mac_table,
&src_key, &src_result,
1);
}
}
enqueue:
vlib_validate_buffer_enqueue_x1 (vm, node, next_index,
to_next, n_left_to_next,
from[0], next);
from++;
n_left--;
}
return frame->n_vectors;
}
코드 설명
-
14~21행
destination MAC으로
clib_bihash_8_8해시 테이블을 검색합니다. 키는 MAC 주소(6바이트) + 브릿지 도메인 인덱스로 구성되며, O(1) 룩업으로 출력 인터페이스를 결정합니다. - 24~30행 Split-horizon 체크는 입력과 출력 인터페이스가 같은 경우 루프를 방지합니다. 필터링 엔트리이면 패킷을 드롭하여 특정 MAC 주소를 차단합니다.
- 41~45행 MAC 테이블에 없는 목적지는 BUM(Broadcast/Unknown unicast/Multicast) 트래픽으로 간주하여 flood 경로로 전환합니다. 브릿지 도메인의 모든 멤버 포트로 복제 전송됩니다.
- 48~72행 source MAC 러닝은 패킷의 출발지 MAC을 테이블에 학습합니다. 새 MAC이면 엔트리를 삽입하고, 이미 존재하지만 인터페이스가 다르면 MAC 이동(migration)으로 갱신합니다.
Process Nodes — vlib_process_signal_event
기초 문서에서 언급한 네 가지 노드 유형 중 PROCESS 노드는 다른 세 유형과 본질적으로 다릅니다. INPUT/INTERNAL 노드는 패킷 벡터를 받아 즉시 처리하는 반면, PROCESS 노드는 타이머·이벤트·컨디션 변수를 기다릴 수 있는 장기 실행 코루틴입니다. 제어 평면 로직(BFD, BGP, DHCP 클라이언트, 플러그인 상태 머신)이 주로 여기에 구현됩니다.
프로세스 노드 생명주기
/* src/vlib/node.h — 프로세스 노드 함수 시그니처 */
static uword
my_process_node(vlib_main_t *vm, vlib_node_runtime_t *rt, vlib_frame_t *f)
{
uword event_type, *event_data = 0;
f64 timeout = 10.0; /* 10초 */
while (1) {
vlib_process_wait_for_event_or_clock(vm, timeout);
event_type = vlib_process_get_events(vm, &event_data);
switch (event_type) {
case ~0: /* 타임아웃 */
/* 주기 작업 수행 */
break;
case EVENT_RECONFIG:
/* 다른 노드가 보낸 신호에 반응 */
break;
}
vec_reset_length(event_data);
}
return 0;
}
VLIB_REGISTER_NODE(my_process_node) = {
.function = my_process_node,
.type = VLIB_NODE_TYPE_PROCESS,
.name = "my-process",
};
외부에서 프로세스 노드에 신호 보내기
다른 노드(예: CLI 핸들러, API 핸들러, 입력 노드)에서 PROCESS 노드를 깨우려면 vlib_process_signal_event()를 호출합니다.
vlib_node_t *n = vlib_get_node_by_name(vm, (u8 *) "my-process");
vlib_process_signal_event(vm, n->index, EVENT_RECONFIG, /* data */ 42);
세 번째 인자는 이벤트 타입이고, 네 번째는 이벤트와 함께 전달할 데이터(uword)입니다. vlib_process_get_events()가 같은 타입의 여러 이벤트를 벡터로 모아 리턴하므로, 폭주 상황에서도 일괄 처리할 수 있습니다.
실전 패턴
- 주기 상태 점검 — 타임아웃만 사용. 예: ARP/ND age-out, 세션 GC, Stats 수집
- 이벤트 구동 — 타임아웃 무시하고 이벤트만 대기. 예: 플러그인 설정 reload, TLS 인증서 rotation 신호
- 혼합 — 타임아웃으로 주기 작업하되, 긴급 이벤트는 즉시 처리. 예: BFD 상태 머신
vlib_process_suspend()로 제어권을 넘기거나, 작업을 여러 틱으로 쪼개야 합니다.