Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Per-CPU Variables

Introduction

Per-CPU variables are one of the most powerful and widely used synchronization avoidance techniques in the Linux kernel. Instead of sharing data between CPUs and protecting it with locks, each CPU gets its own private copy of the variable. This eliminates cache-line bouncing, lock contention, and false sharing—resulting in dramatically better performance on multi-core systems.

The concept is straightforward: if you have a counter that all CPUs increment frequently, rather than using an atomic operation (which forces cache-line transfers between cores), give each CPU its own counter and sum them when you need the total.

Per-CPU variables are used extensively throughout the kernel for:

  • Network statistics (/proc/net/snmp)
  • Process scheduling counters
  • Slab allocator caches
  • RCU (Read-Copy-Update) tracking
  • Interrupt handler state
  • Kernel profiling (perf_event)

Declaring Per-CPU Variables

Compile-Time Declaration

#include <linux/percpu.h>

/* Define a per-CPU variable */
DEFINE_PER_CPU(int, my_counter);

/* With a specific alignment */
DEFINE_PER_CPU_ALIGNED(long, my_aligned_var);

/* Read-mostly per-CPU data */
DEFINE_PER_CPU_READ_MOSTLY(struct my_data, cached_data);

/* Static (file-local) per-CPU variable */
static DEFINE_PER_CPU(unsigned long, local_stat);

Dynamic Allocation

/* Allocate per-CPU memory at runtime */
int __percpu *dynamic_counter;

dynamic_counter = alloc_percpu(int);
if (!dynamic_counter)
    return -ENOMEM;

/* Use it */
this_cpu_inc(*dynamic_counter);

/* Free when done */
free_percpu(dynamic_counter);

Per-CPU Variables in Structures

struct my_device {
    int id;
    char name[32];
    struct percpu_counter rx_packets;  /* Per-CPU counter */
    int __percpu *local_buffer;        /* Per-CPU pointer */
};

/* Initialize */
dev->rx_packets = alloc_percpu_counter();
dev->local_buffer = alloc_percpu(int);

Accessing Per-CPU Variables

Basic Access Macros

/* Get the current CPU's value */
int val = get_cpu_var(my_counter);  /* Disables preemption */
put_cpu_var(my_counter);            /* Re-enables preemption */

/* Alternative (explicit preemption control) */
int val;
preempt_disable();
val = per_cpu(my_counter, smp_processor_id());
preempt_enable();

The this_cpu_* Family (Preferred)

The this_cpu_* macros are the modern, preferred way to access per-CPU variables. They disable preemption internally and generate optimized single-instruction code on many architectures:

/* Read current CPU's value */
int val = this_cpu_read(my_counter);

/* Write to current CPU's value */
this_cpu_write(my_counter, 42);

/* Atomic operations (preemption-safe, no explicit locking needed) */
this_cpu_inc(my_counter);           /* ++ */
this_cpu_dec(my_counter);           /* -- */
this_cpu_add(my_counter, 10);       /* += 10 */
this_cpu_sub(my_counter, 5);        /* -= 5 */

/* Conditional operations */
this_cpu_cmpxchg(my_counter, old, new);  /* Compare and swap */
this_cpu_xchg(my_counter, new);          /* Exchange */

/* Bit operations */
this_cpu_or(flags, MASK);
this_cpu_and(flags, ~MASK);

this_cpu_ptr — Getting the Pointer

/* Get pointer to current CPU's data */
int *ptr = this_cpu_ptr(&my_counter);
*ptr += 1;

/* Common pattern: per-CPU structures */
struct my_stats {
    unsigned long packets;
    unsigned long bytes;
    unsigned long errors;
};
DEFINE_PER_CPU(struct my_stats, net_stats);

/* Increment stats */
struct my_stats *stats = this_cpu_ptr(&net_stats);
stats->packets++;
stats->bytes += len;

Preemption Considerations

The Preemption Problem

Per-CPU access is safe only when the task is guaranteed not to be migrated to another CPU during the access. The kernel provides several levels of protection:

/* LEVEL 1: get_cpu_var / put_cpu_var
 * Disables preemption for the duration
 * Cannot sleep between get and put!
 */
get_cpu_var(my_counter)++;
put_cpu_var(my_counter);

/* LEVEL 2: preempt_disable / preempt_enable
 * More explicit, same effect
 */
preempt_disable();
this_cpu_inc(my_counter);
preempt_enable();

/* LEVEL 3: this_cpu_* macros
 * Internally handle preemption for single operations
 * Best for simple read-modify-write
 */
this_cpu_inc(my_counter);  /* Preemption-safe */

/* LEVEL 4: Disabling preemption + migration
 * For operations that access multiple CPUs' data
 */
get_online_cpus();       /* Prevent CPU hotplug */
for_each_online_cpu(cpu) {
    total += per_cpu(my_counter, cpu);
}
put_online_cpus();

What Can Go Wrong

/* WRONG: Sleeping while holding per-CPU access */
preempt_disable();
mutex_lock(&some_mutex);  /* BUG: cannot sleep with preempt disabled! */
this_cpu_inc(my_counter);
mutex_unlock(&some_mutex);
preempt_enable();

/* WRONG: Accessing another CPU's data without protection */
/* This is technically safe but semantically wrong */
val = per_cpu(my_counter, 5);  /* CPU 5's value — might be stale */

/* WRONG: Long critical section with preemption disabled */
preempt_disable();
/* ... lots of work ... */    /* BAD: other tasks on this CPU are starved */
preempt_enable();

/* RIGHT: Minimize the disabled section */
preempt_disable();
val = this_cpu_read(my_counter);
preempt_enable();
/* Process val with preemption enabled */
preempt_disable();
this_cpu_write(my_counter, new_val);
preempt_enable();

Per-CPU Counter Patterns

Simple Counter with Global Sum

DEFINE_PER_CPU(unsigned long, event_count);

/* Called frequently (lock-free, fast) */
void record_event(void) {
    this_cpu_inc(event_count);
}

/* Called rarely (expensive but that's OK) */
unsigned long get_total_events(void) {
    unsigned long total = 0;
    int cpu;
    
    for_each_possible_cpu(cpu)
        total += per_cpu(event_count, cpu);
    
    return total;
}

Using percpu_counter (Kernel Helper)

The kernel provides struct percpu_counter with batched updates for better performance:

#include <linux/percpu_counter.h>

static DEFINE_PERCPU_COUNTER(my_counter);

/* Increment (may batch, not immediately visible globally) */
percpu_counter_inc(&my_counter);
percpu_counter_dec(&my_counter);
percpu_counter_add(&my_counter, 100);

/* Get approximate count (fast, no locking) */
s64 approx = percpu_counter_read(&my_counter);

/* Get exact count (slower, sums all CPUs) */
s64 exact = percpu_counter_sum(&my_counter);

/* Check if above/below threshold */
if (percpu_counter_compare(&my_counter, limit) > 0) {
    /* Counter exceeds limit */
}

/* Initialize with batch size */
percpu_counter_init(&my_counter, 0, GFP_KERNEL);
percpu_counter_destroy(&my_counter);

Batched Per-CPU Counters

/*
 * percpu_counter uses a "batch" mechanism:
 * Each CPU maintains a local count
 * When the local count exceeds the batch threshold,
 * it's flushed to the global count (with a spinlock)
 * This reduces lock contention significantly
 */

/* Default batch size: max(32, num_online_cpus * 2) */
/* For a 128-core machine, batch = 256 */

/* Custom batch size */
percpu_counter_init(&my_counter, 0, GFP_KERNEL);
my_counter.count = 0;
my_counter.batch = 1024;  /* Larger batch = less locking */

Per-CPU Variables vs Atomics

graph TD
    subgraph "Atomic Counter (contention)"
        A1["CPU 0: atomic_inc(&amp;counter)"] --> CS1["Cache line bounces!"]
        A2["CPU 1: atomic_inc(&amp;counter)"] --> CS1
        A3["CPU 2: atomic_inc(&amp;counter)"] --> CS1
        A4["CPU 3: atomic_inc(&amp;counter)"] --> CS1
    end
    
    subgraph "Per-CPU Counter (no contention)"
        B1["CPU 0: this_cpu_inc(counter)"]
        B2["CPU 1: this_cpu_inc(counter)"]
        B3["CPU 2: this_cpu_inc(counter)"]
        B4["CPU 3: this_cpu_inc(counter)"]
        B1 --> SUM["sum: for_each_possible_cpu()"]
        B2 --> SUM
        B3 --> SUM
        B4 --> SUM
    end
    
    style CS1 fill:#e53e3e,color:#fff
    style SUM fill:#38a169,color:#fff

Performance Comparison

/* Benchmark: atomic vs per-cpu increment (1M operations) */

/* Atomic approach: ~15-80ns per operation (scales poorly) */
for (i = 0; i < 1000000; i++)
    atomic_inc(&shared_counter);

/* Per-CPU approach: ~5-10ns per operation (scales linearly) */
for (i = 0; i < 1000000; i++)
    this_cpu_inc(percpu_counter);

/* Per-CPU sum: ~1μs per CPU (negligible for infrequent reads) */
for_each_possible_cpu(cpu)
    total += per_cpu(percpu_counter, cpu);
AspectAtomicPer-CPU
Increment cost~15-80ns (cache bounce)~5-10ns (local cache)
ScalingPoor (contention)Excellent (no sharing)
Read cost~5ns (single value)~1μs * num_cpus (sum all)
Memory usage4-8 bytes4-8 bytes × num_cpus
Best forRare increments, frequent readsFrequent increments, rare reads

Real-World Usage: Network Statistics

/* From net/core/dev.c — network device statistics */
DEFINE_PER_CPU(struct pcpu_sw_netstats, pcpu_stats);

/* In the packet receive path (called millions of times per second) */
void dev_sw_netstats_rx_add(struct net_device *dev, unsigned int len) {
    struct pcpu_sw_netstats *stats = this_cpu_ptr(&dev->pcpu_stats);
    
    u64_stats_update_begin(&stats->syncp);
    stats->rx_packets++;
    stats->rx_bytes += len;
    u64_stats_update_end(&stats->syncp);
}

/* Reading stats (ethtool, /proc/net/dev) */
void dev_get_stats(struct net_device *dev) {
    struct rtnl_link_stats64 *stats = &dev->stats64;
    int cpu;
    
    for_each_possible_cpu(cpu) {
        struct pcpu_sw_netstats *pstats;
        unsigned int start;
        
        pstats = per_cpu_ptr(dev->pcpu_stats, cpu);
        do {
            start = u64_stats_fetch_begin(&pstats->syncp);
            stats->rx_packets += pstats->rx_packets;
            stats->rx_bytes += pstats->rx_bytes;
        } while (u64_stats_fetch_retry(&pstats->syncp, start));
    }
}

Sequence Counter Protection

When reading another CPU’s per-CPU data, use sequence counters to detect concurrent updates:

/* u64_stats_update_begin/end for writers (this CPU only) */
u64_stats_update_begin(&stats->syncp);
stats->value += delta;
u64_stats_update_end(&stats->syncp);

/* u64_stats_fetch_begin/retry for readers (any CPU) */
unsigned int seq;
do {
    seq = u64_stats_fetch_begin(&stats->syncp);
    val = stats->value;
} while (u64_stats_fetch_retry(&stats->syncp, seq));

Memory Layout and Cache Effects

Cache Line Alignment

/* Per-CPU data is naturally aligned to avoid false sharing */
/* Each CPU's copy is on a separate cache line */

/* Explicit alignment for performance-critical data */
struct __aligned(64) pcpu_hot_data {   /* 64 bytes = cache line */
    unsigned long events;
    unsigned long resched;
    struct task_struct *current_task;
};
DEFINE_PER_CPU_ALIGNED(struct pcpu_hot_data, hot_data);

Visualizing Per-CPU Memory Layout

graph LR
    subgraph "System Memory"
        subgraph "CPU 0's per-CPU area"
            C0A["my_counter: 42"]
            C0B["my_data: {...}"]
        end
        subgraph "CPU 1's per-CPU area"
            C1A["my_counter: 37"]
            C1B["my_data: {...}"]
        end
        subgraph "CPU 2's per-CPU area"
            C2A["my_counter: 55"]
            C2B["my_data: {...}"]
        end
    end
    
    CPU0["CPU 0"] --> C0A
    CPU1["CPU 1"] --> C1A
    CPU2["CPU 2"] --> C2A
    
    style C0A fill:#38a169,color:#fff
    style C1A fill:#3182ce,color:#fff
    style C2A fill:#d69e2e,color:#fff

CPU Hotplug Considerations

/* When a CPU goes offline, its per-CPU data persists */
/* When it comes back online, the data is still there */

/* If you need to initialize per-CPU data on hotplug: */
static int my_cpu_online(unsigned int cpu) {
    struct my_data *data = per_cpu_ptr(&my_percpu_data, cpu);
    data->initialized = true;
    data->count = 0;
    return 0;
}

static int my_cpu_offline(unsigned int cpu) {
    struct my_data *data = per_cpu_ptr(&my_percpu_data, cpu);
    /* Flush or migrate data before CPU goes away */
    migrate_data_to_another_cpu(data);
    return 0;
}

static struct cpuhp_step my_hp_states[] = {
    [CPUHP_AP_ONLINE] = {
        .name = "my:online",
        .startup = my_cpu_online,
        .teardown = my_cpu_offline,
    },
};

/* Register */
cpuhp_setup_state(CPUHP_AP_ONLINE, "my:online",
                   my_cpu_online, my_cpu_offline);

Common Pitfalls

/* PITFALL 1: Accessing per-CPU data with preemption enabled */
int val = per_cpu(my_var, smp_processor_id());
/* BUG: might be migrated between smp_processor_id() and per_cpu() */
/* FIX: use this_cpu_read(my_var) or get_cpu_var/put_cpu_var */

/* PITFALL 2: Allocating per-CPU memory in atomic context */
ptr = alloc_percpu(struct big_struct);  /* May sleep! */
/* FIX: pass GFP_ATOMIC if in interrupt context */

/* PITFALL 3: Iterating CPUs without considering hotplug */
for (cpu = 0; cpu < nr_cpu_ids; cpu++)
    total += per_cpu(count, cpu);
/* OK for most cases, but for_each_possible_cpu() is safer */

/* PITFALL 4: False sharing with adjacent variables */
struct __aligned(64) bad_layout {
    int cpu0_counter;    /* Cache line 0 */
    int cpu1_counter;    /* Same cache line! FALSE SHARING! */
};

struct __aligned(64) good_layout {
    int __percpu *counter;  /* Each CPU's copy on its own line */
};

References

Per-CPU Allocator Internals

First-Chunk Allocation

During boot, the kernel allocates the first per-CPU chunk from a contiguous memory region. Subsequent allocations use dynamic chunks:

flowchart TD
    BOOT[Boot: Reserve per-CPU area] --> FIRST[First chunk: static __percpu data]
    FIRST --> DYNAMIC["Dynamic chunks: alloc per-cpu"]
    DYNAMIC --> VMALLOC[vmalloc area for dynamic chunks]

Per-CPU Memory Layout

# View per-CPU memory layout
cat /sys/kernel/debug/percpu_alloc
# Shows chunk statistics and allocations

# View per-CPU data section sizes
cat /proc/vmstat | grep percpu
# nr_slab_percpu  0

# Check per-CPU memory usage
cat /proc/meminfo | grep Percpu
# Percpu:          1234 kB

Per-CPU Reference Counting

The kernel provides percpu_ref for high-performance reference counting that avoids cache-line bouncing:

#include <linux/percpu-refcount.h>

static DEFINE_PERCPU_REFCOUNT(my_ref);

/* Initialize */
percpu_ref_init(&my_ref, my_release, 0, GFP_KERNEL);

/* Increment (fast, per-CPU) */
percpu_ref_get(&my_ref);

/* Decrement (fast, per-CPU) */
percpu_ref_put(&my_ref);

/* Kill: switch to atomic mode, wait for all refs */
percpu_ref_kill(&my_ref);

/* Check if alive */
if (percpu_ref_is_dying(&my_ref))
    return -ENODEV;

percpu_ref is used extensively in the block layer (request queues) and cgroup subsystem for high-performance reference counting.

percpu_ref Internals

stateDiagram-v2
    [*] --> PerCPU: percpu_ref_init()
    PerCPU --> Atomic: percpu_ref_kill()
    Atomic --> [*]: refcount reaches 0

    state PerCPU {
        [*] --> FastPath: get/put = per-CPU increment/decrement
        FastPath --> [*]
    }

    state Atomic {
        [*] --> SlowPath: get/put = atomic_inc/dec
        SlowPath --> [*]
    }

Per-CPU Work Queues

The kernel’s workqueue subsystem uses per-CPU work queues for cpu-bound work:

#include <linux/workqueue.h>

/* System per-CPU workqueue (bound to each CPU) */
static DECLARE_WORK(my_work, my_work_fn);

/* Schedule on current CPU's workqueue */
schedule_work(&my_work);

/* Schedule on specific CPU's workqueue */
queue_work_on(cpu, system_wq, &my_work);

/* Cancel */
cancel_work_sync(&my_work);

Debugging Per-CPU Issues

CONFIG_DEBUG_PER_CPU_MAPS

Enables runtime checks for per-CPU map consistency:

# Enable in kernel config
CONFIG_DEBUG_PER_CPU_MAPS=y

# Will warn on:
# - Accessing per-CPU data for offline CPUs
# - Incorrect CPU ID in per_cpu()
# - Per-CPU allocation failures

Common Debugging Patterns

# Check per-CPU data consistency
# Sum all CPUs' values, compare with expected total

# Use ftrace to track per-CPU access patterns
echo 1 > /sys/kernel/debug/tracing/events/preemptirq/preempt_disable/enable
echo 1 > /sys/kernel/debug/tracing/events/preemptirq/preempt_enable/enable

# Check for long preempt-disabled sections
cat /sys/kernel/debug/tracing/trace_pipe | grep "preempt_disable"

Per-CPU vs Other Approaches

ApproachIncrementRead TotalMemoryBest For
Atomic counter~15-80ns~5ns4-8 bytesRare increments, frequent reads
Per-CPU counter~5-10ns~1μs × CPUs4-8 × CPUsFrequent increments, rare reads
percpu_counter~5-10ns~1μs × CPUs4-8 × CPUsBatched updates, approximate reads
percpu_ref~5-10ns~1μs × CPUs4-8 × CPUsReference counting

Per-CPU Variables in the Kernel: Case Studies

Network Device Statistics

Every network device maintains per-CPU statistics to avoid contention:

/* net/core/dev.c */
DEFINE_PER_CPU(struct pcpu_sw_netstats, pcpu_stats);

/* In the hot path (packet receive) */
void dev_sw_netstats_rx_add(struct net_device *dev, unsigned int len) {
    struct pcpu_sw_netstats *stats = this_cpu_ptr(&dev->pcpu_stats);

    u64_stats_update_begin(&stats->syncp);
    stats->rx_packets++;
    stats->rx_bytes += len;
    u64_stats_update_end(&stats->syncp);
}

/* Reading stats (cold path) */
void dev_get_stats(struct net_device *dev,
                   struct rtnl_link_stats64 *stats) {
    int cpu;
    for_each_possible_cpu(cpu) {
        struct pcpu_sw_netstats *pstats;
        unsigned int start;

        pstats = per_cpu_ptr(dev->pcpu_stats, cpu);
        do {
            start = u64_stats_fetch_begin(&pstats->syncp);
            stats->rx_packets += pstats->rx_packets;
            stats->rx_bytes += pstats->rx_bytes;
        } while (u64_stats_fetch_retry(&pstats->syncp, start));
    }
}

Slab Allocator (SLUB)

The SLUB allocator uses per-CPU caches for fast allocation:

/* mm/slub.c */
struct kmem_cache_cpu {
    void **freelist;     /* Pointer to first free object */
    unsigned long tid;   /* Transaction ID */
    struct page *page;   /* Current page */
    struct page *partial; /* Partial pages */
};

/* Per-CPU slab cache */
DEFINE_PER_CPU(struct kmem_cache_cpu *, kmem_cache_cpu);

/* Fast path: allocate from per-CPU freelist */
static void *slab_alloc(struct kmem_cache *s, gfp_t gfpflags) {
    struct kmem_cache_cpu *c = this_cpu_ptr(s->cpu_slab);
    void *object;

    object = c->freelist;
    if (likely(object)) {
        c->freelist = next_object(object);
        return object;
    }
    /* Slow path: refill from partial list */
    return __slab_alloc(s, gfpflags);
}

RCU (Read-Copy-Update)

RCU uses per-CPU data for grace period tracking:

/* kernel/rcu/tree.c */
DEFINE_PER_CPU(struct rcu_data, rcu_data);

struct rcu_data {
    unsigned long gp_seq;        /* Grace period sequence */
    unsigned long gp_seq_needed; /* When to report quiescent state */
    struct rcu_head *nxtlist;    /* List of pending callbacks */
    /* ... */
};

/* Each CPU reports quiescent states independently */
void rcu_sched_clock_irq(int user) {
    struct rcu_data *rdp = this_cpu_ptr(&rcu_data);

    if (user || rcu_is_cpu_rrupt_from_idle()) {
        /* Report quiescent state */
        rcu_report_qs_rdp(rdp);
    }
}

Per-CPU Variable Lifecycle

Initialization

flowchart TD
    BOOT["Boot: per-CPU start section"] --> STATIC["Static per-CPU variables placed in per-CPU data"]
    STATIC --> ALLOC["setup_per_cpu_areas allocates per-CPU memory"]
    ALLOC --> COPY["Copy per-CPU data to each CPU area"]
    COPY --> DYNAMIC["Dynamic per-CPU allocation from vmalloc"]

CPU Hotplug

/* When a CPU comes online */
static int percpu_cpu_online(unsigned int cpu) {
    /* Initialize per-CPU data for this CPU */
    struct my_data *data = per_cpu_ptr(&my_percpu_data, cpu);
    data->count = 0;
    data->active = true;
    return 0;
}

/* When a CPU goes offline */
static int percpu_cpu_offline(unsigned int cpu) {
    /* Flush or migrate data */
    struct my_data *data = per_cpu_ptr(&my_percpu_data, cpu);
    migrate_data_to_another_cpu(data);
    return 0;
}