Linux eBPF Maps
In eBPF, Maps are kernel-resident, generic data structures that provide persistent storage and bidirectional communication between kernel-space eBPF programs and user-space applications.
Because eBPF programs are event-driven and stateless—they execute in response to an event (like a packet arrival or system call) and immediately exit—they cannot maintain state in local registers or stack variables. BPF Maps bridge this gap by enabling:
- Statefulness: Retaining data across successive event invocations (e.g. counting requests or tracking connection lifetimes).
- Kernel-to-User Communication: Streaming metrics, security alerts, and network telemetry to user-space collectors.
- User-to-Kernel Configuration: Pushing configuration parameters, routing tables, and IP blocklists into running kernel hooks.
Declaring Maps: The BTF-Defined Map Syntax
Modern eBPF programs use BPF Type Format (BTF) syntax defined in <bpf/bpf_helpers.h>:
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10240);
__type(key, __u32); /* Key: IPv4 address */
__type(value, __u64); /* Value: Packet counter */
} packet_stats SEC(".maps");
When compiled with clang -target bpf, the compiler places this definition into the .maps ELF section. When libbpf loads the object, it automatically executes the bpf(BPF_MAP_CREATE) system call to instantiate the map in the kernel.
Essential eBPF Map Types
Linux provides over 30 distinct BPF map types, each engineered for specific workload patterns:
| Map Type | Key Type | Access Complexity | Best Use Case |
|---|---|---|---|
BPF_MAP_TYPE_HASH |
Arbitrary struct / scalar | Average $O(1)$ | Associative lookups: tracking TCP connections, process metrics, IP allowlists. |
BPF_MAP_TYPE_ARRAY |
32-bit integer index (0 to N-1) |
Guaranteed $O(1)$ direct index | Fixed-size tables, global configuration flags, static metrics arrays. |
BPF_MAP_TYPE_PERCPU_HASH |
Arbitrary | $O(1)$ per core | High-PPS packet counters; eliminates multi-core cache line bouncing. |
BPF_MAP_TYPE_PERCPU_ARRAY |
32-bit integer | $O(1)$ per core | High-frequency metric aggregators on multi-core servers. |
BPF_MAP_TYPE_RINGBUF |
None (FIFO stream) | Lockless multi-core streaming | Sending event notifications and telemetry from kernel to user space. |
BPF_MAP_TYPE_LPM_TRIE |
struct bpf_lpm_trie_key |
Longest prefix match | CIDR routing, IP subnet filtering, firewall rules. |
BPF_MAP_TYPE_PROG_ARRAY |
32-bit integer | $O(1)$ | eBPF Tail Calls (bpf_tail_call), chaining programs dynamically. |
Ring Buffer (BPF_MAP_TYPE_RINGBUF) vs. Perf Buffer
Historically, streaming events to user space relied on Perf Event Arrays (BPF_MAP_TYPE_PERF_EVENT_ARRAY). Each CPU had its own dedicated circular buffer:
- Disadvantage: Memory was partitioned per-CPU. If one core experienced a burst of events, its buffer overflowed (dropping packets) even while other CPU buffers were completely empty. Furthermore, event ordering across cores was not preserved.
Introduced in Linux 5.8, the BPF Ring Buffer (BPF_MAP_TYPE_RINGBUF) solved this:
- Shared Multi-Producer Buffer: A single circular memory buffer shared across all CPU cores.
- Strict Event Ordering: Events are ordered chronologically as they are committed.
- Zero-Copy Memory Mapping: User space maps the ring buffer via
mmap()and consumes events without kernel copying. - Lower Memory Overhead: Memory is dynamically consumed by active cores without idle per-CPU pre-allocations.
// Kernel space: Emitting an event via ringbuf
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (e) {
e->pid = bpf_get_current_pid_tgid() >> 32;
bpf_ringbuf_submit(e, 0); // Dispatches event to user space
}
Map Persistence: BPF Map Pinning
Normally, an eBPF map is held open by user-space file descriptors. If your user-space management process restarts (or crashes), the descriptor closes, and the kernel destroys the map and its data.
To persist maps across process restarts, Linux supports BPF Map Pinning into the BPF virtual filesystem (/sys/fs/bpf):
# Mount the BPF filesystem (usually done automatically by systemd)
mount -t bpf bpffs /sys/fs/bpf
# Inspect pinned maps using bpftool
bpftool map list
bpftool map dump id 42
In C code using libbpf:
// Pin a map to the filesystem
bpf_map__pin(bpf_obj->maps.packet_stats, "/sys/fs/bpf/my_packet_stats");
// In a new process or after restart: open the existing pinned map
int fd = bpf_obj_get("/sys/fs/bpf/my_packet_stats");
Frequently Asked Questions (FAQ)
What is the advantage of PERCPU maps in eBPF?
PERCPU maps allocate an independent copy of the data structure for every CPU core, completely eliminating multi-core lock contention and CPU cache-line bouncing. When high-throughput network packets arrive on multiple cores simultaneously, each core increments its local counter without atomic locking instructions. When user space reads the map, the kernel sums or presents the per-core values.
Why should new eBPF tools use BPF_MAP_TYPE_RINGBUF instead of perf buffers?
BPF_MAP_TYPE_RINGBUF provides a single shared buffer across all cores, preventing event drops on busy cores while preserving chronological event ordering. Unlike perf buffers which allocate rigid memory per CPU, ring buffers adapt to asymmetric workloads and support lockless, zero-copy memory-mapped consumption in user space.
What happens to an eBPF map when all file descriptors are closed?
If an eBPF map is neither pinned to /sys/fs/bpf nor referenced by an active eBPF program, the kernel frees its memory and destroys the map. To persist map state across service restarts or share it between independent programs, the map must be pinned into the BPF virtual filesystem.
Related Topics
- eBPF Overview: Fundamentals of BPF objects and execution lifecycles.
- The eBPF Verifier: Static verification, safety rules, and NULL check enforcement.
- eBPF Program Types & Hooks: Attaching to XDP, TC, kprobes, and BPF LSM.
