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)를 극대화합니다.

핵심 그래프 노드 호출 관계와 책임 분담 dpdk-input rte_eth_rx_burst NIC → vlib_buffer_t ethernet-input sparse vector EtherType → 분기 arp-input (0x0806) ip4-input (0x0800) ip6-input (0x86DD) mpls-input (0x8847) ip4-input checksum · TTL ip4_input_inline() ip4-lookup mtrie 8-8-8-8 LPM → adj/dpo ip4-rewrite MAC 재작성 · TTL-- adj 결과 적용 interface-output 출력 인터페이스 선택 *-tx dpdk/af-pkt/... NIC TX 링 Feature Arc: ip4-unicast — ACL · NAT · SNAT · Policer · IPsec Policy · 커스텀 플러그인 노드가 여기 삽입됨 error-drop (공용 드롭) ip4-punt (host 전달) ip4-local (self 목적지) ip4-mcast 모든 노드는 quad-loop로 벡터를 처리하고, 분기는 next_index 배열에 채워 vlib_validate_buffer_enqueue_*로 한 번에 전달합니다. 아래 소절의 코드 분석은 이 그래프의 각 박스가 실제로 어떤 내부 함수를 호출하는지 보여줍니다.
읽는 팁: 이 절의 코드는 4가지 관점에서 읽으면 좋습니다. ① 함수 시그니처(어떤 컨텍스트를 받는가) ② 벡터 루프 구조(quad/dual-loop) ③ 분기 결정(next_index를 어떻게 채우는가) ④ 트레이스/에러 카운터(관측성 훅이 어디에 있는가). 이 네 가지는 어느 핵심 노드에서도 동일한 패턴으로 반복됩니다.

ethernet-input 노드 (node.c)

ethernet-input 노드는 패킷 그래프의 최초 L2 처리 지점으로, 이더넷 프레임의 EtherType을 분류하여 적절한 다음 노드로 디스패치합니다. 핵심 구현은 ethernet_input_inline() 함수에 있으며, sparse vector를 이용한 O(1) EtherType 룩업이 특징입니다.

EtherType다음 노드설명
ETHERNET_TYPE_IP40x0800ip4-inputIPv4 패킷 처리
ETHERNET_TYPE_IP60x86DDip6-inputIPv6 패킷 처리
ETHERNET_TYPE_ARP0x0806arp-inputARP 요청/응답
ETHERNET_TYPE_MPLS0x8847mpls-inputMPLS 레이블 처리
ETHERNET_TYPE_VLAN0x8100(내부 처리)Single VLAN 태그
ETHERNET_TYPE_DOT1AD0x88A8(내부 처리)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 == 4ip4-drop (BAD_VERSION)
헤더 길이ihl >= 5 (20바이트 이상)ip4-drop (BAD_LENGTH)
총 길이total_length <= buffer_lengthip4-drop (BAD_LENGTH)
체크섬ip4_header_checksum == 0ip4-drop (BAD_CHECKSUM)
TTLttl > 0icmp4-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_buffer opaque 영역에 저장합니다. 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_buffer opaque에서 읽습니다. 노드 간 메타데이터 전달에 버퍼 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 포워딩 노드의 내부 구현을 상세히 분석합니다.

추가 분석 노드 맵 — 유형 · 위치 · 책임 INPUT 타입 INTERNAL 타입 Feature 노드 PROCESS 타입 dpdk-input rte_eth_rx_burst 폴링 최대 256 패킷 벡터 mbuf → vlib_buffer 변환 l2-input / l2-fwd MAC 학습 · BD 조회 bihash 기반 FIB L2 브릿징 처리 nat44-in2out / out2in ip4-unicast arc 세션 bihash 룩업 포트 재작성 · 5-tuple nat44-cleanup 수명 기반 세션 만료 이벤트/클록 대기 배리어 아래서 정리 tcp4-input / established 세션 레이어 진입 상태 머신 · 페이로드 큐잉 svm_fifo_enqueue ip4-midchain 터널 캡슐화(IPsec/VXLAN) adj-midchain 엔트리 재귀 룩업 지점 tcp-timer-process RTO · KEEPALIVE 처리 타이머 휠 스캔 워커별 인스턴스 이 절에서 배울 4가지 공통 패턴 ① quad/dual-loop로 벡터를 4/2개씩 파이프라이닝 ② CLIB_PREFETCH로 다음 패킷을 L1에 미리 적재 ③ next_index 배열을 채워 validate_buffer_enqueue_x1/x4 한 번으로 전달 ④ vlib_node_increment_counter로 관측성 훅 삽입
INPUT · INTERNAL · PROCESS의 차이: INPUT 노드는 프레임을 만들어내는 쪽(NIC/pg), INTERNAL 노드는 프레임을 받아 가공해서 다음에 넘기는 쪽, PROCESS 노드는 패킷 흐름 바깥에서 이벤트/타이머로 깨어나 제어 평면 작업을 수행합니다. 같은 파일 안에서도 이 세 유형이 섞여 있어 코드를 읽을 때 VLIB_REGISTER_NODE.type 필드를 먼저 확인하면 흐름을 빠르게 잡을 수 있습니다.

dpdk-input 노드 (dpdk/device/dpdk.h)

dpdk-inputINPUT 타입 노드로, 그래프의 최상단에서 NIC 수신 링을 폴링하여 패킷 벡터를 생성합니다. VPP의 모든 패킷 처리는 이 노드에서 시작됩니다.

INPUT 노드 폴링 메커니즘은 다음과 같이 동작합니다:

/* 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() — 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() — 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_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()가 같은 타입의 여러 이벤트를 벡터로 모아 리턴하므로, 폭주 상황에서도 일괄 처리할 수 있습니다.

실전 패턴

주의: PROCESS 노드는 배리어 동기화의 대상입니다. 즉, 프로세스가 실행 중일 때 다른 워커 스레드가 배리어에 도달하면 프로세스 완료를 기다려야 합니다. 따라서 PROCESS 노드 함수 안에서 장시간 블록되거나 I/O 대기를 하면 전체 데이터 평면이 스톱됩니다. 필요하면 vlib_process_suspend()로 제어권을 넘기거나, 작업을 여러 틱으로 쪼개야 합니다.