VPP clib 인프라 라이브러리

VPP clib 인프라 라이브러리 상세 분석 — 메모리·벡터·풀·해시·시간·동기화 프리미티브와 CPU 기능 감지, bit 연산, 바이트오더, 포맷터 등 vppinfra 핵심 루틴을 다룹니다.

선행 문서: 이 페이지는 기초와 아키텍처의 벡터 패킷 처리·그래프 노드 개념을 전제로 합니다. 내부 구현 카테고리의 다른 페이지와 함께 읽어 주세요.

VPP의 clib(C Library) 인프라는 src/vlib/, src/vppinfra/ 디렉터리에 위치하며, 표준 C 라이브러리를 대체하는 고성능 유틸리티 함수 모음입니다. 메모리 관리, 동적 배열, 해시 테이블, 바이트 순서 변환, 원자적 연산(Atomic Operation) 등 VPP 전체에서 사용되는 기반 기능을 제공합니다. 풀 할당기와 비트맵(Bitmap), 시간 계측, 에러 경로까지 같은 계층에서 제공하므로 플러그인과 코어 코드가 공통 규약을 유지하기 쉽습니다. 모든 clib 함수는 성능에 최적화되어 있으며, 대부분 static inline으로 선언되어 함수 호출 오버헤드 없이 사용됩니다.

clib 인프라 계층 구조 VPP Application (플러그인, 그래프 노드) vlib (노드 프레임워크, 버퍼 관리) vnet (네트워크 스택) clib / vppinfra 인프라 계층 clib_mem clib_vec clib_pool clib_bihash clib_time clib_atomic clib_error clib_memcpy 모든 clib 함수는 static inline으로 선언되어 함수 호출 오버헤드를 제거합니다 src/vppinfra/ — hugepage 기반 메모리, NUMA 인식, 캐시 최적화

clib_mem — 메모리 할당/해제

Hugepage 기반 할당: VPP는 표준 malloc 대신 clib_mem_alloc() / clib_mem_free()를 사용합니다. 내부적으로 hugepage 위의 mheap(또는 dlmalloc)을 사용하여 TLB 미스를 최소화합니다.

NUMA 인식 할당: clib_mem_alloc_aligned_at_offset()는 지정된 NUMA 노드의 hugepage에서 캐시라인 정렬 메모리를 할당합니다. 멀티 소켓 서버에서 원격 NUMA 접근으로 인한 성능 저하를 방지하는 핵심 메커니즘입니다.

주요 함수 목록 및 분석

함수설명비고
clib_mem_alloc(size)기본 힙 메모리 할당내부적으로 정렬 할당 호출
clib_mem_alloc_aligned(size, align)지정 정렬 힙 메모리 할당캐시라인(64B) 정렬에 주로 사용
clib_mem_free(ptr)힙 메모리 해제NULL-safe
clib_mem_alloc_no_fail(size)할당 실패 시 abortfast-path에서 에러 처리 회피용
clib_mem_set_heap(heap)현재 스레드의 활성 힙 전환이전 힙 포인터 반환
clib_mem_get_heap()현재 스레드의 활성 힙 반환TLS(Thread-Local Storage) 기반
clib_mem_get_heap_size()현재 힙의 총 크기 반환모니터링/디버깅용
clib_mem_get_heap_free_space()현재 힙의 잔여 공간 반환메모리 부족 감지에 활용

clib_mem_alloc_aligned 내부 흐름

static inline void *
clib_mem_alloc_aligned (uword size, uword align)
{
    clib_mem_heap_t *heap = clib_mem_get_per_cpu_heap ();
    void *p;

    /* dlmalloc의 정렬 할당 사용
     * hugepage 위에서 동작하므로 TLB 미스가 발생하지 않습니다 */
    p = mspace_memalign (heap->mspace, align, size);

    /* 할당 통계 갱신 (디버그 빌드에서만 활성) */
    clib_mem_set_thread_alloc_stats (heap, size, 1);

    return p;
}

NUMA 인식 할당 예제

/* NUMA 노드 1에서 캐시라인 정렬 메모리 할당 */
clib_mem_heap_t *prev_heap;
void *data;

/* NUMA 노드 1의 힙으로 전환 */
prev_heap = clib_mem_set_heap (numa_heaps[1]);

/* 64바이트 캐시라인 정렬로 4096바이트 할당 */
data = clib_mem_alloc_aligned (4096, CLIB_CACHE_LINE_BYTES);

/* 원래 힙으로 복원 */
clib_mem_set_heap (prev_heap);

스레드별 힙 전환 패턴

/* 플러그인 초기화 시 전용 힙 생성 및 전환 패턴 */
void
my_plugin_init (my_plugin_main_t *mpm)
{
    clib_mem_heap_t *prev_heap;
    u8 *heap;

    /* 플러그인 전용 힙 생성 (256MB, "my-plugin" 이름) */
    heap = clib_mem_create_heap (0, 256 << 20, 1 /* locked */,
                                  "my-plugin");

    /* 전용 힙으로 전환 후 자원 할당 */
    prev_heap = clib_mem_set_heap (heap);

    /* 이 구간의 모든 clib_mem_alloc()은 전용 힙에서 할당됩니다 */
    pool_alloc (mpm->sessions, 1024);
    vec_validate (mpm->per_thread_data, vlib_num_workers ());

    /* 메인 힙으로 복원 */
    clib_mem_set_heap (prev_heap);
    mpm->heap = heap;
}

clib_vec — 동적 배열 (Vector)

VPP의 기본 컬렉션: vec_* 매크로 군은 C 배열을 동적으로 관리합니다. 배열 포인터 자체가 핸들이며, 포인터 앞 공간(negative offset)에 length와 header를 저장합니다. 이 설계 덕분에 vec_len(v)v가 NULL이면 0을 반환하므로 별도의 NULL 검사가 필요 없습니다.

헤더 레이아웃: vec_header_tlen(u32)과 선택적 사용자 헤더를 포함하며, 사용자가 받는 포인터는 data[0]을 가리킵니다. 즉, ((vec_header_t *)v)[-1].len으로 길이를 참조합니다.

주요 매크로 분석

매크로동작성능
vec_len(v)배열 길이 반환 (NULL이면 0)O(1), 포인터 역참조(Dereference) 1회
vec_add1(v, e)원소 1개 추가 (필요 시 realloc)amortized O(1), 확장 시 O(n)
vec_add2(v, p, n)n개 공간 확보, 포인터 p 반환amortized O(1)
vec_validate(v, i)인덱스 i까지 크기 보장 (0 초기화)확장 필요 시 O(n)
vec_free(v)메모리 해제, v를 NULL로 설정O(1)
vec_reset_length(v)길이만 0으로 설정 (메모리 유지)O(1), 재할당 없는 재사용
vec_elt_at_index(v, i)인덱스 접근 (디버그 시 바운드 체크)O(1)
vec_foreach(var, v)전체 원소 순회 매크로O(n)
vec_sort_with_function(v, cmp)비교 함수 기반 정렬O(n log n)
vec_dup(v)전체 배열 복제O(n)

vec_add1 내부 구현

/* vec_add1(V, E) 매크로 확장 후 핵심 로직 */
#define vec_add1(V, E)                          \
do {                                             \
    word _v_len = vec_len (V);                   \
    /* 현재 용량 초과 여부 확인 */                \
    V = _vec_resize (V, _v_len + 1,              \
                     (_v_len + 1) * sizeof (V[0]),  \
                     sizeof (vec_header_t),      \
                     0);                            \
    /* _vec_resize 내부:
     *   if (새 크기 > 현재 용량)
     *     새 용량 = max(요청 크기, 현재 용량 * 2)
     *     clib_mem_alloc(새 용량)
     *     memcpy(기존 데이터)
     *     clib_mem_free(기존 버퍼)
     */                                          \
    V[_v_len] = (E);                             \
} while (0)

vec_header_t 메모리 레이아웃

벡터 포인터 v가 가리키는 위치 기준으로 메모리 레이아웃은 다음과 같습니다. 포인터 앞(negative offset)에 헤더가 위치하며, 사용자는 v[0]부터 데이터에 접근합니다.

/*
 * 메모리 레이아웃 (주소 낮은 쪽 → 높은 쪽):
 *
 *   [user_header (선택)]  ← vec_header_bytes(user_hdr_size)
 *   [len: u32]           ← ((vec_header_t *)v)[-1].len
 *   [data[0]]            ← v 포인터가 가리키는 위치
 *   [data[1]]
 *   ...
 *   [data[len-1]]
 *
 * vec_len(v) → v가 NULL이면 0, 아니면 헤더의 len 필드
 * vec_bytes(v) → vec_len(v) * sizeof(v[0])
 */
typedef struct {
    u32 len;           /* 현재 원소 수 */
    u8  hdr_size;      /* 사용자 헤더 크기 (바이트) */
    u8  log2_header;   /* 정렬 정보 */
    u8  data[0];       /* 실제 데이터 시작점 */
} vec_header_t;

실제 사용 예제: 인터페이스 목록 관리

/* 활성 인터페이스 인덱스 목록을 vec로 관리하는 패턴 */
u32 *active_sw_if_indices = 0;  /* NULL 초기화 = 빈 벡터 */

/* 인터페이스 활성화 시 추가 */
vec_add1 (active_sw_if_indices, sw_if_index);

/* 전체 순회: vec_foreach는 포인터를 순회합니다 */
u32 *sw_if_idx;
vec_foreach (sw_if_idx, active_sw_if_indices)
{
    vnet_sw_interface_t *swif;
    swif = vnet_get_sw_interface (vnm, *sw_if_idx);
    /* 인터페이스 처리 ... */
}

/* 특정 인덱스 제거 (순서 무관 시 O(1) 삭제) */
vec_del1 (active_sw_if_indices, position);

/* 재사용을 위해 길이만 초기화 (메모리는 유지) */
vec_reset_length (active_sw_if_indices);

/* 완전 해제 */
vec_free (active_sw_if_indices);
/* 이 시점에서 active_sw_if_indices == NULL */

clib_pool — 인덱스 기반 객체 풀

pool_* 매크로는 동일 크기 객체의 할당/해제를 O(1)로 수행합니다. 내부적으로 free bitmap과 vec를 조합하여 구현됩니다. 풀 인덱스(u32)로 객체에 접근하므로, 포인터 대신 인덱스를 저장하여 메모리를 절약하고 직렬화(Serialization)가 용이합니다.

풀의 핵심 장점은 인덱스 안정성(index stability)입니다. 객체가 할당/해제되어도 기존 객체의 인덱스는 변하지 않습니다. 이는 VPP에서 세션 ID, FIB 엔트리 인덱스 등 외부에서 참조하는 핸들로 풀 인덱스를 직접 사용할 수 있게 합니다.

주요 매크로

매크로동작성능
pool_get(pool, elt)빈 슬롯 할당, elt에 포인터 반환O(1), free bitmap에서 first set bit
pool_put(pool, elt)슬롯 반환 (free bitmap에 마킹)O(1)
pool_elt_at_index(pool, i)인덱스로 객체 접근O(1), 배열 인덱싱
pool_foreach(elt, pool)활성 원소만 순회O(n), free bitmap 스킵
pool_elts(pool)현재 활성 원소 수 반환O(1)
pool_free(pool)전체 풀 메모리 해제O(1)

pool_get 내부 동작

/* pool_get(P, E) 매크로 확장 후 핵심 로직 */
#define pool_get(P, E)                                      \
do {                                                         \
    pool_header_t *_pool_hdr = pool_header (P);             \
    uword _pool_len = vec_len (P);                          \
                                                              \
    /* free_bitmap에서 첫 번째 빈 슬롯 탐색 */             \
    if (PREDICT_TRUE (_pool_hdr->free_bitmap != 0))          \
    {                                                         \
        /* O(1): clib_bitmap_first_set()는 하드웨어            \
         * BSF/TZCNT 명령어를 사용합니다 */                   \
        uword _free_idx =                                     \
            clib_bitmap_first_set (_pool_hdr->free_bitmap);   \
        _pool_hdr->free_bitmap =                              \
            clib_bitmap_andnoti (_pool_hdr->free_bitmap,       \
                                  _free_idx);                  \
        (E) = (P) + _free_idx;                                \
    }                                                         \
    else                                                      \
    {                                                         \
        /* 빈 슬롯 없음 → 벡터 확장 */                       \
        P = _vec_resize (P, _pool_len + 1, ...);              \
        (E) = (P) + _pool_len;                                \
    }                                                         \
    /* 슬롯을 0으로 초기화 */                                \
    clib_memset (E, 0, sizeof (*E));                        \
} while (0)

실제 사용 예제: TLS 컨텍스트 풀 관리

/* TLS 세션 컨텍스트를 풀로 관리하는 패턴 */
typedef struct {
    u32 session_index;        /* 연결된 세션의 풀 인덱스 */
    u32 tls_ctx_handle;       /* 엔진별 핸들 */
    u8  is_client;
    u8  *hostname;             /* SNI 호스트명 (vec) */
} tls_ctx_t;

typedef struct {
    tls_ctx_t *ctx_pool;       /* 풀: 스레드별 독립 */
    u32 *free_ctx_indices;     /* 사전 할당된 인덱스 (선택적) */
} tls_main_t;

/* 새 TLS 컨텍스트 할당 */
tls_ctx_t *ctx;
pool_get (tm->ctx_pool, ctx);
clib_memset (ctx, 0, sizeof (*ctx));
ctx->session_index = session_idx;

/* 풀 인덱스를 핸들로 사용 — 포인터 대신 u32 인덱스 저장 */
u32 ctx_index = ctx - tm->ctx_pool;
session->opaque = ctx_index;   /* 32비트 인덱스만 저장 */

/* 인덱스로 컨텍스트 접근 */
tls_ctx_t *ctx = pool_elt_at_index (tm->ctx_pool, ctx_index);

/* TLS 연결 종료 시 반환 */
vec_free (ctx->hostname);       /* 내부 vec 먼저 해제 */
pool_put (tm->ctx_pool, ctx);   /* 슬롯을 free bitmap에 반환 */

/* 활성 컨텍스트만 순회하여 타임아웃 검사 */
pool_foreach (ctx, tm->ctx_pool)
{
    if (now - ctx->last_active > timeout)
        tls_ctx_close (ctx);
}

clib_bihash — 고성능 해시 테이블 구현

Bounded-Index Extensible Hash(BIHASH)는 VPP 전체에서 사용되는 핵심 데이터 구조입니다. FIB 테이블, 세션 테이블, MAC 학습 테이블, NAT 세션 관리 등 고성능 조회가 필요한 거의 모든 서브시스템에서 활용됩니다. Reader는 lock-free로 동작하며, writer는 VPP 배리어(barrier) 하에서만 수행되므로 reader-writer 경합이 발생하지 않습니다.

각 bucket은 4개의 (key, value) 쌍을 저장할 수 있습니다. bucket이 가득 차면 체인(chaining) 대신 새로운 페이지(Page)를 할당하여 확장하는 방식을 사용합니다. 이 구조 덕분에 캐시 라인 내에서 4-way 비교가 완료되어 메모리 접근 패턴이 매우 효율적입니다.

주요 API 함수

bihash_search_inline() 내부 로직

/* clib_bihash_search_inline() 내부 의사 코드 */
static inline int
clib_bihash_search_inline (BVT(clib_bihash) *h,
                           BVT(clib_bihash_kv) *search_key)
{
    u64 hash;
    u32 bucket_index;
    BVT(clib_bihash_bucket) *b;
    BVT(clib_bihash_value) *v;

    /* 1단계: 해시 계산 및 버킷 선택 */
    hash = BV(clib_bihash_hash) (search_key);
    bucket_index = hash & (h->nbuckets - 1);
    b = &h->buckets[bucket_index];

    /* 2단계: 빈 버킷이면 즉시 miss 반환 */
    if (PREDICT_FALSE (BV(clib_bihash_bucket_is_empty) (b)))
        return -1;

    /* 3단계: 버킷 내 value 배열 획득 */
    hash >>= h->log2_nbuckets;
    v = BV(clib_bihash_get_value) (h, b->offset);

    /* 4단계: 4-way 비교 — 캐시 라인 내에서 완료 */
    for (int i = 0; i < BIHASH_KVP_PER_PAGE; i++)
    {
        if (BV(clib_bihash_key_compare) (v->kvp[i].key,
                                         search_key->key))
        {
            /* hit: 결과를 search_key에 복사 */
            *search_key = v->kvp[i];
            return 0;
        }
    }

    /* 5단계: overflow 페이지가 있으면 체인 탐색 */
    if (PREDICT_FALSE (b->linear_search))
    {
        /* 선형 탐색 모드: 모든 페이지 순회 */
        for (int page = 1; page < b->refcnt; page++)
        {
            v++;
            for (int i = 0; i < BIHASH_KVP_PER_PAGE; i++)
            {
                if (BV(clib_bihash_key_compare) (v->kvp[i].key,
                                                 search_key->key))
                {
                    *search_key = v->kvp[i];
                    return 0;
                }
            }
        }
    }

    return -1;  /* miss */
}

사용 예제: NAT 세션 테이블 생성 및 검색

/* NAT 세션 테이블에서 bihash 활용 예제 */
typedef struct {
    BVT(clib_bihash) session_hash;
    /* ... 기타 NAT 상태 ... */
} nat_main_t;

/* 1. 해시 테이블 초기화 */
static void
nat_session_table_init (nat_main_t *nm)
{
    /* 이름, 버킷 수(1024), 메모리 한도(64MB) */
    BV(clib_bihash_init) (&nm->session_hash,
                          "nat-session-table",
                          1024,    /* nbuckets */
                          64 << 20 /* memory_size = 64MB */);
}

/* 2. 세션 추가 */
static void
nat_session_add (nat_main_t *nm,
                 ip4_address_t src, u16 sport,
                 ip4_address_t dst, u16 dport,
                 u32 session_index)
{
    BVT(clib_bihash_kv) kv;

    /* 키 구성: src_ip + dst_ip + ports */
    kv.key = (u64) src.as_u32 << 32 | dst.as_u32;
    kv.value = session_index;

    BV(clib_bihash_add_del) (&nm->session_hash, &kv,
                             1 /* is_add */);
}

/* 3. 세션 검색 — fast-path에서 호출 */
static inline int
nat_session_lookup (nat_main_t *nm,
                    ip4_address_t src, ip4_address_t dst,
                    u32 *session_index)
{
    BVT(clib_bihash_kv) kv, result;

    kv.key = (u64) src.as_u32 << 32 | dst.as_u32;

    if (BV(clib_bihash_search_inline) (&nm->session_hash, &kv) == 0)
    {
        *session_index = kv.value;
        return 0;  /* 세션 발견 */
    }
    return -1;    /* 세션 없음 */
}

clib_memcpy_fast / clib_memset — 최적화된 메모리 조작

VPP는 패킷 처리 hot-path에서 glibc의 memcpy() 대신 자체 최적화된 메모리 조작 함수를 사용합니다. clib_memcpy_fast()는 내부적으로 x86의 __builtin_memcpy() 또는 SSE/AVX 명령을 활용하여 컴파일 타임에 크기가 결정될 때 최적의 성능을 제공합니다.

주요 함수

MAC 주소 복사 최적화

ethernet_mac_address_copy(dst, src) 내부에서는 clib_memcpy_fast를 6바이트에 대해 호출합니다. 또한 ip4_header_t 복사 시 20바이트 고정 크기를 사용하여 컴파일 타임 최적화가 적용됩니다.

/* MAC 주소 복사 — 6바이트 고정 크기 최적화 */
static_always_inline void
ethernet_mac_address_copy (u8 *dst, const u8 *src)
{
    clib_memcpy_fast (dst, src, 6);
}

/* 패킷 처리에서의 전형적인 사용 패턴 */
static_always_inline u32
my_node_inline (vlib_main_t *vm, vlib_node_runtime_t *node,
                vlib_frame_t *frame)
{
    u32 *from = vlib_frame_vector_args (frame);
    u32 n_left = frame->n_vectors;

    while (n_left > 0)
    {
        vlib_buffer_t *b0 = vlib_get_buffer (vm, from[0]);
        ethernet_header_t *eth = vlib_buffer_get_current (b0);

        /* src MAC을 dst MAC 위치로 복사 (6바이트) */
        clib_memcpy_fast (eth->dst_address, eth->src_address, 6);

        /* IP 헤더 전체 복사 (20바이트 고정 크기) */
        ip4_header_t saved_hdr;
        clib_memcpy_fast (&saved_hdr, ip4_next_header (eth),
                          sizeof (ip4_header_t));

        from += 1;
        n_left -= 1;
    }
    return frame->n_vectors;
}

clib_host_to_net / clib_net_to_host — 바이트 순서 변환

패킷 헤더 파싱과 생성에서 네트워크 바이트 순서(Big-endian)와 호스트 바이트 순서 간의 변환은 필수적입니다. VPP는 clib_host_to_net_*clib_net_to_host_* 매크로 계열을 제공합니다.

주요 함수

구현 원리

x86 아키텍처에서는 __builtin_bswap16/32/64를 사용하며, 단일 BSWAP 명령으로 컴파일됩니다. Big-endian 아키텍처에서는 변환이 필요 없으므로 no-op(아무 작업도 하지 않음)으로 처리됩니다.

/* EtherType 비교 — 네트워크 바이트 순서 상수와 비교 */
#define ETHERNET_TYPE_IP4_NBO  clib_host_to_net_u16 (0x0800)
#define ETHERNET_TYPE_IP6_NBO  clib_host_to_net_u16 (0x86DD)
#define ETHERNET_TYPE_ARP_NBO  clib_host_to_net_u16 (0x0806)

static_always_inline u32
classify_packet (ethernet_header_t *eth)
{
    /* 컴파일 타임에 상수가 변환되므로 런타임 비용 없음 */
    if (eth->type == ETHERNET_TYPE_IP4_NBO)
        return NEXT_IP4_INPUT;
    else if (eth->type == ETHERNET_TYPE_IP6_NBO)
        return NEXT_IP6_INPUT;
    return NEXT_DROP;
}

/* IP 주소 변환 — CLI 출력 시 호스트 순서로 변환 */
static clib_error_t *
show_session_command_fn (vlib_main_t *vm, session_t *s)
{
    ip4_address_t src = s->src_ip;
    u16 src_port = clib_net_to_host_u16 (s->src_port);
    u16 dst_port = clib_net_to_host_u16 (s->dst_port);

    vlib_cli_output (vm, "session: %U:%d -> %U:%d",
                     format_ip4_address, &src, src_port,
                     format_ip4_address, &s->dst_ip, dst_port);
    return 0;
}

/* TCP 포트 파싱 — 패킷에서 포트 번호 추출 */
static_always_inline u16
tcp_get_src_port (tcp_header_t *tcp)
{
    return clib_net_to_host_u16 (tcp->src_port);
}

clib_atomic — 원자적 연산

VPP의 멀티 워커(multi-worker) 환경에서는 여러 스레드가 동시에 공유 데이터에 접근합니다. clib_atomic 계열 함수는 lock-free 프로그래밍을 위한 원자적 연산을 제공하며, 공유 카운터, 플래그, 시퀀스 번호 등의 안전한 업데이트에 사용됩니다. 특히 acquire/release 시맨틱은 메모리 접근이 임의로 재배치(Relocation)되지 않도록 제어하는 원자적 규칙의 핵심입니다.

주요 함수

사용 사례

IPsec 시퀀스 번호 증가, per-thread 카운터 집계, 배리어 동기화 플래그 등에서 활용됩니다.

/* IPsec SA 시퀀스 번호 원자적 증가 */
static_always_inline u32
ipsec_sa_assign_seq (ipsec_sa_t *sa)
{
    /* 여러 워커가 동시에 패킷을 암호화할 때 시퀀스 번호 충돌 방지 */
    return clib_atomic_fetch_add (&sa->seq, 1);
}

/* 멀티 워커 환경에서 공유 카운터 업데이트 */
typedef struct
{
    u64 packets;
    u64 bytes;
} shared_counter_t;

static_always_inline void
update_shared_counter (shared_counter_t *ctr, u32 n_packets, u32 n_bytes)
{
    clib_atomic_fetch_add (&ctr->packets, n_packets);
    clib_atomic_fetch_add (&ctr->bytes, n_bytes);
}

/* CAS를 사용한 lock-free 상태 전이 */
static_always_inline int
try_acquire_session (session_t *s)
{
    u32 expected = SESSION_STATE_IDLE;
    u32 desired = SESSION_STATE_ACTIVE;

    /* IDLE 상태일 때만 ACTIVE로 전이 */
    return clib_atomic_cmp_and_swap (&s->state, expected, desired);
}

/* acquire/release 시맨틱을 사용한 플래그 동기화 */
/* 메인 스레드: 설정 변경 완료 후 플래그 설정 */
void main_thread_update_config (my_config_t *cfg)
{
    cfg->new_value = 42;
    /* release: new_value 쓰기가 이 이전에 완료됨을 보장 */
    clib_atomic_store_rel_n (&cfg->ready, 1);
}

/* 워커 스레드: 플래그 확인 후 설정 읽기 */
void worker_thread_read_config (my_config_t *cfg)
{
    /* acquire: ready 읽기 이후에 new_value 읽기가 수행됨을 보장 */
    if (clib_atomic_load_acq_n (&cfg->ready))
    {
        u32 val = cfg->new_value; /* 안전하게 42를 읽음 */
        process_value (val);
    }
}

동기화 프리미티브와 동시성 패턴 — spinlock · rwlock · 배리어 · RCU-like

VPP는 여러 워커 스레드가 같은 물리 자원을 공유하기 때문에, 작은 잘못된 락 하나가 전체 Mpps를 깎아내립니다. 그래서 VPP는 리눅스 커널과 유사한 다양한 동기화 프리미티브를 자체적으로 구현하고 있으며, 각 프리미티브는 정확성 보장의 강도경합 시 비용이 다릅니다. 이 절은 어떤 상황에 어떤 프리미티브를 골라야 하는지, 내부가 어떻게 구현되어 있는지, 그리고 배리어 동기화가 왜 VPP의 중심 축인지를 기초부터 설명합니다.

동기화 프리미티브 선택 가이드 공유 자원을 어떻게 보호할 것인가? 읽기/쓰기 빈도 · 크리티컬 섹션 길이 · 데이터 크기 단일 워드(8/16/32/64b)? 카운터 · 플래그 · 시퀀스 번호 clib_atomic_* fetch_add / cmp_and_swap load_acq / store_rel 비용: ~10-30 cycle lock-free, 블로킹 없음 짧은 크리티컬 섹션? 수십~수백 사이클 이내 읽기 위주? 쓰기는 제어 평면 이벤트에서만 clib_rwlock / 배리어 reader: 락 2개 부담 writer: 모든 reader 대기 비용: reader ~20c FIB 테이블 등 clib_spinlock ticket / CAS 기반 lock → critical → unlock 경합 없음: ~30c 경합 심함: ~수 μs FIFO, SA 타이머 큐 등 per-CPU + 배리어 동기화 워커별 데이터 분할 제어 평면이 배리어 요청 fast path: 락 0 VPP의 표준 패턴 노드 등록 · FIB 갱신 원칙: 가능하면 락을 쓰지 않는다 → per-CPU 분할 + 배리어가 VPP의 표준 해결책. 락은 마지막 수단.

가장 기본: 메모리 오더링부터 이해하기

모든 락과 원자 연산의 정확성은 메모리 오더링(Memory Ordering) 위에 서 있습니다. 현대 CPU는 성능을 위해 로드/스토어를 재정렬(Reordering)할 수 있고, 컴파일러 역시 인라인 최적화 과정에서 명령을 섞어 놓습니다. 이 재정렬이 단일 스레드에서는 문제가 없지만, 두 스레드가 같은 메모리를 공유하는 순간 한쪽이 본 순서다른 쪽이 본 순서가 달라질 수 있습니다. VPP가 사용하는 세 가지 오더링 수준은 다음과 같습니다.

메모리 오더링 수준과 재정렬 허용 범위 Relaxed 원자성만 보장, 순서는 자유 fetch_add(counter, 1) ↑↓ 자유 재정렬 가능 x = data; y = flag; 사용처: 순수 통계 카운터 (다른 변수와 순서 의존 없음) 비용: ~5-10 cycle x86에서는 사실상 공짜 Acquire / Release 일방향 펜스 — release 이전 쓰기가 acquire 이후 읽기에 보임 data = 42; store_rel(flag, 1) —— release 펜스 —— if (load_acq(flag)) read(data); /* 42 보장 */ 사용처: 플래그 기반 handoff (publish-subscribe) 비용: ~10-30 cycle Seq-Cst (전체 순서) 모든 코어가 같은 순서로 관측 x.store(1); — MFENCE (완전 배리어) — y.store(2); — MFENCE — 사용처: 배리어 동기화, 복잡한 상태 머신 전이 비용: ~30-100+ cycle 반드시 필요할 때만 사용

핵심은 가장 약한 오더링으로 충분한 경우에는 더 강한 오더링을 쓰지 않는다입니다. x86-64는 TSO(Total Store Order) 모델이라 relaxed/acquire/release가 거의 같은 비용이지만, ARM/POWER에서는 차이가 크므로 원격 개발자가 ARM 서버에서 성능 회귀를 보는 전형적인 원인이 이 지점입니다.

clib_spinlock — 가장 단순한 상호배제

clib_spinlock_t는 한 단어 크기의 락이며, lock을 호출한 스레드가 유일하게 크리티컬 섹션에 들어갑니다. 내부 구현은 clib_atomic_test_and_set 기반이고, 경합 시 바쁜 대기(Busy Wait)로 락을 기다립니다. 대기 중에는 PAUSE 명령으로 하이퍼스레드 파트너에 CPU 자원을 양보합니다.

/* src/vppinfra/lock.h — 개념적 축약 */
typedef struct {
    volatile u32 lock;
    u32 owner_cpu;  /* 디버그용 */
} clib_spinlock_t;

static_always_inline void
clib_spinlock_lock (clib_spinlock_t *p)
{
    while (clib_atomic_test_and_set (&p->lock)) {
        /* busy wait — 하이퍼스레드에 자원 양보 */
        while (p->lock) CLIB_PAUSE ();
    }
    /* acquire 배리어가 여기에 암묵적으로 존재 */
}

static_always_inline void
clib_spinlock_unlock (clib_spinlock_t *p)
{
    /* release 배리어 + 락 해제 */
    clib_atomic_release (&p->lock);
}

/* 실제 사용 예 — 세션 만료 큐 보호 */
typedef struct {
    clib_spinlock_t lock;
    session_handle_t *pending;   /* clib 벡터 */
} session_cleanup_q_t;

void
cleanup_q_push (session_cleanup_q_t *q, session_handle_t h)
{
    clib_spinlock_lock (&q->lock);
    vec_add1 (q->pending, h);
    clib_spinlock_unlock (&q->lock);
}
스핀락(Spinlock)을 절대 쓰지 말아야 할 곳: ① fast path 노드 함수 내부 (벡터당 여러 번 호출되므로 경합이 즉시 수십 마이크로초로 증폭) ② 크리티컬 섹션에서 malloc/free/시스템 콜을 호출 (컨텍스트 스위치 중 락 보유 → 대기열 전체가 바쁜 대기) ③ 커널 락과 중첩해 보유. 이 세 경우는 거의 예외 없이 성능 붕괴로 이어집니다.

clib_rwlock — 다독자/단일 기록자

clib_rwlock_t는 "읽기는 여러 스레드가 동시에, 쓰기는 혼자"라는 패턴을 위한 락입니다. 내부적으로 리더 카운터와 writer 플래그를 같은 워드에 원자 연산으로 관리합니다. FIB 테이블이나 ACL 규칙처럼 쓰기는 드물고 읽기는 매 패킷마다 일어나는 자료에 적합합니다.

/* 리더 — fast path에서 ACL 규칙 스캔 */
clib_rwlock_reader_lock (&acl->rules_lock);
vec_foreach (r, acl->rules) {
    if (acl_rule_matches (r, &pkt)) {
        verdict = r->action;
        break;
    }
}
clib_rwlock_reader_unlock (&acl->rules_lock);

/* 라이터 — 제어 평면에서 규칙 추가 (드묾) */
clib_rwlock_writer_lock (&acl->rules_lock);
vec_add1 (acl->rules, new_rule);
clib_rwlock_writer_unlock (&acl->rules_lock);

하지만 VPP에서는 rwlock도 fast path 안에서는 거의 쓰지 않습니다. 리더 락조차 경합 시 cache line bouncing을 일으키기 때문입니다. 대신 다음에 설명하는 배리어 동기화 + per-CPU 복제 패턴이 선호됩니다.

vlib 배리어 동기화 — VPP가 선호하는 동시성 모델

VPP의 배리어 동기화(Barrier Synchronization)는 리눅스 커널의 synchronize_rcu()와 유사한 역할을 합니다. 메인 스레드가 "지금 잠깐 모든 워커가 멈춰 주세요"라고 요청하면, 각 워커는 현재 벡터 처리를 완료한 뒤 배리어 지점에서 멈춥니다. 그 동안 메인 스레드는 공유 자료구조를 원하는 대로 갱신하고, release를 부르면 워커가 다시 달립니다. fast path 코드는 자기가 보는 자료가 절대 변경되지 않는다고 가정해도 안전합니다.

vlib 배리어 동기화 타임라인 메인 idle / CLI 처리 barrier_sync FIB 갱신 (안전) release idle wk_0 vec#1 vec#2 STOP (@ barrier) vec#3 vec#4 vec#5 wk_1 vec#1 vec#2 (긴 벡터) STOP vec#3 vec#4 vec#5 wk_2 vec#1 vec#2 STOP vec#3 vec#4 vec#5 요청 수정 완료 release 벡터 처리 중 배리어에서 대기 메인: 요청/release 메인: idle 각 워커는 "진행 중인 벡터를 끝낸 뒤"에만 배리어에 진입 → fast path 코드는 락을 전혀 만지지 않습니다.
/* 제어 평면: FIB 항목 추가 (메인 스레드에서 호출) */
static clib_error_t *
my_add_fib_entry (vlib_main_t *vm, ip4_address_t *dst, u32 next_hop_adj)
{
    /* 1) 모든 워커가 진행 중인 벡터를 끝내기를 기다림 */
    vlib_worker_thread_barrier_sync (vm);

    /* 2) 여기서는 fast path가 절대 이 자료를 보지 않음이 보장됨 — 락 불필요 */
    ip4_fib_table_entry_path_add (fib_index, dst, next_hop_adj, ...);

    /* 3) 워커를 다시 해제 */
    vlib_worker_thread_barrier_release (vm);
    return 0;
}

/* fast path 노드 — 락 없이 안심하고 테이블을 읽음 */
static uword
ip4_lookup_node_fn (vlib_main_t *vm, vlib_node_runtime_t *node,
                    vlib_frame_t *frame)
{
    /* fib_index로 FIB 테이블을 조회 — 배리어가 변경 시점을 격리 */
    u32 adj = ip4_fib_lookup (fib_index, &ip->dst_address);
    /* ... */
}
배리어의 비용: 배리어는 무료가 아닙니다. 요청 → 모든 워커 도달까지 수백 나노초~수 마이크로초가 걸리며, 그 동안 모든 fast path가 멈춥니다. 그래서 VPP는 "많은 작은 변경을 배리어로 쪼개지 말고, 한 배리어 아래에서 일괄 변경"하는 패턴을 권장합니다. 예: 1,000개 FIB 엔트리를 넣을 때 1,000번 배리어가 아닌, 1번 배리어 아래에서 1,000번 fib_table_entry_*를 호출합니다.

clib_bihash — 내부 락까지 고려한 해시 테이블

clib_bihash는 VPP의 범용 고성능 해시 테이블이며, NAT 세션 테이블·플로우 테이블·호스트 맵 등에서 광범위하게 쓰입니다. 내부적으로 버킷당 경량 락을 두어, 전체 테이블 락 없이도 동시 삽입/조회가 가능합니다. fast path의 조회는 락 없이 lock-free 패턴(버전 번호 기반)으로 동작합니다.

/* bihash 버킷 구조 (개념적) */
typedef struct {
    u64 lock : 1;        /* 쓰기 락 비트 */
    u64 version : 31;    /* lock-free 읽기용 */
    u64 offset : 32;     /* 실제 데이터 페이지 */
} bihash_bucket_t;

/* 조회 (fast path) — 완전히 lock-free */
u64
bihash_search (bihash_t *h, u64 key)
{
    retry:
    u32 v1 = h->bucket.version;
    /* 데이터 페이지에서 엔트리 찾기 */
    u64 result = search_in_page (h->bucket.offset, key);
    u32 v2 = h->bucket.version;
    if (v1 != v2) goto retry;  /* 중간에 쓰기 발생 → 재시도 */
    return result;
}

/* 삽입 — 해당 버킷만 잠금 */
void
bihash_add (bihash_t *h, u64 key, u64 value)
{
    while (clib_atomic_test_and_set (&h->bucket.lock)) CLIB_PAUSE ();
    /* version 증가 → 진행 중인 읽기에 "내가 수정했음" 알림 */
    h->bucket.version++;
    insert_into_page (h->bucket.offset, key, value);
    h->bucket.version++;
    h->bucket.lock = 0;
}
bihash가 특별한 이유: 일반 해시 테이블은 리더도 락을 잡아야 하므로 멀티코어 스케일이 제한됩니다. bihash는 리더가 락 대신 version을 두 번 읽는 seqlock 패턴을 사용하여, 쓰기가 드문 조건에서는 리더 N개가 동시에 동작해도 캐시 라인 쟁탈이 발생하지 않습니다. NAT44가 수천만 세션을 유지하면서도 Mpps 성능을 내는 이유가 바로 이것입니다.

프리미티브 한눈 비교

프리미티브경합 없음경합 심함스케일대표 사용처
clib_atomic_*~10c~50c (cache line bounce)◎ (lock-free)카운터, 시퀀스, 상태 CAS
clib_spinlock~30c수 μs+△ (serialize)짧은 공유 큐, 드문 이벤트
clib_rwlockreader ~20cwriter 대기 긺○ (read-heavy)설정 테이블, 거의 쓰이지 않음
vlib 배리어fast path 0제어 평면 지연◎◎FIB · ACL · 노드 등록 · SA 갱신
clib_bihash 내장읽기 0, 쓰기 ~30c버킷 단위만 영향◎◎NAT · 플로우 · 소켓 해시
per-CPU 분할0없음◎◎◎통계, 세션 소유권, 타이머 휠
VPP의 황금 규칙: (1) 단일 값이면 clib_atomic_*, (2) 짧고 드문 보호는 clib_spinlock, (3) 설정/FIB 갱신은 배리어 + 락 없는 자료구조, (4) 해시 테이블은 clib_bihash, (5) 가능하면 데이터 자체를 워커별로 분할. 이 다섯 가지로 VPP 내부의 거의 모든 공유 상태를 다룹니다.

clib_time — 고해상도 시간 측정

VPP는 syscall 오버헤드를 피하기 위해 CPU의 TSC(Time Stamp Counter)를 직접 읽어 시간을 측정합니다. 패킷 처리 루프에서는 시간 측정도 성능에 영향을 미치므로, 여러 수준의 시간 함수가 제공됩니다.

주요 함수

성능 특성

vlib_time_now()는 메인 루프 시작 시 1회 TSC를 읽어 캐시하므로, 루프 내에서 반복 호출해도 오버헤드가 0입니다. 반면 clib_cpu_time_now()는 매번 rdtsc를 실행합니다.

/* 이벤트 타이머 — 세션 타임아웃 체크 */
static_always_inline int
session_is_expired (session_t *s, f64 now, f64 timeout)
{
    return (now - s->last_active_time) > timeout;
}

static uword
session_cleanup_process (vlib_main_t *vm, vlib_node_runtime_t *rt,
                         vlib_frame_t *f)
{
    f64 now = vlib_time_now (vm);  /* 캐시된 시간 — 오버헤드 0 */
    f64 timeout = 300.0;          /* 5분 타임아웃 */

    pool_foreach (s, session_pool)
    {
        if (session_is_expired (s, now, timeout))
            session_close (s);
    }
    return 0;
}

/* 성능 벤치마킹 — TSC 기반 정밀 측정 */
static_always_inline void
benchmark_function (vlib_main_t *vm)
{
    u64 start = clib_cpu_time_now ();

    /* 측정 대상 코드 */
    do_expensive_work ();

    u64 end = clib_cpu_time_now ();
    u64 cycles = end - start;

    vlib_cli_output (vm, "소요 사이클: %llu (%.2f us)",
                     cycles,
                     (f64) cycles / vm->clib_time.clocks_per_second * 1e6);
}

/* 프로세스 노드에서의 주기적 타이머 */
static uword
my_periodic_process (vlib_main_t *vm, vlib_node_runtime_t *rt,
                      vlib_frame_t *f)
{
    while (1)
    {
        /* 10초마다 깨어남 — 이벤트 기반이므로 CPU를 소비하지 않음 */
        vlib_process_wait_for_event_or_clock (vm, 10.0);

        f64 now = vlib_time_now (vm);
        do_periodic_work (now);
    }
    return 0;
}

clib_error — 에러 처리 프레임워크

clib_error_t는 에러 메시지 문자열과 코드를 담는 구조체이며, 함수 반환값으로 사용됩니다. NULL이면 성공, non-NULL이면 에러를 나타냅니다. VPP 전체에서 일관된 에러 처리 패턴을 제공합니다.

주요 매크로

노드별 에러 카운터

VLIB_REGISTER_NODE.error_stringsvlib_node_increment_counter()로 노드별 에러 통계를 관리합니다. show errors CLI 명령으로 확인할 수 있습니다.

/* 초기화 함수에서의 에러 처리 패턴 */
static clib_error_t *
my_plugin_init (vlib_main_t *vm)
{
    my_plugin_main_t *mp = &my_plugin_main;
    clib_error_t *error = 0;

    mp->vlib_main = vm;

    /* 리소스 할당 실패 시 에러 반환 */
    mp->table = clib_mem_alloc (TABLE_SIZE);
    if (!mp->table)
        return clib_error_return (0, "테이블 메모리 할당 실패 (%d 바이트)",
                                  TABLE_SIZE);

    /* 하위 모듈 초기화 — 에러 전파 */
    error = submodule_init (vm);
    if (error)
        return clib_error_return (error, "서브모듈 초기화 실패");

    return 0;  /* 성공 */
}
VLIB_INIT_FUNCTION (my_plugin_init);

/* CLI 핸들러에서의 에러 처리 */
static clib_error_t *
my_command_fn (vlib_main_t *vm, unformat_input_t *input,
               vlib_cli_command_t *cmd)
{
    u32 sw_if_index = ~0;

    while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
    {
        if (unformat (input, "interface %U",
                      unformat_vnet_sw_interface, vnm, &sw_if_index))
            ;
        else
            return clib_error_return (0, "알 수 없는 입력: '%U'",
                                      format_unformat_error, input);
    }

    if (sw_if_index == ~0)
        return clib_error_return (0, "인터페이스를 지정해야 합니다");

    return 0;
}

/* 노드별 에러 카운터 정의 */
#define foreach_my_node_error \
  _(PROCESSED, "packets processed") \
  _(DROPPED, "packets dropped") \
  _(NO_BUFFER, "no buffer available")

typedef enum
{
#define _(sym, str) MY_NODE_ERROR_##sym,
    foreach_my_node_error
#undef _
    MY_NODE_N_ERROR,
} my_node_error_t;

static char *my_node_error_strings[] = {
#define _(sym, str) str,
    foreach_my_node_error
#undef _
};

/* 노드 함수에서 에러 카운터 증가 */
static uword
my_node_fn (vlib_main_t *vm, vlib_node_runtime_t *node,
            vlib_frame_t *frame)
{
    u32 n_processed = 0, n_dropped = 0;

    /* ... 패킷 처리 ... */

    vlib_node_increment_counter (vm, node->node_index,
                                 MY_NODE_ERROR_PROCESSED, n_processed);
    vlib_node_increment_counter (vm, node->node_index,
                                 MY_NODE_ERROR_DROPPED, n_dropped);
    return frame->n_vectors;
}

VLIB_REGISTER_NODE (my_node) = {
    .function = my_node_fn,
    .name = "my-node",
    .vector_size = sizeof (u32),
    .n_errors = MY_NODE_N_ERROR,
    .error_strings = my_node_error_strings,
};

vnet_crypto — 암호화 프레임워크 API

VPP의 vnet_crypto 프레임워크는 IPsec과 TLS가 공통으로 사용하는 암호화 추상화 계층입니다. 다양한 하드웨어 가속기(Intel QAT, AES-NI 등)와 소프트웨어 구현을 통합 인터페이스로 관리하며, 엔진 교체 시 상위 프로토콜 코드를 수정할 필요가 없습니다.

핵심 구조체와 동기/비동기 API

vnet_crypto_op_t 구조체는 개별 암호화 연산을 기술합니다. 연산 유형(AES-GCM 암호화/복호화 등), 키 인덱스, IV(Initialization Vector), 소스/대상 버퍼 포인터, 인증 태그를 포함합니다.

엔진 등록 패턴

암호화 엔진은 vnet_crypto_register_engine()으로 등록됩니다. 각 엔진은 우선순위를 가지며, 동일 알고리즘에 대해 우선순위가 높은 엔진이 자동 선택됩니다. 알고리즘별 핸들러는 vnet_crypto_register_ops_handler()로 개별 등록합니다.

키 관리

IPsec에서 crypto op 생성 및 배치 실행

/* IPsec ESP 암호화에서 vnet_crypto 사용 의사 코드 */
static uword
esp_encrypt_inline (vlib_main_t *vm,
                    vlib_node_runtime_t *node,
                    vlib_frame_t *frame)
{
    u32 n_left = frame->n_vectors;
    u32 *from = vlib_frame_vector_args (frame);
    vnet_crypto_op_t ops[VLIB_FRAME_SIZE];
    vnet_crypto_op_t *op = ops;
    u32 n_ops = 0;

    while (n_left > 0)
    {
        vlib_buffer_t *b = vlib_get_buffer (vm, from[0]);
        ipsec_sa_t *sa = ipsec_sa_get (sa_index);

        /* 1. crypto op 초기화 */
        vnet_crypto_op_init (op, sa->crypto_enc_op_id);

        /* 2. 키 인덱스 설정 */
        op->key_index = sa->crypto_key_index;

        /* 3. IV 설정 (패킷별 고유) */
        op->iv = esp_hdr->iv;
        op->iv_len = sa->crypto_iv_size;

        /* 4. 소스/대상 버퍼 포인터 설정 */
        op->src = op->dst = vlib_buffer_get_current (b)
                           + sizeof (esp_header_t)
                           + sa->crypto_iv_size;
        op->len = payload_len;

        /* 5. AES-GCM: AAD와 태그 설정 */
        op->aad = esp_hdr;
        op->aad_len = 8;
        op->tag = tag_ptr;
        op->tag_len = 16;

        op++;
        n_ops++;
        n_left--;
        from++;
    }

    /* 6. 배치 실행 — 모든 op를 한 번에 처리 */
    vnet_crypto_process_ops (vm, ops, n_ops);

    /* 각 op의 status 필드로 성공/실패 확인 */
    for (u32 i = 0; i < n_ops; i++)
    {
        if (ops[i].status != VNET_CRYPTO_OP_STATUS_COMPLETED)
            vlib_node_increment_counter (vm, node->node_index,
                                         ESP_ENCRYPT_ERROR_CRYPTO, 1);
    }

    return frame->n_vectors;
}

커스텀 crypto 엔진 등록 패턴

/* 커스텀 암호화 엔진 등록 예제 */
typedef struct {
    u32 engine_index;
    /* HW 가속기 상태 */
    void *hw_ctx;
} my_crypto_engine_t;

static u32
my_crypto_aes_gcm_encrypt (vlib_main_t *vm,
                           vnet_crypto_op_t *ops[],
                           u32 n_ops)
{
    u32 n_completed = 0;

    for (u32 i = 0; i < n_ops; i++)
    {
        vnet_crypto_op_t *op = ops[i];

        /* HW 가속기로 AES-GCM 암호화 수행 */
        my_hw_aes_gcm_encrypt (op->iv, op->iv_len,
                               op->src, op->dst, op->len,
                               op->aad, op->aad_len,
                               op->tag, op->tag_len,
                               op->key_index);

        op->status = VNET_CRYPTO_OP_STATUS_COMPLETED;
        n_completed++;
    }

    return n_completed;
}

static clib_error_t *
my_crypto_engine_init (vlib_main_t *vm)
{
    my_crypto_engine_t *em = &my_crypto_engine_main;
    vnet_crypto_main_t *cm = &crypto_main;

    /* 1. 엔진 등록 (이름, 우선순위 80) */
    em->engine_index =
        vnet_crypto_register_engine (vm,
                                     "my_crypto_hw",
                                     80,    /* priority */
                                     "My HW Crypto Engine");

    /* 2. AES-128-GCM 암호화 핸들러 등록 */
    vnet_crypto_register_ops_handler (
        vm, em->engine_index,
        VNET_CRYPTO_OP_AES_128_GCM_ENC,
        my_crypto_aes_gcm_encrypt);

    /* 3. AES-128-GCM 복호화 핸들러 등록 */
    vnet_crypto_register_ops_handler (
        vm, em->engine_index,
        VNET_CRYPTO_OP_AES_128_GCM_DEC,
        my_crypto_aes_gcm_decrypt);

    /* 4. AES-256-GCM 등 추가 알고리즘도 동일 패턴 */
    vnet_crypto_register_ops_handler (
        vm, em->engine_index,
        VNET_CRYPTO_OP_AES_256_GCM_ENC,
        my_crypto_aes_256_gcm_encrypt);

    return 0;
}

/* VPP 부팅 시 자동 호출 */
VLIB_INIT_FUNCTION (my_crypto_engine_init);

플러그인 스켈레톤 코드

/* my_filter/my_filter.c — 커스텀 필터 플러그인 예제 */
#include <vlib/vlib.h>
#include <vnet/vnet.h>
#include <vnet/ip/ip4_packet.h>
#include <vnet/feature/feature.h>

/* 플러그인 등록 */
#include <vpp/app/version.h>
VLIB_PLUGIN_REGISTER () = {
    .version = VPP_BUILD_VER,
    .description = "My custom packet filter",
};

/* next 노드 인덱스 */
typedef enum {
    MY_FILTER_NEXT_PASS,      /* 통과 → ip4-lookup */
    MY_FILTER_NEXT_DROP,      /* 드롭 */
    MY_FILTER_N_NEXT,
} my_filter_next_t;

/* 노드 처리 함수 (벡터 단위) */
VLIB_NODE_FN (my_filter_node) (vlib_main_t *vm,
                                vlib_node_runtime_t *node,
                                vlib_frame_t *frame)
{
    u32 n_left_from, *from, *to_next;
    my_filter_next_t next_index;

    from = vlib_frame_vector_args (frame);
    n_left_from = frame->n_vectors;
    next_index = node->cached_next_index;

    while (n_left_from > 0)
    {
        u32 n_left_to_next;
        vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next);

        while (n_left_from > 0 && n_left_to_next > 0)
        {
            vlib_buffer_t *b0;
            ip4_header_t *ip0;
            u32 bi0, next0 = MY_FILTER_NEXT_PASS;

            bi0 = from[0];
            from += 1;
            n_left_from -= 1;
            to_next[0] = bi0;
            to_next += 1;
            n_left_to_next -= 1;

            b0 = vlib_get_buffer (vm, bi0);
            ip0 = vlib_buffer_get_current (b0);

            /* 예: 특정 목적지 IP 차단 */
            if (ip0->dst_address.as_u32 == clib_host_to_net_u32 (0x0a000001))
                next0 = MY_FILTER_NEXT_DROP;

            vlib_validate_buffer_enqueue_x1 (vm, node, next_index,
                to_next, n_left_to_next, bi0, next0);
        }
        vlib_put_next_frame (vm, node, next_index, n_left_to_next);
    }
    return frame->n_vectors;
}

/* 노드 등록 */
VLIB_REGISTER_NODE (my_filter_node) = {
    .name = "my-filter",
    .vector_size = sizeof (u32),
    .format_trace = format_my_filter_trace,
    .type = VLIB_NODE_TYPE_INTERNAL,
    .n_next_nodes = MY_FILTER_N_NEXT,
    .next_nodes = {
        [MY_FILTER_NEXT_PASS] = "ip4-lookup",
        [MY_FILTER_NEXT_DROP] = "error-drop",
    },
};

/* feature arc에 삽입 (ip4-unicast arc, ip4-lookup 앞) */
VNET_FEATURE_INIT (my_filter_feat, static) = {
    .arc_name = "ip4-unicast",
    .node_name = "my-filter",
    .runs_before = VNET_FEATURES ("ip4-lookup"),
    .runs_after = VNET_FEATURES ("ip4-input"),
};
# 플러그인 빌드 및 테스트
$ cd vpp
$ mkdir -p src/plugins/my_filter

# CMakeLists.txt 작성
$ cat > src/plugins/my_filter/CMakeLists.txt <<'EOF'
add_vpp_plugin(my_filter
  SOURCES
  my_filter.c
)
EOF

# 빌드
$ make build-release

# 플러그인 활성화
vpp# plugin my_filter_plugin.so enable

# feature 적용 (인터페이스별)
vpp# set interface feature GigabitEthernet0/8/0 my-filter arc ip4-unicast

# 검증
vpp# show node my-filter
vpp# show runtime
vpp# trace add my-filter 10

실전 구현 워크스루: 인터페이스별 차단 플러그인

플러그인 예제는 단순히 노드 함수만 있으면 끝나지 않습니다. 실제 운영 가능한 형태가 되려면 Feature Arc 삽입, 인터페이스별 활성화, CLI 제어, trace 검증이 한 묶음으로 준비되어야 합니다. 아래 흐름은 실무에서 가장 자주 쓰는 "특정 인터페이스에만 패킷 필터 적용" 패턴입니다.

커스텀 feature 플러그인이 실제로 동작하는 흐름 1. 플러그인 로드 plugin ... enable VLIB_PLUGIN_REGISTER 2. Feature Arc 등록 VNET_FEATURE_INIT ip4-unicast 체인 삽입 3. 인터페이스별 활성화 vnet_feature_enable_disable() sw_if_index 단위 적용 4. 데이터 경로 실행 my-filter 노드 PASS 또는 DROP 패킷이 들어오면 그래프는 다음 순서로 흐릅니다. ip4-input my-filter ip4-lookup ip4-rewrite error-drop 차단 조건이면 lookup 이전에 드롭
/* cli.c — 인터페이스별 enable/disable 제어 */
static clib_error_t *
my_filter_enable_disable (u32 sw_if_index, int is_enable)
{
    return vnet_feature_enable_disable(
        "ip4-unicast", "my-filter",
        sw_if_index, is_enable, 0, 0);
}

static clib_error_t *
my_filter_cli_fn (vlib_main_t *vm, unformat_input_t *input,
                  vlib_cli_command_t *cmd)
{
    u32 sw_if_index = ~0;
    int is_enable = 1;

    if (!unformat_user(input, unformat_vnet_sw_interface,
                       vnet_get_main(), &sw_if_index))
        return clib_error_return(0, "인터페이스 이름이 필요합니다.");

    if (unformat(input, "disable"))
        is_enable = 0;

    return my_filter_enable_disable(sw_if_index, is_enable);
}

VLIB_CLI_COMMAND (my_filter_cli, static) = {
    .path = "my-filter enable",
    .short_help = "my-filter enable <interface> [disable]",
    .function = my_filter_cli_fn,
};
코드 설명
  • 2~7행 vnet_feature_enable_disable()는 feature를 전역이 아니라 인터페이스 단위로 제어합니다. 따라서 운영 중 일부 포트에만 실험 기능을 붙이는 것이 가능합니다.
  • 9~22행 CLI 함수는 텍스트 입력을 sw_if_index로 해석한 뒤 enable/disable을 전환합니다. 실제 운영에서는 이 함수가 있으면 API가 없어도 즉시 현장 대응이 가능합니다.
  • 24~28행 VLIB_CLI_COMMAND로 제어면 진입점을 노출합니다. 플러그인 기능 자체보다 "켜고 끄는 수단"이 있어야 운영성이 확보됩니다.
# 1. 플러그인 로드
vpp# plugin my_filter_plugin.so enable

# 2. 특정 인터페이스에만 적용
vpp# my-filter enable GigabitEthernet0/8/0

# 3. 체인 확인
vpp# show features verbose

# 4. 패킷 흐름 확인
vpp# trace add my-filter 5
vpp# packet-generator enable
vpp# show trace
파일역할빠지면 생기는 문제
my_filter.c노드 함수와 next 분기 구현실제 패킷 처리 로직이 없습니다.
cli.c운영 중 enable/disable 제어재빌드 없이 인터페이스 단위 전환이 어렵습니다.
CMakeLists.txt플러그인 빌드 등록.so가 생성되지 않습니다.
VNET_FEATURE_INIT기존 그래프에 삽입노드는 존재하지만 실제 데이터 경로에 진입하지 못합니다.
자주 놓치는 지점: 노드만 등록하고 Feature Arc에 넣지 않으면 show node에는 보이지만 패킷은 지나가지 않습니다. 반대로 Arc에만 넣고 인터페이스별 enable을 하지 않으면 역시 실행되지 않습니다. 두 단계를 분리해서 확인해야 합니다.

운영 가능한 정책형 필터 노드 구현

위의 최소 예제는 개념 설명에는 충분하지만, 실무에 바로 쓰기에는 두 가지가 빠져 있습니다. 첫째, 인터페이스별 정책 상태를 런타임에 바꿀 수 있어야 합니다. 둘째, 실제 차단이 일어났을 때 trace와 에러 카운터로 즉시 확인할 수 있어야 합니다. 아래 코드는 목적지 TCP 포트를 인터페이스별로 차단하는 형태로 확장한 예시입니다.

/* 간략화한 운영형 필터 노드 예시 */
#include <vlib/vlib.h>
#include <vnet/vnet.h>
#include <vnet/ip/ip4_packet.h>
#include <vnet/tcp/tcp_packet.h>
#include <vnet/feature/feature.h>

typedef struct {
    u16 dst_port;
    u8 enabled;
    u8 trace;
} my_filter_if_config_t;

typedef struct {
    u32 sw_if_index;
    u16 dst_port;
    u8 dropped;
} my_filter_trace_t;

typedef struct {
    my_filter_if_config_t *if_cfg;
} my_filter_main_t;

my_filter_main_t my_filter_main;

typedef enum {
    MY_FILTER_NEXT_PASS,
    MY_FILTER_NEXT_DROP,
    MY_FILTER_N_NEXT,
} my_filter_next_t;

typedef enum {
    MY_FILTER_ERROR_DROP,
    MY_FILTER_N_ERROR,
} my_filter_error_t;

static char *my_filter_error_strings[] = {
    [MY_FILTER_ERROR_DROP] = "packets dropped by my-filter",
};

static_always_inline my_filter_if_config_t *
my_filter_get_cfg(u32 sw_if_index)
{
    my_filter_main_t *mm = &my_filter_main;
    vec_validate(mm->if_cfg, sw_if_index);
    return vec_elt_at_index(mm->if_cfg, sw_if_index);
}

static u8 *
format_my_filter_trace(u8 *s, va_list *args)
{
    my_filter_trace_t *t = va_arg(*args, my_filter_trace_t *);
    s = format(s, "sw_if_index %u dst-port %u dropped %u",
               t->sw_if_index, t->dst_port, t->dropped);
    return s;
}

VLIB_NODE_FN(my_filter_node) (vlib_main_t *vm,
                              vlib_node_runtime_t *node,
                              vlib_frame_t *frame)
{
    u32 *from = vlib_frame_vector_args(frame);
    u32 n_left_from = frame->n_vectors;

    while (n_left_from > 0) {
        u32 bi0 = from[0];
        u32 next0 = MY_FILTER_NEXT_PASS;
        vlib_buffer_t *b0 = vlib_get_buffer(vm, bi0);
        u32 sw_if_index0 = vnet_buffer(b0)->sw_if_index[VLIB_RX];
        my_filter_if_config_t *cfg0 = my_filter_get_cfg(sw_if_index0);
        ip4_header_t *ip0 = vlib_buffer_get_current(b0);
        u16 dst_port0 = 0;

        if (cfg0->enabled && ip0->protocol == IP_PROTOCOL_TCP) {
            tcp_header_t *tcp0 = (tcp_header_t *)(ip0 + 1);
            dst_port0 = clib_net_to_host_u16(tcp0->dst_port);
            if (dst_port0 == cfg0->dst_port) {
                next0 = MY_FILTER_NEXT_DROP;
                b0->error = node->errors[MY_FILTER_ERROR_DROP];
                vlib_node_increment_counter(vm, node->node_index,
                                            MY_FILTER_ERROR_DROP, 1);
            }
        }

        if (PREDICT_FALSE((b0->flags & VLIB_BUFFER_IS_TRACED) && cfg0->trace)) {
            my_filter_trace_t *tr = vlib_add_trace(vm, node, b0, sizeof(*tr));
            tr->sw_if_index = sw_if_index0;
            tr->dst_port = dst_port0;
            tr->dropped = (next0 == MY_FILTER_NEXT_DROP);
        }

        from += 1;
        n_left_from -= 1;
        vlib_set_next_frame_buffer(vm, node, next0, bi0);
    }

    return frame->n_vectors;
}
코드 설명
  • 7~19행 인터페이스별 설정 벡터와 trace 구조를 분리합니다. 운영 중 포트별 정책을 바꾸려면 전역 상수가 아니라 sw_if_index 기준의 상태 저장소가 필요합니다.
  • 29~34행 vec_validate()로 인터페이스 인덱스만큼 저장소를 자동 확장합니다. 새 인터페이스가 추가되어도 별도 재초기화 없이 정책 슬롯을 확보할 수 있습니다.
  • 47~61행 패킷마다 입력 인터페이스를 읽고, 그 인터페이스에 연결된 정책을 조회합니다. 정책이 켜져 있고 목적지 TCP 포트가 일치하면 MY_FILTER_NEXT_DROP로 분기합니다.
  • 56~58행 드롭 시 b0->error와 노드 카운터를 동시에 기록합니다. 이렇게 해야 show errors와 trace가 같은 사건을 가리키므로 현장 진단 속도가 크게 빨라집니다.
  • 63~68행 trace는 모든 패킷에 무조건 남기지 않고, 이미 trace 대상으로 지정된 패킷에만 추가합니다. 디버깅 때만 비용을 치르고 평상시 오버헤드를 줄이는 전형적인 방식입니다.
# 인터페이스별 정책 예시
vpp# my-filter set GigabitEthernet0/8/0 dst-port 443
vpp# my-filter trace GigabitEthernet0/8/0 on
vpp# my-filter enable GigabitEthernet0/8/0

# 검증
vpp# show features verbose
vpp# clear errors
vpp# trace add my-filter 5
# 외부에서 TCP 443 트래픽을 발생시킨 뒤
vpp# show trace
vpp# show errors
운영 기능왜 필요한가확인 명령
인터페이스별 정책실험 기능을 일부 포트에만 적용하거나 점진적으로 확장할 수 있습니다.show features verbose
드롭 카운터정책이 실제로 맞았는지, 아니면 다른 노드에서 드롭되었는지 구분할 수 있습니다.show errors
선택적 trace평상시 비용을 최소화하면서 문제 패킷만 상세히 볼 수 있습니다.trace add my-filter 5, show trace
실전 기준: 운영 가능한 플러그인은 "패킷을 처리하는 함수"보다 "무엇이 적용되었는지 보여주는 상태"가 더 중요할 때가 많습니다. 정책 벡터, 에러 카운터, trace 포맷까지 있어야 현장 대응 속도가 올라갑니다.