多核上最大的扩展瓶颈是共享数据的锁争抢。per-CPU 变量让每个 CPU 有一份独立副本——读写不需要锁、不需要原子操作、不需要 cache 行 bouncing。
一、先看图
flowchart LR
CPU0[CPU 0] --> V0[per-CPU 副本 0<br/>count=42]
CPU1[CPU 1] --> V1[per-CPU 副本 1<br/>count=37]
CPU2[CPU 2] --> V2[per-CPU 副本 2<br/>count=51]
CPU3[CPU 3] --> V3[per-CPU 副本 3<br/>count=28]
SUM[需要全局值时<br/>sum_over_cpus] --> CPU0
SUM --> CPU1
SUM --> CPU2
SUM --> CPU3
classDef cpu fill:#388bfd22,stroke:#388bfd,color:#adbac7;
class CPU0,CPU1,CPU2,CPU3,V0,V1,V2,V3 cpu
二、静态 per-CPU
编译时声明:
DEFINE_PER_CPU(int, my_counter);
// 使用
this_cpu_inc(my_counter); // 当前 CPU 的副本 +1
int val = this_cpu_read(my_counter);
this_cpu_write(my_counter, 0);
// 读其他 CPU
int remote = per_cpu(my_counter, cpu_id);
// 全局汇总
int total = 0;
for_each_possible_cpu(cpu)
total += per_cpu(my_counter, cpu);布局
链接器把所有 per-CPU 变量放到 .data..percpu
section。启动时为每个 CPU 复制一份。
三、动态 per-CPU
运行时分配:
int __percpu *p = alloc_percpu(int);
this_cpu_inc(*p);
free_percpu(p);底层用 pcpu_alloc:从 vmalloc 区域的 per-CPU
chunk 分配。chunk 按 CPU 偏移排布。
四、this_cpu_* 操作
这组宏利用 x86 %gs 段前缀(arm64 用
TPIDR_EL1)直接偏移到当前 CPU 区域:
this_cpu_read(var)
this_cpu_write(var, val)
this_cpu_add(var, delta)
this_cpu_inc(var)
this_cpu_dec(var)
this_cpu_cmpxchg(var, old, new)
this_cpu_xchg(var, new)单条指令完成(如 addl %gs:offset, $1)——比
get_cpu() + per_cpu() + put_cpu()
快且不禁抢占。
raw_cpu_* vs this_cpu_*
this_cpu_*:含 preemption check(debug 下)raw_cpu_*:无检查,在已知不会迁移时用
五、preempt 与 per-CPU
关键问题:如果你在读 per-CPU 变量的途中被抢占到另一个 CPU 怎么办?
this_cpu_*单指令操作本身是原子的(不会跨 CPU)- 多步操作(读-改-写多条语句)需要禁抢占:
preempt_disable();
val = __this_cpu_read(counter);
val += delta;
__this_cpu_write(counter, val);
preempt_enable();或直接用 this_cpu_add 一步到位。
六、percpu_counter
通用的”近似全局计数器”——热路径 per-CPU 累积,冷路径汇总:
struct percpu_counter fbc;
percpu_counter_init(&fbc, 0, GFP_KERNEL);
percpu_counter_add(&fbc, 1); // 热路径
s64 val = percpu_counter_sum(&fbc); // 冷路径(精确)
s64 approx = percpu_counter_read(&fbc); // 不精确但快阈值机制:本地累积超过 batch(默认
32)时同步到全局 count。
用途:文件系统空闲块计数、nr_dentry、nr_unused。
七、per-CPU 的变种
7.1 per-CPU refcount
percpu_ref:热路径 per-CPU 计数 →
关闭时切换到 atomic_t 集中计数。
percpu_ref_init(&ref, release_fn, 0, GFP_KERNEL);
percpu_ref_get(&ref); // per-CPU 模式
percpu_ref_put(&ref);
percpu_ref_kill(&ref); // 切 atomic 模式block I/O、cgroup 控制器大量使用。
7.2 local_t
per-CPU 整数但允许在中断/NMI 中原子更新:
DEFINE_PER_CPU(local_t, events);
local_inc(&this_cpu(events));用于 perf events 计数。
八、per-CPU 陷阱
A:读其他 CPU 的 per-CPU 变量
per_cpu(var, remote_cpu)
可以读但不要写(除非有其他同步)。per-CPU
的语义是”只有 owner CPU 写”。
B:per-CPU 变量 + preempt_enable 之间写
多步操作不用 this_cpu_* 且不禁抢占 →
可能看到半更新。
C:per-CPU 加 memcg 不感知 per-CPU
内存可能在某些 cgroup 统计之外。SLAB_ACCOUNT
只计 slab 对象。
九、观察
# 全系统 per-CPU 内存
cat /proc/meminfo | grep Percpu
# Percpu: 12345 kB
# per-CPU chunk 详情
cat /proc/percpu_alloc
# 看 percpu_counter
cat /proc/sys/fs/dentry-state # nr_dentry 用 percpu_counter十、小结
- per-CPU 消除共享 = 消除锁争抢
this_cpu_*单指令完成,快且安全- percpu_counter 在热路径本地累积、冷路径汇总
- percpu_ref 用于高频引用计数
- 多步操作需禁抢占或用单条 this_cpu_* 指令
参考文献
- Christoph Lameter, “per-CPU data.” 2014
include/linux/percpu.h、mm/percpu.cinclude/linux/percpu-defs.hDocumentation/core-api/this_cpu_ops.rst- Tejun Heo, “percpu allocator rework.” 2009
工具
/proc/meminfo(Percpu 行)perf stat -e cache-misses(per-CPU 减少 cache bounce)bpftraceper-CPU map
上一篇:vmalloc/kmap/ioremap 下一篇:内核内存调试
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。
【操作系统百科】用户态分配器
glibc malloc、tcmalloc、jemalloc、mimalloc 各有哲学。本文讲 arena、thread cache、size class、madvise 返还策略、碎片与 RSS 膨胀、如何根据负载选分配器。