VPP 패킷 버퍼 구조 (vlib_buffer_t)
VPP vlib_buffer_t 구조체 상세: 2-캐시라인 레이아웃, 메타데이터 영역, current_data/current_length, chained buffer, 메모리 할당 패턴을 다룹니다.
vlib_buffer_t 메모리 레이아웃과 캐시라인
vlib_buffer_t는 VPP에서 패킷 데이터를 관리하는 핵심 구조체로, 커널의 sk_buff에 대응합니다. 정확히 2개의 캐시라인(128바이트)으로 설계되어 캐시 효율을 극대화합니다.
| 필드 | 오프셋(Offset) | 크기 | 용도 |
|---|---|---|---|
current_data | 0x00 | i16 | data[0]부터 현재 패킷 시작 위치까지의 오프셋 |
current_length | 0x02 | u16 | 현재 버퍼의 유효 데이터 길이 |
flags | 0x04 | u32 | VLIB_BUFFER_TOTAL_LENGTH_VALID, IS_TRACED 등 플래그 |
flow_id | 0x08 | u32 | NIC RSS 해시(Hash) 또는 플로우 분류 ID |
next_buffer | 0x0C | u32 | 체인 버퍼의 다음 버퍼 인덱스 (점보 프레임) |
current_config_index | 0x10 | u32 | Feature arc 설정 인덱스 |
error | 0x14 | u16 | 노드별 에러 코드 인덱스 |
n_add_refs | 0x16 | u8 | 추가 참조 카운트(Reference Count) (복제 시) |
buffer_pool_index | 0x17 | u8 | 버퍼가 속한 풀 인덱스 |
opaque[10] | 0x18 | 40B | 노드별 메타데이터 (ip4_header_t *, adjacency index 등) |
opaque2[14] | 0x48 | 56B | 확장 메타데이터 (vnet 계층, 트레이스 정보) |
trace_handle | — | u32 | show trace 결과와 연결되는 핸들 |
핵심 접근 API
/* vlib_buffer_t 핵심 접근 API */
/* 버퍼 인덱스(u32)로 버퍼 포인터 획득 */
vlib_buffer_t *b = vlib_get_buffer(vm, buffer_index);
/* 현재 데이터 시작 포인터 */
void *data = vlib_buffer_get_current(b);
/* 헤더 추가/제거 (current_data 이동) */
vlib_buffer_advance(b, -sizeof(ethernet_header_t)); /* 헤더 추가 */
vlib_buffer_advance(b, sizeof(ip4_header_t)); /* 헤더 제거 */
/* 체인 버퍼의 전체 길이 */
u32 total = vlib_buffer_length_in_chain(vm, b);
/* 버퍼 복제 (멀티캐스트 등) */
u32 clone_bi;
vlib_buffer_clone(vm, buffer_index, &clone_bi, 1, CLIB_CACHE_LINE_BYTES);
코드 설명
-
3행
vlib_get_buffer()는 32비트 인덱스를 버퍼 포인터로 변환합니다. 버퍼 풀의 시작 주소에 인덱스를 더하는 단순 산술 연산으로, O(1) 시간에 완료됩니다. -
6행
vlib_buffer_get_current()는b->data + b->current_data를 반환합니다. 각 노드가 헤더를 파싱한 후current_data오프셋을 전진시키므로, 다음 노드는 자신의 헤더 시작 위치를 바로 얻습니다. -
9~10행
vlib_buffer_advance()는 음수 값이면 헤더를 추가(포인터 후퇴), 양수이면 헤더를 소비(포인터 전진)합니다. 데이터 복사 없이 오프셋만 조정하여 제로 카피 처리를 가능하게 합니다. -
16~17행
vlib_buffer_clone()은 멀티캐스트나 미러링 시 사용됩니다. 원본 데이터는 공유하고 메타데이터만 복제하여 메모리 사용과 복사 비용을 최소화합니다.
opaque 영역과 버퍼 풀 운영
/* opaque 영역 사용 예: ip4 노드가 저장하는 메타데이터 */
typedef struct {
ip4_header_t *ip_header;
u32 adj_index;
u32 flow_hash;
u32 fib_index;
} ip4_buffer_opaque_t;
/* opaque 접근 매크로 */
#define vnet_buffer(b) ((vnet_buffer_opaque_t *)(b)->opaque)
#define vnet_buffer2(b) ((vnet_buffer_opaque2_t *)(b)->opaque2)
show buffers에서 free 카운트가 0에 가까워지면, 패킷이 error-drop 노드로 전달되며 show errors에 no-buffer 에러가 증가합니다. buffers-per-numa 값을 늘리거나 hugepage를 추가 할당하세요.
vlib_buffer_t.trace_handle이 연결하는 이벤트 로거(elog_main)를 SSVM 공유 메모리로 노출하는 selog 플러그인(src/plugins/selog/)이 26.02에서 추가되었습니다(실험적). 외부 모니터링 도구가 selog_get_shm API를 통해 파일 디스크립터를 받아 라이브 이벤트 로그를 읽을 수 있습니다. 기존 show trace / vlib_add_trace API는 변경 없이 유지됩니다.
체이닝된 버퍼 순회 패턴
점보 프레임이나 TSO(TCP Segmentation Offload) 시나리오에서 하나의 패킷은 여러 vlib_buffer_t가 단방향 링크드 리스트로 연결된 체인(chain) 형태로 표현됩니다. 각 버퍼의 next_buffer 필드가 다음 세그먼트의 버퍼 인덱스를 담고 있으며, 체인의 마지막 버퍼에는 VLIB_BUFFER_NEXT_PRESENT 플래그가 설정되지 않습니다.
단일 세그먼트 여부 확인
/* 체인 버퍼인지 단일 버퍼인지 확인 */
if (b->flags & VLIB_BUFFER_NEXT_PRESENT) {
/* 다음 세그먼트가 존재 — 체이닝된 버퍼 */
handle_chained_buffer(vm, b);
} else {
/* 단일 세그먼트 — current_length 가 전체 패킷 길이 */
handle_single_buffer(vm, b);
}
체인 순회 루프
/* 체인의 모든 세그먼트를 순서대로 순회하는 패턴 */
vlib_buffer_t *seg = b;
while (1) {
void *data = vlib_buffer_get_current(seg);
uword len = seg->current_length;
/* 현재 세그먼트의 데이터 처리 */
process_segment(data, len);
if (!(seg->flags & VLIB_BUFFER_NEXT_PRESENT))
break; /* 마지막 세그먼트 */
/* 다음 세그먼트로 이동 */
seg = vlib_get_buffer(vm, seg->next_buffer);
}
체인 순회 시 주의점: 순회 중 버퍼를 수정하거나 vlib_buffer_free를 호출하면 안 됩니다. 수정이 필요하다면 먼저 전체 체인을 파악한 뒤 처리하세요.
vlib_buffer_length_in_chain 활용
/* 체인 전체 길이(바이트) 획득 — 순회 없이 단번에 */
u32 total_len = vlib_buffer_length_in_chain(vm, b);
/* 내부 구현: VLIB_BUFFER_TOTAL_LENGTH_VALID 플래그가 세팅되어 있으면
b->total_length_not_including_first_buffer + b->current_length 반환.
플래그가 없으면 체인을 직접 순회하여 합산하고 플래그를 세팅함. */
/* 노드에서 total_length_not_including_first_buffer 를 직접 설정할 때 */
b->total_length_not_including_first_buffer = total_len - b->current_length;
b->flags |= VLIB_BUFFER_TOTAL_LENGTH_VALID;
체인 구성: vlib_buffer_chain_append_data
/* 새 버퍼를 할당하고 체인에 데이터를 추가하는 패턴 */
u32 n_alloc, new_bi;
u16 written;
/* 테일 버퍼(마지막 세그먼트)의 포인터 추적 */
vlib_buffer_t *tail = b;
/* vlib_buffer_chain_append_data: 공간이 부족하면 새 버퍼를 자동 할당하여 체인에 연결 */
written = vlib_buffer_chain_append_data(vm,
tail, /* 현재 테일 버퍼 */
data_ptr, /* 복사할 데이터 */
data_len /* 복사할 길이 */
);
if (written != data_len) {
/* 버퍼 풀 고갈 — 에러 처리 */
vlib_buffer_free_one(vm, vlib_get_buffer_index(vm, b));
return;
}
코드 설명
-
VLIB_BUFFER_NEXT_PRESENT
이 플래그가 설정된 버퍼는
next_buffer인덱스가 유효하며, 다음 세그먼트가 존재함을 나타냅니다. 마지막 세그먼트에는 이 플래그가 없습니다. -
vlib_buffer_length_in_chain
처음 호출 시에는 체인 전체를 순회하지만,
VLIB_BUFFER_TOTAL_LENGTH_VALID플래그 설정 이후에는 캐시된 값을 바로 반환합니다. 핫 패스에서 반복 호출 비용을 최소화합니다. -
vlib_buffer_chain_append_data
내부적으로 현재 버퍼의 남은 공간을 먼저 채우고, 부족하면
vlib_buffer_alloc으로 새 버퍼를 할당해 체인 끝에 연결합니다. 반환값이 요청 길이보다 작으면 버퍼 풀 고갈입니다.
버퍼 할당 전략과 재사용
VPP의 버퍼 관리는 NUMA 토폴로지를 인식하는 풀(pool) 기반으로 동작합니다. 올바른 할당 API 선택과 재사용 패턴이 성능에 직접 영향을 미칩니다.
vlib_buffer_alloc vs vlib_buffer_alloc_from_free_list
/* 방법 1: 기본 할당 — 현재 워커의 NUMA 노드 기본 풀에서 할당 */
u32 bis[VLIB_FRAME_SIZE];
u32 n_alloc;
n_alloc = vlib_buffer_alloc(vm, bis, 32);
if (n_alloc < 32) {
/* 요청한 개수보다 적게 할당됨 — 풀 고갈 가능성 */
vlib_buffer_free(vm, bis, n_alloc);
goto drop;
}
/* 방법 2: 특정 free-list(풀 인덱스)에서 할당 — 고정 크기 버퍼가 필요할 때 */
u8 pool_index = vlib_buffer_pool_get_default_for_numa(vm, numa_node);
n_alloc = vlib_buffer_alloc_from_free_list(vm, bis, 32, pool_index);
vlib_buffer_alloc은 대부분의 노드에서 충분하며, 특정 NUMA 노드나 버퍼 크기를 명시해야 할 때만 vlib_buffer_alloc_from_free_list를 사용합니다.
풀 할당: buffers-per-numa 설정
startup.conf의 buffers 섹션에서 NUMA 노드별 버퍼 풀 크기를 제어합니다.
## startup.conf 예시 — buffers 섹션
## 기본값: 16384 (16K 버퍼, 약 32MB @ 2K/버퍼)
buffers {
buffers-per-numa 65536 ## NUMA 노드당 버퍼 수 (2의 거듭제곱 권장)
default-data-size 2048 ## 버퍼당 데이터 영역 크기 (기본 2048)
page-size default-hugepage ## 2MB hugepage 사용 (DPDK 연동 시 필수)
}
버퍼 풀 현황은 show buffers CLI로 확인할 수 있습니다. free 컬럼이 전체의 20% 미만으로 떨어지면 buffers-per-numa를 늘려야 합니다.
이그레스 후 버퍼 재사용: vlib_buffer_free
/* TX 완료 또는 drop 시 버퍼 반환 */
/* 단일 버퍼 해제 */
vlib_buffer_free_one(vm, buffer_index);
/* 여러 버퍼 일괄 해제 (프레임 단위) */
vlib_buffer_free(vm, buffer_indices, n_buffers);
/* 체인 버퍼 전체 해제 — next_buffer 링크를 따라 모두 반환 */
vlib_buffer_free_one(vm, head_buffer_index); /* 체인 헤드만 전달하면 됨 */
/* n_add_refs 가 0이 될 때까지 실제 해제가 지연됨 (복제 버퍼 공유 해제) */
/* vlib_buffer_clone() 으로 복제된 버퍼는 참조 카운트가 1씩 증가됨 */
VLIB_BUFFER_REPL_FAIL 플래그
VLIB_BUFFER_REPL_FAIL은 버퍼 복제(replicate) 시도가 실패했음을 나타내는 플래그입니다. 멀티캐스트 복제 또는 vlib_buffer_clone 호출에서 버퍼 풀이 고갈되어 원하는 수만큼 복제하지 못했을 때, VPP 내부에서 이 플래그를 설정합니다.
/* 복제 실패 여부 확인 패턴 */
u32 n_clones;
n_clones = vlib_buffer_clone(vm, src_bi, clone_bis,
n_wanted, CLIB_CACHE_LINE_BYTES);
if (n_clones < n_wanted) {
/* 일부 복제 실패 — VLIB_BUFFER_REPL_FAIL 가 원본 버퍼에 세팅될 수 있음 */
/* 성공한 n_clones 개의 클론만 사용하고 나머지 전달 경로는 drop 처리 */
b->flags |= VLIB_BUFFER_REPL_FAIL;
}
/* 상위 노드(multicast-midchain 등)에서 이 플래그를 확인하여 에러 카운터 증가 */
if (b->flags & VLIB_BUFFER_REPL_FAIL)
vlib_node_increment_counter(vm, node->node_index,
MCAST_REPL_FAIL_ERROR, 1);
vlib_buffer_free 후 해당 버퍼 인덱스를 계속 참조하면 use-after-free 버그가 발생합니다. 해제 직후 인덱스 배열을 0으로 초기화하거나 포인터를 NULL로 설정하는 방어적 코딩을 권장합니다. 또한 n_add_refs > 0인 버퍼를 조기에 해제하면 다른 복제본이 손상됩니다.