Profiling Linux Systems with top
Free Linux Kernel Development Course — Chapter 13: Profiling and Tracing, Part 1
Interactive, source-level debugging shows you what one function is doing at one moment. It is a microscope. Profiling is the opposite instrument — a wide-angle lens that shows you where an entire system is spending its CPU cycles, right now, across every process and every core. This lecture is the first stop in our free embedded Linux course’s profiling chapter, and it starts exactly where every real performance investigation should start: with top.
If you have followed our free linux kernel development course from the toolchain through debugging with GDB, you already have every prerequisite you need to get real value out of this lecture.
What You Will Learn
Prerequisites
You should be comfortable building and booting a custom embedded Linux image (our free linux device drivers course toolchain and rootfs chapters cover this), and you should know your way around a shell. No kernel module code is required for this particular lecture — we’ll write our own small C programs instead.
Why Measuring a System Changes It
Every profiling tool borrows resources from the very system it is trying to describe. A sampling profiler steals CPU cycles to take samples. A tracer writes buffers to memory or disk. Even a lightweight tool like top allocates memory and reads /proc on every refresh. None of this is free, and that has a name in engineering: the observer effect — the act of measuring a quantity disturbs the quantity you are measuring.
This matters practically. If you profile a system that is idling, under synthetic load, with extra logging turned on, or running a debug kernel, you are measuring that variant of the system — not the one your customers will run. Two rules keep profiling data trustworthy:
- Measure on the actual target hardware, not a desktop simulator or emulator, whenever the numbers matter.
- Measure release builds with a realistic workload and as few extra background services as you can get away with.
Notice that top is not a profiler in the strict sense — it never tells you which line of code is expensive. Its job is triage: it points you at which process or subsystem deserves a real profiler’s attention, in seconds, with zero setup.
Symbol Tables and Compile Flags
Before reaching for perf, Ftrace, or LTTng later in this chapter, it helps to understand why profiling tools are picky about how your software was built:
| Requirement | Why it matters | Typical flag / option |
|---|---|---|
| Debug symbols | Translate raw addresses back into function names and source lines | -g, kernel CONFIG_DEBUG_INFO=y |
| Frame pointers | Needed to unwind the call stack and produce call graphs | -fno-omit-frame-pointer |
| Lower optimization | Keeps addresses mapped cleanly to source lines; heavy inlining confuses attribution | -Og instead of -O2/-O3 for the code you’re actively profiling |
| Instrumentation hooks | Some tracers need markers compiled in to capture events | kernel tracepoints, Ftrace/LTTng config options |
Every one of these changes the binary you ship. The rule of thumb: the more you modify a system to make it observable, the less confident you can be that the measurement still reflects production behaviour. Add debug symbols and frame pointers where you must, but do it deliberately, and re-measure the unmodified build once you’ve found the hotspot.
Reading the top Summary Line
top ships in two common flavours on embedded targets: the compact BusyBox version, and the fuller procps build (also present in Buildroot and the Yocto Project). Both put a CPU summary line near the top of the screen — the second line in BusyBox, the third line in procps. htop is a popular third option with a friendlier interface, built on the same underlying counters.
| procps field | BusyBox field | Meaning |
|---|---|---|
| us | usr | User-space time at the default priority |
| sy | sys | Time spent executing kernel code |
| ni | nic | User-space time at a non-default (niced) priority |
| id | idle | CPU sitting idle |
| wa | io | Waiting on I/O to complete |
| hi | irq | Servicing hardware interrupts |
| si | sirq | Servicing software interrupts (softirqs) |
| st | — | Steal time — CPU taken by a hypervisor; only meaningful in a VM |
Reading this line correctly is the whole trick. A system spending most of its time in us/usr is user-space bound — look at the top process. A system spending most of its time in sy/sys is kernel-bound — the culprit is usually a syscall-heavy application, an interrupt storm, or a driver doing more work than it should.
Hands-On: Two Deliberately Different Workloads
Rather than reusing a stock example, let’s build two tiny, original programs — ep_cpuburn and ep_syscall_hammer — that each push a system into one of the two states above, so you can see the difference in your own top output.
1. A user-space-bound workload. This program does nothing but arithmetic in a tight loop — no syscalls, no I/O — so every cycle it burns should show up as us/usr time.
/* ep_cpuburn.c — pure user-space CPU load, no syscalls in the hot loop */
#include <stdio.h>
int main(void)
{
volatile unsigned long acc = 0;
for (unsigned long i = 0; i < 4000000000UL; i++)
acc += i ^ (i << 3);
printf("done, acc=%lu\n", acc);
return 0;
}
$ gcc -O0 -o ep_cpuburn ep_cpuburn.c
$ ./ep_cpuburn &
$ top
Mem: 61200K used, 441900K free, 40K shrd, 3100K buff, 33800K cached
CPU: 97% usr 2% sys 0% nic 0% idle 0% io 1% irq 0% sirq
Load average: 0.62 0.20 0.07 2/50 210
PID PPID USER STAT VSZ %VSZ %CPU COMMAND
210 188 root R 1868 0% 97% ep_cpuburn
Almost the entire CPU budget is usr, and ep_cpuburn is the one line responsible. This is exactly the case where the next lecture’s tool, perf, should be pointed at ep_cpuburn — not at the kernel.
2. A kernel-bound workload. This one repeatedly asks the kernel to do something trivial but expensive to enter and exit: a syscall loop with no real work in user space.
/* ep_syscall_hammer.c — spends its time crossing into the kernel, not computing */
#include <unistd.h>
int main(void)
{
for (long i = 0; i < 200000000L; i++)
getppid(); /* cheap syscall, called relentlessly */
return 0;
}
$ gcc -O0 -o ep_syscall_hammer ep_syscall_hammer.c
$ ./ep_syscall_hammer &
$ top
Mem: 13500K used, 489500K free, 40K shrd, 0K buff, 2700K cached
CPU: 3% usr 96% sys 0% nic 0% idle 0% io 1% irq 0% sirq
Load average: 0.55 0.14 0.05 2/48 214
PID PPID USER STAT VSZ %VSZ %CPU COMMAND
214 188 root R 1912 0% 96% ep_syscall_hammer
Here almost all the time is sys, even though ep_syscall_hammer is still the top line. Profiling this binary’s own code would tell you almost nothing useful — the real work is happening on the kernel side of every getppid() call, which is a job for a whole-system profile with perf, or for Ftrace/LTTng if you need the exact sequence of kernel events.
Per-thread and per-CPU views. By default top aggregates all threads of a process into one line and sums usage across every core. Press H in either BusyBox or procps top to break a multi-threaded process out by thread — essential once you suspect one thread is starving the others. In procps top, press 1 to toggle a per-CPU breakdown, which is how you catch a single-core bottleneck hiding inside a healthy-looking system-wide average.
Common Mistakes and Troubleshooting
- Profiling a debug build and trusting the numbers. Debug builds change timing, sometimes drastically. Confirm a suspected hotspot against a release build before acting on it.
- Ignoring
wa/io. High I/O-wait looks like idle time at a glance but usually means a slow storage device or network call, not a free CPU. - Reading process-level CPU% as core-level CPU%. On a multi-core target, a process pinned to one core can show 100% while seven other cores sit idle — always sanity-check against the per-CPU view.
- Assuming
topcan diagnose lockups or memory corruption. It can’t; those need Valgrind/Helgrind or the memory-diagnosis techniques from earlier in this course.
Best Practices
- Always start a performance investigation with
topbefore reaching for a heavier tool — it costs almost nothing and immediately narrows the search. - Re-run your test with as few background services as possible so the numbers aren’t diluted by unrelated processes.
- Record a baseline summary line when the system is healthy, so a regression shows up as an obvious deviation later.
- Performance consideration:
top‘s own refresh interval and rendering cost is usually negligible, but on very constrained targets, run it with a longer delay (top -d 5) to reduce its own footprint while you observe. - Security consideration:
topand/procreveal command lines, memory maps, and process ownership; on shared or multi-tenant embedded systems restrict who can run it, since this is a real information-disclosure surface.
Summary and Key Takeaways
- Every measurement has a cost — the observer effect — so measure on-target, with release builds, and minimal extra services.
- Debug symbols, frame pointers, and lower optimization make profiling data readable, but each one moves you further from the production binary.
topis a triage tool: its summary line tells you whether to point a real profiler at a user-space process or at the kernel.us/usr-heavy → profile the process;sy/sys-heavy → profile the whole system and drill into the kernel.- Use H for per-thread detail and 1 (procps) for per-CPU detail before drawing conclusions from an aggregated view.
Conclusion
This lecture is the entry point to the rest of our free linux kernel development course’s profiling chapter. Once top has told you where to look — a single user-space process, several coupled processes, or the kernel itself — the next lectures take you deeper with perf for CPU profiling, then Ftrace and LTTng for detailed kernel-level tracing, and strace for watching the exact system calls a program makes. Master this first triage step and every tool after it will be aimed at the right target from the start.
FAQ
Is top itself a profiler?
Not in the strict sense — it doesn’t attribute time to functions or lines of code. It’s a triage tool that tells you which process or which category of CPU time (user vs. kernel) deserves a real profiler.
Why does profiling change the thing being measured?
Because every observation tool consumes CPU cycles, memory, or I/O of its own — this is the observer effect, and it means there is no truly free measurement.
Should I always build with debug symbols before profiling?
Only for the component you’re actively investigating, and only temporarily. Debug symbols and lower optimization change timing, so confirm your findings against a normal release build afterward.
What’s the difference between BusyBox top and procps top?
BusyBox top is smaller and simpler, with the CPU summary on line two. procps top (used by Buildroot and Yocto) is fuller-featured, puts the summary on line three, and adds interactive per-CPU and per-thread views.
My top shows high sy time — what should I check next?
A high sys percentage points at the kernel, not the user-space binary. Profile the whole system with perf next, then use Ftrace or LTTng if you need the exact sequence of kernel events.
Can top detect memory leaks or race conditions?
No. Use Valgrind for memory leaks and Valgrind with the Helgrind plug-in for thread races and lockups.
What does load average actually tell me?
It’s the average number of processes that were runnable or waiting on uninterruptible I/O over the last 1, 5, and 15 minutes — useful for spotting trends, but it doesn’t replace the CPU summary line for real-time diagnosis.
Is htop better than top for embedded targets?
htop is friendlier and adds visual per-core bars, but it’s a heavier binary and not always available in minimal root filesystems — BusyBox top remains the safer default for constrained targets.
Continue the Free Linux Kernel Development Course
Next up: whole-system and per-process CPU profiling with perf. Explore more free embedded systems course material, the free linux device drivers course, and the rest of our free linux development course lectures.
Browse the Full Course Debugging with GDB (Previous Chapter)
2 Comments