Per-CPU variables are one of the Linux kernel’s core scalability tools, and they are the natural, more elegant answer to the false-sharing problem covered in the previous lecture of this free Linux kernel development course. Instead of protecting one shared variable with a lock, the kernel simply gives every CPU its own private copy of the data. No lock, no cacheline contention, no waiting. This lecture explains per-CPU variables from first principles, walks through the modern kernel API (updated for kernel 6.12 LTS through the current 7.1 stable series), and gives you a working kernel module example.
Why Per-CPU Variables Exist
In the previous lecture of this free Linux device drivers course, you saw that when multiple cores write to one shared variable, cache coherency traffic can seriously hurt performance — even when there’s no logical data race. Padding fixes the layout problem, but it doesn’t remove the underlying issue: it’s still one logical counter that every core has to agree on.
Per-CPU variables take a different approach entirely. Instead of one shared counter, the kernel allocates one independent copy of the variable for every CPU on the system. A thread running on CPU 2 only ever touches CPU 2’s copy. There is no sharing at all, so there is nothing to invalidate, nothing to coherency-check across cores, and no lock required. This is roughly the kernel-space equivalent of thread-local storage (__thread / thread_local) in user-space C.
Each core updates only its own copy of rx_count. Reading a global total means summing all copies, only when you actually need the aggregate.
Declaring and Using a Per-CPU Variable
The kernel provides macros to declare per-CPU data, both statically at compile time and dynamically at runtime. A statically declared counter looks like this:
#include <linux/percpu.h>
DEFINE_PER_CPU(unsigned long, rx_packet_count);
This single line allocates one unsigned long for every possible CPU on the system, laid out so that each CPU’s copy lives in memory local to that CPU where the platform supports it (NUMA-aware placement). To safely increment the current CPU’s copy, modern kernel code uses the this_cpu_* family of operations, which are guaranteed atomic with respect to the current CPU without needing to explicitly disable preemption yourself:
/* modern, preferred style (kernel 6.x and current) */
this_cpu_inc(rx_packet_count);
unsigned long total_here = this_cpu_read(rx_packet_count);
The Older get_cpu_var / put_cpu_var Pattern
Older kernel code (and a lot of textbook material) uses a different, more manual pattern:
get_cpu_var(rx_packet_count)++;
put_cpu_var(rx_packet_count);
get_cpu_var() disables kernel preemption, hands you a reference to the local CPU’s copy, and put_cpu_var() re-enables preemption once you’re done. This works, but it’s easy to get wrong — forgetting put_cpu_var(), or doing too much work (like taking a sleeping lock) between the two calls, is a real bug class. This is exactly why current kernel code favors the this_cpu_* operations instead: they fold the “disable preemption, touch it, re-enable” dance into a single call, and the kernel documentation now marks the manual get_cpu_var pattern as something to avoid in new code unless you specifically need the CPU pinned across several operations.
| API Style | Preemption Handling | Recommended Today? |
|---|---|---|
get_cpu_var() / put_cpu_var() |
Manual — you must bracket every access | Legacy; avoid in new code |
this_cpu_read/write/inc/add() |
Handled internally, single call | Yes — current best practice |
percpu_counter API |
Fully managed, with batched global sum | Yes — for counters read globally and often |
Dynamic Per-CPU Allocation
For per-CPU data that isn’t known at compile time (for instance, per-device statistics allocated when a driver probes hardware), the kernel provides dynamic allocation:
struct driver_stats __percpu *stats;
stats = alloc_percpu(struct driver_stats);
if (!stats)
return -ENOMEM;
/* on the fast path, from any CPU: */
this_cpu_inc(stats->rx_packets);
/* on cleanup: */
free_percpu(stats);
The __percpu annotation is a sparse/compiler hint that documents “this pointer refers to per-CPU memory, don’t dereference it directly” — it helps catch a whole class of bugs at build time where someone accidentally treats per-CPU memory like a normal shared pointer.
A Minimal Working Kernel Module Example
Here is a small, original example module demonstrating a static per-CPU counter incremented from a simple kernel timer, and read back on module removal.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/percpu.h>
#include <linux/smp.h>
static DEFINE_PER_CPU(unsigned long, tick_count);
static void bump_counter(void *unused)
{
this_cpu_inc(tick_count);
}
static int __init percpu_demo_init(void)
{
/* run bump_counter once on every online CPU */
on_each_cpu(bump_counter, NULL, 1);
pr_info("percpu_demo: loaded, one tick recorded per CPU\n");
return 0;
}
static void __exit percpu_demo_exit(void)
{
int cpu;
unsigned long total = 0;
for_each_online_cpu(cpu)
total += per_cpu(tick_count, cpu);
pr_info("percpu_demo: total ticks across all CPUs = %lu\n", total);
}
module_init(percpu_demo_init);
module_exit(percpu_demo_exit);
MODULE_LICENSE("GPL");
Notice the pattern in percpu_demo_exit(): reading the aggregate total means explicitly looping over every online CPU with per_cpu(variable, cpu) and summing. There is no automatic “global value” — that is the whole point of per-CPU data, and it’s also the one place many beginners get confused.
The percpu_counter API: When You Need Fast Reads Too
Manually summing across every CPU on every read gets expensive if you need the aggregate frequently (for example, checking free disk blocks on every write). For that pattern, the kernel provides percpu_counter, which keeps a per-CPU delta plus an approximate global counter that’s synced in batches, giving you fast increments and reasonably fast reads:
struct percpu_counter blocks_free;
percpu_counter_init(&blocks_free, 1000, GFP_KERNEL);
percpu_counter_add(&blocks_free, -1); /* fast, per-CPU */
s64 approx_free = percpu_counter_read(&blocks_free); /* fast, approximate */
s64 exact_free = percpu_counter_sum(&blocks_free); /* slower, exact */
percpu_counter_destroy(&blocks_free);
This exact structure is used throughout the kernel for filesystem free-space accounting and network statistics, precisely because those counters are updated extremely often but only need to be read exactly on rare, non-critical paths.
Per-CPU Variables and PREEMPT_RT
Since the PREEMPT_RT real-time preemption patches were merged into mainline starting with kernel 6.12, it’s worth understanding how per-CPU data interacts with a fully preemptible kernel. On a PREEMPT_RT kernel, code that used to rely on implicitly disabling preemption (the older get_cpu_var() pattern) needs extra care, because even “atomic” sections can be preempted by higher-priority real-time tasks in ways that were not possible on a traditional kernel. The this_cpu_* operations remain correct on PREEMPT_RT because they don’t assume the CPU stays fixed across multiple steps — another reason the modern API is the safer default for any driver or module you write today, embedded or otherwise.
Common Mistakes and Troubleshooting
Dereferencing a __percpu pointer directly is undefined behavior. Always go through this_cpu_*, per_cpu(), or per_cpu_ptr() accessors.
Every per-CPU variable is duplicated once per possible CPU. On a system configured for a large number of CPUs, a large per-CPU structure wastes real memory. Keep per-CPU data small.
A plain per-CPU counter has no built-in “sum across all cores” — you must loop and add manually, or use percpu_counter if you need frequent, cheap reads.
Best Practices
- Prefer
this_cpu_*operations over the olderget_cpu_var/put_cpu_varpattern in new code. - Use dynamic
alloc_percpu()for per-device or per-instance data; staticDEFINE_PER_CPU()for truly global counters. - Use
percpu_counterwhen you need both fast writes and reasonably fast aggregate reads. - Keep per-CPU structures small and cacheline-friendly — they are still subject to false sharing internally if you pack multiple hot sub-fields together.
- Always pair
alloc_percpu()withfree_percpu()to avoid per-CPU memory leaks.
Performance and Security Considerations
Per-CPU variables scale close to linearly with core count for the write path, since there’s no coherency traffic at all — this is precisely why they are the kernel’s go-to tool for hot counters on systems with dozens or hundreds of cores. The trade-off is memory: every additional CPU means another copy. From a security and correctness standpoint, remember that per-CPU data protects against races between CPUs, not against races between a normal task and an interrupt handler running on the very same CPU; if your per-CPU variable is also touched from interrupt context on that CPU, you still need local_irq_save()/local_irq_restore() or the interrupt-safe variants of the this_cpu_* operations.
- Per-CPU variables give every core its own private copy of hot data, eliminating cache coherency overhead entirely.
- Modern kernel code favors
this_cpu_*operations over the olderget_cpu_var/put_cpu_varpattern. - Use
percpu_counterwhen you need both cheap increments and reasonably fast global reads. - Per-CPU data protects across cores, not automatically across interrupt context on the same core.
Conclusion
Per-CPU variables are the cleanest, most scalable answer to the cache-contention problems introduced in the previous lecture of this free Linux kernel development course. Once you’re comfortable with DEFINE_PER_CPU(), this_cpu_* operations, and the percpu_counter API, you have the same tool the kernel itself relies on for scheduler statistics, networking counters, and memory-management bookkeeping across every architecture Linux supports. Practice by converting a small shared counter in your own kernel module into a per-CPU counter and measuring the difference with perf stat.
Frequently Asked Questions
They’re conceptually similar — both give each execution context a private copy of data — but per-CPU variables are indexed by CPU, not by thread. A thread that migrates between CPUs will see different per-CPU copies unless it’s pinned to one core.
No. this_cpu_inc() is safe with respect to other CPUs by design. You only need extra protection (like disabling interrupts) if the same per-CPU variable is also touched from an interrupt handler on that same CPU.
Loop with for_each_online_cpu() and sum using per_cpu(variable, cpu), or use the percpu_counter API if you need frequent, fast aggregate reads.
Per-CPU variables as described here are a Linux kernel-space mechanism. In user space, the closest equivalent is thread-local storage (__thread or C11 thread_local), though it’s indexed by thread rather than by CPU.
this_cpu_* operations are less error-prone (no matching put call to forget), and they remain correct on a fully preemptible PREEMPT_RT kernel, where the older manual pattern needs extra care.
The kernel allocates space for possible CPUs, and modern kernels are efficient about this, but very large per-CPU structures on systems configured for a high CPU count can still add up. Keep per-CPU data compact.
This lecture is part of EmbeddedPathashala’s free Linux kernel development course, free Linux device drivers course, and free embedded systems course.
Explore All Free Courses
2 Comments