Ring Buffer (ftrace)
리눅스 커널 ftrace의 핵심 데이터 저장소인 ring buffer(kernel/trace/ring_buffer.c)를 심층 분석합니다. per-CPU 페이지(Page) 기반 lockless 설계, NMI-safe 중첩 쓰기 메커니즘, ring_buffer_event 헤더와 페이로드(Payload) 인코딩, splice/mmap을 통한 zero-copy 읽기, TSC delta 타임스탬프 압축, 덮어쓰기/폐기 모드, 리사이즈와 스냅샷을 커널 소스 기반으로 분석합니다. kfifo 평탄 버퍼(Buffer)와의 구조적 차이, perf 링 버퍼(Ring Buffer)와의 비교, 실전 튜닝과 디버깅(Debugging)까지 포괄합니다.
핵심 요약
- per-CPU lockless -- 각 CPU가 독립된 버퍼를 가지며, 쓰기 시 lock 없이
local_cmpxchg로 공간을 예약합니다. CPU 간 간섭이 없어 확장성이 뛰어납니다. - 페이지 기반 순환 -- 연속 메모리가 아닌
buffer_page연결 리스트로 구성됩니다. 리사이즈 시 페이지 단위 추가/제거가 가능하고, splice로 zero-copy 전송이 됩니다. - NMI-safe 중첩 -- 일반 컨텍스트 → IRQ → NMI 순으로 최대 3단계 중첩 쓰기를 안전하게 처리합니다. 중첩 발생 시 상위 컨텍스트의 쓰기가 완료된 후 하위 컨텍스트가 commit합니다.
- 타임스탬프 압축 -- TSC delta를 27비트 time_delta로 인코딩하고, 범위 초과 시 확장 이벤트(RINGBUF_TYPE_TIME_EXTEND)를 삽입합니다.
- 두 가지 소비 모드 --
ring_buffer_consume은 이벤트를 소비(제거)하고,ring_buffer_peek은 비파괴적으로 읽습니다.
단계별 이해
- 순환 버퍼 기초 이해
생산자-소비자 패턴, head/tail 포인터, 오버플로 처리의 기본 개념을 파악합니다. - per-CPU 구조 파악
왜 CPU별 독립 버퍼가 필요한지, lockless 설계의 이점을 이해합니다. - 페이지 링 구조 추적
buffer_page 연결 리스트, tail_page/commit_page/reader_page의 역할을 따라갑니다. - 쓰기/읽기 경로 분석
이벤트 예약(reserve) → 쓰기 → commit, 그리고 consume/peek/iterator 읽기 경로를 추적합니다. - 실전 활용과 튜닝
buffer_size_kb 조정, trace_pipe 실시간 모니터링, 이벤트 손실 진단을 익힙니다.
kernel/trace/ring_buffer.c (약 6,000줄), include/linux/ring_buffer.h.
Steven Rostedt가 설계하고 현재까지 유지보수하고 있습니다. ftrace 전용으로 시작했지만, trace event 인프라 전체의 저장소로 사용됩니다.
링 버퍼 개요
리눅스 커널의 ring buffer는 ftrace 트레이싱 시스템의 핵심 데이터 저장소입니다. 2008년 Steven Rostedt에 의해 도입된 이 자료 구조는 다음과 같은 요구사항을 충족하도록 설계되었습니다.
| 요구사항 | 설계 결정 | 결과 |
|---|---|---|
| 최소 오버헤드(Overhead) 쓰기 | per-CPU lockless, local_cmpxchg | lock 경합(Contention) 없음, 캐시(Cache) 바운싱 최소 |
| NMI 컨텍스트 안전 | cmpxchg 기반 예약, nesting 카운터 | NMI → IRQ → 일반 3단 중첩 처리 |
| 동적 크기 조정 | 페이지 연결 리스트 (flat buffer 아님) | 런타임 페이지 추가/제거 |
| zero-copy 읽기 | splice 인터페이스, reader_page swap | 커널→사용자 메모리 복사 최소화 |
| 데이터 무결성(Integrity) | commit 포인터 분리, 타임스탬프 검증 | 부분 쓰기 이벤트 노출 방지 |
| 사용자 공간 직접 접근 | mmap 인터페이스, 메타 페이지 | 시스템 콜(System Call) 없는 이벤트 소비 |
ring buffer의 핵심 설계 원칙은 "쓰기 경로의 최소 오버헤드"입니다. 트레이싱은 관찰 대상 시스템에 최소한의 영향을 주어야 하므로, 이벤트 기록 경로에서 lock 획득, 메모리 할당, 시스템 콜이 발생하면 안 됩니다. 이를 위해 다음 기법들이 사용됩니다.
- per-CPU 분리: 각 CPU가 독립된 버퍼를 소유하므로 CPU 간 동기화가 불필요합니다.
- local_cmpxchg: 같은 CPU 내에서도 인터럽트(Interrupt) 중첩 처리를 위해 lock 대신
local_cmpxchg(CPU-local CAS)를 사용합니다. - 사전 할당: 버퍼 페이지는 트레이싱 시작 전에 미리 할당됩니다. 쓰기 경로에서
kmalloc이 호출되지 않습니다. - commit 분리: 예약(reserve)과 확정(commit)을 분리하여, 부분 쓰기 이벤트가 읽기 측에 노출되지 않습니다.
struct ring_buffer로 명명되었으나, 2020년(v5.7) 이후 네임스페이스(Namespace) 충돌 방지를 위해 struct trace_buffer로 변경되었습니다. 내부 구현 파일명(ring_buffer.c)은 그대로 유지됩니다.
설계 진화 타임라인
| 커널 버전 | 변경사항 | 의의 |
|---|---|---|
| 2.6.27 (2008) | ring buffer 최초 도입 | ftrace 전용 lockless per-CPU 버퍼 |
| 2.6.28 | reader_page swap 도입 | 읽기-쓰기 간섭 제거, splice 지원 기반 |
| 2.6.29 | splice 인터페이스 추가 | zero-copy 사용자 공간 전송 |
| 2.6.30 | snapshot 기능 추가 | swap 기반 즉시 버퍼 캡처 |
| 3.3 | per-CPU 개별 크기 설정 | CPU별 버퍼 크기 차등 설정 가능 |
| 3.10 | 타임스탬프 개선 | 절대 타임스탬프 마커, delta 검증 |
| 4.4 | ring buffer 벤치마크 모듈 | CONFIG_RING_BUFFER_BENCHMARK |
| 5.7 | struct trace_buffer 리네이밍 | 네임스페이스 충돌 방지 |
| 5.15 | sub-buffer 크기 설정 | 기본 PAGE_SIZE 외 크기 지원 |
| 6.6 | mmap 인터페이스 추가 | 시스템 콜 없는 이벤트 소비 |
| 6.8 | mmap 안정화, 메타 페이지 개선 | 사용자 공간 프로토콜 확정 |
lockless 설계 철학
ring buffer가 lock 대신 lockless 기법을 선택한 이유는 트레이싱의 특수한 요구사항에 있습니다.
/*
* 트레이싱 시스템의 특수 요구사항:
*
* 1. NMI 안전성
* - NMI에서 spin_lock을 잡을 수 없음
* - 이미 lock을 잡은 상태에서 NMI 발생 → 데드락
* - 따라서 lock 기반 설계 자체가 불가능
*
* 2. 관찰자 효과 최소화
* - 트레이싱은 관찰 대상에 영향을 최소화해야 함
* - lock 경합은 스케줄링 동작을 변화시킴
* - Heisenbug: 트레이싱으로 인해 버그가 사라지는 현상 방지
*
* 3. 인터럽트 컨텍스트 지원
* - IRQ 핸들러 내부 이벤트도 기록해야 함
* - irq_disable + lock은 인터럽트 지연을 유발
*
* 4. per-CPU + local_cmpxchg 조합
* - CPU 간: per-CPU 분리로 동기화 불필요
* - CPU 내: local_cmpxchg로 인터럽트 중첩 처리
* - 결과: 글로벌 lock 없이 완전한 동기화 달성
*/
local_cmpxchg는 x86에서 LOCK prefix 없는 CMPXCHG로 컴파일됩니다. 같은 CPU에서만 원자성이 필요하므로 캐시라인 배타적 소유권을 다른 CPU로 전파할 필요가 없어, 일반 cmpxchg보다 약 10배 빠릅니다. ARM64에서는 ldxr/stxr 쌍의 CPU-local 버전을 사용합니다.
per-CPU 아키텍처
ring buffer의 가장 중요한 설계 결정은 CPU별 독립 버퍼입니다. struct trace_buffer는 CPU 개수만큼의 struct ring_buffer_per_cpu 포인터 배열을 보유합니다.
/* include/linux/ring_buffer.h (simplified) */
struct trace_buffer {
unsigned flags;
int cpus;
atomic_t record_disabled;
cpumask_var_t cpumask;
struct lock_class_key *reader_lock_key;
struct mutex mutex;
struct ring_buffer_per_cpu **buffers; /* per-CPU 버퍼 배열 */
u64 (*clock)(void);
struct rb_irq_work irq_work;
};
각 ring_buffer_per_cpu는 해당 CPU에서만 쓰기가 발생합니다. 이로 인해 다음과 같은 이점이 있습니다.
| 측면 | per-CPU 설계 | 공유 버퍼 설계 (비교) |
|---|---|---|
| 쓰기 동기화 | 불필요 (CPU-local만) | 전역 lock 또는 전역 CAS 필요 |
| 캐시 바운싱 | 없음 (각 CPU가 자기 캐시라인) | 심각 (모든 CPU가 같은 캐시라인 경합) |
| NMI 안전성 | CPU-local cmpxchg로 해결 | NMI에서 lock 불가능 |
| 확장성 | O(1) -- CPU 수에 무관 | O(N) -- CPU 수에 비례하는 경합 |
| 읽기 순서 | CPU별 시간순, 병합 필요 | 전역 시간순 (자연 정렬) |
| 메모리 사용 | CPU 수 x 버퍼 크기 | 단일 버퍼 크기 |
/* kernel/trace/ring_buffer.c */
struct ring_buffer_per_cpu {
int cpu;
atomic_t record_disabled;
atomic_t resize_disabled;
struct trace_buffer *buffer;
raw_spinlock_t reader_lock; /* 읽기 측 직렬화 */
unsigned long nr_pages; /* 페이지 수 */
struct list_head *pages; /* 페이지 순환 리스트 */
struct buffer_page *head_page; /* 가장 오래된 데이터 */
struct buffer_page *tail_page; /* 현재 쓰기 위치 */
struct buffer_page *commit_page; /* 마지막 commit 위치 */
struct buffer_page *reader_page; /* 읽기 전용 분리 페이지 */
local_t entries; /* 총 이벤트 수 */
local_t overrun; /* 덮어쓴 이벤트 수 */
local_t commit_overrun; /* commit 충돌 수 */
local_t dropped_events; /* 폐기된 이벤트 수 */
local_t committing; /* 중첩 commit 추적 */
local_t commits; /* commit 완료 카운터 */
unsigned long read; /* reader_page 내 읽기 오프셋 */
unsigned long read_bytes;
u64 write_stamp; /* 마지막 쓰기 타임스탬프 */
u64 before_stamp; /* 예약 전 타임스탬프 */
rb_time_t write_stamp_rb;
rb_time_t before_stamp_rb;
/* ... irq_work, wakeup 등 생략 ... */
};
reader_lock(raw_spinlock_t)을 사용합니다. 이는 여러 읽기 클라이언트(trace_pipe, splice, iterator)가 동시에 같은 CPU 버퍼를 소비하는 것을 방지합니다. 쓰기-읽기 간 동기화는 commit 포인터와 reader_page swap 프로토콜로 처리됩니다.
페이지 기반 설계
ring buffer는 연속 메모리(flat buffer)가 아닌 buffer_page 순환 연결 리스트로 구성됩니다. 이 결정에는 여러 이유가 있습니다.
| 장점 | 설명 |
|---|---|
| 동적 리사이즈 | 페이지 단위 추가/제거로 런타임 크기 조정 가능 (flat buffer는 재할당 필요) |
| splice zero-copy | 개별 페이지를 파이프라인(Pipeline)에 직접 전달 (flat buffer는 연속 구간 추출이 복잡) |
| 메모리 할당 용이 | PAGE_SIZE 단위 할당은 항상 성공 가능 (대형 연속 블록은 fragmentation에 취약) |
| reader_page swap | 읽기 시 페이지 하나를 통째로 분리하여 쓰기 간섭 없이 처리 |
reader_page swap 상세
reader_page는 ring buffer에서 가장 독특한 메커니즘입니다. 읽기 측은 writer ring에서 페이지를 직접 읽지 않고, head_page를 분리하여 자신의 전용 페이지로 교환합니다.
/* rb_get_reader_page() 핵심 로직 (간략화) */
static struct buffer_page *rb_get_reader_page(
struct ring_buffer_per_cpu *cpu_buffer)
{
struct buffer_page *reader = cpu_buffer->reader_page;
struct buffer_page *head;
/* reader_page에 아직 읽지 않은 데이터가 있으면 그대로 반환 */
if (cpu_buffer->read < rb_page_commit(reader))
return reader;
/* reader_page 소비 완료 → head_page와 swap */
head = cpu_buffer->head_page;
/* cmpxchg로 head_page의 list->prev 포인터를 교체하여
* reader_page를 ring에 삽입하고 head_page를 분리 */
reader->list.next = head->list.next;
rb_set_list_to_head(&reader->list);
/* 원자적 swap: head를 ring에서 분리, reader를 삽입 */
unsigned long ret = cmpxchg(&head->list.prev->next,
&head->list, &reader->list);
cpu_buffer->reader_page = head;
cpu_buffer->read = 0;
return head;
}
tail_page를 전진시키는 도중에 reader가 head_page를 분리하면 리스트가 깨질 수 있으므로, cmpxchg로 원자적(Atomic)으로 교체합니다. swap 실패 시 재시도합니다.
페이지 끝 패딩(Padding)
이벤트가 현재 페이지에 들어가지 않으면, 남은 공간에 PADDING 이벤트를 삽입하고 다음 페이지로 이동합니다. PADDING 이벤트는 읽기 측에서 자동으로 건너뜁니다.
/* 페이지 끝 남은 공간에 PADDING 이벤트 삽입 */
static void rb_event_set_padding(
struct ring_buffer_event *event)
{
/* type_len = RINGBUF_TYPE_PADDING (29) */
event->type_len = RINGBUF_TYPE_PADDING;
event->time_delta = 0;
/* array[0]에 남은 공간 크기 저장 */
event->array[0] = rb_page_size(page) -
local_read(&page->write);
}
/*
* 페이지 구조 예시 (4KB 페이지, BUF_PAGE_SIZE = 4080):
*
* +------------------------------------+
* | buffer_data_page header (16 bytes) |
* | - time_stamp: u64 |
* | - commit: local_t |
* +------------------------------------+
* | event 1 (header + payload) |
* | event 2 (header + payload) |
* | event 3 (header + payload) |
* | ... |
* | PADDING event (남은 공간) |
* +------------------------------------+
*
* BUF_PAGE_SIZE = PAGE_SIZE - sizeof(buffer_data_page)
* = 4096 - 16 = 4080 bytes (사용 가능)
*/
서브버퍼 크기 설정 (5.15+)
Linux 5.15부터 ring buffer의 서브버퍼(페이지) 크기를 PAGE_SIZE가 아닌 값으로 설정할 수 있습니다. 대형 이벤트(예: 긴 문자열, 스택 트레이스)가 단일 페이지를 초과할 때 유용합니다.
# 서브버퍼 크기 설정 (기본: PAGE_SIZE = 4096)
echo 8192 > /sys/kernel/tracing/buffer_subbuf_size_kb # 8KB 서브버퍼
# 현재 설정 확인
cat /sys/kernel/tracing/buffer_subbuf_size_kb
# 주의: 변경 시 기존 버퍼 내용이 삭제됨
ring_buffer 구조체(Struct)
struct trace_buffer(이전 명칭 struct ring_buffer)는 전체 ring buffer 인스턴스를 대표하는 최상위 구조체입니다.
clock 함수
ring buffer는 clock 함수 포인터를 통해 타임스탬프를 얻습니다. 기본값은 trace_clock_local()로, sched_clock()의 CPU-local 변형입니다.
/* 사용 가능한 trace clock 소스 */
static struct {
char *name;
u64 (*func)(void);
} trace_clocks[] = {
{ "local", trace_clock_local }, /* 기본: CPU TSC, 빠름 */
{ "global", trace_clock_global }, /* 전역 동기화, 느림 */
{ "counter", trace_clock_counter }, /* atomic 카운터 */
{ "uptime", ktime_get_boot_fast_ns },/* 부팅 후 시간 */
{ "perf", ktime_get_mono_fast_ns },/* monotonic */
{ "mono", ktime_get_mono_fast_ns },/* monotonic */
{ "mono_raw", ktime_get_raw_fast_ns }, /* 보정 없는 monotonic */
{ "boot", ktime_get_boot_fast_ns },/* 부팅 후 (suspend 포함) */
{ "tai", ktime_get_tai_fast_ns }, /* TAI 시간 */
};
trace_clock_local은 CPU의 TSC를 직접 읽으므로 동기화 오버헤드가 없습니다. 단, CPU 간 TSC 오프셋(Offset)이 있을 수 있어 다른 CPU의 이벤트와 시간 비교가 부정확할 수 있습니다. CPU 간 정밀한 시간 비교가 필요하면 echo global > /sys/kernel/tracing/trace_clock을 사용하되, 쓰기 경로에 global lock이 추가되는 오버헤드를 감수해야 합니다.
buffer_page 구조체
struct buffer_page는 ring buffer의 물리적 저장 단위입니다. 각 buffer_page는 PAGE_SIZE(보통 4KB) 크기의 데이터 영역과 메타데이터를 가집니다.
/* kernel/trace/ring_buffer.c */
struct buffer_page {
struct list_head list; /* 순환 연결 리스트 */
local_t write; /* 현재 쓰기 오프셋 (페이지 내) */
unsigned read; /* reader_page 내 읽기 오프셋 */
local_t entries; /* 이 페이지의 이벤트 수 */
unsigned long real_end; /* 실제 데이터 끝 (패딩 전) */
struct buffer_data_page *page; /* 실제 데이터 페이지 */
};
struct buffer_data_page {
u64 time_stamp; /* 이 페이지의 기준 타임스탬프 */
local_t commit; /* 확정된 데이터 끝 오프셋 */
unsigned char data[]; /* 이벤트 데이터 (가변 길이) */
};
주요 필드의 역할을 정리하면 다음과 같습니다.
| 필드 | 타입 | 역할 |
|---|---|---|
list | list_head | 순환 이중 연결 리스트의 링크. prev->next에 flag 비트를 인코딩하여 head_page를 표시 |
write | local_t | 다음 쓰기 위치(바이트 오프셋). local_cmpxchg로 원자적 갱신 |
read | unsigned | reader_page에서만 사용. 현재 읽기 위치 |
entries | local_t | 이 페이지에 기록된 이벤트 수. 통계 및 빈 페이지 판별용 |
real_end | unsigned long | 마지막 이벤트 뒤의 실제 끝 위치. 페이지 끝 패딩과 구분 |
page->time_stamp | u64 | 이 페이지 첫 이벤트의 절대 타임스탬프. 이후 이벤트는 delta로 기록 |
page->commit | local_t | 확정된 데이터의 끝 오프셋. write와 다를 수 있음 (예약 후 commit 전) |
write를 증가시켜 공간을 예약(reserve)하고, 데이터를 채운 후 commit을 증가시켜 확정합니다. 읽기 측은 commit까지만 볼 수 있으므로, 쓰기 중인 불완전 이벤트가 노출되지 않습니다.
head_page 플래그 인코딩
head_page를 구별하기 위해 list.prev->next 포인터의 최하위 비트에 플래그를 인코딩합니다.
#define RB_PAGE_NORMAL 0 /* 일반 페이지 */
#define RB_PAGE_HEAD 1 /* head_page (가장 오래된 데이터) */
#define RB_PAGE_UPDATE 2 /* swap 진행 중 */
/* 포인터에 플래그 비트 설정/해제 */
static inline struct list_head *
rb_set_head_page(struct ring_buffer_per_cpu *cpu_buffer,
struct buffer_page *head)
{
unsigned long *ptr = (unsigned long *)&head->list.prev->next;
*ptr |= RB_PAGE_HEAD;
/* ... */
}
ring_buffer_event
ring buffer에 저장되는 각 이벤트는 struct ring_buffer_event 헤더와 페이로드로 구성됩니다. 헤더는 공간 효율을 위해 4바이트로 압축됩니다.
/* include/linux/ring_buffer.h */
struct ring_buffer_event {
u32 type_len:5, /* 이벤트 타입 + 길이 인코딩 */
time_delta:27; /* 이전 이벤트로부터의 시간 차이 (ns) */
u32 array[]; /* 확장 길이 또는 페이로드 시작 */
};
/* type_len 상수 */
enum ring_buffer_type {
RINGBUF_TYPE_DATA_TYPE_LEN_MAX = 28, /* 1~28: 인라인 길이 */
RINGBUF_TYPE_PADDING = 29, /* 페이지 끝 패딩 */
RINGBUF_TYPE_TIME_EXTEND = 30, /* 시간 확장 */
RINGBUF_TYPE_TIME_STAMP = 31, /* 절대 타임스탬프 */
};
/* 이벤트 크기 계산 헬퍼 */
static inline unsigned rb_event_data_length(
struct ring_buffer_event *event)
{
unsigned length;
if (event->type_len)
length = event->type_len * 4;
else
length = event->array[0]; /* 대형 이벤트 */
return length;
}
type_len이 1~28인 경우 페이로드 크기는 type_len * 4바이트(4~112바이트)입니다. 112바이트를 초과하는 대형 이벤트는 type_len = 0으로 표시하고, array[0]에 실제 크기를 저장합니다. 최소 이벤트 크기는 헤더(4B) + 페이로드(4B) = 8바이트입니다.
쓰기 경로
ring buffer의 쓰기 경로는 성능에 가장 민감한 부분입니다. 이벤트 하나를 기록하는 전체 흐름을 추적합니다.
reserve 상세: local_cmpxchg 기반 공간 예약
/* rb_reserve_next_event 핵심 로직 (간략화) */
static noinline struct ring_buffer_event *
rb_reserve_next_event(struct trace_buffer *buffer,
struct ring_buffer_per_cpu *cpu_buffer,
unsigned long length)
{
struct buffer_page *tail_page;
struct ring_buffer_event *event;
unsigned long tail, write;
again:
tail_page = cpu_buffer->tail_page;
tail = local_read(&tail_page->write);
write = tail + length;
/* 현재 페이지에 공간이 있는가? */
if (likely(write <= BUF_PAGE_SIZE)) {
/* Fast path: local_cmpxchg로 원자적 예약 */
if (local_cmpxchg(&tail_page->write,
tail, write) != tail)
goto again; /* 중첩(IRQ/NMI) 발생, 재시도 */
event = (struct ring_buffer_event *)
(tail_page->page->data + tail);
rb_update_event(cpu_buffer, event, length,
0, delta);
return event;
}
/* Slow path: 페이지 전환 필요 */
return rb_move_tail(cpu_buffer, tail, tail_page, ...);
}
local_cmpxchg는 같은 CPU 내에서만 원자성을 보장하는 CAS 연산입니다. 다른 CPU와의 동기화 오버헤드가 없어 매우 빠릅니다(x86에서 LOCK prefix 없는 CMPXCHG). 이것이 가능한 이유는 ring buffer가 per-CPU 구조이기 때문입니다. 같은 CPU 내에서 인터럽트 중첩(IRQ, NMI)에 의한 동시 접근만 처리하면 됩니다.
commit 상세
/* ring_buffer_unlock_commit 핵심 로직 */
void ring_buffer_unlock_commit(
struct trace_buffer *buffer)
{
struct ring_buffer_per_cpu *cpu_buffer;
cpu_buffer = buffer->buffers[raw_smp_processor_id()];
/* commit 포인터 전진 시도 */
rb_commit(cpu_buffer);
/* 대기 중인 reader가 있으면 wakeup */
rb_wakeups(buffer, cpu_buffer);
preempt_enable_notrace();
}
static inline void rb_commit(
struct ring_buffer_per_cpu *cpu_buffer)
{
unsigned long commits;
/* committing 카운터 감소 -- 중첩 추적 */
commits = local_read(&cpu_buffer->commits);
/* 가장 바깥쪽 컨텍스트만 실제 commit 수행 */
while (local_read(&cpu_buffer->committing) == commits) {
/* commit 포인터를 write 위치까지 전진 */
local_set(&cpu_buffer->commit_page->page->commit,
local_read(&cpu_buffer->commit_page->write));
/* 다음 페이지로 commit_page 전진 (필요시) */
rb_set_commit_to_write(cpu_buffer);
break;
}
}
NMI-safe 중첩 쓰기
ring buffer가 해결해야 하는 가장 어려운 문제 중 하나는 인터럽트 중첩입니다. 일반 컨텍스트에서 이벤트를 쓰는 도중 IRQ가 발생하고, IRQ 핸들러(Handler) 내에서 또 이벤트를 쓰고, 심지어 NMI가 발생할 수 있습니다.
중첩 안전성의 핵심은 다음 두 가지 메커니즘입니다.
nesting 카운터
/* 중첩 추적 */
static inline int rb_is_nested(
struct ring_buffer_per_cpu *cpu_buffer)
{
/* committing 카운터가 commits보다 크면 중첩 상태 */
return local_read(&cpu_buffer->committing) !=
local_read(&cpu_buffer->commits);
}
/* reserve 시: committing 증가 */
local_inc(&cpu_buffer->committing);
/* commit 시: commits 증가 후, 가장 바깥쪽이면 실제 commit */
local_inc(&cpu_buffer->commits);
if (local_read(&cpu_buffer->committing) ==
local_read(&cpu_buffer->commits))
rb_set_commit_to_write(cpu_buffer);
cmpxchg 기반 예약
local_cmpxchg를 사용한 공간 예약은 인터럽트에 의한 동시 접근을 자연스럽게 처리합니다.
| 시나리오 | 동작 | 결과 |
|---|---|---|
| Normal만 쓰기 | cmpxchg 즉시 성공 | 1회 시도로 예약 완료 |
| Normal 중 IRQ 발생 | IRQ가 write 변경 → Normal의 cmpxchg 실패 | Normal이 재시도(goto again) |
| IRQ 중 NMI 발생 | NMI가 write 변경 → IRQ의 cmpxchg 실패 | IRQ가 재시도 |
| 동시 실패 연쇄 | 각 레벨이 순차적으로 재시도 | 최대 3회 재시도 (Normal→IRQ→NMI) |
spin_lock이나 mutex를 사용할 수 없습니다. ring buffer가 local_cmpxchg만으로 동기화하는 이유가 바로 이것입니다. 만약 ring buffer가 lock 기반이었다면, NMI에서 데드락이 발생할 수 있습니다.
중첩 깊이와 컨텍스트 분류
ring buffer는 최대 4단계 중첩을 지원합니다. 각 컨텍스트 레벨은 preempt_count로 식별됩니다.
| 레벨 | 컨텍스트 | preempt_count 영역 | ring buffer 동작 |
|---|---|---|---|
| 0 | Normal (프로세스(Process)) | PREEMPT_MASK | 가장 바깥쪽, 최종 commit 담당 |
| 1 | Soft IRQ | SOFTIRQ_OFFSET | Normal 예약을 중단시킬 수 있음 |
| 2 | Hard IRQ | HARDIRQ_OFFSET | Soft IRQ 예약을 중단시킬 수 있음 |
| 3 | NMI | NMI 감지 | 최상위 우선순위(Priority), 최내부 예약 |
/* 컨텍스트 레벨 식별 (간략화) */
static inline int rb_ctx_idx(int pc)
{
if (in_nmi())
return 3; /* NMI 컨텍스트 */
if (pc & HARDIRQ_MASK)
return 2; /* Hard IRQ 컨텍스트 */
if (pc & SOFTIRQ_OFFSET)
return 1; /* Soft IRQ 컨텍스트 */
return 0; /* Normal 컨텍스트 */
}
/*
* 각 컨텍스트 레벨은 독립된 nesting 추적:
* - Normal에서 reserve → committing = 1
* - IRQ 발생, IRQ에서 reserve → committing = 2
* - NMI 발생, NMI에서 reserve → committing = 3
* - NMI commit → commits = 1 (커밋 보류)
* - IRQ commit → commits = 2 (커밋 보류)
* - Normal commit → commits = 3 (committing == commits → 실제 commit!)
*/
NMI 중첩과 타임스탬프 문제
NMI 중첩이 발생하면 타임스탬프 순서가 역전될 수 있습니다. 예를 들어, Normal 컨텍스트에서 타임스탬프 T1을 획득한 후 NMI가 발생하여 T2(T2 > T1)를 획득하면, 페이지 내에서 T2 이벤트가 T1 이벤트보다 먼저 기록됩니다.
/* 타임스탬프 역전 감지 및 보정 */
static inline u64
rb_time_delta(struct ring_buffer_event *event,
u64 ts, u64 prev_ts)
{
u64 delta = ts - prev_ts;
/* NMI 중첩으로 역전 발생 시 */
if (unlikely((s64)delta < 0)) {
/* delta를 0으로 처리하고 절대 타임스탬프 삽입 */
delta = 0;
/* rb_add_time_stamp() 호출하여 TIME_STAMP 마커 삽입 */
}
return delta;
}
/*
* before_stamp / write_stamp 프로토콜:
*
* 1. before_stamp = 현재 타임스탬프 (예약 전)
* 2. local_cmpxchg로 공간 예약
* 3. write_stamp = 현재 타임스탬프 (예약 후)
*
* 중첩 발생 시:
* - 상위 컨텍스트의 write_stamp가 하위보다 클 수 있음
* - before_stamp vs write_stamp 비교로 중첩 감지
* - 감지되면 절대 타임스탬프로 재동기화
*/
write_stamp와 before_stamp에 u64를 직접 사용했지만, 32비트 아키텍처에서 원자적 64비트 읽기/쓰기가 불가능하여 문제가 발생했습니다. 이를 해결하기 위해 rb_time_t 추상화가 도입되었으며, 32비트에서는 3개의 unsigned long으로 분할 저장합니다 (상위/하위/MSB).
읽기 경로
ring buffer의 읽기 경로는 쓰기 경로와 달리 reader_lock(raw_spinlock)으로 보호됩니다. 주요 읽기 API는 다음과 같습니다.
| API | 동작 | 소비 여부 | 주 사용처 |
|---|---|---|---|
ring_buffer_peek | 다음 이벤트를 반환 (제거하지 않음) | 비소비 | trace 파일 읽기 |
ring_buffer_consume | 다음 이벤트를 반환하고 제거 | 소비 | trace_pipe 읽기 |
ring_buffer_iter_peek | iterator 위치의 이벤트 반환 | 비소비 | 순차 탐색 |
ring_buffer_read_page | 전체 페이지를 읽기 | 소비 | splice, 대량 전송 |
/* ring_buffer_consume 핵심 로직 */
struct ring_buffer_event *
ring_buffer_consume(struct trace_buffer *buffer, int cpu,
u64 *ts, unsigned long *lost_events)
{
struct ring_buffer_per_cpu *cpu_buffer;
struct ring_buffer_event *event = NULL;
cpu_buffer = buffer->buffers[cpu];
raw_spin_lock_irqsave(&cpu_buffer->reader_lock, flags);
/* reader_page에서 다음 이벤트 가져오기 */
event = rb_buffer_peek(cpu_buffer, ts, lost_events);
if (!event)
goto out;
/* 읽기 포인터 전진 */
rb_advance_reader(cpu_buffer);
out:
raw_spin_unlock_irqrestore(&cpu_buffer->reader_lock, flags);
return event;
}
peek vs consume 차이
peek은 이벤트를 읽되 읽기 포인터를 전진시키지 않으므로, 같은 이벤트를 여러 번 읽을 수 있습니다. /sys/kernel/tracing/trace 파일이 이 방식을 사용합니다. 반면 consume은 읽기 포인터를 전진시켜 이벤트를 "소비"하므로, trace_pipe처럼 실시간 스트리밍에 적합합니다.
cat trace는 버퍼 내용을 비파괴적으로 읽습니다(여러 번 읽기 가능). cat trace_pipe는 이벤트를 소비하므로, 한 번 읽은 이벤트는 사라집니다. 실시간 모니터링에는 trace_pipe가 적합하고, 사후 분석에는 trace가 적합합니다.
rb_advance_reader 상세
이벤트를 소비(consume)할 때 rb_advance_reader가 읽기 포인터를 전진시킵니다. reader_page 내에서 다음 이벤트로 이동하거나, 페이지 소진 시 새 reader_page를 확보합니다.
/* rb_advance_reader 핵심 로직 (간략화) */
static void rb_advance_reader(
struct ring_buffer_per_cpu *cpu_buffer)
{
struct ring_buffer_event *event;
struct buffer_page *reader;
unsigned length;
reader = cpu_buffer->reader_page;
event = rb_reader_event(cpu_buffer);
/* 현재 이벤트의 전체 크기(헤더 + 페이로드 + 정렬 패딩) */
length = rb_event_length(event);
/* PADDING 이벤트면 페이지 끝까지 건너뛰기 */
if (event->type_len == RINGBUF_TYPE_PADDING) {
cpu_buffer->read = rb_page_commit(reader);
return;
}
/* TIME_EXTEND/TIME_STAMP은 건너뛰기 */
if (event->type_len >= RINGBUF_TYPE_TIME_EXTEND) {
cpu_buffer->read += length;
return;
}
/* 일반 데이터 이벤트: 읽기 포인터 전진 */
cpu_buffer->read += length;
local_dec(&cpu_buffer->entries);
cpu_buffer->read_bytes += length;
}
/*
* reader_page 소진 시:
* rb_get_reader_page() → head_page와 swap
* → 새 reader_page에서 cpu_buffer->read = 0 재설정
*/
lost_events 추적
읽기 API에서 lost_events 매개변수는 마지막 읽기 이후 손실된 이벤트 수를 반환합니다. 이는 overwrite 모드에서 reader가 소비하지 못한 사이에 덮어쓰기가 발생한 경우를 추적합니다.
/* lost_events 계산 */
static struct ring_buffer_event *
rb_buffer_peek(struct ring_buffer_per_cpu *cpu_buffer,
u64 *ts, unsigned long *lost_events)
{
struct buffer_page *reader;
reader = rb_get_reader_page(cpu_buffer);
if (!reader)
return NULL;
/* reader_page swap 시 분리된 페이지의 overrun을 기록 */
if (lost_events)
*lost_events = local_read(&cpu_buffer->lost_events);
/* 다음 이벤트 반환 */
return rb_reader_event(cpu_buffer);
}
trace_pipe를 읽을 때 이벤트 손실이 감지되면, 출력에 CPU:X [LOST N EVENTS] 메시지가 삽입됩니다. 이를 통해 사용자는 트레이스 데이터에 빈틈이 있음을 알 수 있습니다. 손실을 줄이려면 버퍼 크기를 늘리거나, 읽기 빈도를 높이세요.
iterator 인터페이스
iterator는 ring buffer를 순차적으로 탐색하기 위한 인터페이스입니다. trace 파일 읽기 시 사용됩니다.
/* iterator 생명주기 */
struct ring_buffer_iter *iter;
/* 1. iterator 시작 -- 쓰기를 비활성화 (정확한 스냅샷) */
iter = ring_buffer_read_prepare(buffer, cpu, GFP_KERNEL);
ring_buffer_read_prepare_sync();
ring_buffer_read_start(iter);
/* 2. 순차 읽기 */
while ((event = ring_buffer_iter_peek(iter, ts))) {
/* 이벤트 처리 */
process_event(event);
ring_buffer_iter_advance(iter);
}
/* 3. iterator 종료 -- 쓰기 재활성화 */
ring_buffer_read_finish(iter);
ring_buffer_read_start는 해당 CPU 버퍼의 쓰기를 비활성화합니다. 이는 읽기 중 쓰기가 발생하면 데이터 일관성이 깨질 수 있기 때문입니다. 따라서 iterator는 짧은 시간만 사용해야 합니다. 장시간 사용하면 해당 CPU의 트레이스 이벤트가 손실됩니다. 실시간 모니터링에는 ring_buffer_consume이 적합합니다.
iterator 내부 구조
struct ring_buffer_iter {
struct ring_buffer_per_cpu *cpu_buffer;
unsigned long head; /* 현재 읽기 위치 */
unsigned long next_event;
struct buffer_page *head_page;
struct buffer_page *cache_reader_page;
unsigned long cache_read;
u64 read_stamp;
u64 page_stamp;
struct ring_buffer_event *event;
int missed_events;
};
splice 인터페이스
trace_pipe를 통한 대량 데이터 전송에서 splice 시스템 콜은 zero-copy 전송을 가능하게 합니다. ring buffer의 ring_buffer_read_page가 전체 buffer_page를 파이프 버퍼에 직접 전달합니다.
/* splice 읽기 경로 (간략화) */
static ssize_t
tracing_buffers_splice_read(struct file *filp,
loff_t *ppos,
struct pipe_inode_info *pipe,
size_t len,
unsigned int flags)
{
struct ftrace_buffer_info *info = filp->private_data;
struct trace_buffer *buffer = info->buffer;
/* 페이지 단위로 ring buffer에서 읽기 */
ref = ring_buffer_alloc_read_page(buffer, cpu);
ring_buffer_read_page(buffer, ref, len, cpu, 1);
/* 읽은 페이지를 pipe 버퍼에 직접 매핑 */
spd.pages[i] = ref->page;
spd.partial[i].len = PAGE_SIZE;
/* splice_to_pipe로 사용자 공간에 전달 */
return splice_to_pipe(pipe, &spd);
}
splice의 장점은 커널 버퍼 페이지를 사용자 공간으로 직접 매핑(Mapping)하여 memcpy를 피한다는 것입니다. 대량 트레이스 데이터를 파일로 저장할 때 특히 효과적입니다.
# splice를 활용한 고속 트레이스 수집
cat /sys/kernel/tracing/trace_pipe > /tmp/trace.log &
# 또는 trace-cmd로 splice 기반 수집
trace-cmd record -e sched_switch -o /tmp/trace.dat
splice vs 일반 read 성능 비교
| 방법 | 데이터 복사 | 시스템 콜 | 처리량 (대략) |
|---|---|---|---|
| read() (trace_pipe) | 커널→사용자 memcpy | read당 1회 | ~200 MB/s |
| splice() (trace_pipe_raw) | 없음 (페이지 참조 전달) | splice+write 2회 | ~1 GB/s |
| mmap (trace_pipe_raw) | 없음 (직접 매핑) | ioctl만 (드물게) | ~2 GB/s |
/* ring_buffer_read_page: splice용 전체 페이지 읽기 */
int ring_buffer_read_page(struct trace_buffer *buffer,
struct buffer_data_read_page *data_page,
size_t len, int cpu, int full)
{
struct ring_buffer_per_cpu *cpu_buffer;
struct buffer_page *reader;
unsigned long flags;
cpu_buffer = buffer->buffers[cpu];
raw_spin_lock_irqsave(&cpu_buffer->reader_lock, flags);
reader = rb_get_reader_page(cpu_buffer);
if (!reader)
goto out_unlock;
/* 페이지 내 commit된 데이터가 있으면 */
if (rb_page_commit(reader) >= len || !full) {
/* reader_page의 데이터를 data_page로 교환 */
swap(reader->page, data_page->data);
/* 새 빈 페이지가 reader_page에 들어감 */
}
out_unlock:
raw_spin_unlock_irqrestore(&cpu_buffer->reader_lock, flags);
return ret;
}
mmap 인터페이스
Linux 6.6부터 ring buffer는 mmap 인터페이스를 지원합니다. 사용자 공간에서 시스템 콜 없이 직접 이벤트를 소비할 수 있어, 고빈도 트레이싱의 오버헤드를 더욱 줄입니다.
/* 사용자 공간에서 mmap 기반 이벤트 소비 (예시) */
int fd = open("/sys/kernel/tracing/per_cpu/cpu0/trace_pipe_raw",
O_RDONLY);
/* mmap: meta page + 서브버퍼 배열 */
size_t meta_len = PAGE_SIZE;
size_t data_len = nr_subbufs * subbuf_size;
void *mmap_addr = mmap(NULL, meta_len + data_len,
PROT_READ, MAP_SHARED, fd, 0);
struct ring_buffer_meta *meta = mmap_addr;
void *data_start = mmap_addr + meta_len;
/* reader 서브버퍼에서 이벤트 소비 */
while (1) {
int reader_id = meta->reader.id;
void *subbuf = data_start + reader_id * subbuf_size;
/* 서브버퍼 내 이벤트 순회 */
process_events_in_subbuf(subbuf, meta->reader.read);
/* 다음 서브버퍼 요청 */
ioctl(fd, TRACE_MMAP_IOCTL_GET_READER);
}
trace_pipe(splice 기반)가 충분하며, 극한 성능이 필요한 경우에만 mmap을 고려합니다.
타임스탬프 메커니즘
ring buffer의 타임스탬프 시스템은 공간 효율과 정밀도를 모두 달성하기 위한 정교한 설계를 가지고 있습니다.
/* 타임스탬프 delta 계산 */
static inline void rb_update_event(
struct ring_buffer_per_cpu *cpu_buffer,
struct ring_buffer_event *event,
struct rb_event_info *info)
{
u64 delta = info->delta;
/* 27비트 이내: 헤더에 직접 저장 */
if (delta < (1ULL << 27)) {
event->time_delta = delta;
} else {
/* 27비트 초과: TIME_EXTEND 이벤트 삽입 */
event->type_len = RINGBUF_TYPE_TIME_EXTEND;
event->time_delta = delta & ((1ULL << 27) - 1);
event->array[0] = delta >> 27;
}
}
절대 타임스탬프 (TIME_STAMP)
RINGBUF_TYPE_TIME_STAMP(type_len=31)은 절대 타임스탬프 재동기화에 사용됩니다. 주로 다음 상황에서 삽입됩니다.
- 새 페이지의 첫 이벤트 (페이지 전환 시)
- NMI 중첩 후 타임스탬프 일관성 복구
- 명시적 동기화 마커 요청 시
덮어쓰기 vs 폐기 모드
ring buffer가 가득 찼을 때 새 이벤트를 어떻게 처리할지 두 가지 정책이 있습니다.
| 모드 | 동작 | 데이터 손실 | 사용 시나리오 |
|---|---|---|---|
OverwriteRB_FL_OVERWRITE |
가장 오래된 페이지를 버리고 새 이벤트 기록 | 오래된 이벤트 손실, overrun 카운터 증가 |
최근 이벤트가 중요한 경우 (기본값) |
| Discard overwrite 비활성 |
새 이벤트를 폐기 | 새 이벤트 손실, dropped_events 증가 |
초기 이벤트가 중요한 경우 |
/* overwrite 모드에서 tail이 head를 따라잡았을 때 */
static void rb_handle_head_page(
struct ring_buffer_per_cpu *cpu_buffer,
struct buffer_page *tail_page,
struct buffer_page *next_page)
{
/* overwrite 모드: head_page를 다음으로 전진 */
if (cpu_buffer->buffer->flags & RB_FL_OVERWRITE) {
/* 가장 오래된 페이지의 이벤트 수를 overrun에 추가 */
local_add(rb_page_entries(next_page),
&cpu_buffer->overrun);
/* head_page를 한 칸 전진 */
rb_inc_page(&cpu_buffer->head_page);
} else {
/* discard 모드: 이벤트 기록 거부 */
local_inc(&cpu_buffer->dropped_events);
}
}
# overwrite 모드 제어
echo "overwrite" > /sys/kernel/tracing/trace_options # 덮어쓰기 활성화 (기본)
echo "nooverwrite" > /sys/kernel/tracing/trace_options # 폐기 모드
# 이벤트 손실 확인
cat /sys/kernel/tracing/per_cpu/cpu0/stats
# entries: 12345
# overrun: 678 ← overwrite 모드에서 덮어쓴 수
# commit overrun: 0
# dropped events: 0 ← discard 모드에서 폐기한 수
덮어쓰기 내부 메커니즘
overwrite 모드에서 tail_page가 head_page를 따라잡으면, head_page의 데이터를 포기하고 한 칸 전진시킵니다. 이때 reader_page와의 충돌도 고려해야 합니다.
/* rb_move_tail에서 overwrite 처리 (간략화) */
static noinline struct ring_buffer_event *
rb_move_tail(struct ring_buffer_per_cpu *cpu_buffer,
unsigned long tail,
struct buffer_page *tail_page)
{
struct buffer_page *next_page = tail_page->list.next;
/* tail_page에 PADDING 이벤트 삽입 (남은 공간 표시) */
rb_event_set_padding(event);
local_sub(length, &cpu_buffer->entries_bytes);
/* 다음 페이지가 head_page인가? (버퍼 가득 참) */
if (next_page == cpu_buffer->head_page) {
if (!(cpu_buffer->buffer->flags & RB_FL_OVERWRITE)) {
/* discard 모드: 이벤트 거부 */
local_inc(&cpu_buffer->dropped_events);
return NULL;
}
/* overwrite 모드: head_page를 밀어내기 */
rb_handle_head_page(cpu_buffer, tail_page, next_page);
}
/* tail_page를 next_page로 전진 */
rb_tail_page_update(cpu_buffer, tail_page, next_page);
/* 새 tail_page에서 이벤트 예약 재시도 */
return rb_reserve_next_event(cpu_buffer, length);
}
overwrite와 reader 충돌
overwrite 모드에서 head_page를 전진시킬 때, reader가 이미 해당 페이지를 분리(swap)했을 수 있습니다. 이 충돌은 RB_PAGE_HEAD 플래그로 감지합니다.
| 상황 | 감지 방법 | 처리 |
|---|---|---|
| head_page가 ring에 있음 | prev->next에 RB_PAGE_HEAD 설정됨 | 정상: head_page 전진 |
| head_page가 reader에게 분리됨 | prev->next에 RB_PAGE_HEAD 없음 | 대기: reader swap 완료 후 재시도 |
| swap 진행 중 | prev->next에 RB_PAGE_UPDATE 설정됨 | 대기: cmpxchg 완료 후 재시도 |
버퍼 가득 참 알림
discard 모드에서 버퍼가 가득 차면, 대기 중인 reader에게 wakeup 신호를 보내 소비를 촉진합니다.
/* irq_work 기반 reader wakeup */
static void rb_wakeups(struct trace_buffer *buffer,
struct ring_buffer_per_cpu *cpu_buffer)
{
/* full 상태이면 waiters 깨우기 */
if (unlikely(atomic_read(&cpu_buffer->record_disabled)))
return;
/* irq_work로 안전하게 wakeup (NMI 컨텍스트에서도 동작) */
if (waitqueue_active(&buffer->irq_work.waiters))
irq_work_queue(&buffer->irq_work.work);
if (waitqueue_active(&cpu_buffer->irq_work.waiters))
irq_work_queue(&cpu_buffer->irq_work.work);
}
wake_up을 직접 호출할 수 없습니다(스케줄링 불가). irq_work는 가능한 빨리 실행되는 안전한 지연(Latency) 실행 메커니즘으로, 인터럽트 복귀 시 reader를 깨웁니다.
리사이즈와 스냅샷
ring buffer는 런타임에 크기를 동적으로 조정할 수 있으며, 특정 시점의 버퍼 상태를 스냅샷으로 저장할 수 있습니다.
동적 리사이즈
/* 버퍼 크기 변경 */
int ring_buffer_resize(struct trace_buffer *buffer,
unsigned long size, int cpu)
{
/* 새 페이지 수 계산 */
unsigned long nr_pages = DIV_ROUND_UP(size, BUF_PAGE_SIZE);
/* 증가: 새 페이지를 할당하여 링에 삽입 */
/* 감소: 여분 페이지를 링에서 제거하고 해제 */
/* 축소 시 트레이싱 일시 정지 필요 */
atomic_inc(&cpu_buffer->resize_disabled);
schedule_work_on(cpu, &cpu_buffer->update_pages_work);
/* ... 워커에서 실제 리사이즈 수행 ... */
}
# 버퍼 크기 조정 (KB 단위)
echo 16384 > /sys/kernel/tracing/buffer_size_kb # 전체 CPU 16MB
echo 32768 > /sys/kernel/tracing/per_cpu/cpu0/buffer_size_kb # CPU0만 32MB
# 현재 크기 확인
cat /sys/kernel/tracing/buffer_size_kb
스냅샷
스냅샷은 현재 ring buffer의 상태를 순간적으로 저장하는 기능입니다. 구현은 매우 효율적입니다 -- 별도의 ring buffer 인스턴스(snapshot buffer)와 현재 버퍼의 페이지 배열을 swap하는 것입니다.
/* 스냅샷 = 두 ring buffer의 swap */
void ring_buffer_swap(struct trace_buffer *buffer_a,
struct trace_buffer *buffer_b)
{
for_each_buffer_cpu(buffer_a, cpu) {
/* 각 CPU의 페이지 리스트를 교환 */
swap(buffer_a->buffers[cpu]->pages,
buffer_b->buffers[cpu]->pages);
swap(buffer_a->buffers[cpu]->head_page,
buffer_b->buffers[cpu]->head_page);
/* ... 나머지 포인터도 swap ... */
}
}
# 스냅샷 활용
echo 1 > /sys/kernel/tracing/snapshot # 현재 상태 저장
cat /sys/kernel/tracing/snapshot # 스냅샷 읽기
echo 0 > /sys/kernel/tracing/snapshot # 스냅샷 해제
# trace-cmd에서 스냅샷
trace-cmd snapshot
스냅샷 트리거
수동 스냅샷 외에, 특정 조건이 만족되면 자동으로 스냅샷을 찍는 트리거를 설정할 수 있습니다.
# 조건부 스냅샷: sched_switch에서 특정 PID 감지 시
echo 'snapshot if prev_pid == 1234' > \
/sys/kernel/tracing/events/sched/sched_switch/trigger
# stacktrace + snapshot 결합
echo 'snapshot:stacktrace if next_comm ~ "bad_proc"' > \
/sys/kernel/tracing/events/sched/sched_switch/trigger
# 지연 임계값 초과 시 자동 스냅샷 (irqsoff tracer)
echo 1000 > /sys/kernel/tracing/tracing_thresh # 1000us 임계값
echo irqsoff > /sys/kernel/tracing/current_tracer
# 스냅샷 확인
cat /sys/kernel/tracing/snapshot
리사이즈 제약사항
| 제약 | 설명 | 대응 방법 |
|---|---|---|
| 최소 크기 | 최소 2 페이지 (ring 구조 유지) | buffer_size_kb >= 8 (4KB 페이지 기준) |
| 트레이싱 중 축소 | 일부 이벤트 손실 가능 | 트레이싱 비활성화 후 축소 |
| 메모리 부족 | 확장 실패 시 기존 크기 유지 | 여유 메모리 확인 후 설정 |
| CPU별 비대칭 | 허용됨 (각 CPU 독립) | 핫 CPU에만 큰 버퍼 설정 |
| 인스턴스 간 독립 | 각 인스턴스 별도 리사이즈 | 총 메모리 = 인스턴스 x CPU x 크기 |
buffer_size_kb x CPU 수 x 인스턴스 수. 예를 들어 16MB/CPU x 64 CPU x 2 인스턴스 = 2GB. 스냅샷 활성화 시 2배(4GB). 서버 시스템에서 무심코 큰 버퍼를 설정하면 상당한 메모리를 소비하므로 주의가 필요합니다.
kfifo 비교
커널에서 순환 버퍼로 널리 사용되는 kfifo와 ring buffer의 구조적 차이를 비교합니다.
사용 시점 가이드
| 상황 | 권장 | 이유 |
|---|---|---|
| 드라이버 내부 FIFO | kfifo | 단순, 경량, 고정 크기 요소에 최적 |
| 단일 생산자-단일 소비자 | kfifo | lock-free 보장, 최소 오버헤드 |
| 트레이싱/모니터링 | ring_buffer | per-CPU, NMI-safe, splice 지원 |
| 가변 길이 레코드 | ring_buffer | 이벤트 헤더로 가변 크기 인코딩 |
| 대용량 + 동적 리사이즈 | ring_buffer | 페이지 단위 증감, 대형 연속 할당 불필요 |
| 사용자 공간 고속 전송 | ring_buffer | splice/mmap zero-copy |
kfifo 내부 구현 비교
/* kfifo: 평탄 버퍼 + power-of-2 마스크 트릭 */
struct __kfifo {
unsigned int in; /* 쓰기 인덱스 (단조 증가) */
unsigned int out; /* 읽기 인덱스 (단조 증가) */
unsigned int mask; /* size - 1 (power-of-2) */
unsigned int esize; /* 요소 크기 */
void *data; /* 연속 버퍼 */
};
/* kfifo_in: 단일 생산자 lock-free 쓰기 */
static inline unsigned int
__kfifo_in(struct __kfifo *fifo,
const void *buf, unsigned int len)
{
unsigned int off = fifo->in & fifo->mask;
memcpy(fifo->data + off, buf, len);
smp_wmb(); /* 데이터 쓰기 → in 갱신 순서 보장 */
fifo->in += len;
return len;
}
/* ring_buffer_lock_reserve: per-CPU lockless + NMI-safe */
/* → 가변 길이, 타임스탬프, 중첩 지원, splice/mmap */
/* kfifo보다 복잡하지만, 트레이싱 요구사항에 필수적 */
BPF ringbuf 비교
커널에는 ring buffer 구현이 세 가지 존재합니다. 각각의 설계 목표가 다릅니다.
| 특성 | ftrace ring_buffer | perf ring buffer | BPF ringbuf |
|---|---|---|---|
| 도입 버전 | 2.6.27 | 2.6.31 | 5.8 |
| 메모리 구조 | 페이지 연결 리스트 | 연속 mmap | 연속 mmap (2x 매핑) |
| 생산자 | per-CPU (각 CPU 전용) | per-CPU | 다중 CPU 공유 가능 |
| 소비자 | 커널/사용자 (tracefs) | 사용자 (mmap) | 사용자 (mmap) |
| NMI 안전 | 예 | 예 | 아니오 |
| 가변 길이 | 예 | 예 | 예 |
| 역압(backpressure) | overwrite/discard | discard | discard (reserve 실패) |
| 주 용도 | ftrace/trace events | PMU 샘플링 | BPF 프로그램 출력 |
ftrace 통합
ring buffer는 ftrace 인프라의 핵심 저장소로, 다양한 tracer와 trace event가 이벤트를 기록하는 단일 경로를 제공합니다.
Trace Event 기록 경로
/* TRACE_EVENT(sched_switch, ...) 가 생성하는 기록 함수 (간략화) */
static void
trace_event_raw_event_sched_switch(void *__data,
bool preempt, struct task_struct *prev,
struct task_struct *next, unsigned int prev_state)
{
struct trace_event_buffer fbuffer;
struct trace_event_raw_sched_switch *entry;
int __data_size;
/* 1. ring buffer에서 공간 예약 */
entry = trace_event_buffer_reserve(&fbuffer, ...,
sizeof(*entry) + __data_size);
if (!entry)
return;
/* 2. 이벤트 페이로드 채우기 */
entry->prev_pid = prev->pid;
entry->prev_prio = prev->prio;
entry->prev_state = prev_state;
entry->next_pid = next->pid;
entry->next_prio = next->prio;
memcpy(entry->prev_comm, prev->comm, TASK_COMM_LEN);
memcpy(entry->next_comm, next->comm, TASK_COMM_LEN);
/* 3. commit */
trace_event_buffer_commit(&fbuffer);
}
trace_array와 ring buffer를 가집니다. /sys/kernel/tracing/instances/ 디렉토리에서 새 인스턴스를 생성하면 별도의 ring buffer가 할당됩니다. 이를 통해 서로 다른 용도의 트레이싱을 간섭 없이 동시에 수행할 수 있습니다.
트레이싱 인스턴스와 ring buffer
# 독립 인스턴스 생성 (별도 ring buffer 할당)
mkdir /sys/kernel/tracing/instances/my_trace
mkdir /sys/kernel/tracing/instances/net_trace
# 각 인스턴스에 별도 설정
echo 8192 > /sys/kernel/tracing/instances/my_trace/buffer_size_kb
echo sched_switch > /sys/kernel/tracing/instances/my_trace/set_event
echo 4096 > /sys/kernel/tracing/instances/net_trace/buffer_size_kb
echo net:* > /sys/kernel/tracing/instances/net_trace/set_event
# 독립적으로 읽기
cat /sys/kernel/tracing/instances/my_trace/trace_pipe &
cat /sys/kernel/tracing/instances/net_trace/trace_pipe &
# 인스턴스 삭제 (ring buffer 해제)
rmdir /sys/kernel/tracing/instances/my_trace
function tracer의 ring buffer 사용
/* function tracer 기록 함수 (간략화) */
static void
function_trace_call(unsigned long ip,
unsigned long parent_ip,
struct ftrace_ops *ops,
struct ftrace_regs *fregs)
{
struct trace_array *tr = ops->private;
struct trace_buffer *buffer;
struct ring_buffer_event *event;
struct ftrace_entry *entry;
int pc;
pc = preempt_count();
buffer = tr->array_buffer.buffer;
/* ring buffer에서 공간 예약 */
event = trace_buffer_lock_reserve(buffer,
TRACE_FN, sizeof(*entry), pc);
if (!event)
return;
/* 이벤트 데이터 채우기 */
entry = ring_buffer_event_data(event);
entry->ip = ip; /* 호출된 함수 주소 */
entry->parent_ip = parent_ip; /* 호출자 주소 */
/* commit */
ring_buffer_unlock_commit(buffer);
}
/*
* function_graph tracer는 진입/탈출 2개 이벤트 기록:
* - TRACE_GRAPH_ENT: 함수 진입 (ip, depth)
* - TRACE_GRAPH_RET: 함수 탈출 (ip, depth, calltime, rettime)
* → 함수 실행 시간 = rettime - calltime
*/
히스토그램 트리거와 ring buffer
ftrace의 히스토그램 트리거(hist trigger)는 ring buffer의 이벤트를 소비하지 않고, 쓰기 경로에서 직접 집계합니다. 이는 ring buffer의 오버헤드를 피하면서 통계를 수집하는 효율적인 방법입니다.
# 히스토그램 트리거: sched_switch의 prev_state별 카운트
echo 'hist:keys=prev_state:sort=hitcount' > \
/sys/kernel/tracing/events/sched/sched_switch/trigger
# 결과 확인 (ring buffer 소비 없이 집계)
cat /sys/kernel/tracing/events/sched/sched_switch/hist
# 지연 분포 히스토그램
echo 'hist:keys=common_pid:vals=hitcount:sort=hitcount.descending' > \
/sys/kernel/tracing/events/sched/sched_wakeup_new/trigger
perf 링 버퍼
perf_event 서브시스템은 ftrace의 ring buffer와는 완전히 다른 별도의 링 버퍼를 사용합니다. 두 구현의 설계 철학과 구조를 비교합니다.
| 비교 항목 | perf ring buffer | ftrace ring buffer |
|---|---|---|
| 메모리 구조 | 연속 mmap 영역 | buffer_page 연결 리스트 |
| 사용자 접근 | 항상 mmap (설계 기본) | tracefs read/splice, mmap (6.6+) |
| 동적 리사이즈 | 불가 (재생성 필요) | 런타임 페이지 추가/제거 |
| 스냅샷 | 미지원 | swap 기반 즉시 스냅샷 |
| aux buffer | 별도 지원 (Intel PT 등) | 미지원 |
| BPF 연동 | bpf_perf_event_output | 간접 (trace event 경유) |
bpf_perf_event_output()으로 perf 링 버퍼에 데이터를 전송하거나, 최신 커널에서는 BPF_MAP_TYPE_RINGBUF(bpf_ringbuf)를 사용합니다. bpf_ringbuf는 perf 링 버퍼도 ftrace ring buffer도 아닌 제3의 구현으로, 다중 생산자-단일 소비자 공유 링 버퍼입니다. 각각의 링 버퍼가 서로 다른 요구사항에 최적화되어 있습니다.
perf mmap 프로토콜
perf 링 버퍼의 사용자 공간 프로토콜은 ring buffer와 다릅니다. perf는 data_head/data_tail 두 인덱스로 생산자-소비자를 조율합니다.
/* perf_event mmap 프로토콜 (사용자 공간 소비) */
struct perf_event_mmap_page *header;
void *data;
/* mmap: 메타 페이지 + 데이터 영역 */
header = mmap(NULL, (1 + 16) * PAGE_SIZE,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
data = (void *)header + PAGE_SIZE;
/* 이벤트 소비 루프 */
while (1) {
__u64 head = __atomic_load_n(&header->data_head,
__ATOMIC_ACQUIRE);
__u64 tail = header->data_tail;
while (tail != head) {
struct perf_event_header *ev =
data + (tail & (data_size - 1));
process_perf_event(ev);
tail += ev->size;
}
/* 소비 완료 알림 */
__atomic_store_n(&header->data_tail, tail,
__ATOMIC_RELEASE);
}
aux_buffer (Intel PT, ARM CoreSight)
perf 링 버퍼는 하드웨어 트레이스 장치(Intel PT, ARM CoreSight)를 위한 aux_buffer를 별도로 제공합니다. 이 영역은 하드웨어가 직접 쓰기하므로 소프트웨어 오버헤드가 없습니다. ftrace ring buffer에는 이에 대응하는 기능이 없습니다.
# Intel PT 수집 (aux_buffer 사용)
perf record -e intel_pt// -a --aux-size=16M -- sleep 5
perf script --itrace=b
# ARM CoreSight 수집
perf record -e cs_etm/@tmc_etr0/ -a -- sleep 5
성능 특성
ring buffer의 성능은 트레이싱 시스템 전체의 오버헤드를 결정합니다. 주요 성능 특성을 분석합니다.
쓰기 오버헤드
| 연산 | 비용 (x86-64 기준) | 설명 |
|---|---|---|
| preempt_disable/enable | ~2 ns | preempt_count 증감 |
| trace_clock_local() | ~10-20 ns | rdtsc 명령어 |
| local_cmpxchg (성공) | ~5 ns | LOCK prefix 없는 CMPXCHG |
| 이벤트 데이터 복사 | ~5-50 ns | 크기에 따라 다름 |
| commit | ~5-10 ns | local_set + 조건 검사 |
| 총 쓰기 오버헤드 | ~30-100 ns | 이벤트 크기와 경합에 따라 |
확장성
/* per-CPU 설계의 확장성 보장 */
/*
* CPU 수별 쓰기 처리량 (이론적):
*
* CPU 수 | 처리량 (이벤트/초)
* ----------|-------------------
* 1 | ~10M
* 4 | ~40M (선형 확장)
* 16 | ~160M (선형 확장)
* 64 | ~640M (선형 확장)
* 128 | ~1.28B (선형 확장)
*
* per-CPU 설계이므로 CPU 간 경합이 없어
* 이론적으로 완벽한 선형 확장성을 보입니다.
* 실제로는 메모리 대역폭이 병목이 될 수 있습니다.
*/
캐시라인 동작
ring buffer의 성능에서 캐시라인 분리가 중요한 역할을 합니다.
| 데이터 | 접근 주체 | 캐시라인 상태 |
|---|---|---|
tail_page->write | 현재 CPU만 (쓰기) | Exclusive/Modified |
commit_page->commit | 현재 CPU 쓰기, 다른 CPU 읽기 | Modified → Shared (읽기 시) |
reader_page | 읽기 측만 | reader 측 Exclusive |
head_page | 쓰기(overwrite), 읽기(swap) | 가끔 바운싱 (swap 시에만) |
메모리 대역폭(Bandwidth)과 버퍼 크기
per-CPU 설계로 CPU 간 경합은 없지만, 대량 이벤트 기록 시 메모리 대역폭이 병목(Bottleneck)이 될 수 있습니다. 특히 function tracer처럼 모든 함수 호출을 기록하는 경우, 각 이벤트는 약 28바이트이고 함수 호출 빈도는 초당 수백만 건에 달합니다.
/*
* 메모리 대역폭 계산 예시:
*
* function tracer 이벤트 크기: ~28 바이트
* 평균 함수 호출 빈도: ~5M/초/CPU
* 쓰기 대역폭: 28 * 5M = 140 MB/초/CPU
*
* 64 코어 시스템: 140 * 64 = 8.96 GB/초
* DDR4-3200 단일 채널: ~25.6 GB/초
*
* → 메모리 컨트롤러 대역폭의 35% 소비
* → 함수 필터링(set_ftrace_filter)으로 완화 필수
*
* 버퍼가 L3 캐시에 들어가는 크기라면 메모리 트래픽 대폭 감소
* 예: 2MB/CPU 버퍼 + 8MB L3 슬라이스 → 대부분 캐시 히트
*/
트레이싱 지연 영향
| 트레이서 | 이벤트당 오버헤드 | 주요 비용 | 권장 사항 |
|---|---|---|---|
| function tracer | ~100-200 ns | mcount hook + ring buffer 쓰기 | set_ftrace_filter로 범위 제한 |
| function_graph | ~200-400 ns | 진입/탈출 모두 기록, 리턴 시간 측정 | max-graph-depth로 깊이 제한 |
| trace events | ~50-150 ns | 트레이스포인트 조건부 분기 + 쓰기 | 필터로 불필요한 이벤트 제거 |
| kprobes | ~200-500 ns | breakpoint/int3 처리 + 쓰기 | 최소한의 프로브(Probe)만 활성화 |
trace-cmd latency나 perf bench sched를 사용합니다. 또한 /sys/kernel/tracing/per_cpu/cpu0/stats에서 entries, overrun, commit overrun 카운터를 모니터링하여 병목을 식별할 수 있습니다.
NUMA 고려사항
NUMA 시스템에서 ring buffer의 페이지가 원격 노드에 할당되면 쓰기 지연이 증가합니다. ring buffer는 alloc_pages_node를 사용하여 각 CPU의 로컬 노드에서 페이지를 할당합니다.
/* NUMA-aware 페이지 할당 */
static struct buffer_page *
rb_allocate_pages(struct ring_buffer_per_cpu *cpu_buffer,
unsigned long nr_pages)
{
struct buffer_page *bpage;
struct list_head *pages = &cpu_buffer->new_pages;
unsigned long i;
for (i = 0; i < nr_pages; i++) {
bpage = kzalloc_node(ALIGN(sizeof(*bpage), cache_line_size()),
GFP_KERNEL,
cpu_to_node(cpu_buffer->cpu));
if (!bpage)
goto free_pages;
/* 데이터 페이지도 로컬 노드에서 할당 */
bpage->page = alloc_pages_node(
cpu_to_node(cpu_buffer->cpu),
GFP_KERNEL | __GFP_ZERO, 0);
if (!bpage->page)
goto free_pages;
list_add_tail(&bpage->list, pages);
}
return pages;
}
튜닝 & 디버깅
ring buffer를 효과적으로 활용하기 위한 실전 튜닝과 디버깅 방법을 정리합니다.
버퍼 크기 튜닝
# 현재 버퍼 크기 확인
cat /sys/kernel/tracing/buffer_size_kb
# 1408 (기본값, 약 1.4MB per CPU)
# 전체 CPU 버퍼 크기 설정
echo 16384 > /sys/kernel/tracing/buffer_size_kb # 16MB per CPU
# CPU별 개별 설정
echo 65536 > /sys/kernel/tracing/per_cpu/cpu0/buffer_size_kb
# 전체 통계 확인
for cpu in /sys/kernel/tracing/per_cpu/cpu*; do
echo "=== $(basename $cpu) ==="
cat "$cpu/stats"
done
# 버퍼 리셋 (모든 이벤트 삭제)
echo > /sys/kernel/tracing/trace
실전 설정 가이드
| 시나리오 | buffer_size_kb | 모드 | 읽기 방법 |
|---|---|---|---|
| 짧은 버그 재현 | 4096~16384 | overwrite | trace (스냅샷) |
| 장시간 모니터링 | 1408 (기본) | nooverwrite | trace_pipe (실시간 소비) |
| 고빈도 이벤트 수집 | 65536+ | nooverwrite | splice (trace-cmd record) |
| 지연 분석 | 8192 | overwrite | trace + 트리거 |
| 메모리 제약 시스템 | 512~1024 | overwrite | trace_pipe |
이벤트 손실 진단
# 이벤트 손실 상세 진단
cat /sys/kernel/tracing/per_cpu/cpu0/stats
# entries: 45123 ← 현재 버퍼에 있는 이벤트 수
# overrun: 12345 ← overwrite로 덮어쓴 이벤트 수
# commit overrun: 0 ← NMI 중첩 commit 충돌 (보통 0)
# bytes: 3612800 ← 총 기록 바이트
# oldest event ts: 123456789.012345
# now ts: 123456890.012345
# dropped events: 0 ← discard 모드에서 폐기된 이벤트
# read events: 30000 ← 소비된 이벤트 수
# 버퍼 사용률 확인
for cpu in /sys/kernel/tracing/per_cpu/cpu*; do
entries=$(grep "entries:" "$cpu/stats" | awk '{print $2}')
overrun=$(grep "overrun:" "$cpu/stats" | awk '{print $2}')
echo "$(basename $cpu): entries=$entries overrun=$overrun"
done
commit overrun이 지속적으로 증가하면, NMI가 매우 높은 빈도로 발생하고 있다는 의미입니다. 이 경우 perf의 NMI watchdog(perf.max_sample_rate)이나 하드웨어 PMU 이벤트를 확인하세요. 대부분의 시스템에서 commit overrun은 0이어야 정상입니다.
이벤트 필터링으로 오버헤드 감소
# 이벤트 필터: 특정 조건만 기록하여 ring buffer 부하 감소
# sched_switch에서 특정 PID만 기록
echo 'prev_pid == 1234 || next_pid == 1234' > \
/sys/kernel/tracing/events/sched/sched_switch/filter
# block 이벤트에서 특정 디바이스만
echo 'dev == 0x800010' > \
/sys/kernel/tracing/events/block/block_rq_issue/filter
# function tracer: 특정 함수만 트레이싱
echo 'do_sys_open' > /sys/kernel/tracing/set_ftrace_filter
echo 'ext4_*' >> /sys/kernel/tracing/set_ftrace_filter
# function_graph: 깊이 제한
echo 5 > /sys/kernel/tracing/max_graph_depth
# 필터 해제
echo 0 > /sys/kernel/tracing/events/sched/sched_switch/filter
per_cpu 디렉토리 구조
# per-CPU 디렉토리 구조
ls /sys/kernel/tracing/per_cpu/cpu0/
# buffer_size_kb ← 이 CPU 버퍼 크기
# stats ← entries, overrun, dropped 등 통계
# trace ← 이 CPU만의 트레이스 (비소비)
# trace_pipe ← 이 CPU만의 실시간 스트림 (소비)
# trace_pipe_raw ← 이 CPU 바이너리 스트림 (splice/mmap)
# snapshot ← 이 CPU 스냅샷
# CPU별 독립 모니터링 예시
# 터미널 1: CPU 0 모니터링
cat /sys/kernel/tracing/per_cpu/cpu0/trace_pipe
# 터미널 2: CPU 1 모니터링
cat /sys/kernel/tracing/per_cpu/cpu1/trace_pipe
# CPU별 버퍼 크기 차등 설정
# 네트워크 인터럽트 처리 CPU에 큰 버퍼
echo 32768 > /sys/kernel/tracing/per_cpu/cpu0/buffer_size_kb
echo 4096 > /sys/kernel/tracing/per_cpu/cpu1/buffer_size_kb
자주 발생하는 문제와 해결
| 증상 | 원인 | 해결 방법 |
|---|---|---|
| trace 파일이 비어 있음 | 트레이싱 비활성화 상태 | echo 1 > tracing_on |
| 이벤트 타임스탬프가 역순 | CPU 간 TSC 불일치 | echo global > trace_clock |
| trace_pipe 읽기 블로킹 | 이벤트 없음 (트레이서/이벤트 미설정) | 이벤트 활성화 확인 |
| 버퍼 크기 변경 실패 | 메모리 부족 | 기존 인스턴스 삭제 후 재시도 |
| overrun 급증 | 이벤트 생산 > 소비 | 버퍼 증가, 필터 강화, 소비 빈도 증가 |
| trace_pipe_raw mmap 실패 | 커널 6.6 미만 | splice 사용 또는 커널 업그레이드 |
trace-cmd 활용 예제
# trace-cmd으로 이벤트 수집 (splice 기반, 손실 최소화)
trace-cmd record -e sched_switch -e sched_wakeup -b 16384
# 특정 CPU만 수집
trace-cmd record -e irq -C 0,1 -b 8192
# 함수 트레이싱 + 스택 추적
trace-cmd record -p function_graph -g do_sys_open --max-graph-depth 5
# 수집 결과 분석
trace-cmd report trace.dat | head -50
# 실시간 모니터링
trace-cmd stream -e sched_switch
커널 설정
ring buffer와 관련된 커널 설정 옵션을 정리합니다.
| 설정 | 기본값 | 설명 |
|---|---|---|
CONFIG_RING_BUFFER | y (자동) | ring buffer 핵심 구현. CONFIG_TRACING 선택 시 자동 활성화 |
CONFIG_TRACING | y | ftrace 프레임워크 전체 활성화 |
CONFIG_FTRACE | y | function tracer 활성화 |
CONFIG_FUNCTION_TRACER | y | 함수 진입/탈출 트레이싱 |
CONFIG_FUNCTION_GRAPH_TRACER | y | 함수 호출 그래프 트레이싱 |
CONFIG_DYNAMIC_FTRACE | y | 동적 ftrace (NOP 패칭) |
CONFIG_TRACE_BRANCH_PROFILING | n | 분기 프로파일링 (오버헤드 있음) |
CONFIG_RING_BUFFER_BENCHMARK | n | ring buffer 성능 벤치마크 모듈 |
CONFIG_RING_BUFFER_VALIDATE_TIME_DELTAS | n | 타임스탬프 delta 검증 (디버그용, 오버헤드) |
CONFIG_TRACING_MAP | y (자동) | 트레이싱용 해시(Hash)맵 (히스토그램 트리거) |
# 현재 커널의 ring buffer 관련 설정 확인
zcat /proc/config.gz | grep -i ring_buffer
# CONFIG_RING_BUFFER=y
# CONFIG_RING_BUFFER_ALLOW_SWAP=y
# # CONFIG_RING_BUFFER_BENCHMARK is not set
# # CONFIG_RING_BUFFER_VALIDATE_TIME_DELTAS is not set
# ring buffer 벤치마크 실행 (CONFIG_RING_BUFFER_BENCHMARK=m 필요)
modprobe ring_buffer_benchmark
# dmesg에서 결과 확인
dmesg | grep ring_buffer_benchmark
벤치마크 모듈
/* kernel/trace/ring_buffer_benchmark.c (발췌) */
/* 벤치마크 파라미터 */
static int write_iteration = 50;
module_param(write_iteration, int, 0644);
MODULE_PARM_DESC(write_iteration,
"# of writes between timestamp readings");
static int producer_nice = MAX_NICE;
module_param(producer_nice, int, 0644);
static int consumer_nice = MAX_NICE;
module_param(consumer_nice, int, 0644);
/*
* 벤치마크 결과 예시 (dmesg):
*
* ring_buffer_benchmark: Running ring buffer benchmark
* ring_buffer_benchmark: Entries: 1000000
* ring_buffer_benchmark: Total: 125 ms
* ring_buffer_benchmark: Overruns: 0
* ring_buffer_benchmark: Read: 1000000
* ring_buffer_benchmark: Avg Write: 50 ns
* ring_buffer_benchmark: Avg Read: 80 ns
*/
/sys/kernel/tracing/snapshot을 통한 스냅샷 기능이 사용 가능합니다. 대부분의 배포판에서 기본 활성화되어 있지만, 메모리 제약이 심한 임베디드 시스템에서는 비활성화하여 스냅샷 버퍼의 메모리를 절약할 수 있습니다.
임베디드 시스템 최소 설정
# 최소 트레이싱 (ring buffer + trace events만)
CONFIG_RING_BUFFER=y
CONFIG_TRACING=y
CONFIG_TRACING_SUPPORT=y
CONFIG_EVENT_TRACING=y
# function tracer 비활성화 (mcount 오버헤드 제거)
# CONFIG_FUNCTION_TRACER is not set
# CONFIG_FUNCTION_GRAPH_TRACER is not set
# 스냅샷 비활성화 (메모리 절약)
# CONFIG_RING_BUFFER_ALLOW_SWAP is not set
# 디버그 비활성화 (오버헤드 제거)
# CONFIG_RING_BUFFER_VALIDATE_TIME_DELTAS is not set
# CONFIG_RING_BUFFER_BENCHMARK is not set
디버그 설정
# ring buffer 디버그 전체 활성화
CONFIG_RING_BUFFER=y
CONFIG_RING_BUFFER_VALIDATE_TIME_DELTAS=y # 타임스탬프 검증
CONFIG_RING_BUFFER_BENCHMARK=m # 벤치마크 모듈
CONFIG_RING_BUFFER_ALLOW_SWAP=y # 스냅샷
# 관련 디버그 옵션
CONFIG_TRACING=y
CONFIG_FTRACE=y
CONFIG_FUNCTION_TRACER=y
CONFIG_FUNCTION_GRAPH_TRACER=y
CONFIG_DYNAMIC_FTRACE=y
CONFIG_FTRACE_SYSCALLS=y
CONFIG_TRACER_MAX_TRACE=y # 최대 지연 추적
CONFIG_TRACER_SNAPSHOT=y # 스냅샷 인터페이스
CONFIG_TRACER_SNAPSHOT_PER_CPU_SWAP=y # CPU별 스냅샷
CONFIG_HIST_TRIGGERS=y # 히스토그램 트리거
부트 파라미터
| 파라미터 | 설명 | 예시 |
|---|---|---|
trace_buf_size= | 부팅 시 ring buffer 크기 설정 | trace_buf_size=16M |
trace_event= | 부팅 시 활성화할 이벤트 | trace_event=sched:sched_switch |
ftrace= | 부팅 시 활성화할 tracer | ftrace=function |
ftrace_filter= | function tracer 필터 | ftrace_filter=schedule |
trace_clock= | 타임스탬프 소스 | trace_clock=global |
ftrace_dump_on_oops | oops/panic 시 ring buffer 덤프(Dump) | ftrace_dump_on_oops=orig_cpu |
traceoff_on_warning | WARN 시 트레이싱 중지 | 커널 커맨드라인에 추가 |
# 부팅 초기 이벤트 캡처 (grub 커맨드라인)
linux /vmlinuz trace_buf_size=64M \
trace_event=initcall:initcall_start,initcall:initcall_finish \
ftrace_dump_on_oops=orig_cpu
ftrace_dump_on_oops=orig_cpu로 설정하면 panic 발생 CPU의 버퍼만 출력하여 출력량을 줄일 수 있습니다.
관련 문서
참고 자료
- kernel/trace/ring_buffer.c — ftrace 링 버퍼 핵심 구현체입니다. per-CPU 페이지 관리, lockless 쓰기, NMI-safe 예약 등 모든 핵심 로직이 포함되어 있습니다.
- include/linux/ring_buffer.h — 링 버퍼 공개 API 헤더입니다.
ring_buffer_event,ring_buffer_alloc(),ring_buffer_write()등의 인터페이스를 정의합니다. - kernel/trace/trace.c — ftrace 프레임워크의 메인 구현체로, 링 버퍼를 소비하여 tracefs 인터페이스를 제공합니다.
- kernel/trace/ring_buffer_benchmark.c — 링 버퍼 벤치마크 커널 모듈 소스입니다. 쓰기/읽기 성능 측정에 사용합니다.
- Documentation/trace/ring-buffer-design.rst — 커널 공식 링 버퍼 설계 문서입니다. lockless 알고리즘, 중첩 쓰기, 타임스탬프 설계를 상세히 설명합니다.
- Documentation/trace/ftrace.rst — ftrace 공식 사용자 가이드입니다. tracefs 인터페이스를 통한 링 버퍼 제어 방법을 다룹니다.
- Documentation/trace/events.rst — trace event 시스템 문서입니다. 링 버퍼에 이벤트를 기록하는 메커니즘을 설명합니다.
- LWN: A look at ftrace (2009) — ftrace의 초기 설계와 링 버퍼 아키텍처를 소개하는 LWN 기사입니다.
- LWN: Debugging the kernel using ftrace - part 1 (2009) — ftrace를 활용한 커널 디버깅 기법을 다루며, 링 버퍼의 실전 활용 사례를 제공합니다.
- LWN: Using the TRACE_EVENT() macro (2010) — TRACE_EVENT 매크로를 통해 링 버퍼에 구조화된 이벤트를 기록하는 방법을 설명합니다.
- LWN: User-space ring buffer for tracing (2023) — 사용자 공간 링 버퍼 제안을 다루는 기사로, 커널 링 버퍼와의 비교 관점을 제공합니다.
- Steven Rostedt, "Internals of the RT Patch" (2009) — 링 버퍼 설계자인 Steven Rostedt의 논문으로, lockless 링 버퍼의 설계 동기와 구현 원리를 설명합니다.
- Steven Rostedt, "ftrace for Embedded" (Linux Plumbers, 2009) — 임베디드 환경에서의 ftrace 활용과 링 버퍼 최적화 전략을 다룬 발표 자료입니다.
- Steven Rostedt, "Understanding the Linux Kernel via Ftrace" (Kernel Recipes, 2017) — ftrace를 활용한 커널 내부 분석 기법을 다루는 발표로, 링 버퍼의 동작 원리를 시각적으로 설명합니다.
- tools/tracing/ — 커널 소스 트리에 포함된 트레이싱 보조 도구 모음입니다.