Modern Linux Observability (Part 1): eBPF Fundamentals, BTF Raw Tracepoints & The Ring Buffer Revolution
If you are studying software architecture, systems analysis, or cloud infrastructure, you have almost certainly heard the term eBPF (Extended Berkeley Packet Filter) hailed as the biggest revolution in the Linux kernel in the past two decades. Prominent tech organizations use it for high-performance Kubernetes networking (Cilium), dynamic security tracing (Falco), and system-wide observability (Brendan Gregg's performance tools).
Yet, when developers first dive into kernel internals, the learning curve can feel brutal: verifier rejections, kernel page faults, KASLR, and obscure memory barriers.
In this 3-part series, we will demystify the core engineering principles behind building low-overhead, production-grade Linux telemetry systems. We will start from fundamental Operating System concepts that every software developer should know, and progressively build up to high-throughput, real-time observability.
1. OS Refresher: The Sacred Boundary Between User and Kernel Space
Whenever a user process needs to write a file to disk or receive a packet from a network socket, it must perform a System Call (like read(), write(), or epoll_wait()). This triggers a CPU context switch from Ring 3 to Ring 0, which introduces small, but measurable, latency.
The Old Observability Dilemma: Polling /proc vs Kernel Modules
Historically, if a software engineer wanted to observe how much disk I/O each virtual machine or process was executing, they had two flawed options:
- Polling
/procor/sysfiles: User applications periodically read text pseudo-files like/proc/diskstatsevery few seconds. This is simple, but it only shows coarse aggregates. Transient I/O spikes occurring between scrapes are completely missed. - Writing a Linux Kernel Module (LKM): Developers wrote custom C code loaded directly into the kernel. While blazingly fast, a single null-pointer dereference or infinite loop in a kernel module causes a Kernel Panic (the Linux equivalent of the Blue Screen of Death), taking down the entire physical machine.
2. Enter eBPF: Programmable, Sandboxed Kernel Execution
eBPF allows developers to write custom C programs that compile down to a specialized bytecode. When loaded into the kernel, this bytecode is not blindly executed. Instead, it is subjected to an exhaustive mathematical safety check by the eBPF Verifier.
- Every pointer dereference must be proven to point to valid memory.
- No unbounded or infinite loops are allowed; all loops must have statically verifiable finite bounds.
- Programs cannot read uninitialized registers or stack variables.
- Memory access bounds must be explicitly checked before slicing data.
3. Choosing the Right Hook: The Power of tp_btf (BTF Raw Tracepoints)
To trace disk I/O, the kernel provides several hook mechanisms. Knowing which one to pick is critical for performance and stability:
| Hook Mechanism | How It Works | Overhead | Data Access |
|---|---|---|---|
Kprobe (kprobe/blk_mq_start_request) |
Dynamically patches instructions at any kernel function entry point. | Moderate to High (breakpoint trap). Fragile across kernel refactors. | Full access to function parameters. |
Classic Tracepoint (tracepoint/block/block_rq_issue) |
Static hooks instrumented into the kernel source code. | Low. Highly stable ABI. | Only provides formatted scalar copies (sector, size, PID). Cannot inspect payload memory! |
BTF Raw Tracepoint (tp_btf/block_rq_issue) |
Direct pointer invocation using in-kernel BPF Type Format (BTF) metadata. | Ultra-Low (direct function call, no trap, zero argument copying). | Direct typed pointer to kernel structs (struct request *, struct bio *). |
tp_btf Wins for Low-Latency Telemetrytp_btf, the kernel provides typed debug symbols directly via BTF. The probe receives the actual struct request *rq pointer directly in register r1!
Inspecting the Probe Implementation
Here is how an ultra-efficient eBPF probe is structured using tp_btf and libbpf:
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "subos_monitor_event.h"
char LICENSE[] SEC("license") = "GPL";
/* Hook directly into the block layer request issue point */
SEC("tp_btf/block_rq_issue")
int BPF_PROG(handle_block_rq_issue, struct request* rq) {
/* Read operation flags safely using CO-RE helper */
unsigned int op = BPF_CORE_READ(rq, cmd_flags) & 0xFF;
/* Discard flushes, discards, and zone maintenance */
if (op != REQ_OP_READ && op != REQ_OP_WRITE) {
return 0;
}
/* Reserve space directly in the lockless ring buffer */
struct block_io_event* event = bpf_ringbuf_reserve(&events_ringbuf, sizeof(*event), 0);
if (!event) {
/* Buffer is saturated; increment a per-CPU counter and drop event safely */
return 0;
}
/* Populate event telemetry */
event->timestamp_ns = bpf_ktime_get_ns();
event->sector = BPF_CORE_READ(rq, __sector);
event->nr_sectors = BPF_CORE_READ(rq, __data_len) >> 9; // 512-byte blocks
event->direction = (op == REQ_OP_WRITE) ? 1 : 0;
/* Atomically commit event to user-space */
bpf_ringbuf_submit(event, 0);
return 0;
}
4. The Data Transfer Highway: Perf Buffer vs BPF Ring Buffer
Once the kernel captures an event in Ring 0, how does it send it to our user-space daemon in Ring 3 without stalling the operating system?
To tackle this, the Linux kernel went through two major generations of communication buffers:
BPF_MAP_TYPE_RINGBUF implements an MPSC queue using lock-free hardware atomics. Multiple CPU cores can simultaneously reserve slices of memory in the buffer.
Furthermore, it supports memory-mapped I/O (mmap): the user-space daemon maps the ring buffer directly into its own virtual memory space. When the daemon reads an event, no extra memory copies take place between the kernel and the application!
5. Key Takeaways & What's Ahead
Understanding eBPF starts with appreciating the boundary between User Space and Kernel Space:
- Safety without Slowness: The eBPF Verifier mathematically guarantees that custom kernel probes cannot crash or freeze your servers, while the JIT compiler ensures near-zero overhead.
- Precision Instrumentation: BTF-powered raw tracepoints (
tp_btf) give typed access to kernel structures (likestruct request) right when events occur in the block layer. - Zero-Copy Data Planes: Modern BPF Ring Buffers replace fragmented per-CPU queues with an efficient, memory-mapped multi-producer pipeline.
In Part 2, we will go deeper into the kernel's memory engine: How does Linux manage memory pages? How does Kernel Address Space Layout Randomization (KASLR) protect against exploits? And how can an eBPF probe accurately identify which Proxmox Virtual Machine or Docker container generated an I/O request using Cgroups v2?
Stay tuned for Part 2 next week! Have questions about eBPF or OS concepts? Drop a comment below!
