Ftrace Function Tracers Explained-Free Linux Device Drivers Training Online

Ftrace Function Tracers Explained

A free Linux kernel development course lesson on tracing function calls with Ftrace

2 Tracers Covered
1 Original Kernel Module
Latest Stable Kernel
If you have ever wondered exactly which kernel function called which other function, in what order, and how long each one took to run, Ftrace has already answered that question for you thousands of times — you just have not asked it yet. Ftrace is the kernel’s own built-in function tracer, and in this lecture, part of our free linux kernel development course, we turn it on, point it at a real driver, and read what it tells us.This lecture continues our Ftrace series. If you have not set up tracefs and browsed available_tracers yet, go back one lecture first — here we assume tracefs is already mounted at /sys/kernel/tracing.

function tracer
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:

Function Tracer Line Anatomy
TASK-PID      bash-1842
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    |  }
function_graph Call Nesting
ep_stage_commit() {   <- opening brace, function entered
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.

 

Leave a Reply

Your email address will not be published. Required fields are marked *