news 2026/9/15 10:04:52

Linux 内核本地原子操作(local_t)深入解析:语义、架构实现与 per-CPU 计数实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 内核本地原子操作(local_t)深入解析:语义、架构实现与 per-CPU 计数实践

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"保证变量修改的原子性。因此使用时必须确保:

  1. 只有一个 CPU 写local_t数据——通常通过 per-CPU 变量并保证在关抢占的上下文(preemption safe context)中修改来实现;
  2. 任何 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_returnlocal_sub_returnlocal_inc_returnlocal_dec_return,以及 CAS/交换类操作local_cmpxchglocal_try_cmpxchglocal_xchglocal_add_unlesslocal_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_declocal_addlocal_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 必须遵守的规则

文档给出了明确的规则清单,归纳如下:

  1. 变量必须是 per-CPU 变量(由 local ops 触碰的变量);
  2. 只有拥有该变量的 CPU 才能写它
  3. 拥有者 CPU 可以在任何上下文(进程、irq、softirq、nmi……)中用 local ops 更新自己的local_t变量;
  4. 进程上下文使用 local ops 时,必须关闭抢占(或中断),防止进程在"取 per-CPU 变量地址"与"执行 local op"之间被迁移到其他 CPU;
  5. 中断上下文使用 local ops 时,主线内核(mainline)下无需特别处理(本地 CPU、抢占已关),但为了兼容-rt(PREEMPT_RT)内核,建议仍然显式关闭抢占;
  6. 读本地 CPU 变量会得到变量的当前副本;
  7. 任何 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_timerfor_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_varthis_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 10:01:48

COLMAP 实用 FAQ 全指南:从特征与相机模型选择到稠密重建调优

COLMAP 实用 FAQ 全指南&#xff1a;从特征与相机模型选择到稠密重建调优 【免费下载链接】colmap COLMAP - Structure-from-Motion and Multi-View Stereo 项目地址: https://gitcode.com/GitHub_Trending/co/colmap 导读 本文是 COLMAP 官方 FAQ&#xff08;doc/faq.…

作者头像 李华
网站建设 2026/9/15 10:00:53

TinaCMS Astro 集成如何注册 TinaIsland 添加新的可编辑区域

TinaCMS Astro 集成如何注册 TinaIsland 添加新的可编辑区域 【免费下载链接】tinacms TinaCMS is the leading open-source headless CMS that supports Markdown and Visual Editing. Your content is stored in your own GitHub repo &#x1f999; ❤️ 项目地址: https:…

作者头像 李华
网站建设 2026/9/15 9:58:19

FiftyOne 视觉AI数据集构建与评估指南

FiftyOne 视觉AI数据集构建与评估指南 【免费下载链接】fiftyone Refine high-quality datasets and visual AI models 项目地址: https://gitcode.com/GitHub_Trending/fi/fiftyone FiftyOne 是一款面向计算机视觉团队的数据集构建与模型评估工具&#xff0c;核心解决视…

作者头像 李华
网站建设 2026/9/15 9:57:48

PHPMailer 6.9 如何用 XCLIENT 扩展属性向 Postfix 网关传递转发身份

PHPMailer 6.9 如何用 XCLIENT 扩展属性向 Postfix 网关传递转发身份 【免费下载链接】PHPMailer The classic email sending library for PHP 项目地址: https://gitcode.com/GitHub_Trending/ph/PHPMailer 当你的 PHP 应用不直接连外网邮件服务器&#xff0c;而是先把…

作者头像 李华
网站建设 2026/9/15 9:57:14

豆包AI生图生视频高效实战:提示词优化与ComfyUI工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华