Linux 内核本地原子操作(local_t)深入解析:语义、架构实现与 per-CPU 计数实践
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
本文以 Linux 内核官方文档 Documentation/core-api/local_ops.rst 为主体,结合仓库内asm-generic、x86、PowerPC、MIPS 等架构的local.h实现与 per-CPU 宏源码,系统讲解内核本地原子操作local_t的设计初衷、原子性边界、跨 CPU 读取的内存序约束,以及如何基于DEFINE_PER_CPU(local_t, ...)构建高性能的 per-CPU 计数器。读完本文,你将掌握local_t的正确使用姿势、架构移植方法、与this_cpu操作的取舍,以及带内存屏障的远程读取同步方案。
一、为什么需要本地原子操作
本地原子操作(Local Atomic Operations)是内核提供的一类"仅对拥有数据的那个 CPU 保证原子性"的操作原语。它的设计目标是提供快速且高度可重入的 per-CPU 计数器:通过去掉标准原子操作(atomic_t)中用于跨 CPU 同步的 LOCK 前缀和内存屏障,将性能开销降到最低(见 Documentation/core-api/local_ops.rst)。
这类快速 per-CPU 原子计数器在多种场景下都很有价值:
- 不需要关闭中断来防护中断处理程序:由于只有本 CPU 写该变量,普通的中断/软中断/NMI 上下文写入不会与其他 CPU 产生竞争;
- 允许在 NMI 处理器中获得一致的计数:对跟踪(tracing)与各类性能监控计数器尤其有用。
需要特别强调的是:本地原子操作只对"拥有数据的 CPU"保证变量修改的原子性。因此使用时必须确保:
- 只有一个 CPU 写
local_t数据——通常通过 per-CPU 变量并保证在关抢占的上下文(preemption safe context)中修改来实现; - 任何 CPU 都可以读
local_t数据,但读到的内容相对于拥有者 CPU 的其他内存写操作而言,可能表现为"乱序"(out of order)。
typedef struct { atomic_long_t a; } local_t;这个定义来自 include/asm-generic/local.h:local_t是一个不透明类型,将atomic_long_t嵌入结构体,刻意让"从该类型强制转换为long"在编译期失败,避免使用者把局部原子操作误当成普通 long 操作。
二、为某架构实现 local_t 的方法
实现本地原子操作并不复杂:只需保留标准原子操作的 UP(单处理器)变体。具体而言,通常是去掉 LOCK 前缀(i386 和 x86_64 上)以及一切 SMP 同步屏障。如果某架构在 SMP 与 UP 下行为没有差异,直接在自己的local.h中包含asm-generic/local.h即可。
2.1 通用默认实现:asm-generic/local.h
仓库中的 include/asm-generic/local.h 是通用兜底实现,完全基于atomic_long_*展开:
#define local_read(l) atomic_long_read(&(l)->a) #define local_set(l,i) atomic_long_set((&(l)->a),(i)) #define local_inc(l) atomic_long_inc(&(l)->a) #define local_dec(l) atomic_long_dec(&(l)->a) #define local_add(i,l) atomic_long_add((i),(&(l)->a)) #define local_sub(i,l) atomic_long_sub((i),(&(l)->a))该头文件还提供带返回值的变体:local_add_return、local_sub_return、local_inc_return、local_dec_return,以及 CAS/交换类操作local_cmpxchg、local_try_cmpxchg、local_xchg、local_add_unless、local_inc_not_zero。文件注释明确指出:local_t的价值在于某些处理器能用"对本 CPU 上的 IRQ 原子"的方式完成加/减,asm-generic版本只是用原子操作模拟,真正收益来自各架构自己的优化实现(见 include/asm-generic/local.h)。
2.2 x86 实现:去掉 LOCK 前缀的单指令
x86 的实现(arch/x86/include/asm/local.h)最能体现"去掉 LOCK 前缀"这一思路。例如local_inc直接退化为一条无锁的单指令:
static inline void local_inc(local_t *l) { asm volatile(_ASM_INC "%0" : "+m" (l->a.counter)); }local_dec、local_add、local_sub同理使用_ASM_DEC、_ASM_ADD、_ASM_SUB(无lock前缀)。而local_add_return使用XADD指令并在约束中仅声明"memory"破坏,local_xchg特意不用 XCHG 指令(因为 XCHG 隐含 LOCK 前缀、代价高且阻止缓存行预取),改用无锁 CMPXCHG 循环实现(见 arch/x86/include/asm/local.h)。头文件末尾的注释还指出:在 x86_32 上这些操作并不比原子变体好,但在 x86-64 的 SMP 内核上,由于省略了 lock 前缀而更优(见 arch/x86/include/asm/local.h)。
2.3 PowerPC 实现:关中断(PMU-safe)方式
PowerPC 的 64 位 Book3S 实现(arch/powerpc/include/asm/local.h)与 x86 截然不同:它不依赖 LL/SC,而是围绕powerpc_local_irq_pmu_save()/powerpc_local_irq_pmu_restore()这对"同时保存中断与 PMU 状态"的辅助函数,在关中断临界区内完成普通 C 运算:
#define LOCAL_OP(op, c_op) \ static __inline__ void local_##op(long i, local_t *l) \ { \ unsigned long flags; \ \ powerpc_local_irq_pmu_save(flags); \ l->v c_op i; \ powerpc_local_irq_pmu_restore(flags); \ }这里local_t直接定义为包含一个long v的结构体,local_read/local_set使用READ_ONCE/WRITE_ONCE(见 arch/powerpc/include/asm/local.h)。该文件在非 Book3S-64 时回退到asm-generic/local.h(见 arch/powerpc/include/asm/local.h)。这种实现展示了本地原子操作的第二种典型形态:用关中断代替锁前缀。
2.4 MIPS 实现:LL/SC 或关中断
MIPS 实现(arch/mips/include/asm/local.h)则根据硬件能力二选一:若kernel_uses_llsc,用LL/SC(Load-Linked/Store-Conditional)指令对实现local_add_return;否则退化为local_irq_save()/local_irq_restore()关中断临界区(见 arch/mips/include/asm/local.h)。这同样印证了文档的核心结论:本地原子操作 = 去掉跨 CPU 同步手段(锁/屏障)的原子操作。
三、使用 local_t 必须遵守的规则
文档给出了明确的规则清单,归纳如下:
- 变量必须是 per-CPU 变量(由 local ops 触碰的变量);
- 只有拥有该变量的 CPU 才能写它;
- 拥有者 CPU 可以在任何上下文(进程、irq、softirq、nmi……)中用 local ops 更新自己的
local_t变量; - 在进程上下文使用 local ops 时,必须关闭抢占(或中断),防止进程在"取 per-CPU 变量地址"与"执行 local op"之间被迁移到其他 CPU;
- 在中断上下文使用 local ops 时,主线内核(mainline)下无需特别处理(本地 CPU、抢占已关),但为了兼容
-rt(PREEMPT_RT)内核,建议仍然显式关闭抢占; - 读本地 CPU 变量会得到变量的当前副本;
- 任何 CPU 都可以读这些变量:对"long"、对齐变量的更新总是原子的;但由于写者 CPU 不做内存同步,读其他CPU 的变量时可能读到过期副本。
其中规则 4 的机制在 include/linux/percpu-defs.h 中有直接体现——get_cpu_var()/put_cpu_var()正是通过preempt_disable()/preempt_enable()包裹取地址与释放:
#define get_cpu_var(var) \ (*({ \ preempt_disable(); \ this_cpu_ptr(&var); \ })) #define put_cpu_var(var) \ do { \ (void)&(var); \ preempt_enable(); \ } while (0)四、如何使用本地原子操作
4.1 声明与初始化
使用前引入 per-CPU 与架构 local 头文件,并用DEFINE_PER_CPU+LOCAL_INIT(0)声明初始化为 0 的 per-CPU 计数器:
#include <linux/percpu.h> #include <asm/local.h> static DEFINE_PER_CPU(local_t, counters) = LOCAL_INIT(0);LOCAL_INIT(i)在通用实现中展开为{ ATOMIC_LONG_INIT(i) }(见 include/asm-generic/local.h)。
4.2 计数:两种上下文写法
计数操作覆盖signed long的全部位宽。在可抢占上下文中,必须用get_cpu_var()/put_cpu_var()包住写操作,确保写 per-CPU 变量期间抢占被关闭:
local_inc(&get_cpu_var(counters)); put_cpu_var(counters);如果已经处于抢占安全上下文,可以直接用this_cpu_ptr()(在 include/linux/percpu-defs.h 中this_cpu_ptr(ptr)即raw_cpu_ptr(ptr),SMP 下等价于 per-CPU 基址加偏移):
local_inc(this_cpu_ptr(&counters));4.3 跨 CPU 读取计数器
本地计数器可以从"别的 CPU"读取并求和,但跨 CPU 的local_read数据相对拥有者 CPU 的其他内存写必须视为乱序:
long sum = 0; for_each_online_cpu(cpu) sum += local_read(&per_cpu(counters, cpu));per_cpu(var, cpu)宏(见 include/linux/percpu-defs.h)在非 SMP 与 SMP 下均按指定 CPU 解析地址。
4.4 需要同步资源时:显式内存屏障
如果你想用"远程local_read"在 CPU 之间同步访问某个资源,则写者 CPU 与读者 CPU 必须分别显式使用smp_wmb()与smp_rmb()内存屏障。文档给出的典型场景是:用local_t变量记录写入缓冲区(buffer)的字节数——
- 缓冲区写入之后、计数器自增之前,写者 CPU 需要
smp_wmb(); - 计数器读取之后、读取缓冲区之前,读者 CPU 需要
smp_rmb()。
这是因为本地原子操作本身不提供任何内存同步,屏障必须由使用者自己补上。
五、完整示例模块:test-local.c
文档附带了一个可直接编译运行的示例模块(见 Documentation/core-api/local_ops.rst),演示了声明、非抢占上下文递增、跨 CPU 读取与定时器驱动的完整流程:
/* test-local.c * * Sample module for local.h usage. */ #include <asm/local.h> #include <linux/module.h> #include <linux/timer.h> static DEFINE_PER_CPU(local_t, counters) = LOCAL_INIT(0); static struct timer_list test_timer; /* IPI called on each CPU. */ static void test_each(void *info) { /* Increment the counter from a non preemptible context */ printk("Increment on cpu %d\n", smp_processor_id()); local_inc(this_cpu_ptr(&counters)); /* This is what incrementing the variable would look like within a * preemptible context (it disables preemption) : * * local_inc(&get_cpu_var(counters)); * put_cpu_var(counters); */ } static void do_test_timer(unsigned long data) { int cpu; /* Increment the counters */ on_each_cpu(test_each, NULL, 1); /* Read all the counters */ printk("Counters read from CPU %d\n", smp_processor_id()); for_each_online_cpu(cpu) { printk("Read : CPU %d, count %ld\n", cpu, local_read(&per_cpu(counters, cpu))); } mod_timer(&test_timer, jiffies + 1000); } static int __init test_init(void) { /* initialize the timer that will increment the counter */ timer_setup(&test_timer, do_test_timer, 0); mod_timer(&test_timer, jiffies + 1); return 0; } static void __exit test_exit(void) { timer_shutdown_sync(&test_timer); } module_init(test_init); module_exit(test_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Mathieu Desnoyers"); MODULE_DESCRIPTION("Local Atomic Ops");该模块的关键路径值得拆解:
on_each_cpu(test_each, NULL, 1)向每个在线 CPU 发送 IPI,在对方 CPU 的非抢占上下文里执行test_each——这正是"只有拥有者 CPU 写自己的local_t"这一规则的实践;test_each中使用this_cpu_ptr()直接递增(注释里同时给出可抢占上下文下用get_cpu_var()/put_cpu_var()的等价写法);do_test_timer用for_each_online_cpu+per_cpu()从本 CPU 汇总所有 CPU 的计数;- 定时器使用现代
timer_setup()API 初始化,退出时用timer_shutdown_sync()同步销毁。
六、与 this_cpu 操作的关系(重要注意事项)
文档开头的注意(note)明确指出:基于local_t的操作不推荐用于一般内核用途,除非确有特殊目的,否则请改用this_cpu操作。内核中大多数local_t用法都已被this_cpu操作取代。this_cpu操作把"per-CPU 重定位(relocation)"与"类似local_t的语义"合并到单条指令中,生成更紧凑、更快的代码。
从源码看,this_cpu一族在 include/linux/percpu-defs.h 中基于不同变量大小的分派实现,例如:
#define this_cpu_read(pcp) __pcpu_size_call_return(this_cpu_read_, pcp) #define this_cpu_add(pcp, val) __pcpu_size_call(this_cpu_add_, pcp, val) #define this_cpu_inc(pcp) this_cpu_add(pcp, 1)(见 include/linux/percpu-defs.h),在 x86 上最终落到%__percpu_seg:段寻址的单指令操作(如this_cpu_add_1,见 arch/x86/include/asm/percpu.h)。也就是说:现代内核中做 per-CPU 计数,首选this_cpu_inc()/this_cpu_add()等操作;local_t主要保留给需要显式以变量形式管理、且与架构本地原子能力深度耦合的特殊场景(如部分追踪与性能计数设施)。
七、总结
local_t是 per-CPU 计数器的轻量原语,以"仅本 CPU 原子"换取无 LOCK、无屏障的极致性能,可安全用于 irq/softirq/NMI 上下文;- 它只允许拥有者 CPU 写入,任何 CPU 都可读,但跨 CPU 读取无内存序保证,需要资源同步时必须自行搭配
smp_wmb()/smp_rmb(); - 架构实现有三类典型范式:x86 的"去 LOCK 单指令"、PowerPC/MIPS 的"关中断临界区"(MIPS 另有 LL/SC 路径)、以及通用兜底的
asm-generic/local.h原子模拟; - 使用规则核心是"per-CPU 变量 + 抢占安全上下文(
get_cpu_var/put_cpu_var或this_cpu_ptr)+ 单写多读"; - 对一般内核用途,应优先使用
this_cpu操作族,它把 per-CPU 寻址与本地原子语义融合为单指令,代码更紧凑、执行更快。
延伸阅读:内核中文翻译版见 Documentation/translations/zh_CN/core-api/local_ops.rst,相关文档索引见 Documentation/core-api/index.rst。
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考