Linux Lesson 34 of 47

Deep Observability: perf, ftrace, eBPF, bcc & bpftrace for Production Debugging

Every engineer eventually meets the wall. A metric dashboard is glowing amber — CPU at 80%, latency doubled, disk busy — and it tells you that the box is unwell without a single clue as to why. What you do not have is the one function, the one syscall, the one disk that is actually eating the machine. Restarting the service sometimes helps and teaches you nothing; adding more logging means a deploy, and the problem is happening now. This is the exact gap that deep observability closes: the ability to ask a running kernel a precise question — “which process is opening files fastest?”, “what is the distribution of disk-I/O latency?” — and get a quantified answer in seconds, live, without recompiling or restarting anything.

For most of Linux’s history this was a dark art reserved for kernel developers with printk and a serial console. Two things changed that. First, the kernel grew rich, stable event sources — tracepoints, kprobes, perf events — that expose almost everything it does. Second, eBPF turned the kernel into a safe, programmable data plane: you can now load a tiny, verified program that runs in kernel context at one of those events, aggregates the answer in-kernel, and streams back a finished histogram — at a few percent overhead, on a production box, without the risk of a kernel panic. The tools in this lesson — perf, ftrace, bcc, and especially bpftrace — are the interfaces to that machinery.

This is an expert lesson and it assumes you have already lived the pain — process states, top/ps, and the idea that a load average is a queue, not a percentage. What you leave with is not a tour but a method: the mental model of what the kernel exposes and which tool reads it, the fluency to drive perf and bpftrace from memory, and a repeatable drill-down — a perf top glance, to a targeted bcc tool, to a bespoke bpftrace one-liner — that takes you from “the box is slow” to “this function, on this device, for this reason” faster than you can open a ticket. And one warning will repeat, because it matters: tracing is not free, and a careless probe on a hot path is itself an outage. We always scope tightly and measure.

Why this matters: metrics tell you what, tracing tells you why

There are three classic pillars of observability, and beginners routinely reach for the wrong one. Metrics are cheap, pre-aggregated numbers over time — CPU%, requests/sec, queue depth. They are perfect for detection (“something changed at 02:14”) and useless for root cause (they cannot tell you which code path caused it). Logs are discrete events your software chose to emit; they are only as good as the log line someone remembered to write, and the one you need is never there. Tracing — the subject of this lesson — is the ability to record events the software did not choose to emit, by instrumenting the running system itself: every open(), every scheduler switch, every disk completion, every call to a specific kernel function, with its arguments and latency.

The distinction is the entire reason this lesson exists. When a metric goes bad, you do not add a metric — you trace to find the cause, fix it, and then maybe add a metric to catch it next time. Here is the mental model laid out side by side:

Pillar What it is Answers Cost Cardinality When you reach for it
Metrics Pre-aggregated numbers over time (counters, gauges, histograms) What changed, and when Very low (constant) Low — labels must be bounded Detection, alerting, dashboards, SLOs
Logs Discrete, timestamped events the app emits What the app decided to tell you Medium–high (per event) High but only what’s logged Audit trails, errors with context
Tracing (this lesson) Events captured by instrumenting the running system Why — the exact code path, arg, latency Low–high (you control scope) Arbitrary — you choose the key Root cause, “what is this box doing?”
Distributed tracing Spans stitched across services (OpenTelemetry) Where in a request path the time went Medium (sampled) Per-request Latency across microservices

Tracing here means system tracing — kernel and application events on a single host — not the distributed request-tracing of OpenTelemetry (a related idea covered elsewhere in the observability world). They complement each other: distributed tracing tells you which service is slow; system tracing tells you why that service, on that box, is slow.

The tracing toolmap: Brendan Gregg’s landscape

The single most useful orientation to Linux tracing is the “toolmap” popularised by Brendan Gregg — a diagram of the kernel with each tracing tool drawn where it plugs in. You do not need the picture memorised; you need its lesson: the classic tools each read one narrow slice, while perf and eBPF span the whole board. Keep this table as your map of the territory:

Tool Era / mechanism Observes Overhead Best at
vmstat / iostat / mpstat / sar Counter readers (/proc, /sys) Per-resource summary counters Negligible The 60-second first glance
top / htop / pidstat /proc samplers Per-process CPU/mem Negligible Which process, roughly
strace ptrace syscall interception One process’s syscalls, verbatim Very high (10–100×) Debugging one process, not production
ltrace Library-call interception Library/function calls of one process Very high App-level call debugging
perf Sampling + tracepoints + PMCs CPU profiles, counters, events (whole system) Low (sampling) Profiling, flame graphs, PMCs
ftrace Built-in function/event tracer Kernel function flow & tracepoints Low–medium “What did the kernel do, in order”
bcc / bpftrace (eBPF) Verified in-kernel programs Almost anything, aggregated in-kernel Low (you scope it) Custom questions, latency histograms

⚠️ Note the row that ends careers: strace multiplies a process’s syscall cost by ten to a hundred times because every syscall traps out to the tracer twice. It is a superb debugging tool on a test box and a loaded gun on production — a single strace -f -p <pid> on a busy database has caused the very outage it was meant to diagnose. The eBPF equivalents (bpftrace, bcc’s syscount, trace) exist precisely so you can ask the same questions at a fraction of the cost.

The event sources: what the kernel actually exposes

Every tool above ultimately hooks one of a small, finite set of event sources. Internalising this list is the highest-leverage thing in the lesson, because once you know what the kernel exposes, every tool becomes “just an interface to these,” and you stop memorising tools and start composing them.

There are two families. Static instrumentation is placed by developers and is stable: the maintainers promise the event exists and its fields do not change — these are tracepoints (in the kernel) and USDT probes (in user programs). Dynamic instrumentation is placed by you, at runtime, anywhere: kprobes can attach to (almost) any kernel function, and uprobes to any user-space function, with no prior cooperation from the code — but nothing promises that function will still exist in the next kernel. The trade is stability versus reach.

Event source Static/Dynamic Where it fires Attach by Stable API? Example
Tracepoint Static Curated points in kernel code Name Yes — maintained sched:sched_switch, syscalls:sys_enter_openat
kprobe Dynamic Entry of (almost) any kernel function Function name No — can vanish kprobe:vfs_read, kprobe:tcp_sendmsg
kretprobe Dynamic Return of a kernel function Function name No kretprobe:vfs_read → capture retval/latency
uprobe Dynamic Entry of a user-space function Binary path + symbol No uprobe:/bin/bash:readline
uretprobe Dynamic Return of a user-space function Binary + symbol No uretprobe:libc:malloc
USDT Static Dev-placed markers in user binaries Provider:name Yes (if shipped) usdt:libc:..., usdt:/usr/bin/mysqld:query__start
perf event (PMC) Hardware CPU performance counters Event name Firm cycles, instructions, cache-misses, LLC-load-misses
perf event (software) Kernel Kernel-maintained counters Event name Yes context-switches, page-faults, cpu-clock
Timed sample perf/eBPF A timer, N times/sec per CPU Frequency Yes profile:hz:99 — the basis of CPU flame graphs

A few things become obvious once this is in your head. Prefer tracepoints over kprobes whenever a suitable one exists — they are the stable, documented contract, so your bpftrace script keeps working across kernel upgrades; drop to a kprobe only when no tracepoint covers what you need. kretprobes are how you measure latency: you stamp the time at the kprobe (entry) and subtract at the kretprobe (return). Sampling at 99 Hz (not 100) is a deliberate trick — an odd frequency avoids sampling in lockstep with periodic kernel activity that runs at round numbers, which would bias your profile. And the whole reason fentry/fexit probes were added in kernel 5.5 is that they attach to function entry/exit like kprobes but through a BPF trampoline that is dramatically cheaper — the modern default when available.

To list what a given kernel actually offers, you enumerate the sources directly:

# Every tracepoint the running kernel exposes (there are thousands)
sudo ls /sys/kernel/tracing/events/ | head
#   block  cgroup  ext4  irq  kmem  net  sched  syscalls  tcp  timer  ...

# List probes the way bpftrace sees them (wildcards allowed)
sudo bpftrace -l 'tracepoint:syscalls:sys_enter_open*'
#   tracepoint:syscalls:sys_enter_open
#   tracepoint:syscalls:sys_enter_openat
#   tracepoint:syscalls:sys_enter_openat2

# Which kernel functions can I kprobe? (filter with grep)
sudo bpftrace -l 'kprobe:tcp_*' | head
#   kprobe:tcp_sendmsg
#   kprobe:tcp_recvmsg
#   kprobe:tcp_retransmit_skb
#   ...

The scheduler tracepoints (sched:sched_switch, sched:sched_wakeup) deserve special mention because they underpin the two most valuable questions in performance work — why is a task not running? (off-CPU analysis) and how long did it wait in the run queue? (scheduler latency). Those questions connect directly to process states: a task in R is on or waiting for CPU, a task in D is blocked in the kernel on I/O. If the R/D/S state model is not yet reflexive, the Processes & Jobs: ps, top, signals & kill lesson is the prerequisite that makes scheduler tracing make sense.

perf: the Swiss Army knife of profiling

perf ships in the kernel tree and is the tool you reach for first, because it answers the most common question — “where is CPU time going?” — better than anything else, and it needs no eBPF. Install it and confirm it can talk to the counters:

# Debian / Ubuntu — perf is versioned to your kernel
sudo apt install linux-tools-common linux-tools-$(uname -r)
# RHEL / Fedora / Rocky
sudo dnf install perf

perf --version    # perf version 6.8.0

perf is really a suite of subcommands. You will use five constantly; the rest are specialist:

Subcommand What it does Canonical invocation
perf stat Count events (PMCs, software) for a command or the system perf stat -d ./app
perf top Live, top-style sampling profiler — hottest functions now perf top -g
perf record Sample and write events to perf.data perf record -F 99 -a -g -- sleep 30
perf report Interactive/textual analysis of perf.data perf report --stdio
perf script Dump raw samples (feeds flame-graph tooling) perf script > out.perf
perf list Enumerate every event this CPU/kernel supports perf list
perf trace strace-like syscall tracer, far cheaper perf trace -p <pid>
perf sched Scheduler latency analysis perf sched record / perf sched latency
perf lock / perf mem / perf c2c Lock contention, memory access, cache-line sharing specialist

perf stat: counters and the IPC verdict

perf stat is the fastest way to characterise what kind of slow you have. It reads the CPU’s performance-monitoring counters and prints them for the run:

# Count core counters for a single command
perf stat -d gzip -9 bigfile.tar
 Performance counter stats for 'gzip -9 bigfile.tar':

          3,204.61 msec task-clock                #    0.999 CPUs utilized
                17      context-switches          #    5.305 /sec
                 1      cpu-migrations            #    0.312 /sec
               182      page-faults               #   56.79 /sec
    11,438,209,911      cycles                    #    3.569 GHz
     9,014,573,022      instructions              #    0.79  insn per cycle
     1,872,004,551      branches                  #  584.16 M/sec
        61,209,338      branch-misses             #    3.27% of all branches
     2,051,884,004      L1-dcache-loads           #  640.29 M/sec
       142,880,101      L1-dcache-load-misses     #    6.96% of all L1-dcache accesses
        18,204,551      LLC-load-misses           #  cache thrash if high

       3.207805241 seconds time elapsed

The number that teaches you the most is IPC — instructions per cycle (here 0.79). Modern CPUs can retire several instructions per cycle; an IPC below ~1.0 means the core is stalling — usually waiting on memory (cache misses) or mispredicted branches — rather than doing useful work. High IPC with high CPU means you are genuinely compute-bound and the fix is a better algorithm; low IPC means the fix is data layout, cache behaviour, or fewer branch mispredicts. The supporting counters name the culprit:

Counter What it measures Healthy Red flag → look at
instructions / cycles = IPC Work done per cycle > 1.0 (often 1–4) < 0.7 → stalls (memory/branches)
cache-misses, LLC-load-misses Last-level cache misses Low relative to references High → memory-bound; fix data locality
branch-misses Mispredicted branches < ~2% > 5% → unpredictable branching
context-switches Task switches Low for compute work High → contention/oversubscription
cpu-migrations Task bounced between cores ~0 High → cache cold on every hop; pin with taskset
page-faults Virtual-memory faults Low after warm-up Growing → memory pressure, mmap churn
stalled-cycles-frontend Cycles the frontend starved Low High → I-cache / decode bound

⚠️ You may see <not supported> or <not counted> for some events — that means the counter is unavailable (common inside a VM or container without the right passthrough) or that too many events were multiplexed onto too few physical counter slots. Ask for fewer events, or run on bare metal, before you trust a partially-counted number.

perf top and perf record: finding the hot function

perf top is your instant “what is on-CPU right now?” It samples the instruction pointer across the whole system and shows the hottest functions live, updating like top:

# Live system-wide profile; -g adds call graphs (who called the hot function)
sudo perf top -g
Samples: 240K of event 'cpu-clock', 4000 Hz
Overhead  Shared Object        Symbol
  18.42%  [kernel]             [k] _raw_spin_unlock_irqrestore
  11.07%  libc.so.6            [.] __memmove_avx_unaligned_erms
   7.93%  postgres             [.] heap_page_prune
   5.16%  [kernel]             [k] copy_user_enhanced_fast_string
   ...

For a profile you can analyse offline and turn into a flame graph, you record to a file and then report:

# Sample all CPUs at 99 Hz for 30 seconds, capturing call stacks
sudo perf record -F 99 -a -g -- sleep 30
#   [ perf record: Woken up 8 times to write data ]
#   [ perf record: Captured and wrote 6.114 MB perf.data (~267k samples) ]

# Analyse: a tree of where time went, most-expensive first
sudo perf report --stdio | head -30

The -g (call-graph) capture has a crucial subtlety. To walk the stack, perf needs frame pointers, and many distro binaries are compiled with -fomit-frame-pointer, which breaks stack walking and gives you broken or truncated stacks. The fix is to choose an explicit unwind method:

--call-graph mode How it walks the stack Cost When to use
fp (frame pointer) Follows the frame-pointer chain Cheapest Only if binaries keep frame pointers
dwarf Copies stack, unwinds with DWARF debuginfo Higher (larger perf.data) Default when frame pointers are omitted
lbr Intel Last Branch Record hardware Low, shallow depth Recent Intel CPUs, shallow stacks
# When stacks look broken, force DWARF unwinding (needs debuginfo)
sudo perf record -F 99 -a --call-graph dwarf -- sleep 30

Flame graphs: reading a profile at a glance

A flame graph turns thousands of sampled stacks into one picture. It is the single most valuable artifact in performance work, and reading it is a skill worth ten minutes. The classic toolchain is Brendan Gregg’s FlameGraph scripts; modern perf can also emit one directly.

# Classic pipeline: record → dump → fold identical stacks → render SVG
git clone https://github.com/brendangregg/FlameGraph
sudo perf record -F 99 -a -g -- sleep 30
sudo perf script > out.perf
./FlameGraph/stackcollapse-perf.pl out.perf > out.folded
./FlameGraph/flamegraph.pl out.folded > flame.svg   # open in a browser

The reading rules are simple and unintuitive until stated:

Axis / feature What it means Common mistake
Width of a box Fraction of samples where that function was on the stack — i.e. on-CPU time Thinking width is call count (it is not)
Y-axis (height) Stack depth: parent below, child above Reading top-to-bottom as time order
X-axis (order) Alphabetical / merge order — not time Reading left-to-right as a timeline
Plateaus (wide flat tops) Functions burning CPU directly Ignoring a wide leaf because its name is dull
Colour Usually just hue for contrast (warm palette) Reading meaning into colour by default

The technique: scan across the top for the widest plateaus — those are the functions actually spending CPU. A tall, narrow tower is a deep call chain that costs little; a short, wide box near the top is where your machine lives. On-CPU flame graphs answer “where is CPU going?”; the mirror image, an off-CPU flame graph (built from scheduler tracepoints via eBPF), answers “where is my thread blocked and waiting?” — the two together explain almost every latency problem.

ftrace: the kernel’s built-in tracer

perf samples; ftrace traces — it can record every entry into every kernel function, in order, with timing. It is built into the kernel and driven entirely through a virtual filesystem, so it needs nothing installed. That filesystem is tracefs, mounted at /sys/kernel/tracing (older systems expose it under /sys/kernel/debug/tracing).

# tracefs is the whole interface — everything is a file you echo into
sudo -i
cd /sys/kernel/tracing

cat available_tracers
#   timerlat osnoise hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop

# Turn on the function tracer, look, then turn it OFF again
echo function > current_tracer
cat trace | head
echo nop > current_tracer          # ⚠️ ALWAYS reset to nop when done

⚠️ Two ftrace footguns bite hard. First, the plain function tracer instruments every kernel function and can add serious overhead system-wide — always narrow it with set_ftrace_filter and always set current_tracer back to nop when finished, or you leave a permanent tax on the box. Second, reading trace gives a static snapshot while trace_pipe consumes the live stream (and blocks) — mixing them up is why “my trace is empty” or “my terminal hung.” Here are the control files that matter:

tracefs file Purpose
current_tracer Which tracer is active (function, function_graph, nop, …)
available_tracers List of tracers this kernel supports
set_ftrace_filter Restrict function tracing to matching functions (e.g. vfs_*)
set_ftrace_pid Trace only a specific PID
set_graph_function For function_graph: trace only this call subtree
trace Static snapshot of the ring buffer (read repeatedly)
trace_pipe Live, consuming stream (blocks until events arrive)
tracing_on 1/0 master switch to pause/resume without reconfiguring
events/<subsys>/<event>/enable Turn an individual tracepoint on/off
available_filter_functions Every function you may filter/trace

The function_graph tracer is the one that produces something beautiful — an indented, C-like call graph with per-function durations, so you can see not just what the kernel called but how long each call took:

echo function_graph > current_tracer
echo vfs_read > set_graph_function     # only the vfs_read subtree
cat trace | head -20
echo nop > current_tracer              # reset
 CPU  DURATION                  FUNCTION CALLS
  0)               |  vfs_read() {
  0)               |    rw_verify_area() {
  0)   0.312 us    |      security_file_permission();
  0)   0.945 us    |    }
  0)   2.104 us    |    __vfs_read() {
  0)   1.560 us    |      ext4_file_read_iter();
  0)   4.220 us    |    }
  0)   7.918 us    |  }

trace-cmd: ftrace without the file wrangling

Echoing into a dozen tracefs files is error-prone. trace-cmd is the friendly front-end that does it for you and records to a trace.dat file you can analyse or move to another machine:

# Debian/Ubuntu: sudo apt install trace-cmd   |   RHEL/Fedora: sudo dnf install trace-cmd

# Record the vfs_read call subtree with function_graph for 1 second
sudo trace-cmd record -p function_graph -g vfs_read sleep 1
sudo trace-cmd report | head

# Record specific tracepoints across the whole system
sudo trace-cmd record -e sched:sched_switch -e block:block_rq_issue sleep 5
sudo trace-cmd report | head
trace-cmd command What it does
trace-cmd list List available tracers, events, and plugins
trace-cmd record -p <tracer> Record with a tracer (function, function_graph)
trace-cmd record -e <event> Record one or more tracepoints
trace-cmd record -g <fn> Limit function_graph to a function subtree
trace-cmd report Human-readable dump of trace.dat
trace-cmd start / stop / reset Control an in-place ftrace session
kernelshark GUI to visualise a trace.dat timeline

Where does ftrace fit next to perf and eBPF? Use ftrace/trace-cmd when you want to see the ordered flow of kernel function calls and their durations — “what did the kernel do, step by step, when I ran this?” Use perf for statistical profiling and counters. Use eBPF when you want to aggregate a custom answer in-kernel — a histogram, a per-process count — rather than a firehose of individual events.

eBPF, explained properly

Here is the idea that changed Linux observability. eBPF (extended Berkeley Packet Filter) lets you load a small program into the running kernel that executes at an event source — a tracepoint, a kprobe, a perf sample — runs in kernel context at native speed, and communicates results back to user space through maps. It is a safe, sandboxed virtual machine inside the kernel that you can program on the fly. That one capability is why the same technology now underpins observability and high-performance networking and runtime security.

The reason it is safe enough to run on production — and the reason it is not just “kernel modules with extra steps” — is the verifier. When you load a program, the kernel does not trust it. An in-kernel verifier statically analyses every possible path through the program before it is allowed to run and proves a set of safety properties: the program terminates (originally: no loops at all; since kernel 5.3, bounded loops are permitted), it never reads uninitialised memory, it never accesses memory outside the regions it is granted, and it stays within an instruction budget (1 million since 5.2). A program that fails any check is rejected at load time and never executes. Only a verified program is then JIT-compiled to native machine code and attached to its hook. This is why a buggy trace cannot panic or hang the kernel — a guarantee no kernel module can make.

The flow from “you type a command” to “you see a histogram” is worth seeing as a picture, because every eBPF tool — bcc, bpftrace, Cilium, Parca — is a variation on it.

How an eBPF trace works, drawn left to right in five zones: in USER SPACE a tool (a bpftrace one-liner or bcc script, drawn as an eye) compiles a small BPF program to bytecode (a box) and loads it with the bpf() syscall; the VERIFIER (a shield) statically proves the program is safe — it terminates, is bounded, and touches no illegal memory — before anything runs; the accepted, JIT-compiled program attaches to KERNEL HOOKS (chips) which are dynamic kprobes and static tracepoints, or a sampled perf event; each time a hook fires the program aggregates into a BPF MAP (a pipeline) as a count or a power-of-two histogram entirely in-kernel; finally back in USER SPACE the result is drained from a ring buffer and printed as a histogram of microseconds versus count (a check). Six numbered badges mark that event sources are the hooks you attach to, that the verifier guarantees safety by rejecting bad programs at load time, that running in the kernel makes overhead tiny because you ship a finished histogram not every event, that maps are the only channel carrying data out, that CO-RE with BTF lets one compiled object run across many kernel versions, and that CAP_BPF gates who is allowed to trace at all

Reading it left to right: your tool compiles a tiny program and hands it to the kernel with the bpf() syscall; the verifier proves it safe and the kernel JITs it; it attaches to a hook; every time the hook fires, the program runs in kernel context and updates a map — a hash keyed by PID, a per-CPU counter, a histogram — so millions of events become one compact summary without leaving the kernel; and user space reads that map on a timer (or drains a ring buffer) to print the answer. The genius is aggregation-in-kernel: you pay for one finished histogram, not a context switch per event.

Two concepts make the rest of the ecosystem legible. Program types determine what a program may attach to and what context it receives; map types determine how it stores and shares data.

eBPF program type Attaches to Typical use
kprobe / kretprobe Kernel function entry/return Latency, argument capture (bcc/bpftrace)
tracepoint / raw_tracepoint Static kernel tracepoints Stable-API tracing
fentry / fexit Function entry/exit via trampoline (5.5+) Cheaper kprobe replacement
perf_event perf PMCs / timed samples CPU profiling, PMC-driven tracing
uprobe / usdt User-space functions/markers App-level tracing
xdp Earliest RX in the NIC driver DDoS drop, load-balancing (Katran)
tc (sched_cls) Traffic-control ingress/egress Policy, shaping (Cilium)
cgroup_* cgroup hooks Per-container network/resource policy
lsm Linux Security Module hooks (5.7+) Runtime security enforcement
sk_* / sockops Socket lifecycle Socket-level redirection, observability
eBPF map type Shape Used for
BPF_MAP_TYPE_HASH Key → value hash Per-PID / per-key aggregation
BPF_MAP_TYPE_ARRAY Index → value Fixed slots, config, counters
PERCPU_HASH / PERCPU_ARRAY One copy per CPU Lock-free high-frequency counting
BPF_MAP_TYPE_HISTOGRAM (via helpers) Power-of-two buckets Latency/size histograms (hist())
STACK_TRACE Stack-id → stack Flame graphs, off-CPU stacks
PERF_EVENT_ARRAY Per-CPU perf channel Streaming events to user space (pre-5.8)
RINGBUF Shared ring buffer (5.8+) Efficient, ordered event streaming
LRU_HASH Hash with eviction Bounded-memory tracking

What the verifier rejects — worth knowing, because you will hit it when you write your own programs, and the error messages are famously cryptic:

The verifier rejects Why How you hit it
Unbounded loops Must prove termination A while with a runtime bound the verifier can’t prove
Out-of-bounds memory Safety of kernel memory Indexing an array without a bounds check first
Uninitialised stack reads No leaking kernel data Reading a variable before assigning it
Reading arbitrary pointers Kernel memory safety Dereferencing without bpf_probe_read
Too many instructions Bounded runtime Program exceeds the 1M-instruction budget
Unprivileged pointer leaks Info-leak prevention Returning a pointer value to user space

Finally, the feature that made eBPF deployable across a real fleet: CO-RE (Compile Once, Run Everywhere). Early bcc shipped C source and recompiled it on every host against that host’s kernel headers, dragging a full LLVM/Clang toolchain onto production. Modern libbpf + BTF (BPF Type Format, the kernel’s own type information at /sys/kernel/btf/vmlinux) records where the program reads each kernel struct field and relocates those offsets at load time to match the running kernel — so one small compiled object runs unchanged across kernel versions. That is what turned eBPF from a lab tool into infrastructure. A short timeline of the milestones you will hear referenced:

Kernel Year Milestone
3.18 2014 eBPF core + bpf() syscall
4.1–4.9 2015–16 kprobes, uprobes, tracepoints, stack traces, profiling
4.18 2018 BTF introduced (foundation for CO-RE)
5.2–5.3 2019 1M-instruction limit; bounded loops allowed
5.5 2020 fentry/fexit BPF trampolines (cheap function tracing)
5.7 2020 BPF LSM (runtime security enforcement)
5.8 2020 CAP_BPF; ring buffer map (BPF_MAP_TYPE_RINGBUF)

bcc: the eBPF tool zoo

You rarely write eBPF C by hand. bcc (the BPF Compiler Collection) ships a large collection of ready-made, single-purpose tools — most named <thing>snoop or <thing>latency or <thing>stat — that each answer one sharp question. Install the collection and note the naming quirk:

# Debian / Ubuntu — tools are SUFFIXED with -bpfcc and live on $PATH
sudo apt install bpfcc-tools linux-headers-$(uname -r)
execsnoop-bpfcc        # note the suffix

# RHEL / Fedora / Rocky — package bcc-tools, unprefixed, in /usr/share/bcc/tools
sudo dnf install bcc-tools
/usr/share/bcc/tools/execsnoop

The essential dozen — commit these to memory, because they cover most real investigations:

bcc tool Question it answers Event source
execsnoop What new processes are being exec’d (and by whom)? execve tracepoint
opensnoop What files is everything opening (and which fail)? open/openat
biolatency What is the distribution of block-I/O latency? block tracepoints
biosnoop Per-I/O: which process, which disk, how long? block tracepoints
tcpconnect Who is making outbound TCP connections, to where? tcp_v4_connect kprobe
tcpaccept Who is receiving inbound TCP connections? inet_csk_accept
tcpretrans Which connections are retransmitting (network pain)? tcp_retransmit_skb
runqlat How long do tasks wait in the run queue (scheduler latency)? sched tracepoints
profile Where is CPU time going? (samples stacks → flame graph) perf timed samples
offcputime Where are threads blocked off-CPU and for how long? sched_switch
cachestat What is the page-cache hit/miss ratio? mm functions
ext4slower / xfsslower Which filesystem ops exceeded N ms? fs functions

Two produce output you will read constantly. execsnoop is a live feed of every process launched — the fastest way to catch a cron job, a runaway fork, or a mystery subprocess:

sudo execsnoop-bpfcc
PCOMM            PID    PPID   RET ARGS
sh               21982  21981    0 /bin/sh -c /opt/app/backup.sh
tar              21983  21982    0 /usr/bin/tar czf /backup/app.tgz /srv/app
gzip             21984  21983    0 /usr/bin/gzip
node             21990  1442     0 /usr/bin/node /srv/app/worker.js

biolatency aggregates disk-I/O latency into a power-of-two histogram in-kernel — the canonical demonstration of why eBPF matters, because it summarises thousands of I/Os into one compact picture at trivial cost:

sudo biolatency-bpfcc 10 1     # 10-second window, 1 sample
     usecs               : count     distribution
       128 -> 255         : 3        |                                        |
       256 -> 511         : 45       |***                                     |
       512 -> 1023        : 380      |*****************                       |
      1024 -> 2047        : 602      |****************************************|
      2048 -> 4095        : 189      |************                            |
      4096 -> 8191        : 44       |**                                      |

A tidy single hump near 1–2 ms is a healthy SSD; a second hump out at tens of milliseconds is the signature of a device that occasionally stalls — a saturated queue or a failing disk. When you need a broader menu than the fixed tools, bcc ships multi-tools that point the same machinery at arbitrary functions:

bcc multi-tool What it does
funccount Count how often a function/tracepoint fires
funclatency Histogram of how long a function takes
stackcount Count unique stack traces reaching a function
trace Print custom per-event lines with args (a targeted mini-strace)
argdist Summarise a function’s argument or return-value distribution

bpftrace: the awk of tracing

bcc tools are pre-built. bpftrace is the language you use when the question is yours — an awk-like, one-line-to-one-screen tracing language that compiles your script to eBPF on the spot. It is the tool you will reach for most, because a useful investigation is often a single line you type from memory. The anatomy of a bpftrace program mirrors awk: a probe (where to attach), an optional filter (/predicate/), and an action ({ ... }) that usually updates a map named with a leading @.

probe /filter/ { action }

kprobe:vfs_read /comm == "nginx"/ { @count = count(); }
└── probe ────┘ └── filter ─────┘ └── action ──────┘

The probe types you attach to are exactly the event sources from earlier, in bpftrace syntax:

bpftrace probe Fires on
kprobe:fn / kretprobe:fn Kernel function entry / return
tracepoint:subsys:event Static kernel tracepoint
uprobe:/path:fn / uretprobe:... User-space function entry / return
usdt:/path:provider:name USDT marker in a user binary
profile:hz:99 Timed sample, 99×/sec per CPU (profiling)
interval:s:1 Once per second (for periodic printing)
software:faults:1 / hardware:cache-misses:... perf software / hardware events
BEGIN / END Program start / end (setup, final print)

Inside an action you have a set of builtins — the runtime context of the event. These are the vocabulary of every one-liner:

Builtin Meaning
pid / tid Process / thread ID
uid / gid User / group ID
comm Process name (e.g. nginx)
nsecs Nanoseconds since boot (for latency: stamp and subtract)
elapsed Nanoseconds since the program started
cpu Current CPU number
curtask Pointer to the current task_struct
arg0, arg1, … Kprobe function arguments (raw)
args Tracepoint arguments, as named fields (args->filename)
retval Return value (in a kretprobe/kretfunc)
kstack / ustack Kernel / user-space stack trace
probe Full name of the firing probe
func Name of the function being probed
username Resolves uid to a name

And the map functions — the aggregations that run in-kernel, which is what keeps bpftrace cheap:

Map function Produces
count() A running count
sum(v) / avg(v) / min(v) / max(v) Arithmetic aggregates
hist(v) Power-of-two histogram (log2 buckets)
lhist(v, min, max, step) Linear histogram with fixed-width buckets
stats(v) Count, average and total together
delete(@map[key]) Remove one entry (essential for latency maps)
clear(@map) / zero(@map) Reset a map
print(@map) Print a map on demand

One-liners that earn their keep

Here are six that solve real problems. Type them, watch them, adapt them — this is the fluency the lesson is really teaching.

# 1. Which processes make the most syscalls? (system call pressure)
sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
@[sshd]: 47
@[bash]: 112
@[node]: 18340
@[postgres]: 204817          # postgres dominates — investigate it next
# 2. Which files are being opened, live, with the opening process
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
    printf("%-16s %s\n", comm, str(args->filename)); }'
nginx            /var/www/html/index.html
nginx            /var/www/html/favicon.ico
node             /srv/app/config.json
node             /srv/app/node_modules/.../huge-lib.js   # a hot require loop?
# 3. Distribution of VFS read latency (kprobe entry + kretprobe return)
sudo bpftrace -e '
  kprobe:vfs_read { @start[tid] = nsecs; }
  kretprobe:vfs_read /@start[tid]/ {
      @ns = hist(nsecs - @start[tid]); delete(@start[tid]); }'
@ns:
[256, 512)          812 |@@@@@@@@@                                            |
[512, 1K)          2451 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@                          |
[1K, 2K)           4210 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[2K, 4K)           1120 |@@@@@@@@@@@@@                                        |
[4K, 8K)             98 |@                                                    |
# 4. TCP retransmits by process — the fingerprint of network trouble
sudo bpftrace -e 'kprobe:tcp_retransmit_skb { @[comm] = count(); }'

# 5. Block-I/O size distribution — are you doing tiny random or big sequential I/O?
sudo bpftrace -e 'tracepoint:block:block_rq_issue { @bytes = hist(args->bytes); }'

# 6. New process executions with their command line (a live audit log)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve {
    printf("%s[%d] -> %s\n", comm, pid, str(args->filename)); }'

The latency-histogram pattern in #3 is the one to internalise, because it generalises to anything: stamp nsecs into a per-thread map at the entry probe, subtract at the return probe, feed the delta to hist(), and delete() the entry so the map does not grow without bound. Swap vfs_read for tcp_sendmsg, a filesystem function, or a uprobe on your own application function, and you have a bespoke latency profiler in one line.

⚠️ The warnings that keep this safe on production. A probe on an extremely hot path — kprobe:__kmalloc, a per-packet function, uretprobe on malloc in a busy allocator — fires millions of times a second and its own overhead can slow or destabilise the box. Attaching a uprobe to a shared library like libc instruments every process using it, not just the one you care about. Always start with a filter (/comm == "..."/ or /pid == .../), prefer counts and histograms (aggregated in-kernel) over per-event printf (which streams every event to user space), and test on a canary before you trace the primary. Reach for tightly-scoped tracepoints before broad kprobes.

A methodology: “what is this box doing, and why is it slow?”

Tools are worthless without a method, or you flail. The discipline that separates a five-minute diagnosis from a two-hour one is to work top-down by resource, let the numbers name a single bottleneck, and only then drill into it with the sharpest tool. Two frameworks structure this.

The USE method (Brendan Gregg) says: for every resource, check Utilization, Saturation, and Errors. The RED method (services) says: for every service, check Rate, Errors, and Duration. USE finds which resource is the problem; RED describes how the service feels. This lesson’s contribution is the third column — the tracing tool that answers each cell when the cheap counter is not enough:

Resource Utilization (cheap) Saturation (cheap) Deep tool when you need why
CPU mpstat, top %us/%sy run-queue length, runqlat perf top / perf record → flame graph
Memory free -h available swap in/out (vmstat si/so) perf record -e page-faults, cachestat
Disk iostat -xz %util iostat aqu-sz, biolatency biolatency, biosnoop, ext4slower
Network sar -n DEV tcpretrans, drops tcpconnect, tcplife, tcpretrans
Scheduler load average runqlat tail offcputime, perf sched latency

The full “USE across CPU, memory, disk and network, change one thing, re-measure” workflow — the cheap-counter layer that sits above this tracing layer — is the subject of the Performance Analysis & Tuning: CPU, Memory, Disk I/O, the USE Method, tuned & sysctl lesson. Deep tracing is where you go when USE has pointed at a resource but the counters cannot say which code path.

The move that makes you fast is the drill-down ladder: never start deep. Start with a system glance, descend one rung only when the current rung points somewhere specific, and stop the moment you have the answer.

Rung Tool You learn Descend when
1. Glance uptime, top, vmstat 1 Is it CPU, memory, I/O, or waiting? A resource is clearly hot
2. Per-resource mpstat, iostat -xz, sar -n DEV Which device/CPU/interface One stands out
3. Profile perf topperf record → flame graph Which function burns CPU A function dominates but you need context
4. Targeted trace bcc tool (biolatency, execsnoop, runqlat) The distribution/culprit for that resource You need a custom cut
5. Bespoke bpftrace one-liner The exact answer to your question — (bottom of the ladder)

A concrete walk. The pager fires: API p99 latency has tripled. Rung 1, top shows CPU only 40% busy but load high — something is waiting, not computing. Rung 2, iostat -xz 1 shows one NVMe device at 95% %util with a deep queue. Rung 3 is skipped (not on-CPU). Rung 4, biolatency-bpfcc shows a bimodal histogram — most I/O at 1 ms, a fat tail at 20–40 ms. Rung 5, biosnoop (or bpftrace on block_rq_issue/block_rq_complete) names the process behind the slow I/O — a nightly report job doing synchronous random reads against the API database’s disk. You did not guess once; each rung pointed at the next, and you stopped with a name. This drill-down is one branch of the broader outage playbook in Systematic Linux Troubleshooting: a repeatable method for boot, disk, network & permission failures — performance is one of its branches, and tracing is how you resolve it.

The reference you will actually keep open mid-incident — which tool answers which question:

The question Reach for One-liner / invocation
Where is CPU going right now? perf top sudo perf top -g
Where is CPU going, as a picture? perf record → flame graph perf record -F 99 -a -g -- sleep 30
Why is my thread not running? offcputime (bcc) sudo offcputime-bpfcc 5
How long do tasks wait to run? runqlat (bcc) sudo runqlat-bpfcc
What new processes are spawning? execsnoop (bcc) sudo execsnoop-bpfcc
What files are being opened / failing? opensnoop (bcc) sudo opensnoop-bpfcc
What is disk-I/O latency? biolatency (bcc) sudo biolatency-bpfcc 10 1
Which process/file causes slow I/O? biosnoop (bcc) sudo biosnoop-bpfcc
Who connects to whom over TCP? tcpconnect (bcc) sudo tcpconnect-bpfcc
Is the network retransmitting? tcpretrans (bcc) sudo tcpretrans-bpfcc
Which process makes the most syscalls? bpftrace bpftrace -e 't:raw_syscalls:sys_enter{@[comm]=count();}'
Latency of any function? bpftrace entry/return hist(nsecs - @start[tid])
Kernel function call flow + timing? ftrace / trace-cmd trace-cmd record -p function_graph -g <fn>
Is the CPU stalling (IPC)? perf stat perf stat -d <cmd>

Permissions, overhead and the eBPF ecosystem

Two operational realities gate all of this. First, privilege: loading eBPF programs and reading most perf events is not something an unprivileged user may do, for the obvious reason that a program running in the kernel is a security boundary. Historically you needed root (CAP_SYS_ADMIN); since kernel 5.8 the capability is split more finely, and two sysctls decide what is possible at all:

Control Governs Typical value Notes
CAP_BPF Loading programs, creating maps granted to tracing tools/root Split out of CAP_SYS_ADMIN in 5.8
CAP_PERFMON perf events, kprobes, profiling with CAP_BPF for tracing The “observe” capability
CAP_NET_ADMIN Networking programs (XDP/tc) for network eBPF Separate from tracing
kernel.unprivileged_bpf_disabled Whether non-root may load any BPF 1/2 (disabled) on most distros sysctl — hardening default
kernel.perf_event_paranoid How much perf unprivileged users get 2 mainline; higher on Debian/Ubuntu Lower = more access; -1 = all
kernel.kptr_restrict Whether /proc/kallsyms shows addresses 1/2 Symbol resolution for stacks

In practice you trace as root (or sudo), or you run a tool that has been granted CAP_BPF+CAP_PERFMON. On a locked-down box you may need to lower kernel.perf_event_paranoid to profile as a normal user — a change you make deliberately and revert, because it widens what any local user can observe. These knobs are kernel tunables set through sysctl and /etc/sysctl.d/, the persistent-versus-live distinction that governs every kernel parameter: sysctl -w changes the running kernel now, a file under /etc/sysctl.d/ makes it survive reboot.

Second, overhead and safety — the discipline that lets you do this on production at all:

Tool / pattern Typical overhead Production caution
perf stat Negligible Safe; counters are free
perf record -F 99 Low (a few %) Fine; higher -F costs more
strace / ltrace 10–100× on the target ⚠️ Never on a hot production process
bpftrace count/hist() Low (in-kernel aggregation) Safe when scoped
bpftrace per-event printf Scales with event rate ⚠️ A hot probe floods user space
kprobe on a hot path Can be significant ⚠️ __kmalloc, per-packet → measure first
uprobe on libc Hits every user of the lib ⚠️ Scope by binary/pid
ftrace function (unfiltered) System-wide tax ⚠️ Filter, and reset to nop

Finally, the wider ecosystem, because eBPF long ago outgrew ad-hoc tracing and now runs production networking and security. You will meet these names:

Project Domain What it is
Cilium Networking eBPF-based Kubernetes CNI: L3–L7 policy, load-balancing, no iptables
Hubble Observability Cilium’s network-flow visibility layer
Pixie Observability Auto-instrumented, no-code app observability via eBPF
Parca Profiling Continuous, always-on fleet-wide CPU profiling
Falco Security Runtime threat detection from syscall/eBPF events
Katran Networking Meta’s XDP-based L4 load balancer
Tetragon Security eBPF runtime enforcement and observability
bpftop Observability Live top-style view of loaded BPF programs and their cost
bpftool Tooling The kernel’s own CLI to inspect/load programs and maps

bpftop deserves a special mention: it shows every eBPF program currently loaded and how much CPU each is costing — how you audit whether your own tracing, or someone else’s agent, is adding overhead. bpftool prog show and bpftool map dump are the low-level equivalents.

Hands-on lab

This lab runs on any Linux VM or WSL2 with a recent kernel (5.8+ recommended). ⚠️ Prefer a throwaway VM, not a production box. In Docker you need --privileged and a host kernel that exposes tracefs, so a real VM is simpler. Everything here is read-only observation — nothing is destroyed — but you are loading kernel programs, so use a machine you can reboot without consequence.

Step 1 — Install the toolchain.

# Debian / Ubuntu
sudo apt update
sudo apt install -y linux-tools-common linux-tools-$(uname -r) \
    bpfcc-tools bpftrace trace-cmd linux-headers-$(uname -r)
# RHEL / Fedora / Rocky
# sudo dnf install -y perf bcc-tools bpftrace trace-cmd kernel-devel

What just happened: you installed perf, the bcc tool zoo, bpftrace, and trace-cmd. On Debian the bcc tools carry a -bpfcc suffix.

Step 2 — Confirm the kernel is eBPF/BTF-ready.

ls /sys/kernel/btf/vmlinux && echo "BTF present — CO-RE will work"
uname -r
sudo bpftrace -e 'BEGIN { printf("bpftrace works\n"); exit(); }'

What just happened: the presence of /sys/kernel/btf/vmlinux confirms the kernel ships BTF; the bpftrace line proves you can load a program. If the last command errors with a permission or paranoid message, note it — Step 9 addresses it.

Step 3 — Characterise a workload with perf stat.

# Generate CPU work and read the counters
perf stat -d openssl speed -seconds 3 aes-256-cbc 2>/dev/null

What just happened: you saw cycles, instructions and IPC. A crypto benchmark is compute-bound, so IPC should be high (often > 2) — contrast that later with a memory-bound workload where IPC drops below 1.

Step 4 — Find the hot function live.

# In one terminal, create sustained CPU load
yes > /dev/null &
# In another, watch the profile
sudo perf top -g
#   ... press 'q' to quit; then:
kill %1

What just happened: perf top showed you the hottest on-CPU symbols in real time — yes and the kernel write path should dominate.

Step 5 — Capture a profile and fold it toward a flame graph.

sudo perf record -F 99 -a -g -- sleep 10
sudo perf report --stdio | head -25
# Optional flame graph:
# git clone https://github.com/brendangregg/FlameGraph
# sudo perf script | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl > flame.svg

What just happened: you recorded 10 seconds of system-wide samples and read the call-graph report. The optional two lines turn it into an SVG flame graph you open in a browser.

Step 6 — Trace kernel function flow with ftrace via trace-cmd.

sudo trace-cmd record -p function_graph -g do_sys_openat2 sleep 1
sudo trace-cmd report | head -20

What just happened: you captured the timed call subtree under the openat path — the kernel’s own step-by-step flow with per-function durations.

Step 7 — Run three bcc tools.

sudo execsnoop-bpfcc &        # watch new processes; run some commands in another shell
sleep 5; kill %1
sudo opensnoop-bpfcc -d 3     # 3 seconds of file opens
sudo biolatency-bpfcc 5 1     # 5-second disk-latency histogram (generate I/O: sudo dd if=/dev/zero of=/tmp/t bs=1M count=200 oflag=direct)

What just happened: you saw live process launches, the files everything opens (watch the ERR column for failures), and a real in-kernel latency histogram of your disk. ⚠️ dd ... oflag=direct writes a 200 MB temp file; delete it afterward with rm /tmp/t.

Step 8 — Write your own bpftrace one-liners.

# Syscall pressure by process
sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'   # Ctrl-C to print

# Your own latency histogram of vfs_read
sudo bpftrace -e '
  kprobe:vfs_read { @s[tid] = nsecs; }
  kretprobe:vfs_read /@s[tid]/ { @us = hist((nsecs - @s[tid]) / 1000); delete(@s[tid]); }'

What just happened: you built two custom tracers from nothing — a per-process syscall counter and a microsecond latency histogram — the exact pattern you will adapt to real incidents.

Step 9 — Inspect what is loaded, and the permission knobs.

sudo bpftool prog show | head          # every BPF program currently attached
sysctl kernel.unprivileged_bpf_disabled kernel.perf_event_paranoid
# If unprivileged profiling is blocked and you accept the risk on this VM:
# sudo sysctl kernel.perf_event_paranoid=1      # live only; revert or persist via /etc/sysctl.d/

What just happened: you listed loaded programs with bpftool and read the two sysctls that gate access. You saw the live-versus-persistent split that governs every kernel tunable.

Step 10 — Clean up.

# Make sure no ftrace tracer is left running (belt-and-braces)
echo nop | sudo tee /sys/kernel/tracing/current_tracer
rm -f /tmp/t

What just happened: you reset ftrace to nop so no residual tracing overhead is left on the box — the single most important cleanup habit in this whole lesson.

Common mistakes and troubleshooting

Symptom Likely cause Fix
perf record -g stacks are broken/[unknown] Binaries built with -fomit-frame-pointer Use --call-graph dwarf (needs debuginfo) or lbr on Intel
perf stat shows <not supported>/<not counted> PMCs unavailable (VM/container) or event multiplexing Run on bare metal; request fewer events
bpftrace: ERROR: Permission denied / paranoid Not root, or perf_event_paranoid too high sudo; lower kernel.perf_event_paranoid deliberately
bcc tool: Unable to find kernel headers linux-headers / kernel-devel not installed Install headers matching uname -r (bcc only)
bpftrace one-liner runs but prints nothing Wrong probe name, or an over-strict filter List with bpftrace -l 'pattern'; relax the /filter/
The box got slower when you started tracing A kprobe on a hot path, or per-event printf Scope by pid/comm; switch to count()/hist(); detach
ftrace “empty” or the terminal hangs Read trace (snapshot) vs trace_pipe (blocking) Use trace for a snapshot; trace_pipe streams live
Tracing overhead persisted after you quit Left current_tracer as function echo nop > /sys/kernel/tracing/current_tracer
str(args->filename) shows garbage Reading a user pointer without a safe copy In tracepoints use str(args->...); for kprobes copy properly
Symbols show as raw addresses kptr_restrict/missing symbols Check kernel.kptr_restrict; install debuginfo

The three gotchas that cause the most lost time deserve prose.

Broken stacks are a build problem, not a perf bug. A flame graph that is a flat sea of [unknown] boxes almost always means the profiled code was compiled without frame pointers, so perf cannot walk the stack the cheap way. The fix is not more flags on the workload — it is telling perf to unwind differently: --call-graph dwarf unwinds using DWARF debug information (install the -dbgsym/-debuginfo packages), or --call-graph lbr uses Intel’s hardware branch records. The industry is slowly shifting back to shipping frame pointers by default because this cost so many people so much time; until then, reach for dwarf.

The tracer can be the outage. Worth saying twice: a probe on a sufficiently hot function fires so often that the instrumentation itself consumes the machine. strace on a busy process is the classic; a naive bpftrace -e 'kprobe:__kmalloc { @[comm] = count(); }' on an allocation-heavy server is the modern equivalent. The discipline is to estimate the event rate first (a quick funccount of the tracepoint), always filter to the one process you care about, and prefer in-kernel aggregation so you are not paying a user-space round trip per event — with bpftop showing you, live, what your own program costs.

Snapshot versus stream in ftrace. Beginners echo a tracer into current_tracer, cat trace, see nothing, and conclude ftrace is broken. Usually the events already scrolled past, or they wanted the live stream. trace is a static snapshot of the ring buffer you can read again and again; trace_pipe is a live, consuming stream that blocks until events arrive and empties the buffer as you read. Choose deliberately: trace to inspect what just happened, trace_pipe to watch it happen — and reset current_tracer to nop when done, or a forgotten function tracer becomes a silent, system-wide tax that outlives your session.

Cheat-sheet

Command What it does
perf stat -d <cmd> Counters + IPC + cache stats for a command
perf top -g Live system-wide profiler with call graphs
perf record -F 99 -a -g -- sleep 30 Sample all CPUs 30 s with stacks → perf.data
perf record --call-graph dwarf ... Record with DWARF stack unwinding
perf report --stdio Textual analysis of perf.data
perf script | stackcollapse-perf.pl | flamegraph.pl > f.svg Build a flame graph
perf list Every event this CPU/kernel supports
perf trace -p <pid> Cheap strace-like syscall trace
cd /sys/kernel/tracing Enter tracefs (ftrace’s interface)
echo function_graph > current_tracer Turn on the call-graph tracer
echo nop > current_tracer ⚠️ Turn tracing OFF — always do this
trace-cmd record -p function_graph -g <fn> sleep 1 Record a function subtree
trace-cmd record -e <subsys:event> sleep 5 Record tracepoints
execsnoop-bpfcc Live new-process feed
opensnoop-bpfcc Live file-open feed (watch ERR)
biolatency-bpfcc 10 1 Disk-I/O latency histogram
biosnoop-bpfcc Per-I/O: process, disk, latency
runqlat-bpfcc Scheduler run-queue latency histogram
offcputime-bpfcc 5 Where threads block off-CPU
tcpconnect-bpfcc / tcpretrans-bpfcc TCP connections / retransmits
profile-bpfcc -F 99 30 CPU profiler (bcc) → flame graph
bpftrace -l 'pattern' List matching probes
bpftrace -e 't:raw_syscalls:sys_enter{@[comm]=count();}' Syscalls by process
bpftrace -e 'k:fn{@s[tid]=nsecs;} kr:fn/@s[tid]/{@=hist(nsecs-@s[tid]);delete(@s[tid]);}' Latency histogram of any function
bpftool prog show / bpftool map dump Inspect loaded programs / maps
bpftop Live cost of loaded BPF programs
sysctl kernel.perf_event_paranoid Read the perf-access gate

Interview and exam questions

Q: What is the fundamental difference between metrics and tracing, and why can’t you fix a performance problem with metrics alone? A: Metrics are pre-aggregated numbers over time — they are excellent at detecting that something changed and when, but they discard the per-event detail needed to explain why. Tracing captures individual events (syscalls, function calls, I/O) by instrumenting the running system, so it can attribute a problem to a specific code path, argument, or device. You detect with metrics and diagnose with tracing.

Q: Explain the eBPF verifier and why it makes eBPF safe to run in production. A: Before the kernel runs a loaded eBPF program, the verifier statically analyses every possible execution path and proves the program terminates (no unbounded loops; bounded loops since 5.3), reads no uninitialised memory, accesses no memory outside its granted regions, and stays within an instruction budget. A program failing any check is rejected at load time and never executes, so a buggy trace cannot panic or hang the kernel — a guarantee a kernel module cannot make. Verified programs are then JIT-compiled to native code.

Q: You run perf stat and see an IPC of 0.6. What does that tell you, and what do you check next? A: IPC (instructions per cycle) below ~1.0 means the CPU is stalling rather than retiring work — usually waiting on memory or mispredicted branches. Next, look at cache-misses/LLC-load-misses (memory-bound → improve data locality) and branch-misses (unpredictable branching). A high IPC would instead mean you are genuinely compute-bound and need a better algorithm, not a memory-layout fix.

Q: What is a kprobe, how does it differ from a tracepoint, and when should you prefer each? A: A kprobe is dynamic instrumentation that can attach to (almost) any kernel function by name at runtime, with no cooperation from the code — but nothing guarantees that function survives a kernel upgrade. A tracepoint is static instrumentation the maintainers placed and promise to keep stable. Prefer a tracepoint whenever one covers your need (portable across kernels); drop to a kprobe only when no tracepoint exposes what you want.

Q: How do you measure the latency of a kernel function using bpftrace, and why is delete() important? A: Stamp nsecs into a per-thread map at the entry probe and subtract at the return probe: kprobe:fn { @s[tid]=nsecs; } kretprobe:fn /@s[tid]/ { @=hist(nsecs-@s[tid]); delete(@s[tid]); }. The delete() removes the per-thread entry after use so the map does not grow without bound — a leak that, on a busy function, would consume kernel memory over time.

Q: A junior engineer runs strace -f -p <pid> on the busiest process in production to debug slowness, and the box gets worse. Explain. A: strace uses ptrace and traps out of the process twice per syscall to the tracer, multiplying syscall cost by roughly 10–100×. On an already-busy process this instrumentation overhead is itself a large new load — the tracer caused the outage. The safe alternatives are perf trace, bcc’s syscount/trace, or a scoped bpftrace, which aggregate cheaply in-kernel.

Q: What is CO-RE and what problem did it solve? A: CO-RE (Compile Once, Run Everywhere) lets a single compiled eBPF object run across differing kernel versions. Using BTF (the kernel’s type information) and libbpf, the toolchain records where the program reads each kernel struct field and relocates those offsets at load time to match the running kernel. It replaced the old bcc model of recompiling from source on every host against local kernel headers, which dragged a full compiler toolchain onto production — CO-RE is what made fleet-wide eBPF practical.

Q: (LFCS/RHCSA-style) Without installing anything, use the kernel’s built-in tracer to see the ordered flow of the vfs_read call subtree. Then make sure you leave no tracing overhead behind. A:

cd /sys/kernel/tracing
echo function_graph > current_tracer
echo vfs_read > set_graph_function
cat trace | head
echo nop > current_tracer          # critical cleanup
echo > set_graph_function

Q: (Practical) The API is slow but CPU is only 40% busy and load is high. Walk the drill-down. A: Load high with idle CPU means tasks are waiting, not computing. Check iostat -xz 1 for a saturated disk (high %util, deep aqu-sz); if found, run biolatency-bpfcc for the latency distribution, then biosnoop-bpfcc (or a bpftrace on block_rq_issue/block_rq_complete) to name the process and files behind the slow I/O. If disk is clean, use offcputime-bpfcc to find where threads block off-CPU (locks, network). Each rung points to the next.

Q: Why is 99 Hz, not 100 Hz, the conventional profiling frequency? A: An odd frequency avoids sampling in lockstep with periodic kernel or application activity that runs at round numbers (100 Hz timers, per-second jobs). Sampling at exactly that cadence would bias the profile by repeatedly catching — or missing — the same periodic work; 99 Hz de-correlates the sampler from it.

Q: Which sysctls gate unprivileged access to eBPF and perf, and what is the live-versus-persistent rule for changing them? A: kernel.unprivileged_bpf_disabled controls whether non-root may load BPF at all, and kernel.perf_event_paranoid controls how much perf access unprivileged users get (lower = more; -1 = everything). sysctl -w key=value changes the running kernel immediately but is lost on reboot; to persist, put the setting in a file under /etc/sysctl.d/ and apply with sysctl --system. Widening these lowers a security boundary, so do it deliberately.

Q: When would you choose ftrace over eBPF, and vice-versa? A: Use ftrace/trace-cmd when you want the ordered flow of kernel function calls with per-call durations — “what did the kernel do, step by step.” Use eBPF (bcc/bpftrace) when you want to aggregate a custom answer in-kernel — a histogram, a per-key count — rather than a stream of individual events, or when you need to combine events, filter richly, or attach to user space. eBPF is more programmable; ftrace is simpler and always present.

Key takeaways

linuxebpfbpftracebccperfftracetracingobservabilitykprobestracepointsflame-graphsperformancekerneltrace-cmd
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments