From: Li Pengfei <ljdlns1987@gmail.com>
To: rostedt@goodmis.org, mhiramat@kernel.org
Cc: mathieu.desnoyers@efficios.com, mark.rutland@arm.com,
corbet@lwn.net, skhan@linuxfoundation.org, lkp@intel.com,
linux-trace-kernel@vger.kernel.org, linux-kernel@vger.kernel.org,
linux-doc@vger.kernel.org, linux-kselftest@vger.kernel.org,
zhangbo56@xiaomi.com, lipengfei28@xiaomi.com
Subject: [RFC PATCH v7 04/10] trace: add stackmap binary export
Date: Sat, 12 Sep 2026 16:37:47 +0800 [thread overview]
Message-ID: <20260912083753.3426176-5-lipengfei28@xiaomi.com> (raw)
In-Reply-To: <20260912083753.3426176-1-lipengfei28@xiaomi.com>
From: Pengfei Li <lipengfei28@xiaomi.com>
Add stack_map_bin, a native-endian version 1 binary export containing a
header followed by stack id, depth, reference count and IP records.
Userspace detects byte order from the magic value.
Stream records through seq_write() instead of allocating a private
payload snapshot. open() records populated slot numbers in a bitmap and
sets nr_stacks to its exact count. The bitmap uses at most 64 KiB, and
exactly those records are emitted. It fixes record membership, not
payload values, so ref_count can change before a selected record is
written.
seq_file releases reader_sem between read() calls. Track resets with a
u64 generation sampled at open and checked at the start of each later
seq pass. All runtime generation accesses are under reader_sem, so a
plain u64 is sufficient. A changed generation returns -ESTALE rather
than -EAGAIN, which seq_file reserves for internal traversal retries.
Data already buffered by the current pass can be returned first.
Once iteration has reached the end, a later read returns normal EOF even
if reset intervened because no new-generation record can be appended.
A reader that receives -ESTALE must reopen the file.
Export the trampoline sentinel unchanged and apply
trace_adjust_address() to other addresses, matching the text export.
The records contain raw kernel instruction pointers after
trace_adjust_address(). The auxiliary file has mode 0440 and open() uses
tracing_check_open_get_tr(), matching the access boundary of existing raw
tracing interfaces. This rejects LOCKDOWN_TRACEFS and pins the owning
trace array. All open failure paths and release drop that reference after
seq_file state is released. Failure to create the auxiliary file does not
disable stackmap.
Signed-off-by: Pengfei Li <lipengfei28@xiaomi.com>
---
kernel/trace/trace.c | 18 +-
kernel/trace/trace_stackmap.c | 327 ++++++++++++++++++++++++++++++++--
kernel/trace/trace_stackmap.h | 20 +++
3 files changed, 343 insertions(+), 22 deletions(-)
diff --git a/kernel/trace/trace.c b/kernel/trace/trace.c
index 17df5f85da7a..68f5eb9a1e96 100644
--- a/kernel/trace/trace.c
+++ b/kernel/trace/trace.c
@@ -4109,8 +4109,8 @@ int set_tracer_flag(struct trace_array *tr, u64 mask, int enabled)
#ifdef CONFIG_FTRACE_STACKMAP
/*
* STACKMAP is intentionally global-instance-only: the dedup map,
- * its tracefs files and the lifetime/reset semantics are tied
- * to the global trace
+ * its tracefs files (stack_map / stack_map_stat / stack_map_bin)
+ * and the lifetime/reset semantics are tied to the global trace
* array. options/stackmap is hidden on secondary instances via
* TOP_LEVEL_TRACE_FLAGS, but writes still reach set_tracer_flag()
* through the aggregate trace_options file. Reject the enable on
@@ -9412,17 +9412,21 @@ static __init void tracer_init_tracefs_work_func(struct work_struct *work)
smp_store_release(&global_trace.stackmap, smap);
WRITE_ONCE(stackmap_init_state, STACKMAP_INIT_DONE);
/*
- * stat is an auxiliary observability
- * surface. If it fails to be created we keep
- * dedup enabled -- the kernel side still
- * works and stack_map alone is enough to
- * resolve and reset; trace_create_file()
+ * stat and bin are auxiliary observability
+ * surfaces. If they fail to be created we
+ * keep dedup enabled (the kernel side still
+ * works, and stack_map alone is enough to
+ * resolve and reset); trace_create_file()
* already pr_warn()s on failure.
*/
trace_create_file("stack_map_stat",
TRACE_MODE_READ, NULL,
smap,
&ftrace_stackmap_stat_fops);
+ trace_create_file("stack_map_bin",
+ TRACE_MODE_READ, NULL,
+ smap,
+ &ftrace_stackmap_bin_fops);
}
} else {
pr_warn("ftrace stackmap init failed, dedup disabled\n");
diff --git a/kernel/trace/trace_stackmap.c b/kernel/trace/trace_stackmap.c
index 2382c7459712..439d3be9e351 100644
--- a/kernel/trace/trace_stackmap.c
+++ b/kernel/trace/trace_stackmap.c
@@ -35,17 +35,19 @@
* collision, linear probing finds the next slot and full memcmp
* confirms the match.
*
- * Concurrent userspace readers (cat stack_map) get a best-effort
- * snapshot. They are coherent with the hot path (smp_load_acquire on
- * entry->val); they are also serialized against reset via
- * smap->reader_sem (readers take it in shared mode, reset in
+ * Concurrent userspace readers (cat stack_map / stack_map_bin) get a
+ * best-effort snapshot. They are coherent with the hot path
+ * (smp_load_acquire on entry->val); they are also serialized against
+ * reset via smap->reader_sem (readers take it in shared mode, reset in
* exclusive mode), so a reset cannot tear a single seq_file pass --
* it waits for the active read() pass to drop the rwsem before clearing
- * the map. The hot path is coordinated with reset separately, via
- * acquire/release on smap->resetting.
+ * the map. Binary readers spanning multiple read() calls additionally
+ * use a generation check. The hot path is coordinated with reset
+ * separately, via acquire/release on smap->resetting.
*/
#include <linux/kernel.h>
+#include <linux/bitmap.h>
#include <linux/slab.h>
#include <linux/jhash.h>
#include <linux/seq_file.h>
@@ -74,7 +76,7 @@
/*
* Memory ordering of entry->val: published with smp_store_release()
* by the inserter; consumed with smp_load_acquire() by every reader
- * that dereferences the elt (get_id, seq_show). This pairs
+ * that dereferences the elt (get_id, seq_show, bin_show). This pairs
* the writes to elt->{nr,ips,ref_count} (initialized BEFORE the
* publish) with the reads of those fields (which happen AFTER the
* load). seq_start / seq_next only test val for NULL and use the
@@ -119,11 +121,19 @@ struct ftrace_stackmap {
struct stackmap_entry *entries; /* hash table */
struct stackmap_elt *elts; /* flat element pool */
atomic_t resetting;
+ /*
+ * Bumped by every completed reset, inside the reader_sem write
+ * section. A reader that spans several read() calls samples this
+ * at open() and revalidates it on each pass, so a reset landing
+ * between two reads is detected instead of silently splicing two
+ * generations of the map into one output stream.
+ */
+ u64 generation;
/*
* Reader/reset serialization. Held in shared mode (read lock)
- * across seq_file iteration; held in exclusive mode (write
- * lock) by reset's clearing phase. The hot path (get_id) does
- * not take this lock — it
+ * across each seq_file pass of the text and binary exports;
+ * held in exclusive mode (write lock) by reset's clearing
+ * phase. The hot path (get_id) does not take this lock — it
* uses smp_load_acquire/smp_store_release on entry->val and
* the resetting flag for the lock-free protocol.
*/
@@ -206,6 +216,7 @@ struct ftrace_stackmap *ftrace_stackmap_create(struct trace_array *tr)
smap->hash_seed = get_random_u32();
atomic_set(&smap->next_elt, 0);
atomic_set(&smap->resetting, 0);
+ smap->generation = 0;
init_rwsem(&smap->reader_sem);
return smap;
@@ -307,10 +318,10 @@ static int ftrace_stackmap_reset(struct ftrace_stackmap *smap)
/*
* Take the reader_sem in exclusive mode. This serializes the
- * memset against any tracefs reader (seq_file iteration) that
- * may currently hold the rwsem for read. The Tasks RCU grace
- * periods already drained the hot path; this rwsem covers
- * process-context export readers.
+ * memset against any tracefs reader (a seq_file pass of the text
+ * or binary export) that may currently hold the rwsem for read.
+ * The Tasks RCU grace periods already drained the hot path; this
+ * rwsem covers process-context export readers.
*/
down_write(&smap->reader_sem);
@@ -323,6 +334,13 @@ static int ftrace_stackmap_reset(struct ftrace_stackmap *smap)
atomic_long_set(per_cpu_ptr(smap->drops, cpu), 0);
}
+ /*
+ * Bump inside the write section so a reader holding the read
+ * side either sees the whole pre-reset generation or the whole
+ * post-reset one, never a counter from each.
+ */
+ smap->generation++;
+
up_write(&smap->reader_sem);
/* Release resetting=0 so new get_id() observes a cleared map. */
@@ -402,7 +420,7 @@ int ftrace_stackmap_get_id(struct ftrace_stackmap *smap,
val = stackmap_load_elt(entry);
/*
* READ_ONCE(val->nr) keeps style consistent with
- * the seq_show reader. nr is write-once
+ * the seq_show / bin_show readers. nr is write-once
* (set before publish, never modified afterwards),
* so the load is data-race-free, but READ_ONCE
* silences any analysis tool that flags a plain
@@ -853,3 +871,282 @@ const struct file_operations ftrace_stackmap_stat_fops = {
.llseek = seq_lseek,
.release = stackmap_stat_release,
};
+
+/* --- Binary export --- */
+
+/*
+ * Binary counterpart of the text export, for userspace that would
+ * rather not parse symbols out of stack_map.
+ *
+ * Between resets, the element pool only grows and populated slots are
+ * not recycled. open() records the populated slot numbers in a bitmap,
+ * then the table is streamed in index order through seq_write(). This
+ * fixes the record set and makes header.nr_stacks exact without copying
+ * stack payloads. Even at the largest supported table, the bitmap is
+ * only 64 KiB per open, rather than a roughly 135 MiB element snapshot.
+ * A reset clears and reuses both the table and pool.
+ *
+ * seq_file drops reader_sem between read() calls, so a reset landing
+ * between two reads of the same fd would otherwise splice two
+ * generations of the map into a single stream. Text output would merely
+ * look confusing, but a binary consumer would parse it as one coherent
+ * dump, so open() samples the generation counter and every pass
+ * revalidates it, failing the read with -ESTALE once it has moved. ESTALE
+ * is intentional: seq_file treats -EAGAIN as an internal traverse retry,
+ * which would make lseek() on a stale fd spin forever. A reader that loses
+ * the race must reopen the file.
+ *
+ * The records contain raw kernel instruction pointers after
+ * trace_adjust_address(). VFS mode checks provide the tracefs permission
+ * boundary, and open additionally uses tracing_check_open_get_tr() for the
+ * tracing lockdown check and to pin the owning trace array for the lifetime
+ * of the file.
+ */
+struct stackmap_bin_iter {
+ struct ftrace_stackmap *smap;
+ u64 generation; /* sampled at open() */
+ u32 nr_stacks; /* selected at open() */
+ bool locked; /* reader_sem held by start() */
+ /*
+ * Staging buffer for one full-depth entry, kept here rather than
+ * on the stack: the IP array alone is 512 bytes, which has no
+ * business in a seq_file show() frame. Emitting an entry in a
+ * single seq_write() also keeps it from being torn across a
+ * buffer-growth retry.
+ */
+ char ebuf[sizeof(struct ftrace_stackmap_bin_entry) +
+ FTRACE_STACKMAP_MAX_DEPTH * sizeof(u64)]
+ __aligned(8);
+ /* Populated table slots selected during the open-time scan. */
+ unsigned long *slots;
+};
+
+/*
+ * Advance to the first populated slot at or after *idx. Sets *idx to
+ * map_size when the table is exhausted, so a later call terminates
+ * immediately.
+ */
+static struct stackmap_entry *stackmap_bin_seek(struct stackmap_bin_iter *iter,
+ loff_t *idx)
+{
+ struct ftrace_stackmap *smap = iter->smap;
+ unsigned long i;
+
+ i = find_next_bit(iter->slots, smap->map_size, *idx);
+ if (i >= smap->map_size) {
+ *idx = smap->map_size;
+ return NULL;
+ }
+
+ *idx = i;
+ return &smap->entries[i];
+}
+
+/*
+ * Stream position 0 is the file header; position idx+1 is table slot
+ * idx. Deriving it from the slot index keeps the iteration idempotent
+ * across the retries seq_file performs when its buffer has to grow.
+ */
+static void *stackmap_bin_seq_start(struct seq_file *m, loff_t *pos)
+{
+ struct stackmap_bin_iter *iter = m->private;
+ struct ftrace_stackmap *smap = iter->smap;
+ struct stackmap_entry *entry;
+ loff_t idx;
+
+ /*
+ * Serializes against the clearing phase of ftrace_stackmap_reset().
+ * Dropped in stackmap_bin_seq_stop(), which seq_file calls after
+ * every pass, including the ones where start() reports an error.
+ */
+ down_read(&smap->reader_sem);
+ iter->locked = true;
+
+ /*
+ * The table has already been walked to the end. There is nothing
+ * left to hand over, so a reset that happened in the meantime
+ * cannot splice generations into this stream: report EOF rather
+ * than an error. Otherwise a reader that had already consumed the
+ * whole export would take a failure on its final read() and treat
+ * the data it holds as invalid.
+ */
+ if (*pos > smap->map_size)
+ return NULL;
+
+ if (smap->generation != iter->generation)
+ return ERR_PTR(-ESTALE);
+
+ if (*pos == 0)
+ return SEQ_START_TOKEN;
+
+ idx = *pos - 1;
+ entry = stackmap_bin_seek(iter, &idx);
+ *pos = idx + 1;
+ return entry;
+}
+
+static void *stackmap_bin_seq_next(struct seq_file *m, void *v, loff_t *pos)
+{
+ struct stackmap_bin_iter *iter = m->private;
+ struct ftrace_stackmap *smap = iter->smap;
+ struct stackmap_entry *entry;
+ loff_t idx;
+
+ if (v == SEQ_START_TOKEN)
+ idx = 0;
+ else
+ idx = ((struct stackmap_entry *)v - smap->entries) + 1;
+
+ entry = stackmap_bin_seek(iter, &idx);
+ *pos = idx + 1;
+ return entry;
+}
+
+static void stackmap_bin_seq_stop(struct seq_file *m, void *v)
+{
+ struct stackmap_bin_iter *iter = m->private;
+
+ if (iter->locked) {
+ up_read(&iter->smap->reader_sem);
+ iter->locked = false;
+ }
+}
+
+static int stackmap_bin_seq_show(struct seq_file *m, void *v)
+{
+ struct stackmap_bin_iter *iter = m->private;
+ struct ftrace_stackmap_bin_entry *e;
+ struct stackmap_entry *entry;
+ struct stackmap_elt *elt;
+ u32 i, nr;
+
+ if (v == SEQ_START_TOKEN) {
+ struct ftrace_stackmap_bin_header hdr = {
+ .magic = FTRACE_STACKMAP_BIN_MAGIC,
+ .version = FTRACE_STACKMAP_BIN_VERSION,
+ .nr_stacks = iter->nr_stacks,
+ .reserved = 0,
+ };
+
+ seq_write(m, &hdr, sizeof(hdr));
+ return 0;
+ }
+
+ entry = v;
+ elt = stackmap_load_elt(entry);
+ if (!elt)
+ return 0;
+
+ nr = READ_ONCE(elt->nr);
+ if (nr > FTRACE_STACKMAP_MAX_DEPTH)
+ nr = FTRACE_STACKMAP_MAX_DEPTH;
+
+ e = (struct ftrace_stackmap_bin_entry *)iter->ebuf;
+ e->stack_id = entry - iter->smap->entries;
+ e->nr = nr;
+ e->ref_count = atomic_read(&elt->ref_count);
+ e->reserved = 0;
+
+ for (i = 0; i < nr; i++) {
+ unsigned long ip = elt->ips[i];
+
+ /*
+ * Emit the trampoline marker verbatim so userspace can
+ * render it as [FTRACE TRAMPOLINE]; pass everything else
+ * through trace_adjust_address() so the binary export
+ * follows the same address-adjustment rules as the text
+ * export.
+ */
+ if (ip == FTRACE_TRAMPOLINE_MARKER)
+ e->ips[i] = (u64)FTRACE_TRAMPOLINE_MARKER;
+ else
+ e->ips[i] = (u64)trace_adjust_address(iter->smap->tr, ip);
+ }
+
+ seq_write(m, e, struct_size(e, ips, nr));
+ return 0;
+}
+
+static const struct seq_operations stackmap_bin_seq_ops = {
+ .start = stackmap_bin_seq_start,
+ .next = stackmap_bin_seq_next,
+ .stop = stackmap_bin_seq_stop,
+ .show = stackmap_bin_seq_show,
+};
+
+static int stackmap_bin_open(struct inode *inode, struct file *file)
+{
+ struct ftrace_stackmap *smap = inode->i_private;
+ struct stackmap_bin_iter *iter;
+ struct seq_file *m;
+ u32 nr = 0;
+ loff_t i;
+ int ret;
+
+ if (!smap)
+ return -ENODEV;
+
+ ret = tracing_check_open_get_tr(smap->tr);
+ if (ret)
+ return ret;
+
+ ret = seq_open_private(file, &stackmap_bin_seq_ops, sizeof(*iter));
+ if (ret)
+ goto put_tr;
+
+ m = file->private_data;
+ iter = m->private;
+ iter->smap = smap;
+ iter->slots = kvcalloc(BITS_TO_LONGS(smap->map_size),
+ sizeof(*iter->slots), GFP_KERNEL_ACCOUNT);
+ if (!iter->slots) {
+ ret = -ENOMEM;
+ goto release_seq;
+ }
+
+ /*
+ * Select the populated slots up front so header.nr_stacks exactly
+ * frames the records emitted by this fd. The bitmap copies only
+ * membership, not stack payloads; at the maximum map size it uses
+ * 64 KiB. Inserts after this scan are deliberately excluded.
+ */
+ down_read(&smap->reader_sem);
+ iter->generation = smap->generation;
+ for (i = 0; i < smap->map_size; i++) {
+ if (READ_ONCE(smap->entries[i].key) &&
+ stackmap_load_elt(&smap->entries[i])) {
+ __set_bit(i, iter->slots);
+ nr++;
+ }
+ }
+ up_read(&smap->reader_sem);
+
+ iter->nr_stacks = nr;
+ return 0;
+
+release_seq:
+ seq_release_private(inode, file);
+put_tr:
+ trace_array_put(smap->tr);
+ return ret;
+}
+
+static int stackmap_bin_release(struct inode *inode, struct file *file)
+{
+ struct seq_file *m = file->private_data;
+ struct stackmap_bin_iter *iter = m->private;
+ struct ftrace_stackmap *smap = iter->smap;
+ int ret;
+
+ kvfree(iter->slots);
+ ret = seq_release_private(inode, file);
+ trace_array_put(smap->tr);
+ return ret;
+}
+
+const struct file_operations ftrace_stackmap_bin_fops = {
+ .open = stackmap_bin_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = stackmap_bin_release,
+};
diff --git a/kernel/trace/trace_stackmap.h b/kernel/trace/trace_stackmap.h
index 7615e346dfa6..80415b985627 100644
--- a/kernel/trace/trace_stackmap.h
+++ b/kernel/trace/trace_stackmap.h
@@ -7,6 +7,25 @@
#define FTRACE_STACKMAP_MAX_DEPTH 64
+/* Binary export format */
+#define FTRACE_STACKMAP_BIN_MAGIC 0x46534D42 /* 'FSMB' */
+#define FTRACE_STACKMAP_BIN_VERSION 1
+
+struct ftrace_stackmap_bin_header {
+ u32 magic;
+ u32 version;
+ u32 nr_stacks;
+ u32 reserved;
+};
+
+struct ftrace_stackmap_bin_entry {
+ u32 stack_id;
+ u32 nr;
+ u32 ref_count;
+ u32 reserved;
+ u64 ips[]; /* nr entries follow the header */
+};
+
struct trace_array;
#ifdef CONFIG_FTRACE_STACKMAP
@@ -20,6 +39,7 @@ int ftrace_stackmap_get_id(struct ftrace_stackmap *smap,
extern const struct file_operations ftrace_stackmap_fops;
extern const struct file_operations ftrace_stackmap_stat_fops;
+extern const struct file_operations ftrace_stackmap_bin_fops;
#else
--
2.34.1
next prev parent reply other threads:[~2026-09-12 8:39 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-12 8:37 [RFC PATCH v7 00/10] trace: stack trace deduplication for ftrace ring buffer Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 01/10] trace: add lock-free stackmap for stack trace deduplication Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 02/10] trace: use the stackmap from the ftrace stack recording path Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 03/10] trace: add stackmap statistics interface Li Pengfei
2026-09-12 8:37 ` Li Pengfei [this message]
2026-09-12 8:37 ` [RFC PATCH v7 05/10] trace: make the stackmap capacity settable on the kernel command line Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 06/10] Documentation: tracing: document the ftrace stackmap Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 07/10] tools/tracing: add a parser for the stackmap binary export Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 08/10] selftests/ftrace: add a stackmap basic functionality test Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 09/10] selftests/ftrace: add a stackmap reset and binary ABI test Li Pengfei
2026-09-12 8:37 ` [RFC PATCH v7 10/10] selftests/ftrace: add a stackmap instance gating test Li Pengfei
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260912083753.3426176-5-lipengfei28@xiaomi.com \
--to=ljdlns1987@gmail.com \
--cc=corbet@lwn.net \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=linux-trace-kernel@vger.kernel.org \
--cc=lipengfei28@xiaomi.com \
--cc=lkp@intel.com \
--cc=mark.rutland@arm.com \
--cc=mathieu.desnoyers@efficios.com \
--cc=mhiramat@kernel.org \
--cc=rostedt@goodmis.org \
--cc=skhan@linuxfoundation.org \
--cc=zhangbo56@xiaomi.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox