ftrace / Tracepoints

관측 오버헤드(Overhead)를 통제하면서 커널 실행 경로를 추적하는 방법을 단계적으로 다룹니다. ftrace function/function_graph 트레이서, tracepoint 이벤트 ABI, kprobe/kretprobe·uprobe 동적 삽입, perf와 trace-cmd의 데이터 수집·상관 분석, BTF 활용 타입 해석, 버퍼(Buffer) 손실·샘플링 왜곡을 줄이는 설정, 운영 환경에서 안전하게 트레이싱하는 실무 규칙까지 상세히 설명합니다.

전제 조건: 커널 디버깅(Debugging)프로세스(Process) 스케줄러(Scheduler) 문서를 먼저 읽으세요. ftrace는 함수 호출과 지연(Latency) 구간을 실시간(Real-time) 관측하므로, tracer별 오버헤드와 필터 범위를 먼저 설계해야 데이터가 유의미해집니다.
일상 비유: 이 주제는 도시 교통 CCTV 타임라인 분석과 비슷합니다. 모든 카메라를 한 번에 켜면 노이즈가 커지듯이, 문제 구간에 맞춰 추적 범위를 좁혀야 병목(Bottleneck) 원인이 선명해집니다.

핵심 요약

  • 재현 우선 — 증상보다 재현 조건 고정이 먼저입니다.
  • 도구 선택 — 로그, 트레이스, 코어덤프 용도를 분리합니다.
  • 증거 보존 — 원인 추적 전 관측 데이터를 먼저 확보합니다.
  • 오버헤드 관리 — 추적 범위를 최소화해 왜곡을 줄입니다.
  • 사후 검증 — 수정 후 동일 조건에서 재발 여부를 확인합니다.

단계별 이해

  1. 재현 시나리오 정의
    입력 조건과 타이밍을 고정합니다.
  2. 관측 포인트 배치
    핵심 함수/이벤트만 선별 추적합니다.
  3. 원인 축소
    가설을 하나씩 배제하며 범위를 줄입니다.
  4. 수정 검증
    회귀 테스트와 운영 지표를 함께 확인합니다.
관련 표준: (커널 내부 트레이싱 도구, 외부 표준 없음) Linux 커널 전용 관측성 프레임워크입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

커널 트레이싱 개요

커널 트레이싱은 실행 중인 커널의 내부 동작을 관찰하는 기술입니다. 커널 개발자와 시스템 엔지니어는 트레이싱을 통해 성능 병목, 레이턴시 원인, 함수 호출 경로, 스케줄링 이벤트 등을 분석할 수 있습니다. 유저 공간의 strace가 시스템 콜(System Call)만 추적하는 것과 달리, 커널 트레이싱은 커널 내부의 모든 함수와 이벤트를 대상으로 합니다.

관측성(Observability) 계층

Linux 커널의 관측성 도구는 크게 세 가지 계층으로 구분됩니다. 각 계층은 서로 다른 수준의 상세도와 오버헤드를 제공합니다.

계층도구계측 방식오버헤드용도
카운터/통계perf stat, /procPMU, 소프트웨어 카운터매우 낮음성능 요약, CPU 사이클, 캐시(Cache) 미스
샘플링perf record, OProfile주기적 인터럽트(Interrupt)낮음~중간CPU 프로파일링(Profiling), flame graph
이벤트 트레이싱ftrace, tracepoints, kprobes정적/동적 계측중간~높음상세 실행 흐름, 레이턴시 분석

정적 계측 vs 동적 계측

커널 트레이싱의 두 가지 근본적인 접근법은 정적 계측(static instrumentation)과 동적 계측(dynamic instrumentation)입니다.

특성정적 계측동적 계측
대표 기술tracepoints, TRACE_EVENTkprobes, uprobes, ftrace function tracer
계측점 정의소스 코드에 미리 삽입런타임에 임의 주소에 삽입
안정성커널 ABI의 일부, 비교적 안정내부 심볼에 의존, 커널 버전 간 변동 가능
오버헤드(비활성)거의 0 (static key)0 (설치 전 존재하지 않음)
유연성미리 정의된 지점만 가능거의 모든 커널 함수에 적용 가능
데이터 접근구조화된 필드 제공레지스터(Register)/스택에서 직접 읽어야 함
ℹ️

커널 CONFIG 옵션: 트레이싱 기능을 사용하려면 커널 빌드 시 관련 옵션을 활성화해야 합니다. 대부분의 배포판 커널은 기본적으로 ftrace와 tracepoints를 활성화합니다. CONFIG_FTRACE=y, CONFIG_FUNCTION_TRACER=y, CONFIG_FUNCTION_GRAPH_TRACER=y, CONFIG_KPROBES=y, CONFIG_UPROBES=y, CONFIG_TRACEPOINTS=y를 확인하십시오.

트레이싱 도구 선택 가이드

다양한 트레이싱 도구 중 적절한 도구를 선택하는 것이 효율적인 분석의 핵심입니다. 아래 가이드는 상황별 최적 도구를 안내합니다.

분석 목적 추천 도구 주요 명령어 오버헤드 학습 곡선
CPU 핫스팟 perf record/report perf record -F 99 -a -g -- sleep 30 낮음 쉬움
함수 호출 경로 ftrace function_graph trace-cmd record -p function_graph 중간 보통
레이턴시 분석 ftrace + tracepoints trace-cmd record -e sched 중간 보통
시스템 콜 추적 perf trace perf trace -p PID 낮음 쉬움
특정 함수 진입/종료 kprobe perf probe -a function_name 낮음~중간 보통
동적 스크립팅 bpftrace bpftrace -e 'kprobe:...' 매우 낮음 어려움
프로덕션 모니터링 perf stat, eBPF perf stat -a -I 1000 매우 낮음 쉬움
💡

필터링 최적화 원칙:

  • 좁은 범위: PID/CPU/함수명 필터로 이벤트 수 최소화
  • 짧은 시간: 5~30초 내로 트레이스 수집 (버퍼 오버플로(Buffer Overflow) 방지)
  • 버퍼 크기 조정: buffer_size_kb 증가 (기본 7MB → 100MB+)
  • 단계적 축소: 전체 → 서브시스템 → 특정 함수 순으로 범위 축소
  • 오버헤드 측정: 트레이싱 on/off CPU 사용률 비교

트레이스 출력 분석 템플릿

ftrace 출력은 표준 포맷을 따릅니다. 각 필드를 정확히 해석하는 것이 중요합니다.

# tracer: function_graph
#
# CPU  DURATION                  FUNCTION CALLS
# |     |   |                     |   |   |   |
  2)               |  sys_read() {
  2)               |    vfs_read() {
  2)               |      __vfs_read() {
  2)   0.631 us    |        rw_verify_area();
  2)               |        ext4_file_read_iter() {
  2)   1.234 us    |          generic_file_read_iter();
  2)   3.456 us    |        }
  2)   5.123 us    |      }
  2)   6.789 us    |    }
  2)   8.901 us    |  }
필드 의미 분석 포인트
CPU 실행 CPU 번호 CPU 간 이동 추적, 친화성 확인
DURATION 함수 실행 시간 (us/ms) 병목 함수 식별 (1ms 이상 주목)
FUNCTION CALLS 호출 스택 (들여쓰기) 호출 경로, 깊이 (5단계 이상 의심)
+ 기호 프로세스 선점(Preemption)됨 스케줄링 지연 발생 지점
! 기호 인터럽트 발생 IRQ 핸들러(Handler) 개입 지점
⚠️

트레이싱 함정 (Tracing Pitfalls):

  • Heisenbug: 트레이싱 오버헤드가 타이밍 변경 → 버그 재현 불가
  • 버퍼 손실: "LOST EVENTS" 메시지 → buffer_size_kb 증가 필요
  • 시간 역전: 멀티코어에서 TSC 동기화 실패 → trace_clock=global 사용
  • 과도한 출력: 초당 수백만 이벤트 → 디스크 I/O 병목
  • 심볼 누락: 모듈 언로드 후 주소만 표시 → 트레이스 중 모듈 유지

ftrace 아키텍처

ftrace(Function Tracer)는 Linux 커널에 내장된 트레이싱 프레임워크입니다. Steven Rostedt가 개발하여 커널 2.6.27에 도입되었으며, 이후 Linux 트레이싱의 핵심 인프라로 자리잡았습니다. ftrace는 단순한 함수 트레이서를 넘어 다양한 트레이서 플러그인을 지원하는 프레임워크입니다.

mcount/fentry 메커니즘

ftrace의 함수 트레이싱은 컴파일러가 삽입하는 프로파일링 훅에 의존합니다. GCC의 -pg 옵션은 모든 함수 진입점(Entry Point)에 mcount() 호출을 삽입합니다. 최신 커널은 -pg -mfentry 옵션을 사용하여 함수 프롤로그 이전에 __fentry__()를 호출합니다.

/* 컴파일러가 삽입하는 코드 (개념적) */
void some_kernel_function(int arg)
{
    __fentry__();       /* 컴파일러가 자동 삽입 */
    /* 원래 함수 코드 ... */
}

/*
 * 부팅 시 __fentry__() 호출은 NOP으로 패치됨 (dynamic ftrace).
 * ftrace가 활성화되면 해당 NOP이 ftrace trampoline으로 다시 패치됨.
 *
 * x86_64에서:
 *   비활성: 0f 1f 44 00 00   (5-byte NOP)
 *   활성:   e8 xx xx xx xx   (CALL ftrace_caller)
 */

ftrace_ops 구조체(Struct)

ftrace 클라이언트(tracer, kprobe 등)는 ftrace_ops 구조체를 통해 콜백(Callback)을 등록합니다. 여러 클라이언트가 동시에 등록될 수 있으며, ftrace는 등록된 모든 콜백을 순회합니다.

/* include/linux/ftrace.h */
struct ftrace_ops {
    ftrace_func_t             func;           /* 콜백 함수 */
    struct ftrace_ops        *next;           /* 연결 리스트 */
    unsigned long             flags;          /* FTRACE_OPS_FL_* */
    struct ftrace_ops_hash    local_hash;     /* 함수 필터 해시 */
    struct ftrace_ops_hash   *func_hash;     /* 필터 해시 포인터 */
    /* ... */
};

/* 콜백 함수 시그니처 */
typedef void (*ftrace_func_t)(unsigned long ip,
                               unsigned long parent_ip,
                               struct ftrace_ops *op,
                               struct ftrace_regs *regs);

Ring Buffer

ftrace의 이벤트 데이터는 per-CPU 링 버퍼(Ring Buffer)에 저장됩니다. 이 링 버퍼는 lockless 설계로 트레이싱 오버헤드를 최소화합니다. 각 CPU는 독립적인 버퍼 페이지(Page)를 가지며, 생산자(트레이서)와 소비자(reader)가 동시에 접근해도 lock 없이 안전하게 동작합니다.

/* kernel/trace/ring_buffer.c - 링 버퍼 핵심 구조 (단순화) */
struct ring_buffer_per_cpu {
    int                        cpu;
    struct ring_buffer        *buffer;
    struct list_head           pages;       /* 버퍼 페이지 리스트 */
    struct buffer_page        *head_page;  /* reader용 */
    struct buffer_page        *tail_page;  /* writer용 */
    struct buffer_page        *commit_page;/* 커밋 완료 페이지 */
    unsigned long              entries;     /* 엔트리 수 */
    unsigned long              overrun;     /* 오버런 카운트 */
    /* ... */
};

/* 링 버퍼 크기 조절 (tracefs) */
/* echo 8192 > /sys/kernel/tracing/buffer_size_kb */
💡

Dynamic ftrace: CONFIG_DYNAMIC_FTRACE=y(기본 활성)를 사용하면, 부팅 시 모든 __fentry__ 호출이 NOP으로 패치(Patch)됩니다. ftrace가 비활성 상태일 때 성능 오버헤드가 사실상 0입니다. 트레이싱을 활성화하면 필요한 함수의 NOP만 선택적으로 콜백 호출로 패치합니다.

ftrace 전체 아키텍처 심층 다이어그램

ftrace 프레임워크의 전체 구조를 이해하면 각 구성 요소의 역할과 데이터 흐름을 명확히 파악할 수 있습니다. 아래 다이어그램은 tracefs 인터페이스, 트레이서 플러그인, ring buffer, per-CPU 버퍼, trace_pipe의 관계를 보여줍니다.

ftrace 프레임워크 전체 아키텍처 유저 공간 trace-cmd perf ftrace bpftrace KernelShark cat trace_pipe 사용자 스크립트 tracefs (/sys/kernel/tracing/) current_tracer trace / trace_pipe events/ set_ftrace_filter kprobe_events instances/ 트레이서 플러그인 레이어 function function_graph wakeup_rt irqsoff preemptoff osnoise blk nop ftrace 코어 엔진 ftrace_ops 콜백 체인 NOP ↔ CALL 패칭 __fentry__ 후크 ftrace_hash 필터 text_poke_bp() SMP-safe 패칭 이벤트 서브시스템 tracepoints (static_key) kprobes / uprobes TRACE_EVENT() 매크로 fprobe (6.2+) hist 트리거 / synthetic 이벤트 Per-CPU Ring Buffer (lockless) CPU 0 버퍼 head/tail/commit CPU 1 버퍼 head/tail/commit CPU 2 버퍼 head/tail/commit ... CPU N 버퍼 head/tail/commit Reader Page (swap 방식 읽기)

데이터 흐름 상세

ftrace의 데이터 흐름은 다음 세 가지 경로로 나뉩니다.

경로데이터 흐름소비 방식특징
tracering buffer → 정적 읽기cat trace읽어도 데이터 유지, 반복 읽기 가능
trace_pipering buffer → 스트리밍cat trace_pipe읽으면 소비됨, 실시간 모니터링용
trace_pipe_rawring buffer → 바이너리(Binary)splice() 시스템 콜zero-copy, trace-cmd가 내부 사용
trace / trace_pipe / trace_pipe_raw 비교 Per-CPU Ring Buffer 이벤트 데이터 저장소 trace (정적 읽기) 데이터 보존, seq_file 기반, 반복 가능 cat $T/trace > output.txt 시점 고정 스냅샷, 디버깅 후 분석용 trace_pipe (스트리밍) 읽으면 소비됨, blocking I/O cat $T/trace_pipe | tee live.log 실시간 모니터링, 장시간 수집용 trace_pipe_raw (바이너리) splice() zero-copy, 최고 효율 trace-cmd 내부 사용 per-CPU 파일, 대량 데이터 수집 선택 기준 trace 장애 직후 버퍼 확인, 반복 분석 trace_pipe 실시간 스트림, 외부 파이프라인 trace_pipe_raw 고성능 수집, trace-cmd record
💡

splice와 trace_pipe_raw: trace-cmd record가 내부적으로 사용하는 trace_pipe_raw는 per-CPU 파일(per_cpu/cpuN/trace_pipe_raw)로 구성됩니다. splice() 시스템 콜로 커널 버퍼에서 파일 디스크립터(File Descriptor)로 직접 데이터를 이동시켜, 유저 공간 복사 없이 최고 효율로 트레이스 데이터를 수집합니다. 대량 이벤트를 초당 수백만 개 이상 수집할 때 필수적인 경로입니다.

ftrace 콜백 체인과 trampolines

여러 ftrace 클라이언트(function tracer, kprobe, BPF 등)가 동시에 활성화되면, 하나의 계측 사이트에서 다중 콜백이 호출됩니다. ftrace는 trampoline이라는 중간 코드를 동적으로 생성하여 등록된 모든 ftrace_ops의 콜백을 순회합니다.

다중 ftrace_ops 콜백 체인 계측 사이트 CALL ftrace_caller ftrace_caller trampoline 레지스터 저장 → 콜백 순회 ops[0]: function tracer 콜백 ring buffer에 함수명/타임스탬프 기록 ops[1]: kprobe ftrace 기반 콜백 pre_handler 실행 ops[2]: BPF direct trampoline BPF 프로그램 직접 실행 최적화: 단일 ops vs 다중 ops 단일 ops 등록 시 전용 trampoline 생성 → 최소 오버헤드 ftrace_ops_list에 하나만 존재하면 직접 호출 다중 ops 등록 시 공용 trampoline → 리스트 순회 hash 필터로 각 ops의 대상 함수 매칭 확인
/* ftrace 콜백 호출 흐름 (단순화) */
/* arch/x86/kernel/ftrace_64.S — ftrace_caller trampoline */
/*
 * 1. 모든 인자 레지스터 저장 (RDI, RSI, RDX, RCX, R8, R9)
 * 2. ftrace_ops_list 순회
 * 3. 각 ops의 func_hash로 현재 함수가 대상인지 확인
 * 4. 대상이면 ops->func(ip, parent_ip, ops, regs) 호출
 * 5. 레지스터 복원 후 원래 함수로 점프
 */

/* 단일 ops 최적화: FTRACE_OPS_FL_IPMODIFY */
/* 하나의 ops만 등록되고 IPMODIFY 플래그가 없으면,
 * ftrace는 전용 trampoline을 생성하여 리스트 순회 없이
 * 직접 콜백을 호출합니다. live-patching이 이 최적화를 활용합니다. */

tracefs 인터페이스

ftrace는 tracefs 파일시스템(Filesystem)을 통해 사용자 공간(User Space)과 상호작용합니다. 커널 4.1 이전에는 debugfs(/sys/kernel/debug/tracing/) 아래에 있었으나, 이후 독립된 tracefs(/sys/kernel/tracing/)로 분리되었습니다. 대부분의 시스템에서 두 경로 모두 사용 가능합니다.

# tracefs 마운트 확인
mount | grep tracefs
# tracefs on /sys/kernel/tracing type tracefs (rw,nosuid,nodev,noexec,relatime)

# 마운트되지 않은 경우 수동 마운트
mount -t tracefs nodev /sys/kernel/tracing

핵심 제어 파일

tracefs의 주요 파일들과 역할을 정리하면 다음과 같습니다.

파일읽기/쓰기설명
current_tracerRW현재 활성 트레이서 설정/확인 (nop, function, function_graph 등)
available_tracersR사용 가능한 트레이서 목록
tracing_onRW트레이싱 활성/비활성 (1/0)
traceR링 버퍼 내용 읽기 (정적, 읽으면 멈추지 않음)
trace_pipeR링 버퍼 내용 스트리밍 (읽은 데이터는 소비됨)
buffer_size_kbRWper-CPU 링 버퍼 크기 (KB)
set_ftrace_filterRW트레이싱할 함수 화이트리스트
set_ftrace_notraceRW트레이싱 제외 함수 블랙리스트
set_ftrace_pidRW특정 PID만 트레이싱
available_filter_functionsR필터 가능한 함수 목록
trace_clockRW타임스탬프 소스 (local, global, x86-tsc 등)
trace_optionsRW트레이서 옵션 토글
events/디렉토리이벤트(tracepoint) 제어 계층
kprobe_eventsRW동적 kprobe 이벤트 정의
uprobe_eventsRW동적 uprobe 이벤트 정의

기본 워크플로우

# 변수 정의 (편의)
T=/sys/kernel/tracing

# 1. 사용 가능한 트레이서 확인
cat $T/available_tracers
# blk function_graph function nop

# 2. 트레이싱 비활성화 (설정 변경 전)
echo 0 > $T/tracing_on

# 3. 트레이서 설정
echo function > $T/current_tracer

# 4. 필터 설정 (선택)
echo 'schedule*' > $T/set_ftrace_filter

# 5. 버퍼 초기화
echo > $T/trace

# 6. 트레이싱 시작
echo 1 > $T/tracing_on

# 7. 워크로드 실행
sleep 1

# 8. 트레이싱 중지
echo 0 > $T/tracing_on

# 9. 결과 확인
cat $T/trace | head -30

# 10. 정리 (nop 트레이서로 복원)
echo nop > $T/current_tracer
echo > $T/set_ftrace_filter

Function Tracer

function 트레이서는 ftrace의 가장 기본적인 트레이서로, 커널 함수 호출을 기록합니다. 각 엔트리에는 타임스탬프, CPU 번호, 프로세스명, PID, 호출 함수명, 부모 함수명이 포함됩니다.

# function tracer 활성화
echo function > /sys/kernel/tracing/current_tracer
echo 1 > /sys/kernel/tracing/tracing_on

# 출력 예시
cat /sys/kernel/tracing/trace
#           TASK-PID    CPU#  |||||  TIMESTAMP  FUNCTION
#              | |        |   |||||     |         |
#        <idle>-0     [003] d..1  1234.567890: tick_nohz_idle_exit <-do_idle
#        <idle>-0     [003] d..1  1234.567892: ktime_get <-tick_nohz_idle_exit
#        <idle>-0     [003] d..1  1234.567893: update_ts_time_stats <-...

function_graph 트레이서

function_graph 트레이서는 함수의 진입과 반환을 모두 기록하여 C 코드와 유사한 계층적 호출 구조를 보여줍니다. 각 함수의 실행 시간도 표시됩니다.

echo function_graph > /sys/kernel/tracing/current_tracer
echo 1 > /sys/kernel/tracing/tracing_on
sleep 0.1
echo 0 > /sys/kernel/tracing/tracing_on
cat /sys/kernel/tracing/trace

# 출력 예시 (들여쓰기로 호출 깊이 표현)
# CPU  DURATION                  FUNCTION CALLS
#  |     |   |                     |   |   |   |
#  3)               |  schedule() {
#  3)               |    __schedule() {
#  3)   0.120 us    |      rcu_note_context_switch();
#  3)               |      pick_next_task_fair() {
#  3)   0.085 us    |        update_curr();
#  3)   0.071 us    |        __pick_next_entity();
#  3)   0.712 us    |      }
#  3)               |      finish_task_switch.isra.0() {
#  3)   0.089 us    |        __perf_event_task_sched_in();
#  3)   0.341 us    |      }
#  3)   2.154 us    |    }
#  3)   2.483 us    |  }

function_graph: 시간 측정과 해석

function_graph 트레이서의 출력에는 다양한 시간 표시 기호가 포함됩니다. 이 기호를 정확히 해석하면 성능 병목을 빠르게 식별할 수 있습니다.

function_graph 출력 기호 해석 공백 (10us 미만) 0.215 us | security_file_permission(); + (10us ~ 100us) + 45.123 us | ext4_readdir(); ! (100us ~ 1s) — 주의 필요 ! 128.347 us | ext4_file_read_iter(); # (1s 이상) — 심각한 지연 # 2145678 us | nfs_readpage(); funcgraph 옵션 (trace_options) funcgraph-duration 함수별 실행 시간 표시 (기본 ON) funcgraph-overhead +, !, # 기호 표시 (기본 ON) funcgraph-abstime 절대 타임스탬프 추가 (기본 OFF) funcgraph-proc 프로세스명/PID 표시 (기본 OFF) funcgraph-irqs 인터럽트 중 호출 표시 (기본 ON) 꼬리 지연(Tail Latency) 분석 전략 1. funcgraph-overhead 활성화 → ! 또는 # 기호가 붙은 함수 식별 2. set_graph_function으로 해당 함수만 집중 추적 → 하위 호출에서 지연 원인 식별 3. funcgraph-irqs를 OFF하여 인터럽트 간섭 제거 → 순수 실행 시간 측정

호출 깊이 제어와 노이즈 감소

커널 함수는 수십 단계까지 중첩 호출될 수 있습니다. max_graph_depth를 적절히 설정하면 출력 노이즈를 줄이고 관심 있는 수준의 함수만 확인할 수 있습니다.

T=/sys/kernel/tracing

# 깊이 제어 전략: 단계적 축소

# 1단계: 전체 구조 파악 (깊이 2)
echo function_graph > $T/current_tracer
echo 2 > $T/max_graph_depth
echo vfs_read > $T/set_graph_function
echo 1 > $T/tracing_on
cat /dev/null
echo 0 > $T/tracing_on
cat $T/trace
# 출력: vfs_read() { __vfs_read(); } — 상위 구조만

# 2단계: 관심 함수 발견 후 깊이 확장 (깊이 5)
echo 5 > $T/max_graph_depth
echo __vfs_read > $T/set_graph_function
# ... 더 상세한 호출 트리

# 3단계: 노이즈 함수 제외
echo 'mutex_lock' > $T/set_graph_notrace
echo 'mutex_unlock' >> $T/set_graph_notrace
echo 'preempt_count_add' >> $T/set_graph_notrace
echo 'preempt_count_sub' >> $T/set_graph_notrace
echo 'rcu_read_lock' >> $T/set_graph_notrace
echo 'rcu_read_unlock' >> $T/set_graph_notrace

# 깊이별 오버헤드 대략적 비교
# depth=1: 최소 오버헤드, 최상위 함수만
# depth=3: 적당한 상세도, 대부분의 분석에 충분
# depth=5: 상세 분석, 오버헤드 증가 주의
# depth=0 (무제한): 전체 호출 트리, 프로덕션 금지

PID 기반 격리 추적

특정 프로세스의 함수 호출만 추적하면 다른 프로세스의 노이즈를 완전히 제거할 수 있습니다. 이 기법은 특정 애플리케이션의 시스템 콜 경로를 분석할 때 필수적입니다.

T=/sys/kernel/tracing

# 방법 1: set_ftrace_pid로 특정 PID 격리
echo 0 > $T/tracing_on
echo function_graph > $T/current_tracer
echo $$ > $T/set_ftrace_pid        # 현재 셸 PID만 추적
echo funcgraph-proc > $T/trace_options  # 프로세스명 표시
echo > $T/trace
echo 1 > $T/tracing_on

# 추적 대상 명령 실행
ls /tmp > /dev/null

echo 0 > $T/tracing_on
cat $T/trace

# 방법 2: 셸 스크립트로 자식 프로세스 자동 추적
echo 0 > $T/tracing_on
echo function_graph > $T/current_tracer
echo 'vfs_*' > $T/set_graph_function
echo 3 > $T/max_graph_depth

# set_ftrace_pid에 여러 PID 추가 가능
echo $$ > $T/set_ftrace_pid
echo 1 > $T/options/function-fork   # 자식 프로세스도 자동 추적

echo > $T/trace
echo 1 > $T/tracing_on
# 대상 워크로드 실행...
my_application --run-test
echo 0 > $T/tracing_on

# 정리
echo > $T/set_ftrace_pid
echo 0 > $T/options/function-fork

함수 필터링

커널에는 수만 개의 함수가 있으므로 모든 함수를 트레이싱하면 오버헤드가 극심합니다. set_ftrace_filterset_ftrace_notrace를 사용하여 관심 있는 함수만 선택적으로 트레이싱합니다.

T=/sys/kernel/tracing

# 와일드카드로 함수 필터링
echo 'schedule*' > $T/set_ftrace_filter
echo '*_schedule' > $T/set_ftrace_filter      # 덮어쓰기
echo 'tcp_*' >> $T/set_ftrace_filter           # 추가 (>>)

# 특정 모듈의 함수만 필터링
echo ':mod:e1000e' > $T/set_ftrace_filter

# 함수 제외 (블랙리스트)
echo 'rcu_*' > $T/set_ftrace_notrace

# 현재 필터 확인
cat $T/set_ftrace_filter

# 필터 초기화 (모든 함수 트레이싱)
echo > $T/set_ftrace_filter

# 필터 가능한 함수 수 확인
wc -l $T/available_filter_functions
# 약 50000~80000개 (커널 빌드 옵션에 따라 다름)

# 특정 PID만 트레이싱
echo 1234 > $T/set_ftrace_pid

# function_graph의 호출 깊이 제한
echo 3 > $T/max_graph_depth
⚠️

필터 없는 function tracer 주의: set_ftrace_filter를 비워둔 채 function 트레이서를 활성화하면 모든 커널 함수가 트레이싱됩니다. 이는 극심한 성능 저하를 유발하며, 링 버퍼가 빠르게 오버플로우됩니다. 반드시 필터를 설정한 후 트레이싱을 시작하십시오.

Tracepoints

Tracepoint는 커널 소스 코드에 미리 정의된 정적 계측점입니다. 개발자가 의미 있는 지점에 삽입하며, 활성화되지 않은 상태에서는 사실상 오버헤드가 없습니다(static key/jump label 메커니즘). tracepoint가 활성화되면 등록된 프로브(Probe) 함수가 호출됩니다.

TRACE_EVENT 매크로(Macro)

TRACE_EVENT()는 tracepoint를 정의하는 핵심 매크로입니다. 하나의 매크로 호출로 tracepoint 선언, 레코드 구조체, 포맷 문자열, ftrace 출력 등을 모두 생성합니다.

/* include/trace/events/sched.h 에서 발췌 (단순화) */
#include <linux/tracepoint.h>

TRACE_EVENT(sched_switch,

    /* 프로토타입: tracepoint 콜백의 인자 */
    TP_PROTO(bool preempt,
             struct task_struct *prev,
             struct task_struct *next,
             unsigned int prev_state),

    /* 호출 시 전달할 인자 */
    TP_ARGS(preempt, prev, next, prev_state),

    /* 링 버퍼에 저장할 구조체 필드 */
    TP_STRUCT__entry(
        __array(  char, prev_comm, TASK_COMM_LEN )
        __field(  pid_t, prev_pid               )
        __field(  int,   prev_prio               )
        __field(  long,  prev_state              )
        __array(  char, next_comm, TASK_COMM_LEN )
        __field(  pid_t, next_pid               )
        __field(  int,   next_prio               )
    ),

    /* 링 버퍼에 데이터 기록 방법 */
    TP_fast_assign(
        memcpy(__entry->prev_comm, prev->comm, TASK_COMM_LEN);
        __entry->prev_pid   = prev->pid;
        __entry->prev_prio  = prev->prio;
        __entry->prev_state = prev_state;
        memcpy(__entry->next_comm, next->comm, TASK_COMM_LEN);
        __entry->next_pid   = next->pid;
        __entry->next_prio  = next->prio;
    ),

    /* 텍스트 출력 포맷 */
    TP_printk("prev_comm=%s prev_pid=%d prev_prio=%d prev_state=%s%s ==> next_comm=%s next_pid=%d next_prio=%d",
        __entry->prev_comm, __entry->prev_pid, __entry->prev_prio,
        (__entry->prev_state & (TASK_REPORT_MAX - 1)) ?
            __print_flags(__entry->prev_state & (TASK_REPORT_MAX - 1), "|",
                { TASK_INTERRUPTIBLE, "S" },
                { TASK_UNINTERRUPTIBLE, "D" }) : "R",
        __entry->prev_state & TASK_REPORT_MAX ? "+" : "",
        __entry->next_comm, __entry->next_pid, __entry->next_prio)
);

tracefs를 통한 이벤트 제어

T=/sys/kernel/tracing

# 사용 가능한 이벤트 카테고리 확인
ls $T/events/
# block  ext4  irq  kmem  net  power  sched  signal  skb  tcp  timer  ...

# 스케줄러 이벤트 목록
ls $T/events/sched/
# sched_switch  sched_wakeup  sched_migrate_task  ...

# 특정 이벤트 활성화
echo 1 > $T/events/sched/sched_switch/enable

# 카테고리 전체 활성화
echo 1 > $T/events/sched/enable

# 모든 이벤트 활성화 (주의: 대량 데이터)
echo 1 > $T/events/enable

# 이벤트 format 확인 (필드 구조)
cat $T/events/sched/sched_switch/format
# name: sched_switch
# ID: 316
# format:
#   field:unsigned short common_type;    offset:0; size:2; signed:0;
#   field:char prev_comm[16];            offset:8; size:16; signed:0;
#   field:pid_t prev_pid;                offset:24; size:4; signed:1;
#   ...

# 이벤트 필터링 (특정 조건만 기록)
echo 'next_pid == 0' > $T/events/sched/sched_switch/filter
echo 'prev_state == 1' > $T/events/sched/sched_switch/filter

# 필터 해제
echo 0 > $T/events/sched/sched_switch/filter

커널 코드에서 tracepoint 호출

/* kernel/sched/core.c - 스케줄러에서 tracepoint 호출 */
static void __sched __schedule(unsigned int sched_mode)
{
    struct task_struct *prev, *next;
    /* ... */

    /* tracepoint 호출 - 비활성 시 static key로 건너뜀 */
    trace_sched_switch(sched_mode & SM_MASK_PREEMPT, prev, next,
                       prev_state);
    /* ... */
}

kprobes

kprobes(Kernel Probes)는 거의 모든 커널 주소에 동적으로 프로브를 삽입할 수 있는 디버깅/트레이싱 메커니즘입니다. 2004년 커널 2.6.9에서 IBM의 Prasanna S Panchamukhi 등이 도입했으며, 소스 코드 수정이나 재컴파일 없이 런타임에 커널 내부 동작을 관찰할 수 있습니다. kprobes는 대상 주소의 명령어를 breakpoint 명령어(x86에서 int3, ARM64에서 BRK #0x004)로 교체하고, 프로브가 적중하면 등록된 핸들러를 호출합니다. ftrace, perf, BPF, SystemTap 등 상위 트레이싱 도구들이 내부적으로 kprobes를 활용합니다.

kprobe 유형

유형설명핸들러 시점CONFIG 옵션
kprobe함수 진입점이나 임의 커널 주소에 프로브pre_handler (명령어 실행 전), post_handler (실행 후)CONFIG_KPROBES
kretprobe함수 반환 시점에 프로브entry_handler (진입 시), handler (반환 시, 반환값 접근 가능)CONFIG_KRETPROBES
⚠️

jprobe 폐기: 커널 4.15 이전에는 jprobe라는 세 번째 유형이 존재했습니다. 함수 인자에 타입 안전하게 접근할 수 있었으나, kprobe + BTF 기반 접근이 더 유연하므로 커널 5.0에서 완전히 제거되었습니다. 기존 코드에서 jprobe 참조를 발견하면 kprobe/kretprobe로 전환해야 합니다.

내부 동작 메커니즘

kprobe의 동작은 등록트랩(int3)pre_handler단일 스텝 실행post_handler복귀의 6단계로 구성됩니다. x86_64 아키텍처를 기준으로 핵심 흐름을 설명합니다.

① 등록(register) 원래 opcode → int3(0xCC) ② 트랩 발생 do_int3 → kprobe_handler ③ pre_handler 사용자 콜백 실행 ④ 단일 스텝 OOL 슬롯에서 실행 ⑤ post_handler 사용자 콜백 실행 ⑥ 복귀 원래 실행 흐름 계속 kretprobe 추가 동작 ③에서 스택의 리턴 주소를 kretprobe_trampoline으로 교체 함수 반환 시 trampoline → ret_handler 콜백 → 원래 caller로 복귀
/* arch/x86/kernel/kprobes/core.c — kprobe 등록 시 핵심 로직 (단순화) */
int arch_prepare_kprobe(struct kprobe *p)
{
    /* 1. 프로브 대상 주소의 원래 명령어를 백업 */
    p->opcode = *p->addr;

    /* 2. out-of-line(OOL) 실행 슬롯 할당 — single-step 실행에 사용 */
    p->ainsn.insn = get_insn_slot();
    memcpy(p->ainsn.insn, p->addr, MAX_INSN_SIZE);

    /* 3. 대상 주소를 int3(0xCC)로 교체 — text_poke는 SMP-safe 코드 패칭 */
    text_poke(p->addr, &int3, 1);
    return 0;
}

/* int3 예외 발생 시 호출되는 핸들러 (단순화) */
int kprobe_int3_handler(struct pt_regs *regs)
{
    struct kprobe *p;
    struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();

    /* int3이 1바이트이므로 IP-1이 원래 프로브 주소 */
    p = get_kprobe((kprobe_opcode_t *)regs->ip - 1);
    if (!p)
        return 0;   /* 우리 kprobe가 아님 */

    /* pre_handler 콜백 실행 */
    if (p->pre_handler && p->pre_handler(p, regs))
        return 1;   /* 핸들러가 실행을 변경함 */

    /* single-step 준비: IP를 OOL 슬롯으로, TF 설정 */
    regs->ip = (unsigned long)p->ainsn.insn;
    regs->flags |= X86_EFLAGS_TF;   /* Trap Flag → debug 예외 발생 */
    regs->flags &= ~X86_EFLAGS_IF;  /* 인터럽트 비활성화 */
    kcb->kprobe_status = KPROBE_HIT_SS;
    return 1;
}

/* debug 예외(single-step 완료) 시 — post_handler 실행 후 원래 흐름으로 복귀 */
int kprobe_debug_handler(struct pt_regs *regs)
{
    struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();
    struct kprobe *p = kcb->kprobe_saved;

    /* post_handler 콜백 실행 */
    if (p->post_handler)
        p->post_handler(p, regs, 0);

    /* IP를 원래 명령어 다음 주소로 복원 */
    resume_execution(p, regs, kcb);
    return 1;
}

kprobe 구조체 상세

/* include/linux/kprobes.h */
struct kprobe {
    struct hlist_node  hlist;         /* kprobe 해시 테이블 연결 */
    struct list_head   list;          /* 동일 주소 다중 프로브 리스트 (aggregate) */
    unsigned long      nmissed;       /* 재진입으로 놓친 프로브 수 */
    kprobe_opcode_t   *addr;         /* 프로브 대상 주소 (자동 해석) */
    const char        *symbol_name;  /* 심볼 이름 (addr 대신 사용 가능) */
    unsigned int       offset;        /* 심볼 시작점으로부터의 오프셋 */

    /* 콜백 핸들러 */
    kprobe_pre_handler_t    pre_handler;   /* 명령어 실행 전 */
    kprobe_post_handler_t   post_handler;  /* 명령어 실행 후 */

    /* 내부 관리용 */
    struct arch_specific_insn ainsn;   /* OOL 슬롯, 명령어 디코딩 정보 */
    u32               flags;         /* KPROBE_FLAG_* */
    kprobe_opcode_t   opcode;        /* 백업된 원래 opcode */
};

/* 주요 플래그 */
#define KPROBE_FLAG_GONE       1   /* 모듈 언로드 등으로 프로브 무효화 */
#define KPROBE_FLAG_DISABLED   2   /* disable_kprobe()로 일시 비활성화 */
#define KPROBE_FLAG_OPTIMIZED  4   /* jump 최적화 적용됨 (OPTPROBES) */
#define KPROBE_FLAG_FTRACE     8   /* ftrace 기반 프로브 (함수 진입점) */
ℹ️

동일 주소 다중 프로브: 같은 주소에 여러 kprobe를 등록할 수 있습니다. 커널은 내부적으로 aggregate kprobe를 생성하여 등록된 모든 핸들러를 순차적으로 호출합니다. list 필드가 이 연결에 사용됩니다.

kretprobe 내부 구조

kretprobe는 함수의 반환 시점을 캡처합니다. 등록 시 대상 함수 진입점에 kprobe를 설치하고, 함수 진입 시 스택의 리턴 주소를 kretprobe_trampoline으로 교체합니다. 함수가 반환하면 trampoline이 실행되어 ret_handler를 호출한 뒤, 원래 caller 주소로 복귀합니다.

/* include/linux/kprobes.h */
struct kretprobe {
    struct kprobe    kp;              /* 내장 kprobe (함수 진입점에 설치) */
    kretprobe_handler_t handler;     /* 반환 시 콜백 */
    kretprobe_handler_t entry_handler;/* 진입 시 콜백 (데이터 수집용) */
    int             maxactive;       /* 동시 인스턴스 최대 수 */
    int             nmissed;         /* 인스턴스 부족으로 놓친 수 */
    size_t          data_size;       /* 인스턴스별 private 데이터 크기 */
    struct freelist_head freelist;    /* 사용 가능한 인스턴스 pool */
};

/* 각 활성 호출마다 하나씩 할당되는 인스턴스 */
struct kretprobe_instance {
    union {
        struct freelist_node freelist;
        struct rcu_head      rcu;
    };
    struct llist_node   llist;
    struct kretprobe   *rp;             /* 소속 kretprobe */
    kprobe_opcode_t    *ret_addr;       /* 원래 리턴 주소 (백업) */
    struct task_struct *task;           /* 연관 태스크 */
    char               data[];          /* private 데이터 (flexible array) */
};

/*
 * maxactive 가이드:
 *   0         → num_possible_cpus()로 자동 설정
 *   N > 0     → 동시에 N개의 호출만 추적
 *   nmissed가 증가하면 maxactive 증가 필요
 *   재귀/고빈도 함수: 2 * num_possible_cpus() 이상 권장
 */

커널 API로 kprobe 등록

#include <linux/kprobes.h>
#include <linux/module.h>

/* pre_handler: 프로브 지점 도달 시 호출 */
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
    /* x86_64: 첫 번째 인자 = rdi 레지스터 */
    pr_info("kprobe hit: %s, ip=%pS, arg0=0x%lx\\n",
            p->symbol_name,
            (void *)regs->ip,
            regs->di);
    return 0;
}

/* post_handler: 프로브 지점의 원래 명령어 실행 후 호출 */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
                         unsigned long flags)
{
    pr_info("kprobe post: ip=%pS, flags=0x%lx\\n",
            (void *)regs->ip, regs->flags);
}

static struct kprobe my_kp = {
    .symbol_name = "do_sys_openat2",
    .pre_handler = handler_pre,
    .post_handler = handler_post,
};

static int __init kp_init(void)
{
    int ret = register_kprobe(&my_kp);
    if (ret < 0) {
        pr_err("register_kprobe 실패: %d\\n", ret);
        return ret;
    }
    pr_info("kprobe 등록: %s at %pS\\n",
            my_kp.symbol_name, my_kp.addr);
    return 0;
}

static void __exit kp_exit(void)
{
    unregister_kprobe(&my_kp);
    pr_info("kprobe 해제: missed=%lu\\n", my_kp.nmissed);
}

module_init(kp_init);
module_exit(kp_exit);
MODULE_LICENSE("GPL");

kretprobe 사용

#include <linux/kprobes.h>

/* kretprobe 인스턴스별 데이터 */
struct my_data {
    ktime_t entry_stamp;
};

/* 함수 진입 시 호출 */
static int entry_handler(struct kretprobe_instance *ri,
                          struct pt_regs *regs)
{
    struct my_data *data = (struct my_data *)ri->data;
    data->entry_stamp = ktime_get();
    return 0;
}

/* 함수 반환 시 호출 */
static int ret_handler(struct kretprobe_instance *ri,
                        struct pt_regs *regs)
{
    struct my_data *data = (struct my_data *)ri->data;
    s64 delta = ktime_to_ns(ktime_sub(ktime_get(), data->entry_stamp));
    unsigned long retval = regs_return_value(regs);

    pr_info("함수 %s 반환: retval=%lu, 소요시간=%lldns\\n",
            ri->rp->kp.symbol_name, retval, delta);
    return 0;
}

static struct kretprobe my_kretprobe = {
    .handler         = ret_handler,
    .entry_handler   = entry_handler,
    .data_size       = sizeof(struct my_data),
    .maxactive       = 20,    /* 동시 프로브 인스턴스 수 */
    .kp.symbol_name  = "vfs_read",
};

static int __init krp_init(void)
{
    return register_kretprobe(&my_kretprobe);
}

static void __exit krp_exit(void)
{
    unregister_kretprobe(&my_kretprobe);
    pr_info("missed=%lu\\n", my_kretprobe.nmissed);
}

kprobe 최적화 (OPTPROBES)

기본 kprobe는 int3 예외를 사용하므로 trap 처리 비용이 발생합니다. CONFIG_OPTPROBES=y(x86 기본 활성)를 사용하면, 커널은 적격한 kprobe를 jump 최적화합니다. int3(1바이트) 대신 JMP rel32(5바이트)로 패치하여 detour buffer(trampoline)로 직접 점프합니다. 이로써 trap 오버헤드가 사라지고, 단순 함수 호출 수준의 비용만 남습니다.

설명 요약:
  • 최적화 전: int3 기반 (느림) */
  • probe point: CC (int3) → 예외 발생 → 핸들러 */
  • 최적화 후: jump 기반 (빠름) */
  • probe point: E9 xx xx xx xx (JMP) → detour buffer → 핸들러 */
  • 최적화 조건 (x86_64):
  • 프로브 주소부터 5바이트 이내에 다른 프로브나 점프 대상이 없어야 함
  • 프로브 대상 명령어가 RIP-relative가 아니어야 함 (또는 보정 가능해야 함)
  • pre_handler만 사용하는 경우 (post_handler 미등록)
  • 함수 진입점의 경우 ftrace 기반 최적화도 가능 (KPROBE_FLAG_FTRACE)
  • ftrace 기반 kprobe: 함수 진입점에 프로브를 설치하면
  • int3 대신 ftrace의 __fentry__ 메커니즘을 재사용합니다.
  • CONFIG_KPROBES_ON_FTRACE=y일 때 자동 적용. */
💡

최적화 상태 확인: cat /sys/kernel/debug/kprobes/list에서 각 프로브의 상태를 확인할 수 있습니다. [OPTIMIZED] 표시가 있으면 jump 최적화 적용, [FTRACE] 표시는 ftrace 기반 프로브입니다.

kprobe 블랙리스트와 안전성

모든 커널 함수에 kprobe를 설치할 수 있는 것은 아닙니다. 커널은 블랙리스트를 유지하여 프로브 설치 시 안전하지 않은 함수를 거부합니다.

블랙리스트 유형이유예시
__kprobes 섹션kprobe 인프라 자체 함수 (재진입 방지)kprobe_handler, do_int3
NOKPROBE_SYMBOL()명시적으로 프로브 금지 선언notrace 함수, 저수준 예외 핸들러
인라인 함수(Inline Function)독립 심볼이 없어 주소를 특정할 수 없음컴파일러가 인라인한 static 함수
어셈블리(Assembly) 엔트리C 호출 규약(Calling Convention)을 따르지 않음entry_SYSCALL_64, ret_from_fork
# 블랙리스트 확인
cat /sys/kernel/debug/kprobes/blacklist | head -20
# 0xffffffff81000000-0xffffffff81000100 .text.head
# 0xffffffff8100e000-0xffffffff8100e200 kprobe_handler

# 등록된 모든 kprobe 확인
cat /sys/kernel/debug/kprobes/list
# ffffffff812a5f60  k  do_sys_openat2+0x0  [OPTIMIZED]
# ffffffff8130b2a0  r  vfs_read+0x0        [FTRACE]
# (k=kprobe, r=kretprobe)

# kprobe 전체 비활성화/재활성화 (디버깅 시 유용)
echo 0 > /sys/kernel/debug/kprobes/enabled   # 모든 kprobe 비활성화
echo 1 > /sys/kernel/debug/kprobes/enabled   # 재활성화
☢️

안전성 주의: kprobe pre_handler 내에서는 preempt_disable() 상태이며, IRQ가 비활성화될 수 있습니다. 슬립(Sleep) 가능 함수(kmalloc(GFP_KERNEL), mutex_lock() 등)를 호출하면 안 됩니다. 핸들러는 가능한 짧게 유지하고, 데이터는 per-CPU 버퍼나 BPF 맵에 기록하는 것이 안전합니다.

명령어 패칭(Instruction Patching) 상세

kprobe의 명령어 패칭은 실행 중인 커널의 코드 텍스트를 안전하게 수정하는 정교한 과정입니다. SMP 환경에서 다른 CPU가 동일 코드를 실행 중일 수 있으므로, 원자적 패칭이 필수적입니다.

kprobe 명령어 패칭: x86_64 기준 원본 코드 48 89 e5 mov %rsp,%rbp ← 프로브 대상 명령어 (3B) 41 57 ... push %r15 ... ① opcode 백업 p->opcode = *p->addr; // 0x48 원래 명령어를 OOL 슬롯에 복사 get_insn_slot()로 슬롯 할당 ② int3 삽입 CC 89 e5 int3 + 쓰레기 text_poke()로 원자적 교체 1바이트 쓰기 → 항상 원자적 ③ 프로브 적중 시 실행 흐름 int3 트랩 발생 do_int3 진입 pre_handler OOL single-step post_handler Out-of-Line (OOL) 실행 슬롯 원래 명령어 복사본이 저장됨 (insn_slot) IP를 이 슬롯으로 변경 후 TF(Trap Flag) 설정 single-step 완료 후 debug 예외 발생 IP를 원래 다음 명령어로 복원 → 정상 흐름 계속 기본 kprobe (int3 기반) ~40ns/hit, 범용, 임의 오프셋 지원 OPTPROBES (JMP 기반) ~10ns/hit, 함수 진입점, 5B 여유 필요
ℹ️

ARM64에서의 kprobe: ARM64는 BRK #0x004 명령어(4바이트)를 사용합니다. x86의 int3(1바이트)와 달리 4바이트 정렬이 보장되므로, 명령어 경계 문제가 발생하지 않습니다. single-step은 PSTATE.SS 비트를 사용합니다. RISC-V에서는 EBREAK 명령어(2바이트 또는 4바이트)를 사용하며, compressed 확장(C extension)이 있으면 2바이트 명령어도 존재합니다.

kprobe 안전성 검증 체크리스트

kprobe를 프로덕션 환경에 적용하기 전에 반드시 확인해야 할 안전성 항목입니다.

검증 항목확인 방법위험 시나리오
블랙리스트 확인cat /sys/kernel/debug/kprobes/blacklist블랙리스트 함수에 프로브 → 커널 패닉 가능
재진입 방지핸들러 내 kprobe 대상 함수 호출 금지무한 재귀 → 스택 오버플로우
핸들러 실행 시간핸들러 실행 시간 < 10us 유지긴 핸들러 → 시스템 지연, watchdog 트리거
Sleep 금지핸들러 내 GFP_KERNEL, mutex 등 사용 금지preempt_disable 상태에서 sleep → 데드락
nmissed 모니터링cat /sys/kernel/debug/kprobes/listnmissed 증가 → maxactive 부족 (kretprobe)
모듈 의존성모듈 언로드 전 프로브 해제 확인언로드된 주소 접근 → 페이지 폴트
심볼 존재 확인grep func /proc/kallsyms인라인된 함수 → 심볼 없음 → 등록 실패

tracefs를 통한 kprobe 이벤트

커널 모듈(Kernel Module) 작성 없이 tracefs 인터페이스만으로도 kprobe 이벤트를 정의하고 사용할 수 있습니다.

T=/sys/kernel/tracing

# kprobe 이벤트 정의: do_sys_openat2 진입 시 첫 번째 인자(dfd)와 두 번째 인자(filename) 기록
echo 'p:myprobe do_sys_openat2 dfd=%di:s32 filename=+0(%si):string' > $T/kprobe_events

# kretprobe 이벤트 정의: vfs_read 반환값 기록
echo 'r:myretprobe vfs_read ret=$retval:s64' >> $T/kprobe_events

# 정의된 이벤트 확인
cat $T/kprobe_events

# 이벤트 활성화
echo 1 > $T/events/kprobes/myprobe/enable
echo 1 > $T/events/kprobes/myretprobe/enable

# 트레이싱 결과 확인
cat $T/trace_pipe
#  bash-1234 [002] .... 5678.901234: myprobe: (do_sys_openat2+0x0/0x...) dfd=-100 filename="/etc/passwd"
#  bash-1234 [002] d... 5678.901345: myretprobe: (SyS_read+0x../0x..) func=vfs_read ret=4096

# 이벤트 비활성화 및 제거
echo 0 > $T/events/kprobes/myprobe/enable
echo '-:myprobe' >> $T/kprobe_events
echo > $T/kprobe_events  # 모든 kprobe 이벤트 제거

Fetch 인자 구문 참조

tracefs kprobe 이벤트에서 인자를 캡처할 때 사용하는 fetch-arg 구문의 전체 참조입니다.

구문설명예시
%REGCPU 레지스터 값%di, %si, %ax
@SYMBOL전역 심볼의 메모리 값@jiffies
@SYMBOL+offset심볼 + 오프셋(Offset) 위치의 값@task_struct+16
$stack스택 포인터 (kprobe만)$stack
$stackNN번째 스택 엔트리$stack0, $stack3
$retval함수 반환값 (kretprobe/uretprobe만)$retval
$comm현재 태스크(Task) 이름$comm
+OFFSET(%REG)레지스터 역참조 (포인터 따라감)+0(%di), +8(%si)
+OFFSET(+OFFSET(%REG))이중 역참조 (포인터의 포인터)+0(+16(%di))
\IMM즉시값 상수\1234

캡처된 값의 타입을 지정할 수 있습니다:

타입 접미사설명크기
:u8 / :s8부호 없는/있는 8비트1바이트
:u16 / :s16부호 없는/있는 16비트2바이트
:u32 / :s32부호 없는/있는 32비트4바이트
:u64 / :s64부호 없는/있는 64비트8바이트
:x8 ~ :x6416진수 출력1~8바이트
:stringNULL 종료 문자열가변
:ustring유저 공간 문자열 (커널 5.8+)가변
:symbol커널 심볼(Kernel Symbol) 이름으로 변환포인터

아키텍처별 레지스터 매핑(Mapping)

인자x86_64ARM64 (AArch64)RISC-V
arg1%di (RDI)%x0%a0
arg2%si (RSI)%x1%a1
arg3%dx (RDX)%x2%a2
arg4%cx (RCX)%x3%a3
arg5%r8%x4%a4
arg6%r9%x5%a5
반환값%ax (RAX)%x0%a0
스택 포인터%sp (RSP)%sp%sp
7번째+ 인자+offset(%sp)%x6, %x7, 스택%a6, %a7, 스택

BTF 기반 kprobe (커널 5.8+)

커널 5.8부터 BTF(BPF Type Format) 정보를 활용하여 구조체 필드에 이름으로 접근할 수 있습니다. 레지스터 오프셋을 수동으로 계산할 필요 없이, 커널 데이터 구조를 타입 안전하게 탐색합니다.

T=/sys/kernel/tracing

# BTF 활성화 여부 확인
ls /sys/kernel/btf/vmlinux
# /sys/kernel/btf/vmlinux  (존재하면 BTF 활성)

# BTF 기반 kprobe: 구조체 필드를 이름으로 접근
# do_filp_open()의 pathname 인자에서 name 필드 추적
echo 'p:myprobe do_filp_open pathname=+0(%si):string' > $T/kprobe_events

# BPF 프로그램에서 BTF를 활용한 kprobe (bpftrace 예시)
# 구조체 필드에 직접 접근 가능 (오프셋 계산 불필요)
bpftrace -e '
kprobe:tcp_sendmsg {
    $sk = (struct sock *)arg0;
    $inet = (struct inet_sock *)$sk;
    printf("pid=%d dport=%d\\n", pid,
           $inet->inet_dport);
}'

# BTF 기반 kprobe (커널 모듈에서 BPF 프로그램 사용)
# SEC("kprobe/tcp_sendmsg")를 사용하면
# BPF CO-RE (Compile Once, Run Everywhere)로
# 커널 버전 간 호환성 확보 가능

kprobe multi-attach (BPF, 커널 5.18+)

커널 5.18에서 도입된 BPF_LINK_TYPE_KPROBE_MULTI는 하나의 BPF 프로그램을 수천 개의 함수에 동시에 연결할 수 있습니다. 기존에는 함수마다 개별 kprobe를 등록해야 했으나, multi-attach는 fprobe(ftrace 기반 고속 프로브) 인프라를 사용하여 대량 프로브를 효율적으로 처리합니다.

# bpftrace에서 와일드카드로 다중 kprobe 사용
bpftrace -e 'kprobe:vfs_* { @[func] = count(); }'

# libbpf C 코드에서 kprobe.multi 사용 예시
# LIBBPF_OPTS(bpf_kprobe_multi_opts, opts);
# opts.syms = (const char *[]){"vfs_read", "vfs_write", ...};
# opts.cnt = n_syms;
# bpf_program__attach_kprobe_multi_opts(prog, NULL, &opts);
ℹ️

fprobe vs kprobe: fprobe는 커널 5.18+에서 사용 가능한 ftrace 기반 고속 프로브입니다. kprobe의 int3 트랩 대신 ftrace의 NOP→CALL 패칭을 사용하므로 오버헤드가 훨씬 낮습니다. BPF kprobe.multi는 내부적으로 fprobe를 사용합니다. 단, fprobe는 함수 진입점에만 설치할 수 있고 임의 오프셋은 지원하지 않습니다.

uprobes

uprobes(User-space Probes)는 유저 공간 프로그램의 함수나 임의 주소에 동적으로 프로브를 삽입하는 메커니즘입니다. 커널 3.5에서 Srikar Dronamraju 등이 도입했으며, kprobes와 유사한 원리로 동작하지만 대상이 유저 공간 바이너리입니다. 커널이 ELF 바이너리의 지정된 오프셋에 breakpoint를 삽입하고, 프로브가 적중하면 커널 내에서 핸들러를 실행합니다. 유저 공간 코드 수정 없이 애플리케이션 성능 분석, 함수 호출 추적(Call Trace), 메모리 할당 패턴 분석 등이 가능합니다.

내부 동작 메커니즘

uprobe의 동작은 kprobe와 유사하지만, 유저 공간 특유의 메커니즘이 추가됩니다. 핵심은 페이지 캐시(Page Cache) 조작XOL(eXecute Out of Line) 영역입니다.

유저 공간 커널 공간 ELF 바이너리 원본 명령어 보존 페이지 캐시 int3(0xCC)로 교체 프로세스 메모리 COW 페이지 매핑 XOL 영역 원래 명령어 복사본 uprobe_handler pre_handler 콜백 single-step 준비 IP → XOL 영역 post_handler 원래 흐름 복귀 ①등록 ②int3 트랩 ④XOL 실행 모든 프로세스가 동일 페이지 캐시를 공유 → 하나의 uprobe가 모든 인스턴스에 적용
/* kernel/events/uprobes.c — 핵심 구조 (단순화) */
struct uprobe {
    struct rb_node       rb_node;     /* inode별 RB 트리 */
    struct inode        *inode;       /* 대상 ELF 파일 inode */
    loff_t               offset;      /* 파일 내 오프셋 */
    struct uprobe_consumer *consumers; /* 등록된 consumer 리스트 */
    struct arch_uprobe    arch;        /* 아키텍처별 정보 */
    refcount_t           ref;
    unsigned long        flags;
};

/*
 * XOL(eXecute Out of Line) 영역:
 * - 각 프로세스의 주소 공간에 할당되는 특수 페이지
 * - uprobe가 교체한 원래 명령어의 복사본을 저장
 * - single-step 실행 시 이 영역에서 원래 명령어를 실행
 * - 유저 공간에 매핑되지만 프로세스가 직접 접근할 수 없음
 */
struct xol_area {
    wait_queue_head_t   wq;          /* 슬롯 대기 큐 */
    unsigned long       *bitmap;     /* 슬롯 할당 비트맵 */
    struct page        *page;        /* XOL 페이지 */
    unsigned long        vaddr;       /* 프로세스 주소 공간의 가상 주소 */
};
ℹ️

페이지 캐시와 COW: uprobe는 대상 바이너리의 페이지 캐시에서 명령어를 int3로 교체합니다. 디스크의 원본 파일은 변경되지 않습니다. COW(Copy-on-Write) 메커니즘으로 페이지 캐시의 변경된 페이지가 관리되며, uprobe 해제 시 원래 명령어가 복원됩니다. 같은 바이너리를 실행하는 모든 프로세스가 동일 페이지 캐시를 공유하므로, 하나의 uprobe 등록으로 모든 인스턴스가 추적됩니다.

tracefs를 통한 uprobe 사용

T=/sys/kernel/tracing

# 1. 심볼 오프셋 확인 — uprobe는 파일 오프셋을 사용 (가상 주소 아님)
# 방법 A: objdump
objdump -tT /bin/bash | grep readline
# 방법 B: readelf
readelf -s /bin/bash | grep readline
# 방법 C: nm (디버그 심볼 있는 경우)
nm /usr/bin/python3 | grep PyObject_Call

# 2. uprobe 이벤트 정의 (p=진입, r=반환)
echo 'p:bash_readline /bin/bash:0x4a2e0' > $T/uprobe_events

# 3. 인자 캡처 — 유저 공간 레지스터 접근
# x86_64: %di=arg1, %si=arg2 (C ABI와 동일)
echo 'p:malloc_call /usr/lib/x86_64-linux-gnu/libc.so.6:0x9d460 size=%di:u64' >> $T/uprobe_events

# 4. uretprobe: 반환값 캡처
echo 'r:malloc_ret /usr/lib/x86_64-linux-gnu/libc.so.6:0x9d460 ret=$retval:x64' >> $T/uprobe_events

# 5. 활성화 및 트레이싱
echo 1 > $T/events/uprobes/bash_readline/enable
echo 1 > $T/events/uprobes/malloc_call/enable
echo 1 > $T/events/uprobes/malloc_ret/enable
echo 1 > $T/tracing_on
cat $T/trace_pipe
#  bash-1234 [002] d... 5678.901: bash_readline: (0x55a0004a2e0)
#  bash-1234 [002] d... 5678.902: malloc_call: (0x7f...) size=128
#  bash-1234 [002] d... 5678.902: malloc_ret: (0x7f...) ret=0x55a0012f450

# 6. 정리
echo 0 > $T/events/uprobes/enable   # 모든 uprobe 이벤트 비활성화
echo > $T/uprobe_events              # 모든 uprobe 이벤트 제거

커널 API: uprobe_register

#include <linux/uprobes.h>

/* uprobe consumer — 프로브 적중 시 호출되는 콜백 */
struct uprobe_consumer {
    /* 프로브 진입 시 호출 (return 0 = 계속, return UPROBE_HANDLER_REMOVE = 제거) */
    int (*handler)(struct uprobe_consumer *self,
                   struct pt_regs *regs);
    /* 프로브 반환 시 호출 (uretprobe) */
    int (*ret_handler)(struct uprobe_consumer *self,
                        unsigned long func,
                        struct pt_regs *regs);
    /* 프로세스 필터 — true를 반환하는 프로세스만 추적 */
    bool (*filter)(struct uprobe_consumer *self,
                   struct mm_struct *mm);
};

/*
 * uprobe 등록
 * inode: 대상 ELF 파일의 inode (파일시스템 수준)
 * offset: 파일 내 오프셋 (가상 주소가 아님!)
 *         가상 주소 → 파일 오프셋 변환: objdump 또는 /proc/pid/maps 활용
 * uc: consumer 콜백 구조체
 */
int uprobe_register(struct inode *inode,
                    loff_t offset,
                    struct uprobe_consumer *uc);
void uprobe_unregister(struct inode *inode,
                      loff_t offset,
                      struct uprobe_consumer *uc);

/* uprobe_register_refctr: 세마포어 기반 uprobe (USDT용)
 * ref_ctr_offset: ELF .note.stapsdt 섹션의 참조 카운터 오프셋 */
int uprobe_register_refctr(struct inode *inode,
                          loff_t offset,
                          loff_t ref_ctr_offset,
                          struct uprobe_consumer *uc);

uretprobe 상세

uretprobe는 유저 공간 함수의 반환 시점을 캡처합니다. kretprobe와 유사하게 함수 진입 시 스택의 리턴 주소를 trampoline으로 교체하여 반환을 가로챕니다. tracefs에서는 r: 접두사로 정의합니다.

T=/sys/kernel/tracing

# uretprobe 활용 예시: 함수 실행 시간 측정
# SSL_read 진입/반환 추적으로 SSL 복호화 레이턴시 분석
echo 'p:ssl_read_entry /usr/lib/x86_64-linux-gnu/libssl.so.3:SSL_read' > $T/uprobe_events
echo 'r:ssl_read_ret /usr/lib/x86_64-linux-gnu/libssl.so.3:SSL_read ret=$retval:s32' >> $T/uprobe_events

# bpftrace로 uretprobe 사용 — 더 유연한 분석 가능
bpftrace -e '
uprobe:/usr/lib/x86_64-linux-gnu/libc.so.6:malloc {
    @start[tid] = nsecs;
    @size[tid] = arg0;
}
uretprobe:/usr/lib/x86_64-linux-gnu/libc.so.6:malloc /@start[tid]/ {
    $dur = nsecs - @start[tid];
    printf("malloc(%d) = %p [%d ns]\\n", @size[tid], retval, $dur);
    delete(@start[tid]); delete(@size[tid]);
}'
⚠️

uretprobe와 tail call: 컴파일러가 tail call 최적화를 적용한 함수에서는 uretprobe가 정상 동작하지 않을 수 있습니다. tail call은 리턴 주소를 교체하지 않고 JMP로 다음 함수를 호출하므로, trampoline이 실행되지 않습니다. -fno-optimize-sibling-calls로 빌드하면 이 문제를 피할 수 있습니다.

USDT (User Statically Defined Tracing)

USDT는 유저 공간 프로그램에 미리 정의된 정적 트레이스포인트입니다. 커널의 tracepoints에 대응하는 유저 공간 버전으로, 개발자가 소스 코드에 프로브 포인트를 명시적으로 삽입합니다. USDT는 ELF .note.stapsdt 섹션에 메타데이터를 저장하며, uprobe 인프라 위에서 동작합니다.

# USDT 프로브가 포함된 바이너리 확인
readelf -n /usr/lib/x86_64-linux-gnu/libc.so.6 | grep -A4 stapsdt
# stapsdt    0x00000039    NT_STAPSDT (SystemTap probe descriptors)
#     Provider: libc
#     Name: memory_malloc_retry
#     Location: 0x00000000000973a0, Base: 0x..., Semaphore: 0x...

# Python USDT 프로브 확인
bpftrace -l 'usdt:/usr/bin/python3:*'
# usdt:/usr/bin/python3:python:function__entry
# usdt:/usr/bin/python3:python:function__return
# usdt:/usr/bin/python3:python:gc__start
# usdt:/usr/bin/python3:python:gc__done

# Python 함수 호출 추적 (USDT)
bpftrace -e '
usdt:/usr/bin/python3:python:function__entry {
    printf("%s %s:%d\\n", str(arg0), str(arg1), arg2);
}' -p $(pidof python3)

# MySQL/MariaDB 쿼리 추적 (USDT)
bpftrace -e '
usdt:/usr/sbin/mysqld:mysql:query__start {
    printf("query: %s\\n", str(arg0));
}'
/* USDT 프로브 정의 (C 소스에서) — SystemTap SDT 헤더 사용 */
#include <sys/sdt.h>

void process_request(struct request *req)
{
    /* USDT 프로브: provider=myapp, name=request_start */
    DTRACE_PROBE2(myapp, request_start, req->id, req->size);

    /* ... 실제 처리 ... */

    DTRACE_PROBE1(myapp, request_done, req->id);
}

/*
 * 빌드: gcc -o myapp myapp.c (SDT NOP이 자동 삽입)
 * 프로브 비활성 시: NOP 명령어로 오버헤드 거의 0
 * 프로브 활성 시: NOP → int3 교체 (uprobe 메커니즘)
 *
 * USDT 세마포어:
 * - .note.stapsdt에 Semaphore 필드가 있으면
 *   uprobe 등록 시 세마포어 카운터를 증가
 *   프로그램이 세마포어를 확인하여 추가 데이터를 수집할 수 있음
 *   uprobe_register_refctr()로 세마포어 지원 등록
 */

perf probe를 이용한 uprobe

perf probe는 tracefs를 직접 조작하지 않고도 uprobe를 편리하게 등록/관리할 수 있는 도구입니다. DWARF 디버그 정보가 있으면 함수 이름과 변수 이름으로 직접 접근할 수 있습니다.

# 디버그 심볼이 있는 바이너리에서 사용 가능한 프로브 포인트 확인
perf probe -x /usr/lib/x86_64-linux-gnu/libc.so.6 -F | grep malloc
# __libc_malloc
# __libc_calloc

# uprobe 등록 (perf probe)
perf probe -x /usr/lib/x86_64-linux-gnu/libc.so.6 --add 'malloc_entry=__libc_malloc size'

# DWARF 변수 접근 — 디버그 심볼 필요 (-dbg 패키지 설치)
perf probe -x /usr/bin/python3 --add 'pymain=Py_Main argc'

# 등록된 프로브로 레코딩
perf record -e probe_libc:malloc_entry -aR -- sleep 5
perf script
#  python3  1234 [001]  5678.901: probe_libc:malloc_entry: (7f...+0x9d460) size=128

# 프로브 목록 확인 및 삭제
perf probe --list
perf probe --del malloc_entry

uprobe와 BPF

BPF 프로그램을 uprobe에 연결하면, 유저 공간 함수의 인자/반환값을 안전하게 캡처하고 BPF 맵으로 집계할 수 있습니다. bpf_probe_read_user() 헬퍼로 유저 공간 메모리를 안전하게 읽으며, 커널 5.5+에서는 bpf_probe_read_user_str()로 유저 공간 문자열도 읽을 수 있습니다.

/* BPF 프로그램 (libbpf CO-RE 스타일) */
SEC("uprobe//usr/lib/x86_64-linux-gnu/libc.so.6:malloc")
int BPF_KPROBE(malloc_enter, size_t size)
{
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    bpf_printk("pid=%d malloc(%zu)\\n", pid, size);

    /* BPF 맵에 할당 크기 히스토그램 기록 */
    u64 slot = bpf_log2l(size);
    bpf_map_update_elem(&alloc_hist, &slot, &(u64){1}, BPF_ANY);
    return 0;
}

SEC("uretprobe//usr/lib/x86_64-linux-gnu/libc.so.6:malloc")
int BPF_KRETPROBE(malloc_exit, void *ret)
{
    /* 반환된 포인터 (할당된 메모리 주소) 기록 */
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    bpf_printk("pid=%d malloc returned %p\\n", pid, ret);
    return 0;
}

/*
 * uprobe.multi (커널 6.6+):
 * - kprobe.multi와 유사하게 하나의 BPF 프로그램을
 *   여러 uprobe에 동시 연결
 * - SEC("uprobe.multi") 사용
 * - 대량의 함수를 추적할 때 효율적
 */

제약사항과 고려사항

제약설명대응 방안
심볼 스트립릴리스 바이너리에서 심볼이 제거되면 함수 오프셋 찾기 어려움-dbg 패키지 설치, 또는 /proc/pid/maps + objdump로 수동 계산
ASLR주소 공간(Address Space) 배치 랜덤화로 실행마다 주소 변경uprobe는 파일 오프셋을 사용하므로 ASLR 영향 없음
공유 라이브러리(Shared Library)라이브러리의 uprobe가 모든 프로세스에 영향filter 콜백 또는 BPF에서 PID 필터링
인라인 함수컴파일러가 인라인한 함수는 독립 심볼 없음DWARF 정보 활용 (perf probe --line), 또는 noinline 속성
JIT 코드JIT 생성 코드(Java, V8 등)에 직접 uprobe 불가USDT 프로브 사용 (예: Node.js --enable-dtrace)
오버헤드고빈도 함수에서 int3 트랩 비용이 누적BPF 필터로 조건부 기록, 또는 USDT 세마포어(Semaphore) 활용
PIE 바이너리Position Independent Executable의 오프셋 계산readelf -l로 로드 주소 확인, 오프셋 = VA - 로드 기본 주소
💡

uprobe 디버깅 팁: uprobe가 기대대로 동작하지 않을 때 /sys/kernel/debug/tracing/uprobe_profile을 확인하십시오. 각 uprobe의 적중 횟수를 보여줍니다. 적중이 0이면 오프셋 계산이 잘못되었을 가능성이 높습니다. PIE 바이너리의 경우 readelf -l binary | grep LOAD로 첫 번째 LOAD 세그먼트의 VirtAddr을 확인하고, 심볼 주소에서 이를 빼서 파일 오프셋을 구합니다.

perf 이벤트

perf는 Linux 커널에 내장된 성능 분석 도구입니다. perf_event_open(2) 시스템 콜을 기반으로 하드웨어 PMU(Performance Monitoring Unit), 소프트웨어 이벤트, tracepoints, kprobes 등 다양한 이벤트 소스를 통합적으로 활용합니다.

perf_event_open 시스템 콜

#include <linux/perf_event.h>
#include <sys/syscall.h>

/* perf_event_attr: 이벤트 설정 구조체 */
struct perf_event_attr attr = {
    .type           = PERF_TYPE_HARDWARE,        /* 이벤트 유형 */
    .size           = sizeof(struct perf_event_attr),
    .config         = PERF_COUNT_HW_CPU_CYCLES,  /* CPU 사이클 카운트 */
    .disabled       = 1,                          /* 생성 시 비활성 */
    .exclude_kernel = 0,                          /* 커널 포함 */
    .exclude_hv     = 1,                          /* 하이퍼바이저 제외 */
    .sample_period  = 100000,                     /* 샘플 주기 */
    .sample_type    = PERF_SAMPLE_IP |
                      PERF_SAMPLE_TID |
                      PERF_SAMPLE_CALLCHAIN,
};

/* 시스템 콜로 perf 이벤트 생성 */
int fd = syscall(__NR_perf_event_open, &attr,
                 pid,     /* 대상 PID (-1 = 모든 프로세스) */
                 cpu,     /* 대상 CPU (-1 = 모든 CPU) */
                 group_fd,/* 그룹 리더 fd (-1 = 새 그룹) */
                 flags);  /* PERF_FLAG_FD_CLOEXEC 등 */

/* 이벤트 활성화/비활성화 */
ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);
ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);

/* 카운터 읽기 */
long long count;
read(fd, &count, sizeof(count));

이벤트 유형

PERF_TYPE_*설명예시
HARDWARE하드웨어 PMU 이벤트CPU cycles, instructions, cache misses, branch misses
SOFTWARE커널 소프트웨어 이벤트context switches, page faults, CPU migrations
TRACEPOINT커널 tracepointsched:sched_switch, block:block_rq_issue
HW_CACHE하드웨어 캐시 이벤트L1-dcache-load-misses, LLC-store-misses
RAWCPU 고유 raw PMU 이벤트Intel/AMD 매뉴얼의 이벤트 코드

perf 커맨드라인 도구

# === perf stat: 성능 카운터 요약 ===

# 기본 통계
perf stat ls
# Performance counter stats for 'ls':
#        1.23 msec  task-clock                #    0.723 CPUs utilized
#            3      context-switches           #    2.439 K/sec
#            0      cpu-migrations             #    0.000 /sec
#          112      page-faults                #   91.057 K/sec
#    3,456,789      cycles                     #    2.811 GHz
#    2,345,678      instructions               #    0.68  insn per cycle
#      456,789      branches                   #  371.373 M/sec
#       12,345      branch-misses              #    2.70% of all branches

# 특정 이벤트 지정
perf stat -e cycles,instructions,cache-misses,LLC-load-misses ./workload

# 시스템 전체 10초간 수집
perf stat -a -d sleep 10

# 특정 CPU에서 수집
perf stat -C 0,1,2,3 -e instructions sleep 5

# === perf record / perf report: 샘플링 프로파일링 ===

# CPU 사이클 기반 프로파일링 (콜스택 포함)
perf record -g ./workload

# 결과 분석
perf report
# Overhead  Command  Shared Object      Symbol
#  25.30%   workload [kernel.vmlinux]  [k] __schedule
#  12.45%   workload libc.so.6         [.] __memmove_avx_unaligned
#   8.90%   workload [kernel.vmlinux]  [k] copy_user_enhanced_fast_string

# 커널 전체 프로파일링 (30초)
perf record -a -g -- sleep 30

# 특정 이벤트로 프로파일링
perf record -e cache-misses -c 10000 -g ./workload

# tracepoint 이벤트 기록
perf record -e sched:sched_switch -a -- sleep 5

# === perf top: 실시간 프로파일링 ===

# 시스템 전체 실시간 모니터링 (top과 유사)
perf top
perf top -e cache-misses
perf top -p 1234  # 특정 프로세스
⚠️

perf 권한: /proc/sys/kernel/perf_event_paranoid 값에 따라 일반 사용자의 perf 사용이 제한됩니다. -1=무제한, 0=커널 트레이싱 허용, 1=커널 프로파일링 제한, 2=커널 트레이싱 제한(기본값). root 권한이나 CAP_PERFMON 케이퍼빌리티가 필요할 수 있습니다.

trace-cmd

trace-cmd는 ftrace의 tracefs 인터페이스를 추상화한 커맨드라인 프론트엔드입니다. tracefs 파일을 직접 조작하는 것보다 훨씬 편리하게 트레이싱을 수행할 수 있습니다. Steven Rostedt(ftrace 개발자)가 만들었습니다.

기본 사용법

# === trace-cmd record: 트레이싱 데이터 수집 ===

# 스케줄러 이벤트 기록
trace-cmd record -e sched_switch -e sched_wakeup sleep 5

# function_graph 트레이서로 특정 함수 추적
trace-cmd record -p function_graph -g do_sys_openat2 ls

# 여러 이벤트와 함수 필터 조합
trace-cmd record -p function \
    -l 'tcp_*' \
    -e net:net_dev_xmit \
    -e tcp:tcp_retransmit_skb \
    -- curl -s https://example.com > /dev/null

# 특정 PID만 트레이싱
trace-cmd record -P 1234 -e sched_switch

# 시스템 전체 이벤트 수집 (5초)
trace-cmd record -e all sleep 5

# === trace-cmd report: 결과 분석 ===

# 기본 출력
trace-cmd report
#      <idle>-0   [003] 12345.678: sched_switch: prev_comm=swapper ...
#        bash-1234 [001] 12345.679: sched_wakeup: comm=kworker ...

# 특정 이벤트만 필터링
trace-cmd report -F 'sched_switch'

# 특정 CPU만 출력
trace-cmd report --cpu 0

# 타임스탬프 범위 필터
trace-cmd report -t --ts-offset=12345.000

# 다른 파일 지정
trace-cmd report -i trace.dat

# === trace-cmd 기타 서브커맨드 ===

# 사용 가능한 이벤트 목록
trace-cmd list -e

# 사용 가능한 트레이서 목록
trace-cmd list -t

# 사용 가능한 필터 함수
trace-cmd list -f

# 실시간 스트리밍 (trace_pipe와 유사)
trace-cmd stream -e sched_switch

# ftrace 설정 초기화
trace-cmd reset

KernelShark

KernelShark는 trace-cmd의 데이터를 시각화하는 GUI 도구입니다. 타임라인 기반으로 이벤트, CPU 활용, 프로세스 스케줄링을 그래픽으로 분석할 수 있습니다.

# trace-cmd로 데이터 수집 후 KernelShark로 시각화
trace-cmd record -e sched_switch -e sched_wakeup -e irq -a sleep 10
kernelshark trace.dat

# KernelShark 설치 (Ubuntu/Debian)
apt install kernelshark

# KernelShark 주요 기능:
# - CPU별 타임라인 뷰
# - 프로세스별 색상 구분
# - 이벤트 필터링 (정규식, 이벤트 유형)
# - 두 이벤트 간 시간 차이 측정 (마커)
# - 이벤트 그래프, 히스토그램

trace-cmd: split, listen, 원격 트레이싱

trace-cmd는 record/report 외에도 데이터 분할, 원격 수집, 프로파일링 등 다양한 고급 기능을 제공합니다.

trace-cmd 워크플로우 전체 구조 수집 (Record) trace-cmd record trace-cmd start / stop trace-cmd stream trace.dat 바이너리 트레이스 파일 report (텍스트 분석) split (분할) hist (히스토그램) KernelShark GUI 원격 트레이싱 (listen/record) 타겟 서버 trace-cmd listen -p 12345 네트워크 수집 호스트 trace-cmd record -N host:12345 분석 워크스테이션 trace-cmd report / KernelShark trace-cmd profile 이벤트 기반 자동 통계 집계 스케줄링 지연, syscall 시간, IRQ 처리 시간 per-프로세스 요약 출력 trace-cmd profile -e sched sleep 10 trace-cmd stat 현재 ftrace 설정 상태 확인 활성 트레이서, 이벤트, 필터, 버퍼 크기 인스턴스 목록, 트리거 상태 디버깅 전 현재 상태 점검용
# === trace-cmd split: 대용량 트레이스 분할 ===

# 시간 기준 분할 (초 단위)
trace-cmd record -e sched -a sleep 60
trace-cmd split -s 10 trace.dat
# 결과: trace.dat.001, trace.dat.002, ... (10초씩)

# CPU 기준 분할
trace-cmd split -c trace.dat
# 결과: trace.dat.cpu0, trace.dat.cpu1, ...

# 이벤트 기준 분할
trace-cmd split -e sched_switch trace.dat
# sched_switch 이벤트만 포함된 파일 생성

# === trace-cmd listen: 원격 트레이싱 ===

# 타겟 서버에서 listen 모드 실행
trace-cmd listen -p 12345 -D       # 데몬 모드, 포트 12345

# 수집 호스트에서 원격 record
trace-cmd record -N target_host:12345 \
    -e sched_switch -e sched_wakeup \
    -p function_graph -g schedule \
    sleep 10
# 트레이스 데이터가 네트워크를 통해 수집 호스트로 전송됨

# === trace-cmd profile: 자동 통계 ===

trace-cmd profile -e sched -e irq -e block sleep 10
# 출력 예:
# task: bash-1234
#   Avg run time:     15.23 us
#   Max run time:    245.67 us
#   Avg sleep time: 5432.10 us
#   Total events:      1234
#   sched_switch:       567
#   IRQ handler:        89 (avg 2.3 us)

# === trace-cmd stat: 현재 상태 점검 ===

trace-cmd stat
# Tracer: function_graph
# Events:
#   sched:sched_switch (enabled)
#   sched:sched_wakeup (enabled)
# Filter: ext4_*
# Instances:
#   monitor (events: block:*)
# Buffer size: 4096 KB

# === trace-cmd extract: 현재 버퍼 추출 ===

# record 없이 현재 ring buffer를 파일로 저장
trace-cmd extract -o current_trace.dat
trace-cmd report -i current_trace.dat

# === trace-cmd show: 간단한 trace 파일 읽기 ===
trace-cmd show                      # cat trace와 동일
trace-cmd show -p                   # trace_pipe 스트리밍
trace-cmd show -B monitor           # 특정 인스턴스의 trace 읽기

KernelShark 시각화 분석 기법

KernelShark는 trace-cmd의 데이터를 그래픽으로 분석하는 GUI 도구입니다. 대량의 이벤트를 타임라인 기반으로 시각화하여, 텍스트 분석으로는 발견하기 어려운 패턴을 직관적으로 파악할 수 있습니다.

KernelShark 분석 워크플로우 ① 데이터 수집 trace-cmd record ... ② KernelShark 열기 kernelshark trace.dat ③ 필터 적용 프로세스/이벤트 선별 ④ 마커 측정 두 이벤트 간 시간차 KernelShark 주요 분석 기능 CPU 타임라인 뷰 CPU별 프로세스 실행 구간 색상으로 프로세스 구분 이벤트 리스트 뷰 타임스탬프 정렬 이벤트 목록 정규식 필터, 컬럼 정렬 듀얼 마커 측정 마커 A/B 배치 후 시간차 계산 레이턴시 구간 정밀 측정 플러그인: sched_switch 시각화 프로세스 상태 전이(R/S/D), wakeup 화살표 세션 저장/불러오기 필터, 줌, 마커 위치를 세션 파일로 관리
# KernelShark 분석 레시피

# 1. 스케줄링 분석용 데이터 수집
trace-cmd record -e sched -e irq -e workqueue -a sleep 10
kernelshark trace.dat

# 2. 특정 프로세스 중심 분석
trace-cmd record -e sched_switch -e sched_wakeup \
    -e sched_migrate_task \
    -P $(pgrep myapp) \
    sleep 30
kernelshark trace.dat
# → CPU 타임라인에서 myapp의 마이그레이션 패턴 확인
# → 듀얼 마커로 wakeup → on-cpu 지연 측정

# 3. KernelShark 2.x CLI 필터링
kernelshark trace.dat \
    --filter 'sched_switch && (next_comm == "myapp" || prev_comm == "myapp")'

# 4. 대용량 파일 처리: 시간 범위로 분할 후 분석
trace-cmd split -s 5 trace.dat
kernelshark trace.dat.001   # 첫 5초만 분석

perf ftrace

perf ftrace는 perf 도구 내에서 ftrace 기능을 사용할 수 있게 하는 서브커맨드입니다. tracefs를 직접 조작하지 않고 perf의 통합된 인터페이스로 ftrace를 사용할 수 있습니다.

# 기본 function trace (현재 실행중인 시스템)
perf ftrace -T schedule sleep 1

# function_graph tracer
perf ftrace --graph -G do_sys_openat2 ls

# 특정 함수만 필터
perf ftrace -T 'tcp_sendmsg' -T 'tcp_recvmsg' -- curl -s https://example.com

# 특정 함수 제외
perf ftrace -N 'rcu_*' -T 'schedule*' sleep 1

# 호출 깊이 제한
perf ftrace --graph --graph-depth 3 -G __x64_sys_read cat /dev/null

# perf ftrace latency: 함수 레이턴시 히스토그램
perf ftrace latency -T do_sys_openat2 -a sleep 5

perf ftrace latency 출력 예시:

Duration (us)Count그래프(상대 빈도)
0-10
1-215###
2-4124##########################
4-889###################
8-1623#####
16-323#
32-641

이벤트 트리거

이벤트 트리거(Event Triggers)는 tracepoint 이벤트가 발생할 때 자동으로 특정 동작을 수행하는 메커니즘입니다. 조건부 트레이싱, 스냅샷 캡처, 히스토그램 생성 등에 활용됩니다.

기본 트리거

T=/sys/kernel/tracing

# traceon/traceoff 트리거: 특정 이벤트 발생 시 트레이싱 on/off
# sched_switch에서 next_pid가 0이면 트레이싱 중지
echo 'traceoff if next_pid == 0' > $T/events/sched/sched_switch/trigger

# snapshot 트리거: 이벤트 발생 시 버퍼 스냅샷 저장
echo 'snapshot if prev_state == 2' > $T/events/sched/sched_switch/trigger
cat $T/snapshot  # 스냅샷 확인

# stacktrace 트리거: 이벤트 발생 시 스택 트레이스 기록
echo 'stacktrace' > $T/events/kmem/kmalloc/trigger

# 특정 횟수만 트리거 (count:N)
echo 'traceoff:1' > $T/events/sched/sched_switch/trigger  # 한 번만

# 트리거 확인
cat $T/events/sched/sched_switch/trigger

# 트리거 제거 (앞에 ! 추가)
echo '!traceoff' > $T/events/sched/sched_switch/trigger

hist 트리거 (히스토그램)

hist 트리거는 이벤트 데이터의 분포를 히스토그램으로 집계합니다. 커널 4.7에서 도입되었으며, 커널 내에서 실시간으로 데이터를 집계하므로 대량의 이벤트 데이터를 효율적으로 분석할 수 있습니다.

T=/sys/kernel/tracing

# 시스템 콜별 호출 빈도 히스토그램
echo 'hist:key=id:sort=hitcount:size=64' > \
    $T/events/raw_syscalls/sys_enter/trigger
sleep 5
cat $T/events/raw_syscalls/sys_enter/hist
# { id:   0 } hitcount:     12345
# { id:   1 } hitcount:      6789
# { id: 262 } hitcount:      3456

# 프로세스별 스케줄링 횟수
echo 'hist:key=next_comm:sort=hitcount.descending' > \
    $T/events/sched/sched_switch/trigger
sleep 10
cat $T/events/sched/sched_switch/hist

# 복합 키: CPU + 이전 프로세스별 컨텍스트 스위치
echo 'hist:keys=common_cpu,prev_comm:sort=hitcount' > \
    $T/events/sched/sched_switch/trigger

# 레이턴시 히스토그램 (값 필드 사용)
echo 'hist:key=common_pid.execname:val=bytes_req:sort=bytes_req.descending' > \
    $T/events/kmem/kmalloc/trigger

# 히스토그램 초기화
echo 'hist:key=id:clear' > $T/events/raw_syscalls/sys_enter/trigger

# 히스토그램 트리거 제거
echo '!hist:key=id' > $T/events/raw_syscalls/sys_enter/trigger

Synthetic Events

Synthetic events(합성 이벤트)는 여러 이벤트를 결합하여 새로운 이벤트를 생성합니다. 예를 들어, 함수 진입과 반환 이벤트를 결합하여 실행 시간을 계산할 수 있습니다.

T=/sys/kernel/tracing

# 합성 이벤트 정의: 스케줄링 레이턴시 (wakeup → switch 시간 차이)
echo 'wakeup_lat u64 lat; pid_t pid; char comm[16]' > $T/synthetic_events

# sched_wakeup에서 타임스탬프 저장
echo 'hist:keys=pid:ts0=common_timestamp.usecs' > \
    $T/events/sched/sched_wakeup/trigger

# sched_switch에서 레이턴시 계산 및 합성 이벤트 생성
echo 'hist:keys=next_pid:lat=common_timestamp.usecs-$ts0:onmatch(sched.sched_wakeup).trace(wakeup_lat,$lat,next_pid,next_comm)' > \
    $T/events/sched/sched_switch/trigger

# 합성 이벤트 히스토그램
echo 'hist:keys=comm:vals=lat:sort=lat.descending' > \
    $T/events/synthetic/wakeup_lat/trigger

# 결과 확인
sleep 10
cat $T/events/synthetic/wakeup_lat/hist
ℹ️

hist 트리거 활용: hist 트리거는 BPF 맵과 유사한 역할을 수행하지만, BPF 프로그램 작성 없이 tracefs 인터페이스만으로 사용할 수 있습니다. 대량의 이벤트를 집계할 때 링 버퍼 오버플로우 없이 효율적인 분석이 가능합니다. 커널 4.17부터 synthetic events, 4.20부터 onmax()/onchange() 핸들러가 지원됩니다.

bpftrace

bpftrace는 BPF(eBPF)를 기반으로 한 고수준 동적 트레이싱 언어입니다. AWK에서 영감을 받은 간결한 문법으로 복잡한 트레이싱 작업을 한 줄로 수행할 수 있습니다. 내부적으로 LLVM을 사용하여 BPF 바이트코드로 컴파일합니다.

프로브 유형

프로브설명예시
kprobe커널 함수 진입kprobe:vfs_read
kretprobe커널 함수 반환kretprobe:vfs_read
tracepoint커널 정적 tracepointtracepoint:sched:sched_switch
uprobe유저 함수 진입uprobe:/bin/bash:readline
uretprobe유저 함수 반환uretprobe:/lib/libc.so.6:malloc
software커널 소프트웨어 이벤트software:page-faults:100
hardware하드웨어 PMU 이벤트hardware:cache-misses:1000
profile타이머(Timer) 기반 샘플링profile:hz:99
interval주기적 출력interval:s:1
BEGIN/END시작/종료 시 실행BEGIN { ... }

원라이너 예제

# 시스템 콜별 호출 빈도 (Ctrl+C로 종료 시 출력)
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

# 프로세스별 read() 바이트 히스토그램
bpftrace -e 'kretprobe:vfs_read /retval > 0/ { @bytes[comm] = hist(retval); }'

# 스케줄링 레이턴시 (wakeup → on-cpu)
bpftrace -e '
tracepoint:sched:sched_wakeup {
    @qtime[args->pid] = nsecs;
}
tracepoint:sched:sched_switch /args->next_pid/ {
    $ns = @qtime[args->next_pid];
    if ($ns) {
        @usecs = hist((nsecs - $ns) / 1000);
    }
    delete(@qtime[args->next_pid]);
}'

# open() 시스템 콜로 열리는 파일 추적
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\\n", comm, str(args->filename)); }'

# 블록 I/O 레이턴시 히스토그램
bpftrace -e '
kprobe:blk_account_io_start { @start[arg0] = nsecs; }
kprobe:blk_account_io_done /@start[arg0]/ {
    @usecs = hist((nsecs - @start[arg0]) / 1000);
    delete(@start[arg0]);
}'

# TCP retransmit 추적
bpftrace -e 'kprobe:tcp_retransmit_skb {
    @[kstack] = count();
}'

# 1초마다 컨텍스트 스위치 수 출력
bpftrace -e 'tracepoint:sched:sched_switch { @++; }
interval:s:1 { print(@); clear(@); }'

# 커널 메모리 할당 (kmalloc) 크기 분포
bpftrace -e 'tracepoint:kmem:kmalloc { @size = hist(args->bytes_alloc); }'

# 특정 프로세스의 페이지 폴트 스택 트레이스
bpftrace -e 'software:page-faults:1 /comm == "myapp"/ { @[kstack, ustack] = count(); }'
💡

bpftrace vs tracefs: 간단한 이벤트 확인은 tracefs가 편리하지만, 조건부 필터링, 맵 기반 집계, 복잡한 히스토그램이 필요하면 bpftrace가 훨씬 강력합니다. bpftrace는 내부적으로 BPF 프로그램을 생성하므로 커널 내에서 안전하게 실행되며, BPF verifier가 무한 루프나 메모리 접근 오류를 방지합니다.

커널 새니타이저 연동

커널 새니타이저(KASAN, KCSAN, UBSAN, KMSAN)는 메모리 오류, 데이터 레이스, 정의되지 않은 동작, 초기화되지 않은 메모리 사용을 탐지합니다. 이 새니타이저들은 ftrace 이벤트를 통해 탐지 결과를 보고하며, 트레이싱 도구와 결합하여 더 상세한 분석이 가능합니다.

KASAN (Kernel Address Sanitizer)

# KASAN 활성화를 위한 커널 CONFIG
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y          # 소프트웨어 기반 (느리지만 범용)
# CONFIG_KASAN_SW_TAGS=y        # ARM64 MTE 기반
# CONFIG_KASAN_HW_TAGS=y        # ARM64 하드웨어 MTE 기반
CONFIG_KASAN_INLINE=y           # 인라인 계측 (더 빠름)
CONFIG_STACKTRACE=y

# KASAN 오류 보고 예시 (dmesg)
# BUG: KASAN: slab-out-of-bounds in kmalloc_oob_right+0x6c/0x94
# Write of size 1 at addr ffff8881234abcde by task test/1234
# Call Trace:
#  dump_stack_lvl+0x34/0x48
#  print_report+0x170/0x4a0
#  kasan_report+0xc0/0x100
#  kmalloc_oob_right+0x6c/0x94

# ftrace와 KASAN 결합: KASAN 보고 직전 함수 호출 추적
echo 'stacktrace' > /sys/kernel/tracing/events/kasan/kasan_report/trigger
echo 1 > /sys/kernel/tracing/events/kasan/kasan_report/enable

KCSAN (Kernel Concurrency Sanitizer)

# KCSAN: 데이터 레이스 탐지
CONFIG_KCSAN=y
CONFIG_KCSAN_STRICT=y           # 엄격 모드
CONFIG_KCSAN_REPORT_ONCE_IN_MS=3000
CONFIG_KCSAN_VERBOSE=y

# KCSAN 오류 보고 예시
# BUG: KCSAN: data-race in process_one_work / schedule_work
# write to 0xffff88810abcdef0 of 8 bytes by task 1234 on cpu 2:
#  process_one_work+0x1a3/0x5d0
# read to 0xffff88810abcdef0 of 8 bytes by task 5678 on cpu 5:
#  schedule_work+0x56/0x90

# perf로 KCSAN 이벤트 기록
perf record -e kcsan:kcsan_report -a -- sleep 60
perf report

UBSAN (Undefined Behavior Sanitizer)

# UBSAN: 정의되지 않은 동작 탐지
CONFIG_UBSAN=y
CONFIG_UBSAN_BOUNDS=y           # 배열 범위 검사
CONFIG_UBSAN_SHIFT=y            # 시프트 오버플로우
CONFIG_UBSAN_DIV_ZERO=y         # 0으로 나누기
CONFIG_UBSAN_SIGNED_OVERFLOW=y  # 부호 있는 정수 오버플로우

# UBSAN 오류 예시
# UBSAN: shift-out-of-bounds in drivers/xxx/yyy.c:123:4
# shift exponent 64 is too large for 64-bit type 'unsigned long'

KMSAN (Kernel Memory Sanitizer)

# KMSAN: 초기화되지 않은 메모리 사용 탐지 (커널 6.1+)
CONFIG_KMSAN=y

# KMSAN은 KASAN과 동시 사용 불가
# Clang 컴파일러 필수 (GCC 미지원)

# 트레이싱과 새니타이저 결합 활용
# bpftrace로 KASAN 오류 발생 시 컨텍스트 정보 수집
bpftrace -e 'kprobe:kasan_report {
    printf("KASAN report by %s (pid=%d) on CPU %d\\n",
           comm, pid, cpu);
    print(kstack);
}'
⚠️

새니타이저 성능 오버헤드: KASAN은 약 1.5~3배, KCSAN은 약 2~5배, KMSAN은 약 3~5배의 성능 저하를 유발합니다. 프로덕션 환경에서는 사용하지 마십시오. 개발 및 테스트 환경에서만 활성화하여 잠재적 버그를 조기에 발견하십시오. CI/CD 파이프라인(Pipeline)에서 새니타이저 활성 커널로 테스트를 자동화하는 것이 권장됩니다.

NOP 패칭 상세 흐름

ftrace의 핵심 기술은 동적 코드 패칭(dynamic code patching)입니다. 커널 빌드 시 -pg 또는 -fpatchable-function-entry 옵션으로 삽입된 계측 사이트가, 부팅 과정(Boot Process)에서 NOP(No Operation) 명령어로 대체됩니다. 트레이싱이 활성화되면 NOP가 다시 CALL ftrace_caller로 패칭되어 콜백이 호출됩니다. 이 메커니즘 덕분에 트레이싱 비활성 상태에서는 거의 제로 오버헤드를 달성합니다.

x86_64에서 mcount 기반 컴파일러는 함수 프롤로그에 5바이트 call __fentry__를 삽입합니다. GCC 4.6 이후 도입된 -mfentry 옵션은 함수 시작 지점(프레임 포인터 설정 이전)에 호출을 삽입하여, 인자 레지스터가 보존된 상태에서 콜백을 실행할 수 있게 합니다. ARM64에서는 -fpatchable-function-entry=N으로 함수 시작점에 N개의 NOP을 예약합니다.

① 컴파일 시 call __fentry__ (5B) mcount 또는 -mfentry ② 부팅 NOP 변환 ftrace_init → NOP (0F 1F 44 00 00) available_filter_functions 등록 ③ 트레이싱 활성화 NOP → CALL ftrace_caller text_poke_bp() 안전 패칭 text_poke_bp() 3단계 안전 패칭 (x86) 1. 첫 바이트 → INT3 (0xCC) 2. 나머지 바이트 패칭 3. INT3 → CALL opcode (0xE8) ARM64: BTI + patchable-function-entry BTI C 명령어 보존, NOP → BL ftrace_caller aarch64_insn_patch_text_nosync() 사용 RISC-V: AUIPC+JALR → NOP+NOP 8바이트 계측 사이트 patch_text_nosync() atomic 패칭

x86 fentry vs mcount

fentry는 함수의 첫 번째 명령어로 삽입되며, 프레임 포인터 설정 이전에 호출됩니다. 이는 인자 레지스터(RDI, RSI, RDX 등)가 손상되지 않은 상태에서 콜백을 실행할 수 있게 해줍니다. 반면 mcount는 프롤로그 이후에 호출되므로 스택 프레임(Stack Frame) 접근이 필요합니다.

항목mcountfentry (-mfentry)
삽입 위치프롤로그 이후 (push %rbp 뒤)함수 첫 바이트
인자 접근스택에서 복원 필요레지스터에서 직접 접근
live-patching 호환제한적완전 지원
return trampoline스택 프레임 의존독립적 동작
GCC 최소 버전모든 버전GCC 4.6+

부팅 시 NOP 패칭 타이밍

커널 부팅 시 ftrace_init()은 링커(Linker)가 수집한 __mcount_loc 섹션을 순회하며 모든 call mcount/call __fentry__ 사이트를 NOP으로 변환합니다. 이 작업은 SMP 초기화 이전(단일 CPU 상태)에 수행되므로 동기화 걱정 없이 안전합니다.

/* kernel/trace/ftrace.c — 부팅 시 NOP 변환 흐름 (단순화) */
void __init ftrace_init(void)
{
    unsigned long *p;
    unsigned long count;

    /* __mcount_loc 섹션의 시작/끝 주소 */
    extern unsigned long __start_mcount_loc[];
    extern unsigned long __stop_mcount_loc[];

    count = __stop_mcount_loc - __start_mcount_loc;
    pr_info("ftrace: allocating %ld entries in %ld pages\n",
            count, DIV_ROUND_UP(count, ENTRIES_PER_PAGE));

    /* 각 계측 사이트를 순회하며 NOP으로 변환 */
    for (p = __start_mcount_loc; p < __stop_mcount_loc; p++)
        ftrace_nop_initialize(*p);  /* call → 5-byte NOP */

    /* available_filter_functions에 등록 */
    ftrace_process_locs("core", __start_mcount_loc, __stop_mcount_loc);
}
💡

available_filter_functions 확인: wc -l /sys/kernel/tracing/available_filter_functions로 패칭 가능한 함수 수를 확인할 수 있습니다. 일반적인 커널에서 5만~8만 개의 함수가 등록됩니다. notrace 어트리뷰트가 붙은 함수와 __init 섹션 함수는 제외됩니다.

Ring Buffer 구조

ftrace의 트레이스 데이터는 per-CPU ring buffer에 저장됩니다. 각 CPU는 독립적인 버퍼를 가지며, 락 없이(lockless) 기록과 읽기가 동시에 진행됩니다. 이 설계는 트레이싱의 오버헤드를 최소화하면서 데이터 손실을 제어할 수 있게 합니다.

ring buffer는 연결 리스트(Linked List)로 구성된 페이지 단위 버퍼입니다. 각 페이지는 4KB(기본값)이며, writer는 현재 페이지의 commit 포인터를 전진시키며 이벤트를 기록합니다. 페이지가 가득 차면 다음 페이지로 이동합니다. reader는 별도의 reader 페이지를 사용하여 writer와 충돌하지 않습니다.

Per-CPU Ring Buffer 페이지 구조 Page 0 commit (완료) Page 1 commit (완료) Page 2 (write) write 포인터 ▶ Page 3 (빈 페이지) 순환 Reader Page (독립) reader는 완료 페이지와 swap하여 읽음 swap write: 현재 기록 위치 commit: 완료된 마지막 위치 overwrite: 가장 오래된 데이터 덮어쓰기 discard: 새 데이터 버림 entries: 저장된 이벤트 수 overrun: 덮어쓴 이벤트 수

overwrite vs discard 모드

ring buffer는 두 가지 가득 참(full) 정책을 제공합니다. 기본값은 overwrite 모드로, 가장 오래된 데이터를 덮어씁니다. discard 모드에서는 새 데이터를 버립니다.

모드설정 명령동작적합한 상황
overwrite (기본)echo 1 > overwrite오래된 이벤트 덮어쓰기최신 이벤트가 중요한 경우 (장애 직전 분석)
discardecho 0 > overwrite새 이벤트 버림초기 이벤트가 중요한 경우 (부팅 분석)
# 버퍼 크기 설정 (per-CPU, 단위: KB)
T=/sys/kernel/tracing
echo 16384 > $T/buffer_size_kb       # CPU당 16MB

# CPU별 개별 설정
echo 32768 > $T/per_cpu/cpu0/buffer_size_kb  # CPU 0만 32MB

# 현재 버퍼 상태 확인
cat $T/per_cpu/cpu0/stats
# entries: 42317          ← 저장된 이벤트 수
# overrun: 0              ← 덮어쓴 수 (0이면 손실 없음)
# commit overrun: 0       ← commit 경합 발생 수
# bytes: 1695432          ← 사용 중인 바이트

# snapshot 활용: 현재 버퍼를 즉시 복사
echo 1 > $T/snapshot              # 현재 트레이스를 snapshot으로 복사
cat $T/snapshot                      # snapshot 읽기 (원본은 계속 기록)
⚠️

버퍼 크기와 메모리: buffer_size_kb는 CPU당 설정입니다. 128-CPU 서버에서 16MB로 설정하면 총 2GB의 커널 메모리를 소비합니다. 프로덕션에서는 per_cpu/cpuN/statsoverrun 값을 모니터링하여 손실이 발생하는 최소 버퍼 크기를 찾으십시오.

Ring Buffer 이벤트 포맷

ring buffer에 저장되는 각 이벤트는 공통 헤더와 이벤트별 데이터로 구성됩니다. 이 포맷을 이해하면 trace_pipe_raw의 바이너리 데이터를 직접 해석할 수 있습니다.

Ring Buffer 이벤트 내부 포맷 버퍼 페이지 (4KB 기본) 페이지 헤더(timestamp, commit, data_offset) + 이벤트 배열 이벤트 공통 헤더 (ring_buffer_event) type_len (5비트) time_delta (27비트) array[0] (32비트) array[1..N] (이벤트 데이터) 트레이스 이벤트 공통 필드 type (u16) flags (u8) preempt (u8) pid (i32) 이벤트별 고유 필드 (TP_STRUCT__entry) flags 필드 비트 구조 IRQS_OFF (비트 0) IRQS_NOSUPPORT (비트 1) NEED_RESCHED (비트 2) HARDIRQ/SOFTIRQ (비트 3-4) 이 flags는 trace 출력의 ||||| 컬럼에 해당: d=IRQ off, N=need_resched, H=hardirq, s=softirq
/* kernel/trace/ring_buffer.c — 이벤트 기록 흐름 (단순화) */

/* 1. 이벤트 공간 예약 */
struct ring_buffer_event *
ring_buffer_lock_reserve(struct trace_buffer *buffer, unsigned long length)
{
    struct ring_buffer_per_cpu *cpu_buffer;
    struct ring_buffer_event *event;

    cpu_buffer = buffer->buffers[smp_processor_id()];

    /* preempt_disable()로 CPU 고정 — 같은 CPU의 이벤트 순서 보장 */
    preempt_disable_notrace();

    /* tail_page의 현재 위치에서 length만큼 예약 */
    event = rb_reserve_next_event(cpu_buffer, length);
    if (!event) {
        /* 공간 부족: overwrite 모드면 head 전진, discard 모드면 NULL 반환 */
        preempt_enable_notrace();
        return NULL;
    }
    return event;
}

/* 2. 이벤트 커밋 (쓰기 완료) */
int ring_buffer_unlock_commit(struct trace_buffer *buffer)
{
    struct ring_buffer_per_cpu *cpu_buffer;

    cpu_buffer = buffer->buffers[smp_processor_id()];

    /* commit 포인터 전진 — 이 시점부터 reader가 읽을 수 있음 */
    rb_commit(cpu_buffer);

    preempt_enable_notrace();
    return 0;
}

/* lockless 설계의 핵심:
 * - writer는 tail_page의 write 포인터를 원자적으로 전진
 * - reader는 head_page와 reader_page를 swap하여 읽기
 * - writer와 reader가 다른 페이지에서 작업하므로 충돌 없음
 * - 같은 CPU의 중첩 이벤트는 NMI만 가능 (cmpxchg로 처리)
 */

Ring Buffer 튜닝 가이드

ring buffer 설정은 트레이싱의 신뢰성과 성능에 직접적인 영향을 미칩니다. 상황별 최적 설정을 정리합니다.

상황buffer_size_kboverwritetrace_clock비고
짧은 디버깅 (5초)4096 (4MB)overwritelocal기본 설정으로 충분
장시간 모니터링1024 (1MB)overwritemono최소 버퍼 + trace_pipe로 스트리밍
고빈도 이벤트 수집65536 (64MB)discardlocal초기 이벤트 보존, 버퍼 손실 방지
부팅 분석커널 파라미터discardboottrace_buf_size=20M
멀티 CPU 상관 분석8192 (8MB)overwritex86-tsc/global이벤트 순서 보장 필요
실시간 시스템2048 (2MB)overwritemono_rawNTP 영향 배제, 최소 오버헤드
# Ring buffer 상태 진단 스크립트
T=/sys/kernel/tracing

echo "=== Ring Buffer 상태 ==="
for cpu in $T/per_cpu/cpu*; do
    name=$(basename $cpu)
    entries=$(grep 'entries:' $cpu/stats | awk '{print $2}')
    overrun=$(grep 'overrun:' $cpu/stats | awk '{print $2}')
    commit_overrun=$(grep 'commit overrun:' $cpu/stats | awk '{print $3}')
    bytes=$(grep 'bytes:' $cpu/stats | awk '{print $2}')
    size=$(cat $cpu/buffer_size_kb)

    status="OK"
    [ "$overrun" -gt 0 ] && status="WARNING: 데이터 손실"
    [ "$commit_overrun" -gt 0 ] && status="WARNING: 커밋 경합"

    echo "$name: entries=$entries overrun=$overrun bytes=$bytes size=${size}KB [$status]"
done

# 동적 버퍼 크기 조정: overrun이 발생하면 2배로 증가
adjust_buffer() {
    local current=$(cat $T/buffer_size_kb)
    local total_overrun=0
    for cpu in $T/per_cpu/cpu*/stats; do
        overrun=$(grep 'overrun:' $cpu | awk '{print $2}')
        total_overrun=$(( total_overrun + overrun ))
    done

    if [ $total_overrun -gt 0 ]; then
        local new_size=$(( current * 2 ))
        echo $new_size > $T/buffer_size_kb
        echo "버퍼 증가: ${current}KB → ${new_size}KB (overrun=$total_overrun)"
    else
        echo "버퍼 상태 양호: ${current}KB (overrun=0)"
    fi
}

function_graph 내부

function_graph 트레이서는 함수의 진입(entry)과 퇴장(return)을 모두 기록하여 호출 트리와 실행 시간을 시각화합니다. 이를 위해 함수 반환 주소를 return_to_handler 트램폴린으로 교체하는 기법을 사용합니다.

함수 진입 시 ftrace는 스택의 반환 주소를 return_to_handler로 교체하고, 원래 반환 주소를 per-task ret_stack 배열에 저장합니다. 함수가 반환되면 return_to_handler가 실행되어 반환 시간을 기록한 후, 원래 반환 주소로 점프합니다.

① 함수 진입 ftrace_graph_caller 호출 진입 시간 t₀ 기록 ② 반환 주소 교체 원본 → ret_stack[depth] 저장 스택 → return_to_handler ③ 함수 실행 원래 함수 본문 중첩 호출 가능 ④ return_to_handler 반환 시간 t₁ 기록, 지속시간 = t₁ - t₀ 원래 반환 주소로 점프 per-task ret_stack (FTRACE_RETFUNC_DEPTH 기본값 50) depth 0 caller_a() depth 1 caller_b() depth 2 caller_c() depth 3 (빈 슬롯) ... depth N

max_graph_depth와 필터링

max_graph_depth는 function_graph 트레이서가 추적할 최대 중첩 깊이를 제한합니다. 깊이를 제한하면 출력 노이즈가 줄어들고 오버헤드가 감소합니다.

T=/sys/kernel/tracing

# function_graph 설정
echo function_graph > $T/current_tracer
echo 3 > $T/max_graph_depth            # 3단계 깊이까지만

# 특정 함수의 호출 그래프만 추적
echo vfs_read > $T/set_graph_function
echo vfs_write >> $T/set_graph_function

# 특정 함수를 그래프에서 제외
echo '!mutex_lock' > $T/set_graph_notrace

# funcgraph 출력 열 제어
echo funcgraph-overhead > $T/trace_options   # 지연 시간 표시 (+, !, # 기호)
echo funcgraph-duration > $T/trace_options   # 실행 시간(μs) 표시
echo funcgraph-proc > $T/trace_options       # 프로세스명 표시
echo funcgraph-abstime > $T/trace_options    # 절대 타임스탬프

# 출력 예시:
#  3)               |  vfs_read() {
#  3)               |    rw_verify_area() {
#  3)   0.215 us    |      security_file_permission();
#  3)   0.602 us    |    }
#  3)               |    __vfs_read() {
#  3) ! 128.347 us  |      ext4_file_read_iter();
#  3) ! 129.012 us  |    }
#  3) ! 130.518 us  |  }
ℹ️

오버헤드 마커 기호: funcgraph-overhead가 활성화되면 실행 시간에 따라 다음 기호가 붙습니다: +(10μs~100μs), !(100μs~1s), #(1s 이상). 이 기호를 이용하면 긴 지연 구간을 빠르게 찾을 수 있습니다.

tracefs 파일 계층

tracefs는 /sys/kernel/tracing/(또는 레거시 /sys/kernel/debug/tracing/)에 마운트(Mount)되는 가상 파일시스템(VFS)으로, ftrace의 모든 제어와 데이터 접근이 이 디렉토리를 통해 이루어집니다. 각 파일의 역할을 이해하면 셸 스크립트만으로도 강력한 트레이싱을 구성할 수 있습니다.

/sys/kernel/tracing/ current_tracer활성 트레이서 (nop/function/function_graph 등) available_tracers사용 가능한 트레이서 목록 trace트레이스 출력 (읽으면 소비되지 않음) trace_pipe스트리밍 출력 (읽으면 소비됨) tracing_on0/1 — 트레이싱 활성화 스위치 set_ftrace_filter추적할 함수 화이트리스트 set_ftrace_notrace추적 제외 함수 블랙리스트 set_ftrace_pid특정 PID만 추적 buffer_size_kbCPU당 ring buffer 크기 trace_clock타임스탬프 클록 소스 선택 events/이벤트 카테고리 (sched/, block/, net/, irq/ 등) events/sched/sched_switch/enable개별 이벤트 on/off events/sched/sched_switch/filter이벤트 필터 (예: prev_pid==1234) events/sched/sched_switch/trigger트리거 액션 (hist, stacktrace 등) per_cpu/CPU별 독립 버퍼 (trace, stats, buffer_size_kb) instances/독립 트레이싱 인스턴스 (mkdir로 생성) available_filter_functions패칭 가능한 전체 함수 목록 kprobe_events동적 kprobe 이벤트 등록 uprobe_events동적 uprobe 이벤트 등록 snapshot현재 트레이스 스냅샷 (쓰기=저장, 읽기=출력)

events/ 계층 구조

이벤트 디렉토리는 서브시스템/이벤트명/ 형태의 2단계 구조입니다. 각 이벤트 디렉토리에는 enable, filter, format, trigger, id 파일이 있습니다.

# 사용 가능한 이벤트 서브시스템 확인
ls /sys/kernel/tracing/events/
# block  ext4  filemap  irq  kmem  mmc  net  power  sched  signal  skb  tcp  timer  workqueue ...

# 이벤트 필드(포맷) 확인 — 필터와 hist에 사용할 필드명
cat /sys/kernel/tracing/events/sched/sched_switch/format
# field:char prev_comm[16]; offset:8; size:16; signed:0;
# field:pid_t prev_pid;     offset:24; size:4; signed:1;
# field:int prev_prio;      offset:28; size:4; signed:1;
# ...

# 서브시스템 전체 활성화
echo 1 > /sys/kernel/tracing/events/sched/enable

# 개별 이벤트 + 필터
echo 1 > /sys/kernel/tracing/events/sched/sched_switch/enable
echo 'prev_pid == 1234 || next_pid == 1234' > \
    /sys/kernel/tracing/events/sched/sched_switch/filter

TRACE_EVENT 매크로 확장

TRACE_EVENT() 매크로는 커널의 정적 tracepoint를 정의하는 핵심 기반입니다. 하나의 매크로 호출이 컴파일 시점에 수십 개의 구조체, 콜백, 등록 코드로 확장되어, 트레이스 이벤트의 등록·활성화·데이터 기록·출력 포맷을 모두 자동 생성합니다.

TRACE_EVENT(name, TP_PROTO, TP_ARGS, TP_STRUCT__entry, TP_fast_assign, TP_printk) ① trace_event_class 생성 ② trace_event_call 등록 컴파일 시 생성되는 코드 struct trace_event_raw_name trace_event_reg() / unreg() ring buffer 기록 함수 런타임 이벤트 기록 흐름 tracepoint 호출 trace_sched_switch() static_key 체크 비활성 → NOP 분기 TP_fast_assign 필드 값 기록 ring buffer 저장 reserve → commit DECLARE_EVENT_CLASS / DEFINE_EVENT 패턴 DECLARE_EVENT_CLASS(sched_switch, TP_PROTO, TP_STRUCT__entry, TP_fast_assign, TP_printk) → 클래스 1개 정의 (코드 공유) DEFINE_EVENT(sched_switch, sched_switch, ...) DEFINE_EVENT(sched_switch, sched_switch_v2, ...)

다음은 TRACE_EVENT 매크로의 실제 사용 예시입니다. include/trace/events/ 디렉토리의 헤더 파일에서 이벤트를 정의하고, 커널 코드에서 trace_*() 함수로 호출합니다.

/* include/trace/events/sched.h — sched_switch 이벤트 정의 (단순화) */
TRACE_EVENT(sched_switch,
    TP_PROTO(bool preempt,
             struct task_struct *prev,
             struct task_struct *next),

    TP_ARGS(preempt, prev, next),

    TP_STRUCT__entry(
        __array(char, prev_comm, TASK_COMM_LEN)
        __field(pid_t, prev_pid)
        __field(int, prev_prio)
        __field(long, prev_state)
        __array(char, next_comm, TASK_COMM_LEN)
        __field(pid_t, next_pid)
        __field(int, next_prio)
    ),

    TP_fast_assign(
        memcpy(__entry->prev_comm, prev->comm, TASK_COMM_LEN);
        __entry->prev_pid   = prev->pid;
        __entry->prev_prio  = prev->prio;
        __entry->prev_state = __trace_sched_switch_state(preempt, prev);
        memcpy(__entry->next_comm, next->comm, TASK_COMM_LEN);
        __entry->next_pid   = next->pid;
        __entry->next_prio  = next->prio;
    ),

    TP_printk("prev_comm=%s prev_pid=%d ... ==> next_comm=%s next_pid=%d",
        __entry->prev_comm, __entry->prev_pid,
        __entry->next_comm, __entry->next_pid)
);
ℹ️

DECLARE_EVENT_CLASS 장점: 동일한 데이터 구조를 공유하는 여러 이벤트가 있을 때, DECLARE_EVENT_CLASS로 클래스를 한 번 정의하고 DEFINE_EVENT로 개별 이벤트를 생성합니다. 이 패턴은 커널 코드 크기를 크게 줄입니다. 예를 들어 sched 서브시스템에서는 하나의 클래스에서 여러 이벤트를 파생합니다.

커스텀 TRACE_EVENT 작성 가이드

커널 모듈이나 서브시스템에 새로운 tracepoint를 추가하려면 TRACE_EVENT() 매크로를 사용합니다. 다음은 커스텀 이벤트를 처음부터 작성하는 전체 과정입니다.

커스텀 TRACE_EVENT 작성 3단계 ① 이벤트 헤더 정의 include/trace/events/mysubsys.h TRACE_EVENT(mysubsys_event, TP_PROTO, TP_ARGS, TP_STRUCT__entry, TP_fast_assign, TP_printk) ② 소스에서 호출 drivers/mydriver/core.c #define CREATE_TRACE_POINTS #include <trace/events/mysubsys.h> trace_mysubsys_event(arg1, ...); ③ 사용 events/mysubsys/mysubsys_event/ enable, filter, format, trigger cat format → 필드 확인 perf, trace-cmd, bpftrace 호환 TP_STRUCT__entry 필드 타입 __field(type, name) 스칼라 값: int, u64, ... __array(type, name, len) 고정 배열: char[16] __string(name, src) 가변 문자열 (동적 크기) __dynamic_array() 가변 배열 (런타임 크기) __string_len(name, src, len) 길이 제한 문자열 (NULL 없을 수 있음, 커널 5.10+) __bitmask(name, nr_bits) 비트마스크 필드 (CPU 마스크 등, 커널 4.4+)
/* ===== 커스텀 TRACE_EVENT 완전한 예시 ===== */

/* 파일: include/trace/events/mydriver.h */
#undef  TRACE_SYSTEM
#define TRACE_SYSTEM mydriver

#if !defined(_TRACE_MYDRIVER_H) || defined(TRACE_HEADER_MULTI_READ)
#define _TRACE_MYDRIVER_H

#include <linux/tracepoint.h>

/* 이벤트 정의: DMA 전송 완료 추적 */
TRACE_EVENT(mydriver_dma_complete,
    /* TP_PROTO: 함수 프로토타입 — tracepoint 호출 시 전달할 인자 */
    TP_PROTO(struct device *dev, dma_addr_t addr,
             size_t len, int direction, s64 elapsed_ns),

    /* TP_ARGS: 인자 이름 (TP_PROTO와 일치) */
    TP_ARGS(dev, addr, len, direction, elapsed_ns),

    /* TP_STRUCT__entry: ring buffer에 저장할 필드 구조 */
    TP_STRUCT__entry(
        __string(devname, dev_name(dev))
        __field(u64, dma_addr)
        __field(size_t, length)
        __field(int, dir)
        __field(s64, elapsed_ns)
    ),

    /* TP_fast_assign: 인자에서 필드로 값 복사 (빠른 경로) */
    TP_fast_assign(
        __assign_str(devname, dev_name(dev));
        __entry->dma_addr   = addr;
        __entry->length     = len;
        __entry->dir        = direction;
        __entry->elapsed_ns = elapsed_ns;
    ),

    /* TP_printk: cat trace 시 출력 포맷 */
    TP_printk("dev=%s addr=0x%llx len=%zu dir=%s elapsed=%lldns",
        __get_str(devname),
        __entry->dma_addr,
        __entry->length,
        __entry->dir == 0 ? "TO_DEVICE" : "FROM_DEVICE",
        __entry->elapsed_ns)
);

#endif /* _TRACE_MYDRIVER_H */

/* 필수: 헤더 경로 지정 */
#include <trace/define_trace.h>
/* 파일: drivers/mydriver/core.c — tracepoint 호출부 */
#define CREATE_TRACE_POINTS        /* 이 파일에서만 정의 */
#include <trace/events/mydriver.h>

static void mydriver_dma_done_handler(struct device *dev,
                                      struct dma_transfer *xfer)
{
    s64 elapsed = ktime_to_ns(ktime_sub(ktime_get(), xfer->start_time));

    /* tracepoint 호출 — 비활성 시 static_key로 건너뜀 (오버헤드 ~0) */
    trace_mydriver_dma_complete(dev, xfer->dma_addr,
                                xfer->length, xfer->direction, elapsed);

    /* 후속 처리... */
}
# 커스텀 이벤트 사용 예시
T=/sys/kernel/tracing

# 이벤트 필드 확인
cat $T/events/mydriver/mydriver_dma_complete/format
# field:__data_loc char[] devname; offset:8; size:4; signed:0;
# field:u64 dma_addr;               offset:12; size:8; signed:0;
# field:size_t length;              offset:20; size:8; signed:0;
# ...

# 활성화 및 필터링
echo 1 > $T/events/mydriver/mydriver_dma_complete/enable
echo 'elapsed_ns > 1000000' > $T/events/mydriver/mydriver_dma_complete/filter
# 1ms 이상 걸린 DMA 전송만 기록

# hist 트리거로 디바이스별 DMA 통계
echo 'hist:keys=devname:vals=elapsed_ns:sort=elapsed_ns.descending' > \
    $T/events/mydriver/mydriver_dma_complete/trigger

# bpftrace에서 사용
bpftrace -e 'tracepoint:mydriver:mydriver_dma_complete /args->elapsed_ns > 1000000/ {
    printf("slow DMA: %s %lld ns\n", str(args->devname), args->elapsed_ns);
}'
⚠️

TRACE_EVENT 작성 시 주의사항:

  • TP_fast_assign에서 슬립 가능 함수 호출 금지 — 인터럽트 컨텍스트에서 호출될 수 있음
  • TP_printk는 출력 전용이며 실제 ring buffer 기록과 무관 — 성능에 영향 없음
  • __string 필드의 원본 문자열은 TP_fast_assign 이후 해제될 수 있음 — 반드시 복사
  • 하나의 .c 파일에서만 #define CREATE_TRACE_POINTS 사용 — 다른 파일은 헤더만 include
  • TRACE_SYSTEM 이름은 알파벳과 밑줄만 사용 (하이픈 불가)

hist 트리거 / synthetic 이벤트

hist 트리거는 커널 내부에서 이벤트 데이터를 실시간 집계(aggregation)하는 기능입니다. 유저 공간으로 데이터를 전송하지 않고 커널에서 직접 히스토그램을 생성하므로, 대량 이벤트를 효율적으로 분석할 수 있습니다. synthetic 이벤트와 결합하면 두 이벤트 사이의 레이턴시를 커널 내부에서 계산할 수 있습니다.

① 이벤트 발생 sched_waking ② hist 트리거 keys=pid, vals=hitcount ③ 집계 업데이트 hitcount++, 합계 갱신 ④ 결과 읽기 cat hist synthetic 이벤트로 레이턴시 측정 sched_waking onmatch → 타임스탬프 저장 sched_switch onmatch → synthetic 생성 wakeup_latency (synth) lat = t_switch - t_waking hist 집계 keys=lat:sort=lat hist 트리거 액션 유형 onmatch 조건 충족 시 synthetic 생성 onmax 최댓값 갱신 시 동작 onchange 값 변경 시 동작 snapshot() 조건부 스냅샷 저장
T=/sys/kernel/tracing

# 1. 기본 hist 트리거: PID별 스케줄 이벤트 횟수
echo 'hist:keys=next_pid:vals=hitcount:sort=hitcount.descending' > \
    $T/events/sched/sched_switch/trigger
cat $T/events/sched/sched_switch/hist

# 2. synthetic 이벤트로 wakeup 레이턴시 측정
# 2a. synthetic 이벤트 정의
echo 'wakeup_latency u64 lat; pid_t pid; char comm[16]' > \
    $T/synthetic_events

# 2b. sched_waking에서 타임스탬프 저장
echo 'hist:keys=pid:ts0=common_timestamp.usecs' > \
    $T/events/sched/sched_waking/trigger

# 2c. sched_switch에서 레이턴시 계산 후 synthetic 이벤트 발생
echo 'hist:keys=next_pid:lat=common_timestamp.usecs-$ts0:onmatch(sched.sched_waking).wakeup_latency($lat,next_pid,next_comm)' > \
    $T/events/sched/sched_switch/trigger

# 2d. synthetic 이벤트의 히스토그램
echo 'hist:keys=pid,comm:vals=lat:sort=lat.descending' > \
    $T/events/synthetic/wakeup_latency/trigger
cat $T/events/synthetic/wakeup_latency/hist

# 3. onmax 액션: 최대 레이턴시 갱신 시 스냅샷
echo 'hist:keys=next_pid:lat=common_timestamp.usecs-$ts0:onmax($lat).snapshot()' > \
    $T/events/sched/sched_switch/trigger

# 정리
echo '!hist:keys=next_pid' > $T/events/sched/sched_switch/trigger
💡

hist 트리거 vs BPF: hist 트리거는 BPF 없이 순수 tracefs 인터페이스만으로 커널 내 집계를 수행합니다. BPF보다 기능은 제한적이지만, BPF 도구 설치 없이 표준 커널만으로 사용 가능하므로 임베디드 환경이나 프로덕션 서버에서 유용합니다. 더 복잡한 집계가 필요하면 BPF/XDP 문서를 참고하세요.

히스토그램 트리거 고급 기법

hist 트리거는 단순 카운팅을 넘어 복잡한 통계 집계, 변수(Variables) 기반 조건 분석, 다중 히스토그램 연결 등의 고급 기능을 제공합니다.

hist 트리거 고급 기능 체계 변수(Variables) 시스템 hist 트리거 간 데이터 공유 named variable $ts0, $lat, $myvar common_timestamp .usecs / .nsecs 액션(Actions) 핸들러 onmatch() onmax() onchange() .trace() .snapshot() .save() 다중 이벤트 연쇄(Chaining) 패턴 이벤트 A $ts0 = timestamp 이벤트 B $lat = ts - $ts0 synthetic 이벤트 onmatch → trace() 최종 hist 통계 집계 onmax 활용: 최악 케이스 포착 값이 기존 최대를 초과할 때만 동작 onmax($lat).snapshot() → 최악 시점 버퍼 저장 onmax($lat).save(comm,pid) → 해당 정보 보존 onchange 활용: 상태 전이 감지 값이 변경될 때만 동작 onchange($state).trace(state_change,...) 상태 머신 전이 추적에 유용
T=/sys/kernel/tracing

# ===== 고급 hist 트리거 예시 모음 =====

# 1. 멀티 키 히스토그램: CPU + 프로세스별 I/O 크기 분포
echo 'hist:keys=common_cpu,common_pid.execname:vals=bytes_req:sort=bytes_req.descending' > \
    $T/events/kmem/kmalloc/trigger
sleep 10
cat $T/events/kmem/kmalloc/hist

# 2. onmax로 최대 스케줄링 지연 시 스냅샷 자동 저장
# 2a. synthetic 이벤트 정의
echo 'sched_lat u64 lat; pid_t pid; char comm[16]' > $T/synthetic_events

# 2b. wakeup에서 타임스탬프 저장
echo 'hist:keys=pid:ts0=common_timestamp.usecs' > \
    $T/events/sched/sched_waking/trigger

# 2c. switch에서 레이턴시 계산 + onmax로 최대 지연 시 스냅샷
echo 'hist:keys=next_pid:lat=common_timestamp.usecs-$ts0:onmax($lat).snapshot():onmatch(sched.sched_waking).sched_lat($lat,next_pid,next_comm)' > \
    $T/events/sched/sched_switch/trigger

# 최악 스냅샷 확인
cat $T/snapshot

# 3. onchange로 CPU 마이그레이션 감지
echo 'cpu_change u64 ts; pid_t pid; char comm[16]; int old_cpu; int new_cpu' > \
    $T/synthetic_events

echo 'hist:keys=next_pid:cpu=common_cpu:onchange($cpu).trace(cpu_change,common_timestamp.usecs,next_pid,next_comm,$cpu,common_cpu)' > \
    $T/events/sched/sched_switch/trigger

echo 1 > $T/events/synthetic/cpu_change/enable
cat $T/trace_pipe
# CPU 마이그레이션이 발생할 때마다 이벤트 기록

# 4. 2차원 히스토그램: 시스템 콜 번호 × 프로세스명
echo 'hist:keys=id,common_pid.execname:vals=hitcount:sort=hitcount.descending:size=256' > \
    $T/events/raw_syscalls/sys_enter/trigger

# 5. hist 트리거 일시 정지/재개
echo 'hist:keys=id:pause' > $T/events/raw_syscalls/sys_enter/trigger
# ... 다른 작업 ...
echo 'hist:keys=id:cont' > $T/events/raw_syscalls/sys_enter/trigger

# 정리
echo '!hist:keys=id' > $T/events/raw_syscalls/sys_enter/trigger
echo > $T/synthetic_events
ℹ️

hist 트리거 성능 특성: hist 트리거는 커널 내부의 해시 테이블(Hash Table)에 집계 결과를 저장합니다. size= 파라미터로 해시 테이블 크기를 조절할 수 있습니다 (기본값 2048). 키가 많으면 해시 충돌이 증가하여 성능이 저하됩니다. 고빈도 이벤트에서는 size=8192 이상을 권장합니다. 또한 .log2 또는 .buckets=N 수식자를 사용하여 연속값을 구간으로 묶을 수 있습니다.

boot-time tracing

부팅 초기 단계의 이벤트를 포착하려면 커널 커맨드라인 파라미터로 ftrace를 활성화해야 합니다. tracefs가 마운트되기 전에 트레이싱을 시작할 수 있으므로, init 프로세스 실행 이전의 커널 초기화 과정을 분석할 수 있습니다.

커널 커맨드라인 파라미터

파라미터설명예시
ftrace=부팅 시 활성화할 트레이서ftrace=function_graph
ftrace_filter=추적할 함수 목록ftrace_filter=do_sys_open,vfs_read
ftrace_notrace=추적 제외 함수ftrace_notrace=rcu_*
ftrace_graph_filter=function_graph 추적 대상ftrace_graph_filter=do_mount
trace_event=활성화할 이벤트trace_event=sched:sched_switch,irq:*
trace_buf_size=트레이스 버퍼 크기trace_buf_size=10M
traceoff_on_warningWARN 발생 시 자동 중지(플래그)
tp_printk트레이스 이벤트를 콘솔에 출력(플래그)
# GRUB 설정 예시: 부팅 시 function_graph 활성화
# /etc/default/grub
GRUB_CMDLINE_LINUX="ftrace=function_graph ftrace_graph_filter=do_mount,do_sys_open trace_buf_size=10M"

# initcall 디버깅 (부팅 지연 분석)
GRUB_CMDLINE_LINUX="initcall_debug trace_event=initcall:* trace_buf_size=20M"

# 부팅 후 트레이스 확인
cat /sys/kernel/tracing/trace > /tmp/boot_trace.txt

# Boot config (5.15+): /proc/bootconfig 활용
# initrd에 bootconfig 데이터를 첨부하여 더 정밀한 설정 가능
# bootconfig 파일 예시:
# ftrace {
#     tracer = function_graph
#     buffer_size = 10M
#     event.enable {
#         sched.sched_switch
#         irq.*
#     }
# }
ℹ️

tp_printk 주의: tp_printk 파라미터는 트레이스 이벤트를 printk로 콘솔에 출력합니다. 이벤트 빈도가 높으면 콘솔 출력이 병목이 되어 시스템이 느려질 수 있습니다. 직렬 콘솔(115200 baud)에서는 특히 주의하세요. 부팅 실패 디버깅에만 제한적으로 사용하십시오.

initcall 디버깅

initcall_debug 커널 파라미터는 모든 __init 함수의 호출 시간과 반환값을 dmesg에 출력합니다. 부팅 지연의 원인이 되는 드라이버를 찾을 때 유용합니다.

# initcall 지연 분석
dmesg | grep 'initcall' | sort -t'=' -k2 -n -r | head -20
# 출력 예:
# initcall ahci_pci_driver_init+0x0/0x30 returned 0 after 125432 usecs
# initcall e1000e_init_module+0x0/0x58 returned 0 after 89234 usecs
# initcall nvme_init+0x0/0x58 returned 0 after 45123 usecs

# ftrace와 결합: initcall 이벤트의 function_graph
# 커널 커맨드라인:
# ftrace=function_graph ftrace_graph_filter=ahci_* trace_event=initcall:*

tracefs 인스턴스 멀티플렉싱

tracefs 인스턴스는 독립적인 트레이싱 세션을 동시에 운영할 수 있게 해줍니다. 각 인스턴스는 자체 ring buffer, 이벤트 필터, 트레이서를 갖습니다. 프로덕션 모니터링과 임시 디버깅을 간섭 없이 병행할 수 있습니다.

글로벌 인스턴스 /sys/kernel/tracing/ tracer: function_graph events: sched:* buffer: 4096KB/CPU (프로덕션 모니터링) 인스턴스: monitor instances/monitor/ tracer: nop events: block:*, net:* buffer: 8192KB/CPU (I/O 모니터링) 인스턴스: debug instances/debug/ tracer: function filter: ext4_* buffer: 16384KB/CPU (임시 디버깅) 공유 커널 tracepoint 인프라 이벤트 소스는 공유, 버퍼와 필터는 인스턴스별 독립 인스턴스 생성: mkdir instances/NAME | 삭제: rmdir instances/NAME 각 인스턴스는 trace, trace_pipe, events/, buffer_size_kb 등 독립 파일 보유
T=/sys/kernel/tracing

# 인스턴스 생성
mkdir $T/instances/monitor
mkdir $T/instances/debug

# 인스턴스별 독립 설정
# monitor: block I/O 이벤트만 수집
echo 8192 > $T/instances/monitor/buffer_size_kb
echo 1 > $T/instances/monitor/events/block/enable
echo 1 > $T/instances/monitor/tracing_on

# debug: ext4 함수 추적
echo function > $T/instances/debug/current_tracer
echo 'ext4_*' > $T/instances/debug/set_ftrace_filter
echo 16384 > $T/instances/debug/buffer_size_kb
echo 1 > $T/instances/debug/tracing_on

# 각 인스턴스의 트레이스 독립 읽기
cat $T/instances/monitor/trace_pipe > /tmp/block_trace.log &
cat $T/instances/debug/trace > /tmp/ext4_debug.txt

# 디버깅 완료 후 인스턴스 삭제
echo 0 > $T/instances/debug/tracing_on
rmdir $T/instances/debug

# 인스턴스 목록 확인
ls $T/instances/
⚠️

인스턴스 메모리: 각 인스턴스는 독립적인 per-CPU ring buffer를 할당합니다. 다수의 인스턴스를 큰 버퍼 크기로 생성하면 상당한 커널 메모리를 소비합니다. rmdir로 불필요한 인스턴스를 제거하면 메모리가 즉시 반환됩니다. trace-cmd는 내부적으로 인스턴스를 사용하므로, trace-cmd reset으로 정리할 수 있습니다.

인스턴스 활용 시나리오

실무에서 인스턴스를 효과적으로 활용하는 대표적인 시나리오를 소개합니다. 핵심은 관심사 분리입니다 — 서로 다른 목적의 트레이싱을 독립 버퍼에서 병행합니다.

인스턴스 기반 다중 관심사 병렬 추적 상시 모니터링 인스턴스 instances/monitor/ 목적: 24/7 운영 관측 이벤트: sched_switch, block_rq_* 버퍼: 2048KB (최소화) hist 트리거로 집계만 수행 overrun 모니터링 알림 임시 디버깅 인스턴스 instances/debug-issue-1234/ 목적: 특정 버그 추적 tracer: function_graph filter: ext4_*, jbd2_* PID: 문제 프로세스만 완료 후 rmdir로 즉시 삭제 성능 프로파일 인스턴스 instances/perf-baseline/ 목적: 성능 회귀 테스트 이벤트: syscalls/sys_enter_* 버퍼: 16384KB (큰 버퍼) hist로 syscall 레이턴시 분포 before/after 비교 공유 리소스: tracepoint 등록, kprobe/uprobe 인프라 각 인스턴스는 이벤트 소스를 공유하되, 활성화/필터/버퍼는 완전히 독립 인스턴스 운영 베스트 프랙티스 이름 규칙: 목적-이슈번호 monitor, debug-12345 버퍼 크기: 목적에 맞게 최소화 128CPU × 16MB = 2GB 주의 정리: 사용 후 반드시 rmdir trace-cmd reset으로 일괄 정리 CPU 격리: per_cpu/cpuN/ 파일로 특정 CPU의 이벤트만 수집 가능 인스턴스 + set_event_pid로 프로세스별 완전 격리 달성
T=/sys/kernel/tracing

# ===== 실전 인스턴스 운영 스크립트 =====

# 상시 모니터링 인스턴스 설정 (서버 부팅 시 실행)
setup_monitor() {
    mkdir -p $T/instances/monitor
    M=$T/instances/monitor

    # 최소 버퍼 (모니터링용이므로 작게)
    echo 2048 > $M/buffer_size_kb

    # 스케줄링 이상 감지
    echo 1 > $M/events/sched/sched_switch/enable
    echo 'hist:keys=next_comm:vals=hitcount:sort=hitcount.descending:size=1024' > \
        $M/events/sched/sched_switch/trigger

    # 블록 I/O 레이턴시 모니터링
    echo 1 > $M/events/block/block_rq_complete/enable

    echo 1 > $M/tracing_on
    echo "monitor 인스턴스 시작"
}

# 임시 디버깅 인스턴스 (문제 발생 시 즉시 실행)
start_debug() {
    local issue_id=$1
    local target_pid=$2
    mkdir -p $T/instances/debug-$issue_id
    D=$T/instances/debug-$issue_id

    echo function_graph > $D/current_tracer
    echo 16384 > $D/buffer_size_kb
    echo 5 > $D/max_graph_depth
    echo $target_pid > $D/set_ftrace_pid
    echo > $D/trace
    echo 1 > $D/tracing_on
    echo "디버깅 인스턴스 debug-$issue_id 시작 (PID=$target_pid)"
}

# 디버깅 완료 후 정리
stop_debug() {
    local issue_id=$1
    D=$T/instances/debug-$issue_id

    echo 0 > $D/tracing_on
    cat $D/trace > /tmp/debug-$issue_id-$(date +%Y%m%d_%H%M%S).txt
    rmdir $D
    echo "디버깅 인스턴스 debug-$issue_id 종료 및 정리 완료"
}

# 모든 인스턴스 상태 확인
check_instances() {
    echo "=== 활성 인스턴스 ==="
    for inst in $T/instances/*/; do
        [ -d "$inst" ] || continue
        name=$(basename $inst)
        tracer=$(cat $inst/current_tracer)
        on=$(cat $inst/tracing_on)
        size=$(cat $inst/buffer_size_kb)
        echo "  $name: tracer=$tracer on=$on buffer=${size}KB/CPU"
    done
}

# trace-cmd에서 인스턴스 활용
trace-cmd record -B monitor -e block -a sleep 10
# -B monitor: 'monitor' 인스턴스에 기록

trace-cmd report -B monitor
# 특정 인스턴스의 데이터만 보고

타임스탬프 클록 소스 비교

ftrace는 여러 타임스탬프 클록 소스를 지원하며, 분석 목적에 따라 적절한 클록을 선택해야 합니다. 멀티 CPU 환경에서 타임스탬프의 정렬(ordering)은 클록 소스에 따라 달라지므로, 이를 이해하지 않으면 이벤트 순서가 뒤바뀌어 보이는 오류에 빠질 수 있습니다.

trace_clock 선택에 따른 특성 local CPU별 독립 카운터 가장 빠름, 정렬 불가 단일 CPU 분석용 global 글로벌 시퀀스 카운터 완벽 정렬, 경합 발생 느림 (spinlock) x86-tsc 하드웨어 TSC 직접 읽기 빠름, TSC 동기화 의존 x86 전용 mono / mono_raw ktime_get_mono_fast_ns NTP 보정 유/무 외부 도구 시간 일치 boot 부팅 이후 경과 시간 멀티 CPU 타임스탬프 정렬 문제 CPU 0: event_A (t=1000) → CPU 1: event_B (t=999) → 출력에서 B가 A보다 먼저 나타남 해결: x86-tsc(TSC 동기화된 경우) 또는 global 클록 사용, trace-cmd report --ts-diff로 보정
클록오버헤드멀티CPU 정렬외부 시간 일치권장 용도
local최저 (~10ns)불가불가단일 CPU per-CPU 분석
global높음 (~50ns)완벽불가이벤트 순서가 중요한 경우
x86-tsc낮음 (~15ns)TSC 동기 시 가능불가x86 고정밀 측정
mono중간 (~20ns)가능NTP 보정됨외부 로그와 시간 매칭
mono_raw중간 (~20ns)가능미보정NTP 간섭 없는 순수 측정
boot중간 (~20ns)가능부팅 기준suspend/resume 포함 분석
# 현재 클록 확인 ([x86-tsc]처럼 대괄호로 활성 표시)
cat /sys/kernel/tracing/trace_clock
# [local] global counter x86-tsc mono mono_raw boot

# 클록 변경
echo mono > /sys/kernel/tracing/trace_clock

# TSC 동기화 상태 확인 (x86)
dmesg | grep -i 'tsc'
# tsc: Refined TSC clocksource calibration: 2999.999 MHz
# tsc: Detected 3000.000 MHz processor

# trace-cmd로 타임스탬프 보정 보고
trace-cmd report --ts-diff input.dat

오버헤드 벤치마크

트레이싱 도구를 프로덕션에 적용하기 전에 오버헤드를 정량적으로 이해해야 합니다. 다음은 일반적인 x86_64 시스템에서 측정한 tracer별 오버헤드입니다. 실제 수치는 하드웨어와 커널 버전에 따라 달라지지만, 상대적 비율은 유사합니다.

트레이서별 함수 호출 오버헤드 (ns/call, x86_64) nop (비활성) function function_graph kprobe kretprobe uprobe fprobe ~1 ns ~10 ns ~25 ns (진입+퇴장) ~40 ns (int3 트랩) ~60 ns (진입+반환 트랩) ~80 ns (유저↔커널 전환 포함) ~20 ns (ftrace 기반, int3 없음) 0 20 40 60 80 100 ns

측정 방법

ftrace 자체에 내장된 tracing_threshfunction_profile을 사용하면 오버헤드를 측정할 수 있습니다.

T=/sys/kernel/tracing

# 방법 1: function 프로파일링으로 평균 오버헤드 측정
echo nop > $T/current_tracer
echo 1 > $T/function_profile_enabled
# 워크로드 실행...
sleep 5
echo 0 > $T/function_profile_enabled
cat $T/trace_stat/function0
#  Function                    Hit    Time(ns)  Avg(ns)  s^2
#  --------                    ---    --------  -------  ---
#  schedule                  12453    1245300    100.0   45.2
#  __schedule               12453     623400     50.1   12.3

# 방법 2: tracing_thresh로 지연 임계값 감지
echo function > $T/current_tracer
echo 100 > $T/tracing_thresh      # 100μs 이상만 기록
echo 1 > $T/tracing_on
# 워크로드 실행...
cat $T/trace

# 방법 3: perf로 ftrace 오버헤드 자체 측정
perf stat -e cycles -r 10 -- cat /sys/kernel/tracing/trace > /dev/null

# 버퍼 손실(overrun) 감지
for cpu in /sys/kernel/tracing/per_cpu/cpu*; do
    overrun=$(grep 'overrun:' $cpu/stats | awk '{print $2}')
    if [ "$overrun" -gt 0 ]; then
        echo "$(basename $cpu): overrun=$overrun — 버퍼 크기 증가 필요"
    fi
done
⚠️

프로덕션 오버헤드 지침: (1) function 트레이서로 전체 함수를 추적하지 마십시오 — 반드시 set_ftrace_filter로 범위를 제한하세요. (2) function_graphmax_graph_depthset_graph_function을 항상 설정하세요. (3) ring buffer overrun이 0이 아니면 데이터가 손실된 것입니다 — 버퍼를 키우거나 필터를 좁히세요. (4) perf의 샘플링 모드가 트레이싱보다 오버헤드가 낮으므로, 통계적 분석에는 perf를 우선 고려하세요.

커널 6.x ftrace 변경

커널 6.x 시리즈에서 ftrace와 트레이싱 서브시스템에 여러 중요한 개선이 이루어졌습니다. 새로운 프로브 유형, 사용자 공간 이벤트, BTF 통합 강화 등이 추가되어 관측성 도구의 표현력과 성능이 크게 향상되었습니다.

6.1 6.2 6.3 6.4 6.5 6.8+ eprobe (event probe) 기존 이벤트에 동적 프로브 필드 변환/필터링 fprobe ftrace 기반 함수 프로브 kprobe보다 낮은 오버헤드 direct trampoline 개선 BPF 직접 연결 multi-attach 지원 user_events 유저 공간 → tracefs 이벤트 mmap 기반 제로카피 function_graph 개선 multi-graph 지원 per-CPU 깊이 독립 trace_pipe_raw 개선 BTF 연동 강화 splice zero-copy 읽기 주요 변경 요약 fprobe: kprobe 대비 2~3배 낮은 오버헤드, ftrace 인프라 직접 사용 (int3 트랩 불필요) user_events: 유저 앱에서 tracefs 이벤트 등록, perf/ftrace/BPF에서 동일하게 소비 eprobe: 기존 tracepoint 출력을 입력으로 받아 새 이벤트 생성, 필드 변환/집계 가능 direct trampoline: BPF 프로그램을 ftrace 콜사이트에 직접 연결, 중간 계층 제거로 오버헤드 최소화

fprobe (6.2+)

fprobe는 ftrace 인프라를 직접 활용하는 새로운 함수 프로브입니다. kprobe처럼 INT3 트랩을 사용하지 않고, ftrace의 NOP 패칭 메커니즘을 통해 콜백을 삽입합니다. 이로 인해 kprobe 대비 2~3배 낮은 오버헤드를 달성합니다.

/* fprobe 사용 예시 (커널 모듈) */
#include <linux/fprobe.h>

static int entry_handler(struct fprobe *fp,
                         unsigned long entry_ip,
                         unsigned long ret_ip,
                         struct pt_regs *regs,
                         void *data)
{
    pr_info("fprobe: entering %pS\n", (void *)entry_ip);
    return 0;
}

static void exit_handler(struct fprobe *fp,
                          unsigned long entry_ip,
                          unsigned long ret_ip,
                          struct pt_regs *regs,
                          void *data)
{
    pr_info("fprobe: exiting %pS, retval=%lx\n",
            (void *)entry_ip, regs_return_value(regs));
}

static struct fprobe my_fprobe = {
    .entry_handler = entry_handler,
    .exit_handler  = exit_handler,
};

/* 대상 함수들 */
static const char *targets[] = { "vfs_read", "vfs_write", NULL };

static int __init my_init(void)
{
    return register_fprobe_syms(&my_fprobe, targets, 2);
}

static void __exit my_exit(void)
{
    unregister_fprobe(&my_fprobe);
}

user_events (6.4+)

user_events는 유저 공간 애플리케이션이 tracefs에 이벤트를 등록하고, 해당 이벤트가 활성화될 때만 데이터를 기록하는 메커니즘입니다. mmap 기반의 enable 상태 확인으로 비활성 시 오버헤드가 거의 없습니다.

# user_events 등록 및 사용
# 1. 이벤트 등록
echo 'myapp_request u64 latency_ns; u32 status; char msg[64]' > \
    /sys/kernel/tracing/user_events_data

# 2. 이벤트가 events/user_events/ 아래에 나타남
ls /sys/kernel/tracing/events/user_events/myapp_request/
# enable  filter  format  id  trigger

# 3. 활성화하면 앱이 mmap 상태 비트로 감지
echo 1 > /sys/kernel/tracing/events/user_events/myapp_request/enable

# 4. perf, trace-cmd, BPF에서 동일하게 소비 가능
perf record -e user_events:myapp_request -a -- sleep 10
trace-cmd record -e user_events:myapp_request

eprobe (event probe)

eprobe는 기존 tracepoint의 출력 필드를 입력으로 받아 새로운 이벤트를 생성합니다. 커널 코드를 수정하지 않고 이벤트 데이터를 변환하거나 필터링할 수 있습니다.

# eprobe: sched_switch에서 커스텀 이벤트 파생
# next_pid가 0이 아닌 경우만 캡처하는 eprobe 생성
echo 'e:my_group/my_switch sched/sched_switch next_comm=$next_comm next_pid=$next_pid' > \
    /sys/kernel/tracing/dynamic_events

# eprobe 이벤트 활성화
echo 1 > /sys/kernel/tracing/events/my_group/my_switch/enable
echo 'next_pid != 0' > /sys/kernel/tracing/events/my_group/my_switch/filter

# 결과 확인
cat /sys/kernel/tracing/trace

# 정리
echo '-:my_group/my_switch' > /sys/kernel/tracing/dynamic_events
ℹ️

RTLA와의 연계: 커널 6.x에서 강화된 ftrace 인프라는 RTLA(Real-Time Linux Analysis) 도구와 깊이 통합됩니다. rtla timerlat은 내부적으로 ftrace의 osnoise 트레이서를 사용하며, rtla osnoise hist는 hist 트리거를 활용합니다. 실시간 시스템의 레이턴시 분석에는 두 도구를 함께 활용하세요.

실전 디버깅 시나리오

실제 커널 디버깅 상황에서 트레이싱 도구를 어떻게 활용하는지 구체적인 시나리오를 통해 살펴봅니다.

시나리오 1: 스케줄링 레이턴시 추적

프로세스가 깨어난 후 실제 CPU에서 실행되기까지의 지연 시간이 비정상적으로 긴 경우를 분석합니다.

# 방법 1: ftrace의 wakeup_rt 트레이서
T=/sys/kernel/tracing
echo 0 > $T/tracing_on
echo wakeup_rt > $T/current_tracer
echo 1 > $T/tracing_on
# 워크로드 실행 후...
echo 0 > $T/tracing_on
cat $T/trace
# 최대 레이턴시를 유발한 스케줄링 경로가 출력됨

# 방법 2: perf sched 서브커맨드
perf sched record -- sleep 10
perf sched latency

perf sched timehist
# 타임라인 기반 스케줄링 이력

# 방법 3: bpftrace 원라이너
bpftrace -e '
tracepoint:sched:sched_wakeup /args->comm == "myapp"/ {
    @ts[args->pid] = nsecs;
}
tracepoint:sched:sched_switch /args->next_comm == "myapp" && @ts[args->next_pid]/ {
    $lat = (nsecs - @ts[args->next_pid]) / 1000;
    printf("myapp wakeup latency: %d us\\n", $lat);
    if ($lat > 1000) {
        printf("  HIGH LATENCY! stack:\\n");
        print(kstack);
    }
    delete(@ts[args->next_pid]);
}'

시나리오 2: 함수 호출 경로 분석

특정 시스템 콜이 커널 내부에서 어떤 경로로 실행되는지 상세히 추적합니다.

# function_graph로 sys_read의 전체 호출 트리 추적
T=/sys/kernel/tracing

echo 0 > $T/tracing_on
echo function_graph > $T/current_tracer
echo '__x64_sys_read' > $T/set_graph_function
echo 5 > $T/max_graph_depth       # 깊이 제한
echo $$ > $T/set_ftrace_pid       # 현재 셸만 추적
echo > $T/trace
echo 1 > $T/tracing_on
cat /dev/null                          # read 시스템 콜 트리거
echo 0 > $T/tracing_on
cat $T/trace

# trace-cmd 사용 (더 편리)
trace-cmd record -p function_graph \
    -g __x64_sys_read \
    --max-graph-depth 5 \
    -P $$ \
    -- cat /dev/null

trace-cmd report

시나리오 3: Flame Graph 생성

Flame graph는 Brendan Gregg가 고안한 시각화 기법으로, CPU 프로파일 데이터를 직관적으로 표현합니다. perf와 결합하여 커널과 유저 공간의 병목을 한눈에 파악할 수 있습니다.

# 1단계: perf로 프로파일 데이터 수집
perf record -F 99 -a -g -- sleep 30
# -F 99: 99Hz 샘플링 (lockstep 방지를 위해 100이 아닌 99 사용)
# -a: 모든 CPU
# -g: 콜 그래프 (스택 트레이스)

# 2단계: perf script로 텍스트 변환
perf script > out.perf

# 3단계: FlameGraph 도구로 변환
# (https://github.com/brendangregg/FlameGraph)
./stackcollapse-perf.pl out.perf > out.folded
./flamegraph.pl out.folded > flamegraph.svg

# Off-CPU flame graph (I/O 대기 시간 분석)
# bpftrace로 off-cpu 스택 수집
bpftrace -e '
tracepoint:sched:sched_switch {
    @start[tid] = nsecs;
}
tracepoint:sched:sched_switch /@start[args->next_pid]/ {
    $delta = nsecs - @start[args->next_pid];
    @off[kstack(args->prev_pid), comm] = sum($delta);
    delete(@start[args->next_pid]);
}
END { print(@off); }' > offcpu.txt

# 또는 perf의 offcpu 프로파일링 (커널 6.7+)
perf record --off-cpu -a -g -- sleep 30

시나리오 4: I/O 레이턴시 분석

# 블록 I/O 이벤트 추적
trace-cmd record -e block:block_rq_issue -e block:block_rq_complete \
    -e block:block_bio_queue sleep 10
trace-cmd report

# bpftrace로 I/O 레이턴시 히스토그램
bpftrace -e '
tracepoint:block:block_rq_issue {
    @start[args->dev, args->sector] = nsecs;
}
tracepoint:block:block_rq_complete /@start[args->dev, args->sector]/ {
    $lat = (nsecs - @start[args->dev, args->sector]) / 1000;
    @us_hist = hist($lat);
    if ($lat > 10000) {
        printf("slow I/O: %d us, dev=%d, sector=%lu, comm=%s\\n",
               $lat, args->dev, args->sector, comm);
    }
    delete(@start[args->dev, args->sector]);
}'

# perf로 I/O 이벤트 기반 프로파일링
perf record -e block:block_rq_issue -e block:block_rq_complete \
    -a -g -- sleep 30
perf script | head -50

시나리오 5: 네트워크 패킷(Packet) 경로 추적

# 네트워크 스택의 주요 함수 호출 추적
trace-cmd record -p function_graph \
    -g ip_rcv \
    -g tcp_v4_rcv \
    -g __dev_queue_xmit \
    --max-graph-depth 4 \
    -- ping -c 3 8.8.8.8
trace-cmd report

# TCP 재전송 추적 및 원인 분석
bpftrace -e 'kprobe:tcp_retransmit_skb {
    $sk = (struct sock *)arg0;
    $inet = (struct inet_sock *)arg0;
    printf("retransmit: pid=%d comm=%s saddr=%s sport=%d\\n",
           pid, comm,
           ntop($inet->inet_saddr),
           $inet->inet_sport);
    print(kstack);
}'

# 네트워크 이벤트 종합 트레이싱
perf record -e net:net_dev_xmit \
    -e net:netif_receive_skb \
    -e tcp:tcp_retransmit_skb \
    -e tcp:tcp_send_reset \
    -a -- sleep 60

시나리오 6: 락 경합(Lock Contention) 분석

뮤텍스(Mutex)나 스핀락(Spinlock) 경합이 성능 병목이 되는 경우, ftrace와 lock 이벤트를 결합하여 경합 지점과 대기 시간을 분석합니다.

락 경합 분석 흐름 스레드 A mutex_lock(L) 성공 임계 구역(Critical) 실행 중... 시간: t1 ~ t2 스레드 B mutex_lock(L) 대기 경합 대기 스케줄 아웃 (D상태) 대기 시간: t2 - t1 분석 도구 lock:lock_contended lock:lock_acquired perf lock contention bpftrace mutex 추적 락 경합 진단 단계 ① 경합 빈도 확인 perf lock contention ② 경합 시간 측정 hist 트리거 + synthetic ③ 홀더 식별 stacktrace 트리거 ④ 개선 락 분할/RCU 전환 핵심: 경합 빈도보다 경합 시간이 중요 — 짧은 경합 1000회보다 긴 경합 1회가 더 해로움
# === 락 경합 분석 방법들 ===

# 방법 1: perf lock contention (커널 5.18+, 가장 간편)
perf lock contention -a -b -- sleep 10
# 출력 예:
#  contended  total wait    max wait  avg wait     type  caller
#      12345    234.56 ms    12.34 ms    19.01 us  mutex  ext4_writepages+0x1a
#       5678     89.12 ms     5.67 ms    15.70 us  rwsem  mmap_write_lock+0x0

# 방법 2: ftrace lock 이벤트 + hist 트리거
T=/sys/kernel/tracing

# lock 이벤트 활성화 (CONFIG_LOCK_STAT 또는 CONFIG_LOCKDEP 필요)
echo 1 > $T/events/lock/lock_contended/enable
echo 1 > $T/events/lock/lock_acquired/enable

# 경합이 발생한 위치별 횟수 집계
echo 'hist:keys=common_pid.execname.stacktrace:vals=hitcount:sort=hitcount.descending' > \
    $T/events/lock/lock_contended/trigger

# 경합 시 스택 트레이스 자동 기록
echo 'stacktrace' > $T/events/lock/lock_contended/trigger

sleep 10
cat $T/events/lock/lock_contended/hist
cat $T/trace

# 방법 3: bpftrace로 mutex 경합 시간 측정
bpftrace -e '
kprobe:mutex_lock {
    @start[tid] = nsecs;
}
kretprobe:mutex_lock /@start[tid]/ {
    $lat = (nsecs - @start[tid]) / 1000;
    if ($lat > 100) {  /* 100us 이상 경합 */
        printf("mutex contention: %s (pid=%d) %d us\n", comm, pid, $lat);
        print(kstack);
    }
    @us_hist = hist($lat);
    delete(@start[tid]);
}'

# 방법 4: function_graph로 락 대기 구간 시각화
echo function_graph > $T/current_tracer
echo 'mutex_lock' > $T/set_graph_function
echo 'mutex_lock_interruptible' >> $T/set_graph_function
echo 3 > $T/max_graph_depth
# ! 기호가 붙은 mutex_lock()이 있으면 경합 발생

시나리오 7: 인터럽트 지연(IRQ Latency) 분석

인터럽트 처리 지연은 실시간 시스템에서 심각한 문제를 유발합니다. ftrace의 irqsoff 트레이서와 IRQ 이벤트를 활용하여 인터럽트 비활성화 구간과 핸들러 실행 시간을 분석합니다.

# === 인터럽트 지연 분석 ===
T=/sys/kernel/tracing

# 방법 1: irqsoff 트레이서 — 인터럽트 비활성화 최대 구간 추적
echo 0 > $T/tracing_on
echo irqsoff > $T/current_tracer
echo 1 > $T/tracing_on
# 워크로드 실행...
sleep 10
echo 0 > $T/tracing_on
cat $T/trace
# 최대 IRQ-off 구간과 그 동안의 함수 호출 경로 출력
# tracing_max_latency에 최대 레이턴시(us) 기록됨
cat $T/tracing_max_latency
# 예: 156  → 최대 156us 동안 인터럽트 비활성화

# tracing_thresh로 임계값 설정 (해당 값 초과 시만 기록)
echo 100 > $T/tracing_thresh   # 100us 초과 시만

# 방법 2: preemptirqsoff 트레이서 — preempt + IRQ off 결합
echo preemptirqsoff > $T/current_tracer
# 선점과 인터럽트가 모두 비활성화된 최장 구간 추적

# 방법 3: IRQ 이벤트로 핸들러 실행 시간 분석
echo nop > $T/current_tracer
echo 1 > $T/events/irq/irq_handler_entry/enable
echo 1 > $T/events/irq/irq_handler_exit/enable
echo 1 > $T/tracing_on
sleep 5
echo 0 > $T/tracing_on

# IRQ 핸들러별 실행 시간 hist 트리거
echo 'hist:keys=name:vals=hitcount:sort=hitcount.descending' > \
    $T/events/irq/irq_handler_entry/trigger
sleep 10
cat $T/events/irq/irq_handler_entry/hist

# 방법 4: bpftrace로 IRQ 핸들러 레이턴시 히스토그램
bpftrace -e '
tracepoint:irq:irq_handler_entry {
    @start[args->irq] = nsecs;
}
tracepoint:irq:irq_handler_exit /@start[args->irq]/ {
    $lat = (nsecs - @start[args->irq]) / 1000;
    @us[args->name] = hist($lat);
    if ($lat > 100) {
        printf("slow IRQ: %s %d us\n", str(args->name), $lat);
    }
    delete(@start[args->irq]);
}'

# 방법 5: osnoise 트레이서 (커널 5.14+, RT 시스템용)
echo osnoise > $T/current_tracer
echo 1 > $T/tracing_on
sleep 10
echo 0 > $T/tracing_on
cat $T/trace
# OS 노이즈 소스(IRQ, softirq, NMI, 스레드)별 지연 분석

시나리오 8: wakeup 레이턴시 추적

wakeup 레이턴시는 프로세스가 깨어난 시점(sched_waking)부터 실제 CPU에서 실행되기까지(sched_switch)의 시간입니다. 이 구간이 길면 응답 지연(Response Latency)이 발생합니다.

wakeup 레이턴시 구간 분해 sched_waking sched_wakeup sched_switch 실행 시작 IPI + 큐잉 지연 런큐 대기 (가장 큰 부분) 컨텍스트 스위치 비용 총 wakeup 레이턴시 wakeup 지연 원인별 분석 런큐 깊이 높은 로드 → 대기 프로세스 많음 선점 비활성화 preempt_disable() 구간이 길면 지연 CPU 친화성 제약 cpuset/isolcpus로 마이그레이션 제한 IRQ 처리 중 긴 IRQ 핸들러 → 스케줄러 호출 지연 NUMA 간 wakeup 원격 CPU IPI → 추가 지연 CFS bandwidth throttle cgroup CPU 한도 초과 → 대기
# === wakeup 레이턴시 분석 ===

# 방법 1: wakeup_rt 트레이서 (RT 태스크용)
T=/sys/kernel/tracing
echo wakeup_rt > $T/current_tracer
echo 0 > $T/tracing_max_latency  # 리셋
echo 1 > $T/tracing_on
sleep 30
echo 0 > $T/tracing_on
echo "최대 wakeup 레이턴시: $(cat $T/tracing_max_latency) us"
cat $T/trace
# 최대 지연을 유발한 전체 함수 호출 경로 출력

# 방법 2: synthetic 이벤트로 프로세스별 wakeup 레이턴시 통계
echo 'wakeup_lat u64 lat; pid_t pid; char comm[16]; int target_cpu' > $T/synthetic_events

echo 'hist:keys=pid:ts0=common_timestamp.usecs:target_cpu=target_cpu' > \
    $T/events/sched/sched_waking/trigger

echo 'hist:keys=next_pid:lat=common_timestamp.usecs-$ts0:onmatch(sched.sched_waking).wakeup_lat($lat,next_pid,next_comm,$target_cpu)' > \
    $T/events/sched/sched_switch/trigger

echo 'hist:keys=comm,pid:vals=lat:sort=lat.descending' > \
    $T/events/synthetic/wakeup_lat/trigger

echo 1 > $T/events/synthetic/wakeup_lat/enable
sleep 30
cat $T/events/synthetic/wakeup_lat/hist

# 방법 3: 특정 프로세스의 wakeup 원인 추적
bpftrace -e '
tracepoint:sched:sched_waking /args->comm == "myapp"/ {
    @waker[comm, kstack] = count();
    @wake_ts[args->pid] = nsecs;
}
tracepoint:sched:sched_switch /args->next_comm == "myapp" && @wake_ts[args->next_pid]/ {
    $lat_us = (nsecs - @wake_ts[args->next_pid]) / 1000;
    @lat = hist($lat_us);
    if ($lat_us > 5000) {
        printf("HIGH wakeup latency: %d us on CPU %d\n", $lat_us, cpu);
    }
    delete(@wake_ts[args->next_pid]);
}
END {
    printf("\n=== Waker 스택 ===\n");
    print(@waker);
}'

# 방법 4: perf sched로 전체 스케줄링 타임라인
perf sched record -- sleep 10
perf sched latency --sort max
# 프로세스별 최대/평균 wakeup 레이턴시 출력

perf sched timehist -V
# -V: wakeup 이벤트 포함 타임라인

시나리오 9: 메모리 할당 병목 추적

커널 메모리 할당(kmalloc, page alloc)이 지연되는 경우, 할당 크기 분포, 슬로우 패스(slow path) 진입 빈도, direct reclaim 발생 여부를 분석합니다.

# === 메모리 할당 병목 분석 ===

# 1. kmalloc 크기 분포와 호출 빈도
T=/sys/kernel/tracing
echo 'hist:keys=bytes_req.log2,call_site.sym:vals=hitcount:sort=hitcount.descending' > \
    $T/events/kmem/kmalloc/trigger
sleep 10
cat $T/events/kmem/kmalloc/hist

# 2. 페이지 할당 슬로우 패스 추적
echo 1 > $T/events/kmem/mm_page_alloc/enable
echo 'order > 0' > $T/events/kmem/mm_page_alloc/filter
echo 'stacktrace if order > 2' > $T/events/kmem/mm_page_alloc/trigger
# order > 2 (16페이지 이상)인 대규모 할당 시 스택 트레이스

# 3. direct reclaim 발생 추적
echo 1 > $T/events/vmscan/mm_vmscan_direct_reclaim_begin/enable
echo 1 > $T/events/vmscan/mm_vmscan_direct_reclaim_end/enable
echo 'stacktrace' > $T/events/vmscan/mm_vmscan_direct_reclaim_begin/trigger

# 4. bpftrace로 메모리 할당 레이턴시
bpftrace -e '
kprobe:__alloc_pages {
    @start[tid] = nsecs;
}
kretprobe:__alloc_pages /@start[tid]/ {
    $lat = (nsecs - @start[tid]) / 1000;
    @alloc_lat = hist($lat);
    if ($lat > 1000) {
        printf("slow alloc: %s %d us (order=%d)\n", comm, $lat, arg1);
        print(kstack);
    }
    delete(@start[tid]);
}'

# 5. OOM 킬러 이벤트 트리거
echo 'stacktrace' > $T/events/oom/mark_victim/trigger
echo 1 > $T/events/oom/mark_victim/enable

시나리오 10: softirq/workqueue 지연 분석

# === softirq 처리 지연 분석 ===

# 1. softirq 핸들러별 실행 시간
T=/sys/kernel/tracing
echo 1 > $T/events/irq/softirq_entry/enable
echo 1 > $T/events/irq/softirq_exit/enable

echo 'hist:keys=vec:vals=hitcount:sort=hitcount.descending' > \
    $T/events/irq/softirq_entry/trigger

# 2. bpftrace로 softirq 레이턴시 측정
bpftrace -e '
tracepoint:irq:softirq_entry {
    @start[cpu, args->vec] = nsecs;
}
tracepoint:irq:softirq_exit /@start[cpu, args->vec]/ {
    $lat = (nsecs - @start[cpu, args->vec]) / 1000;
    @us[@softirq_name[args->vec]] = hist($lat);
    delete(@start[cpu, args->vec]);
}

BEGIN {
    @softirq_name[0] = "HI";
    @softirq_name[1] = "TIMER";
    @softirq_name[2] = "NET_TX";
    @softirq_name[3] = "NET_RX";
    @softirq_name[4] = "BLOCK";
    @softirq_name[5] = "IRQ_POLL";
    @softirq_name[6] = "TASKLET";
    @softirq_name[7] = "SCHED";
    @softirq_name[8] = "HRTIMER";
    @softirq_name[9] = "RCU";
}'

# 3. workqueue 작업 실행 시간 분석
echo 1 > $T/events/workqueue/workqueue_execute_start/enable
echo 1 > $T/events/workqueue/workqueue_execute_end/enable

echo 'hist:keys=function.sym:vals=hitcount:sort=hitcount.descending' > \
    $T/events/workqueue/workqueue_execute_start/trigger

# 4. function_graph로 특정 softirq 핸들러의 내부 호출 추적
echo function_graph > $T/current_tracer
echo 'net_rx_action' > $T/set_graph_function   # NET_RX softirq
echo 4 > $T/max_graph_depth
echo 1 > $T/tracing_on
sleep 3
echo 0 > $T/tracing_on
cat $T/trace
# NAPI 처리 → 드라이버 poll → skb 처리 경로 시각화
💡

실전 디버깅 체크리스트:

  • 재현 조건 확인: 문제를 안정적으로 재현할 수 있는지 먼저 확인합니다
  • 최소 추적 범위: 전체 함수가 아닌, 의심 서브시스템의 핵심 함수만 추적합니다
  • overrun 모니터링: 추적 중 per_cpu/cpuN/stats의 overrun이 0인지 확인합니다
  • 시간 동기화: 멀티 CPU 분석 시 trace_clock=mono 또는 x86-tsc 사용
  • 증거 보존: 수정 전에 반드시 trace-cmd extract로 데이터를 파일로 저장합니다
  • before/after 비교: 수정 전후 동일 조건에서 트레이스를 수집하여 효과를 검증합니다

도구 선택 가이드

상황에 따라 적절한 트레이싱 도구를 선택하는 것이 중요합니다.

상황권장 도구이유
CPU 프로파일링perf record + flame graph낮은 오버헤드, 풍부한 시각화
함수 호출 경로ftrace function_graph 또는 trace-cmd상세한 호출 트리, 실행 시간 포함
이벤트 기반 분석trace-cmd 또는 perf record -e구조화된 이벤트 데이터
복잡한 조건부 분석bpftrace유연한 스크립팅, 맵 기반 집계
레이턴시 히스토그램bpftrace 또는 hist 트리거커널 내 집계로 효율적
실시간 모니터링perf top 또는 trace-cmd stream즉각적인 피드백
커널 모듈 개발ftrace (trace_printk) + kprobes모듈과 직접 통합 가능
메모리 버그 탐지KASAN + ftrace 이벤트정확한 오류 위치와 컨텍스트
시각적 타임라인 분석trace-cmd + KernelSharkGUI 기반 멀티 CPU 타임라인
💡

trace_printk() 활용: 커널 모듈 개발 시 pr_info() 대신 trace_printk()를 사용하면 ftrace 링 버퍼에 기록되어 콘솔 출력 오버헤드가 없습니다. 핫 패스에서 디버깅할 때 특히 유용합니다. 단, trace_printk()가 코드에 남아있으면 커널 빌드 시 경고가 출력되므로, 디버깅이 끝나면 반드시 제거하십시오.

CONFIG 옵션 종합

트레이싱 관련 커널 CONFIG 옵션을 정리합니다.

# ===== ftrace 기본 =====
CONFIG_FTRACE=y                     # ftrace 프레임워크
CONFIG_FUNCTION_TRACER=y            # function 트레이서
CONFIG_FUNCTION_GRAPH_TRACER=y      # function_graph 트레이서
CONFIG_DYNAMIC_FTRACE=y             # 동적 ftrace (NOP 패칭)
CONFIG_FPROBE=y                     # fprobe (ftrace 기반 kprobe 대체)

# ===== tracepoints / 이벤트 =====
CONFIG_TRACEPOINTS=y                # tracepoint 지원
CONFIG_EVENT_TRACING=y              # 이벤트 트레이싱
CONFIG_HIST_TRIGGERS=y              # hist 트리거

# ===== kprobes / uprobes =====
CONFIG_KPROBES=y                    # kprobe 지원
CONFIG_KRETPROBES=y                 # kretprobe 지원
CONFIG_HAVE_KPROBES=y               # 아키텍처 지원
CONFIG_KPROBE_EVENTS=y              # tracefs kprobe 이벤트
CONFIG_UPROBES=y                    # uprobe 지원
CONFIG_UPROBE_EVENTS=y              # tracefs uprobe 이벤트

# ===== perf =====
CONFIG_PERF_EVENTS=y                # perf 이벤트 시스템
CONFIG_HW_PERF_EVENTS=y             # 하드웨어 PMU

# ===== BPF (bpftrace용) =====
CONFIG_BPF=y                        # BPF 시스템
CONFIG_BPF_SYSCALL=y                # bpf() 시스템 콜
CONFIG_BPF_JIT=y                    # BPF JIT 컴파일
CONFIG_BPF_EVENTS=y                 # BPF 이벤트 연결

# ===== 디버그 정보 =====
CONFIG_DEBUG_INFO=y                 # DWARF 디버그 심볼
CONFIG_DEBUG_INFO_BTF=y             # BTF (BPF Type Format)
CONFIG_FRAME_POINTER=y              # 정확한 스택 트레이스
CONFIG_STACKTRACE=y                 # 스택 트레이스 지원

참고 링크

커널 공식 문서

LWN 기사

trace-cmd / KernelShark 문서

발표 자료 / 튜토리얼

BPF 트레이싱 통합

커널 소스

ftrace/Tracepoints와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.