Printing debug messages with pr_*() and dev_*() gets you a long way, but it
cannot answer timing questions: how long did that interrupt handler take? Which function ate 40%
of a scheduling latency spike? Which task got starved of CPU time and for how long? For this class
of problem the Linux kernel ships its own built-in tracer — Ftrace — and this
lecture is your practical entry point into it.
We will close out the last piece of the printk timestamp discussion, then build a mental model
of how Ftrace is organized, enable it on a real kernel, and finish with a small original kernel
module you can trace end to end.
free linux device drivers course
free embedded systems course
ftrace tutorial
linux kernel tracing
What You Will Learn
- How to control whether printk timestamps are shown, without touching whether they’re logged
- What Ftrace actually is, who built it, and why it lives inside the kernel rather than as an external tool
- The kernel config options Ftrace depends on, and how to check them on a running system
- How tracefs gets mounted and what each important file under it does
- How to read the list of available tracers and pick the right one for a debugging session
- How to build, load, and trace an original demo module with
function_graph
Prerequisites
- Comfortable building and loading out-of-tree kernel modules (
insmod/rmmod) - Familiarity with
pr_*()logging anddmesg, covered in the previous lecture of this chapter - Root access on a Linux machine or VM running a reasonably recent (6.x) kernel with
CONFIG_DEBUG_FSenabled
Finishing the printk timestamp story
One detail is easy to misread: the knob at /sys/module/printk/parameters/time does
not decide whether a timestamp gets attached to a kernel message. Every line in the
kernel log buffer already carries a monotonic timestamp (seconds.microseconds since the kernel’s
internal clock started, i.e. since the bootloader handed off control) regardless of this setting.
What the knob controls is purely a display decision — whether that stored timestamp is
printed when the buffer is dumped, whether that’s at boot, on the console, or through dmesg.
# check current state
cat /sys/module/printk/parameters/time
# disable timestamp printing (log buffer keeps recording them internally either way)
echo 0 > /sys/module/printk/parameters/time
# re-enable
echo 1 > /sys/module/printk/parameters/time
On boot-constrained embedded targets, some teams disable this print step to shave a small amount
of time off log rendering — a micro-optimization worth knowing about, not one to chase unless you’ve
actually profiled boot time and found it matters.
What Ftrace actually is
Ftrace (“Function Trace”) is a tracing framework that has shipped in the mainline kernel since
2.6.27 (2008), originally authored by Steven Rostedt. Despite the name, it is not limited to tracing
function calls — that’s just the tracer it started with. Today the same framework backs function
tracing, function-graph (call/return) tracing, scheduling latency tracers, interrupt/preemption-off
latency tracers, block I/O tracing, and the event-tracing subsystem that tools like perf
and trace-cmd build on top of.
The core idea is simple: Ftrace maintains an in-kernel ring buffer. Depending on which tracer is
active, entries get written into that buffer as events happen — a function entered, an IRQ disabled,
a task woken up. You read the buffer back out through a pseudo-filesystem interface, not through a
separate daemon or agent, which is why Ftrace has near-zero setup cost compared to external profilers.
Kernel configuration Ftrace depends on
Ftrace’s individual tracers are gated by separate config symbols, so what you can use depends on
how the running kernel was built. The commonly needed ones:
| Config option | Enables |
|---|---|
CONFIG_FUNCTION_TRACER |
The base function tracer (every traceable function call) |
CONFIG_FUNCTION_GRAPH_TRACER |
The function_graph tracer — entry and return, with call depth |
CONFIG_STACK_TRACER |
Recording of the deepest kernel stack usage seen |
CONFIG_DYNAMIC_FTRACE |
Runtime patching so tracing has near-zero overhead when disabled |
These in turn rely on architecture support flags — CONFIG_HAVE_FUNCTION_TRACER,
CONFIG_HAVE_FUNCTION_GRAPH_TRACER, and CONFIG_HAVE_DYNAMIC_FTRACE — which is
why Ftrace’s feature set can differ slightly between architectures. On x86-64 and arm64 distro
kernels, all of the above are typically already built in.
# confirm the running kernel has them, without rebuilding anything
grep -E 'CONFIG_(FUNCTION_TRACER|FUNCTION_GRAPH_TRACER|DYNAMIC_FTRACE)' /boot/config-$(uname -r)
Mounting tracefs
Ftrace’s control interface lives on its own pseudo-filesystem, tracefs, historically
nested under debugfs. Most modern distributions mount it automatically at boot, but it’s
worth knowing how to do it yourself for minimal embedded root filesystems.
Persistent, via /etc/fstab:
tracefs /sys/kernel/debug/tracing tracefs defaults 0 0
Or on demand, at runtime (root only):
mount -t tracefs nodev /sys/kernel/debug/tracing
Once mounted, everything happens by reading and writing plain files under
/sys/kernel/debug/tracing/ — no special ioctl or netlink protocol involved.
The files that matter first
The tracefs directory has dozens of entries; most of them are for advanced filtering. As a
beginner you only need a handful to get productive:
| File | Purpose |
|---|---|
available_tracers |
Lists which tracers this kernel build supports |
current_tracer |
Read: which tracer is active. Write: switch tracers |
tracing_on |
Write 1 to start recording into the ring buffer, 0 to pause it |
trace |
Human-readable snapshot of the current ring buffer contents |
trace_pipe |
Same data as trace, but streamed live and consumed as it’s read |
tracing_cpumask |
Hex bitmask selecting which CPUs get traced |
set_ftrace_filter |
Restrict function tracing to a specific set of function names |
The tracing_cpumask file deserves a quick example since the hex encoding trips people
up: it’s a plain bitmask, one bit per CPU. To trace only CPU 0, write 1. For CPU 1 alone,
write 2. For CPU 3 alone, write 8. To trace CPUs 0 and 1 together, write
3 (bit 0 + bit 1).
Reading the available tracers
Before picking a tracer, check what your kernel actually offers:
cat /sys/kernel/debug/tracing/available_tracers
A typical distro kernel output looks like this — the exact list depends on your config and
architecture:
blk function_graph wakeup_dl wakeup_rt wakeup irqsoff function nop
| Tracer | What it measures |
|---|---|
function |
Every traced function call, no arguments, no nesting shown |
function_graph |
Function calls with their return, shown as a nested call graph with duration |
blk |
Block-layer I/O events (the backend used by blktrace) |
irqsoff |
Longest stretch the kernel spent with interrupts disabled |
preemptoff |
Longest stretch with preemption disabled |
preemptirqsoff |
Longest stretch with either interrupts or preemption disabled |
wakeup / wakeup_rt |
Worst-case latency between a task being woken and actually running (all tasks / real-time tasks only) |
nop |
Does nothing on its own — just shows manual trace_printk() output |
irqsoff, preemptoff, and preemptirqsoff are grouped together
as the latency tracers: instead of logging every event, each keeps only the single worst-case
interval it has observed since being enabled, which makes them cheap to leave running for a long time
while hunting an intermittent spike.
Original demo: tracing a small module with function_graph
Rather than trace the whole kernel, let’s build a tiny original module and watch Ftrace follow its
own call chain. The module (ep_ftrace_demo) deliberately calls through three nested
functions on load, so the graph tracer has something visible to show.
// ep_ftrace_demo.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static noinline void ep_step_c(void)
{
pr_info("ep_ftrace_demo: reached step C\n");
}
static noinline void ep_step_b(void)
{
ep_step_c();
}
static noinline void ep_step_a(void)
{
ep_step_b();
}
static int __init ep_ftrace_demo_init(void)
{
pr_info("ep_ftrace_demo: loaded, calling step chain\n");
ep_step_a();
return 0;
}
static void __exit ep_ftrace_demo_exit(void)
{
pr_info("ep_ftrace_demo: unloaded\n");
}
module_init(ep_ftrace_demo_init);
module_exit(ep_ftrace_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala Ftrace demo module");
// Makefile
obj-m += ep_ftrace_demo.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
noinline is important here — without it the compiler may inline the whole chain into
one function, and there would be nothing left for the graph tracer to show.
Build and set up tracing before loading it, so the very first call chain gets captured:
# build
make
# select the function_graph tracer
echo function_graph > /sys/kernel/debug/tracing/current_tracer
# restrict tracing to only our functions, so the trace stays readable
echo 'ep_step_*' > /sys/kernel/debug/tracing/set_ftrace_filter
# clear any old data and enable recording
echo > /sys/kernel/debug/tracing/trace
echo 1 > /sys/kernel/debug/tracing/tracing_on
# now load the module
insmod ep_ftrace_demo.ko
# stop recording and read the result
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace
Expected output (durations will vary run to run):
1) | ep_step_a() {
1) | ep_step_b() {
1) 0.196 us | ep_step_c();
1) 0.512 us | }
1) 0.781 us | }
Clean up afterward:
rmmod ep_ftrace_demo
echo nop > /sys/kernel/debug/tracing/current_tracer
echo > /sys/kernel/debug/tracing/set_ftrace_filter
Common mistakes and troubleshooting
- Forgetting to reset
set_ftrace_filter— an old filter silently
hides everything outside it in your next session. Clear it with an empty write when you’re done. - Reading
available_tracersand assuming every listed tracer is always
usable — some (likewakeup_dl) needCONFIG_SCHED_TRACERand only
make sense with real-time or deadline-scheduled tasks present. - Leaving
tracing_onat 1 with a busy tracer like plain
function— the ring buffer fills fast on a busy system; check
buffer_size_kbif traces look truncated. - Expecting
trace_pipeto behave liketrace—
trace_pipeconsumes entries as they’re read (like a real pipe), so reading it twice
won’t show the same data twice.
Best practices
- Always narrow scope with
set_ftrace_filterortracing_cpumaskbefore
tracing on production or resource-constrained targets. - Prefer the latency tracers (
irqsoff/wakeup) over raw
functiontracing when you only care about worst-case timing, not every call. - Reset
current_tracertonopand clear filters when you’re finished —
don’t leave heavier tracers running unattended. - On constrained embedded targets, check
buffer_size_kbbefore a long tracing
session; the default per-CPU buffer size can be adjusted to fit available RAM.
Performance considerations
Thanks to CONFIG_DYNAMIC_FTRACE, function tracing costs essentially nothing when
disabled — the kernel patches call sites back to no-ops. Once enabled, function_graph
costs more than plain function tracing because it instruments both entry and return;
on a busy system this is measurable and worth confining to a filtered function set.
Security considerations
Tracefs is only reachable by root by default, which is intentional — function tracing exposes
kernel internals (addresses, call timing, scheduling behavior) that shouldn’t be handed to
unprivileged users. Don’t relax the permissions on /sys/kernel/debug on a production or
multi-user system.
Interview Questions
What does CONFIG_DYNAMIC_FTRACE actually buy you?
Near-zero overhead when tracing is off. Instead of leaving tracing calls active everywhere,
the kernel patches instrumented call sites into no-ops at boot and re-patches them only when a
tracer is enabled, so unused instrumentation doesn’t cost cycles on every function call.
Why would you pick irqsoff over function for a latency bug?
irqsoff only records the single worst interrupts-disabled interval it has seen, which is cheap
to leave running for hours. function traces every call, generating far more data and overhead —
impractical for long unattended runs.
How do you trace only CPU 2 on a quad-core system?
Write the hex bitmask with only bit 2 set to tracing_cpumask — that’s decimal 4, so
echo 4 > /sys/kernel/debug/tracing/tracing_cpumask.
Does disabling printk timestamp printing stop timestamps from being recorded?
No. Every kernel log line already carries a timestamp internally; the knob only controls whether
that stored timestamp is displayed when the buffer is dumped or read via dmesg.
Summary and Key Takeaways
- Printk’s timestamp knob is a display setting, not a logging setting — timestamps are always recorded.
- Ftrace is a built-in kernel tracing framework, no external daemon required, driven entirely through tracefs files.
CONFIG_FUNCTION_TRACER,CONFIG_FUNCTION_GRAPH_TRACER, and
CONFIG_DYNAMIC_FTRACEare the config options that matter most for getting started.current_tracer,tracing_on, andtraceare the three files
you’ll touch in almost every session.- Filtering with
set_ftrace_filterandtracing_cpumaskkeeps traces
readable and cheap on real hardware.
Conclusion
Ftrace turns “I think this function is slow” into a measured call graph with real numbers, without
installing anything or leaving the kernel you’re already running. The workflow you practiced here —
pick a tracer, filter it down, enable, capture, disable, read — is the same workflow you’ll reuse for
every real debugging session, whether you’re chasing an interrupt latency spike or a misbehaving
driver’s call sequence. The next lecture in this chapter builds on this foundation with event
tracing and trace_printk() for finer-grained instrumentation of your own drivers.
Frequently Asked Questions
Do I need to rebuild my kernel to use Ftrace?
Usually not. Most distribution kernels already enable the core Ftrace config options. Check with
grep CONFIG_FUNCTION_TRACER /boot/config-$(uname -r) before assuming you need a custom build.
What’s the difference between trace and trace_pipe?
trace shows a static, re-readable snapshot of the current buffer. trace_pipe streams entries live
and consumes them as they’re read, similar to tailing a real pipe.
Can I use Ftrace on a production embedded device?
Yes, and it’s designed for that — the near-zero overhead when disabled is exactly why it’s safe
to build into production kernels rather than only debug builds.
Is Ftrace the same thing as perf?
No. perf is a separate profiling tool that can use Ftrace’s event infrastructure as one of its data
sources, but Ftrace itself works standalone through tracefs with no extra tooling required.
Why did my set_ftrace_filter show no output at all?
The filter pattern likely didn’t match any traceable symbol name — function names must match
exactly or with a trailing wildcard as shown, and static/inlined functions may not be traceable at all.
What happens if I forget to set tracing_on back to 1 after clearing the buffer?
Nothing gets recorded — clearing trace doesn’t affect tracing_on, but it’s easy to assume it does.
Always confirm tracing_on reads 1 before triggering the event you want captured.
Does function_graph tracing work on interrupt handlers too?
Yes, as long as the handler function is traceable (not force-inlined and not explicitly excluded),
it appears in the graph like any other traced function.
Want the next lecture in this series first?
Continue with event tracing and trace_printk() for driver-level instrumentation.
