* [PATCH bpf-next v10 0/2] bpftool: Use batch lookups for map dumps
@ 2026-11-11 7:18 Tianyi Chen
2026-11-11 7:18 ` [PATCH bpf-next v10 1/2] " Tianyi Chen
2026-11-11 7:18 ` [PATCH bpf-next v10 2/2] selftests/bpf: Check bpftool batch " Tianyi Chen
0 siblings, 2 replies; 3+ messages in thread
From: Tianyi Chen @ 2026-11-11 7:18 UTC (permalink / raw)
To: bpf
Cc: Tianyi Chen, ast, daniel, andrii, eddyz87, memxor, shuah,
linux-kselftest, qmo, ihor.solodrai
Use bounded batch lookups for map dumps, falling back to individual
lookups after the last printed key on a batch error.
RHASH concurrent-delete correctness requires "bpf: Fix RHASH batch
cursor loss" v2 [1]. Please take that kernel fix first.
Changes in v10:
- Document the non-zero key/value size precondition.
- Rename the tests to bpftool_map_dump_batch, drop duplicate LRU cases
and use small per-CPU maps. Reserve JSON checks for representative
raw, BTF, per-CPU and multi-batch paths.
- Carry Quentin's Acked-by tags.
The 14 batch subtests pass in x86-64 KVM. Combined with recursive v12,
all 33 dump/access subtests pass with both 2 and 128 possible CPUs
(2 CPUs online), without skips.
[1] https://lore.kernel.org/r/cover.1794379108.git.hi@tychen.cc
v9: https://lore.kernel.org/r/cover.1793821981.git.hi@tychen.cc
Tianyi Chen (2):
bpftool: Use batch lookups for map dumps
selftests/bpf: Check bpftool batch map dumps
tools/bpf/bpftool/map.c | 98 ++++++--
.../bpf/prog_tests/bpftool_map_dump_batch.c | 229 ++++++++++++++++++
.../bpf/progs/bpftool_map_dump_batch.c | 42 ++++
3 files changed, 354 insertions(+), 15 deletions(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_map_dump_batch.c
create mode 100644 tools/testing/selftests/bpf/progs/bpftool_map_dump_batch.c
base-commit: 0fb18ce8a2e9de830433923cf65838933d615359
--
2.56.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH bpf-next v10 1/2] bpftool: Use batch lookups for map dumps
2026-11-11 7:18 [PATCH bpf-next v10 0/2] bpftool: Use batch lookups for map dumps Tianyi Chen
@ 2026-11-11 7:18 ` Tianyi Chen
2026-11-11 7:18 ` [PATCH bpf-next v10 2/2] selftests/bpf: Check bpftool batch " Tianyi Chen
1 sibling, 0 replies; 3+ messages in thread
From: Tianyi Chen @ 2026-11-11 7:18 UTC (permalink / raw)
To: bpf
Cc: Tianyi Chen, ast, daniel, andrii, eddyz87, memxor, shuah,
linux-kselftest, qmo, ihor.solodrai
Dumping a map takes two BPF syscalls per element: one to get the next
key and one to look it up. Use BPF_MAP_LOOKUP_BATCH instead, with keys
and values bounded to 1 MiB and per-CPU value strides accounted for.
The output is unchanged.
For a hash map with 100,000 4-byte keys and values, a plain dump takes 5
BPF syscalls instead of 200,005. With output to /dev/null, the median of
15 runs in an x86-64 KVM guest on Linux 7.3 dropped from 86 ms to 52 ms
(JSON: 78 ms to 42 ms). Printing to a terminal dominates either way.
If a batch lookup fails, discard its output and continue with individual
lookups after the last printed key. bpf_map_get_next_key() walks the map
in the same order. This covers maps without batch support, and hash
maps whose bucket lock cannot be taken, which individual lookups do not
need.
Link: https://github.com/libbpf/bpftool/issues/63
Suggested-by: Alexei Starovoitov <ast@kernel.org>
Acked-by: Quentin Monnet <qmo@kernel.org>
Assisted-by: LLM
Signed-off-by: Tianyi Chen <hi@tychen.cc>
---
tools/bpf/bpftool/map.c | 98 ++++++++++++++++++++++++++++++++++-------
1 file changed, 83 insertions(+), 15 deletions(-)
diff --git a/tools/bpf/bpftool/map.c b/tools/bpf/bpftool/map.c
index 4b65edf74859..c5c56323f062 100644
--- a/tools/bpf/bpftool/map.c
+++ b/tools/bpf/bpftool/map.c
@@ -59,13 +59,16 @@ static int map_type_from_str(const char *type)
return -1;
}
-static void *alloc_value(struct bpf_map_info *info)
+static size_t map_value_size(struct bpf_map_info *info)
{
if (map_is_per_cpu(info->type))
- return malloc(round_up(info->value_size, 8) *
- get_possible_cpus());
- else
- return malloc(info->value_size);
+ return (size_t)round_up(info->value_size, 8) * get_possible_cpus();
+ return info->value_size;
+}
+
+static void *alloc_value(struct bpf_map_info *info)
+{
+ return malloc(map_value_size(info));
}
static int do_dump_btf(const struct btf_dumper *d,
@@ -741,15 +744,10 @@ static int do_show(int argc, char **argv)
return errno == ENOENT ? 0 : -1;
}
-static int dump_map_elem(int fd, void *key, void *value,
- struct bpf_map_info *map_info, struct btf *btf,
- json_writer_t *btf_wtr, const int *cpu_ids, int cpu_cnt)
+static void print_map_elem(void *key, void *value,
+ struct bpf_map_info *map_info, struct btf *btf,
+ json_writer_t *btf_wtr, const int *cpu_ids, int cpu_cnt)
{
- if (bpf_map_lookup_elem(fd, key, value)) {
- print_entry_error(map_info, key, errno);
- return -1;
- }
-
if (json_output) {
print_entry_json(map_info, key, value, btf, cpu_ids, cpu_cnt);
} else if (btf) {
@@ -763,10 +761,75 @@ static int dump_map_elem(int fd, void *key, void *value,
} else {
print_entry_plain(map_info, key, value, cpu_ids, cpu_cnt);
}
+}
+static int dump_map_elem(int fd, void *key, void *value,
+ struct bpf_map_info *map_info, struct btf *btf,
+ json_writer_t *btf_wtr, const int *cpu_ids, int cpu_cnt)
+{
+ if (bpf_map_lookup_elem(fd, key, value)) {
+ print_entry_error(map_info, key, errno);
+ return -1;
+ }
+
+ print_map_elem(key, value, map_info, btf, btf_wtr, cpu_ids, cpu_cnt);
return 0;
}
+#define MAP_DUMP_BATCH_BYTES (1U << 20)
+
+/*
+ * Print the elements returned by batched lookups, leaving the last one in
+ * @key. Return true at the end of the map. After any error, the caller
+ * continues with bpf_map_get_next_key(), which walks the map in the same order.
+ * The caller must ensure that key_size + value_size is non-zero.
+ */
+static bool dump_map_batch(int fd, void *key, void *value,
+ struct bpf_map_info *info, struct btf *btf,
+ json_writer_t *wtr, const int *cpu_ids, int cpu_cnt,
+ unsigned int *num_elems)
+{
+ size_t value_size = map_value_size(info);
+ void *keys, *values, *batch, *in_batch = NULL;
+ __u32 capacity, count, i;
+ bool done = false;
+ int err;
+
+ capacity = min(MAP_DUMP_BATCH_BYTES / (info->key_size + value_size),
+ (size_t)info->max_entries);
+ if (!capacity)
+ return false;
+
+ /* Hash table batch cursors are u32 bucket indices; other maps use keys. */
+ batch = malloc(max(info->key_size, 4U));
+ keys = malloc((size_t)capacity * info->key_size);
+ values = malloc((size_t)capacity * value_size);
+ if (!batch || !keys || !values)
+ goto out;
+
+ do {
+ count = capacity;
+ err = bpf_map_lookup_batch(fd, in_batch, batch, keys, values,
+ &count, NULL);
+ if (err && err != -ENOENT)
+ goto out;
+ for (i = 0; i < count; i++) {
+ /* Preserve alignment for odd-sized BTF keys and values. */
+ memcpy(key, keys + (size_t)i * info->key_size, info->key_size);
+ memcpy(value, values + (size_t)i * value_size, value_size);
+ print_map_elem(key, value, info, btf, wtr, cpu_ids, cpu_cnt);
+ }
+ *num_elems += count;
+ in_batch = batch;
+ } while (!err);
+ done = true;
+out:
+ free(batch);
+ free(keys);
+ free(values);
+ return done;
+}
+
static int maps_have_btf(int *fds, int nb_fds)
{
struct bpf_map_info info = {};
@@ -836,7 +899,8 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
struct btf *btf = NULL;
int *cpu_ids = NULL;
int cpu_cnt = 0;
- int err;
+ bool done = false;
+ int err = 0;
key = malloc(info->key_size);
value = alloc_value(info);
@@ -895,7 +959,11 @@ map_dump(int fd, struct bpf_map_info *info, json_writer_t *wtr,
p_info("Warning: cannot read values from %s map with value_size != 8",
map_type_str);
}
- while (true) {
+ done = dump_map_batch(fd, key, value, info, btf, wtr, cpu_ids, cpu_cnt,
+ &num_elems);
+ if (num_elems)
+ prev_key = key;
+ while (!done) {
err = bpf_map_get_next_key(fd, prev_key, key);
if (err) {
if (errno == ENOENT) {
--
2.56.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* [PATCH bpf-next v10 2/2] selftests/bpf: Check bpftool batch map dumps
2026-11-11 7:18 [PATCH bpf-next v10 0/2] bpftool: Use batch lookups for map dumps Tianyi Chen
2026-11-11 7:18 ` [PATCH bpf-next v10 1/2] " Tianyi Chen
@ 2026-11-11 7:18 ` Tianyi Chen
1 sibling, 0 replies; 3+ messages in thread
From: Tianyi Chen @ 2026-11-11 7:18 UTC (permalink / raw)
To: bpf
Cc: Tianyi Chen, ast, daniel, andrii, eddyz87, memxor, shuah,
linux-kselftest, qmo, ihor.solodrai
Batched dumps must print the same output as individual lookups. Use an
fmod_ret program on security_bpf() to count the map lookups made by
bpftool children of the test, and to fail batch lookups with -EBUSY.
Compare plain dumps across the batch lookup implementations with a dump
whose batch lookups all fail, and check that no individual lookups were
needed. Cover BTF and per-CPU output, an empty map, 1-byte keys and
unaligned values. With 4 KiB values, check multiple batches and fallback
before and after a batch has been printed, without restarting the dump.
Repeat representative raw, BTF, per-CPU and multi-batch cases in JSON.
Pre-size RHASH maps so asynchronous growth does not race with the
order-sensitive dump comparisons.
Acked-by: Quentin Monnet <qmo@kernel.org>
Assisted-by: LLM
Signed-off-by: Tianyi Chen <hi@tychen.cc>
---
.../bpf/prog_tests/bpftool_map_dump_batch.c | 229 ++++++++++++++++++
.../bpf/progs/bpftool_map_dump_batch.c | 42 ++++
2 files changed, 271 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/bpftool_map_dump_batch.c
create mode 100644 tools/testing/selftests/bpf/progs/bpftool_map_dump_batch.c
diff --git a/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump_batch.c b/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump_batch.c
new file mode 100644
index 000000000000..7c97a50896fa
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/bpftool_map_dump_batch.c
@@ -0,0 +1,229 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <arpa/inet.h>
+#include <test_progs.h>
+#include <bpf/btf.h>
+#include "bpftool_helpers.h"
+#include "bpftool_map_dump_batch.skel.h"
+
+/* Fits the plain and JSON dumps of the multi-batch cases below. */
+#define OUTPUT_SIZE (8U << 20)
+
+struct dump_case {
+ const char *name;
+ enum bpf_map_type type;
+ __u32 key_size;
+ __u32 value_size;
+ __u32 entries;
+ bool btf;
+ /* Values large enough that the 1 MiB budget needs several batches. */
+ bool multi_batch;
+ bool json;
+};
+
+static bool is_percpu(enum bpf_map_type type)
+{
+ return type == BPF_MAP_TYPE_PERCPU_HASH ||
+ type == BPF_MAP_TYPE_PERCPU_ARRAY;
+}
+
+static int run_dump(struct bpftool_map_dump_batch *skel, __u32 id, bool json,
+ int fail_batch, char *output)
+{
+ char command[64];
+ int err;
+
+ snprintf(command, sizeof(command), "%smap dump id %u",
+ json ? "-j " : "", id);
+ skel->bss->fail_batch = fail_batch;
+ skel->bss->batch_calls = 0;
+ skel->bss->elem_calls = 0;
+ output[0] = '\0';
+ err = get_bpftool_command_output(command, output, OUTPUT_SIZE);
+ if (!ASSERT_LT(strlen(output), OUTPUT_SIZE - 1, "output_fits"))
+ return -1;
+ return err;
+}
+
+static void check_same(const char *output, const char *expected, const char *name)
+{
+ size_t i;
+
+ for (i = 0; output[i] && output[i] == expected[i]; i++)
+ ;
+ if (!ASSERT_EQ(output[i], expected[i], name))
+ printf("%s: first difference at offset %zu\n", name, i);
+}
+
+/* Failing every batch lookup gives the dump of individual lookups. */
+static void check_dump(struct bpftool_map_dump_batch *skel, const struct dump_case *test,
+ __u32 id, bool json, char *expected, char *output)
+{
+ int fail;
+
+ if (!ASSERT_OK(run_dump(skel, id, json, -1, expected), "individual_dump") ||
+ !ASSERT_EQ(skel->bss->elem_calls, test->entries, "individual_lookups"))
+ return;
+
+ if (!ASSERT_OK(run_dump(skel, id, json, 0, output), "batch_dump"))
+ return;
+ ASSERT_GE(skel->bss->batch_calls, test->multi_batch ? 2 : 1, "batch_calls");
+ ASSERT_EQ(skel->bss->elem_calls, 0, "batch_only");
+ check_same(output, expected, "batch_output");
+
+ if (!test->multi_batch)
+ return;
+ /* Fail before and after a batch has been printed. */
+ for (fail = 1; fail <= 2; fail++) {
+ if (!ASSERT_OK(run_dump(skel, id, json, fail, output), "fallback_dump"))
+ return;
+ ASSERT_EQ(skel->bss->batch_calls, fail, "fallback_batch_calls");
+ if (fail == 1) {
+ ASSERT_EQ(skel->bss->elem_calls, test->entries, "fallback_lookups");
+ } else {
+ ASSERT_GT(skel->bss->elem_calls, 0, "resumed_lookups");
+ ASSERT_LT(skel->bss->elem_calls, test->entries, "not_restarted");
+ }
+ check_same(output, expected, "fallback_output");
+ }
+}
+
+static void fill_bytes(void *buf, __u32 size, __u32 seed)
+{
+ __u8 *bytes = buf;
+ __u32 i;
+
+ for (i = 0; i < size; i++)
+ bytes[i] = seed * 37 + i;
+}
+
+static void run_case(struct bpftool_map_dump_batch *skel, const struct dump_case *test,
+ char *expected, char *output)
+{
+ LIBBPF_OPTS(bpf_map_create_opts, opts);
+ int fd = -1, inner_fd = -1, cpus = 1, cpu;
+ struct bpf_map_info info = {};
+ __u32 len = sizeof(info);
+ __u32 i, max_entries, stride;
+ struct btf *btf = NULL;
+ __u8 key[8] = {};
+ void *values = NULL;
+
+ if (is_percpu(test->type)) {
+ cpus = libbpf_num_possible_cpus();
+ if (!ASSERT_GT(cpus, 0, "possible_cpus"))
+ return;
+ }
+ stride = is_percpu(test->type) ? (test->value_size + 7) & ~7U : test->value_size;
+ values = calloc(cpus, stride);
+ if (!ASSERT_OK_PTR(values, "values"))
+ return;
+
+ if (test->btf) {
+ btf = btf__new_empty();
+ if (!ASSERT_OK_PTR(btf, "btf_new") ||
+ !ASSERT_EQ(btf__add_int(btf, "unsigned int", 4, 0), 1, "btf_int") ||
+ !ASSERT_OK(btf__load_into_kernel(btf), "btf_load"))
+ goto out;
+ opts.btf_fd = btf__fd(btf);
+ opts.btf_key_type_id = 1;
+ opts.btf_value_type_id = 1;
+ }
+ if (test->type == BPF_MAP_TYPE_ARRAY_OF_MAPS ||
+ test->type == BPF_MAP_TYPE_HASH_OF_MAPS) {
+ inner_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "batch_inner", 4, 4, 1, NULL);
+ if (!ASSERT_OK_FD(inner_fd, "inner_map"))
+ goto out;
+ opts.inner_map_fd = inner_fd;
+ }
+ if (test->type == BPF_MAP_TYPE_LPM_TRIE || test->type == BPF_MAP_TYPE_RHASH)
+ opts.map_flags = BPF_F_NO_PREALLOC;
+ max_entries = test->entries ?: 1;
+ /* Avoid asynchronous growth during dump comparisons. */
+ if (test->type == BPF_MAP_TYPE_RHASH)
+ opts.map_extra = max_entries;
+
+ fd = bpf_map_create(test->type, "batch_dump", test->key_size,
+ test->value_size, max_entries, &opts);
+ if (!ASSERT_OK_FD(fd, "map_create"))
+ goto out;
+ for (i = 0; i < test->entries; i++) {
+ if (test->type == BPF_MAP_TYPE_LPM_TRIE) {
+ __u32 prefix[] = { 32, htonl(i) };
+
+ memcpy(key, prefix, sizeof(prefix));
+ } else if (test->key_size == 1) {
+ key[0] = (__u8)i;
+ } else {
+ /* Array map indices use native byte order. */
+ memcpy(key, &i, sizeof(i));
+ }
+ if (inner_fd >= 0)
+ memcpy(values, &inner_fd, sizeof(inner_fd));
+ else
+ for (cpu = 0; cpu < cpus; cpu++)
+ fill_bytes(values + cpu * stride, test->value_size,
+ i + cpu * 101);
+ if (!ASSERT_OK(bpf_map_update_elem(fd, key, values, BPF_ANY), "map_update"))
+ goto out;
+ }
+ if (!ASSERT_OK(bpf_map_get_info_by_fd(fd, &info, &len), "map_info"))
+ goto out;
+
+ check_dump(skel, test, info.id, false, expected, output);
+ if (test->json)
+ check_dump(skel, test, info.id, true, expected, output);
+out:
+ if (fd >= 0)
+ close(fd);
+ if (inner_fd >= 0)
+ close(inner_fd);
+ btf__free(btf);
+ free(values);
+}
+
+void test_bpftool_map_dump_batch(void)
+{
+ static const struct dump_case cases[] = {
+ { "hash", BPF_MAP_TYPE_HASH, 4, 4, 100, .json = true },
+ { "empty", BPF_MAP_TYPE_HASH, 4, 4, 0 },
+ /* The hash cursor is wider than the key; values are unaligned. */
+ { "odd_sizes", BPF_MAP_TYPE_HASH, 1, 3, 100 },
+ { "btf", BPF_MAP_TYPE_HASH, 4, 4, 100, .btf = true, .json = true },
+ { "percpu_hash", BPF_MAP_TYPE_PERCPU_HASH, 4, 3, 4, .json = true },
+ { "btf_percpu_hash", BPF_MAP_TYPE_PERCPU_HASH, 4, 4, 4,
+ .btf = true, .json = true },
+ { "rhash", BPF_MAP_TYPE_RHASH, 4, 4, 100 },
+ { "array", BPF_MAP_TYPE_ARRAY, 4, 4, 100 },
+ { "percpu_array", BPF_MAP_TYPE_PERCPU_ARRAY, 4, 3, 4 },
+ { "lpm_trie", BPF_MAP_TYPE_LPM_TRIE, 8, 4, 100 },
+ { "array_of_maps", BPF_MAP_TYPE_ARRAY_OF_MAPS, 4, 4, 100 },
+ { "hash_of_maps", BPF_MAP_TYPE_HASH_OF_MAPS, 4, 4, 100 },
+ /* Bucket and key cursors, up to 255 entries per batch. */
+ { "multi_batch_hash", BPF_MAP_TYPE_HASH, 4, 4096, 256,
+ .multi_batch = true, .json = true },
+ { "multi_batch_array", BPF_MAP_TYPE_ARRAY, 4, 4096, 256,
+ .multi_batch = true },
+ };
+ struct bpftool_map_dump_batch *skel;
+ char *expected = NULL, *output = NULL;
+ unsigned int i;
+
+ skel = bpftool_map_dump_batch__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "skel_open_and_load"))
+ return;
+ skel->bss->test_tgid = getpid();
+ if (!ASSERT_OK(bpftool_map_dump_batch__attach(skel), "skel_attach"))
+ goto out;
+ expected = malloc(OUTPUT_SIZE);
+ output = malloc(OUTPUT_SIZE);
+ if (!ASSERT_OK_PTR(expected, "expected") || !ASSERT_OK_PTR(output, "output"))
+ goto out;
+
+ for (i = 0; i < ARRAY_SIZE(cases); i++)
+ if (test__start_subtest(cases[i].name))
+ run_case(skel, &cases[i], expected, output);
+out:
+ free(expected);
+ free(output);
+ bpftool_map_dump_batch__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/progs/bpftool_map_dump_batch.c b/tools/testing/selftests/bpf/progs/bpftool_map_dump_batch.c
new file mode 100644
index 000000000000..0ca6f7973e88
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/bpftool_map_dump_batch.c
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "vmlinux.h"
+#include <bpf/bpf_core_read.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+#define EBUSY 16
+
+char _license[] SEC("license") = "GPL";
+
+/* The test process; bpftool runs as its child or grandchild via popen(). */
+int test_tgid;
+/* 1-based batch lookup to fail, -1 to fail all of them, 0 for none. */
+int fail_batch;
+int batch_calls;
+int elem_calls;
+
+static bool is_test_bpftool(void)
+{
+ struct task_struct *task = bpf_get_current_task_btf();
+
+ return BPF_CORE_READ(task, real_parent, tgid) == test_tgid ||
+ BPF_CORE_READ(task, real_parent, real_parent, tgid) == test_tgid;
+}
+
+SEC("fmod_ret/security_bpf")
+int BPF_PROG(count_bpftool_lookups, int cmd, union bpf_attr *attr,
+ unsigned int size, bool kernel)
+{
+ if (kernel || !is_test_bpftool())
+ return 0;
+
+ if (cmd == BPF_MAP_LOOKUP_ELEM) {
+ elem_calls++;
+ } else if (cmd == BPF_MAP_LOOKUP_BATCH) {
+ batch_calls++;
+ /* Trigger fallback rather than end-of-iteration (-ENOENT). */
+ if (fail_batch < 0 || fail_batch == batch_calls)
+ return -EBUSY;
+ }
+ return 0;
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-11 7:19 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-11 7:18 [PATCH bpf-next v10 0/2] bpftool: Use batch lookups for map dumps Tianyi Chen
2026-11-11 7:18 ` [PATCH bpf-next v10 1/2] " Tianyi Chen
2026-11-11 7:18 ` [PATCH bpf-next v10 2/2] selftests/bpf: Check bpftool batch " Tianyi Chen
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox