요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
===============
BPF ring buffer
===============
This document describes BPF ring buffer design, API, and implementation details.
.. contents::
:local:
:depth: 2
Motivation
----------
There are two distinctive motivators for this work, which are not satisfied by
existing perf buffer, which prompted creation of a new ring buffer
implementation.
- more efficient memory utilization by sharing ring buffer across CPUs;
- preserving ordering of events that happen sequentially in time, even across
multiple CPUs (e.g., fork/exec/exit events for a task).
These two problems are independent, but perf buffer fails to satisfy both.
Both are a result of a choice to have per-CPU perf ring buffer. Both can be
also solved by having an MPSC implementation of ring buffer. The ordering
problem could technically be solved for perf buffer with some in-kernel
counting, but given the first one requires an MPSC buffer, the same solution
would solve the second problem automatically.
Semantics and APIs
------------------
Single ring buffer is presented to BPF programs as an instance of BPF map of
type ``BPF_MAP_TYPE_RINGBUF``. Two other alternatives considered, but
ultimately rejected.
One way would be to, similar to ``BPF_MAP_TYPE_PERF_EVENT_ARRAY``, make
``BPF_MAP_TYPE_RINGBUF`` could represent an array of ring buffers, but not
enforce "same CPU only" rule. This would be more familiar interface compatible
with existing perf buffer use in BPF, but would fail if application needed more
advanced logic to lookup ring buffer by arbitrary key.
``BPF_MAP_TYPE_HASH_OF_MAPS`` addresses this with current approach.
Additionally, given the performance of BPF ringbuf, many use cases would just
opt into a simple single ring buffer shared among all CPUs, for which current
approach would be an overkill.
Another approach could introduce a new concept, alongside BPF map, to represent
generic "container" object, which doesn't necessarily have key/value interface
with lookup/update/delete operations. This approach would add a lot of extra
infrastructure that has to be built for observability and verifier support. It
would also add another concept that BPF developers would have to familiarize
themselves with, new syntax in libbpf, etc. But then would really provide no
additional benefits over the approach of using a map. ``BPF_MAP_TYPE_RINGBUF``
doesn't support lookup/update/delete operations, but so doesn't few other map
types (e.g., queue and stack; array doesn't support delete, etc).
The approach chosen has an advantage of re-using existing BPF map
infrastructure (introspection APIs in kernel, libbpf support, etc), being
familiar concept (no need to teach users a new type of object in BPF program),
and utilizing existing tooling (bpftool). For common scenario of using a single
ring buffer for all CPUs, it's as simple and straightforward, as would be with
a dedicated "container" object. On the other hand, by being a map, it can be
combined with ``ARRAY_OF_MAPS`` and ``HASH_OF_MAPS`` map-in-maps to implement
a wide variety of topologies, from one ring buffer for each CPU (e.g., as
a replacement for perf buffer use cases), to a complicated application
hashing/sharding of ring buffers (e.g., having a small pool of ring buffers
with hashed task's tgid being a look up key to preserve order, but reduce
contention).
Key and value sizes are enforced to be zero. ``max_entries`` is used to specify
the size of ring buffer and has to be a power of 2 value.
There are a bunch of similarities between perf buffer
(``BPF_MAP_TYPE_PERF_EVENT_ARRAY``) and new BPF ring buffer semantics:
- variable-length records;
- if there is no more space left in ring buffer, reservation fails, no
blocking;
- memory-mappable data area for user-space applications for ease of
consumption and high performance;
- epoll notifications for new incoming data;
- but still the ability to do busy polling for new data to achieve the
lowest latency, if necessary.
BPF ringbuf provides two sets of APIs to BPF programs:
- ``bpf_ringbuf_output()`` allows to *copy* data from one place to a ring
buffer, similarly to ``bpf_perf_event_output()``;
- ``bpf_ringbuf_reserve()``/``bpf_ringbuf_commit()``/``bpf_ringbuf_discard()``
APIs split the whole process into two steps. First, a fixed amount of space
is reserved. If successful, a pointer to a data inside ring buffer data
area is returned, which BPF programs can use similarly to a data inside
array/hash maps. Once ready, this piece of memory is either committed or
discarded. Discard is similar to commit, but makes consumer ignore the
record.
``bpf_ringbuf_output()`` has disadvantage of incurring extra memory copy,
because record has to be prepared in some other place first. But it allows to
submit records of the length that's not known to verifier beforehand. It also
closely matches ``bpf_perf_event_output()``, so will simplify migration
significantly.
``bpf_ringbuf_reserve()`` avoids the extra copy of memory by providing a memory
pointer directly to ring buffer memory. In a lot of cases records are larger
than BPF stack space allows, so many programs have use extra per-CPU array as
a temporary heap for preparing sample. bpf_ringbuf_reserve() avoid this needs
completely. But in exchange, it only allows a known constant size of memory to
be reserved, such that verifier can verify that BPF program can't access memory
outside its reserved record space. bpf_ringbuf_output(), while slightly slower
due to extra memory copy, covers some use cases that are not suitable for
``bpf_ringbuf_reserve()``.
The difference between commit and discard is very small. Discard just marks
a record as discarded, and such records are supposed to be ignored by consumer
code. Discard is useful for some advanced use-cases, such as ensuring
all-or-nothing multi-record submission, or emulating temporary
``malloc()``/``free()`` within single BPF program invocation.
Each reserved record is tracked by verifier through existing
reference-tracking logic, similar to socket ref-tracking. It is thus
impossible to reserve a record, but forget to submit (or discard) it.
``bpf_ringbuf_query()`` helper allows to query various properties of ring
buffer. Currently 4 are supported:
- ``BPF_RB_AVAIL_DATA`` returns amount of unconsumed data in ring buffer;
- ``BPF_RB_RING_SIZE`` returns the size of ring buffer;
- ``BPF_RB_CONS_POS``/``BPF_RB_PROD_POS`` returns current logical position
of consumer/producer, respectively.
Returned values are momentarily snapshots of ring buffer state and could be
off by the time helper returns, so this should be used only for
debugging/reporting reasons or for implementing various heuristics, that take
into account highly-changeable nature of some of those characteristics.
One such heuristic might involve more fine-grained control over poll/epoll
notifications about new data availability in ring buffer. Together with
``BPF_RB_NO_WAKEUP``/``BPF_RB_FORCE_WAKEUP`` flags for output/commit/discard
helpers, it allows BPF program a high degree of control and, e.g., more
efficient batched notifications. Default self-balancing strategy, though,
should be adequate for most applications and will work reliable and efficiently
already.
Design and Implementation
-------------------------
This reserve/commit schema allows a natural way for multiple producers, either
on different CPUs or even on the same CPU/in the same BPF program, to reserve
independent records and work with them without blocking other producers. This
means that if BPF program was interrupted by another BPF program sharing the
same ring buffer, they will both get a record reserved (provided there is
enough space left) and can work with it and submit it independently. This
applies to NMI context as well, except that due to using a spinlock during
reservation, in NMI context, ``bpf_ringbuf_reserve()`` might fail to get
a lock, in which case reservation will fail even if ring buffer is not full.
The ring buffer itself internally is implemented as a power-of-2 sized
circular buffer, with two logical and ever-increasing counters (which might
wrap around on 32-bit architectures, that's not a problem):
- consumer counter shows up to which logical position consumer consumed the
data;
- producer counter denotes amount of data reserved by all producers.
Each time a record is reserved, producer that "owns" the record will
successfully advance producer counter. At that point, data is still not yet
ready to be consumed, though. Each record has 8 byte header, which contains the
length of reserved record, as well as two extra bits: busy bit to denote that
record is still being worked on, and discard bit, which might be set at commit
time if record is discarded. In the latter case, consumer is supposed to skip
the record and move on to the next one. Record header also encodes record's
relative offset from the beginning of ring buffer data area (in pages). This
allows ``bpf_ringbuf_commit()``/``bpf_ringbuf_discard()`` to accept only the
pointer to the record itself, without requiring also the pointer to ring buffer
itself. Ring buffer memory location will be restored from record metadata
header. This significantly simplifies verifier, as well as improving API
usability.
Producer counter increments are serialized under spinlock, so there is
a strict ordering between reservations. Commits, on the other hand, are
completely lockless and independent. All records become available to consumer
in the order of reservations, but only after all previous records where
already committed. It is thus possible for slow producers to temporarily hold
off submitted records, that were reserved later.
One interesting implementation bit, that significantly simplifies (and thus
speeds up as well) implementation of both producers and consumers is how data
area is mapped twice contiguously back-to-back in the virtual memory. This
allows to not take any special measures for samples that have to wrap around
at the end of the circular buffer data area, because the next page after the
last data page would be first data page again, and thus the sample will still
appear completely contiguous in virtual memory. See comment and a simple ASCII
diagram showing this visually in ``bpf_ringbuf_area_alloc()``.
Another feature that distinguishes BPF ringbuf from perf ring buffer is
a self-pacing notifications of new data being availability.
``bpf_ringbuf_commit()`` implementation will send a notification of new record
being available after commit only if consumer has already caught up right up to
the record being committed. If not, consumer still has to catch up and thus
will see new data anyways without needing an extra poll notification.
Benchmarks (see tools/testing/selftests/bpf/benchs/bench_ringbufs.c) show that
this allows to achieve a very high throughput without having to resort to
tricks like "notify only every Nth sample", which are necessary with perf
buffer. For extreme cases, when BPF program wants more manual control of
notifications, commit/discard/output helpers accept ``BPF_RB_NO_WAKEUP`` and
``BPF_RB_FORCE_WAKEUP`` flags, which give full control over notifications of
data availability, but require extra caution and diligence in using this API.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
공유 MPSC 링 버퍼의 동기
1-28`BPF ring buffer` 문서는 BPF 링 버퍼의 설계, API와 구현 세부 사항을 설명합니다.
.. contents::
:local:
:depth: 2
기존 perf buffer가 충족하지 못한 두 가지 요구가 새로운 링 버퍼 구현을 만들게 했습니다.
- CPU들이 링 버퍼를 공유하여 메모리를 더 효율적으로 사용하는 것
- 여러 CPU에 걸쳐 있더라도 시간상 연속해서 발생한 이벤트의 순서를 보존하는 것. 예를 들어 한 태스크의 fork/exec/exit 이벤트 순서를 유지해야 합니다.
두 문제는 서로 독립적이지만 per-CPU perf ring buffer라는 선택 때문에 perf buffer는 둘 다 해결하지 못합니다. 이벤트 순서만이라면 커널 안의 별도 계수로 보완할 수도 있지만, 메모리 공유에는 MPSC 구현이 필요하므로 MPSC 링 버퍼 하나가 순서 문제도 함께 해결합니다.
맵 형식과 토폴로지
29-70BPF 프로그램에는 단일 링 버퍼가 `BPF_MAP_TYPE_RINGBUF` 형식의 BPF 맵 인스턴스로 제공됩니다. 설계 과정에서 다른 두 가지 방식도 검토했지만 채택하지 않았습니다.
첫 번째 방식은 `BPF_MAP_TYPE_PERF_EVENT_ARRAY`처럼 `BPF_MAP_TYPE_RINGBUF`를 링 버퍼 배열로 만들되 같은 CPU만 사용해야 한다는 규칙을 강제하지 않는 것이었습니다. 기존 perf buffer 사용자에게 익숙하지만 임의 키로 링 버퍼를 찾는 고급 로직을 지원하지 못합니다. 현재 방식에서는 `BPF_MAP_TYPE_HASH_OF_MAPS`로 이를 해결합니다. 모든 CPU가 링 버퍼 하나를 공유하는 일반적인 구성에는 배열 방식이 지나치게 복잡하기도 합니다.
두 번째 방식은 조회·갱신·삭제의 키/값 인터페이스가 꼭 필요하지 않은 범용 `container` 객체를 BPF 맵과 별도로 도입하는 것이었습니다. 그러나 관찰성과 verifier 지원을 위한 기반 시설, libbpf 문법과 새로운 개념을 추가하면서도 맵 방식보다 실질적인 이점이 없습니다. `BPF_MAP_TYPE_RINGBUF`가 조회·갱신·삭제를 지원하지 않는 것처럼 queue, stack 등 일부 기존 맵도 모든 연산을 지원하지 않습니다.
채택된 맵 방식은 커널의 introspection API, libbpf 지원과 bpftool 같은 기존 BPF 맵 기반 시설과 도구를 재사용합니다. 단일 공유 링 버퍼는 단순하게 구성할 수 있고, 필요하면 `ARRAY_OF_MAPS`와 `HASH_OF_MAPS` map-in-maps를 조합해 CPU별 링 버퍼부터 태스크 `tgid` 해싱·샤딩까지 다양한 토폴로지를 만들 수 있습니다. 후자는 순서를 유지하면서 경합을 줄이는 작은 링 버퍼 풀에 적합합니다.
키 크기와 값 크기는 반드시 0이어야 합니다. `max_entries`는 링 버퍼 크기를 지정하며 power of 2 값이어야 합니다.
perf buffer와의 공통점 및 제출 API
71-100새 BPF 링 버퍼와 `BPF_MAP_TYPE_PERF_EVENT_ARRAY` 기반 perf buffer는 다음 의미를 공유합니다.
- 가변 길이 레코드
- 공간이 없으면 블로킹하지 않고 예약 실패
- 사용자 공간이 쉽게 고성능으로 소비할 수 있는 mmap 가능 데이터 영역
- 새 데이터에 대한 epoll 알림
- 최저 지연 시간이 필요할 때 새 데이터를 busy polling하는 기능
BPF ringbuf는 BPF 프로그램에 두 종류의 API를 제공합니다.
- `bpf_ringbuf_output()`은 `bpf_perf_event_output()`과 비슷하게 다른 위치의 데이터를 링 버퍼로 복사합니다.
- `bpf_ringbuf_reserve()`/`bpf_ringbuf_commit()`/`bpf_ringbuf_discard()`는 과정을 두 단계로 나눕니다. 먼저 고정 크기 공간을 예약해 링 버퍼 데이터 영역의 포인터를 얻고, 데이터를 작성한 뒤 메모리를 commit하거나 discard합니다. discard된 레코드는 소비자가 무시합니다.
`bpf_ringbuf_output()`은 레코드를 다른 곳에서 먼저 준비하므로 메모리 복사가 한 번 더 필요합니다. 대신 verifier가 미리 알 수 없는 길이의 레코드도 제출할 수 있고 `bpf_perf_event_output()`과 매우 비슷하여 마이그레이션이 쉽습니다.
예약, 폐기와 verifier 수명 추적
101-120`bpf_ringbuf_reserve()`는 링 버퍼 메모리 포인터를 직접 반환해 추가 복사를 피합니다. BPF 스택보다 큰 레코드를 준비하기 위한 임시 per-CPU 배열도 필요하지 않습니다. 그 대신 verifier가 예약 영역 밖 접근이 없음을 검증할 수 있도록 컴파일 시 알려진 상수 크기만 예약할 수 있습니다. 동적 길이가 필요하면 조금 느리더라도 `bpf_ringbuf_output()`을 사용합니다.
commit과 discard의 차이는 작습니다. discard는 레코드를 폐기됨으로 표시하여 소비자가 건너뛰게 합니다. 이는 여러 레코드를 전부 제출하거나 전부 취소하는 처리, 또는 한 BPF 프로그램 호출 안에서 임시 `malloc()`/`free()` 동작을 흉내 내는 고급 용도에 유용합니다.
verifier는 기존 소켓 참조 추적과 비슷한 reference-tracking 로직으로 예약된 각 레코드를 추적합니다. 따라서 레코드를 예약한 뒤 submit 또는 discard하는 일을 잊을 수 없습니다.
상태 조회와 알림 제어
121-142`bpf_ringbuf_query()` 도우미는 현재 다음 네 종류의 링 버퍼 속성을 조회합니다.
- `BPF_RB_AVAIL_DATA`: 링 버퍼에서 아직 소비하지 않은 데이터 양
- `BPF_RB_RING_SIZE`: 링 버퍼 크기
- `BPF_RB_CONS_POS`/`BPF_RB_PROD_POS`: 소비자와 생산자의 현재 논리 위치
반환값은 순간적인 상태 스냅샷이므로 도우미가 돌아올 때는 이미 달라졌을 수 있습니다. 따라서 디버깅·보고 또는 값이 빠르게 변한다는 점을 고려하는 휴리스틱에만 사용해야 합니다.
한 가지 휴리스틱은 새 데이터에 대한 poll/epoll 알림을 더 세밀하게 제어하는 것입니다. output/commit/discard 도우미의 `BPF_RB_NO_WAKEUP`/`BPF_RB_FORCE_WAKEUP` 플래그를 함께 사용하면 효율적인 배치 알림 등을 구현할 수 있습니다. 다만 기본 self-balancing 전략도 대부분의 응용에서 안정적이고 효율적입니다.
다중 생산자 예약과 레코드 헤더
143-176reserve/commit 구조에서는 다른 CPU 또는 같은 CPU와 같은 BPF 프로그램의 여러 생산자가 서로를 블로킹하지 않고 독립된 레코드를 예약하고 작성할 수 있습니다. 공유 링 버퍼를 쓰는 BPF 프로그램이 다른 프로그램에 의해 중단되어도 공간만 충분하면 둘 다 레코드를 예약하고 독립적으로 제출합니다. NMI 컨텍스트에도 적용되지만 예약 중 spinlock을 사용하므로 `bpf_ringbuf_reserve()`가 잠금을 얻지 못하면 링 버퍼가 가득 차지 않았어도 예약이 실패할 수 있습니다.
링 버퍼는 내부적으로 power of 2 크기의 원형 버퍼이며, 계속 증가하는 두 논리 카운터를 사용합니다. 32비트 아키텍처에서 wrap-around가 발생해도 문제없습니다.
- consumer counter는 소비자가 데이터를 소비한 논리 위치까지를 나타냅니다.
- producer counter는 모든 생산자가 예약한 데이터의 양을 나타냅니다.
레코드를 예약한 생산자는 producer counter를 전진시키지만 그 시점에는 아직 소비할 수 없습니다. 각 레코드에는 예약 길이, 작업 중임을 나타내는 busy bit, commit 시 폐기를 나타내는 discard bit가 들어 있는 8 byte header가 있습니다. 폐기된 레코드는 소비자가 건너뜁니다.
레코드 헤더는 링 버퍼 데이터 영역 시작점으로부터의 상대 페이지 오프셋도 인코딩합니다. 따라서 `bpf_ringbuf_commit()`과 `bpf_ringbuf_discard()`는 링 버퍼 포인터 없이 레코드 자체 포인터만 받으며, 링 버퍼 위치는 메타데이터 헤더에서 복원됩니다. 이 구조는 verifier와 API 사용을 모두 단순화합니다.
예약 순서와 이중 가상 메모리 매핑
177-193producer counter 증가는 spinlock 아래 직렬화되므로 예약 사이에는 엄격한 순서가 있습니다. 반면 commit은 완전히 lockless하며 서로 독립적입니다. 모든 레코드는 예약 순서대로 소비자에게 공개되지만 앞선 레코드가 모두 commit된 뒤에만 보입니다. 따라서 느린 생산자가 나중에 예약·제출된 레코드를 일시적으로 막을 수 있습니다.
생산자와 소비자 구현을 단순하고 빠르게 만드는 핵심은 데이터 영역을 가상 메모리에 앞뒤로 두 번 연속 매핑하는 것입니다. 원형 버퍼 끝을 넘어가는 샘플도 마지막 데이터 페이지 다음에 첫 데이터 페이지가 다시 나타나므로 가상 메모리에서는 연속된 것처럼 보입니다. 자세한 설명과 ASCII diagram은 `bpf_ringbuf_area_alloc()`의 주석을 참조하십시오.
자율 조절 알림과 수동 플래그
194-206BPF ringbuf를 perf ring buffer와 구별하는 또 다른 기능은 새 데이터 가용성에 대한 self-pacing 알림입니다. `bpf_ringbuf_commit()`은 소비자가 방금 commit하는 레코드까지 이미 따라잡았을 때만 새 레코드 알림을 보냅니다. 그렇지 않으면 소비자가 기존 데이터를 따라잡는 과정에서 새 데이터도 자연스럽게 보므로 추가 poll 알림이 필요하지 않습니다.
`tools/testing/selftests/bpf/benchs/bench_ringbufs.c`의 벤치마크는 이 방식이 perf buffer에서 필요한 `N번째 샘플마다 알림` 같은 편법 없이도 매우 높은 처리량을 달성함을 보여 줍니다. 극단적인 경우 commit/discard/output 도우미의 `BPF_RB_NO_WAKEUP`과 `BPF_RB_FORCE_WAKEUP`으로 알림을 완전히 제어할 수 있지만, API를 매우 신중하게 사용해야 합니다.
요약과 해설
ringbuf.rst:1-206`BPF_MAP_TYPE_RINGBUF`는 모든 CPU가 공유할 수 있는 MPSC 링 버퍼로, per-CPU perf buffer의 메모리 중복을 줄이고 CPU를 넘나드는 이벤트의 시간 순서를 보존합니다.
고정 크기 레코드는 `reserve` 후 링 버퍼 메모리에 직접 작성하여 `commit` 또는 `discard`하고, 동적 크기 레코드는 추가 복사를 감수하고 `output`으로 제출합니다. verifier는 예약 수명을 추적하여 미제출 레코드와 범위 밖 접근을 막습니다.
예약은 spinlock으로 순서를 정하지만 commit은 lockless입니다. 이중 가상 메모리 매핑으로 원형 버퍼 경계 처리를 없애고, 소비자 진행 상태에 따른 자율 알림으로 불필요한 wakeup을 줄입니다.