恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Linux 内核本地原子操作(local_t)深入解析:语义、架构实现与 per-CPU 计数实践
首页
资讯中心
/
Linux 内核本地原子操作(local_t)深入解析:语义、架构实现与 per-CPU 计数实践
Linux 内核本地原子操作(local_t)深入解析:语义、架构实现与 per-CPU 计数实践
发布时间:2026/9/15 10:05:29
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.hlocal_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/SCLoad-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、抢占已关但为了兼容-rtPREEMPT_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_CPULOCAL_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_cpuper_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),仅供参考