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)까지 포괄합니다.

전제 조건: kfifo, ftrace 문서를 먼저 읽으세요. ring buffer는 kfifo와 유사한 생산자-소비자 패턴이지만, per-CPU 페이지 연결 리스트(Linked List) 기반이므로 평탄 버퍼와의 차이를 이해해야 합니다.
일상 비유: ring buffer는 CCTV 순환 녹화 시스템과 같습니다. 여러 대의 카메라(CPU)가 각자 독립된 녹화 디스크(per-CPU 버퍼)에 영상을 기록합니다. 디스크가 가득 차면 가장 오래된 영상부터 덮어쓰거나(overwrite 모드), 녹화를 멈추고 경고를 표시합니다(discard 모드). 관제실(사용자 공간(User Space))에서는 실시간(Real-time) 모니터링(trace_pipe)이나 특정 시점의 스냅샷(snapshot)을 확인할 수 있습니다.

핵심 요약

  • 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은 비파괴적으로 읽습니다.

단계별 이해

  1. 순환 버퍼 기초 이해
    생산자-소비자 패턴, head/tail 포인터, 오버플로 처리의 기본 개념을 파악합니다.
  2. per-CPU 구조 파악
    왜 CPU별 독립 버퍼가 필요한지, lockless 설계의 이점을 이해합니다.
  3. 페이지 링 구조 추적
    buffer_page 연결 리스트, tail_page/commit_page/reader_page의 역할을 따라갑니다.
  4. 쓰기/읽기 경로 분석
    이벤트 예약(reserve) → 쓰기 → commit, 그리고 consume/peek/iterator 읽기 경로를 추적합니다.
  5. 실전 활용과 튜닝
    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_cmpxchglock 경합(Contention) 없음, 캐시(Cache) 바운싱 최소
NMI 컨텍스트 안전cmpxchg 기반 예약, nesting 카운터NMI → IRQ → 일반 3단 중첩 처리
동적 크기 조정페이지 연결 리스트 (flat buffer 아님)런타임 페이지 추가/제거
zero-copy 읽기splice 인터페이스, reader_page swap커널→사용자 메모리 복사 최소화
데이터 무결성(Integrity)commit 포인터 분리, 타임스탬프 검증부분 쓰기 이벤트 노출 방지
사용자 공간 직접 접근mmap 인터페이스, 메타 페이지시스템 콜(System Call) 없는 이벤트 소비
ftrace Ring Buffer 전체 아키텍처 User Space trace_pipe (read) trace (snapshot) splice (zero-copy) mmap (direct) perf_event_open tracefs / Kernel API ring_buffer_consume ring_buffer_peek ring_buffer_read_page ring_buffer_map struct trace_buffer (ring_buffer) CPU 0 버퍼 CPU 1 버퍼 CPU 2 버퍼 ... CPU N-1 버퍼 buffer_page 연결 리스트 (per-CPU) tail_page (쓰기 위치) commit_page (확정 위치) page page reader_page (읽기 전용)
ftrace ring buffer 전체 아키텍처: 사용자 공간 인터페이스, 커널 API, per-CPU 버퍼, buffer_page 연결 리스트

ring buffer의 핵심 설계 원칙은 "쓰기 경로의 최소 오버헤드"입니다. 트레이싱은 관찰 대상 시스템에 최소한의 영향을 주어야 하므로, 이벤트 기록 경로에서 lock 획득, 메모리 할당, 시스템 콜이 발생하면 안 됩니다. 이를 위해 다음 기법들이 사용됩니다.

역사: ring buffer는 2008년 Linux 2.6.27에서 ftrace와 함께 도입되었습니다. 초기에는 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.28reader_page swap 도입읽기-쓰기 간섭 제거, splice 지원 기반
2.6.29splice 인터페이스 추가zero-copy 사용자 공간 전송
2.6.30snapshot 기능 추가swap 기반 즉시 버퍼 캡처
3.3per-CPU 개별 크기 설정CPU별 버퍼 크기 차등 설정 가능
3.10타임스탬프 개선절대 타임스탬프 마커, delta 검증
4.4ring buffer 벤치마크 모듈CONFIG_RING_BUFFER_BENCHMARK
5.7struct trace_buffer 리네이밍네임스페이스 충돌 방지
5.15sub-buffer 크기 설정기본 PAGE_SIZE 외 크기 지원
6.6mmap 인터페이스 추가시스템 콜 없는 이벤트 소비
6.8mmap 안정화, 메타 페이지 개선사용자 공간 프로토콜 확정

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 vs cmpxchg: 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의 역할: 쓰기 경로는 lockless이지만, 읽기 경로에서는 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읽기 시 페이지 하나를 통째로 분리하여 쓰기 간섭 없이 처리
buffer_page 순환 연결 리스트와 reader_page Writer Ring (순환 연결 리스트) head_page 가장 오래된 데이터 page data (4KB) buffer_page committed 데이터 page data (4KB) commit_page 마지막 확정 위치 page data (4KB) tail_page 현재 쓰기 위치 page data (4KB) 순환 reader_page (분리된 페이지) reader_page Writer ring에서 분리 -- Reader 전용 page data (4KB) -- 읽기 중 reader_page swap 프로토콜 1. reader_page 소비 완료 (빈 페이지가 됨) 2. head_page를 reader 에게 분리 (cmpxchg) 3. 빈 reader_page를 ring에 삽입 4. head_page를 다음 페이지로 전진
buffer_page 순환 연결 리스트와 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;
}
cmpxchg 필요 이유: reader_page swap은 쓰기 경로와 동시에 발생할 수 있습니다. writer가 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

# 주의: 변경 시 기존 버퍼 내용이 삭제됨
서브버퍼 크기 트레이드오프: 큰 서브버퍼는 페이지 전환 빈도를 줄여 오버헤드가 감소하지만, splice 전송 시 단위 크기가 커지고 메모리 낭비(패딩)가 증가할 수 있습니다. 대부분의 트레이스 이벤트는 100바이트 이내이므로, 기본 PAGE_SIZE가 적합합니다.

ring_buffer 구조체(Struct)

struct trace_buffer(이전 명칭 struct ring_buffer)는 전체 ring buffer 인스턴스를 대표하는 최상위 구조체입니다.

Ring Buffer 구조체 관계도 struct trace_buffer flags: unsigned cpus: int buffers: **ring_buffer_per_cpu clock: u64 (*)(void) mutex: struct mutex ring_buffer_per_cpu [0] cpu: 0 tail_page: *buffer_page commit_page: *buffer_page head_page: *buffer_page reader_page: *buffer_page entries/overrun: local_t write_stamp: u64 ring_buffer_per_cpu [1] cpu: 1 tail_page: *buffer_page ... ring_buffer_per_cpu [N] cpu: N tail_page: *buffer_page ... buffers[0] buffers[1] buffers[N]
trace_buffer와 ring_buffer_per_cpu 배열의 관계: 각 CPU가 독립된 버퍼를 소유

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이 기본인 이유: 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[];     /* 이벤트 데이터 (가변 길이) */
};

주요 필드의 역할을 정리하면 다음과 같습니다.

필드타입역할
listlist_head순환 이중 연결 리스트의 링크. prev->next에 flag 비트를 인코딩하여 head_page를 표시
writelocal_t다음 쓰기 위치(바이트 오프셋). local_cmpxchg로 원자적 갱신
readunsignedreader_page에서만 사용. 현재 읽기 위치
entrieslocal_t이 페이지에 기록된 이벤트 수. 통계 및 빈 페이지 판별용
real_endunsigned long마지막 이벤트 뒤의 실제 끝 위치. 페이지 끝 패딩과 구분
page->time_stampu64이 페이지 첫 이벤트의 절대 타임스탬프. 이후 이벤트는 delta로 기록
page->commitlocal_t확정된 데이터의 끝 오프셋. write와 다를 수 있음 (예약 후 commit 전)
write vs commit 분리: 이벤트 쓰기는 2단계입니다. 먼저 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바이트로 압축됩니다.

ring_buffer_event 이벤트 레이아웃 이벤트 헤더 (32비트) 31 27 4..0 type_len [5비트] time_delta [27비트] array[0] (확장용) type_len 인코딩 규칙 type_len = 1..28: DATA 이벤트 페이로드 크기 = type_len x 4 바이트 (4B~112B) type_len = 0: PADDING 페이지 끝 남은 공간 채움, array[0]에 총 크기 type_len = 29: TIME_EXTEND 27비트 초과 시간 간격, array[0]에 상위 32비트 type_len = 30: TIME_STAMP 절대 타임스탬프 (59비트), 동기화 마커 type_len = 0 (이전)과 구분: 대형 DATA (112B 초과) array[0]에 실제 페이로드 크기 저장, 이후 가변 길이 데이터 메모리 레이아웃 예시 header 4B payload (type_len x 4) header 4B payload TIME_EXT header 4B payload
ring_buffer_event 헤더 비트 필드와 type_len 인코딩 규칙
/* 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;
}
4바이트 정렬: 모든 이벤트는 4바이트 정렬됩니다. type_len이 1~28인 경우 페이로드 크기는 type_len * 4바이트(4~112바이트)입니다. 112바이트를 초과하는 대형 이벤트는 type_len = 0으로 표시하고, array[0]에 실제 크기를 저장합니다. 최소 이벤트 크기는 헤더(4B) + 페이로드(4B) = 8바이트입니다.

쓰기 경로

ring buffer의 쓰기 경로는 성능에 가장 민감한 부분입니다. 이벤트 하나를 기록하는 전체 흐름을 추적합니다.

Ring Buffer 쓰기 경로 ring_buffer_lock_reserve() preempt_disable + 타임스탬프 획득 rb_reserve_next_event() tail_page에서 공간 예약 (local_cmpxchg) 현재 페이지에 여유 있음 write += event_size (fast path) 페이지 공간 부족 rb_move_tail() 호출 rb_move_tail() 현재 페이지에 PADDING 삽입, tail_page 전진 Overwrite 모드 head_page 전진 (가장 오래된 데이터 손실) Discard 모드 이벤트 폐기, dropped++ 증가 memcpy(event->data, payload, size) -- 데이터 기록 ring_buffer_unlock_commit() -- commit + preempt_enable
ring buffer 쓰기 경로: reserve → write → commit 3단계

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의 핵심: 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가 발생할 수 있습니다.

NMI-safe 중첩 쓰기 시나리오 시간 Normal: reserve(A) -- write=100 IRQ 발생! IRQ: reserve(B) -- write=132 NMI 발생! NMI: reserve(C) -- write=160 NMI: commit(C) -- committing=3, commits=3 C 완료, commit 불가 (중첩 때문) NMI 복귀 IRQ: commit(B) -- committing=2, commits=3 B 완료, commit 불가 IRQ 복귀 Normal: commit(A) -- committing=1 Normal이 A,B,C 모두 commit 수행 (committing==commits 확인) 페이지 내 레이아웃: [A: 100-131][B: 132-159][C: 160-187] -- commit=188
NMI 중첩 쓰기: 가장 바깥쪽 컨텍스트(Normal)가 모든 이벤트의 최종 commit을 수행

중첩 안전성의 핵심은 다음 두 가지 메커니즘입니다.

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)
NMI에서의 제약: NMI 컨텍스트에서는 spin_lock이나 mutex를 사용할 수 없습니다. ring buffer가 local_cmpxchg만으로 동기화하는 이유가 바로 이것입니다. 만약 ring buffer가 lock 기반이었다면, NMI에서 데드락이 발생할 수 있습니다.

중첩 깊이와 컨텍스트 분류

ring buffer는 최대 4단계 중첩을 지원합니다. 각 컨텍스트 레벨은 preempt_count로 식별됩니다.

레벨컨텍스트preempt_count 영역ring buffer 동작
0Normal (프로세스(Process))PREEMPT_MASK가장 바깥쪽, 최종 commit 담당
1Soft IRQSOFTIRQ_OFFSETNormal 예약을 중단시킬 수 있음
2Hard IRQHARDIRQ_OFFSETSoft IRQ 예약을 중단시킬 수 있음
3NMINMI 감지최상위 우선순위(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 비교로 중첩 감지
 * - 감지되면 절대 타임스탬프로 재동기화
 */
rb_time_t 자료형: 커널 5.6 이전에는 write_stampbefore_stampu64를 직접 사용했지만, 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_peekiterator 위치의 이벤트 반환비소비순차 탐색
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처럼 실시간 스트리밍에 적합합니다.

trace vs 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);
}
tracefs에서 lost_events 표시: 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);
iterator와 쓰기 비활성화: 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)커널→사용자 memcpyread당 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 인터페이스를 지원합니다. 사용자 공간에서 시스템 콜 없이 직접 이벤트를 소비할 수 있어, 고빈도 트레이싱의 오버헤드를 더욱 줄입니다.

ring buffer mmap 레이아웃 User Space (mmap 영역) Meta Page Subbuf 0 Subbuf 1 Reader Subbuf Subbuf 3 ... Subbuf N struct ring_buffer_meta magic: RING_BUFFER_META_MAGIC struct_sizes: 이벤트/페이지 크기 nr_subbufs: 총 서브버퍼 수 subbuf_size: 서브버퍼 크기 (PAGE_SIZE) reader.id: 현재 reader 서브버퍼 인덱스 reader.read: reader 내 읽기 오프셋 flags: overrun, overwrite 상태 entries: 총 이벤트 수 overrun: 덮어쓴 이벤트 수 mmap 프로토콜 1. meta.reader.id로 현재 reader 서브버퍼 확인 2. 해당 서브버퍼에서 이벤트 순회 (메모리 직접 접근) 3. ioctl(TRACE_MMAP_IOCTL_GET_READER) 다음 페이지 요청 4. meta.reader.id 업데이트 확인 후 반복
ring buffer mmap 레이아웃: Meta Page + 서브버퍼 배열
/* 사용자 공간에서 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);
}
mmap vs splice 비교: mmap은 시스템 콜 없이 메모리에서 직접 이벤트를 읽을 수 있어 가장 낮은 오버헤드를 제공합니다. 그러나 커널 버퍼를 사용자 공간에 직접 노출하므로 보안과 동기화에 더 주의가 필요합니다. splice는 zero-copy이면서도 커널이 동기화를 관리하므로 더 안전합니다. 일반적인 사용에서는 trace_pipe(splice 기반)가 충분하며, 극한 성능이 필요한 경우에만 mmap을 고려합니다.

타임스탬프 메커니즘

ring buffer의 타임스탬프 시스템은 공간 효율과 정밀도를 모두 달성하기 위한 정교한 설계를 가지고 있습니다.

타임스탬프 인코딩 방식 페이지 기준 타임스탬프 (buffer_data_page.time_stamp) 64비트 절대 타임스탬프 -- 이 페이지 첫 이벤트의 시각 (예: 1000000 ns) 이벤트 1: time_delta = 500 (27비트 이내) 절대 시각 = 1000000 + 500 = 1000500 ns 이벤트 2: time_delta = 200 절대 시각 = 1000500 + 200 = 1000700 ns 긴 시간 간격 (134,217,727 ns = ~134ms 초과) TIME_EXTEND 이벤트 삽입: time_delta[27] + array[0][32] = 59비트 delta 표현 TIME_EXTEND (type_len=30) header: time_delta = 하위 27비트 array[0]: 상위 32비트 이벤트 3: time_delta = 0 TIME_EXTEND가 시간을 이미 전진시켰으므로 이벤트 자체의 delta는 0 27비트 delta: 최대 ~134ms 간격 | TIME_EXTEND: 최대 ~18년 간격 | TIME_STAMP: 절대 재동기화
타임스탬프 delta 인코딩: 대부분의 이벤트는 27비트 delta로 충분하며, 긴 간격은 TIME_EXTEND로 확장
/* 타임스탬프 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)은 절대 타임스탬프 재동기화에 사용됩니다. 주로 다음 상황에서 삽입됩니다.

왜 delta 인코딩인가? 대부분의 트레이스 이벤트는 수 마이크로초 간격으로 발생합니다. 27비트 delta는 최대 약 134ms(134,217,727 ns)까지 표현하므로, 99.9% 이상의 이벤트에서 추가 TIME_EXTEND 없이 4바이트 헤더만으로 타임스탬프를 인코딩할 수 있습니다. 매 이벤트마다 64비트 절대 타임스탬프를 저장하는 것 대비 이벤트당 4바이트를 절약합니다.

덮어쓰기 vs 폐기 모드

ring buffer가 가득 찼을 때 새 이벤트를 어떻게 처리할지 두 가지 정책이 있습니다.

모드동작데이터 손실사용 시나리오
Overwrite
RB_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가 적합합니다 (버그 발생 직전의 최근 이벤트가 중요). 이벤트 수집이 목적이면 nooverwrite가 적합합니다 (모든 이벤트를 빠짐없이 기록하되, 버퍼가 차면 알림). trace-cmd는 기본적으로 nooverwrite + splice로 이벤트를 지속 소비하여 손실을 최소화합니다.

덮어쓰기 내부 메커니즘

overwrite 모드에서 tail_pagehead_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);
}
irq_work가 필요한 이유: ring buffer 쓰기 경로는 NMI나 하드 IRQ 컨텍스트에서 실행될 수 있는데, 이 컨텍스트에서는 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
스냅샷의 비용: swap은 포인터 교환이므로 O(CPU 수) 시간에 완료됩니다. 데이터 복사가 없으므로 대용량 버퍼에서도 즉각적입니다. 단, 스냅샷 버퍼가 미리 할당되어 있어야 하므로 메모리 사용량이 2배가 됩니다.

스냅샷 트리거

수동 스냅샷 외에, 특정 조건이 만족되면 자동으로 스냅샷을 찍는 트리거를 설정할 수 있습니다.

# 조건부 스냅샷: 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 크기
메모리 계산: 전체 ring buffer 메모리 사용량 = buffer_size_kb x CPU 수 x 인스턴스 수. 예를 들어 16MB/CPU x 64 CPU x 2 인스턴스 = 2GB. 스냅샷 활성화 시 2배(4GB). 서버 시스템에서 무심코 큰 버퍼를 설정하면 상당한 메모리를 소비하므로 주의가 필요합니다.

kfifo 비교

커널에서 순환 버퍼로 널리 사용되는 kfifo와 ring buffer의 구조적 차이를 비교합니다.

kfifo vs ring_buffer 구조 비교 kfifo (평탄 버퍼) 연속 메모리 블록 (2^N 크기) kmalloc/vmalloc으로 한 번에 할당 in (write) out (read) mask = size-1 in & mask = 실제 오프셋 (power-of-2 트릭) ring_buffer (페이지 리스트) page 0 page 1 page 2 page N tail_page commit_page head_page 페이지별 독립 할당, 순환 연결 리스트 상세 비교 특성 kfifo ring_buffer 메모리 구조 연속 블록 (2^N) 페이지 연결 리스트 동기화 단일 생산자/소비자 lockless per-CPU lockless + NMI-safe 리사이즈 재할당 필요 페이지 단위 추가/제거 zero-copy copy_to_user 필요 splice/mmap 지원 용도 범용 FIFO (드라이버 등) 고성능 트레이싱 전용
kfifo 평탄 버퍼 vs ring_buffer 페이지 리스트: 설계 목표가 다르면 구조도 다르다

사용 시점 가이드

상황권장이유
드라이버 내부 FIFOkfifo단순, 경량, 고정 크기 요소에 최적
단일 생산자-단일 소비자kfifolock-free 보장, 최소 오버헤드
트레이싱/모니터링ring_bufferper-CPU, NMI-safe, splice 지원
가변 길이 레코드ring_buffer이벤트 헤더로 가변 크기 인코딩
대용량 + 동적 리사이즈ring_buffer페이지 단위 증감, 대형 연속 할당 불필요
사용자 공간 고속 전송ring_buffersplice/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_bufferperf ring bufferBPF ringbuf
도입 버전2.6.272.6.315.8
메모리 구조페이지 연결 리스트연속 mmap연속 mmap (2x 매핑)
생산자per-CPU (각 CPU 전용)per-CPU다중 CPU 공유 가능
소비자커널/사용자 (tracefs)사용자 (mmap)사용자 (mmap)
NMI 안전아니오
가변 길이
역압(backpressure)overwrite/discarddiscarddiscard (reserve 실패)
주 용도ftrace/trace eventsPMU 샘플링BPF 프로그램 출력

ftrace 통합

ring buffer는 ftrace 인프라의 핵심 저장소로, 다양한 tracer와 trace event가 이벤트를 기록하는 단일 경로를 제공합니다.

ftrace 스택과 ring buffer 통합 Tracers function function_graph irqsoff wakeup blk mmiotrace hwlat Trace Events (TRACE_EVENT 매크로) sched:sched_switch, irq:irq_handler_entry, block:block_rq_issue, net:netif_rx, syscalls:*, ... struct trace_array array_buffer.buffer = struct trace_buffer * trace_event_buffer_reserve() trace_event_buffer_commit() ring_buffer_lock_reserve() ring_buffer_unlock_commit() per-CPU ring_buffer_per_cpu -- lockless 쓰기
ftrace 스택: Tracers → Trace Events → trace_array → ring_buffer API → per-CPU 버퍼

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와 인스턴스: ftrace는 여러 독립적인 트레이싱 인스턴스를 지원합니다. 각 인스턴스는 자체 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 링 버퍼 vs ftrace ring buffer perf_event 링 버퍼 perf_event_mmap_page (메타 페이지) data_head, data_tail, time_shift, ... 연속 메모리 링 (2^N 페이지) 구조: 단일 연속 mmap 영역 동기화: data_head/tail 원자적 갱신 읽기: 사용자 공간에서 직접 (mmap) 오버플로: PERF_FLAG_FD_OUTPUT NMI: 지원 (perf_output_begin) aux_buffer: 별도 (PT, BTS 등) 용도: PMU 샘플링, SW 이벤트 BPF: bpf_perf_event_output() 크기 단위: 페이지 (mmap 크기로 결정) ftrace ring buffer ring_buffer_per_cpu tail_page, commit_page, head_page, ... buffer_page 순환 연결 리스트 구조: 페이지 연결 리스트 동기화: local_cmpxchg + commit 분리 읽기: tracefs, splice, mmap (6.6+) 오버플로: overwrite/discard 선택 NMI: 지원 (nesting 카운터) 스냅샷: swap 기반 즉시 캡처 용도: ftrace, trace events BPF: (trace events 경유) 크기 단위: KB (buffer_size_kb)
perf 링 버퍼(연속 mmap)와 ftrace ring buffer(페이지 리스트): 설계 목적에 따른 구조적 차이
비교 항목perf ring bufferftrace 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와 ring buffer: BPF 프로그램에서는 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 nspreempt_count 증감
trace_clock_local()~10-20 nsrdtsc 명령어
local_cmpxchg (성공)~5 nsLOCK prefix 없는 CMPXCHG
이벤트 데이터 복사~5-50 ns크기에 따라 다름
commit~5-10 nslocal_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 nsmcount hook + ring buffer 쓰기set_ftrace_filter로 범위 제한
function_graph~200-400 ns진입/탈출 모두 기록, 리턴 시간 측정max-graph-depth로 깊이 제한
trace events~50-150 ns트레이스포인트 조건부 분기 + 쓰기필터로 불필요한 이벤트 제거
kprobes~200-500 nsbreakpoint/int3 처리 + 쓰기최소한의 프로브(Probe)만 활성화
측정 방법: ring buffer 오버헤드를 직접 측정하려면 trace-cmd latencyperf 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를 효과적으로 활용하기 위한 실전 튜닝과 디버깅 방법을 정리합니다.

Ring Buffer 진단 흐름 이벤트 손실 감지 cat per_cpu/cpu*/stats 확인 overrun > 0 덮어쓰기로 오래된 이벤트 손실 dropped_events > 0 폐기 모드에서 새 이벤트 손실 해결 방법: 1. buffer_size_kb 증가 2. trace_pipe로 실시간 소비 3. 이벤트 필터로 기록량 감소 해결 방법: 1. buffer_size_kb 증가 2. overwrite 모드로 전환 3. trace-cmd로 지속 수집 commit_overrun > 0 (드문 경우) NMI 중첩으로 commit 경합 발생 일반적으로 무시 가능, 지속 증가시 NMI 빈도 확인
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~16384overwritetrace (스냅샷)
장시간 모니터링1408 (기본)nooverwritetrace_pipe (실시간 소비)
고빈도 이벤트 수집65536+nooverwritesplice (trace-cmd record)
지연 분석8192overwritetrace + 트리거
메모리 제약 시스템512~1024overwritetrace_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 주의: 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_BUFFERy (자동)ring buffer 핵심 구현. CONFIG_TRACING 선택 시 자동 활성화
CONFIG_TRACINGyftrace 프레임워크 전체 활성화
CONFIG_FTRACEyfunction tracer 활성화
CONFIG_FUNCTION_TRACERy함수 진입/탈출 트레이싱
CONFIG_FUNCTION_GRAPH_TRACERy함수 호출 그래프 트레이싱
CONFIG_DYNAMIC_FTRACEy동적 ftrace (NOP 패칭)
CONFIG_TRACE_BRANCH_PROFILINGn분기 프로파일링 (오버헤드 있음)
CONFIG_RING_BUFFER_BENCHMARKnring buffer 성능 벤치마크 모듈
CONFIG_RING_BUFFER_VALIDATE_TIME_DELTASn타임스탬프 delta 검증 (디버그용, 오버헤드)
CONFIG_TRACING_MAPy (자동)트레이싱용 해시(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
 */
CONFIG_RING_BUFFER_ALLOW_SWAP: 이 옵션이 활성화되면 /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=부팅 시 활성화할 tracerftrace=function
ftrace_filter=function tracer 필터ftrace_filter=schedule
trace_clock=타임스탬프 소스trace_clock=global
ftrace_dump_on_oopsoops/panic 시 ring buffer 덤프(Dump)ftrace_dump_on_oops=orig_cpu
traceoff_on_warningWARN 시 트레이싱 중지커널 커맨드라인에 추가
# 부팅 초기 이벤트 캡처 (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: 이 옵션은 커널 panic이나 oops 발생 시 ring buffer의 내용을 콘솔(시리얼 콘솔 등)로 출력합니다. 디버깅에 매우 유용하지만, 대용량 버퍼에서는 출력량이 방대하여 시리얼 콘솔의 대역폭을 초과할 수 있습니다. 이 경우 ftrace_dump_on_oops=orig_cpu로 설정하면 panic 발생 CPU의 버퍼만 출력하여 출력량을 줄일 수 있습니다.

참고 자료

학습 경로 제안:
  1. 먼저 kfifo로 기본 순환 버퍼 개념을 익히세요.
  2. ftrace 문서에서 실전 사용법을 배우세요.
  3. 이 문서로 ring buffer의 내부 구현을 이해하세요.
  4. perfBPF/XDP로 다른 트레이싱 기법과 비교하세요.
  5. kernel/trace/ring_buffer.c 소스 코드를 직접 읽어보세요 (약 6,000줄).