BPF Ring Buffer

The BPF ring buffer (BPF_MAP_TYPE_RINGBUF) is a multi-producer, single-consumer (MPSC) circular byte buffer that an in-kernel BPF program uses to stream variable-length event records to a userspace consumer losslessly and in global time order. It was written by Andrii Nakryiko at Meta (then Facebook), posted to the BPF mailing list on 2020-05-13 (LWN/patch cover letter) and merged in Linux 5.8 to replace the older per-CPU perf buffer (BPF_MAP_TYPE_PERF_EVENT_ARRAY) for the common “fire an event up to userspace” pattern. A single ringbuf instance is shared across all CPUs rather than split per-CPU, which eliminates per-CPU memory waste, preserves the ordering of events that happen sequentially across CPUs (e.g. a task’s fork/exec/exit), and — via the reserve/commit API — lets a program write event payload directly into ring memory with no intermediate copy (ringbuf.rst). As of the 6.12 LTS kernel (released 2024-11-17) it is the default, recommended event-streaming primitive; a sibling map, BPF_MAP_TYPE_USER_RINGBUF (added in Linux 6.1, verified below), inverts the direction so userspace produces and a BPF program drains.

The ring buffer is one of the BPF map types, but a degenerate one: it has no keys or values in the usual sense (its key_size and value_size are forced to zero — see below), and it does not support lookup/update/delete. It is a map purely so that it reuses the existing map infrastructure: the bpf() syscall machinery, bpftool introspection, libbpf support, and map-in-maps composition. This note covers the kernel-side producer API, the lockless internal design, the userspace epoll-driven consumer, and the reverse-direction user ring buffer. The data structures that are ordinary key/value stores live in BPF Maps; the legacy primitive this one replaces is described from the program-type side in perf_event BPF Programs.


Mental Model

Think of the BPF ring buffer as a shared mailroom with a single conveyor belt. Any number of CPUs (producers) can walk up at the same time, each grab a contiguous slot on the belt big enough for their letter, fill it in privately, and drop it. One reader at the far end (the userspace consumer) picks letters off the belt strictly in the order the slots were allocated. The two clever parts are: (1) producers reserve their slot atomically under a tiny lock so the slots are laid out in a strict order, but they fill and release them lock-free and independently; and (2) the reader and writers coordinate through two ever-increasing counters — a producer position (how far slots have been handed out) and a consumer position (how far the reader has caught up) — that live in shared, memory-mapped pages.

flowchart LR
  subgraph K["Kernel (producers, any CPU / NMI)"]
    P0["BPF prog on CPU0<br/>reserve -> fill -> submit"]
    P1["BPF prog on CPU1<br/>reserve -> fill -> submit"]
    P2["BPF prog on CPU2<br/>reserve -> discard"]
  end
  subgraph RB["BPF_MAP_TYPE_RINGBUF (one shared MPSC buffer)"]
    SPIN["reserve under raw_spinlock<br/>(advances producer_pos)"]
    DATA["data area<br/>(power-of-2, double-mapped pages)<br/>each record: 8B header + payload"]
    SPIN --> DATA
  end
  subgraph U["Userspace (single consumer)"]
    EPOLL["epoll_wait on ring fd"]
    LOOP["ring_buffer__poll<br/>walk records cons_pos..prod_pos"]
    CB["sample_cb(ctx, data, len)"]
    EPOLL --> LOOP --> CB
  end
  P0 --> SPIN
  P1 --> SPIN
  P2 --> SPIN
  DATA -. "producer_pos / consumer_pos<br/>(mmap'd shared pages)" .-> LOOP
  DATA -. "self-paced wakeup<br/>(irq_work -> wake_up_all)" .-> EPOLL

The MPSC ring buffer end to end. What it shows: multiple BPF programs on different CPUs (and even nested in NMI context) reserve record slots under a single short-held spinlock that serializes slot allocation, then fill and submit each slot independently and lock-free; userspace blocks in epoll_wait, is woken by a self-paced notification, and walks committed records from consumer_pos up to producer_pos. The insight to take: the spinlock guards only the ordering of reservations, not the data copy — so throughput stays high while ordering across CPUs is preserved, which is exactly the pair of properties the old per-CPU perf buffer could not deliver simultaneously.


Why It Replaced the Per-CPU Perf Buffer

To understand the ring buffer you have to understand what was wrong with the thing it replaced. The legacy mechanism is BPF_MAP_TYPE_PERF_EVENT_ARRAY combined with the bpf_perf_event_output() helper — the “perf buffer.” It is an array of per-CPU ring buffers built on the kernel’s perf subsystem. A BPF program running on CPU N outputs its record into CPU N’s buffer; userspace opens one buffer per CPU and reads them all. The kernel ring-buffer documentation states the two distinct problems this design cannot solve at once (ringbuf.rst, “Motivation”):

  1. Memory waste from per-CPU sizing. Because each CPU has its own buffer, you must size every buffer for the worst-case burst on any single CPU. On a 64- or 128-core box that means dozens of buffers, most of them idle most of the time, all reserving memory. If you under-size them you drop events on the hot CPU; if you size them safely you waste memory on the cold ones. The total footprint scales with CPU count regardless of aggregate event rate. A single shared buffer is sized for the aggregate rate, so a 16 MiB shared ring replaces what might have been 64 × several-MiB per-CPU rings.

  2. Lost ordering across CPUs. Events that are causally ordered in real time — a process forks on CPU 3, the child execs on CPU 7, then exits on CPU 1 — land in three different per-CPU buffers. Userspace reading the buffers round-robin has no way to reconstruct the true sequence. You can stamp every record with a timestamp and sort, but that requires buffering and a heuristic about how long to wait for stragglers; you can never be sure you have seen the earliest unread event. The doc notes the ordering problem could in principle be patched onto the perf buffer “with some in-kernel counting,” but since fixing the memory problem already requires an MPSC buffer, the same single-buffer solution fixes ordering for free.

Both defects are direct consequences of the per-CPU choice. An MPSC (multi-producer single-consumer) buffer shared across CPUs dissolves both: one allocation, sized for aggregate load; and a single serialized producer position that imposes a total order on reservations. There is a third, subtler win: the reserve/commit API removes a memory copy (covered below) that bpf_perf_event_output() cannot avoid.

There is one honest trade-off. Per-CPU buffers never contend: each CPU writes only its own buffer, so there is zero cross-core synchronization on the write path. The shared ring takes a short spinlock during reservation, so at extreme rates many CPUs can contend on that one lock. In practice the lock is held for only the few instructions that advance the producer counter, and the benchmarks shipped with the patch (tools/testing/selftests/bpf/benchs/bench_ringbufs.c) showed the ringbuf matching or beating the perf buffer on throughput while using far less memory (LWN patch cover letter). If you genuinely need per-CPU isolation you can still build it: put several RINGBUF maps inside an ARRAY_OF_MAPS/HASH_OF_MAPS and shard by CPU or by tgid, recovering per-CPU (or per-shard) behavior on top of the ringbuf primitive (ringbuf.rst, “Semantics and APIs”).


The Producer API: reserve/commit vs output

The kernel side exposes two ways for a BPF program to put a record into the ring, and the difference between them is the single most important practical fact about the API.

bpf_ringbuf_reserve → fill → bpf_ringbuf_submit / bpf_ringbuf_discard

The reserve/commit path splits writing into two steps. First the program reserves a fixed amount of space:

struct event *e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
    return 0;          /* ring full: no space, reservation failed, no blocking */
e->pid  = pid;
e->ts   = bpf_ktime_get_ns();
bpf_probe_read_kernel_str(e->comm, sizeof(e->comm), task->comm);
bpf_ringbuf_submit(e, 0);   /* or bpf_ringbuf_discard(e, 0) to drop it */

bpf_ringbuf_reserve(map, size, flags) returns a pointer directly into the ring buffer’s data area (or NULL if there is not enough free space — there is no blocking, ever; a full ring fails the reservation and the event is simply lost, which is the application’s signal to enlarge the ring or shed load). The BPF program then writes the event fields straight into that ring memory and calls bpf_ringbuf_submit() to make the record visible to the consumer, or bpf_ringbuf_discard() to throw it away. The flags argument on reserve must be zero — the kernel rejects any non-zero value (ringbuf.c, bpf_ringbuf_reserve).

This path’s advantage is no extra copy: the payload is constructed in place in the ring. That matters because BPF’s stack is only 512 bytes, so larger records would otherwise need a per-CPU array as scratch heap, then a copy into the ring; reserve eliminates that entirely. Its constraint is that size must be a constant the verifier can see: the verifier’s proto declares arg2_type = ARG_CONST_ALLOC_SIZE_OR_ZERO, so the size has to be a compile-time constant. The verifier then bounds-checks every access to the returned pointer against exactly that reserved size — you cannot read or write outside your record. discard versus submit differ by a single bit (covered below); discard is for “all-or-nothing” multi-record protocols or for using reserve/discard as a scratch malloc/free within one program invocation (ringbuf.rst).

A reserved-but-never-submitted record is a bug the verifier catches at load time. Each reservation is tracked by the verifier’s reference-tracking machinery (the same logic that tracks acquired socket references), so a program that reserves a record on one path and returns without submitting or discarding it on that path is rejected — you cannot leak a reservation (ringbuf.rst, “Semantics and APIs”).

bpf_ringbuf_output

long bpf_ringbuf_output(void *ringbuf, void *data, u64 size, u64 flags);

bpf_ringbuf_output() copies size bytes from data (e.g. a struct on the BPF stack or in a per-CPU array) into a freshly reserved record and submits it in one call. Internally it is literally __bpf_ringbuf_reserve() + memcpy + bpf_ringbuf_commit() (ringbuf.c, bpf_ringbuf_output). It is the close analogue of bpf_perf_event_output(), which is exactly why it exists: migrating perf-buffer code to the ring buffer is nearly mechanical if you use output. Its two trade-offs are the inverse of reserve’s: it incurs one extra memory copy (you build the record elsewhere, then it is copied into the ring), but in exchange it accepts a size not known to the verifier at compile time, covering use cases reserve cannot.

The rule of thumb: prefer reserve/submit for performance and for records larger than the 512-byte stack; fall back to output when the record size is dynamic or when porting perf-buffer code with minimal churn.

bpf_ringbuf_query and wakeup flags

bpf_ringbuf_query(map, flags) returns a momentary snapshot of ring state: BPF_RB_AVAIL_DATA (unconsumed bytes), BPF_RB_RING_SIZE (total size), BPF_RB_CONS_POS and BPF_RB_PROD_POS (current consumer/producer logical positions) (bpf.h enum, BPF_RB_*). These are inherently racy — the value can be stale the instant the helper returns — so they are for debugging, reporting, or heuristics only. The submit/discard/output helpers also accept two flags that override the default self-paced notification: BPF_RB_NO_WAKEUP (suppress the wakeup even if the consumer is idle) and BPF_RB_FORCE_WAKEUP (always wake), letting a program batch notifications manually when it knows better than the default heuristic.


Internal Design: Lockless Commit, Double-Mapped Pages, Self-Paced Wakeups

The ring is a power-of-two-sized circular byte buffer tracked by two ever-increasing 64-bit logical counters (they may wrap on 32-bit, which is harmless because only differences are ever compared) (ringbuf.rst, “Design and Implementation”):

  • consumer_pos — how far the consumer has consumed.
  • producer_pos — how much space all producers together have reserved.

Each record carries an 8-byte header (struct bpf_ringbuf_hdr): a 32-bit len field and a 32-bit pg_off (the record’s page offset from the start of the data area). The top two bits of len are flags: BPF_RINGBUF_BUSY_BIT (1<<31) means “still being written, do not consume yet,” and BPF_RINGBUF_DISCARD_BIT (1<<30) means “skip this record” (bpf.h, BPF_RINGBUF_* enum).

Reservation under a spinlock. __bpf_ringbuf_reserve() rounds the requested size up to include the 8-byte header and 8-byte alignment, then takes rb->spinlock. Under the lock it reads producer_pos, computes new_prod_pos = producer_pos + len, checks there is room (the buffer must not be over-full and the span of uncommitted records must not exceed the ring), writes the header with BUSY_BIT set, and publishes the new producer position with smp_store_release() so the consumer’s smp_load_acquire() sees it correctly (ringbuf.c, __bpf_ringbuf_reserve). The lock is held only for these few instructions — it serializes the order of reservations, nothing more. Crucially, in NMI context the code uses raw_spin_trylock_irqsave: if it cannot get the lock it fails the reservation rather than deadlock, so a reservation can fail even when the ring is not full. The pending_pos cursor lets reservation skip past records that are already committed when reclaiming space.

Lockless commit. bpf_ringbuf_commit() is taken outside the lock. It clears the BUSY_BIT (XOR), optionally sets the DISCARD_BIT, and atomically writes the final header with xchg(). Because commits are independent and lockless, two producers can commit in any order — but records become visible to the consumer strictly in reservation order, and only after every earlier record has committed (ringbuf.rst). This is what preserves global ordering: a slow producer that reserved an early slot temporarily holds back the visibility of later, already-committed records until it commits. The consumer walking from consumer_pos stops the moment it hits a record whose BUSY_BIT is still set.

The double-mapping trick. The data area is mapped twice, contiguously, back to back in virtual memory (vmap of nr_meta_pages + 2 * nr_data_pages, with each data page’s struct page placed at index i and nr_data_pages + i) (ringbuf.c, bpf_ringbuf_area_alloc). The effect is that a record that would wrap around the physical end of the circular buffer instead spills into the second mapping of the same pages and therefore looks completely contiguous in virtual memory. Neither the kernel producer nor the userspace consumer ever has to special-case wrap-around with a split read or a bounce buffer — they just read len contiguous bytes and it works. This single trick simplifies and speeds up both sides. The kernel source even includes an ASCII diagram of it; the layout is | meta pages | real data pages | same data pages again |.

Memory-mapped position pages. The consumer and producer counters live in their own page-aligned slots so they can be mapped into userspace with different permissions: for a kernel-producer ring, the producer page and the data are mapped read-only into userspace (userspace must not be able to corrupt the kernel’s tracking), while the consumer page is writable so userspace can advance consumer_pos directly without a syscall (ringbuf.c, struct bpf_ringbuf comment). This is why consuming is essentially syscall-free in the steady state.

Self-paced wakeups. When a producer commits, it sends a wakeup only if the consumer has already caught up to exactly the record just committed — i.e. only if the consumer is idle and waiting. If the consumer is still behind, it will see the new data on its own next pass and no wakeup is needed (ringbuf.c, bpf_ringbuf_commit). The wakeup itself is deferred to an irq_work that runs wake_up_all() on the wait queue, because commit can happen in any context (including NMI) where you cannot wake a task directly. This “self-pacing” is why the ring buffer achieves high throughput without the perf buffer’s crude “notify only every Nth sample” workaround.

The maximum ring size is bounded by the 32-bit page-offset header field: with 8 bits reserved for future use, that caps a single ring at roughly 64 GiB of data area — far more than any realistic use (ringbuf.c, bpf_ringbuf_alloc comment).


The Consumer Side: epoll + libbpf

Userspace almost never touches the raw mmap’d pages directly; it uses libbpf’s ring-buffer API (tools/lib/bpf/ringbuf.c). A consumer is created with a sample callback:

/* sample_cb is invoked once per committed (non-discarded) record */
static int handle_event(void *ctx, void *data, size_t len) {
    struct event *e = data;
    printf("pid=%d comm=%s\n", e->pid, e->comm);
    return 0;          /* return <0 to stop consuming and propagate an error */
}
 
struct ring_buffer *rb = ring_buffer__new(map_fd, handle_event, NULL, NULL);
for (;;)
    ring_buffer__poll(rb, 100 /* timeout_ms */);   /* blocks in epoll_wait */

ring_buffer__new() mmaps the consumer page (writable) and the producer page plus the double-mapped data area (read-only), registers the ring’s notification fd in an epoll instance with EPOLLIN, and stores the sample callback (tools/lib/bpf/ringbuf.c, ring_buffer__add/ring_buffer__new). One ring_buffer object can hold several ring maps (each added with ring_buffer__add), all multiplexed onto the same epoll fd.

The actual consumption loop is ringbuf_process_ring(). It smp_load_acquires consumer_pos, then smp_load_acquires producer_pos, and walks records while cons_pos < prod_pos. For each record it loads the 8-byte length header with acquire semantics; if BPF_RINGBUF_BUSY_BIT is set it bails out immediately (that record is still being written, so nothing past it is consumable yet); otherwise it advances cons_pos by the 8-byte-rounded record length, and — unless BPF_RINGBUF_DISCARD_BIT is set — invokes the sample callback on the payload. After each record it publishes the advanced consumer_pos with smp_store_release so the kernel can reclaim that space (tools/lib/bpf/ringbuf.c, ringbuf_process_ring). The three consumption entry points differ only in how they decide to run that loop:

  • ring_buffer__poll(rb, timeout_ms) — calls epoll_wait() first, so it sleeps until the kernel’s self-paced wakeup arrives (or the timeout elapses), then drains every ring that signaled. This is the normal, CPU-efficient path.
  • ring_buffer__consume(rb) — skips epoll entirely and drains all rings immediately. This is busy-polling: lowest latency, but it burns a CPU spinning. Use it only when you are latency-bound and have a core to spare.
  • ring_buffer__consume_n / ring__consume_n — bounded variants that stop after n records.

ring_buffer__epoll_fd() exposes the underlying epoll fd so the ring can be folded into a larger event loop (e.g. one shared epoll/io_uring reactor alongside sockets and timers) instead of being polled on its own thread. Because consuming only reads the read-only data pages and writes the writable consumer page, the steady-state consume path makes no syscalls at all between wakeups — the throughput win is real.


The Reverse Direction: BPF_MAP_TYPE_USER_RINGBUF

The ordinary ring buffer flows kernel → userspace. The user ring buffer, BPF_MAP_TYPE_USER_RINGBUF, flows the other way: userspace produces, a BPF program consumes. It first appears in the UAPI header at the v6.1 tag and is absent at v6.0 (verified by diffing the pinned include/uapi/linux/bpf.h blobs at v6.0 and v6.1), so it merged in Linux 6.1 (released 2022-12). It exists for the increasingly common pattern where userspace needs to hand work or configuration into a BPF program efficiently — for example feeding a sched_ext BPF scheduler dispatch decisions, or a control plane streaming policy updates — without one bpf() syscall (or one map update) per item.

The userspace side uses a symmetric libbpf API:

struct user_ring_buffer *urb = user_ring_buffer__new(map_fd, NULL);
struct sample *s = user_ring_buffer__reserve(urb, sizeof(*s));   /* or reserve_blocking */
if (s) {
    s->x = 42;
    user_ring_buffer__submit(urb, s);   /* or user_ring_buffer__discard */
}

user_ring_buffer__reserve() is the userspace analogue of the kernel’s reserve: it loads both positions with acquire semantics, computes available space, and on success writes a BUSY_BIT-marked header and returns a pointer into the ring; on a full ring it returns NULL with errno = ENOSPC (or E2BIG if the record is larger than the whole ring) (tools/lib/bpf/ringbuf.c, user_ring_buffer__reserve). The blocking variant user_ring_buffer__reserve_blocking(urb, size, timeout_ms) retries, sleeping in epoll_wait between attempts — here the kernel sends a wakeup when it drains samples, the mirror image of the forward direction. The same library comment documents the guarantee: the kernel delivers at least one event notification per bpf_user_ringbuf_drain() invocation that drains a sample, unless the program passes BPF_RB_NO_WAKEUP.

On the kernel side the BPF program drains with a single helper:

long bpf_user_ringbuf_drain(struct bpf_map *map, void *callback_fn, void *ctx, u64 flags);

bpf_user_ringbuf_drain() invokes callback_fn once per available sample, passing each as a read-only dynptr (a verifier-tracked dynamic pointer with a runtime length), up to BPF_MAX_USER_RINGBUF_SAMPLES per call, and returns the number of samples processed (ringbuf.c, bpf_user_ringbuf_drain). Because userspace is now the untrusted producer, the kernel cannot use the kernel-producer spinlock approach — it instead serializes consumers with an atomic_t busy flag (returning -EBUSY if another drain is in progress) and, critically, validates every sample: each record’s claimed length must be properly formatted and fully contained within the ring before the callback sees it. The struct comment is explicit that “the kernel must carefully check and validate each sample.” This is the security-critical difference from the forward direction, where the kernel trusts itself.

Uncertain

Verify: that BPF_MAP_TYPE_USER_RINGBUF was first released in a stable 6.1 kernel rather than merged into 6.1’s merge window from an earlier bpf-next cycle. Reason: presence in the v6.1 tag’s UAPI header (and absence at v6.0) was confirmed, which pins the release, but the exact patch/commit and the bpf-next merge timing were not fetched. To resolve: check the git log for kernel/bpf/ringbuf.c bpf_user_ringbuf_drain introduction commit and its Fixes:/merge tag. uncertain


Configuration and Map Definition

A ring buffer is declared in BPF C with the standard BTF-style map definition. Two constraints are enforced at BPF_MAP_CREATE time by ringbuf_map_alloc(): key_size and value_size must both be zero, and max_entries (which here means the byte size of the data area) must be a power of two and page-aligned (ringbuf.c, ringbuf_map_alloc):

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);   /* 256 KiB data area; MUST be 2^n and page-aligned */
} events SEC(".maps");

The only create-time flag accepted is BPF_F_NUMA_NODE (to pin the allocation to a NUMA node); any other flag yields -EINVAL (ringbuf.c, RINGBUF_CREATE_FLAG_MASK). The real consumed memory is the data area plus a few metadata pages (the non-mmappable header, plus one page each for the consumer and producer positions). The single most common configuration mistake is sizing max_entries to a non-power-of-two or non-page-multiple value, which fails creation outright rather than rounding.


Failure Modes and Common Misunderstandings

  • “The ring buffer never drops events.” It is lossless only while there is space. A full ring fails bpf_ringbuf_reserve/output (returns NULL/error) and the event is dropped — there is no blocking on the producer side, by design (a BPF program in NMI or IRQ context cannot block). Lossless means “no torn or duplicated records,” not “infinite buffer.” Diagnose drops by tracking reserve failures in the program and by watching BPF_RB_AVAIL_DATA approach BPF_RB_RING_SIZE.
  • NMI reservation can fail spuriously. In NMI context reservation uses trylock; under contention it can return NULL even when the ring has room (ringbuf.c). Programs that fire in NMI (e.g. perf-event-driven) must tolerate this.
  • Forgetting to submit/discard a reservation. This does not silently leak at runtime — the verifier rejects the program at load time via reference tracking. The symptom is a load failure (“Unreleased reference”), not a runtime hang.
  • Head-of-line blocking by a slow producer. Because records become visible strictly in reservation order, one producer that reserves a slot and is slow to commit (e.g. preempted) holds back the visibility of every later record until it commits. This is the price of ordering; it is usually invisible but can show up as latency spikes under heavy nesting.
  • Reserve size must be constant. A common porting bug is passing a runtime-variable size to bpf_ringbuf_reserve; the verifier rejects it (ARG_CONST_ALLOC_SIZE_OR_ZERO). Use bpf_ringbuf_output for variable sizes.
  • Assuming per-CPU isolation. Unlike the perf buffer, one ring is shared; a single firehose CPU can fill it and starve others. If you need isolation, shard with ARRAY_OF_MAPS/HASH_OF_MAPS.

Alternatives and When to Choose Them

  • Perf buffer (BPF_MAP_TYPE_PERF_EVENT_ARRAY + bpf_perf_event_output). Choose it only when you genuinely need per-CPU buffers with zero cross-core contention and do not care about global ordering, or for compatibility with very old kernels (pre-5.8) that lack the ring buffer. For everything else the ring buffer is strictly better. See perf_event BPF Programs for the perf-event program-type side.
  • Sharing state via ordinary maps. If you don’t need a stream of events but rather current state (counters, latest value per key), a array map read from userspace is simpler than a ring buffer — no consumer loop, no ordering concerns.
  • User ring buffer. The right tool when the data flows into the kernel program from userspace (control plane, work queue) rather than out.

Production Notes

The ring buffer is now the default event channel in the major eBPF tracing stacks. libbpf-tools and the modern BCC tools use ring_buffer__poll for their event streams; bpftrace uses it for printf/event output on kernels that support it. Cilium and other production eBPF systems moved event/notification paths to the ring buffer for the memory and ordering benefits described above. The canonical “first real program” tutorial — bootstrap in libbpf-bootstrap — is built entirely around BPF_MAP_TYPE_RINGBUF with bpf_ringbuf_reserve/submit and ring_buffer__poll, precisely because it is the cleanest end-to-end example of the kernel→userspace path. When tuning, size the data area to the aggregate burst (not per-CPU), prefer reserve/submit over output for hot paths to save the copy, and reach for ring_buffer__consume (busy-poll) only when you are latency-bound and CPU-rich.


See Also