Ftrace Function Tracers Explained
A hands-on guide to the function and function_graph tracers in Ftrace, part of our free Linux kernel development course
If you have ever asked “what is my kernel actually doing right now, function by function?” the answer is Ftrace. Ftrace is a built-in tracing framework that ships with mainline Linux and needs no external tooling to get started. In this lecture from our free linux kernel development course we build a small original kernel module, trace it with the function and function_graph tracers, and learn how to filter tens of thousands of trace points down to the ones we actually care about.
This lecture continues our free embedded systems course track on debugging and observability. If you are looking for a free linux device drivers course that goes beyond “hello world” drivers into real diagnostic skills, this is exactly the kind of material we cover.
What You Will Learn
By the end of this lecture you will be able to:
- Mount and navigate tracefs on a modern kernel
- Trace kernel function calls with the
functiontracer - Read nested call graphs produced by the
function_graphtracer - Restrict tracing to specific functions using
set_ftrace_filter - Explain why
CONFIG_DYNAMIC_FTRACEmakes Ftrace safe for production kernels - Build and trace a small original kernel module end to end
Prerequisites
- Building and inserting a basic out-of-tree kernel module
- Reading
dmesgoutput - Basic shell redirection (
echo,cat,>)
Where Ftrace Lives Today
On current mainline kernels, the tracing filesystem (tracefs) is usually auto-mounted at /sys/kernel/tracing. Older material you may find online still refers to /sys/kernel/debug/tracing, which was the path when tracefs was only reachable through debugfs. Both paths still work on most distributions today because tracefs is bind-mounted under debugfs for compatibility, but the standalone tracefs mount is the one to prefer, since it does not require debugfs to be mounted at all.
# Confirm tracefs is mounted
mount | grep tracefs
# If it is not mounted, mount it directly
sudo mount -t tracefs nodev /sys/kernel/tracing
cd /sys/kernel/tracing
ls
An Original Demo Module To Trace
Rather than tracing an existing kernel path, we will build a tiny original module, ep_ftrace_demo, that does a small amount of deliberately traceable work every time we write to a sysfs attribute. This gives us a controlled, repeatable target for both tracers.
// ep_ftrace_demo.c
#include <linux/module.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/delay.h>
static struct kobject *ep_kobj;
static u32 ep_step_one(u32 seed)
{
return (seed * 2654435761u) ^ 0x9e3779b9u;
}
static u32 ep_step_two(u32 value)
{
udelay(50);
return value + ep_step_one(value);
}
static void ep_run_pipeline(u32 seed)
{
u32 result = ep_step_two(seed);
pr_info("ep_ftrace_demo: pipeline result = %u\n", result);
}
static ssize_t trigger_store(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t count)
{
u32 seed;
if (kstrtou32(buf, 10, &seed))
return -EINVAL;
ep_run_pipeline(seed);
return count;
}
static struct kobj_attribute trigger_attr = __ATTR_WO(trigger);
static int __init ep_ftrace_demo_init(void)
{
ep_kobj = kobject_create_and_add("ep_ftrace_demo", kernel_kobj);
if (!ep_kobj)
return -ENOMEM;
return sysfs_create_file(ep_kobj, &trigger_attr.attr);
}
static void __exit ep_ftrace_demo_exit(void)
{
kobject_put(ep_kobj);
}
module_init(ep_ftrace_demo_init);
module_exit(ep_ftrace_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala Ftrace demo module");
# Build and load
make -C /lib/modules/$(uname -r)/build M=$PWD modules
sudo insmod ep_ftrace_demo.ko
# Trigger the traced path
echo 7 | sudo tee /sys/kernel/kernel/ep_ftrace_demo/trigger
Using the Function Tracer
The function tracer records every traced function entry along with the function that called it. It is the lightest-weight tracer Ftrace offers and a good starting point before reaching for heavier tools like perf.
cd /sys/kernel/tracing
echo function > current_tracer
echo "ep_step_one" > set_ftrace_filter
echo "ep_step_two" >> set_ftrace_filter
echo 1 > tracing_on
echo 7 | sudo tee /sys/kernel/kernel/ep_ftrace_demo/trigger
echo 0 > tracing_on
cat trace
#
# TASK-PID CPU# TIMESTAMP FUNCTION
# | | | | |
tee-2381 [001] 1234.001122: ep_step_two <-ep_run_pipeline
tee-2381 [001] 1234.001125: ep_step_one <-ep_step_two
Each line reads right to left: the function on the right called the function on the left. This is enough to answer “was my function called, and by what?” without any instrumentation code of your own.
Reading the function_graph Tracer
The function_graph tracer goes further: it shows entry and exit as matching braces and reports how long each call took. This makes nested call relationships and hot paths immediately visible.
echo function_graph > current_tracer
echo 1 > tracing_on
echo 9 | sudo tee /sys/kernel/kernel/ep_ftrace_demo/trigger
echo 0 > tracing_on
cat trace
# | | | | | |
1) | ep_run_pipeline() {
1) | ep_step_two() {
1) 0.180 us | ep_step_one();
1) + 51.220 us | }
1) + 51.400 us | }
A + before the duration means the call took more than 10 microseconds; an ! would mean more than 100 microseconds. Here the udelay(50) inside ep_step_two() is clearly visible as the dominant cost, which is exactly the kind of insight the flat function tracer cannot give you at a glance.
| Tracer | Shows call graph nesting | Shows duration | Overhead |
|---|---|---|---|
| function | No | No | Very low |
| function_graph | Yes | Yes | Low-moderate |
Dynamic Ftrace and Filtering at Scale
A stock kernel exposes tens of thousands of traceable functions in available_filter_functions. Tracing all of them at once would be both noisy and expensive. CONFIG_DYNAMIC_FTRACE, enabled on virtually every distribution kernel today, solves this in two ways.
Why CONFIG_DYNAMIC_FTRACE matters
- At boot, unused trace call sites are patched into NOP instructions, so disabled tracing has effectively zero runtime cost — safe to leave built into production kernels.
- You selectively re-enable only the functions you care about, instead of tracing everything.
You move a function name from available_filter_functions into set_ftrace_filter to trace it, and into set_ftrace_notrace to explicitly exclude it. Wildcards are supported, which is useful for tracing an entire subsystem by prefix.
# Trace every function starting with ep_
echo "ep_*" > set_ftrace_filter
# Exclude one noisy function from that set
echo "ep_step_one" > set_ftrace_notrace
# Clear all filters (trace everything again)
echo > set_ftrace_filter
Real-World Use Cases
- Driver bring-up: confirming a probe path or interrupt handler is actually being entered on real hardware.
- Latency hunting: using function_graph durations to spot an unexpectedly slow function in an interrupt or IPC path.
- Regression triage: filtering to one subsystem prefix (e.g.
tcp_*) after a kernel bump to see if call patterns changed.
Common Mistakes and Troubleshooting
- Forgetting to set tracing_on back to 0 before reading
trace— the buffer keeps filling and your output gets noisy. - Filtering by a static/inlined function — the compiler may inline or rename it under LTO/optimization, so it never appears in
available_filter_functions. - Assuming /sys/kernel/debug/tracing is the only path — on hardened systems debugfs may be disabled entirely while tracefs at
/sys/kernel/tracingstill works. - Not clearing old filters — a leftover
set_ftrace_filterfrom a previous session silently narrows your next trace.
Best Practices
- Always set
current_tracerbefore touchingset_ftrace_filter, to avoid a moment where an unfiltered tracer is briefly active. - Use
function_graphwhen you need timing; use plainfunctionwhen you only need call presence, since it is cheaper. - Reset state with
echo nop > current_tracerand clear filters when you finish a debugging session, especially on shared systems.
Performance and Security Considerations
Performance
Thanks to dynamic patching, an idle Ftrace costs essentially nothing. Once active, function_graph costs more than function due to the extra entry/exit bookkeeping — prefer narrow filters over broad wildcards on latency-sensitive systems.
Summary and Key Takeaways
- Ftrace lives in tracefs, normally auto-mounted at
/sys/kernel/tracingon modern kernels. - The
functiontracer shows call presence;function_graphadds nesting and duration. CONFIG_DYNAMIC_FTRACEmakes disabled tracing free and enables selective filtering viaset_ftrace_filter/set_ftrace_notrace.
Conclusion
Ftrace’s function and function_graph tracers are often the fastest path from “something feels slow” to “here is the exact function and duration responsible.” Because they ship in the mainline kernel with zero extra tooling, they belong in every kernel developer’s first line of debugging tools — well before reaching for heavier profilers. In the next lecture of this free linux kernel development course, we go one level deeper with trace events, which attach real parameters to each recorded call.
Frequently Asked Questions
Is Ftrace available on every Linux kernel?
It is available on virtually every modern distribution kernel, since CONFIG_FTRACE and CONFIG_DYNAMIC_FTRACE are enabled by default on mainstream distros. Embedded kernels may need it enabled explicitly in the kernel config.
Do I need debugfs mounted to use Ftrace?
No. Modern kernels expose tracefs directly at /sys/kernel/tracing, independent of debugfs.
What is the performance cost of an idle tracer?
Effectively none, because CONFIG_DYNAMIC_FTRACE patches unused trace call sites into NOP instructions at boot.
Can I trace a specific process only?
Yes, by writing its PID to set_ftrace_pid, which restricts function tracing to that process or thread.
Why doesn’t my function show up in available_filter_functions?
It may have been inlined or optimized away by the compiler. Static inline functions are common culprits.
What’s the difference between set_ftrace_filter and set_ftrace_notrace?
set_ftrace_filter is an allow-list of functions to trace; set_ftrace_notrace is a deny-list excluding functions from an otherwise active trace.
Does function_graph work the same on ARM and x86?
Yes, function_graph is architecture-independent, though enabling it requires CONFIG_FUNCTION_GRAPH_TRACER to be built for that architecture.
Continue Your Free Linux Kernel Development Course
Next, we look at Ftrace trace events — richer, parameterized trace points used across Ftrace, perf, and LTTng.
Next Lecture Course Index
2 Comments