← Documents Documentation/bpf/map_hash.rst GitHub 원문 ↗

Linux 6.18.37 · BPF

BPF_MAP_TYPE_HASH, with PERCPU and LRU Variants

일반·per-CPU·LRU hash map의 storage 특성, CRUD API, concurrency, userspace 순회와 LRU eviction 내부 algorithm을 설명합니다.

Source pathDocumentation/bpf/map_hash.rst
Source versionLinux v6.18.37
TranslationDUJINLABS 전문 번역 + 해설

요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.

1. 요약·해설

원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.

요약과 해설

map_hash.rst:1-265

BPF 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이 더 안전합니다.

2. 영어 원문 전체

번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.

원문 전체 펼치기
1 .. SPDX-License-Identifier: GPL-2.0-only
2 .. Copyright (C) 2022 Red Hat, Inc.
3 .. Copyright (C) 2022-2023 Isovalent, Inc.
4
5 ===============================================
6 BPF_MAP_TYPE_HASH, with PERCPU and LRU Variants
7 ===============================================
8
9 .. note::
10 - ``BPF_MAP_TYPE_HASH`` was introduced in kernel version 3.19
11 - ``BPF_MAP_TYPE_PERCPU_HASH`` was introduced in version 4.6
12 - Both ``BPF_MAP_TYPE_LRU_HASH`` and ``BPF_MAP_TYPE_LRU_PERCPU_HASH``
13 were introduced in version 4.10
14
15 ``BPF_MAP_TYPE_HASH`` and ``BPF_MAP_TYPE_PERCPU_HASH`` provide general
16 purpose hash map storage. Both the key and the value can be structs,
17 allowing for composite keys and values.
18
19 The kernel is responsible for allocating and freeing key/value pairs, up
20 to the max_entries limit that you specify. Hash maps use pre-allocation
21 of hash table elements by default. The ``BPF_F_NO_PREALLOC`` flag can be
22 used to disable pre-allocation when it is too memory expensive.
23
24 ``BPF_MAP_TYPE_PERCPU_HASH`` provides a separate value slot per
25 CPU. The per-cpu values are stored internally in an array.
26
27 The ``BPF_MAP_TYPE_LRU_HASH`` and ``BPF_MAP_TYPE_LRU_PERCPU_HASH``
28 variants add LRU semantics to their respective hash tables. An LRU hash
29 will automatically evict the least recently used entries when the hash
30 table reaches capacity. An LRU hash maintains an internal LRU list that
31 is used to select elements for eviction. This internal LRU list is
32 shared across CPUs but it is possible to request a per CPU LRU list with
33 the ``BPF_F_NO_COMMON_LRU`` flag when calling ``bpf_map_create``. The
34 following table outlines the properties of LRU maps depending on the a
35 map type and the flags used to create the map.
36
37 ======================== ========================= ================================
38 Flag ``BPF_MAP_TYPE_LRU_HASH`` ``BPF_MAP_TYPE_LRU_PERCPU_HASH``
39 ======================== ========================= ================================
40 **BPF_F_NO_COMMON_LRU** Per-CPU LRU, global map Per-CPU LRU, per-cpu map
41 **!BPF_F_NO_COMMON_LRU** Global LRU, global map Global LRU, per-cpu map
42 ======================== ========================= ================================
43
44 Usage
45 =====
46
47 Kernel BPF
48 ----------
49
50 bpf_map_update_elem()
51 ~~~~~~~~~~~~~~~~~~~~~
52
53 .. code-block:: c
54
55 long bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, u64 flags)
56
57 Hash entries can be added or updated using the ``bpf_map_update_elem()``
58 helper. This helper replaces existing elements atomically. The ``flags``
59 parameter can be used to control the update behaviour:
60
61 - ``BPF_ANY`` will create a new element or update an existing element
62 - ``BPF_NOEXIST`` will create a new element only if one did not already
63 exist
64 - ``BPF_EXIST`` will update an existing element
65
66 ``bpf_map_update_elem()`` returns 0 on success, or negative error in
67 case of failure.
68
69 bpf_map_lookup_elem()
70 ~~~~~~~~~~~~~~~~~~~~~
71
72 .. code-block:: c
73
74 void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)
75
76 Hash entries can be retrieved using the ``bpf_map_lookup_elem()``
77 helper. This helper returns a pointer to the value associated with
78 ``key``, or ``NULL`` if no entry was found.
79
80 bpf_map_delete_elem()
81 ~~~~~~~~~~~~~~~~~~~~~
82
83 .. code-block:: c
84
85 long bpf_map_delete_elem(struct bpf_map *map, const void *key)
86
87 Hash entries can be deleted using the ``bpf_map_delete_elem()``
88 helper. This helper will return 0 on success, or negative error in case
89 of failure.
90
91 Per CPU Hashes
92 --------------
93
94 For ``BPF_MAP_TYPE_PERCPU_HASH`` and ``BPF_MAP_TYPE_LRU_PERCPU_HASH``
95 the ``bpf_map_update_elem()`` and ``bpf_map_lookup_elem()`` helpers
96 automatically access the hash slot for the current CPU.
97
98 bpf_map_lookup_percpu_elem()
99 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
100
101 .. code-block:: c
102
103 void *bpf_map_lookup_percpu_elem(struct bpf_map *map, const void *key, u32 cpu)
104
105 The ``bpf_map_lookup_percpu_elem()`` helper can be used to lookup the
106 value in the hash slot for a specific CPU. Returns value associated with
107 ``key`` on ``cpu`` , or ``NULL`` if no entry was found or ``cpu`` is
108 invalid.
109
110 Concurrency
111 -----------
112
113 Values stored in ``BPF_MAP_TYPE_HASH`` can be accessed concurrently by
114 programs running on different CPUs. Since Kernel version 5.1, the BPF
115 infrastructure provides ``struct bpf_spin_lock`` to synchronise access.
116 See ``tools/testing/selftests/bpf/progs/test_spin_lock.c``.
117
118 Userspace
119 ---------
120
121 bpf_map_get_next_key()
122 ~~~~~~~~~~~~~~~~~~~~~~
123
124 .. code-block:: c
125
126 int bpf_map_get_next_key(int fd, const void *cur_key, void *next_key)
127
128 In userspace, it is possible to iterate through the keys of a hash using
129 libbpf's ``bpf_map_get_next_key()`` function. The first key can be fetched by
130 calling ``bpf_map_get_next_key()`` with ``cur_key`` set to
131 ``NULL``. Subsequent calls will fetch the next key that follows the
132 current key. ``bpf_map_get_next_key()`` returns 0 on success, -ENOENT if
133 cur_key is the last key in the hash, or negative error in case of
134 failure.
135
136 Note that if ``cur_key`` gets deleted then ``bpf_map_get_next_key()``
137 will instead return the *first* key in the hash table which is
138 undesirable. It is recommended to use batched lookup if there is going
139 to be key deletion intermixed with ``bpf_map_get_next_key()``.
140
141 Examples
142 ========
143
144 Please see the ``tools/testing/selftests/bpf`` directory for functional
145 examples. The code snippets below demonstrates API usage.
146
147 This example shows how to declare an LRU Hash with a struct key and a
148 struct value.
149
150 .. code-block:: c
151
152 #include <linux/bpf.h>
153 #include <bpf/bpf_helpers.h>
154
155 struct key {
156 __u32 srcip;
157 };
158
159 struct value {
160 __u64 packets;
161 __u64 bytes;
162 };
163
164 struct {
165 __uint(type, BPF_MAP_TYPE_LRU_HASH);
166 __uint(max_entries, 32);
167 __type(key, struct key);
168 __type(value, struct value);
169 } packet_stats SEC(".maps");
170
171 This example shows how to create or update hash values using atomic
172 instructions:
173
174 .. code-block:: c
175
176 static void update_stats(__u32 srcip, int bytes)
177 {
178 struct key key = {
179 .srcip = srcip,
180 };
181 struct value *value = bpf_map_lookup_elem(&packet_stats, &key);
182
183 if (value) {
184 __sync_fetch_and_add(&value->packets, 1);
185 __sync_fetch_and_add(&value->bytes, bytes);
186 } else {
187 struct value newval = { 1, bytes };
188
189 bpf_map_update_elem(&packet_stats, &key, &newval, BPF_NOEXIST);
190 }
191 }
192
193 Userspace walking the map elements from the map declared above:
194
195 .. code-block:: c
196
197 #include <bpf/libbpf.h>
198 #include <bpf/bpf.h>
199
200 static void walk_hash_elements(int map_fd)
201 {
202 struct key *cur_key = NULL;
203 struct key next_key;
204 struct value value;
205 int err;
206
207 for (;;) {
208 err = bpf_map_get_next_key(map_fd, cur_key, &next_key);
209 if (err)
210 break;
211
212 bpf_map_lookup_elem(map_fd, &next_key, &value);
213
214 // Use key and value here
215
216 cur_key = &next_key;
217 }
218 }
219
220 Internals
221 =========
222
223 This section of the document is targeted at Linux developers and describes
224 aspects of the map implementations that are not considered stable ABI. The
225 following details are subject to change in future versions of the kernel.
226
227 ``BPF_MAP_TYPE_LRU_HASH`` and variants
228 --------------------------------------
229
230 Updating elements in LRU maps may trigger eviction behaviour when the capacity
231 of the map is reached. There are various steps that the update algorithm
232 attempts in order to enforce the LRU property which have increasing impacts on
233 other CPUs involved in the following operation attempts:
234
235 - Attempt to use CPU-local state to batch operations
236 - Attempt to fetch ``target_free`` free nodes from global lists
237 - Attempt to pull any node from a global list and remove it from the hashmap
238 - Attempt to pull any node from any CPU's list and remove it from the hashmap
239
240 The number of nodes to borrow from the global list in a batch, ``target_free``,
241 depends on the size of the map. Larger batch size reduces lock contention, but
242 may also exhaust the global structure. The value is computed at map init to
243 avoid exhaustion, by limiting aggregate reservation by all CPUs to half the map
244 size. With a minimum of a single element and maximum budget of 128 at a time.
245
246 This algorithm is described visually in the following diagram. See the
247 description in commit 3a08c2fd7634 ("bpf: LRU List") for a full explanation of
248 the corresponding operations:
249
250 .. kernel-figure:: map_lru_hash_update.dot
251 :alt: Diagram outlining the LRU eviction steps taken during map update.
252
253 LRU hash eviction during map update for ``BPF_MAP_TYPE_LRU_HASH`` and
254 variants. See the dot file source for kernel function name code references.
255
256 Map updates start from the oval in the top right "begin ``bpf_map_update()``"
257 and progress through the graph towards the bottom where the result may be
258 either a successful update or a failure with various error codes. The key in
259 the top right provides indicators for which locks may be involved in specific
260 operations. This is intended as a visual hint for reasoning about how map
261 contention may impact update operations, though the map type and flags may
262 impact the actual contention on those locks, based on the logic described in
263 the table above. For instance, if the map is created with type
264 ``BPF_MAP_TYPE_LRU_PERCPU_HASH`` and flags ``BPF_F_NO_COMMON_LRU`` then all map
265 properties would be per-cpu.
266

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 조합별 특성은 다음과 같습니다.

FlagBPF_MAP_TYPE_LRU_HASHBPF_MAP_TYPE_LRU_PERCPU_HASH
BPF_F_NO_COMMON_LRUPer-CPU LRU, global mapPer-CPU LRU, per-CPU map
!BPF_F_NO_COMMON_LRUGlobal LRU, global mapGlobal LRU, per-CPU map

Kernel BPF update, lookup, delete

44-90

Hash 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-140

Userspace에서는 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-219

Functional 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을 자세히 설명합니다.

LRU hash map update와 eviction 순서
시작begin bpf_map_update()
CPU-local fast pathlocal free/pending node와 batch state 사용
Global free listtarget_free free node 확보
Global LRUnode를 가져와 hashmap에서 제거
Remote CPU LRU다른 CPU list의 node까지 회수
종료update 성공 또는 error code 반환

`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가 됩니다.