요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. SPDX-License-Identifier: GPL-2.0-only
.. Copyright (C) 2022 Red Hat, Inc.
.. Copyright (C) 2022-2023 Isovalent, Inc.
===============================================
BPF_MAP_TYPE_HASH, with PERCPU and LRU Variants
===============================================
.. note::
- ``BPF_MAP_TYPE_HASH`` was introduced in kernel version 3.19
- ``BPF_MAP_TYPE_PERCPU_HASH`` was introduced in version 4.6
- Both ``BPF_MAP_TYPE_LRU_HASH`` and ``BPF_MAP_TYPE_LRU_PERCPU_HASH``
were introduced in version 4.10
``BPF_MAP_TYPE_HASH`` and ``BPF_MAP_TYPE_PERCPU_HASH`` provide general
purpose hash map storage. Both the key and the value can be structs,
allowing for composite keys and values.
The kernel is responsible for allocating and freeing key/value pairs, up
to the max_entries limit that you specify. Hash maps use pre-allocation
of hash table elements by default. The ``BPF_F_NO_PREALLOC`` flag can be
used to disable pre-allocation when it is too memory expensive.
``BPF_MAP_TYPE_PERCPU_HASH`` provides a separate value slot per
CPU. The per-cpu values are stored internally in an array.
The ``BPF_MAP_TYPE_LRU_HASH`` and ``BPF_MAP_TYPE_LRU_PERCPU_HASH``
variants add LRU semantics to their respective hash tables. An LRU hash
will automatically evict the least recently used entries when the hash
table reaches capacity. An LRU hash maintains an internal LRU list that
is used to select elements for eviction. This internal LRU list is
shared across CPUs but it is possible to request a per CPU LRU list with
the ``BPF_F_NO_COMMON_LRU`` flag when calling ``bpf_map_create``. The
following table outlines the properties of LRU maps depending on the a
map type and the flags used to create the map.
======================== ========================= ================================
Flag ``BPF_MAP_TYPE_LRU_HASH`` ``BPF_MAP_TYPE_LRU_PERCPU_HASH``
======================== ========================= ================================
**BPF_F_NO_COMMON_LRU** Per-CPU LRU, global map Per-CPU LRU, per-cpu map
**!BPF_F_NO_COMMON_LRU** Global LRU, global map Global LRU, per-cpu map
======================== ========================= ================================
Usage
=====
Kernel BPF
----------
bpf_map_update_elem()
~~~~~~~~~~~~~~~~~~~~~
.. code-block:: c
long bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, u64 flags)
Hash entries can be added or updated using the ``bpf_map_update_elem()``
helper. This helper replaces existing elements atomically. The ``flags``
parameter can be used to control the update behaviour:
- ``BPF_ANY`` will create a new element or update an existing element
- ``BPF_NOEXIST`` will create a new element only if one did not already
exist
- ``BPF_EXIST`` will update an existing element
``bpf_map_update_elem()`` returns 0 on success, or negative error in
case of failure.
bpf_map_lookup_elem()
~~~~~~~~~~~~~~~~~~~~~
.. code-block:: c
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)
Hash entries can be retrieved using the ``bpf_map_lookup_elem()``
helper. This helper returns a pointer to the value associated with
``key``, or ``NULL`` if no entry was found.
bpf_map_delete_elem()
~~~~~~~~~~~~~~~~~~~~~
.. code-block:: c
long bpf_map_delete_elem(struct bpf_map *map, const void *key)
Hash entries can be deleted using the ``bpf_map_delete_elem()``
helper. This helper will return 0 on success, or negative error in case
of failure.
Per CPU Hashes
--------------
For ``BPF_MAP_TYPE_PERCPU_HASH`` and ``BPF_MAP_TYPE_LRU_PERCPU_HASH``
the ``bpf_map_update_elem()`` and ``bpf_map_lookup_elem()`` helpers
automatically access the hash slot for the current CPU.
bpf_map_lookup_percpu_elem()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: c
void *bpf_map_lookup_percpu_elem(struct bpf_map *map, const void *key, u32 cpu)
The ``bpf_map_lookup_percpu_elem()`` helper can be used to lookup the
value in the hash slot for a specific CPU. Returns value associated with
``key`` on ``cpu`` , or ``NULL`` if no entry was found or ``cpu`` is
invalid.
Concurrency
-----------
Values stored in ``BPF_MAP_TYPE_HASH`` can be accessed concurrently by
programs running on different CPUs. Since Kernel version 5.1, the BPF
infrastructure provides ``struct bpf_spin_lock`` to synchronise access.
See ``tools/testing/selftests/bpf/progs/test_spin_lock.c``.
Userspace
---------
bpf_map_get_next_key()
~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: c
int bpf_map_get_next_key(int fd, const void *cur_key, void *next_key)
In userspace, it is possible to iterate through the keys of a hash using
libbpf's ``bpf_map_get_next_key()`` function. The first key can be fetched by
calling ``bpf_map_get_next_key()`` with ``cur_key`` set to
``NULL``. Subsequent calls will fetch the next key that follows the
current key. ``bpf_map_get_next_key()`` returns 0 on success, -ENOENT if
cur_key is the last key in the hash, or negative error in case of
failure.
Note that if ``cur_key`` gets deleted then ``bpf_map_get_next_key()``
will instead return the *first* key in the hash table which is
undesirable. It is recommended to use batched lookup if there is going
to be key deletion intermixed with ``bpf_map_get_next_key()``.
Examples
========
Please see the ``tools/testing/selftests/bpf`` directory for functional
examples. The code snippets below demonstrates API usage.
This example shows how to declare an LRU Hash with a struct key and a
struct value.
.. code-block:: c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
struct key {
__u32 srcip;
};
struct value {
__u64 packets;
__u64 bytes;
};
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 32);
__type(key, struct key);
__type(value, struct value);
} packet_stats SEC(".maps");
This example shows how to create or update hash values using atomic
instructions:
.. code-block:: c
static void update_stats(__u32 srcip, int bytes)
{
struct key key = {
.srcip = srcip,
};
struct value *value = bpf_map_lookup_elem(&packet_stats, &key);
if (value) {
__sync_fetch_and_add(&value->packets, 1);
__sync_fetch_and_add(&value->bytes, bytes);
} else {
struct value newval = { 1, bytes };
bpf_map_update_elem(&packet_stats, &key, &newval, BPF_NOEXIST);
}
}
Userspace walking the map elements from the map declared above:
.. code-block:: c
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
static void walk_hash_elements(int map_fd)
{
struct key *cur_key = NULL;
struct key next_key;
struct value value;
int err;
for (;;) {
err = bpf_map_get_next_key(map_fd, cur_key, &next_key);
if (err)
break;
bpf_map_lookup_elem(map_fd, &next_key, &value);
// Use key and value here
cur_key = &next_key;
}
}
Internals
=========
This section of the document is targeted at Linux developers and describes
aspects of the map implementations that are not considered stable ABI. The
following details are subject to change in future versions of the kernel.
``BPF_MAP_TYPE_LRU_HASH`` and variants
--------------------------------------
Updating elements in LRU maps may trigger eviction behaviour when the capacity
of the map is reached. There are various steps that the update algorithm
attempts in order to enforce the LRU property which have increasing impacts on
other CPUs involved in the following operation attempts:
- Attempt to use CPU-local state to batch operations
- Attempt to fetch ``target_free`` free nodes from global lists
- Attempt to pull any node from a global list and remove it from the hashmap
- Attempt to pull any node from any CPU's list and remove it from the hashmap
The number of nodes to borrow from the global list in a batch, ``target_free``,
depends on the size of the map. Larger batch size reduces lock contention, but
may also exhaust the global structure. The value is computed at map init to
avoid exhaustion, by limiting aggregate reservation by all CPUs to half the map
size. With a minimum of a single element and maximum budget of 128 at a time.
This algorithm is described visually in the following diagram. See the
description in commit 3a08c2fd7634 ("bpf: LRU List") for a full explanation of
the corresponding operations:
.. kernel-figure:: map_lru_hash_update.dot
:alt: Diagram outlining the LRU eviction steps taken during map update.
LRU hash eviction during map update for ``BPF_MAP_TYPE_LRU_HASH`` and
variants. See the dot file source for kernel function name code references.
Map updates start from the oval in the top right "begin ``bpf_map_update()``"
and progress through the graph towards the bottom where the result may be
either a successful update or a failure with various error codes. The key in
the top right provides indicators for which locks may be involved in specific
operations. This is intended as a visual hint for reasoning about how map
contention may impact update operations, though the map type and flags may
impact the actual contention on those locks, based on the logic described in
the table above. For instance, if the map is created with type
``BPF_MAP_TYPE_LRU_PERCPU_HASH`` and flags ``BPF_F_NO_COMMON_LRU`` then all map
properties would be per-cpu.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
Hash, per-CPU, LRU variant 개요
1-43`BPF_MAP_TYPE_HASH` 문서는 `GPL-2.0-only`, `Copyright (C) 2022 Red Hat, Inc.`, `Copyright (C) 2022-2023 Isovalent, Inc.`를 명시합니다.
`BPF_MAP_TYPE_HASH`는 `kernel version 3.19`, `BPF_MAP_TYPE_PERCPU_HASH`는 version 4.6, `BPF_MAP_TYPE_LRU_HASH`와 `BPF_MAP_TYPE_LRU_PERCPU_HASH`는 version 4.10에 도입되었습니다.
`BPF_MAP_TYPE_HASH`와 `BPF_MAP_TYPE_PERCPU_HASH`는 general-purpose hash map storage를 제공합니다. Key와 value 모두 struct일 수 있으므로 composite key와 composite value를 구성할 수 있습니다.
Kernel은 지정된 `max_entries` 한도까지 key/value pair를 할당하고 해제합니다. Hash map은 기본적으로 hash table element를 pre-allocation하며, memory 비용이 너무 크면 `BPF_F_NO_PREALLOC` flag로 이를 끌 수 있습니다.
`BPF_MAP_TYPE_PERCPU_HASH`는 CPU마다 별도의 value slot을 제공하며 per-CPU value는 내부 array에 저장됩니다.
`BPF_MAP_TYPE_LRU_HASH`와 `BPF_MAP_TYPE_LRU_PERCPU_HASH`는 각각의 hash table에 LRU semantics를 추가합니다. Capacity에 도달하면 internal LRU list를 사용해 least recently used entry를 자동으로 evict합니다.
기본 LRU list는 CPU 사이에서 공유됩니다. `bpf_map_create` 호출 시 `BPF_F_NO_COMMON_LRU`를 지정하면 per-CPU LRU list를 요청할 수 있습니다. Map type과 flag 조합별 특성은 다음과 같습니다.
| Flag | BPF_MAP_TYPE_LRU_HASH | BPF_MAP_TYPE_LRU_PERCPU_HASH |
|---|---|---|
| BPF_F_NO_COMMON_LRU | Per-CPU LRU, global map | Per-CPU LRU, per-CPU map |
| !BPF_F_NO_COMMON_LRU | Global LRU, global map | Global LRU, per-CPU map |
Kernel BPF update, lookup, delete
44-90Hash entry를 추가하거나 갱신할 때는 다음 `bpf_map_update_elem()` helper를 사용합니다. Existing element는 atomically 교체됩니다.
long bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, u64 flags)
Update behavior는 `flags` parameter로 제어합니다.
- `BPF_ANY`: 새 element를 만들거나 기존 element를 갱신합니다.
- `BPF_NOEXIST`: element가 없을 때만 새로 만듭니다.
- `BPF_EXIST`: 기존 element만 갱신합니다.
`bpf_map_update_elem()`은 성공하면 0, 실패하면 negative error를 반환합니다.
Hash entry를 가져올 때는 다음 `bpf_map_lookup_elem()` helper를 사용합니다.
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)
Lookup은 `key`에 연결된 value pointer를 반환하며 entry가 없으면 `NULL`을 반환합니다.
Hash entry를 삭제할 때는 다음 `bpf_map_delete_elem()` helper를 사용합니다.
long bpf_map_delete_elem(struct bpf_map *map, const void *key)
Delete는 성공하면 0, 실패하면 negative error를 반환합니다.
Per-CPU slot과 concurrency
91-117`BPF_MAP_TYPE_PERCPU_HASH`와 `BPF_MAP_TYPE_LRU_PERCPU_HASH`에서 `bpf_map_update_elem()` 및 `bpf_map_lookup_elem()`은 현재 CPU의 hash slot에 자동으로 접근합니다.
특정 CPU의 slot을 조회하려면 다음 `bpf_map_lookup_percpu_elem()` helper를 사용합니다.
void *bpf_map_lookup_percpu_elem(struct bpf_map *map, const void *key, u32 cpu)
Helper는 지정한 `cpu`에서 `key`에 연결된 value를 반환합니다. Entry가 없거나 `cpu`가 유효하지 않으면 `NULL`을 반환합니다.
`BPF_MAP_TYPE_HASH`의 value는 서로 다른 CPU에서 실행되는 program이 동시에 접근할 수 있습니다. `Kernel version 5.1`부터 BPF infrastructure는 access synchronization을 위한 `struct bpf_spin_lock`을 제공하며 예제는 `tools/testing/selftests/bpf/progs/test_spin_lock.c`에 있습니다.
Userspace key 순회
118-140Userspace에서는 libbpf의 다음 `bpf_map_get_next_key()` function으로 hash key를 순회할 수 있습니다.
int bpf_map_get_next_key(int fd, const void *cur_key, void *next_key)
첫 key는 `cur_key`를 `NULL`로 지정해 가져옵니다. 이후 호출은 current key 다음의 key를 반환합니다. 성공 시 0, `cur_key`가 마지막 key이면 `-ENOENT`, 그 밖의 실패에는 negative error를 반환합니다.
순회 중 `cur_key`가 삭제되면 `bpf_map_get_next_key()`는 hash table의 첫 key를 반환하므로 바람직하지 않습니다. Key 삭제가 순회와 섞일 수 있다면 batched lookup을 사용하는 것이 권장됩니다.
LRU hash 선언과 update·순회 예제
141-219Functional example은 `tools/testing/selftests/bpf` directory에 있으며 다음 snippet은 API 사용법을 보여 줍니다. 첫 예제는 struct key와 struct value를 갖는 LRU hash `packet_stats`를 선언합니다.
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
struct key {
__u32 srcip;
};
struct value {
__u64 packets;
__u64 bytes;
};
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 32);
__type(key, struct key);
__type(value, struct value);
} packet_stats SEC(".maps");
다음 `update_stats()`는 먼저 `packet_stats`에서 source IP key를 조회합니다. Existing value가 있으면 `__sync_fetch_and_add()` atomic instruction으로 packet과 byte counter를 더하고, 없으면 `BPF_NOEXIST`로 새 value를 만듭니다.
static void update_stats(__u32 srcip, int bytes)
{
struct key key = {
.srcip = srcip,
};
struct value *value = bpf_map_lookup_elem(&packet_stats, &key);
if (value) {
__sync_fetch_and_add(&value->packets, 1);
__sync_fetch_and_add(&value->bytes, bytes);
} else {
struct value newval = { 1, bytes };
bpf_map_update_elem(&packet_stats, &key, &newval, BPF_NOEXIST);
}
}
다음 userspace code는 앞서 선언한 map의 element를 순회합니다. `cur_key = NULL`에서 시작해 next key를 얻고 value를 lookup한 뒤 current key pointer를 갱신합니다.
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
static void walk_hash_elements(int map_fd)
{
struct key *cur_key = NULL;
struct key next_key;
struct value value;
int err;
for (;;) {
err = bpf_map_get_next_key(map_fd, cur_key, &next_key);
if (err)
break;
bpf_map_lookup_elem(map_fd, &next_key, &value);
// Use key and value here
cur_key = &next_key;
}
}
LRU eviction 내부 동작
220-265이 절은 Linux developer를 위한 implementation 설명이며 stable ABI가 아닙니다. 세부 동작은 이후 kernel version에서 바뀔 수 있습니다.
LRU map이 capacity에 도달한 상태에서 element를 update하면 eviction이 일어날 수 있습니다. Algorithm은 다른 CPU에 미치는 영향이 점차 커지는 다음 순서로 LRU property를 유지하려고 시도합니다.
- CPU-local state를 사용해 operation을 batch합니다.
- Global list에서 `target_free`개의 free node를 가져옵니다.
- Global list에서 임의의 node를 가져와 hashmap에서 제거합니다.
- 임의 CPU의 list에서 node를 가져와 hashmap에서 제거합니다.
한 batch에서 global list로부터 빌리는 node 수 `target_free`는 map size에 따라 정해집니다. 큰 batch는 lock contention을 줄이지만 global structure를 고갈시킬 수 있으므로, 모든 CPU의 총 reservation이 map size 절반을 넘지 않게 init 시 계산합니다. 최소 한 element, 한 번에 최대 128개가 budget입니다.
Commit `3a08c2fd7634` (`bpf: LRU List`)는 다음 update flow의 operation을 자세히 설명합니다.
`map_lru_hash_update.dot`의 LRU eviction graph를 operation 영향 범위가 커지는 순서로 구조화했습니다.
원문의 `map_lru_hash_update.dot` kernel figure는 `BPF_MAP_TYPE_LRU_HASH`와 variant의 map update 중 LRU eviction 단계를 나타내며 kernel function name code reference를 포함합니다.
Update는 오른쪽 위의 `begin bpf_map_update()` oval에서 시작해 아래쪽의 성공 또는 여러 error code로 진행합니다. Graph의 key는 각 operation에 관여할 수 있는 lock을 표시해 contention을 추론하도록 돕습니다.
실제 lock contention은 map type과 flag에 따라 달라집니다. 예를 들어 `BPF_MAP_TYPE_LRU_PERCPU_HASH`에 `BPF_F_NO_COMMON_LRU`를 지정하면 모든 map property가 per-CPU가 됩니다.
요약과 해설
map_hash.rst:1-265BPF hash map은 struct key와 value를 지원하며 per-CPU variant는 CPU마다 독립된 value slot을 제공합니다. LRU variant는 capacity에 도달하면 오래 사용하지 않은 entry를 자동으로 제거합니다.
기본 pre-allocation은 빠른 operation을 돕지만 memory 비용이 크면 `BPF_F_NO_PREALLOC`로 끌 수 있습니다. LRU list 공유 방식은 `BPF_F_NO_COMMON_LRU`로 global 또는 per-CPU 중 선택합니다.
Concurrent global value update에는 `bpf_spin_lock` 또는 atomic operation이 필요합니다. Userspace key 순회와 삭제를 섞을 때는 iterator가 첫 key로 돌아갈 수 있으므로 batched lookup이 더 안전합니다.