Ftrace Function Tracers Explained
A free Linux kernel development course lesson on tracing function calls with Ftrace
available_tracers yet, go back one lecture first — here we assume tracefs is already mounted at /sys/kernel/tracing.
function_graph tracer
tracing_thresh
set_ftrace_filter
free linux device drivers course
free embedded systems course
What You Will Learn
- How the function tracer records every kernel function call
- Reading TASK-PID, CPU#, TIMESTAMP, and FUNCTION columns
- How function_graph adds call depth, braces, and duration
- Interpreting the + and ! duration warning markers
- Using tracing_thresh to only capture slow functions
- Narrowing output with set_ftrace_filter and set_ftrace_notrace
- Tracing a real original kernel module end to end
Prerequisites
- tracefs mounted, kernel built with CONFIG_FUNCTION_TRACER and CONFIG_FUNCTION_GRAPH_TRACER
- Comfortable building and loading out-of-tree kernel modules
- Root shell access on a Linux board or VM
Our Test Subject: an Original Kernel Module
Rather than tracing a random kernel function, let’s build a tiny original driver called ep_iotrace_demo, so we can watch Ftrace follow calls we wrote ourselves. It exposes a debugfs file; writing to it triggers a small chain of function calls.
// ep_iotrace_demo.c
#include <linux/module.h>
#include <linux/debugfs.h>
#include <linux/slab.h>
static struct dentry *ep_dbg_dir;
static noinline void ep_stage_validate(int value)
{
if (value < 0)
pr_warn("ep_iotrace_demo: negative value ignored\n");
}
static noinline void ep_stage_transform(int value)
{
ep_stage_validate(value);
value *= 2;
pr_info("ep_iotrace_demo: transformed value = %d\n", value);
}
static noinline void ep_stage_commit(int value)
{
ep_stage_transform(value);
}
static ssize_t ep_trigger_write(struct file *f, const char __user *buf,
size_t count, loff_t *ppos)
{
int value;
char kbuf[16];
if (count >= sizeof(kbuf))
return -EINVAL;
if (copy_from_user(kbuf, buf, count))
return -EFAULT;
kbuf[count] = '\0';
if (kstrtoint(kbuf, 10, &value))
return -EINVAL;
ep_stage_commit(value);
return count;
}
static const struct file_operations ep_trigger_fops = {
.owner = THIS_MODULE,
.write = ep_trigger_write,
};
static int __init ep_iotrace_init(void)
{
ep_dbg_dir = debugfs_create_dir("ep_iotrace_demo", NULL);
debugfs_create_file("trigger", 0200, ep_dbg_dir, NULL, &ep_trigger_fops);
pr_info("ep_iotrace_demo: loaded, write an int to /sys/kernel/debug/ep_iotrace_demo/trigger\n");
return 0;
}
static void __exit ep_iotrace_exit(void)
{
debugfs_remove_recursive(ep_dbg_dir);
}
module_init(ep_iotrace_init);
module_exit(ep_iotrace_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala Ftrace demo module");
The noinline attribute matters here: without it, the compiler may inline these small functions and Ftrace would never see them as separate call frames. Build and load it:
$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
$ sudo insmod ep_iotrace_demo.ko
$ dmesg | tail -1
ep_iotrace_demo: loaded, write an int to /sys/kernel/debug/ep_iotrace_demo/trigger
The Function Tracer
The function tracer is the simplest tracer Ftrace ships: it logs the name of every function entered on every CPU, along with the parent that called it. Nothing else — no timing, no nesting, just a flat list. It is the right starting point because its output is easy to read and its overhead is the lowest among the call-tracing tracers.
Enable it, generate some activity, and disable it:
$ cd /sys/kernel/tracing
$ echo function > current_tracer
$ echo 1 > tracing_on
$ echo 7 > /sys/kernel/debug/ep_iotrace_demo/trigger
$ echo 0 > tracing_on
$ cat trace | grep ep_stage
Expected fragment of the output:
bash-1842 [001] ...1 4820.221033: ep_stage_commit <-ep_trigger_write
bash-1842 [001] ...1 4820.221035: ep_stage_transform <-ep_stage_commit
bash-1842 [001] ...1 4820.221036: ep_stage_validate <-ep_stage_transform
Each line answers one question: which function ran, and who called it. Reading the columns left to right:
CPU# [001]
IRQ/PREEMPT FLAGS …1
TIMESTAMP 4820.221033 (seconds since boot)
FUNCTION ep_stage_commit
CALLER ep_trigger_write (after <-)
Notice the ordering: ep_trigger_write called ep_stage_commit, which called ep_stage_transform, which called ep_stage_validate — exactly the chain we wrote in the module, now visible without adding a single debug print.
| Field | Meaning |
|---|---|
| TASK-PID | Process name and PID that was running when the function executed |
| CPU# | Logical CPU the function ran on |
| TIMESTAMP | Seconds.microseconds since boot, not wall clock time |
| FUNCTION <- CALLER | Function that ran, and the parent that invoked it |
The function_graph Tracer
function_graph is the function tracer’s more detailed sibling. Instead of one line per call, it prints an entry line and an exit line, indents nested calls, and measures how long each function took. This turns a flat call list into an actual call graph you can read top to bottom like source code.
$ echo function_graph > current_tracer
$ echo 1 > tracing_on
$ echo 7 > /sys/kernel/debug/ep_iotrace_demo/trigger
$ echo 0 > tracing_on
$ cat trace | grep -A6 ep_stage_commit
1) | ep_stage_commit() {
1) | ep_stage_transform() {
1) | ep_stage_validate();
1) 0.325 us | }
1) 0.812 us | }
ep_stage_transform() { <- nested one level deeper
ep_stage_validate(); <- semicolon = leaf, no further calls
} <- exit, duration printed on this line
}
Two symbols in the DURATION column are worth memorizing, since they are the first thing an experienced kernel engineer scans for in a long trace:
| Symbol | Meaning |
|---|---|
| (none) | Function completed in well under 10 microseconds |
| + | Function took more than 10 microseconds |
| ! | Function took more than 100 microseconds — worth investigating |
On a busy system you will see plenty of + markers; a ! next to a function you own is usually the first clue when chasing a latency bug.
Filtering By Duration: tracing_thresh
A full function_graph trace of a busy kernel is enormous, and most of it completes in nanoseconds and is irrelevant to a performance investigation. tracing_thresh tells Ftrace to record only calls that ran longer than a threshold you set, in microseconds.
$ echo 50 > tracing_thresh
$ echo function_graph > current_tracer
$ echo 1 > tracing_on
$ echo 7 > /sys/kernel/debug/ep_iotrace_demo/trigger
$ echo 0 > tracing_on
$ cat trace
With the threshold set to 50, any call chain that finishes in under 50 microseconds — including our whole ep_stage_commit chain in the earlier example — disappears entirely, leaving only genuinely slow functions. This same threshold can be set at boot time on the kernel command line, which is especially useful for catching functions that run long only during early boot:
tracing_thresh=200 ftrace=function_graph
That line traces every function taking longer than 200 microseconds from the moment tracing starts at boot, letting you spot boot-time stalls without instrumenting anything by hand. Remember to reset the threshold afterward:
$ echo 0 > tracing_thresh
Narrowing the Scope: Function Filters
Threshold filtering trims by time; function filters trim by name. Instead of tracing every function in the kernel, set_ftrace_filter restricts tracing to only the functions you list, which keeps the buffer small and the overhead low — important advice for anyone building a free embedded systems course lab around Ftrace on constrained hardware.
$ echo ep_stage_transform > set_ftrace_filter
$ echo function > current_tracer
$ echo 1 > tracing_on
$ echo 7 > /sys/kernel/debug/ep_iotrace_demo/trigger
$ echo 0 > tracing_on
$ cat trace | grep ep_stage
bash-1901 [000] ...1 5011.884201: ep_stage_transform <-ep_stage_commit
Only ep_stage_transform appears — ep_stage_commit and ep_stage_validate are silently skipped even though they still executed. Clear the filter with an empty write so tracing returns to normal:
$ echo > set_ftrace_filter
set_ftrace_notrace does the opposite: it excludes named functions while tracing everything else. This is handy when one noisy function (a timer callback, a scheduler helper) floods your trace and you just want it gone:
$ echo ep_stage_validate > set_ftrace_notrace
$ echo function > current_tracer
$ echo 1 > tracing_on
$ echo 7 > /sys/kernel/debug/ep_iotrace_demo/trigger
$ echo 0 > tracing_on
$ cat trace | grep ep_stage
bash-1912 [000] ...1 5090.114420: ep_stage_commit <-ep_trigger_write
bash-1912 [000] ...1 5090.114422: ep_stage_transform <-ep_stage_commit
ep_stage_validate ran, but it never appears in the trace — the exact inverse of what set_ftrace_filter produced.
| File | Effect | Empty write does |
|---|---|---|
| set_ftrace_filter | Trace only the listed functions | Removes the restriction, trace everything again |
| set_ftrace_notrace | Trace everything except the listed functions | Removes the exclusion |
Common Mistakes and Troubleshooting
- Writing “1 >tracing_on” without a space before the redirect silently fails on some shells — always leave a space
- Forgetting to clear set_ftrace_filter afterward, so the next unrelated trace looks empty
- Expecting function_graph timestamps to be wall-clock time — they are seconds since boot
- Small helper functions vanishing from the trace because the compiler inlined them — mark them noinline for tracing sessions
- Leaving tracing_thresh set after debugging, causing later traces to look unexpectedly empty
Best Practices
- Always disable tracing_on before reading trace, so the buffer stops changing mid-read
- Filter to specific functions on production or resource-constrained boards to keep overhead low
- Use function_graph only when you need call depth or duration; plain function tracing is cheaper
- Reset current_tracer to nop, and clear filters and tracing_thresh, once you are done
Performance Considerations
function_graph tracing adds measurable overhead on every function call system-wide, because entry and exit are both instrumented. On a busy embedded target this can visibly slow things down and even shift the timing you are trying to measure. Combining tight function filters with a sensible tracing_thresh keeps the buffer relevant and the overhead close to what plain function tracing costs.
Summary and Key Takeaways
- function tracer: a flat, low-overhead log of every function call and its caller
- function_graph tracer: nested entry/exit view with per-function duration and + / ! markers
- tracing_thresh filters by time, kernel-command-line tracing_thresh works from boot
- set_ftrace_filter includes only named functions; set_ftrace_notrace excludes them
- An empty write to either filter file always resets it back to tracing everything
Conclusion
The function and function_graph tracers turn “what is my kernel actually doing right now” from a guessing game into a readable log, without adding a single printk to your driver. Once you can filter that log by duration and by function name, Ftrace stops being noisy and becomes a precision tool you will reach for on every debugging session from here on — a core skill in any free linux device drivers course. In the next lecture, we will look at event tracing and trace-cmd, which build directly on the tracer concepts covered here.
Frequently Asked Questions
What is the difference between the function tracer and function_graph tracer?
The function tracer prints one line per call showing only the function and its caller. function_graph additionally shows entry and exit, nesting depth, and how long each function took to execute.
Why do my small helper functions not show up in an Ftrace trace?
The compiler likely inlined them into their caller, so there is no separate function symbol left to trace. Mark them noinline while you are debugging.
What do the + and ! symbols mean in function_graph output?
A plus sign marks a function that took over 10 microseconds; an exclamation mark marks one that took over 100 microseconds, flagging it as worth investigating.
How do I trace only functions slower than a certain time?
Write the threshold in microseconds to tracing_thresh, for example echo 50 > tracing_thresh, then run function_graph as usual.
What is the difference between set_ftrace_filter and set_ftrace_notrace?
set_ftrace_filter restricts tracing to only the listed functions. set_ftrace_notrace does the opposite, tracing everything except the listed functions.
How do I clear a function filter once I am done?
Write an empty line to the filter file, for example echo > set_ftrace_filter, which restores tracing to all functions.
Are TIMESTAMP values in Ftrace wall-clock time?
No. They represent seconds and microseconds since the system booted, not the current date and time.
Can I set a tracing threshold from the kernel command line?
Yes. Adding tracing_thresh=200 ftrace=function_graph to the boot command line traces every function slower than 200 microseconds starting at boot, useful for catching boot-time stalls.
Want More Free Linux Kernel Lessons?
Continue this free embedded Linux course with the next Ftrace lecture, covering event tracing and trace-cmd.
