mirror of
https://github.com/torvalds/linux.git
synced 2026-07-27 09:36:22 +02:00
Merge branch 'bpf-introduce-resizable-hash-map'
Mykyta Yatsenko says:
====================
bpf: Introduce resizable hash map
This patch series introduces BPF_MAP_TYPE_RHASH, a new hash map type that
leverages the kernel's rhashtable to provide resizable hash map for BPF.
The existing BPF_MAP_TYPE_HASH uses a fixed number of buckets determined at
map creation time. While this works well for many use cases, it presents
challenges when:
1. The number of elements is unknown at creation time
2. The element count varies significantly during runtime
3. Memory efficiency is important (over-provisioning wastes memory,
under-provisioning hurts performance)
BPF_MAP_TYPE_RHASH addresses these issues by using rhashtable, which
automatically grows and shrinks based on load factor.
The implementation wraps the kernel's rhashtable with BPF map operations:
- Uses bpf_mem_alloc for RCU-safe memory management
- Supports all standard map operations (lookup, update, delete, get_next_key)
- Supports batch operations (lookup_batch, lookup_and_delete_batch)
- Supports BPF iterators for traversal
- Supports BPF_F_LOCK for spin locks in values
- Requires BPF_F_NO_PREALLOC flag (elements allocated on demand)
- In-place updates for improved performance.
- max_entries serves as a hard limit, not bucket count
- Uses bit_spin_lock() + local_irq_save() for bucket locking,
similar to existing BPF hashmap's raw_spin_lock_irqsave(), insertions and
deletes may fail.
- Iterations are best-effort, if resize, insertions, deletions take place
concurrently, iterations may visit same elements multiple times or skip
elements.
- Lock out insertions, when running special fields destructor to guarantee
its completion.
The series includes comprehensive tests:
- Basic operations in test_maps (lookup, update, delete, get_next_key)
- BPF program tests for lookup/update/delete semantics
- Seq file tests
Signed-off-by: Mykyta Yatsenko <yatsenko@meta.com>
---
Update implementation
---------------------
Current implementation of the BPF_MAP_TYPE_RHASH does not provide
the same strong guarantees on the values consistency under concurrent
reads/writes as BPF_MAP_TYPE_HASH.
BPF_MAP_TYPE_HASH allocates a new element and atomically swaps the
pointer. BPF_MAP_TYPE_RHASH does memcpy in place with no lock held.
rhash trades consistency for speed, concurrent readers can observe
partially updated data. Two concurrent writers to the same key can
also interleave, producing mixed values. This is similar to arraymap
update implementation, including handling of the special fields.
As a solution, user may use BPF_F_LOCK to guarantee consistent reads
and write serialization.
Summary of the read consistency guarantees:
map type | write mechanism | read consistency
-------------+------------------+--------------------------
htab | alloc, swap ptr | always consistent (RCU)
htab F_LOCK | in-place + lock | consistent if reader locks
-------------+------------------+--------------------------
rhtab | in-place memcpy | torn reads
rhtab F_LOCK | in-place + lock | consistent if reader locks
Benchmarks
----------
1. LOOKUP (single producer, M events/sec)
key | max | nr | htab | rhtab | ratio | delta
----+-----+-------+---------+---------+-------+-------
8 | 1K | 750 | 99.85 | 81.92 | 0.82x | -18 %
8 | 1K | 1K | 100.71 | 80.19 | 0.80x | -20 %
8 | 1M | 750K | 23.37 | 72.09 | 3.08x | +208 %
8 | 1M | 1M | 13.39 | 53.72 | 4.01x | +301 %
32 | 1K | 750 | 51.57 | 42.78 | 0.83x | -17 %
32 | 1K | 1K | 50.81 | 45.83 | 0.90x | -10 %
32 | 1M | 750K | 11.27 | 15.29 | 1.36x | +36 %
32 | 1M | 1M | 7.32 | 8.75 | 1.19x | +19 %
256 | 1K | 750 | 7.58 | 7.88 | 1.04x | +4 %
256 | 1K | 1K | 7.43 | 7.81 | 1.05x | +5 %
256 | 1M | 750K | 3.69 | 4.27 | 1.16x | +16 %
256 | 1M | 1M | 2.60 | 3.12 | 1.20x | +20 %
Pattern:
* Small map (1K): htab wins for 8 / 32 byte keys by 10-20 %
because the preallocated bucket array fits in L1. Equalises
at 256 byte keys.
* Large map (1M): rhtab wins everywhere, up to 4x at high load
factor with 8 byte keys.
* Higher load factor amplifies rhtab's lead: rhtab grows the
bucket array; htab stays at user-declared max.
2. FULL UPDATE (M events/sec per producer, -p 7)
htab per-producer:
20.33 22.02 19.27 23.61 24.18 23.17 21.07
mean 21.94 range 19.27 - 24.18
rhtab per-producer:
133.51 129.47 74.52 129.29 102.26 129.98 107.64
mean 115.24 range 74.52 - 133.51
speedup (mean): 5.25x (+425 %)
In-place memcpy avoids the per-update alloc + RCU pointer swap
that htab pays.
3. MEMORY (overwrite, -p 8, no --preallocated)
value_size | htab ops/s | rhtab ops/s | htab mem | rhtab mem
-----------+-------------+-------------+----------+----------
32 B | 122.87 k/s | 133.04 k/s | 2.47 MiB | 2.49 MiB
4096 B | 64.43 k/s | 65.38 k/s | 6.74 MiB | 6.44 MiB
rhtab/htab : +8 % ops, +0.8 % mem (32 B)
+1 % ops, -4 % mem (4096 B)
SUMMARY
* Small / well-fitting map: htab is faster (cache-friendly
fixed bucket array), but only by ~10-20 %.
* Large / high-load-factor map: rhtab is dramatically faster
(1.2x to 4x) because rhashtable resizes to keep the load
factor sane while htab stays stuck at user-declared max.
* Update-heavy workloads: rhtab is ~5x faster per producer
via in-place memcpy.
* Memory benchmark: effectively on par
---
Changes in v7:
- rhashtable_next_key: move into lib/rhashtable.c, drop params argument
(Herbert).
- rhashtable_next_key: kdoc clarifies that behavior on tables with
duplicate keys is undefined (sashiko).
- rhashtable: include Herbert's "Use irq work for shrinking" patch so
__rhashtable_remove_fast_one() can fire the shrink path from NMI
context (Herbert).
- hashtab: fix u32 multiply overflow in __rhtab_map_lookup_and_delete_batch
copy_to_user; cast total to size_t before multiplying by key_size /
value_size (sashiko, bot+bpf-ci).
- hashtab: allow kptr/refcount fields in rhtab values (same model as
array map).
- Link to v6: https://patch.msgid.link/20260602-rhash-v6-0-1bfd35a4184f@meta.com
Changes in v6:
- rhashtable_next_key: advance past duplicate keys in the main bucket
chain to avoid an infinite loop when there are duplicate keys
(sashiko).
- rhashtable_next_key: return ERR_PTR(-EOPNOTSUPP) on rhltable (sashiko).
- rhashtable: selftest pre-sizes the table to avoid concurrent rehash
triggering spurious failures (sashiko).
- hashtab: real rhtab_map_mem_usage in the basic commit; move
bpf_map_free_internal_structs from rhtab_free_elem into the
special-fields commit where it does meaningful work (bot+bpf-ci).
- bpf_iter (seq_file): switch to rhashtable_walk_* for stronger
coverage under concurrent rehash; get_next_key and batch keep
rhashtable_next_key (sashiko).
- iter ops: rhtab_map_get_next_key adds IS_ERR check
before dereferencing the element pointer (sashiko).
- iter ops: bpf_each_rhash_elem removes cond_resched() (sashiko).
- iter ops: batch returns -EAGAIN (not -ENOENT) on cursor delete,
so userspace can distinguish lost cursor from end-of-iteration
and restart from NULL (sashiko).
- Link to v5: https://patch.msgid.link/20260528-rhash-v5-0-7205191b6c57@meta.com
Changes in v5:
- rhashtable_next_key: add kdoc WARNING to highlight lack of rehash
detection and unbounded iteration (Herbert).
- rhashtable: selftest now checks IS_ERR() before PTR_ERR comparison
on the missing-key path (bot+bpf-ci).
- hashtab: drop dead stub bodies and unused map_ops registrations
from the basic commit; iteration commit adds bodies, structs, and
registrations together. .map_get_next_key keeps a stub registration
in the basic commit because the syscall dispatcher does not
NULL-check it; iteration commit replaces the stub body with the
real implementation (bot+bpf-ci).
- hashtab: fix batch cursor advancement. v4 stashed the lookahead
element key but then resumed via next_key(cursor), skipping that
element across batch boundaries and orphaning it on
lookup_and_delete_batch. v5 stashes the lookahead key and looks
it up directly on the next batch entry (bot+bpf-ci, sashiko v3).
- hashtab: document torn-read race in rhtab_map_update_existing,
matching arraymap semantics (bot+bpf-ci).
- Link to v4: https://patch.msgid.link/20260513-rhash-v4-0-dd3d541ccb0b@meta.com
Changes in v4:
- rhashtable: introduce rhashtable_next_key(), drop walker-based
iteration for BPF (also drops earlier rhashtable_walk_enter_from()
proposal).
- map_extra: presize hint via lower 32 bits (nelem_hint), capped at
U16_MAX.
- Automatic shrinking enabled (was missing despite being advertised).
- Reject key_size > U16_MAX (rhashtable_params.key_len is u16).
- Replace irqs_disabled() guard with bpf_disable_instrumentation around
bucket-lock paths: closes same-CPU NMI tracing recursion without
rejecting legitimate IRQ-context callers.
- lookup_and_delete reordered: unlink before copy to avoid populating
user buffer on concurrent-unlink -ENOENT.
- update_existing reordered: copy then free_fields, matching arraymap.
- Word-sized key fast path (sizeof(long) bytes), inlined hashfn/cmpfn
via static-const rhashtable_params; works on both 32-bit and 64-bit.
- check_and_init_map_value() on insert (zero special-field bytes from
recycled bpf_mem_alloc memory; previously bpf_spin_lock could read
garbage and qspinlock would deadlock).
- BPF_SPIN_LOCK / BPF_RES_SPIN_LOCK allowlist moved to the special-
fields commit so each commit is bisect-safe.
- Link to v3: https://patch.msgid.link/20260424-rhash-v3-0-d0fa0ce4379b@meta.com
Changes in v3:
- Squash all commits implementing basic functions into one (Alexei)
- Remove selftests that were not necessary (Alexei)
- Resize detection for kernel full iterations, error out on resize (Alexei)
- Remove second lookup in get_next_key() (Emil)
- __acquires(RCU)/__releases(RCU) on seq_start/seq_stop (Emil)
- Use bpf_map_check_op_flags() where it makes sense (Leon)
- Benchmarks refresh, experiment with alternative hash functions
- Rely on iterator invalidation during rehash to handle table resizes:
fail on resize where we fully iterate on table inside kernel, dont fail on
resize where iteration goes through userspace. Exception -
rhtab_map_free_internal_structs() should be just safe to iterate fully
in kernel, no risk of infinite loop, because no user holding reference.
- Handle special fields during in-place updates (Emil, sashiko)
- Link to v2: https://lore.kernel.org/all/20260408-rhash-v2-0-3b3675da1f6e@meta.com/
Changes in v2:
- Added benchmarks
- Reworked all functions that walk the rhashtable, use walk API, instead
of directly accessing tbl and future_tbl
- Added rhashtable_walk_enter_from() into rhashtable to support O(1)
iteration continuations
- Link to v1: https://lore.kernel.org/r/20260205-rhash-v1-0-30dd6d63c462@meta.com
---
====================
Link: https://patch.msgid.link/20260605-rhash-v7-0-5b8e05f8630d@meta.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
This commit is contained in:
commit
87d119abc4
|
|
@ -134,6 +134,7 @@ BPF_MAP_TYPE(BPF_MAP_TYPE_BLOOM_FILTER, bloom_filter_map_ops)
|
|||
BPF_MAP_TYPE(BPF_MAP_TYPE_USER_RINGBUF, user_ringbuf_map_ops)
|
||||
BPF_MAP_TYPE(BPF_MAP_TYPE_ARENA, arena_map_ops)
|
||||
BPF_MAP_TYPE(BPF_MAP_TYPE_INSN_ARRAY, insn_array_map_ops)
|
||||
BPF_MAP_TYPE(BPF_MAP_TYPE_RHASH, rhtab_map_ops)
|
||||
|
||||
BPF_LINK_TYPE(BPF_LINK_TYPE_RAW_TRACEPOINT, raw_tracepoint)
|
||||
BPF_LINK_TYPE(BPF_LINK_TYPE_TRACING, tracing)
|
||||
|
|
|
|||
|
|
@ -650,6 +650,46 @@ static __always_inline struct rhash_head *__rhashtable_lookup(
|
|||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* rhashtable_next_key - return next element after a given key
|
||||
* @ht: hash table
|
||||
* @prev_key: pointer to previous key, or NULL for the first element
|
||||
*
|
||||
* WARNING: this walk is highly unstable. Unlike rhashtable_walk_*(),
|
||||
* it cannot detect a concurrent resize or rehash, so a full iteration
|
||||
* is NOT guaranteed to terminate under adversarial or sustained
|
||||
* rehashing. Callers MUST tolerate skipped and duplicated elements and
|
||||
* SHOULD bound their loop externally.
|
||||
*
|
||||
* Returns the next element in best-effort iteration order, walking the
|
||||
* @tbl chain (including any future_tbl in flight). Caller must hold RCU.
|
||||
*
|
||||
* Pass @prev_key == NULL to obtain the first element. To iterate, set
|
||||
* @prev_key to the key of the previously returned element on each call,
|
||||
* and stop when NULL is returned.
|
||||
*
|
||||
* Best-effort semantics:
|
||||
* - Across the tbl->future_tbl chain, an element being migrated may
|
||||
* transiently appear in both tables and be observed twice.
|
||||
* - Concurrent inserts may or may not be observed.
|
||||
* - Termination of a full iteration loop is NOT guaranteed under
|
||||
* adversarial continuous rehash; callers MUST tolerate skips and
|
||||
* repeats and SHOULD bound their loop externally.
|
||||
* - Behavior on tables that contain duplicate keys is undefined:
|
||||
* duplicates may be skipped, repeated, or trap the walk in a
|
||||
* cycle. Callers requiring duplicate-key iteration must use
|
||||
* rhashtable_walk_*() instead.
|
||||
* - rhltable instances are not supported and return
|
||||
* ERR_PTR(-EOPNOTSUPP).
|
||||
* - If prev_key was concurrently deleted and is not present in any
|
||||
* in-flight table, returns ERR_PTR(-ENOENT).
|
||||
*
|
||||
* Returns entry of the next element, or NULL when iteration is exhausted,
|
||||
* or ERR_PTR(-ENOENT) if prev_key is not found, or
|
||||
* ERR_PTR(-EOPNOTSUPP) if @ht is an rhltable.
|
||||
*/
|
||||
void *rhashtable_next_key(struct rhashtable *ht, const void *prev_key);
|
||||
|
||||
/**
|
||||
* rhashtable_lookup - search hash table
|
||||
* @ht: hash table
|
||||
|
|
@ -1117,7 +1157,7 @@ static __always_inline int __rhashtable_remove_fast_one(
|
|||
atomic_dec(&ht->nelems);
|
||||
if (unlikely(ht->p.automatic_shrinking &&
|
||||
rht_shrink_below_30(ht, tbl)))
|
||||
schedule_work(&ht->run_work);
|
||||
irq_work_queue(&ht->run_irq_work);
|
||||
err = 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1047,6 +1047,7 @@ enum bpf_map_type {
|
|||
BPF_MAP_TYPE_CGRP_STORAGE,
|
||||
BPF_MAP_TYPE_ARENA,
|
||||
BPF_MAP_TYPE_INSN_ARRAY,
|
||||
BPF_MAP_TYPE_RHASH,
|
||||
__MAX_BPF_MAP_TYPE
|
||||
};
|
||||
|
||||
|
|
@ -1545,6 +1546,11 @@ union bpf_attr {
|
|||
*
|
||||
* BPF_MAP_TYPE_ARENA - contains the address where user space
|
||||
* is going to mmap() the arena. It has to be page aligned.
|
||||
*
|
||||
* BPF_MAP_TYPE_RHASH - initial table size hint
|
||||
* (nelem_hint). 0 = use rhashtable default. Must be
|
||||
* <= min(max_entries, U16_MAX). Upper 32 bits reserved,
|
||||
* must be zero.
|
||||
*/
|
||||
__u64 map_extra;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include <linux/rculist_nulls.h>
|
||||
#include <linux/rcupdate_wait.h>
|
||||
#include <linux/random.h>
|
||||
#include <linux/rhashtable.h>
|
||||
#include <uapi/linux/btf.h>
|
||||
#include <linux/rcupdate_trace.h>
|
||||
#include <linux/btf_ids.h>
|
||||
|
|
@ -496,28 +497,26 @@ static void htab_dtor_ctx_free(void *ctx)
|
|||
kfree(ctx);
|
||||
}
|
||||
|
||||
static int htab_set_dtor(struct bpf_htab *htab, void (*dtor)(void *, void *))
|
||||
static int bpf_ma_set_dtor(struct bpf_map *map, struct bpf_mem_alloc *ma,
|
||||
void (*dtor)(void *, void *))
|
||||
{
|
||||
u32 key_size = htab->map.key_size;
|
||||
struct bpf_mem_alloc *ma;
|
||||
struct htab_btf_record *hrec;
|
||||
int err;
|
||||
|
||||
/* No need for dtors. */
|
||||
if (IS_ERR_OR_NULL(htab->map.record))
|
||||
if (IS_ERR_OR_NULL(map->record))
|
||||
return 0;
|
||||
|
||||
hrec = kzalloc(sizeof(*hrec), GFP_KERNEL);
|
||||
if (!hrec)
|
||||
return -ENOMEM;
|
||||
hrec->key_size = key_size;
|
||||
hrec->record = btf_record_dup(htab->map.record);
|
||||
hrec->key_size = map->key_size;
|
||||
hrec->record = btf_record_dup(map->record);
|
||||
if (IS_ERR(hrec->record)) {
|
||||
err = PTR_ERR(hrec->record);
|
||||
kfree(hrec);
|
||||
return err;
|
||||
}
|
||||
ma = htab_is_percpu(htab) ? &htab->pcpu_ma : &htab->ma;
|
||||
bpf_mem_alloc_set_dtor(ma, dtor, htab_dtor_ctx_free, hrec);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -534,9 +533,9 @@ static int htab_map_check_btf(struct bpf_map *map, const struct btf *btf,
|
|||
* populated in htab_map_alloc(), so it will always appear as NULL.
|
||||
*/
|
||||
if (htab_is_percpu(htab))
|
||||
return htab_set_dtor(htab, htab_pcpu_mem_dtor);
|
||||
return bpf_ma_set_dtor(map, &htab->pcpu_ma, htab_pcpu_mem_dtor);
|
||||
else
|
||||
return htab_set_dtor(htab, htab_mem_dtor);
|
||||
return bpf_ma_set_dtor(map, &htab->ma, htab_mem_dtor);
|
||||
}
|
||||
|
||||
static struct bpf_map *htab_map_alloc(union bpf_attr *attr)
|
||||
|
|
@ -2739,3 +2738,794 @@ const struct bpf_map_ops htab_of_maps_map_ops = {
|
|||
BATCH_OPS(htab),
|
||||
.map_btf_id = &htab_map_btf_ids[0],
|
||||
};
|
||||
|
||||
struct rhtab_elem {
|
||||
struct rhash_head node;
|
||||
/* key bytes, then value bytes follow */
|
||||
u8 data[] __aligned(8);
|
||||
};
|
||||
|
||||
struct bpf_rhtab {
|
||||
struct bpf_map map;
|
||||
struct rhashtable ht;
|
||||
struct bpf_mem_alloc ma;
|
||||
u32 elem_size;
|
||||
bool freeing_internal;
|
||||
};
|
||||
|
||||
static const struct rhashtable_params rhtab_params = {
|
||||
.head_offset = offsetof(struct rhtab_elem, node),
|
||||
.key_offset = offsetof(struct rhtab_elem, data),
|
||||
};
|
||||
|
||||
static inline void *rhtab_elem_value(struct rhtab_elem *l, u32 key_size)
|
||||
{
|
||||
return l->data + round_up(key_size, 8);
|
||||
}
|
||||
|
||||
/* Specialize hash function and objcmp for long sized key */
|
||||
static __always_inline int rhtab_key_cmp_long(struct rhashtable_compare_arg *arg,
|
||||
const void *ptr)
|
||||
{
|
||||
const unsigned long key1 = *(const unsigned long *)arg->key;
|
||||
const struct rhtab_elem *key2 = ptr;
|
||||
|
||||
return key1 != *(const unsigned long *)key2->data;
|
||||
}
|
||||
|
||||
static __always_inline u32 rhtab_hashfn_long(const void *data, u32 len, u32 seed)
|
||||
{
|
||||
u64 k = *(const unsigned long *)data;
|
||||
|
||||
return (u32)(k ^ (k >> 32)) ^ seed;
|
||||
}
|
||||
|
||||
static const struct rhashtable_params rhtab_params_long = {
|
||||
.head_offset = offsetof(struct rhtab_elem, node),
|
||||
.key_offset = offsetof(struct rhtab_elem, data),
|
||||
.key_len = sizeof(long),
|
||||
.hashfn = rhtab_hashfn_long,
|
||||
.obj_cmpfn = rhtab_key_cmp_long,
|
||||
};
|
||||
|
||||
static struct bpf_map *rhtab_map_alloc(union bpf_attr *attr)
|
||||
{
|
||||
struct rhashtable_params params;
|
||||
struct bpf_rhtab *rhtab;
|
||||
int err = 0;
|
||||
|
||||
rhtab = bpf_map_area_alloc(sizeof(*rhtab), NUMA_NO_NODE);
|
||||
if (!rhtab)
|
||||
return ERR_PTR(-ENOMEM);
|
||||
|
||||
bpf_map_init_from_attr(&rhtab->map, attr);
|
||||
|
||||
if (rhtab->map.max_entries > 1UL << 31) {
|
||||
err = -E2BIG;
|
||||
goto free_rhtab;
|
||||
}
|
||||
|
||||
rhtab->elem_size = sizeof(struct rhtab_elem) + round_up(rhtab->map.key_size, 8) +
|
||||
round_up(rhtab->map.value_size, 8);
|
||||
|
||||
params = rhtab_params;
|
||||
params.key_len = rhtab->map.key_size;
|
||||
params.nelem_hint = (u32)attr->map_extra;
|
||||
params.automatic_shrinking = true;
|
||||
|
||||
if (rhtab->map.key_size == sizeof(long)) {
|
||||
params.hashfn = rhtab_hashfn_long;
|
||||
params.obj_cmpfn = rhtab_key_cmp_long;
|
||||
}
|
||||
|
||||
err = rhashtable_init(&rhtab->ht, ¶ms);
|
||||
if (err)
|
||||
goto free_rhtab;
|
||||
|
||||
/* Set max_elems after rhashtable_init() since init zeroes the struct */
|
||||
rhtab->ht.max_elems = rhtab->map.max_entries;
|
||||
|
||||
err = bpf_mem_alloc_init(&rhtab->ma, rhtab->elem_size, false);
|
||||
if (err)
|
||||
goto destroy_rhtab;
|
||||
|
||||
return &rhtab->map;
|
||||
|
||||
destroy_rhtab:
|
||||
rhashtable_destroy(&rhtab->ht);
|
||||
free_rhtab:
|
||||
bpf_map_area_free(rhtab);
|
||||
return ERR_PTR(err);
|
||||
}
|
||||
|
||||
static int rhtab_map_alloc_check(union bpf_attr *attr)
|
||||
{
|
||||
if (!(attr->map_flags & BPF_F_NO_PREALLOC))
|
||||
return -EINVAL;
|
||||
|
||||
if (attr->map_flags & BPF_F_ZERO_SEED)
|
||||
return -EINVAL;
|
||||
|
||||
if (attr->key_size > U16_MAX)
|
||||
return -E2BIG;
|
||||
|
||||
if (attr->map_extra >> 32)
|
||||
return -EINVAL;
|
||||
|
||||
if ((u32)attr->map_extra > U16_MAX)
|
||||
return -E2BIG;
|
||||
|
||||
if ((u32)attr->map_extra > attr->max_entries)
|
||||
return -EINVAL;
|
||||
|
||||
return htab_map_alloc_check(attr);
|
||||
}
|
||||
|
||||
static void rhtab_check_and_free_fields(struct bpf_rhtab *rhtab,
|
||||
struct rhtab_elem *elem)
|
||||
{
|
||||
if (IS_ERR_OR_NULL(rhtab->map.record))
|
||||
return;
|
||||
|
||||
bpf_obj_free_fields(rhtab->map.record,
|
||||
rhtab_elem_value(elem, rhtab->map.key_size));
|
||||
}
|
||||
|
||||
static void rhtab_mem_dtor(void *obj, void *ctx)
|
||||
{
|
||||
struct htab_btf_record *hrec = ctx;
|
||||
struct rhtab_elem *elem = obj;
|
||||
|
||||
if (IS_ERR_OR_NULL(hrec->record))
|
||||
return;
|
||||
|
||||
bpf_obj_free_fields(hrec->record,
|
||||
rhtab_elem_value(elem, hrec->key_size));
|
||||
}
|
||||
|
||||
static void rhtab_free_elem(void *ptr, void *arg)
|
||||
{
|
||||
struct bpf_rhtab *rhtab = arg;
|
||||
struct rhtab_elem *elem = ptr;
|
||||
|
||||
bpf_map_free_internal_structs(&rhtab->map, rhtab_elem_value(elem, rhtab->map.key_size));
|
||||
bpf_mem_cache_free_rcu(&rhtab->ma, elem);
|
||||
}
|
||||
|
||||
static void rhtab_map_free(struct bpf_map *map)
|
||||
{
|
||||
struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
|
||||
|
||||
rhashtable_free_and_destroy(&rhtab->ht, rhtab_free_elem, rhtab);
|
||||
bpf_mem_alloc_destroy(&rhtab->ma);
|
||||
bpf_map_area_free(rhtab);
|
||||
}
|
||||
|
||||
static void *rhtab_lookup_elem(struct bpf_map *map, void *key)
|
||||
{
|
||||
struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
|
||||
|
||||
/* Hold RCU lock in case sleepable program calls via gen_lookup */
|
||||
guard(rcu)();
|
||||
|
||||
if (map->key_size == sizeof(long))
|
||||
return rhashtable_lookup_likely(&rhtab->ht, key, rhtab_params_long);
|
||||
|
||||
return rhashtable_lookup_likely(&rhtab->ht, key, rhtab_params);
|
||||
}
|
||||
|
||||
static void *rhtab_map_lookup_elem(struct bpf_map *map, void *key) __must_hold(RCU)
|
||||
{
|
||||
struct rhtab_elem *l;
|
||||
|
||||
l = rhtab_lookup_elem(map, key);
|
||||
return l ? rhtab_elem_value(l, map->key_size) : NULL;
|
||||
}
|
||||
|
||||
static void rhtab_read_elem_value(struct bpf_map *map, void *dst, struct rhtab_elem *elem,
|
||||
u64 flags)
|
||||
{
|
||||
void *src = rhtab_elem_value(elem, map->key_size);
|
||||
|
||||
if (flags & BPF_F_LOCK)
|
||||
copy_map_value_locked(map, dst, src, true);
|
||||
else
|
||||
copy_map_value(map, dst, src);
|
||||
}
|
||||
|
||||
static int rhtab_delete_elem(struct bpf_rhtab *rhtab, struct rhtab_elem *elem, void *copy,
|
||||
u64 flags)
|
||||
{
|
||||
int err;
|
||||
|
||||
/*
|
||||
* disable_instrumentation() mitigates the deadlock for programs running in NMI context.
|
||||
* rhashtable locks bucket with local_irq_save(). Only NMI programs may reenter
|
||||
* rhashtable code, bpf_disable_instrumentation() disables programs running in NMI, except
|
||||
* raw tracepoints, which we don't have in rhashtable.
|
||||
*/
|
||||
bpf_disable_instrumentation();
|
||||
|
||||
if (rhtab->map.key_size == sizeof(long))
|
||||
err = rhashtable_remove_fast(&rhtab->ht, &elem->node, rhtab_params_long);
|
||||
else
|
||||
err = rhashtable_remove_fast(&rhtab->ht, &elem->node, rhtab_params);
|
||||
|
||||
bpf_enable_instrumentation();
|
||||
|
||||
if (err)
|
||||
return err;
|
||||
|
||||
if (copy) {
|
||||
rhtab_read_elem_value(&rhtab->map, copy, elem, flags);
|
||||
check_and_init_map_value(&rhtab->map, copy);
|
||||
}
|
||||
/* Release internal structs: kptr, bpf_timer, task_work, wq */
|
||||
rhtab_check_and_free_fields(rhtab, elem);
|
||||
bpf_mem_cache_free_rcu(&rhtab->ma, elem);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static long rhtab_map_delete_elem(struct bpf_map *map, void *key)
|
||||
{
|
||||
struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
|
||||
struct rhtab_elem *elem;
|
||||
|
||||
guard(rcu)();
|
||||
|
||||
elem = rhtab_lookup_elem(map, key);
|
||||
if (!elem)
|
||||
return -ENOENT;
|
||||
|
||||
return rhtab_delete_elem(rhtab, elem, NULL, 0);
|
||||
}
|
||||
|
||||
static int rhtab_map_lookup_and_delete_elem(struct bpf_map *map, void *key, void *value, u64 flags)
|
||||
{
|
||||
struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
|
||||
struct rhtab_elem *elem;
|
||||
int err;
|
||||
|
||||
err = bpf_map_check_op_flags(map, flags, BPF_F_LOCK);
|
||||
if (err)
|
||||
return err;
|
||||
|
||||
guard(rcu)();
|
||||
|
||||
elem = rhtab_lookup_elem(map, key);
|
||||
if (!elem)
|
||||
return -ENOENT;
|
||||
|
||||
return rhtab_delete_elem(rhtab, elem, value, flags);
|
||||
}
|
||||
|
||||
static long rhtab_map_update_existing(struct bpf_map *map, struct rhtab_elem *elem, void *value,
|
||||
u64 map_flags)
|
||||
{
|
||||
struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
|
||||
void *old_val = rhtab_elem_value(elem, map->key_size);
|
||||
|
||||
if (map_flags & BPF_NOEXIST)
|
||||
return -EEXIST;
|
||||
|
||||
if (map_flags & BPF_F_LOCK)
|
||||
copy_map_value_locked(map, old_val, value, false);
|
||||
else
|
||||
copy_map_value(map, old_val, value);
|
||||
|
||||
/*
|
||||
* Torn reads: a concurrent reader without BPF_F_LOCK may observe
|
||||
* the value mid-copy. Callers requiring consistent reads must use
|
||||
* BPF_F_LOCK, matching arraymap semantics.
|
||||
*
|
||||
* copy_map_value() skips special-field offsets, so old timers/
|
||||
* kptrs/etc. still sit in the slot. Cancel them after the copy
|
||||
* to match arraymap's update semantics.
|
||||
*/
|
||||
rhtab_check_and_free_fields(rhtab, elem);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static long rhtab_map_update_elem(struct bpf_map *map, void *key, void *value, u64 map_flags)
|
||||
{
|
||||
struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
|
||||
struct rhtab_elem *elem, *tmp;
|
||||
|
||||
if (unlikely((map_flags & ~BPF_F_LOCK) > BPF_EXIST))
|
||||
return -EINVAL;
|
||||
|
||||
if ((map_flags & BPF_F_LOCK) && !btf_record_has_field(map->record, BPF_SPIN_LOCK))
|
||||
return -EINVAL;
|
||||
|
||||
guard(rcu)();
|
||||
elem = rhtab_lookup_elem(map, key);
|
||||
if (elem)
|
||||
return rhtab_map_update_existing(map, elem, value, map_flags);
|
||||
|
||||
if (map_flags & BPF_EXIST)
|
||||
return -ENOENT;
|
||||
|
||||
/*
|
||||
* Reject new insertions while map_release_uref cleanup walks the
|
||||
* table. Without this, new elements could keep triggering rehash
|
||||
* and prevent the walk from terminating.
|
||||
*/
|
||||
if (READ_ONCE(rhtab->freeing_internal))
|
||||
return -EBUSY;
|
||||
|
||||
/* Check max_entries limit before inserting new element */
|
||||
if (atomic_read(&rhtab->ht.nelems) >= map->max_entries)
|
||||
return -E2BIG;
|
||||
|
||||
elem = bpf_mem_cache_alloc(&rhtab->ma);
|
||||
if (!elem)
|
||||
return -ENOMEM;
|
||||
|
||||
memcpy(elem->data, key, map->key_size);
|
||||
copy_map_value(map, rhtab_elem_value(elem, map->key_size), value);
|
||||
check_and_init_map_value(map, rhtab_elem_value(elem, map->key_size));
|
||||
|
||||
/* Prevent deadlock for NMI programs attempting to take bucket lock */
|
||||
bpf_disable_instrumentation();
|
||||
|
||||
if (map->key_size == sizeof(long))
|
||||
tmp = rhashtable_lookup_get_insert_fast(&rhtab->ht, &elem->node, rhtab_params_long);
|
||||
else
|
||||
tmp = rhashtable_lookup_get_insert_fast(&rhtab->ht, &elem->node, rhtab_params);
|
||||
|
||||
bpf_enable_instrumentation();
|
||||
|
||||
if (tmp) {
|
||||
bpf_mem_cache_free(&rhtab->ma, elem);
|
||||
if (IS_ERR(tmp))
|
||||
return PTR_ERR(tmp);
|
||||
|
||||
return rhtab_map_update_existing(map, tmp, value, map_flags);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int rhtab_map_gen_lookup(struct bpf_map *map, struct bpf_insn *insn_buf)
|
||||
{
|
||||
struct bpf_insn *insn = insn_buf;
|
||||
const int ret = BPF_REG_0;
|
||||
|
||||
BUILD_BUG_ON(!__same_type(&rhtab_lookup_elem,
|
||||
(void *(*)(struct bpf_map *map, void *key)) NULL));
|
||||
*insn++ = BPF_EMIT_CALL(rhtab_lookup_elem);
|
||||
*insn++ = BPF_JMP_IMM(BPF_JEQ, ret, 0, 1);
|
||||
*insn++ = BPF_ALU64_IMM(BPF_ADD, ret,
|
||||
offsetof(struct rhtab_elem, data) + round_up(map->key_size, 8));
|
||||
|
||||
return insn - insn_buf;
|
||||
}
|
||||
|
||||
static int rhtab_map_check_btf(struct bpf_map *map, const struct btf *btf,
|
||||
const struct btf_type *key_type,
|
||||
const struct btf_type *value_type)
|
||||
{
|
||||
struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
|
||||
|
||||
return bpf_ma_set_dtor(map, &rhtab->ma, rhtab_mem_dtor);
|
||||
}
|
||||
|
||||
static void rhtab_map_free_internal_structs(struct bpf_map *map)
|
||||
{
|
||||
struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
|
||||
struct rhashtable_iter iter;
|
||||
struct rhtab_elem *elem;
|
||||
|
||||
if (!bpf_map_has_internal_structs(map))
|
||||
return;
|
||||
|
||||
/*
|
||||
* Block new insertions. Once observed, no new growth is triggered,
|
||||
* so any in-flight rehash will drain and the walker is guaranteed
|
||||
* to stop returning -EAGAIN. Treat -EAGAIN as "rehash in progress,
|
||||
* retry"; do not wait for the worker.
|
||||
*/
|
||||
WRITE_ONCE(rhtab->freeing_internal, true);
|
||||
|
||||
rhashtable_walk_enter(&rhtab->ht, &iter);
|
||||
rhashtable_walk_start(&iter);
|
||||
|
||||
while ((elem = rhashtable_walk_next(&iter))) {
|
||||
if (IS_ERR(elem)) {
|
||||
if (PTR_ERR(elem) == -EAGAIN)
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
|
||||
bpf_map_free_internal_structs(map, rhtab_elem_value(elem, map->key_size));
|
||||
|
||||
if (need_resched()) { /* Avoid stalls on large maps */
|
||||
rhashtable_walk_stop(&iter);
|
||||
cond_resched();
|
||||
rhashtable_walk_start(&iter);
|
||||
}
|
||||
}
|
||||
|
||||
rhashtable_walk_stop(&iter);
|
||||
rhashtable_walk_exit(&iter);
|
||||
WRITE_ONCE(rhtab->freeing_internal, false);
|
||||
}
|
||||
|
||||
static int rhtab_map_get_next_key(struct bpf_map *map, void *key, void *next_key)
|
||||
__must_hold_shared(RCU)
|
||||
{
|
||||
struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
|
||||
struct rhtab_elem *elem;
|
||||
|
||||
elem = rhashtable_next_key(&rhtab->ht, key);
|
||||
|
||||
/* if not found, return the first key */
|
||||
if (PTR_ERR(elem) == -ENOENT)
|
||||
elem = rhashtable_next_key(&rhtab->ht, NULL);
|
||||
|
||||
if (IS_ERR(elem))
|
||||
return PTR_ERR(elem);
|
||||
if (!elem)
|
||||
return -ENOENT;
|
||||
|
||||
memcpy(next_key, elem->data, map->key_size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void rhtab_map_seq_show_elem(struct bpf_map *map, void *key, struct seq_file *m)
|
||||
{
|
||||
void *value;
|
||||
|
||||
/* Guarantee that hashtab value is not freed */
|
||||
guard(rcu)();
|
||||
|
||||
value = rhtab_map_lookup_elem(map, key);
|
||||
if (!value)
|
||||
return;
|
||||
|
||||
btf_type_seq_show(map->btf, map->btf_key_type_id, key, m);
|
||||
seq_puts(m, ": ");
|
||||
btf_type_seq_show(map->btf, map->btf_value_type_id, value, m);
|
||||
seq_putc(m, '\n');
|
||||
}
|
||||
|
||||
static long bpf_each_rhash_elem(struct bpf_map *map, bpf_callback_t callback_fn,
|
||||
void *callback_ctx, u64 flags)
|
||||
{
|
||||
struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
|
||||
void *prev_key = NULL;
|
||||
struct rhtab_elem *elem;
|
||||
int num_elems = 0;
|
||||
u64 ret = 0;
|
||||
|
||||
cant_migrate();
|
||||
|
||||
if (flags != 0)
|
||||
return -EINVAL;
|
||||
|
||||
rcu_read_lock();
|
||||
/*
|
||||
* Best-effort iteration: if rhashtable is concurrently resized or
|
||||
* elements are deleted/inserted, there may be missed or duplicate
|
||||
* elements visited.
|
||||
*/
|
||||
while ((elem = rhashtable_next_key(&rhtab->ht, prev_key))) {
|
||||
if (IS_ERR(elem))
|
||||
break;
|
||||
num_elems++;
|
||||
ret = callback_fn((u64)(long)map,
|
||||
(u64)(long)elem->data,
|
||||
(u64)(long)rhtab_elem_value(elem, map->key_size),
|
||||
(u64)(long)callback_ctx, 0);
|
||||
if (ret)
|
||||
break;
|
||||
|
||||
prev_key = elem->data; /* valid while RCU held */
|
||||
}
|
||||
rcu_read_unlock();
|
||||
|
||||
return num_elems;
|
||||
}
|
||||
|
||||
static u64 rhtab_map_mem_usage(const struct bpf_map *map)
|
||||
{
|
||||
struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
|
||||
u64 num_entries;
|
||||
|
||||
/* Excludes rhashtable bucket overhead (~ nelems * sizeof(void *) at 75% load). */
|
||||
num_entries = atomic_read(&rhtab->ht.nelems);
|
||||
return sizeof(struct bpf_rhtab) + rhtab->elem_size * num_entries;
|
||||
}
|
||||
|
||||
static int __rhtab_map_lookup_and_delete_batch(struct bpf_map *map,
|
||||
const union bpf_attr *attr,
|
||||
union bpf_attr __user *uattr,
|
||||
bool do_delete)
|
||||
{
|
||||
struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
|
||||
void __user *uvalues = u64_to_user_ptr(attr->batch.values);
|
||||
void __user *ukeys = u64_to_user_ptr(attr->batch.keys);
|
||||
void __user *ubatch = u64_to_user_ptr(attr->batch.in_batch);
|
||||
void *cursor = NULL, *keys = NULL, *values = NULL, *dst_key, *dst_val;
|
||||
struct rhtab_elem **del_elems = NULL;
|
||||
u32 max_count, total, key_size, value_size, i;
|
||||
bool has_next_cursor = false;
|
||||
struct rhtab_elem *elem;
|
||||
u64 elem_map_flags, map_flags;
|
||||
int ret = 0;
|
||||
|
||||
elem_map_flags = attr->batch.elem_flags;
|
||||
ret = bpf_map_check_op_flags(map, elem_map_flags, BPF_F_LOCK);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
map_flags = attr->batch.flags;
|
||||
if (map_flags)
|
||||
return -EINVAL;
|
||||
|
||||
max_count = attr->batch.count;
|
||||
if (!max_count)
|
||||
return 0;
|
||||
|
||||
if (put_user(0, &uattr->batch.count))
|
||||
return -EFAULT;
|
||||
|
||||
key_size = map->key_size;
|
||||
value_size = map->value_size;
|
||||
|
||||
keys = kvmalloc_array(max_count, key_size, GFP_USER | __GFP_NOWARN);
|
||||
values = kvmalloc_array(max_count, value_size, GFP_USER | __GFP_NOWARN);
|
||||
if (do_delete)
|
||||
del_elems = kvmalloc_array(max_count, sizeof(void *),
|
||||
GFP_USER | __GFP_NOWARN);
|
||||
cursor = kmalloc(key_size, GFP_USER | __GFP_NOWARN);
|
||||
|
||||
if (!keys || !values || !cursor || (do_delete && !del_elems)) {
|
||||
ret = -ENOMEM;
|
||||
goto free;
|
||||
}
|
||||
|
||||
if (ubatch && copy_from_user(cursor, ubatch, key_size)) {
|
||||
ret = -EFAULT;
|
||||
goto free;
|
||||
}
|
||||
|
||||
dst_key = keys;
|
||||
dst_val = values;
|
||||
total = 0;
|
||||
|
||||
rcu_read_lock();
|
||||
|
||||
/*
|
||||
* Cursor stores the key of the next-to-process element (stashed by
|
||||
* the previous batch). Look it up directly so the element is included
|
||||
* here rather than skipped by next_key(). If the cursor was deleted
|
||||
* concurrently (or by the previous do_delete batch), return -EAGAIN
|
||||
* so userspace can distinguish a lost cursor from end-of-iteration
|
||||
* (-ENOENT) and restart from a NULL cursor.
|
||||
*/
|
||||
if (ubatch) {
|
||||
elem = rhtab_lookup_elem(map, cursor);
|
||||
if (!elem) {
|
||||
rcu_read_unlock();
|
||||
ret = -EAGAIN;
|
||||
goto free;
|
||||
}
|
||||
} else {
|
||||
elem = rhashtable_next_key(&rhtab->ht, NULL);
|
||||
}
|
||||
|
||||
while (elem && !IS_ERR(elem) && total < max_count) {
|
||||
memcpy(dst_key, elem->data, key_size);
|
||||
rhtab_read_elem_value(map, dst_val, elem, elem_map_flags);
|
||||
check_and_init_map_value(map, dst_val);
|
||||
|
||||
if (do_delete)
|
||||
del_elems[total] = elem;
|
||||
|
||||
elem = rhashtable_next_key(&rhtab->ht, dst_key);
|
||||
dst_key += key_size;
|
||||
dst_val += value_size;
|
||||
total++;
|
||||
|
||||
/* Bail to userspace to avoid stalls. */
|
||||
if (need_resched())
|
||||
break;
|
||||
}
|
||||
|
||||
if (elem && !IS_ERR(elem)) {
|
||||
/* Stash next-to-process key as cursor for the next batch. */
|
||||
memcpy(cursor, elem->data, key_size);
|
||||
has_next_cursor = true;
|
||||
}
|
||||
|
||||
if (do_delete) {
|
||||
for (i = 0; i < total; i++)
|
||||
rhtab_delete_elem(rhtab, del_elems[i], NULL, 0);
|
||||
}
|
||||
|
||||
rcu_read_unlock();
|
||||
|
||||
if (total == 0) {
|
||||
ret = -ENOENT;
|
||||
goto free;
|
||||
}
|
||||
|
||||
/* No more elements after this batch. */
|
||||
if (!has_next_cursor)
|
||||
ret = -ENOENT;
|
||||
|
||||
if (copy_to_user(ukeys, keys, (size_t)total * key_size) ||
|
||||
copy_to_user(uvalues, values, (size_t)total * value_size) ||
|
||||
put_user(total, &uattr->batch.count) ||
|
||||
(has_next_cursor &&
|
||||
copy_to_user(u64_to_user_ptr(attr->batch.out_batch),
|
||||
cursor, key_size))) {
|
||||
ret = -EFAULT;
|
||||
goto free;
|
||||
}
|
||||
|
||||
free:
|
||||
kfree(cursor);
|
||||
kvfree(keys);
|
||||
kvfree(values);
|
||||
kvfree(del_elems);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int rhtab_map_lookup_batch(struct bpf_map *map, const union bpf_attr *attr,
|
||||
union bpf_attr __user *uattr)
|
||||
{
|
||||
return __rhtab_map_lookup_and_delete_batch(map, attr, uattr, false);
|
||||
}
|
||||
|
||||
static int rhtab_map_lookup_and_delete_batch(struct bpf_map *map, const union bpf_attr *attr,
|
||||
union bpf_attr __user *uattr)
|
||||
{
|
||||
return __rhtab_map_lookup_and_delete_batch(map, attr, uattr, true);
|
||||
}
|
||||
|
||||
struct bpf_iter_seq_rhash_map_info {
|
||||
struct bpf_map *map;
|
||||
struct bpf_rhtab *rhtab;
|
||||
struct rhashtable_iter iter;
|
||||
};
|
||||
|
||||
static void *bpf_rhash_map_seq_start(struct seq_file *seq, loff_t *pos)
|
||||
__acquires(RCU)
|
||||
{
|
||||
struct bpf_iter_seq_rhash_map_info *info = seq->private;
|
||||
struct rhtab_elem *elem;
|
||||
|
||||
rhashtable_walk_start(&info->iter);
|
||||
/*
|
||||
* Re-deliver the element returned by walk_next() at the end of the
|
||||
* previous read() — bpf_seq_read may have stopped before show()
|
||||
* consumed it. Rehash rewinds the walker; retry on -EAGAIN.
|
||||
*/
|
||||
do {
|
||||
elem = rhashtable_walk_peek(&info->iter);
|
||||
} while (PTR_ERR(elem) == -EAGAIN);
|
||||
|
||||
if (IS_ERR(elem))
|
||||
return NULL;
|
||||
|
||||
if (elem && *pos == 0)
|
||||
++*pos;
|
||||
return elem;
|
||||
}
|
||||
|
||||
static void *bpf_rhash_map_seq_next(struct seq_file *seq, void *v, loff_t *pos)
|
||||
{
|
||||
struct bpf_iter_seq_rhash_map_info *info = seq->private;
|
||||
struct rhtab_elem *elem;
|
||||
|
||||
++*pos;
|
||||
|
||||
/* Rehash rewinds the walker; retry until it stops returning -EAGAIN. */
|
||||
do {
|
||||
elem = rhashtable_walk_next(&info->iter);
|
||||
} while (PTR_ERR(elem) == -EAGAIN);
|
||||
|
||||
if (IS_ERR(elem))
|
||||
return NULL;
|
||||
return elem;
|
||||
}
|
||||
|
||||
static int __bpf_rhash_map_seq_show(struct seq_file *seq,
|
||||
struct rhtab_elem *elem)
|
||||
{
|
||||
struct bpf_iter_seq_rhash_map_info *info = seq->private;
|
||||
struct bpf_iter__bpf_map_elem ctx = {};
|
||||
struct bpf_iter_meta meta;
|
||||
struct bpf_prog *prog;
|
||||
int ret = 0;
|
||||
|
||||
meta.seq = seq;
|
||||
prog = bpf_iter_get_info(&meta, elem == NULL);
|
||||
if (prog) {
|
||||
ctx.meta = &meta;
|
||||
ctx.map = info->map;
|
||||
if (elem) {
|
||||
ctx.key = elem->data;
|
||||
ctx.value = rhtab_elem_value(elem, info->map->key_size);
|
||||
}
|
||||
ret = bpf_iter_run_prog(prog, &ctx);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int bpf_rhash_map_seq_show(struct seq_file *seq, void *v)
|
||||
{
|
||||
return __bpf_rhash_map_seq_show(seq, v);
|
||||
}
|
||||
|
||||
static void bpf_rhash_map_seq_stop(struct seq_file *seq, void *v)
|
||||
__releases(RCU)
|
||||
{
|
||||
struct bpf_iter_seq_rhash_map_info *info = seq->private;
|
||||
|
||||
if (!v)
|
||||
(void)__bpf_rhash_map_seq_show(seq, NULL);
|
||||
|
||||
rhashtable_walk_stop(&info->iter);
|
||||
}
|
||||
|
||||
static int bpf_iter_init_rhash_map(void *priv_data, struct bpf_iter_aux_info *aux)
|
||||
{
|
||||
struct bpf_iter_seq_rhash_map_info *info = priv_data;
|
||||
struct bpf_map *map = aux->map;
|
||||
|
||||
bpf_map_inc_with_uref(map);
|
||||
info->map = map;
|
||||
info->rhtab = container_of(map, struct bpf_rhtab, map);
|
||||
rhashtable_walk_enter(&info->rhtab->ht, &info->iter);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void bpf_iter_fini_rhash_map(void *priv_data)
|
||||
{
|
||||
struct bpf_iter_seq_rhash_map_info *info = priv_data;
|
||||
|
||||
rhashtable_walk_exit(&info->iter);
|
||||
bpf_map_put_with_uref(info->map);
|
||||
}
|
||||
|
||||
static const struct seq_operations bpf_rhash_map_seq_ops = {
|
||||
.start = bpf_rhash_map_seq_start,
|
||||
.next = bpf_rhash_map_seq_next,
|
||||
.stop = bpf_rhash_map_seq_stop,
|
||||
.show = bpf_rhash_map_seq_show,
|
||||
};
|
||||
|
||||
static const struct bpf_iter_seq_info rhash_iter_seq_info = {
|
||||
.seq_ops = &bpf_rhash_map_seq_ops,
|
||||
.init_seq_private = bpf_iter_init_rhash_map,
|
||||
.fini_seq_private = bpf_iter_fini_rhash_map,
|
||||
.seq_priv_size = sizeof(struct bpf_iter_seq_rhash_map_info),
|
||||
};
|
||||
|
||||
BTF_ID_LIST_SINGLE(rhtab_map_btf_ids, struct, bpf_rhtab)
|
||||
const struct bpf_map_ops rhtab_map_ops = {
|
||||
.map_meta_equal = bpf_map_meta_equal,
|
||||
.map_alloc_check = rhtab_map_alloc_check,
|
||||
.map_alloc = rhtab_map_alloc,
|
||||
.map_free = rhtab_map_free,
|
||||
.map_get_next_key = rhtab_map_get_next_key,
|
||||
.map_release_uref = rhtab_map_free_internal_structs,
|
||||
.map_check_btf = rhtab_map_check_btf,
|
||||
.map_lookup_elem = rhtab_map_lookup_elem,
|
||||
.map_lookup_and_delete_elem = rhtab_map_lookup_and_delete_elem,
|
||||
.map_update_elem = rhtab_map_update_elem,
|
||||
.map_delete_elem = rhtab_map_delete_elem,
|
||||
.map_gen_lookup = rhtab_map_gen_lookup,
|
||||
.map_seq_show_elem = rhtab_map_seq_show_elem,
|
||||
.map_set_for_each_callback_args = map_set_for_each_callback_args,
|
||||
.map_for_each_callback = bpf_each_rhash_elem,
|
||||
.map_mem_usage = rhtab_map_mem_usage,
|
||||
BATCH_OPS(rhtab),
|
||||
.map_btf_id = &rhtab_map_btf_ids[0],
|
||||
.iter_seq_info = &rhash_iter_seq_info,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -123,7 +123,8 @@ static int bpf_iter_attach_map(struct bpf_prog *prog,
|
|||
is_percpu = true;
|
||||
else if (map->map_type != BPF_MAP_TYPE_HASH &&
|
||||
map->map_type != BPF_MAP_TYPE_LRU_HASH &&
|
||||
map->map_type != BPF_MAP_TYPE_ARRAY)
|
||||
map->map_type != BPF_MAP_TYPE_ARRAY &&
|
||||
map->map_type != BPF_MAP_TYPE_RHASH)
|
||||
goto put_map;
|
||||
|
||||
key_acc_size = prog->aux->max_rdonly_access;
|
||||
|
|
|
|||
|
|
@ -1280,6 +1280,7 @@ static int map_check_btf(struct bpf_map *map, struct bpf_token *token,
|
|||
case BPF_SPIN_LOCK:
|
||||
case BPF_RES_SPIN_LOCK:
|
||||
if (map->map_type != BPF_MAP_TYPE_HASH &&
|
||||
map->map_type != BPF_MAP_TYPE_RHASH &&
|
||||
map->map_type != BPF_MAP_TYPE_ARRAY &&
|
||||
map->map_type != BPF_MAP_TYPE_CGROUP_STORAGE &&
|
||||
map->map_type != BPF_MAP_TYPE_SK_STORAGE &&
|
||||
|
|
@ -1294,6 +1295,7 @@ static int map_check_btf(struct bpf_map *map, struct bpf_token *token,
|
|||
case BPF_WORKQUEUE:
|
||||
case BPF_TASK_WORK:
|
||||
if (map->map_type != BPF_MAP_TYPE_HASH &&
|
||||
map->map_type != BPF_MAP_TYPE_RHASH &&
|
||||
map->map_type != BPF_MAP_TYPE_LRU_HASH &&
|
||||
map->map_type != BPF_MAP_TYPE_ARRAY) {
|
||||
ret = -EOPNOTSUPP;
|
||||
|
|
@ -1305,6 +1307,7 @@ static int map_check_btf(struct bpf_map *map, struct bpf_token *token,
|
|||
case BPF_KPTR_PERCPU:
|
||||
case BPF_REFCOUNT:
|
||||
if (map->map_type != BPF_MAP_TYPE_HASH &&
|
||||
map->map_type != BPF_MAP_TYPE_RHASH &&
|
||||
map->map_type != BPF_MAP_TYPE_PERCPU_HASH &&
|
||||
map->map_type != BPF_MAP_TYPE_LRU_HASH &&
|
||||
map->map_type != BPF_MAP_TYPE_LRU_PERCPU_HASH &&
|
||||
|
|
@ -1398,6 +1401,7 @@ static int map_create_alloc(union bpf_attr *attr, bpfptr_t uattr, struct bpf_ver
|
|||
|
||||
if (attr->map_type != BPF_MAP_TYPE_BLOOM_FILTER &&
|
||||
attr->map_type != BPF_MAP_TYPE_ARENA &&
|
||||
attr->map_type != BPF_MAP_TYPE_RHASH &&
|
||||
attr->map_extra != 0) {
|
||||
bpf_log(log, "Invalid map_extra.\n");
|
||||
return -EINVAL;
|
||||
|
|
@ -1469,6 +1473,7 @@ static int map_create_alloc(union bpf_attr *attr, bpfptr_t uattr, struct bpf_ver
|
|||
case BPF_MAP_TYPE_CGROUP_ARRAY:
|
||||
case BPF_MAP_TYPE_ARRAY_OF_MAPS:
|
||||
case BPF_MAP_TYPE_HASH:
|
||||
case BPF_MAP_TYPE_RHASH:
|
||||
case BPF_MAP_TYPE_PERCPU_HASH:
|
||||
case BPF_MAP_TYPE_HASH_OF_MAPS:
|
||||
case BPF_MAP_TYPE_RINGBUF:
|
||||
|
|
@ -2259,6 +2264,7 @@ static int map_lookup_and_delete_elem(union bpf_attr *attr)
|
|||
map->map_type == BPF_MAP_TYPE_PERCPU_HASH ||
|
||||
map->map_type == BPF_MAP_TYPE_LRU_HASH ||
|
||||
map->map_type == BPF_MAP_TYPE_LRU_PERCPU_HASH ||
|
||||
map->map_type == BPF_MAP_TYPE_RHASH ||
|
||||
map->map_type == BPF_MAP_TYPE_STACK_TRACE) {
|
||||
if (!bpf_map_is_offloaded(map)) {
|
||||
bpf_disable_instrumentation();
|
||||
|
|
|
|||
|
|
@ -17657,6 +17657,7 @@ static int check_map_prog_compatibility(struct bpf_verifier_env *env,
|
|||
if (prog->sleepable)
|
||||
switch (map->map_type) {
|
||||
case BPF_MAP_TYPE_HASH:
|
||||
case BPF_MAP_TYPE_RHASH:
|
||||
case BPF_MAP_TYPE_LRU_HASH:
|
||||
case BPF_MAP_TYPE_ARRAY:
|
||||
case BPF_MAP_TYPE_PERCPU_HASH:
|
||||
|
|
|
|||
|
|
@ -687,6 +687,75 @@ void *rhashtable_insert_slow(struct rhashtable *ht, const void *key,
|
|||
}
|
||||
EXPORT_SYMBOL_GPL(rhashtable_insert_slow);
|
||||
|
||||
/* Scan one element forward from prev_key's position in @tbl.
|
||||
* Returns first rhash_head whose bucket > prev_key's bucket, or the
|
||||
* element immediately after prev_key inside prev_key's bucket.
|
||||
* Returns the first element if prev_key is NULL, NULL when @tbl is
|
||||
* exhausted, or ERR_PTR(-ENOENT) if prev_key is not found in @tbl.
|
||||
*/
|
||||
static struct rhash_head *__rhashtable_next_in_table(
|
||||
struct rhashtable *ht, struct bucket_table *tbl,
|
||||
const void *prev_key)
|
||||
{
|
||||
struct rhashtable_compare_arg arg = { .ht = ht, .key = prev_key };
|
||||
const struct rhashtable_params params = ht->p;
|
||||
struct rhash_head *he;
|
||||
unsigned int b = 0;
|
||||
bool found = false;
|
||||
|
||||
if (prev_key) {
|
||||
b = rht_key_hashfn(ht, tbl, prev_key, params);
|
||||
rht_for_each_rcu(he, tbl, b) {
|
||||
bool match = params.obj_cmpfn
|
||||
? !params.obj_cmpfn(&arg, rht_obj(ht, he))
|
||||
: !rhashtable_compare(&arg, rht_obj(ht, he));
|
||||
if (found) {
|
||||
if (match)
|
||||
continue;
|
||||
return he;
|
||||
}
|
||||
if (match)
|
||||
found = true;
|
||||
}
|
||||
if (!found)
|
||||
return ERR_PTR(-ENOENT);
|
||||
b++;
|
||||
}
|
||||
|
||||
for (; b < tbl->size; b++)
|
||||
rht_for_each_rcu(he, tbl, b)
|
||||
return he;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* rhashtable_next_key - return next element after a given key
|
||||
*
|
||||
* See include/linux/rhashtable.h for the full contract.
|
||||
*/
|
||||
void *rhashtable_next_key(struct rhashtable *ht, const void *prev_key)
|
||||
{
|
||||
struct bucket_table *tbl;
|
||||
struct rhash_head *he;
|
||||
|
||||
if (unlikely(ht->rhlist))
|
||||
return ERR_PTR(-EOPNOTSUPP);
|
||||
|
||||
tbl = rht_dereference_rcu(ht->tbl, ht);
|
||||
do {
|
||||
he = __rhashtable_next_in_table(ht, tbl, prev_key);
|
||||
if (!IS_ERR_OR_NULL(he))
|
||||
return rht_obj(ht, he);
|
||||
if (!he)
|
||||
prev_key = NULL;
|
||||
/* See any new future_tbl attached during a rehash. */
|
||||
smp_rmb();
|
||||
tbl = rht_dereference_rcu(tbl->future_tbl, ht);
|
||||
} while (tbl);
|
||||
return he; /* NULL or -ENOENT */
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(rhashtable_next_key);
|
||||
|
||||
/**
|
||||
* rhashtable_walk_enter - Initialise an iterator
|
||||
* @ht: Table to walk over
|
||||
|
|
|
|||
|
|
@ -679,6 +679,78 @@ static int threadfunc(void *data)
|
|||
return err;
|
||||
}
|
||||
|
||||
static int __init test_rhashtable_next_key(void)
|
||||
{
|
||||
struct rhashtable_params params = test_rht_params;
|
||||
struct test_obj_val key_missing = { .id = 99999, .tid = 0 };
|
||||
struct test_obj_val *prev_key = NULL;
|
||||
struct rhashtable ht;
|
||||
struct test_obj *objs, *cur;
|
||||
int i, count = 0, err;
|
||||
int visited_keys[8] = { 0 };
|
||||
const int n = ARRAY_SIZE(visited_keys);
|
||||
|
||||
params.nelem_hint = n;
|
||||
|
||||
err = rhashtable_init(&ht, ¶ms);
|
||||
if (err)
|
||||
return err;
|
||||
|
||||
objs = kcalloc(n, sizeof(*objs), GFP_KERNEL);
|
||||
if (!objs) {
|
||||
rhashtable_destroy(&ht);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
for (i = 0; i < n; i++) {
|
||||
objs[i].value.id = i;
|
||||
err = rhashtable_insert_fast(&ht, &objs[i].node, params);
|
||||
if (err)
|
||||
goto out;
|
||||
}
|
||||
|
||||
rcu_read_lock();
|
||||
|
||||
/* NULL prev_key: walk from the beginning, expect all n elements. */
|
||||
while ((cur = rhashtable_next_key(&ht, prev_key))) {
|
||||
if (IS_ERR(cur)) {
|
||||
err = -EINVAL;
|
||||
goto unlock;
|
||||
}
|
||||
count++;
|
||||
prev_key = &cur->value;
|
||||
visited_keys[cur->value.id] = 1;
|
||||
if (count > n)
|
||||
break;
|
||||
}
|
||||
|
||||
if (count != n) {
|
||||
err = -EINVAL;
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
for (i = 0; i < n; i++) {
|
||||
if (!visited_keys[i]) {
|
||||
err = -EINVAL;
|
||||
goto unlock;
|
||||
}
|
||||
}
|
||||
|
||||
/* Non-existing prev_key: must return ERR_PTR(-ENOENT). */
|
||||
cur = rhashtable_next_key(&ht, &key_missing);
|
||||
if (!IS_ERR(cur) || PTR_ERR(cur) != -ENOENT)
|
||||
err = -EINVAL;
|
||||
|
||||
unlock:
|
||||
rcu_read_unlock();
|
||||
out:
|
||||
for (i = 0; i < n; i++)
|
||||
rhashtable_remove_fast(&ht, &objs[i].node, params);
|
||||
kfree(objs);
|
||||
rhashtable_destroy(&ht);
|
||||
return err;
|
||||
}
|
||||
|
||||
static int __init test_rht_init(void)
|
||||
{
|
||||
unsigned int entries;
|
||||
|
|
@ -738,6 +810,9 @@ static int __init test_rht_init(void)
|
|||
|
||||
test_insert_duplicates_run();
|
||||
|
||||
pr_info("Testing rhashtable_next_key: %s\n",
|
||||
test_rhashtable_next_key() == 0 ? "pass" : "FAIL");
|
||||
|
||||
if (!tcount)
|
||||
return 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ MAP COMMANDS
|
|||
| | **cgroup_storage** | **reuseport_sockarray** | **percpu_cgroup_storage**
|
||||
| | **queue** | **stack** | **sk_storage** | **struct_ops** | **ringbuf** | **inode_storage**
|
||||
| | **task_storage** | **bloom_filter** | **user_ringbuf** | **cgrp_storage** | **arena**
|
||||
| | **insn_array** }
|
||||
| | **insn_array** | **rhash** }
|
||||
|
||||
DESCRIPTION
|
||||
===========
|
||||
|
|
|
|||
|
|
@ -1478,7 +1478,7 @@ static int do_help(int argc, char **argv)
|
|||
" cgroup_storage | reuseport_sockarray | percpu_cgroup_storage |\n"
|
||||
" queue | stack | sk_storage | struct_ops | ringbuf | inode_storage |\n"
|
||||
" task_storage | bloom_filter | user_ringbuf | cgrp_storage | arena |\n"
|
||||
" insn_array }\n"
|
||||
" insn_array | rhash }\n"
|
||||
" " HELP_SPEC_OPTIONS " |\n"
|
||||
" {-f|--bpffs} | {-n|--nomount} }\n"
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -1047,6 +1047,7 @@ enum bpf_map_type {
|
|||
BPF_MAP_TYPE_CGRP_STORAGE,
|
||||
BPF_MAP_TYPE_ARENA,
|
||||
BPF_MAP_TYPE_INSN_ARRAY,
|
||||
BPF_MAP_TYPE_RHASH,
|
||||
__MAX_BPF_MAP_TYPE
|
||||
};
|
||||
|
||||
|
|
@ -1545,6 +1546,11 @@ union bpf_attr {
|
|||
*
|
||||
* BPF_MAP_TYPE_ARENA - contains the address where user space
|
||||
* is going to mmap() the arena. It has to be page aligned.
|
||||
*
|
||||
* BPF_MAP_TYPE_RHASH - initial table size hint
|
||||
* (nelem_hint). 0 = use rhashtable default. Must be
|
||||
* <= min(max_entries, U16_MAX). Upper 32 bits reserved,
|
||||
* must be zero.
|
||||
*/
|
||||
__u64 map_extra;
|
||||
|
||||
|
|
|
|||
|
|
@ -192,6 +192,7 @@ static const char * const map_type_name[] = {
|
|||
[BPF_MAP_TYPE_CGRP_STORAGE] = "cgrp_storage",
|
||||
[BPF_MAP_TYPE_ARENA] = "arena",
|
||||
[BPF_MAP_TYPE_INSN_ARRAY] = "insn_array",
|
||||
[BPF_MAP_TYPE_RHASH] = "rhash",
|
||||
};
|
||||
|
||||
static const char * const prog_type_name[] = {
|
||||
|
|
|
|||
|
|
@ -309,6 +309,9 @@ static int probe_map_create(enum bpf_map_type map_type)
|
|||
value_size = sizeof(__u64);
|
||||
opts.map_flags = BPF_F_NO_PREALLOC;
|
||||
break;
|
||||
case BPF_MAP_TYPE_RHASH:
|
||||
opts.map_flags = BPF_F_NO_PREALLOC;
|
||||
break;
|
||||
case BPF_MAP_TYPE_CGROUP_STORAGE:
|
||||
case BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE:
|
||||
key_size = sizeof(struct bpf_cgroup_storage_key);
|
||||
|
|
|
|||
|
|
@ -560,13 +560,16 @@ extern const struct bench bench_bpf_loop;
|
|||
extern const struct bench bench_strncmp_no_helper;
|
||||
extern const struct bench bench_strncmp_helper;
|
||||
extern const struct bench bench_bpf_hashmap_full_update;
|
||||
extern const struct bench bench_bpf_rhashmap_full_update;
|
||||
extern const struct bench bench_local_storage_cache_seq_get;
|
||||
extern const struct bench bench_local_storage_cache_interleaved_get;
|
||||
extern const struct bench bench_local_storage_cache_hashmap_control;
|
||||
extern const struct bench bench_local_storage_tasks_trace;
|
||||
extern const struct bench bench_bpf_hashmap_lookup;
|
||||
extern const struct bench bench_bpf_rhashmap_lookup;
|
||||
extern const struct bench bench_local_storage_create;
|
||||
extern const struct bench bench_htab_mem;
|
||||
extern const struct bench bench_rhtab_mem;
|
||||
extern const struct bench bench_crypto_encrypt;
|
||||
extern const struct bench bench_crypto_decrypt;
|
||||
extern const struct bench bench_sockmap;
|
||||
|
|
@ -640,13 +643,16 @@ static const struct bench *benchs[] = {
|
|||
&bench_strncmp_no_helper,
|
||||
&bench_strncmp_helper,
|
||||
&bench_bpf_hashmap_full_update,
|
||||
&bench_bpf_rhashmap_full_update,
|
||||
&bench_local_storage_cache_seq_get,
|
||||
&bench_local_storage_cache_interleaved_get,
|
||||
&bench_local_storage_cache_hashmap_control,
|
||||
&bench_local_storage_tasks_trace,
|
||||
&bench_bpf_hashmap_lookup,
|
||||
&bench_bpf_rhashmap_lookup,
|
||||
&bench_local_storage_create,
|
||||
&bench_htab_mem,
|
||||
&bench_rhtab_mem,
|
||||
&bench_crypto_encrypt,
|
||||
&bench_crypto_decrypt,
|
||||
&bench_sockmap,
|
||||
|
|
|
|||
|
|
@ -34,19 +34,29 @@ static void measure(struct bench_res *res)
|
|||
{
|
||||
}
|
||||
|
||||
static void setup(void)
|
||||
static void hashmap_full_update_setup(enum bpf_map_type map_type)
|
||||
{
|
||||
struct bpf_link *link;
|
||||
int map_fd, i, max_entries;
|
||||
|
||||
setup_libbpf();
|
||||
|
||||
ctx.skel = bpf_hashmap_full_update_bench__open_and_load();
|
||||
ctx.skel = bpf_hashmap_full_update_bench__open();
|
||||
if (!ctx.skel) {
|
||||
fprintf(stderr, "failed to open skeleton\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
bpf_map__set_type(ctx.skel->maps.hash_map_bench, map_type);
|
||||
if (map_type == BPF_MAP_TYPE_RHASH)
|
||||
bpf_map__set_map_flags(ctx.skel->maps.hash_map_bench,
|
||||
BPF_F_NO_PREALLOC);
|
||||
|
||||
if (bpf_hashmap_full_update_bench__load(ctx.skel)) {
|
||||
fprintf(stderr, "failed to load skeleton\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
ctx.skel->bss->nr_loops = MAX_LOOP_NUM;
|
||||
|
||||
link = bpf_program__attach(ctx.skel->progs.benchmark);
|
||||
|
|
@ -62,6 +72,16 @@ static void setup(void)
|
|||
bpf_map_update_elem(map_fd, &i, &i, BPF_ANY);
|
||||
}
|
||||
|
||||
static void setup(void)
|
||||
{
|
||||
hashmap_full_update_setup(BPF_MAP_TYPE_HASH);
|
||||
}
|
||||
|
||||
static void rhash_setup(void)
|
||||
{
|
||||
hashmap_full_update_setup(BPF_MAP_TYPE_RHASH);
|
||||
}
|
||||
|
||||
static void hashmap_report_final(struct bench_res res[], int res_cnt)
|
||||
{
|
||||
unsigned int nr_cpus = bpf_num_possible_cpus();
|
||||
|
|
@ -87,3 +107,13 @@ const struct bench bench_bpf_hashmap_full_update = {
|
|||
.report_progress = NULL,
|
||||
.report_final = hashmap_report_final,
|
||||
};
|
||||
|
||||
const struct bench bench_bpf_rhashmap_full_update = {
|
||||
.name = "bpf-rhashmap-full-update",
|
||||
.validate = validate,
|
||||
.setup = rhash_setup,
|
||||
.producer_thread = producer,
|
||||
.measure = measure,
|
||||
.report_progress = NULL,
|
||||
.report_final = hashmap_report_final,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -148,9 +148,10 @@ static inline void patch_key(u32 i, u32 *key)
|
|||
/* the rest of key is random */
|
||||
}
|
||||
|
||||
static void setup(void)
|
||||
static void hashmap_lookup_setup(enum bpf_map_type map_type)
|
||||
{
|
||||
struct bpf_link *link;
|
||||
__u32 map_flags;
|
||||
int map_fd;
|
||||
int ret;
|
||||
int i;
|
||||
|
|
@ -163,10 +164,15 @@ static void setup(void)
|
|||
exit(1);
|
||||
}
|
||||
|
||||
map_flags = args.map_flags;
|
||||
if (map_type == BPF_MAP_TYPE_RHASH)
|
||||
map_flags |= BPF_F_NO_PREALLOC;
|
||||
|
||||
bpf_map__set_type(ctx.skel->maps.hash_map_bench, map_type);
|
||||
bpf_map__set_max_entries(ctx.skel->maps.hash_map_bench, args.max_entries);
|
||||
bpf_map__set_key_size(ctx.skel->maps.hash_map_bench, args.key_size);
|
||||
bpf_map__set_value_size(ctx.skel->maps.hash_map_bench, 8);
|
||||
bpf_map__set_map_flags(ctx.skel->maps.hash_map_bench, args.map_flags);
|
||||
bpf_map__set_map_flags(ctx.skel->maps.hash_map_bench, map_flags);
|
||||
|
||||
ctx.skel->bss->nr_entries = args.nr_entries;
|
||||
ctx.skel->bss->nr_loops = args.nr_loops / args.nr_entries;
|
||||
|
|
@ -197,6 +203,16 @@ static void setup(void)
|
|||
}
|
||||
}
|
||||
|
||||
static void setup(void)
|
||||
{
|
||||
hashmap_lookup_setup(BPF_MAP_TYPE_HASH);
|
||||
}
|
||||
|
||||
static void rhash_setup(void)
|
||||
{
|
||||
hashmap_lookup_setup(BPF_MAP_TYPE_RHASH);
|
||||
}
|
||||
|
||||
static inline double events_from_time(u64 time)
|
||||
{
|
||||
if (time)
|
||||
|
|
@ -275,3 +291,14 @@ const struct bench bench_bpf_hashmap_lookup = {
|
|||
.report_progress = NULL,
|
||||
.report_final = hashmap_report_final,
|
||||
};
|
||||
|
||||
const struct bench bench_bpf_rhashmap_lookup = {
|
||||
.name = "bpf-rhashmap-lookup",
|
||||
.argp = &bench_hashmap_lookup_argp,
|
||||
.validate = validate,
|
||||
.setup = rhash_setup,
|
||||
.producer_thread = producer,
|
||||
.measure = measure,
|
||||
.report_progress = NULL,
|
||||
.report_final = hashmap_report_final,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ static const struct htab_mem_use_case *htab_mem_find_use_case_or_exit(const char
|
|||
exit(1);
|
||||
}
|
||||
|
||||
static void htab_mem_setup(void)
|
||||
static void htab_mem_setup_impl(enum bpf_map_type map_type)
|
||||
{
|
||||
struct bpf_map *map;
|
||||
const char **names;
|
||||
|
|
@ -178,10 +178,11 @@ static void htab_mem_setup(void)
|
|||
}
|
||||
|
||||
map = ctx.skel->maps.htab;
|
||||
bpf_map__set_type(map, map_type);
|
||||
bpf_map__set_value_size(map, args.value_size);
|
||||
/* Ensure that different CPUs can operate on different subset */
|
||||
bpf_map__set_max_entries(map, MAX(8192, 64 * env.nr_cpus));
|
||||
if (args.preallocated)
|
||||
if (map_type != BPF_MAP_TYPE_RHASH && args.preallocated)
|
||||
bpf_map__set_map_flags(map, bpf_map__map_flags(map) & ~BPF_F_NO_PREALLOC);
|
||||
|
||||
names = ctx.uc->progs;
|
||||
|
|
@ -220,6 +221,16 @@ static void htab_mem_setup(void)
|
|||
exit(1);
|
||||
}
|
||||
|
||||
static void htab_mem_setup(void)
|
||||
{
|
||||
htab_mem_setup_impl(BPF_MAP_TYPE_HASH);
|
||||
}
|
||||
|
||||
static void rhtab_mem_setup(void)
|
||||
{
|
||||
htab_mem_setup_impl(BPF_MAP_TYPE_RHASH);
|
||||
}
|
||||
|
||||
static void htab_mem_add_fn(pthread_barrier_t *notify)
|
||||
{
|
||||
while (true) {
|
||||
|
|
@ -338,6 +349,15 @@ static void htab_mem_report_final(struct bench_res res[], int res_cnt)
|
|||
cleanup_cgroup_environment();
|
||||
}
|
||||
|
||||
static void rhtab_mem_validate(void)
|
||||
{
|
||||
if (args.preallocated) {
|
||||
fprintf(stderr, "rhash map does not support preallocation\n");
|
||||
exit(1);
|
||||
}
|
||||
htab_mem_validate();
|
||||
}
|
||||
|
||||
const struct bench bench_htab_mem = {
|
||||
.name = "htab-mem",
|
||||
.argp = &bench_htab_mem_argp,
|
||||
|
|
@ -348,3 +368,14 @@ const struct bench bench_htab_mem = {
|
|||
.report_progress = htab_mem_report_progress,
|
||||
.report_final = htab_mem_report_final,
|
||||
};
|
||||
|
||||
const struct bench bench_rhtab_mem = {
|
||||
.name = "rhtab-mem",
|
||||
.argp = &bench_htab_mem_argp,
|
||||
.validate = rhtab_mem_validate,
|
||||
.setup = rhtab_mem_setup,
|
||||
.producer_thread = htab_mem_producer,
|
||||
.measure = htab_mem_measure,
|
||||
.report_progress = htab_mem_report_progress,
|
||||
.report_final = htab_mem_report_final,
|
||||
};
|
||||
|
|
|
|||
183
tools/testing/selftests/bpf/prog_tests/rhash.c
Normal file
183
tools/testing/selftests/bpf/prog_tests/rhash.c
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
|
||||
#include <test_progs.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "rhash.skel.h"
|
||||
#include "bpf_iter_bpf_rhash_map.skel.h"
|
||||
#include <linux/bpf.h>
|
||||
#include <linux/perf_event.h>
|
||||
#include <sys/syscall.h>
|
||||
|
||||
static void rhash_run(const char *prog_name)
|
||||
{
|
||||
struct rhash *skel;
|
||||
struct bpf_program *prog;
|
||||
LIBBPF_OPTS(bpf_test_run_opts, opts);
|
||||
int err;
|
||||
|
||||
skel = rhash__open();
|
||||
if (!ASSERT_OK_PTR(skel, "rhash__open"))
|
||||
return;
|
||||
|
||||
prog = bpf_object__find_program_by_name(skel->obj, prog_name);
|
||||
if (!ASSERT_OK_PTR(prog, "bpf_object__find_program_by_name"))
|
||||
goto cleanup;
|
||||
bpf_program__set_autoload(prog, true);
|
||||
|
||||
err = rhash__load(skel);
|
||||
if (!ASSERT_OK(err, "skel_load"))
|
||||
goto cleanup;
|
||||
|
||||
err = bpf_prog_test_run_opts(bpf_program__fd(prog), &opts);
|
||||
if (!ASSERT_OK(err, "prog run"))
|
||||
goto cleanup;
|
||||
|
||||
if (!ASSERT_OK(opts.retval, "prog retval"))
|
||||
goto cleanup;
|
||||
|
||||
if (!ASSERT_OK(skel->bss->err, "bss->err"))
|
||||
goto cleanup;
|
||||
|
||||
cleanup:
|
||||
rhash__destroy(skel);
|
||||
}
|
||||
|
||||
static int rhash_map_create(__u32 max_entries, __u64 map_extra)
|
||||
{
|
||||
LIBBPF_OPTS(bpf_map_create_opts, opts,
|
||||
.map_flags = BPF_F_NO_PREALLOC,
|
||||
.map_extra = map_extra);
|
||||
|
||||
return bpf_map_create(BPF_MAP_TYPE_RHASH, "rhash_extra",
|
||||
sizeof(__u32), sizeof(__u64), max_entries, &opts);
|
||||
}
|
||||
|
||||
static void rhash_map_extra_presize(void)
|
||||
{
|
||||
const __u32 max_entries = 1024;
|
||||
const __u32 nelem_hint = 256;
|
||||
struct bpf_map_info info = {};
|
||||
__u32 info_len = sizeof(info);
|
||||
__u64 val = 0;
|
||||
__u32 key;
|
||||
int fd, i;
|
||||
|
||||
fd = rhash_map_create(max_entries, nelem_hint);
|
||||
if (!ASSERT_GE(fd, 0, "rhash_map_create presize"))
|
||||
return;
|
||||
|
||||
if (!ASSERT_OK(bpf_map_get_info_by_fd(fd, &info, &info_len), "info"))
|
||||
goto close;
|
||||
ASSERT_EQ(info.map_extra, nelem_hint, "info.map_extra");
|
||||
|
||||
for (i = 0; i < (int)nelem_hint; i++) {
|
||||
key = i;
|
||||
if (!ASSERT_OK(bpf_map_update_elem(fd, &key, &val, BPF_NOEXIST),
|
||||
"update"))
|
||||
goto close;
|
||||
}
|
||||
close:
|
||||
close(fd);
|
||||
}
|
||||
|
||||
static void rhash_map_extra_too_big(void)
|
||||
{
|
||||
int fd;
|
||||
|
||||
fd = rhash_map_create(1U << 20, 0x10000);
|
||||
if (!ASSERT_LT(fd, 0, "rhash_map_create hint > U16_MAX"))
|
||||
close(fd);
|
||||
}
|
||||
|
||||
static void rhash_iter_test(void)
|
||||
{
|
||||
DECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts);
|
||||
struct bpf_iter_bpf_rhash_map *skel;
|
||||
int err, i, len, map_fd, iter_fd;
|
||||
union bpf_iter_link_info linfo;
|
||||
u32 expected_key_sum = 0, key;
|
||||
struct bpf_link *link;
|
||||
u64 val = 0;
|
||||
char buf[64];
|
||||
|
||||
skel = bpf_iter_bpf_rhash_map__open();
|
||||
if (!ASSERT_OK_PTR(skel, "bpf_iter_bpf_rhash_map__open"))
|
||||
return;
|
||||
|
||||
err = bpf_iter_bpf_rhash_map__load(skel);
|
||||
if (!ASSERT_OK(err, "bpf_iter_bpf_rhash_map__load"))
|
||||
goto out;
|
||||
|
||||
map_fd = bpf_map__fd(skel->maps.rhashmap);
|
||||
|
||||
/* Populate map with test data */
|
||||
for (i = 0; i < 64; i++) {
|
||||
key = i + 1;
|
||||
expected_key_sum += key;
|
||||
|
||||
err = bpf_map_update_elem(map_fd, &key, &val, BPF_NOEXIST);
|
||||
if (!ASSERT_OK(err, "map_update"))
|
||||
goto out;
|
||||
}
|
||||
|
||||
memset(&linfo, 0, sizeof(linfo));
|
||||
linfo.map.map_fd = map_fd;
|
||||
opts.link_info = &linfo;
|
||||
opts.link_info_len = sizeof(linfo);
|
||||
|
||||
link = bpf_program__attach_iter(skel->progs.dump_bpf_rhash_map, &opts);
|
||||
if (!ASSERT_OK_PTR(link, "attach_iter"))
|
||||
goto out;
|
||||
|
||||
iter_fd = bpf_iter_create(bpf_link__fd(link));
|
||||
if (!ASSERT_GE(iter_fd, 0, "create_iter"))
|
||||
goto free_link;
|
||||
|
||||
do {
|
||||
len = read(iter_fd, buf, sizeof(buf));
|
||||
} while (len > 0);
|
||||
|
||||
ASSERT_EQ(skel->bss->key_sum, expected_key_sum, "key_sum");
|
||||
ASSERT_EQ(skel->bss->elem_count, 64, "elem_count");
|
||||
|
||||
close(iter_fd);
|
||||
|
||||
free_link:
|
||||
bpf_link__destroy(link);
|
||||
out:
|
||||
bpf_iter_bpf_rhash_map__destroy(skel);
|
||||
}
|
||||
|
||||
void test_rhash(void)
|
||||
{
|
||||
if (test__start_subtest("test_rhash_lookup_update"))
|
||||
rhash_run("test_rhash_lookup_update");
|
||||
|
||||
if (test__start_subtest("test_rhash_update_delete"))
|
||||
rhash_run("test_rhash_update_delete");
|
||||
|
||||
if (test__start_subtest("test_rhash_update_elements"))
|
||||
rhash_run("test_rhash_update_elements");
|
||||
|
||||
if (test__start_subtest("test_rhash_update_exist"))
|
||||
rhash_run("test_rhash_update_exist");
|
||||
|
||||
if (test__start_subtest("test_rhash_update_any"))
|
||||
rhash_run("test_rhash_update_any");
|
||||
|
||||
if (test__start_subtest("test_rhash_noexist_duplicate"))
|
||||
rhash_run("test_rhash_noexist_duplicate");
|
||||
|
||||
if (test__start_subtest("test_rhash_delete_nonexistent"))
|
||||
rhash_run("test_rhash_delete_nonexistent");
|
||||
|
||||
if (test__start_subtest("test_rhash_map_extra_presize"))
|
||||
rhash_map_extra_presize();
|
||||
|
||||
if (test__start_subtest("test_rhash_map_extra_too_big"))
|
||||
rhash_map_extra_too_big();
|
||||
|
||||
if (test__start_subtest("test_rhash_iter"))
|
||||
rhash_iter_test();
|
||||
}
|
||||
34
tools/testing/selftests/bpf/progs/bpf_iter_bpf_rhash_map.c
Normal file
34
tools/testing/selftests/bpf/progs/bpf_iter_bpf_rhash_map.c
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
|
||||
#include <vmlinux.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RHASH);
|
||||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||||
__uint(max_entries, 64);
|
||||
__type(key, __u32);
|
||||
__type(value, __u64);
|
||||
} rhashmap SEC(".maps");
|
||||
|
||||
__u32 key_sum = 0;
|
||||
__u64 val_sum = 0;
|
||||
__u32 elem_count = 0;
|
||||
__u32 err = 0;
|
||||
|
||||
SEC("iter/bpf_map_elem")
|
||||
int dump_bpf_rhash_map(struct bpf_iter__bpf_map_elem *ctx)
|
||||
{
|
||||
__u32 *key = ctx->key;
|
||||
__u64 *val = ctx->value;
|
||||
|
||||
if (!key || !val)
|
||||
return 0;
|
||||
|
||||
key_sum += *key;
|
||||
val_sum += *val;
|
||||
elem_count++;
|
||||
return 0;
|
||||
}
|
||||
248
tools/testing/selftests/bpf/progs/rhash.c
Normal file
248
tools/testing/selftests/bpf/progs/rhash.c
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
|
||||
|
||||
#include <vmlinux.h>
|
||||
#include <stdbool.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
#include "bpf_misc.h"
|
||||
|
||||
#define ENOENT 2
|
||||
#define EEXIST 17
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
||||
|
||||
int err;
|
||||
|
||||
struct elem {
|
||||
char arr[128];
|
||||
int val;
|
||||
};
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RHASH);
|
||||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||||
__uint(max_entries, 128);
|
||||
__type(key, int);
|
||||
__type(value, struct elem);
|
||||
} rhmap SEC(".maps");
|
||||
|
||||
SEC("syscall")
|
||||
int test_rhash_lookup_update(void *ctx)
|
||||
{
|
||||
int key = 5;
|
||||
struct elem empty = {.val = 3, .arr = {0}};
|
||||
struct elem *e;
|
||||
|
||||
err = 1;
|
||||
e = bpf_map_lookup_elem(&rhmap, &key);
|
||||
if (e)
|
||||
return 1;
|
||||
|
||||
err = bpf_map_update_elem(&rhmap, &key, &empty, BPF_NOEXIST);
|
||||
if (err)
|
||||
return 1;
|
||||
|
||||
e = bpf_map_lookup_elem(&rhmap, &key);
|
||||
if (!e || e->val != empty.val) {
|
||||
err = 2;
|
||||
return 2;
|
||||
}
|
||||
|
||||
err = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("syscall")
|
||||
int test_rhash_update_delete(void *ctx)
|
||||
{
|
||||
int key = 6;
|
||||
struct elem empty = {.val = 4, .arr = {0}};
|
||||
struct elem *e;
|
||||
|
||||
err = 1;
|
||||
e = bpf_map_lookup_elem(&rhmap, &key);
|
||||
if (e)
|
||||
return 1;
|
||||
|
||||
err = bpf_map_update_elem(&rhmap, &key, &empty, BPF_NOEXIST);
|
||||
if (err)
|
||||
return 2;
|
||||
|
||||
err = bpf_map_delete_elem(&rhmap, &key);
|
||||
if (err)
|
||||
return 3;
|
||||
|
||||
e = bpf_map_lookup_elem(&rhmap, &key);
|
||||
if (e) {
|
||||
err = 4;
|
||||
return 4;
|
||||
}
|
||||
|
||||
err = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("syscall")
|
||||
int test_rhash_update_elements(void *ctx)
|
||||
{
|
||||
int key = 0;
|
||||
struct elem empty = {.val = 4, .arr = {0}};
|
||||
struct elem *e;
|
||||
int i;
|
||||
|
||||
err = 1;
|
||||
|
||||
for (i = 0; i < 128; ++i) {
|
||||
key = i;
|
||||
e = bpf_map_lookup_elem(&rhmap, &key);
|
||||
if (e)
|
||||
return 1;
|
||||
|
||||
empty.val = key;
|
||||
err = bpf_map_update_elem(&rhmap, &key, &empty, BPF_NOEXIST);
|
||||
if (err)
|
||||
return 2;
|
||||
|
||||
e = bpf_map_lookup_elem(&rhmap, &key);
|
||||
if (!e || e->val != key) {
|
||||
err = 4;
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < 128; ++i) {
|
||||
key = i;
|
||||
err = bpf_map_delete_elem(&rhmap, &key);
|
||||
if (err)
|
||||
return 3;
|
||||
|
||||
e = bpf_map_lookup_elem(&rhmap, &key);
|
||||
if (e) {
|
||||
err = 5;
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
|
||||
err = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("syscall")
|
||||
int test_rhash_update_exist(void *ctx)
|
||||
{
|
||||
int key = 10;
|
||||
struct elem val1 = {.val = 100, .arr = {0}};
|
||||
struct elem val2 = {.val = 200, .arr = {0}};
|
||||
struct elem *e;
|
||||
int ret;
|
||||
|
||||
err = 1;
|
||||
|
||||
/* BPF_EXIST on non-existent key should fail with -ENOENT */
|
||||
ret = bpf_map_update_elem(&rhmap, &key, &val1, BPF_EXIST);
|
||||
if (ret != -ENOENT)
|
||||
return 1;
|
||||
|
||||
/* Insert element first */
|
||||
ret = bpf_map_update_elem(&rhmap, &key, &val1, BPF_NOEXIST);
|
||||
if (ret)
|
||||
return 2;
|
||||
|
||||
/* Verify initial value */
|
||||
e = bpf_map_lookup_elem(&rhmap, &key);
|
||||
if (!e || e->val != 100)
|
||||
return 3;
|
||||
|
||||
/* BPF_EXIST on existing key should succeed and update value */
|
||||
ret = bpf_map_update_elem(&rhmap, &key, &val2, BPF_EXIST);
|
||||
if (ret)
|
||||
return 4;
|
||||
|
||||
/* Verify value was updated */
|
||||
e = bpf_map_lookup_elem(&rhmap, &key);
|
||||
if (!e || e->val != 200)
|
||||
return 5;
|
||||
|
||||
/* Cleanup */
|
||||
bpf_map_delete_elem(&rhmap, &key);
|
||||
err = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("syscall")
|
||||
int test_rhash_update_any(void *ctx)
|
||||
{
|
||||
int key = 11;
|
||||
struct elem val1 = {.val = 111, .arr = {0}};
|
||||
struct elem val2 = {.val = 222, .arr = {0}};
|
||||
struct elem *e;
|
||||
int ret;
|
||||
|
||||
err = 1;
|
||||
|
||||
/* BPF_ANY on non-existent key should insert */
|
||||
ret = bpf_map_update_elem(&rhmap, &key, &val1, BPF_ANY);
|
||||
if (ret)
|
||||
return 1;
|
||||
|
||||
e = bpf_map_lookup_elem(&rhmap, &key);
|
||||
if (!e || e->val != 111)
|
||||
return 2;
|
||||
|
||||
/* BPF_ANY on existing key should update */
|
||||
ret = bpf_map_update_elem(&rhmap, &key, &val2, BPF_ANY);
|
||||
if (ret)
|
||||
return 3;
|
||||
|
||||
e = bpf_map_lookup_elem(&rhmap, &key);
|
||||
if (!e || e->val != 222)
|
||||
return 4;
|
||||
|
||||
/* Cleanup */
|
||||
bpf_map_delete_elem(&rhmap, &key);
|
||||
err = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("syscall")
|
||||
int test_rhash_noexist_duplicate(void *ctx)
|
||||
{
|
||||
int key = 12;
|
||||
struct elem val = {.val = 600, .arr = {0}};
|
||||
int ret;
|
||||
|
||||
err = 1;
|
||||
|
||||
/* Insert element */
|
||||
ret = bpf_map_update_elem(&rhmap, &key, &val, BPF_NOEXIST);
|
||||
if (ret)
|
||||
return 1;
|
||||
|
||||
/* Try to insert again with BPF_NOEXIST - should fail with -EEXIST */
|
||||
ret = bpf_map_update_elem(&rhmap, &key, &val, BPF_NOEXIST);
|
||||
if (ret != -EEXIST)
|
||||
return 2;
|
||||
|
||||
/* Cleanup */
|
||||
bpf_map_delete_elem(&rhmap, &key);
|
||||
err = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("syscall")
|
||||
int test_rhash_delete_nonexistent(void *ctx)
|
||||
{
|
||||
int key = 99999;
|
||||
int ret;
|
||||
|
||||
err = 1;
|
||||
|
||||
/* Delete non-existent key should return -ENOENT */
|
||||
ret = bpf_map_delete_elem(&rhmap, &key);
|
||||
if (ret != -ENOENT)
|
||||
return 1;
|
||||
|
||||
err = 0;
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user